Clientes
This commit is contained in:
parent
366f90d403
commit
8f9f161794
@ -21,13 +21,21 @@
|
||||
"description": "Manage your customers",
|
||||
"list": {
|
||||
"title": "Customer list",
|
||||
"description": "List all customers",
|
||||
"description": "Manage your customers and their contact information",
|
||||
"grid_columns": {
|
||||
"customer": "Customer",
|
||||
"status": "Status",
|
||||
"contact": "Contact",
|
||||
"address": "Address",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"actions": {
|
||||
"more": "More",
|
||||
"view": "View",
|
||||
"edit": "Edit customer",
|
||||
"delete": "Delete customer",
|
||||
"visit_website": "Visit website",
|
||||
"copy_email": "Copy email"
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
@ -192,6 +200,9 @@
|
||||
"no_entities_found": "No results found for \"{{search}}\"",
|
||||
"select_or_create": "Select an item from the list or create a new one.",
|
||||
"create_label": "Create new item"
|
||||
},
|
||||
"address_cell": {
|
||||
"open_in_google_maps": "Open address in Google Maps"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -21,13 +21,21 @@
|
||||
"description": "Gestiona tus clientes",
|
||||
"list": {
|
||||
"title": "Lista de clientes",
|
||||
"description": "Lista todos los clientes",
|
||||
"description": "Gestiona la información de tus clientes y sus datos de contacto",
|
||||
"grid_columns": {
|
||||
"customer": "Cliente",
|
||||
"status": "Estado",
|
||||
"contact": "Contacto",
|
||||
"address": "Dirección",
|
||||
"actions": "Acciones"
|
||||
},
|
||||
"actions": {
|
||||
"more": "Más",
|
||||
"view": "Ver",
|
||||
"edit": "Editar cliente",
|
||||
"delete": "Eliminar cliente",
|
||||
"visit_website": "Visitar sitio web",
|
||||
"copy_email": "Copiar email"
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
@ -53,8 +61,8 @@
|
||||
"customer_type": {
|
||||
"label": "Este cliente es...",
|
||||
"description": "Seleccione el tipo de cliente",
|
||||
"company": "una empresa",
|
||||
"individual": "una persona"
|
||||
"company": "Empresa",
|
||||
"individual": "Particular"
|
||||
},
|
||||
"name": {
|
||||
"label": "Nombre",
|
||||
@ -194,6 +202,9 @@
|
||||
"no_entities_found": "No se encontraron resultados para \"{{search}}\"",
|
||||
"select_or_create": "Seleccione un elemento de la lista o cree uno nuevo.",
|
||||
"create_label": "Crear nuevo elemento"
|
||||
},
|
||||
"address_cell": {
|
||||
"open_in_google_maps": "Abrir dirección en Google Maps"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -40,7 +40,7 @@ export function useCustomersGridColumns(
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.customers.list.gridColumns.customer")}
|
||||
title={t("pages.list.grid_columns.customer")}
|
||||
/>
|
||||
),
|
||||
accessorFn: (row) => row.name,
|
||||
@ -51,45 +51,53 @@ export function useCustomersGridColumns(
|
||||
const customer = row.original;
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-3">
|
||||
<Avatar className="size-10 border-2 border-background shadow-sm">
|
||||
<AvatarFallback
|
||||
className={
|
||||
customer.status === "active"
|
||||
? "bg-blue-100 text-blue-700"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}
|
||||
>
|
||||
<Initials name={customer.name} />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<button
|
||||
className="flex items-start gap-3 text-left transition-colors hover:opacity-80 cursor-pointer"
|
||||
onClick={onViewClick ? () => onViewClick(customer) : undefined}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Avatar className="size-10 border-2 border-background shadow-sm">
|
||||
<AvatarFallback
|
||||
className={
|
||||
customer.status === "active"
|
||||
? "bg-blue-100 text-blue-700"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}
|
||||
>
|
||||
<Initials name={customer.name} />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-foreground">{customer.name}</span>
|
||||
{customer.trade_name && (
|
||||
<span className="text-muted-foreground">({customer.trade_name})</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span className="font-mono">{customer.tin}</span>
|
||||
<Badge className="gap-1 px-1.5 py-0 text-xs font-normal" variant="outline">
|
||||
{customer.is_company ? (
|
||||
<>
|
||||
<Building2Icon className="size-3" />
|
||||
{t("pages.customers.list.customerKind.company")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserIcon className="size-3" />
|
||||
{t("pages.customers.list.customerKind.individual")}
|
||||
</>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-foreground hover:underline">
|
||||
{customer.name}
|
||||
</span>
|
||||
{customer.trade_name && (
|
||||
<span className="text-muted-foreground">({customer.trade_name})</span>
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span className="font-mono">{customer.tin}</span>
|
||||
<Badge className="gap-1 px-1.5 py-0 text-xs font-normal" variant="outline">
|
||||
{customer.is_company ? (
|
||||
<>
|
||||
<Building2Icon className="size-3" />
|
||||
{t("form_fields.customer_type.company")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserIcon className="size-3" />
|
||||
{t("form_fields.customer_type.individual")}
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
},
|
||||
},
|
||||
@ -99,11 +107,12 @@ export function useCustomersGridColumns(
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.customers.list.gridColumns.contact")}
|
||||
title={t("pages.list.grid_columns.contact")}
|
||||
/>
|
||||
),
|
||||
accessorFn: (row) =>
|
||||
`${row.email_primary} ${row.phone_primary} ${row.mobile_primary} ${row.website}`,
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
cell: ({ row }) => <ContactCell customer={row.original} />,
|
||||
@ -114,11 +123,12 @@ export function useCustomersGridColumns(
|
||||
<DataTableColumnHeader
|
||||
className="text-left"
|
||||
column={column}
|
||||
title={t("pages.customers.list.gridColumns.address")}
|
||||
title={t("pages.list.grid_columns.address")}
|
||||
/>
|
||||
),
|
||||
accessorFn: (row) =>
|
||||
`${row.street} ${row.street2} ${row.city} ${row.postal_code} ${row.province} ${row.country}`,
|
||||
enableSorting: false,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
cell: ({ row }) => <AddressCell customer={row.original} />,
|
||||
@ -129,7 +139,7 @@ export function useCustomersGridColumns(
|
||||
<DataTableColumnHeader
|
||||
className="text-right"
|
||||
column={column}
|
||||
title={t("pages.customers.list.gridColumns.actions")}
|
||||
title={t("pages.list.grid_columns.actions")}
|
||||
/>
|
||||
),
|
||||
size: 64,
|
||||
@ -144,7 +154,7 @@ export function useCustomersGridColumns(
|
||||
<div className="flex justify-end">
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
aria-label={t("pages.customers.list.actions.view")}
|
||||
aria-label={t("pages.list.actions.view")}
|
||||
onClick={() => onViewClick?.(customer)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
@ -154,26 +164,22 @@ export function useCustomersGridColumns(
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-label={t("pages.customers.list.actions.more")}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<Button aria-label={t("pages.list.actions.more")} size="icon" variant="ghost">
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>{t("pages.customers.list.actions.label")}</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>{t("pages.list.grid_columns.actions")}</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem onClick={() => onViewClick?.(customer)}>
|
||||
{t("pages.customers.list.actions.open")}
|
||||
{t("pages.list.actions.view")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={() => onEditClick?.(customer)}>
|
||||
{t("pages.customers.list.actions.edit")}
|
||||
{t("pages.list.actions.edit")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
@ -184,14 +190,14 @@ export function useCustomersGridColumns(
|
||||
window.open(safeHTTPUrl(website), "_blank", "noopener,noreferrer")
|
||||
}
|
||||
>
|
||||
{t("pages.customers.list.actions.visitWebsite")}
|
||||
{t("pages.list.actions.visit_website")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
disabled={!email_primary}
|
||||
onClick={() => navigator.clipboard.writeText(email_primary)}
|
||||
>
|
||||
{t("pages.customers.list.actions.copyEmail")}
|
||||
{t("pages.list.actions.copy_email")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
@ -200,7 +206,7 @@ export function useCustomersGridColumns(
|
||||
className="text-destructive"
|
||||
onClick={() => onDeleteClick?.(customer)}
|
||||
>
|
||||
{t("pages.customers.list.actions.delete")}
|
||||
{t("pages.list.actions.delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@ -1,19 +1,36 @@
|
||||
import type { CustomerListRow } from "@erp/customers/web/shared";
|
||||
import { MapPinIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../i18n";
|
||||
import type { CustomerListRow } from "../../../shared";
|
||||
|
||||
const getGoogleMapsUrl = (customer: CustomerListRow) => {
|
||||
const fullAddress = `${customer.street}, ${customer.postal_code} ${customer.city}, ${customer.province}, ${customer.country}`;
|
||||
return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(fullAddress)}`;
|
||||
};
|
||||
|
||||
export const AddressCell = ({ customer }: { customer: CustomerListRow }) => {
|
||||
const { t } = useTranslation();
|
||||
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(", ");
|
||||
return (
|
||||
<address className="not-italic flex items-start gap-2 text-muted-foreground">
|
||||
<MapPinIcon className="mt-0.5 size-3.5" />
|
||||
<div className="text-sm">
|
||||
<p>{line1}</p>
|
||||
<p>
|
||||
{line2} · {line3}
|
||||
</p>
|
||||
</div>
|
||||
<address className="not-italic flex items-start gap-2 text-muted-foreground hover:text-primary transition-colors">
|
||||
<a
|
||||
aria-label={t("components.address_cell.open_in_google_maps")}
|
||||
className="group/address flex items-start gap-2 rounded-md p-1 -m-1 transition-colors hover:bg-muted"
|
||||
href={getGoogleMapsUrl(customer)}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
title={t("components.address_cell.open_in_google_maps")}
|
||||
>
|
||||
<MapPinIcon className="mt-0.5 size-3.5" />
|
||||
<div className="text-sm">
|
||||
<p>{line1}</p>
|
||||
<p>
|
||||
{line2} · {line3}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</address>
|
||||
);
|
||||
};
|
||||
|
||||
@ -3,13 +3,16 @@ import { MailIcon, PhoneIcon } from "lucide-react";
|
||||
|
||||
export const ContactCell = ({ customer }: { customer: CustomerListRow }) => (
|
||||
<div className="flex flex-col gap-1.5 text-sm text-muted-foreground transition-colors ">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:text-foreground"
|
||||
href={`mailto:${customer.email_primary}`}
|
||||
>
|
||||
<MailIcon className="size-3.5" />
|
||||
{customer.email_primary}
|
||||
</a>
|
||||
{customer.email_primary && (
|
||||
<a
|
||||
className="flex items-center gap-2 hover:text-foreground"
|
||||
href={`mailto:${customer.email_primary}`}
|
||||
>
|
||||
<MailIcon className="size-3.5" />
|
||||
{customer.email_primary}
|
||||
</a>
|
||||
)}
|
||||
|
||||
{customer.email_secondary && (
|
||||
<a
|
||||
className="flex items-center gap-2 hover:text-foreground"
|
||||
@ -19,7 +22,7 @@ export const ContactCell = ({ customer }: { customer: CustomerListRow }) => (
|
||||
{customer.email_secondary}
|
||||
</a>
|
||||
)}
|
||||
{customer.phone_primary ? (
|
||||
{customer.phone_primary && (
|
||||
<a
|
||||
className="flex items-center gap-2 hover:text-foreground"
|
||||
href={`tel:${customer.phone_primary}`}
|
||||
@ -27,10 +30,6 @@ export const ContactCell = ({ customer }: { customer: CustomerListRow }) => (
|
||||
<PhoneIcon className="size-3.5" />
|
||||
{customer.phone_primary}
|
||||
</a>
|
||||
) : (
|
||||
<span className="flex items-center gap-2 text-muted-foreground/50">
|
||||
<PhoneIcon className="size-3.5" />-
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -25,8 +25,8 @@ export const ListCustomersPage = () => {
|
||||
return (
|
||||
<AppContent>
|
||||
<ErrorAlert
|
||||
message={(listCtrl.error as Error)?.message || t("pages.customers.list.loadErrorMessage")}
|
||||
title={t("pages.customers.list.loadErrorTitle")}
|
||||
message={(listCtrl.error as Error)?.message || t("pages.list.loadErrorMessage")}
|
||||
title={t("pages.list.loadErrorTitle")}
|
||||
/>
|
||||
<BackHistoryButton />
|
||||
</AppContent>
|
||||
@ -37,17 +37,17 @@ export const ListCustomersPage = () => {
|
||||
<>
|
||||
<AppHeader>
|
||||
<PageHeader
|
||||
description={t("pages.customers.list.description")}
|
||||
description={t("pages.list.description")}
|
||||
rightSlot={
|
||||
<Button
|
||||
aria-label={t("pages.customers.create.title")}
|
||||
aria-label={t("pages.create.title")}
|
||||
onClick={() => navigate("/customers/create")}
|
||||
>
|
||||
<PlusIcon aria-hidden className="mr-2 size-4" />
|
||||
{t("pages.customers.create.title")}
|
||||
{t("pages.create.title")}
|
||||
</Button>
|
||||
}
|
||||
title={t("pages.customers.list.title")}
|
||||
title={t("pages.list.title")}
|
||||
/>
|
||||
</AppHeader>
|
||||
<AppContent>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user