Form from template

This commit is contained in:
2025-03-26 10:31:00 +09:00
parent 3bcea3f1ac
commit 442255ebaf
5 changed files with 439 additions and 148 deletions

View File

@ -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 = `<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
@ -905,165 +910,403 @@ const xslTemplate = `<?xml version="1.0" encoding="utf-8"?>
</xsl:template>
</xsl:stylesheet>`
const PrintReport = () => {
const [loading, setLoading] = useState(false);
const handleGenerateExcel = () => {
// Define the example XML data
const xmlData = `
<root>
<Kvp>
<style_id>1</style_id>
<region>Region 1</region>
<city>City 1</city>
<house_count>10</house_count>
<square>1000</square>
<people_count>500</people_count>
<volume_heat>200</volume_heat>
<sum_heat>1000</sum_heat>
<volume_hwater>300</volume_hwater>
<sum_hwater>1500</sum_hwater>
<volume_cwater>400</volume_cwater>
<sum_cwater>2000</sum_cwater>
<volume_sewers>500</volume_sewers>
<sum_sewers>2500</sum_sewers>
<saldo_out>300</saldo_out>
</Kvp>
</root>
`
const generateDocx = 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")
// 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 = `<?xml version="1.0" encoding="utf-8"?>\n` + resultXml
// 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<Uint8Array | null>(null)
const [loading, setLoading] = useState(false)
const loadTemplate = async (templateUrl: string) => {
setLoading(true)
try {
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])
return (
<div>
</div>
)
}
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}.`, "")
}
block.push({
...command,
code,
})
i++
}
}
return [block, i]
}
const [parsed] = parseBlock(0)
return parsed
}
const FormLoop = ({
control,
register,
command,
}: {
control: Control<FieldValues, any>,
register: UseFormRegister<FieldValues>,
command: TemplateCommand,
}) => {
const { fields, append, remove } = useFieldArray({
name: command.code,
control
})
return (
<Stack align="center">
<Stack w='100%' key={command.code}>
{
fields.map((field, index) => (
<Flex w='100%' justify='space-between' align='flex-end' key={field.id}>
{command.children &&
command.children.map(c =>
renderCommand(
control,
register,
c,
`${c.code}`,
`${command.code}.${index}.${c.code}`,
`${command.code}.${index}.${c.code}`
)
)}
<Button variant='subtle' onClick={() => {
remove(index)
}}>
<IconX />
</Button>
</Flex>
))
}
</Stack>
<ActionIcon onClick={() => {
if (command.children) {
append(command.children.map(c => c.code).reduce((acc, key) => {
acc[key] = '';
return acc;
}, {} as Record<string, string>))
}
}}>
<IconPlus />
</ActionIcon>
</Stack>
)
}
const renderCommand = (
control: Control<FieldValues, any>,
register: UseFormRegister<FieldValues>,
command: CommandSummary,
label: string,
key: string,
name: string,
) => {
if (command.type === 'INS') {
return (
<TextInput
label={label}
key={key}
{...register(name)}
/>
)
}
if (command.type === 'IMAGE') {
return (
<Controller
key={key}
name={name}
control={control}
render={({ field: { onChange, value } }) => (
<Dropzone
accept={IMAGE_MIME_TYPE}
maxSize={5 * 1024 ** 2}
onReject={(files) => 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}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconFileTypeDocx size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag files here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
</Text>
</div>
</Group>
</Dropzone>
)}
/>
)
}
}
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<Uint8Array | null>(null)
const [commandList, setCommandList] = useState<TemplateCommand[]>([])
const saveTest = async (templateUint8Array: Uint8Array, data: any) => {
setSaving(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();
// Convert ArrayBuffer to Uint8Array (Fix TypeScript error)
const templateUint8Array = new Uint8Array(templateArrayBuffer);
const templateUint8Array_table = new Uint8Array(templateArrayBuffer_table);
// 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);
// 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);
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);
// 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 = `
<root>
<Kvp>
<style_id>1</style_id>
<region>Region 1</region>
<city>City 1</city>
<house_count>10</house_count>
<square>1000</square>
<people_count>500</people_count>
<volume_heat>200</volume_heat>
<sum_heat>1000</sum_heat>
<volume_hwater>300</volume_hwater>
<sum_hwater>1500</sum_hwater>
<volume_cwater>400</volume_cwater>
<sum_cwater>2000</sum_cwater>
<volume_sewers>500</volume_sewers>
<sum_sewers>2500</sum_sewers>
<saldo_out>300</saldo_out>
</Kvp>
</root>
`;
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);
// Serialize the result to a string
const serializer = new XMLSerializer();
const resultXml = serializer.serializeToString(resultDocument);
// Add missing Excel-specific headers if needed
const correctedXml = `<?xml version="1.0" encoding="utf-8"?>\n` + resultXml
// 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();
// Clean up
URL.revokeObjectURL(url);
setTemplateUint8Array(new Uint8Array(templateArrayBuffer))
} catch (error) {
console.error("Error generating DOCX:", error)
} finally {
setLoading(false)
}
}
const loadCommands = async (templateUint8Array: Uint8Array) => {
try {
await listCommands(templateUint8Array).then(l => {
setCommandList(parseCommandList(l))
})
} catch (error) {
console.error("Error loading commands from DOCX:", error)
}
}
useEffect(() => {
if (templateUint8Array) {
loadCommands(templateUint8Array)
}
}, [templateUint8Array])
const onSubmit: SubmitHandler<any> = async (data) => {
try {
if (templateUint8Array && data) {
saveTest(templateUint8Array, data)
}
} catch (error) {
console.error(error)
}
}
useEffect(() => {
if (templateUrl) {
loadTemplate()
}
}, [templateUrl])
if (commandList) {
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Stack>
{commandList.map(command => {
if (command.type === 'FOR') {
return (
<FormLoop key={command.code} control={control} register={register} command={command} />
)
} else {
return renderCommand(control, register, command, command.code, command.code, command.code)
}
})}
<Button type='submit'>Submit</Button>
</Stack>
</form>
)
}
}
const PrintReport = () => {
const [loading, setLoading] = useState(false)
return (
<Flex p='sm' gap='sm'>
<Button onClick={generateDocx} disabled={loading}>{loading ? "Генерация отчета..." : "Сохранить в docx"}</Button>
<Button onClick={handleGenerateExcel}>Сохранить в Excel</Button>
</Flex>
<Stack p='sm' gap='sm' w='100%'>
<TemplateForm templateUrl="/template_table.docx" />
<Flex gap='sm'>
<Button onClick={handleGenerateExcel}>Сохранить в Excel</Button>
</Flex>
</Stack>
)
}