build_angebot_beispiel.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. const fs = require('fs');
  2. // ============ ANPASSEN: Ausgabepfad ============
  3. const OUTPUT = "/home/claude/output/Angebot_Kurzkonzept.docx";
  4. require('fs').mkdirSync(require('path').dirname(OUTPUT), { recursive: true });
  5. const {
  6. Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
  7. AlignmentType, HeadingLevel, BorderStyle, WidthType, ShadingType,
  8. PageNumber, Footer, VerticalAlign, LevelFormat, ImageRun
  9. } = require('docx');
  10. // vision4it Farben (aus branding/colors.md)
  11. const C_DARK = "1A1A1A", C_GREEN = "13A983", C_GREY = "5C5C5C",
  12. C_LIGHT = "F4F4F4", C_BORDER = "D0D0D0";
  13. const LOGO = fs.readFileSync('/home/claude/branding/logo/logo.png');
  14. const LOGO_WHITE = fs.readFileSync('/home/claude/branding/logo/logo-white.png');
  15. // --- Helpers ---------------------------------------------------------------
  16. const noBorder = { style: BorderStyle.NONE, size: 0, color: "FFFFFF" };
  17. const noBorders = { top: noBorder, bottom: noBorder, left: noBorder, right: noBorder };
  18. const thinBorder = { style: BorderStyle.SINGLE, size: 4, color: C_BORDER };
  19. const boxBorders = { top: thinBorder, bottom: thinBorder, left: thinBorder, right: thinBorder };
  20. const H1 = (text) => new Paragraph({
  21. heading: HeadingLevel.HEADING_1,
  22. spacing: { before: 340, after: 60 },
  23. border: { bottom: { style: BorderStyle.SINGLE, size: 8, color: C_GREEN, space: 4 } },
  24. children: [new TextRun({ text, bold: true, color: C_DARK, size: 28, font: "Calibri" })],
  25. });
  26. const H2 = (text) => new Paragraph({
  27. heading: HeadingLevel.HEADING_2,
  28. spacing: { before: 240, after: 100 },
  29. children: [new TextRun({ text, bold: true, color: C_DARK, size: 24, font: "Calibri" })],
  30. });
  31. const P = (text, opts = {}) => new Paragraph({
  32. spacing: { after: 120 },
  33. alignment: AlignmentType.JUSTIFIED,
  34. children: [new TextRun({ text, size: 22, color: opts.color || C_DARK, font: "Calibri" })],
  35. });
  36. const Bullet = (text) => new Paragraph({
  37. numbering: { reference: "bullets", level: 0 },
  38. spacing: { after: 60 },
  39. children: [new TextRun({ text, size: 22, color: C_DARK, font: "Calibri" })],
  40. });
  41. const Spacer = (h) => new Paragraph({ spacing: { after: h }, children: [] });
  42. // Phasentabelle
  43. const phaseRow = (pos, leistung, stunden, opts = {}) => new TableRow({
  44. children: [
  45. new TableCell({
  46. borders: { top: noBorder, left: noBorder, right: noBorder, bottom: thinBorder },
  47. width: { size: 900, type: WidthType.DXA },
  48. shading: opts.shade ? { fill: opts.shade, type: ShadingType.CLEAR } : undefined,
  49. margins: { top: 80, bottom: 80, left: 100, right: 100 },
  50. children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [
  51. new TextRun({ text: pos, size: 22, bold: opts.bold, color: C_DARK, font: "Calibri" })] })],
  52. }),
  53. new TableCell({
  54. borders: { top: noBorder, left: noBorder, right: noBorder, bottom: thinBorder },
  55. width: { size: 6600, type: WidthType.DXA },
  56. shading: opts.shade ? { fill: opts.shade, type: ShadingType.CLEAR } : undefined,
  57. margins: { top: 80, bottom: 80, left: 100, right: 100 },
  58. children: [new Paragraph({ children: [
  59. new TextRun({ text: leistung, size: 22, bold: opts.bold, color: C_DARK, font: "Calibri" })] })],
  60. }),
  61. new TableCell({
  62. borders: { top: noBorder, left: noBorder, right: noBorder, bottom: thinBorder },
  63. width: { size: 1860, type: WidthType.DXA },
  64. shading: opts.shade ? { fill: opts.shade, type: ShadingType.CLEAR } : undefined,
  65. margins: { top: 80, bottom: 80, left: 100, right: 100 },
  66. children: [new Paragraph({ alignment: AlignmentType.RIGHT, children: [
  67. new TextRun({ text: stunden, size: 22, bold: opts.bold, color: opts.green ? C_GREEN : C_DARK, font: "Calibri" })] })],
  68. }),
  69. ],
  70. });
  71. const phaseTable = () => new Table({
  72. width: { size: 9360, type: WidthType.DXA },
  73. columnWidths: [900, 6600, 1860],
  74. rows: [
  75. new TableRow({ tableHeader: true, children: [
  76. ...[["Pos", 900, AlignmentType.CENTER], ["Leistung", 6600, AlignmentType.LEFT], ["Stunden", 1860, AlignmentType.RIGHT]].map(([t, w, al]) =>
  77. new TableCell({
  78. borders: { top: noBorder, left: noBorder, right: noBorder, bottom: { style: BorderStyle.SINGLE, size: 8, color: C_GREEN } },
  79. width: { size: w, type: WidthType.DXA },
  80. shading: { fill: C_LIGHT, type: ShadingType.CLEAR },
  81. margins: { top: 80, bottom: 80, left: 100, right: 100 },
  82. children: [new Paragraph({ alignment: al, children: [
  83. new TextRun({ text: t, size: 22, bold: true, color: C_DARK, font: "Calibri" })] })],
  84. })),
  85. ]}),
  86. phaseRow("1", "Vorbereitung & Planung (Bestandsaufnahme, Detail-Design, Projektmanagement)", "10"),
  87. phaseRow("2", "Basis-Infrastruktur (Proxmox-Host, OPNsense, VLAN-Segmentierung)", "14"),
  88. phaseRow("3", "Server-Rollen (DC, Fileserver, Fachanwendung)", "18"),
  89. phaseRow("4", "Sicherheit & Monitoring (Firewall-Regelwerk, TacticalRMM, Backup)", "12"),
  90. phaseRow("5", "Migration & Rollout (Datenübernahme, Client-Onboarding, Schulung)", "16"),
  91. phaseRow("6", "Übergabe & Dokumentation (Hudu, Hypercare, Abnahme)", "8"),
  92. phaseRow("", "Zwischensumme", "78", { bold: true, shade: C_LIGHT }),
  93. phaseRow("7", "Projektpuffer (15 %)", "12"),
  94. phaseRow("", "Gesamt Dienstleistung", "90 h", { bold: true, shade: C_LIGHT, green: true }),
  95. ],
  96. });
  97. // Infozeile Deckblatt
  98. const coverMeta = (label, value) => new Paragraph({
  99. spacing: { after: 60 },
  100. children: [
  101. new TextRun({ text: label + " ", size: 20, color: "9E9E9E", font: "Calibri" }),
  102. new TextRun({ text: value, size: 22, color: "FFFFFF", font: "Calibri" }),
  103. ],
  104. });
  105. // --- Dokument --------------------------------------------------------------
  106. const doc = new Document({
  107. numbering: {
  108. config: [{
  109. reference: "bullets",
  110. levels: [{
  111. level: 0, format: LevelFormat.BULLET, text: "\u2022",
  112. alignment: AlignmentType.LEFT,
  113. style: { paragraph: { indent: { left: 720, hanging: 360 } } },
  114. }],
  115. }],
  116. },
  117. styles: {
  118. default: { document: { run: { font: "Calibri", size: 22, color: C_DARK } } },
  119. },
  120. sections: [
  121. // ============ Section 1: Deckblatt (hell, druckfreundlich, ohne Fußzeile) ============
  122. {
  123. properties: { page: { margin: { top: 1700, bottom: 1134, left: 1418, right: 1418 } } },
  124. children: [
  125. new Paragraph({
  126. alignment: AlignmentType.RIGHT,
  127. spacing: { after: 3000 },
  128. children: [new ImageRun({ data: LOGO, transformation: { width: 247, height: 60 }, type: 'png' })],
  129. }),
  130. new Paragraph({
  131. spacing: { after: 100 },
  132. children: [new TextRun({ text: "ANGEBOT & KURZKONZEPT", size: 22, color: C_GREEN, bold: true, font: "Calibri", characterSpacing: 40 })],
  133. }),
  134. new Paragraph({
  135. spacing: { after: 140 },
  136. children: [new TextRun({ text: "IT-Infrastruktur", size: 72, bold: true, color: C_DARK, font: "Calibri" })],
  137. }),
  138. new Paragraph({
  139. spacing: { after: 200 },
  140. border: { bottom: { style: BorderStyle.SINGLE, size: 12, color: C_GREEN, space: 12 } },
  141. children: [new TextRun({ text: "Modernisierung Serverumgebung & Netzwerk", size: 30, color: C_GREY, font: "Calibri" })],
  142. }),
  143. Spacer(2600),
  144. new Table({
  145. width: { size: 9071, type: WidthType.DXA },
  146. columnWidths: [2200, 6871],
  147. rows: [
  148. ["Kunde", "Musterfirma GmbH, Rommerskirchen"],
  149. ["Datum", "21. Juli 2026"],
  150. ["Angebots-Nr.", "AN-2026-042 \u00b7 Version 1.0"],
  151. ["Ansprechpartner", "Matthias, vision4it GmbH"],
  152. ].map(([label, value]) => new TableRow({
  153. children: [
  154. new TableCell({
  155. borders: { top: noBorder, left: noBorder, right: noBorder, bottom: thinBorder },
  156. width: { size: 2200, type: WidthType.DXA },
  157. margins: { top: 100, bottom: 100, left: 0, right: 100 },
  158. children: [new Paragraph({ children: [new TextRun({ text: label, size: 20, color: C_GREY, font: "Calibri" })] })],
  159. }),
  160. new TableCell({
  161. borders: { top: noBorder, left: noBorder, right: noBorder, bottom: thinBorder },
  162. width: { size: 6871, type: WidthType.DXA },
  163. margins: { top: 100, bottom: 100, left: 100, right: 0 },
  164. children: [new Paragraph({ children: [new TextRun({ text: value, size: 22, color: C_DARK, font: "Calibri" })] })],
  165. }),
  166. ],
  167. })),
  168. }),
  169. Spacer(2200),
  170. new Paragraph({
  171. border: { top: { style: BorderStyle.SINGLE, size: 4, color: C_BORDER, space: 8 } },
  172. spacing: { after: 40 },
  173. children: [new TextRun({ text: "vision4it GmbH \u00b7 Siemensstra\u00dfe 8 \u00b7 41542 Dormagen \u00b7 Tel. 02133 8688890 \u00b7 office@vision4it.de \u00b7 vision4it.de", size: 16, color: C_GREY, font: "Calibri" })],
  174. }),
  175. new Paragraph({
  176. children: [new TextRun({ text: "Gesch\u00e4ftsf\u00fchrer: Matthias Gehlhaar, Phillip Gehlhaar \u00b7 Amtsgericht M\u00f6nchengladbach, HRB 22650 \u00b7 USt-IdNr. DE369304034", size: 16, color: C_GREY, font: "Calibri" })],
  177. }),
  178. ],
  179. },
  180. // ============ Section 2: Kurzkonzept (hell, mit Fußzeile) ============
  181. {
  182. properties: { page: { margin: { top: 1134, bottom: 1134, left: 1134, right: 1134 } } },
  183. footers: {
  184. default: new Footer({
  185. children: [
  186. new Paragraph({
  187. border: { top: { style: BorderStyle.SINGLE, size: 4, color: C_BORDER, space: 4 } },
  188. spacing: { after: 20 },
  189. children: [new TextRun({ text: "vision4it GmbH \u00b7 Siemensstra\u00dfe 8 \u00b7 41542 Dormagen \u00b7 Tel. 02133 8688890 \u00b7 office@vision4it.de", size: 14, color: C_GREY, font: "Calibri" })],
  190. }),
  191. new Paragraph({
  192. children: [
  193. new TextRun({ text: "Gesch\u00e4ftsf\u00fchrer: Matthias Gehlhaar, Phillip Gehlhaar \u00b7 Amtsgericht M\u00f6nchengladbach, HRB 22650 \u00b7 USt-IdNr. DE369304034", size: 14, color: C_GREY, font: "Calibri" }),
  194. new TextRun({ text: "\u2003\u2003Angebot AN-2026-042 \u00b7 Seite ", size: 14, color: C_GREY, font: "Calibri" }),
  195. new TextRun({ children: [PageNumber.CURRENT], size: 14, color: C_GREY, font: "Calibri" }),
  196. ],
  197. }),
  198. ],
  199. }),
  200. },
  201. children: [
  202. new Paragraph({
  203. alignment: AlignmentType.RIGHT,
  204. spacing: { after: 300 },
  205. children: [new ImageRun({ data: LOGO, transformation: { width: 124, height: 30 }, type: 'png' })],
  206. }),
  207. H1("1. Ausgangslage"),
  208. P("Die Musterfirma GmbH betreibt derzeit einen einzelnen physischen Server (Windows Server 2016) ohne Virtualisierung, auf dem Domänencontroller, Dateiablage und die Warenwirtschaft gemeinsam laufen. Das Backup erfolgt manuell auf USB-Festplatten, eine Netzwerksegmentierung existiert nicht. Der Support für das Betriebssystem ist ausgelaufen; Ausfälle einzelner Dienste betreffen unmittelbar den gesamten Betrieb."),
  209. H1("2. Zielsetzung"),
  210. Bullet("Ablösung der Altumgebung durch eine virtualisierte, wartbare Infrastruktur"),
  211. Bullet("Trennung der Dienste in eigene virtuelle Maschinen mit definierten Ressourcen"),
  212. Bullet("Automatisiertes, überwachtes Backup mit täglicher Erfolgskontrolle"),
  213. Bullet("Segmentiertes Netzwerk mit zentraler Firewall und klarem Regelwerk"),
  214. Bullet("Proaktives Monitoring und Patch-Management durch vision4it"),
  215. H1("3. Ziel-Architektur"),
  216. P("Als Virtualisierungsplattform kommt Proxmox VE auf einem neuen Server-Host zum Einsatz. Die bisherigen Dienste werden in getrennte virtuelle Maschinen aufgeteilt: Domänencontroller, Fileserver und Applikationsserver für die Warenwirtschaft. Am Netzwerkrand ersetzt eine OPNsense-Firewall den vorhandenen Consumer-Router und übernimmt Routing, Regelwerk und VPN-Zugänge. Alle Systeme werden self-hosted beim Kunden betrieben; es entstehen keine Cloud-Abhängigkeiten."),
  217. H1("4. Netzwerk & Segmentierung"),
  218. P("Das Netzwerk wird in getrennte VLANs aufgeteilt: Server, Clients, Drucker, Gäste-WLAN und Management. Der Verkehr zwischen den Segmenten läuft ausschließlich über die OPNsense und wird durch ein dokumentiertes Regelwerk kontrolliert. Der Gastzugang bleibt vollständig vom Firmennetz isoliert."),
  219. H1("5. Backup & Monitoring"),
  220. P("Die Datensicherung erfolgt über Proxmox Backup Server auf ein dediziertes Sicherungsziel mit täglichen, verschlüsselten Sicherungen und definierter Aufbewahrung. Alle Systeme werden in das Monitoring von vision4it (TacticalRMM) aufgenommen: Patch-Management, Ereignisüberwachung und tägliche Backup-Kontrolle inklusive."),
  221. H1("6. Nächste Schritte"),
  222. Bullet("Standortbegehung und Bestandsaufnahme"),
  223. Bullet("Detail-Design mit Netzplan, Firewall-Regelwerk und IP-Konzept"),
  224. Bullet("Hardware-Bestellung (Server, Switch, Firewall)"),
  225. Bullet("Umsetzung mit Parallelbetrieb und geplanter Umschaltung"),
  226. Bullet("Einweisung der Mitarbeiter und Übergabe in den Regelbetrieb"),
  227. H1("7. Zeitplan & Aufwand"),
  228. P("Die Umsetzung gliedert sich in sechs Phasen. Die Schätzung umfasst Aufbau, Konfiguration, Migration und eine Hypercare-Phase nach dem Go-Live."),
  229. Spacer(100),
  230. phaseTable(),
  231. Spacer(160),
  232. H2("Kalenderzeit"),
  233. P("Bei einem Techniker mit etwa 70 % Auslastung auf diesem Projekt ergibt sich eine Laufzeit von rund 4 bis 5 Wochen vom Kick-off bis zum Ende der Hypercare-Phase."),
  234. H2("Nicht enthalten"),
  235. Bullet("Anfahrtszeiten (separate Abrechnung)"),
  236. Bullet("Hardware-Lieferzeiten"),
  237. Bullet("Laufender Wartungsvertrag (separates Angebot)"),
  238. Bullet("Spätere Erweiterungen"),
  239. ],
  240. },
  241. ],
  242. });
  243. Packer.toBuffer(doc).then(buffer => {
  244. fs.writeFileSync(OUTPUT, buffer);
  245. console.log("OK");
  246. });