Compare commits
2 Commits
d90eaccde0
...
d8f7c70e7e
| Author | SHA1 | Date | |
|---|---|---|---|
| d8f7c70e7e | |||
| 5acf018a22 |
@ -2,7 +2,7 @@ import { Description, FieldGroup, Fieldset, Legend } from "@repo/rdx-ui/componen
|
||||
import { FieldErrors, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormDebug } from "@erp/core/components";
|
||||
import { CustomerModalSelector } from "@erp/customers/components";
|
||||
import { CustomerModalSelectorField } from "@erp/customers/components";
|
||||
import { UserIcon } from "lucide-react";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { CustomerInvoiceFormData } from "../../schemas";
|
||||
@ -42,7 +42,7 @@ export const CustomerInvoiceEditForm = ({
|
||||
</Legend>
|
||||
<Description>{t("form_groups.customer.description")}</Description>
|
||||
<FieldGroup>
|
||||
<CustomerModalSelector />
|
||||
<CustomerModalSelectorField control={form.control} name='customer_id' />
|
||||
</FieldGroup>
|
||||
</Fieldset>
|
||||
</div>
|
||||
|
||||
@ -8,7 +8,7 @@ import { CustomerInvoiceFormData } from "../../schemas";
|
||||
import { BlocksView, TableView } from "./items";
|
||||
|
||||
export const InvoiceItems = () => {
|
||||
const [viewMode, setViewMode] = useState<"blocks" | "table">("blocks");
|
||||
const [viewMode, setViewMode] = useState<"blocks" | "table">("table");
|
||||
const { t } = useTranslation();
|
||||
const { control } = useFormContext<CustomerInvoiceFormData>();
|
||||
|
||||
@ -86,7 +86,7 @@ export const InvoiceItems = () => {
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='flex items-center border rounded-lg p-1'>
|
||||
<Button
|
||||
variant={viewMode === "blocks" ? "default" : "ghost"}
|
||||
variant={viewMode === "blocks" ? "secondary" : "ghost"}
|
||||
size='sm'
|
||||
onClick={() => setViewMode("blocks")}
|
||||
className='h-8 px-3'
|
||||
@ -95,7 +95,7 @@ export const InvoiceItems = () => {
|
||||
Bloques
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === "table" ? "default" : "ghost"}
|
||||
variant={viewMode === "table" ? "secondary" : "ghost"}
|
||||
size='sm'
|
||||
onClick={() => setViewMode("table")}
|
||||
className='h-8 px-3'
|
||||
|
||||
@ -11,6 +11,7 @@ export const CustomerInvoiceFormSchema = z.object({
|
||||
|
||||
customer_id: z.string().optional(),
|
||||
|
||||
description: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
|
||||
language_code: z
|
||||
@ -77,6 +78,7 @@ export const defaultCustomerInvoiceFormData: CustomerInvoiceFormData = {
|
||||
invoice_date: "",
|
||||
operation_date: "",
|
||||
|
||||
description: "",
|
||||
notes: "",
|
||||
|
||||
language_code: "es",
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { ArrayElement } from "@repo/rdx-utils";
|
||||
import { Building2Icon, FileTextIcon, Mail, Phone, Search, User } from "lucide-react";
|
||||
import { CustomersListData } from "../../schemas";
|
||||
import { Building2Icon, FileTextIcon, Mail, Phone, User } from "lucide-react";
|
||||
import { CustomerSummary } from "../../schemas";
|
||||
|
||||
interface CustomerCardProps {
|
||||
customer: ArrayElement<CustomersListData["items"]>;
|
||||
customer: CustomerSummary;
|
||||
}
|
||||
|
||||
export const CustomerCard = ({ customer }: CustomerCardProps) => {
|
||||
@ -13,15 +12,15 @@ export const CustomerCard = ({ customer }: CustomerCardProps) => {
|
||||
<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>
|
||||
<p className='text-left font-semibold text-foreground text-base'>{customer.name}</p>
|
||||
<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.email_primary && (
|
||||
<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' />
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { SearchIcon, UserPlusIcon } from "lucide-react";
|
||||
import { UserSearchIcon } 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' />
|
||||
<UserSearchIcon 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'>
|
||||
@ -14,7 +14,6 @@ export const CustomerEmptyCard = () => {
|
||||
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,40 @@
|
||||
import { FormField, FormItem } from "@repo/shadcn-ui/components";
|
||||
|
||||
import { Control, FieldPath, FieldValues } from "react-hook-form";
|
||||
import { CustomerModalSelector } from "./customer-modal-selector";
|
||||
|
||||
type CustomerModalSelectorFieldProps<TFormValues extends FieldValues> = {
|
||||
control: Control<TFormValues>;
|
||||
name: FieldPath<TFormValues>;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
readOnly?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function CustomerModalSelectorField<TFormValues extends FieldValues>({
|
||||
control,
|
||||
name,
|
||||
disabled = false,
|
||||
required = false,
|
||||
readOnly = false,
|
||||
className,
|
||||
}: CustomerModalSelectorFieldProps<TFormValues>) {
|
||||
const isDisabled = disabled;
|
||||
const isReadOnly = readOnly && !disabled;
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field }) => {
|
||||
console.log(field);
|
||||
return (
|
||||
<FormItem className={className}>
|
||||
<CustomerModalSelector />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useCustomersSearchQuery } from "../../hooks";
|
||||
import { CustomerSummary } from "../../schemas";
|
||||
import { CustomerSummary, defaultCustomerFormData } from "../../schemas";
|
||||
import { CustomerCard } from "./customer-card";
|
||||
import { CustomerEmptyCard } from "./customer-empty-card";
|
||||
import { CreateCustomerFormDialog } from "./customer-form-dialog";
|
||||
@ -30,7 +30,8 @@ export const CustomerModalSelector = ({ value, onValueChange }: CustomerModalSel
|
||||
|
||||
// Cliente seleccionado y creación local optimista
|
||||
const [selected, setSelected] = useState<CustomerSummary | null>(null);
|
||||
const [newClient, setNewClient] = useState<Omit<CustomerSummary, "id">>({ name: "", email: "" });
|
||||
const [newClient, setNewClient] =
|
||||
useState<Omit<CustomerSummary, "id" | "status" | "company_id">>(defaultCustomerFormData);
|
||||
const [localCreated, setLocalCreated] = useState<CustomerSummary[]>([]);
|
||||
|
||||
const {
|
||||
@ -61,19 +62,17 @@ export const CustomerModalSelector = ({ value, onValueChange }: CustomerModalSel
|
||||
|
||||
const handleCreate = () => {
|
||||
if (!newClient.name || !newClient.email) return;
|
||||
const client: CustomerSummary = {
|
||||
const newCustomer: CustomerSummary = {
|
||||
id: crypto.randomUUID?.() ?? Date.now().toString(),
|
||||
...newClient,
|
||||
};
|
||||
setLocalCreated((prev) => [client, ...prev]);
|
||||
setSelected(client);
|
||||
onValueChange?.(client.id);
|
||||
setLocalCreated((prev) => [newCustomer, ...prev]);
|
||||
setSelected(newCustomer);
|
||||
onValueChange?.(newCustomer.id);
|
||||
setShowForm(false);
|
||||
setShowSearch(false);
|
||||
};
|
||||
|
||||
console.log(customers);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
|
||||
@ -10,11 +10,20 @@ import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
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 {
|
||||
AsteriskIcon,
|
||||
Check,
|
||||
MailIcon,
|
||||
SmartphoneIcon,
|
||||
User,
|
||||
UserIcon,
|
||||
UserPlusIcon,
|
||||
} from "lucide-react";
|
||||
import { CustomerSummary } from "../../schemas";
|
||||
|
||||
interface CustomerSearchDialogProps {
|
||||
@ -40,7 +49,12 @@ export const CustomerSearchDialog = ({
|
||||
selectedClient,
|
||||
onSelectClient,
|
||||
onCreateClient,
|
||||
isLoading,
|
||||
isError,
|
||||
errorMessage,
|
||||
}: CustomerSearchDialogProps) => {
|
||||
const isEmpty = !isLoading && !isError && customers && customers.length === 0;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className='sm:max-w-2xl bg-card border-border p-0'>
|
||||
@ -54,87 +68,98 @@ export const CustomerSearchDialog = ({
|
||||
<DialogDescription>Busca un cliente existente o crea uno nuevo.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className='px-6 pb-6'>
|
||||
<Command className='border rounded-lg'>
|
||||
<div className='px-6 pb-3'>
|
||||
<Command className='border rounded-lg' shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder='Buscar cliente...'
|
||||
value={searchQuery}
|
||||
onValueChange={onSearchQueryChange}
|
||||
/>
|
||||
<CommandList className='max-h-[400px]'>
|
||||
|
||||
<CommandList className='max-h-[600px]'>
|
||||
<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>
|
||||
<User className='size-8 text-muted-foreground/50' />
|
||||
{isLoading && <p>Cargando…</p>}
|
||||
{isError && <p className='text-destructive'>{errorMessage}</p>}
|
||||
{!isLoading && !isError && (
|
||||
<>
|
||||
<p>No se encontraron clientes</p>
|
||||
{searchQuery && (
|
||||
<Button
|
||||
onClick={() => onCreateClient(searchQuery)}
|
||||
className='cursor-pointer'
|
||||
>
|
||||
<UserPlusIcon 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>
|
||||
)}
|
||||
{customers.map((customer) => {
|
||||
return (
|
||||
<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 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 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-6 text-sm font-medium text-muted-foreground'>
|
||||
{customer.tin && (
|
||||
<span className='flex items-center gap-0'>
|
||||
<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>
|
||||
</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>
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto size-4",
|
||||
selectedClient?.id === customer.id ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</div>
|
||||
<DialogFooter className='sm:justify-center px-6 pb-6'>
|
||||
<Button
|
||||
onClick={() => onCreateClient(searchQuery)}
|
||||
className='cursor-pointer text-center'
|
||||
>
|
||||
<UserPlusIcon className='mr-2 size-4' />
|
||||
Añadir nuevo cliente
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./customer-modal-selector";
|
||||
export * from "./customer-modal-selector-field";
|
||||
|
||||
@ -22,4 +22,4 @@ export type CustomerUpdateInput = z.infer<typeof CustomerUpdateSchema>; // Cuerp
|
||||
export type CustomersPage = ListCustomersResponseDTO;
|
||||
|
||||
// Ítem simplificado dentro del listado (no toda la entidad)
|
||||
export type CustomerSummary = ArrayElement<CustomersPage["items"]>;
|
||||
export type CustomerSummary = Omit<ArrayElement<CustomersPage["items"]>, "metadata">;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user