feat(POS-006): completed feature
This commit is contained in:
76
project/apps/pos/src/app/(auth)/login/page.tsx
Normal file
76
project/apps/pos/src/app/(auth)/login/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
7
project/apps/pos/src/app/(terminal)/layout.tsx
Normal file
7
project/apps/pos/src/app/(terminal)/layout.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { NextLayout } from 'next';
|
||||
|
||||
const TerminalLayout: NextLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default TerminalLayout;
|
||||
33
project/apps/pos/src/app/(terminal)/page.tsx
Normal file
33
project/apps/pos/src/app/(terminal)/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
32
project/apps/pos/src/app/api/[...path]/route.ts
Normal file
32
project/apps/pos/src/app/api/[...path]/route.ts
Normal 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' } });
|
||||
}
|
||||
13
project/apps/pos/src/app/globals.css
Normal file
13
project/apps/pos/src/app/globals.css
Normal 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;
|
||||
}
|
||||
15
project/apps/pos/src/app/layout.tsx
Normal file
15
project/apps/pos/src/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user