diff --git a/modules/customer-invoices/src/api/infrastructure/invoice-series/express/invoice-series.routes.ts b/modules/customer-invoices/src/api/infrastructure/invoice-series/express/invoice-series.routes.ts index 0e227b0b..8950905d 100644 --- a/modules/customer-invoices/src/api/infrastructure/invoice-series/express/invoice-series.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/invoice-series/express/invoice-series.routes.ts @@ -1,8 +1,9 @@ -import { type StartParams } from "@erp/core/api"; +import type { StartParams } from "@erp/core/api"; import { requireIdentityTenant } from "@erp/identity/api"; import { type NextFunction, type Request, type Response, Router } from "express"; import type { InvoiceSeriesInternalDeps } from "../di/invoice-series.di"; + import { ListInvoiceSeriesController } from "./controllers"; export const invoiceSeriesRouter = (params: StartParams) => { @@ -20,5 +21,5 @@ export const invoiceSeriesRouter = (params: StartParams) => { return controller.execute(req, res, next); }); - app.use(`${config.server.apiBasePath}/invoice-series`, router); + app.use(`${config.server.apiBasePath}/catalogs/invoice-series`, router); }; diff --git a/modules/customer-invoices/src/api/infrastructure/invoice-series/persistence/sequelize/repositories/invoice-series.repository.ts b/modules/customer-invoices/src/api/infrastructure/invoice-series/persistence/sequelize/repositories/invoice-series.repository.ts index fbb4bc45..f2793f3a 100644 --- a/modules/customer-invoices/src/api/infrastructure/invoice-series/persistence/sequelize/repositories/invoice-series.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/invoice-series/persistence/sequelize/repositories/invoice-series.repository.ts @@ -1,7 +1,4 @@ -import { - SequelizeRepository, - translateSequelizeError, -} from "@erp/core/api"; +import { SequelizeRepository, translateSequelizeError } from "@erp/core/api"; import type { UniqueID } from "@repo/rdx-ddd"; import { Collection, Result } from "@repo/rdx-utils"; import type { Sequelize, Transaction } from "sequelize"; @@ -11,7 +8,7 @@ import { InvoiceSeriesConcurrencyError, InvoiceSeriesLockRequiredError, InvoiceSeriesTransactionRequiredError, -} from "../../../../../application/invoice-series"; +} from "../../../../../application"; import type { InvoiceSeries, InvoiceSeriesCode } from "../../../../../domain"; import { CustomerInvoiceSeriesModel } from "../../../../common"; import type { InvoiceSeriesDomainMapper } from "../mappers"; @@ -27,10 +24,7 @@ export class SequelizeInvoiceSeriesRepository super({ database }); } - public async findActiveByCompany(params: { - companyId: UniqueID; - transaction?: unknown; - }) { + public async findActiveByCompany(params: { companyId: UniqueID; transaction?: unknown }) { try { const rows = await CustomerInvoiceSeriesModel.findAll({ where: { diff --git a/modules/customer-invoices/src/web/proformas/create/controllers/use-create-proforma-controller.ts b/modules/customer-invoices/src/web/proformas/create/controllers/use-create-proforma-controller.ts index 84c3802c..982f6e9a 100644 --- a/modules/customer-invoices/src/web/proformas/create/controllers/use-create-proforma-controller.ts +++ b/modules/customer-invoices/src/web/proformas/create/controllers/use-create-proforma-controller.ts @@ -16,10 +16,14 @@ import { useEffect, useId, useMemo, useState } from "react"; import type { FieldErrors } from "react-hook-form"; import type { Proforma } from "../../shared"; -import { useProformaCreateMutation, useProformasListQuery } from "../../shared/hooks"; +import { + buildInvoiceSeriesSelectItems, + useInvoiceSeriesQuery, + useProformaCreateMutation, +} from "../../shared"; import { buildCreateProformaParams } from "../adapters"; import { type ProformaCreateForm, ProformaCreateFormSchema } from "../entities"; -import { buildProformaCreateDefault, buildProformaSeriesOptions } from "../utils"; +import { buildProformaCreateDefault } from "../utils"; export interface UseCreateProformaControllerOptions { onCreated?(proforma: Proforma): void; @@ -48,12 +52,7 @@ export const useCreateProformaController = (options?: UseCreateProformaControlle error: createError, } = useProformaCreateMutation(); - const seriesQuery = useProformasListQuery({ - criteria: { - pageNumber: 1, - pageSize: 100, - }, - }); + const seriesQuery = useInvoiceSeriesQuery(); const initialValues = useMemo(() => buildProformaCreateDefault(), []); @@ -64,20 +63,28 @@ export const useCreateProformaController = (options?: UseCreateProformaControlle }); const seriesOptions = useMemo( - () => buildProformaSeriesOptions(seriesQuery.data), + () => buildInvoiceSeriesSelectItems(seriesQuery.data ?? []), [seriesQuery.data] ); - useEffect(() => { - if (seriesOptions.length !== 1) return; - if (form.getValues("series")) return; + const defaultSeries = useMemo(() => { + const invoiceSeries = seriesQuery.data ?? []; - form.setValue("series", seriesOptions[0].value, { + return ( + invoiceSeries.find((series) => series.isDefault)?.code ?? invoiceSeries[0]?.code ?? "" + ); + }, [seriesQuery.data]); + + useEffect(() => { + if (form.getValues("series")) return; + if (!defaultSeries) return; + + form.setValue("series", defaultSeries, { shouldDirty: false, shouldTouch: false, shouldValidate: true, }); - }, [form, seriesOptions]); + }, [defaultSeries, form]); const setCustomer = (customer: CustomerSelectionOption) => { setSelectedCustomer(customer); @@ -163,8 +170,8 @@ export const useCreateProformaController = (options?: UseCreateProformaControlle setCustomer, clearCustomer, seriesOptions, - isLoading: seriesQuery.isLoading, - isLoadError: seriesQuery.isError, - loadError: seriesQuery.error, + isSeriesLoading: seriesQuery.isLoading, + isSeriesLoadError: seriesQuery.isError, + seriesLoadError: seriesQuery.error, }; }; diff --git a/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-form.tsx b/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-form.tsx index 83c54bdf..12b71eb1 100644 --- a/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-form.tsx +++ b/modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-form.tsx @@ -9,6 +9,7 @@ import { ProformaCreateInitialInfoCard } from "./proforma-create-initial-info-ca interface ProformaCreateFormProps { formId: string; isSubmitting: boolean; + isSeriesLoading?: boolean; onSubmit: React.SubmitEventHandler; selectedCustomer?: CustomerSelectionOption | null; onSelectCustomer: () => void; @@ -20,6 +21,7 @@ interface ProformaCreateFormProps { export const ProformaCreateForm = ({ formId, isSubmitting, + isSeriesLoading = false, onSubmit, selectedCustomer, onSelectCustomer, @@ -33,6 +35,7 @@ export const ProformaCreateForm = ({
void; onClearCustomer: () => void; - seriesOptions: { value: string; label: string }[]; + seriesOptions: SelectFieldItem[]; } export const ProformaCreateInitialInfoCard = ({ disabled = false, + isSeriesLoading = false, selectedCustomer, onSelectCustomer, onClearCustomer, @@ -44,11 +52,17 @@ export const ProformaCreateInitialInfoCard = ({ className="md:col-span-4" - disabled={disabled} + disabled={disabled || isSeriesLoading} items={seriesOptions} label="Serie" name="series" - placeholder={seriesOptions.length > 0 ? "Selecciona una serie" : "Sin serie"} + placeholder={ + isSeriesLoading + ? "Cargando series..." + : seriesOptions.length > 0 + ? "Selecciona una serie" + : "Sin serie" + } /> diff --git a/modules/customer-invoices/src/web/proformas/create/ui/pages/proforma-create-page.tsx b/modules/customer-invoices/src/web/proformas/create/ui/pages/proforma-create-page.tsx index c7028e7a..23d50adb 100644 --- a/modules/customer-invoices/src/web/proformas/create/ui/pages/proforma-create-page.tsx +++ b/modules/customer-invoices/src/web/proformas/create/ui/pages/proforma-create-page.tsx @@ -1,13 +1,11 @@ import { ErrorAlert } from "@erp/core/components"; import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks"; import { SelectCustomerDialog } from "@erp/customers"; -import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components"; import { FormProvider } from "react-hook-form"; import { ProformaInfoAlert } from "../../../update/ui/components"; import { useCreateProformaPageController } from "../../controllers"; import { ProformaCreateForm, ProformaCreateHeader } from "../blocks"; -import { ProformaCreateSkeleton } from "../components"; export const ProformaCreatePage = () => { const { createCtrl, selectCustomerCtrl, returnTo } = useCreateProformaPageController(); @@ -16,29 +14,6 @@ export const ProformaCreatePage = () => { fallbackPath: returnTo, }); - if (createCtrl.isLoading) { - return ; - } - - if (createCtrl.isLoadError) { - return ( - - - -
- navigateBack()} /> -
-
- ); - } - return (
@@ -65,9 +40,21 @@ export const ProformaCreatePage = () => { /> )} + {createCtrl.isSeriesLoadError && ( + + )} +
{ return { customerId: "", invoiceDate: DateHelper.buildTodayIsoDate(), - series: "F26", + series: "", languageCode: "es", currencyCode: "EUR", }; diff --git a/modules/customer-invoices/src/web/proformas/create/utils/build-proforma-series-options.ts b/modules/customer-invoices/src/web/proformas/create/utils/build-proforma-series-options.ts deleted file mode 100644 index c203995c..00000000 --- a/modules/customer-invoices/src/web/proformas/create/utils/build-proforma-series-options.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { SelectFieldItem } from "@repo/rdx-ui/components"; - -import type { ProformaList } from "../../shared"; - -export const buildProformaSeriesOptions = (proformas?: ProformaList): SelectFieldItem[] => { - const uniqueSeries = new Set(); - - for (const item of proformas?.items ?? []) { - const value = item.series?.trim(); - if (!value) continue; - uniqueSeries.add(value); - } - - return [...uniqueSeries] - .sort((left, right) => left.localeCompare(right)) - .map((value) => ({ value, label: value })); -}; diff --git a/modules/customer-invoices/src/web/proformas/create/utils/index.ts b/modules/customer-invoices/src/web/proformas/create/utils/index.ts index 3c2c7a03..6020b31d 100644 --- a/modules/customer-invoices/src/web/proformas/create/utils/index.ts +++ b/modules/customer-invoices/src/web/proformas/create/utils/index.ts @@ -1,2 +1 @@ export * from "./build-proforma-create-default"; -export * from "./build-proforma-series-options"; diff --git a/modules/customer-invoices/src/web/proformas/shared/adapters/index.ts b/modules/customer-invoices/src/web/proformas/shared/adapters/index.ts index 58ff273b..5783a072 100644 --- a/modules/customer-invoices/src/web/proformas/shared/adapters/index.ts +++ b/modules/customer-invoices/src/web/proformas/shared/adapters/index.ts @@ -1,3 +1,4 @@ export * from "./get-proforma-by-id.adapter"; +export * from "./list-invoice-series.adapter"; export * from "./list-proformas.adapter"; export * from "./proforma-to-list-row-patch.adapter"; diff --git a/modules/customer-invoices/src/web/proformas/shared/adapters/list-invoice-series.adapter.ts b/modules/customer-invoices/src/web/proformas/shared/adapters/list-invoice-series.adapter.ts new file mode 100644 index 00000000..5e80e32e --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/shared/adapters/list-invoice-series.adapter.ts @@ -0,0 +1,18 @@ +import type { ListInvoiceSeriesResponseDTO } from "../../../../common"; +import type { InvoiceSeriesSummary } from "../entities"; + +export const mapInvoiceSeriesSummaryDTOToInvoiceSeriesSummary = ( + dto: ListInvoiceSeriesResponseDTO["items"][number] +): InvoiceSeriesSummary => ({ + id: dto.id, + code: dto.code, + nextNumber: dto.next_number, + paddingLength: dto.padding_length, + isDefault: dto.is_default, +}); + +export const ListInvoiceSeriesAdapter = { + fromDto(dto: ListInvoiceSeriesResponseDTO): InvoiceSeriesSummary[] { + return dto.items.map(mapInvoiceSeriesSummaryDTOToInvoiceSeriesSummary); + }, +}; diff --git a/modules/customer-invoices/src/web/proformas/shared/api/index.ts b/modules/customer-invoices/src/web/proformas/shared/api/index.ts index b00e6b8f..1168573c 100644 --- a/modules/customer-invoices/src/web/proformas/shared/api/index.ts +++ b/modules/customer-invoices/src/web/proformas/shared/api/index.ts @@ -3,5 +3,6 @@ export * from "./create-proforma.api"; export * from "./delete-proforma-by-id.api"; export * from "./get-proforma-by-id.api"; export * from "./issue-proforma-by-id.api"; +export * from "./list-invoice-series.api"; export * from "./list-proformas-by-criteria.api"; export * from "./update-proforma-by-id.api"; diff --git a/modules/customer-invoices/src/web/proformas/shared/api/list-invoice-series.api.ts b/modules/customer-invoices/src/web/proformas/shared/api/list-invoice-series.api.ts new file mode 100644 index 00000000..6f194824 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/shared/api/list-invoice-series.api.ts @@ -0,0 +1,12 @@ +import type { IDataSource } from "@erp/core/client"; + +import type { ListInvoiceSeriesResponseDTO } from "../../../../common"; + +export const listInvoiceSeries = ( + dataSource: IDataSource, + signal?: AbortSignal +): Promise => { + return dataSource.getList("catalogs/invoice-series", { + signal, + }); +}; diff --git a/modules/customer-invoices/src/web/proformas/shared/entities/index.ts b/modules/customer-invoices/src/web/proformas/shared/entities/index.ts index ca6e9619..e1e25f31 100644 --- a/modules/customer-invoices/src/web/proformas/shared/entities/index.ts +++ b/modules/customer-invoices/src/web/proformas/shared/entities/index.ts @@ -1,4 +1,5 @@ export * from "./forms/proforma-item-form.entity"; +export * from "./invoice-series-summary.entity"; export * from "./proforma.entity"; export * from "./proforma-item.entity"; export * from "./proforma-list.entity"; diff --git a/modules/customer-invoices/src/web/proformas/shared/entities/invoice-series-summary.entity.ts b/modules/customer-invoices/src/web/proformas/shared/entities/invoice-series-summary.entity.ts new file mode 100644 index 00000000..30362bba --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/shared/entities/invoice-series-summary.entity.ts @@ -0,0 +1,7 @@ +export interface InvoiceSeriesSummary { + id: string; + code: string; + nextNumber: number; + paddingLength: number; + isDefault: boolean; +} diff --git a/modules/customer-invoices/src/web/proformas/shared/hooks/index.ts b/modules/customer-invoices/src/web/proformas/shared/hooks/index.ts index 92bd72af..08d283a2 100644 --- a/modules/customer-invoices/src/web/proformas/shared/hooks/index.ts +++ b/modules/customer-invoices/src/web/proformas/shared/hooks/index.ts @@ -1,4 +1,5 @@ export * from "./keys"; +export * from "./use-invoice-series-query"; export * from "./use-proforma-change-status-mutation"; export * from "./use-proforma-create-mutation"; export * from "./use-proforma-delete-mutation"; diff --git a/modules/customer-invoices/src/web/proformas/shared/hooks/keys.ts b/modules/customer-invoices/src/web/proformas/shared/hooks/keys.ts index 5a1bc9ad..2c19cd4e 100644 --- a/modules/customer-invoices/src/web/proformas/shared/hooks/keys.ts +++ b/modules/customer-invoices/src/web/proformas/shared/hooks/keys.ts @@ -6,6 +6,7 @@ import type { ProformasListRequestDTO } from "../../../../common"; * Prefijo base para listados */ export const LIST_PROFORMAS_QUERY_KEY_PREFIX = ["proformas"] as const; +export const INVOICE_SERIES_QUERY_KEY = ["customer-invoices", "invoice-series"] as const; /** * Query key para listado de proformas diff --git a/modules/customer-invoices/src/web/proformas/shared/hooks/use-invoice-series-query.ts b/modules/customer-invoices/src/web/proformas/shared/hooks/use-invoice-series-query.ts new file mode 100644 index 00000000..db4a12e8 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/shared/hooks/use-invoice-series-query.ts @@ -0,0 +1,20 @@ +import { useDataSource } from "@erp/core/hooks"; +import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query"; + +import { ListInvoiceSeriesAdapter } from "../adapters"; +import { listInvoiceSeries } from "../api"; +import type { InvoiceSeriesSummary } from "../entities"; + +import { INVOICE_SERIES_QUERY_KEY } from "./keys"; + +export const useInvoiceSeriesQuery = (): UseQueryResult => { + const dataSource = useDataSource(); + + return useQuery({ + queryKey: INVOICE_SERIES_QUERY_KEY, + queryFn: async ({ signal }) => { + const dto = await listInvoiceSeries(dataSource, signal); + return ListInvoiceSeriesAdapter.fromDto(dto); + }, + }); +}; diff --git a/modules/customer-invoices/src/web/proformas/shared/utils/build-invoice-series-select-items.ts b/modules/customer-invoices/src/web/proformas/shared/utils/build-invoice-series-select-items.ts new file mode 100644 index 00000000..6876a83f --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/shared/utils/build-invoice-series-select-items.ts @@ -0,0 +1,12 @@ +import type { SelectFieldItem } from "@repo/rdx-ui/components"; + +import type { InvoiceSeriesSummary } from "../entities"; + +export const buildInvoiceSeriesSelectItems = ( + invoiceSeries: InvoiceSeriesSummary[] +): SelectFieldItem[] => { + return invoiceSeries.map((series) => ({ + value: series.code, + label: series.code, + })); +}; diff --git a/modules/customer-invoices/src/web/proformas/shared/utils/ensure-current-value-in-select-items.ts b/modules/customer-invoices/src/web/proformas/shared/utils/ensure-current-value-in-select-items.ts new file mode 100644 index 00000000..951bbf77 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/shared/utils/ensure-current-value-in-select-items.ts @@ -0,0 +1,19 @@ +import type { SelectFieldItem } from "@repo/rdx-ui/components"; + +export const ensureCurrentValueInSelectItems = ( + items: SelectFieldItem[], + currentValue?: string | null +): SelectFieldItem[] => { + if (!currentValue) return items; + + const exists = items.some((item) => item.value === currentValue); + if (exists) return items; + + return [ + { + value: currentValue, + label: currentValue, + }, + ...items, + ]; +}; diff --git a/modules/customer-invoices/src/web/proformas/shared/utils/index.ts b/modules/customer-invoices/src/web/proformas/shared/utils/index.ts index c63eeb8d..3fbccf65 100644 --- a/modules/customer-invoices/src/web/proformas/shared/utils/index.ts +++ b/modules/customer-invoices/src/web/proformas/shared/utils/index.ts @@ -1,2 +1,4 @@ +export * from "./build-invoice-series-select-items"; export * from "./calculate-proforma-due-dates"; +export * from "./ensure-current-value-in-select-items"; export * from "./proforma-fiscal-options.utils"; diff --git a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-controller.ts b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-controller.ts index 8a5ff263..a73b2c54 100644 --- a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-controller.ts +++ b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-controller.ts @@ -15,7 +15,13 @@ import type { FieldErrors } from "react-hook-form"; import { useTranslation } from "../../../i18n"; import type { UpdateProformaByIdParams } from "../../shared"; import type { Proforma } from "../../shared/entities"; -import { useProformaGetQuery, useProformaUpdateMutation } from "../../shared/hooks"; +import { + buildInvoiceSeriesSelectItems, + ensureCurrentValueInSelectItems, + useInvoiceSeriesQuery, + useProformaGetQuery, + useProformaUpdateMutation, +} from "../../shared"; import { mapProformaToProformaUpdateForm, mapProformaToSelectedCustomer } from "../adapters"; import { type ProformaUpdateForm, ProformaUpdateFormSchema } from "../entities"; import { @@ -72,6 +78,8 @@ export const useUpdateProformaController = ( error: updateError, } = useProformaUpdateMutation(); + const invoiceSeriesQuery = useInvoiceSeriesQuery(); + const initialValues = useMemo(() => { if (!proformaData) return buildProformaUpdateDefault(); @@ -221,7 +229,7 @@ export const useUpdateProformaController = ( options?.onError?.(normalizedError, params); } }, - (errors: FieldErrors) => { + (_errors: FieldErrors) => { focusFirstInputFormError(form); showWarningToast( @@ -239,6 +247,12 @@ export const useUpdateProformaController = ( const currencyCode = form.watch("currencyCode"); const languageCode = form.watch("languageCode"); + const currentSeries = form.watch("series"); + + const seriesOptions = useMemo(() => { + const activeSeriesItems = buildInvoiceSeriesSelectItems(invoiceSeriesQuery.data ?? []); + return ensureCurrentValueInSelectItems(activeSeriesItems, currentSeries); + }, [currentSeries, invoiceSeriesQuery.data]); const taxCtrl = useUpdateProformaTaxController({ form, @@ -263,6 +277,10 @@ export const useUpdateProformaController = ( taxCtrl, totalsCtrl, paymentCtrl, + seriesOptions, + isSeriesLoading: invoiceSeriesQuery.isLoading, + isSeriesLoadError: invoiceSeriesQuery.isError, + seriesLoadError: invoiceSeriesQuery.error, // currencyCode, diff --git a/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-editor-form.tsx b/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-editor-form.tsx index f224fe4c..61db6a12 100644 --- a/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-editor-form.tsx +++ b/modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-editor-form.tsx @@ -1,6 +1,7 @@ // modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-update-editor.tsx import type { CustomerSelectionOption } from "@erp/customers"; +import type { SelectFieldItem } from "@repo/rdx-ui/components"; import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers"; import { cn } from "@repo/shadcn-ui/lib/utils"; @@ -27,6 +28,7 @@ type ProformaUpdateEditorProps = { selectedCustomer?: CustomerSelectionOption | null; onChangeCustomerClick: () => void; onCreateCustomerClick: () => void; + seriesOptions: SelectFieldItem[]; itemsCtrl: UseUpdateProformaItemsControllerResult; taxCtrl: UseUpdateProformaTaxControllerResult; @@ -44,6 +46,7 @@ export const ProformaUpdateEditorForm = ({ selectedCustomer, onChangeCustomerClick, onCreateCustomerClick, + seriesOptions, itemsCtrl, taxCtrl, totalsCtrl, @@ -58,7 +61,11 @@ export const ProformaUpdateEditorForm = ({
- +
- - + + { title={t("pages.proformas.update.error_title", "No se pudo guardar los cambios")} /> )} + + {updateCtrl.isSeriesLoadError && ( + + )}
{ onSubmit={updateCtrl.onSubmit} paymentCtrl={updateCtrl.paymentCtrl} selectedCustomer={updateCtrl.selectedCustomer} + seriesOptions={updateCtrl.seriesOptions} taxCtrl={updateCtrl.taxCtrl} totalsCtrl={updateCtrl.totalsCtrl} />