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