Clientes -> arreglos varios: imports, formateo, quitar AG Grid

This commit is contained in:
David Arranz 2025-11-04 17:01:13 +01:00
parent 7380b425ea
commit d062c4b5fe
7 changed files with 139 additions and 156 deletions

View File

@ -39,7 +39,6 @@
"@repo/rdx-utils": "workspace:*", "@repo/rdx-utils": "workspace:*",
"@repo/shadcn-ui": "workspace:*", "@repo/shadcn-ui": "workspace:*",
"@tanstack/react-query": "^5.75.4", "@tanstack/react-query": "^5.75.4",
"ag-grid-community": "^33.3.0",
"axios": "^1.9.0", "axios": "^1.9.0",
"express": "^4.18.2", "express": "^4.18.2",
"http-status": "^2.1.0", "http-status": "^2.1.0",
@ -48,7 +47,6 @@
"react-hook-form": "^7.58.1", "react-hook-form": "^7.58.1",
"react-i18next": "^15.5.1", "react-i18next": "^15.5.1",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.0",
"sequelize": "^6.37.5",
"zod": "^4.1.11" "zod": "^4.1.11"
} }
} }

View File

@ -1,5 +1,4 @@
export * from "./json-tax-catalog.provider"; export * from "./json-tax-catalog.provider";
export * from "./spain-tax-catalog.provider"; export * from "./spain-tax-catalog.provider";
export * from "./tax-catalog-types";
export * from "./tax-catalog.provider"; export * from "./tax-catalog.provider";
export * from "./tax-catalog-types";

View File

@ -1,8 +1,8 @@
// --- Adaptador que carga el catálogo JSON en memoria e indexa por code --- // --- Adaptador que carga el catálogo JSON en memoria e indexa por code ---
import { Maybe } from "@repo/rdx-utils"; import { Maybe } from "@repo/rdx-utils";
import { TaxCatalogType, TaxItemType, TaxLookupItems } from "./tax-catalog-types";
import { TaxCatalogProvider } from "./tax-catalog.provider"; import { TaxCatalogProvider } from "./tax-catalog.provider";
import { TaxCatalogType, TaxItemType, TaxLookupItems } from "./tax-catalog-types";
export class JsonTaxCatalogProvider implements TaxCatalogProvider { export class JsonTaxCatalogProvider implements TaxCatalogProvider {
// Índice por código normalizado // Índice por código normalizado

View File

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

@ -1,6 +1,2 @@
import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
ModuleRegistry.registerModules([AllCommunityModule]);
export * from "./form"; export * from "./form";
export * from "./page-header"; export * from "./page-header";

View File

@ -1,10 +1,8 @@
import { Button } from '@repo/shadcn-ui/components'; import { Button } from "@repo/shadcn-ui/components";
import { cn } from '@repo/shadcn-ui/lib/utils'; import { cn } from "@repo/shadcn-ui/lib/utils";
import { ChevronLeftIcon } from 'lucide-react'; import { ChevronLeftIcon } from "lucide-react";
// features/common/components/page-header.tsx
import type { ReactNode } from "react"; import type { ReactNode } from "react";
interface PageHeaderProps { interface PageHeaderProps {
backIcon?: ReactNode; backIcon?: ReactNode;
title: ReactNode; title: ReactNode;
@ -15,8 +13,13 @@ interface PageHeaderProps {
className?: string; className?: string;
} }
export function PageHeader({
export function PageHeader({ backIcon, title, description, rightSlot, className }: PageHeaderProps) { backIcon,
title,
description,
rightSlot,
className,
}: PageHeaderProps) {
return ( return (
<div className={cn("pt-6 pb-6 lg:flex lg:items-center lg:justify-between", className)}> <div className={cn("pt-6 pb-6 lg:flex lg:items-center lg:justify-between", className)}>
{/* Lado izquierdo */} {/* Lado izquierdo */}
@ -34,16 +37,16 @@ export function PageHeader({ backIcon, title, description, rightSlot, className
)} )}
<div> <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>} {description && <p className='text-sm text-muted-foreground'>{description}</p>}
</div> </div>
</div> </div>
</div> </div>
{/* Lado derecho parametrizable */} {/* Lado derecho parametrizable */}
<div className="mt-4 flex lg:mt-0 lg:ml-4"> <div className='mt-4 flex lg:mt-0 lg:ml-4'>{rightSlot}</div>
{rightSlot}
</div>
</div> </div>
); );
} }

View File

@ -6,7 +6,7 @@ const MODULE_VERSION = "1.0.0";
export const CoreModuleManifiest: IModuleClient = { export const CoreModuleManifiest: IModuleClient = {
name: MODULE_NAME, name: MODULE_NAME,
version: MODULE_VERSION, version: MODULE_VERSION,
dependencies: ["core"], dependencies: [],
protected: true, protected: true,
layout: "app", layout: "app",