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-basic-info-fields";
|
||||||
export * from "./proforma-totals";
|
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-controller";
|
||||||
|
export * from "./use-update-proforma-items-controller";
|
||||||
export * from "./use-update-proforma-page-controller";
|
export * from "./use-update-proforma-page-controller";
|
||||||
|
|||||||
@ -20,6 +20,8 @@ import {
|
|||||||
focusFirstProformaUpdateError,
|
focusFirstProformaUpdateError,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
|
import { useUpdateProformaItemsController } from "./use-update-proforma-items-controller";
|
||||||
|
|
||||||
export interface UseUpdateProformaControllerOptions {
|
export interface UseUpdateProformaControllerOptions {
|
||||||
onUpdated?(updated: Proforma): void;
|
onUpdated?(updated: Proforma): void;
|
||||||
successToasts?: boolean;
|
successToasts?: boolean;
|
||||||
@ -172,11 +174,17 @@ export const useUpdateProformaController = (
|
|||||||
submitHandler(event);
|
submitHandler(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const itemsCtrl = useUpdateProformaItemsController({
|
||||||
|
form,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// form
|
// form
|
||||||
formId,
|
formId,
|
||||||
form,
|
form,
|
||||||
|
|
||||||
|
itemsCtrl,
|
||||||
|
|
||||||
// handlers del form
|
// handlers del form
|
||||||
onSubmit,
|
onSubmit,
|
||||||
resetForm,
|
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 {
|
export interface ProformaItemUpdateForm {
|
||||||
id: string;
|
id: string;
|
||||||
position: string;
|
position: number;
|
||||||
description: string;
|
description: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
unitAmount: number;
|
unitAmount: number;
|
||||||
discountPercentage: 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: "",
|
paymentMethod: "",
|
||||||
|
|
||||||
globalDiscountPercentage: 0,
|
globalDiscountPercentage: 0,
|
||||||
|
|
||||||
|
items: [],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,6 +13,8 @@
|
|||||||
* - sin detalles impuestos por el widget
|
* - sin detalles impuestos por el widget
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { ProformaItemUpdateForm } from "./proforma-item-update-form.schema";
|
||||||
|
|
||||||
export interface ProformaUpdateForm {
|
export interface ProformaUpdateForm {
|
||||||
series: string;
|
series: string;
|
||||||
|
|
||||||
@ -32,5 +34,5 @@ export interface ProformaUpdateForm {
|
|||||||
|
|
||||||
paymentMethod: string;
|
paymentMethod: string;
|
||||||
|
|
||||||
//items: ProformaItemForm[];
|
items: ProformaItemUpdateForm[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { z } from "zod/v4";
|
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.
|
* 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.
|
* 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),
|
globalDiscountPercentage: z.number().default(0),
|
||||||
|
|
||||||
paymentMethod: z.string().default(""),
|
paymentMethod: z.string().default(""),
|
||||||
|
|
||||||
|
items: z.array(ProformaItemUpdateFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ProformaUpdateFormSchemaType = z.infer<typeof ProformaUpdateFormSchema>;
|
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-basic-info-fields";
|
||||||
export * from "./proforma-form-field-shell";
|
export * from "./proforma-form-field-shell";
|
||||||
export * from "./proforma-header-fields-card";
|
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;
|
const createEmptyItem = () => defaultProformaItemFormData;
|
||||||
|
|
||||||
export const ItemsEditor = () => {
|
export const ItemsEditor = () => {
|
||||||
@ -7,20 +7,24 @@ import { ProformaUpdateRecipientEditor } from ".";
|
|||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type { Proforma } from "../../../shared/entities";
|
import type { Proforma } from "../../../shared/entities";
|
||||||
|
import type { UseUpdateProformaItemsControllerResult } from "../../controllers";
|
||||||
|
|
||||||
import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor";
|
import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor";
|
||||||
|
import { ProformaUpdateItemsEditor } from "./proforma-update-items-editor";
|
||||||
|
|
||||||
type ProformaUpdateEditorProps = {
|
type ProformaUpdateEditorProps = {
|
||||||
formId: string;
|
formId: string;
|
||||||
proforma?: Proforma;
|
proforma?: Proforma;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
onSubmit: React.FormEventHandler<HTMLFormElement>;
|
onSubmit: React.SubmitEventHandler<HTMLFormElement>;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
|
|
||||||
selectedCustomer?: CustomerSelectionOption | null;
|
selectedCustomer?: CustomerSelectionOption | null;
|
||||||
onChangeCustomerClick: () => void;
|
onChangeCustomerClick: () => void;
|
||||||
onCreateCustomerClick: () => void;
|
onCreateCustomerClick: () => void;
|
||||||
|
|
||||||
|
itemsCtrl: UseUpdateProformaItemsControllerResult;
|
||||||
|
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -32,6 +36,7 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
selectedCustomer,
|
selectedCustomer,
|
||||||
onChangeCustomerClick,
|
onChangeCustomerClick,
|
||||||
onCreateCustomerClick,
|
onCreateCustomerClick,
|
||||||
|
itemsCtrl,
|
||||||
className,
|
className,
|
||||||
}: ProformaUpdateEditorProps) => {
|
}: ProformaUpdateEditorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -47,6 +52,8 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
selectedCustomer={selectedCustomer}
|
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">
|
<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">
|
<Button disabled={isSubmitting} onClick={onReset} type="button" variant="outline">
|
||||||
{t("common.reset", "Restablecer")}
|
{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
|
<ProformaUpdateEditorForm
|
||||||
formId={updateCtrl.formId}
|
formId={updateCtrl.formId}
|
||||||
isSubmitting={updateCtrl.isUpdating}
|
isSubmitting={updateCtrl.isUpdating}
|
||||||
|
itemsCtrl={updateCtrl.itemsCtrl}
|
||||||
onChangeCustomerClick={selectCustomerCtrl.selectCtrl.openDialog}
|
onChangeCustomerClick={selectCustomerCtrl.selectCtrl.openDialog}
|
||||||
//onCreateCustomerClick={selectCustomerCtrl.createCtrl.openDialog}
|
//onCreateCustomerClick={selectCustomerCtrl.createCtrl.openDialog}
|
||||||
onCreateCustomerClick={() => null}
|
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-proforma-update-patch";
|
||||||
export * from "./build-update-proforma-by-id-params";
|
export * from "./build-update-proforma-by-id-params";
|
||||||
export * from "./focus-first-proforma-update-error";
|
export * from "./focus-first-proforma-update-error";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user