diff --git a/src/app/api/tenant/clients/route.ts b/src/app/api/tenant/clients/route.ts new file mode 100644 index 0000000..34bee47 --- /dev/null +++ b/src/app/api/tenant/clients/route.ts @@ -0,0 +1,97 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { query } from '@/lib/db'; + +export async function GET(req: NextRequest) { + const { searchParams } = new URL(req.url); + const slug = searchParams.get('slug'); + + if (!slug) { + return NextResponse.json({ error: 'Missing tenant slug' }, { status: 400 }); + } + + try { + const tenantRes = await query('SELECT id FROM tenants WHERE slug = $1', [slug]); + if (tenantRes.rows.length === 0) return NextResponse.json([]); + const tenantId = tenantRes.rows[0].id; + + const res = await query(` + SELECT id, name, email, phone, cpf, + TO_CHAR(birth_date, 'YYYY-MM-DD') as birthDate, + notes, total_visits as visits, total_spent as spent, + TO_CHAR(last_visit_at, 'YYYY-MM-DD') as "lastVisit" + FROM clients + WHERE tenant_id = $1 AND is_active = true + ORDER BY name ASC + `, [tenantId]); + + const clients = res.rows.map(row => ({ + ...row, + spent: Number(row.spent || 0), + visits: Number(row.visits || 0), + email: row.email || '', + phone: row.phone || '', + cpf: row.cpf || '', + birthDate: row.birthdate || '', + notes: row.notes || '', + lastVisit: row.lastVisit || '' + })); + + return NextResponse.json(clients); + } catch (error: any) { + console.error('GET Clients error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +export async function POST(req: NextRequest) { + try { + const { slug, name, email, phone, cpf, birthDate, notes } = await req.json(); + if (!slug || !name) return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); + + const tenantRes = await query('SELECT id FROM tenants WHERE slug = $1', [slug]); + if (tenantRes.rows.length === 0) return NextResponse.json({ error: 'Tenant not found' }, { status: 404 }); + const tenantId = tenantRes.rows[0].id; + + const res = await query( + `INSERT INTO clients (tenant_id, name, email, phone, cpf, birth_date, notes) + VALUES ($1, $2, $3, $4, $5, NULLIF($6, '')::DATE, $7) RETURNING *`, + [tenantId, name, email, phone, cpf, birthDate, notes] + ); + + return NextResponse.json(res.rows[0]); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +export async function PUT(req: NextRequest) { + try { + const { id, name, email, phone, cpf, birthDate, notes } = await req.json(); + if (!id || !name) return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); + + const res = await query( + `UPDATE clients + SET name = $1, email = $2, phone = $3, cpf = $4, birth_date = NULLIF($5, '')::DATE, notes = $6, updated_at = NOW() + WHERE id = $7 RETURNING *`, + [name, email, phone, cpf, birthDate, notes, id] + ); + + return NextResponse.json(res.rows[0]); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +export async function DELETE(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const id = searchParams.get('id'); + if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 }); + + await query('DELETE FROM clients WHERE id = $1', [id]); + + return NextResponse.json({ success: true }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/tenant/services/route.ts b/src/app/api/tenant/services/route.ts new file mode 100644 index 0000000..e802191 --- /dev/null +++ b/src/app/api/tenant/services/route.ts @@ -0,0 +1,113 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { query } from '@/lib/db'; + +export async function GET(req: NextRequest) { + const { searchParams } = new URL(req.url); + const slug = searchParams.get('slug'); + + if (!slug) { + return NextResponse.json({ error: 'Missing tenant slug' }, { status: 400 }); + } + + try { + const tenantRes = await query('SELECT id FROM tenants WHERE slug = $1', [slug]); + if (tenantRes.rows.length === 0) return NextResponse.json([]); + const tenantId = tenantRes.rows[0].id; + + const res = await query(` + SELECT s.id, s.name, s.description, s.duration_minutes as duration, s.price, s.price_type as type, + COALESCE(c.name, 'Geral') as category + FROM services s + LEFT JOIN service_categories c ON s.category_id = c.id + WHERE s.tenant_id = $1 AND s.is_active = true + ORDER BY s.sort_order ASC, s.name ASC + `, [tenantId]); + + const services = res.rows.map(row => ({ + ...row, + duration: Number(row.duration || 30), + price: Number(row.price || 0) + })); + + return NextResponse.json(services); + } catch (error: any) { + console.error('GET Services error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +export async function POST(req: NextRequest) { + try { + const { slug, name, category, price, duration, type, description } = await req.json(); + if (!slug || !name) return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); + + const tenantRes = await query('SELECT id FROM tenants WHERE slug = $1', [slug]); + if (tenantRes.rows.length === 0) return NextResponse.json({ error: 'Tenant not found' }, { status: 404 }); + const tenantId = tenantRes.rows[0].id; + + // Get or create category + let categoryId = null; + if (category) { + let catRes = await query('SELECT id FROM service_categories WHERE tenant_id = $1 AND name = $2', [tenantId, category]); + if (catRes.rows.length === 0) { + catRes = await query('INSERT INTO service_categories (tenant_id, name) VALUES ($1, $2) RETURNING id', [tenantId, category]); + } + categoryId = catRes.rows[0].id; + } + + const res = await query( + `INSERT INTO services (tenant_id, category_id, name, description, duration_minutes, price, price_type) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`, + [tenantId, categoryId, name, description, duration, price, type] + ); + + return NextResponse.json(res.rows[0]); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +export async function PUT(req: NextRequest) { + try { + const { id, slug, name, category, price, duration, type, description } = await req.json(); + if (!id || !name || !slug) return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); + + const tenantRes = await query('SELECT id FROM tenants WHERE slug = $1', [slug]); + if (tenantRes.rows.length === 0) return NextResponse.json({ error: 'Tenant not found' }, { status: 404 }); + const tenantId = tenantRes.rows[0].id; + + let categoryId = null; + if (category) { + let catRes = await query('SELECT id FROM service_categories WHERE tenant_id = $1 AND name = $2', [tenantId, category]); + if (catRes.rows.length === 0) { + catRes = await query('INSERT INTO service_categories (tenant_id, name) VALUES ($1, $2) RETURNING id', [tenantId, category]); + } + categoryId = catRes.rows[0].id; + } + + const res = await query( + `UPDATE services + SET category_id = $1, name = $2, description = $3, duration_minutes = $4, price = $5, price_type = $6, updated_at = NOW() + WHERE id = $7 RETURNING *`, + [categoryId, name, description, duration, price, type, id] + ); + + return NextResponse.json(res.rows[0]); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +export async function DELETE(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const id = searchParams.get('id'); + if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 }); + + await query('DELETE FROM services WHERE id = $1', [id]); + + return NextResponse.json({ success: true }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/components/Agenda.tsx b/src/components/Agenda.tsx index 1b0877b..155f5de 100644 --- a/src/components/Agenda.tsx +++ b/src/components/Agenda.tsx @@ -22,17 +22,26 @@ export default function Agenda() { const [currentDate, setCurrentDate] = useState(today); const [appointments, setAppointments] = useState([]); const [professionals, setProfessionals] = useState([]); + const [services, setServices] = useState([]); const [loading, setLoading] = useState(true); const fetchAgenda = async () => { try { const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium'; - const res = await fetch(`/api/tenant/agenda?slug=${slug}`); - if (res.ok) { - const data = await res.json(); + const [agendaRes, servicesRes] = await Promise.all([ + fetch(`/api/tenant/agenda?slug=${slug}`), + fetch(`/api/tenant/services?slug=${slug}`) + ]); + + if (agendaRes.ok) { + const data = await agendaRes.json(); setProfessionals(data.professionals || []); setAppointments(data.appointments || []); } + if (servicesRes.ok) { + const data = await servicesRes.json(); + setServices(data || []); + } } catch (e) { console.error(e); } finally { @@ -197,11 +206,12 @@ export default function Agenda() {
diff --git a/src/components/Clients.tsx b/src/components/Clients.tsx index d83998f..20adac9 100644 --- a/src/components/Clients.tsx +++ b/src/components/Clients.tsx @@ -5,31 +5,34 @@ interface Client { id: string; name: string; email: string; phone: string; cpf: string; birthDate: string; notes: string; visits: number; spent: number; lastVisit: string; } -const initialClients: Client[] = [ - { id: '1', name: 'João Mendes', email: 'joao@email.com', phone: '(11) 97777-1111', cpf: '123.456.789-00', birthDate: '1990-05-15', notes: 'Cliente VIP', visits: 12, spent: 780, lastVisit: '2026-05-30' }, - { id: '2', name: 'Pedro Oliveira', email: 'pedro@email.com', phone: '(11) 97777-2222', cpf: '', birthDate: '', notes: '', visits: 8, spent: 520, lastVisit: '2026-05-28' }, - { id: '3', name: 'Lucas Ferreira', email: 'lucas@email.com', phone: '(11) 97777-3333', cpf: '', birthDate: '', notes: '', visits: 5, spent: 275, lastVisit: '2026-05-25' }, - { id: '4', name: 'Mateus Costa', email: 'mateus@email.com', phone: '(11) 97777-4444', cpf: '', birthDate: '', notes: '', visits: 3, spent: 165, lastVisit: '2026-05-20' }, - { id: '5', name: 'Gabriel Souza', email: 'gabriel@email.com', phone: '(11) 97777-5555', cpf: '', birthDate: '', notes: 'Prefere corte degradê', visits: 15, spent: 1120, lastVisit: '2026-05-31' }, - { id: '6', name: 'André Lima', email: 'andre@email.com', phone: '(11) 97777-6666', cpf: '', birthDate: '', notes: '', visits: 22, spent: 1540, lastVisit: '2026-05-31' }, - { id: '7', name: 'Ricardo Alves', email: 'ricardo@email.com', phone: '(11) 97777-7777', cpf: '', birthDate: '', notes: '', visits: 6, spent: 330, lastVisit: '2026-05-27' }, -]; - const emptyClient: Client = { id: '', name: '', email: '', phone: '', cpf: '', birthDate: '', notes: '', visits: 0, spent: 0, lastVisit: '' }; export default function Clients() { - const [clients, setClients] = useState(initialClients); + const [clients, setClients] = useState([]); const [search, setSearch] = useState(''); const [showModal, setShowModal] = useState(false); const [editingClient, setEditingClient] = useState(null); const [formData, setFormData] = useState(emptyClient); const [deleteConfirm, setDeleteConfirm] = useState(null); + const [loading, setLoading] = useState(true); + + const fetchClients = async () => { + try { + const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium'; + const res = await fetch(`/api/tenant/clients?slug=${slug}`); + if (res.ok) { + const data = await res.json(); + setClients(data); + } + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; useEffect(() => { - const savedName = localStorage.getItem('agendapro_tenant_name'); - if (savedName) { - setClients([]); - } + fetchClients(); }, []); const cleanSearch = search.replace(/\D/g, ''); @@ -41,7 +44,7 @@ export default function Clients() { const openAdd = () => { setEditingClient(null); - setFormData({ ...emptyClient, id: Date.now().toString() }); + setFormData({ ...emptyClient }); setShowModal(true); }; @@ -51,26 +54,43 @@ export default function Clients() { setShowModal(true); }; - const handleSave = () => { + const handleSave = async () => { if (!formData.name.trim()) return; - if (editingClient) { - setClients(prev => prev.map(c => c.id === editingClient.id ? { ...formData } : c)); - } else { - setClients(prev => [...prev, { ...formData, lastVisit: new Date().toISOString().split('T')[0] }]); + const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium'; + const method = editingClient ? 'PUT' : 'POST'; + + try { + const res = await fetch('/api/tenant/clients', { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...formData, slug }) + }); + if (res.ok) { + fetchClients(); + setShowModal(false); + setEditingClient(null); + } + } catch (e) { + console.error(e); } - setShowModal(false); - setEditingClient(null); }; - const handleDelete = (id: string) => { - setClients(prev => prev.filter(c => c.id !== id)); - setDeleteConfirm(null); + const handleDelete = async (id: string) => { + try { + await fetch(`/api/tenant/clients?id=${id}`, { method: 'DELETE' }); + setClients(prev => prev.filter(c => c.id !== id)); + setDeleteConfirm(null); + } catch (e) { + console.error(e); + } }; const updateForm = (field: keyof Client, value: string) => { setFormData(prev => ({ ...prev, [field]: value })); }; + if (loading) return
Carregando clientes...
; + return (
@@ -112,10 +132,10 @@ export default function Clients() {
- {c.phone} + {c.phone || '-'} - {c.email} + {c.email || '-'}
diff --git a/src/components/Services.tsx b/src/components/Services.tsx index 974abfa..bb289f6 100644 --- a/src/components/Services.tsx +++ b/src/components/Services.tsx @@ -1,79 +1,100 @@ import { useState, useEffect } from 'react'; -import { Plus, X, Clock, DollarSign, Edit, Trash2, AlertTriangle } from 'lucide-react'; - -const categories = [ - { id: '1', name: 'Cabelo', icon: '✂️' }, - { id: '2', name: 'Barba', icon: '🧔' }, - { id: '3', name: 'Combos', icon: '⭐' }, - { id: '4', name: 'Manicure', icon: '💅' }, - { id: '5', name: 'Massagem', icon: '💆' }, -]; +import { Plus, Search, Clock, DollarSign, X, Edit, Trash2, AlertTriangle, Layers } from 'lucide-react'; interface Service { - id: string; catId: string; name: string; desc: string; duration: number; price: number; active: boolean; + id: string; name: string; category: string; price: number; duration: number; type: 'fixed' | 'starting' | 'variable'; description: string; } -const initialServices: Service[] = [ - { id: '1', catId: '1', name: 'Corte Masculino', desc: 'Corte moderno com lavagem e finalização', duration: 40, price: 55, active: true }, - { id: '2', catId: '1', name: 'Corte Infantil', desc: 'Corte para crianças até 12 anos', duration: 30, price: 40, active: true }, - { id: '3', catId: '2', name: 'Barba Completa', desc: 'Aparar, modelar e hidratação', duration: 30, price: 40, active: true }, - { id: '4', catId: '2', name: 'Design de Sobrancelha', desc: 'Alinhamento com navalha', duration: 15, price: 25, active: true }, - { id: '5', catId: '3', name: 'Combo Corte + Barba', desc: 'Corte masculino + barba completa', duration: 60, price: 85, active: true }, - { id: '6', catId: '4', name: 'Manicure Completa', desc: 'Lixar, cuticular e esmaltação', duration: 45, price: 50, active: true }, - { id: '7', catId: '4', name: 'Pedicure', desc: 'Tratamento completo para os pés', duration: 50, price: 55, active: true }, - { id: '8', catId: '5', name: 'Massagem Relaxante', desc: 'Massagem corpo inteiro 60min', duration: 60, price: 120, active: true }, - { id: '9', catId: '5', name: 'Quick Massage', desc: 'Massagem expressa 20min', duration: 20, price: 45, active: true }, -]; - -const emptyService: Service = { id: '', catId: '1', name: '', desc: '', duration: 30, price: 50, active: true }; +const emptyService: Service = { id: '', name: '', category: 'Cabelo', price: 0, duration: 30, type: 'fixed', description: '' }; export default function Services() { - const [services, setServices] = useState(initialServices); + const [services, setServices] = useState([]); + const [search, setSearch] = useState(''); const [showModal, setShowModal] = useState(false); const [selectedCat, setSelectedCat] = useState(null); const [editingService, setEditingService] = useState(null); const [formData, setFormData] = useState(emptyService); const [deleteConfirm, setDeleteConfirm] = useState(null); + const [loading, setLoading] = useState(true); + + const fetchServices = async () => { + try { + const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium'; + const res = await fetch(`/api/tenant/services?slug=${slug}`); + if (res.ok) { + const data = await res.json(); + setServices(data); + } + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; useEffect(() => { - const savedName = localStorage.getItem('agendapro_tenant_name'); - if (savedName) { - setServices([ - { id: '1', catId: '1', name: 'Corte de Cabelo (Exemplo)', desc: 'Corte padrão do estabelecimento', duration: 30, price: 40, active: true } - ]); - } + fetchServices(); }, []); - const filtered = selectedCat ? services.filter(s => s.catId === selectedCat) : services; + // Derive unique categories from fetched services + const categories = Array.from(new Set(services.map(s => s.category || 'Geral'))); + + const filtered = services.filter(s => { + const matchesSearch = s.name.toLowerCase().includes(search.toLowerCase()) || + (s.category && s.category.toLowerCase().includes(search.toLowerCase())); + const matchesCat = selectedCat ? s.category === selectedCat : true; + return matchesSearch && matchesCat; + }); const openAdd = () => { setEditingService(null); - setFormData({ ...emptyService, id: Date.now().toString() }); + setFormData({ ...emptyService }); setShowModal(true); }; - const openEdit = (svc: Service) => { - setEditingService(svc); - setFormData({ ...svc }); + const openEdit = (service: Service) => { + setEditingService(service); + setFormData({ ...service }); setShowModal(true); }; - const handleSave = () => { + const handleSave = async () => { if (!formData.name.trim()) return; - if (editingService) { - setServices(prev => prev.map(s => s.id === editingService.id ? { ...formData } : s)); - } else { - setServices(prev => [...prev, { ...formData }]); + const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium'; + const method = editingService ? 'PUT' : 'POST'; + + try { + const res = await fetch('/api/tenant/services', { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...formData, slug }) + }); + if (res.ok) { + fetchServices(); + setShowModal(false); + setEditingService(null); + } + } catch (e) { + console.error(e); } - setShowModal(false); - setEditingService(null); }; - const handleDelete = (id: string) => { - setServices(prev => prev.filter(s => s.id !== id)); - setDeleteConfirm(null); + const handleDelete = async (id: string) => { + try { + await fetch(`/api/tenant/services?id=${id}`, { method: 'DELETE' }); + setServices(prev => prev.filter(s => s.id !== id)); + setDeleteConfirm(null); + } catch (e) { + console.error(e); + } }; + const updateForm = (field: keyof Service, value: string | number) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + + if (loading) return
Carregando serviços...
; + return (
@@ -81,10 +102,10 @@ export default function Services() { - {categories.filter(c => services.some(s => s.catId === c.id)).map(c => ( - ))}
@@ -93,6 +114,12 @@ export default function Services() {
+
+ + setSearch(e.target.value)} /> +
+
{filtered.map(svc => (
@@ -110,7 +137,9 @@ export default function Services() {
)}
- {categories.find(c => c.id === svc.catId)?.icon} + + {svc.category || 'Geral'} +

{svc.name}

-

{svc.desc}

+

{svc.description || 'Sem descrição'}

- {svc.duration}min + {svc.duration}min
- R$ {svc.price.toFixed(2)} + {svc.type === 'starting' && A partir de} + {svc.type === 'variable' && Variável} + {svc.type !== 'variable' && `R$ ${svc.price.toFixed(2)}`}
@@ -147,33 +178,38 @@ export default function Services() {
- - -
-
- - setFormData(p => ({ ...p, name: e.target.value }))} /> -
-
- -