Facturas de cliente
This commit is contained in:
parent
fe1faaab4d
commit
220cc4c3a4
@ -1,7 +1,8 @@
|
||||
import cors, { CorsOptions } from "cors";
|
||||
import express, { Application } from "express";
|
||||
import cors, { type CorsOptions } from "cors";
|
||||
import express, { type Application } from "express";
|
||||
import helmet from "helmet";
|
||||
import responseTime from "response-time";
|
||||
|
||||
// ❗️ No cargamos dotenv aquí. Debe hacerse en el entrypoint o en ./config.
|
||||
// dotenv.config();
|
||||
import { ENV } from "./config";
|
||||
@ -9,22 +10,6 @@ import { logger } from "./lib/logger";
|
||||
|
||||
export function createApp(): Application {
|
||||
const app = express();
|
||||
app.set("port", process.env.SERVER_PORT ?? 3002);
|
||||
|
||||
// Oculta la cabecera x-powered-by
|
||||
app.disable("x-powered-by");
|
||||
|
||||
// Desactiva ETag correctamente a nivel de Express
|
||||
app.set("etag", false);
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Parsers
|
||||
app.use(express.json());
|
||||
app.use(express.text());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Métrica de tiempo de respuesta
|
||||
app.use(responseTime());
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// CORS
|
||||
@ -60,6 +45,24 @@ export function createApp(): Application {
|
||||
};
|
||||
|
||||
app.use(cors(ENV.NODE_ENV === "development" ? devCors : prodCors));
|
||||
app.options("*", cors(ENV.NODE_ENV === "development" ? devCors : prodCors));
|
||||
|
||||
app.set("port", process.env.SERVER_PORT ?? 3002);
|
||||
|
||||
// Oculta la cabecera x-powered-by
|
||||
app.disable("x-powered-by");
|
||||
|
||||
// Desactiva ETag correctamente a nivel de Express
|
||||
app.set("etag", false);
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Parsers
|
||||
app.use(express.json());
|
||||
app.use(express.text());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Métrica de tiempo de respuesta
|
||||
app.use(responseTime());
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Seguridad HTTP
|
||||
|
||||
@ -248,7 +248,7 @@ process.on("uncaughtException", async (error: Error) => {
|
||||
logger.info("✅ Server is READY (readiness=true)");
|
||||
logger.info(`startup_duration_ms=${DateTime.now().diff(currentState.launchedAt).toMillis()}`);
|
||||
|
||||
server.listen(currentState.port, () => {
|
||||
server.listen(currentState.port, "0.0.0.0", () => {
|
||||
server.emit("listening");
|
||||
|
||||
const networkInterfaces = os.networkInterfaces();
|
||||
|
||||
@ -52,15 +52,46 @@
|
||||
},
|
||||
"issued_invoices": {
|
||||
"status": {
|
||||
"all": "Todos",
|
||||
"pendiente": "Pendiente",
|
||||
"aceptado_con_error": "Aceptado con error",
|
||||
"incorrecto": "Incorrecto",
|
||||
"duplicado": "Duplicado",
|
||||
"anulado": "Anulado",
|
||||
"factura_inexistente": "Factura inexistente",
|
||||
"rechazado": "Rechazado",
|
||||
"error": "Error"
|
||||
"all": {
|
||||
"label": "All",
|
||||
"description": "Proforma in preparation"
|
||||
},
|
||||
"pendiente": {
|
||||
"label": "Pending",
|
||||
"description": "Queued record not yet processed"
|
||||
},
|
||||
"correcto": {
|
||||
"label": "Correct",
|
||||
"description": "Record successfully processed by the AEAT"
|
||||
},
|
||||
"aceptado_con_error": {
|
||||
"label": "Accepted with errors",
|
||||
"description": "Record accepted with errors by the AEAT. A correction record or a rectifying invoice is required"
|
||||
},
|
||||
"incorrecto": {
|
||||
"label": "Incorrect",
|
||||
"description": "Record considered incorrect by the AEAT. A correction record with rechazo_previo=S or rechazo_previo=X, or a rectifying invoice, is required"
|
||||
},
|
||||
"duplicado": {
|
||||
"label": "Duplicate",
|
||||
"description": "Record not accepted by the AEAT because another record with the same (series, number, issue_date) already exists"
|
||||
},
|
||||
"anulado": {
|
||||
"label": "Cancelled",
|
||||
"description": "Cancellation record successfully processed by the AEAT"
|
||||
},
|
||||
"factura_inexistente": {
|
||||
"label": "Non-existent invoice",
|
||||
"description": "Cancellation record not accepted by the AEAT because the invoice does not exist"
|
||||
},
|
||||
"rechazado": {
|
||||
"label": "Not registered",
|
||||
"description": "Record rejected by the AEAT"
|
||||
},
|
||||
"error": {
|
||||
"label": "Error",
|
||||
"description": "AEAT server error. The invoice record will be retried"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -52,15 +52,46 @@
|
||||
},
|
||||
"issued_invoices": {
|
||||
"status": {
|
||||
"all": "All",
|
||||
"pendiente": "Pendiente",
|
||||
"aceptado_con_error": "Aceptado con error",
|
||||
"incorrecto": "Incorrecto",
|
||||
"duplicado": "Duplicado",
|
||||
"anulado": "Anulado",
|
||||
"factura_inexistente": "Factura inexistente",
|
||||
"rechazado": "Rechazado",
|
||||
"error": "Error"
|
||||
"all": {
|
||||
"label": "All",
|
||||
"description": "Proforma en preparación"
|
||||
},
|
||||
"pendiente": {
|
||||
"label": "Pendiente",
|
||||
"description": "Registro encolado y no procesado aún"
|
||||
},
|
||||
"correcto": {
|
||||
"label": "Correcto",
|
||||
"description": "Registro procesado correctamente por la AEAT"
|
||||
},
|
||||
"aceptado_con_error": {
|
||||
"label": "Aceptado con errores",
|
||||
"description": "Registro aceptado con errores por la AEAT. Se requiere enviar un registro de subsanación o emitir una rectificativa"
|
||||
},
|
||||
"incorrecto": {
|
||||
"label": "Incorrecto",
|
||||
"description": "Registro considerado incorrecto por la AEAT. Se requiere enviar un registro de subsanación con rechazo_previo=S o rechazo_previo=X o emitir una rectificativa"
|
||||
},
|
||||
"duplicado": {
|
||||
"label": "Duplicado",
|
||||
"description": "Registro no aceptado por la AEAT por existir un registro con el mismo (serie, numero, fecha_expedicion)"
|
||||
},
|
||||
"anulado": {
|
||||
"label": "Anulado",
|
||||
"description": "Registro de anulación procesado correctamente por la AEAT"
|
||||
},
|
||||
"factura_inexistente": {
|
||||
"label": "Factura inexistente",
|
||||
"description": "Registro de anulación no aceptado por la AEAT por no existir la factura"
|
||||
},
|
||||
"rechazado": {
|
||||
"label": "No registrado",
|
||||
"description": "Registro rechazado por la AEAT"
|
||||
},
|
||||
"error": {
|
||||
"label": "Error",
|
||||
"description": "Error en el servidor de la AEAT. Se intentará reenviar el registro de facturación de nuevo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -33,6 +33,7 @@ export const useIssuedInvoiceListQuery = (options?: IssuedInvoicesQueryOptions)
|
||||
queryKey: ISSUED_INVOICES_QUERY_KEY(criteria),
|
||||
queryFn: async ({ signal }) => getIssuedInvoiceListApi(dataSource, signal, criteria),
|
||||
enabled,
|
||||
staleTime: 5000,
|
||||
placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
|
||||
});
|
||||
};
|
||||
|
||||
@ -19,6 +19,7 @@ import QrCode from "react-qr-code";
|
||||
|
||||
import { useTranslation } from "../../../../../i18n";
|
||||
import type { IssuedInvoiceSummaryData } from "../../../../types";
|
||||
import { VerifactuStatusBadge } from "../../components";
|
||||
|
||||
type GridActionHandlers = {
|
||||
onDownloadPdf?: (issuedInvoice: IssuedInvoiceSummaryData) => void;
|
||||
@ -68,6 +69,15 @@ export function useIssuedInvoicesGridColumns(
|
||||
/>
|
||||
),
|
||||
accessorFn: (row) => row.verifactu.status, // para ordenar/buscar por nombre
|
||||
cell: ({ row }) => {
|
||||
const invoice = row.original;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<VerifactuStatusBadge status={invoice.verifactu.status} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
enableHiding: false,
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./verifactu-status-badge";
|
||||
@ -0,0 +1,46 @@
|
||||
import { Badge, Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import {
|
||||
type VerifactuRecordStatus,
|
||||
getVerifactuRecordStatusButtonVariant,
|
||||
getVerifactuRecordStatusColor,
|
||||
getVerifactuRecordStatusIcon,
|
||||
} from "../../../types";
|
||||
|
||||
export type VerifactuStatusBadgeProps = {
|
||||
status: string | VerifactuRecordStatus; // permitir cualquier valor
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const VerifactuStatusBadge = ({ status, className }: VerifactuStatusBadgeProps) => {
|
||||
const { t } = useTranslation();
|
||||
const normalizedStatus = status as VerifactuRecordStatus;
|
||||
const Icon = getVerifactuRecordStatusIcon(normalizedStatus);
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
className={cn(
|
||||
getVerifactuRecordStatusColor(normalizedStatus),
|
||||
"font-semibold",
|
||||
className
|
||||
)}
|
||||
variant={getVerifactuRecordStatusButtonVariant(normalizedStatus)}
|
||||
>
|
||||
<Icon />
|
||||
{t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.label`, {
|
||||
defaultValue: status,
|
||||
})}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t(`catalog.issued_invoices.status.${normalizedStatus.toLowerCase()}.description`)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
VerifactuStatusBadge.displayName = "VerifactuStatusBadge";
|
||||
@ -96,19 +96,30 @@ export const IssuedInvoiceListPage = () => {
|
||||
<SelectValue placeholder={t("filters.status")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t("catalog.issuedInvoices.status.all.label")}</SelectItem>
|
||||
<SelectItem value="draft">
|
||||
{t("catalog.issuedInvoices.status.draft.label")}
|
||||
<SelectItem value="all">{t("catalog.issued_invoices.status.all.label")}</SelectItem>
|
||||
<SelectItem value="Correcto">
|
||||
{t("catalog.issued_invoices.status.correcto.label")}
|
||||
</SelectItem>
|
||||
<SelectItem value="sent">{t("catalog.issuedInvoices.status.sent.label")}</SelectItem>
|
||||
<SelectItem value="approved">
|
||||
{t("catalog.issuedInvoices.status.approved.label")}
|
||||
<SelectItem value="Pendiente">
|
||||
{t("catalog.issued_invoices.status.pendiente.label")}
|
||||
</SelectItem>
|
||||
<SelectItem value="rejected">
|
||||
{t("catalog.issuedInvoices.status.rejected.label")}
|
||||
<SelectItem value="aceptado_con_error">
|
||||
{t("catalog.issued_invoices.status.aceptado_con_error.label")}
|
||||
</SelectItem>
|
||||
<SelectItem value="issued">
|
||||
{t("catalog.issuedInvoices.status.issued.label")}
|
||||
<SelectItem value="incorrecto">
|
||||
{t("catalog.issued_invoices.status.incorrecto.label")}
|
||||
</SelectItem>
|
||||
<SelectItem value="duplicado">
|
||||
{t("catalog.issued_invoices.status.duplicado.label")}
|
||||
</SelectItem>
|
||||
<SelectItem value="anulado">
|
||||
{t("catalog.issued_invoices.status.anulado.label")}
|
||||
</SelectItem>
|
||||
<SelectItem value="factura_inexistente">
|
||||
{t("catalog.issued_invoices.status.duplicado.label")}
|
||||
</SelectItem>
|
||||
<SelectItem value="rechazado">
|
||||
{t("catalog.issued_invoices.status.rechazado.label")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./issued-invoice.api.schema";
|
||||
export * from "./issued-invoice-summary.web.schema";
|
||||
export * from "./verifactu-record-status";
|
||||
|
||||
@ -0,0 +1,89 @@
|
||||
import {
|
||||
BugIcon,
|
||||
CheckCheckIcon,
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
FileQuestionIcon,
|
||||
HourglassIcon,
|
||||
StopCircleIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
export enum VERIFACTU_RECORD_STATUS {
|
||||
PENDIENTE = "Pendiente", // <- Registro encolado y no procesado aún
|
||||
CORRECTO = "Correcto", // <- Registro procesado correctamente por la AEAT
|
||||
ACEPTADO_CON_ERROR = "Aceptado con errores", // <- Registro aceptado con errores por la AEAT. Se requiere enviar un registro de subsanación o emitir una rectificativa
|
||||
INCORRECTO = "Incorrecto", // <- Registro considerado incorrecto por la AEAT. Se requiere enviar un registro de subsanación con rechazo_previo=S o rechazo_previo=X o emitir una rectificativa
|
||||
DUPLICADO = "Duplicado", // <- Registro no aceptado por la AEAT por existir un registro con el mismo (serie, numero, fecha_expedicion)
|
||||
ANULADO = "Anulado", // <- Registro de anulación procesado correctamente por la AEAT
|
||||
FACTURA_INEXISTENTE = "Factura inexistente", // <- Registro de anulación no aceptado por la AEAT por no existir la factura.
|
||||
RECHAZADO = "No registrado", // <- Registro rechazado por la AEAT
|
||||
ERROR = "Error servidor AEAT", // <- Error en el servidor de la AEAT. Se intentará reenviar el registro de facturación de nuevo
|
||||
}
|
||||
|
||||
export type VerifactuRecordStatus = `${VERIFACTU_RECORD_STATUS}`;
|
||||
|
||||
export const getVerifactuRecordStatusButtonVariant = (
|
||||
status: VerifactuRecordStatus
|
||||
): "default" | "secondary" | "outline" | "destructive" => {
|
||||
switch (status) {
|
||||
case "Pendiente":
|
||||
return "outline";
|
||||
case "Aceptado con errores":
|
||||
case "Duplicado":
|
||||
return "secondary";
|
||||
case "Anulado":
|
||||
return "default";
|
||||
case "Incorrecto":
|
||||
case "Error servidor AEAT":
|
||||
case "Factura inexistente":
|
||||
case "No registrado":
|
||||
return "destructive";
|
||||
case "Correcto":
|
||||
return "default";
|
||||
default:
|
||||
return "outline";
|
||||
}
|
||||
};
|
||||
|
||||
export const getVerifactuRecordStatusColor = (status: VerifactuRecordStatus): string => {
|
||||
switch (status) {
|
||||
case "Pendiente":
|
||||
return "bg-gray-100 text-gray-700 hover:bg-gray-100";
|
||||
case "Aceptado con errores":
|
||||
case "Duplicado":
|
||||
return "bg-yellow-100 text-yellow-700 hover:bg-yellow-100";
|
||||
case "Anulado":
|
||||
return "bg-green-100 text-green-700 hover:bg-green-100";
|
||||
case "Incorrecto":
|
||||
case "Error servidor AEAT":
|
||||
case "Factura inexistente":
|
||||
case "No registrado":
|
||||
return "bg-red-100 text-red-700 hover:bg-red-100";
|
||||
case "Correcto":
|
||||
return "bg-blue-100 text-blue-700 hover:bg-blue-100";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-700 hover:bg-gray-100";
|
||||
}
|
||||
};
|
||||
|
||||
export const getVerifactuRecordStatusIcon = (status: VerifactuRecordStatus) => {
|
||||
switch (status) {
|
||||
case "Pendiente":
|
||||
return HourglassIcon;
|
||||
case "Aceptado con errores":
|
||||
return CheckIcon;
|
||||
case "Duplicado":
|
||||
return CopyIcon;
|
||||
case "Anulado":
|
||||
return StopCircleIcon;
|
||||
case "Incorrecto":
|
||||
case "Error servidor AEAT":
|
||||
case "Factura inexistente":
|
||||
case "No registrado":
|
||||
return BugIcon;
|
||||
case "Correcto":
|
||||
return CheckCheckIcon;
|
||||
default:
|
||||
return FileQuestionIcon;
|
||||
}
|
||||
};
|
||||
@ -1,6 +1,5 @@
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
@ -39,7 +38,7 @@ export function useProformasGridColumns(
|
||||
|
||||
return React.useMemo<ColumnDef<ProformaSummaryData, unknown>[]>(
|
||||
() => [
|
||||
{
|
||||
/*{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
@ -60,7 +59,7 @@ export function useProformasGridColumns(
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
},*/
|
||||
{
|
||||
accessorKey: "invoice_number",
|
||||
header: ({ column }) => {
|
||||
|
||||
@ -3,14 +3,11 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="{{ asset 'tailwind.css' }}" />
|
||||
|
||||
<title>Factura</title>
|
||||
<style type="text/css">{{ asset 'tailwind.min.css' }}</style>
|
||||
|
||||
<style type="text/css">
|
||||
/* ---------------------------- */
|
||||
/* ESTRUCTURA CABECERA */
|
||||
/* ---------------------------- */
|
||||
|
||||
header {
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
@ -44,12 +41,14 @@
|
||||
|
||||
/* Bloque derecho */
|
||||
.right-block {
|
||||
display: flex;
|
||||
flex-direction: column; /* uno encima de otro */
|
||||
align-items: flex-end; /* o flex-start / center según quieras */
|
||||
justify-content: flex-start;
|
||||
width: 40%;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* uno encima de otro */
|
||||
align-items: flex-end;
|
||||
/* o flex-start / center según quieras */
|
||||
justify-content: flex-start;
|
||||
width: 40%;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.factura-img {
|
||||
@ -222,18 +221,18 @@
|
||||
|
||||
<div class="right-block">
|
||||
<div>
|
||||
<img src="{{asset 'factura_acana.jpg'}}" alt="Factura" class="factura-img" />
|
||||
<img src="{{asset 'factura_acana.jpg'}}" alt="Factura" class="factura-img" />
|
||||
</div>
|
||||
{{#if verifactu.qr_code}}
|
||||
<div style="display: flex; align-items: center; gap: 8px; padding-top: 10px;">
|
||||
<div style="text-align: right;">
|
||||
<div style="display: flex; align-items: center; gap: 4px; padding-top: 10px; align-content: stretch">
|
||||
<div style="text-align: right; flex-grow: 1; flex-shrink: 1; flex-basis: auto;">
|
||||
QR tributario factura verificable en sede electronica de AEAT VERI*FACTU
|
||||
</div>
|
||||
<div>
|
||||
<div style="flex-grow: 0; flex-shrink: 0; flex-basis:100px;">
|
||||
<img src="{{verifactu.qr_code}}" alt="QR factura" style="width: 100px; height: 100px;" />
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -246,7 +245,7 @@
|
||||
</div>
|
||||
|
||||
<div class="info-box info-dire">
|
||||
<h2 class="font-semibold uppercase mb-1">{{recipient.name}}</h2>
|
||||
<h2 style="font-weight:600; text-transform:uppercase; margin-bottom:0.25rem;">{{recipient.name}}</h2>
|
||||
<p>{{recipient.tin}}</p>
|
||||
<p>{{recipient.street}}</p>
|
||||
<p>{{recipient.postal_code}} {{recipient.city}} {{recipient.province}}</p>
|
||||
@ -263,11 +262,11 @@
|
||||
<table class="table-header">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="py-2">Concepto</th>
|
||||
<th class="py-2">Ud.</th>
|
||||
<th class="py-2">Imp.</th>
|
||||
<th class="py-2"> </th>
|
||||
<th class="py-2">Imp. total</th>
|
||||
<th style="padding-top:0.5rem; padding-bottom:0.5rem;">Concepto</th>
|
||||
<th style="padding-top:0.5rem; padding-bottom:0.5rem;">Ud.</th>
|
||||
<th style="padding-top:0.5rem; padding-bottom:0.5rem;">Imp.</th>
|
||||
<th style="padding-top:0.5rem; padding-bottom:0.5rem;"> </th>
|
||||
<th style="padding-top:0.5rem; padding-bottom:0.5rem;">Imp. total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@ -275,10 +274,10 @@
|
||||
{{#each items}}
|
||||
<tr>
|
||||
<td>{{description}}</td>
|
||||
<td class="text-right">{{#if quantity}}{{quantity}}{{else}} {{/if}}</td>
|
||||
<td class="text-right">{{#if unit_amount}}{{unit_amount}}{{else}} {{/if}}</td>
|
||||
<td class="text-right">{{#if discount_percentage}}{{discount_percentage}}{{else}} {{/if}}</td>
|
||||
<td class="text-right">{{#if taxable_amount}}{{taxable_amount}}{{else}} {{/if}}</td>
|
||||
<td style="text-align:right;">{{#if quantity}}{{quantity}}{{else}} {{/if}}</td>
|
||||
<td style="text-align:right;">{{#if unit_amount}}{{unit_amount}}{{else}} {{/if}}</td>
|
||||
<td style="text-align:right;">{{#if discount_percentage}}{{discount_percentage}}{{else}} {{/if}}</td>
|
||||
<td style="text-align:right;">{{#if taxable_amount}}{{taxable_amount}}{{else}} {{/if}}</td>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
@ -291,7 +290,7 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if notes}}
|
||||
<p class="mt-2"><strong>Notas:</strong> {{notes}}</p>
|
||||
<p style="margin-top:0.5rem;"><strong>Notas:</strong> {{notes}}</p>
|
||||
{{/if}}
|
||||
</td>
|
||||
<!-- Columna derecha: totales -->
|
||||
@ -332,13 +331,13 @@
|
||||
</main>
|
||||
|
||||
|
||||
<footer id="footer" class="mt-4 border-t border-black">
|
||||
<aside class="mt-4">
|
||||
<footer id="footer" style="margin-top:1rem; border-top:1px solid #000000;">
|
||||
<aside style="margin-top: 1rem;">
|
||||
<tfoot>
|
||||
<p class="text-center">Insc. en el Reg. Merc. de Madrid, Tomo 31.839, Libro 0, Folio 191, Sección 8, Hoja
|
||||
<p style="text-align: center;">Insc. en el Reg. Merc. de Madrid, Tomo 31.839, Libro 0, Folio 191, Sección 8, Hoja
|
||||
M-572991
|
||||
CIF: B86913910</p>
|
||||
<p class="text-left" style="font-size: 6pt;">Información en protección de datos<br />De conformidad con lo
|
||||
<p style="text-align: left; font-size: 6pt;">Información en protección de datos:<br />De conformidad con lo
|
||||
dispuesto en el RGPD y LOPDGDD,
|
||||
informamos que los datos personales serán tratados por
|
||||
ALISO DESIGN S.L para cumplir con la obligación tributaria de emitir facturas. Podrá solicitar más
|
||||
@ -353,5 +352,4 @@
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1
modules/customer-invoices/templates/acana/tailwind.min.css
vendored
Normal file
1
modules/customer-invoices/templates/acana/tailwind.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
||||
import { ModuleClientParams } from "@erp/core/client";
|
||||
import type { ModuleClientParams } from "@erp/core/client";
|
||||
import { lazy } from "react";
|
||||
import { Outlet, RouteObject } from "react-router-dom";
|
||||
import { Outlet, type RouteObject } from "react-router-dom";
|
||||
|
||||
// Lazy load components
|
||||
const CustomersLayout = lazy(() =>
|
||||
@ -26,9 +26,9 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
||||
children: [
|
||||
{ path: "", index: true, element: <CustomersList /> }, // index
|
||||
{ path: "list", element: <CustomersList /> },
|
||||
{ path: "create", element: <CustomerAdd /> },
|
||||
//{ path: "create", element: <CustomerAdd /> },
|
||||
{ path: ":id", element: <CustomerView /> },
|
||||
{ path: ":id/edit", element: <CustomerUpdate /> },
|
||||
//{ path: ":id/edit", element: <CustomerUpdate /> },
|
||||
|
||||
//
|
||||
/*{ path: "create", element: <CustomersList /> },
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
|
||||
import { SimpleSearchInput } from '@erp/core/components';
|
||||
import { SimpleSearchInput } from "@erp/core/components";
|
||||
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerSummaryFormData, CustomersPageFormData } from '../../schemas';
|
||||
import { useCustomersListColumns } from './use-customers-list-columns';
|
||||
|
||||
import { useTranslation } from "../../i18n";
|
||||
import type { CustomerSummaryFormData, CustomersPageFormData } from "../../schemas";
|
||||
|
||||
import { useCustomersListColumns } from "./use-customers-list-columns";
|
||||
|
||||
export type CustomerUpdateCompProps = {
|
||||
customersPage: CustomersPageFormData;
|
||||
@ -20,8 +20,12 @@ export type CustomerUpdateCompProps = {
|
||||
searchValue: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
|
||||
onRowClick?: (row: CustomerSummaryFormData, index: number, event: React.MouseEvent<HTMLTableRowElement>) => void;
|
||||
}
|
||||
onRowClick?: (
|
||||
row: CustomerSummaryFormData,
|
||||
index: number,
|
||||
event: React.MouseEvent<HTMLTableRowElement>
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const CustomersListGrid = ({
|
||||
customersPage,
|
||||
@ -30,8 +34,9 @@ export const CustomersListGrid = ({
|
||||
pageSize,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
searchValue, onSearchChange,
|
||||
onRowClick
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
onRowClick,
|
||||
}: CustomerUpdateCompProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@ -41,11 +46,10 @@ export const CustomersListGrid = ({
|
||||
|
||||
const columns = useCustomersListColumns({
|
||||
onEdit: (customer) => navigate(`/customers/${customer.id}/edit`),
|
||||
onView: (customer) => null, //duplicateInvoice(inv.id),
|
||||
onView: (customer) => navigate(`/customers/${customer.id}`),
|
||||
onDelete: (customer) => null, //confirmDelete(inv.id),
|
||||
});
|
||||
|
||||
|
||||
// Navegación centralizada (click/teclado)
|
||||
const goToRow = useCallback(
|
||||
(id: string, newTab = false) => {
|
||||
@ -60,7 +64,8 @@ export const CustomersListGrid = ({
|
||||
);
|
||||
|
||||
// Handlers de búsqueda
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => onSearchChange(e.target.value);
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onSearchChange(e.target.value);
|
||||
const handleClear = () => onSearchChange("");
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
@ -83,9 +88,9 @@ export const CustomersListGrid = ({
|
||||
<div className="flex flex-col gap-4">
|
||||
<SkeletonDataTable
|
||||
columns={columns.length}
|
||||
footerProps={{ pageIndex, pageSize, totalItems: total_items ?? 0 }}
|
||||
rows={Math.max(6, pageSize)}
|
||||
showFooter
|
||||
footerProps={{ pageIndex, pageSize, totalItems: total_items ?? 0 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -96,23 +101,23 @@ export const CustomersListGrid = ({
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Barra de filtros */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
||||
<SimpleSearchInput onSearchChange={onSearchChange} loading={loading} />
|
||||
<SimpleSearchInput loading={loading} onSearchChange={onSearchChange} />
|
||||
</div>
|
||||
<div className="relative flex">
|
||||
<div className={/*preview.isPinned ? "flex-1 mr-[500px]" : */"flex-1"}>
|
||||
<div className={/*preview.isPinned ? "flex-1 mr-[500px]" : */ "flex-1"}>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={items}
|
||||
readOnly
|
||||
enableRowSelection
|
||||
enablePagination
|
||||
enableRowSelection
|
||||
manualPagination
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
totalItems={total_items}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
onRowClick={() => null /*handleRowClick*/}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
readOnly
|
||||
totalItems={total_items}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -129,4 +134,4 @@ export const CustomersListGrid = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@ -4,9 +4,11 @@ import { Button } from "@repo/shadcn-ui/components";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Outlet, useNavigate } from "react-router-dom";
|
||||
|
||||
import { ErrorAlert } from "../../components";
|
||||
import { useCustomerListQuery } from "../../hooks";
|
||||
import { useTranslation } from "../../i18n";
|
||||
|
||||
import { CustomersListGrid } from "./customers-list-grid";
|
||||
|
||||
export const CustomersListPage = () => {
|
||||
@ -49,8 +51,8 @@ export const CustomersListPage = () => {
|
||||
return (
|
||||
<AppContent>
|
||||
<ErrorAlert
|
||||
title={t("pages.list.loadErrorTitle")}
|
||||
message={(error as Error)?.message || "Error al cargar el listado"}
|
||||
title={t("pages.list.loadErrorTitle")}
|
||||
/>
|
||||
<BackHistoryButton />
|
||||
</AppContent>
|
||||
@ -61,35 +63,35 @@ export const CustomersListPage = () => {
|
||||
<>
|
||||
<AppHeader>
|
||||
<PageHeader
|
||||
title={t("pages.list.title")}
|
||||
description={t("pages.list.description")}
|
||||
rightSlot={
|
||||
<div className='flex items-center space-x-2'>
|
||||
<div className="flex items-center space-x-2 hidden">
|
||||
<Button
|
||||
aria-label={t("pages.create.title")}
|
||||
className="cursor-pointer"
|
||||
onClick={() => navigate("/customers/create")}
|
||||
variant={"default"}
|
||||
aria-label={t("pages.create.title")}
|
||||
className='cursor-pointer'
|
||||
>
|
||||
<PlusIcon className='mr-2 h-4 w-4' aria-hidden />
|
||||
<PlusIcon aria-hidden className="mr-2 h-4 w-4" />
|
||||
{t("pages.create.title")}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
title={t("pages.list.title")}
|
||||
/>
|
||||
</AppHeader>
|
||||
<AppContent>
|
||||
<div className='flex flex-col w-full h-full py-3'>
|
||||
<div className="flex flex-col w-full h-full py-3">
|
||||
<div className={"flex-1"}>
|
||||
<CustomersListGrid
|
||||
customersPage={customersPageData}
|
||||
loading={isLoading}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
searchValue={search}
|
||||
onSearchChange={handleSearchChange}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
searchValue={search}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -14,9 +14,9 @@ import {
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
Building2Icon,
|
||||
EyeIcon,
|
||||
MailIcon,
|
||||
MoreHorizontalIcon,
|
||||
PencilIcon,
|
||||
PhoneIcon,
|
||||
User2Icon,
|
||||
} from "lucide-react";
|
||||
@ -193,40 +193,44 @@ export function useCustomersListColumns(
|
||||
<div className="flex justify-end">
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
aria-label="Edit customer"
|
||||
onClick={() => onEdit?.(customer)}
|
||||
aria-label="Ver cliente"
|
||||
onClick={() => onView?.(customer)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<PencilIcon className="size-4" />
|
||||
<EyeIcon className="size-4" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button aria-label="More actions" size="icon" variant="ghost">
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onView?.(customer)}>Open</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onEdit?.(customer)}>Edit</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => window.open(safeHttp(website), "_blank")}>
|
||||
Visit website
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(email_primary)}>
|
||||
Copy email
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onClick={() => onDelete?.(customer)}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{0 === false && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button aria-label="More actions" size="icon" variant="ghost">
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onView?.(customer)}>Open</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onEdit?.(customer)}>Edit</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => window.open(safeHttp(website), "_blank")}>
|
||||
Visit website
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigator.clipboard.writeText(email_primary)}
|
||||
>
|
||||
Copy email
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onClick={() => onDelete?.(customer)}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user