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.
This commit is contained in:
David Arranz 2026-08-14 10:51:36 +02:00
parent ef2ae58046
commit 7f305c930e
51 changed files with 1573 additions and 9 deletions

View File

@ -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."
}
}
}
}
}

View File

@ -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."
}
}
}
}
}

View File

@ -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: <PaymentMethodsSettingsPage />,
},
{
path: "settings/general/tax-regimes",
handle: {
layout: "app-sidebar",
protected: true,
},
element: <TaxRegimesSettingsPage />,
},
];
};

View File

@ -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);
});

View File

@ -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),

View File

@ -1 +1,2 @@
export * from "./payment-methods";
export * from "./tax-regimes";

View File

@ -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,

View File

@ -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 ? <Spinner aria-hidden className="mr-2 size-4" /> : null}
{paymentMethod.isActive
? t("settings.paymentMethods.actions.disable")
: t("settings.paymentMethods.actions.enable")}

View File

@ -160,6 +160,7 @@ export const PaymentMethodsSettingsPage = () => {
paymentMethod.isDefault,
paymentMethod.isSystem
)}
isTogglingActive={featureCtrl.isTogglingActive}
onDelete={featureCtrl.openDeleteDialog}
onEdit={featureCtrl.openEditDialog}
onToggleActive={(item) => {

View File

@ -0,0 +1,2 @@
export * from "./use-tax-regimes-settings-controller";
export * from "./use-tax-regimes-settings-page-controller";

View File

@ -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<string | null>(null);
const [deletingTaxRegime, setDeletingTaxRegime] = useState<TaxRegimeListRow | null>(null);
const editingTaxRegimeQuery = useTaxRegimeDetailQuery(editingTaxRegimeId ?? undefined, {
enabled: editingTaxRegimeId !== null,
});
const editingTaxRegime = editingTaxRegimeQuery.data ?? null;
const form = useHookForm<TaxRegimeForm>({
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<TaxRegimeForm>) => {
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<HTMLFormElement>) => {
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,
};
};

View File

@ -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,
};
};

View File

@ -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";

View File

@ -0,0 +1,5 @@
export interface TaxRegimeForm {
code: string;
description: string;
isActive: boolean;
}

View File

@ -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<typeof TaxRegimeFormSchema>;

View File

@ -0,0 +1,9 @@
import type { PageErrorStateVariant } from "@erp/core/components";
export interface TaxRegimesSettingsPageErrorViewModel {
variant: PageErrorStateVariant;
title: string;
description: string;
reference?: string;
canRetry: boolean;
}

View File

@ -0,0 +1 @@
export * from "./ui";

View File

@ -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 (
<AlertDialog onOpenChange={onOpenChange} open={open}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("settings.taxRegimes.delete.title")}</AlertDialogTitle>
<AlertDialogDescription>
{t("settings.taxRegimes.delete.description", {
code: target?.code ?? "",
})}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<Button disabled={isSubmitting} onClick={() => onOpenChange(false)} variant="outline">
{t("settings.taxRegimes.actions.cancel")}
</Button>
<Button disabled={isSubmitting || !target} onClick={onConfirm} variant="destructive">
{isSubmitting ? (
<>
<Spinner aria-hidden className="mr-2 size-4" />
{t("settings.taxRegimes.actions.deleting")}
</>
) : (
t("settings.taxRegimes.actions.delete")
)}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@ -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";

View File

