diff --git a/modules/customer-invoices/src/web/customer-invoice-routes.tsx b/modules/customer-invoices/src/web/customer-invoice-routes.tsx index e74eee7e..e27fc6b1 100644 --- a/modules/customer-invoices/src/web/customer-invoice-routes.tsx +++ b/modules/customer-invoices/src/web/customer-invoice-routes.tsx @@ -10,9 +10,9 @@ const ProformasListPage = lazy(() => import("./proformas/list").then((m) => ({ default: m.ListProformasPage })) ); -/*const ProformasCreatePage = lazy(() => +const ProformasCreatePage = lazy(() => import("./proformas/create").then((m) => ({ default: m.ProformaCreatePage })) -);*/ +); const ProformaUpdatePage = lazy(() => import("./proformas/update").then((m) => ({ default: m.ProformaUpdatePage })) @@ -51,6 +51,19 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] ], }, + { + path: "proformas/create", + handle: { + layout: "app-fullscreen", + protected: true, + }, + element: ( + + + + ), + }, + { path: "proformas/:id/edit", handle: { diff --git a/modules/customer-invoices/src/web/proformas/create.bak.no-vale/entities/index.ts b/modules/customer-invoices/src/web/proformas/create.bak.no-vale/entities/index.ts new file mode 100644 index 00000000..a1605cb3 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create.bak.no-vale/entities/index.ts @@ -0,0 +1,3 @@ +export * from "./proforma-create-form.entity"; +export * from "./proforma-create-form.schema"; +export * from "./proforma-create-form-default.entity"; diff --git a/modules/customer-invoices/src/web/proformas/create/entities/proforma-create-form-default.entity.ts b/modules/customer-invoices/src/web/proformas/create.bak.no-vale/entities/proforma-create-form-default.entity.ts similarity index 100% rename from modules/customer-invoices/src/web/proformas/create/entities/proforma-create-form-default.entity.ts rename to modules/customer-invoices/src/web/proformas/create.bak.no-vale/entities/proforma-create-form-default.entity.ts diff --git a/modules/customer-invoices/src/web/proformas/create.bak.no-vale/entities/proforma-create-form.entity.ts b/modules/customer-invoices/src/web/proformas/create.bak.no-vale/entities/proforma-create-form.entity.ts new file mode 100644 index 00000000..f936f93a --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create.bak.no-vale/entities/proforma-create-form.entity.ts @@ -0,0 +1,37 @@ +/** + * ProformaCreateForm representa el shape de datos del formulario de creación de proforma. + * Es decir, los campos que se muestran en el formulario y que el usuario puede editar. + * + * Este shape es específico para la UI y no tiene por qué coincidir + * con el shape del dominio ni con el de la API. + * + * Debe cumplir las siguientes reglas: + * - 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 + */ + +import type { ProformaItemForm } from "../../shared/entities"; + +export interface ProformaCreateForm { + series: string; + + invoiceDate: string; + operationDate: string; + + customerId: string; + + reference: string; + notes: string; + + languageCode: string; + currencyCode: string; + + globalDiscountPercentage: number; + + paymentMethod: string; + + items: ProformaItemForm[]; +} diff --git a/modules/customer-invoices/src/web/proformas/create.bak.no-vale/entities/proforma-create-form.schema.ts b/modules/customer-invoices/src/web/proformas/create.bak.no-vale/entities/proforma-create-form.schema.ts new file mode 100644 index 00000000..c21cdcb2 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create.bak.no-vale/entities/proforma-create-form.schema.ts @@ -0,0 +1,48 @@ +import { z } from "zod/v4"; + +/** + * Este esquema es para validar los datos del formulario de creación de cliente. + * 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 formulario 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 CustomerCreateFormSchema = z.object({ + reference: z.string(), + isCompany: z.boolean(), + name: z.string().min(1, "El nombre es obligatorio"), + tradeName: z.string(), + tin: z.string(), + + defaultTaxes: z.array(z.string()), + + street: z.string(), + street2: z.string(), + city: z.string(), + province: z.string(), + postalCode: z.string(), + country: z.string().min(1, "El país es obligatorio"), + + primaryEmail: z.email("Email inválido"), + secondaryEmail: z.email("Email inválido"), + + primaryPhone: z.string(), + secondaryPhone: z.string(), + primaryMobile: z.string(), + secondaryMobile: z.string(), + + fax: z.string(), + website: z.url("URL inválida"), + + legalRecord: z.string(), + + languageCode: z.string().min(1, "El idioma es obligatorio"), + currencyCode: z.string().min(1, "La moneda es obligatoria"), +}); diff --git a/modules/customer-invoices/src/web/proformas/create/entities/proforma-item-create-form.entity.ts b/modules/customer-invoices/src/web/proformas/create.bak.no-vale/entities/proforma-item-create-form.entity.ts similarity index 100% rename from modules/customer-invoices/src/web/proformas/create/entities/proforma-item-create-form.entity.ts rename to modules/customer-invoices/src/web/proformas/create.bak.no-vale/entities/proforma-item-create-form.entity.ts diff --git a/modules/customer-invoices/src/web/proformas/create.bak.no-vale/index.ts b/modules/customer-invoices/src/web/proformas/create.bak.no-vale/index.ts new file mode 100644 index 00000000..4aedf593 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create.bak.no-vale/index.ts @@ -0,0 +1 @@ +export * from "./ui"; diff --git a/modules/customer-invoices/src/web/proformas/create.bak.no-vale/ui/index.ts b/modules/customer-invoices/src/web/proformas/create.bak.no-vale/ui/index.ts new file mode 100644 index 00000000..c4e34b27 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create.bak.no-vale/ui/index.ts @@ -0,0 +1 @@ +export * from "./pages"; diff --git a/modules/customer-invoices/src/web/proformas/create.bak.no-vale/ui/pages/index.ts b/modules/customer-invoices/src/web/proformas/create.bak.no-vale/ui/pages/index.ts new file mode 100644 index 00000000..6af8f18e --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create.bak.no-vale/ui/pages/index.ts @@ -0,0 +1 @@ +export * from "./proforma-create-page"; diff --git a/modules/customer-invoices/src/web/proformas/create.bak.no-vale/ui/pages/proforma-create-page.tsx b/modules/customer-invoices/src/web/proformas/create.bak.no-vale/ui/pages/proforma-create-page.tsx new file mode 100644 index 00000000..7d75e5c8 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create.bak.no-vale/ui/pages/proforma-create-page.tsx @@ -0,0 +1,48 @@ +import { PageHeader } from "@erp/core/components"; +import { UnsavedChangesProvider } from "@erp/core/hooks"; +import { AppContent, AppHeader } from "@repo/rdx-ui/components"; +import { Button } from "@repo/shadcn-ui/components"; +import { PlusIcon } from "lucide-react"; +import { useNavigate } from "react-router-dom"; + +import { useTranslation } from "../../../../i18n"; + +export const ProformaCreatePage = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + return ( + + + navigate("/proformas/create")} + > + + {t("pages.proformas.create.title")} + + } + title={t("pages.proformas.list.title")} + /> + + + +
+
+

{t("pages.create.title")}

+

{t("pages.create.description")}

+
+
+ +
+
+
hola
+
+
+ ); +}; diff --git a/modules/customer-invoices/src/web/proformas/create/adapters/build-create-proforma-params.ts b/modules/customer-invoices/src/web/proformas/create/adapters/build-create-proforma-params.ts new file mode 100644 index 00000000..3b632743 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/adapters/build-create-proforma-params.ts @@ -0,0 +1,27 @@ +import { PercentageDTOHelper } from "@erp/core"; +import type { CreateProformaParams } from "../../shared/api"; + +import type { ProformaCreateForm } from "../entities"; + +export const buildCreateProformaParams = ( + formData: ProformaCreateForm, + proformaId: string +): CreateProformaParams => { + return { + id: proformaId, + data: { + id: proformaId, + invoice_number: "", + series: formData.series.trim() || null, + invoice_date: formData.invoiceDate, + customer_id: formData.customerId, + language_code: formData.languageCode, + currency_code: formData.currencyCode, + global_discount_percentage: PercentageDTOHelper.fromNumber(0, 2), + payment_method_id: null, + payment_term_id: null, + tax_regime_code: null, + items: [], + }, + }; +}; diff --git a/modules/customer-invoices/src/web/proformas/create/adapters/index.ts b/modules/customer-invoices/src/web/proformas/create/adapters/index.ts new file mode 100644 index 00000000..73e44bc5 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/adapters/index.ts @@ -0,0 +1 @@ +export * from "./build-create-proforma-params"; diff --git a/modules/customer-invoices/src/web/proformas/create/controllers/index.ts b/modules/customer-invoices/src/web/proformas/create/controllers/index.ts new file mode 100644 index 00000000..a0c1718c --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/controllers/index.ts @@ -0,0 +1,2 @@ +export * from "./use-create-proforma-controller"; +export * from "./use-create-proforma-page-controller"; diff --git a/modules/customer-invoices/src/web/proformas/create/controllers/use-create-proforma-controller.ts b/modules/customer-invoices/src/web/proformas/create/controllers/use-create-proforma-controller.ts new file mode 100644 index 00000000..eeaefc99 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/controllers/use-create-proforma-controller.ts @@ -0,0 +1,158 @@ +import { applyValidationErrorCollection } from "@erp/core"; +import { useHookForm } from "@erp/core/hooks"; +import type { CustomerSelectionOption } from "@erp/customers"; +import { + UniqueID, + type ValidationErrorCollection, + isValidationErrorCollection, +} from "@repo/rdx-ddd"; +import { + focusFirstInputFormError, + showErrorToast, + showSuccessToast, + showWarningToast, +} from "@repo/rdx-ui/helpers"; +import { useEffect, useId, useMemo, useState } from "react"; +import type { FieldErrors } from "react-hook-form"; + +import type { Proforma } from "../../shared"; +import { useProformaCreateMutation, useProformasListQuery } from "../../shared/hooks"; +import { buildCreateProformaParams } from "../adapters"; +import { type ProformaCreateForm, ProformaCreateFormSchema } from "../entities"; +import { buildProformaCreateDefault, buildProformaSeriesOptions } from "../utils"; + +export interface UseCreateProformaControllerOptions { + onCreated?(proforma: Proforma): void; +} + +const normalizeSubmitError = (error: unknown): Error | ValidationErrorCollection => { + if (isValidationErrorCollection(error)) { + return error satisfies ValidationErrorCollection; + } + + if (error instanceof Error) { + return error satisfies Error; + } + + return new Error("Unknown error"); +}; + +export const useCreateProformaController = (options?: UseCreateProformaControllerOptions) => { + const formId = useId(); + const [selectedCustomer, setSelectedCustomer] = useState(null); + + const { + mutateAsync, + isPending: isCreating, + isError: isCreateError, + error: createError, + } = useProformaCreateMutation(); + + const seriesQuery = useProformasListQuery({ + criteria: { + pageNumber: 1, + pageSize: 100, + }, + }); + + const initialValues = useMemo(() => buildProformaCreateDefault(), []); + + const form = useHookForm({ + resolverSchema: ProformaCreateFormSchema, + initialValues, + disabled: isCreating, + }); + + const seriesOptions = useMemo( + () => buildProformaSeriesOptions(seriesQuery.data), + [seriesQuery.data] + ); + + useEffect(() => { + if (seriesOptions.length !== 1) return; + if (form.getValues("series")) return; + + form.setValue("series", seriesOptions[0].value, { + shouldDirty: false, + shouldTouch: false, + shouldValidate: true, + }); + }, [form, seriesOptions]); + + const setCustomer = (customer: CustomerSelectionOption) => { + setSelectedCustomer(customer); + form.setValue("customerId", customer.id, { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }); + }; + + const clearCustomer = () => { + setSelectedCustomer(null); + form.setValue("customerId", "", { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }); + }; + + const submitHandler = form.handleSubmit( + async (formData: ProformaCreateForm) => { + const proformaId = UniqueID.generateNewID().toString(); + const params = buildCreateProformaParams(formData, proformaId); + + try { + const created = await mutateAsync(params); + + form.reset( + { + ...buildProformaCreateDefault(), + series: formData.series, + }, + { keepDirty: false } + ); + setSelectedCustomer(null); + + showSuccessToast("Proforma creada", "Proforma creada como borrador."); + options?.onCreated?.(created); + } catch (error: unknown) { + const normalizedError = normalizeSubmitError(error); + + if (isValidationErrorCollection(normalizedError)) { + applyValidationErrorCollection(form, normalizedError); + focusFirstInputFormError(form); + showWarningToast("Revisa los campos", "Hay errores de validación en el formulario."); + return; + } + + showErrorToast("No se pudo crear la proforma", normalizedError.message); + } + }, + (_errors: FieldErrors) => { + focusFirstInputFormError(form); + showWarningToast("Revisa los campos", "Hay errores de validación en el formulario."); + } + ); + + const onSubmit = (event: React.SubmitEvent) => { + event.stopPropagation(); + submitHandler(event); + }; + + return { + formId, + form, + onSubmit, + isCreating, + isCreateError, + createError, + selectedCustomer, + setCustomer, + clearCustomer, + seriesOptions, + isLoading: seriesQuery.isLoading, + isLoadError: seriesQuery.isError, + loadError: seriesQuery.error, + }; +}; diff --git a/modules/customer-invoices/src/web/proformas/create/controllers/use-create-proforma-page-controller.ts b/modules/customer-invoices/src/web/proformas/create/controllers/use-create-proforma-page-controller.ts new file mode 100644 index 00000000..93f864c2 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/controllers/use-create-proforma-page-controller.ts @@ -0,0 +1,35 @@ +import { useCustomerSelectionFlow } from "@erp/customers/common"; +import { createSearchParams, useNavigate, useSearchParams } from "react-router-dom"; + +import { useCreateProformaController } from "./use-create-proforma-controller"; + +export const useCreateProformaPageController = () => { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const returnTo = searchParams.get("returnTo") ?? "/proformas"; + + const createCtrl = useCreateProformaController({ + onCreated: (created) => { + navigate({ + pathname: `/proformas/${created.id}/edit`, + search: createSearchParams({ + returnTo, + }).toString(), + }); + }, + }); + + const selectCustomerCtrl = useCustomerSelectionFlow({ + defaultLanguageCode: createCtrl.form.watch("languageCode"), + defaultCurrencyCode: createCtrl.form.watch("currencyCode"), + onCustomerSelected: (customer) => { + createCtrl.setCustomer(customer); + }, + }); + + return { + createCtrl, + selectCustomerCtrl, + returnTo, + }; +}; diff --git a/modules/customer-invoices/src/web/proformas/create/entities/index.ts b/modules/customer-invoices/src/web/proformas/create/entities/index.ts index a1605cb3..b58cfbe3 100644 --- a/modules/customer-invoices/src/web/proformas/create/entities/index.ts +++ b/modules/customer-invoices/src/web/proformas/create/entities/index.ts @@ -1,3 +1,2 @@ export * from "./proforma-create-form.entity"; export * from "./proforma-create-form.schema"; -export * from "./proforma-create-form-default.entity"; diff --git a/modules/customer-invoices/src/web/proformas/create/entities/proforma-create-form.entity.ts b/modules/customer-invoices/src/web/proformas/create/entities/proforma-create-form.entity.ts index f936f93a..cd8c12f8 100644 --- a/modules/customer-invoices/src/web/proformas/create/entities/proforma-create-form.entity.ts +++ b/modules/customer-invoices/src/web/proformas/create/entities/proforma-create-form.entity.ts @@ -1,37 +1,7 @@ -/** - * ProformaCreateForm representa el shape de datos del formulario de creación de proforma. - * Es decir, los campos que se muestran en el formulario y que el usuario puede editar. - * - * Este shape es específico para la UI y no tiene por qué coincidir - * con el shape del dominio ni con el de la API. - * - * Debe cumplir las siguientes reglas: - * - 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 - */ - -import type { ProformaItemForm } from "../../shared/entities"; - export interface ProformaCreateForm { - series: string; - - invoiceDate: string; - operationDate: string; - customerId: string; - - reference: string; - notes: string; - + invoiceDate: string; + series: string; languageCode: string; currencyCode: string; - - globalDiscountPercentage: number; - - paymentMethod: string; - - items: ProformaItemForm[]; } diff --git a/modules/customer-invoices/src/web/proformas/create/entities/proforma-create-form.schema.ts b/modules/customer-invoices/src/web/proformas/create/entities/proforma-create-form.schema.ts index c21cdcb2..dfe70b58 100644 --- a/modules/customer-invoices/src/web/proformas/create/entities/proforma-create-form.schema.ts +++ b/modules/customer-invoices/src/web/proformas/create/entities/proforma-create-form.schema.ts @@ -1,48 +1,16 @@ import { z } from "zod/v4"; -/** - * Este esquema es para validar los datos del formulario de creación de cliente. - * 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 formulario 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 - */ +const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; -export const CustomerCreateFormSchema = z.object({ - reference: z.string(), - isCompany: z.boolean(), - name: z.string().min(1, "El nombre es obligatorio"), - tradeName: z.string(), - tin: z.string(), - - defaultTaxes: z.array(z.string()), - - street: z.string(), - street2: z.string(), - city: z.string(), - province: z.string(), - postalCode: z.string(), - country: z.string().min(1, "El país es obligatorio"), - - primaryEmail: z.email("Email inválido"), - secondaryEmail: z.email("Email inválido"), - - primaryPhone: z.string(), - secondaryPhone: z.string(), - primaryMobile: z.string(), - secondaryMobile: z.string(), - - fax: z.string(), - website: z.url("URL inválida"), - - legalRecord: z.string(), - - languageCode: z.string().min(1, "El idioma es obligatorio"), - currencyCode: z.string().min(1, "La moneda es obligatoria"), +export const ProformaCreateFormSchema = z.object({ + customerId: z.string().min(1, "Selecciona un cliente."), + invoiceDate: z + .string() + .min(1, "Introduce una fecha válida.") + .regex(ISO_DATE_PATTERN, "Introduce una fecha válida."), + series: z.string(), + languageCode: z.string().min(1), + currencyCode: z.string().min(1), }); + +export type ProformaCreateFormSchemaType = z.infer; diff --git a/modules/customer-invoices/src/web/proformas/create/ui/blocks/index.ts b/modules/customer-invoices/src/web/proformas/create/ui/blocks/index.ts new file mode 100644 index 00000000..dfbfebd4 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/ui/blocks/index.ts @@ -0,0 +1,6 @@ +export * from "./proforma-create-auto-applied-info"; +export * from "./proforma-create-customer-field"; +export * from "./proforma-create-empty-lines-card"; +export * from "./proforma-create-form"; +export * from "./proforma-create-header"; +export * from "./proforma-create-initial-info-card"; diff --git a/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-auto-applied-info.tsx b/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-auto-applied-info.tsx new file mode 100644 index 00000000..c04cef4f --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-auto-applied-info.tsx @@ -0,0 +1,57 @@ +import { AlertCircleIcon, BanknoteIcon, CircleDollarSignIcon, ReceiptTextIcon } from "lucide-react"; + +export const ProformaCreateAutoAppliedInfo = () => { + return ( +
+
+ +
+
+

Se aplicará automáticamente

+

+ Los siguientes datos se tomarán del cliente o de la configuración de empresa y + podrás modificarlos después en la edición. +

+
+ +
+
+ +
+

Moneda

+

EUR

+
+
+ +
+ +
+

Método de pago

+

Según cliente

+
+
+ +
+ +
+

Condiciones de pago

+

Según cliente

+
+
+
+ +
+ +
+

Fiscalidad inicial

+

+ La fiscalidad se tomará del cliente o de la configuración de empresa y podrás + modificarla después en la edición. +

+
+
+
+
+
+ ); +}; diff --git a/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-customer-field.tsx b/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-customer-field.tsx new file mode 100644 index 00000000..4f7d1fa1 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-customer-field.tsx @@ -0,0 +1,73 @@ +import type { CustomerSelectionOption } from "@erp/customers"; +import { Button, Field, FieldError } from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import { ChevronsUpDownIcon, XIcon } from "lucide-react"; +import { Controller, useFormContext } from "react-hook-form"; + +import type { ProformaCreateForm } from "../../entities"; + +interface ProformaCreateCustomerFieldProps { + disabled?: boolean; + selectedCustomer?: CustomerSelectionOption | null; + onSelectCustomer: () => void; + onClearCustomer: () => void; +} + +export const ProformaCreateCustomerField = ({ + disabled = false, + selectedCustomer, + onSelectCustomer, + onClearCustomer, +}: ProformaCreateCustomerFieldProps) => { + const triggerId = "proforma-create-customer"; + const { control, formState } = useFormContext(); + const isDisabled = disabled || formState.isSubmitting; + + return ( + ( + + + +
+ + + {selectedCustomer ? ( + + ) : null} +
+ + +
+ )} + /> + ); +}; diff --git a/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-empty-lines-card.tsx b/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-empty-lines-card.tsx new file mode 100644 index 00000000..6d0b9169 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-empty-lines-card.tsx @@ -0,0 +1,31 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@repo/shadcn-ui/components"; +import { ListIcon } from "lucide-react"; + +export const ProformaCreateEmptyLinesCard = () => { + return ( + + + + + Líneas de detalle + + + La proforma se creará sin líneas. Podrás añadir productos o servicios en la siguiente + pantalla. + + + + +
+ +
+
+

Sin líneas de detalle

+

+ Añade productos o servicios después de crear el borrador. +

+
+
+
+ ); +}; diff --git a/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-form.tsx b/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-form.tsx new file mode 100644 index 00000000..83c54bdf --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-form.tsx @@ -0,0 +1,49 @@ +import type { CustomerSelectionOption } from "@erp/customers"; +import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers"; +import { cn } from "@repo/shadcn-ui/lib/utils"; + +import { ProformaCreateAutoAppliedInfo } from "./proforma-create-auto-applied-info"; +import { ProformaCreateEmptyLinesCard } from "./proforma-create-empty-lines-card"; +import { ProformaCreateInitialInfoCard } from "./proforma-create-initial-info-card"; + +interface ProformaCreateFormProps { + formId: string; + isSubmitting: boolean; + onSubmit: React.SubmitEventHandler; + selectedCustomer?: CustomerSelectionOption | null; + onSelectCustomer: () => void; + onClearCustomer: () => void; + seriesOptions: { value: string; label: string }[]; + className?: string; +} + +export const ProformaCreateForm = ({ + formId, + isSubmitting, + onSubmit, + selectedCustomer, + onSelectCustomer, + onClearCustomer, + seriesOptions, + className, +}: ProformaCreateFormProps) => { + return ( +
+
+
+ + + + + +
+
+
+ ); +}; diff --git a/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-header.tsx b/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-header.tsx new file mode 100644 index 00000000..68066753 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-header.tsx @@ -0,0 +1,55 @@ +import { PageFormHeader, PageKeyboardShortcutsButton } from "@erp/core/components"; +import { CancelActionButton, FormActionsBar, RhfSubmitActionButton } from "@repo/rdx-ui/components"; +import type { ReactNode } from "react"; + +export interface ProformaCreateHeaderProps { + formId: string; + isSaving: boolean; + onCancel: () => void; + children?: ReactNode; +} + +export const ProformaCreateHeader = ({ + formId, + isSaving, + onCancel, + children, +}: ProformaCreateHeaderProps) => { + return ( + + + + + + + + } + backLabel="Volver" + onBack={onCancel} + title="Nueva proforma" + > + {children} + + ); +}; diff --git a/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-initial-info-card.tsx b/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-initial-info-card.tsx new file mode 100644 index 00000000..0cf3b726 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-initial-info-card.tsx @@ -0,0 +1,56 @@ +import { DatePickerField, FormSectionCard, FormSectionGrid, SelectField } from "@repo/rdx-ui/components"; +import { FileTextIcon } from "lucide-react"; + +import type { CustomerSelectionOption } from "@erp/customers"; + +import { type ProformaCreateForm } from "../../entities"; +import { ProformaCreateCustomerField } from "./proforma-create-customer-field"; + +interface ProformaCreateInitialInfoCardProps { + disabled?: boolean; + selectedCustomer?: CustomerSelectionOption | null; + onSelectCustomer: () => void; + onClearCustomer: () => void; + seriesOptions: { value: string; label: string }[]; +} + +export const ProformaCreateInitialInfoCard = ({ + disabled = false, + selectedCustomer, + onSelectCustomer, + onClearCustomer, + seriesOptions, +}: ProformaCreateInitialInfoCardProps) => { + return ( + } title="Información inicial"> + +
+ +
+ + + className="md:col-span-3" + disabled={disabled} + label="Fecha" + name="invoiceDate" + placeholder="Selecciona una fecha" + required + /> + + + className="md:col-span-4" + disabled={disabled} + items={seriesOptions} + label="Serie" + name="series" + placeholder={seriesOptions.length > 0 ? "Selecciona una serie" : "Sin serie"} + /> +
+
+ ); +}; diff --git a/modules/customer-invoices/src/web/proformas/create/ui/components/index.ts b/modules/customer-invoices/src/web/proformas/create/ui/components/index.ts new file mode 100644 index 00000000..0cb33abb --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/ui/components/index.ts @@ -0,0 +1 @@ +export * from "./proforma-create-skeleton"; diff --git a/modules/customer-invoices/src/web/proformas/create/ui/components/proforma-create-skeleton.tsx b/modules/customer-invoices/src/web/proformas/create/ui/components/proforma-create-skeleton.tsx new file mode 100644 index 00000000..dc8d2e3e --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/ui/components/proforma-create-skeleton.tsx @@ -0,0 +1,16 @@ +import { Skeleton } from "@repo/shadcn-ui/components"; + +export const ProformaCreateSkeleton = () => { + return ( +
+
+ +
+
+ + + +
+
+ ); +}; diff --git a/modules/customer-invoices/src/web/proformas/create/ui/pages/proforma-create-page.tsx b/modules/customer-invoices/src/web/proformas/create/ui/pages/proforma-create-page.tsx index 7d75e5c8..c7028e7a 100644 --- a/modules/customer-invoices/src/web/proformas/create/ui/pages/proforma-create-page.tsx +++ b/modules/customer-invoices/src/web/proformas/create/ui/pages/proforma-create-page.tsx @@ -1,48 +1,85 @@ -import { PageHeader } from "@erp/core/components"; -import { UnsavedChangesProvider } from "@erp/core/hooks"; -import { AppContent, AppHeader } from "@repo/rdx-ui/components"; -import { Button } from "@repo/shadcn-ui/components"; -import { PlusIcon } from "lucide-react"; -import { useNavigate } from "react-router-dom"; +import { ErrorAlert } from "@erp/core/components"; +import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks"; +import { SelectCustomerDialog } from "@erp/customers"; +import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components"; +import { FormProvider } from "react-hook-form"; -import { useTranslation } from "../../../../i18n"; +import { ProformaInfoAlert } from "../../../update/ui/components"; +import { useCreateProformaPageController } from "../../controllers"; +import { ProformaCreateForm, ProformaCreateHeader } from "../blocks"; +import { ProformaCreateSkeleton } from "../components"; export const ProformaCreatePage = () => { - const { t } = useTranslation(); - const navigate = useNavigate(); + const { createCtrl, selectCustomerCtrl, returnTo } = useCreateProformaPageController(); + + const { navigateBack } = useReturnToNavigation({ + fallbackPath: returnTo, + }); + + if (createCtrl.isLoading) { + return ; + } + + if (createCtrl.isLoadError) { + return ( + + + +
+ navigateBack()} /> +
+
+ ); + } return ( - - - navigate("/proformas/create")} - > - - {t("pages.proformas.create.title")} - - } - title={t("pages.proformas.list.title")} - /> - +
+ + + + + Importante: Las proformas no tienen validez + fiscal. Puedes convertirlas en facturas cuando sean aceptadas por el cliente. + + - -
-
-

