Crear una proforma
This commit is contained in:
parent
95fe0127a3
commit
3c5fd7c57a
@ -10,9 +10,9 @@ const ProformasListPage = lazy(() =>
|
|||||||
import("./proformas/list").then((m) => ({ default: m.ListProformasPage }))
|
import("./proformas/list").then((m) => ({ default: m.ListProformasPage }))
|
||||||
);
|
);
|
||||||
|
|
||||||
/*const ProformasCreatePage = lazy(() =>
|
const ProformasCreatePage = lazy(() =>
|
||||||
import("./proformas/create").then((m) => ({ default: m.ProformaCreatePage }))
|
import("./proformas/create").then((m) => ({ default: m.ProformaCreatePage }))
|
||||||
);*/
|
);
|
||||||
|
|
||||||
const ProformaUpdatePage = lazy(() =>
|
const ProformaUpdatePage = lazy(() =>
|
||||||
import("./proformas/update").then((m) => ({ default: m.ProformaUpdatePage }))
|
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: (
|
||||||
|
<ProformaLayout>
|
||||||
|
<ProformasCreatePage />
|
||||||
|
</ProformaLayout>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "proformas/:id/edit",
|
path: "proformas/:id/edit",
|
||||||
handle: {
|
handle: {
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./proforma-create-form.entity";
|
||||||
|
export * from "./proforma-create-form.schema";
|
||||||
|
export * from "./proforma-create-form-default.entity";
|
||||||
@ -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[];
|
||||||
|
}
|
||||||
@ -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"),
|
||||||
|
});
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./ui";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./pages";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./proforma-create-page";
|
||||||
@ -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 (
|
||||||
|
<UnsavedChangesProvider isDirty={true}>
|
||||||
|
<AppHeader>
|
||||||
|
<PageHeader
|
||||||
|
description={t("pages.proformas.list.description")}
|
||||||
|
rightSlot={
|
||||||
|
<Button
|
||||||
|
aria-label={t("pages.proformas.create.title")}
|
||||||
|
onClick={() => navigate("/proformas/create")}
|
||||||
|
>
|
||||||
|
<PlusIcon aria-hidden className="mr-2 size-4" />
|
||||||
|
{t("pages.proformas.create.title")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
title={t("pages.proformas.list.title")}
|
||||||
|
/>
|
||||||
|
</AppHeader>
|
||||||
|
|
||||||
|
<AppContent>
|
||||||
|
<div className="flex items-center justify-between space-y-2">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight">{t("pages.create.title")}</h2>
|
||||||
|
<p className="text-muted-foreground">{t("pages.create.description")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end mb-4">
|
||||||
|
<Button className="cursor-pointer" onClick={() => navigate("/customer-invoices/list")}>
|
||||||
|
{t("pages.create.back_to_list")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col gap-4 p-4">hola</div>
|
||||||
|
</AppContent>
|
||||||
|
</UnsavedChangesProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./build-create-proforma-params";
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./use-create-proforma-controller";
|
||||||
|
export * from "./use-create-proforma-page-controller";
|
||||||
@ -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<CustomerSelectionOption | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutateAsync,
|
||||||
|
isPending: isCreating,
|
||||||
|
isError: isCreateError,
|
||||||
|
error: createError,
|
||||||
|
} = useProformaCreateMutation();
|
||||||
|
|
||||||
|
const seriesQuery = useProformasListQuery({
|
||||||
|
criteria: {
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialValues = useMemo<ProformaCreateForm>(() => buildProformaCreateDefault(), []);
|
||||||
|
|
||||||
|
const form = useHookForm<ProformaCreateForm>({
|
||||||
|
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<ProformaCreateForm>) => {
|
||||||
|
focusFirstInputFormError(form);
|
||||||
|
showWarningToast("Revisa los campos", "Hay errores de validación en el formulario.");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = (event: React.SubmitEvent<HTMLFormElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
submitHandler(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
formId,
|
||||||
|
form,
|
||||||
|
onSubmit,
|
||||||
|
isCreating,
|
||||||
|
isCreateError,
|
||||||
|
createError,
|
||||||
|
selectedCustomer,
|
||||||
|
setCustomer,
|
||||||
|
clearCustomer,
|
||||||
|
seriesOptions,
|
||||||
|
isLoading: seriesQuery.isLoading,
|
||||||
|
isLoadError: seriesQuery.isError,
|
||||||
|
loadError: seriesQuery.error,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,3 +1,2 @@
|
|||||||
export * from "./proforma-create-form.entity";
|
export * from "./proforma-create-form.entity";
|
||||||
export * from "./proforma-create-form.schema";
|
export * from "./proforma-create-form.schema";
|
||||||
export * from "./proforma-create-form-default.entity";
|
|
||||||
|
|||||||
@ -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 {
|
export interface ProformaCreateForm {
|
||||||
series: string;
|
|
||||||
|
|
||||||
invoiceDate: string;
|
|
||||||
operationDate: string;
|
|
||||||
|
|
||||||
customerId: string;
|
customerId: string;
|
||||||
|
invoiceDate: string;
|
||||||
reference: string;
|
series: string;
|
||||||
notes: string;
|
|
||||||
|
|
||||||
languageCode: string;
|
languageCode: string;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
|
|
||||||
globalDiscountPercentage: number;
|
|
||||||
|
|
||||||
paymentMethod: string;
|
|
||||||
|
|
||||||
items: ProformaItemForm[];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,48 +1,16 @@
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
/**
|
const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
* 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({
|
export const ProformaCreateFormSchema = z.object({
|
||||||
reference: z.string(),
|
customerId: z.string().min(1, "Selecciona un cliente."),
|
||||||
isCompany: z.boolean(),
|
invoiceDate: z
|
||||||
name: z.string().min(1, "El nombre es obligatorio"),
|
.string()
|
||||||
tradeName: z.string(),
|
.min(1, "Introduce una fecha válida.")
|
||||||
tin: z.string(),
|
.regex(ISO_DATE_PATTERN, "Introduce una fecha válida."),
|
||||||
|
series: z.string(),
|
||||||
defaultTaxes: z.array(z.string()),
|
languageCode: z.string().min(1),
|
||||||
|
currencyCode: z.string().min(1),
|
||||||
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 type ProformaCreateFormSchemaType = z.infer<typeof ProformaCreateFormSchema>;
|
||||||
|
|||||||
@ -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";
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import { AlertCircleIcon, BanknoteIcon, CircleDollarSignIcon, ReceiptTextIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export const ProformaCreateAutoAppliedInfo = () => {
|
||||||
|
return (
|
||||||
|
<section className="rounded-xl border border-blue-200 bg-blue-50/60 p-6 text-slate-800">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<AlertCircleIcon className="mt-0.5 size-5 text-blue-600" />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold">Se aplicará automáticamente</h2>
|
||||||
|
<p className="text-sm text-slate-600">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 border-b border-blue-200/80 py-4 md:grid-cols-3">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<CircleDollarSignIcon className="mt-0.5 size-5 text-blue-600" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">Moneda</p>
|
||||||
|
<p className="text-sm text-slate-600">EUR</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<BanknoteIcon className="mt-0.5 size-5 text-blue-600" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">Método de pago</p>
|
||||||
|
<p className="text-sm text-slate-600">Según cliente</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<ReceiptTextIcon className="mt-0.5 size-5 text-blue-600" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">Condiciones de pago</p>
|
||||||
|
<p className="text-sm text-slate-600">Según cliente</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3 pt-1">
|
||||||
|
<AlertCircleIcon className="mt-0.5 size-5 text-blue-600" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">Fiscalidad inicial</p>
|
||||||
|
<p className="text-sm text-slate-600">
|
||||||
|
La fiscalidad se tomará del cliente o de la configuración de empresa y podrás
|
||||||
|
modificarla después en la edición.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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<ProformaCreateForm>();
|
||||||
|
const isDisabled = disabled || formState.isSubmitting;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="customerId"
|
||||||
|
render={({ fieldState }) => (
|
||||||
|
<Field className="gap-1" data-invalid={fieldState.invalid}>
|
||||||
|
<label className="text-sm font-medium leading-none" htmlFor={triggerId}>
|
||||||
|
Cliente <span className="text-destructive">*</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
"h-10 w-full justify-between border bg-muted/50 px-3 font-normal text-left text-foreground hover:bg-muted/50",
|
||||||
|
!selectedCustomer && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
id={triggerId}
|
||||||
|
onClick={onSelectCustomer}
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<span className="truncate">
|
||||||
|
{selectedCustomer?.name || selectedCustomer?.tradeName || "Buscar o seleccionar cliente..."}
|
||||||
|
</span>
|
||||||
|
<ChevronsUpDownIcon className="ml-2 size-4 shrink-0 opacity-60" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{selectedCustomer ? (
|
||||||
|
<Button
|
||||||
|
className="absolute right-9 top-1/2 size-7 -translate-y-1/2"
|
||||||
|
disabled={isDisabled}
|
||||||
|
onClick={onClearCustomer}
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
<XIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FieldError errors={[fieldState.error]} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
|
||||||
|
import { ListIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export const ProformaCreateEmptyLinesCard = () => {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-xl">
|
||||||
|
<ListIcon className="size-5 text-primary" />
|
||||||
|
Líneas de detalle
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-base">
|
||||||
|
La proforma se creará sin líneas. Podrás añadir productos o servicios en la siguiente
|
||||||
|
pantalla.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="flex min-h-64 flex-col items-center justify-center gap-4 text-center">
|
||||||
|
<div className="flex size-20 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||||
|
<ListIcon className="size-9" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-2xl font-semibold tracking-tight">Sin líneas de detalle</p>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Añade productos o servicios después de crear el borrador.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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<HTMLFormElement>;
|
||||||
|
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 (
|
||||||
|
<div className={cn("p-6", className)}>
|
||||||
|
<form id={formId} noValidate onKeyDown={preventEnterKeySubmitForm} onSubmit={onSubmit}>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<ProformaCreateInitialInfoCard
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClearCustomer={onClearCustomer}
|
||||||
|
onSelectCustomer={onSelectCustomer}
|
||||||
|
selectedCustomer={selectedCustomer}
|
||||||
|
seriesOptions={seriesOptions}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProformaCreateAutoAppliedInfo />
|
||||||
|
|
||||||
|
<ProformaCreateEmptyLinesCard />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 (
|
||||||
|
<PageFormHeader
|
||||||
|
actions={
|
||||||
|
<FormActionsBar align="end" reverseOnMobile={false}>
|
||||||
|
<PageKeyboardShortcutsButton
|
||||||
|
className="hidden sm:flex"
|
||||||
|
label="Ver atajos de teclado"
|
||||||
|
shortcuts={[
|
||||||
|
{ keys: "Ctrl+S", label: "Crear y continuar" },
|
||||||
|
{ keys: "Esc", label: "Cancelar" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CancelActionButton
|
||||||
|
className="hidden sm:flex"
|
||||||
|
disabled={isSaving}
|
||||||
|
label="Cancelar"
|
||||||
|
onCancel={onCancel}
|
||||||
|
variant="outline"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RhfSubmitActionButton
|
||||||
|
busyLabel="Creando borrador..."
|
||||||
|
disabled={isSaving}
|
||||||
|
formId={formId}
|
||||||
|
isBusy={isSaving}
|
||||||
|
label="Crear y continuar"
|
||||||
|
/>
|
||||||
|
</FormActionsBar>
|
||||||
|
}
|
||||||
|
backLabel="Volver"
|
||||||
|
onBack={onCancel}
|
||||||
|
title="Nueva proforma"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PageFormHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 (
|
||||||
|
<FormSectionCard icon={<FileTextIcon className="size-5" />} title="Información inicial">
|
||||||
|
<FormSectionGrid>
|
||||||
|
<div className="md:col-span-5">
|
||||||
|
<ProformaCreateCustomerField
|
||||||
|
disabled={disabled}
|
||||||
|
onClearCustomer={onClearCustomer}
|
||||||
|
onSelectCustomer={onSelectCustomer}
|
||||||
|
selectedCustomer={selectedCustomer}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DatePickerField<ProformaCreateForm>
|
||||||
|
className="md:col-span-3"
|
||||||
|
disabled={disabled}
|
||||||
|
label="Fecha"
|
||||||
|
name="invoiceDate"
|
||||||
|
placeholder="Selecciona una fecha"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectField<ProformaCreateForm>
|
||||||
|
className="md:col-span-4"
|
||||||
|
disabled={disabled}
|
||||||
|
items={seriesOptions}
|
||||||
|
label="Serie"
|
||||||
|
name="series"
|
||||||
|
placeholder={seriesOptions.length > 0 ? "Selecciona una serie" : "Sin serie"}
|
||||||
|
/>
|
||||||
|
</FormSectionGrid>
|
||||||
|
</FormSectionCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./proforma-create-skeleton";
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { Skeleton } from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
|
export const ProformaCreateSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
||||||
|
<div className="border-b px-6 py-4">
|
||||||
|
<Skeleton className="h-10 w-full max-w-md" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 space-y-6 overflow-y-auto p-6">
|
||||||
|
<Skeleton className="h-10 w-full" />
|
||||||
|
<Skeleton className="h-64 w-full" />
|
||||||
|
<Skeleton className="h-48 w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,48 +1,85 @@
|
|||||||
import { PageHeader } from "@erp/core/components";
|
import { ErrorAlert } from "@erp/core/components";
|
||||||
import { UnsavedChangesProvider } from "@erp/core/hooks";
|
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import { AppContent, AppHeader } from "@repo/rdx-ui/components";
|
import { SelectCustomerDialog } from "@erp/customers";
|
||||||
import { Button } from "@repo/shadcn-ui/components";
|
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { FormProvider } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
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 = () => {
|
export const ProformaCreatePage = () => {
|
||||||
const { t } = useTranslation();
|
const { createCtrl, selectCustomerCtrl, returnTo } = useCreateProformaPageController();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
const { navigateBack } = useReturnToNavigation({
|
||||||
|
fallbackPath: returnTo,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (createCtrl.isLoading) {
|
||||||
|
return <ProformaCreateSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createCtrl.isLoadError) {
|
||||||
|
return (
|
||||||
|
<AppContent>
|
||||||
|
<ErrorAlert
|
||||||
|
message={
|
||||||
|
createCtrl.loadError instanceof Error
|
||||||
|
? createCtrl.loadError.message
|
||||||
|
: "Inténtalo de nuevo más tarde."
|
||||||
|
}
|
||||||
|
title="No se pudo preparar la creación de la proforma"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<BackHistoryButton onClick={() => navigateBack()} />
|
||||||
|
</div>
|
||||||
|
</AppContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UnsavedChangesProvider isDirty={true}>
|
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
||||||
<AppHeader>
|
<FormProvider {...createCtrl.form}>
|
||||||
<PageHeader
|
<UnsavedChangesProvider isDirty={createCtrl.form.formState.isDirty}>
|
||||||
description={t("pages.proformas.list.description")}
|
<ProformaCreateHeader
|
||||||
rightSlot={
|
formId={createCtrl.formId}
|
||||||
<Button
|
isSaving={createCtrl.isCreating}
|
||||||
aria-label={t("pages.proformas.create.title")}
|
onCancel={navigateBack}
|
||||||
onClick={() => navigate("/proformas/create")}
|
>
|
||||||
>
|
<ProformaInfoAlert>
|
||||||
<PlusIcon aria-hidden className="mr-2 size-4" />
|
<span className="font-semibold">Importante: </span>Las proformas no tienen validez
|
||||||
{t("pages.proformas.create.title")}
|
fiscal. Puedes convertirlas en facturas cuando sean aceptadas por el cliente.
|
||||||
</Button>
|
</ProformaInfoAlert>
|
||||||
}
|
</ProformaCreateHeader>
|
||||||
title={t("pages.proformas.list.title")}
|
|
||||||
/>
|
|
||||||
</AppHeader>
|
|
||||||
|
|
||||||
<AppContent>
|
{createCtrl.isCreateError && (
|
||||||
<div className="flex items-center justify-between space-y-2">
|
<ErrorAlert
|
||||||
<div>
|
message={
|
||||||
<h2 className="text-2xl font-bold tracking-tight">{t("pages.create.title")}</h2>
|
createCtrl.createError instanceof Error
|
||||||
<p className="text-muted-foreground">{t("pages.create.description")}</p>
|
? createCtrl.createError.message
|
||||||
|
: "Revisa los datos e inténtalo de nuevo."
|
||||||
|
}
|
||||||
|
title="No se pudo crear la proforma"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
<ProformaCreateForm
|
||||||
|
formId={createCtrl.formId}
|
||||||
|
isSubmitting={createCtrl.isCreating}
|
||||||
|
onClearCustomer={createCtrl.clearCustomer}
|
||||||
|
onSelectCustomer={selectCustomerCtrl.selectCtrl.openDialog}
|
||||||
|
onSubmit={createCtrl.onSubmit}
|
||||||
|
selectedCustomer={createCtrl.selectedCustomer}
|
||||||
|
seriesOptions={createCtrl.seriesOptions}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end mb-4">
|
|
||||||
<Button className="cursor-pointer" onClick={() => navigate("/customer-invoices/list")}>
|
<SelectCustomerDialog ctrl={selectCustomerCtrl.selectCtrl} />
|
||||||
{t("pages.create.back_to_list")}
|
</UnsavedChangesProvider>
|
||||||
</Button>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="flex flex-1 flex-col gap-4 p-4">hola</div>
|
|
||||||
</AppContent>
|
|
||||||
</UnsavedChangesProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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",
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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<string>();
|
||||||
|
|
||||||
|
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 }));
|
||||||
|
};
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./build-proforma-create-default";
|
||||||
|
export * from "./build-proforma-series-options";
|
||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./list";
|
export * from "./list";
|
||||||
|
export * from "./create";
|
||||||
export * from "./update";
|
export * from "./update";
|
||||||
|
|||||||
@ -167,7 +167,14 @@ export const ListProformasPage = () => {
|
|||||||
rightSlot={
|
rightSlot={
|
||||||
<Button
|
<Button
|
||||||
aria-label={t("pages.proformas.create.title")}
|
aria-label={t("pages.proformas.create.title")}
|
||||||
onClick={() => navigate("/proformas/create")}
|
onClick={() =>
|
||||||
|
navigate({
|
||||||
|
pathname: "/proformas/create",
|
||||||
|
search: createSearchParams({
|
||||||
|
returnTo: currentReturnTo,
|
||||||
|
}).toString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
size={"default"}
|
size={"default"}
|
||||||
>
|
>
|
||||||
<PlusIcon aria-hidden className="mr-2 size-4" />
|
<PlusIcon aria-hidden className="mr-2 size-4" />
|
||||||
|
|||||||
@ -53,6 +53,17 @@ function normalizeToDate(input: DateInput): Date | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildTodayIsoDate = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(now.getDate()).padStart(2, "0");
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const DateHelper = {
|
export const DateHelper = {
|
||||||
format: formatDate,
|
format: formatDate,
|
||||||
|
normalizeToDate,
|
||||||
|
buildTodayIsoDate,
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user