#!/usr/bin/env node /** * create-decision-structure.js — טיוטת מבנה החלטת ועדת ערר * * מייצר קובץ DOCX מעוצב עם כל חלקי ההחלטה (בלוקים א-יב). * בלוקים א-ט ממולאים בתוכן, בלוק י (דיון) ויא (סיכום) = placeholders. * * שימוש: * node create-decision-structure.js [output.docx] * * מבוסס על create-legal-doc.js מתוך legal-docx skill. * כללי RTL: START/END (לא LEFT/RIGHT), bidi+bidirectional+rightToLeft בכל רמה. * * v1.0 */ const fs = require('fs'); const path = require('path'); const { Document, Packer, Paragraph, TextRun, Header, Footer, AlignmentType, HeadingLevel, PageNumber, LevelFormat, Table, TableRow, TableCell, WidthType, BorderStyle, ShadingType, UnderlineType } = require(path.join(process.cwd(), 'node_modules', 'docx')); // ═══════════════════════════════════════════════ // CONFIGURATION // ═══════════════════════════════════════════════ const FONT = "David"; const FONT_SIZE = 24; // 12pt const HEADING1_SIZE = 32; // 16pt — "החלטה" const HEADING2_SIZE = 28; // 14pt — כותרות פרקים const MARGIN_CM = 2.5; const MARGIN_DXA = Math.round(MARGIN_CM / 2.54 * 1440); // 1417 const PAGE_WIDTH = 11906; // A4 const PAGE_HEIGHT = 16838; const CONTENT_WIDTH = PAGE_WIDTH - MARGIN_DXA * 2; // 9072 const noBorders = { top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" } }; // ═══════════════════════════════════════════════ // RTL HELPERS — מבוסס על create-legal-doc.js // ═══════════════════════════════════════════════ const rtlRun = (text, opts = {}) => new TextRun({ text, font: opts.font || FONT, size: opts.size || FONT_SIZE, bold: opts.bold || false, underline: opts.underline ? { type: UnderlineType.SINGLE } : undefined, rightToLeft: true, }); const rtlPara = (children, opts = {}) => new Paragraph({ bidirectional: true, alignment: opts.alignment || AlignmentType.BOTH, spacing: opts.spacing || { after: 120, line: 276 }, // 1.15 line spacing = 276 twips indent: opts.indent, children: Array.isArray(children) ? children : [children], ...(opts.heading ? { heading: opts.heading } : {}), }); // כותרת ראשית — "החלטה" const mainTitle = (text) => rtlPara( rtlRun(text, { bold: true, size: HEADING1_SIZE }), { heading: HeadingLevel.HEADING_1, alignment: AlignmentType.CENTER, spacing: { before: 240, after: 240 } } ); // כותרת פרק — "תמצית טענות הצדדים", "דיון והכרעה" const sectionTitle = (text) => rtlPara( rtlRun(text, { bold: true, size: HEADING2_SIZE, underline: true }), { heading: HeadingLevel.HEADING_2, alignment: AlignmentType.CENTER, spacing: { before: 360, after: 240 } } ); // כותרת משנה — "טענות העוררים" const subTitle = (text) => rtlPara( rtlRun(text, { bold: true, size: FONT_SIZE }), { alignment: AlignmentType.CENTER, spacing: { before: 240, after: 160 } } ); // סעיף ממוספר — מספר bold + טקסט רגיל const numberedPara = (num, text) => rtlPara([ rtlRun(`${num}. `, { bold: true }), rtlRun(text), ], { spacing: { after: 120, line: 276 } }); // ציטוט (blockquote) — הזחה משני הצדדים const blockquote = (text) => rtlPara( rtlRun(text), { indent: { left: 567, right: 567 }, spacing: { before: 120, after: 120, line: 276 } } ); // תיבת תמונה — מסגרת אפורה עם הנחיה const imageBox = (description) => new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER, spacing: { before: 200, after: 200 }, shading: { type: ShadingType.CLEAR, fill: "F0F0F0" }, children: [ new TextRun({ text: `📷 תמונה: ${description}`, font: FONT, size: FONT_SIZE, rightToLeft: true, bold: true, }), ], }); // Placeholder — טקסט אפור שמסמן מקום const placeholder = (text) => rtlPara( new TextRun({ text: `[${text}]`, font: FONT, size: FONT_SIZE, rightToLeft: true, italics: true, color: "808080", }), { alignment: AlignmentType.CENTER, spacing: { before: 200, after: 200 } } ); // רווח const spacer = (after = 200) => rtlPara(rtlRun(""), { spacing: { after, before: 0 } }); // תא בטבלה const rtlCell = (children, width, opts = {}) => new TableCell({ borders: noBorders, width: { size: width, type: WidthType.DXA }, children: Array.isArray(children) ? children : [children], ...(opts.verticalAlign ? { verticalAlign: opts.verticalAlign } : {}), }); // ═══════════════════════════════════════════════ // BLOCK BUILDERS — בוני הבלוקים // ═══════════════════════════════════════════════ // בלוק א — כותרת מוסדית (טבלה 2 טורים) function buildInstitutionalHeader(data) { const leftCol = CONTENT_WIDTH * 0.5; const rightCol = CONTENT_WIDTH * 0.5; // צד ימין — מוסד const rightCellContent = [ rtlPara(rtlRun("מדינת ישראל", { bold: true }), { alignment: AlignmentType.START, spacing: { after: 0 } }), rtlPara(rtlRun("ועדת ערר לתכנון ובניה"), { alignment: AlignmentType.START, spacing: { after: 0 } }), rtlPara(rtlRun("מחוז ירושלים"), { alignment: AlignmentType.START, spacing: { after: 80 } }), ]; // צד שמאל — מספרי תיק const leftLines = []; if (data.case_numbers) { data.case_numbers.forEach(cn => { leftLines.push(rtlPara([ rtlRun("מס' תיק: "), rtlRun(cn, { bold: true }), ], { alignment: AlignmentType.START, spacing: { after: 0 } })); }); } if (data.plan_number) { leftLines.push(rtlPara([ rtlRun("מס' תכנית: "), rtlRun(data.plan_number, { bold: true }), ], { alignment: AlignmentType.START, spacing: { after: 0 } })); } if (data.request_number) { leftLines.push(rtlPara([ rtlRun("מס' בקשה: "), rtlRun(data.request_number, { bold: true }), ], { alignment: AlignmentType.START, spacing: { after: 0 } })); } return new Table({ visuallyRightToLeft: true, width: { size: CONTENT_WIDTH, type: WidthType.DXA }, columnWidths: [rightCol, leftCol], rows: [ new TableRow({ children: [ rtlCell(rightCellContent, rightCol), rtlCell(leftLines.length ? leftLines : [rtlPara(rtlRun(""))], leftCol), ] }) ] }); } // בלוק ב — הרכב הוועדה function buildPanel(data) { const lines = []; lines.push(spacer(120)); lines.push(rtlPara([ rtlRun("בפני:", { bold: true }), ], { spacing: { after: 40 } })); lines.push(rtlPara([ rtlRun("יו\"ר הוועדה: ", { bold: true }), rtlRun(data.panel?.chair || "עו\"ד דפנה תמיר"), ], { spacing: { after: 40 } })); if (data.panel?.members) { lines.push(rtlPara([ rtlRun("חברי הוועדה: ", { bold: true }), rtlRun(data.panel.members[0] || ""), ], { spacing: { after: 40 } })); for (let i = 1; i < data.panel.members.length; i++) { lines.push(rtlPara(rtlRun(data.panel.members[i]), { spacing: { after: 40 } })); } } return lines; } // בלוק ג — צדדים function buildParties(data) { const lines = []; lines.push(spacer(120)); // עוררים if (data.appellants) { const label = data.appellants.length > 1 ? "העוררים:" : "העורר:"; lines.push(rtlPara(rtlRun(label, { bold: true }), { spacing: { after: 40 } })); data.appellants.forEach(a => { lines.push(rtlPara(rtlRun(a.name), { spacing: { after: 20 } })); if (a.representative) { lines.push(rtlPara(rtlRun(`ע"י ב"כ ${a.representative}`), { spacing: { after: 20 } })); } }); } // נגד lines.push(spacer(80)); lines.push(rtlPara(rtlRun("נגד", { bold: true }), { alignment: AlignmentType.CENTER, spacing: { before: 80, after: 80 } })); // משיבים if (data.respondents) { lines.push(rtlPara(rtlRun("המשיבים:", { bold: true }), { spacing: { after: 40 } })); data.respondents.forEach((r, i) => { lines.push(rtlPara(rtlRun(`${i + 1}. ${r.name}`), { spacing: { after: 20 } })); if (r.representative) { lines.push(rtlPara(rtlRun(`ע"י ב"כ ${r.representative}`), { spacing: { after: 20 } })); } }); } return lines; } // בלוק ה — פתיחה function buildOpening(data) { const paras = []; if (data.opening_paragraphs) { data.opening_paragraphs.forEach((text, i) => { paras.push(numberedPara(i + 1, text)); }); } else { paras.push(numberedPara(1, `לפנינו ${data.appeal_description || "[תיאור הערר]"}.`)); } return paras; } // בלוק ו — רקע עובדתי function buildBackground(data) { const paras = []; let num = (data.opening_paragraphs?.length || 1) + 1; if (data.use_petach_davar !== false) { paras.push(sectionTitle("פתח דבר")); } // מקרקעין if (data.property_description) { paras.push(numberedPara(num++, data.property_description)); } else { paras.push(numberedPara(num++, "[תיאור המקרקעין — מיקום, שטח, שכונה, ייעוד, מאפיינים ייחודיים]")); } // היסטוריה תכנונית if (data.planning_history) { data.planning_history.forEach(text => { paras.push(numberedPara(num++, text)); }); } else { paras.push(numberedPara(num++, "[היסטוריה תכנונית — תכניות קודמות, החלטות קודמות, היתרים]")); } // תמונה — מיקום paras.push(imageBox("תשריט מיקום המגרש מתוך מערכת GIS — לסמן את המגרש")); // מהות הבקשה if (data.request_essence) { if (Array.isArray(data.request_essence)) { data.request_essence.forEach(text => { paras.push(numberedPara(num++, text)); }); } else { paras.push(numberedPara(num++, data.request_essence)); } } else { paras.push(numberedPara(num++, "[מהות הבקשה — פירוט מלא של מה שהתבקש]")); } // תמונה — תשריט paras.push(imageBox("תשריט הבקשה / נספח בינוי / תכנית מוצעת")); // ציטוט מפרוטוקול if (data.committee_protocol_quote) { paras.push(numberedPara(num++, "להלן מתוך פרוטוקול הדיון בוועדה המקומית:")); paras.push(blockquote(data.committee_protocol_quote)); } else { paras.push(numberedPara(num++, "[ציטוט מלא מפרוטוקול הוועדה המקומית]")); } // החלטת הוועדה + תנאים if (data.committee_decision) { paras.push(numberedPara(num++, data.committee_decision)); } else { paras.push(numberedPara(num++, "[החלטת הוועדה המקומית — מה הוחלט, אילו תנאים נקבעו]")); } // תמונה אופציונלית — סביבה if (data.include_aerial_photo !== false) { paras.push(imageBox("צילום אוויר / מבט על הסביבה עם סימון המגרש")); } // סביבת המקרקעין if (data.surroundings) { paras.push(numberedPara(num++, data.surroundings)); } // הגשת הערר if (data.appeal_filing) { paras.push(numberedPara(num++, data.appeal_filing)); } else { paras.push(numberedPara(num++, "[הגשת הערר — תאריך, מי הגיש, הצטרפויות]")); } return { paras, nextNum: num }; } // בלוק ז — טענות הצדדים function buildClaims(data, startNum) { const paras = []; let num = startNum; paras.push(sectionTitle("תמצית טענות הצדדים")); // טענות העוררים if (data.appellant_claims_sections) { // מספר עוררים עם כותרות נפרדות data.appellant_claims_sections.forEach(section => { paras.push(subTitle(section.title)); section.claims.forEach(text => { paras.push(numberedPara(num++, text)); }); }); } else { paras.push(subTitle("טענות העוררים")); if (data.appellant_claims) { data.appellant_claims.forEach(text => { paras.push(numberedPara(num++, text)); }); } else { paras.push(numberedPara(num++, "[טענות העוררים]")); } } // עמדת הוועדה המקומית paras.push(subTitle("עמדת הוועדה המקומית")); if (data.committee_position) { data.committee_position.forEach(text => { paras.push(numberedPara(num++, text)); }); } else { paras.push(numberedPara(num++, "[עמדת הוועדה המקומית]")); } // עמדת מבקשי ההיתר / מגישי התכנית const applicantTitle = data.type === "plan" ? "עמדת מגישי התכנית" : "עמדת מבקשי ההיתר"; paras.push(subTitle(applicantTitle)); if (data.applicant_position) { data.applicant_position.forEach(text => { paras.push(numberedPara(num++, text)); }); } else { paras.push(numberedPara(num++, `[${applicantTitle}]`)); } return { paras, nextNum: num }; } // בלוק ח — ההליכים בפני ועדת הערר function buildProceedings(data, startNum) { const paras = []; let num = startNum; paras.push(sectionTitle("ההליכים בפני ועדת הערר")); if (data.proceedings) { data.proceedings.forEach(item => { if (item.type === "image") { paras.push(imageBox(item.description)); } else if (item.type === "quote") { paras.push(numberedPara(num++, item.intro || "")); paras.push(blockquote(item.text)); } else { paras.push(numberedPara(num++, item.text)); } }); } else { paras.push(numberedPara(num++, "[דיון — תאריך, נוכחים, עיקרי הדברים]")); paras.push(numberedPara(num++, "[סיור (אם היה) — תאריך, תיאור]")); paras.push(imageBox("צילומים מהסיור")); paras.push(numberedPara(num++, "[החלטות ביניים]")); paras.push(numberedPara(num++, "[השלמות טיעון — כרונולוגי]")); paras.push(numberedPara(num++, "[חוו\"ד מקצועיות שהתקבלו]")); paras.push(imageBox("הדמיות / חתכי בינוי מהשלמות טיעון (אם יש)")); } return { paras, nextNum: num }; } // בלוק ט — תכניות חלות (אופציונלי) function buildPlans(data, startNum) { if (data.skip_plans_section) return { paras: [], nextNum: startNum }; const paras = []; let num = startNum; paras.push(sectionTitle("התכניות החלות על המקרקעין")); if (data.applicable_plans) { data.applicable_plans.forEach(item => { if (item.type === "quote") { paras.push(numberedPara(num++, item.intro || "")); paras.push(blockquote(item.text)); } else { paras.push(numberedPara(num++, item.text)); } }); } else { paras.push(numberedPara(num++, "[פירוט התכניות הרלוונטיות עם ציטוט מהוראותיהן]")); } return { paras, nextNum: num }; } // בלוק יב — חתימות function buildSignatures(data) { const chairName = data.panel?.chair || "עו\"ד דפנה תמיר"; const secretaryName = data.secretary || ""; const halfWidth = Math.floor(CONTENT_WIDTH / 2); return [ spacer(400), rtlPara(rtlRun("ניתנה פה אחד, היום ______________, ______________."), { spacing: { after: 400 } }), new Table({ visuallyRightToLeft: true, width: { size: CONTENT_WIDTH, type: WidthType.DXA }, columnWidths: [halfWidth, halfWidth], rows: [ new TableRow({ children: [ rtlCell([ rtlPara(rtlRun("________________________"), { alignment: AlignmentType.CENTER, spacing: { after: 40 } }), rtlPara(rtlRun(chairName, { bold: true }), { alignment: AlignmentType.CENTER, spacing: { after: 20 } }), rtlPara(rtlRun("יו\"ר ועדת הערר"), { alignment: AlignmentType.CENTER, spacing: { after: 20 } }), ], halfWidth), rtlCell([ rtlPara(rtlRun("________________________"), { alignment: AlignmentType.CENTER, spacing: { after: 40 } }), rtlPara(rtlRun(secretaryName || ""), { alignment: AlignmentType.CENTER, spacing: { after: 20 } }), rtlPara(rtlRun("מזכירת ועדת הערר"), { alignment: AlignmentType.CENTER, spacing: { after: 20 } }), ], halfWidth), ] }) ] }), ]; } // ═══════════════════════════════════════════════ // MAIN — הרכבת המסמך // ═══════════════════════════════════════════════ function buildDocument(data) { const content = []; // בלוק א — כותרת מוסדית content.push(buildInstitutionalHeader(data)); content.push(spacer(160)); // בלוק ב — הרכב content.push(...buildPanel(data)); content.push(spacer(80)); // בלוק ג — צדדים content.push(...buildParties(data)); content.push(spacer(160)); // בלוק ד — כותרת "החלטה" content.push(mainTitle("החלטה")); // בלוק ה — פתיחה content.push(...buildOpening(data)); // בלוק ו — רקע const bg = buildBackground(data); content.push(...bg.paras); // בלוק ז — טענות const claims = buildClaims(data, bg.nextNum); content.push(...claims.paras); // בלוק ח — הליכים const proc = buildProceedings(data, claims.nextNum); content.push(...proc.paras); // בלוק ט — תכניות (אופציונלי) const plans = buildPlans(data, proc.nextNum); content.push(...plans.paras); // בלוק י — דיון והכרעה (placeholder) content.push(sectionTitle("דיון והכרעה")); content.push(placeholder("כאן מתחיל פרק הדיון וההכרעה — ייכתב בשלב הבא")); // בלוק יא — סיכום (placeholder) content.push(sectionTitle("סיכום")); content.push(placeholder("ייכתב לאחר השלמת פרק הדיון")); // בלוק יב — חתימות content.push(...buildSignatures(data)); return new Document({ styles: { default: { document: { run: { font: FONT, size: FONT_SIZE, rightToLeft: true }, paragraph: { bidirectional: true, alignment: AlignmentType.BOTH } } }, paragraphStyles: [ { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, run: { size: HEADING1_SIZE, bold: true, font: FONT, rightToLeft: true }, paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0, bidirectional: true, alignment: AlignmentType.CENTER } }, { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true, run: { size: HEADING2_SIZE, bold: true, font: FONT, rightToLeft: true }, paragraph: { spacing: { before: 200, after: 200 }, outlineLevel: 1, bidirectional: true, alignment: AlignmentType.CENTER } }, ] }, sections: [{ properties: { page: { size: { width: PAGE_WIDTH, height: PAGE_HEIGHT }, margin: { top: MARGIN_DXA, right: MARGIN_DXA, bottom: MARGIN_DXA, left: MARGIN_DXA } }, bidi: true, }, footers: { default: new Footer({ children: [new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER, children: [ rtlRun("עמוד ", { size: 18 }), new TextRun({ children: [PageNumber.CURRENT], font: FONT, size: 18 }), rtlRun(" מתוך ", { size: 18 }), new TextRun({ children: [PageNumber.TOTAL_PAGES], font: FONT, size: 18 }), ] })] }) }, children: content }] }); } // ═══════════════════════════════════════════════ // CLI // ═══════════════════════════════════════════════ async function main() { const inputFile = process.argv[2]; if (!inputFile) { console.error('שימוש: node create-decision-structure.js [output.docx]'); console.error(''); console.error('קובץ ה-JSON צריך לכלול:'); console.error(' case_numbers, panel, appellants, respondents,'); console.error(' opening_paragraphs, property_description, planning_history,'); console.error(' request_essence, committee_protocol_quote, committee_decision,'); console.error(' appellant_claims, committee_position, applicant_position,'); console.error(' proceedings, applicable_plans'); console.error(''); console.error('ראה: .claude/skills/legal-decision/references/decision-template.json'); process.exit(1); } const data = JSON.parse(fs.readFileSync(inputFile, 'utf-8')); const outputFile = process.argv[3] || `החלטה-ערר-${(data.case_numbers?.[0] || 'draft').replace(/\//g, '-')}-מבנה.docx`; const doc = buildDocument(data); const buffer = await Packer.toBuffer(doc); fs.writeFileSync(outputFile, buffer); console.log(`✅ ${outputFile}`); console.log(` פונט: ${FONT} ${FONT_SIZE / 2}pt`); console.log(` שוליים: ${MARGIN_CM} ס"מ`); console.log(` RTL: bidi + bidirectional + rightToLeft ✓`); console.log(` Alignment: START/END ✓`); console.log(` גודל: ${(buffer.length / 1024).toFixed(1)} KB`); } main().catch(err => { console.error('❌ שגיאה:', err.message); process.exit(1); });