Proforma update
This commit is contained in:
parent
2eab047e90
commit
ec8e734851
@ -16,6 +16,9 @@ type SimpleSearchInputProps = {
|
|||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
maxHistory?: number;
|
maxHistory?: number;
|
||||||
|
|
||||||
|
placeholder?: string;
|
||||||
|
autoFocus?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SEARCH_HISTORY_KEY = "search_history";
|
const SEARCH_HISTORY_KEY = "search_history";
|
||||||
@ -27,6 +30,8 @@ export const SimpleSearchInput = ({
|
|||||||
onSearchChange,
|
onSearchChange,
|
||||||
loading = false,
|
loading = false,
|
||||||
maxHistory = 8,
|
maxHistory = 8,
|
||||||
|
placeholder,
|
||||||
|
autoFocus = false,
|
||||||
}: SimpleSearchInputProps) => {
|
}: SimpleSearchInputProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchValue, setSearchValue] = useState(value);
|
const [searchValue, setSearchValue] = useState(value);
|
||||||
@ -131,11 +136,14 @@ export const SimpleSearchInput = ({
|
|||||||
<InputGroup className="bg-background">
|
<InputGroup className="bg-background">
|
||||||
<InputGroupInput
|
<InputGroupInput
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
autoFocus={autoFocus}
|
||||||
inputMode="search"
|
inputMode="search"
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onFocus={() => history.length > 0 && setOpen(true)}
|
onFocus={() => history.length > 0 && setOpen(true)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={t("components.simple_search_input.search_placeholder", "Search...")}
|
placeholder={
|
||||||
|
placeholder || t("components.simple_search_input.search_placeholder", "Search...")
|
||||||
|
}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export * from "./proforma-to-proforma-update-form.adapter";
|
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 { useHookForm } from "@erp/core/hooks";
|
||||||
|
import type { CustomerSelectionOption } from "@erp/customers";
|
||||||
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
|
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 type { FieldErrors } from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import type { UpdateProformaByIdParams } from "../../shared";
|
import type { UpdateProformaByIdParams } from "../../shared";
|
||||||
import type { Proforma } from "../../shared/entities";
|
import type { Proforma } from "../../shared/entities";
|
||||||
import { useProformaGetQuery, useProformaUpdateMutation } from "../../shared/hooks";
|
import { useProformaGetQuery, useProformaUpdateMutation } from "../../shared/hooks";
|
||||||
import { mapProformaToProformaUpdateForm } from "../adapters";
|
import { mapProformaToProformaUpdateForm, mapProformaToSelectedCustomer } from "../adapters";
|
||||||
import {
|
import {
|
||||||
type ProformaUpdateForm,
|
type ProformaUpdateForm,
|
||||||
ProformaUpdateFormSchema,
|
ProformaUpdateFormSchema,
|
||||||
@ -63,6 +64,8 @@ export const useUpdateProformaController = (
|
|||||||
disabled: isLoading || isUpdating,
|
disabled: isLoading || isUpdating,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [selectedCustomer, setSelectedCustomer] = useState<CustomerSelectionOption | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!proformaData) return;
|
if (!proformaData) return;
|
||||||
|
|
||||||
@ -70,6 +73,8 @@ export const useUpdateProformaController = (
|
|||||||
form.reset(mapProformaToProformaUpdateForm(proformaData), {
|
form.reset(mapProformaToProformaUpdateForm(proformaData), {
|
||||||
keepDirty: false, // <-- importante: no marca el form como "dirty" al cargar los datos reales
|
keepDirty: false, // <-- importante: no marca el form como "dirty" al cargar los datos reales
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setSelectedCustomer(mapProformaToSelectedCustomer(proformaData));
|
||||||
}, [proformaData, form]);
|
}, [proformaData, form]);
|
||||||
|
|
||||||
/** Handlers */
|
/** Handlers */
|
||||||
@ -80,6 +85,27 @@ export const useUpdateProformaController = (
|
|||||||
: defaultProformaUpdateForm;
|
: defaultProformaUpdateForm;
|
||||||
|
|
||||||
form.reset(initialData, { keepDirty: false });
|
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(
|
const submitHandler = form.handleSubmit(
|
||||||
@ -104,6 +130,8 @@ export const useUpdateProformaController = (
|
|||||||
keepDirty: false,
|
keepDirty: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setSelectedCustomer(mapProformaToSelectedCustomer(proformaData));
|
||||||
|
|
||||||
if (options?.successToasts !== false) {
|
if (options?.successToasts !== false) {
|
||||||
showSuccessToast(
|
showSuccessToast(
|
||||||
t("proformas.update.success.title"),
|
t("proformas.update.success.title"),
|
||||||
@ -164,6 +192,10 @@ export const useUpdateProformaController = (
|
|||||||
isUpdateError,
|
isUpdateError,
|
||||||
updateError,
|
updateError,
|
||||||
|
|
||||||
|
selectedCustomer,
|
||||||
|
setCustomer,
|
||||||
|
clearCustomer,
|
||||||
|
|
||||||
// No devolver FormProvider, así el controller es más
|
// No devolver FormProvider, así el controller es más
|
||||||
// flexible y reusable (p.ej. para un modal)
|
// flexible y reusable (p.ej. para un modal)
|
||||||
// FormProvider,
|
// FormProvider,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useUrlParamId } from "@erp/core/hooks";
|
import { useUrlParamId } from "@erp/core/hooks";
|
||||||
|
import { useCustomerSelectionFlow } from "@erp/customers/common";
|
||||||
|
|
||||||
import { useUpdateProformaController } from "./use-update-proforma-controller";
|
import { useUpdateProformaController } from "./use-update-proforma-controller";
|
||||||
|
|
||||||
@ -7,7 +8,18 @@ export const useUpdateProformaPageController = () => {
|
|||||||
|
|
||||||
const updateCtrl = useUpdateProformaController(proformaId);
|
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 {
|
return {
|
||||||
updateCtrl,
|
updateCtrl,
|
||||||
|
selectCustomerCtrl,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
export * from "./proforma-basic-info-fields";
|
||||||
export * from "./proforma-form-field-shell";
|
export * from "./proforma-form-field-shell";
|
||||||
export * from "./proforma-header-fields-card";
|
export * from "./proforma-header-fields-card";
|
||||||
export * from "./proforma-header-form-grid";
|
export * from "./proforma-header-form-grid";
|
||||||
export * from "./proforma-section-card";
|
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-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
|
// 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 { Button } from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
|
||||||
import { ProformaUpdateHeaderEditor } from ".";
|
import { ProformaUpdateRecipientEditor } from ".";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type { Proforma } from "../../../shared/entities";
|
import type { Proforma } from "../../../shared/entities";
|
||||||
import { ProformaHeaderFieldsCard } from "../blocks/proforma-header-fields-card";
|
import { ProformaHeaderFieldsCard } from "../blocks/proforma-header-fields-card";
|
||||||
|
|
||||||
|
import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor";
|
||||||
|
|
||||||
type ProformaUpdateEditorProps = {
|
type ProformaUpdateEditorProps = {
|
||||||
formId: string;
|
formId: string;
|
||||||
proforma?: Proforma;
|
proforma?: Proforma;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
onSubmit: React.FormEventHandler<HTMLFormElement>;
|
onSubmit: React.FormEventHandler<HTMLFormElement>;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
|
|
||||||
|
selectedCustomer?: CustomerSelectionOption | null;
|
||||||
|
onChangeCustomerClick: () => void;
|
||||||
|
onCreateCustomerClick: () => void;
|
||||||
|
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -23,6 +31,9 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
isSubmitting,
|
isSubmitting,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onReset,
|
onReset,
|
||||||
|
selectedCustomer,
|
||||||
|
onChangeCustomerClick,
|
||||||
|
onCreateCustomerClick,
|
||||||
className,
|
className,
|
||||||
}: ProformaUpdateEditorProps) => {
|
}: ProformaUpdateEditorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -30,16 +41,13 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<ProformaUpdateHeaderEditor
|
<ProformaUpdateHeaderEditor disabled={isSubmitting} />
|
||||||
currencyOptions={[]}
|
|
||||||
customerOptions={[]}
|
<ProformaUpdateRecipientEditor
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
paymentMethodOptions={[]}
|
onChangeCustomerClick={onChangeCustomerClick}
|
||||||
priceListOptions={[]}
|
onCreateCustomerClick={onCreateCustomerClick}
|
||||||
salesPersonOptions={[]}
|
selectedCustomer={selectedCustomer}
|
||||||
serieOptions={[]}
|
|
||||||
statusOptions={[]}
|
|
||||||
warehouseOptions={[]}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col-reverse gap-3 border-t pt-4 sm:flex-row sm:justify-end">
|
<div className="flex flex-col-reverse gap-3 border-t pt-4 sm:flex-row sm:justify-end">
|
||||||
|
|||||||
@ -1,355 +1,63 @@
|
|||||||
import { DatePickerField, SelectField, TextField } from "@repo/rdx-ui/components";
|
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 { useTranslation } from "../../../../i18n";
|
||||||
import type { ProformaUpdateForm } from "../../entities";
|
import { ProformaHeaderFormGrid, ProformaSectionCard } from "../blocks";
|
||||||
import { ProformaFormFieldShell, ProformaHeaderFormGrid, ProformaSectionCard } from "../blocks";
|
|
||||||
|
|
||||||
interface SelectOption {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProformaUpdateHeaderEditorProps {
|
interface ProformaUpdateHeaderEditorProps {
|
||||||
statusOptions: SelectOption[];
|
|
||||||
serieOptions: SelectOption[];
|
|
||||||
customerOptions: SelectOption[];
|
|
||||||
currencyOptions: SelectOption[];
|
|
||||||
paymentMethodOptions: SelectOption[];
|
|
||||||
salesPersonOptions: SelectOption[];
|
|
||||||
warehouseOptions: SelectOption[];
|
|
||||||
priceListOptions: SelectOption[];
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProformaUpdateHeaderEditor = ({
|
export const ProformaUpdateHeaderEditor = ({
|
||||||
statusOptions,
|
|
||||||
serieOptions,
|
|
||||||
customerOptions,
|
|
||||||
currencyOptions,
|
|
||||||
paymentMethodOptions,
|
|
||||||
salesPersonOptions,
|
|
||||||
warehouseOptions,
|
|
||||||
priceListOptions,
|
|
||||||
disabled = false,
|
disabled = false,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
}: ProformaUpdateHeaderEditorProps) => {
|
}: ProformaUpdateHeaderEditorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
control,
|
|
||||||
formState: { errors },
|
|
||||||
} = useFormContext<ProformaUpdateForm>();
|
|
||||||
|
|
||||||
const isFieldLocked = disabled || readOnly;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<ProformaSectionCard
|
||||||
<ProformaSectionCard
|
description={t("form_groups.proformas.basic_info.description")}
|
||||||
description={t("form_groups.proformas.basic_info.description")}
|
title={t("form_groups.proformas.basic_info.title")}
|
||||||
title={t("form_groups.proformas.basic_info.title")}
|
>
|
||||||
>
|
<ProformaHeaderFormGrid>
|
||||||
<ProformaHeaderFormGrid>
|
<SelectField
|
||||||
<SelectField
|
className="md:col-span-2"
|
||||||
className="md:col-span-2"
|
label={t("form_fields.proformas.series.label")}
|
||||||
label={t("form_fields.proformas.series.label")}
|
name="series"
|
||||||
name="series"
|
placeholder={t("form_fields.proformas.series.placeholder")}
|
||||||
placeholder={t("form_fields.proformas.series.placeholder")}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<DatePickerField
|
<DatePickerField
|
||||||
className="md:col-span-3"
|
className="md:col-span-3"
|
||||||
label={t("form_fields.proformas.invoice_date.label")}
|
label={t("form_fields.proformas.invoice_date.label")}
|
||||||
name="invoice_date"
|
name="invoice_date"
|
||||||
placeholder={t("form_fields.proformas.invoice_date.placeholder")}
|
placeholder={t("form_fields.proformas.invoice_date.placeholder")}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DatePickerField
|
<DatePickerField
|
||||||
className="md:col-span-3"
|
className="md:col-span-3"
|
||||||
label={t("form_fields.proformas.operation_date.label")}
|
label={t("form_fields.proformas.operation_date.label")}
|
||||||
name="operation_date"
|
name="operation_date"
|
||||||
placeholder={t("form_fields.proformas.operation_date.placeholder")}
|
placeholder={t("form_fields.proformas.operation_date.placeholder")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="md:col-span-4"
|
className="md:col-span-4"
|
||||||
label={t("form_fields.proformas.reference.label")}
|
label={t("form_fields.proformas.reference.label")}
|
||||||
maxLength={256}
|
maxLength={256}
|
||||||
name="reference"
|
name="reference"
|
||||||
placeholder={t("form_fields.proformas.reference.placeholder")}
|
placeholder={t("form_fields.proformas.reference.placeholder")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="md:col-span-12"
|
className="md:col-span-12"
|
||||||
label={t("form_fields.proformas.description.label")}
|
label={t("form_fields.proformas.description.label")}
|
||||||
maxLength={256}
|
maxLength={256}
|
||||||
name="description"
|
name="description"
|
||||||
placeholder={t("form_fields.proformas.description.placeholder")}
|
placeholder={t("form_fields.proformas.description.placeholder")}
|
||||||
/>
|
/>
|
||||||
</ProformaHeaderFormGrid>
|
</ProformaHeaderFormGrid>
|
||||||
</ProformaSectionCard>
|
</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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 { SpainTaxCatalogProvider } from "@erp/core";
|
||||||
import { ErrorAlert, NotFoundCard, PageHeader } from "@erp/core/components";
|
import { ErrorAlert, NotFoundCard, PageHeader } from "@erp/core/components";
|
||||||
import { UnsavedChangesProvider, UpdateCommitButtonGroup } from "@erp/core/hooks";
|
import { UnsavedChangesProvider, UpdateCommitButtonGroup } from "@erp/core/hooks";
|
||||||
|
import { SelectCustomerDialog } from "@erp/customers";
|
||||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
import { Spinner } from "@repo/shadcn-ui/components";
|
import { Spinner } from "@repo/shadcn-ui/components";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
@ -15,7 +16,7 @@ export const ProformaUpdatePage = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []);
|
const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []);
|
||||||
|
|
||||||
const { updateCtrl } = useUpdateProformaPageController();
|
const { updateCtrl, selectCustomerCtrl } = useUpdateProformaPageController();
|
||||||
|
|
||||||
if (updateCtrl.isLoading) {
|
if (updateCtrl.isLoading) {
|
||||||
return <ProformaUpdateSkeleton />;
|
return <ProformaUpdateSkeleton />;
|
||||||
@ -95,14 +96,23 @@ export const ProformaUpdatePage = () => {
|
|||||||
{updateCtrl.isLoading && <Spinner />}
|
{updateCtrl.isLoading && <Spinner />}
|
||||||
|
|
||||||
{!updateCtrl.isLoading && (
|
{!updateCtrl.isLoading && (
|
||||||
<FormProvider {...updateCtrl.form}>
|
<>
|
||||||
<ProformaUpdateEditorForm
|
<FormProvider {...updateCtrl.form}>
|
||||||
formId={updateCtrl.formId}
|
<ProformaUpdateEditorForm
|
||||||
isSubmitting={updateCtrl.isUpdating}
|
formId={updateCtrl.formId}
|
||||||
onReset={updateCtrl.resetForm}
|
isSubmitting={updateCtrl.isUpdating}
|
||||||
onSubmit={updateCtrl.onSubmit}
|
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>
|
</AppContent>
|
||||||
</UnsavedChangesProvider>
|
</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 "./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 { useUrlParamId } from "@erp/core/hooks";
|
||||||
|
|
||||||
import { useCustomerUpdateController } from "./use-customer-update.controller";
|
import { useCustomerUpdateController } from "./use-customer-update.controller";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook para gestionar el controlador de la página de actualización de cliente.
|
* Hook para gestionar el controlador de la página de actualización de cliente.
|
||||||
*
|
*
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const useCustomerUpdatePageController = () => {
|
export const useCustomerUpdatePageController = () => {
|
||||||
@ -15,4 +16,4 @@ export const useCustomerUpdatePageController = () => {
|
|||||||
return {
|
return {
|
||||||
updateCtrl,
|
updateCtrl,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -81,7 +81,7 @@ export const TextField = <TFormValues extends FieldValues>({
|
|||||||
|
|
||||||
<InputGroup
|
<InputGroup
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted/50 font-medium",
|
"bg-muted/50 font-medium transition",
|
||||||
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||||
"placeholder:text-muted-foreground/50",
|
"placeholder:text-muted-foreground/50",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user