Compare commits
No commits in common. "3182771455c9351d831a913c11f1aa3837a5fd43" and "c28a5e02c334d5cd905c44fb5112fe68914f62f1" have entirely different histories.
3182771455
...
c28a5e02c3
@ -1,6 +1,6 @@
|
|||||||
import { ErrorAlert } from "@erp/core/components";
|
import { ErrorAlert } from "@erp/core/components";
|
||||||
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import { QuickCreateCustomerDialog, SelectCustomerDialog } from "@erp/customers";
|
import { SelectCustomerDialog } from "@erp/customers";
|
||||||
import { FormProvider } from "react-hook-form";
|
import { FormProvider } from "react-hook-form";
|
||||||
|
|
||||||
import { ProformaInfoAlert } from "../../../update/ui/components";
|
import { ProformaInfoAlert } from "../../../update/ui/components";
|
||||||
@ -66,11 +66,7 @@ export const ProformaCreatePage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SelectCustomerDialog
|
<SelectCustomerDialog ctrl={selectCustomerCtrl.selectCtrl} />
|
||||||
ctrl={selectCustomerCtrl.selectCtrl}
|
|
||||||
onCreateNewCustomerClick={selectCustomerCtrl.openQuickCreateFromSelection}
|
|
||||||
/>
|
|
||||||
<QuickCreateCustomerDialog ctrl={selectCustomerCtrl.createCtrl} />
|
|
||||||
</UnsavedChangesProvider>
|
</UnsavedChangesProvider>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { CustomerSelectionOption } from "@erp/customers";
|
import type { CustomerSelectionOption } from "@erp/customers/common";
|
||||||
|
|
||||||
import type { Proforma } from "../../shared/entities";
|
import type { Proforma } from "../../shared/entities";
|
||||||
|
|
||||||
@ -21,19 +21,19 @@ export const mapProformaToSelectedCustomer = (
|
|||||||
|
|
||||||
id: proforma.customerId,
|
id: proforma.customerId,
|
||||||
name: proforma.recipient.name ?? "",
|
name: proforma.recipient.name ?? "",
|
||||||
tradeName: null,
|
tradeName: "",
|
||||||
tin: proforma.recipient.tin,
|
tin: proforma.recipient.tin ?? "",
|
||||||
|
|
||||||
street: proforma.recipient.street,
|
street: proforma.recipient.street ?? "",
|
||||||
street2: proforma.recipient.street2,
|
street2: proforma.recipient.street2 ?? "",
|
||||||
city: proforma.recipient.city,
|
city: proforma.recipient.city ?? "",
|
||||||
province: proforma.recipient.province,
|
province: proforma.recipient.province ?? "",
|
||||||
postalCode: proforma.recipient.postalCode,
|
postalCode: proforma.recipient.postalCode ?? "",
|
||||||
country: proforma.recipient.country,
|
country: proforma.recipient.country ?? "",
|
||||||
|
|
||||||
primaryEmail: null,
|
primaryEmail: "",
|
||||||
primaryMobile: null,
|
primaryMobile: "",
|
||||||
primaryPhone: null,
|
primaryPhone: "",
|
||||||
|
|
||||||
isCompany: true,
|
isCompany: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -56,21 +56,6 @@ const normalizeSubmitError = (error: unknown): Error | ValidationErrorCollection
|
|||||||
return new Error("Unknown error");
|
return new Error("Unknown error");
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveSelectedCustomerState = (
|
|
||||||
proforma: Proforma | null | undefined,
|
|
||||||
currentSelectedCustomer: CustomerSelectionOption | null
|
|
||||||
): CustomerSelectionOption | null => {
|
|
||||||
if (!proforma?.customerId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentSelectedCustomer?.id === proforma.customerId) {
|
|
||||||
return currentSelectedCustomer;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapProformaToSelectedCustomer(proforma);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useUpdateProformaController = (
|
export const useUpdateProformaController = (
|
||||||
proformaId?: string,
|
proformaId?: string,
|
||||||
options?: UseUpdateProformaControllerOptions
|
options?: UseUpdateProformaControllerOptions
|
||||||
@ -122,9 +107,7 @@ export const useUpdateProformaController = (
|
|||||||
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((currentSelectedCustomer) =>
|
setSelectedCustomer(mapProformaToSelectedCustomer(proformaData));
|
||||||
resolveSelectedCustomerState(proformaData, currentSelectedCustomer)
|
|
||||||
);
|
|
||||||
}, [proformaData, form]);
|
}, [proformaData, form]);
|
||||||
|
|
||||||
/** Handlers */
|
/** Handlers */
|
||||||
@ -135,9 +118,7 @@ export const useUpdateProformaController = (
|
|||||||
: buildProformaUpdateDefault();
|
: buildProformaUpdateDefault();
|
||||||
|
|
||||||
form.reset(initialData, { keepDirty: false });
|
form.reset(initialData, { keepDirty: false });
|
||||||
setSelectedCustomer((currentSelectedCustomer) =>
|
setSelectedCustomer(mapProformaToSelectedCustomer(proformaData));
|
||||||
resolveSelectedCustomerState(proformaData, currentSelectedCustomer)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setCustomer = (customer: CustomerSelectionOption) => {
|
const setCustomer = (customer: CustomerSelectionOption) => {
|
||||||
@ -199,9 +180,7 @@ export const useUpdateProformaController = (
|
|||||||
keepDirty: false,
|
keepDirty: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
setSelectedCustomer((currentSelectedCustomer) =>
|
setSelectedCustomer(mapProformaToSelectedCustomer(updated));
|
||||||
resolveSelectedCustomerState(updated, currentSelectedCustomer)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (options?.successToasts !== false) {
|
if (options?.successToasts !== false) {
|
||||||
showSuccessToast(
|
showSuccessToast(
|
||||||
|
|||||||
@ -1,169 +0,0 @@
|
|||||||
import assert from "node:assert/strict";
|
|
||||||
import { mock, test } from "node:test";
|
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
|
||||||
|
|
||||||
test("conecta la alta rápida directa y la alta rápida desde selector con callbacks distintos", async () => {
|
|
||||||
const openSelectDialog = () => undefined;
|
|
||||||
const openQuickCreateDialog = () => undefined;
|
|
||||||
const openQuickCreateFromSelection = () => undefined;
|
|
||||||
const quickCreateCtrl = {
|
|
||||||
isOpen: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let capturedEditorProps: Record<string, unknown> | undefined;
|
|
||||||
let capturedSelectDialogProps: Record<string, unknown> | undefined;
|
|
||||||
let capturedQuickCreateDialogProps: Record<string, unknown> | undefined;
|
|
||||||
|
|
||||||
mock.module("@erp/core/components", {
|
|
||||||
namedExports: {
|
|
||||||
ErrorAlert: () => null,
|
|
||||||
PageErrorState: () => null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mock.module("@erp/core/hooks", {
|
|
||||||
namedExports: {
|
|
||||||
UnsavedChangesProvider: ({ children }: { children: ReactNode }) => children,
|
|
||||||
useReturnToNavigation: () => ({
|
|
||||||
navigateBack: () => undefined,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mock.module("@erp/customers", {
|
|
||||||
namedExports: {
|
|
||||||
QuickCreateCustomerDialog: (props: Record<string, unknown>) => {
|
|
||||||
capturedQuickCreateDialogProps = props;
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
SelectCustomerDialog: (props: Record<string, unknown>) => {
|
|
||||||
capturedSelectDialogProps = props;
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mock.module("@repo/rdx-ui/components", {
|
|
||||||
namedExports: {
|
|
||||||
AppContent: ({ children }: { children: ReactNode }) => children,
|
|
||||||
CancelActionButton: () => null,
|
|
||||||
PercentageField: () => null,
|
|
||||||
RdxEditorStickyFooter: () => null,
|
|
||||||
RhfSubmitActionButton: () => null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mock.module("react-hook-form", {
|
|
||||||
namedExports: {
|
|
||||||
FormProvider: ({ children }: { children: ReactNode }) => children,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mock.module("../../../../i18n", {
|
|
||||||
namedExports: {
|
|
||||||
useTranslation: () => ({
|
|
||||||
t: (_key: string, fallback?: string) => fallback ?? _key,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mock.module("../../controllers/use-update-proforma-page-controller", {
|
|
||||||
namedExports: {
|
|
||||||
useUpdateProformaPageController: () => ({
|
|
||||||
updateCtrl: {
|
|
||||||
form: {
|
|
||||||
formState: {
|
|
||||||
isDirty: false,
|
|
||||||
isSubmitting: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
formId: "proforma-update-form",
|
|
||||||
isUpdating: false,
|
|
||||||
isUpdateError: false,
|
|
||||||
isSeriesLoadError: false,
|
|
||||||
itemsCtrl: {},
|
|
||||||
onSubmit: () => undefined,
|
|
||||||
paymentCtrl: {},
|
|
||||||
resetForm: () => undefined,
|
|
||||||
selectedCustomer: {
|
|
||||||
id: "customer-1",
|
|
||||||
status: "active",
|
|
||||||
isCompany: true,
|
|
||||||
name: "Acme",
|
|
||||||
tradeName: null,
|
|
||||||
tin: null,
|
|
||||||
street: null,
|
|
||||||
street2: null,
|
|
||||||
city: null,
|
|
||||||
province: null,
|
|
||||||
postalCode: null,
|
|
||||||
country: null,
|
|
||||||
primaryEmail: null,
|
|
||||||
primaryPhone: null,
|
|
||||||
primaryMobile: null,
|
|
||||||
},
|
|
||||||
seriesOptions: [],
|
|
||||||
taxCtrl: {
|
|
||||||
hasRecPercentage: false,
|
|
||||||
hasRetentionPercentage: false,
|
|
||||||
},
|
|
||||||
totalsCtrl: {
|
|
||||||
totals: {},
|
|
||||||
},
|
|
||||||
currencyCode: "EUR",
|
|
||||||
},
|
|
||||||
selectCustomerCtrl: {
|
|
||||||
selectCtrl: {
|
|
||||||
openDialog: openSelectDialog,
|
|
||||||
},
|
|
||||||
createCtrl: quickCreateCtrl,
|
|
||||||
openQuickCreateDialog,
|
|
||||||
openQuickCreateFromSelection,
|
|
||||||
},
|
|
||||||
returnTo: "/proformas",
|
|
||||||
pageError: null,
|
|
||||||
retryUpdate: async () => undefined,
|
|
||||||
goHome: () => undefined,
|
|
||||||
goBackToList: () => undefined,
|
|
||||||
isRetrying: false,
|
|
||||||
isInitialLoading: false,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mock.module("../blocks", {
|
|
||||||
namedExports: {
|
|
||||||
ProformaCompactTotals: () => null,
|
|
||||||
ProformaUpdateHeader: ({ children }: { children: ReactNode }) => children,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mock.module("../components", {
|
|
||||||
namedExports: {
|
|
||||||
ProformaInfoAlert: ({ children }: { children: ReactNode }) => children,
|
|
||||||
ProformaUpdateSkeleton: () => null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mock.module("../editors", {
|
|
||||||
namedExports: {
|
|
||||||
ProformaUpdateEditorForm: (props: Record<string, unknown>) => {
|
|
||||||
capturedEditorProps = props;
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { ProformaUpdatePage } = await import("./proforma-update-page.tsx");
|
|
||||||
|
|
||||||
renderToStaticMarkup(<ProformaUpdatePage />);
|
|
||||||
|
|
||||||
assert.equal(capturedEditorProps?.onChangeCustomerClick, openSelectDialog);
|
|
||||||
assert.equal(capturedEditorProps?.onCreateCustomerClick, openQuickCreateDialog);
|
|
||||||
assert.equal(capturedSelectDialogProps?.onCreateNewCustomerClick, openQuickCreateFromSelection);
|
|
||||||
assert.equal(capturedQuickCreateDialogProps?.ctrl, quickCreateCtrl);
|
|
||||||
|
|
||||||
mock.restoreAll();
|
|
||||||
});
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { ErrorAlert, PageErrorState } from "@erp/core/components";
|
import { ErrorAlert, PageErrorState } from "@erp/core/components";
|
||||||
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import { QuickCreateCustomerDialog, SelectCustomerDialog } from "@erp/customers";
|
import { SelectCustomerDialog } from "@erp/customers";
|
||||||
import {
|
import {
|
||||||
AppContent,
|
AppContent,
|
||||||
CancelActionButton,
|
CancelActionButton,
|
||||||
@ -141,7 +141,7 @@ export const ProformaUpdatePage = () => {
|
|||||||
isSubmitting={updateCtrl.isUpdating}
|
isSubmitting={updateCtrl.isUpdating}
|
||||||
itemsCtrl={updateCtrl.itemsCtrl}
|
itemsCtrl={updateCtrl.itemsCtrl}
|
||||||
onChangeCustomerClick={selectCustomerCtrl.selectCtrl.openDialog}
|
onChangeCustomerClick={selectCustomerCtrl.selectCtrl.openDialog}
|
||||||
onCreateCustomerClick={selectCustomerCtrl.openQuickCreateDialog}
|
onCreateCustomerClick={() => null}
|
||||||
onReset={updateCtrl.resetForm}
|
onReset={updateCtrl.resetForm}
|
||||||
onSubmit={updateCtrl.onSubmit}
|
onSubmit={updateCtrl.onSubmit}
|
||||||
paymentCtrl={updateCtrl.paymentCtrl}
|
paymentCtrl={updateCtrl.paymentCtrl}
|
||||||
@ -194,11 +194,7 @@ export const ProformaUpdatePage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SelectCustomerDialog
|
<SelectCustomerDialog ctrl={selectCustomerCtrl.selectCtrl} />
|
||||||
ctrl={selectCustomerCtrl.selectCtrl}
|
|
||||||
onCreateNewCustomerClick={selectCustomerCtrl.openQuickCreateFromSelection}
|
|
||||||
/>
|
|
||||||
<QuickCreateCustomerDialog ctrl={selectCustomerCtrl.createCtrl} />
|
|
||||||
</UnsavedChangesProvider>
|
</UnsavedChangesProvider>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,89 +0,0 @@
|
|||||||
import assert from "node:assert/strict";
|
|
||||||
import { mock, test } from "node:test";
|
|
||||||
|
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
|
||||||
|
|
||||||
test("expone apertura directa y apertura desde selector con comportamientos distintos", async () => {
|
|
||||||
let selectDialogOpenCalls = 0;
|
|
||||||
let selectDialogCloseCalls = 0;
|
|
||||||
let quickCreateOpenCalls = 0;
|
|
||||||
let quickCreateOptions:
|
|
||||||
| {
|
|
||||||
onCustomerCreated(customer: { id: string }): void;
|
|
||||||
onCancel?(): void;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
mock.module("../../quick-create-customer", {
|
|
||||||
namedExports: {
|
|
||||||
QuickCreateCustomerDialog: () => null,
|
|
||||||
useQuickCreateCustomerFlow: (options: NonNullable<typeof quickCreateOptions>) => {
|
|
||||||
quickCreateOptions = options;
|
|
||||||
|
|
||||||
return {
|
|
||||||
createCtrl: {
|
|
||||||
openDialog: () => {
|
|
||||||
quickCreateOpenCalls += 1;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mock.module("./use-select-customer-dialog-controller", {
|
|
||||||
namedExports: {
|
|
||||||
useSelectCustomerDialogController: () => ({
|
|
||||||
openDialog: () => {
|
|
||||||
selectDialogOpenCalls += 1;
|
|
||||||
},
|
|
||||||
closeDialog: () => {
|
|
||||||
selectDialogCloseCalls += 1;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mock.module("../ui", {
|
|
||||||
namedExports: {
|
|
||||||
SelectCustomerDialog: () => null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { useCustomerSelectionFlow } = await import("./use-customer-selection-flow.ts");
|
|
||||||
|
|
||||||
const selectedCustomers: Array<{ id: string }> = [];
|
|
||||||
let flow: ReturnType<typeof useCustomerSelectionFlow> | undefined;
|
|
||||||
|
|
||||||
const Harness = () => {
|
|
||||||
flow = useCustomerSelectionFlow({
|
|
||||||
onCustomerSelected: (customer) => {
|
|
||||||
selectedCustomers.push(customer as { id: string });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
renderToStaticMarkup(<Harness />);
|
|
||||||
|
|
||||||
assert.ok(flow);
|
|
||||||
|
|
||||||
flow.openQuickCreateDialog();
|
|
||||||
assert.equal(quickCreateOpenCalls, 1);
|
|
||||||
assert.equal(selectDialogCloseCalls, 0);
|
|
||||||
|
|
||||||
flow.openQuickCreateFromSelection();
|
|
||||||
assert.equal(selectDialogCloseCalls, 1);
|
|
||||||
assert.equal(quickCreateOpenCalls, 2);
|
|
||||||
|
|
||||||
assert.ok(quickCreateOptions);
|
|
||||||
|
|
||||||
quickCreateOptions.onCustomerCreated({ id: "customer-1" });
|
|
||||||
assert.deepEqual(selectedCustomers, [{ id: "customer-1" }]);
|
|
||||||
|
|
||||||
quickCreateOptions.onCancel?.();
|
|
||||||
assert.equal(selectDialogOpenCalls, 1);
|
|
||||||
|
|
||||||
mock.restoreAll();
|
|
||||||
});
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import { QuickCreateCustomerDialog, useQuickCreateCustomerFlow } from "../../quick-create-customer";
|
|
||||||
import type { CustomerSelectionOption } from "../entities";
|
import type { CustomerSelectionOption } from "../entities";
|
||||||
import { SelectCustomerDialog } from "../ui";
|
import { SelectCustomerDialog } from "../ui";
|
||||||
|
|
||||||
@ -13,33 +12,19 @@ export const useCustomerSelectionFlow = (options: {
|
|||||||
onSelect: options.onCustomerSelected,
|
onSelect: options.onCustomerSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
const quickCreateFlow = useQuickCreateCustomerFlow({
|
/*const createCtrl = useQuickCreateCustomerDialogController({
|
||||||
onCustomerCreated: (customer) => {
|
onCreated: (customer) => {
|
||||||
options.onCustomerSelected(customer);
|
options.onCustomerSelected(customer);
|
||||||
},
|
selectCtrl.closeDialog();
|
||||||
onCancel: () => {
|
|
||||||
selectCtrl.openDialog();
|
|
||||||
},
|
},
|
||||||
defaultLanguageCode: options.defaultLanguageCode,
|
defaultLanguageCode: options.defaultLanguageCode,
|
||||||
defaultCurrencyCode: options.defaultCurrencyCode,
|
defaultCurrencyCode: options.defaultCurrencyCode,
|
||||||
});
|
});*/
|
||||||
|
|
||||||
const openQuickCreateFromSelection = () => {
|
|
||||||
selectCtrl.closeDialog();
|
|
||||||
quickCreateFlow.createCtrl.openDialog();
|
|
||||||
};
|
|
||||||
|
|
||||||
const openQuickCreateDialog = () => {
|
|
||||||
quickCreateFlow.createCtrl.openDialog();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectCtrl,
|
selectCtrl,
|
||||||
createCtrl: quickCreateFlow.createCtrl,
|
//createCtrl,
|
||||||
openQuickCreateDialog,
|
|
||||||
openQuickCreateFromSelection,
|
|
||||||
|
|
||||||
SelectCustomerDialog,
|
SelectCustomerDialog,
|
||||||
QuickCreateCustomerDialog,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import type { CustomerSelectionOption } from "../../../../../common/features";
|
import type { CustomerSelectionOption } from "../../../../../common/features";
|
||||||
import { useTranslation } from "../../../../../web/i18n";
|
import { useTranslation } from "../../../../../web/i18n";
|
||||||
@ -41,12 +41,6 @@ export const SelectCustomerDialog = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!ctrl.isOpen) {
|
|
||||||
setSelectedCustomer(undefined);
|
|
||||||
}
|
|
||||||
}, [ctrl.isOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog onOpenChange={(open) => !open && ctrl.closeDialog()} open={ctrl.isOpen}>
|
<Dialog onOpenChange={(open) => !open && ctrl.closeDialog()} open={ctrl.isOpen}>
|
||||||
<DialogContent className="flex max-h-[85vh] flex-col sm:max-w-3xl">
|
<DialogContent className="flex max-h-[85vh] flex-col sm:max-w-3xl">
|
||||||
@ -65,6 +59,14 @@ export const SelectCustomerDialog = ({
|
|||||||
)}
|
)}
|
||||||
value={ctrl.search}
|
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>
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-hidden">
|
<div className="min-h-0 flex-1 overflow-hidden">
|
||||||
@ -76,22 +78,13 @@ export const SelectCustomerDialog = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="gap-2 flex items-center sm:justify-between">
|
<DialogFooter className="gap-2">
|
||||||
<div className="flex items-center">
|
<Button onClick={ctrl.closeDialog} variant="outline">
|
||||||
{onCreateNewCustomerClick ? (
|
Cancelar
|
||||||
<Button onClick={onCreateNewCustomerClick} type="button" variant="outline">
|
</Button>
|
||||||
{t("customers.select_customer.new_customer", "Nuevo cliente")}
|
<Button disabled={ctrl.isLoading || !selectedCustomer} onClick={handleConfirm}>
|
||||||
</Button>
|
{selectedCustomer ? "Confirmar selección" : "Seleccionar cliente"}
|
||||||
) : null}
|
</Button>
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button onClick={ctrl.closeDialog} variant="outline">
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
<Button disabled={ctrl.isLoading || !selectedCustomer} onClick={handleConfirm}>
|
|
||||||
{selectedCustomer ? "Confirmar selección" : "Seleccionar cliente"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@ -1,31 +0,0 @@
|
|||||||
# Quick Create Customer
|
|
||||||
|
|
||||||
Feature pública reutilizable del módulo `customers` para alta rápida embebida.
|
|
||||||
|
|
||||||
## API pública
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import {
|
|
||||||
QuickCreateCustomerDialog,
|
|
||||||
useQuickCreateCustomerFlow,
|
|
||||||
} from "@erp/customers/common";
|
|
||||||
```
|
|
||||||
|
|
||||||
## Ejemplo mínimo
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
const quickCreate = useQuickCreateCustomerFlow({
|
|
||||||
onCustomerCreated: (customer) => {
|
|
||||||
formCtrl.setCustomer(customer);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
<Button onClick={quickCreate.createCtrl.openDialog}>Crear cliente</Button>
|
|
||||||
<QuickCreateCustomerDialog ctrl={quickCreate.createCtrl} />;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contrato
|
|
||||||
|
|
||||||
- Éxito: devuelve un `CustomerSelectionOption`, listo para reutilizarse en flujos de selección.
|
|
||||||
- Cancelación: no crea nada, cierra el diálogo y dispara `onCancel` si el consumidor lo ha proporcionado.
|
|
||||||
- Integración con `customer-selection`: puede abrirse desde el selector y devolver el cliente para dejarlo seleccionado automáticamente.
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./use-quick-create-customer-dialog-controller";
|
|
||||||
export * from "./use-quick-create-customer-flow";
|
|
||||||
@ -1,170 +0,0 @@
|
|||||||
import { applyValidationErrorCollection } from "@erp/core";
|
|
||||||
import { useHookForm } from "@erp/core/hooks";
|
|
||||||
import {
|
|
||||||
UniqueID,
|
|
||||||
type ValidationErrorCollection,
|
|
||||||
isValidationErrorCollection,
|
|
||||||
} from "@repo/rdx-ddd";
|
|
||||||
import {
|
|
||||||
focusFirstInputFormError,
|
|
||||||
showErrorToast,
|
|
||||||
showSuccessToast,
|
|
||||||
showWarningToast,
|
|
||||||
} from "@repo/rdx-ui/helpers";
|
|
||||||
import { useId, useState } from "react";
|
|
||||||
import type { FieldErrors } from "react-hook-form";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../../web/i18n";
|
|
||||||
import { useCustomerCreateMutation } from "../../../../web/shared/hooks";
|
|
||||||
import type { CustomerSelectionOption } from "../../customer-selection";
|
|
||||||
import { buildCustomerSelectionOption } from "../../customer-selection";
|
|
||||||
import { type QuickCustomerCreateForm, QuickCustomerCreateFormSchema } from "../entities";
|
|
||||||
import {
|
|
||||||
buildCreateCustomerParamsFromQuickForm,
|
|
||||||
buildQuickCustomerCreateDefaultValues,
|
|
||||||
} from "../utils";
|
|
||||||
|
|
||||||
type UseQuickCreateCustomerDialogControllerOptions = {
|
|
||||||
onCreated(customer: CustomerSelectionOption): void;
|
|
||||||
onCancel?(): void;
|
|
||||||
defaultLanguageCode?: string;
|
|
||||||
defaultCurrencyCode?: string;
|
|
||||||
defaultCountry?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeSubmitError = (error: unknown): Error | ValidationErrorCollection => {
|
|
||||||
if (isValidationErrorCollection(error)) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Error("Unknown error");
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controla el flujo embebible de alta rápida de cliente.
|
|
||||||
* Mantiene el contrato público desacoplado de páginas o rutas internas.
|
|
||||||
*/
|
|
||||||
export const useQuickCreateCustomerDialogController = (
|
|
||||||
options: UseQuickCreateCustomerDialogControllerOptions
|
|
||||||
) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const formId = useId();
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [submitError, setSubmitError] = useState<Error | null>(null);
|
|
||||||
|
|
||||||
const {
|
|
||||||
mutateAsync,
|
|
||||||
isPending: isSubmitting,
|
|
||||||
isError: isCreateError,
|
|
||||||
} = useCustomerCreateMutation();
|
|
||||||
|
|
||||||
const form = useHookForm<QuickCustomerCreateForm>({
|
|
||||||
resolverSchema: QuickCustomerCreateFormSchema,
|
|
||||||
initialValues: buildQuickCustomerCreateDefaultValues(),
|
|
||||||
disabled: isSubmitting,
|
|
||||||
});
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
setSubmitError(null);
|
|
||||||
form.reset(buildQuickCustomerCreateDefaultValues(), { keepDirty: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
const openDialog = () => {
|
|
||||||
resetForm();
|
|
||||||
setIsOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeDialog = () => {
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelDialog = () => {
|
|
||||||
closeDialog();
|
|
||||||
resetForm();
|
|
||||||
options.onCancel?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitHandler = form.handleSubmit(
|
|
||||||
async (formData) => {
|
|
||||||
setSubmitError(null);
|
|
||||||
|
|
||||||
const params = buildCreateCustomerParamsFromQuickForm(
|
|
||||||
UniqueID.generateNewID().toString(),
|
|
||||||
formData,
|
|
||||||
{
|
|
||||||
country: options.defaultCountry,
|
|
||||||
languageCode: options.defaultLanguageCode,
|
|
||||||
currencyCode: options.defaultCurrencyCode,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const created = await mutateAsync(params);
|
|
||||||
const customer = buildCustomerSelectionOption(created);
|
|
||||||
|
|
||||||
options.onCreated(customer);
|
|
||||||
|
|
||||||
showSuccessToast(
|
|
||||||
t("toasts.quick_create_customer.success.title"),
|
|
||||||
t("toasts.quick_create_customer.success.message")
|
|
||||||
);
|
|
||||||
|
|
||||||
closeDialog();
|
|
||||||
resetForm();
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const normalizedError = normalizeSubmitError(error);
|
|
||||||
|
|
||||||
if (isValidationErrorCollection(normalizedError)) {
|
|
||||||
applyValidationErrorCollection(form, normalizedError);
|
|
||||||
focusFirstInputFormError(form.formState.errors, form);
|
|
||||||
|
|
||||||
showWarningToast(
|
|
||||||
t("create.feedback.validation.title"),
|
|
||||||
t("create.feedback.validation.message")
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitError(normalizedError);
|
|
||||||
|
|
||||||
showErrorToast(
|
|
||||||
t("toasts.quick_create_customer.error.title"),
|
|
||||||
normalizedError.message || t("errors.unexpected")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(errors: FieldErrors<QuickCustomerCreateForm>) => {
|
|
||||||
console.error(errors);
|
|
||||||
setSubmitError(null);
|
|
||||||
focusFirstInputFormError(errors, form);
|
|
||||||
|
|
||||||
showWarningToast(
|
|
||||||
t("common.feedback.validation.title"),
|
|
||||||
t("common.feedback.validation.message")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSubmit = (event: React.SubmitEvent<HTMLFormElement>) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
submitHandler(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
formId,
|
|
||||||
form,
|
|
||||||
isOpen,
|
|
||||||
isSubmitting,
|
|
||||||
isCreateError,
|
|
||||||
submitError,
|
|
||||||
openDialog,
|
|
||||||
closeDialog,
|
|
||||||
cancelDialog,
|
|
||||||
resetForm,
|
|
||||||
onSubmit,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import type { CustomerSelectionOption } from "../../customer-selection";
|
|
||||||
import { QuickCreateCustomerDialog } from "../ui";
|
|
||||||
|
|
||||||
import { useQuickCreateCustomerDialogController } from "./use-quick-create-customer-dialog-controller";
|
|
||||||
|
|
||||||
type UseQuickCreateCustomerFlowOptions = {
|
|
||||||
onCustomerCreated(customer: CustomerSelectionOption): void;
|
|
||||||
onCancel?(): void;
|
|
||||||
defaultLanguageCode?: string;
|
|
||||||
defaultCurrencyCode?: string;
|
|
||||||
defaultCountry?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expone el mismo modelo mental que otras features públicas:
|
|
||||||
* hook de flujo + componente de diálogo reutilizable.
|
|
||||||
*/
|
|
||||||
export const useQuickCreateCustomerFlow = (options: UseQuickCreateCustomerFlowOptions) => {
|
|
||||||
const createCtrl = useQuickCreateCustomerDialogController({
|
|
||||||
onCreated: options.onCustomerCreated,
|
|
||||||
onCancel: options.onCancel,
|
|
||||||
defaultLanguageCode: options.defaultLanguageCode,
|
|
||||||
defaultCurrencyCode: options.defaultCurrencyCode,
|
|
||||||
defaultCountry: options.defaultCountry,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
createCtrl,
|
|
||||||
QuickCreateCustomerDialog,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,3 +1,2 @@
|
|||||||
export * from './quick-customer-create-form.entity';
|
export * from './quick-customer-create-form.entity';
|
||||||
export * from "./quick-customer-create-form.schema";
|
|
||||||
export * from './quick-customer-create-result.entity';
|
export * from './quick-customer-create-result.entity';
|
||||||
|
|||||||
@ -1 +1,15 @@
|
|||||||
export type { QuickCustomerCreateForm } from "./quick-customer-create-form.schema";
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
import assert from "node:assert/strict";
|
|
||||||
import test from "node:test";
|
|
||||||
|
|
||||||
import { QuickCustomerCreateFormSchema } from "./quick-customer-create-form.schema";
|
|
||||||
|
|
||||||
test("valida los campos mínimos del alta rápida", () => {
|
|
||||||
const result = QuickCustomerCreateFormSchema.safeParse({
|
|
||||||
isCompany: true,
|
|
||||||
name: "Acme Corp",
|
|
||||||
tin: "B12345678",
|
|
||||||
primaryEmail: "billing@acme.test",
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.equal(result.success, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("falla cuando falta un dato técnicamente obligatorio", () => {
|
|
||||||
const result = QuickCustomerCreateFormSchema.safeParse({
|
|
||||||
isCompany: true,
|
|
||||||
name: "",
|
|
||||||
tin: "",
|
|
||||||
primaryEmail: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.equal(result.success, false);
|
|
||||||
});
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import type { z } from "zod/v4";
|
|
||||||
|
|
||||||
import { CustomerCreateFormSchema } from "../../../../web/create/entities";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SSOT del formulario de alta rápida.
|
|
||||||
* Reutiliza las validaciones del formulario oficial y limita el shape
|
|
||||||
* a los campos mínimos de esta operación pública.
|
|
||||||
*/
|
|
||||||
export const QuickCustomerCreateFormSchema = CustomerCreateFormSchema.pick({
|
|
||||||
isCompany: true,
|
|
||||||
name: true,
|
|
||||||
tin: true,
|
|
||||||
primaryEmail: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type QuickCustomerCreateForm = z.infer<typeof QuickCustomerCreateFormSchema>;
|
|
||||||
@ -1,7 +1,17 @@
|
|||||||
import type { CustomerSelectionOption } from "../../customer-selection";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* El resultado público de alta rápida coincide con el contrato estable
|
* Resultado simplificado del alta rápida.
|
||||||
* reutilizado por la selección de cliente.
|
* Compatible con CustomerSelectionOption para integración directa.
|
||||||
*/
|
*/
|
||||||
export type QuickCustomerCreateResult = CustomerSelectionOption;
|
export interface QuickCustomerCreateResult {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
isCompany?: boolean;
|
||||||
|
name: string;
|
||||||
|
tin: string;
|
||||||
|
|
||||||
|
EmailPrimary: string;
|
||||||
|
|
||||||
|
languageCode: string;
|
||||||
|
currencyCode: string;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,2 @@
|
|||||||
export * from "./controllers/use-quick-create-customer-flow";
|
export * from "./entities";
|
||||||
export * from "./entities/quick-customer-create-result.entity";
|
export * from "./utils";
|
||||||
export * from "./ui";
|
|
||||||
|
|||||||
@ -1,135 +0,0 @@
|
|||||||
import {
|
|
||||||
FormFieldLabel,
|
|
||||||
RdxEditorSection,
|
|
||||||
RdxFormGrid,
|
|
||||||
TextField,
|
|
||||||
} from "@repo/rdx-ui/components";
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldContent,
|
|
||||||
FieldDescription,
|
|
||||||
FieldError,
|
|
||||||
FieldLabel,
|
|
||||||
RadioGroup,
|
|
||||||
RadioGroupItem,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
import { AtSignIcon, Building2Icon, UserIcon } from "lucide-react";
|
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../../../web/i18n";
|
|
||||||
|
|
||||||
type QuickCustomerCreateFormFieldsProps = {
|
|
||||||
disabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const QuickCustomerCreateFormFields = ({
|
|
||||||
disabled = false,
|
|
||||||
}: QuickCustomerCreateFormFieldsProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { control } = useFormContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex min-w-0 flex-col gap-6">
|
|
||||||
<RdxEditorSection
|
|
||||||
description={t("form_groups.basic_info.description")}
|
|
||||||
title={t("form_groups.basic_info.title")}
|
|
||||||
>
|
|
||||||
<RdxFormGrid className="md:grid-cols-2">
|
|
||||||
<TextField
|
|
||||||
description={t("fields.name.description")}
|
|
||||||
disabled={disabled}
|
|
||||||
label={t("fields.name.label")}
|
|
||||||
name="name"
|
|
||||||
placeholder={t("fields.name.placeholder")}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
description={t("fields.tin.description")}
|
|
||||||
disabled={disabled}
|
|
||||||
label={t("fields.tin.label")}
|
|
||||||
name="tin"
|
|
||||||
placeholder={t("fields.tin.placeholder")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="isCompany"
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<Field className="md:col-span-2" data-invalid={fieldState.invalid}>
|
|
||||||
<div className="flex flex-col gap-2 pt-1">
|
|
||||||
<FormFieldLabel required>{t("fields.customer_type.label")}</FormFieldLabel>
|
|
||||||
|
|
||||||
<RadioGroup
|
|
||||||
className="gap-3 md:grid md:grid-cols-2"
|
|
||||||
disabled={field.disabled || disabled}
|
|
||||||
name={field.name}
|
|
||||||
onValueChange={(value: boolean) => {
|
|
||||||
field.onChange(value);
|
|
||||||
}}
|
|
||||||
required
|
|
||||||
value={field.value}
|
|
||||||
>
|
|
||||||
<Field data-invalid={fieldState.invalid} orientation="horizontal">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldLabel
|
|
||||||
className="cursor-pointer text-sm font-medium leading-none"
|
|
||||||
htmlFor="quick-customer-type-company"
|
|
||||||
>
|
|
||||||
<RadioGroupItem
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
id="quick-customer-type-company"
|
|
||||||
value={true}
|
|
||||||
/>
|
|
||||||
<Building2Icon className="size-4 text-muted-foreground" />
|
|
||||||
{t("fields.customer_type.options.company")}
|
|
||||||
</FieldLabel>
|
|
||||||
</FieldContent>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field data-invalid={fieldState.invalid} orientation="horizontal">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldLabel
|
|
||||||
className="cursor-pointer text-sm font-medium leading-none"
|
|
||||||
htmlFor="quick-customer-type-individual"
|
|
||||||
>
|
|
||||||
<RadioGroupItem
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
id="quick-customer-type-individual"
|
|
||||||
value={false}
|
|
||||||
/>
|
|
||||||
<UserIcon className="size-4 text-muted-foreground" />
|
|
||||||
{t("fields.customer_type.options.individual")}
|
|
||||||
</FieldLabel>
|
|
||||||
</FieldContent>
|
|
||||||
</Field>
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
<FieldDescription>{t("fields.customer_type.description")}</FieldDescription>
|
|
||||||
<FieldError errors={[fieldState.error]} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</RdxFormGrid>
|
|
||||||
</RdxEditorSection>
|
|
||||||
|
|
||||||
<RdxEditorSection
|
|
||||||
description={t("form_groups.contact_info.description")}
|
|
||||||
title={t("form_groups.contact_info.title")}
|
|
||||||
>
|
|
||||||
<RdxFormGrid>
|
|
||||||
<TextField
|
|
||||||
className="md:col-span-6"
|
|
||||||
description={t("fields.email_primary.description")}
|
|
||||||
disabled={disabled}
|
|
||||||
label={t("fields.email_primary.label")}
|
|
||||||
leftIcon={<AtSignIcon className="size-4" strokeWidth={1.5} />}
|
|
||||||
name="primaryEmail"
|
|
||||||
placeholder={t("fields.email_primary.placeholder")}
|
|
||||||
typePreset="email"
|
|
||||||
/>
|
|
||||||
</RdxFormGrid>
|
|
||||||
</RdxEditorSection>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./quick-create-customer-dialog";
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
import assert from "node:assert/strict";
|
|
||||||
import test from "node:test";
|
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
import type { QuickCustomerCreateForm } from "../../entities";
|
|
||||||
import { buildQuickCustomerCreateDefaultValues } from "../../utils";
|
|
||||||
import { QuickCreateCustomerDialog } from "./quick-create-customer-dialog";
|
|
||||||
|
|
||||||
const renderDialog = () => {
|
|
||||||
const Harness = () => {
|
|
||||||
const form = useForm<QuickCustomerCreateForm>({
|
|
||||||
defaultValues: buildQuickCustomerCreateDefaultValues(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormProvider {...form}>
|
|
||||||
<QuickCreateCustomerDialog
|
|
||||||
ctrl={{
|
|
||||||
formId: "quick-create-customer-form",
|
|
||||||
form,
|
|
||||||
isOpen: true,
|
|
||||||
isSubmitting: false,
|
|
||||||
isCreateError: false,
|
|
||||||
submitError: null,
|
|
||||||
openDialog: () => undefined,
|
|
||||||
closeDialog: () => undefined,
|
|
||||||
cancelDialog: () => undefined,
|
|
||||||
resetForm: () => undefined,
|
|
||||||
onSubmit: (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return renderToStaticMarkup(<Harness />);
|
|
||||||
};
|
|
||||||
|
|
||||||
test("renderiza el diálogo rápido con los campos mínimos reutilizables", () => {
|
|
||||||
const html = renderDialog();
|
|
||||||
|
|
||||||
assert.match(html, /id="quick-create-customer-form"/);
|
|
||||||
assert.match(html, /name="name"/);
|
|
||||||
assert.match(html, /name="tin"/);
|
|
||||||
assert.match(html, /name="primaryEmail"/);
|
|
||||||
assert.doesNotMatch(html, /name="country"/);
|
|
||||||
assert.doesNotMatch(html, /name="languageCode"/);
|
|
||||||
assert.doesNotMatch(html, /name="currencyCode"/);
|
|
||||||
});
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
import assert from "node:assert/strict";
|
|
||||||
import test from "node:test";
|
|
||||||
|
|
||||||
import { buildCreateCustomerParamsFromQuickForm } from "./build-create-customer-params-from-quick-form";
|
|
||||||
|
|
||||||
test("construye el request oficial de customers a partir del formulario rápido", () => {
|
|
||||||
const params = buildCreateCustomerParamsFromQuickForm("bf0d3f59-7b96-4ab6-a74b-2f2b8bcd0fb5", {
|
|
||||||
isCompany: false,
|
|
||||||
name: "Jane Doe",
|
|
||||||
tin: "",
|
|
||||||
primaryEmail: "jane@example.com",
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.equal(params.id, "bf0d3f59-7b96-4ab6-a74b-2f2b8bcd0fb5");
|
|
||||||
assert.deepEqual(params.data, {
|
|
||||||
id: "bf0d3f59-7b96-4ab6-a74b-2f2b8bcd0fb5",
|
|
||||||
is_company: false,
|
|
||||||
name: "Jane Doe",
|
|
||||||
address: {
|
|
||||||
country: "ES",
|
|
||||||
},
|
|
||||||
contact: {
|
|
||||||
email_primary: "jane@example.com",
|
|
||||||
},
|
|
||||||
uses_equivalence_surcharge: false,
|
|
||||||
uses_retention: false,
|
|
||||||
language_code: "es",
|
|
||||||
currency_code: "EUR",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("permite sobrescribir los defaults de contexto sin pedirlos al usuario", () => {
|
|
||||||
const params = buildCreateCustomerParamsFromQuickForm("bf0d3f59-7b96-4ab6-a74b-2f2b8bcd0fb5", {
|
|
||||||
isCompany: true,
|
|
||||||
name: "Acme",
|
|
||||||
tin: "B12345678",
|
|
||||||
primaryEmail: "",
|
|
||||||
}, {
|
|
||||||
country: "pt",
|
|
||||||
languageCode: "EN",
|
|
||||||
currencyCode: "usd",
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.equal(params.id, "bf0d3f59-7b96-4ab6-a74b-2f2b8bcd0fb5");
|
|
||||||
assert.deepEqual(params.data, {
|
|
||||||
id: "bf0d3f59-7b96-4ab6-a74b-2f2b8bcd0fb5",
|
|
||||||
is_company: true,
|
|
||||||
name: "Acme",
|
|
||||||
tin: "B12345678",
|
|
||||||
address: {
|
|
||||||
country: "PT",
|
|
||||||
},
|
|
||||||
uses_equivalence_surcharge: false,
|
|
||||||
uses_retention: false,
|
|
||||||
language_code: "en",
|
|
||||||
currency_code: "USD",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,61 +1,20 @@
|
|||||||
import { buildCustomerCreateDefault } from "../../../../web/create/utils/build-customer-create-defaults";
|
|
||||||
import { toNullableText, toRequiredText } from "@erp/core/client";
|
|
||||||
import type { CreateCustomerParams } from "../../../../web/shared";
|
import type { CreateCustomerParams } from "../../../../web/shared";
|
||||||
import type { QuickCustomerCreateForm } from "../entities";
|
import type { QuickCustomerCreateForm } from "../entities";
|
||||||
|
|
||||||
type QuickCustomerCreateContextDefaults = {
|
|
||||||
country?: string;
|
|
||||||
languageCode?: string;
|
|
||||||
currencyCode?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builder explícito Form → Params reutilizando la misma normalización
|
* Builder explícito Form → Params (regla del proyecto).
|
||||||
* de creación sin depender de componentes o páginas concretas.
|
|
||||||
*/
|
*/
|
||||||
export const buildCreateCustomerParamsFromQuickForm = (
|
export const buildCreateCustomerParamsFromQuickForm = (
|
||||||
customerId: string,
|
form: QuickCustomerCreateForm
|
||||||
form: QuickCustomerCreateForm,
|
|
||||||
contextDefaults?: QuickCustomerCreateContextDefaults
|
|
||||||
): CreateCustomerParams => {
|
): CreateCustomerParams => {
|
||||||
const createDefaults = buildCustomerCreateDefault();
|
return {
|
||||||
const resolvedCountry = toRequiredText(
|
data: {
|
||||||
contextDefaults?.country ?? createDefaults.country
|
name: form.name,
|
||||||
).toUpperCase();
|
is_company: form.isCompany ? "1" : "0",
|
||||||
const resolvedLanguageCode = toRequiredText(
|
tin: form.tin,
|
||||||
contextDefaults?.languageCode ?? createDefaults.languageCode
|
email_primary: form.EmailPrimary,
|
||||||
).toLowerCase();
|
language_code: form.languageCode,
|
||||||
const resolvedCurrencyCode = toRequiredText(
|
currency_code: form.currencyCode,
|
||||||
contextDefaults?.currencyCode ?? createDefaults.currencyCode
|
|
||||||
).toUpperCase();
|
|
||||||
|
|
||||||
const data: CreateCustomerParams["data"] = {
|
|
||||||
id: customerId,
|
|
||||||
is_company: form.isCompany,
|
|
||||||
name: toRequiredText(form.name),
|
|
||||||
language_code: resolvedLanguageCode,
|
|
||||||
currency_code: resolvedCurrencyCode,
|
|
||||||
uses_equivalence_surcharge: createDefaults.usesEquivalenceSurcharge,
|
|
||||||
uses_retention: createDefaults.usesRetention,
|
|
||||||
address: {
|
|
||||||
country: resolvedCountry,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const tin = toNullableText(form.tin);
|
|
||||||
if (tin !== null) {
|
|
||||||
data.tin = tin;
|
|
||||||
}
|
|
||||||
|
|
||||||
const primaryEmail = toNullableText(form.primaryEmail);
|
|
||||||
if (primaryEmail !== null) {
|
|
||||||
data.contact = {
|
|
||||||
email_primary: primaryEmail,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: customerId,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
import assert from "node:assert/strict";
|
|
||||||
import test from "node:test";
|
|
||||||
|
|
||||||
import { buildQuickCustomerCreateDefaultValues } from "./build-quick-customer-create-default-values";
|
|
||||||
|
|
||||||
test("reutiliza los defaults oficiales de creación para los campos visibles", () => {
|
|
||||||
const defaults = buildQuickCustomerCreateDefaultValues();
|
|
||||||
|
|
||||||
assert.deepEqual(defaults, {
|
|
||||||
isCompany: true,
|
|
||||||
name: "",
|
|
||||||
tin: "",
|
|
||||||
primaryEmail: "",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,13 +1,19 @@
|
|||||||
import type { QuickCustomerCreateForm } from "../entities";
|
import type { QuickCustomerCreateForm } from "../entities";
|
||||||
import { buildCustomerCreateDefault } from "../../../../web/create/utils/build-customer-create-defaults";
|
|
||||||
|
|
||||||
export const buildQuickCustomerCreateDefaultValues = (): QuickCustomerCreateForm => {
|
|
||||||
const defaults = buildCustomerCreateDefault();
|
|
||||||
|
|
||||||
|
export const buildQuickCustomerCreateDefaultValues = (
|
||||||
|
overrides?: Partial<QuickCustomerCreateForm>
|
||||||
|
): QuickCustomerCreateForm => {
|
||||||
return {
|
return {
|
||||||
isCompany: defaults.isCompany,
|
isCompany: true,
|
||||||
name: defaults.name,
|
name: "",
|
||||||
tin: defaults.tin,
|
tradeName: "",
|
||||||
primaryEmail: "",
|
tin: "",
|
||||||
|
|
||||||
|
EmailPrimary: "",
|
||||||
|
|
||||||
|
languageCode: overrides?.languageCode ?? "es",
|
||||||
|
currencyCode: overrides?.currencyCode ?? "EUR",
|
||||||
|
|
||||||
|
...overrides,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -110,7 +110,7 @@ export const useCreateCustomerController = (options?: UseCustomerCreateControlle
|
|||||||
|
|
||||||
console.log("Errores de validación aplicados al form:", form.formState.errors);
|
console.log("Errores de validación aplicados al form:", form.formState.errors);
|
||||||
|
|
||||||
focusFirstInputFormError(form.formState.errors, form);
|
focusFirstInputFormError(form);
|
||||||
|
|
||||||
if (options?.errorToasts !== false) {
|
if (options?.errorToasts !== false) {
|
||||||
showWarningToast(
|
showWarningToast(
|
||||||
@ -135,7 +135,7 @@ export const useCreateCustomerController = (options?: UseCustomerCreateControlle
|
|||||||
},
|
},
|
||||||
(errors: FieldErrors<CustomerCreateForm>) => {
|
(errors: FieldErrors<CustomerCreateForm>) => {
|
||||||
console.error(errors);
|
console.error(errors);
|
||||||
focusFirstInputFormError(errors, form);
|
focusFirstInputFormError(form);
|
||||||
|
|
||||||
showWarningToast(
|
showWarningToast(
|
||||||
t("common.feedback.validation.title"),
|
t("common.feedback.validation.title"),
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
export * from "../../common/features/customer-selection";
|
export * from "./customer-selection";
|
||||||
export * from "../../common/features/quick-create-customer";
|
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("toasts.quick_create_customer.success.title"),
|
||||||
|
t("toasts.quick_create_customer.success.message")
|
||||||
|
);
|
||||||
|
|
||||||
|
form.reset(
|
||||||
|
buildQuickCustomerCreateDefaultValues({
|
||||||
|
languageCode: options.defaultLanguageCode,
|
||||||
|
currencyCode: options.defaultCurrencyCode,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
closeDialog();
|
||||||
|
} catch (error) {
|
||||||
|
const err = error instanceof Error ? error : new Error("Unknown error");
|
||||||
|
|
||||||
|
showErrorToast(
|
||||||
|
t("toasts.quick_create_customer.error.title"),
|
||||||
|
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-create-customer-dialog';
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import { ErrorAlert } from "@erp/core/components";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -8,13 +7,16 @@ import {
|
|||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { FormProvider } from "react-hook-form";
|
import { FormProvider } from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../../web/i18n.ts";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type { useQuickCreateCustomerDialogController } from "../../controllers";
|
|
||||||
import { QuickCustomerCreateFormFields } from "../blocks";
|
|
||||||
|
|
||||||
type QuickCreateCustomerDialogController = ReturnType<
|
type QuickCreateCustomerDialogController = {
|
||||||
typeof useQuickCreateCustomerDialogController
|
formId: string;
|
||||||
>;
|
form: ReturnType<any>;
|
||||||
|
isOpen: boolean;
|
||||||
|
closeDialog: () => void;
|
||||||
|
onSubmit: React.FormEventHandler<HTMLFormElement>;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type QuickCreateCustomerDialogProps = {
|
type QuickCreateCustomerDialogProps = {
|
||||||
ctrl: QuickCreateCustomerDialogController;
|
ctrl: QuickCreateCustomerDialogController;
|
||||||
@ -24,32 +26,20 @@ export const QuickCreateCustomerDialog = ({ ctrl }: QuickCreateCustomerDialogPro
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog onOpenChange={(open) => !open && ctrl.cancelDialog()} open={ctrl.isOpen}>
|
<Dialog onOpenChange={(open) => !open && ctrl.closeDialog()} open={ctrl.isOpen}>
|
||||||
<DialogContent className="flex max-h-[85vh] flex-col sm:max-w-2xl">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t("dialogs.quick_create_customer.title")}</DialogTitle>
|
<DialogTitle>{t("dialogs.quick_create_customer.title")}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<FormProvider {...ctrl.form}>
|
<FormProvider {...ctrl.form}>
|
||||||
<form
|
<form className="space-y-6" id={ctrl.formId} onSubmit={ctrl.onSubmit}>
|
||||||
className="min-h-0 space-y-6 overflow-y-auto pr-1"
|
<QuickCustomerCreateFormFields />
|
||||||
id={ctrl.formId}
|
|
||||||
noValidate
|
|
||||||
onSubmit={ctrl.onSubmit}
|
|
||||||
>
|
|
||||||
{ctrl.submitError ? (
|
|
||||||
<ErrorAlert
|
|
||||||
message={ctrl.submitError.message || t("errors.unexpected")}
|
|
||||||
title={t("toasts.quick_create_customer.error.title")}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<QuickCustomerCreateFormFields disabled={ctrl.isSubmitting} />
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
disabled={ctrl.isSubmitting}
|
disabled={ctrl.isSubmitting}
|
||||||
onClick={ctrl.cancelDialog}
|
onClick={ctrl.closeDialog}
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
@ -121,6 +121,7 @@ export function restoreCustomerListCaches(
|
|||||||
|
|
||||||
export function syncCreatedCustomerCaches(queryClient: QueryClient, customer: Customer) {
|
export function syncCreatedCustomerCaches(queryClient: QueryClient, customer: Customer) {
|
||||||
setCustomerDetailCache(queryClient, customer.id, customer);
|
setCustomerDetailCache(queryClient, customer.id, customer);
|
||||||
|
return invalidateCustomerListQueries(queryClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function syncUpdatedCustomerCaches(queryClient: QueryClient, customer: Customer) {
|
export function syncUpdatedCustomerCaches(queryClient: QueryClient, customer: Customer) {
|
||||||
|
|||||||
@ -173,7 +173,7 @@ export const useCustomerUpdateController = (
|
|||||||
|
|
||||||
console.log("Errores de validación aplicados al form:", form.formState.errors);
|
console.log("Errores de validación aplicados al form:", form.formState.errors);
|
||||||
|
|
||||||
focusFirstInputFormError(form.formState.errors, form);
|
focusFirstInputFormError(form);
|
||||||
|
|
||||||
if (options?.errorToasts !== false) {
|
if (options?.errorToasts !== false) {
|
||||||
showWarningToast(
|
showWarningToast(
|
||||||
@ -198,7 +198,7 @@ export const useCustomerUpdateController = (
|
|||||||
},
|
},
|
||||||
(errors: FieldErrors<CustomerUpdateForm>) => {
|
(errors: FieldErrors<CustomerUpdateForm>) => {
|
||||||
console.error(errors);
|
console.error(errors);
|
||||||
focusFirstInputFormError(errors, form);
|
focusFirstInputFormError(form);
|
||||||
|
|
||||||
showWarningToast(
|
showWarningToast(
|
||||||
t("common.feedback.validation.title"),
|
t("common.feedback.validation.title"),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user