diff --git a/DOCX_TEMPLATES_README.md b/DOCX_TEMPLATES_README.md new file mode 100644 index 0000000..1d5ff07 --- /dev/null +++ b/DOCX_TEMPLATES_README.md @@ -0,0 +1,155 @@ +# DOCX Template System + +This system allows you to generate DOCX documents by filling templates with project data. + +## How to Create Templates + +1. **Create a DOCX Template**: Use Microsoft Word or any DOCX editor to create your template. + +2. **Add Placeholders**: Use single curly braces `{variableName}` to mark where data should be inserted. Available variables: + +### Available Variables (with duplicates for repeated use) + +#### Project Information +- `{project_name}`, `{project_name_1}`, `{project_name_2}`, `{project_name_3}` - Project name +- `{project_number}`, `{project_number_1}`, `{project_number_2}` - Project number +- `{address}`, `{address_1}`, `{address_2}` - Project address +- `{city}`, `{city_1}`, `{city_2}` - City +- `{plot}` - Plot number +- `{district}` - District +- `{unit}` - Unit +- `{investment_number}` - Investment number +- `{wp}` - WP number +- `{coordinates}` - GPS coordinates +- `{notes}` - Project notes + +#### Processed/Transformed Fields +- `{investment_number_short}` - Last part of investment number after last dash (e.g., "1234567" from "I-BC-DE-1234567") +- `{project_number_short}` - Last part of project number after last dash +- `{project_name_upper}` - Project name in uppercase +- `{project_name_lower}` - Project name in lowercase +- `{city_upper}` - City name in uppercase +- `{customer_upper}` - Customer name in uppercase + +#### Contract Information +- `{contract_number}` - Contract number +- `{customer_contract_number}` - Customer contract number +- `{customer}`, `{customer_1}`, `{customer_2}` - Customer name +- `{investor}` - Investor name + +#### Dates +- `{finish_date}` - Finish date (formatted) +- `{completion_date}` - Completion date (formatted) +- `{today_date}` - Today's date + +#### Project Type & Status +- `{project_type}` - Project type (design/construction/design+construction) +- `{project_status}` - Project status + +#### Financial +- `{wartosc_zlecenia}`, `{wartosc_zlecenia_1}`, `{wartosc_zlecenia_2}` - Contract value + +#### Contacts +- `{contacts}` - Array of contacts (use loops in advanced templates) +- `{primary_contact}` - Primary contact name +- `{primary_contact_phone}` - Primary contact phone +- `{primary_contact_email}` - Primary contact email + +## Example Template Content + +``` +Project Report + +Project Name: {project_name} ({project_name_upper}) +Project Number: {project_number} (Short: {project_number_short}) +Location: {city_upper}, {address} + +Investment Details: +Full Investment Number: {investment_number} +Short Investment Number: {investment_number_short} + +Contract Details: +Contract Number: {contract_number} +Customer: {customer} ({customer_upper}) +Value: {wartosc_zlecenia} PLN + +Custom Information: +Meeting Notes: {meeting_notes} +Special Instructions: {special_instructions} +Additional Comments: {additional_comments} + +Primary Contact: +Name: {primary_contact} +Phone: {primary_contact_phone} +Email: {primary_contact_email} + +Generated on: {today_date} +``` + +## Uploading Templates + +1. Go to the Templates page (`/templates`) +2. Click "Add Template" +3. Provide a name and description +4. Upload your DOCX file +5. The template will be available for generating documents + +## Generating Documents + +1. Open any project page +2. In the sidebar, find the "Generate Document" section +3. Select a template from the dropdown +4. **Optional**: Click "Pokaż dodatkowe pola" to add custom data +5. Add any custom fields you need (e.g., `custom_note`, `additional_info`) +6. Click "Generate Document" +7. The filled document will be downloaded automatically with filename: `{template_name}_{project_name}_{timestamp}.docx` + +## Custom Data Fields + +During document generation, you can add custom data that will be merged with the project data: + +- **Custom fields** override project data if they have the same name +- Use descriptive names like `meeting_notes`, `special_instructions`, `custom_date` +- Custom fields are available in templates as `{custom_field_name}` +- Empty custom fields are ignored + +### Example Custom Fields: +- `meeting_notes`: "Please bring project documentation" +- `special_instructions`: "Use company letterhead" +- `custom_date`: "2025-01-15" +- `additional_comments`: "Follow up required" + +## Template Syntax + +The system uses `docxtemplater` library which supports: +- Simple variable replacement: `{variable}` +- Loops: `{#contacts}{name}{/contacts}` +- Conditions: `{#primary_contact}Primary: {name}{/primary_contact}` +- Formatting and styling from your DOCX template is preserved + +## Data Processing & Transformations + +The system automatically provides processed versions of common fields: + +- **Short codes**: `{investment_number_short}` extracts the last segment after dashes (e.g., "1234567" from "I-BC-DE-1234567") +- **Case transformations**: `{project_name_upper}`, `{city_upper}`, `{customer_upper}` for uppercase versions +- **Duplicate fields**: Multiple versions of the same field for repeated use (`{project_name_1}`, `{project_name_2}`, etc.) + +If you need additional transformations (like extracting different parts of codes, custom formatting, calculations, etc.), please let us know and we can add them to the system. + +## Tips + +- Test your templates with sample data first +- Use descriptive variable names +- Keep formatting simple for best results +- Save templates with `.docx` extension only +- Maximum file size: 10MB +- **For repeated information**: If you need the same data to appear multiple times, create unique placeholders like `{project_name_header}` and `{project_name_footer}` and provide the same value for both + +## Troubleshooting + +- **Duplicate tags error**: Each placeholder can only be used once in the template. If you need the same information to appear multiple times, use unique placeholders like `{project_name_1}` and `{project_name_2}` with the same data value. +- **Template not rendering**: Check for syntax errors in placeholders +- **Missing data**: Ensure the project has the required information +- **Formatting issues**: Try simplifying the template formatting +- **File not downloading**: Check browser popup blockers \ No newline at end of file diff --git a/migrate-add-docx-templates-table.mjs b/migrate-add-docx-templates-table.mjs new file mode 100644 index 0000000..6315bbb --- /dev/null +++ b/migrate-add-docx-templates-table.mjs @@ -0,0 +1,38 @@ +import db from "./src/lib/db.js"; + +// Migration to add docx_templates table +const migration = () => { + console.log("Running migration: add-docx-templates-table"); + + try { + db.exec(` + -- Table: docx_templates + CREATE TABLE IF NOT EXISTS docx_templates ( + template_id INTEGER PRIMARY KEY AUTOINCREMENT, + template_name TEXT NOT NULL, + description TEXT, + original_filename TEXT NOT NULL, + stored_filename TEXT NOT NULL, + file_path TEXT NOT NULL, + file_size INTEGER NOT NULL, + mime_type TEXT NOT NULL, + is_active INTEGER DEFAULT 1, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + created_by TEXT, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (created_by) REFERENCES users(id) + ); + + -- Indexes for templates + CREATE INDEX IF NOT EXISTS idx_docx_templates_active ON docx_templates(is_active); + CREATE INDEX IF NOT EXISTS idx_docx_templates_created_by ON docx_templates(created_by); + `); + + console.log("Migration completed successfully"); + } catch (error) { + console.error("Migration failed:", error); + throw error; + } +}; + +migration(); \ No newline at end of file diff --git a/src/app/api/templates/[templateId]/route.js b/src/app/api/templates/[templateId]/route.js new file mode 100644 index 0000000..4489249 --- /dev/null +++ b/src/app/api/templates/[templateId]/route.js @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from "next/server"; +import { unlink } from "fs/promises"; +import path from "path"; +import db from "@/lib/db"; + +export async function DELETE(request, { params }) { + try { + const { templateId } = params; + + // Get template info + const template = db.prepare(` + SELECT * FROM docx_templates WHERE template_id = ? + `).get(templateId); + + if (!template) { + return NextResponse.json( + { error: "Template not found" }, + { status: 404 } + ); + } + + // Soft delete by setting is_active to 0 + db.prepare(` + UPDATE docx_templates + SET is_active = 0, updated_at = CURRENT_TIMESTAMP + WHERE template_id = ? + `).run(templateId); + + // Optionally delete the file (uncomment if you want hard delete) + // try { + // const filePath = path.join(process.cwd(), "public", template.file_path); + // await unlink(filePath); + // } catch (fileError) { + // console.warn("Could not delete template file:", fileError); + // } + + return NextResponse.json({ success: true }); + + } catch (error) { + console.error("Template deletion error:", error); + return NextResponse.json( + { error: "Failed to delete template" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/templates/generate/route.js b/src/app/api/templates/generate/route.js new file mode 100644 index 0000000..2124929 --- /dev/null +++ b/src/app/api/templates/generate/route.js @@ -0,0 +1,209 @@ +import { NextRequest, NextResponse } from "next/server"; +import PizZip from "pizzip"; +import Docxtemplater from "docxtemplater"; +import { readFile, writeFile } from "fs/promises"; +import path from "path"; +import db from "@/lib/db"; +import { formatDate, formatCoordinates } from "@/lib/utils"; + +export async function POST(request) { + try { + const { templateId, projectId, customData } = await request.json(); + + if (!templateId || !projectId) { + return NextResponse.json( + { error: "templateId and projectId are required" }, + { status: 400 } + ); + } + + // Get template + const template = db.prepare(` + SELECT * FROM docx_templates WHERE template_id = ? AND is_active = 1 + `).get(templateId); + + if (!template) { + return NextResponse.json( + { error: "Template not found" }, + { status: 404 } + ); + } + + // Get project data + const project = db.prepare(` + SELECT + p.*, + c.contract_number, + c.customer_contract_number, + c.customer, + c.investor + FROM projects p + LEFT JOIN contracts c ON p.contract_id = c.contract_id + WHERE p.project_id = ? + `).get(projectId); + + if (!project) { + return NextResponse.json( + { error: "Project not found" }, + { status: 404 } + ); + } + + // Get project contacts + const contacts = db.prepare(` + SELECT + pc.*, + ct.name, + ct.phone, + ct.email, + ct.company, + ct.contact_type + FROM project_contacts pc + JOIN contacts ct ON pc.contact_id = ct.contact_id + WHERE pc.project_id = ? + ORDER BY pc.is_primary DESC, ct.name + `).all(projectId); + + // Load template file + const templatePath = path.join(process.cwd(), "public", template.file_path); + const templateContent = await readFile(templatePath); + + // Load the docx file as a binary + const zip = new PizZip(templateContent); + + const doc = new Docxtemplater(zip, { + paragraphLoop: true, + linebreaks: true, + }); + + // Prepare data for template + const templateData = { + // Project basic info + project_name: project.project_name || "", + project_number: project.project_number || "", + address: project.address || "", + city: project.city || "", + plot: project.plot || "", + district: project.district || "", + unit: project.unit || "", + investment_number: project.investment_number || "", + wp: project.wp || "", + coordinates: project.coordinates || "", + notes: project.notes || "", + + // Processed fields (extracted/transformed data) + investment_number_short: project.investment_number ? project.investment_number.split('-').pop() : "", + project_number_short: project.project_number ? project.project_number.split('-').pop() : "", + project_name_upper: project.project_name ? project.project_name.toUpperCase() : "", + project_name_lower: project.project_name ? project.project_name.toLowerCase() : "", + city_upper: project.city ? project.city.toUpperCase() : "", + customer_upper: project.customer ? project.customer.toUpperCase() : "", + + // Contract info + contract_number: project.contract_number || "", + customer_contract_number: project.customer_contract_number || "", + customer: project.customer || "", + investor: project.investor || "", + + // Dates + finish_date: project.finish_date ? formatDate(project.finish_date) : "", + completion_date: project.completion_date ? formatDate(project.completion_date) : "", + today_date: formatDate(new Date()), + + // Project type and status + project_type: project.project_type || "", + project_status: project.project_status || "", + + // Financial + wartosc_zlecenia: project.wartosc_zlecenia ? project.wartosc_zlecenia.toString() : "", + + // Contacts + contacts: contacts.map(contact => ({ + name: contact.name || "", + phone: contact.phone || "", + email: contact.email || "", + company: contact.company || "", + contact_type: contact.contact_type || "", + is_primary: contact.is_primary ? "Tak" : "Nie" + })), + + // Primary contact + primary_contact: contacts.find(c => c.is_primary)?.name || "", + primary_contact_phone: contacts.find(c => c.is_primary)?.phone || "", + primary_contact_email: contacts.find(c => c.is_primary)?.email || "", + + // Duplicate fields for repeated use (common fields that users might want to repeat) + project_name_1: project.project_name || "", + project_name_2: project.project_name || "", + project_name_3: project.project_name || "", + project_number_1: project.project_number || "", + project_number_2: project.project_number || "", + customer_1: project.customer || "", + customer_2: project.customer || "", + address_1: project.address || "", + address_2: project.address || "", + city_1: project.city || "", + city_2: project.city || "", + wartosc_zlecenia_1: project.wartosc_zlecenia ? project.wartosc_zlecenia.toString() : "", + wartosc_zlecenia_2: project.wartosc_zlecenia ? project.wartosc_zlecenia.toString() : "", + }; + + // Merge custom data (custom data takes precedence over project data) + if (customData && typeof customData === 'object') { + Object.assign(templateData, customData); + } + + // Set the template variables + doc.setData(templateData); + + try { + // Render the document + doc.render(); + } catch (error) { + console.error("Template rendering error:", error); + + // Check if it's a duplicate tags error + if (error.name === 'TemplateError' && error.properties?.id === 'duplicate_open_tag') { + return NextResponse.json( + { + error: "Template contains duplicate placeholders. Each placeholder (like {{project_name}}) can only be used once in the template. Please modify your DOCX template to use unique placeholders or remove duplicates.", + details: `Duplicate tag found: ${error.properties?.xtag || 'unknown'}` + }, + { status: 400 } + ); + } + + return NextResponse.json( + { error: "Failed to render template. Please check template syntax and ensure all placeholders are properly formatted." }, + { status: 400 } + ); + } + + // Get the generated document + const buf = doc.getZip().generate({ + type: "nodebuffer", + compression: "DEFLATE", + }); + + // Generate filename + const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, "-"); + const sanitizedTemplateName = template.template_name.replace(/[^a-zA-Z0-9]/g, "_"); + const sanitizedProjectName = project.project_name.replace(/[^a-zA-Z0-9]/g, "_"); + const filename = `${sanitizedTemplateName}_${sanitizedProjectName}_${timestamp}.docx`; + + // Return the file as a downloadable response + return new NextResponse(buf, { + headers: { + "Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "Content-Disposition": `attachment; filename="${filename}"`, + }, + }); + + } catch (error) { + console.error("Template generation error:", error); + return NextResponse.json( + { error: "Failed to generate document" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/templates/route.js b/src/app/api/templates/route.js new file mode 100644 index 0000000..f9e430b --- /dev/null +++ b/src/app/api/templates/route.js @@ -0,0 +1,133 @@ +import { NextRequest, NextResponse } from "next/server"; +import { writeFile, mkdir, unlink } from "fs/promises"; +import { existsSync } from "fs"; +import path from "path"; +import db from "@/lib/db"; + +const TEMPLATES_DIR = path.join(process.cwd(), "public", "templates"); +const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB +const ALLOWED_TYPES = [ + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" +]; + +export async function POST(request) { + try { + const formData = await request.formData(); + const file = formData.get("file"); + const templateName = formData.get("templateName"); + const description = formData.get("description") || ""; + + if (!file || !templateName) { + return NextResponse.json( + { error: "File and templateName are required" }, + { status: 400 } + ); + } + + // Validate file + if (file.size > MAX_FILE_SIZE) { + return NextResponse.json( + { error: "File size too large (max 10MB)" }, + { status: 400 } + ); + } + + if (!ALLOWED_TYPES.includes(file.type)) { + return NextResponse.json( + { error: "Only DOCX files are allowed" }, + { status: 400 } + ); + } + + // Create templates directory + if (!existsSync(TEMPLATES_DIR)) { + await mkdir(TEMPLATES_DIR, { recursive: true }); + } + + // Generate unique filename + const timestamp = Date.now(); + const sanitizedOriginalName = file.name.replace(/[^a-zA-Z0-9.-]/g, "_"); + const storedFilename = `${timestamp}_${sanitizedOriginalName}`; + const filePath = path.join(TEMPLATES_DIR, storedFilename); + const relativePath = `/templates/${storedFilename}`; + + // Save file + const bytes = await file.arrayBuffer(); + const buffer = Buffer.from(bytes); + await writeFile(filePath, buffer); + + // Save to database + const stmt = db.prepare(` + INSERT INTO docx_templates ( + template_name, description, original_filename, stored_filename, + file_path, file_size, mime_type, created_by + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + + const result = stmt.run( + templateName, + description, + file.name, + storedFilename, + relativePath, + file.size, + file.type, + null // TODO: Get from session when auth is implemented + ); + + const newTemplate = { + template_id: result.lastInsertRowid, + template_name: templateName, + description: description, + original_filename: file.name, + stored_filename: storedFilename, + file_path: relativePath, + file_size: file.size, + mime_type: file.type, + is_active: 1, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + }; + + return NextResponse.json(newTemplate, { status: 201 }); + + } catch (error) { + console.error("Template upload error:", error); + return NextResponse.json( + { error: "Failed to upload template" }, + { status: 500 } + ); + } +} + +export async function GET(request) { + try { + const templates = db.prepare(` + SELECT + template_id, + template_name, + description, + original_filename, + stored_filename, + file_path, + file_size, + mime_type, + is_active, + created_at, + created_by, + updated_at + FROM docx_templates + WHERE is_active = 1 + ORDER BY created_at DESC + `).all(); + + return NextResponse.json(templates); + + } catch (error) { + console.error("Error fetching templates:", error); + return NextResponse.json( + { error: "Failed to fetch templates" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/projects/[id]/page.js b/src/app/projects/[id]/page.js index 014d066..679fb7f 100644 --- a/src/app/projects/[id]/page.js +++ b/src/app/projects/[id]/page.js @@ -19,6 +19,7 @@ import ProjectAssigneeDropdown from "@/components/ProjectAssigneeDropdown"; import ClientProjectMap from "@/components/ui/ClientProjectMap"; import FileUploadBox from "@/components/FileUploadBox"; import FileItem from "@/components/FileItem"; +import DocumentGenerator from "@/components/DocumentGenerator"; import proj4 from "proj4"; export default function ProjectViewPage() { @@ -31,6 +32,7 @@ export default function ProjectViewPage() { const [editingNoteId, setEditingNoteId] = useState(null); const [editText, setEditText] = useState(''); const [projectContacts, setProjectContacts] = useState([]); + const [showDocumentModal, setShowDocumentModal] = useState(false); // Helper function to parse note text with links const parseNoteText = (text) => { @@ -800,6 +802,27 @@ export default function ProjectViewPage() { Zobacz wszystkie na mapie + @@ -816,9 +839,9 @@ export default function ProjectViewPage() {

