@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
|
@@ -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 función de restaurar aún no está implementada.");
|
|
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
|
};
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CodeMindEC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|