Facturas de cliente
This commit is contained in:
parent
1d1f412e6c
commit
645d4e8adb
@ -67,6 +67,37 @@
|
||||
"placeholder": "Select a 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": {
|
||||
"quantity": {
|
||||
"label": "Quantity",
|
||||
|
||||
@ -67,6 +67,36 @@
|
||||
"placeholder": "Seleccionar una fecha",
|
||||
"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": {
|
||||
"quantity": {
|
||||
"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
|
||||
import { AgGridReact } from "ag-grid-react";
|
||||
import { useCustomerInvoicesQuery } from "../hooks";
|
||||
import { useTranslation } from "../i18n";
|
||||
import { CustomerInvoiceStatusBadge } from "./customer-invoice-status-badge";
|
||||
|
||||
// Create new GridExample component
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./customer-invoice-prices-card";
|
||||
export * from "./customer-invoice-status-badge";
|
||||
export * from "./customer-invoices-layout";
|
||||
export * from "./customer-invoices-list-grid";
|
||||
|
||||
@ -35,6 +35,7 @@ import {
|
||||
import { format } from "date-fns";
|
||||
import { es } from "date-fns/locale";
|
||||
import { CalendarIcon, PlusIcon, Save, Trash2Icon, X } from "lucide-react";
|
||||
import { CustomerInvoicePricesCard } from "../../components";
|
||||
import { CustomerInvoiceItemsCardEditor } from "../../components/items";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerInvoiceData } from "./customer-invoice.schema";
|
||||
@ -464,6 +465,9 @@ export const CustomerInvoiceEditForm = ({
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<CustomerInvoicePricesCard />
|
||||
|
||||
{/* Configuración de Impuestos */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} 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 { format, isValid, parse } from "date-fns";
|
||||
@ -60,16 +60,23 @@ export function DatePickerInputField<TFormValues extends FieldValues>({
|
||||
|
||||
const handleInputChange = (value: string) => {
|
||||
setInputValue(value);
|
||||
setInputError(null); // Reset error on typing
|
||||
setInputError(null);
|
||||
};
|
||||
|
||||
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)) {
|
||||
field.onChange(parsed.toISOString());
|
||||
setInputError(null);
|
||||
} 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}
|
||||
/>
|
||||
<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 ? (
|
||||
<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>
|
||||
@ -128,6 +149,9 @@ export function DatePickerInputField<TFormValues extends FieldValues>({
|
||||
field.onChange(iso);
|
||||
setInputValue(formatDateFn(iso));
|
||||
setInputError(null);
|
||||
} else {
|
||||
field.onChange(undefined);
|
||||
setInputValue("");
|
||||
}
|
||||
}}
|
||||
initialFocus
|
||||
@ -136,15 +160,22 @@ export function DatePickerInputField<TFormValues extends FieldValues>({
|
||||
)}
|
||||
</Popover>
|
||||
|
||||
{isReadOnly && (
|
||||
<p className='text-xs text-muted-foreground italic mt-1 flex items-center gap-1'>
|
||||
<LockIcon className='w-3 h-3' /> {t("common.readOnly") || "Solo lectura"}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{(inputError || description) && (
|
||||
<p
|
||||
className={cn(
|
||||
"text-xs",
|
||||
inputError ? "text-destructive" : "text-muted-foreground",
|
||||
!description && !inputError && "invisible"
|
||||
"text-xs mt-1",
|
||||
inputError ? "text-destructive" : "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{inputError || description || "\u00A0"}
|
||||
{inputError || description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useTranslation } from "@repo/rdx-ui/locales/i18n.ts";
|
||||
import { JSX } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LoadingIndicator } from "./loading-indicator.tsx";
|
||||
|
||||
export type LoadingOverlayProps = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user