Proforma update
This commit is contained in:
parent
2eab047e90
commit
ec8e734851
@ -16,6 +16,9 @@ type SimpleSearchInputProps = {
|
||||
onSearchChange: (value: string) => void;
|
||||
loading?: boolean;
|
||||
maxHistory?: number;
|
||||
|
||||
placeholder?: string;
|
||||
autoFocus?: boolean;
|
||||
};
|
||||
|
||||
const SEARCH_HISTORY_KEY = "search_history";
|
||||
@ -27,6 +30,8 @@ export const SimpleSearchInput = ({
|
||||
onSearchChange,
|
||||
loading = false,
|
||||
maxHistory = 8,
|
||||
placeholder,
|
||||
autoFocus = false,
|
||||
}: SimpleSearchInputProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [searchValue, setSearchValue] = useState(value);
|
||||
@ -131,11 +136,14 @@ export const SimpleSearchInput = ({
|
||||
<InputGroup className="bg-background">
|
||||
<InputGroupInput
|
||||
autoComplete="off"
|
||||
autoFocus={autoFocus}
|
||||
inputMode="search"
|
||||
onChange={handleInputChange}
|
||||
onFocus={() => history.length > 0 && setOpen(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={t("components.simple_search_input.search_placeholder", "Search...")}
|
||||
placeholder={
|
||||
placeholder || t("components.simple_search_input.search_placeholder", "Search...")
|
||||
}
|
||||
ref={inputRef}
|
||||
spellCheck={false}
|
||||
value={searchValue}
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./proforma-to-proforma-update-form.adapter";
|
||||
export * from "./proforma-to-selected-customer.adapter";
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
import type { CustomerSelectionOption } from "@erp/customers/common";
|
||||
|
||||
import type { Proforma } from "../../shared/entities";
|
||||
|
||||
export const mapProformaToSelectedCustomer = (
|
||||
proforma?: Proforma | null
|
||||
): CustomerSelectionOption | null => {
|
||||
if (!proforma?.customerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: proforma.customerId,
|
||||
name: proforma.recipient.name ?? "",
|
||||
tin: proforma.recipient.tin ?? "",
|
||||
|
||||
languageCode: proforma.languageCode ?? "",
|
||||
currencyCode: proforma.currencyCode ?? "",
|
||||
};
|
||||
};
|
||||
@ -1,13 +1,14 @@
|
||||
import { useHookForm } from "@erp/core/hooks";
|
||||
import type { CustomerSelectionOption } from "@erp/customers";
|
||||
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
|
||||
import { useEffect, useId, useMemo } from "react";
|
||||
import { useEffect, useId, useMemo, useState } from "react";
|
||||
import type { FieldErrors } from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "../../../i18n";
|
||||
import type { UpdateProformaByIdParams } from "../../shared";
|
||||
import type { Proforma } from "../../shared/entities";
|
||||
import { useProformaGetQuery, useProformaUpdateMutation } from "../../shared/hooks";
|
||||
import { mapProformaToProformaUpdateForm } from "../adapters";
|
||||
import { mapProformaToProformaUpdateForm, mapProformaToSelectedCustomer } from "../adapters";
|
||||
import {
|
||||
type ProformaUpdateForm,
|
||||
ProformaUpdateFormSchema,
|
||||
@ -63,6 +64,8 @@ export const useUpdateProformaController = (
|
||||
disabled: isLoading || isUpdating,
|
||||
});
|
||||
|
||||
const [selectedCustomer, setSelectedCustomer] = useState<CustomerSelectionOption | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!proformaData) return;
|
||||
|
||||
@ -70,6 +73,8 @@ export const useUpdateProformaController = (
|
||||
form.reset(mapProformaToProformaUpdateForm(proformaData), {
|
||||
keepDirty: false, // <-- importante: no marca el form como "dirty" al cargar los datos reales
|
||||
});
|
||||
|
||||
setSelectedCustomer(mapProformaToSelectedCustomer(proformaData));
|
||||
}, [proformaData, form]);
|
||||
|
||||
/** Handlers */
|
||||
@ -80,6 +85,27 @@ export const useUpdateProformaController = (
|
||||
: defaultProformaUpdateForm;
|
||||
|
||||
form.reset(initialData, { keepDirty: false });
|
||||
setSelectedCustomer(mapProformaToSelectedCustomer(proformaData));
|
||||
};
|
||||
|
||||
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(
|
||||
@ -104,6 +130,8 @@ export const useUpdateProformaController = (
|
||||
keepDirty: false,
|
||||
});
|
||||
|
||||
setSelectedCustomer(mapProformaToSelectedCustomer(proformaData));
|
||||
|
||||
if (options?.successToasts !== false) {
|
||||
showSuccessToast(
|
||||
t("proformas.update.success.title"),
|
||||
@ -164,6 +192,10 @@ export const useUpdateProformaController = (
|
||||
isUpdateError,
|
||||
updateError,
|
||||
|
||||
selectedCustomer,
|
||||
setCustomer,
|
||||
clearCustomer,
|
||||
|
||||
// No devolver FormProvider, así el controller es más
|
||||
// flexible y reusable (p.ej. para un modal)
|
||||
// FormProvider,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useUrlParamId } from "@erp/core/hooks";
|
||||
import { useCustomerSelectionFlow } from "@erp/customers/common";
|
||||
|
||||
import { useUpdateProformaController } from "./use-update-proforma-controller";
|
||||
|
||||
@ -7,7 +8,18 @@ export const useUpdateProformaPageController = () => {
|
||||
|
||||
const updateCtrl = useUpdateProformaController(proformaId);
|
||||
|
||||
const selectCustomerCtrl = useCustomerSelectionFlow({
|
||||
defaultLanguageCode: updateCtrl.form.watch("languageCode"),
|
||||
defaultCurrencyCode: updateCtrl.form.watch("currencyCode"),
|
||||
onCustomerSelected: (customer) => {
|
||||
console.log(customer);
|
||||
alert("cliente seleccionado");
|
||||
//updateCtrl.setCustomer(customer);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
updateCtrl,
|
||||
selectCustomerCtrl,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
export * from "./proforma-basic-info-fields";
|
||||
export * from "./proforma-form-field-shell";
|
||||
export * from "./proforma-header-fields-card";
|
||||
export * from "./proforma-header-form-grid";
|
||||
export * from "./proforma-section-card";
|
||||
export * from "./selected-recipient-summary";
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
import type { CustomerSelectionOption } from "@erp/customers";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
|
||||
type SelectedRecipientSummaryProps = {
|
||||
recipient?: CustomerSelectionOption | null;
|
||||
onChangeClick: () => void;
|
||||
onCreateClick?: () => void;
|
||||
};
|
||||
|
||||
export const SelectedRecipientSummary = ({
|
||||
recipient,
|
||||
onChangeClick,
|
||||
onCreateClick,
|
||||
}: SelectedRecipientSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border p-4">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium">{t("customers.selected_customer.title", "Cliente")}</p>
|
||||
|
||||
{recipient ? (
|
||||
<div className="mt-2 space-y-1">
|
||||
<p className="truncate font-medium">{recipient.name}</p>
|
||||
{recipient.tin ? (
|
||||
<p className="truncate text-sm text-muted-foreground">{recipient.tin}</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{t("customers.selected_customer.empty", "No hay ningún cliente seleccionado")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{onCreateClick ? (
|
||||
<Button onClick={onCreateClick} type="button" variant="outline">
|
||||
{t("customers.selected_customer.new_customer", "Nuevo cliente")}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
<Button onClick={onChangeClick} type="button" variant="outline">
|
||||
{recipient
|
||||
? t("customers.selected_customer.change", "Cambiar cliente")
|
||||
: t("customers.selected_customer.select", "Seleccionar cliente")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./proforma-update-editor-form";
|
||||
export * from "./proforma-update-header-editor";
|
||||
export * from "./proforma-update-recipient-editor";
|
||||
|
||||
@ -1,20 +1,28 @@
|
||||
// modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-update-editor.tsx
|
||||
|
||||
import type { CustomerSelectionOption } from "@erp/customers";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
|
||||
import { ProformaUpdateHeaderEditor } from ".";
|
||||
import { ProformaUpdateRecipientEditor } from ".";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { Proforma } from "../../../shared/entities";
|
||||
import { ProformaHeaderFieldsCard } from "../blocks/proforma-header-fields-card";
|
||||
|
||||
import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor";
|
||||
|
||||
type ProformaUpdateEditorProps = {
|
||||
formId: string;
|
||||
proforma?: Proforma;
|
||||
isSubmitting: boolean;
|
||||
onSubmit: React.FormEventHandler<HTMLFormElement>;
|
||||
onReset: () => void;
|
||||
|
||||
selectedCustomer?: CustomerSelectionOption | null;
|
||||
onChangeCustomerClick: () => void;
|
||||
onCreateCustomerClick: () => void;
|
||||
|
||||
className?: string;
|
||||
};
|
||||
|
||||
@ -23,6 +31,9 @@ export const ProformaUpdateEditorForm = ({
|
||||
isSubmitting,
|
||||
onSubmit,
|
||||
onReset,
|
||||
selectedCustomer,
|
||||
onChangeCustomerClick,
|
||||
onCreateCustomerClick,
|
||||
className,
|
||||
}: ProformaUpdateEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
@ -30,16 +41,13 @@ export const ProformaUpdateEditorForm = ({
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="space-y-6">
|
||||
<ProformaUpdateHeaderEditor
|
||||
currencyOptions={[]}
|
||||
customerOptions={[]}
|
||||
<ProformaUpdateHeaderEditor disabled={isSubmitting} />
|
||||
|
||||
<ProformaUpdateRecipientEditor
|
||||
disabled={isSubmitting}
|
||||
paymentMethodOptions={[]}
|
||||
priceListOptions={[]}
|
||||
salesPersonOptions={[]}
|
||||
serieOptions={[]}
|
||||
statusOptions={[]}
|
||||
warehouseOptions={[]}
|
||||
onChangeCustomerClick={onChangeCustomerClick}
|
||||
onCreateCustomerClick={onCreateCustomerClick}
|
||||
selectedCustomer={selectedCustomer}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col-reverse gap-3 border-t pt-4 sm:flex-row sm:justify-end">
|
||||
|
||||
@ -1,355 +1,63 @@
|
||||
import { DatePickerField, SelectField, TextField } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Input,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Textarea,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { ProformaUpdateForm } from "../../entities";
|
||||
import { ProformaFormFieldShell, ProformaHeaderFormGrid, ProformaSectionCard } from "../blocks";
|
||||
|
||||
interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
import { ProformaHeaderFormGrid, ProformaSectionCard } from "../blocks";
|
||||
|
||||
interface ProformaUpdateHeaderEditorProps {
|
||||
statusOptions: SelectOption[];
|
||||
serieOptions: SelectOption[];
|
||||
customerOptions: SelectOption[];
|
||||
currencyOptions: SelectOption[];
|
||||
paymentMethodOptions: SelectOption[];
|
||||
salesPersonOptions: SelectOption[];
|
||||
warehouseOptions: SelectOption[];
|
||||
priceListOptions: SelectOption[];
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export const ProformaUpdateHeaderEditor = ({
|
||||
statusOptions,
|
||||
serieOptions,
|
||||
customerOptions,
|
||||
currencyOptions,
|
||||
paymentMethodOptions,
|
||||
salesPersonOptions,
|
||||
warehouseOptions,
|
||||
priceListOptions,
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
}: ProformaUpdateHeaderEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext<ProformaUpdateForm>();
|
||||
|
||||
const isFieldLocked = disabled || readOnly;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<ProformaSectionCard
|
||||
description={t("form_groups.proformas.basic_info.description")}
|
||||
title={t("form_groups.proformas.basic_info.title")}
|
||||
>
|
||||
<ProformaHeaderFormGrid>
|
||||
<SelectField
|
||||
className="md:col-span-2"
|
||||
label={t("form_fields.proformas.series.label")}
|
||||
name="series"
|
||||
placeholder={t("form_fields.proformas.series.placeholder")}
|
||||
/>
|
||||
<ProformaSectionCard
|
||||
description={t("form_groups.proformas.basic_info.description")}
|
||||
title={t("form_groups.proformas.basic_info.title")}
|
||||
>
|
||||
<ProformaHeaderFormGrid>
|
||||
<SelectField
|
||||
className="md:col-span-2"
|
||||
label={t("form_fields.proformas.series.label")}
|
||||
name="series"
|
||||
placeholder={t("form_fields.proformas.series.placeholder")}
|
||||
/>
|
||||
|
||||
<DatePickerField
|
||||
className="md:col-span-3"
|
||||
label={t("form_fields.proformas.invoice_date.label")}
|
||||
name="invoice_date"
|
||||
placeholder={t("form_fields.proformas.invoice_date.placeholder")}
|
||||
required
|
||||
/>
|
||||
<DatePickerField
|
||||
className="md:col-span-3"
|
||||
label={t("form_fields.proformas.invoice_date.label")}
|
||||
name="invoice_date"
|
||||
placeholder={t("form_fields.proformas.invoice_date.placeholder")}
|
||||
required
|
||||
/>
|
||||
|
||||
<DatePickerField
|
||||
className="md:col-span-3"
|
||||
label={t("form_fields.proformas.operation_date.label")}
|
||||
name="operation_date"
|
||||
placeholder={t("form_fields.proformas.operation_date.placeholder")}
|
||||
/>
|
||||
<DatePickerField
|
||||
className="md:col-span-3"
|
||||
label={t("form_fields.proformas.operation_date.label")}
|
||||
name="operation_date"
|
||||
placeholder={t("form_fields.proformas.operation_date.placeholder")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="md:col-span-4"
|
||||
label={t("form_fields.proformas.reference.label")}
|
||||
maxLength={256}
|
||||
name="reference"
|
||||
placeholder={t("form_fields.proformas.reference.placeholder")}
|
||||
/>
|
||||
<TextField
|
||||
className="md:col-span-4"
|
||||
label={t("form_fields.proformas.reference.label")}
|
||||
maxLength={256}
|
||||
name="reference"
|
||||
placeholder={t("form_fields.proformas.reference.placeholder")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="md:col-span-12"
|
||||
label={t("form_fields.proformas.description.label")}
|
||||
maxLength={256}
|
||||
name="description"
|
||||
placeholder={t("form_fields.proformas.description.placeholder")}
|
||||
/>
|
||||
</ProformaHeaderFormGrid>
|
||||
</ProformaSectionCard>
|
||||
|
||||
<ProformaSectionCard
|
||||
description={t("proformas.update.sections.customer_description")}
|
||||
title={t("proformas.update.sections.customer")}
|
||||
>
|
||||
<ProformaHeaderFormGrid>
|
||||
<ProformaFormFieldShell
|
||||
error={errors.customer_id?.message}
|
||||
htmlFor="customer_id"
|
||||
label={t("proformas.fields.customer")}
|
||||
required
|
||||
span="lg"
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="customer_id"
|
||||
render={({ field }) => (
|
||||
<Select disabled={isFieldLocked} onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="h-10 w-full" id="customer_id">
|
||||
<SelectValue placeholder={t("common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{customerOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.customer_tax_id?.message}
|
||||
htmlFor="customer_tax_id"
|
||||
label={t("proformas.fields.customer_tax_id")}
|
||||
span="sm"
|
||||
>
|
||||
<Input
|
||||
className="h-10"
|
||||
disabled={isFieldLocked}
|
||||
id="customer_tax_id"
|
||||
{...register("customer_tax_id")}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.customer_reference?.message}
|
||||
htmlFor="customer_reference"
|
||||
label={t("proformas.fields.customer_reference")}
|
||||
span="md"
|
||||
>
|
||||
<Input
|
||||
className="h-10"
|
||||
disabled={isFieldLocked}
|
||||
id="customer_reference"
|
||||
{...register("customer_reference")}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
</ProformaHeaderFormGrid>
|
||||
</ProformaSectionCard>
|
||||
|
||||
<ProformaSectionCard
|
||||
description={t("proformas.update.sections.commercial_description")}
|
||||
title={t("proformas.update.sections.commercial")}
|
||||
>
|
||||
<ProformaHeaderFormGrid>
|
||||
<ProformaFormFieldShell
|
||||
error={errors.currency_code?.message}
|
||||
htmlFor="currency_code"
|
||||
label={t("proformas.fields.currency_code")}
|
||||
required
|
||||
span="sm"
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="currency_code"
|
||||
render={({ field }) => (
|
||||
<Select disabled={isFieldLocked} onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="h-10 w-full" id="currency_code">
|
||||
<SelectValue placeholder={t("common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{currencyOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.payment_method_id?.message}
|
||||
htmlFor="payment_method_id"
|
||||
label={t("proformas.fields.payment_method")}
|
||||
span="md"
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="payment_method_id"
|
||||
render={({ field }) => (
|
||||
<Select disabled={isFieldLocked} onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="h-10 w-full" id="payment_method_id">
|
||||
<SelectValue placeholder={t("common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{paymentMethodOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.due_date?.message}
|
||||
htmlFor="due_date"
|
||||
label={t("proformas.fields.due_date")}
|
||||
span="sm"
|
||||
>
|
||||
<Input
|
||||
className="h-10"
|
||||
disabled={isFieldLocked}
|
||||
id="due_date"
|
||||
type="date"
|
||||
{...register("due_date")}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.sales_person_id?.message}
|
||||
htmlFor="sales_person_id"
|
||||
label={t("proformas.fields.sales_person")}
|
||||
span="md"
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="sales_person_id"
|
||||
render={({ field }) => (
|
||||
<Select disabled={isFieldLocked} onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="h-10 w-full" id="sales_person_id">
|
||||
<SelectValue placeholder={t("common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{salesPersonOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.warehouse_id?.message}
|
||||
htmlFor="warehouse_id"
|
||||
label={t("proformas.fields.warehouse")}
|
||||
span="md"
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="warehouse_id"
|
||||
render={({ field }) => (
|
||||
<Select disabled={isFieldLocked} onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="h-10 w-full" id="warehouse_id">
|
||||
<SelectValue placeholder={t("common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{warehouseOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.price_list_id?.message}
|
||||
htmlFor="price_list_id"
|
||||
label={t("proformas.fields.price_list")}
|
||||
span="md"
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="price_list_id"
|
||||
render={({ field }) => (
|
||||
<Select disabled={isFieldLocked} onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="h-10 w-full" id="price_list_id">
|
||||
<SelectValue placeholder={t("common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{priceListOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
</ProformaHeaderFormGrid>
|
||||
</ProformaSectionCard>
|
||||
|
||||
<ProformaSectionCard
|
||||
description={t("proformas.update.sections.content_description")}
|
||||
title={t("proformas.update.sections.content")}
|
||||
>
|
||||
<ProformaHeaderFormGrid>
|
||||
<ProformaFormFieldShell
|
||||
error={errors.subject?.message}
|
||||
htmlFor="subject"
|
||||
label={t("proformas.fields.subject")}
|
||||
span="full"
|
||||
>
|
||||
<Input
|
||||
className="h-10"
|
||||
disabled={isFieldLocked}
|
||||
id="subject"
|
||||
{...register("subject")}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
|
||||
<ProformaFormFieldShell
|
||||
error={errors.notes?.message}
|
||||
htmlFor="notes"
|
||||
label={t("proformas.fields.notes")}
|
||||
span="full"
|
||||
>
|
||||
<Textarea
|
||||
className="min-h-28 resize-y"
|
||||
disabled={isFieldLocked}
|
||||
id="notes"
|
||||
{...register("notes")}
|
||||
/>
|
||||
</ProformaFormFieldShell>
|
||||
</ProformaHeaderFormGrid>
|
||||
</ProformaSectionCard>
|
||||
</div>
|
||||
<TextField
|
||||
className="md:col-span-12"
|
||||
label={t("form_fields.proformas.description.label")}
|
||||
maxLength={256}
|
||||
name="description"
|
||||
placeholder={t("form_fields.proformas.description.placeholder")}
|
||||
/>
|
||||
</ProformaHeaderFormGrid>
|
||||
</ProformaSectionCard>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
import type { CustomerSelectionOption } from "@erp/customers";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { ProformaUpdateForm } from "../../entities";
|
||||
import { ProformaHeaderFormGrid, ProformaSectionCard, SelectedRecipientSummary } from "../blocks";
|
||||
|
||||
interface ProformaUpdateRecipientEditorProps {
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
|
||||
selectedCustomer?: CustomerSelectionOption | null;
|
||||
onChangeCustomerClick: () => void;
|
||||
onCreateCustomerClick: () => void;
|
||||
}
|
||||
|
||||
export const ProformaUpdateRecipientEditor = ({
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
|
||||
selectedCustomer,
|
||||
onChangeCustomerClick,
|
||||
onCreateCustomerClick,
|
||||
}: ProformaUpdateRecipientEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext<ProformaUpdateForm>();
|
||||
|
||||
const isFieldLocked = disabled || readOnly;
|
||||
|
||||
return (
|
||||
<ProformaSectionCard
|
||||
description={t("form_groups.proformas.customer.description")}
|
||||
title={t("form_groups.proformas.customer.title")}
|
||||
>
|
||||
<ProformaHeaderFormGrid>
|
||||
<SelectedRecipientSummary
|
||||
onChangeClick={onChangeCustomerClick}
|
||||
onCreateClick={onCreateCustomerClick}
|
||||
recipient={selectedCustomer}
|
||||
/>
|
||||
</ProformaHeaderFormGrid>
|
||||
</ProformaSectionCard>
|
||||
);
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
import { SpainTaxCatalogProvider } from "@erp/core";
|
||||
import { ErrorAlert, NotFoundCard, PageHeader } from "@erp/core/components";
|
||||
import { UnsavedChangesProvider, UpdateCommitButtonGroup } from "@erp/core/hooks";
|
||||
import { SelectCustomerDialog } from "@erp/customers";
|
||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
import { Spinner } from "@repo/shadcn-ui/components";
|
||||
import { useMemo } from "react";
|
||||
@ -15,7 +16,7 @@ export const ProformaUpdatePage = () => {
|
||||
const { t } = useTranslation();
|
||||
const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []);
|
||||
|
||||
const { updateCtrl } = useUpdateProformaPageController();
|
||||
const { updateCtrl, selectCustomerCtrl } = useUpdateProformaPageController();
|
||||
|
||||
if (updateCtrl.isLoading) {
|
||||
return <ProformaUpdateSkeleton />;
|
||||
@ -95,14 +96,23 @@ export const ProformaUpdatePage = () => {
|
||||
{updateCtrl.isLoading && <Spinner />}
|
||||
|
||||
{!updateCtrl.isLoading && (
|
||||
<FormProvider {...updateCtrl.form}>
|
||||
<ProformaUpdateEditorForm
|
||||
formId={updateCtrl.formId}
|
||||
isSubmitting={updateCtrl.isUpdating}
|
||||
onReset={updateCtrl.resetForm}
|
||||
onSubmit={updateCtrl.onSubmit}
|
||||
<>
|
||||
<FormProvider {...updateCtrl.form}>
|
||||
<ProformaUpdateEditorForm
|
||||
formId={updateCtrl.formId}
|
||||
isSubmitting={updateCtrl.isUpdating}
|
||||
onChangeCustomerClick={selectCustomerCtrl.selectCtrl.openDialog}
|
||||
//onCreateCustomerClick={selectCustomerCtrl.createCtrl.openDialog}
|
||||
onReset={updateCtrl.resetForm}
|
||||
onSubmit={updateCtrl.onSubmit}
|
||||
selectedCustomer={updateCtrl.selectedCustomer}
|
||||
/>
|
||||
</FormProvider>
|
||||
<SelectCustomerDialog
|
||||
ctrl={selectCustomerCtrl.selectCtrl}
|
||||
//onCreateNewCustomerClick={selectCustomerCtrl.createCtrl.openDialog}
|
||||
/>
|
||||
</FormProvider>
|
||||
</>
|
||||
)}
|
||||
</AppContent>
|
||||
</UnsavedChangesProvider>
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./use-customer-selection-flow";
|
||||
export * from "./use-select-customer-dialog-controller";
|
||||
@ -0,0 +1,30 @@
|
||||
import type { CustomerSelectionOption } from "../entities";
|
||||
import { SelectCustomerDialog } from "../ui";
|
||||
|
||||
import { useSelectCustomerDialogController } from "./use-select-customer-dialog-controller";
|
||||
|
||||
export const useCustomerSelectionFlow = (options: {
|
||||
onCustomerSelected: (customer: CustomerSelectionOption) => void;
|
||||
defaultLanguageCode?: string;
|
||||
defaultCurrencyCode?: string;
|
||||
}) => {
|
||||
const selectCtrl = useSelectCustomerDialogController({
|
||||
onSelect: options.onCustomerSelected,
|
||||
});
|
||||
|
||||
/*const createCtrl = useQuickCreateCustomerDialogController({
|
||||
onCreated: (customer) => {
|
||||
options.onCustomerSelected(customer);
|
||||
selectCtrl.closeDialog();
|
||||
},
|
||||
defaultLanguageCode: options.defaultLanguageCode,
|
||||
defaultCurrencyCode: options.defaultCurrencyCode,
|
||||
});*/
|
||||
|
||||
return {
|
||||
selectCtrl,
|
||||
//createCtrl,
|
||||
|
||||
SelectCustomerDialog,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,97 @@
|
||||
import { useDebounce } from "@repo/rdx-ui/components";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { type ListCustomersByCriteriaParams, useCustomersListQuery } from "../../../../web/shared";
|
||||
import { type CustomerSelectionOption, buildCustomerSelectionOption } from "../..";
|
||||
|
||||
type UseSelectCustomerDialogControllerOptions = {
|
||||
onSelect(customer: CustomerSelectionOption): void;
|
||||
};
|
||||
|
||||
export const useSelectCustomerDialogController = (
|
||||
options: UseSelectCustomerDialogControllerOptions
|
||||
) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const debouncedQ = useDebounce(search, 300);
|
||||
|
||||
const criteria = useMemo<NonNullable<ListCustomersByCriteriaParams["criteria"]>>(
|
||||
() => ({
|
||||
q: debouncedQ || "",
|
||||
pageNumber: pageIndex,
|
||||
pageSize,
|
||||
order: "desc",
|
||||
orderBy: "name",
|
||||
//filters: statusFilter === "all" ? [] : [{ field: "status", operator: "eq", value: statusFilter }],
|
||||
}),
|
||||
[debouncedQ, pageIndex, pageSize /*statusFilter*/]
|
||||
);
|
||||
|
||||
const query = useCustomersListQuery({
|
||||
criteria,
|
||||
enabled: isOpen,
|
||||
});
|
||||
|
||||
const setSearchValue = (value: string) => {
|
||||
const nextValue = value.trim().replace(/\s+/g, " ");
|
||||
|
||||
setSearch((prev) => {
|
||||
if (prev === nextValue) return prev;
|
||||
|
||||
// Sólo si la búsqueda realmente cambia,
|
||||
// reseteamos la página a 0 para evitar inconsistencias
|
||||
setPageIndex(0);
|
||||
return nextValue;
|
||||
});
|
||||
};
|
||||
|
||||
const setPageSizeValue = (value: number) => {
|
||||
setPageSize((prev) => {
|
||||
if (prev === value) return prev;
|
||||
|
||||
// Sólo si el tamaño de página realmente cambia,
|
||||
// reseteamos la página a 0 para evitar inconsistencias
|
||||
setPageIndex(0);
|
||||
return value;
|
||||
});
|
||||
};
|
||||
|
||||
const customers: CustomerSelectionOption[] =
|
||||
query.data?.items?.map(buildCustomerSelectionOption) ?? [];
|
||||
|
||||
const openDialog = () => setIsOpen(true);
|
||||
const closeDialog = () => setIsOpen(false);
|
||||
|
||||
const selectCustomer = (customer: CustomerSelectionOption) => {
|
||||
options.onSelect(customer);
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
openDialog,
|
||||
closeDialog,
|
||||
|
||||
selectCustomer,
|
||||
|
||||
customers,
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
|
||||
isError: query.isError,
|
||||
error: query.error,
|
||||
|
||||
refetch: query.refetch,
|
||||
|
||||
pageIndex,
|
||||
pageSize,
|
||||
setPageIndex,
|
||||
setPageSize: setPageSizeValue,
|
||||
|
||||
search,
|
||||
setSearchValue,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Criteria para búsqueda/paginación de clientes.
|
||||
* Alineado con queries típicas del backend.
|
||||
*/
|
||||
export interface CustomerSelectionCriteria {
|
||||
q: string;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Representa un cliente seleccionable desde cualquier módulo.
|
||||
* Debe ser lo suficientemente rico para mostrar en UI,
|
||||
* pero sin arrastrar todo el shape de Customer.
|
||||
*/
|
||||
export interface CustomerSelectionOption {
|
||||
id: string;
|
||||
//status: string;
|
||||
|
||||
//isCompany: boolean;
|
||||
name: string;
|
||||
//tradeName: string;
|
||||
tin: string;
|
||||
|
||||
/*street: string;
|
||||
street2: string;
|
||||
city: string;
|
||||
province: string;
|
||||
postalCode: string;
|
||||
country: string;
|
||||
*/
|
||||
|
||||
//emailPrimary: string;
|
||||
//primaryPhone: string;
|
||||
//primaryMobile: string;
|
||||
|
||||
languageCode: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./customer-selection-criteria.entity";
|
||||
export * from "./customer-selection-option.entity";
|
||||
@ -0,0 +1,4 @@
|
||||
export * from "./controllers/use-customer-selection-flow";
|
||||
export * from "./entities";
|
||||
export * from "./ui";
|
||||
export * from "./utils";
|
||||
@ -0,0 +1 @@
|
||||
export * from "./select-customer-dialog";
|
||||
@ -0,0 +1,139 @@
|
||||
import { SimpleSearchInput } from "@erp/core/components";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemTitle,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
|
||||
import type { CustomerSelectionOption } from "../../../../../common/features";
|
||||
import { useTranslation } from "../../../../../web/i18n";
|
||||
import type { useSelectCustomerDialogController } from "../../controllers";
|
||||
|
||||
import { SelectCustomerEmptyCard } from "./select-customer-empty-card";
|
||||
|
||||
type SelectCustomerDialogController = ReturnType<typeof useSelectCustomerDialogController>;
|
||||
|
||||
/*export const useSelectCustomerDialogController = () => {
|
||||
isOpen: boolean;
|
||||
closeDialog: () => void;
|
||||
|
||||
search: string;
|
||||
setSearchValue: (value: string) => void;
|
||||
|
||||
customers: CustomerSelectionOption[];
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
|
||||
isErrror: boolean;
|
||||
error: unknown;
|
||||
|
||||
|
||||
|
||||
selectCustomer: (customer: CustomerSelectionOption) => void;
|
||||
};*/
|
||||
|
||||
type SelectCustomerDialogProps = {
|
||||
ctrl: SelectCustomerDialogController;
|
||||
onCreateNewCustomerClick?: () => void;
|
||||
};
|
||||
|
||||
export const SelectCustomerDialog = ({
|
||||
ctrl,
|
||||
onCreateNewCustomerClick,
|
||||
}: SelectCustomerDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog onOpenChange={(open) => !open && ctrl.closeDialog()} open={ctrl.isOpen}>
|
||||
<DialogContent className="flex max-h-[85vh] flex-col sm:max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("customers.select_customer.title", "Seleccionar cliente")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<SimpleSearchInput
|
||||
autoFocus
|
||||
loading={ctrl.isLoading}
|
||||
onSearchChange={ctrl.setSearchValue}
|
||||
placeholder={t(
|
||||
"customers.select_customer.search_placeholder",
|
||||
"Buscar por nombre, NIF o email"
|
||||
)}
|
||||
value={ctrl.search}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
{onCreateNewCustomerClick ? (
|
||||
<Button onClick={onCreateNewCustomerClick} type="button" variant="outline">
|
||||
{t("customers.select_customer.new_customer", "Nuevo cliente")}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<CustomerSelectionList
|
||||
customers={ctrl.customers}
|
||||
isLoading={ctrl.isLoading}
|
||||
onSelect={ctrl.selectCustomer}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
type CustomerSelectionListProps = {
|
||||
customers: CustomerSelectionOption[];
|
||||
isLoading: boolean;
|
||||
onSelect: (customer: CustomerSelectionOption) => void;
|
||||
};
|
||||
|
||||
const CustomerSelectionList = ({ customers, isLoading, onSelect }: CustomerSelectionListProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
//const columns = useCustomersGridColumns({});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-2 py-2">
|
||||
<div className="h-14 rounded-md border" />
|
||||
<div className="h-14 rounded-md border" />
|
||||
<div className="h-14 rounded-md border" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (customers.length === 0) {
|
||||
return <SelectCustomerEmptyCard />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemGroup>
|
||||
{customers.map((customer) => (
|
||||
<Item
|
||||
asChild
|
||||
className={cn("bg-muted/50 font-medium transition hover:text-primary hover:bg-muted")}
|
||||
key={customer.id}
|
||||
onClick={() => onSelect(customer)}
|
||||
role="listitem"
|
||||
>
|
||||
<a href="#" rel="noopener noreferrer" target="_blank">
|
||||
<ItemContent>
|
||||
<ItemTitle className="line-clamp-1">{customer.name}</ItemTitle>
|
||||
<ItemDescription>{customer.tin}</ItemDescription>
|
||||
</ItemContent>
|
||||
</a>
|
||||
</Item>
|
||||
))}
|
||||
</ItemGroup>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { UserSearchIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../../web/i18n";
|
||||
|
||||
export const SelectCustomerEmptyCard = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Empty className="h-full bg-muted/30">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<UserSearchIcon />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>
|
||||
{t("customers.select_customer.empty", "No se han encontrado clientes")}
|
||||
</EmptyTitle>
|
||||
<EmptyDescription className="max-w-xs text-pretty">
|
||||
You're all caught up. New notifications will appear here.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from "./dialogs/select-customer-dialog";
|
||||
@ -0,0 +1,23 @@
|
||||
import type { Customer, CustomerListRow } from "../../../../web/shared";
|
||||
import type { CustomerSelectionOption } from "../entities";
|
||||
|
||||
/**
|
||||
* Mapper explícito para desacoplar UI del shape de customers/shared.
|
||||
*/
|
||||
export const buildCustomerSelectionOption = (
|
||||
customer: CustomerListRow | Customer
|
||||
): CustomerSelectionOption => {
|
||||
return {
|
||||
id: customer.id,
|
||||
status: customer.status,
|
||||
|
||||
isCompany: customer.isCompany,
|
||||
name: customer.name,
|
||||
tin: customer.tin,
|
||||
|
||||
EmailPrimary: customer.EmailPrimary,
|
||||
|
||||
languageCode: customer.languageCode,
|
||||
currencyCode: customer.currencyCode,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from './build-customer-selection-option';
|
||||
2
modules/customers/src/common/features/index.ts
Normal file
2
modules/customers/src/common/features/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./customer-selection";
|
||||
export * from "./quick-create-customer";
|
||||
@ -0,0 +1,2 @@
|
||||
export * from './quick-customer-create-form.entity';
|
||||
export * from './quick-customer-create-result.entity';
|
||||
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Formulario mínimo para alta rápida desde otros módulos.
|
||||
* SOLO campos necesarios para poder usar el cliente inmediatamente.
|
||||
*/
|
||||
export interface QuickCustomerCreateForm {
|
||||
isCompany: boolean;
|
||||
name: string;
|
||||
tradeName: string;
|
||||
tin: string;
|
||||
|
||||
EmailPrimary: string;
|
||||
|
||||
languageCode: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Resultado simplificado del alta rápida.
|
||||
* Compatible con CustomerSelectionOption para integración directa.
|
||||
*/
|
||||
export interface QuickCustomerCreateResult {
|
||||
id: string;
|
||||
status: string;
|
||||
|
||||
isCompany?: boolean;
|
||||
name: string;
|
||||
tin: string;
|
||||
|
||||
EmailPrimary: string;
|
||||
|
||||
languageCode: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./entities";
|
||||
export * from "./utils";
|
||||
@ -0,0 +1,20 @@
|
||||
import type { CreateCustomerParams } from "../../../../web/shared";
|
||||
import type { QuickCustomerCreateForm } from "../entities";
|
||||
|
||||
/**
|
||||
* Builder explícito Form → Params (regla del proyecto).
|
||||
*/
|
||||
export const buildCreateCustomerParamsFromQuickForm = (
|
||||
form: QuickCustomerCreateForm
|
||||
): CreateCustomerParams => {
|
||||
return {
|
||||
data: {
|
||||
name: form.name,
|
||||
is_company: form.isCompany ? "1" : "0",
|
||||
tin: form.tin,
|
||||
email_primary: form.EmailPrimary,
|
||||
language_code: form.languageCode,
|
||||
currency_code: form.currencyCode,
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
import type { QuickCustomerCreateForm } from "../entities";
|
||||
|
||||
export const buildQuickCustomerCreateDefaultValues = (
|
||||
overrides?: Partial<QuickCustomerCreateForm>
|
||||
): QuickCustomerCreateForm => {
|
||||
return {
|
||||
isCompany: true,
|
||||
name: "",
|
||||
tradeName: "",
|
||||
tin: "",
|
||||
|
||||
EmailPrimary: "",
|
||||
|
||||
languageCode: overrides?.languageCode ?? "es",
|
||||
currencyCode: overrides?.currencyCode ?? "EUR",
|
||||
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
export * from './build-create-customer-params-from-quick-form';
|
||||
export * from './build-quick-customer-create-default-values';
|
||||
@ -1 +1,2 @@
|
||||
export * from "./dto";
|
||||
export * from "./features";
|
||||
|
||||
2
modules/customers/src/web/features/index.ts
Normal file
2
modules/customers/src/web/features/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./customer-selection";
|
||||
export * from "./quick-create-customer";
|
||||
@ -0,0 +1 @@
|
||||
export * from './use-quick-create-customer-dialog-controller';
|
||||
@ -0,0 +1,87 @@
|
||||
import { useHookForm } from "@erp/core/hooks";
|
||||
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
||||
import { useId, useState } from "react";
|
||||
|
||||
import {
|
||||
type CustomerSelectionOption,
|
||||
type QuickCustomerCreateForm,
|
||||
buildCreateCustomerParamsFromQuickForm,
|
||||
buildCustomerSelectionOption,
|
||||
buildQuickCustomerCreateDefaultValues,
|
||||
} from "../../../../common/features";
|
||||
import { useTranslation } from "../../../i18n";
|
||||
import { useCustomerCreateMutation } from "../../../shared/hooks";
|
||||
|
||||
type UseQuickCreateCustomerDialogControllerOptions = {
|
||||
onCreated(customer: CustomerSelectionOption): void;
|
||||
defaultLanguageCode?: string;
|
||||
defaultCurrencyCode?: string;
|
||||
};
|
||||
|
||||
export const useQuickCreateCustomerDialogController = (
|
||||
options: UseQuickCreateCustomerDialogControllerOptions
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const formId = useId();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { mutateAsync, isPending } = useCustomerCreateMutation();
|
||||
|
||||
const form = useHookForm<QuickCustomerCreateForm>({
|
||||
initialValues: buildQuickCustomerCreateDefaultValues({
|
||||
languageCode: options.defaultLanguageCode,
|
||||
currencyCode: options.defaultCurrencyCode,
|
||||
}),
|
||||
});
|
||||
|
||||
const openDialog = () => setIsOpen(true);
|
||||
|
||||
const closeDialog = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const onSubmit = form.handleSubmit(async (formData) => {
|
||||
try {
|
||||
const params = buildCreateCustomerParamsFromQuickForm(formData);
|
||||
const created = await mutateAsync(params);
|
||||
|
||||
const option = buildCustomerSelectionOption(created);
|
||||
|
||||
options.onCreated(option);
|
||||
|
||||
showSuccessToast(
|
||||
t("customers.quick_create.success.title", "Cliente creado"),
|
||||
t("customers.quick_create.success.message", "Se ha creado correctamente")
|
||||
);
|
||||
|
||||
form.reset(
|
||||
buildQuickCustomerCreateDefaultValues({
|
||||
languageCode: options.defaultLanguageCode,
|
||||
currencyCode: options.defaultCurrencyCode,
|
||||
})
|
||||
);
|
||||
|
||||
closeDialog();
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error("Unknown error");
|
||||
|
||||
showErrorToast(
|
||||
t("customers.quick_create.error.title", "Error al crear cliente"),
|
||||
err.message
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
formId,
|
||||
form,
|
||||
|
||||
isOpen,
|
||||
openDialog,
|
||||
closeDialog,
|
||||
|
||||
onSubmit,
|
||||
isSubmitting: isPending,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./controllers";
|
||||
export * from "./ui";
|
||||
@ -0,0 +1 @@
|
||||
export * from "./quick-customer-create-form-fields";
|
||||
@ -0,0 +1 @@
|
||||
export * from './quick-create-customer-dialog';
|
||||
@ -0,0 +1,60 @@
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { FormProvider } from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
|
||||
type QuickCreateCustomerDialogController = {
|
||||
formId: string;
|
||||
form: ReturnType<any>;
|
||||
isOpen: boolean;
|
||||
closeDialog: () => void;
|
||||
onSubmit: React.FormEventHandler<HTMLFormElement>;
|
||||
isSubmitting: boolean;
|
||||
};
|
||||
|
||||
type QuickCreateCustomerDialogProps = {
|
||||
ctrl: QuickCreateCustomerDialogController;
|
||||
};
|
||||
|
||||
export const QuickCreateCustomerDialog = ({ ctrl }: QuickCreateCustomerDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog onOpenChange={(open) => !open && ctrl.closeDialog()} open={ctrl.isOpen}>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("customers.quick_create.title", "Nuevo cliente")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<FormProvider {...ctrl.form}>
|
||||
<form className="space-y-6" id={ctrl.formId} onSubmit={ctrl.onSubmit}>
|
||||
<QuickCustomerCreateFormFields />
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
disabled={ctrl.isSubmitting}
|
||||
onClick={ctrl.closeDialog}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
{t("common.cancel", "Cancelar")}
|
||||
</Button>
|
||||
|
||||
<Button disabled={ctrl.isSubmitting} type="submit">
|
||||
{ctrl.isSubmitting
|
||||
? t("common.saving", "Guardando...")
|
||||
: t("customers.quick_create.submit", "Crear cliente")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from "./dialogs";
|
||||
@ -1,10 +1,11 @@
|
||||
import { useUrlParamId } from "@erp/core/hooks";
|
||||
|
||||
import { useCustomerUpdateController } from "./use-customer-update.controller";
|
||||
|
||||
/**
|
||||
* Hook para gestionar el controlador de la página de actualización de cliente.
|
||||
*
|
||||
* @returns
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export const useCustomerUpdatePageController = () => {
|
||||
@ -15,4 +16,4 @@ export const useCustomerUpdatePageController = () => {
|
||||
return {
|
||||
updateCtrl,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@ -81,7 +81,7 @@ export const TextField = <TFormValues extends FieldValues>({
|
||||
|
||||
<InputGroup
|
||||
className={cn(
|
||||
"bg-muted/50 font-medium",
|
||||
"bg-muted/50 font-medium transition",
|
||||
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||
"placeholder:text-muted-foreground/50",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user