Clientes y Facturas de cliente

This commit is contained in:
David Arranz 2025-10-22 17:47:59 +02:00
parent 50a19381ce
commit 8efd73abb4
4 changed files with 169 additions and 5 deletions

View File

@ -1 +1,2 @@
export * from "./form-debug.tsx";
export * from "./simple-search-input.tsx";

View File

@ -0,0 +1,163 @@
import { useDebounce } from '@repo/rdx-ui/components';
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput
} from "@repo/shadcn-ui/components";
import { Spinner } from "@repo/shadcn-ui/components/spinner";
import { SearchIcon, XIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
type SimpleSearchInputProps = {
onSearchChange: (value: string) => void;
loading?: boolean;
maxHistory?: number;
};
const SEARCH_HISTORY_KEY = "search_history";
export const SimpleSearchInput = ({
onSearchChange,
loading = false,
maxHistory = 8,
}: SimpleSearchInputProps) => {
const { t } = useTranslation();
const [searchValue, setSearchValue] = useState("");
const [lastSearch, setLastSearch] = useState("");
const [history, setHistory] = useState<string[]>([]);
const [open, setOpen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const debouncedValue = useDebounce(searchValue, 300);
// Load from localStorage on mount
useEffect(() => {
const stored = localStorage.getItem(SEARCH_HISTORY_KEY);
if (stored) setHistory(JSON.parse(stored));
}, []);
// Emit changes after debounce
useEffect(() => {
onSearchChange(debouncedValue);
}, [debouncedValue, onSearchChange]);
// Save history to localStorage
const saveHistory = (term: string) => {
if (!term.trim()) return;
const cleaned = term.trim();
const newHistory = [cleaned, ...history.filter((h) => h !== cleaned)].slice(
0,
maxHistory
);
setHistory(newHistory);
localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(newHistory));
};
const clearHistory = () => {
setHistory([]);
localStorage.removeItem(SEARCH_HISTORY_KEY);
};
// Input handlers
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const cleaned = e.target.value.trimStart().replace(/\s+/g, " ");
setSearchValue(cleaned);
};
const handleClear = () => {
setSearchValue("");
onSearchChange("");
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
// Enter → búsqueda inmediata
if (e.key === "Enter" && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
e.preventDefault();
onSearchChange(searchValue);
setLastSearch(searchValue);
saveHistory(searchValue);
setOpen(false);
}
// Shift+Enter → repetir última búsqueda
if (e.key === "Enter" && e.shiftKey) {
e.preventDefault();
if (lastSearch) {
onSearchChange(lastSearch);
setSearchValue(lastSearch);
}
}
// Ctrl/Cmd+Enter → limpiar
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
handleClear();
inputRef.current?.focus();
}
};
const handleSelectHistory = (term: string) => {
setSearchValue(term);
onSearchChange(term);
setLastSearch(term);
setOpen(false);
};
return (
<div
className="relative flex-1 max-w-xl"
aria-label={t("pages.list.searchPlaceholder", "Search input")}
>
<InputGroup className="bg-background" data-disabled={loading}>
<InputGroupInput
ref={inputRef}
placeholder={t("common.search_placeholder", "Search...")}
value={searchValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
inputMode="search"
autoComplete="off"
spellCheck={false}
disabled={loading}
onFocus={() => history.length > 0 && setOpen(true)}
/>
<InputGroupAddon>
<SearchIcon aria-hidden />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
{loading && (
<Spinner aria-label={t("common.loading", "Loading")} />
)}
{!searchValue && !loading && (
<InputGroupButton
variant="secondary"
className="cursor-pointer"
onClick={() => onSearchChange(searchValue)}
>
{t("common.search", "Search")}
</InputGroupButton>
)}
{searchValue && !loading && (
<InputGroupButton
variant="secondary"
className="cursor-pointer"
aria-label={t("common.clear", "Clear search")}
onClick={handleClear}
>
<XIcon className="size-4" aria-hidden />
<span className="sr-only">{t("common.clear", "Clear")}</span>
</InputGroupButton>
)}
</InputGroupAddon>
</InputGroup>
</div>
);
};

View File

@ -28,7 +28,7 @@ export const InvoiceTotals = (props: ComponentProps<"fieldset">) => {
</FieldLegend>
<FieldDescription className='hidden'>{t("form_groups.totals.description")}</FieldDescription>
<FieldGroup className='grid grid-cols-1 border rounded-lg bg-muted/10 p-6 gap-4'>
<FieldGroup className='grid grid-cols-1 border rounded-lg bg-muted/10 p-4 gap-4'>
<div className='space-y-1.5'>
{/* Sección: Subtotal y Descuentos */}
@ -128,9 +128,9 @@ export const InvoiceTotals = (props: ComponentProps<"fieldset">) => {
<Separator />
<div className="flex justify-between text-sm mt-3">
<span className="font-semibold text-foreground">Total de la factura</span>
<span className="font-semibold tabular-nums">
<div className="flex justify-between text-sm ">
<span className="font-bold text-foreground">Total de la factura</span>
<span className="font-bold tabular-nums">
{formatCurrency(getValues('total_amount'), 2, currency_code, language_code)}
</span>
</div>

View File

@ -228,7 +228,7 @@ export function DataTable<TData, TValue>({
role="button"
tabIndex={0}
data-state={row.getIsSelected() && "selected"}
className={`group bg-background ${readOnly ? "cursor-default" : "cursor-pointer"}`}
className={"group bg-background cursor-pointer"}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") onRowClick?.(row.original, rowIndex, e as any);
}}