Compare commits

..

No commits in common. "51adb8d2b01e6f0dfb917f882231e1cc6adb9eab" and "366f90d403456d5e17d949bb359f5df0c03db96b" have entirely different histories.

15 changed files with 99 additions and 164 deletions

View File

@ -40,7 +40,7 @@ export function PageHeader({
<h2 className="h-8 text-xl font-semibold text-foreground sm:truncate sm:tracking-tight">
{title}
</h2>
{description && <div className="text-sm text-muted-foreground">{description}</div>}
{description && <div className="text-sm text-primary">{description}</div>}
</div>
</div>
</div>

View File

@ -21,21 +21,13 @@
"description": "Manage your customers",
"list": {
"title": "Customer list",
"description": "Manage your customers and their contact information",
"description": "List all customers",
"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": {
@ -200,9 +192,6 @@
"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"
}
}
}

View File

@ -21,21 +21,13 @@
"description": "Gestiona tus clientes",
"list": {
"title": "Lista de clientes",
"description": "Gestiona la información de tus clientes y sus datos de contacto",
"description": "Lista todos los clientes",
"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": {
@ -61,8 +53,8 @@
"customer_type": {
"label": "Este cliente es...",
"description": "Seleccione el tipo de cliente",
"company": "Empresa",
"individual": "Particular"
"company": "una empresa",
"individual": "una persona"
},
"name": {
"label": "Nombre",
@ -202,9 +194,6 @@
"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"
}
}
}

View File

@ -40,7 +40,7 @@ export function useCustomersGridColumns(
<DataTableColumnHeader
className="text-left"
column={column}
title={t("pages.list.grid_columns.customer")}
title={t("pages.customers.list.gridColumns.customer")}
/>
),
accessorFn: (row) => row.name,
@ -51,53 +51,45 @@ export function useCustomersGridColumns(
const customer = row.original;
return (
<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 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 hover:underline">
{customer.name}
</span>
{customer.trade_name && (
<span className="text-muted-foreground">({customer.trade_name})</span>
<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>
<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>
</Badge>
</div>
</div>
</button>
</div>
);
},
},
@ -107,12 +99,11 @@ export function useCustomersGridColumns(
<DataTableColumnHeader
className="text-left"
column={column}
title={t("pages.list.grid_columns.contact")}
title={t("pages.customers.list.gridColumns.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} />,
@ -123,12 +114,11 @@ export function useCustomersGridColumns(
<DataTableColumnHeader
className="text-left"
column={column}
title={t("pages.list.grid_columns.address")}
title={t("pages.customers.list.gridColumns.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} />,
@ -139,7 +129,7 @@ export function useCustomersGridColumns(
<DataTableColumnHeader
className="text-right"
column={column}
title={t("pages.list.grid_columns.actions")}
title={t("pages.customers.list.gridColumns.actions")}
/>
),
size: 64,
@ -154,7 +144,7 @@ export function useCustomersGridColumns(
<div className="flex justify-end">
<div className="flex items-center gap-1">
<Button
aria-label={t("pages.list.actions.view")}
aria-label={t("pages.customers.list.actions.view")}
onClick={() => onViewClick?.(customer)}
size="icon"
variant="ghost"
@ -164,22 +154,26 @@ export function useCustomersGridColumns(
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-label={t("pages.list.actions.more")} size="icon" variant="ghost">
<Button
aria-label={t("pages.customers.list.actions.more")}
size="icon"
variant="ghost"
>
<MoreHorizontalIcon className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{t("pages.list.grid_columns.actions")}</DropdownMenuLabel>
<DropdownMenuLabel>{t("pages.customers.list.actions.label")}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onViewClick?.(customer)}>
{t("pages.list.actions.view")}
{t("pages.customers.list.actions.open")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onEditClick?.(customer)}>
{t("pages.list.actions.edit")}
{t("pages.customers.list.actions.edit")}
</DropdownMenuItem>
<DropdownMenuSeparator />
@ -190,14 +184,14 @@ export function useCustomersGridColumns(
window.open(safeHTTPUrl(website), "_blank", "noopener,noreferrer")
}
>
{t("pages.list.actions.visit_website")}
{t("pages.customers.list.actions.visitWebsite")}
</DropdownMenuItem>
<DropdownMenuItem
disabled={!email_primary}
onClick={() => navigator.clipboard.writeText(email_primary)}
>
{t("pages.list.actions.copy_email")}
{t("pages.customers.list.actions.copyEmail")}
</DropdownMenuItem>
<DropdownMenuSeparator />
@ -206,7 +200,7 @@ export function useCustomersGridColumns(
className="text-destructive"
onClick={() => onDeleteClick?.(customer)}
>
{t("pages.list.actions.delete")}
{t("pages.customers.list.actions.delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -1,36 +1,19 @@
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 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 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>
);
};

View File

@ -3,16 +3,13 @@ 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 ">
{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>
)}
<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"
@ -22,7 +19,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}`}
@ -30,6 +27,10 @@ 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>
);

View File

@ -25,8 +25,8 @@ export const ListCustomersPage = () => {
return (
<AppContent>
<ErrorAlert
message={(listCtrl.error as Error)?.message || t("pages.list.loadErrorMessage")}
title={t("pages.list.loadErrorTitle")}
message={(listCtrl.error as Error)?.message || t("pages.customers.list.loadErrorMessage")}
title={t("pages.customers.list.loadErrorTitle")}
/>
<BackHistoryButton />
</AppContent>
@ -37,17 +37,17 @@ export const ListCustomersPage = () => {
<>
<AppHeader>
<PageHeader
description={t("pages.list.description")}
description={t("pages.customers.list.description")}
rightSlot={
<Button
aria-label={t("pages.create.title")}
aria-label={t("pages.customers.create.title")}
onClick={() => navigate("/customers/create")}
>
<PlusIcon aria-hidden className="mr-2 size-4" />
{t("pages.create.title")}
{t("pages.customers.create.title")}
</Button>
}
title={t("pages.list.title")}
title={t("pages.customers.list.title")}
/>
</AppHeader>
<AppContent>

View File

@ -25,7 +25,7 @@ export function DataTableColumnHeader<TData, TValue>({
const { t } = useTranslation();
if (!column.getCanSort()) {
return <div className={cn("text-foreground text-[0.8rem]", className)}>{title}</div>;
return <div className={cn("text-foreground", className)}>{title}</div>;
}
return (

View File

@ -94,7 +94,7 @@ export function DataTablePagination<TData>({
<SelectValue placeholder={String(pageSize)} />
</SelectTrigger>
<SelectContent>
{[5, 10, 20, 25, 30, 40, 50].map((size) => (
{[10, 20, 25, 30, 40, 50].map((size) => (
<SelectItem key={size} value={String(size)}>
{size}
</SelectItem>

View File

@ -10,7 +10,7 @@ import {
DropdownMenuTrigger,
} from "@repo/shadcn-ui/components";
import type { Column, Table } from "@tanstack/react-table";
import { Settings2Icon } from "lucide-react";
import { SettingsIcon } from "lucide-react";
import { useTranslation } from "../../locales/i18n.ts";
@ -19,13 +19,8 @@ export function DataTableViewOptions<TData>({ table }: { table: Table<TData> })
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="ml-auto hidden h-8 lg:flex gap-2 items"
size="sm"
type="button"
variant="outline"
>
<Settings2Icon />
<Button className="ml-auto hidden h-8 lg:flex" size={"sm"} type="button" variant="ghost">
<SettingsIcon />
{t("components.datatable_view_options.columns_button")}
</Button>
</DropdownMenuTrigger>

View File

@ -8,10 +8,7 @@ export const AppContent = ({
}: PropsWithChildren<{ className?: string }>) => {
return (
<div
className={cn(
"app-content flex flex-1 flex-col gap-4 p-4 pt-6 min-h-screen bg-muted/20",
className
)}
className={cn("app-content flex flex-1 flex-col gap-4 p-4 pt-6 min-h-screen", className)}
{...props}
>
{children}

View File

@ -15,7 +15,7 @@ export const AppLayout = () => {
>
<AppSidebar variant="inset" />
{/* Aquí está el MAIN */}
<SidebarInset className="app-main">
<SidebarInset className="app-main bg-muted">
<Outlet />
</SidebarInset>
</SidebarProvider>

View File

@ -40,8 +40,7 @@
"typescript": "^5.6.0"
},
"dependencies": {
"@fontsource-variable/geist": "^5.2.8",
"@fontsource-variable/geist-mono": "^5.2.7",
"@fontsource-variable/inter": "^5.2.8",
"@hookform/resolvers": "^5.2.2",
"add": "^2.0.6",
"class-variance-authority": "^0.7.1",

View File

@ -1,8 +1,6 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "@fontsource-variable/geist";
@import "@fontsource-variable/geist-mono";
@import "@fontsource-variable/inter";
@custom-variant dark (&:is(.dark *));
@ -100,8 +98,7 @@
@layer utilities {
body {
font-family: var(--font-sans);
font-variant-ligatures: none;
font-family: "Inter", ui-sans-serif, sans-serif, system-ui;
}
}
@ -114,8 +111,7 @@
**/
:root {
--font-sans: "Geist Variable", ui-sans-serif, sans-serif, system-ui;
--font-mono: "Geist Mono Variable", ui-monospace, monospace;
--font-sans: "Inter", ui-sans-serif, sans-serif, system-ui;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
@ -149,7 +145,7 @@
--sidebar-border: oklch(0.6427 0.1407 253.94);
--sidebar-ring: oklch(1 0 0);
--radius: 0.25rem;
--radius: 0.625rem;
--shadow-x: 0;
--shadow-y: 1px;
--shadow-blur: 3px;

View File

@ -995,12 +995,9 @@ importers:
packages/shadcn-ui:
dependencies:
'@fontsource-variable/geist':
'@fontsource-variable/inter':
specifier: ^5.2.8
version: 5.2.8
'@fontsource-variable/geist-mono':
specifier: ^5.2.7
version: 5.2.7
'@hookform/resolvers':
specifier: ^5.2.2
version: 5.2.2(react-hook-form@7.66.0(react@19.2.0))
@ -1683,11 +1680,8 @@ packages:
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@fontsource-variable/geist-mono@5.2.7':
resolution: {integrity: sha512-ZKlZ5sjtalb2TwXKs400mAGDlt/+2ENLNySPx0wTz3bP3mWARCsUW+rpxzZc7e05d2qGch70pItt3K4qttbIYA==}
'@fontsource-variable/geist@5.2.8':
resolution: {integrity: sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==}
'@fontsource-variable/inter@5.2.8':
resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==}
'@hapi/hoek@9.3.0':
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
@ -7724,9 +7718,7 @@ snapshots:
'@floating-ui/utils@0.2.10': {}
'@fontsource-variable/geist-mono@5.2.7': {}
'@fontsource-variable/geist@5.2.8': {}
'@fontsource-variable/inter@5.2.8': {}
'@hapi/hoek@9.3.0': {}