Facturas de cliente
This commit is contained in:
parent
1d1f412e6c
commit
645d4e8adb
@ -67,6 +67,37 @@
|
|||||||
"placeholder": "Select a date",
|
"placeholder": "Select a date",
|
||||||
"description": "Invoice operation date"
|
"description": "Invoice operation date"
|
||||||
},
|
},
|
||||||
|
"subtotal_price": {
|
||||||
|
"label": "Subtotal",
|
||||||
|
"placeholder": "",
|
||||||
|
"desc": "Invoice subtotal"
|
||||||
|
},
|
||||||
|
"discount": {
|
||||||
|
"label": "Discount (%)",
|
||||||
|
"placeholder": "",
|
||||||
|
"desc": "Percentage discount"
|
||||||
|
},
|
||||||
|
"discount_price": {
|
||||||
|
"label": "Discount price",
|
||||||
|
"placeholder": "",
|
||||||
|
"desc": "Percentage discount price"
|
||||||
|
},
|
||||||
|
"tax": {
|
||||||
|
"label": "Tax (%)",
|
||||||
|
"placeholder": "",
|
||||||
|
"desc": "Percentage Tax"
|
||||||
|
},
|
||||||
|
"tax_price": {
|
||||||
|
"label": "Tax price",
|
||||||
|
"placeholder": "",
|
||||||
|
"desc": "Percentage tax price"
|
||||||
|
},
|
||||||
|
"total_price": {
|
||||||
|
"label": "Total price",
|
||||||
|
"placeholder": "",
|
||||||
|
"desc": "Invoice total price"
|
||||||
|
},
|
||||||
|
|
||||||
"items": {
|
"items": {
|
||||||
"quantity": {
|
"quantity": {
|
||||||
"label": "Quantity",
|
"label": "Quantity",
|
||||||
|
|||||||
@ -67,6 +67,36 @@
|
|||||||
"placeholder": "Seleccionar una fecha",
|
"placeholder": "Seleccionar una fecha",
|
||||||
"description": "Fecha de intervención de los trabajos"
|
"description": "Fecha de intervención de los trabajos"
|
||||||
},
|
},
|
||||||
|
"subtotal_price": {
|
||||||
|
"label": "Subtotal",
|
||||||
|
"placeholder": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"discount": {
|
||||||
|
"label": "Dto (%)",
|
||||||
|
"placeholder": "",
|
||||||
|
"description": "Porcentaje de descuento"
|
||||||
|
},
|
||||||
|
"discount_price": {
|
||||||
|
"label": "Imp. descuento",
|
||||||
|
"placeholder": "",
|
||||||
|
"desc": "Importe del descuento"
|
||||||
|
},
|
||||||
|
"tax": {
|
||||||
|
"label": "IVA (%)",
|
||||||
|
"placeholder": "",
|
||||||
|
"desc": "Porcentaje de IVA"
|
||||||
|
},
|
||||||
|
"tax_price": {
|
||||||
|
"label": "Imp. IVA",
|
||||||
|
"placeholder": "",
|
||||||
|
"desc": "Importe del IVA"
|
||||||
|
},
|
||||||
|
"total_price": {
|
||||||
|
"label": "Imp. total",
|
||||||
|
"placeholder": "",
|
||||||
|
"description": "Importe total con el descuento ya aplicado"
|
||||||
|
},
|
||||||
"items": {
|
"items": {
|
||||||
"quantity": {
|
"quantity": {
|
||||||
"label": "Cantidad",
|
"label": "Cantidad",
|
||||||
|
|||||||
@ -0,0 +1,88 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
Separator,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import { useTranslation } from "../i18n";
|
||||||
|
import { formatCurrency } from "../pages/create/utils";
|
||||||
|
|
||||||
|
export const CustomerInvoicePricesCard = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { register, formState, control, watch } = useFormContext();
|
||||||
|
|
||||||
|
/*const pricesWatch = useWatch({ control, name: ["subtotal_price", "discount", "tax"] });
|
||||||
|
|
||||||
|
const totals = calculateQuoteTotals(pricesWatch);
|
||||||
|
|
||||||
|
const subtotal_price = formatNumber(totals.subtotalPrice);
|
||||||
|
const discount_price = formatNumber(totals.discountPrice);
|
||||||
|
const tax_price = formatNumber(totals.taxesPrice);
|
||||||
|
const total_price = formatNumber(totals.totalPrice);*/
|
||||||
|
|
||||||
|
const currency_symbol = watch("currency");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Impuestos y Totales</CardTitle>
|
||||||
|
<CardDescription>Configuración de impuestos y resumen de totales</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className='flex flex-row items-end gap-2 p-4'>
|
||||||
|
<div className='grid flex-1 h-16 grid-cols-1 auto-rows-max'>
|
||||||
|
<div className='grid gap-1 font-semibold text-right text-muted-foreground'>
|
||||||
|
<CardDescription className='text-sm'>
|
||||||
|
{t("form_fields.subtotal_price.label")}
|
||||||
|
</CardDescription>
|
||||||
|
<CardTitle className='flex items-baseline justify-end text-2xl tabular-nums'>
|
||||||
|
{formatCurrency(watch("subtotal_price.amount"), 2, watch("currency"))}
|
||||||
|
</CardTitle>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator orientation='vertical' className='w-px h-16 mx-2' />
|
||||||
|
<div className='grid flex-1 h-16 grid-cols-2 gap-6 auto-rows-max'>
|
||||||
|
<div className='grid gap-1 font-medium text-muted-foreground'>
|
||||||
|
<CardDescription className='text-sm'>{t("form_fields.discount.label")}</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className='grid gap-1 font-semibold text-muted-foreground'>
|
||||||
|
<CardDescription className='text-sm text-right'>
|
||||||
|
{t("form_fields.discount_price.label")}
|
||||||
|
</CardDescription>
|
||||||
|
<CardTitle className='flex items-baseline justify-end text-2xl tabular-nums'>
|
||||||
|
{"-"} {formatCurrency(watch("discount_price.amount"), 2, watch("currency"))}
|
||||||
|
</CardTitle>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator orientation='vertical' className='w-px h-16 mx-2' />
|
||||||
|
<div className='grid flex-1 h-16 grid-cols-2 gap-6 auto-rows-max'>
|
||||||
|
<div className='grid gap-1 font-medium text-muted-foreground'>
|
||||||
|
<CardDescription className='text-sm'>{t("form_fields.tax.label")}</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className='grid gap-1 font-semibold text-muted-foreground'>
|
||||||
|
<CardDescription className='text-sm text-right'>
|
||||||
|
{t("form_fields.tax_price.label")}
|
||||||
|
</CardDescription>
|
||||||
|
<CardTitle className='flex items-baseline justify-end gap-1 text-2xl tabular-nums'>
|
||||||
|
{formatCurrency(watch("tax_price.amount"), 2, watch("currency"))}
|
||||||
|
</CardTitle>
|
||||||
|
</div>
|
||||||
|
</div>{" "}
|
||||||
|
<Separator orientation='vertical' className='w-px h-16 mx-2' />
|
||||||
|
<div className='grid flex-1 h-16 grid-cols-1 auto-rows-max'>
|
||||||
|
<div className='grid gap-0'>
|
||||||
|
<CardDescription className='text-sm font-semibold text-right text-foreground'>
|
||||||
|
{t("form_fields.total_price.label")}
|
||||||
|
</CardDescription>
|
||||||
|
<CardTitle className='flex items-baseline justify-end gap-1 text-3xl tabular-nums'>
|
||||||
|
{formatCurrency(watch("total_price.amount"), 2, watch("currency"))}
|
||||||
|
</CardTitle>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -12,6 +12,7 @@ import { formatDate, formatMoney } from "@erp/core/client";
|
|||||||
// Core CSS
|
// Core CSS
|
||||||
import { AgGridReact } from "ag-grid-react";
|
import { AgGridReact } from "ag-grid-react";
|
||||||
import { useCustomerInvoicesQuery } from "../hooks";
|
import { useCustomerInvoicesQuery } from "../hooks";
|
||||||
|
import { useTranslation } from "../i18n";
|
||||||
import { CustomerInvoiceStatusBadge } from "./customer-invoice-status-badge";
|
import { CustomerInvoiceStatusBadge } from "./customer-invoice-status-badge";
|
||||||
|
|
||||||
// Create new GridExample component
|
// Create new GridExample component
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./customer-invoice-prices-card";
|
||||||
export * from "./customer-invoice-status-badge";
|
export * from "./customer-invoice-status-badge";
|
||||||
export * from "./customer-invoices-layout";
|
export * from "./customer-invoices-layout";
|
||||||
export * from "./customer-invoices-list-grid";
|
export * from "./customer-invoices-list-grid";
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import {
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { es } from "date-fns/locale";
|
import { es } from "date-fns/locale";
|
||||||
import { CalendarIcon, PlusIcon, Save, Trash2Icon, X } from "lucide-react";
|
import { CalendarIcon, PlusIcon, Save, Trash2Icon, X } from "lucide-react";
|
||||||
|
import { CustomerInvoicePricesCard } from "../../components";
|
||||||
import { CustomerInvoiceItemsCardEditor } from "../../components/items";
|
import { CustomerInvoiceItemsCardEditor } from "../../components/items";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { CustomerInvoiceData } from "./customer-invoice.schema";
|
import { CustomerInvoiceData } from "./customer-invoice.schema";
|
||||||
@ -464,6 +465,9 @@ export const CustomerInvoiceEditForm = ({
|
|||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<CustomerInvoicePricesCard />
|
||||||
|
|
||||||
{/* Configuración de Impuestos */}
|
{/* Configuración de Impuestos */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { CalendarIcon, LockIcon } from "lucide-react";
|
import { CalendarIcon, LockIcon, XIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import { format, isValid, parse } from "date-fns";
|
import { format, isValid, parse } from "date-fns";
|
||||||
@ -60,16 +60,23 @@ export function DatePickerInputField<TFormValues extends FieldValues>({
|
|||||||
|
|
||||||
const handleInputChange = (value: string) => {
|
const handleInputChange = (value: string) => {
|
||||||
setInputValue(value);
|
setInputValue(value);
|
||||||
setInputError(null); // Reset error on typing
|
setInputError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateAndSetDate = () => {
|
const validateAndSetDate = () => {
|
||||||
const parsed = parse(inputValue, parseDateFormat, new Date());
|
const trimmed = inputValue.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
field.onChange(undefined);
|
||||||
|
setInputError(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parse(trimmed, parseDateFormat, new Date());
|
||||||
if (isValid(parsed)) {
|
if (isValid(parsed)) {
|
||||||
field.onChange(parsed.toISOString());
|
field.onChange(parsed.toISOString());
|
||||||
setInputError(null);
|
setInputError(null);
|
||||||
} else {
|
} else {
|
||||||
setInputError(t("common.invalid_date") || "Fecha no válida");
|
setInputError(t("common.invalidDate") || "Fecha no válida");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -106,11 +113,25 @@ export function DatePickerInputField<TFormValues extends FieldValues>({
|
|||||||
)}
|
)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
<div className='absolute inset-y-0 right-2 flex items-center pointer-events-none'>
|
<div className='absolute inset-y-0 right-2 flex items-center gap-2 pr-1'>
|
||||||
|
{!isReadOnly && !required && inputValue && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={() => {
|
||||||
|
setInputValue("");
|
||||||
|
field.onChange(undefined);
|
||||||
|
setInputError(null);
|
||||||
|
}}
|
||||||
|
aria-label={t("common.clearDate") || "Limpiar fecha"}
|
||||||
|
className='text-muted-foreground hover:text-foreground focus:outline-none'
|
||||||
|
>
|
||||||
|
<XIcon className='w-4 h-4' />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{isReadOnly ? (
|
{isReadOnly ? (
|
||||||
<LockIcon className='h-4 w-4 text-muted-foreground' />
|
<LockIcon className='w-4 h-4 text-muted-foreground' />
|
||||||
) : (
|
) : (
|
||||||
<CalendarIcon className='h-4 w-4 text-muted-foreground' />
|
<CalendarIcon className='w-4 h-4 text-muted-foreground' />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -128,6 +149,9 @@ export function DatePickerInputField<TFormValues extends FieldValues>({
|
|||||||
field.onChange(iso);
|
field.onChange(iso);
|
||||||
setInputValue(formatDateFn(iso));
|
setInputValue(formatDateFn(iso));
|
||||||
setInputError(null);
|
setInputError(null);
|
||||||
|
} else {
|
||||||
|
field.onChange(undefined);
|
||||||
|
setInputValue("");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
initialFocus
|
initialFocus
|
||||||
@ -136,15 +160,22 @@ export function DatePickerInputField<TFormValues extends FieldValues>({
|
|||||||
)}
|
)}
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<p
|
{isReadOnly && (
|
||||||
className={cn(
|
<p className='text-xs text-muted-foreground italic mt-1 flex items-center gap-1'>
|
||||||
"text-xs",
|
<LockIcon className='w-3 h-3' /> {t("common.readOnly") || "Solo lectura"}
|
||||||
inputError ? "text-destructive" : "text-muted-foreground",
|
</p>
|
||||||
!description && !inputError && "invisible"
|
)}
|
||||||
)}
|
|
||||||
>
|
{(inputError || description) && (
|
||||||
{inputError || description || "\u00A0"}
|
<p
|
||||||
</p>
|
className={cn(
|
||||||
|
"text-xs mt-1",
|
||||||
|
inputError ? "text-destructive" : "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{inputError || description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
import { useTranslation } from "@repo/rdx-ui/locales/i18n.ts";
|
||||||
import { JSX } from "react";
|
import { JSX } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { LoadingIndicator } from "./loading-indicator.tsx";
|
import { LoadingIndicator } from "./loading-indicator.tsx";
|
||||||
|
|
||||||
export type LoadingOverlayProps = {
|
export type LoadingOverlayProps = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user