@ -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<TaxRegimeForm>;
onSubmit: FormEventHandler<HTMLFormElement>;
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 (
<Dialog onOpenChange={openChange} open={open}>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<FormProvider {...form}>
<form className="space-y-6" id={formId} noValidate onSubmit={onSubmit}>
<RdxFormGrid className="grid-cols-1 md:grid-cols-2">
<TextField
autoComplete="off"
className="w-full"
description={
isEditMode
? t("settings.taxRegimes.protections.codeEdit")
: t("settings.taxRegimes.fields.codeDescription")
}
disabled={isSubmitting || isEditMode}
label={t("settings.taxRegimes.fields.code")}
name="code"
placeholder={t("settings.taxRegimes.fields.codePlaceholder")}
/>
<TextField
autoComplete="off"
className="w-full"
description={t("settings.taxRegimes.fields.descriptionDescription")}
disabled={isSubmitting}
label={t("settings.taxRegimes.fields.description")}
name="description"
placeholder={t("settings.taxRegimes.fields.descriptionPlaceholder")}
/>
<div className="md:col-span-2">
<SwitchField
className="flex items-center justify-between rounded-lg border border-border px-4 py-3"
description={t("settings.taxRegimes.fields.activeDescription")}
disabled={isSubmitting}
label={t("settings.taxRegimes.fields.active")}
name="isActive"
/>
</div>
</RdxFormGrid>
</form>
</FormProvider>
<DialogFooter>
<Button disabled={isSubmitting} onClick={() => openChange(false)} variant="outline">
{t("settings.taxRegimes.actions.cancel")}
</Button>
<Button disabled={isSubmitting} form={formId} type="submit">
{isSubmitting ? (
<>
<Spinner aria-hidden className="mr-2 size-4" />
{isEditMode
? t("settings.taxRegimes.actions.saving")
: t("settings.taxRegimes.actions.creating")}
</>
) : (
submitLabel
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@ -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 (
<div className="flex items-center gap-2">
<Button
aria-label={t("settings.taxRegimes.actions.edit")}
disabled={editDisabled}
onClick={() => onEdit(taxRegime)}
size="sm"
title={editDisabled ? t("settings.taxRegimes.protections.systemEdit") : undefined}
variant="outline"
>
{t("settings.taxRegimes.actions.edit")}
</Button>
<Button
aria-label={
taxRegime.isActive
? t("settings.taxRegimes.actions.disable")
: t("settings.taxRegimes.actions.enable")
}
aria-busy={isTogglingActive}
disabled={toggleDisabled}
onClick={() => onToggleActive(taxRegime)}
size="sm"
title={toggleDisabled ? activeDisabledReason : undefined}
variant="outline"
>
{isTogglingActive ? <Spinner aria-hidden className="mr-2 size-4" /> : null}
{taxRegime.isActive
? t("settings.taxRegimes.actions.disable")
: t("settings.taxRegimes.actions.enable")}
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button aria-label={t("settings.taxRegimes.actions.more")} size="icon" variant="ghost">
<MoreHorizontalIcon className="size-4" />
</Button>
}
/>
<DropdownMenuContent
align="end"
className="w-48"
render={
<DropdownMenuItem
disabled={deleteDisabled}
onClick={() => onDelete(taxRegime)}
title={deleteDisabled ? deleteDisabledReason : undefined}
variant="destructive"
>
{t("settings.taxRegimes.actions.delete")}
</DropdownMenuItem>
}
/>
</DropdownMenu>
</div>
);
};

View File

@ -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 (
<div className="rounded-xl border border-dashed border-border bg-muted/20 p-8 text-center">
<div className="space-y-2">
<h3 className="text-lg font-semibold">{t("settings.taxRegimes.empty.title")}</h3>
<p className="text-sm text-muted-foreground">
{t("settings.taxRegimes.empty.description")}
</p>
</div>
<Button className="mt-6" onClick={onCreate}>
{t("settings.taxRegimes.actions.create")}
</Button>
</div>
);
};

View File

@ -0,0 +1,15 @@
import { Skeleton } from "@repo/shadcn-ui/components";
export const TaxRegimesSettingsSkeleton = () => {
return (
<div className="space-y-4">
<Skeleton className="h-8 w-56" />
<Skeleton className="h-4 w-80" />
<div className="space-y-3">
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
</div>
</div>
);
};

View File

@ -0,0 +1,2 @@
export * from "./pages";
export * from "./components";

View File

@ -0,0 +1 @@
export * from "./tax-regimes-settings-page";

View File

