Clientes y Facturas de cliente
This commit is contained in:
parent
50a19381ce
commit
8efd73abb4
@ -1 +1,2 @@
|
||||
export * from "./form-debug.tsx";
|
||||
export * from "./simple-search-input.tsx";
|
||||
|
||||
163
modules/core/src/web/components/form/simple-search-input.tsx
Normal file
163
modules/core/src/web/components/form/simple-search-input.tsx
Normal 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>
|
||||
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user