{t("pages.create.title")}

-

{t("pages.create.description")}

+ {createCtrl.isCreateError && ( + + )} + +
+
-
- -
-
-
hola
- - + + + + +
); }; diff --git a/modules/customer-invoices/src/web/proformas/create/utils/build-proforma-create-default.ts b/modules/customer-invoices/src/web/proformas/create/utils/build-proforma-create-default.ts new file mode 100644 index 00000000..980fd914 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/utils/build-proforma-create-default.ts @@ -0,0 +1,13 @@ +import { DateHelper } from "@repo/rdx-utils"; + +import type { ProformaCreateForm } from "../entities"; + +export const buildProformaCreateDefault = (): ProformaCreateForm => { + return { + customerId: "", + invoiceDate: DateHelper.buildTodayIsoDate(), + series: "", + languageCode: "es", + currencyCode: "EUR", + }; +}; diff --git a/modules/customer-invoices/src/web/proformas/create/utils/build-proforma-series-options.ts b/modules/customer-invoices/src/web/proformas/create/utils/build-proforma-series-options.ts new file mode 100644 index 00000000..c203995c --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/utils/build-proforma-series-options.ts @@ -0,0 +1,17 @@ +import type { SelectFieldItem } from "@repo/rdx-ui/components"; + +import type { ProformaList } from "../../shared"; + +export const buildProformaSeriesOptions = (proformas?: ProformaList): SelectFieldItem[] => { + const uniqueSeries = new Set(); + + for (const item of proformas?.items ?? []) { + const value = item.series?.trim(); + if (!value) continue; + uniqueSeries.add(value); + } + + return [...uniqueSeries] + .sort((left, right) => left.localeCompare(right)) + .map((value) => ({ value, label: value })); +}; diff --git a/modules/customer-invoices/src/web/proformas/create/utils/index.ts b/modules/customer-invoices/src/web/proformas/create/utils/index.ts new file mode 100644 index 00000000..3c2c7a03 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/create/utils/index.ts @@ -0,0 +1,2 @@ +export * from "./build-proforma-create-default"; +export * from "./build-proforma-series-options"; diff --git a/modules/customer-invoices/src/web/proformas/index.ts b/modules/customer-invoices/src/web/proformas/index.ts index eee8384f..1c77bc4e 100644 --- a/modules/customer-invoices/src/web/proformas/index.ts +++ b/modules/customer-invoices/src/web/proformas/index.ts @@ -1,2 +1,3 @@ export * from "./list"; +export * from "./create"; export * from "./update"; diff --git a/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx b/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx index f48f830b..b27a600e 100644 --- a/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx +++ b/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx @@ -167,7 +167,14 @@ export const ListProformasPage = () => { rightSlot={