diff --git a/client/package-lock.json b/client/package-lock.json index 30b439d..09c7289 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -42,8 +42,10 @@ "buffer": "^6.0.3", "dayjs": "^1.11.13", "docx-templates": "^4.13.0", + "easy-template-x": "^5.1.0", "embla-carousel-react": "^8.3.0", "file-type": "^19.0.0", + "html2canvas": "^1.4.1", "jspdf": "^2.5.2", "ol": "^10.0.0", "ol-ext": "^4.0.23", @@ -4257,6 +4259,15 @@ "vite": "^4 || ^5" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@zeit/schemas": { "version": "2.36.0", "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", @@ -4643,7 +4654,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "optional": true, "engines": { "node": ">= 0.6.0" } @@ -5560,7 +5570,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -5963,6 +5972,18 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/easy-template-x": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/easy-template-x/-/easy-template-x-5.1.0.tgz", + "integrity": "sha512-vypMbIMLWLXoooA9rsL3SVN2oQtZwmmx1m4H8gi6JfbEXQQ5VLHGOUHYi9APbvN9R8Gx93r1fphdSFRHxozeYw==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "0.8.10", + "json5": "2.2.3", + "jszip": "3.10.1", + "lodash.get": "4.4.2" + } + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -7150,7 +7171,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "optional": true, + "license": "MIT", "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" @@ -7851,7 +7872,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -7884,6 +7904,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.2", "atob": "^2.1.2", @@ -8046,6 +8067,13 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -11280,7 +11308,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", - "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -11801,7 +11828,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", - "optional": true, "dependencies": { "base64-arraybuffer": "^1.0.2" } diff --git a/client/package.json b/client/package.json index f14d041..3c5cddd 100644 --- a/client/package.json +++ b/client/package.json @@ -45,8 +45,10 @@ "buffer": "^6.0.3", "dayjs": "^1.11.13", "docx-templates": "^4.13.0", + "easy-template-x": "^5.1.0", "embla-carousel-react": "^8.3.0", "file-type": "^19.0.0", + "html2canvas": "^1.4.1", "jspdf": "^2.5.2", "ol": "^10.0.0", "ol-ext": "^4.0.23", diff --git a/client/src/pages/PrintReport.tsx b/client/src/pages/PrintReport.tsx index 5b658e0..f310f8f 100644 --- a/client/src/pages/PrintReport.tsx +++ b/client/src/pages/PrintReport.tsx @@ -1,6 +1,11 @@ -import { Button, Flex } from "@mantine/core"; -import { useState } from "react"; -import createReport from 'docx-templates' +import { ActionIcon, Button, Flex, Group, Input, Stack, Text, TextInput } from "@mantine/core" +import { useEffect, useState } from "react" +import createReport, { listCommands } from 'docx-templates' +import { Dropzone, DropzoneProps, IMAGE_MIME_TYPE, MS_WORD_MIME_TYPE } from '@mantine/dropzone' +import { IconFile, IconFileTypeDocx, IconPhoto, IconPlus, IconUpload, IconX } from "@tabler/icons-react" +import { CommandSummary } from "docx-templates/lib/types" +import { Control, Controller, FieldValues, SubmitHandler, useFieldArray, useForm, UseFormRegister } from "react-hook-form" +import { TemplateHandler } from 'easy-template-x' const xslTemplate = ` @@ -905,165 +910,403 @@ const xslTemplate = ` ` -const PrintReport = () => { - const [loading, setLoading] = useState(false); +const handleGenerateExcel = () => { + // Define the example XML data + const xmlData = ` + + + 1 + Region 1 + City 1 + 10 + 1000 + 500 + 200 + 1000 + 300 + 1500 + 400 + 2000 + 500 + 2500 + 300 + + + ` + + // Parse the XSL template and XML data + const parser = new DOMParser() + const xslDoc = parser.parseFromString(xslTemplate, "application/xml") + const xmlDoc = parser.parseFromString(xmlData, "application/xml") + + // Apply the transformation + const xsltProcessor = new XSLTProcessor() + xsltProcessor.importStylesheet(xslDoc) + const resultDocument = xsltProcessor.transformToDocument(xmlDoc) + + // Serialize the result to a string + const serializer = new XMLSerializer() + const resultXml = serializer.serializeToString(resultDocument) + + // Add missing Excel-specific headers if needed + const correctedXml = `\n` + resultXml - const generateDocx = async () => { - setLoading(true); + // Convert to Blob and trigger download + const blob = new Blob([correctedXml], { type: "application/vnd.ms-excel" }) + const url = URL.createObjectURL(blob) + const link = document.createElement("a") + link.href = url + link.download = "template.xls" + link.click() + URL.revokeObjectURL(url) +} + +const TemplateFormET = ({ + templateUrl +}: { + templateUrl: string +}) => { + const [templateUint8Array, setTemplateUint8Array] = useState(null) + const [loading, setLoading] = useState(false) + + const loadTemplate = async (templateUrl: string) => { + setLoading(true) try { - // Fetch the DOCX template from the public folder - const response = await fetch("/template.docx"); - const response_table = await fetch("/template_table.docx"); - const templateArrayBuffer = await response.arrayBuffer(); - const templateArrayBuffer_table = await response_table.arrayBuffer(); + const response = await fetch(templateUrl) + const templateArrayBuffer = await response.arrayBuffer() + + setTemplateUint8Array(new Uint8Array(templateArrayBuffer)) + } catch (error) { + console.error("Error generating DOCX:", error) + } finally { + setLoading(false) + } + } + + const loadTags = async (templateUint8Array: Uint8Array) => { + const handler = new TemplateHandler() + const tags = await handler.parseTags(templateUint8Array) + + console.log(tags) + } + + useEffect(() => { + if (templateUint8Array) { + loadTags(templateUint8Array) + } + }, [templateUint8Array]) + + useEffect(() => { + if (templateUrl) { + loadTemplate(templateUrl) + } + }, [templateUrl]) - // Convert ArrayBuffer to Uint8Array (Fix TypeScript error) - const templateUint8Array = new Uint8Array(templateArrayBuffer); - const templateUint8Array_table = new Uint8Array(templateArrayBuffer_table); + return ( + + + + ) +} + +interface TemplateCommand extends CommandSummary { + children?: CommandSummary[] +} + +export function parseCommandList(commands: CommandSummary[]): TemplateCommand[] { + function parseBlock(startIndex: number, currentElement?: string): [TemplateCommand[], number] { + const block: TemplateCommand[] = [] + let i = startIndex + + while (i < commands.length) { + const command = commands[i] + + if (command.type === "FOR") { + const [elementName, , arrayName] = command.code.split(" ") + const forCommand: TemplateCommand = { + raw: command.raw, + type: command.type, + code: arrayName, + children: [], + } + + const [children, nextIndex] = parseBlock(i + 1, elementName) + forCommand.children = children + i = nextIndex + block.push(forCommand) + } else if (command.type === "END-FOR") { + return [block, i + 1] + } else { + let code = command.code + if (currentElement && (code.startsWith(`${currentElement}.`) || code.startsWith(`$${currentElement}.`))) { + code = code.replace(`$${currentElement}.`, "").replace(`${currentElement}.`, "") + } - // Fetch the image (Example: Load from public folder) - const imageResponse = await fetch("/test.png"); // Change this to your image path - const imageBlob = await imageResponse.blob(); - const imageArrayBuffer = await imageBlob.arrayBuffer(); - const imageUint8Array = new Uint8Array(imageArrayBuffer); + block.push({ + ...command, + code, + }) + i++ + } + } + + return [block, i] + } + + const [parsed] = parseBlock(0) + return parsed +} + +const FormLoop = ({ + control, + register, + command, +}: { + control: Control, + register: UseFormRegister, + command: TemplateCommand, +}) => { + const { fields, append, remove } = useFieldArray({ + name: command.code, + control + }) + + return ( + + + { + fields.map((field, index) => ( + + {command.children && + command.children.map(c => + renderCommand( + control, + register, + c, + `${c.code}`, + `${command.code}.${index}.${c.code}`, + `${command.code}.${index}.${c.code}` + ) + )} + { + remove(index) + }}> + + + + )) + } + + { + if (command.children) { + append(command.children.map(c => c.code).reduce((acc, key) => { + acc[key] = ''; + return acc; + }, {} as Record)) + } + }}> + + + + ) +} +const renderCommand = ( + control: Control, + register: UseFormRegister, + command: CommandSummary, + label: string, + key: string, + name: string, +) => { + if (command.type === 'INS') { + return ( + + ) + } + + if (command.type === 'IMAGE') { + return ( + ( + console.log('rejected files', files)} + onDrop={(files) => { + console.log(files[0]) + files[0].arrayBuffer().then(res => { + onChange({ + width: 6, + height: 6, + data: new Uint8Array(res), + extension: files[0]?.path?.match(/\.[^.]+$/)?.[0] || "" + }) + }) + }} + maxFiles={1} + > + + + + + + + + + + + + + + Drag files here or click to select files + + + Attach as many files as you like, each file should not exceed 5mb + + + + + )} + /> + ) + } +} + +const TemplateForm = ({ + templateUrl, +}: { + templateUrl: string, +}) => { + const { register, control, handleSubmit, reset, watch, formState } = useForm({ + mode: 'onChange', + }) + + const [loading, setLoading] = useState(false) + const [saving, setSaving] = useState(false) + + const [templateUint8Array, setTemplateUint8Array] = useState(null) + + const [commandList, setCommandList] = useState([]) + + const saveTest = async (templateUint8Array: Uint8Array, data: any) => { + setSaving(true) + + try { // Generate the DOCX file with the replacement const report = await createReport({ template: templateUint8Array, // Ensure it's Uint8Array - data: { - test: "Hello World", - myImage: { - width: 6, // Width in cm - height: 6, // Height in cm - data: imageUint8Array, // Image binary data - extension: ".png", // Specify the image format - }, - }, - }); - - const report_table = await createReport({ - template: templateUint8Array_table, // Ensure it's Uint8Array - data: { - test: "Hello World", - rows: [ - { - first: 'A', - second: 'B', - third: 'C', - fourth: 'D', - }, - { - first: 'E', - second: 'F', - third: 'G', - fourth: 'H', - }, - { - first: 'I', - second: 'J', - third: 'K', - fourth: 'L', - } - ] - }, - }); + data: data, + }) // Convert Uint8Array to a Blob const blob = new Blob([report], { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - }); - - const blob_table = new Blob([report_table], { - type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - }); - - // Create a download link and trigger the download - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "report.docx"; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); + }) // Create a download link and trigger the download - const url_table = URL.createObjectURL(blob_table); - const a_table = document.createElement("a"); - a_table.href = url_table; - a_table.download = "report_table.docx"; - document.body.appendChild(a_table); - a_table.click(); - document.body.removeChild(a_table); + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = url + a.download = "report.docx" + document.body.appendChild(a) + a.click() + document.body.removeChild(a) - // Revoke the object URL after download - URL.revokeObjectURL(url); - URL.revokeObjectURL(url_table); + URL.revokeObjectURL(url) } catch (error) { - console.error("Error generating DOCX:", error); + console.error("Error generating DOCX:", error) } finally { - setLoading(false); + setSaving(false) } } - const handleGenerateExcel = () => { - // Define the example XML data - const xmlData = ` - - - 1 - Region 1 - City 1 - 10 - 1000 - 500 - 200 - 1000 - 300 - 1500 - 400 - 2000 - 500 - 2500 - 300 - - - `; + const loadTemplate = async () => { + setLoading(true) - // Parse the XSL template and XML data - const parser = new DOMParser(); - const xslDoc = parser.parseFromString(xslTemplate, "application/xml"); - const xmlDoc = parser.parseFromString(xmlData, "application/xml"); + try { + const response = await fetch(templateUrl) + const templateArrayBuffer = await response.arrayBuffer() - // Apply the transformation - const xsltProcessor = new XSLTProcessor(); - xsltProcessor.importStylesheet(xslDoc); - const resultDocument = xsltProcessor.transformToDocument(xmlDoc); + setTemplateUint8Array(new Uint8Array(templateArrayBuffer)) + } catch (error) { + console.error("Error generating DOCX:", error) + } finally { + setLoading(false) + } + } - // Serialize the result to a string - const serializer = new XMLSerializer(); - const resultXml = serializer.serializeToString(resultDocument); + const loadCommands = async (templateUint8Array: Uint8Array) => { + try { + await listCommands(templateUint8Array).then(l => { + setCommandList(parseCommandList(l)) + }) + } catch (error) { + console.error("Error loading commands from DOCX:", error) + } + } - // Add missing Excel-specific headers if needed - const correctedXml = `\n` + resultXml + useEffect(() => { + if (templateUint8Array) { + loadCommands(templateUint8Array) + } + }, [templateUint8Array]) - // Convert to Blob and trigger download - const blob = new Blob([correctedXml], { type: "application/vnd.ms-excel" }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = "template.xls"; - link.click(); + const onSubmit: SubmitHandler = async (data) => { + try { + if (templateUint8Array && data) { + saveTest(templateUint8Array, data) + } + } catch (error) { + console.error(error) + } + } - // Clean up - URL.revokeObjectURL(url); + useEffect(() => { + if (templateUrl) { + loadTemplate() + } + }, [templateUrl]) + + if (commandList) { + return ( + + + {commandList.map(command => { + if (command.type === 'FOR') { + return ( + + ) + } else { + return renderCommand(control, register, command, command.code, command.code, command.code) + } + })} + Submit + + + ) } +} + +const PrintReport = () => { + const [loading, setLoading] = useState(false) return ( - - {loading ? "Генерация отчета..." : "Сохранить в docx"} - Сохранить в Excel - + + + + + Сохранить в Excel + + ) } diff --git a/client/yarn.lock b/client/yarn.lock index c965585..e35451d 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1869,6 +1869,11 @@ dependencies: "@swc/core" "^1.5.7" +"@xmldom/xmldom@0.8.10": + version "0.8.10" + resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz" + integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== + "@zeit/schemas@2.36.0": version "2.36.0" resolved "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz" @@ -2932,6 +2937,16 @@ eastasianwidth@^0.2.0: resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +easy-template-x@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/easy-template-x/-/easy-template-x-5.1.0.tgz" + integrity sha512-vypMbIMLWLXoooA9rsL3SVN2oQtZwmmx1m4H8gi6JfbEXQQ5VLHGOUHYi9APbvN9R8Gx93r1fphdSFRHxozeYw== + dependencies: + "@xmldom/xmldom" "0.8.10" + json5 "2.2.3" + jszip "3.10.1" + lodash.get "4.4.2" + ejs@^3.1.6: version "3.1.10" resolved "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz" @@ -3665,7 +3680,7 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -html2canvas@^1.0.0-rc.5: +html2canvas@^1.0.0-rc.5, html2canvas@^1.4.1: version "1.4.1" resolved "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz" integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA== @@ -4056,7 +4071,7 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json5@^2.2.0, json5@^2.2.3: +json5@^2.2.0, json5@^2.2.3, json5@2.2.3: version "2.2.3" resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -4090,7 +4105,7 @@ jspdf@^2.5.1, jspdf@^2.5.2: dompurify "^2.5.4" html2canvas "^1.0.0-rc.5" -jszip@^3.10.1: +jszip@^3.10.1, jszip@3.10.1: version "3.10.1" resolved "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz" integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== @@ -4176,6 +4191,11 @@ lodash.debounce@^4.0.8: resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.get@4.4.2: + version "4.4.2" + resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"