@ -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 <TaxRegimesSettingsSkeleton />;
}
if (ctrl.pageError) {
return (
<RdxPageContainer width="full">
<div className="mx-auto w-full max-w-3xl">
<PageErrorState
description={ctrl.pageError.description}
primaryAction={
ctrl.pageError.canRetry
? {
label: ctrl.isRetrying
? t("settings.taxRegimes.pageError.actions.retrying")
: t("settings.taxRegimes.pageError.actions.retry"),
onClick: ctrl.retry,
isLoading: ctrl.isRetrying,
}
: undefined
}
reference={ctrl.pageError.reference}
title={ctrl.pageError.title}
variant={ctrl.pageError.variant}
/>
</div>
</RdxPageContainer>
);
}
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 (
<RdxPageContainer width="full">
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6">
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div className="space-y-2">
<h2 className="text-2xl font-semibold tracking-tight">
{t("settings.taxRegimes.title")}
</h2>
<p className="max-w-3xl text-sm text-muted-foreground">
{t("settings.taxRegimes.description")}
</p>
</div>
<Button onClick={featureCtrl.openCreateDialog}>
<PlusIcon aria-hidden className="mr-2 size-4" />
{t("settings.taxRegimes.actions.create")}
</Button>
</div>
{ctrl.list.length === 0 ? (
<TaxRegimesEmptyState onCreate={featureCtrl.openCreateDialog} />
) : (
<div className="space-y-3">
{ctrl.list.map((taxRegime) => (
<Card key={taxRegime.id}>
<CardContent className="flex flex-col gap-4 p-4 md:flex-row md:items-center md:justify-between">
<div className="min-w-0 space-y-2">
<div className="flex flex-wrap items-center gap-2">
<span className="rounded-md border border-border bg-muted px-2 py-1 font-mono text-xs font-medium text-muted-foreground">
{taxRegime.code}
</span>
{taxRegime.isSystem ? (
<Badge variant="secondary">{t("settings.taxRegimes.status.system")}</Badge>
) : null}
<Badge variant={taxRegime.isActive ? "outline" : "secondary"}>
{taxRegime.isActive
? t("settings.taxRegimes.status.active")
: t("settings.taxRegimes.status.inactive")}
</Badge>
</div>
<div className="space-y-1">
<p className="text-sm font-medium text-foreground">{taxRegime.description}</p>
</div>
</div>
<TaxRegimeRowActions
activeDisabledReason={activeDisabledReason(taxRegime.isSystem)}
deleteDisabledReason={deleteDisabledReason(taxRegime.isSystem)}
isTogglingActive={featureCtrl.isTogglingActive}
onDelete={featureCtrl.openDeleteDialog}
onEdit={featureCtrl.openEditDialog}
onToggleActive={(item) => {
if (item.isActive) {
featureCtrl.disable(item);
return;
}
featureCtrl.enable(item);
}}
taxRegime={taxRegime}
/>
</CardContent>
</Card>
))}
</div>
)}
<TaxRegimeFormDialog
description={t("settings.taxRegimes.create.description")}
form={featureCtrl.form}
formId={featureCtrl.formId}
isEditMode={false}
isSubmitting={featureCtrl.isSaving}
onSubmit={featureCtrl.onSubmit}
open={featureCtrl.isCreateOpen}
openChange={(open) => {
if (!open) {
featureCtrl.closeCreateDialog();
}
}}
submitLabel={t("settings.taxRegimes.actions.create")}
title={t("settings.taxRegimes.create.title")}
/>
<TaxRegimeFormDialog
description={t("settings.taxRegimes.edit.description")}
form={featureCtrl.form}
formId={`${featureCtrl.formId}-edit`}
isEditMode
isSubmitting={featureCtrl.isSaving || featureCtrl.isEditingTaxRegimeLoading}
onSubmit={featureCtrl.onSubmit}
open={Boolean(featureCtrl.editingTaxRegime) || featureCtrl.isEditingTaxRegimeLoading}
openChange={(open) => {
if (!open) {
featureCtrl.closeEditDialog();
}
}}
submitLabel={t("settings.taxRegimes.actions.save")}
title={t("settings.taxRegimes.edit.title")}
/>
<DeleteTaxRegimeDialog
isSubmitting={featureCtrl.isDeleting}
onConfirm={featureCtrl.confirmDelete}
onOpenChange={(open) => {
if (!open) {
featureCtrl.closeDeleteDialog();
}
}}
open={Boolean(featureCtrl.deletingTaxRegime)}
target={featureCtrl.deletingTaxRegime}
/>
</div>
</RdxPageContainer>
);
};