Przesłane pliki:

{uploadedFiles.map((file) => ( - @@ -827,6 +850,7 @@ export default function ProjectViewPage() { )} +
{/* Project Location Map */} @@ -1045,6 +1069,31 @@ export default function ProjectViewPage() { )} + + {/* Document Generator Modal */} + {showDocumentModal && ( +
e.target === e.currentTarget && setShowDocumentModal(false)} + > +
+
+

+ Generuj dokument +

+ +
+ +
+
+ )} ); } diff --git a/src/app/projects/page.js b/src/app/projects/page.js index 4f6079b..9d71d37 100644 --- a/src/app/projects/page.js +++ b/src/app/projects/page.js @@ -274,6 +274,24 @@ export default function ProjectListPage() { + + + + + } + /> + + {showUploadForm && ( +
+ +
+ setShowUploadForm(false)} + /> +
+
+
+ )} + + +
+ +
+
+ + ); +} \ No newline at end of file diff --git a/src/components/DocumentGenerator.js b/src/components/DocumentGenerator.js new file mode 100644 index 0000000..4d835c1 --- /dev/null +++ b/src/components/DocumentGenerator.js @@ -0,0 +1,248 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Button from "@/components/ui/Button"; +import Card from "@/components/ui/Card"; + +export default function DocumentGenerator({ projectId }) { + const [templates, setTemplates] = useState([]); + const [selectedTemplate, setSelectedTemplate] = useState(""); + const [generating, setGenerating] = useState(false); + const [loading, setLoading] = useState(true); + const [customFields, setCustomFields] = useState([{ key: "", value: "" }]); + const [showCustomFields, setShowCustomFields] = useState(false); + + useEffect(() => { + fetchTemplates(); + }, []); + + const fetchTemplates = async () => { + try { + const response = await fetch("/api/templates"); + if (response.ok) { + const data = await response.json(); + setTemplates(data); + } + } catch (error) { + console.error("Error fetching templates:", error); + } finally { + setLoading(false); + } + }; + + const handleCustomFieldChange = (index, field, value) => { + const updatedFields = [...customFields]; + updatedFields[index][field] = value; + setCustomFields(updatedFields); + }; + + const addCustomField = () => { + setCustomFields([...customFields, { key: "", value: "" }]); + }; + + const removeCustomField = (index) => { + if (customFields.length > 1) { + setCustomFields(customFields.filter((_, i) => i !== index)); + } + }; + + const getCustomData = () => { + const customData = {}; + customFields.forEach(field => { + if (field.key.trim()) { + customData[field.key.trim()] = field.value; + } + }); + return customData; + }; + + const handleGenerate = async () => { + if (!selectedTemplate) { + alert("Proszę wybrać szablon"); + return; + } + + setGenerating(true); + + try { + const customData = getCustomData(); + const response = await fetch("/api/templates/generate", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + templateId: selectedTemplate, + projectId: projectId, + customData: customData, + }), + }); + + if (response.ok) { + // Create a blob from the response and download it + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + + // Get filename from response headers + const contentDisposition = response.headers.get("Content-Disposition"); + let filename = "generated_document.docx"; + if (contentDisposition) { + const filenameMatch = contentDisposition.match(/filename="(.+)"/); + if (filenameMatch) { + filename = filenameMatch[1]; + } + } + + a.download = filename; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } else { + const error = await response.json(); + let errorMessage = error.error; + + // Provide more helpful error messages + if (error.details) { + errorMessage += `\n\nSzczegóły: ${error.details}`; + } + + if (error.error.includes("duplicate")) { + errorMessage += "\n\nRozwiązanie: Użyj unikalnych nazw dla każdego wystąpienia tej samej informacji, np. {{project_name_1}} i {{project_name_2}}."; + } + + alert(`Błąd podczas generowania dokumentu:\n\n${errorMessage}`); + } + } catch (error) { + console.error("Generation error:", error); + alert("Błąd podczas generowania dokumentu"); + } finally { + setGenerating(false); + } + }; + + if (loading) { + return ( +
Ładowanie szablonów...
+ ); + } + + if (templates.length === 0) { + return ( +
+ Brak dostępnych szablonów.{" "} + + Dodaj szablon + +
+ ); + } + + return ( +
+
+ + +
+ + {/* Custom Fields Section */} +
+
+ + +
+ + {showCustomFields && ( +
+ {customFields.map((field, index) => ( +
+ handleCustomFieldChange(index, 'key', e.target.value)} + className="flex-1 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-transparent" + /> + handleCustomFieldChange(index, 'value', e.target.value)} + className="flex-1 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-transparent" + /> + {customFields.length > 1 && ( + + )} +
+ ))} + +
+ Wprowadź dodatkowe dane, które będą dostępne w szablonie jako {"{custom_note}"}, {"{additional_info}"} itp. +
+
+ )} +
+ + + +
+ Dokument zostanie wygenerowany na podstawie danych projektu i pobrany automatycznie. +
+
+ ); +} \ No newline at end of file diff --git a/src/components/TemplateList.js b/src/components/TemplateList.js new file mode 100644 index 0000000..0944640 --- /dev/null +++ b/src/components/TemplateList.js @@ -0,0 +1,146 @@ +"use client"; + +import { useState } from "react"; +import Button from "@/components/ui/Button"; +import Badge from "@/components/ui/Badge"; + +export default function TemplateList({ templates, onTemplateDeleted }) { + const [deletingId, setDeletingId] = useState(null); + + const handleDelete = async (templateId) => { + if (!confirm("Czy na pewno chcesz usunąć ten szablon?")) { + return; + } + + setDeletingId(templateId); + + try { + const response = await fetch(`/api/templates/${templateId}`, { + method: "DELETE", + }); + + if (response.ok) { + onTemplateDeleted(templateId); + } else { + alert("Błąd podczas usuwania szablonu"); + } + } catch (error) { + console.error("Delete error:", error); + alert("Błąd podczas usuwania szablonu"); + } finally { + setDeletingId(null); + } + }; + + const formatFileSize = (bytes) => { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; + }; + + const formatDate = (dateString) => { + return new Date(dateString).toLocaleDateString("pl-PL", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); + }; + + if (templates.length === 0) { + return ( +
+
+ + + +
+

+ Brak szablonów +

+

+ Dodaj swój pierwszy szablon dokumentów DOCX. +

+
+ ); + } + + return ( +
+

+ Dostępne szablony ({templates.length}) +

+ +
+ {templates.map((template) => ( +
+
+
+
+

+ {template.template_name} +

+ + Aktywny + +
+ + {template.description && ( +

+ {template.description} +

+ )} + +
+ + Plik: {template.original_filename} + + + Rozmiar: {formatFileSize(template.file_size)} + + + Dodano: {formatDate(template.created_at)} + +
+
+ +
+ + +
+
+
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/TemplateUploadForm.js b/src/components/TemplateUploadForm.js new file mode 100644 index 0000000..c40b8a6 --- /dev/null +++ b/src/components/TemplateUploadForm.js @@ -0,0 +1,134 @@ +"use client"; + +import { useState, useRef } from "react"; +import Button from "@/components/ui/Button"; + +export default function TemplateUploadForm({ onTemplateUploaded, onCancel }) { + const [uploading, setUploading] = useState(false); + const [formData, setFormData] = useState({ + templateName: "", + description: "", + }); + const fileInputRef = useRef(null); + + const handleSubmit = async (e) => { + e.preventDefault(); + + const file = fileInputRef.current?.files[0]; + if (!file) { + alert("Proszę wybrać plik DOCX"); + return; + } + + if (!formData.templateName.trim()) { + alert("Proszę podać nazwę szablonu"); + return; + } + + setUploading(true); + + try { + const uploadData = new FormData(); + uploadData.append("file", file); + uploadData.append("templateName", formData.templateName); + uploadData.append("description", formData.description); + + const response = await fetch("/api/templates", { + method: "POST", + body: uploadData, + }); + + if (response.ok) { + const newTemplate = await response.json(); + onTemplateUploaded(newTemplate); + setFormData({ templateName: "", description: "" }); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + } else { + const error = await response.json(); + alert(`Błąd podczas przesyłania: ${error.error}`); + } + } catch (error) { + console.error("Upload error:", error); + alert("Błąd podczas przesyłania szablonu"); + } finally { + setUploading(false); + } + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + return ( +
+
+ + +
+ +
+ +