feat(POS-006): completed feature

This commit is contained in:
chattie
2026-08-22 13:39:11 +02:00
parent 915fbe0ba9
commit 25e4e5b622
24 changed files with 448 additions and 62 deletions

View File

@@ -0,0 +1,76 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function LoginPage() {
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
credentials: 'include',
});
if (res.ok) {
router.push('/');
} else {
const data = await res.json() as { message?: string };
setError(data.message ?? 'Login failed');
}
} catch {
setError('Network error');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="bg-white rounded-2xl shadow-lg p-8 w-full max-w-sm">
<h1 className="text-2xl font-bold text-center mb-6" style={{ color: 'var(--color-primary)' }}>
Mercado de Vida
</h1>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-[#2D6A4F] outline-none"
required
autoFocus
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Contraseña</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-[#2D6A4F] outline-none"
required
/>
</div>
{error && <p className="text-sm text-red-600">{error}</p>}
<button
type="submit"
disabled={loading}
className="w-full py-2.5 bg-[#2D6A4F] hover:bg-[#1B4332] disabled:opacity-50 text-white font-semibold rounded-xl transition-colors"
>
{loading ? 'Entrando…' : 'Entrar'}
</button>
</form>
</div>
</div>
);
}

View File

@@ -0,0 +1,7 @@
import type { NextLayout } from 'next';
const TerminalLayout: NextLayout = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
export default TerminalLayout;

View File

@@ -0,0 +1,33 @@
'use client';
import { useEffect, useState } from 'react';
import { posApi } from '@/lib/api-client';
export default function TerminalPage() {
const [status, setStatus] = useState<'loading' | 'bound' | 'no-session'>('loading');
useEffect(() => {
posApi.me()
.then(() => setStatus('bound'))
.catch(() => setStatus('no-session'));
}, []);
if (status === 'loading') {
return <div className="flex items-center justify-center min-h-screen text-gray-500">Cargando TPV</div>;
}
if (status === 'no-session') {
return (
<div className="flex flex-col items-center justify-center min-h-screen p-8 text-center">
<h1 className="text-3xl font-bold mb-4" style={{ color: 'var(--color-primary)' }}>TPV sin vincular</h1>
<p className="text-gray-500 mb-6">Usa un código de vinculación desde la admin para activar este terminal.</p>
</div>
);
}
return (
<div className="flex flex-col items-center justify-center min-h-screen text-center">
<h1 className="text-4xl font-bold mb-4" style={{ color: 'var(--color-primary)' }}>Mercado de Vida</h1>
<p className="text-gray-500">TPV listo. Implementa la pantalla de venta en POS-007.</p>
</div>
);
}

View File

@@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from 'next/server';
const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3000';
export async function GET(request: NextRequest) {
const path = request.nextUrl.pathname.replace('/api/', '');
const search = request.nextUrl.search;
const cookie = request.headers.get('cookie') ?? '';
const terminalId = request.headers.get('x-terminal-id');
const headers: Record<string, string> = { Cookie: cookie };
if (terminalId) headers['x-terminal-id'] = terminalId;
const res = await fetch(`${API}/${path}${search}`, { headers, credentials: 'include' });
const body = await res.text();
return new NextResponse(body, { status: res.status, headers: { 'content-type': res.headers.get('content-type') ?? 'application/json' } });
}
export async function POST(request: NextRequest) {
const path = request.nextUrl.pathname.replace('/api/', '');
const search = request.nextUrl.search;
const cookie = request.headers.get('cookie') ?? '';
const terminalId = request.headers.get('x-terminal-id');
const body = await request.text();
const headers: Record<string, string> = { 'Content-Type': 'application/json', Cookie: cookie };
if (terminalId) headers['x-terminal-id'] = terminalId;
const res = await fetch(`${API}/${path}${search}`, { method: 'POST', headers, body, credentials: 'include' });
const resBody = await res.text();
return new NextResponse(resBody, { status: res.status, headers: { 'content-type': res.headers.get('content-type') ?? 'application/json' } });
}

View File

@@ -0,0 +1,13 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--color-primary: #2D6A4F;
--color-primary-dark: #1B4332;
--color-bg: #f9fafb;
--color-surface: #ffffff;
--color-border: #e5e7eb;
--color-text: #111827;
--color-text-muted: #6b7280;
}

View File

@@ -0,0 +1,15 @@
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'Mercado de Vida — TPV',
description: 'Terminal punto de venta',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es">
<body className="bg-gray-50 text-gray-900 antialiased">{children}</body>
</html>
);
}