From 7f305c930e404d51d41e5275b339df107f6f8dd6 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 14 Aug 2026 10:51:36 +0200 Subject: [PATCH] feat(tax-regimes): implement tax regime management features - Add entities for tax regime forms and error view models. - Create schemas for tax regime forms using Zod for validation. - Implement UI components for creating, deleting, and managing tax regimes. - Develop hooks for CRUD operations on tax regimes, including create, update, delete, and enable/disable functionalities. - Add error handling and loading states for better user experience. - Create utility functions for building form data and handling API responses. - Implement skeleton and empty state components for improved UI feedback. --- modules/catalogs/src/common/locales/en.json | 102 +++++++ modules/catalogs/src/common/locales/es.json | 102 +++++++ modules/catalogs/src/web/catalogs-routes.tsx | 14 + modules/catalogs/src/web/manifest.test.ts | 15 + modules/catalogs/src/web/manifest.ts | 6 + modules/catalogs/src/web/settings/index.ts | 1 + ...use-payment-methods-settings-controller.ts | 14 +- .../components/payment-method-row-actions.tsx | 8 +- .../pages/payment-methods-settings-page.tsx | 1 + .../settings/tax-regimes/controllers/index.ts | 2 + .../use-tax-regimes-settings-controller.ts | 282 ++++++++++++++++++ ...se-tax-regimes-settings-page-controller.ts | 45 +++ .../settings/tax-regimes/entities/index.ts | 3 + .../entities/tax-regime-form.entity.ts | 5 + .../entities/tax-regime-form.schema.ts | 9 + ...s-settings-page-error-view-model.entity.ts | 9 + .../src/web/settings/tax-regimes/index.ts | 1 + .../components/delete-tax-regime-dialog.tsx | 62 ++++ .../tax-regimes/ui/components/index.ts | 5 + .../ui/components/tax-regime-form-dialog.tsx | 124 ++++++++ .../ui/components/tax-regime-row-actions.tsx | 89 ++++++ .../ui/components/tax-regimes-empty-state.tsx | 22 ++ .../tax-regimes-settings-skeleton.tsx | 15 + .../src/web/settings/tax-regimes/ui/index.ts | 2 + .../settings/tax-regimes/ui/pages/index.ts | 1 + .../ui/pages/tax-regimes-settings-page.tsx | 183 ++++++++++++ .../utils/build-tax-regime-form.ts | 14 + .../utils/build-tax-regime-params.ts | 26 ++ ...-regimes-settings-page-error-view-model.ts | 43 +++ .../web/settings/tax-regimes/utils/index.ts | 4 + ...lve-tax-regimes-settings-feedback-state.ts | 14 + .../adapters/get-tax-regime-by-id.adapter.ts | 15 + .../web/tax-regimes/shared/adapters/index.ts | 1 + .../adapters/list-tax-regimes.adapter.ts | 6 +- .../shared/api/create-tax-regime.api.ts | 18 ++ .../shared/api/delete-tax-regime.api.ts | 9 + .../shared/api/disable-tax-regime.api.ts | 18 ++ .../shared/api/enable-tax-regime.api.ts | 18 ++ .../shared/api/get-tax-regime-by-id.api.ts | 17 ++ .../src/web/tax-regimes/shared/api/index.ts | 6 + .../shared/api/update-tax-regime.api.ts | 19 ++ .../src/web/tax-regimes/shared/hooks/index.ts | 6 + .../src/web/tax-regimes/shared/hooks/keys.ts | 12 +- .../shared/hooks/to-validation-errors.ts | 9 + .../hooks/use-tax-regime-create-mutation.ts | 44 +++ .../hooks/use-tax-regime-delete-mutation.ts | 28 ++ .../hooks/use-tax-regime-detail-query.ts | 29 ++ .../hooks/use-tax-regime-disable-mutation.ts | 29 ++ .../hooks/use-tax-regime-enable-mutation.ts | 29 ++ .../hooks/use-tax-regime-update-mutation.ts | 44 +++ .../src/web/tax-regimes/shared/index.ts | 2 + 51 files changed, 1573 insertions(+), 9 deletions(-) create mode 100644 modules/catalogs/src/web/settings/tax-regimes/controllers/index.ts create mode 100644 modules/catalogs/src/web/settings/tax-regimes/controllers/use-tax-regimes-settings-controller.ts create mode 100644 modules/catalogs/src/web/settings/tax-regimes/controllers/use-tax-regimes-settings-page-controller.ts create mode 100644 modules/catalogs/src/web/settings/tax-regimes/entities/index.ts create mode 100644 modules/catalogs/src/web/settings/tax-regimes/entities/tax-regime-form.entity.ts create mode 100644 modules/catalogs/src/web/settings/tax-regimes/entities/tax-regime-form.schema.ts create mode 100644 modules/catalogs/src/web/settings/tax-regimes/entities/tax-regimes-settings-page-error-view-model.entity.ts create mode 100644 modules/catalogs/src/web/settings/tax-regimes/index.ts create mode 100644 modules/catalogs/src/web/settings/tax-regimes/ui/components/delete-tax-regime-dialog.tsx create mode 100644 modules/catalogs/src/web/settings/tax-regimes/ui/components/index.ts create mode 100644 modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regime-form-dialog.tsx create mode 100644 modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regime-row-actions.tsx create mode 100644 modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regimes-empty-state.tsx create mode 100644 modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regimes-settings-skeleton.tsx create mode 100644 modules/catalogs/src/web/settings/tax-regimes/ui/index.ts create mode 100644 modules/catalogs/src/web/settings/tax-regimes/ui/pages/index.ts create mode 100644 modules/catalogs/src/web/settings/tax-regimes/ui/pages/tax-regimes-settings-page.tsx create mode 100644 modules/catalogs/src/web/settings/tax-regimes/utils/build-tax-regime-form.ts create mode 100644 modules/catalogs/src/web/settings/tax-regimes/utils/build-tax-regime-params.ts create mode 100644 modules/catalogs/src/web/settings/tax-regimes/utils/build-tax-regimes-settings-page-error-view-model.ts create mode 100644 modules/catalogs/src/web/settings/tax-regimes/utils/index.ts create mode 100644 modules/catalogs/src/web/settings/tax-regimes/utils/resolve-tax-regimes-settings-feedback-state.ts create mode 100644 modules/catalogs/src/web/tax-regimes/shared/adapters/get-tax-regime-by-id.adapter.ts create mode 100644 modules/catalogs/src/web/tax-regimes/shared/api/create-tax-regime.api.ts create mode 100644 modules/catalogs/src/web/tax-regimes/shared/api/delete-tax-regime.api.ts create mode 100644 modules/catalogs/src/web/tax-regimes/shared/api/disable-tax-regime.api.ts create mode 100644 modules/catalogs/src/web/tax-regimes/shared/api/enable-tax-regime.api.ts create mode 100644 modules/catalogs/src/web/tax-regimes/shared/api/get-tax-regime-by-id.api.ts create mode 100644 modules/catalogs/src/web/tax-regimes/shared/api/update-tax-regime.api.ts create mode 100644 modules/catalogs/src/web/tax-regimes/shared/hooks/to-validation-errors.ts create mode 100644 modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-create-mutation.ts create mode 100644 modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-delete-mutation.ts create mode 100644 modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-detail-query.ts create mode 100644 modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-disable-mutation.ts create mode 100644 modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-enable-mutation.ts create mode 100644 modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-update-mutation.ts diff --git a/modules/catalogs/src/common/locales/en.json b/modules/catalogs/src/common/locales/en.json index 493a5a35..512ef50e 100644 --- a/modules/catalogs/src/common/locales/en.json +++ b/modules/catalogs/src/common/locales/en.json @@ -127,6 +127,108 @@ "description": "An unexpected error occurred while loading payment methods." } } + }, + "taxRegimes": { + "title": "Tax regimes", + "description": "Manage the tax regimes available for the company.", + "fields": { + "code": "Code", + "codeDescription": "Official tax regime code.", + "codePlaceholder": "e.g. 01", + "description": "Description", + "descriptionDescription": "Visible name of the tax regime.", + "descriptionPlaceholder": "e.g. General regime", + "active": "Active", + "activeDescription": "Controls whether the tax regime is available." + }, + "actions": { + "create": "New tax regime", + "creating": "Creating…", + "edit": "Edit", + "save": "Save", + "saving": "Saving…", + "cancel": "Cancel", + "enable": "Enable", + "disable": "Disable", + "delete": "Delete", + "deleting": "Deleting…", + "more": "More actions" + }, + "status": { + "active": "Active", + "inactive": "Inactive", + "system": "System" + }, + "create": { + "title": "New tax regime", + "description": "Create a new tax regime for this company." + }, + "edit": { + "title": "Edit tax regime", + "description": "Update the selected tax regime information." + }, + "delete": { + "title": "Delete tax regime", + "description": "This tax regime will stop being available for the company." + }, + "empty": { + "title": "There are no tax regimes configured.", + "description": "Available tax regimes will appear here." + }, + "messages": { + "createdTitle": "Tax regime created", + "created": "The tax regime was created successfully.", + "updatedTitle": "Tax regime updated", + "updated": "Changes were saved successfully.", + "enabledTitle": "Tax regime enabled", + "enabled": "The tax regime is available again.", + "disabledTitle": "Tax regime disabled", + "disabled": "The tax regime is no longer available.", + "deletedTitle": "Tax regime deleted", + "deleted": "The tax regime is no longer available.", + "validationTitle": "Review the fields", + "validationDescription": "There are validation errors in the form." + }, + "protections": { + "systemEdit": "System tax regimes cannot be edited.", + "systemDisable": "System tax regimes cannot be disabled.", + "systemDelete": "System tax regimes cannot be deleted.", + "codeEdit": "The code cannot be changed once created." + }, + "pageError": { + "actions": { + "retry": "Retry", + "retrying": "Retrying…" + }, + "forbidden": { + "title": "You do not have access to this setting", + "description": "You do not have permission to view tax regimes." + }, + "notFound": { + "title": "The settings could not be loaded", + "description": "The requested settings are no longer available." + }, + "network": { + "title": "Could not connect to the server", + "description": "Check your connection and try again." + }, + "timeout": { + "title": "Loading is taking too long", + "description": "The server did not respond in time. Try again." + }, + "unavailable": { + "title": "Service unavailable", + "description": "The settings are not available right now." + }, + "server": { + "title": "The settings could not be loaded", + "description": "An internal error occurred while loading tax regimes." + }, + "unknown": { + "title": "The settings could not be loaded", + "description": "An unexpected error occurred while loading tax regimes." + } + } } } } diff --git a/modules/catalogs/src/common/locales/es.json b/modules/catalogs/src/common/locales/es.json index 20b8081f..fde6dbd4 100644 --- a/modules/catalogs/src/common/locales/es.json +++ b/modules/catalogs/src/common/locales/es.json @@ -127,6 +127,108 @@ "description": "Se produjo un error inesperado al obtener los métodos de pago." } } + }, + "taxRegimes": { + "title": "Regímenes fiscales", + "description": "Gestiona los regímenes fiscales disponibles para la empresa.", + "fields": { + "code": "Código", + "codeDescription": "Código oficial del régimen fiscal.", + "codePlaceholder": "Ej. 01", + "description": "Descripción", + "descriptionDescription": "Nombre visible del régimen fiscal.", + "descriptionPlaceholder": "Ej. Régimen general", + "active": "Activo", + "activeDescription": "Controla si el régimen fiscal aparece como disponible." + }, + "actions": { + "create": "Nuevo régimen", + "creating": "Creando…", + "edit": "Editar", + "save": "Guardar", + "saving": "Guardando…", + "cancel": "Cancelar", + "enable": "Activar", + "disable": "Desactivar", + "delete": "Eliminar", + "deleting": "Eliminando…", + "more": "Más acciones" + }, + "status": { + "active": "Activo", + "inactive": "Inactivo", + "system": "Sistema" + }, + "create": { + "title": "Nuevo régimen fiscal", + "description": "Crea un nuevo régimen fiscal para esta empresa." + }, + "edit": { + "title": "Editar régimen fiscal", + "description": "Actualiza la información del régimen fiscal seleccionado." + }, + "delete": { + "title": "Eliminar régimen fiscal", + "description": "Este régimen fiscal dejará de estar disponible para la empresa." + }, + "empty": { + "title": "No hay regímenes fiscales configurados.", + "description": "Cuando existan regímenes fiscales disponibles aparecerán aquí." + }, + "messages": { + "createdTitle": "Régimen fiscal creado", + "created": "El régimen fiscal se ha creado correctamente.", + "updatedTitle": "Régimen fiscal actualizado", + "updated": "Los cambios se han guardado correctamente.", + "enabledTitle": "Régimen fiscal activado", + "enabled": "El régimen fiscal vuelve a estar disponible.", + "disabledTitle": "Régimen fiscal desactivado", + "disabled": "El régimen fiscal ha dejado de estar disponible.", + "deletedTitle": "Régimen fiscal eliminado", + "deleted": "El régimen fiscal ha dejado de estar disponible.", + "validationTitle": "Revisa los campos", + "validationDescription": "Hay errores de validación en el formulario." + }, + "protections": { + "systemEdit": "Los regímenes fiscales del sistema no se pueden editar.", + "systemDisable": "Los regímenes fiscales del sistema no se pueden desactivar.", + "systemDelete": "Los regímenes fiscales del sistema no se pueden eliminar.", + "codeEdit": "El código no se puede modificar una vez creado." + }, + "pageError": { + "actions": { + "retry": "Reintentar", + "retrying": "Reintentando…" + }, + "forbidden": { + "title": "No tienes acceso a esta configuración", + "description": "No dispones de permisos para consultar los regímenes fiscales." + }, + "notFound": { + "title": "No se pudo cargar la configuración", + "description": "La configuración solicitada ya no está disponible." + }, + "network": { + "title": "No se pudo conectar con el servidor", + "description": "Comprueba tu conexión e inténtalo de nuevo." + }, + "timeout": { + "title": "La carga está tardando demasiado", + "description": "El servidor no respondió a tiempo. Inténtalo de nuevo." + }, + "unavailable": { + "title": "Servicio no disponible", + "description": "La configuración no está disponible en este momento." + }, + "server": { + "title": "No se pudo cargar la configuración", + "description": "Se produjo un error interno al obtener los regímenes fiscales." + }, + "unknown": { + "title": "No se pudo cargar la configuración", + "description": "Se produjo un error inesperado al obtener los regímenes fiscales." + } + } } } } diff --git a/modules/catalogs/src/web/catalogs-routes.tsx b/modules/catalogs/src/web/catalogs-routes.tsx index b83eaef4..47ac8dee 100644 --- a/modules/catalogs/src/web/catalogs-routes.tsx +++ b/modules/catalogs/src/web/catalogs-routes.tsx @@ -8,6 +8,12 @@ const PaymentMethodsSettingsPage = lazy(() => })) ); +const TaxRegimesSettingsPage = lazy(() => + import("./settings/tax-regimes").then((module) => ({ + default: module.TaxRegimesSettingsPage, + })) +); + export const CatalogsRoutes = (_params: ModuleClientParams): ModuleRouteObject[] => { return [ { @@ -18,5 +24,13 @@ export const CatalogsRoutes = (_params: ModuleClientParams): ModuleRouteObject[] }, element: , }, + { + path: "settings/general/tax-regimes", + handle: { + layout: "app-sidebar", + protected: true, + }, + element: , + }, ]; }; diff --git a/modules/catalogs/src/web/manifest.test.ts b/modules/catalogs/src/web/manifest.test.ts index 792dd007..d90890b5 100644 --- a/modules/catalogs/src/web/manifest.test.ts +++ b/modules/catalogs/src/web/manifest.test.ts @@ -12,6 +12,12 @@ test("publica la entry de Settings para payment methods", () => { labelKey: "settings.paymentMethods.title", path: "/settings/general/payment-methods", }, + { + id: "tax-regimes", + sectionId: "general", + labelKey: "settings.taxRegimes.title", + path: "/settings/general/tax-regimes", + }, ], }); }); @@ -24,3 +30,12 @@ test("expone una ruta modular para settings/general/payment-methods", () => { assert.equal(settingsRoute?.handle?.layout, "app-sidebar"); assert.equal(settingsRoute?.handle?.protected, true); }); + +test("expone una ruta modular para settings/general/tax-regimes", () => { + const routes = CatalogsModuleManifest.routes?.({}); + const settingsRoute = routes?.find((route) => route.path === "settings/general/tax-regimes"); + + assert.ok(settingsRoute); + assert.equal(settingsRoute?.handle?.layout, "app-sidebar"); + assert.equal(settingsRoute?.handle?.protected, true); +}); diff --git a/modules/catalogs/src/web/manifest.ts b/modules/catalogs/src/web/manifest.ts index d9d36987..1b10311c 100644 --- a/modules/catalogs/src/web/manifest.ts +++ b/modules/catalogs/src/web/manifest.ts @@ -22,6 +22,12 @@ export const CatalogsModuleManifest: IModuleClient = { labelKey: "settings.paymentMethods.title", path: "/settings/general/payment-methods", }, + { + id: "tax-regimes", + sectionId: "general", + labelKey: "settings.taxRegimes.title", + path: "/settings/general/tax-regimes", + }, ], }, routes: (params) => CatalogsRoutes(params), diff --git a/modules/catalogs/src/web/settings/index.ts b/modules/catalogs/src/web/settings/index.ts index cd29a5d4..b56fbeed 100644 --- a/modules/catalogs/src/web/settings/index.ts +++ b/modules/catalogs/src/web/settings/index.ts @@ -1 +1,2 @@ export * from "./payment-methods"; +export * from "./tax-regimes"; diff --git a/modules/catalogs/src/web/settings/payment-methods/controllers/use-payment-methods-settings-controller.ts b/modules/catalogs/src/web/settings/payment-methods/controllers/use-payment-methods-settings-controller.ts index 9336bb61..befa7879 100644 --- a/modules/catalogs/src/web/settings/payment-methods/controllers/use-payment-methods-settings-controller.ts +++ b/modules/catalogs/src/web/settings/payment-methods/controllers/use-payment-methods-settings-controller.ts @@ -78,6 +78,7 @@ export const usePaymentMethodsSettingsController = () => { const isSaving = createMutation.isPending || updateMutation.isPending || editingPaymentMethodQuery.isLoading; + const isTogglingActive = enableMutation.isPending || disableMutation.isPending; const closeCreateDialog = useCallback(() => { if (isSaving) { @@ -209,6 +210,10 @@ export const usePaymentMethodsSettingsController = () => { const enable = useCallback( (paymentMethod: PaymentMethodListRow) => { + if (isTogglingActive) { + return; + } + enableMutation .mutateAsync({ paymentMethodId: paymentMethod.id }) .then(() => { @@ -222,11 +227,15 @@ export const usePaymentMethodsSettingsController = () => { showErrorToast(userMessage.title, userMessage.description); }); }, - [enableMutation, t] + [enableMutation, isTogglingActive, t] ); const disable = useCallback( (paymentMethod: PaymentMethodListRow) => { + if (isTogglingActive) { + return; + } + disableMutation .mutateAsync({ paymentMethodId: paymentMethod.id }) .then(() => { @@ -240,7 +249,7 @@ export const usePaymentMethodsSettingsController = () => { showErrorToast(userMessage.title, userMessage.description); }); }, - [disableMutation, t] + [disableMutation, isTogglingActive, t] ); const setDefault = useCallback( @@ -307,6 +316,7 @@ export const usePaymentMethodsSettingsController = () => { deletingPaymentMethod, isSaving, isDeleting: deleteMutation.isPending, + isTogglingActive, isChangingDefault: setDefaultMutation.isPending, openCreateDialog, closeCreateDialog, diff --git a/modules/catalogs/src/web/settings/payment-methods/ui/components/payment-method-row-actions.tsx b/modules/catalogs/src/web/settings/payment-methods/ui/components/payment-method-row-actions.tsx index 712d4cf3..da6bb337 100644 --- a/modules/catalogs/src/web/settings/payment-methods/ui/components/payment-method-row-actions.tsx +++ b/modules/catalogs/src/web/settings/payment-methods/ui/components/payment-method-row-actions.tsx @@ -1,5 +1,5 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@repo/shadcn-ui/components"; -import { Button } from "@repo/shadcn-ui/components"; +import { Button, Spinner } from "@repo/shadcn-ui/components"; import { MoreHorizontalIcon } from "lucide-react"; import { useTranslation } from "../../../../i18n"; @@ -12,6 +12,7 @@ interface PaymentMethodRowActionsProps { onDelete: (paymentMethod: PaymentMethodListRow) => void; activeDisabledReason?: string; deleteDisabledReason?: string; + isTogglingActive?: boolean; } export const PaymentMethodRowActions = ({ @@ -21,11 +22,12 @@ export const PaymentMethodRowActions = ({ onDelete, activeDisabledReason, deleteDisabledReason, + isTogglingActive = false, }: PaymentMethodRowActionsProps) => { const { t } = useTranslation(); const editDisabled = paymentMethod.isSystem; - const toggleDisabled = paymentMethod.isSystem || paymentMethod.isDefault; + const toggleDisabled = paymentMethod.isSystem || paymentMethod.isDefault || isTogglingActive; const deleteDisabled = paymentMethod.isSystem || paymentMethod.isDefault; return ( @@ -49,12 +51,14 @@ export const PaymentMethodRowActions = ({ ? t("settings.paymentMethods.actions.disable") : t("settings.paymentMethods.actions.enable") } + aria-busy={isTogglingActive} disabled={toggleDisabled} onClick={() => onToggleActive(paymentMethod)} size="sm" title={toggleDisabled ? activeDisabledReason : undefined} variant="outline" > + {isTogglingActive ? : null} {paymentMethod.isActive ? t("settings.paymentMethods.actions.disable") : t("settings.paymentMethods.actions.enable")} diff --git a/modules/catalogs/src/web/settings/payment-methods/ui/pages/payment-methods-settings-page.tsx b/modules/catalogs/src/web/settings/payment-methods/ui/pages/payment-methods-settings-page.tsx index 37991ba4..64e94e46 100644 --- a/modules/catalogs/src/web/settings/payment-methods/ui/pages/payment-methods-settings-page.tsx +++ b/modules/catalogs/src/web/settings/payment-methods/ui/pages/payment-methods-settings-page.tsx @@ -160,6 +160,7 @@ export const PaymentMethodsSettingsPage = () => { paymentMethod.isDefault, paymentMethod.isSystem )} + isTogglingActive={featureCtrl.isTogglingActive} onDelete={featureCtrl.openDeleteDialog} onEdit={featureCtrl.openEditDialog} onToggleActive={(item) => { diff --git a/modules/catalogs/src/web/settings/tax-regimes/controllers/index.ts b/modules/catalogs/src/web/settings/tax-regimes/controllers/index.ts new file mode 100644 index 00000000..29137e57 --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/controllers/index.ts @@ -0,0 +1,2 @@ +export * from "./use-tax-regimes-settings-controller"; +export * from "./use-tax-regimes-settings-page-controller"; diff --git a/modules/catalogs/src/web/settings/tax-regimes/controllers/use-tax-regimes-settings-controller.ts b/modules/catalogs/src/web/settings/tax-regimes/controllers/use-tax-regimes-settings-controller.ts new file mode 100644 index 00000000..2a22b45e --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/controllers/use-tax-regimes-settings-controller.ts @@ -0,0 +1,282 @@ +import { applyValidationErrorCollection } from "@erp/core"; +import { getUserErrorMessage } from "@erp/core/client"; +import { useHookForm } from "@erp/core/hooks"; +import { UniqueID, ValidationErrorCollection, isValidationErrorCollection } from "@repo/rdx-ddd"; +import { + focusFirstInputFormError, + showErrorToast, + showSuccessToast, + showWarningToast, +} from "@repo/rdx-ui/helpers"; +import { useCallback, useEffect, useId, useState } from "react"; +import type { FormEvent } from "react"; +import type { FieldErrors } from "react-hook-form"; + +import { useTranslation } from "../../../i18n"; +import { + type TaxRegimeListRow, + useTaxRegimeCreateMutation, + useTaxRegimeDeleteMutation, + useTaxRegimeDetailQuery, + useTaxRegimeDisableMutation, + useTaxRegimeEnableMutation, + useTaxRegimeUpdateMutation, +} from "../../../tax-regimes/shared"; +import type { TaxRegimeForm } from "../entities"; +import { TaxRegimeFormSchema } from "../entities"; +import { buildTaxRegimeFormDefault, buildTaxRegimeFormFromEntity } from "../utils"; +import { buildCreateTaxRegimeParams, buildUpdateTaxRegimeParams } from "../utils/build-tax-regime-params"; + +const normalizeFormSubmitError = (error: unknown): Error | ValidationErrorCollection => { + if (isValidationErrorCollection(error)) { + return error; + } + + if (error instanceof Error) { + return error; + } + + return new Error("Unknown error"); +}; + +export const useTaxRegimesSettingsController = () => { + const { t } = useTranslation(); + const formId = useId(); + const createMutation = useTaxRegimeCreateMutation(); + const updateMutation = useTaxRegimeUpdateMutation(); + const deleteMutation = useTaxRegimeDeleteMutation(); + const enableMutation = useTaxRegimeEnableMutation(); + const disableMutation = useTaxRegimeDisableMutation(); + + const [isCreateOpen, setIsCreateOpen] = useState(false); + const [editingTaxRegimeId, setEditingTaxRegimeId] = useState(null); + const [deletingTaxRegime, setDeletingTaxRegime] = useState(null); + + const editingTaxRegimeQuery = useTaxRegimeDetailQuery(editingTaxRegimeId ?? undefined, { + enabled: editingTaxRegimeId !== null, + }); + const editingTaxRegime = editingTaxRegimeQuery.data ?? null; + + const form = useHookForm({ + resolverSchema: TaxRegimeFormSchema, + initialValues: buildTaxRegimeFormDefault(), + disabled: + createMutation.isPending || updateMutation.isPending || editingTaxRegimeQuery.isLoading, + }); + + const isSaving = + createMutation.isPending || updateMutation.isPending || editingTaxRegimeQuery.isLoading; + const isTogglingActive = enableMutation.isPending || disableMutation.isPending; + + const closeCreateDialog = useCallback(() => { + if (isSaving) { + return; + } + + setIsCreateOpen(false); + form.reset(buildTaxRegimeFormDefault(), { keepDirty: false }); + form.clearErrors(); + }, [form, isSaving]); + + const openCreateDialog = useCallback(() => { + setEditingTaxRegimeId(null); + form.reset(buildTaxRegimeFormDefault(), { keepDirty: false }); + form.clearErrors(); + setIsCreateOpen(true); + }, [form]); + + const openEditDialog = useCallback( + (taxRegime: TaxRegimeListRow) => { + if (taxRegime.isSystem) { + return; + } + + setIsCreateOpen(false); + setEditingTaxRegimeId(taxRegime.id); + form.reset(buildTaxRegimeFormDefault(), { keepDirty: false }); + form.clearErrors(); + }, + [form] + ); + + const closeEditDialog = useCallback(() => { + if (isSaving) { + return; + } + + setEditingTaxRegimeId(null); + form.reset(buildTaxRegimeFormDefault(), { keepDirty: false }); + form.clearErrors(); + }, [form, isSaving]); + + const openDeleteDialog = useCallback((taxRegime: TaxRegimeListRow) => { + setDeletingTaxRegime(taxRegime); + }, []); + + const closeDeleteDialog = useCallback(() => { + if (deleteMutation.isPending) { + return; + } + + setDeletingTaxRegime(null); + }, [deleteMutation.isPending]); + + const onInvalidSubmit = useCallback( + (errors: FieldErrors) => { + focusFirstInputFormError(errors, form); + showWarningToast( + t("settings.taxRegimes.messages.validationTitle"), + t("settings.taxRegimes.messages.validationDescription") + ); + }, + [form, t] + ); + + const submitTaxRegime = form.handleSubmit( + async (values) => { + try { + if (editingTaxRegime) { + await updateMutation.mutateAsync(buildUpdateTaxRegimeParams(editingTaxRegime.id, values)); + + showSuccessToast( + t("settings.taxRegimes.messages.updatedTitle"), + t("settings.taxRegimes.messages.updated") + ); + + closeEditDialog(); + return; + } + + const id = UniqueID.generateNewID().toString(); + await createMutation.mutateAsync(buildCreateTaxRegimeParams(id, values)); + + showSuccessToast( + t("settings.taxRegimes.messages.createdTitle"), + t("settings.taxRegimes.messages.created") + ); + + closeCreateDialog(); + } catch (error: unknown) { + const normalizedError = normalizeFormSubmitError(error); + + if (isValidationErrorCollection(normalizedError)) { + applyValidationErrorCollection(form, normalizedError); + focusFirstInputFormError(form.formState.errors, form); + showWarningToast( + t("settings.taxRegimes.messages.validationTitle"), + t("settings.taxRegimes.messages.validationDescription") + ); + return; + } + + const userMessage = getUserErrorMessage(normalizedError); + showErrorToast(userMessage.title, userMessage.description); + } + }, + onInvalidSubmit + ); + + const onSubmit = useCallback( + (event: FormEvent) => { + submitTaxRegime(event).catch((error: unknown) => { + const userMessage = getUserErrorMessage(error); + showErrorToast(userMessage.title, userMessage.description); + }); + }, + [submitTaxRegime] + ); + + const enable = useCallback( + (taxRegime: TaxRegimeListRow) => { + if (isTogglingActive) { + return; + } + + enableMutation + .mutateAsync({ taxRegimeId: taxRegime.id }) + .then(() => { + showSuccessToast( + t("settings.taxRegimes.messages.enabledTitle"), + t("settings.taxRegimes.messages.enabled") + ); + }) + .catch((error: unknown) => { + const userMessage = getUserErrorMessage(error); + showErrorToast(userMessage.title, userMessage.description); + }); + }, + [enableMutation, isTogglingActive, t] + ); + + const disable = useCallback( + (taxRegime: TaxRegimeListRow) => { + if (isTogglingActive) { + return; + } + + disableMutation + .mutateAsync({ taxRegimeId: taxRegime.id }) + .then(() => { + showSuccessToast( + t("settings.taxRegimes.messages.disabledTitle"), + t("settings.taxRegimes.messages.disabled") + ); + }) + .catch((error: unknown) => { + const userMessage = getUserErrorMessage(error); + showErrorToast(userMessage.title, userMessage.description); + }); + }, + [disableMutation, isTogglingActive, t] + ); + + const confirmDelete = useCallback(() => { + if (!deletingTaxRegime || deleteMutation.isPending) { + return; + } + + deleteMutation + .mutateAsync({ taxRegimeId: deletingTaxRegime.id }) + .then(() => { + showSuccessToast( + t("settings.taxRegimes.messages.deletedTitle"), + t("settings.taxRegimes.messages.deleted") + ); + closeDeleteDialog(); + }) + .catch((error: unknown) => { + const userMessage = getUserErrorMessage(error); + showErrorToast(userMessage.title, userMessage.description); + }); + }, [closeDeleteDialog, deleteMutation, deletingTaxRegime, t]); + + useEffect(() => { + if (!editingTaxRegime) { + return; + } + + form.reset(buildTaxRegimeFormFromEntity(editingTaxRegime), { keepDirty: false }); + }, [editingTaxRegime, form]); + + return { + form, + formId, + isCreateOpen, + editingTaxRegime, + isEditingTaxRegimeLoading: editingTaxRegimeQuery.isLoading, + deletingTaxRegime, + isSaving, + isDeleting: deleteMutation.isPending, + isTogglingActive, + openCreateDialog, + closeCreateDialog, + openEditDialog, + closeEditDialog, + openDeleteDialog, + closeDeleteDialog, + onSubmit, + enable, + disable, + confirmDelete, + }; +}; diff --git a/modules/catalogs/src/web/settings/tax-regimes/controllers/use-tax-regimes-settings-page-controller.ts b/modules/catalogs/src/web/settings/tax-regimes/controllers/use-tax-regimes-settings-page-controller.ts new file mode 100644 index 00000000..6673ba02 --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/controllers/use-tax-regimes-settings-page-controller.ts @@ -0,0 +1,45 @@ +import { useCallback, useMemo } from "react"; + +import { useTranslation } from "../../../i18n"; +import { useTaxRegimesListQuery } from "../../../tax-regimes/shared"; +import { + buildTaxRegimesSettingsPageErrorViewModel, + resolveTaxRegimesSettingsFeedbackState, +} from "../utils"; + +import { useTaxRegimesSettingsController } from "./use-tax-regimes-settings-controller"; + +export const useTaxRegimesSettingsPageController = () => { + const { t } = useTranslation(); + const listQuery = useTaxRegimesListQuery(); + const controller = useTaxRegimesSettingsController(); + + const feedbackState = resolveTaxRegimesSettingsFeedbackState({ + isLoading: listQuery.isLoading, + isError: listQuery.isError, + error: listQuery.error, + }); + + const pageError = useMemo(() => { + if (!feedbackState.hasBlockingError) { + return null; + } + + return buildTaxRegimesSettingsPageErrorViewModel(listQuery.error, (key) => t(key)); + }, [feedbackState.hasBlockingError, listQuery.error, t]); + + const retry = useCallback(async () => { + await listQuery.refetch(); + }, [listQuery]); + + return { + list: listQuery.data?.items ?? [], + totalItems: listQuery.data?.totalItems ?? 0, + isInitialLoading: feedbackState.isInitialLoading, + pageError, + retry, + isRetrying: listQuery.isFetching && !listQuery.isLoading, + listQuery, + controller, + }; +}; diff --git a/modules/catalogs/src/web/settings/tax-regimes/entities/index.ts b/modules/catalogs/src/web/settings/tax-regimes/entities/index.ts new file mode 100644 index 00000000..fc1f747e --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/entities/index.ts @@ -0,0 +1,3 @@ +export * from "./tax-regime-form.entity"; +export * from "./tax-regime-form.schema"; +export * from "./tax-regimes-settings-page-error-view-model.entity"; diff --git a/modules/catalogs/src/web/settings/tax-regimes/entities/tax-regime-form.entity.ts b/modules/catalogs/src/web/settings/tax-regimes/entities/tax-regime-form.entity.ts new file mode 100644 index 00000000..79ca5427 --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/entities/tax-regime-form.entity.ts @@ -0,0 +1,5 @@ +export interface TaxRegimeForm { + code: string; + description: string; + isActive: boolean; +} diff --git a/modules/catalogs/src/web/settings/tax-regimes/entities/tax-regime-form.schema.ts b/modules/catalogs/src/web/settings/tax-regimes/entities/tax-regime-form.schema.ts new file mode 100644 index 00000000..437dd500 --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/entities/tax-regime-form.schema.ts @@ -0,0 +1,9 @@ +import { z } from "zod/v4"; + +export const TaxRegimeFormSchema = z.object({ + code: z.string().trim().regex(/^\d{2}$/), + description: z.string().trim().min(1), + isActive: z.boolean(), +}); + +export type TaxRegimeFormValues = z.infer; diff --git a/modules/catalogs/src/web/settings/tax-regimes/entities/tax-regimes-settings-page-error-view-model.entity.ts b/modules/catalogs/src/web/settings/tax-regimes/entities/tax-regimes-settings-page-error-view-model.entity.ts new file mode 100644 index 00000000..8b73e048 --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/entities/tax-regimes-settings-page-error-view-model.entity.ts @@ -0,0 +1,9 @@ +import type { PageErrorStateVariant } from "@erp/core/components"; + +export interface TaxRegimesSettingsPageErrorViewModel { + variant: PageErrorStateVariant; + title: string; + description: string; + reference?: string; + canRetry: boolean; +} diff --git a/modules/catalogs/src/web/settings/tax-regimes/index.ts b/modules/catalogs/src/web/settings/tax-regimes/index.ts new file mode 100644 index 00000000..4aedf593 --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/index.ts @@ -0,0 +1 @@ +export * from "./ui"; diff --git a/modules/catalogs/src/web/settings/tax-regimes/ui/components/delete-tax-regime-dialog.tsx b/modules/catalogs/src/web/settings/tax-regimes/ui/components/delete-tax-regime-dialog.tsx new file mode 100644 index 00000000..046206ac --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/ui/components/delete-tax-regime-dialog.tsx @@ -0,0 +1,62 @@ +import { + AlertDialog, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + Button, + Spinner, +} from "@repo/shadcn-ui/components"; + +import { useTranslation } from "../../../../i18n"; +import type { TaxRegimeListRow } from "../../../../tax-regimes/shared"; + +interface DeleteTaxRegimeDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + target: TaxRegimeListRow | null; + isSubmitting: boolean; + onConfirm: () => void; +} + +export const DeleteTaxRegimeDialog = ({ + open, + onOpenChange, + target, + isSubmitting, + onConfirm, +}: DeleteTaxRegimeDialogProps) => { + const { t } = useTranslation(); + + return ( + + + + {t("settings.taxRegimes.delete.title")} + + {t("settings.taxRegimes.delete.description", { + code: target?.code ?? "", + })} + + + + + + + + + + ); +}; diff --git a/modules/catalogs/src/web/settings/tax-regimes/ui/components/index.ts b/modules/catalogs/src/web/settings/tax-regimes/ui/components/index.ts new file mode 100644 index 00000000..94009a8f --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/ui/components/index.ts @@ -0,0 +1,5 @@ +export * from "./tax-regime-form-dialog"; +export * from "./delete-tax-regime-dialog"; +export * from "./tax-regime-row-actions"; +export * from "./tax-regimes-empty-state"; +export * from "./tax-regimes-settings-skeleton"; diff --git a/modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regime-form-dialog.tsx b/modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regime-form-dialog.tsx new file mode 100644 index 00000000..53279917 --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regime-form-dialog.tsx @@ -0,0 +1,124 @@ +import { useEffect } from "react"; +import type { FormEventHandler } from "react"; +import { FormProvider } from "react-hook-form"; +import type { UseFormReturn } from "react-hook-form"; + +import { RdxFormGrid, SwitchField, TextField } from "@repo/rdx-ui/components"; +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + Spinner, +} from "@repo/shadcn-ui/components"; + +import { useTranslation } from "../../../../i18n"; +import type { TaxRegimeForm } from "../../entities"; + +interface TaxRegimeFormDialogProps { + open: boolean; + title: string; + description: string; + submitLabel: string; + openChange: (open: boolean) => void; + formId: string; + form: UseFormReturn; + onSubmit: FormEventHandler; + isSubmitting: boolean; + isEditMode: boolean; +} + +export const TaxRegimeFormDialog = ({ + open, + title, + description, + submitLabel, + openChange, + formId, + form, + onSubmit, + isSubmitting, + isEditMode, +}: TaxRegimeFormDialogProps) => { + const { t } = useTranslation(); + + useEffect(() => { + if (!open) { + return; + } + + form.setFocus(isEditMode ? "description" : "code"); + }, [form, isEditMode, open]); + + return ( + + + + {title} + {description} + + + +
+ + + + + +
+ +
+
+
+
+ + + + + +
+
+ ); +}; diff --git a/modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regime-row-actions.tsx b/modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regime-row-actions.tsx new file mode 100644 index 00000000..d8fc125b --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regime-row-actions.tsx @@ -0,0 +1,89 @@ +import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Spinner } from "@repo/shadcn-ui/components"; +import { MoreHorizontalIcon } from "lucide-react"; + +import { useTranslation } from "../../../../i18n"; +import type { TaxRegimeListRow } from "../../../../tax-regimes/shared"; + +interface TaxRegimeRowActionsProps { + taxRegime: TaxRegimeListRow; + onEdit: (taxRegime: TaxRegimeListRow) => void; + onToggleActive: (taxRegime: TaxRegimeListRow) => void; + onDelete: (taxRegime: TaxRegimeListRow) => void; + activeDisabledReason?: string; + deleteDisabledReason?: string; + isTogglingActive?: boolean; +} + +export const TaxRegimeRowActions = ({ + taxRegime, + onEdit, + onToggleActive, + onDelete, + activeDisabledReason, + deleteDisabledReason, + isTogglingActive = false, +}: TaxRegimeRowActionsProps) => { + const { t } = useTranslation(); + + const editDisabled = taxRegime.isSystem; + const toggleDisabled = taxRegime.isSystem || isTogglingActive; + const deleteDisabled = taxRegime.isSystem; + + return ( +
+ + + + + + + + + } + /> + onDelete(taxRegime)} + title={deleteDisabled ? deleteDisabledReason : undefined} + variant="destructive" + > + {t("settings.taxRegimes.actions.delete")} + + } + /> + +
+ ); +}; diff --git a/modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regimes-empty-state.tsx b/modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regimes-empty-state.tsx new file mode 100644 index 00000000..8bcefeb3 --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regimes-empty-state.tsx @@ -0,0 +1,22 @@ +import { Button } from "@repo/shadcn-ui/components"; + +import { useTranslation } from "../../../../i18n"; + +export const TaxRegimesEmptyState = ({ onCreate }: { onCreate: () => void }) => { + const { t } = useTranslation(); + + return ( +
+
+

{t("settings.taxRegimes.empty.title")}

+

+ {t("settings.taxRegimes.empty.description")} +

+
+ + +
+ ); +}; diff --git a/modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regimes-settings-skeleton.tsx b/modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regimes-settings-skeleton.tsx new file mode 100644 index 00000000..4ebd7713 --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regimes-settings-skeleton.tsx @@ -0,0 +1,15 @@ +import { Skeleton } from "@repo/shadcn-ui/components"; + +export const TaxRegimesSettingsSkeleton = () => { + return ( +
+ + +
+ + + +
+
+ ); +}; diff --git a/modules/catalogs/src/web/settings/tax-regimes/ui/index.ts b/modules/catalogs/src/web/settings/tax-regimes/ui/index.ts new file mode 100644 index 00000000..566fb90c --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/ui/index.ts @@ -0,0 +1,2 @@ +export * from "./pages"; +export * from "./components"; diff --git a/modules/catalogs/src/web/settings/tax-regimes/ui/pages/index.ts b/modules/catalogs/src/web/settings/tax-regimes/ui/pages/index.ts new file mode 100644 index 00000000..a77bede2 --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/ui/pages/index.ts @@ -0,0 +1 @@ +export * from "./tax-regimes-settings-page"; diff --git a/modules/catalogs/src/web/settings/tax-regimes/ui/pages/tax-regimes-settings-page.tsx b/modules/catalogs/src/web/settings/tax-regimes/ui/pages/tax-regimes-settings-page.tsx new file mode 100644 index 00000000..757c230d --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/ui/pages/tax-regimes-settings-page.tsx @@ -0,0 +1,183 @@ +import { PageErrorState } from "@erp/core/components"; +import { RdxPageContainer } from "@repo/rdx-ui/components"; +import { Badge, Button, Card, CardContent } from "@repo/shadcn-ui/components"; +import { PlusIcon } from "lucide-react"; + +import { useTranslation } from "../../../../i18n"; +import { useTaxRegimesSettingsPageController } from "../../controllers"; +import { DeleteTaxRegimeDialog } from "../components/delete-tax-regime-dialog"; +import { TaxRegimeFormDialog } from "../components/tax-regime-form-dialog"; +import { TaxRegimeRowActions } from "../components/tax-regime-row-actions"; +import { TaxRegimesEmptyState } from "../components/tax-regimes-empty-state"; +import { TaxRegimesSettingsSkeleton } from "../components/tax-regimes-settings-skeleton"; + +export const TaxRegimesSettingsPage = () => { + const { t } = useTranslation(); + const ctrl = useTaxRegimesSettingsPageController(); + const featureCtrl = ctrl.controller; + + if (ctrl.isInitialLoading) { + return ; + } + + if (ctrl.pageError) { + return ( + +
+ +
+
+ ); + } + + const activeDisabledReason = (isSystem: boolean) => { + if (isSystem) { + return t("settings.taxRegimes.protections.systemDisable"); + } + + return undefined; + }; + + const deleteDisabledReason = (isSystem: boolean) => { + if (isSystem) { + return t("settings.taxRegimes.protections.systemDelete"); + } + + return undefined; + }; + + return ( + +
+
+
+

+ {t("settings.taxRegimes.title")} +

+

+ {t("settings.taxRegimes.description")} +

+
+ + +
+ + {ctrl.list.length === 0 ? ( + + ) : ( +
+ {ctrl.list.map((taxRegime) => ( + + +
+
+ + {taxRegime.code} + + + {taxRegime.isSystem ? ( + {t("settings.taxRegimes.status.system")} + ) : null} + + + {taxRegime.isActive + ? t("settings.taxRegimes.status.active") + : t("settings.taxRegimes.status.inactive")} + +
+ +
+

{taxRegime.description}

+
+
+ + { + if (item.isActive) { + featureCtrl.disable(item); + return; + } + + featureCtrl.enable(item); + }} + taxRegime={taxRegime} + /> +
+
+ ))} +
+ )} + + { + if (!open) { + featureCtrl.closeCreateDialog(); + } + }} + submitLabel={t("settings.taxRegimes.actions.create")} + title={t("settings.taxRegimes.create.title")} + /> + + { + if (!open) { + featureCtrl.closeEditDialog(); + } + }} + submitLabel={t("settings.taxRegimes.actions.save")} + title={t("settings.taxRegimes.edit.title")} + /> + + { + if (!open) { + featureCtrl.closeDeleteDialog(); + } + }} + open={Boolean(featureCtrl.deletingTaxRegime)} + target={featureCtrl.deletingTaxRegime} + /> +
+
+ ); +}; diff --git a/modules/catalogs/src/web/settings/tax-regimes/utils/build-tax-regime-form.ts b/modules/catalogs/src/web/settings/tax-regimes/utils/build-tax-regime-form.ts new file mode 100644 index 00000000..c1e76909 --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/utils/build-tax-regime-form.ts @@ -0,0 +1,14 @@ +import type { TaxRegime } from "../../../tax-regimes/shared"; +import type { TaxRegimeForm } from "../entities"; + +export const buildTaxRegimeFormDefault = (): TaxRegimeForm => ({ + code: "", + description: "", + isActive: true, +}); + +export const buildTaxRegimeFormFromEntity = (taxRegime: TaxRegime): TaxRegimeForm => ({ + code: taxRegime.code, + description: taxRegime.description, + isActive: taxRegime.isActive, +}); diff --git a/modules/catalogs/src/web/settings/tax-regimes/utils/build-tax-regime-params.ts b/modules/catalogs/src/web/settings/tax-regimes/utils/build-tax-regime-params.ts new file mode 100644 index 00000000..f76ff144 --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/utils/build-tax-regime-params.ts @@ -0,0 +1,26 @@ +import type { CreateTaxRegimeParams, UpdateTaxRegimeParams } from "../../../tax-regimes/shared"; +import type { TaxRegimeForm } from "../entities"; + +export const buildCreateTaxRegimeParams = ( + id: string, + form: TaxRegimeForm +): CreateTaxRegimeParams => ({ + id, + data: { + id, + code: form.code.trim(), + description: form.description.trim(), + is_active: form.isActive, + }, +}); + +export const buildUpdateTaxRegimeParams = ( + taxRegimeId: string, + form: TaxRegimeForm +): UpdateTaxRegimeParams => ({ + taxRegimeId, + data: { + description: form.description.trim(), + is_active: form.isActive, + }, +}); diff --git a/modules/catalogs/src/web/settings/tax-regimes/utils/build-tax-regimes-settings-page-error-view-model.ts b/modules/catalogs/src/web/settings/tax-regimes/utils/build-tax-regimes-settings-page-error-view-model.ts new file mode 100644 index 00000000..7ef8c634 --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/utils/build-tax-regimes-settings-page-error-view-model.ts @@ -0,0 +1,43 @@ +import { isApiClientError } from "@erp/core/client"; + +import type { TaxRegimesSettingsPageErrorViewModel } from "../entities"; + +export const buildTaxRegimesSettingsPageErrorViewModel = ( + error: unknown, + translate: (key: string) => string +): TaxRegimesSettingsPageErrorViewModel => { + if (!isApiClientError(error)) { + return { + variant: "server", + title: translate("settings.taxRegimes.pageError.unknown.title"), + description: translate("settings.taxRegimes.pageError.unknown.description"), + canRetry: true, + }; + } + + if (error.kind === "forbidden") { + return { + variant: "forbidden", + title: translate("settings.taxRegimes.pageError.forbidden.title"), + description: translate("settings.taxRegimes.pageError.forbidden.description"), + reference: error.correlationId, + canRetry: false, + }; + } + + const variant = + error.kind === "network" || + error.kind === "timeout" || + error.kind === "unavailable" || + error.kind === "notFound" + ? error.kind + : "server"; + + return { + variant, + title: translate(`settings.taxRegimes.pageError.${variant}.title`), + description: translate(`settings.taxRegimes.pageError.${variant}.description`), + reference: error.correlationId, + canRetry: true, + }; +}; diff --git a/modules/catalogs/src/web/settings/tax-regimes/utils/index.ts b/modules/catalogs/src/web/settings/tax-regimes/utils/index.ts new file mode 100644 index 00000000..c29f70e9 --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/utils/index.ts @@ -0,0 +1,4 @@ +export * from "./build-tax-regime-form"; +export * from "./build-tax-regime-params"; +export * from "./build-tax-regimes-settings-page-error-view-model"; +export * from "./resolve-tax-regimes-settings-feedback-state"; diff --git a/modules/catalogs/src/web/settings/tax-regimes/utils/resolve-tax-regimes-settings-feedback-state.ts b/modules/catalogs/src/web/settings/tax-regimes/utils/resolve-tax-regimes-settings-feedback-state.ts new file mode 100644 index 00000000..46b782e7 --- /dev/null +++ b/modules/catalogs/src/web/settings/tax-regimes/utils/resolve-tax-regimes-settings-feedback-state.ts @@ -0,0 +1,14 @@ +import { isApiClientError } from "@erp/core/client"; + +export const resolveTaxRegimesSettingsFeedbackState = (params: { + isLoading: boolean; + isError: boolean; + error: unknown; +}) => { + const isCancelled = isApiClientError(params.error) && params.error.kind === "cancelled"; + + return { + isInitialLoading: params.isLoading, + hasBlockingError: params.isError && !isCancelled, + }; +}; diff --git a/modules/catalogs/src/web/tax-regimes/shared/adapters/get-tax-regime-by-id.adapter.ts b/modules/catalogs/src/web/tax-regimes/shared/adapters/get-tax-regime-by-id.adapter.ts new file mode 100644 index 00000000..4dfba7d5 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/adapters/get-tax-regime-by-id.adapter.ts @@ -0,0 +1,15 @@ +import type { TaxRegimeDetailDTO } from "../../../../common"; +import type { TaxRegime } from "../entities"; + +export const GetTaxRegimeByIdAdapter = { + fromDto(dto: TaxRegimeDetailDTO): TaxRegime { + return { + id: dto.id, + companyId: dto.company_id, + code: dto.code, + description: dto.description, + isActive: dto.is_active, + isSystem: dto.is_system, + }; + }, +}; diff --git a/modules/catalogs/src/web/tax-regimes/shared/adapters/index.ts b/modules/catalogs/src/web/tax-regimes/shared/adapters/index.ts index 7d8bb369..167d9fe1 100644 --- a/modules/catalogs/src/web/tax-regimes/shared/adapters/index.ts +++ b/modules/catalogs/src/web/tax-regimes/shared/adapters/index.ts @@ -1 +1,2 @@ export * from "./list-tax-regimes.adapter"; +export * from "./get-tax-regime-by-id.adapter"; diff --git a/modules/catalogs/src/web/tax-regimes/shared/adapters/list-tax-regimes.adapter.ts b/modules/catalogs/src/web/tax-regimes/shared/adapters/list-tax-regimes.adapter.ts index d15db0c3..a302ffa5 100644 --- a/modules/catalogs/src/web/tax-regimes/shared/adapters/list-tax-regimes.adapter.ts +++ b/modules/catalogs/src/web/tax-regimes/shared/adapters/list-tax-regimes.adapter.ts @@ -15,13 +15,13 @@ import type { TaxRegimeList, TaxRegimeListRow } from "../entities"; */ export const ListTaxRegimesAdapter = { - fromDto(dto: ListTaxRegimesResult, context?: unknown): TaxRegimeList { + fromDto(dto: ListTaxRegimesResult): TaxRegimeList { return { page: dto.page, perPage: dto.per_page, totalPages: dto.total_pages, totalItems: dto.total_items, - items: dto.items.map((rowDto) => TaxRegimeListRowAdapter.fromDto(rowDto, context)), + items: dto.items.map((rowDto) => TaxRegimeListRowAdapter.fromDto(rowDto)), }; }, }; @@ -39,7 +39,7 @@ export const ListTaxRegimesAdapter = { type ListTaxRegimesItemOutput = ListTaxRegimesResponseDTO["items"][number]; const TaxRegimeListRowAdapter = { - fromDto(dto: ListTaxRegimesItemOutput, context?: unknown): TaxRegimeListRow { + fromDto(dto: ListTaxRegimesItemOutput): TaxRegimeListRow { return { id: dto.id, companyId: dto.company_id, diff --git a/modules/catalogs/src/web/tax-regimes/shared/api/create-tax-regime.api.ts b/modules/catalogs/src/web/tax-regimes/shared/api/create-tax-regime.api.ts new file mode 100644 index 00000000..73b1e027 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/api/create-tax-regime.api.ts @@ -0,0 +1,18 @@ +import type { IDataSource } from "@erp/core/client"; + +import type { CreateTaxRegimeRequestDTO, CreateTaxRegimeResponseDTO } from "../../../../common"; + +export interface CreateTaxRegimeParams { + id: string; + data: CreateTaxRegimeRequestDTO; +} + +export function createTaxRegime( + dataSource: IDataSource, + params: CreateTaxRegimeParams +): Promise { + return dataSource.createOne( + "catalogs/tax-regimes", + params.data + ); +} diff --git a/modules/catalogs/src/web/tax-regimes/shared/api/delete-tax-regime.api.ts b/modules/catalogs/src/web/tax-regimes/shared/api/delete-tax-regime.api.ts new file mode 100644 index 00000000..1c2d1031 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/api/delete-tax-regime.api.ts @@ -0,0 +1,9 @@ +import type { IDataSource } from "@erp/core/client"; + +export interface DeleteTaxRegimeParams { + taxRegimeId: string; +} + +export function deleteTaxRegime(dataSource: IDataSource, params: DeleteTaxRegimeParams) { + return dataSource.deleteOne("catalogs/tax-regimes", params.taxRegimeId); +} diff --git a/modules/catalogs/src/web/tax-regimes/shared/api/disable-tax-regime.api.ts b/modules/catalogs/src/web/tax-regimes/shared/api/disable-tax-regime.api.ts new file mode 100644 index 00000000..5cc550b3 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/api/disable-tax-regime.api.ts @@ -0,0 +1,18 @@ +import type { IDataSource } from "@erp/core/client"; + +import type { DisableTaxRegimeByIdResponseDTO } from "../../../../common"; + +export interface DisableTaxRegimeParams { + taxRegimeId: string; +} + +export function disableTaxRegime( + dataSource: IDataSource, + params: DisableTaxRegimeParams +): Promise { + return dataSource.custom({ + method: "patch", + path: `catalogs/tax-regimes/${params.taxRegimeId}/disable`, + data: undefined, + }); +} diff --git a/modules/catalogs/src/web/tax-regimes/shared/api/enable-tax-regime.api.ts b/modules/catalogs/src/web/tax-regimes/shared/api/enable-tax-regime.api.ts new file mode 100644 index 00000000..6bc3dd93 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/api/enable-tax-regime.api.ts @@ -0,0 +1,18 @@ +import type { IDataSource } from "@erp/core/client"; + +import type { EnableTaxRegimeByIdResponseDTO } from "../../../../common"; + +export interface EnableTaxRegimeParams { + taxRegimeId: string; +} + +export function enableTaxRegime( + dataSource: IDataSource, + params: EnableTaxRegimeParams +): Promise { + return dataSource.custom({ + method: "patch", + path: `catalogs/tax-regimes/${params.taxRegimeId}/enable`, + data: undefined, + }); +} diff --git a/modules/catalogs/src/web/tax-regimes/shared/api/get-tax-regime-by-id.api.ts b/modules/catalogs/src/web/tax-regimes/shared/api/get-tax-regime-by-id.api.ts new file mode 100644 index 00000000..4a62c32a --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/api/get-tax-regime-by-id.api.ts @@ -0,0 +1,17 @@ +import type { IDataSource } from "@erp/core/client"; + +import type { GetTaxRegimeByIdResponseDTO } from "../../../../common"; + +export interface GetTaxRegimeByIdParams { + taxRegimeId: string; + signal?: AbortSignal; +} + +export function getTaxRegimeById( + dataSource: IDataSource, + params: GetTaxRegimeByIdParams +): Promise { + return dataSource.getOne("catalogs/tax-regimes", params.taxRegimeId, { + signal: params.signal, + }); +} diff --git a/modules/catalogs/src/web/tax-regimes/shared/api/index.ts b/modules/catalogs/src/web/tax-regimes/shared/api/index.ts index 427e1e45..98d183b5 100644 --- a/modules/catalogs/src/web/tax-regimes/shared/api/index.ts +++ b/modules/catalogs/src/web/tax-regimes/shared/api/index.ts @@ -1 +1,7 @@ export * from "./list-tax-regimes-by-criteria.api"; +export * from "./get-tax-regime-by-id.api"; +export * from "./create-tax-regime.api"; +export * from "./update-tax-regime.api"; +export * from "./delete-tax-regime.api"; +export * from "./enable-tax-regime.api"; +export * from "./disable-tax-regime.api"; diff --git a/modules/catalogs/src/web/tax-regimes/shared/api/update-tax-regime.api.ts b/modules/catalogs/src/web/tax-regimes/shared/api/update-tax-regime.api.ts new file mode 100644 index 00000000..f4600218 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/api/update-tax-regime.api.ts @@ -0,0 +1,19 @@ +import type { IDataSource } from "@erp/core/client"; + +import type { UpdateTaxRegimeByIdRequestDTO, UpdateTaxRegimeByIdResponseDTO } from "../../../../common"; + +export interface UpdateTaxRegimeParams { + taxRegimeId: string; + data: UpdateTaxRegimeByIdRequestDTO; +} + +export function updateTaxRegime( + dataSource: IDataSource, + params: UpdateTaxRegimeParams +): Promise { + return dataSource.updateOne( + "catalogs/tax-regimes", + params.taxRegimeId, + params.data + ); +} diff --git a/modules/catalogs/src/web/tax-regimes/shared/hooks/index.ts b/modules/catalogs/src/web/tax-regimes/shared/hooks/index.ts index 6c7a9979..0d4e6c4e 100644 --- a/modules/catalogs/src/web/tax-regimes/shared/hooks/index.ts +++ b/modules/catalogs/src/web/tax-regimes/shared/hooks/index.ts @@ -1 +1,7 @@ export * from "./use-tax-regime-list-query"; +export * from "./use-tax-regime-detail-query"; +export * from "./use-tax-regime-create-mutation"; +export * from "./use-tax-regime-update-mutation"; +export * from "./use-tax-regime-delete-mutation"; +export * from "./use-tax-regime-enable-mutation"; +export * from "./use-tax-regime-disable-mutation"; diff --git a/modules/catalogs/src/web/tax-regimes/shared/hooks/keys.ts b/modules/catalogs/src/web/tax-regimes/shared/hooks/keys.ts index 8b57fab8..acd14f72 100644 --- a/modules/catalogs/src/web/tax-regimes/shared/hooks/keys.ts +++ b/modules/catalogs/src/web/tax-regimes/shared/hooks/keys.ts @@ -1,3 +1,4 @@ +import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria"; import type { QueryKey } from "@tanstack/react-query"; import type { ListTaxRegimesRequestDTO } from "../../../../common"; @@ -14,8 +15,8 @@ export const LIST_TAX_REGIMES_QUERY_KEY = (criteria?: ListTaxRegimesRequestDTO): [ ...LIST_TAX_REGIMES_QUERY_KEY_PREFIX, { - pageNumber: criteria?.pageNumber ?? 1, - pageSize: criteria?.pageSize ?? 5, + pageNumber: criteria?.pageNumber ?? INITIAL_PAGE_INDEX, + pageSize: criteria?.pageSize ?? INITIAL_PAGE_SIZE, q: criteria?.q ?? "", filters: criteria?.filters ?? [], orderBy: criteria?.orderBy ?? "", @@ -38,7 +39,14 @@ export const TAX_REGIME_QUERY_KEY = (taxRegimeId?: string): QueryKey => [ export const CREATE_TAX_REGIME_MUTATION_KEY = ["tax-regimes:create"] as const; export const UPDATE_TAX_REGIME_MUTATION_KEY = ["tax-regimes:update"] as const; export const DELETE_TAX_REGIME_MUTATION_KEY = ["tax-regimes:delete"] as const; +export const ENABLE_TAX_REGIME_MUTATION_KEY = ["tax-regimes:enable"] as const; +export const DISABLE_TAX_REGIME_MUTATION_KEY = ["tax-regimes:disable"] as const; /** * Operaciones de dominio */ +export const taxRegimeKeys = { + all: LIST_TAX_REGIMES_QUERY_KEY_PREFIX, + list: (criteria?: ListTaxRegimesRequestDTO) => LIST_TAX_REGIMES_QUERY_KEY(criteria), + detail: (taxRegimeId?: string) => TAX_REGIME_QUERY_KEY(taxRegimeId), +}; diff --git a/modules/catalogs/src/web/tax-regimes/shared/hooks/to-validation-errors.ts b/modules/catalogs/src/web/tax-regimes/shared/hooks/to-validation-errors.ts new file mode 100644 index 00000000..43e54858 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/hooks/to-validation-errors.ts @@ -0,0 +1,9 @@ +import type { ValidationErrorDetail } from "@repo/rdx-ddd"; +import type { ZodError } from "zod"; + +export const toValidationErrors = (error: ZodError): ValidationErrorDetail[] => + error.issues.map((issue) => ({ + path: issue.path.join("."), + message: issue.message, + value: issue.input, + })); diff --git a/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-create-mutation.ts b/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-create-mutation.ts new file mode 100644 index 00000000..d9cac9de --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-create-mutation.ts @@ -0,0 +1,44 @@ +import { useDataSource } from "@erp/core/hooks"; +import { ValidationErrorCollection } from "@repo/rdx-ddd"; +import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; + +import { CreateTaxRegimeRequestSchema } from "../../../../common"; +import { GetTaxRegimeByIdAdapter } from "../adapters"; +import { createTaxRegime, type CreateTaxRegimeParams } from "../api"; +import type { TaxRegime } from "../entities"; + +import { + CREATE_TAX_REGIME_MUTATION_KEY, + LIST_TAX_REGIMES_QUERY_KEY_PREFIX, + TAX_REGIME_QUERY_KEY, +} from "./keys"; +import { toValidationErrors } from "./to-validation-errors"; + +export const useTaxRegimeCreateMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + + return useMutation({ + mutationKey: CREATE_TAX_REGIME_MUTATION_KEY, + mutationFn: async (params) => { + if (!params.id) { + throw new Error("taxRegimeId is required"); + } + + const result = CreateTaxRegimeRequestSchema.safeParse(params.data); + + if (!result.success) { + throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error)); + } + + const dto = await createTaxRegime(dataSource, params); + return GetTaxRegimeByIdAdapter.fromDto(dto); + }, + onSuccess: async (taxRegime) => { + queryClient.setQueryData(TAX_REGIME_QUERY_KEY(taxRegime.id), taxRegime); + await queryClient.invalidateQueries({ + queryKey: LIST_TAX_REGIMES_QUERY_KEY_PREFIX, + }); + }, + }); +}; diff --git a/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-delete-mutation.ts b/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-delete-mutation.ts new file mode 100644 index 00000000..4f1b07d0 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-delete-mutation.ts @@ -0,0 +1,28 @@ +import { useDataSource } from "@erp/core/hooks"; +import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; + +import { deleteTaxRegime, type DeleteTaxRegimeParams } from "../api"; + +import { + DELETE_TAX_REGIME_MUTATION_KEY, + LIST_TAX_REGIMES_QUERY_KEY_PREFIX, + TAX_REGIMES_DETAIL_QUERY_KEY_PREFIX, +} from "./keys"; + +export const useTaxRegimeDeleteMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + + return useMutation({ + mutationKey: DELETE_TAX_REGIME_MUTATION_KEY, + mutationFn: async (params) => { + await deleteTaxRegime(dataSource, params); + }, + onSuccess: async () => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: LIST_TAX_REGIMES_QUERY_KEY_PREFIX }), + queryClient.invalidateQueries({ queryKey: TAX_REGIMES_DETAIL_QUERY_KEY_PREFIX }), + ]); + }, + }); +}; diff --git a/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-detail-query.ts b/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-detail-query.ts new file mode 100644 index 00000000..a950e3a1 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-detail-query.ts @@ -0,0 +1,29 @@ +import { useDataSource } from "@erp/core/hooks"; +import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query"; + +import { GetTaxRegimeByIdAdapter } from "../adapters"; +import { getTaxRegimeById } from "../api"; +import type { TaxRegime } from "../entities"; + +import { TAX_REGIME_QUERY_KEY } from "./keys"; + +export const useTaxRegimeDetailQuery = ( + taxRegimeId?: string, + options?: { enabled?: boolean } +): UseQueryResult => { + const dataSource = useDataSource(); + const enabled = (options?.enabled ?? true) && Boolean(taxRegimeId); + + return useQuery({ + queryKey: TAX_REGIME_QUERY_KEY(taxRegimeId), + enabled, + queryFn: async ({ signal }) => { + const dto = await getTaxRegimeById(dataSource, { + taxRegimeId: taxRegimeId as string, + signal, + }); + + return GetTaxRegimeByIdAdapter.fromDto(dto); + }, + }); +}; diff --git a/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-disable-mutation.ts b/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-disable-mutation.ts new file mode 100644 index 00000000..eb72466b --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-disable-mutation.ts @@ -0,0 +1,29 @@ +import { useDataSource } from "@erp/core/hooks"; +import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; + +import { GetTaxRegimeByIdAdapter } from "../adapters"; +import { disableTaxRegime, type DisableTaxRegimeParams } from "../api"; +import type { TaxRegime } from "../entities"; + +import { + DISABLE_TAX_REGIME_MUTATION_KEY, + LIST_TAX_REGIMES_QUERY_KEY_PREFIX, + TAX_REGIME_QUERY_KEY, +} from "./keys"; + +export const useTaxRegimeDisableMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + + return useMutation({ + mutationKey: DISABLE_TAX_REGIME_MUTATION_KEY, + mutationFn: async (params) => { + const dto = await disableTaxRegime(dataSource, params); + return GetTaxRegimeByIdAdapter.fromDto(dto); + }, + onSuccess: async (taxRegime) => { + queryClient.setQueryData(TAX_REGIME_QUERY_KEY(taxRegime.id), taxRegime); + await queryClient.invalidateQueries({ queryKey: LIST_TAX_REGIMES_QUERY_KEY_PREFIX }); + }, + }); +}; diff --git a/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-enable-mutation.ts b/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-enable-mutation.ts new file mode 100644 index 00000000..28f545f5 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-enable-mutation.ts @@ -0,0 +1,29 @@ +import { useDataSource } from "@erp/core/hooks"; +import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; + +import { GetTaxRegimeByIdAdapter } from "../adapters"; +import { enableTaxRegime, type EnableTaxRegimeParams } from "../api"; +import type { TaxRegime } from "../entities"; + +import { + ENABLE_TAX_REGIME_MUTATION_KEY, + LIST_TAX_REGIMES_QUERY_KEY_PREFIX, + TAX_REGIME_QUERY_KEY, +} from "./keys"; + +export const useTaxRegimeEnableMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + + return useMutation({ + mutationKey: ENABLE_TAX_REGIME_MUTATION_KEY, + mutationFn: async (params) => { + const dto = await enableTaxRegime(dataSource, params); + return GetTaxRegimeByIdAdapter.fromDto(dto); + }, + onSuccess: async (taxRegime) => { + queryClient.setQueryData(TAX_REGIME_QUERY_KEY(taxRegime.id), taxRegime); + await queryClient.invalidateQueries({ queryKey: LIST_TAX_REGIMES_QUERY_KEY_PREFIX }); + }, + }); +}; diff --git a/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-update-mutation.ts b/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-update-mutation.ts new file mode 100644 index 00000000..d709f258 --- /dev/null +++ b/modules/catalogs/src/web/tax-regimes/shared/hooks/use-tax-regime-update-mutation.ts @@ -0,0 +1,44 @@ +import { useDataSource } from "@erp/core/hooks"; +import { ValidationErrorCollection } from "@repo/rdx-ddd"; +import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query"; + +import { UpdateTaxRegimeByIdRequestSchema } from "../../../../common"; +import { GetTaxRegimeByIdAdapter } from "../adapters"; +import { type UpdateTaxRegimeParams, updateTaxRegime } from "../api"; +import type { TaxRegime } from "../entities"; + +import { + LIST_TAX_REGIMES_QUERY_KEY_PREFIX, + TAX_REGIME_QUERY_KEY, + UPDATE_TAX_REGIME_MUTATION_KEY, +} from "./keys"; +import { toValidationErrors } from "./to-validation-errors"; + +export const useTaxRegimeUpdateMutation = () => { + const queryClient = useQueryClient(); + const dataSource = useDataSource(); + + return useMutation({ + mutationKey: UPDATE_TAX_REGIME_MUTATION_KEY, + mutationFn: async (params) => { + if (!params.taxRegimeId) { + throw new Error("taxRegimeId is required"); + } + + const result = UpdateTaxRegimeByIdRequestSchema.safeParse(params.data); + + if (!result.success) { + throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error)); + } + + const dto = await updateTaxRegime(dataSource, params); + return GetTaxRegimeByIdAdapter.fromDto(dto); + }, + onSuccess: async (taxRegime) => { + queryClient.setQueryData(TAX_REGIME_QUERY_KEY(taxRegime.id), taxRegime); + await queryClient.invalidateQueries({ + queryKey: LIST_TAX_REGIMES_QUERY_KEY_PREFIX, + }); + }, + }); +}; diff --git a/modules/catalogs/src/web/tax-regimes/shared/index.ts b/modules/catalogs/src/web/tax-regimes/shared/index.ts index 3a4614c9..d2bc2683 100644 --- a/modules/catalogs/src/web/tax-regimes/shared/index.ts +++ b/modules/catalogs/src/web/tax-regimes/shared/index.ts @@ -1,2 +1,4 @@ export * from "./entities"; export * from "./hooks"; +export * from "./api"; +export * from "./adapters";