View File

@ -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,
});

View File

@ -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,
},
});

View File

@ -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,
};
};

View File

@ -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";

View File

@ -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,
};
};

View File

@ -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,
};
},
};

View File

@ -1 +1,2 @@
export * from "./list-tax-regimes.adapter";
export * from "./get-tax-regime-by-id.adapter";

View File

@ -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,

View File

@ -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<CreateTaxRegimeResponseDTO> {
return dataSource.createOne<CreateTaxRegimeRequestDTO, CreateTaxRegimeResponseDTO>(
"catalogs/tax-regimes",
params.data
);
}

View File

@ -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);
}

View File

@ -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<DisableTaxRegimeByIdResponseDTO> {
return dataSource.custom<undefined, DisableTaxRegimeByIdResponseDTO>({
method: "patch",
path: `catalogs/tax-regimes/${params.taxRegimeId}/disable`,
data: undefined,
});
}

View File

@ -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<EnableTaxRegimeByIdResponseDTO> {
return dataSource.custom<undefined, EnableTaxRegimeByIdResponseDTO>({
method: "patch",
path: `catalogs/tax-regimes/${params.taxRegimeId}/enable`,
data: undefined,
});
}

View File

@ -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<GetTaxRegimeByIdResponseDTO> {
return dataSource.getOne<GetTaxRegimeByIdResponseDTO>("catalogs/tax-regimes", params.taxRegimeId, {
signal: params.signal,
});
}

View File

@ -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";

View File

@ -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<UpdateTaxRegimeByIdResponseDTO> {
return dataSource.updateOne<UpdateTaxRegimeByIdRequestDTO, UpdateTaxRegimeByIdResponseDTO>(
"catalogs/tax-regimes",
params.taxRegimeId,
params.data
);
}

View File

@ -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";

View File

@ -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),
};

View File

@ -0,0 +1,9 @@
import type { ValidationErrorDetail } from "@repo/rdx-ddd";
import type { ZodError } from "zod";
export const toValidationErrors = (error: ZodError<unknown>): ValidationErrorDetail[] =>
error.issues.map((issue) => ({
path: issue.path.join("."),
message: issue.message,
value: issue.input,
}));

View File

@ -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<TaxRegime, DefaultError, CreateTaxRegimeParams>({
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,
});
},
});
};

View File

@ -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<void, DefaultError, DeleteTaxRegimeParams>({
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 }),
]);
},
});
};

View File

@ -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<TaxRegime, DefaultError> => {
const dataSource = useDataSource();
const enabled = (options?.enabled ?? true) && Boolean(taxRegimeId);
return useQuery<TaxRegime, DefaultError>({
queryKey: TAX_REGIME_QUERY_KEY(taxRegimeId),
enabled,
queryFn: async ({ signal }) => {
const dto = await getTaxRegimeById(dataSource, {
taxRegimeId: taxRegimeId as string,
signal,
});
return GetTaxRegimeByIdAdapter.fromDto(dto);
},
});
};

View File

@ -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<TaxRegime, DefaultError, DisableTaxRegimeParams>({
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 });
},
});
};

View File

@ -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<TaxRegime, DefaultError, EnableTaxRegimeParams>({
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 });
},
});
};

View File

@ -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<TaxRegime, DefaultError, UpdateTaxRegimeParams>({
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,
});
},
});
};

View File

@ -1,2 +1,4 @@
export * from "./entities";
export * from "./hooks";
export * from "./api";
export * from "./adapters";