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 { FieldErrors, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { FormDebug } from "@erp/core/components";
|
import { FormDebug } from "@erp/core/components";
|
||||||
import { CustomerModalSelector } from "@erp/customers/components";
|
import { CustomerModalSelectorField } from "@erp/customers/components";
|
||||||
import { UserIcon } from "lucide-react";
|
import { UserIcon } from "lucide-react";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { CustomerInvoiceFormData } from "../../schemas";
|
import { CustomerInvoiceFormData } from "../../schemas";
|
||||||
@ -42,7 +42,7 @@ export const CustomerInvoiceEditForm = ({
|
|||||||
</Legend>
|
</Legend>
|
||||||
<Description>{t("form_groups.customer.description")}</Description>
|
<Description>{t("form_groups.customer.description")}</Description>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<CustomerModalSelector />
|
<CustomerModalSelectorField control={form.control} name='customer_id' />
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { CustomerInvoiceFormData } from "../../schemas";
|
|||||||
import { BlocksView, TableView } from "./items";
|
import { BlocksView, TableView } from "./items";
|
||||||
|
|
||||||
export const InvoiceItems = () => {
|
export const InvoiceItems = () => {
|
||||||
const [viewMode, setViewMode] = useState<"blocks" | "table">("blocks");
|
const [viewMode, setViewMode] = useState<"blocks" | "table">("table");
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { control } = useFormContext<CustomerInvoiceFormData>();
|
const { control } = useFormContext<CustomerInvoiceFormData>();
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ export const InvoiceItems = () => {
|
|||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<div className='flex items-center border rounded-lg p-1'>
|
<div className='flex items-center border rounded-lg p-1'>
|
||||||
<Button
|
<Button
|
||||||
variant={viewMode === "blocks" ? "default" : "ghost"}
|
variant={viewMode === "blocks" ? "secondary" : "ghost"}
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={() => setViewMode("blocks")}
|
onClick={() => setViewMode("blocks")}
|
||||||
className='h-8 px-3'
|
className='h-8 px-3'
|
||||||
@ -95,7 +95,7 @@ export const InvoiceItems = () => {
|
|||||||
Bloques
|
Bloques
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={viewMode === "table" ? "default" : "ghost"}
|
variant={viewMode === "table" ? "secondary" : "ghost"}
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={() => setViewMode("table")}
|
onClick={() => setViewMode("table")}
|
||||||
className='h-8 px-3'
|
className='h-8 px-3'
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export const CustomerInvoiceFormSchema = z.object({
|
|||||||
|
|
||||||
customer_id: z.string().optional(),
|
customer_id: z.string().optional(),
|
||||||
|
|
||||||
|
description: z.string().optional(),
|
||||||
notes: z.string().optional(),
|
notes: z.string().optional(),
|
||||||
|
|
||||||
language_code: z
|
language_code: z
|
||||||
@ -77,6 +78,7 @@ export const defaultCustomerInvoiceFormData: CustomerInvoiceFormData = {
|
|||||||
invoice_date: "",
|
invoice_date: "",
|
||||||
operation_date: "",
|
operation_date: "",
|
||||||
|
|
||||||
|
description: "",
|
||||||
notes: "",
|
notes: "",
|
||||||
|
|
||||||
language_code: "es",
|
language_code: "es",
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import { ArrayElement } from "@repo/rdx-utils";
|
import { Building2Icon, FileTextIcon, Mail, Phone, User } from "lucide-react";
|
||||||
import { Building2Icon, FileTextIcon, Mail, Phone, Search, User } from "lucide-react";
|
import { CustomerSummary } from "../../schemas";
|
||||||
import { CustomersListData } from "../../schemas";
|
|
||||||
|
|
||||||
interface CustomerCardProps {
|
interface CustomerCardProps {
|
||||||
customer: ArrayElement<CustomersListData["items"]>;
|
customer: CustomerSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CustomerCard = ({ customer }: CustomerCardProps) => {
|
export const CustomerCard = ({ customer }: CustomerCardProps) => {
|
||||||
@ -13,15 +12,15 @@ export const CustomerCard = ({ customer }: CustomerCardProps) => {
|
|||||||
<User className='h-6 w-6 text-primary' />
|
<User className='h-6 w-6 text-primary' />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex-1 space-y-2'>
|
<div className='flex-1 space-y-2'>
|
||||||
<div className='flex items-center justify-between'>
|
<p className='text-left font-semibold text-foreground text-base'>{customer.name}</p>
|
||||||
<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='space-y-1 text-sm text-muted-foreground'>
|
||||||
<div className='flex items-center gap-2'>
|
{customer.email_primary && (
|
||||||
<Mail className='h-3 w-3' />
|
<div className='flex items-center gap-2'>
|
||||||
{customer.email_primary}
|
<Mail className='h-3 w-3' />
|
||||||
</div>
|
{customer.email_primary}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{customer.mobile_primary && (
|
{customer.mobile_primary && (
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Phone className='h-3 w-3' />
|
<Phone className='h-3 w-3' />
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { SearchIcon, UserPlusIcon } from "lucide-react";
|
import { UserSearchIcon } from "lucide-react";
|
||||||
|
|
||||||
export const CustomerEmptyCard = () => {
|
export const CustomerEmptyCard = () => {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-4 group-hover:text-primary'>
|
<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'>
|
<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>
|
||||||
<div className='flex-1 text-left'>
|
<div className='flex-1 text-left'>
|
||||||
<h3 className='font-medium text-muted-foreground mb-1 group-hover:text-primary'>
|
<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
|
Haz clic para buscar un cliente existente o crear uno nuevo
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<SearchIcon className='size-5 text-muted-foreground group-hover:text-primary' />
|
|
||||||
</div>
|
</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 { useEffect, useMemo, useState } from "react";
|
||||||
import { useCustomersSearchQuery } from "../../hooks";
|
import { useCustomersSearchQuery } from "../../hooks";
|
||||||
import { CustomerSummary } from "../../schemas";
|
import { CustomerSummary, defaultCustomerFormData } from "../../schemas";
|
||||||
import { CustomerCard } from "./customer-card";
|
import { CustomerCard } from "./customer-card";
|
||||||
import { CustomerEmptyCard } from "./customer-empty-card";
|
import { CustomerEmptyCard } from "./customer-empty-card";
|
||||||
import { CreateCustomerFormDialog } from "./customer-form-dialog";
|
import { CreateCustomerFormDialog } from "./customer-form-dialog";
|
||||||
@ -30,7 +30,8 @@ export const CustomerModalSelector = ({ value, onValueChange }: CustomerModalSel
|
|||||||
|
|
||||||
// Cliente seleccionado y creación local optimista
|
// Cliente seleccionado y creación local optimista
|
||||||
const [selected, setSelected] = useState<CustomerSummary | null>(null);
|
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 [localCreated, setLocalCreated] = useState<CustomerSummary[]>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -61,19 +62,17 @@ export const CustomerModalSelector = ({ value, onValueChange }: CustomerModalSel
|
|||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
if (!newClient.name || !newClient.email) return;
|
if (!newClient.name || !newClient.email) return;
|
||||||
const client: CustomerSummary = {
|
const newCustomer: CustomerSummary = {
|
||||||
id: crypto.randomUUID?.() ?? Date.now().toString(),
|
id: crypto.randomUUID?.() ?? Date.now().toString(),
|
||||||
...newClient,
|
...newClient,
|
||||||
};
|
};
|
||||||
setLocalCreated((prev) => [client, ...prev]);
|
setLocalCreated((prev) => [newCustomer, ...prev]);
|
||||||
setSelected(client);
|
setSelected(newCustomer);
|
||||||
onValueChange?.(client.id);
|
onValueChange?.(newCustomer.id);
|
||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
setShowSearch(false);
|
setShowSearch(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(customers);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -10,11 +10,20 @@ import {
|
|||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
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";
|
import { CustomerSummary } from "../../schemas";
|
||||||
|
|
||||||
interface CustomerSearchDialogProps {
|
interface CustomerSearchDialogProps {
|
||||||
@ -40,7 +49,12 @@ export const CustomerSearchDialog = ({
|
|||||||
selectedClient,
|
selectedClient,
|
||||||
onSelectClient,
|
onSelectClient,
|
||||||
onCreateClient,
|
onCreateClient,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
errorMessage,
|
||||||
}: CustomerSearchDialogProps) => {
|
}: CustomerSearchDialogProps) => {
|
||||||
|
const isEmpty = !isLoading && !isError && customers && customers.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className='sm:max-w-2xl bg-card border-border p-0'>
|
<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>
|
<DialogDescription>Busca un cliente existente o crea uno nuevo.</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className='px-6 pb-6'>
|
<div className='px-6 pb-3'>
|
||||||
<Command className='border rounded-lg'>
|
<Command className='border rounded-lg' shouldFilter={false}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder='Buscar cliente...'
|
placeholder='Buscar cliente...'
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onValueChange={onSearchQueryChange}
|
onValueChange={onSearchQueryChange}
|
||||||
/>
|
/>
|
||||||
<CommandList className='max-h-[400px]'>
|
|
||||||
|
<CommandList className='max-h-[600px]'>
|
||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
<div className='flex flex-col items-center gap-2 py-6 text-sm'>
|
<div className='flex flex-col items-center gap-2 py-6 text-sm'>
|
||||||
<User className='h-8 w-8 text-muted-foreground/50' />
|
<User className='size-8 text-muted-foreground/50' />
|
||||||
<p>No se encontraron clientes</p>
|
{isLoading && <p>Cargando…</p>}
|
||||||
{searchQuery && (
|
{isError && <p className='text-destructive'>{errorMessage}</p>}
|
||||||
<Button variant='outline' size='sm' onClick={() => onCreateClient(searchQuery)}>
|
{!isLoading && !isError && (
|
||||||
<Plus className='mr-2 size-4' />
|
<>
|
||||||
Crear cliente "{searchQuery}"
|
<p>No se encontraron clientes</p>
|
||||||
</Button>
|
{searchQuery && (
|
||||||
|
<Button
|
||||||
|
onClick={() => onCreateClient(searchQuery)}
|
||||||
|
className='cursor-pointer'
|
||||||
|
>
|
||||||
|
<UserPlusIcon className='mr-2 size-4' />
|
||||||
|
Crear cliente "{searchQuery}"
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
|
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{customers.map((customer) => (
|
{customers.map((customer) => {
|
||||||
<CommandItem
|
return (
|
||||||
key={customer.id}
|
<CommandItem
|
||||||
value={customer.id}
|
key={customer.id}
|
||||||
onSelect={() => onSelectClient(customer)}
|
value={customer.id}
|
||||||
className='flex items-center gap-x-4 py-5 cursor-pointer'
|
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 className='flex size-12 items-center justify-center rounded-full bg-primary/10'>
|
||||||
</div>
|
<UserIcon className='size-8 stroke-1 text-primary' />
|
||||||
<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>
|
||||||
<div className='flex items-center gap-4 text-sm font-medium text-muted-foreground'>
|
<div className='flex-1 space-y-1 min-w-0'>
|
||||||
{customer.tin && (
|
<div className='flex items-center gap-2'>
|
||||||
<span className='flex items-center gap-1'>
|
<span className='text-sm font-semibold'>{customer.name}</span>
|
||||||
<AsteriskIcon className='size-4' /> {customer.tin}
|
{customer.trade_name && (
|
||||||
</span>
|
<Badge variant='secondary' className='text-sm'>
|
||||||
)}
|
{customer.trade_name}
|
||||||
{customer.email_primary && (
|
</Badge>
|
||||||
<span className='flex items-center gap-1'>
|
)}
|
||||||
<MailIcon className='size-4' /> {customer.email_primary}
|
</div>
|
||||||
</span>
|
<div className='flex items-center gap-6 text-sm font-medium text-muted-foreground'>
|
||||||
)}
|
{customer.tin && (
|
||||||
{customer.mobile_primary && (
|
<span className='flex items-center gap-0'>
|
||||||
<span className='flex items-center gap-1'>
|
<AsteriskIcon className='size-4' /> {customer.tin}
|
||||||
<SmartphoneIcon className='size-4' /> {customer.mobile_primary}
|
</span>
|
||||||
</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>
|
||||||
</div>
|
<Check
|
||||||
<Check
|
className={cn(
|
||||||
className={cn(
|
"ml-auto size-4",
|
||||||
"ml-auto size-4",
|
selectedClient?.id === customer.id ? "opacity-100" : "opacity-0"
|
||||||
selectedClient?.id === customer.id ? "opacity-100" : "opacity-0"
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
</CommandItem>
|
||||||
</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>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
</div>
|
</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>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export * from "./customer-modal-selector";
|
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;
|
export type CustomersPage = ListCustomersResponseDTO;
|
||||||
|
|
||||||
// Ítem simplificado dentro del listado (no toda la entidad)
|
// Í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