Clientes y Facturas de cliente
This commit is contained in:
parent
50a19381ce
commit
8efd73abb4
@ -1 +1,2 @@
|
|||||||
export * from "./form-debug.tsx";
|
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>
|
</FieldLegend>
|
||||||
|
|
||||||
<FieldDescription className='hidden'>{t("form_groups.totals.description")}</FieldDescription>
|
<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'>
|
<div className='space-y-1.5'>
|
||||||
{/* Sección: Subtotal y Descuentos */}
|
{/* Sección: Subtotal y Descuentos */}
|
||||||
@ -128,9 +128,9 @@ export const InvoiceTotals = (props: ComponentProps<"fieldset">) => {
|
|||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div className="flex justify-between text-sm mt-3">
|
<div className="flex justify-between text-sm ">
|
||||||
<span className="font-semibold text-foreground">Total de la factura</span>
|
<span className="font-bold text-foreground">Total de la factura</span>
|
||||||
<span className="font-semibold tabular-nums">
|
<span className="font-bold tabular-nums">
|
||||||
{formatCurrency(getValues('total_amount'), 2, currency_code, language_code)}
|
{formatCurrency(getValues('total_amount'), 2, currency_code, language_code)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -228,7 +228,7 @@ export function DataTable<TData, TValue>({
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
data-state={row.getIsSelected() && "selected"}
|
data-state={row.getIsSelected() && "selected"}
|
||||||
className={`group bg-background ${readOnly ? "cursor-default" : "cursor-pointer"}`}
|
className={"group bg-background cursor-pointer"}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" || e.key === " ") onRowClick?.(row.original, rowIndex, e as any);
|
if (e.key === "Enter" || e.key === " ") onRowClick?.(row.original, rowIndex, e as any);
|
||||||
}}
|
}}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user