Clientes -> arreglos varios: imports, formateo, quitar AG Grid
This commit is contained in:
parent
7380b425ea
commit
d062c4b5fe
@ -39,7 +39,6 @@
|
||||
"@repo/rdx-utils": "workspace:*",
|
||||
"@repo/shadcn-ui": "workspace:*",
|
||||
"@tanstack/react-query": "^5.75.4",
|
||||
"ag-grid-community": "^33.3.0",
|
||||
"axios": "^1.9.0",
|
||||
"express": "^4.18.2",
|
||||
"http-status": "^2.1.0",
|
||||
@ -48,7 +47,6 @@
|
||||
"react-hook-form": "^7.58.1",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"sequelize": "^6.37.5",
|
||||
"zod": "^4.1.11"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
export * from "./json-tax-catalog.provider";
|
||||
export * from "./spain-tax-catalog.provider";
|
||||
export * from "./tax-catalog-types";
|
||||
export * from "./tax-catalog.provider";
|
||||
|
||||
export * from "./tax-catalog-types";
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
// --- Adaptador que carga el catálogo JSON en memoria e indexa por code ---
|
||||
|
||||
import { Maybe } from "@repo/rdx-utils";
|
||||
import { TaxCatalogType, TaxItemType, TaxLookupItems } from "./tax-catalog-types";
|
||||
import { TaxCatalogProvider } from "./tax-catalog.provider";
|
||||
import { TaxCatalogType, TaxItemType, TaxLookupItems } from "./tax-catalog-types";
|
||||
|
||||
export class JsonTaxCatalogProvider implements TaxCatalogProvider {
|
||||
// Índice por código normalizado
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { useDebounce } from '@repo/rdx-ui/components';
|
||||
import { useDebounce } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { Spinner } from "@repo/shadcn-ui/components/spinner";
|
||||
import { SearchIcon, XIcon } from "lucide-react";
|
||||
@ -11,153 +11,140 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type SimpleSearchInputProps = {
|
||||
onSearchChange: (value: string) => void;
|
||||
loading?: boolean;
|
||||
maxHistory?: number;
|
||||
onSearchChange: (value: string) => void;
|
||||
loading?: boolean;
|
||||
maxHistory?: number;
|
||||
};
|
||||
|
||||
const SEARCH_HISTORY_KEY = "search_history";
|
||||
|
||||
export const SimpleSearchInput = ({
|
||||
onSearchChange,
|
||||
loading = false,
|
||||
maxHistory = 8,
|
||||
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 { 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);
|
||||
const debouncedValue = useDebounce(searchValue, 300);
|
||||
|
||||
// Load from localStorage on mount
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem(SEARCH_HISTORY_KEY);
|
||||
if (stored) setHistory(JSON.parse(stored));
|
||||
}, []);
|
||||
// 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]);
|
||||
// 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));
|
||||
};
|
||||
// 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);
|
||||
};
|
||||
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);
|
||||
};
|
||||
// Input handlers
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const cleaned = e.target.value.trimStart().replace(/\s+/g, " ");
|
||||
setSearchValue(cleaned);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setSearchValue("");
|
||||
onSearchChange("");
|
||||
};
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
};
|
||||
// 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);
|
||||
};
|
||||
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")}
|
||||
>
|
||||
return (
|
||||
<div className='relative flex-1 max-w-xl'>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
);
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,6 +1,2 @@
|
||||
import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
|
||||
|
||||
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||
|
||||
export * from "./form";
|
||||
export * from "./page-header";
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { Button } from '@repo/shadcn-ui/components';
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
import { ChevronLeftIcon } from 'lucide-react';
|
||||
// features/common/components/page-header.tsx
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { ChevronLeftIcon } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
|
||||
interface PageHeaderProps {
|
||||
backIcon?: ReactNode;
|
||||
title: ReactNode;
|
||||
@ -15,8 +13,13 @@ interface PageHeaderProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
||||
export function PageHeader({ backIcon, title, description, rightSlot, className }: PageHeaderProps) {
|
||||
export function PageHeader({
|
||||
backIcon,
|
||||
title,
|
||||
description,
|
||||
rightSlot,
|
||||
className,
|
||||
}: PageHeaderProps) {
|
||||
return (
|
||||
<div className={cn("pt-6 pb-6 lg:flex lg:items-center lg:justify-between", className)}>
|
||||
{/* Lado izquierdo */}
|
||||
@ -34,16 +37,16 @@ export function PageHeader({ backIcon, title, description, rightSlot, className
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h2 className='h-8 text-xl font-semibold text-foreground sm:truncate sm:tracking-tight'>{title}</h2>
|
||||
<h2 className='h-8 text-xl font-semibold text-foreground sm:truncate sm:tracking-tight'>
|
||||
{title}
|
||||
</h2>
|
||||
{description && <p className='text-sm text-muted-foreground'>{description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lado derecho parametrizable */}
|
||||
<div className="mt-4 flex lg:mt-0 lg:ml-4">
|
||||
{rightSlot}
|
||||
</div>
|
||||
<div className='mt-4 flex lg:mt-0 lg:ml-4'>{rightSlot}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ const MODULE_VERSION = "1.0.0";
|
||||
export const CoreModuleManifiest: IModuleClient = {
|
||||
name: MODULE_NAME,
|
||||
version: MODULE_VERSION,
|
||||
dependencies: ["core"],
|
||||
dependencies: [],
|
||||
protected: true,
|
||||
layout: "app",
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user