@codemind.ec/medusa-plugin-invoice 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/admin/index.js +512 -7
- package/.medusa/server/src/admin/index.mjs +513 -10
- package/.medusa/server/src/api/admin/invoice-config/route.js +26 -0
- package/.medusa/server/src/api/admin/invoice-templates/[id]/preview/route.js +119 -0
- package/.medusa/server/src/api/admin/invoice-templates/[id]/restore/route.js +21 -0
- package/.medusa/server/src/api/admin/invoice-templates/[id]/route.js +31 -0
- package/.medusa/server/src/api/admin/invoice-templates/route.js +20 -0
- package/.medusa/server/src/api/admin/invoice-templates/validators.js +18 -0
- package/.medusa/server/src/api/middlewares.js +36 -0
- package/.medusa/server/src/index.js +16 -1
- package/.medusa/server/src/modules/invoice-generator/errors.js +30 -0
- package/.medusa/server/src/modules/invoice-generator/index.js +15 -0
- package/.medusa/server/src/modules/invoice-generator/loaders/create-default-config.js +34 -0
- package/.medusa/server/src/modules/invoice-generator/migrations/Migration20250728094738.js +18 -0
- package/.medusa/server/src/modules/invoice-generator/migrations/Migration20250728124134.js +14 -0
- package/.medusa/server/src/modules/invoice-generator/migrations/Migration20250728151454.js +14 -0
- package/.medusa/server/src/modules/invoice-generator/migrations/Migration20260120005559.js +14 -0
- package/.medusa/server/src/modules/invoice-generator/migrations/Migration20260216191700.js +14 -0
- package/.medusa/server/src/modules/invoice-generator/migrations/Migration20260601120000.js +17 -0
- package/.medusa/server/src/modules/invoice-generator/models/invoice-config.js +16 -0
- package/.medusa/server/src/modules/invoice-generator/models/invoice-template.js +19 -0
- package/.medusa/server/src/modules/invoice-generator/models/invoice.js +17 -0
- package/.medusa/server/src/modules/invoice-generator/service.js +84 -0
- package/.medusa/server/src/modules/invoice-generator/templates/defaults/index.js +380 -0
- package/.medusa/server/src/modules/invoice-generator/templates/order-invoice.js +348 -0
- package/.medusa/server/src/modules/invoice-generator/templates/quote-proforma.js +468 -0
- package/.medusa/server/src/modules/invoice-generator/templates/strategy.js +110 -0
- package/LICENSE +21 -0
- package/README.md +204 -0
- package/index.d.ts +138 -0
- package/package.json +26 -2
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
2
|
import { defineWidgetConfig, defineRouteConfig } from "@medusajs/admin-sdk";
|
|
3
|
-
import { Container, Heading, Text, Button, toast, Label, Input, Textarea } from "@medusajs/ui";
|
|
3
|
+
import { Container, Heading, Text, Button, toast, Table, Badge, Label, Input, Textarea, Select } from "@medusajs/ui";
|
|
4
4
|
import Medusa from "@medusajs/js-sdk";
|
|
5
|
-
import { useState, useCallback, useEffect } from "react";
|
|
6
|
-
import {
|
|
5
|
+
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
|
|
6
|
+
import { DocumentText } from "@medusajs/icons";
|
|
7
|
+
import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query";
|
|
8
|
+
import { useNavigate, useParams } from "react-router-dom";
|
|
7
9
|
import { useForm, FormProvider, Controller } from "react-hook-form";
|
|
8
10
|
import { z } from "@medusajs/framework/zod";
|
|
11
|
+
import CodeMirror from "@uiw/react-codemirror";
|
|
12
|
+
import { html } from "@codemirror/lang-html";
|
|
13
|
+
import { oneDark } from "@codemirror/theme-one-dark";
|
|
14
|
+
import Handlebars from "handlebars";
|
|
9
15
|
import "@medusajs/admin-shared";
|
|
10
16
|
const sdk = new Medusa({
|
|
11
17
|
baseUrl: "/",
|
|
@@ -61,17 +67,94 @@ const OrderInvoiceWidget = ({ data: order }) => {
|
|
|
61
67
|
defineWidgetConfig({
|
|
62
68
|
zone: "order.details.side.before"
|
|
63
69
|
});
|
|
70
|
+
const TYPE_LABELS = {
|
|
71
|
+
order_invoice: "Comprobante de Pedido",
|
|
72
|
+
quote_proforma: "Cotización Proforma"
|
|
73
|
+
};
|
|
74
|
+
const InvoiceTemplatesPage = () => {
|
|
75
|
+
const navigate = useNavigate();
|
|
76
|
+
const queryClient = useQueryClient();
|
|
77
|
+
const { data, isLoading } = useQuery({
|
|
78
|
+
queryFn: () => sdk.client.fetch("/admin/invoice-templates"),
|
|
79
|
+
queryKey: ["invoice-templates"]
|
|
80
|
+
});
|
|
81
|
+
const deleteMutation = useMutation({
|
|
82
|
+
mutationFn: (id) => sdk.client.fetch(`/admin/invoice-templates/${id}`, { method: "DELETE" }),
|
|
83
|
+
onSuccess: () => {
|
|
84
|
+
queryClient.invalidateQueries({ queryKey: ["invoice-templates"] });
|
|
85
|
+
toast.success("Plantilla eliminada");
|
|
86
|
+
},
|
|
87
|
+
onError: () => {
|
|
88
|
+
toast.error("No se pudo eliminar la plantilla");
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
const templates = (data == null ? void 0 : data.invoice_templates) ?? [];
|
|
92
|
+
return /* @__PURE__ */ jsxs(Container, { children: [
|
|
93
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 24 }, children: [
|
|
94
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", children: "Plantillas de Documentos" }),
|
|
95
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
|
|
96
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => navigate("/invoice-config"), children: "Configuración" }),
|
|
97
|
+
/* @__PURE__ */ jsx(Button, { onClick: () => navigate("/invoice-templates/new"), children: "+ Nueva Plantilla" })
|
|
98
|
+
] })
|
|
99
|
+
] }),
|
|
100
|
+
isLoading ? /* @__PURE__ */ jsx("div", { style: { textAlign: "center", padding: 40 }, children: "Cargando..." }) : templates.length === 0 ? /* @__PURE__ */ jsx("div", { style: { textAlign: "center", padding: 40, color: "#6c757d" }, children: "No hay plantillas. Se crearán automáticamente al reiniciar el servidor." }) : /* @__PURE__ */ jsxs(Table, { children: [
|
|
101
|
+
/* @__PURE__ */ jsx(Table.Header, { children: /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
102
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Nombre" }),
|
|
103
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Slug" }),
|
|
104
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Tipo" }),
|
|
105
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Estado" }),
|
|
106
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Actualizado" }),
|
|
107
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { style: { textAlign: "right" }, children: "Acciones" })
|
|
108
|
+
] }) }),
|
|
109
|
+
/* @__PURE__ */ jsx(Table.Body, { children: templates.map((tpl) => /* @__PURE__ */ jsxs(
|
|
110
|
+
Table.Row,
|
|
111
|
+
{
|
|
112
|
+
onClick: () => navigate(`/invoice-templates/${tpl.id}`),
|
|
113
|
+
style: { cursor: "pointer" },
|
|
114
|
+
children: [
|
|
115
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: tpl.name }),
|
|
116
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx("code", { style: { fontSize: 12 }, children: tpl.slug }) }),
|
|
117
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: TYPE_LABELS[tpl.type] ?? tpl.type }),
|
|
118
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: tpl.is_default ? /* @__PURE__ */ jsx(Badge, { color: "blue", children: "Por defecto" }) : /* @__PURE__ */ jsx(Badge, { color: "grey", children: "Personalizada" }) }),
|
|
119
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: new Date(tpl.updated_at).toLocaleDateString("es-ES") }),
|
|
120
|
+
/* @__PURE__ */ jsx(Table.Cell, { style: { textAlign: "right" }, children: /* @__PURE__ */ jsx(
|
|
121
|
+
Button,
|
|
122
|
+
{
|
|
123
|
+
variant: "danger",
|
|
124
|
+
size: "small",
|
|
125
|
+
disabled: tpl.is_default,
|
|
126
|
+
onClick: (e) => {
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
if (confirm("¿Eliminar esta plantilla?")) {
|
|
129
|
+
deleteMutation.mutate(tpl.id);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
children: "Eliminar"
|
|
133
|
+
}
|
|
134
|
+
) })
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
tpl.id
|
|
138
|
+
)) })
|
|
139
|
+
] })
|
|
140
|
+
] });
|
|
141
|
+
};
|
|
142
|
+
const config$1 = defineRouteConfig({
|
|
143
|
+
label: "Plantillas PDF",
|
|
144
|
+
icon: DocumentText
|
|
145
|
+
});
|
|
64
146
|
z.object({
|
|
65
147
|
company_name: z.string().optional(),
|
|
66
148
|
company_ruc: z.string().optional(),
|
|
67
149
|
company_address: z.string().optional(),
|
|
68
150
|
company_phone: z.string().optional(),
|
|
69
|
-
company_email: z.string().email().optional(),
|
|
70
|
-
company_logo: z.string().
|
|
151
|
+
company_email: z.string().email().optional().or(z.literal("")),
|
|
152
|
+
company_logo: z.string().optional(),
|
|
71
153
|
notes: z.string().optional(),
|
|
72
154
|
admin_notification_email: z.string().email().optional().or(z.literal(""))
|
|
73
155
|
});
|
|
74
156
|
const InvoiceConfigPage = () => {
|
|
157
|
+
const navigate = useNavigate();
|
|
75
158
|
const { data, isLoading, refetch } = useQuery({
|
|
76
159
|
queryFn: () => sdk.client.fetch("/admin/invoice-config"),
|
|
77
160
|
queryKey: ["invoice-config"]
|
|
@@ -84,6 +167,9 @@ const InvoiceConfigPage = () => {
|
|
|
84
167
|
onSuccess: () => {
|
|
85
168
|
refetch();
|
|
86
169
|
toast.success("Configuración actualizada exitosamente");
|
|
170
|
+
},
|
|
171
|
+
onError: () => {
|
|
172
|
+
toast.error("Error al guardar la configuración");
|
|
87
173
|
}
|
|
88
174
|
});
|
|
89
175
|
const getFormDefaultValues = useCallback(() => {
|
|
@@ -108,16 +194,23 @@ const InvoiceConfigPage = () => {
|
|
|
108
194
|
if (!file) {
|
|
109
195
|
return;
|
|
110
196
|
}
|
|
111
|
-
|
|
112
|
-
files
|
|
113
|
-
|
|
114
|
-
|
|
197
|
+
try {
|
|
198
|
+
const { files } = await sdk.admin.upload.create({
|
|
199
|
+
files: [file]
|
|
200
|
+
});
|
|
201
|
+
form.setValue("company_logo", files[0].url);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
toast.error("Error al subir el logo. Verifica que el archivo sea una imagen válida.");
|
|
204
|
+
}
|
|
115
205
|
};
|
|
116
206
|
useEffect(() => {
|
|
117
207
|
form.reset(getFormDefaultValues());
|
|
118
208
|
}, [getFormDefaultValues]);
|
|
119
209
|
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
120
|
-
/* @__PURE__ */
|
|
210
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
211
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", children: "Configuración de Comprobante" }),
|
|
212
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => navigate("/invoice-templates"), children: "Gestionar Plantillas" })
|
|
213
|
+
] }),
|
|
121
214
|
/* @__PURE__ */ jsx(FormProvider, { ...form, children: /* @__PURE__ */ jsxs(
|
|
122
215
|
"form",
|
|
123
216
|
{
|
|
@@ -246,6 +339,396 @@ const InvoiceConfigPage = () => {
|
|
|
246
339
|
const config = defineRouteConfig({
|
|
247
340
|
label: "Comprobante de Pedido"
|
|
248
341
|
});
|
|
342
|
+
const NewTemplatePage = () => {
|
|
343
|
+
const navigate = useNavigate();
|
|
344
|
+
const [name, setName] = useState("");
|
|
345
|
+
const [slug, setSlug] = useState("");
|
|
346
|
+
const [type, setType] = useState("order_invoice");
|
|
347
|
+
const createMutation = useMutation({
|
|
348
|
+
mutationFn: (payload) => sdk.client.fetch("/admin/invoice-templates", {
|
|
349
|
+
method: "POST",
|
|
350
|
+
body: payload
|
|
351
|
+
}),
|
|
352
|
+
onSuccess: (data) => {
|
|
353
|
+
toast.success("Plantilla creada");
|
|
354
|
+
navigate(`/invoice-templates/${data.invoice_template.id}`);
|
|
355
|
+
},
|
|
356
|
+
onError: () => {
|
|
357
|
+
toast.error("Error al crear la plantilla");
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
const handleCreate = () => {
|
|
361
|
+
if (!name || !slug) {
|
|
362
|
+
toast.error("Nombre y slug son requeridos");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
createMutation.mutate({
|
|
366
|
+
name,
|
|
367
|
+
slug,
|
|
368
|
+
type,
|
|
369
|
+
html_content: getDefaultHtml(type)
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
return /* @__PURE__ */ jsxs(Container, { children: [
|
|
373
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", style: { marginBottom: 24 }, children: "Nueva Plantilla" }),
|
|
374
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 16, maxWidth: 500 }, children: [
|
|
375
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
376
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "name", children: "Nombre" }),
|
|
377
|
+
/* @__PURE__ */ jsx(Input, { id: "name", value: name, onChange: (e) => setName(e.target.value), placeholder: "Ej: Mi Comprobante" })
|
|
378
|
+
] }),
|
|
379
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
380
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "slug", children: "Slug (identificador único)" }),
|
|
381
|
+
/* @__PURE__ */ jsx(
|
|
382
|
+
Input,
|
|
383
|
+
{
|
|
384
|
+
id: "slug",
|
|
385
|
+
value: slug,
|
|
386
|
+
onChange: (e) => setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, "_")),
|
|
387
|
+
placeholder: "Ej: my_custom_invoice"
|
|
388
|
+
}
|
|
389
|
+
)
|
|
390
|
+
] }),
|
|
391
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
392
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "type", children: "Tipo de documento" }),
|
|
393
|
+
/* @__PURE__ */ jsxs(Select, { value: type, onValueChange: setType, children: [
|
|
394
|
+
/* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, { placeholder: "Seleccionar tipo" }) }),
|
|
395
|
+
/* @__PURE__ */ jsxs(Select.Content, { children: [
|
|
396
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "order_invoice", children: "Comprobante de Pedido" }),
|
|
397
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "quote_proforma", children: "Cotización Proforma" })
|
|
398
|
+
] })
|
|
399
|
+
] })
|
|
400
|
+
] }),
|
|
401
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, marginTop: 16 }, children: [
|
|
402
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => navigate("/invoice-templates"), children: "Cancelar" }),
|
|
403
|
+
/* @__PURE__ */ jsx(Button, { onClick: handleCreate, isLoading: createMutation.isPending, children: "Crear Plantilla" })
|
|
404
|
+
] })
|
|
405
|
+
] })
|
|
406
|
+
] });
|
|
407
|
+
};
|
|
408
|
+
function getDefaultHtml(type) {
|
|
409
|
+
if (type === "quote_proforma") {
|
|
410
|
+
return `<!DOCTYPE html>
|
|
411
|
+
<html>
|
|
412
|
+
<head><style>body { font-family: Helvetica, sans-serif; }</style></head>
|
|
413
|
+
<body>
|
|
414
|
+
<h1>COTIZACIÓN {{quote_number}}</h1>
|
|
415
|
+
<p>Fecha: {{date_str}}</p>
|
|
416
|
+
<p>Servicio: {{service_name}}</p>
|
|
417
|
+
<!-- Personaliza tu plantilla aquí -->
|
|
418
|
+
</body>
|
|
419
|
+
</html>`;
|
|
420
|
+
}
|
|
421
|
+
return `<!DOCTYPE html>
|
|
422
|
+
<html>
|
|
423
|
+
<head><style>body { font-family: Helvetica, sans-serif; }</style></head>
|
|
424
|
+
<body>
|
|
425
|
+
<h1>COMPROBANTE {{invoice_id}}</h1>
|
|
426
|
+
<p>Fecha: {{invoice_date}}</p>
|
|
427
|
+
<p>Empresa: {{company_name}}</p>
|
|
428
|
+
<!-- Personaliza tu plantilla aquí -->
|
|
429
|
+
</body>
|
|
430
|
+
</html>`;
|
|
431
|
+
}
|
|
432
|
+
const VARIABLES = {
|
|
433
|
+
order_invoice: [
|
|
434
|
+
"company_name",
|
|
435
|
+
"company_ruc",
|
|
436
|
+
"company_address",
|
|
437
|
+
"company_phone",
|
|
438
|
+
"company_email",
|
|
439
|
+
"company_logo_base64",
|
|
440
|
+
"invoice_id",
|
|
441
|
+
"invoice_date",
|
|
442
|
+
"order_display_id",
|
|
443
|
+
"order_date",
|
|
444
|
+
"billing_address",
|
|
445
|
+
"shipping_address",
|
|
446
|
+
"cedula",
|
|
447
|
+
"subtotal",
|
|
448
|
+
"tax_total",
|
|
449
|
+
"shipping_total",
|
|
450
|
+
"discount_total",
|
|
451
|
+
"total",
|
|
452
|
+
"is_home_delivery",
|
|
453
|
+
"notes",
|
|
454
|
+
"{{#each items}}",
|
|
455
|
+
"this.title",
|
|
456
|
+
"this.variant_title",
|
|
457
|
+
"this.quantity",
|
|
458
|
+
"this.unit_price",
|
|
459
|
+
"this.total",
|
|
460
|
+
"{{/each}}"
|
|
461
|
+
],
|
|
462
|
+
quote_proforma: [
|
|
463
|
+
"company_name",
|
|
464
|
+
"company_website",
|
|
465
|
+
"quote_number",
|
|
466
|
+
"date_str",
|
|
467
|
+
"service_name",
|
|
468
|
+
"is_home_delivery",
|
|
469
|
+
"{{#each config_fields}}",
|
|
470
|
+
"this.label",
|
|
471
|
+
"this.value",
|
|
472
|
+
"{{/each}}",
|
|
473
|
+
"{{#each breakdown}}",
|
|
474
|
+
"this.label",
|
|
475
|
+
"this.total_formatted",
|
|
476
|
+
"{{/each}}",
|
|
477
|
+
"totals.subtotal_formatted",
|
|
478
|
+
"totals.extras_formatted",
|
|
479
|
+
"totals.discount_formatted",
|
|
480
|
+
"totals.shipping_formatted",
|
|
481
|
+
"totals.taxes_provided",
|
|
482
|
+
"totals.taxes_formatted",
|
|
483
|
+
"totals.total_formatted",
|
|
484
|
+
"{{#each includes_left}}",
|
|
485
|
+
"{{#each includes_right}}",
|
|
486
|
+
"{{#each contact_rows}}",
|
|
487
|
+
"this.label",
|
|
488
|
+
"this.value",
|
|
489
|
+
"{{/each}}"
|
|
490
|
+
]
|
|
491
|
+
};
|
|
492
|
+
const SAMPLE_DATA = {
|
|
493
|
+
order_invoice: {
|
|
494
|
+
company_name: "Mi Empresa",
|
|
495
|
+
company_ruc: "1234567890001",
|
|
496
|
+
company_address: "Av. Principal 123, Quito",
|
|
497
|
+
company_phone: "+593 99 123 4567",
|
|
498
|
+
company_email: "info@miempresa.com",
|
|
499
|
+
company_logo_base64: "",
|
|
500
|
+
invoice_id: "INV-000001",
|
|
501
|
+
invoice_date: "19/06/2025",
|
|
502
|
+
order_display_id: "000042",
|
|
503
|
+
order_date: "18/06/2025",
|
|
504
|
+
billing_address: "Juan Pérez\nCalle Ejemplo 456\nQuito, Pichincha",
|
|
505
|
+
shipping_address: "Juan Pérez\nCalle Ejemplo 456\nQuito, Pichincha",
|
|
506
|
+
cedula: "1712345678",
|
|
507
|
+
items: [
|
|
508
|
+
{ title: "Producto ejemplo", variant_title: "Grande", quantity: "2", unit_price: "$25.00", total: "$50.00" },
|
|
509
|
+
{ title: "Otro producto", variant_title: "", quantity: "1", unit_price: "$15.00", total: "$15.00" }
|
|
510
|
+
],
|
|
511
|
+
subtotal: "$65.00",
|
|
512
|
+
tax_total: "$7.80",
|
|
513
|
+
shipping_total: "$5.00",
|
|
514
|
+
discount_total: "",
|
|
515
|
+
total: "$77.80",
|
|
516
|
+
is_home_delivery: false,
|
|
517
|
+
notes: "Gracias por su compra."
|
|
518
|
+
},
|
|
519
|
+
quote_proforma: {
|
|
520
|
+
company_name: "Mi Empresa",
|
|
521
|
+
company_website: "www.miempresa.com",
|
|
522
|
+
quote_number: "QP-2025-0001",
|
|
523
|
+
date_str: "19/06/2025",
|
|
524
|
+
service_name: "SERVICIO DE EJEMPLO",
|
|
525
|
+
config_fields: [
|
|
526
|
+
{ label: "Nro. personas", value: "50" },
|
|
527
|
+
{ label: "Menú", value: "Premium" }
|
|
528
|
+
],
|
|
529
|
+
breakdown: [
|
|
530
|
+
{ label: "Servicio base", total_formatted: "$500.00" },
|
|
531
|
+
{ label: "Extras", total_formatted: "$100.00" }
|
|
532
|
+
],
|
|
533
|
+
totals: {
|
|
534
|
+
subtotal_formatted: "$500.00",
|
|
535
|
+
extras_formatted: "$100.00",
|
|
536
|
+
discount: 0,
|
|
537
|
+
discount_formatted: "",
|
|
538
|
+
shipping: 25,
|
|
539
|
+
shipping_formatted: "$25.00",
|
|
540
|
+
taxes_provided: true,
|
|
541
|
+
taxes_formatted: "$75.00",
|
|
542
|
+
total_formatted: "700.00"
|
|
543
|
+
},
|
|
544
|
+
is_home_delivery: false,
|
|
545
|
+
includes: ["Vajilla", "Mantelería", "Personal", "Montaje"],
|
|
546
|
+
includes_left: ["Vajilla", "Personal"],
|
|
547
|
+
includes_right: ["Mantelería", "Montaje"],
|
|
548
|
+
contact_rows: [
|
|
549
|
+
{ label: "Nombre", value: "María García" },
|
|
550
|
+
{ label: "Email", value: "maria@ejemplo.com" }
|
|
551
|
+
]
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
const TemplateEditorPage = () => {
|
|
555
|
+
const { id } = useParams();
|
|
556
|
+
const navigate = useNavigate();
|
|
557
|
+
const queryClient = useQueryClient();
|
|
558
|
+
const editorRef = useRef(null);
|
|
559
|
+
const { data, isLoading } = useQuery({
|
|
560
|
+
queryFn: () => sdk.client.fetch(`/admin/invoice-templates/${id}`),
|
|
561
|
+
queryKey: ["invoice-template", id],
|
|
562
|
+
enabled: !!id
|
|
563
|
+
});
|
|
564
|
+
const template = data == null ? void 0 : data.invoice_template;
|
|
565
|
+
const [name, setName] = useState("");
|
|
566
|
+
const [htmlContent, setHtmlContent] = useState("");
|
|
567
|
+
const [showVariables, setShowVariables] = useState(false);
|
|
568
|
+
useEffect(() => {
|
|
569
|
+
if (template) {
|
|
570
|
+
setName(template.name);
|
|
571
|
+
setHtmlContent(template.html_content);
|
|
572
|
+
}
|
|
573
|
+
}, [template]);
|
|
574
|
+
const saveMutation = useMutation({
|
|
575
|
+
mutationFn: (payload) => sdk.client.fetch(`/admin/invoice-templates/${id}`, {
|
|
576
|
+
method: "POST",
|
|
577
|
+
body: payload
|
|
578
|
+
}),
|
|
579
|
+
onSuccess: () => {
|
|
580
|
+
queryClient.invalidateQueries({ queryKey: ["invoice-template", id] });
|
|
581
|
+
toast.success("Plantilla guardada");
|
|
582
|
+
},
|
|
583
|
+
onError: () => {
|
|
584
|
+
toast.error("Error al guardar");
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
const handleSave = () => {
|
|
588
|
+
saveMutation.mutate({ name, html_content: htmlContent });
|
|
589
|
+
};
|
|
590
|
+
const handlePreviewPdf = async () => {
|
|
591
|
+
try {
|
|
592
|
+
const response = await fetch(`/admin/invoice-templates/${id}/preview`, {
|
|
593
|
+
method: "POST",
|
|
594
|
+
headers: { "Content-Type": "application/json" },
|
|
595
|
+
credentials: "include"
|
|
596
|
+
});
|
|
597
|
+
if (!response.ok) throw new Error("Preview failed");
|
|
598
|
+
const blob = await response.blob();
|
|
599
|
+
const url = URL.createObjectURL(blob);
|
|
600
|
+
window.open(url, "_blank");
|
|
601
|
+
} catch {
|
|
602
|
+
toast.error("Error al generar preview PDF");
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
const handleRestoreDefault = async () => {
|
|
606
|
+
if (!(template == null ? void 0 : template.is_default)) return;
|
|
607
|
+
if (!confirm("¿Restaurar la plantilla a su contenido original?")) return;
|
|
608
|
+
try {
|
|
609
|
+
const res = await sdk.client.fetch(
|
|
610
|
+
`/admin/invoice-templates/${id}/restore`,
|
|
611
|
+
{ method: "POST" }
|
|
612
|
+
);
|
|
613
|
+
toast.success("Plantilla restaurada");
|
|
614
|
+
queryClient.invalidateQueries({ queryKey: ["invoice-template", id] });
|
|
615
|
+
} catch {
|
|
616
|
+
toast.error("Error al restaurar la plantilla");
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
const previewHtml = useMemo(() => {
|
|
620
|
+
if (!htmlContent || !template) return "";
|
|
621
|
+
try {
|
|
622
|
+
const compiled = Handlebars.compile(htmlContent);
|
|
623
|
+
return compiled(SAMPLE_DATA[template.type] ?? {});
|
|
624
|
+
} catch {
|
|
625
|
+
return `<div style="color:red;padding:20px;">Error en la plantilla Handlebars</div>`;
|
|
626
|
+
}
|
|
627
|
+
}, [htmlContent, template]);
|
|
628
|
+
const insertVariable = useCallback((variable) => {
|
|
629
|
+
const isBlock = variable.startsWith("{{#") || variable.startsWith("{{/");
|
|
630
|
+
const insertion = isBlock ? variable : `{{${variable}}}`;
|
|
631
|
+
setHtmlContent((prev) => prev + insertion);
|
|
632
|
+
}, []);
|
|
633
|
+
const availableVars = template ? VARIABLES[template.type] ?? [] : [];
|
|
634
|
+
if (isLoading) {
|
|
635
|
+
return /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsx("div", { style: { textAlign: "center", padding: 40 }, children: "Cargando..." }) });
|
|
636
|
+
}
|
|
637
|
+
if (!template) {
|
|
638
|
+
return /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsx("div", { style: { textAlign: "center", padding: 40 }, children: "Plantilla no encontrada" }) });
|
|
639
|
+
}
|
|
640
|
+
return /* @__PURE__ */ jsxs(Container, { children: [
|
|
641
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }, children: [
|
|
642
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [
|
|
643
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: () => navigate("/invoice-templates"), children: "← Volver" }),
|
|
644
|
+
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
|
|
645
|
+
Input,
|
|
646
|
+
{
|
|
647
|
+
value: name,
|
|
648
|
+
onChange: (e) => setName(e.target.value),
|
|
649
|
+
style: { fontWeight: "bold", fontSize: 16 }
|
|
650
|
+
}
|
|
651
|
+
) }),
|
|
652
|
+
/* @__PURE__ */ jsxs("code", { style: { fontSize: 12, color: "#6c757d" }, children: [
|
|
653
|
+
template.slug,
|
|
654
|
+
" (",
|
|
655
|
+
template.type,
|
|
656
|
+
")"
|
|
657
|
+
] })
|
|
658
|
+
] }),
|
|
659
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
|
|
660
|
+
template.is_default && /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: handleRestoreDefault, children: "Restaurar Default" }),
|
|
661
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: () => setShowVariables(!showVariables), children: showVariables ? "Ocultar Variables" : "Variables" }),
|
|
662
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: handlePreviewPdf, children: "Preview PDF" }),
|
|
663
|
+
/* @__PURE__ */ jsx(Button, { onClick: handleSave, isLoading: saveMutation.isPending, children: "Guardar" })
|
|
664
|
+
] })
|
|
665
|
+
] }),
|
|
666
|
+
showVariables && /* @__PURE__ */ jsxs("div", { style: {
|
|
667
|
+
background: "#f8f9fa",
|
|
668
|
+
border: "1px solid #e2e8f0",
|
|
669
|
+
borderRadius: 8,
|
|
670
|
+
padding: 12,
|
|
671
|
+
marginBottom: 16,
|
|
672
|
+
display: "flex",
|
|
673
|
+
flexWrap: "wrap",
|
|
674
|
+
gap: 6
|
|
675
|
+
}, children: [
|
|
676
|
+
/* @__PURE__ */ jsx(Label, { style: { width: "100%", marginBottom: 4 }, children: "Variables Handlebars — click para insertar:" }),
|
|
677
|
+
availableVars.map((v, i) => /* @__PURE__ */ jsx(
|
|
678
|
+
"button",
|
|
679
|
+
{
|
|
680
|
+
onClick: () => insertVariable(v),
|
|
681
|
+
style: {
|
|
682
|
+
background: v.startsWith("{{") ? "#e2e8f0" : "#dbeafe",
|
|
683
|
+
border: "1px solid #cbd5e0",
|
|
684
|
+
borderRadius: 4,
|
|
685
|
+
padding: "2px 8px",
|
|
686
|
+
fontSize: 11,
|
|
687
|
+
cursor: "pointer",
|
|
688
|
+
fontFamily: "monospace"
|
|
689
|
+
},
|
|
690
|
+
children: v.startsWith("{{") ? v : `{{${v}}}`
|
|
691
|
+
},
|
|
692
|
+
i
|
|
693
|
+
))
|
|
694
|
+
] }),
|
|
695
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 16, height: "calc(100vh - 260px)" }, children: [
|
|
696
|
+
/* @__PURE__ */ jsxs("div", { style: { flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }, children: [
|
|
697
|
+
/* @__PURE__ */ jsx(Label, { style: { marginBottom: 8 }, children: "Editor HTML + Handlebars" }),
|
|
698
|
+
/* @__PURE__ */ jsx("div", { style: { flex: 1, border: "1px solid #e2e8f0", borderRadius: 8, overflow: "hidden" }, children: /* @__PURE__ */ jsx(
|
|
699
|
+
CodeMirror,
|
|
700
|
+
{
|
|
701
|
+
ref: editorRef,
|
|
702
|
+
value: htmlContent,
|
|
703
|
+
onChange: (value) => setHtmlContent(value),
|
|
704
|
+
extensions: [html()],
|
|
705
|
+
theme: oneDark,
|
|
706
|
+
height: "100%",
|
|
707
|
+
style: { height: "100%" }
|
|
708
|
+
}
|
|
709
|
+
) })
|
|
710
|
+
] }),
|
|
711
|
+
/* @__PURE__ */ jsxs("div", { style: { flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }, children: [
|
|
712
|
+
/* @__PURE__ */ jsx(Label, { style: { marginBottom: 8 }, children: "Preview en vivo (datos de ejemplo)" }),
|
|
713
|
+
/* @__PURE__ */ jsx("div", { style: {
|
|
714
|
+
flex: 1,
|
|
715
|
+
border: "1px solid #e2e8f0",
|
|
716
|
+
borderRadius: 8,
|
|
717
|
+
overflow: "auto",
|
|
718
|
+
background: "#fff"
|
|
719
|
+
}, children: /* @__PURE__ */ jsx(
|
|
720
|
+
"iframe",
|
|
721
|
+
{
|
|
722
|
+
srcDoc: previewHtml,
|
|
723
|
+
style: { width: "100%", height: "100%", border: "none" },
|
|
724
|
+
sandbox: "allow-same-origin",
|
|
725
|
+
title: "Template Preview"
|
|
726
|
+
}
|
|
727
|
+
) })
|
|
728
|
+
] })
|
|
729
|
+
] })
|
|
730
|
+
] });
|
|
731
|
+
};
|
|
249
732
|
const i18nTranslations0 = {};
|
|
250
733
|
const widgetModule = { widgets: [
|
|
251
734
|
{
|
|
@@ -255,9 +738,21 @@ const widgetModule = { widgets: [
|
|
|
255
738
|
] };
|
|
256
739
|
const routeModule = {
|
|
257
740
|
routes: [
|
|
741
|
+
{
|
|
742
|
+
Component: InvoiceTemplatesPage,
|
|
743
|
+
path: "/invoice-templates"
|
|
744
|
+
},
|
|
258
745
|
{
|
|
259
746
|
Component: InvoiceConfigPage,
|
|
260
747
|
path: "/invoice-config"
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
Component: NewTemplatePage,
|
|
751
|
+
path: "/invoice-templates/new"
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
Component: TemplateEditorPage,
|
|
755
|
+
path: "/invoice-templates/:id"
|
|
261
756
|
}
|
|
262
757
|
]
|
|
263
758
|
};
|
|
@@ -270,6 +765,14 @@ const menuItemModule = {
|
|
|
270
765
|
nested: void 0,
|
|
271
766
|
rank: void 0,
|
|
272
767
|
translationNs: void 0
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
label: config$1.label,
|
|
771
|
+
icon: config$1.icon,
|
|
772
|
+
path: "/invoice-templates",
|
|
773
|
+
nested: void 0,
|
|
774
|
+
rank: void 0,
|
|
775
|
+
translationNs: void 0
|
|
273
776
|
}
|
|
274
777
|
]
|
|
275
778
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GET = GET;
|
|
4
|
+
exports.POST = POST;
|
|
5
|
+
const invoice_generator_1 = require("../../../modules/invoice-generator");
|
|
6
|
+
async function GET(req, res) {
|
|
7
|
+
const query = req.scope.resolve("query");
|
|
8
|
+
const { data: [invoiceConfig] } = await query.graph({
|
|
9
|
+
entity: "invoice_config",
|
|
10
|
+
fields: ["*"],
|
|
11
|
+
});
|
|
12
|
+
res.json({
|
|
13
|
+
invoice_config: invoiceConfig
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
async function POST(req, res) {
|
|
17
|
+
const body = req.validatedBody ?? req.body;
|
|
18
|
+
const service = req.scope.resolve(invoice_generator_1.INVOICE_MODULE);
|
|
19
|
+
const configs = await service.listInvoiceConfigs();
|
|
20
|
+
const existing = configs[0];
|
|
21
|
+
const updated = await service.updateInvoiceConfigs({ id: existing.id, ...body });
|
|
22
|
+
res.json({
|
|
23
|
+
invoice_config: updated
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL2ludm9pY2UtY29uZmlnL3JvdXRlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBSUEsa0JBY0M7QUFFRCxvQkFlQztBQWxDRCwwRUFBbUU7QUFHNUQsS0FBSyxVQUFVLEdBQUcsQ0FDdkIsR0FBa0IsRUFDbEIsR0FBbUI7SUFFbkIsTUFBTSxLQUFLLEdBQUcsR0FBRyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLENBQUE7SUFFeEMsTUFBTSxFQUFFLElBQUksRUFBRSxDQUFDLGFBQWEsQ0FBQyxFQUFFLEdBQUcsTUFBTSxLQUFLLENBQUMsS0FBSyxDQUFDO1FBQ2xELE1BQU0sRUFBRSxnQkFBZ0I7UUFDeEIsTUFBTSxFQUFFLENBQUMsR0FBRyxDQUFDO0tBQ2QsQ0FBQyxDQUFBO0lBRUYsR0FBRyxDQUFDLElBQUksQ0FBQztRQUNQLGNBQWMsRUFBRSxhQUFhO0tBQzlCLENBQUMsQ0FBQTtBQUNKLENBQUM7QUFFTSxLQUFLLFVBQVUsSUFBSSxDQUN4QixHQUFrQixFQUNsQixHQUFtQjtJQUVuQixNQUFNLElBQUksR0FBRyxHQUFHLENBQUMsYUFBYSxJQUFJLEdBQUcsQ0FBQyxJQUFJLENBQUE7SUFDMUMsTUFBTSxPQUFPLEdBQUcsR0FBRyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsa0NBQWMsQ0FBNEIsQ0FBQTtJQUU1RSxNQUFNLE9BQU8sR0FBRyxNQUFNLE9BQU8sQ0FBQyxrQkFBa0IsRUFBRSxDQUFBO0lBQ2xELE1BQU0sUUFBUSxHQUFHLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQTtJQUUzQixNQUFNLE9BQU8sR0FBRyxNQUFNLE9BQU8sQ0FBQyxvQkFBb0IsQ0FBQyxFQUFFLEVBQUUsRUFBRSxRQUFRLENBQUMsRUFBRSxFQUFFLEdBQUksSUFBZ0MsRUFBRSxDQUFDLENBQUE7SUFFN0csR0FBRyxDQUFDLElBQUksQ0FBQztRQUNQLGNBQWMsRUFBRSxPQUFPO0tBQ3hCLENBQUMsQ0FBQTtBQUNKLENBQUMifQ==
|