Clientes
This commit is contained in:
parent
64b48d707b
commit
24f7bd0fb9
15
modules/core/src/web/components/error-alert.tsx
Normal file
15
modules/core/src/web/components/error-alert.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
interface ErrorAlertProps {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorAlert = ({ title, message }: ErrorAlertProps) => (
|
||||||
|
<div
|
||||||
|
aria-live="assertive"
|
||||||
|
className="mb-4 rounded-lg border border-destructive/50 bg-destructive/10 p-4"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<p className="font-semibold text-destructive-foreground">{title}</p>
|
||||||
|
<p className="text-sm text-destructive-foreground/90">{message}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@ -1,2 +1,3 @@
|
|||||||
|
export * from "./error-alert";
|
||||||
export * from "./form";
|
export * from "./form";
|
||||||
export * from "./page-header";
|
export * from "./page-header";
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
export * from "./customer-edit-form";
|
//export * from "./customer-edit-form";
|
||||||
export * from "./customer-editor-skeleton";
|
export * from "../../view/ui/components/customer-editor-skeleton";
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
// components/ErrorAlert.tsx
|
|
||||||
interface ErrorAlertProps {
|
|
||||||
title: string;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ErrorAlert = ({ title, message }: ErrorAlertProps) => (
|
|
||||||
<div
|
|
||||||
className='mb-4 rounded-lg border border-destructive/50 bg-destructive/10 p-4'
|
|
||||||
role='alert'
|
|
||||||
aria-live='assertive'
|
|
||||||
>
|
|
||||||
<p className='font-semibold text-destructive-foreground'>{title}</p>
|
|
||||||
<p className='text-sm text-destructive-foreground/90'>{message}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
export * from "./client-selector-modal";
|
//export * from "./client-selector-modal";
|
||||||
export * from "./customer-modal-selector";
|
//export * from "./customer-modal-selector";
|
||||||
export * from "./editor";
|
//export * from "./editor";
|
||||||
export * from "./error-alert";
|
export * from "../../../../core/src/web/components/error-alert";
|
||||||
export * from "./not-found-card";
|
//export * from "./not-found-card";
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { Outlet, type RouteObject } from "react-router-dom";
|
|||||||
// Lazy load components
|
// Lazy load components
|
||||||
const CustomerLayout = lazy(() => import("./ui").then((m) => ({ default: m.CustomerLayout })));
|
const CustomerLayout = lazy(() => import("./ui").then((m) => ({ default: m.CustomerLayout })));
|
||||||
const CustomersList = lazy(() => import("./list").then((m) => ({ default: m.CustomerListPage })));
|
const CustomersList = lazy(() => import("./list").then((m) => ({ default: m.CustomerListPage })));
|
||||||
|
const CustomerView = lazy(() => import("./view").then((m) => ({ default: m.CustomerViewPage })));
|
||||||
|
|
||||||
//const CustomerView = lazy(() => import("./pages").then((m) => ({ default: m.CustomerViewPage })));
|
|
||||||
//const CustomerAdd = lazy(() => import("./pages").then((m) => ({ default: m.CustomerCreatePage })));
|
//const CustomerAdd = lazy(() => import("./pages").then((m) => ({ default: m.CustomerCreatePage })));
|
||||||
/*const CustomerUpdate = lazy(() =>
|
/*const CustomerUpdate = lazy(() =>
|
||||||
import("./pages").then((m) => ({ default: m.CustomerUpdatePage }))
|
import("./pages").then((m) => ({ default: m.CustomerUpdatePage }))
|
||||||
@ -25,7 +25,7 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
|||||||
{ path: "", index: true, element: <CustomersList /> }, // index
|
{ path: "", index: true, element: <CustomersList /> }, // index
|
||||||
{ path: "list", element: <CustomersList /> },
|
{ path: "list", element: <CustomersList /> },
|
||||||
//{ path: "create", element: <CustomerAdd /> },
|
//{ path: "create", element: <CustomerAdd /> },
|
||||||
//{ path: ":id", element: <CustomerView /> },
|
{ path: ":id", element: <CustomerView /> },
|
||||||
//{ path: ":id/edit", element: <CustomerUpdate /> },
|
//{ path: ":id/edit", element: <CustomerUpdate /> },
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export * from "./use-create-customer-mutation";
|
//export * from "./use-create-customer-mutation";
|
||||||
export * from "./use-customer-query";
|
//export * from "./use-customer-query";
|
||||||
export * from "./use-customers-context";
|
//export * from "./use-customers-context";
|
||||||
export * from "./use-update-customer-mutation";
|
//export * from "./use-update-customer-mutation";
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
|
import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||||
import { DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { ZodError } from "zod/v4";
|
import type { ZodError } from "zod/v4";
|
||||||
|
|
||||||
import { CreateCustomerRequestSchema } from "../../common";
|
import { CreateCustomerRequestSchema } from "../../common";
|
||||||
import { Customer, CustomerFormData } from "../schemas";
|
import type { Customer, CustomerFormData } from "../schemas";
|
||||||
|
|
||||||
import { CUSTOMERS_LIST_KEY, invalidateCustomerListCache } from "./use-customer-list-query";
|
import { CUSTOMERS_LIST_KEY, invalidateCustomerListCache } from "./use-customer-list-query";
|
||||||
import { setCustomerDetailCache } from "./use-customer-query";
|
import { setCustomerDetailCache } from "./use-customer-query";
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||||
import { DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { UpdateCustomerByIdRequestSchema } from "../../common";
|
import { UpdateCustomerByIdRequestSchema } from "../../common";
|
||||||
import { Customer, CustomerFormData } from "../schemas";
|
import type { Customer, CustomerFormData } from "../schemas";
|
||||||
|
|
||||||
import { toValidationErrors } from "./use-create-customer-mutation";
|
import { toValidationErrors } from "./use-create-customer-mutation";
|
||||||
import {
|
import {
|
||||||
invalidateCustomerListCache,
|
invalidateCustomerListCache,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { CustomerSummaryPage, CustomerSummaryPageData } from "../../types";
|
import type { CustomerSummaryPage } from "../api";
|
||||||
|
import type { CustomerSummaryPageData } from "../types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
||||||
|
|||||||
6
modules/customers/src/web/list/api/api-types.ts
Normal file
6
modules/customers/src/web/list/api/api-types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import type { ListCustomersResponseDTO } from "@erp/customers/common";
|
||||||
|
import type { ArrayElement } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
// Resultado de consulta con criteria (paginado, etc.)
|
||||||
|
export type CustomerSummaryPage = Omit<ListCustomersResponseDTO, "metadata">;
|
||||||
|
export type CustomerSummary = Omit<ArrayElement<CustomerSummaryPage>, "metadata">;
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import type { CriteriaDTO } from "@erp/core";
|
import type { CriteriaDTO } from "@erp/core";
|
||||||
import type { IDataSource } from "@erp/core/client";
|
import type { IDataSource } from "@erp/core/client";
|
||||||
|
|
||||||
import type { CustomerSummaryPage } from "../../types";
|
import type { CustomerSummaryPage } from "./api-types";
|
||||||
|
|
||||||
export async function getCustomerListApi(
|
export async function getCustomerListApi(
|
||||||
dataSource: IDataSource,
|
dataSource: IDataSource,
|
||||||
@ -13,6 +13,5 @@ export async function getCustomerListApi(
|
|||||||
...criteria,
|
...criteria,
|
||||||
});
|
});
|
||||||
|
|
||||||
//return mapIssuedInvoiceList(raw);
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
|
export * from "./api-types";
|
||||||
export * from "./get-customer-list.api";
|
export * from "./get-customer-list.api";
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import type { CriteriaDTO } from "@erp/core";
|
import type { CriteriaDTO } from "@erp/core";
|
||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria";
|
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria";
|
||||||
import { type DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
import {
|
||||||
|
type DefaultError,
|
||||||
|
type QueryKey,
|
||||||
|
type UseQueryResult,
|
||||||
|
useQuery,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
|
||||||
import type { CustomerSummaryPage } from "../../types";
|
import { type CustomerSummaryPage, getCustomerListApi } from "../api";
|
||||||
import { getCustomerListApi } from "../api";
|
|
||||||
|
|
||||||
export const CUSTOMERS_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [
|
export const CUSTOMERS_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [
|
||||||
"customers",
|
"customers",
|
||||||
@ -23,7 +27,9 @@ type CustomersQueryOptions = {
|
|||||||
criteria?: CriteriaDTO;
|
criteria?: CriteriaDTO;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCustomerListQuery = (options?: CustomersQueryOptions) => {
|
export const useCustomerListQuery = (
|
||||||
|
options?: CustomersQueryOptions
|
||||||
|
): UseQueryResult<CustomerSummaryPage, DefaultError> => {
|
||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const enabled = options?.enabled ?? true;
|
const enabled = options?.enabled ?? true;
|
||||||
const criteria = options?.criteria ?? {};
|
const criteria = options?.criteria ?? {};
|
||||||
@ -37,7 +43,6 @@ export const useCustomerListQuery = (options?: CustomersQueryOptions) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
export function cancelCustomerListQueries(qc: QueryClient) {
|
export function cancelCustomerListQueries(qc: QueryClient) {
|
||||||
return qc.cancelQueries({ queryKey: CUSTOMERS_LIST_KEY });
|
return qc.cancelQueries({ queryKey: CUSTOMERS_LIST_KEY });
|
||||||
}
|
}
|
||||||
|
|||||||
1
modules/customers/src/web/list/types/index.ts
Normal file
1
modules/customers/src/web/list/types/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./types";
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import type { CustomerSummary } from "../schemas";
|
import type { CustomerSummary } from "../../schemas";
|
||||||
|
import type { CustomerSummaryPage } from "../api";
|
||||||
import type { CustomerSummaryPage } from "./customer.api.schema";
|
|
||||||
|
|
||||||
export type CustomerSummaryData = CustomerSummary;
|
export type CustomerSummaryData = CustomerSummary;
|
||||||
|
|
||||||
@ -3,7 +3,7 @@ import type { ColumnDef } from "@tanstack/react-table";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type { CustomerSummaryData, CustomerSummaryPageData } from "../../../../types";
|
import type { CustomerSummaryData, CustomerSummaryPageData } from "../../../types";
|
||||||
|
|
||||||
interface CustomersGridProps {
|
interface CustomersGridProps {
|
||||||
data: CustomerSummaryPageData;
|
data: CustomerSummaryPageData;
|
||||||
@ -58,4 +58,4 @@ export const CustomersGrid = ({
|
|||||||
totalItems={total_items}
|
totalItems={total_items}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@ -2,24 +2,21 @@ import { DataTableColumnHeader } from "@repo/rdx-ui/components";
|
|||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
AvatarFallback,
|
AvatarFallback,
|
||||||
Button, DropdownMenu,
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import {
|
import { EyeIcon, MoreHorizontalIcon } from "lucide-react";
|
||||||
EyeIcon, MoreHorizontalIcon
|
|
||||||
} from "lucide-react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import {
|
|
||||||
type CustomerSummaryData,
|
|
||||||
} from "../../../../types";
|
|
||||||
import { CustomerStatusBadge } from "../../../../ui";
|
import { CustomerStatusBadge } from "../../../../ui";
|
||||||
|
import type { CustomerSummaryData } from "../../../types";
|
||||||
import { AddressCell, ContactCell, Initials } from "../../components";
|
import { AddressCell, ContactCell, Initials } from "../../components";
|
||||||
import { KindBadge } from "../../components/kind-badge";
|
import { KindBadge } from "../../components/kind-badge";
|
||||||
import { Soft } from "../../components/soft";
|
import { Soft } from "../../components/soft";
|
||||||
@ -86,12 +83,14 @@ export function useCustomersGridColumns(
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-start gap-1 my-1.5">
|
<div className="flex items-start gap-1 my-1.5">
|
||||||
<Avatar className="size-10 hidden">
|
<Avatar className="size-10 hidden">
|
||||||
<AvatarFallback aria-label={customer.name}><Initials name={customer.name} /></AvatarFallback>
|
<AvatarFallback aria-label={customer.name}>
|
||||||
|
<Initials name={customer.name} />
|
||||||
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="min-w-0 grid gap-1">
|
<div className="min-w-0 grid gap-1">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<CustomerStatusBadge status={customer.status} />{" "}
|
<CustomerStatusBadge status={customer.status} />{" "}
|
||||||
<span className="font-medium truncate text-primary">{customer.name}</span>
|
<span className="font-bold truncate text-primary">{customer.name}</span>
|
||||||
{customer.trade_name && <Soft>({customer.trade_name})</Soft>}
|
{customer.trade_name && <Soft>({customer.trade_name})</Soft>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
@ -105,22 +104,22 @@ export function useCustomersGridColumns(
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Contacto (emails, teléfonos, web)
|
// Contacto (emails, teléfonos, web)
|
||||||
{
|
{
|
||||||
id: "contact",
|
id: "contact",
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<DataTableColumnHeader
|
<DataTableColumnHeader
|
||||||
className="text-left"
|
className="text-left"
|
||||||
column={column}
|
column={column}
|
||||||
title={t("pages.list.grid_columns.contact")}
|
title={t("pages.list.grid_columns.contact")}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
accessorFn: (r) => `${r.email_primary} ${r.phone_primary} ${r.mobile_primary} ${r.website}`,
|
accessorFn: (r) => `${r.email_primary} ${r.phone_primary} ${r.mobile_primary} ${r.website}`,
|
||||||
size: 140,
|
size: 140,
|
||||||
minSize: 120,
|
minSize: 120,
|
||||||
cell: ({ row }) => <ContactCell customer={row.original} />,
|
cell: ({ row }) => <ContactCell customer={row.original} />,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Dirección (múltiples campos en bloque)
|
// Dirección (múltiples campos en bloque)
|
||||||
{
|
{
|
||||||
id: "address",
|
id: "address",
|
||||||
header: t("pages.list.grid_columns.address"),
|
header: t("pages.list.grid_columns.address"),
|
||||||
@ -159,37 +158,37 @@ export function useCustomersGridColumns(
|
|||||||
<EyeIcon className="size-4" />
|
<EyeIcon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{0 === false && (
|
<DropdownMenu>
|
||||||
<DropdownMenu>
|
<DropdownMenuTrigger asChild>
|
||||||
<DropdownMenuTrigger asChild>
|
<Button aria-label="More actions" size="icon" variant="ghost">
|
||||||
<Button aria-label="More actions" size="icon" variant="ghost">
|
<MoreHorizontalIcon className="size-4" />
|
||||||
<MoreHorizontalIcon className="size-4" />
|
</Button>
|
||||||
</Button>
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuItem onClick={() => actionHandlers.onViewClick?.(customer)}>
|
||||||
<DropdownMenuItem onClick={() => actionHandlers.onViewClick?.(customer)}>Open</DropdownMenuItem>
|
Open
|
||||||
<DropdownMenuItem onClick={() => actionHandlers.onEditClick?.(customer)}>Edit</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuItem onClick={() => actionHandlers.onEditClick?.(customer)}>
|
||||||
<DropdownMenuItem onClick={() => window.open(safeHttp(website), "_blank")}>
|
Edit
|
||||||
Visit website
|
</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem onClick={() => window.open(safeHttp(website), "_blank")}>
|
||||||
onClick={() => navigator.clipboard.writeText(email_primary)}
|
Visit website
|
||||||
>
|
</DropdownMenuItem>
|
||||||
Copy email
|
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(email_primary)}>
|
||||||
</DropdownMenuItem>
|
Copy email
|
||||||
<DropdownMenuSeparator />
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuSeparator />
|
||||||
className="text-destructive"
|
<DropdownMenuItem
|
||||||
onClick={() => actionHandlers.onDeleteClick?.(customer)}
|
className="text-destructive"
|
||||||
>
|
onClick={() => actionHandlers.onDeleteClick?.(customer)}
|
||||||
Delete
|
>
|
||||||
</DropdownMenuItem>
|
Delete
|
||||||
</DropdownMenuContent>
|
</DropdownMenuItem>
|
||||||
</DropdownMenu>
|
</DropdownMenuContent>
|
||||||
)}
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
import {
|
import type { CustomerSummaryData } from "../../types";
|
||||||
type CustomerSummaryData,
|
|
||||||
} from "../../../types";
|
|
||||||
|
|
||||||
import { Soft } from './soft';
|
import { Soft } from "./soft";
|
||||||
|
|
||||||
|
export const AddressCell = ({ customer }: { customer: CustomerSummaryData }) => {
|
||||||
export const AddressCell = ({ customer }: { customer: CustomerSummaryData; }) => {
|
|
||||||
const line1 = [customer.street, customer.street2].filter(Boolean).join(", ");
|
const line1 = [customer.street, customer.street2].filter(Boolean).join(", ");
|
||||||
const line2 = [customer.postal_code, customer.city].filter(Boolean).join(" ");
|
const line2 = [customer.postal_code, customer.city].filter(Boolean).join(" ");
|
||||||
const line3 = [customer.province, customer.country].filter(Boolean).join(", ");
|
const line3 = [customer.province, customer.country].filter(Boolean).join(", ");
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import { MailIcon, PhoneIcon } from 'lucide-react';
|
import { MailIcon, PhoneIcon } from "lucide-react";
|
||||||
import { Soft } from './soft';
|
|
||||||
import {
|
|
||||||
type CustomerSummaryData,
|
|
||||||
} from "../../../types";
|
|
||||||
|
|
||||||
|
import type { CustomerSummaryData } from "../../types";
|
||||||
|
|
||||||
|
import { Soft } from "./soft";
|
||||||
|
|
||||||
export const ContactCell = ({ customer }: { customer: CustomerSummaryData }) => (
|
export const ContactCell = ({ customer }: { customer: CustomerSummaryData }) => (
|
||||||
<div className="grid gap-1 text-foreground text-sm my-1.5">
|
<div className="grid gap-1 text-foreground text-sm my-1.5">
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
|
import { PageHeader, SimpleSearchInput } from "@erp/core/components";
|
||||||
|
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
|
import { PlusIcon } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { useCustomerListPageController } from '../../controllers';
|
import { ErrorAlert } from "../../../ui";
|
||||||
import { useCustomersGridColumns } from '../blocks';
|
import { useCustomerListPageController } from "../../controllers";
|
||||||
import { AppContent, BackHistoryButton } from '@repo/rdx-ui/components';
|
import { CustomersGrid, useCustomersGridColumns } from "../blocks";
|
||||||
import { ErrorAlert } from '../../../ui';
|
|
||||||
|
|
||||||
export const CustomerListPage = () => {
|
export const CustomerListPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -12,9 +15,11 @@ export const CustomerListPage = () => {
|
|||||||
|
|
||||||
const { listCtrl } = useCustomerListPageController();
|
const { listCtrl } = useCustomerListPageController();
|
||||||
|
|
||||||
const columns = useCustomersGridColumns({});
|
const columns = useCustomersGridColumns({
|
||||||
|
onEditClick: (customer) => navigate(`/customers/${customer.id}/edit`),
|
||||||
|
onViewClick: (customer) => navigate(`/customers/${customer.id}`),
|
||||||
|
onDeleteClick: (customer) => null, //confirmDelete(inv.id),
|
||||||
|
});
|
||||||
|
|
||||||
if (listCtrl.isError || !listCtrl.data) {
|
if (listCtrl.isError || !listCtrl.data) {
|
||||||
return (
|
return (
|
||||||
@ -26,10 +31,8 @@ export const CustomerListPage = () => {
|
|||||||
<BackHistoryButton />
|
<BackHistoryButton />
|
||||||
</AppContent>
|
</AppContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <h1>gika</h1>;
|
|
||||||
/*
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppHeader>
|
<AppHeader>
|
||||||
@ -68,6 +71,5 @@ export const CustomerListPage = () => {
|
|||||||
/>
|
/>
|
||||||
</AppContent>
|
</AppContent>
|
||||||
</>
|
</>
|
||||||
);*/
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { z } from "zod/v4";
|
import type { PaginationSchema } from "@erp/core";
|
||||||
|
import type { ArrayElement } from "@repo/rdx-utils";
|
||||||
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import { PaginationSchema } from "@erp/core";
|
|
||||||
import { ArrayElement } from "@repo/rdx-utils";
|
|
||||||
import {
|
import {
|
||||||
CreateCustomerRequestSchema,
|
CreateCustomerRequestSchema,
|
||||||
GetCustomerByIdResponseSchema,
|
GetCustomerByIdResponseSchema,
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
import {
|
|
||||||
GetCustomerByIdResponseSchema,
|
|
||||||
type ListCustomersResponseDTO,
|
|
||||||
} from "@erp/customers/common";
|
|
||||||
import type { ArrayElement } from "@repo/rdx-utils";
|
|
||||||
import type { z } from "zod/v4";
|
|
||||||
|
|
||||||
// IssuedInvoices
|
|
||||||
export const CustomerSchema = GetCustomerByIdResponseSchema.omit({
|
|
||||||
metadata: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Customer = z.infer<typeof CustomerSchema>;
|
|
||||||
|
|
||||||
// Resultado de consulta con criteria (paginado, etc.)
|
|
||||||
export type CustomerSummaryPage = Omit<ListCustomersResponseDTO, "metadata">;
|
|
||||||
export type CustomerSummary = Omit<ArrayElement<CustomerSummaryPage>, "metadata">;
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./customer.api.schema";
|
|
||||||
export * from "./customer-summary.web.schema";
|
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import type { Customer } from "../api";
|
||||||
|
import type { CustomerData } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convierte el DTO completo de API a datos numéricos para el formulario.
|
||||||
|
*/
|
||||||
|
export const CustomerDtoAdapter = {
|
||||||
|
fromDto(customerDto: Customer, context?: unknown): CustomerData {
|
||||||
|
return {
|
||||||
|
...customerDto,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
1
modules/customers/src/web/view/adapters/index.ts
Normal file
1
modules/customers/src/web/view/adapters/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-dto.adapter";
|
||||||
3
modules/customers/src/web/view/api/api-types.ts
Normal file
3
modules/customers/src/web/view/api/api-types.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import type { GetCustomerByIdResponseDTO } from "@erp/customers/common";
|
||||||
|
|
||||||
|
export type Customer = Omit<GetCustomerByIdResponseDTO, "metadata">;
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import type { IDataSource } from "@erp/core/client";
|
||||||
|
|
||||||
|
import type { Customer } from "./api-types";
|
||||||
|
|
||||||
|
export async function getCustomerById(dataSource: IDataSource, signal: AbortSignal, id?: string) {
|
||||||
|
if (!id) throw new Error("customerId is required");
|
||||||
|
const response = dataSource.getOne<Customer>("customers", id, { signal });
|
||||||
|
return response;
|
||||||
|
}
|
||||||
2
modules/customers/src/web/view/api/index.ts
Normal file
2
modules/customers/src/web/view/api/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./api-types";
|
||||||
|
export * from "./get-customer-by-ip.api";
|
||||||
2
modules/customers/src/web/view/controllers/index.ts
Normal file
2
modules/customers/src/web/view/controllers/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./use-customer-view.controller";
|
||||||
|
export * from "./use-customer-view-page.controller";
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { useCustomerViewController } from "./use-customer-view.controller";
|
||||||
|
|
||||||
|
export function useCustomerViewPageController() {
|
||||||
|
const viewCtrl = useCustomerViewController();
|
||||||
|
|
||||||
|
return {
|
||||||
|
viewCtrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { CustomerDtoAdapter } from "../adapters";
|
||||||
|
import { useCustomerGetQuery } from "../hooks";
|
||||||
|
|
||||||
|
export const useCustomerViewController = () => {
|
||||||
|
const [customerId, setCustomerId] = useState("");
|
||||||
|
|
||||||
|
const query = useCustomerGetQuery(customerId);
|
||||||
|
const data = useMemo(
|
||||||
|
() => (query.data ? CustomerDtoAdapter.fromDto(query.data) : undefined),
|
||||||
|
[query.data]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
data,
|
||||||
|
customerId,
|
||||||
|
setCustomerId,
|
||||||
|
};
|
||||||
|
};
|
||||||
1
modules/customers/src/web/view/hooks/index.ts
Normal file
1
modules/customers/src/web/view/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./use-customer-query";
|
||||||
47
modules/customers/src/web/view/hooks/use-customer-query.ts
Normal file
47
modules/customers/src/web/view/hooks/use-customer-query.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
|
import {
|
||||||
|
type DefaultError,
|
||||||
|
type QueryKey,
|
||||||
|
type UseQueryResult,
|
||||||
|
useQuery,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { type Customer, getCustomerById } from "../api";
|
||||||
|
|
||||||
|
export const CUSTOMER_QUERY_KEY = (customerId?: string): QueryKey => [
|
||||||
|
"customers:detail",
|
||||||
|
{
|
||||||
|
customerId,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type CustomerQueryOptions = {
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCustomerGetQuery = (
|
||||||
|
customerId?: string,
|
||||||
|
options?: CustomerQueryOptions
|
||||||
|
): UseQueryResult<Customer, DefaultError> => {
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
const enabled = options?.enabled ?? Boolean(customerId);
|
||||||
|
|
||||||
|
return useQuery<Customer, DefaultError>({
|
||||||
|
queryKey: CUSTOMER_QUERY_KEY(customerId),
|
||||||
|
queryFn: async ({ signal }) => getCustomerById(dataSource, signal, customerId),
|
||||||
|
enabled,
|
||||||
|
placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*export function invalidateCustomerDetailCache(qc: QueryClient, id: string) {
|
||||||
|
return qc.invalidateQueries({
|
||||||
|
queryKey: getCustomerQueryKey(id ?? "unknown"),
|
||||||
|
exact: Boolean(id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCustomerDetailCache(qc: QueryClient, id: string, data: unknown) {
|
||||||
|
qc.setQueryData(getCustomerQueryKey(id), data);
|
||||||
|
}
|
||||||
|
*/
|
||||||
1
modules/customers/src/web/view/index.ts
Normal file
1
modules/customers/src/web/view/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./ui";
|
||||||
1
modules/customers/src/web/view/types/index.ts
Normal file
1
modules/customers/src/web/view/types/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./types";
|
||||||
3
modules/customers/src/web/view/types/types.ts
Normal file
3
modules/customers/src/web/view/types/types.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import type { Customer } from "../api";
|
||||||
|
|
||||||
|
export type CustomerData = Customer;
|
||||||
@ -2,7 +2,7 @@
|
|||||||
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
import { Button } from "@repo/shadcn-ui/components";
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
|
|
||||||
export const CustomerEditorSkeleton = () => {
|
export const CustomerEditorSkeleton = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
1
modules/customers/src/web/view/ui/components/index.ts
Normal file
1
modules/customers/src/web/view/ui/components/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-editor-skeleton";
|
||||||
1
modules/customers/src/web/view/ui/index.ts
Normal file
1
modules/customers/src/web/view/ui/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./pages";
|
||||||
333
modules/customers/src/web/view/ui/pages/customer-view-page copy
Normal file
333
modules/customers/src/web/view/ui/pages/customer-view-page copy
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
import { PageHeader } from "@erp/core/components";
|
||||||
|
import { useUrlParamId } from "@erp/core/hooks";
|
||||||
|
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import {
|
||||||
|
Banknote,
|
||||||
|
EditIcon,
|
||||||
|
FileText,
|
||||||
|
Globe,
|
||||||
|
Languages,
|
||||||
|
Mail,
|
||||||
|
MapPin,
|
||||||
|
MoreVertical,
|
||||||
|
Phone,
|
||||||
|
Smartphone,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { CustomerEditorSkeleton, ErrorAlert } from "../../../components";
|
||||||
|
import { useTranslation } from "../../../i18n";
|
||||||
|
import { useCustomerGetQuery } from "../../hooks";
|
||||||
|
|
||||||
|
export const CustomerViewPage2 = () => {
|
||||||
|
const customerId = useUrlParamId();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// 1) Estado de carga del cliente (query)
|
||||||
|
const {
|
||||||
|
data: customer,
|
||||||
|
isLoading: isLoadingCustomer,
|
||||||
|
isError: isLoadError,
|
||||||
|
error: loadError,
|
||||||
|
} = useCustomerGetQuery(customerId, { enabled: !!customerId });
|
||||||
|
|
||||||
|
if (isLoadingCustomer) {
|
||||||
|
return <CustomerEditorSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoadError) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppContent>
|
||||||
|
<ErrorAlert
|
||||||
|
message={
|
||||||
|
(loadError as Error)?.message ??
|
||||||
|
t("pages.update.loadErrorMsg", "Inténtalo de nuevo más tarde.")
|
||||||
|
}
|
||||||
|
title={t("pages.update.loadErrorTitle", "No se pudo cargar el cliente")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<BackHistoryButton />
|
||||||
|
</div>
|
||||||
|
</AppContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppHeader>
|
||||||
|
<PageHeader
|
||||||
|
backIcon
|
||||||
|
description={
|
||||||
|
<div className="mt-2 flex items-center gap-3">
|
||||||
|
<Badge className="font-mono" variant="secondary">
|
||||||
|
{customer?.tin}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline">{customer?.is_company ? "Empresa" : "Persona"}</Badge>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
rightSlot={
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={() => navigate("/customers/list")} size="icon" variant="outline">
|
||||||
|
<MoreVertical className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button>
|
||||||
|
<EditIcon className="mr-2 h-4 w-4" />
|
||||||
|
Editar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{customer?.name}{" "}
|
||||||
|
{customer?.trade_name && (
|
||||||
|
<span className="text-muted-foreground">({customer.trade_name})</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</AppHeader>
|
||||||
|
<AppContent>
|
||||||
|
{/* Main Content Grid */}
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
{/* Información Básica */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<FileText className="size-5 text-primary" />
|
||||||
|
Información Básica
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Nombre</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{customer?.name}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Referencia</dt>
|
||||||
|
<dd className="mt-1 font-mono text-base text-foreground">{customer?.reference}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Registro Legal</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{customer?.legal_record}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Impuestos por Defecto</dt>
|
||||||
|
<dd className="mt-1">
|
||||||
|
{customer?.default_taxes.map((tax) => (
|
||||||
|
<Badge key={tax} variant={"secondary"}>
|
||||||
|
{tax}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Dirección */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<MapPin className="size-5 text-primary" />
|
||||||
|
Dirección
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Calle</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">
|
||||||
|
{customer?.street}
|
||||||
|
{customer?.street2 && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
{customer?.street2}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Ciudad</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{customer?.city}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Código Postal</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{customer?.postal_code}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Provincia</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{customer?.province}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">País</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{customer?.country}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Información de Contacto */}
|
||||||
|
<Card className="md:col-span-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<Mail className="size-5 text-primary" />
|
||||||
|
Información de Contacto
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
{/* Contacto Principal */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="font-semibold text-foreground">Contacto Principal</h3>
|
||||||
|
{customer?.email_primary && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Email</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">
|
||||||
|
{customer?.email_primary}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{customer?.mobile_primary && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Móvil</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">
|
||||||
|
{customer?.mobile_primary}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{customer?.phone_primary && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Teléfono</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">
|
||||||
|
{customer?.phone_primary}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contacto Secundario */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="font-semibold text-foreground">Contacto Secundario</h3>
|
||||||
|
{customer?.email_secondary && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Email</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">
|
||||||
|
{customer?.email_secondary}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{customer?.mobile_secondary && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Móvil</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">
|
||||||
|
{customer?.mobile_secondary}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{customer?.phone_secondary && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Teléfono</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">
|
||||||
|
{customer?.phone_secondary}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Otros Contactos */}
|
||||||
|
{(customer?.website || customer?.fax) && (
|
||||||
|
<div className="space-y-4 md:col-span-2">
|
||||||
|
<h3 className="font-semibold text-foreground">Otros</h3>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
{customer?.website && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Globe className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Sitio Web</dt>
|
||||||
|
<dd className="mt-1 text-base text-primary hover:underline">
|
||||||
|
<a href={customer?.website} rel="noopener noreferrer" target="_blank">
|
||||||
|
{customer?.website}
|
||||||
|
</a>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{customer?.fax && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Fax</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{customer?.fax}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Preferencias */}
|
||||||
|
<Card className="md:col-span-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<Languages className="size-5 text-primary" />
|
||||||
|
Preferencias
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Languages className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Idioma Preferido</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{customer?.language_code}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Banknote className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Moneda Preferida</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{customer?.currency_code}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</AppContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
324
modules/customers/src/web/view/ui/pages/customer-view-page.tsx
Normal file
324
modules/customers/src/web/view/ui/pages/customer-view-page.tsx
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
import { ErrorAlert, PageHeader } from "@erp/core/components";
|
||||||
|
import { useUrlParamId } from "@erp/core/hooks";
|
||||||
|
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import {
|
||||||
|
Banknote,
|
||||||
|
EditIcon,
|
||||||
|
FileText,
|
||||||
|
Globe,
|
||||||
|
Languages,
|
||||||
|
Mail,
|
||||||
|
MapPin,
|
||||||
|
MoreVertical,
|
||||||
|
Phone,
|
||||||
|
Smartphone,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../i18n";
|
||||||
|
import { useCustomerViewPageController } from "../../controllers/use-customer-view-page.controller";
|
||||||
|
import { CustomerEditorSkeleton } from "../components";
|
||||||
|
|
||||||
|
export const CustomerViewPage = () => {
|
||||||
|
const initialCustomerId = useUrlParamId();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const {
|
||||||
|
viewCtrl: { setCustomerId, customerId, data, isError, error, isLoading },
|
||||||
|
} = useCustomerViewPageController();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialCustomerId && customerId !== initialCustomerId) {
|
||||||
|
setCustomerId(initialCustomerId);
|
||||||
|
}
|
||||||
|
}, [initialCustomerId, customerId, setCustomerId]);
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppContent>
|
||||||
|
<ErrorAlert
|
||||||
|
message={
|
||||||
|
(error as Error)?.message ??
|
||||||
|
t("pages.update.loadErrorMsg", "Inténtalo de nuevo más tarde.")
|
||||||
|
}
|
||||||
|
title={t("pages.update.loadErrorTitle", "No se pudo cargar el cliente")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<BackHistoryButton />
|
||||||
|
</div>
|
||||||
|
</AppContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <CustomerEditorSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppHeader>
|
||||||
|
<PageHeader
|
||||||
|
backIcon
|
||||||
|
description={
|
||||||
|
<div className="mt-2 flex items-center gap-3">
|
||||||
|
<Badge className="font-mono" variant="secondary">
|
||||||
|
{data?.tin}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline">{data?.is_company ? "Empresa" : "Persona"}</Badge>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
rightSlot={
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={() => navigate("/customers/list")} size="icon" variant="outline">
|
||||||
|
<MoreVertical className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button>
|
||||||
|
<EditIcon className="mr-2 h-4 w-4" />
|
||||||
|
Editar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{data?.name}{" "}
|
||||||
|
{data?.trade_name && (
|
||||||
|
<span className="text-muted-foreground">({data.trade_name})</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</AppHeader>
|
||||||
|
<AppContent>
|
||||||
|
{/* Main Content Grid */}
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
{/* Información Básica */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<FileText className="size-5 text-primary" />
|
||||||
|
Información Básica
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Nombre</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{data?.name}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Referencia</dt>
|
||||||
|
<dd className="mt-1 font-mono text-base text-foreground">{data?.reference}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Registro Legal</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{data?.legal_record}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Impuestos por Defecto</dt>
|
||||||
|
<dd className="mt-1">
|
||||||
|
{data?.default_taxes.map((tax) => (
|
||||||
|
<Badge key={tax} variant={"secondary"}>
|
||||||
|
{tax}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Dirección */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<MapPin className="size-5 text-primary" />
|
||||||
|
Dirección
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Calle</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">
|
||||||
|
{data?.street}
|
||||||
|
{data?.street2 && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
{data?.street2}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Ciudad</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{data?.city}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Código Postal</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{data?.postal_code}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Provincia</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{data?.province}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">País</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{data?.country}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Información de Contacto */}
|
||||||
|
<Card className="md:col-span-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<Mail className="size-5 text-primary" />
|
||||||
|
Información de Contacto
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
{/* Contacto Principal */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="font-semibold text-foreground">Contacto Principal</h3>
|
||||||
|
{data?.email_primary && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Email</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{data?.email_primary}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data?.mobile_primary && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Móvil</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{data?.mobile_primary}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data?.phone_primary && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Teléfono</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{data?.phone_primary}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contacto Secundario */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="font-semibold text-foreground">Contacto Secundario</h3>
|
||||||
|
{data?.email_secondary && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Email</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{data?.email_secondary}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data?.mobile_secondary && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Móvil</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{data?.mobile_secondary}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data?.phone_secondary && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Teléfono</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{data?.phone_secondary}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Otros Contactos */}
|
||||||
|
{(data?.website || data?.fax) && (
|
||||||
|
<div className="space-y-4 md:col-span-2">
|
||||||
|
<h3 className="font-semibold text-foreground">Otros</h3>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
{data?.website && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Globe className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Sitio Web</dt>
|
||||||
|
<dd className="mt-1 text-base text-primary hover:underline">
|
||||||
|
<a href={data?.website} rel="noopener noreferrer" target="_blank">
|
||||||
|
{data?.website}
|
||||||
|
</a>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data?.fax && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Fax</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{data?.fax}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Preferencias */}
|
||||||
|
<Card className="md:col-span-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<Languages className="size-5 text-primary" />
|
||||||
|
Preferencias
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Languages className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Idioma Preferido</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{data?.language_code}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Banknote className="mt-0.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<dt className="text-sm font-medium text-muted-foreground">Moneda Preferida</dt>
|
||||||
|
<dd className="mt-1 text-base text-foreground">{data?.currency_code}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</AppContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
modules/customers/src/web/view/ui/pages/index.ts
Normal file
1
modules/customers/src/web/view/ui/pages/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-view-page";
|
||||||
@ -28,6 +28,6 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src", "../core/src/web/components/error-alert.tsx"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user