Edición de Items de proformas
This commit is contained in:
parent
8ca2ef1fab
commit
65c3e1f324
@ -1,3 +1,2 @@
|
||||
export * from "./items";
|
||||
export * from "./proforma-basic-info-fields";
|
||||
export * from "./proforma-totals";
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./proforma-items-editor";
|
||||
@ -1,22 +0,0 @@
|
||||
import { FieldDescription, FieldGroup, FieldLegend, FieldSet } from "@repo/shadcn-ui/components";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { useTranslation } from "../../../../../../i18n";
|
||||
import { ItemsEditor } from "../../components";
|
||||
|
||||
export const ProformaItems = (props: ComponentProps<"fieldset">) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<FieldSet {...props}>
|
||||
<FieldLegend className="hidden text-foreground" variant="label">
|
||||
{t("form_groups.items.title")}
|
||||
</FieldLegend>
|
||||
<FieldDescription className="hidden">{t("form_groups.items.description")}</FieldDescription>
|
||||
|
||||
<FieldGroup className="grid grid-cols-1">
|
||||
<ItemsEditor />
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
);
|
||||
};
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./use-update-proforma-controller";
|
||||
export * from "./use-update-proforma-items-controller";
|
||||
export * from "./use-update-proforma-page-controller";
|
||||
|
||||
@ -20,6 +20,8 @@ import {
|
||||
focusFirstProformaUpdateError,
|
||||
} from "../utils";
|
||||
|
||||
import { useUpdateProformaItemsController } from "./use-update-proforma-items-controller";
|
||||
|
||||
export interface UseUpdateProformaControllerOptions {
|
||||
onUpdated?(updated: Proforma): void;
|
||||
successToasts?: boolean;
|
||||
@ -172,11 +174,17 @@ export const useUpdateProformaController = (
|
||||
submitHandler(event);
|
||||
};
|
||||
|
||||
const itemsCtrl = useUpdateProformaItemsController({
|
||||
form,
|
||||
});
|
||||
|
||||
return {
|
||||
// form
|
||||
formId,
|
||||
form,
|
||||
|
||||
itemsCtrl,
|
||||
|
||||
// handlers del form
|
||||
onSubmit,
|
||||
resetForm,
|
||||
|
||||
@ -0,0 +1,229 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
type FieldArrayWithId,
|
||||
type UseFormReturn,
|
||||
useFieldArray,
|
||||
useWatch,
|
||||
} from "react-hook-form";
|
||||
|
||||
import type { ProformaItemUpdateForm, ProformaUpdateForm } from "../entities";
|
||||
import { buildProformaItemUpdateDefault } from "../utils/build-proforma-item-update-default";
|
||||
|
||||
export interface ProformaItemAmounts {
|
||||
subtotal: number;
|
||||
discountAmount: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ProformaItemsTotals {
|
||||
subtotal: number;
|
||||
discountAmount: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export type ProformaItemField = FieldArrayWithId<ProformaUpdateForm, "items", "fieldId">;
|
||||
|
||||
export interface UseUpdateProformaItemsControllerResult {
|
||||
fields: ProformaItemField[];
|
||||
items: ProformaItemUpdateForm[];
|
||||
|
||||
hasItems: boolean;
|
||||
itemCount: number;
|
||||
|
||||
appendItem: () => void;
|
||||
removeItem: (index: number) => void;
|
||||
duplicateItem: (index: number) => void;
|
||||
moveItemUp: (index: number) => void;
|
||||
moveItemDown: (index: number) => void;
|
||||
|
||||
getItemAmounts: (index: number) => ProformaItemAmounts;
|
||||
totals: ProformaItemsTotals;
|
||||
}
|
||||
|
||||
interface UseUpdateProformaItemsControllerParams {
|
||||
form: UseFormReturn<ProformaUpdateForm>;
|
||||
}
|
||||
|
||||
const roundCurrency = (value: number): number => {
|
||||
return Math.round(value * 100) / 100;
|
||||
};
|
||||
|
||||
const normalizeItemPositions = (items: ProformaItemUpdateForm[]): ProformaItemUpdateForm[] => {
|
||||
return items.map((item, index) => ({
|
||||
...item,
|
||||
position: index,
|
||||
}));
|
||||
};
|
||||
|
||||
const calculateItemAmounts = (
|
||||
item?: Pick<ProformaItemUpdateForm, "quantity" | "unitAmount" | "discountPercentage">
|
||||
): ProformaItemAmounts => {
|
||||
if (!item) {
|
||||
return {
|
||||
subtotal: 0,
|
||||
discountAmount: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const subtotal = roundCurrency(item.quantity * item.unitAmount);
|
||||
const discountAmount = roundCurrency(subtotal * (item.discountPercentage / 100));
|
||||
const total = roundCurrency(subtotal - discountAmount);
|
||||
|
||||
return {
|
||||
subtotal,
|
||||
discountAmount,
|
||||
total,
|
||||
};
|
||||
};
|
||||
|
||||
const calculateItemsTotals = (items: ProformaItemUpdateForm[]): ProformaItemsTotals => {
|
||||
return items.reduce<ProformaItemsTotals>(
|
||||
(acc, item) => {
|
||||
const amounts = calculateItemAmounts(item);
|
||||
|
||||
return {
|
||||
subtotal: roundCurrency(acc.subtotal + amounts.subtotal),
|
||||
discountAmount: roundCurrency(acc.discountAmount + amounts.discountAmount),
|
||||
total: roundCurrency(acc.total + amounts.total),
|
||||
};
|
||||
},
|
||||
{
|
||||
subtotal: 0,
|
||||
discountAmount: 0,
|
||||
total: 0,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const useUpdateProformaItemsController = ({
|
||||
form,
|
||||
}: UseUpdateProformaItemsControllerParams): UseUpdateProformaItemsControllerResult => {
|
||||
const { control, getValues, setValue } = form;
|
||||
|
||||
const { fields, append } = useFieldArray({
|
||||
control,
|
||||
name: "items",
|
||||
keyName: "fieldId",
|
||||
});
|
||||
|
||||
const watchedItems = useWatch({
|
||||
control,
|
||||
name: "items",
|
||||
});
|
||||
|
||||
const items = React.useMemo(() => watchedItems ?? [], [watchedItems]);
|
||||
|
||||
const replaceItems = React.useCallback(
|
||||
(nextItems: ProformaItemUpdateForm[]) => {
|
||||
setValue("items", normalizeItemPositions(nextItems), {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const appendItem = React.useCallback(() => {
|
||||
const nextPosition = getValues("items")?.length ?? 0;
|
||||
|
||||
append(buildProformaItemUpdateDefault(nextPosition), {
|
||||
shouldFocus: false,
|
||||
});
|
||||
}, [append, getValues]);
|
||||
|
||||
const removeItem = React.useCallback(
|
||||
(index: number) => {
|
||||
const currentItems = getValues("items") ?? [];
|
||||
|
||||
if (index < 0 || index >= currentItems.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
replaceItems(currentItems.filter((_, currentIndex) => currentIndex !== index));
|
||||
},
|
||||
[getValues, replaceItems]
|
||||
);
|
||||
|
||||
const duplicateItem = React.useCallback(
|
||||
(index: number) => {
|
||||
const currentItems = getValues("items") ?? [];
|
||||
const item = currentItems[index];
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const duplicatedItem: ProformaItemUpdateForm = {
|
||||
...item,
|
||||
id: buildProformaItemUpdateDefault(index + 1).id,
|
||||
position: index + 1,
|
||||
};
|
||||
|
||||
const nextItems = [...currentItems];
|
||||
nextItems.splice(index + 1, 0, duplicatedItem);
|
||||
|
||||
replaceItems(nextItems);
|
||||
},
|
||||
[getValues, replaceItems]
|
||||
);
|
||||
|
||||
const moveItemUp = React.useCallback(
|
||||
(index: number) => {
|
||||
const currentItems = getValues("items") ?? [];
|
||||
|
||||
if (index <= 0 || index >= currentItems.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextItems = [...currentItems];
|
||||
[nextItems[index - 1], nextItems[index]] = [nextItems[index], nextItems[index - 1]];
|
||||
|
||||
replaceItems(nextItems);
|
||||
},
|
||||
[getValues, replaceItems]
|
||||
);
|
||||
|
||||
const moveItemDown = React.useCallback(
|
||||
(index: number) => {
|
||||
const currentItems = getValues("items") ?? [];
|
||||
|
||||
if (index < 0 || index >= currentItems.length - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextItems = [...currentItems];
|
||||
[nextItems[index], nextItems[index + 1]] = [nextItems[index + 1], nextItems[index]];
|
||||
|
||||
replaceItems(nextItems);
|
||||
},
|
||||
[getValues, replaceItems]
|
||||
);
|
||||
|
||||
const getItemAmounts = React.useCallback(
|
||||
(index: number): ProformaItemAmounts => {
|
||||
return calculateItemAmounts(items[index]);
|
||||
},
|
||||
[items]
|
||||
);
|
||||
|
||||
const totals = React.useMemo(() => calculateItemsTotals(items), [items]);
|
||||
|
||||
return {
|
||||
fields,
|
||||
items,
|
||||
|
||||
hasItems: items.length > 0,
|
||||
itemCount: items.length,
|
||||
|
||||
appendItem,
|
||||
removeItem,
|
||||
duplicateItem,
|
||||
moveItemUp,
|
||||
moveItemDown,
|
||||
|
||||
getItemAmounts,
|
||||
totals,
|
||||
};
|
||||
};
|
||||
@ -1,9 +1,8 @@
|
||||
export interface ProformaItemUpdateForm {
|
||||
id: string;
|
||||
position: string;
|
||||
position: number;
|
||||
description: string;
|
||||
quantity: number;
|
||||
unitAmount: number;
|
||||
discountPercentage: number;
|
||||
taxes: string;
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
/**
|
||||
* Este esquema es para validar los datos de los items de proformas.
|
||||
* No tiene por qué coincidir con el shape de la entidad ni con el de la API.
|
||||
* Solo define los campos que se muestran en el editor y sus validaciones.
|
||||
*
|
||||
* Reglas:
|
||||
* - no meter transformaciones silenciosas raras en el esquema (ej: .toUpperCase())
|
||||
* - nombres en camelCase
|
||||
* - tipos orientados a UI/form
|
||||
* - sin campos de solo lectura que no se editen
|
||||
* - sin shape DTO
|
||||
* - sin detalles impuestos por el widget
|
||||
*/
|
||||
|
||||
export const ProformaItemUpdateFormSchema = z.object({
|
||||
id: z.uuid(),
|
||||
position: z.number().int().nonnegative(),
|
||||
description: z.string().trim(),
|
||||
quantity: z.number().positive(),
|
||||
unitAmount: z.number().nonnegative(),
|
||||
discountPercentage: z.number().min(0).max(100),
|
||||
});
|
||||
|
||||
export type ProformaItemUpdateForm = z.infer<typeof ProformaItemUpdateFormSchema>;
|
||||
@ -18,4 +18,6 @@ export const defaultProformaUpdateForm: ProformaUpdateForm = {
|
||||
paymentMethod: "",
|
||||
|
||||
globalDiscountPercentage: 0,
|
||||
|
||||
items: [],
|
||||
};
|
||||
|
||||
@ -13,6 +13,8 @@
|
||||
* - sin detalles impuestos por el widget
|
||||
*/
|
||||
|
||||
import type { ProformaItemUpdateForm } from "./proforma-item-update-form.schema";
|
||||
|
||||
export interface ProformaUpdateForm {
|
||||
series: string;
|
||||
|
||||
@ -32,5 +34,5 @@ export interface ProformaUpdateForm {
|
||||
|
||||
paymentMethod: string;
|
||||
|
||||
//items: ProformaItemForm[];
|
||||
items: ProformaItemUpdateForm[];
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { ProformaItemUpdateFormSchema } from "./proforma-item-update-form.schema";
|
||||
|
||||
/**
|
||||
* Este esquema es para validar los datos del formulario de actualización de proformas.
|
||||
* No tiene por qué coincidir con el shape de la entidad ni con el de la API.
|
||||
@ -32,86 +34,8 @@ export const ProformaUpdateFormSchema = z.object({
|
||||
globalDiscountPercentage: z.number().default(0),
|
||||
|
||||
paymentMethod: z.string().default(""),
|
||||
|
||||
items: z.array(ProformaItemUpdateFormSchema),
|
||||
});
|
||||
|
||||
export type ProformaUpdateFormSchemaType = z.infer<typeof ProformaUpdateFormSchema>;
|
||||
|
||||
const ProformaItemFormSchema = z.object({
|
||||
description: z.string().max(2000).optional().default(""),
|
||||
quantity: z.any(), //NumericStringSchema.optional(),
|
||||
unit_amount: z.any(), //NumericStringSchema.optional(),
|
||||
|
||||
subtotal_amount: z.any(), //z.number(),
|
||||
discount_percentage: z.any(), //NumericStringSchema.optional(),
|
||||
discount_amount: z.number(),
|
||||
taxable_amount: z.number(),
|
||||
|
||||
tax_codes: z.array(z.string()).default([]),
|
||||
|
||||
taxes_amount: z.number(),
|
||||
total_amount: z.number(),
|
||||
});
|
||||
|
||||
const ProformaFormSchema = z.object({
|
||||
invoice_number: z.string().optional(),
|
||||
series: z.string().optional(),
|
||||
|
||||
invoice_date: z.string().optional(),
|
||||
operation_date: z.string().optional(),
|
||||
|
||||
customer_id: z.string().optional(),
|
||||
recipient: z
|
||||
.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
tin: z.string().optional(),
|
||||
street: z.string().optional(),
|
||||
street2: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
province: z.string().optional(),
|
||||
postal_code: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
reference: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
|
||||
language_code: z
|
||||
.string({
|
||||
error: "El idioma es obligatorio",
|
||||
})
|
||||
.min(1, "Debe indicar un idioma")
|
||||
.toUpperCase() // asegura mayúsculas
|
||||
.default("es"),
|
||||
|
||||
currency_code: z
|
||||
.string({
|
||||
error: "La moneda es obligatoria",
|
||||
})
|
||||
.min(1, "La moneda no puede estar vacía")
|
||||
.toUpperCase() // asegura mayúsculas
|
||||
.default("EUR"),
|
||||
|
||||
taxes: z
|
||||
.array(
|
||||
z.object({
|
||||
tax_code: z.string(),
|
||||
tax_label: z.string(),
|
||||
taxable_amount: z.number(),
|
||||
taxes_amount: z.number(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
|
||||
items: z.array(ProformaItemFormSchema).optional(),
|
||||
|
||||
subtotal_amount: z.number(),
|
||||
items_discount_amount: z.number(),
|
||||
discount_percentage: z.number(),
|
||||
discount_amount: z.number(),
|
||||
taxable_amount: z.number(),
|
||||
taxes_amount: z.number(),
|
||||
total_amount: z.number(),
|
||||
});
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./items-editor";
|
||||
export * from "./proforma-basic-info-fields";
|
||||
export * from "./proforma-form-field-shell";
|
||||
export * from "./proforma-header-fields-card";
|
||||
|
||||
@ -1,18 +1,3 @@
|
||||
/** biome-ignore-all lint/complexity/noForEach: <explanation> */
|
||||
/** biome-ignore-all lint/suspicious/useIterableCallbackReturn: <explanation> */
|
||||
|
||||
import { useProformaGridColumns } from "@erp/customer-invoices/web/proformas/hooks";
|
||||
import { DataTable, useWithRowSelection } from "@repo/rdx-ui/components";
|
||||
import { useMemo } from "react";
|
||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||
|
||||
import { useProformaAutoRecalc } from "../../../../../../hooks";
|
||||
import { useTranslation } from "../../../../../../i18n";
|
||||
import { type ProformaFormData, defaultProformaItemFormData } from "../../../../../types";
|
||||
import { useProformaContext } from "../../../context";
|
||||
|
||||
import { ItemRowEditor } from "./item-row-editor";
|
||||
|
||||
const createEmptyItem = () => defaultProformaItemFormData;
|
||||
|
||||
export const ItemsEditor = () => {
|
||||
@ -7,20 +7,24 @@ import { ProformaUpdateRecipientEditor } from ".";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { Proforma } from "../../../shared/entities";
|
||||
import type { UseUpdateProformaItemsControllerResult } from "../../controllers";
|
||||
|
||||
import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor";
|
||||
import { ProformaUpdateItemsEditor } from "./proforma-update-items-editor";
|
||||
|
||||
type ProformaUpdateEditorProps = {
|
||||
formId: string;
|
||||
proforma?: Proforma;
|
||||
isSubmitting: boolean;
|
||||
onSubmit: React.FormEventHandler<HTMLFormElement>;
|
||||
onSubmit: React.SubmitEventHandler<HTMLFormElement>;
|
||||
onReset: () => void;
|
||||
|
||||
selectedCustomer?: CustomerSelectionOption | null;
|
||||
onChangeCustomerClick: () => void;
|
||||
onCreateCustomerClick: () => void;
|
||||
|
||||
itemsCtrl: UseUpdateProformaItemsControllerResult;
|
||||
|
||||
className?: string;
|
||||
};
|
||||
|
||||
@ -32,6 +36,7 @@ export const ProformaUpdateEditorForm = ({
|
||||
selectedCustomer,
|
||||
onChangeCustomerClick,
|
||||
onCreateCustomerClick,
|
||||
itemsCtrl,
|
||||
className,
|
||||
}: ProformaUpdateEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
@ -47,6 +52,8 @@ export const ProformaUpdateEditorForm = ({
|
||||
selectedCustomer={selectedCustomer}
|
||||
/>
|
||||
|
||||
<ProformaUpdateItemsEditor disabled={isSubmitting} itemsCtrl={itemsCtrl} />
|
||||
|
||||
<div className="flex flex-col-reverse gap-3 border-t pt-4 sm:flex-row sm:justify-end">
|
||||
<Button disabled={isSubmitting} onClick={onReset} type="button" variant="outline">
|
||||
{t("common.reset", "Restablecer")}
|
||||
|
||||
@ -0,0 +1,107 @@
|
||||
import { TextField } from "@repo/rdx-ui/components";
|
||||
import { Button, Card, CardContent } from "@repo/shadcn-ui/components";
|
||||
import { CopyIcon, MoveDownIcon, MoveUpIcon, Trash2Icon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { ProformaItemAmounts } from "../../controllers/use-update-proforma-items-controller";
|
||||
|
||||
interface ProformaUpdateItemRowEditorProps {
|
||||
index: number;
|
||||
amounts: ProformaItemAmounts;
|
||||
|
||||
canMoveUp: boolean;
|
||||
canMoveDown: boolean;
|
||||
|
||||
onRemove: () => void;
|
||||
onDuplicate: () => void;
|
||||
onMoveUp: () => void;
|
||||
onMoveDown: () => void;
|
||||
}
|
||||
|
||||
export const ProformaUpdateItemRowEditor = ({
|
||||
index,
|
||||
amounts,
|
||||
canMoveUp,
|
||||
canMoveDown,
|
||||
onRemove,
|
||||
onDuplicate,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
}: ProformaUpdateItemRowEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="space-y-4 pt-6">
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<TextField
|
||||
className="col-span-12"
|
||||
label={t("form_fields.items.description.label", "Descripción")}
|
||||
name={`items.${index}.description`}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="col-span-12 md:col-span-3"
|
||||
label={t("form_fields.items.quantity.label", "Cantidad")}
|
||||
name={`items.${index}.quantity`}
|
||||
typePreset="number"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="col-span-12 md:col-span-3"
|
||||
label={t("form_fields.items.unit_amount.label", "Importe unitario")}
|
||||
name={`items.${index}.unitAmount`}
|
||||
typePreset="number"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="col-span-12 md:col-span-3"
|
||||
label={t("form_fields.items.discount_percentage.label", "Descuento %")}
|
||||
name={`items.${index}.discountPercentage`}
|
||||
typePreset="number"
|
||||
/>
|
||||
|
||||
<div className="col-span-12 md:col-span-3 rounded-md border p-3 text-sm">
|
||||
<div className="flex justify-between gap-3">
|
||||
<span>{t("form_fields.items.subtotal.label", "Subtotal")}</span>
|
||||
<span>{amounts.subtotal}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-3">
|
||||
<span>{t("form_fields.items.discount_amount.label", "Descuento")}</span>
|
||||
<span>{amounts.discountAmount}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-3 font-medium">
|
||||
<span>{t("form_fields.items.total.label", "Total")}</span>
|
||||
<span>{amounts.total}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<Button disabled={!canMoveUp} onClick={onMoveUp} type="button" variant="outline">
|
||||
<MoveUpIcon />
|
||||
{t("common.move_up", "Subir")}
|
||||
</Button>
|
||||
|
||||
<Button disabled={!canMoveDown} onClick={onMoveDown} type="button" variant="outline">
|
||||
<MoveDownIcon />
|
||||
{t("common.move_down", "Bajar")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={onDuplicate} type="button" variant="outline">
|
||||
<CopyIcon />
|
||||
{t("common.duplicate", "Duplicar")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={onRemove} type="button" variant="destructive">
|
||||
<Trash2Icon />
|
||||
{t("common.remove", "Eliminar")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,62 @@
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { UseUpdateProformaItemsControllerResult } from "../../controllers/use-update-proforma-items-controller";
|
||||
import { ProformaSectionCard } from "../blocks";
|
||||
|
||||
import { ProformaUpdateItemRowEditor } from "./proforma-update-item-row-editor";
|
||||
import { ProformaUpdateItemsTotals } from "./proforma-update-items-totals";
|
||||
|
||||
interface ProformaUpdateItemsEditorProps extends ComponentProps<"fieldset"> {
|
||||
itemsCtrl: UseUpdateProformaItemsControllerResult;
|
||||
}
|
||||
|
||||
export const ProformaUpdateItemsEditor = ({
|
||||
itemsCtrl,
|
||||
disabled,
|
||||
...props
|
||||
}: ProformaUpdateItemsEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ProformaSectionCard
|
||||
description={t("form_groups.items.description")}
|
||||
title={t("form_groups.items.title")}
|
||||
>
|
||||
<fieldset className="space-y-4" disabled={disabled} {...props}>
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={itemsCtrl.appendItem} type="button" variant="outline">
|
||||
<PlusIcon />
|
||||
{t("common.add", "Añadir")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{itemsCtrl.hasItems ? (
|
||||
<div className="space-y-4">
|
||||
{itemsCtrl.fields.map((field, index) => (
|
||||
<ProformaUpdateItemRowEditor
|
||||
amounts={itemsCtrl.getItemAmounts(index)}
|
||||
canMoveDown={index < itemsCtrl.itemCount - 1}
|
||||
canMoveUp={index > 0}
|
||||
index={index}
|
||||
key={field.fieldId}
|
||||
onDuplicate={() => itemsCtrl.duplicateItem(index)}
|
||||
onMoveDown={() => itemsCtrl.moveItemDown(index)}
|
||||
onMoveUp={() => itemsCtrl.moveItemUp(index)}
|
||||
onRemove={() => itemsCtrl.removeItem(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("form_groups.items.empty", "Todavía no hay líneas en la proforma.")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ProformaUpdateItemsTotals totals={itemsCtrl.totals} />
|
||||
</fieldset>
|
||||
</ProformaSectionCard>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { ProformaItemsTotals } from "../../controllers";
|
||||
|
||||
interface ProformaUpdateItemsTotalsProps {
|
||||
totals: ProformaItemsTotals;
|
||||
}
|
||||
|
||||
export const ProformaUpdateItemsTotals = ({ totals }: ProformaUpdateItemsTotalsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="ml-auto w-full max-w-md space-y-2 rounded-lg border p-4 text-sm">
|
||||
<div className="flex justify-between gap-3">
|
||||
<span>{t("form_groups.items.totals.subtotal", "Subtotal")}</span>
|
||||
<span>{totals.subtotal}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-3">
|
||||
<span>{t("form_groups.items.totals.discount", "Descuento")}</span>
|
||||
<span>{totals.discountAmount}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-3 text-base font-semibold">
|
||||
<span>{t("form_groups.items.totals.total", "Total")}</span>
|
||||
<span>{totals.total}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -101,6 +101,7 @@ export const ProformaUpdatePage = () => {
|
||||
<ProformaUpdateEditorForm
|
||||
formId={updateCtrl.formId}
|
||||
isSubmitting={updateCtrl.isUpdating}
|
||||
itemsCtrl={updateCtrl.itemsCtrl}
|
||||
onChangeCustomerClick={selectCustomerCtrl.selectCtrl.openDialog}
|
||||
//onCreateCustomerClick={selectCustomerCtrl.createCtrl.openDialog}
|
||||
onCreateCustomerClick={() => null}
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
|
||||
import type { ProformaItemUpdateForm } from "../entities";
|
||||
|
||||
export const buildProformaItemUpdateDefault = (position: number): ProformaItemUpdateForm => {
|
||||
return {
|
||||
id: UniqueID.generateNewID().toString(),
|
||||
position,
|
||||
description: "",
|
||||
quantity: 1,
|
||||
unitAmount: 0,
|
||||
discountPercentage: 0,
|
||||
};
|
||||
};
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./build-proforma-item-update-default";
|
||||
export * from "./build-proforma-update-patch";
|
||||
export * from "./build-update-proforma-by-id-params";
|
||||
export * from "./focus-first-proforma-update-error";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user