Clientes
This commit is contained in:
parent
ae6afd2c28
commit
d90eaccde0
@ -42,13 +42,9 @@ export const createAxiosDataSource = (client: AxiosInstance): IDataSource => {
|
|||||||
getBaseUrl: () => (client as AxiosInstance).getUri(),
|
getBaseUrl: () => (client as AxiosInstance).getUri(),
|
||||||
|
|
||||||
getList: async <T, R>(resource: string, params?: Record<string, unknown>): Promise<R> => {
|
getList: async <T, R>(resource: string, params?: Record<string, unknown>): Promise<R> => {
|
||||||
const { pagination } = params as any;
|
const { signal, ...rest } = params as any; // en 'rest' puede venir el "criteria".
|
||||||
|
|
||||||
const res = await (client as AxiosInstance).get<T[]>(resource, {
|
const res = await (client as AxiosInstance).get<T[]>(resource, { signal, params: rest });
|
||||||
params: {
|
|
||||||
...pagination,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return <R>res.data;
|
return <R>res.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -143,7 +143,7 @@ export default (database: Sequelize) => {
|
|||||||
is_proforma: {
|
is_proforma: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: false,
|
defaultValue: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
status: {
|
status: {
|
||||||
|
|||||||
@ -34,7 +34,6 @@ export const InvoiceBasicInfoFields = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
control={control}
|
control={control}
|
||||||
name='invoice_number'
|
name='invoice_number'
|
||||||
required
|
|
||||||
readOnly
|
readOnly
|
||||||
label={t("form_fields.invoice_number.label")}
|
label={t("form_fields.invoice_number.label")}
|
||||||
placeholder={t("form_fields.invoice_number.placeholder")}
|
placeholder={t("form_fields.invoice_number.placeholder")}
|
||||||
|
|||||||
@ -138,6 +138,8 @@ export class CustomerRepository
|
|||||||
company_id: companyId.toString(),
|
company_id: companyId.toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(query);
|
||||||
|
|
||||||
const { rows, count } = await CustomerModel.findAndCountAll({
|
const { rows, count } = await CustomerModel.findAndCountAll({
|
||||||
...query,
|
...query,
|
||||||
transaction,
|
transaction,
|
||||||
|
|||||||
@ -0,0 +1,47 @@
|
|||||||
|
import { ArrayElement } from "@repo/rdx-utils";
|
||||||
|
import { Building2Icon, FileTextIcon, Mail, Phone, Search, User } from "lucide-react";
|
||||||
|
import { CustomersListData } from "../../schemas";
|
||||||
|
|
||||||
|
interface CustomerCardProps {
|
||||||
|
customer: ArrayElement<CustomersListData["items"]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomerCard = ({ customer }: CustomerCardProps) => {
|
||||||
|
return (
|
||||||
|
<div className='flex items-start gap-4'>
|
||||||
|
<div className='flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 border border-primary/20'>
|
||||||
|
<User className='h-6 w-6 text-primary' />
|
||||||
|
</div>
|
||||||
|
<div className='flex-1 space-y-2'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<h3 className='font-semibold text-foreground text-lg'>{customer.name}</h3>
|
||||||
|
<Search className='size-4 text-muted-foreground' />
|
||||||
|
</div>
|
||||||
|
<div className='space-y-1 text-sm text-muted-foreground'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Mail className='h-3 w-3' />
|
||||||
|
{customer.email_primary}
|
||||||
|
</div>
|
||||||
|
{customer.mobile_primary && (
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Phone className='h-3 w-3' />
|
||||||
|
{customer.mobile_primary}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{customer.trade_name && (
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Building2Icon className='h-3 w-3' />
|
||||||
|
{customer.trade_name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{customer.tin && (
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<FileTextIcon className='h-3 w-3' />
|
||||||
|
{customer.tin}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { SearchIcon, UserPlusIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export const CustomerEmptyCard = () => {
|
||||||
|
return (
|
||||||
|
<div className='flex items-center gap-4 group-hover:text-primary'>
|
||||||
|
<div className='flex size-12 items-center justify-center rounded-full bg-muted group-hover:bg-primary/15'>
|
||||||
|
<UserPlusIcon className='size-6 text-muted-foreground group-hover:text-primary' />
|
||||||
|
</div>
|
||||||
|
<div className='flex-1 text-left'>
|
||||||
|
<h3 className='font-medium text-muted-foreground mb-1 group-hover:text-primary'>
|
||||||
|
Seleccionar Cliente
|
||||||
|
</h3>
|
||||||
|
<p className='text-sm text-muted-foreground/70 group-hover:text-primary'>
|
||||||
|
Haz clic para buscar un cliente existente o crear uno nuevo
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<SearchIcon className='size-5 text-muted-foreground group-hover:text-primary' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
|
||||||
|
interface CustomerFormDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (o: boolean) => void;
|
||||||
|
client: Omit<Client, "id">;
|
||||||
|
onChange: (c: Omit<Client, "id">) => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateCustomerFormDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
client,
|
||||||
|
onChange,
|
||||||
|
onSubmit,
|
||||||
|
}: CustomerFormDialogProps) => {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className='sm:max-w-[500px] bg-card border-border'>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className='flex items-center gap-2'>
|
||||||
|
<Plus className='h-5 w-5' /> Agregar Nuevo Cliente
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Complete la información del cliente. Los campos marcados con * son obligatorios.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className='grid gap-4 py-4'>
|
||||||
|
{/* Nombre */}
|
||||||
|
<div className='grid gap-2'>
|
||||||
|
<Label htmlFor='name'>Nombre completo *</Label>
|
||||||
|
<Input
|
||||||
|
id='name'
|
||||||
|
value={client.name}
|
||||||
|
onChange={(e) => onChange({ ...client, name: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Email */}
|
||||||
|
<div className='grid gap-2'>
|
||||||
|
<Label htmlFor='email'>Email *</Label>
|
||||||
|
<Input
|
||||||
|
id='email'
|
||||||
|
type='email'
|
||||||
|
value={client.email}
|
||||||
|
onChange={(e) => onChange({ ...client, email: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Teléfono / NIF */}
|
||||||
|
<div className='grid grid-cols-2 gap-4'>
|
||||||
|
<div className='grid gap-2'>
|
||||||
|
<Label htmlFor='phone'>Teléfono</Label>
|
||||||
|
<Input
|
||||||
|
id='phone'
|
||||||
|
value={client.phone ?? ""}
|
||||||
|
onChange={(e) => onChange({ ...client, phone: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='grid gap-2'>
|
||||||
|
<Label htmlFor='taxId'>NIF/CIF</Label>
|
||||||
|
<Input
|
||||||
|
id='taxId'
|
||||||
|
value={client.taxId ?? ""}
|
||||||
|
onChange={(e) => onChange({ ...client, taxId: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Empresa */}
|
||||||
|
<div className='grid gap-2'>
|
||||||
|
<Label htmlFor='company'>Empresa</Label>
|
||||||
|
<Input
|
||||||
|
id='company'
|
||||||
|
value={client.company ?? ""}
|
||||||
|
onChange={(e) => onChange({ ...client, company: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant='outline' onClick={() => onOpenChange(false)}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onSubmit} disabled={!client.name || !client.email}>
|
||||||
|
<Plus className='mr-2 size-4' /> Crear Cliente
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,436 +1,122 @@
|
|||||||
"use client";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useCustomersSearchQuery } from "../../hooks";
|
||||||
import {
|
import { CustomerSummary } from "../../schemas";
|
||||||
Badge,
|
import { CustomerCard } from "./customer-card";
|
||||||
Button,
|
import { CustomerEmptyCard } from "./customer-empty-card";
|
||||||
Command,
|
import { CreateCustomerFormDialog } from "./customer-form-dialog";
|
||||||
CommandEmpty,
|
import { CustomerSearchDialog } from "./customer-search-dialog";
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
|
||||||
import {
|
|
||||||
Building2Icon,
|
|
||||||
Check,
|
|
||||||
FileTextIcon,
|
|
||||||
Mail,
|
|
||||||
Phone,
|
|
||||||
Plus,
|
|
||||||
PlusIcon,
|
|
||||||
Search,
|
|
||||||
SearchIcon,
|
|
||||||
User,
|
|
||||||
XIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
interface Client {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
phone?: string;
|
|
||||||
company?: string;
|
|
||||||
taxId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClientSelectorProps {
|
|
||||||
value?: string;
|
|
||||||
onValueChange?: (clientId: string) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Datos de ejemplo - en una app real vendrían de tu base de datos
|
|
||||||
const mockClients: Client[] = [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
name: "María García",
|
|
||||||
email: "maria.garcia@empresa.com",
|
|
||||||
phone: "+34 666 123 456",
|
|
||||||
company: "Empresa ABC S.L.",
|
|
||||||
taxId: "B12345678",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
name: "Juan Pérez",
|
|
||||||
email: "juan.perez@gmail.com",
|
|
||||||
phone: "+34 677 987 654",
|
|
||||||
company: "Autónomo",
|
|
||||||
taxId: "12345678Z",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
name: "Ana Martínez",
|
|
||||||
email: "ana@startup.io",
|
|
||||||
phone: "+34 688 555 777",
|
|
||||||
company: "StartUp Innovadora",
|
|
||||||
taxId: "B87654321",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "4",
|
|
||||||
name: "Carlos López",
|
|
||||||
email: "carlos.lopez@corporacion.es",
|
|
||||||
phone: "+34 699 111 222",
|
|
||||||
company: "Corporación XYZ",
|
|
||||||
taxId: "A11111111",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function CustomerModalSelector({
|
|
||||||
value,
|
|
||||||
onValueChange,
|
|
||||||
placeholder = "Seleccionar cliente...",
|
|
||||||
}: ClientSelectorProps) {
|
|
||||||
const [showClientSelector, setShowClientSelector] = useState(false);
|
|
||||||
const [clients, setClients] = useState<Client[]>(mockClients);
|
|
||||||
const [selectedClient, setSelectedClient] = useState<Client | null>(null);
|
|
||||||
const [showNewClientDialog, setShowNewClientDialog] = useState(false);
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
|
||||||
|
|
||||||
const [newClient, setNewClient] = useState({
|
|
||||||
name: "",
|
|
||||||
email: "",
|
|
||||||
phone: "",
|
|
||||||
company: "",
|
|
||||||
taxId: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Debounce pequeño y tipado
|
||||||
|
function useDebouncedValue<T>(value: T, delay = 300) {
|
||||||
|
const [debounced, setDebounced] = useState<T>(value);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value) {
|
const id = setTimeout(() => setDebounced(value), delay);
|
||||||
const client = clients.find((c) => c.id === value);
|
return () => clearTimeout(id);
|
||||||
setSelectedClient(client || null);
|
}, [value, delay]);
|
||||||
}
|
return debounced;
|
||||||
}, [value, clients]);
|
}
|
||||||
|
|
||||||
const filteredClients = clients.filter(
|
interface CustomerModalSelectorProps {
|
||||||
(client) =>
|
value?: string;
|
||||||
client.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
onValueChange?: (id: string) => void;
|
||||||
client.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
}
|
||||||
client.company?.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSelectClient = (client: Client) => {
|
export const CustomerModalSelector = ({ value, onValueChange }: CustomerModalSelectorProps) => {
|
||||||
setSelectedClient(client);
|
// UI state
|
||||||
onValueChange?.(client.id);
|
const [showSearch, setShowSearch] = useState(false);
|
||||||
setShowClientSelector(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
};
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const debouncedQuery = useDebouncedValue(searchQuery, 300);
|
||||||
|
|
||||||
const handleCreateClient = () => {
|
// Cliente seleccionado y creación local optimista
|
||||||
if (!newClient.name || !newClient.email) return;
|
const [selected, setSelected] = useState<CustomerSummary | null>(null);
|
||||||
|
const [newClient, setNewClient] = useState<Omit<CustomerSummary, "id">>({ name: "", email: "" });
|
||||||
|
const [localCreated, setLocalCreated] = useState<CustomerSummary[]>([]);
|
||||||
|
|
||||||
const client: Client = {
|
const {
|
||||||
id: Date.now().toString(),
|
data: remoteCustomers = [],
|
||||||
name: newClient.name,
|
isLoading,
|
||||||
email: newClient.email,
|
isError,
|
||||||
phone: newClient.phone || undefined,
|
error,
|
||||||
company: newClient.company || undefined,
|
} = useCustomersSearchQuery({
|
||||||
taxId: newClient.taxId || undefined,
|
q: debouncedQuery,
|
||||||
};
|
pageSize: 5,
|
||||||
|
orderBy: "updated_at",
|
||||||
setClients((prev) => [...prev, client]);
|
order: "asc",
|
||||||
setSelectedClient(client);
|
|
||||||
onValueChange?.(client.id);
|
|
||||||
setShowNewClientDialog(false);
|
|
||||||
setShowClientSelector(false);
|
|
||||||
|
|
||||||
setNewClient({
|
|
||||||
name: "",
|
|
||||||
email: "",
|
|
||||||
phone: "",
|
|
||||||
company: "",
|
|
||||||
taxId: "",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Combinar locales optimistas + remotos
|
||||||
|
const customers: CustomerSummary[] = useMemo(() => {
|
||||||
|
const byId = new Map<string, CustomerSummary>();
|
||||||
|
[...localCreated, ...remoteCustomers].forEach((c) => byId.set(c.id, c as CustomerSummary));
|
||||||
|
return Array.from(byId.values());
|
||||||
|
}, [localCreated, remoteCustomers]);
|
||||||
|
|
||||||
|
// Sync con `value`
|
||||||
|
useEffect(() => {
|
||||||
|
if (!value) return;
|
||||||
|
const found = customers.find((c) => c.id === value) ?? null;
|
||||||
|
setSelected(found);
|
||||||
|
}, [value, customers]);
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
if (!newClient.name || !newClient.email) return;
|
||||||
|
const client: CustomerSummary = {
|
||||||
|
id: crypto.randomUUID?.() ?? Date.now().toString(),
|
||||||
|
...newClient,
|
||||||
|
};
|
||||||
|
setLocalCreated((prev) => [client, ...prev]);
|
||||||
|
setSelected(client);
|
||||||
|
onValueChange?.(client.id);
|
||||||
|
setShowForm(false);
|
||||||
|
setShowSearch(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openNewClientDialog = () => {
|
console.log(customers);
|
||||||
setNewClient({ ...newClient, name: searchQuery });
|
|
||||||
setShowNewClientDialog(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<button
|
||||||
role='button'
|
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => setShowClientSelector(true)}
|
type='button'
|
||||||
onKeyDown={(e) => {
|
onClick={() => setShowSearch(true)}
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
onKeyDown={(e) => (e.key === "Enter" || e.key === " ") && setShowSearch(true)}
|
||||||
e.preventDefault();
|
className='group w-full cursor-pointer rounded-lg border border-border bg-card p-4 transition hover:bg-accent/50 hover:border-primary'
|
||||||
setShowClientSelector(true);
|
aria-label='Seleccionar cliente'
|
||||||
}
|
>
|
||||||
|
{selected ? <CustomerCard customer={selected} /> : <CustomerEmptyCard />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<CustomerSearchDialog
|
||||||
|
open={showSearch}
|
||||||
|
onOpenChange={setShowSearch}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
onSearchQueryChange={setSearchQuery}
|
||||||
|
customers={customers}
|
||||||
|
selectedClient={selected}
|
||||||
|
onSelectClient={(c) => {
|
||||||
|
setSelected(c);
|
||||||
|
onValueChange?.(c.id);
|
||||||
|
setShowSearch(false);
|
||||||
}}
|
}}
|
||||||
className='group w-full cursor-pointer rounded-lg border border-border bg-card p-4 transition-all hover:bg-accent/50 hover:border-primary'
|
onCreateClient={(name) => {
|
||||||
>
|
/*setNewClient({ name: name ?? "", email: "" });
|
||||||
{selectedClient ? (
|
setShowForm(true);*/
|
||||||
<div className='flex items-start gap-4'>
|
}}
|
||||||
<div className='flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 border border-primary/20'>
|
isLoading={isLoading}
|
||||||
<User className='h-6 w-6 text-primary' />
|
isError={isError}
|
||||||
</div>
|
errorMessage={
|
||||||
<div className='flex-1 space-y-2'>
|
isError ? ((error as Error)?.message ?? "Error al cargar clientes") : undefined
|
||||||
<div className='flex items-center justify-between'>
|
}
|
||||||
<h3 className='font-semibold text-foreground text-lg'>{selectedClient.name}</h3>
|
/>
|
||||||
<Search className='size-4 text-muted-foreground' />
|
|
||||||
</div>
|
|
||||||
<div className='space-y-1'>
|
|
||||||
<div className='flex items-center gap-2 text-sm text-muted-foreground'>
|
|
||||||
<Mail className='h-3 w-3' />
|
|
||||||
{selectedClient.email}
|
|
||||||
</div>
|
|
||||||
{selectedClient.phone && (
|
|
||||||
<div className='flex items-center gap-2 text-sm text-muted-foreground'>
|
|
||||||
<Phone className='h-3 w-3' />
|
|
||||||
{selectedClient.phone}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{selectedClient.company && (
|
|
||||||
<div className='flex items-center gap-2 text-sm text-muted-foreground'>
|
|
||||||
<Building2Icon className='h-3 w-3' />
|
|
||||||
{selectedClient.company}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{selectedClient.taxId && (
|
|
||||||
<div className='flex items-center gap-2 text-sm text-muted-foreground'>
|
|
||||||
<FileTextIcon className='h-3 w-3' />
|
|
||||||
{selectedClient.taxId}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='flex items-center gap-4 group-hover:text-primary'>
|
|
||||||
<div className='flex h-12 w-12 items-center justify-center rounded-full bg-muted border-2 border-dashed border-muted-foreground/30 group-hover:border-primary/60'>
|
|
||||||
<PlusIcon className='h-6 w-6 text-muted-foreground group-hover:text-primary' />
|
|
||||||
</div>
|
|
||||||
<div className='flex-1'>
|
|
||||||
<h3 className='font-medium text-muted-foreground mb-1 group-hover:text-primary'>
|
|
||||||
Seleccionar Cliente
|
|
||||||
</h3>
|
|
||||||
<p className='text-sm text-muted-foreground/70 group-hover:text-primary'>
|
|
||||||
Haz clic para buscar un cliente existente o crear uno nuevo
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<SearchIcon className='size-5 text-muted-foreground group-hover:text-primary' />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Dialog open={showClientSelector} onOpenChange={setShowClientSelector}>
|
<CreateCustomerFormDialog
|
||||||
<DialogContent className='sm:max-w-[600px] bg-card border-border p-0'>
|
open={showForm}
|
||||||
<DialogHeader className='px-6 pt-6 pb-4'>
|
onOpenChange={setShowForm}
|
||||||
<DialogTitle className='flex items-center justify-between text-foreground'>
|
client={newClient}
|
||||||
<div className='flex items-center gap-2'>
|
onChange={setNewClient}
|
||||||
<User className='h-5 w-5' />
|
onSubmit={handleCreate}
|
||||||
Seleccionar Cliente
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
size='sm'
|
|
||||||
onClick={() => setShowClientSelector(false)}
|
|
||||||
className='size-6 p-0 hover:bg-accent'
|
|
||||||
>
|
|
||||||
<XIcon className='size-4' />
|
|
||||||
</Button>
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription className='text-muted-foreground'>
|
|
||||||
Busca un cliente existente o crea uno nuevo.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className='px-6 pb-6'>
|
|
||||||
<Command className='border border-border rounded-lg'>
|
|
||||||
<div className='flex items-center border-b border-border px-3'>
|
|
||||||
<Search className='mr-2 size-4 shrink-0 opacity-50' />
|
|
||||||
<CommandInput
|
|
||||||
placeholder='Buscar cliente por nombre, email o empresa...'
|
|
||||||
value={searchQuery}
|
|
||||||
onValueChange={setSearchQuery}
|
|
||||||
className='flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50'
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<CommandList className='max-h-[400px]'>
|
|
||||||
<CommandEmpty className='py-6 text-center text-sm text-muted-foreground'>
|
|
||||||
<div className='flex flex-col items-center gap-2'>
|
|
||||||
<User className='h-8 w-8 text-muted-foreground/50' />
|
|
||||||
<p>No se encontraron clientes</p>
|
|
||||||
{searchQuery && (
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
size='sm'
|
|
||||||
onClick={openNewClientDialog}
|
|
||||||
className='mt-2 bg-transparent'
|
|
||||||
>
|
|
||||||
<Plus className='mr-2 size-4' />
|
|
||||||
Crear cliente "{searchQuery}"
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{filteredClients.map((client) => (
|
|
||||||
<CommandItem
|
|
||||||
key={client.id}
|
|
||||||
value={client.id}
|
|
||||||
onSelect={() => handleSelectClient(client)}
|
|
||||||
className='flex items-center gap-3 p-3 cursor-pointer hover:bg-accent'
|
|
||||||
>
|
|
||||||
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-primary/10'>
|
|
||||||
<User className='size-4 text-primary' />
|
|
||||||
</div>
|
|
||||||
<div className='flex-1 space-y-1'>
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<span className='font-medium text-foreground'>{client.name}</span>
|
|
||||||
{client.company && (
|
|
||||||
<Badge variant='secondary' className='text-xs'>
|
|
||||||
{client.company}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center gap-4 text-xs text-muted-foreground'>
|
|
||||||
<div className='flex items-center gap-1'>
|
|
||||||
<Mail className='h-3 w-3' />
|
|
||||||
{client.email}
|
|
||||||
</div>
|
|
||||||
{client.phone && (
|
|
||||||
<div className='flex items-center gap-1'>
|
|
||||||
<Phone className='h-3 w-3' />
|
|
||||||
{client.phone}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"ml-auto size-4",
|
|
||||||
selectedClient?.id === client.id ? "opacity-100" : "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
{filteredClients.length > 0 && (
|
|
||||||
<CommandGroup>
|
|
||||||
<CommandItem
|
|
||||||
onSelect={openNewClientDialog}
|
|
||||||
className='flex items-center gap-3 p-3 cursor-pointer hover:bg-accent border-t border-border'
|
|
||||||
>
|
|
||||||
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-primary'>
|
|
||||||
<Plus className='size-4 text-primary-foreground' />
|
|
||||||
</div>
|
|
||||||
<span className='font-medium text-foreground'>Agregar nuevo cliente</span>
|
|
||||||
</CommandItem>
|
|
||||||
</CommandGroup>
|
|
||||||
)}
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<Dialog open={showNewClientDialog} onOpenChange={setShowNewClientDialog}>
|
|
||||||
<DialogContent className='sm:max-w-[500px] bg-card border-border'>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className='flex items-center gap-2 text-foreground'>
|
|
||||||
<Plus className='h-5 w-5' />
|
|
||||||
Agregar Nuevo Cliente
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription className='text-muted-foreground'>
|
|
||||||
Complete la información del cliente. Los campos marcados con * son obligatorios.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className='grid gap-4 py-4'>
|
|
||||||
<div className='grid gap-2'>
|
|
||||||
<Label htmlFor='name' className='text-foreground'>
|
|
||||||
Nombre completo *
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id='name'
|
|
||||||
value={newClient.name}
|
|
||||||
onChange={(e) => setNewClient({ ...newClient, name: e.target.value })}
|
|
||||||
placeholder='Ej: María García'
|
|
||||||
className='bg-input border-border text-foreground'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='grid gap-2'>
|
|
||||||
<Label htmlFor='email' className='text-foreground'>
|
|
||||||
Email *
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id='email'
|
|
||||||
type='email'
|
|
||||||
value={newClient.email}
|
|
||||||
onChange={(e) => setNewClient({ ...newClient, email: e.target.value })}
|
|
||||||
placeholder='Ej: maria@empresa.com'
|
|
||||||
className='bg-input border-border text-foreground'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='grid grid-cols-2 gap-4'>
|
|
||||||
<div className='grid gap-2'>
|
|
||||||
<Label htmlFor='phone' className='text-foreground'>
|
|
||||||
Teléfono
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id='phone'
|
|
||||||
value={newClient.phone}
|
|
||||||
onChange={(e) => setNewClient({ ...newClient, phone: e.target.value })}
|
|
||||||
placeholder='Ej: +34 666 123 456'
|
|
||||||
className='bg-input border-border text-foreground'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='grid gap-2'>
|
|
||||||
<Label htmlFor='taxId' className='text-foreground'>
|
|
||||||
NIF/CIF
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id='taxId'
|
|
||||||
value={newClient.taxId}
|
|
||||||
onChange={(e) => setNewClient({ ...newClient, taxId: e.target.value })}
|
|
||||||
placeholder='Ej: 12345678Z'
|
|
||||||
className='bg-input border-border text-foreground'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='grid gap-2'>
|
|
||||||
<Label htmlFor='company' className='text-foreground'>
|
|
||||||
Empresa
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id='company'
|
|
||||||
value={newClient.company}
|
|
||||||
onChange={(e) => setNewClient({ ...newClient, company: e.target.value })}
|
|
||||||
placeholder='Ej: Empresa ABC S.L.'
|
|
||||||
className='bg-input border-border text-foreground'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => setShowNewClientDialog(false)}
|
|
||||||
className='border-border text-foreground hover:bg-accent'
|
|
||||||
>
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleCreateClient}
|
|
||||||
disabled={!newClient.name || !newClient.email}
|
|
||||||
className='bg-primary text-primary-foreground hover:bg-primary/90'
|
|
||||||
>
|
|
||||||
<Plus className='mr-2 size-4' />
|
|
||||||
Crear Cliente
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@ -0,0 +1,141 @@
|
|||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
import { AsteriskIcon, Check, MailIcon, Plus, SmartphoneIcon, User, UserIcon } from "lucide-react";
|
||||||
|
import { CustomerSummary } from "../../schemas";
|
||||||
|
|
||||||
|
interface CustomerSearchDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
searchQuery: string;
|
||||||
|
onSearchQueryChange: (q: string) => void;
|
||||||
|
customers: CustomerSummary[];
|
||||||
|
selectedClient: CustomerSummary | null;
|
||||||
|
onSelectClient: (c: CustomerSummary) => void;
|
||||||
|
onCreateClient: (name?: string) => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
isError?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomerSearchDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
searchQuery,
|
||||||
|
onSearchQueryChange,
|
||||||
|
customers,
|
||||||
|
selectedClient,
|
||||||
|
onSelectClient,
|
||||||
|
onCreateClient,
|
||||||
|
}: CustomerSearchDialogProps) => {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className='sm:max-w-2xl bg-card border-border p-0'>
|
||||||
|
<DialogHeader className='px-6 pt-6 pb-4'>
|
||||||
|
<DialogTitle className='flex items-center justify-between'>
|
||||||
|
<span className='flex items-center gap-2'>
|
||||||
|
<User className='h-5 w-5' />
|
||||||
|
Seleccionar Cliente
|
||||||
|
</span>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>Busca un cliente existente o crea uno nuevo.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className='px-6 pb-6'>
|
||||||
|
<Command className='border rounded-lg'>
|
||||||
|
<CommandInput
|
||||||
|
placeholder='Buscar cliente...'
|
||||||
|
value={searchQuery}
|
||||||
|
onValueChange={onSearchQueryChange}
|
||||||
|
/>
|
||||||
|
<CommandList className='max-h-[400px]'>
|
||||||
|
<CommandEmpty>
|
||||||
|
<div className='flex flex-col items-center gap-2 py-6 text-sm'>
|
||||||
|
<User className='h-8 w-8 text-muted-foreground/50' />
|
||||||
|
<p>No se encontraron clientes</p>
|
||||||
|
{searchQuery && (
|
||||||
|
<Button variant='outline' size='sm' onClick={() => onCreateClient(searchQuery)}>
|
||||||
|
<Plus className='mr-2 size-4' />
|
||||||
|
Crear cliente "{searchQuery}"
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{customers.map((customer) => (
|
||||||
|
<CommandItem
|
||||||
|
key={customer.id}
|
||||||
|
value={customer.id}
|
||||||
|
onSelect={() => onSelectClient(customer)}
|
||||||
|
className='flex items-center gap-x-4 py-5 cursor-pointer'
|
||||||
|
>
|
||||||
|
<div className='flex size-12 items-center justify-center rounded-full bg-primary/10'>
|
||||||
|
<UserIcon className='size-8 stroke-1 text-primary' />
|
||||||
|
</div>
|
||||||
|
<div className='flex-1 space-y-1 min-w-0'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<span className='text-sm font-semibold'>{customer.name}</span>
|
||||||
|
{customer.trade_name && (
|
||||||
|
<Badge variant='secondary' className='text-sm'>
|
||||||
|
{customer.trade_name}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-4 text-sm font-medium text-muted-foreground'>
|
||||||
|
{customer.tin && (
|
||||||
|
<span className='flex items-center gap-1'>
|
||||||
|
<AsteriskIcon className='size-4' /> {customer.tin}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{customer.email_primary && (
|
||||||
|
<span className='flex items-center gap-1'>
|
||||||
|
<MailIcon className='size-4' /> {customer.email_primary}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{customer.mobile_primary && (
|
||||||
|
<span className='flex items-center gap-1'>
|
||||||
|
<SmartphoneIcon className='size-4' /> {customer.mobile_primary}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"ml-auto size-4",
|
||||||
|
selectedClient?.id === customer.id ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
<CommandGroup>
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => onCreateClient()}
|
||||||
|
className='flex items-center gap-3 p-3 border-t'
|
||||||
|
>
|
||||||
|
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-primary'>
|
||||||
|
<Plus className='size-4 text-primary-foreground' />
|
||||||
|
</div>
|
||||||
|
<span className='font-medium'>Agregar nuevo cliente</span>
|
||||||
|
</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -2,4 +2,5 @@ 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-customers-query";
|
export * from "./use-customers-query";
|
||||||
|
export * from "./use-customers-search-query";
|
||||||
export * from "./use-update-customer-mutation";
|
export * from "./use-update-customer-mutation";
|
||||||
|
|||||||
@ -2,7 +2,7 @@ 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 { DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { CreateCustomerRequestSchema } from "../../common";
|
import { CreateCustomerRequestSchema } from "../../common";
|
||||||
import { CustomerData, CustomerFormData } from "../schemas";
|
import { Customer, CustomerFormData } from "../schemas";
|
||||||
import { CUSTOMERS_LIST_KEY } from "./use-update-customer-mutation";
|
import { CUSTOMERS_LIST_KEY } from "./use-update-customer-mutation";
|
||||||
|
|
||||||
type CreateCustomerPayload = {
|
type CreateCustomerPayload = {
|
||||||
@ -14,7 +14,7 @@ export function useCreateCustomer() {
|
|||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const schema = CreateCustomerRequestSchema;
|
const schema = CreateCustomerRequestSchema;
|
||||||
|
|
||||||
return useMutation<CustomerData, DefaultError, CreateCustomerPayload>({
|
return useMutation<Customer, DefaultError, CreateCustomerPayload>({
|
||||||
mutationKey: ["customer:create"],
|
mutationKey: ["customer:create"],
|
||||||
|
|
||||||
mutationFn: async (payload) => {
|
mutationFn: async (payload) => {
|
||||||
@ -38,7 +38,7 @@ export function useCreateCustomer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const created = await dataSource.createOne("customers", newCustomerData);
|
const created = await dataSource.createOne("customers", newCustomerData);
|
||||||
return created as CustomerData;
|
return created as Customer;
|
||||||
},
|
},
|
||||||
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import { DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
import { DefaultError, type QueryKey, useQuery } from "@tanstack/react-query";
|
||||||
import { CustomerData } from "../schemas";
|
import { Customer } from "../schemas";
|
||||||
|
|
||||||
export const CUSTOMER_QUERY_KEY = (id: string): QueryKey => ["customer", id] as const;
|
export const CUSTOMER_QUERY_KEY = (id: string): QueryKey => ["customer", id] as const;
|
||||||
|
|
||||||
@ -12,14 +12,14 @@ export function useCustomerQuery(customerId?: string, options?: CustomerQueryOpt
|
|||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const enabled = (options?.enabled ?? true) && !!customerId;
|
const enabled = (options?.enabled ?? true) && !!customerId;
|
||||||
|
|
||||||
return useQuery<CustomerData, DefaultError>({
|
return useQuery<Customer, DefaultError>({
|
||||||
queryKey: CUSTOMER_QUERY_KEY(customerId ?? "unknown"),
|
queryKey: CUSTOMER_QUERY_KEY(customerId ?? "unknown"),
|
||||||
queryFn: async (context) => {
|
queryFn: async (context) => {
|
||||||
const { signal } = context;
|
const { signal } = context;
|
||||||
if (!customerId) {
|
if (!customerId) {
|
||||||
if (!customerId) throw new Error("customerId is required");
|
if (!customerId) throw new Error("customerId is required");
|
||||||
}
|
}
|
||||||
return await dataSource.getOne<CustomerData>("customers", customerId, {
|
return await dataSource.getOne<Customer>("customers", customerId, {
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { CustomerSummary, CustomersPage } from "../schemas";
|
||||||
|
|
||||||
|
export interface CustomersCriteria {
|
||||||
|
q?: string;
|
||||||
|
orderBy?: string;
|
||||||
|
order?: string;
|
||||||
|
pageSize?: number;
|
||||||
|
pageNumber?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener todos los clientes
|
||||||
|
export const useCustomersSearchQuery = (criteria: CustomersCriteria) => {
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["customer", criteria],
|
||||||
|
queryFn: async (context) => {
|
||||||
|
const { signal } = context;
|
||||||
|
|
||||||
|
const customers = await dataSource.getList("customers", {
|
||||||
|
signal,
|
||||||
|
...criteria,
|
||||||
|
});
|
||||||
|
|
||||||
|
return customers as CustomersPage;
|
||||||
|
},
|
||||||
|
select: (data) => data.items as CustomerSummary[],
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
import { ArrayElement } from "@repo/rdx-utils";
|
||||||
import {
|
import {
|
||||||
CreateCustomerRequestSchema,
|
CreateCustomerRequestSchema,
|
||||||
GetCustomerByIdResponseSchema,
|
GetCustomerByIdResponseSchema,
|
||||||
@ -7,12 +8,18 @@ import {
|
|||||||
UpdateCustomerByIdRequestSchema,
|
UpdateCustomerByIdRequestSchema,
|
||||||
} from "../../common";
|
} from "../../common";
|
||||||
|
|
||||||
|
// Esquemas (Zod) provenientes del servidor
|
||||||
|
export const CustomerSchema = GetCustomerByIdResponseSchema.omit({ metadata: true });
|
||||||
export const CustomerCreateSchema = CreateCustomerRequestSchema;
|
export const CustomerCreateSchema = CreateCustomerRequestSchema;
|
||||||
export const CustomerUpdateSchema = UpdateCustomerByIdRequestSchema;
|
export const CustomerUpdateSchema = UpdateCustomerByIdRequestSchema;
|
||||||
export const CustomerSchema = GetCustomerByIdResponseSchema.omit({
|
|
||||||
metadata: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type CustomerData = z.infer<typeof CustomerSchema>;
|
// Tipos (derivados de Zod o DTOs del backend)
|
||||||
|
export type Customer = z.infer<typeof CustomerSchema>; // Entidad completa (GET/POST/PUT result)
|
||||||
|
export type CustomerCreateInput = z.infer<typeof CustomerCreateSchema>; // Cuerpo para crear
|
||||||
|
export type CustomerUpdateInput = z.infer<typeof CustomerUpdateSchema>; // Cuerpo para actualizar
|
||||||
|
|
||||||
export type CustomersListData = ListCustomersResponseDTO;
|
// Resultado de consulta con criteria (paginado, etc.)
|
||||||
|
export type CustomersPage = ListCustomersResponseDTO;
|
||||||
|
|
||||||
|
// Ítem simplificado dentro del listado (no toda la entidad)
|
||||||
|
export type CustomerSummary = ArrayElement<CustomersPage["items"]>;
|
||||||
|
|||||||
@ -106,7 +106,7 @@ export function DatePickerInputField<TFormValues extends FieldValues>({
|
|||||||
readOnly={isReadOnly}
|
readOnly={isReadOnly}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full rounded-md border px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-ring",
|
"w-full rounded-md border px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-ring placeholder:font-normal placeholder:italic",
|
||||||
isDisabled && "bg-muted text-muted-foreground cursor-not-allowed",
|
isDisabled && "bg-muted text-muted-foreground cursor-not-allowed",
|
||||||
isReadOnly && "bg-muted text-foreground cursor-default",
|
isReadOnly && "bg-muted text-foreground cursor-default",
|
||||||
!isDisabled && !isReadOnly && "bg-white text-foreground",
|
!isDisabled && !isReadOnly && "bg-white text-foreground",
|
||||||
|
|||||||
@ -73,7 +73,10 @@ export function SelectField<TFormValues extends FieldValues>({
|
|||||||
<Select onValueChange={field.onChange} defaultValue={field.value} disabled={isDisabled}>
|
<Select onValueChange={field.onChange} defaultValue={field.value} disabled={isDisabled}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger className='w-full bg-background h-8'>
|
<SelectTrigger className='w-full bg-background h-8'>
|
||||||
<SelectValue placeholder={placeholder} />
|
<SelectValue
|
||||||
|
placeholder={placeholder}
|
||||||
|
className=' placeholder:font-normal placeholder:italic '
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export function TextAreaField<TFormValues extends FieldValues>({
|
|||||||
<Textarea
|
<Textarea
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={"bg-background"}
|
className={"placeholder:font-normal placeholder:italic bg-background"}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@ -330,7 +330,10 @@ export function TextField<TFormValues extends FieldValues>({
|
|||||||
{label && (
|
{label && (
|
||||||
<div className='mb-1 flex justify-between gap-2'>
|
<div className='mb-1 flex justify-between gap-2'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<FormLabel htmlFor={name} className='m-0'>
|
<FormLabel
|
||||||
|
htmlFor={name}
|
||||||
|
className={cn("m-0", disabled ? "text-muted-foreground" : "")}
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
{required && (
|
{required && (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user