Facturas de cliente
This commit is contained in:
parent
817dcff8c5
commit
af7d3dcf28
@ -43,8 +43,8 @@
|
||||
"dependencies": {
|
||||
"@erp/auth": "workspace:*",
|
||||
"@erp/core": "workspace:*",
|
||||
"@erp/customers": "workspace:*",
|
||||
"@erp/customer-invoices": "workspace:*",
|
||||
"@erp/customers": "workspace:*",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cls-rtracer": "^2.6.3",
|
||||
"cors": "^2.8.5",
|
||||
@ -52,6 +52,7 @@
|
||||
"dotenv": "^16.5.0",
|
||||
"express": "^4.18.2",
|
||||
"express-list-routes": "^1.3.1",
|
||||
"handlebars": "^4.7.8",
|
||||
"helmet": "^8.0.0",
|
||||
"http": "0.0.1-security",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
@ -62,6 +63,8 @@
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"path": "^0.12.7",
|
||||
"puppeteer": "^24.20.0",
|
||||
"puppeteer-report": "^3.2.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"response-time": "^2.3.3",
|
||||
"sequelize": "^6.37.5",
|
||||
@ -76,9 +79,14 @@
|
||||
"node": ">=22"
|
||||
},
|
||||
"tsup": {
|
||||
"entry": ["src/index.ts"],
|
||||
"entry": [
|
||||
"src/index.ts"
|
||||
],
|
||||
"outDir": "dist",
|
||||
"format": ["esm", "cjs"],
|
||||
"format": [
|
||||
"esm",
|
||||
"cjs"
|
||||
],
|
||||
"target": "ES2022",
|
||||
"sourcemap": true,
|
||||
"clean": true,
|
||||
|
||||
@ -5,7 +5,7 @@ export type IPresenterParams = {
|
||||
presenterRegistry: IPresenterRegistry;
|
||||
} & Record<string, unknown>;
|
||||
|
||||
export abstract class Presenter implements IPresenter {
|
||||
export abstract class Presenter<T, S> implements IPresenter<T, S> {
|
||||
constructor(protected presenterRegistry: IPresenterRegistry) {}
|
||||
abstract toOutput(source: unknown): unknown;
|
||||
abstract toOutput(source: T): S;
|
||||
}
|
||||
|
||||
@ -123,7 +123,7 @@ export abstract class ExpressController {
|
||||
this.res.set({
|
||||
"Content-Type": "application/pdf",
|
||||
"Content-Disposition": `attachment; filename=${filename}`,
|
||||
//"Content-Length": buffer.length,
|
||||
"Content-Length": pdfBuffer.length,
|
||||
});
|
||||
|
||||
return this.res.send(pdfBuffer);
|
||||
|
||||
@ -13,7 +13,10 @@
|
||||
"@tanstack/react-query": "^5.74.11",
|
||||
"dinero.js": "^1.9.1",
|
||||
"express": "^4.18.2",
|
||||
"handlebars": "^4.7.8",
|
||||
"i18next": "^25.1.1",
|
||||
"puppeteer": "^24.20.0",
|
||||
"puppeteer-report": "^3.2.0",
|
||||
"react-hook-form": "^7.58.1",
|
||||
"react-i18next": "^15.5.1",
|
||||
"sequelize": "^6.37.5",
|
||||
@ -45,11 +48,8 @@
|
||||
"ag-grid-community": "^33.3.0",
|
||||
"ag-grid-react": "^33.3.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"libphonenumber-js": "^1.12.7",
|
||||
"lucide-react": "^0.503.0",
|
||||
"puppeteer": "^24.20.0",
|
||||
"puppeteer-report": "^3.2.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^6.26.0"
|
||||
|
||||
@ -45,6 +45,7 @@ export class CustomerInvoiceFullPresenter extends Presenter {
|
||||
|
||||
metadata: {
|
||||
entity: "customer-invoices",
|
||||
link: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { CustomerInvoiceService } from "../../domain";
|
||||
import { CustomerInvoiceReportPDFPresenter } from "./reporter";
|
||||
|
||||
type ReportCustomerInvoiceUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
@ -15,7 +16,7 @@ export class ReportCustomerInvoiceUseCase {
|
||||
private readonly presenterRegistry: IPresenterRegistry
|
||||
) {}
|
||||
|
||||
public execute(params: ReportCustomerInvoiceUseCaseInput) {
|
||||
public async execute(params: ReportCustomerInvoiceUseCaseInput) {
|
||||
const { invoice_id, companyId } = params;
|
||||
|
||||
const idOrError = UniqueID.create(invoice_id);
|
||||
@ -29,7 +30,7 @@ export class ReportCustomerInvoiceUseCase {
|
||||
resource: "customer-invoice",
|
||||
projection: "REPORT",
|
||||
format: "PDF",
|
||||
});
|
||||
}) as CustomerInvoiceReportPDFPresenter;
|
||||
|
||||
return this.transactionManager.complete(async (transaction) => {
|
||||
try {
|
||||
@ -43,7 +44,7 @@ export class ReportCustomerInvoiceUseCase {
|
||||
}
|
||||
|
||||
const invoice = invoiceOrError.data;
|
||||
const pdfData = pdfPresenter.toOutput(invoiceOrError.data);
|
||||
const pdfData = await pdfPresenter.toOutput(invoice);
|
||||
return Result.ok({
|
||||
data: pdfData,
|
||||
filename: `invoice-${invoice.invoiceNumber}.pdf`,
|
||||
|
||||
@ -15,7 +15,7 @@ export class CustomerInvoiceReportHTMLPresenter extends Presenter {
|
||||
|
||||
// Obtener y compilar la plantilla HTML
|
||||
const templateHtml = readFileSync(
|
||||
path.join(__dirname, "./templates/quote/template.hbs")
|
||||
path.join(__dirname, "./templates/customer-invoice/template.hbs")
|
||||
).toString();
|
||||
const template = handlebars.compile(templateHtml, {});
|
||||
return template(invoiceDTO);
|
||||
|
||||
@ -4,50 +4,53 @@ import report from "puppeteer-report";
|
||||
import { CustomerInvoice } from "../../../domain";
|
||||
import { CustomerInvoiceReportHTMLPresenter } from "./customer-invoice.report.html";
|
||||
|
||||
export interface ICustomerInvoiceReporter {
|
||||
toHTML: (invoice: CustomerInvoice) => Promise<string>;
|
||||
toPDF: (invoice: CustomerInvoice) => Promise<Buffer>;
|
||||
}
|
||||
|
||||
// https://plnkr.co/edit/lWk6Yd?preview
|
||||
|
||||
export class CustomerInvoiceReportPDFPresenter extends Presenter {
|
||||
async toOutput(customerInvoice: CustomerInvoice): Promise<Buffer> {
|
||||
const htmlPresenter = this.presenterRegistry.getPresenter({
|
||||
resource: "customer-invoice",
|
||||
projection: "REPORT",
|
||||
format: "HTML",
|
||||
}) as CustomerInvoiceReportHTMLPresenter;
|
||||
export class CustomerInvoiceReportPDFPresenter extends Presenter<
|
||||
CustomerInvoice,
|
||||
Promise<Buffer<ArrayBuffer>>
|
||||
> {
|
||||
async toOutput(customerInvoice: CustomerInvoice): Promise<Buffer<ArrayBuffer>> {
|
||||
try {
|
||||
const htmlPresenter = this.presenterRegistry.getPresenter({
|
||||
resource: "customer-invoice",
|
||||
projection: "REPORT",
|
||||
format: "HTML",
|
||||
}) as CustomerInvoiceReportHTMLPresenter;
|
||||
|
||||
const htmlData = htmlPresenter.toOutput(customerInvoice);
|
||||
const htmlData = htmlPresenter.toOutput(customerInvoice);
|
||||
|
||||
// Generar el PDF con Puppeteer
|
||||
const browser = await puppeteer.launch({
|
||||
args: [
|
||||
"--disable-extensions",
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-gpu",
|
||||
],
|
||||
});
|
||||
// Generar el PDF con Puppeteer
|
||||
const browser = await puppeteer.launch({
|
||||
args: [
|
||||
"--disable-extensions",
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-gpu",
|
||||
],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
const navigationPromise = page.waitForNavigation();
|
||||
await page.setContent(htmlData, { waitUntil: "networkidle2" });
|
||||
const page = await browser.newPage();
|
||||
const navigationPromise = page.waitForNavigation();
|
||||
await page.setContent(htmlData, { waitUntil: "networkidle2" });
|
||||
|
||||
await navigationPromise;
|
||||
const reportPDF = await report.pdfPage(page, {
|
||||
format: "A4",
|
||||
margin: {
|
||||
bottom: "10mm",
|
||||
left: "10mm",
|
||||
right: "10mm",
|
||||
top: "10mm",
|
||||
},
|
||||
});
|
||||
await navigationPromise;
|
||||
const reportPDF = await report.pdfPage(page, {
|
||||
format: "A4",
|
||||
margin: {
|
||||
bottom: "10mm",
|
||||
left: "10mm",
|
||||
right: "10mm",
|
||||
top: "10mm",
|
||||
},
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
return Buffer.from(reportPDF);
|
||||
await browser.close();
|
||||
return Buffer.from(reportPDF);
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
throw err as Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
<html lang="{{lang_code}}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css"
|
||||
referrerpolicy="no-referrer" />
|
||||
<title>Presupuesto #{{id}}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: #000;
|
||||
font-size: 11pt;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
#header {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#footer {}
|
||||
|
||||
@media print {
|
||||
thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
|
||||
tfoot {
|
||||
display: table-footer-group;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
<footer id="footer" class="mt-4">
|
||||
<aside><img src="https://uecko.com/assets/img/uecko-footer_logos.jpg" class="w-full" /></aside>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
@ -1,152 +0,0 @@
|
||||
<html lang="{{lang_code}}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css"
|
||||
referrerpolicy="no-referrer" />
|
||||
<title>Presupuesto #{{id}}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: #000;
|
||||
font-size: 11pt;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
#header {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#footer {}
|
||||
|
||||
@media print {
|
||||
thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
|
||||
tfoot {
|
||||
display: table-footer-group;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header id="header">
|
||||
<aside class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold">DISTRIBUIDOR OFICIAL</h3>
|
||||
<div id="logo" class="w-32">
|
||||
<svg viewBox="0 0 336 133" fill="none" xmlns="http://www.w3.org/2000/svg" class="uecko-logo">
|
||||
<path
|
||||
d="M49.7002 83.0001H66.9002V22.5001H49.7002V56.2001C49.7002 64.3001 45.5002 68.5001 39.0002 68.5001C32.5002 68.5001 28.6002 64.3001 28.6002 56.2001V22.5001H0.700195V33.2001H11.4002V61.6001C11.4002 75.5001 19.0002 84.1001 31.9002 84.1001C40.6002 84.1001 45.7002 79.5001 49.6002 74.4001V83.0001H49.7002ZM120.6 48.0001H94.8002C96.2002 40.2001 100.8 35.1001 107.9 35.1001C115.1 35.2001 119.6 40.3001 120.6 48.0001ZM137.1 58.7001C137.2 57.1001 137.3 56.1001 137.3 54.4001V54.2001C137.3 37.0001 128 21.4001 107.8 21.4001C90.2002 21.4001 77.9002 35.6001 77.9002 52.9001V53.1001C77.9002 71.6001 91.3002 84.4001 109.5 84.4001C120.4 84.4001 128.6 80.1001 134.2 73.1001L124.4 64.4001C119.7 68.8001 115.5 70.6001 109.7 70.6001C102 70.6001 96.6002 66.5001 94.9002 58.7001H137.1ZM162.2 52.9001V52.7001C162.2 43.8001 168.3 36.2001 176.9 36.2001C183 36.2001 186.8 38.8001 190.7 42.9001L201.2 31.6001C195.6 25.3001 188.4 21.4001 177 21.4001C158.5 21.4001 145.3 35.6001 145.3 52.9001V53.1001C145.3 70.4001 158.6 84.4001 176.8 84.4001C188.9 84.4001 195.6 79.8001 201.5 73.3001L191.5 63.1001C187.3 67.1001 183.4 69.5001 177.6 69.5001C168.2 69.6001 162.2 62.1001 162.2 52.9001ZM269.1 83.0001L245.3 46.3001L268.3 22.5001H247.8L227.7 44.5001V0.600098H210.5V83.0001H227.7V64.6001L233.7 58.3001L249.5 83.0001H269.1ZM318.5 53.1001C318.5 62.0001 312.6 69.6001 302.8 69.6001C293.3 69.6001 286.9 61.8001 286.9 52.9001V52.7001C286.9 43.8001 292.8 36.2001 302.6 36.2001C312.1 36.2001 318.5 44.0001 318.5 52.9001V53.1001ZM335.4 52.9001V52.7001C335.4 35.3001 321.5 21.4001 302.8 21.4001C284 21.4001 270 35.5001 270 52.9001V53.1001C270 70.5001 283.9 84.4001 302.6 84.4001C321.4 84.4001 335.4 70.3001 335.4 52.9001Z"
|
||||
fill="black" class="uecko-logo"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</aside>
|
||||
<section class="flex pb-2 space-x-4">
|
||||
<img id="dealer-logo" src={{dealer.logo}} alt="Logo distribuidor" />
|
||||
<address class="text-base not-italic font-medium whitespace-pre-line" id="from">{{dealer.contact_information}}
|
||||
</address>
|
||||
</section>
|
||||
<section class="grid grid-cols-2 gap-4 pb-4 mb-4 border-b">
|
||||
<aside>
|
||||
<p class="text-sm"><strong>Presupuesto nº:</strong> {{reference}}</p>
|
||||
<p class="text-sm"><strong>Fecha:</strong> {{date}}</p>
|
||||
<p class="text-sm"><strong>Validez:</strong> {{validity}}</p>
|
||||
<p class="text-sm"><strong>Vendedor:</strong> {{dealer.name}}</p>
|
||||
<p class="text-sm"><strong>Referencia cliente:</strong> {{customer_reference}}</p>
|
||||
</aside>
|
||||
<address class="text-base not-italic font-semibold whitespace-pre-line" id="to">{{customer_information}}
|
||||
</address>
|
||||
</section>
|
||||
<aside class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-2xl font-normal">PRESUPUESTO</h3>
|
||||
<div id="header-pagination">
|
||||
Página <span class="pageNumber"></span> de <span class="totalPages"></span>
|
||||
</div>
|
||||
</aside>
|
||||
</header>
|
||||
<main id="main">
|
||||
<section id="details">
|
||||
<table class="table-header">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-2 py-2 text-right border">Cant.</th>
|
||||
<th class="px-2 py-2 border">Descripción</th>
|
||||
<th class="px-2 py-2 text-right border">Prec. Unitario</th>
|
||||
<th class="px-2 py-2 text-right border">Subtotal</th>
|
||||
<th class="px-2 py-2 text-right border">Dto (%)</th>
|
||||
<th class="px-2 py-2 text-right border">Importe total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-body">
|
||||
{{#each items}}
|
||||
<tr class="text-sm border-b">
|
||||
<td class="content-start px-2 py-2 text-right">{{quantity}}</td>
|
||||
<td class="px-2 py-2 font-medium">{{description}}</td>
|
||||
<td class="content-start px-2 py-2 text-right">{{unit_price}}</td>
|
||||
<td class="content-start px-2 py-2 text-right">{{subtotal_price}}</td>
|
||||
<td class="content-start px-2 py-2 text-right">{{discount}}</td>
|
||||
<td class="content-start px-2 py-2 text-right">{{total_price}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="resume" class="flex items-center justify-between pb-4 mb-4">
|
||||
|
||||
<div class="grow">
|
||||
<div class="pt-4">
|
||||
<p class="text-sm"><strong>Forma de pago:</strong> {{payment_method}}</p>
|
||||
</div>
|
||||
<div class="pt-4">
|
||||
<p class="text-sm"><strong>Notas:</strong> {{notes}} </p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grow">
|
||||
<table class="min-w-full bg-transparent">
|
||||
<tbody>
|
||||
<tr class="border-b">
|
||||
<td class="px-4 py-2">Importe neto</td>
|
||||
<td class="px-4 py-2"></td>
|
||||
<td class="px-4 py-2 text-right border">{{subtotal_price}}</td>
|
||||
</tr>
|
||||
<tr class="border-b">
|
||||
<td class="px-4 py-2">% Descuento</td>
|
||||
<td class="px-4 py-2 text-right border">{{discount.amount}}</td>
|
||||
<td class="px-4 py-2 text-right border">{{discount_price}}</td>
|
||||
</tr>
|
||||
<tr class="border-b">
|
||||
<td class="px-4 py-2">Base imponible</td>
|
||||
<td class="px-4 py-2"></td>
|
||||
<td class="px-4 py-2 text-right border">{{before_tax_price}}</td>
|
||||
</tr>
|
||||
<tr class="border-b">
|
||||
<td class="px-4 py-2">% IVA</td>
|
||||
<td class="px-4 py-2 text-right border">{{tax}}</td>
|
||||
<td class="px-4 py-2 text-right border">{{tax_price}}</td>
|
||||
</tr>
|
||||
<tr class="border-b">
|
||||
<td class="px-4 py-2">Importe total</td>
|
||||
<td class="px-4 py-2"></td>
|
||||
<td class="px-4 py-2 text-right border">{{total_price}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<section id="legal_terms">
|
||||
<p class="text-xs text-gray-500">{{quote.default_legal_terms}}</p>
|
||||
</section>
|
||||
</main>
|
||||
<footer id="footer" class="mt-4">
|
||||
<aside><img src="https://uecko.com/assets/img/uecko-footer_logos.jpg" class="w-full" /></aside>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,5 +1,5 @@
|
||||
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||
import { GetCustomerInvoiceUseCase as ReportCustomerInvoiceUseCase } from "../../../application";
|
||||
import { ReportCustomerInvoiceUseCase } from "../../../application";
|
||||
|
||||
export class ReportCustomerInvoiceController extends ExpressController {
|
||||
public constructor(private readonly useCase: ReportCustomerInvoiceUseCase) {
|
||||
@ -15,7 +15,7 @@ export class ReportCustomerInvoiceController extends ExpressController {
|
||||
const result = await this.useCase.execute({ invoice_id, companyId });
|
||||
|
||||
return result.match(
|
||||
({ pdfData, filename }) => this.downloadPDF(pdfData, filename),
|
||||
({ data, filename }) => this.downloadPDF(data, filename),
|
||||
(err) => this.handleError(err)
|
||||
);
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import { MoneyDTO } from "@erp/core";
|
||||
import { formatDate, formatMoney } from "@erp/core/client";
|
||||
// Core CSS
|
||||
import { AgGridReact } from "ag-grid-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useCustomerInvoicesQuery } from "../hooks";
|
||||
import { useTranslation } from "../i18n";
|
||||
import { CustomerInvoiceStatusBadge } from "./customer-invoice-status-badge";
|
||||
@ -76,6 +77,13 @@ export const CustomerInvoicesListGrid = () => {
|
||||
return formatMoney(rawValue);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "id",
|
||||
headerName: t("pages.list.grid_columns.total_amount"),
|
||||
cellRenderer: (params: ValueFormatterParams) => {
|
||||
return <Link to={params.value}>Hola</Link>;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const gridOptions: GridOptions = {
|
||||
|
||||
@ -276,98 +276,11 @@ export const CustomerInvoiceEditForm = ({
|
||||
form.reset(initialData);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit, handleError)}>
|
||||
<div className='grid xl:grid-cols-2 space-y-6'>
|
||||
<Card className='border-0 shadow-none xl:border-r xl:border-dashed rounded-none'>
|
||||
<CardHeader>
|
||||
<CardTitle>Cliente</CardTitle>
|
||||
<CardDescription>Description</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant='link'>Sign Up</Button>
|
||||
<Button variant='link'>Sign Up</Button>
|
||||
<Button variant='link'>Sign Up</Button>
|
||||
<Button variant='link'>Sign Up</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className='grid grid-cols-1 gap-4 space-y-6'>
|
||||
<ClientSelector />
|
||||
</CardContent>
|
||||
<CardFooter className='flex-col gap-2'>
|
||||
<Button type='submit' className='w-full'>
|
||||
Login
|
||||
</Button>
|
||||
<Button variant='outline' className='w-full'>
|
||||
Login with Google
|
||||
</Button>
|
||||
</CardFooter>{" "}
|
||||
</Card>
|
||||
{/* Información básica */}
|
||||
<Card className='@container border-0 shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle>Información Básica</CardTitle>
|
||||
<CardDescription>Detalles generales de la factura</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='@xl:grid @xl:grid-cols-2 @xl:gap-x-6 gap-y-8'>
|
||||
<TextField
|
||||
control={form.control}
|
||||
name='invoice_number'
|
||||
required
|
||||
disabled
|
||||
readOnly
|
||||
label={t("form_fields.invoice_number.label")}
|
||||
placeholder={t("form_fields.invoice_number.placeholder")}
|
||||
description={t("form_fields.invoice_number.description")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
control={form.control}
|
||||
name='invoice_series'
|
||||
required
|
||||
label={t("form_fields.invoice_series.label")}
|
||||
placeholder={t("form_fields.invoice_series.placeholder")}
|
||||
description={t("form_fields.invoice_series.description")}
|
||||
/>
|
||||
|
||||
<DatePickerInputField
|
||||
control={form.control}
|
||||
name='invoice_date'
|
||||
required
|
||||
label={t("form_fields.invoice_date.label")}
|
||||
placeholder={t("form_fields.invoice_date.placeholder")}
|
||||
description={t("form_fields.invoice_date.description")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className='@xl:col-start-1 @xl:col-span-full'
|
||||
control={form.control}
|
||||
name='description'
|
||||
required
|
||||
label={t("form_fields.description.label")}
|
||||
placeholder={t("form_fields.description.placeholder")}
|
||||
description={t("form_fields.description.description")}
|
||||
/>
|
||||
<TextAreaField
|
||||
className='field-sizing-content @xl:col-start-1 @xl:col-span-full'
|
||||
control={form.control}
|
||||
name='notes'
|
||||
label={t("form_fields.notes.label")}
|
||||
placeholder={t("form_fields.notes.placeholder")}
|
||||
description={t("form_fields.notes.description")}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit, handleError)}
|
||||
className='grid grid-cols-1 md:gap-6 md:grid-cols-6'
|
||||
className='grid grid-cols-1 md:gap-6 md:grid-cols-2'
|
||||
>
|
||||
<Card className='border-0 shadow-none md:grid-span-2'>
|
||||
<CardHeader>
|
||||
|
||||
@ -189,8 +189,9 @@ export const ClientSelector = () => {
|
||||
setSelectedCustomer(customer);
|
||||
setOpen(false);
|
||||
}}
|
||||
onCreate={() => {
|
||||
setOpen(false);
|
||||
onCreate={(e) => {
|
||||
e.preventDefault();
|
||||
setOpen(true);
|
||||
console.log("Crear nuevo cliente");
|
||||
}}
|
||||
page={pageNumber}
|
||||
|
||||
@ -62,6 +62,9 @@ importers:
|
||||
express-list-routes:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1
|
||||
handlebars:
|
||||
specifier: ^4.7.8
|
||||
version: 4.7.8
|
||||
helmet:
|
||||
specifier: ^8.0.0
|
||||
version: 8.1.0
|
||||
@ -92,6 +95,12 @@ importers:
|
||||
path:
|
||||
specifier: ^0.12.7
|
||||
version: 0.12.7
|
||||
puppeteer:
|
||||
specifier: ^24.20.0
|
||||
version: 24.20.0(typescript@5.8.3)
|
||||
puppeteer-report:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
reflect-metadata:
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.2
|
||||
@ -170,7 +179,7 @@ importers:
|
||||
version: 29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3))
|
||||
ts-jest:
|
||||
specifier: ^29.2.5
|
||||
version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
tsconfig-paths:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
@ -12283,7 +12292,7 @@ snapshots:
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3):
|
||||
ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
ejs: 3.1.10
|
||||
@ -12301,6 +12310,7 @@ snapshots:
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.27.4)
|
||||
esbuild: 0.25.5
|
||||
jest-util: 29.7.0
|
||||
|
||||
ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user