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 "./page-header";
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./customer-edit-form";
|
||||
export * from "./customer-editor-skeleton";
|
||||
//export * from "./customer-edit-form";
|
||||
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 "./customer-modal-selector";
|
||||
export * from "./editor";
|
||||
export * from "./error-alert";
|
||||
export * from "./not-found-card";
|
||||
//export * from "./client-selector-modal";
|
||||
//export * from "./customer-modal-selector";
|
||||
//export * from "./editor";
|
||||
export * from "../../../../core/src/web/components/error-alert";
|
||||
//export * from "./not-found-card";
|
||||
|
||||
@ -5,8 +5,8 @@ import { Outlet, type RouteObject } from "react-router-dom";
|
||||
// Lazy load components
|
||||
const CustomerLayout = lazy(() => import("./ui").then((m) => ({ default: m.CustomerLayout })));
|
||||
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 CustomerUpdate = lazy(() =>
|
||||
import("./pages").then((m) => ({ default: m.CustomerUpdatePage }))
|
||||
@ -25,7 +25,7 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
||||
{ path: "", index: true, element: <CustomersList /> }, // index
|
||||
{ path: "list", element: <CustomersList /> },
|
||||
//{ path: "create", element: <CustomerAdd /> },
|
||||
//{ path: ":id", element: <CustomerView /> },
|
||||
{ path: ":id", element: <CustomerView /> },
|
||||
//{ path: ":id/edit", element: <CustomerUpdate /> },
|
||||
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export * from "./use-create-customer-mutation";
|
||||
export * from "./use-customer-query";
|
||||
export * from "./use-customers-context";
|
||||
export * from "./use-update-customer-mutation";
|
||||
//export * from "./use-create-customer-mutation";
|
||||
//export * from "./use-customer-query";
|
||||
//export * from "./use-customers-context";
|
||||
//export * from "./use-update-customer-mutation";
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { useDataSource } from "@erp/core/hooks";
|
||||
import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
|
||||
import { DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { ZodError } from "zod/v4";
|
||||
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { ZodError } from "zod/v4";
|
||||
|
||||
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 { setCustomerDetailCache } from "./use-customer-query";
|
||||
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { useDataSource } from "@erp/core/hooks";
|
||||
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 { Customer, CustomerFormData } from "../schemas";
|
||||
import type { Customer, CustomerFormData } from "../schemas";
|
||||
|
||||
import { toValidationErrors } from "./use-create-customer-mutation";
|
||||
import {
|
||||
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.
|
||||
|
||||
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 { IDataSource } from "@erp/core/client";
|
||||
|
||||
import type { CustomerSummaryPage } from "../../types";
|
||||
import type { CustomerSummaryPage } from "./api-types";
|
||||
|
||||
export async function getCustomerListApi(
|
||||
dataSource: IDataSource,
|
||||
@ -13,6 +13,5 @@ export async function getCustomerListApi(
|
||||
...criteria,
|
||||
});
|
||||
|
||||
//return mapIssuedInvoiceList(raw);
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./api-types";
|
||||
export * from "./get-customer-list.api";
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import type { CriteriaDTO } from "@erp/core";
|
||||
import { useDataSource } from "@erp/core/hooks";
|
||||
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 { getCustomerListApi } from "../api";
|
||||
import { type CustomerSummaryPage, getCustomerListApi } from "../api";
|
||||
|
||||
export const CUSTOMERS_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [
|
||||
"customers",
|
||||
@ -23,7 +27,9 @@ type CustomersQueryOptions = {
|
||||
criteria?: CriteriaDTO;
|
||||
};
|
||||
|
||||
export const useCustomerListQuery = (options?: CustomersQueryOptions) => {
|
||||
export const useCustomerListQuery = (
|
||||
options?: CustomersQueryOptions
|
||||
): UseQueryResult<CustomerSummaryPage, DefaultError> => {
|
||||
const dataSource = useDataSource();
|
||||
const enabled = options?.enabled ?? true;
|
||||
const criteria = options?.criteria ?? {};
|
||||
@ -37,7 +43,6 @@ export const useCustomerListQuery = (options?: CustomersQueryOptions) => {
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
export function cancelCustomerListQueries(qc: QueryClient) {
|
||||
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 { CustomerSummaryPage } from "./customer.api.schema";
|
||||
import type { CustomerSummary } from "../../schemas";
|
||||
import type { CustomerSummaryPage } from "../api";
|
||||
|
||||
export type CustomerSummaryData = CustomerSummary;
|
||||
|
||||
@ -3,7 +3,7 @@ import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { CustomerSummaryData, CustomerSummaryPageData } from "../../../../types";
|
||||
import type { CustomerSummaryData, CustomerSummaryPageData } from "../../../types";
|
||||
|
||||
interface CustomersGridProps {
|
||||
data: CustomerSummaryPageData;
|
||||
@ -58,4 +58,4 @@ export const CustomersGrid = ({
|
||||
totalItems={total_items}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -2,24 +2,21 @@ import { DataTableColumnHeader } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
Button, DropdownMenu,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
EyeIcon, MoreHorizontalIcon
|
||||
} from "lucide-react";
|
||||
import { EyeIcon, MoreHorizontalIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import {
|
||||
type CustomerSummaryData,
|
||||
} from "../../../../types";
|
||||
import { CustomerStatusBadge } from "../../../../ui";
|
||||
import type { CustomerSummaryData } from "../../../types";
|
||||
import { AddressCell, ContactCell, Initials } from "../../components";
|
||||
import { KindBadge } from "../../components/kind-badge";
|
||||
import { Soft } from "../../components/soft";
|
||||
@ -86,12 +83,14 @@ export function useCustomersGridColumns(
|
||||
return (
|
||||
<div className="flex items-start gap-1 my-1.5">
|
||||
<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>
|
||||
<div className="min-w-0 grid gap-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<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>}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@ -105,22 +104,22 @@ export function useCustomersGridColumns(
|
||||
},
|
||||
|
||||
// Contacto (emails, teléfonos, web)
|
||||
{
|
||||
id: "contact",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.list.grid_columns.contact")}
|
||||
/>
|
||||
),
|
||||
accessorFn: (r) => `${r.email_primary} ${r.phone_primary} ${r.mobile_primary} ${r.website}`,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
cell: ({ row }) => <ContactCell customer={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "contact",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.list.grid_columns.contact")}
|
||||
/>
|
||||
),
|
||||
accessorFn: (r) => `${r.email_primary} ${r.phone_primary} ${r.mobile_primary} ${r.website}`,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
cell: ({ row }) => <ContactCell customer={row.original} />,
|
||||
},
|
||||
|
||||
// Dirección (múltiples campos en bloque)
|
||||
// Dirección (múltiples campos en bloque)
|
||||
{
|
||||
id: "address",
|
||||
header: t("pages.list.grid_columns.address"),
|
||||
@ -159,37 +158,37 @@ export function useCustomersGridColumns(
|
||||
<EyeIcon className="size-4" />
|
||||
</Button>
|
||||
|
||||
{0 === false && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button aria-label="More actions" size="icon" variant="ghost">
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => actionHandlers.onViewClick?.(customer)}>Open</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => actionHandlers.onEditClick?.(customer)}>Edit</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => window.open(safeHttp(website), "_blank")}>
|
||||
Visit website
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigator.clipboard.writeText(email_primary)}
|
||||
>
|
||||
Copy email
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onClick={() => actionHandlers.onDeleteClick?.(customer)}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button aria-label="More actions" size="icon" variant="ghost">
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => actionHandlers.onViewClick?.(customer)}>
|
||||
Open
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => actionHandlers.onEditClick?.(customer)}>
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => window.open(safeHttp(website), "_blank")}>
|
||||
Visit website
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(email_primary)}>
|
||||
Copy email
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onClick={() => actionHandlers.onDeleteClick?.(customer)}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import {
|
||||
type CustomerSummaryData,
|
||||
} from "../../../types";
|
||||
import 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 line2 = [customer.postal_code, customer.city].filter(Boolean).join(" ");
|
||||
const line3 = [customer.province, customer.country].filter(Boolean).join(", ");
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { MailIcon, PhoneIcon } from 'lucide-react';
|
||||
import { Soft } from './soft';
|
||||
import {
|
||||
type CustomerSummaryData,
|
||||
} from "../../../types";
|
||||
import { MailIcon, PhoneIcon } from "lucide-react";
|
||||
|
||||
import type { CustomerSummaryData } from "../../types";
|
||||
|
||||
import { Soft } from "./soft";
|
||||
|
||||
export const ContactCell = ({ customer }: { customer: CustomerSummaryData }) => (
|
||||
<div className="grid gap-1 text-foreground text-sm my-1.5">
|
||||
@ -32,4 +31,4 @@ export const ContactCell = ({ customer }: { customer: CustomerSummaryData }) =>
|
||||
</div>
|
||||
{false}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
|
||||
@ -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 { useTranslation } from "../../../i18n";
|
||||
import { useCustomerListPageController } from '../../controllers';
|
||||
import { useCustomersGridColumns } from '../blocks';
|
||||
import { AppContent, BackHistoryButton } from '@repo/rdx-ui/components';
|
||||
import { ErrorAlert } from '../../../ui';
|
||||
import { ErrorAlert } from "../../../ui";
|
||||
import { useCustomerListPageController } from "../../controllers";
|
||||
import { CustomersGrid, useCustomersGridColumns } from "../blocks";
|
||||
|
||||
export const CustomerListPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -12,9 +15,11 @@ export const CustomerListPage = () => {
|
||||
|
||||
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) {
|
||||
return (
|
||||
@ -26,10 +31,8 @@ export const CustomerListPage = () => {
|
||||
<BackHistoryButton />
|
||||
</AppContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <h1>gika</h1>;
|
||||
/*
|
||||
return (
|
||||
<>
|
||||
<AppHeader>
|
||||
@ -68,6 +71,5 @@ export const CustomerListPage = () => {
|
||||
/>
|
||||
</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 {
|
||||
CreateCustomerRequestSchema,
|
||||
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 { Button } from "@repo/shadcn-ui/components";
|
||||
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { useTranslation } from "../../../i18n";
|
||||
|
||||
export const CustomerEditorSkeleton = () => {
|
||||
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,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"include": ["src", "../core/src/web/components/error-alert.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user