feat(F-158): completed feature

This commit is contained in:
chattie
2026-08-22 17:50:55 +02:00
parent 7159baf851
commit 3e1a447e43
16 changed files with 348 additions and 77 deletions

View File

@@ -30,26 +30,65 @@ interface Config {
paymentMethods: { id: string; code: string; label: string; kind: string }[];
}
interface Customer {
id: string;
email: string;
firstName?: string;
lastName?: string;
}
export default function RegisterPage() {
const [config, setConfig] = useState<Config | null>(null);
const [configError, setConfigError] = useState('');
const [needsBinding, setNeedsBinding] = useState(false);
const [bindingCode, setBindingCode] = useState('');
const [binding, setBinding] = useState(false);
const [cart, setCart] = useState<CartItem[]>([]);
const [search, setSearch] = useState('');
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
const [searching, setSearching] = useState(false);
const [selectedItem, setSelectedItem] = useState<CartItem | null>(null);
const [showDiscountPanel, setShowDiscountPanel] = useState(false);
const [customer, setCustomer] = useState<{ id: string; email: string; firstName?: string; lastName?: string } | null>(null);
const [customer, setCustomer] = useState<Customer | null>(null);
const [showCustomerSearch, setShowCustomerSearch] = useState(false);
const [customerQuery, setCustomerQuery] = useState('');
const [customerResults, setCustomerResults] = useState<typeof customer[]>([]);
const [customerResults, setCustomerResults] = useState<Customer[]>([]);
const [processing, setProcessing] = useState(false);
const [lastSale, setLastSale] = useState<{ orderId: string; totalCents: number } | null>(null);
const [error, setError] = useState('');
useEffect(() => {
posApi.config().then(setConfig).catch(() => setConfig(null));
const loadConfig = useCallback(async () => {
setConfigError('');
try {
setConfig(await posApi.config<Config>());
setNeedsBinding(false);
} catch (err: unknown) {
const apiError = err as { code?: string; message?: string };
setConfig(null);
setNeedsBinding(apiError.code === 'MISSING_TERMINAL_ID');
setConfigError(apiError.message ?? 'No se pudo cargar la configuración del TPV');
}
}, []);
useEffect(() => {
void loadConfig();
}, [loadConfig]);
const bindTerminal = async (event: React.FormEvent) => {
event.preventDefault();
setBinding(true);
setConfigError('');
try {
await posApi.bind(bindingCode.trim().toUpperCase());
setBindingCode('');
await loadConfig();
} catch (err: unknown) {
setConfigError(err instanceof Error ? err.message : 'No se pudo vincular el terminal');
} finally {
setBinding(false);
}
};
const doSearch = useCallback(async (q: string) => {
if (q.trim().length < 2) { setSearchResults([]); return; }
setSearching(true);
@@ -104,7 +143,7 @@ export default function RegisterPage() {
try {
const res = await fetch(`/api/pos/customers/search?q=${encodeURIComponent(q)}`, { credentials: 'include' });
if (res.ok) {
const data = await res.json() as { items: typeof customer[] };
const data = await res.json() as { items: Customer[] };
setCustomerResults(data.items ?? []);
}
} catch { setCustomerResults([]); }
@@ -138,15 +177,56 @@ export default function RegisterPage() {
setCart([]);
setCustomer(null);
setTimeout(() => setLastSale(null), 5000);
} catch (err: { message?: string }) {
setError((err as { message?: string }).message ?? 'Error');
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Error');
} finally {
setProcessing(false);
}
};
if (!config && needsBinding) {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-100 p-6">
<form onSubmit={bindTerminal} className="w-full max-w-sm space-y-4 rounded-2xl bg-white p-8 shadow-lg">
<div>
<h1 className="text-2xl font-bold text-gray-900">Vincular terminal</h1>
<p className="mt-1 text-sm text-gray-500">Introduce el código de 8 caracteres generado en administración.</p>
</div>
<input
value={bindingCode}
onChange={(event) => setBindingCode(event.target.value.toUpperCase())}
minLength={8}
maxLength={8}
autoComplete="off"
className="w-full rounded-xl border border-gray-300 px-4 py-3 text-center font-mono text-xl tracking-widest outline-none focus:ring-2 focus:ring-[#2D6A4F]"
placeholder="AB12CD34"
required
autoFocus
/>
{configError && <p className="text-sm text-red-600">{configError}</p>}
<button
type="submit"
disabled={binding || bindingCode.trim().length !== 8}
className="w-full rounded-xl bg-[#2D6A4F] py-2.5 font-semibold text-white transition-colors hover:bg-[#1B4332] disabled:opacity-50"
>
{binding ? 'Vinculando…' : 'Vincular TPV'}
</button>
</form>
</div>
);
}
if (!config) {
return <div className="flex items-center justify-center min-h-screen text-gray-500">Cargando TPV</div>;
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-3 text-gray-500">
<p>{configError || 'Cargando TPV…'}</p>
{configError && (
<button onClick={() => void loadConfig()} className="text-sm font-medium text-[#2D6A4F] hover:underline">
Reintentar
</button>
)}
</div>
);
}
if (config.session?.status !== 'OPEN') {