Facturas de cliente
This commit is contained in:
parent
d971411757
commit
606601026a
@ -182,7 +182,7 @@
|
||||
},
|
||||
|
||||
{
|
||||
"name": "REC 5,2%",
|
||||
"name": "Rec. de equivalencia 5,2%",
|
||||
"code": "rec_5_2",
|
||||
"value": "520",
|
||||
"scale": "2",
|
||||
@ -191,7 +191,7 @@
|
||||
"aeat_code": "51"
|
||||
},
|
||||
{
|
||||
"name": "REC 1,75%",
|
||||
"name": "Rec. de equivalencia 1,75%",
|
||||
"code": "rec_1_75",
|
||||
"value": "175",
|
||||
"scale": "2",
|
||||
@ -200,7 +200,7 @@
|
||||
"aeat_code": "52"
|
||||
},
|
||||
{
|
||||
"name": "REC 1,4%",
|
||||
"name": "Rec. de equivalencia 1,4%",
|
||||
"code": "rec_1_4",
|
||||
"value": "140",
|
||||
"scale": "2",
|
||||
@ -209,7 +209,7 @@
|
||||
"aeat_code": null
|
||||
},
|
||||
{
|
||||
"name": "REC 1%",
|
||||
"name": "Rec. de equivalencia 1%",
|
||||
"code": "rec_1",
|
||||
"value": "100",
|
||||
"scale": "2",
|
||||
@ -218,7 +218,7 @@
|
||||
"aeat_code": null
|
||||
},
|
||||
{
|
||||
"name": "REC 0,62%",
|
||||
"name": "Rec. de equivalencia 0,62%",
|
||||
"code": "rec_0_62",
|
||||
"value": "62",
|
||||
"scale": "2",
|
||||
@ -227,7 +227,7 @@
|
||||
"aeat_code": null
|
||||
},
|
||||
{
|
||||
"name": "REC 0,5%",
|
||||
"name": "Rec. de equivalencia 0,5%",
|
||||
"code": "rec_0_5",
|
||||
"value": "50",
|
||||
"scale": "2",
|
||||
@ -236,7 +236,7 @@
|
||||
"aeat_code": null
|
||||
},
|
||||
{
|
||||
"name": "REC 0,26%",
|
||||
"name": "Rec. de equivalencia 0,26%",
|
||||
"code": "rec_0_26",
|
||||
"value": "26",
|
||||
"scale": "2",
|
||||
@ -245,7 +245,7 @@
|
||||
"aeat_code": null
|
||||
},
|
||||
{
|
||||
"name": "REC 0%",
|
||||
"name": "Rec. de equivalencia 0%",
|
||||
"code": "rec_0",
|
||||
"value": "0",
|
||||
"scale": "2",
|
||||
|
||||
@ -110,18 +110,17 @@
|
||||
|
||||
<body>
|
||||
|
||||
<header id="header">
|
||||
<aside class="flex items-start mb-4 w-full ">
|
||||
<header>
|
||||
<aside class="flex items-start mb-4 w-full">
|
||||
<!-- Bloque IZQUIERDO: imagen arriba + texto abajo, alineado a la izquierda -->
|
||||
<div class="flex flex-col items-start text-left" style="flex:0 0 70%;">
|
||||
<div class="w-[70%] flex flex-col items-start text-left">
|
||||
<img src="https://rodax-software.com/images/logo1.jpg" alt="Logo Rodax" class="block h-14 w-auto mb-1" />
|
||||
<div class="flex w-full">
|
||||
<div class="p-1 ">
|
||||
<p>Nº:<strong> {{invoice_number}}</strong></p>
|
||||
<p>Factura nº:<strong> {{invoice_number}}</strong></p>
|
||||
<p><span>Fecha:<strong> {{invoice_date}}</strong></p>
|
||||
<p>Página <span class="pageNumber"></span> de <span class="totalPages"></span></p>
|
||||
</div>
|
||||
<div class="p-1 ml-28">
|
||||
<div class="p-1 ml-9">
|
||||
<h2 class="font-semibold uppercase mb-1">{{recipient.name}}</h2>
|
||||
<p>{{recipient.tin}}</p>
|
||||
<p>{{recipient.street}}</p>
|
||||
@ -131,10 +130,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Bloque DERECHO: logo2 arriba y texto DEBAJO -->
|
||||
<div class="ml-auto flex flex-col items-end text-right h-full">
|
||||
<div class="ml-auto flex flex-col items-end text-right">
|
||||
<img src="https://rodax-software.com/images/logo2.jpg" alt="Logo secundario"
|
||||
class="block h-5 w-auto md:h-8 mb-1" />
|
||||
<div class="not-italic text-xs leading-tight h-full">
|
||||
<div class="not-italic text-xs leading-tight">
|
||||
<p>Telf: 91 785 02 47 / 686 62 10 59</p>
|
||||
<p><a href="mailto:info@rodax-software.com" class="hover:underline">info@rodax-software.com</a></p>
|
||||
<p><a href="https://www.rodax-software.com" target="_blank" rel="noopener"
|
||||
@ -145,8 +144,8 @@
|
||||
</header>
|
||||
|
||||
<main id="main">
|
||||
<h1>FACTURA PROFORMA</h1>
|
||||
<section id="details" class="border-b border-black ">
|
||||
|
||||
<div class="relative pt-0 border-b border-black">
|
||||
<!-- Badge TOTAL decorado con imagen -->
|
||||
<div class="absolute -top-9 right-0">
|
||||
@ -226,11 +225,15 @@
|
||||
<td class="w-5"> </td>
|
||||
<td class="px-4 text-right">{{taxable_amount}}</td>
|
||||
</tr>
|
||||
{{#if taxes_amount }}
|
||||
<tr>
|
||||
<td class="px-4 text-right">IVA 21%</td>
|
||||
<td class="w-5"> </td>
|
||||
<td class="px-4 text-right">{{taxes_amount}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<!-- iva 0-->
|
||||
{{/if}}
|
||||
<tr class="">
|
||||
<td class="px-4 text-right accent-color">
|
||||
Total factura
|
||||
@ -245,12 +248,12 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
<footer id="footer" class="mt-4">
|
||||
<aside>
|
||||
<p class="text-left"><strong>Factura Proforma.</strong>
|
||||
Este documento es de carácter informativo y no tiene validez contable ni fiscal. Contiene precios y condiciones
|
||||
de venta sujetos a confirmación del cliente. Solo adquirirá validez como factura definitiva una vez aceptados
|
||||
dichos términos.</p>
|
||||
<p class="text-center">Insc. en el Reg. Merc. de Madrid, Tomo 20.073, Libro 0, Folio 141, Sección 8, Hoja M-354212
|
||||
| CIF: B83999441 -
|
||||
Rodax Software S.L.</p>
|
||||
</aside>
|
||||
</footer>
|
||||
|
||||
|
||||
@ -110,17 +110,18 @@
|
||||
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<aside class="flex items-start mb-4 w-full">
|
||||
<header id="header">
|
||||
<aside class="flex items-start mb-4 w-full ">
|
||||
<!-- Bloque IZQUIERDO: imagen arriba + texto abajo, alineado a la izquierda -->
|
||||
<div class="w-[70%] flex flex-col items-start text-left">
|
||||
<div class="flex flex-col items-start text-left" style="flex:0 0 70%;">
|
||||
<img src="https://rodax-software.com/images/logo1.jpg" alt="Logo Rodax" class="block h-14 w-auto mb-1" />
|
||||
<div class="flex w-full">
|
||||
<div class="p-1 ">
|
||||
<p>Factura nº:<strong> {{invoice_number}}</strong></p>
|
||||
<p>Nº:<strong> {{invoice_number}}</strong></p>
|
||||
<p><span>Fecha:<strong> {{invoice_date}}</strong></p>
|
||||
<p>Página <span class="pageNumber"></span> de <span class="totalPages"></span></p>
|
||||
</div>
|
||||
<div class="p-1 ml-9">
|
||||
<div class="p-1 ml-28">
|
||||
<h2 class="font-semibold uppercase mb-1">{{recipient.name}}</h2>
|
||||
<p>{{recipient.tin}}</p>
|
||||
<p>{{recipient.street}}</p>
|
||||
@ -130,10 +131,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Bloque DERECHO: logo2 arriba y texto DEBAJO -->
|
||||
<div class="ml-auto flex flex-col items-end text-right">
|
||||
<div class="ml-auto flex flex-col items-end text-right h-full">
|
||||
<img src="https://rodax-software.com/images/logo2.jpg" alt="Logo secundario"
|
||||
class="block h-5 w-auto md:h-8 mb-1" />
|
||||
<div class="not-italic text-xs leading-tight">
|
||||
<div class="not-italic text-xs leading-tight h-full">
|
||||
<p>Telf: 91 785 02 47 / 686 62 10 59</p>
|
||||
<p><a href="mailto:info@rodax-software.com" class="hover:underline">info@rodax-software.com</a></p>
|
||||
<p><a href="https://www.rodax-software.com" target="_blank" rel="noopener"
|
||||
@ -144,8 +145,8 @@
|
||||
</header>
|
||||
|
||||
<main id="main">
|
||||
<h1>FACTURA PROFORMA</h1>
|
||||
<section id="details" class="border-b border-black ">
|
||||
|
||||
<div class="relative pt-0 border-b border-black">
|
||||
<!-- Badge TOTAL decorado con imagen -->
|
||||
<div class="absolute -top-9 right-0">
|
||||
@ -225,15 +226,11 @@
|
||||
<td class="w-5"> </td>
|
||||
<td class="px-4 text-right">{{taxable_amount}}</td>
|
||||
</tr>
|
||||
{{#if taxes_amount }}
|
||||
<tr>
|
||||
<td class="px-4 text-right">IVA 21%</td>
|
||||
<td class="w-5"> </td>
|
||||
<td class="px-4 text-right">{{taxes_amount}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<!-- iva 0-->
|
||||
{{/if}}
|
||||
<tr class="">
|
||||
<td class="px-4 text-right accent-color">
|
||||
Total factura
|
||||
@ -248,12 +245,12 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
<footer id="footer" class="mt-4">
|
||||
<aside>
|
||||
<p class="text-center">Insc. en el Reg. Merc. de Madrid, Tomo 20.073, Libro 0, Folio 141, Sección 8, Hoja M-354212
|
||||
| CIF: B83999441 -
|
||||
Rodax Software S.L.</p>
|
||||
<p class="text-left"><strong>Factura Proforma.</strong>
|
||||
Este documento es de carácter informativo y no tiene validez contable ni fiscal. Contiene precios y condiciones
|
||||
de venta sujetos a confirmación del cliente. Solo adquirirá validez como factura definitiva una vez aceptados
|
||||
dichos términos.</p>
|
||||
</aside>
|
||||
</footer>
|
||||
|
||||
@ -65,7 +65,7 @@
|
||||
"form_groups": {
|
||||
"customer": {
|
||||
"title": "Customer",
|
||||
"description": "Select the customer for this invoice"
|
||||
"description": "Select the customer for this invoice."
|
||||
},
|
||||
"items": {
|
||||
"title": "Invoice details",
|
||||
@ -77,7 +77,7 @@
|
||||
},
|
||||
"totals": {
|
||||
"title": "Invoice totals",
|
||||
"description": ""
|
||||
"description": "Breakdown of invoice amounts with discounts and taxes."
|
||||
},
|
||||
"tax_resume": {
|
||||
"title": "Resumen de impuestos",
|
||||
|
||||
@ -78,7 +78,7 @@
|
||||
},
|
||||
"totals": {
|
||||
"title": "Totales de la factura",
|
||||
"description": ""
|
||||
"description": "Desglose de los importes de la factura con descuentos e impuestos."
|
||||
},
|
||||
"preferences": {
|
||||
"title": "Preferencias",
|
||||
|
||||
@ -4,8 +4,6 @@ import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
import { InvoiceFormData } from "../../schemas";
|
||||
import { InvoiceBasicInfoFields } from "./invoice-basic-info-fields";
|
||||
import { InvoiceItems } from './invoice-items-editor';
|
||||
import { InvoiceNotes } from './invoice-notes';
|
||||
import { InvoiceTaxSummary } from './invoice-tax-summary';
|
||||
import { InvoiceTotals } from './invoice-totals';
|
||||
import { InvoiceRecipient } from "./recipient";
|
||||
|
||||
@ -27,33 +25,27 @@ export const CustomerInvoiceEditForm = ({
|
||||
return (
|
||||
<form noValidate id={formId} onSubmit={form.handleSubmit(onSubmit, onError)}>
|
||||
<section className={cn("space-y-6", className)}>
|
||||
<div className='w-full'>
|
||||
|
||||
<div className='w-full grid grid-cols-4'>
|
||||
<div className="col-span-3">
|
||||
<InvoiceBasicInfoFields className="flex flex-col" />
|
||||
</div>
|
||||
<div className='col-span-1'>
|
||||
<InvoiceRecipient className="flex flex-col" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto grid w-full grid-cols-1 grid-flow-col gap-6 lg:grid-cols-2 items-stretch">
|
||||
<div className="lg:col-start-1 lg:row-span-2 h-full">
|
||||
<InvoiceBasicInfoFields className="h-full flex flex-col" />
|
||||
</div>
|
||||
|
||||
<div className="h-full ">
|
||||
<InvoiceRecipient className="h-full flex flex-col" />
|
||||
</div>
|
||||
|
||||
<div className="h-full ">
|
||||
<InvoiceNotes className="h-full flex flex-col" />
|
||||
</div>
|
||||
|
||||
<div className="lg:col-start-1 lg:col-span-full h-full">
|
||||
<div className="mx-auto grid w-full grid-cols-1 grid-flow-col gap-6 lg:grid-cols-4 items-stretch">
|
||||
<div className="lg:col-start-1 lg:col-span-3 h-full">
|
||||
<InvoiceItems className="h-full flex flex-col" />
|
||||
</div>
|
||||
|
||||
<div className="h-full lg:col-start-1">
|
||||
<InvoiceTaxSummary className="h-full flex flex-col" />
|
||||
</div>
|
||||
|
||||
<div className="h-full ">
|
||||
<InvoiceTotals className="h-full flex flex-col" />
|
||||
<InvoiceTotals />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@ -19,15 +19,16 @@ export const InvoiceBasicInfoFields = (props: ComponentProps<"fieldset">) => {
|
||||
|
||||
|
||||
return (
|
||||
<Fieldset {...props}>
|
||||
<Legend className='flex items-center gap-2 text-foreground'>
|
||||
<FileTextIcon className='size-5' /> {t("form_groups.basic_into.title")}
|
||||
<Fieldset {...props} className='border n'>
|
||||
<Legend>
|
||||
<FileTextIcon className='size-4 stroke-2' />{t("form_groups.basic_into.title")}
|
||||
</Legend>
|
||||
|
||||
<Description>{t("form_groups.basic_into.description")}</Description>
|
||||
<FieldGroup className='grid grid-cols-1 gap-x-6 xl:grid-cols-2'>
|
||||
<FieldGroup className='grid grid-cols-1'>
|
||||
<Field>
|
||||
<TextField
|
||||
className='hidden'
|
||||
control={control}
|
||||
name='invoice_number'
|
||||
readOnly
|
||||
@ -48,6 +49,17 @@ export const InvoiceBasicInfoFields = (props: ComponentProps<"fieldset">) => {
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<DatePickerInputField
|
||||
control={control}
|
||||
numberOfMonths={2}
|
||||
name='operation_date'
|
||||
label={t("form_fields.operation_date.label")}
|
||||
placeholder={t("form_fields.operation_date.placeholder")}
|
||||
description={t("form_fields.operation_date.description")}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field >
|
||||
<TextField
|
||||
typePreset='text'
|
||||
@ -59,16 +71,7 @@ export const InvoiceBasicInfoFields = (props: ComponentProps<"fieldset">) => {
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<DatePickerInputField
|
||||
control={control}
|
||||
numberOfMonths={2}
|
||||
name='operation_date'
|
||||
label={t("form_fields.operation_date.label")}
|
||||
placeholder={t("form_fields.operation_date.placeholder")}
|
||||
description={t("form_fields.operation_date.description")}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
|
||||
<Field>
|
||||
<TextField
|
||||
|
||||
@ -1,28 +1,25 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
|
||||
import { Package } from "lucide-react";
|
||||
import { Rows4Icon } from "lucide-react";
|
||||
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
import { Description, FieldGroup, Fieldset, Legend } from '@repo/rdx-ui/components';
|
||||
import { ComponentProps } from 'react';
|
||||
import { useTranslation } from '../../i18n';
|
||||
import { ItemsEditor } from "./items";
|
||||
|
||||
|
||||
export const InvoiceItems = ({ className, ...props }: ComponentProps<"div">) => {
|
||||
export const InvoiceItems = ({ className, ...props }: ComponentProps<"fieldset">) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card className={cn("border-none shadow-none", className)} {...props}>
|
||||
<CardHeader>
|
||||
<div className='flex items-center justify-between'>
|
||||
<CardTitle className='text-lg font-medium flex items-center gap-2'>
|
||||
<Package className='size-5' />
|
||||
{t('form_groups.items.title')}
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className='overflow-auto'>
|
||||
<Fieldset {...props}>
|
||||
<Legend>
|
||||
<Rows4Icon className='size-6 text-muted-foreground' />{t('form_groups.items.title')}
|
||||
</Legend>
|
||||
|
||||
<Description>{t("form_groups.items.description")}</Description>
|
||||
<FieldGroup className='grid grid-cols-1'>
|
||||
<ItemsEditor />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</FieldGroup>
|
||||
</Fieldset>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
@ -11,8 +11,8 @@ export const InvoiceNotes = (props: ComponentProps<"fieldset">) => {
|
||||
|
||||
return (
|
||||
<Fieldset {...props}>
|
||||
<Legend className='flex items-center gap-2 text-foreground'>
|
||||
<StickyNoteIcon className='size-5' /> {t("form_groups.basic_into.title")}
|
||||
<Legend>
|
||||
<StickyNoteIcon className='size-6 text-muted-foreground' />{t("form_groups.basic_into.title")}
|
||||
</Legend>
|
||||
|
||||
<Description>{t("form_groups.basic_into.description")}</Description>
|
||||
|
||||
@ -1,82 +1,150 @@
|
||||
import { formatCurrency } from '@erp/core';
|
||||
import { formatCurrency } from "@erp/core";
|
||||
import { Description, FieldGroup, Fieldset, Legend } from "@repo/rdx-ui/components";
|
||||
import { Input, Label, Separator } from "@repo/shadcn-ui/components";
|
||||
import { CalculatorIcon } from "lucide-react";
|
||||
import { ComponentProps } from 'react';
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { useInvoiceContext } from '../../context';
|
||||
import { Separator } from "@repo/shadcn-ui/components";
|
||||
import { ReceiptIcon } from "lucide-react";
|
||||
import { ComponentProps } from "react";
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
import { useInvoiceContext } from "../../context";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { InvoiceFormData } from "../../schemas";
|
||||
import { PercentageInputField } from "./items/percentage-input-field";
|
||||
|
||||
export const InvoiceTotals = (props: ComponentProps<"fieldset">) => {
|
||||
const { t } = useTranslation();
|
||||
const { control, getValues } = useFormContext<InvoiceFormData>();
|
||||
const { currency_code, language_code } = useInvoiceContext();
|
||||
const { currency_code, language_code, readOnly, taxCatalog } = useInvoiceContext();
|
||||
|
||||
const displayTaxes = useWatch({
|
||||
control,
|
||||
name: "taxes",
|
||||
defaultValue: [],
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<Fieldset {...props}>
|
||||
<Legend className='flex items-center gap-2 text-foreground'>
|
||||
<CalculatorIcon className='size-5' /> {t("form_groups.totals.title")}
|
||||
<Legend>
|
||||
<ReceiptIcon className='size-6 text-muted-foreground' />{t("form_groups.totals.title")}
|
||||
</Legend>
|
||||
|
||||
<Description>{t("form_groups.totals.description")}</Description>
|
||||
<FieldGroup className='grid grid-cols-1'>
|
||||
<div className='space-y-3'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<Label className='text-sm'>Subtotal</Label>
|
||||
<span className='font-medium tabular-nums'>{formatCurrency(getValues('subtotal_amount'), 2, currency_code, language_code)}</span>
|
||||
{/* Sección: Subtotal y Descuentos */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-semibold text-base">Subtotal</span>
|
||||
<span className="font-bold text-lg tabular-nums pr-4">
|
||||
{formatCurrency(getValues('subtotal_amount'), 2, currency_code, language_code)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between items-center gap-4'>
|
||||
<Label className='text-sm'>Descuento global (%)</Label>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Controller
|
||||
control={control}
|
||||
name={"discount_percentage"}
|
||||
render={({
|
||||
field, fieldState
|
||||
}) => (
|
||||
<Input
|
||||
readOnly={false}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
disabled={fieldState.isValidating}
|
||||
onBlur={field.onBlur}
|
||||
className='w-20 text-right'
|
||||
/>)}
|
||||
/>
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">Descuento global</h3>
|
||||
|
||||
<div className="rounded-lg bg-accent/30 p-4 space-y-2.5">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className='text-sm font-medium'>Descuento global</span>
|
||||
<PercentageInputField
|
||||
control={control}
|
||||
name={"discount_percentage"}
|
||||
readOnly={readOnly}
|
||||
inputId={"header-discount-percentage"}
|
||||
showSuffix={true}
|
||||
className='w-20 h-9 text-right tabular-nums'
|
||||
/>
|
||||
</div>
|
||||
<span className="font-medium text-destructive tabular-nums">-{formatCurrency(getValues("discount_amount"), 2, currency_code, language_code)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between items-center'>
|
||||
<Label className='text-sm'>Importe del descuento</Label>
|
||||
<span className='font-medium text-destructive tabular-nums'>
|
||||
-{formatCurrency(getValues("discount_amount"), 2, currency_code, language_code)}
|
||||
</span>
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
<Separator className='bg-muted-foreground' />
|
||||
|
||||
<div className='flex justify-between items-center'>
|
||||
<Label className='text-sm'>Base imponible</Label>
|
||||
<span className='font-medium tabular-nums'>{formatCurrency(getValues('taxable_amount'), 2, currency_code, language_code)}</span>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between items-center'>
|
||||
<Label className='text-sm'>Total de impuestos</Label>
|
||||
<span className='font-medium tabular-nums'>{formatCurrency(getValues('taxes_amount'), 2, currency_code, language_code)}</span>
|
||||
</div>
|
||||
|
||||
<Separator className='bg-muted-foreground h-0.5' />
|
||||
|
||||
<div className='flex justify-between items-center'>
|
||||
<Label className='text-xl font-semibold'>Total de la factura</Label>
|
||||
<span className='text-xl font-bold text-primary tabular-nums'>
|
||||
{formatCurrency(getValues('total_amount'), 2, currency_code, language_code)}
|
||||
{/* Sección: Base Imponible */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-semibold text-base">Base imponible</span>
|
||||
<span className="font-bold text-lg tabular-nums pr-4">
|
||||
{formatCurrency(getValues('taxable_amount'), 2, currency_code, language_code)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Sección: Impuestos */}
|
||||
<div className="space-y-1.5">
|
||||
<h3
|
||||
className="text-sm font-semibold text-muted-foreground uppercase tracking-wide"
|
||||
>
|
||||
Impuestos y retenciones
|
||||
</h3>
|
||||
|
||||
{taxCatalog.groups().map((group) => {
|
||||
// Filtra impuestos de ese grupo
|
||||
const taxesInGroup = displayTaxes?.filter((item) => {
|
||||
const tax = taxCatalog.findByCode(item.tax_code).match(
|
||||
(t) => t,
|
||||
() => undefined
|
||||
);
|
||||
return tax?.group === group;
|
||||
});
|
||||
|
||||
// Si el grupo no tiene impuestos, no renderiza nada
|
||||
if (taxesInGroup?.length === 0) return null;
|
||||
|
||||
return (
|
||||
|
||||
|
||||
<div key={`tax-group-${group}`} className="rounded-lg bg-accent/30 p-4 space-y-1.5">
|
||||
{taxesInGroup?.map((item) => {
|
||||
const tax = taxCatalog.findByCode(item.tax_code).match(
|
||||
(t) => t,
|
||||
() => undefined
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={`${group}:${item.tax_code}`}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<span className="text-sm font-medium">{tax?.name}</span>
|
||||
<span className="font-medium tabular-nums">
|
||||
{formatCurrency(
|
||||
item.taxes_amount,
|
||||
2,
|
||||
currency_code,
|
||||
language_code
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
);
|
||||
})}
|
||||
|
||||
|
||||
<div className="flex items-center justify-between text-base mt-3">
|
||||
<span className="font-semibold text-base">Total de impuestos</span>
|
||||
<span className="font-bold text-lg tabular-nums pr-4">{formatCurrency(getValues('taxes_amount'), 2, currency_code, language_code)}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<Separator className='bg-foreground' />
|
||||
<div className="space-y-1.5">
|
||||
<div className="rounded-lg bg-primary p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-lg font-bold text-background/90">Total de la factura</span>
|
||||
<span className="text-2xl font-bold text-background">{formatCurrency(getValues('total_amount'), 2, currency_code, language_code)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</Fieldset>
|
||||
</Fieldset >
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { useMoney } from '@erp/core/hooks';
|
||||
import { formatCurrency } from '@erp/core';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger,
|
||||
HoverCard, HoverCardContent, HoverCardTrigger
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
import { useInvoiceContext } from '../../../context';
|
||||
import { useTranslation } from "../../../i18n";
|
||||
|
||||
|
||||
@ -16,15 +17,25 @@ type HoverCardTotalsSummaryProps = PropsWithChildren & {
|
||||
* Muestra un desglose financiero del total de línea.
|
||||
* Lee directamente los importes del formulario vía react-hook-form.
|
||||
*/
|
||||
|
||||
|
||||
// Aparcado por ahora
|
||||
|
||||
export const HoverCardTotalsSummary = ({
|
||||
children,
|
||||
rowIndex,
|
||||
}: HoverCardTotalsSummaryProps) => <>{children}</>
|
||||
|
||||
|
||||
const HoverCardTotalsSummary2 = ({
|
||||
children,
|
||||
rowIndex,
|
||||
}: HoverCardTotalsSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { formatCurrency } = useMoney();
|
||||
const { control } = useFormContext();
|
||||
const { currency_code, language_code } = useInvoiceContext();
|
||||
|
||||
// 👀 Observar los valores actuales del formulario
|
||||
// Observar los valores actuales del formulario
|
||||
const [subtotal, discountPercentage, discountAmount, taxableBase, total] =
|
||||
useWatch({
|
||||
control,
|
||||
@ -48,7 +59,7 @@ export const HoverCardTotalsSummary = ({
|
||||
<span className="text-muted-foreground">
|
||||
{t("components.hover_card_totals_summary.fields.subtotal_amount")}:
|
||||
</span>
|
||||
<span className="font-mono">{formatCurrency(subtotal)}</span>
|
||||
<span className="font-mono tabular-nums">{formatCurrency(subtotal, 4, currency_code, language_code)}</span>
|
||||
</div>
|
||||
|
||||
{/* Descuento (si aplica) */}
|
||||
@ -65,8 +76,8 @@ export const HoverCardTotalsSummary = ({
|
||||
: 0}
|
||||
%):
|
||||
</span>
|
||||
<span className="font-mono text-destructive">
|
||||
-{formatCurrency(discountAmount)}
|
||||
<span className="font-mono tabular-nums text-destructive">
|
||||
-{formatCurrency(discountAmount, 4, currency_code, language_code)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@ -76,8 +87,8 @@ export const HoverCardTotalsSummary = ({
|
||||
<span className="text-muted-foreground">
|
||||
{t("components.hover_card_totals_summary.fields.taxable_amount")}:
|
||||
</span>
|
||||
<span className="font-mono font-medium">
|
||||
{formatCurrency(taxableBase)}
|
||||
<span className="font-mono tabular-nums font-medium">
|
||||
{formatCurrency(taxableBase, 4, currency_code, language_code)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -86,7 +97,7 @@ export const HoverCardTotalsSummary = ({
|
||||
<span>
|
||||
{t("components.hover_card_totals_summary.fields.total_amount")}:
|
||||
</span>
|
||||
<span className="font-mono">{formatCurrency(total)}</span>
|
||||
<span className="font-mono tabular-nums">{formatCurrency(total, 4, currency_code, language_code)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -103,6 +103,7 @@ export const ItemRow = ({
|
||||
data-row-index={rowIndex}
|
||||
data-col-index={4}
|
||||
data-cell-focus
|
||||
className='font-medium'
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
@ -119,6 +120,7 @@ export const ItemRow = ({
|
||||
data-row-index={rowIndex}
|
||||
data-col-index={5}
|
||||
data-cell-focus
|
||||
className='font-medium'
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
@ -133,6 +135,7 @@ export const ItemRow = ({
|
||||
data-row-index={rowIndex}
|
||||
data-col-index={6}
|
||||
data-cell-focus
|
||||
className='font-medium'
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
@ -163,6 +166,7 @@ export const ItemRow = ({
|
||||
inputId={`total-amount-${rowIndex}`}
|
||||
currencyCode={currency_code}
|
||||
languageCode={language_code}
|
||||
className='font-semibold'
|
||||
/>
|
||||
</HoverCardTotalsSummary>
|
||||
</TableCell>
|
||||
|
||||
@ -28,7 +28,7 @@ export function PageHeader({ icon, title, description, status, rightSlot, classN
|
||||
{icon && <div className='shrink-0'>{icon}</div>}
|
||||
<div>
|
||||
<div className='flex items-center gap-3'>
|
||||
<h1 className='text-2xl font-semibold text-foreground'>{title}</h1>
|
||||
<h1 className='text-xl font-semibold text-foreground'>{title}</h1>
|
||||
{status && <CustomerInvoiceStatusBadge status={status} />}
|
||||
</div>
|
||||
{description && <p className='text-sm text-muted-foreground'>{description}</p>}
|
||||
|
||||
@ -9,6 +9,7 @@ export type InvoiceContextValue = {
|
||||
language_code: string;
|
||||
is_proforma: boolean;
|
||||
|
||||
readOnly: boolean;
|
||||
taxCatalog: TaxCatalogProvider;
|
||||
|
||||
changeLanguage: (lang: string) => void;
|
||||
@ -26,18 +27,20 @@ export interface InvoiceProviderParams {
|
||||
language_code?: string; // default "es"
|
||||
currency_code?: string; // default "EUR"
|
||||
is_proforma?: boolean; // default 'true'
|
||||
readOnly?: boolean; // default 'false'
|
||||
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, company_id, status: initialStatus = "draft", language_code: initialLang = "es",
|
||||
currency_code: initialCurrency = "EUR",
|
||||
currency_code: initialCurrency = "EUR", readOnly: initialReadOnly = false,
|
||||
is_proforma: initialProforma = true, children }: PropsWithChildren<InvoiceProviderParams>) => {
|
||||
|
||||
// Estado interno local para campos dinámicos
|
||||
const [language_code, setLanguage] = useState(initialLang);
|
||||
const [currency_code, setCurrency] = useState(initialCurrency);
|
||||
const [is_proforma, setIsProforma] = useState(initialProforma);
|
||||
const [readOnly, setReadOnly] = useState(initialReadOnly);
|
||||
const [status] = useState(initialStatus);
|
||||
const [taxCatalog] = useState(initialTaxCatalog);
|
||||
|
||||
@ -45,6 +48,7 @@ export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, com
|
||||
const setLanguageMemo = useCallback((language_code: string) => setLanguage(language_code), []);
|
||||
const setCurrencyMemo = useCallback((currency_code: string) => setCurrency(currency_code), []);
|
||||
const setIsProformaMemo = useCallback((is_proforma: boolean) => setIsProforma(is_proforma), []);
|
||||
const setReadOnlyMemo = useCallback((readOnly: boolean) => setReadOnly(readOnly), []);
|
||||
|
||||
const value = useMemo<InvoiceContextValue>(() => {
|
||||
|
||||
@ -57,12 +61,14 @@ export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, com
|
||||
is_proforma,
|
||||
|
||||
taxCatalog,
|
||||
readOnly,
|
||||
|
||||
changeLanguage: setLanguageMemo,
|
||||
changeCurrency: setCurrencyMemo,
|
||||
changeIsProforma: setIsProformaMemo
|
||||
changeIsProforma: setIsProformaMemo,
|
||||
setReadOnly: setReadOnlyMemo,
|
||||
}
|
||||
}, [company_id, invoice_id, status, language_code, currency_code, is_proforma, taxCatalog, setLanguageMemo, setCurrencyMemo, setIsProformaMemo]);
|
||||
}, [readOnly, company_id, invoice_id, status, language_code, currency_code, is_proforma, taxCatalog, setLanguageMemo, setCurrencyMemo, setIsProformaMemo, setReadOnlyMemo]);
|
||||
|
||||
return <InvoiceContext.Provider value={value}>{children}</InvoiceContext.Provider>;
|
||||
};
|
||||
|
||||
@ -81,13 +81,9 @@ export function useInvoiceAutoRecalc(
|
||||
|
||||
// Observamos el formulario esperando cualquier cambio
|
||||
React.useEffect(() => {
|
||||
console.log("recalculo algo?");
|
||||
|
||||
if (!isDirty || isLoading || isSubmitting) return;
|
||||
|
||||
const subscription = watch((formData, { name, type }) => {
|
||||
console.log(name, type);
|
||||
|
||||
const items = (formData?.items || []) as InvoiceItemFormData[];
|
||||
const header_discount_percentage = formData?.discount_percentage || 0;
|
||||
|
||||
@ -119,13 +115,10 @@ export function useInvoiceAutoRecalc(
|
||||
|
||||
// 2. Cambio puntual de una línea
|
||||
if (name?.startsWith("items.") && type === "change") {
|
||||
console.log("2. items!");
|
||||
|
||||
const index = Number(name.split(".")[1]);
|
||||
const field = name.split(".")[2];
|
||||
|
||||
if (["quantity", "unit_amount", "discount_percentage", "tax_codes"].includes(field)) {
|
||||
console.log("2.1. recalculo items!");
|
||||
const item = items[index] as InvoiceItemFormData;
|
||||
const prevTotals = itemCache.current.get(index);
|
||||
const newTotals = calculateItemTotals(item, header_discount_percentage);
|
||||
@ -134,14 +127,7 @@ export function useInvoiceAutoRecalc(
|
||||
const itemHasChanges =
|
||||
!prevTotals || JSON.stringify(prevTotals) !== JSON.stringify(newTotals);
|
||||
|
||||
console.log(
|
||||
JSON.stringify(prevTotals),
|
||||
JSON.stringify(newTotals),
|
||||
itemHasChanges ? "hay cambios" : "no hay cambios"
|
||||
);
|
||||
|
||||
if (!itemHasChanges) {
|
||||
console.log("No hay cambios, me voy!!!");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -154,8 +140,6 @@ export function useInvoiceAutoRecalc(
|
||||
// Recalcular totales de factura
|
||||
const invoiceTotals = calculateInvoiceTotals(items, header_discount_percentage);
|
||||
|
||||
console.log(invoiceTotals);
|
||||
|
||||
// Estableer valores en cabecera
|
||||
setInvoiceTotals(form, invoiceTotals);
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ export const Fieldset = ({ className, children, ...props }: React.ComponentProps
|
||||
<fieldset
|
||||
data-slot='fieldset'
|
||||
className={cn(
|
||||
" *:data-[slot=text]:mt-1 [&>*+[data-slot=control]]:mt-6 bg-card rounded-xl p-6",
|
||||
"bg-card rounded-xl p-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@ -37,7 +37,7 @@ export const Legend = ({ className, children, ...props }: React.ComponentProps<"
|
||||
<div
|
||||
data-slot='legend'
|
||||
className={cn(
|
||||
"text-base/6 font-semibold data-disabled:opacity-50 sm:text-sm/6 text-foreground",
|
||||
"text-sm flex items-center gap-2 text-muted-foreground font-medium",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@ -49,7 +49,7 @@ export const Legend = ({ className, children, ...props }: React.ComponentProps<"
|
||||
export const Description = ({ className, children, ...props }: React.ComponentProps<"p">) => (
|
||||
<p
|
||||
data-slot='text'
|
||||
className={cn("text-base/6 sm:text-sm/6 text-muted-foreground", className)}
|
||||
className={cn("text-base text-muted-foreground", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -148,5 +148,6 @@ export {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
useFormField,
|
||||
useFormField
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
/*@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");*/
|
||||
/*@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=PT+Mono&family=PT+Serif:ital,wght@0,400;0,700;1,400;1,700&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto+Slab:wght@100..900&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter+Tight:ital,wght@0,100..900;1,100..900&display=swap");*/
|
||||
|
||||
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+Mono:wght@100..900&family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Noto+Serif:ital,wght@0,100..900;1,100..900&display=swap");
|
||||
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@ -40,9 +44,9 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans: "Inter", ui-sans-serif, sans-serif, system-ui;
|
||||
--font-serif: "PT Serif", ui-serif, serif;
|
||||
--font-mono: "PT Mono", ui-monospace, monospace;
|
||||
--font-sans: "Noto Sans", ui-sans-serif, sans-serif, system-ui;
|
||||
--font-serif: "Noto Serif", ui-serif, serif;
|
||||
--font-mono: "Noto Sans Mono", ui-monospace, monospace;
|
||||
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(13.636% 0.02685 282.25);
|
||||
@ -60,7 +64,7 @@
|
||||
--accent-foreground: oklch(0.3729 0.0306 259.7328);
|
||||
--destructive: oklch(0.583 0.2387 28.4765);
|
||||
--destructive-foreground: oklch(1.0 0 0);
|
||||
--border: oklch(0.9173 0.0067 286.2663);
|
||||
--border: oklch(0.9273 0.0067 286.2663);
|
||||
--input: oklch(0.9173 0.0067 286.2663);
|
||||
--ring: oklch(0.6187 0.2067 259.2316);
|
||||
--chart-1: oklch(0.6471 0.2173 36.8511);
|
||||
@ -76,7 +80,13 @@
|
||||
--sidebar-accent-foreground: oklch(0.2069 0.0098 285.5081);
|
||||
--sidebar-border: oklch(0.9173 0.0067 286.2663);
|
||||
--sidebar-ring: oklch(0.623 0.214 259.815);
|
||||
--radius: 0.25rem;
|
||||
--radius: 0.40rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 1px;
|
||||
--shadow-blur: 3px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.1;
|
||||
--shadow-color: oklch(0 0 0);
|
||||
--shadow-2xs: 1px 1px 6px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 1px 1px 6px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 1px 1px 6px 0px hsl(0 0% 0% / 0.1), 1px 1px 2px -1px hsl(0 0% 0% / 0.1);
|
||||
@ -86,7 +96,7 @@
|
||||
--shadow-xl: 1px 1px 6px 0px hsl(0 0% 0% / 0.1), 1px 8px 10px -1px hsl(0 0% 0% / 0.1);
|
||||
--shadow-2xl: 1px 1px 6px 0px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
--spacing: 0.20rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user