diff --git a/src/app/api/tenant/agenda/route.ts b/src/app/api/tenant/agenda/route.ts new file mode 100644 index 0000000..3d0042d --- /dev/null +++ b/src/app/api/tenant/agenda/route.ts @@ -0,0 +1,84 @@ +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({ professionals: [], appointments: [] }); + const tenantId = tenantRes.rows[0].id; + + const profsRes = await query(`SELECT id, name FROM users WHERE tenant_id = $1 AND role = 'professional'`, [tenantId]); + + // In PostgreSQL, DATE columns return as Date objects or strings depending on driver. Let's cast to text. + const apptsRes = await query(` + SELECT + a.id, a.professional_id as "professionalId", + COALESCE(c.name, a.client_name, 'Cliente Avulso') as client, + COALESCE(s.name, 'Serviço Personalizado') as service, + EXTRACT(HOUR FROM a.start_time) as "startHour", + EXTRACT(MINUTE FROM a.start_time) as "startMin", + EXTRACT(EPOCH FROM (a.end_time - a.start_time))/60 as duration, + a.status, a.price + FROM appointments a + LEFT JOIN clients c ON a.client_id = c.id + LEFT JOIN services s ON a.service_id = s.id + WHERE a.tenant_id = $1 AND a.date = CURRENT_DATE + `, [tenantId]); + + const colors = ['#6C63FF', '#a855f7', '#10b981', '#f59e0b', '#ef4444']; + const professionals = profsRes.rows.map((p, i) => ({ + id: p.id, + name: p.name, + color: colors[i % colors.length] + })); + + const appointments = apptsRes.rows.map(a => ({ + ...a, + startHour: Number(a.startHour), + startMin: Number(a.startMin), + duration: Number(a.duration), + price: Number(a.price) + })); + + return NextResponse.json({ professionals, appointments }); + } catch (error: any) { + console.error('GET Agenda error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +export async function POST(req: NextRequest) { + try { + const { slug, professionalId, client, service, startHour, startMin, duration, price } = await req.json(); + if (!slug || !professionalId) 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 startTime = `${String(startHour).padStart(2, '0')}:${String(startMin).padStart(2, '0')}:00`; + + // Calculate end time + const dateObj = new Date(`1970-01-01T${startTime}Z`); + dateObj.setMinutes(dateObj.getMinutes() + Number(duration)); + const endTime = dateObj.toISOString().substr(11, 8); + + const res = await query( + `INSERT INTO appointments (tenant_id, professional_id, client_name, notes, date, start_time, end_time, price, status) + VALUES ($1, $2, $3, $4, CURRENT_DATE, $5, $6, $7, 'confirmed') RETURNING *`, + [tenantId, professionalId, client, service, startTime, endTime, price] + ); + + return NextResponse.json(res.rows[0]); + } catch (error: any) { + console.error('POST Agenda error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/tenant/team/route.ts b/src/app/api/tenant/team/route.ts new file mode 100644 index 0000000..a70e0a8 --- /dev/null +++ b/src/app/api/tenant/team/route.ts @@ -0,0 +1,107 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { query } from '@/lib/db'; +import crypto from 'crypto'; + +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 profsRes = await query(` + SELECT + u.id, u.name, u.role, u.phone, u.email, + (SELECT COALESCE(AVG(commission_percent), 0) FROM professional_services WHERE professional_id = u.id) as commission, + COALESCE( + (SELECT json_agg(s.name) FROM professional_services ps JOIN services s ON ps.service_id = s.id WHERE ps.professional_id = u.id), + '[]'::json + ) as services, + (SELECT COUNT(*) FROM appointments WHERE professional_id = u.id AND status = 'completed') as total_clients + FROM users u + WHERE u.tenant_id = $1 AND u.role = 'professional' AND u.is_active = true + ORDER BY u.created_at DESC + `, [tenantId]); + + const professionals = profsRes.rows.map(row => ({ + id: row.id, + name: row.name, + role: row.role || 'Profissional', + phone: row.phone || '', + email: row.email || '', + commission: parseInt(row.commission || 0), + services: row.services || [], + schedule: { seg: '09:00-18:00', ter: '09:00-18:00', qua: '09:00-18:00', qui: '09:00-18:00', sex: '09:00-18:00', sab: '09:00-18:00' }, + rating: 5.0, + totalClients: parseInt(row.total_clients || 0) + })); + + return NextResponse.json(professionals); + } catch (error: any) { + console.error('GET Team error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +export async function POST(req: NextRequest) { + try { + const { slug, name, email, phone, role, commission } = 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; + + // Use a random hash for password since they are created by admin + const passwordHash = crypto.randomBytes(16).toString('hex'); + const safeEmail = email || `${Date.now()}@agendapro.com`; + + const res = await query( + `INSERT INTO users (tenant_id, name, email, phone, role, password_hash) + VALUES ($1, $2, $3, $4, 'professional', $5) RETURNING *`, + [tenantId, name, safeEmail, phone || null, passwordHash] + ); + + return NextResponse.json(res.rows[0]); + } catch (error: any) { + console.error('POST Team error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +export async function PUT(req: NextRequest) { + try { + const { id, name, email, phone, role } = await req.json(); + if (!id || !name) return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); + + const res = await query( + `UPDATE users SET name = $1, email = $2, phone = $3, updated_at = NOW() WHERE id = $4 AND role = 'professional' RETURNING *`, + [name, email, phone, 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 }); + + // Hard delete for now, or soft delete `is_active = false`. Let's do hard delete to clean up immediately. + await query('DELETE FROM users 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 86eb3a9..1b0877b 100644 --- a/src/components/Agenda.tsx +++ b/src/components/Agenda.tsx @@ -1,19 +1,6 @@ import { useState, useEffect } from 'react'; import { Plus, ChevronLeft, ChevronRight, X, Clock, User, Calendar } from 'lucide-react'; -const professionals = [ - { id: '1', name: 'Carlos Silva', color: '#6C63FF' }, - { id: '2', name: 'Rafael Santos', color: '#a855f7' }, -]; - -const mockAppointments = [ - { id: '1', professionalId: '1', client: 'João Mendes', service: 'Corte Masculino', startHour: 10, startMin: 0, duration: 40, status: 'confirmed', price: 55 }, - { id: '2', professionalId: '1', client: 'Pedro Oliveira', service: 'Combo Corte + Barba', startHour: 11, startMin: 0, duration: 60, status: 'confirmed', price: 85 }, - { id: '3', professionalId: '2', client: 'Lucas Ferreira', service: 'Barba Completa', startHour: 14, startMin: 0, duration: 30, status: 'pending', price: 40 }, - { id: '4', professionalId: '2', client: 'Gabriel Souza', service: 'Corte Masculino', startHour: 10, startMin: 0, duration: 40, status: 'completed', price: 55 }, - { id: '5', professionalId: '1', client: 'Mateus Costa', service: 'Design de Sobrancelha', startHour: 15, startMin: 30, duration: 15, status: 'confirmed', price: 25 }, -]; - const hours = Array.from({ length: 12 }, (_, i) => i + 8); const statusColors: Record = { @@ -33,31 +20,43 @@ export default function Agenda() { const [showModal, setShowModal] = useState(false); const today = new Date(); const [currentDate, setCurrentDate] = useState(today); - const [appointments, setAppointments] = useState(mockAppointments); + const [appointments, setAppointments] = useState([]); + const [professionals, setProfessionals] = 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(); + setProfessionals(data.professionals || []); + setAppointments(data.appointments || []); + } + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; useEffect(() => { - const activeSlug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium'; - const savedAppts = localStorage.getItem(`agendapro_appointments_${activeSlug}`); - if (savedAppts) { - setAppointments(JSON.parse(savedAppts)); - } else { - const savedName = localStorage.getItem('agendapro_tenant_name'); - if (savedName) { - setAppointments([]); - localStorage.setItem(`agendapro_appointments_${activeSlug}`, JSON.stringify([])); - } else { - setAppointments(mockAppointments); - } - } - }, []); + fetchAgenda(); + }, [currentDate]); // States do Modal de Novo Agendamento const [newClient, setNewClient] = useState(''); const [newPhone, setNewPhone] = useState(''); const [newService, setNewService] = useState('Corte Masculino - R$ 55,00 (40min)'); - const [newProf, setNewProf] = useState('1'); + const [newProf, setNewProf] = useState(''); const [newTime, setNewTime] = useState('10:00'); + useEffect(() => { + if (professionals.length > 0 && !newProf) { + setNewProf(professionals[0].id); + } + }, [professionals]); + const dateStr = currentDate.toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); const changeDate = (days: number) => { @@ -70,6 +69,10 @@ export default function Agenda() { setCurrentDate(new Date()); }; + if (loading && professionals.length === 0) { + return
Carregando agenda...
; + } + return (
@@ -86,8 +89,14 @@ export default function Agenda() {
+ {professionals.length === 0 ? ( +
+

Sua agenda está vazia

+

Você precisa adicionar um profissional na aba Equipe antes de agendar clientes.

+
+ ) : (
-
+
{professionals.map(p => (
-
+
{hours.map(h => (
{professionals.map(prof => { - // Lógica simples para simular agendamentos diferentes em dias diferentes - const daySeed = currentDate.getDate(); - const isToday = daySeed === new Date().getDate(); - - // Se for hoje, mostra tudo que for do profissional. Se não, filtra baseado no dia para parecer que mudou. - const visibleAppointments = appointments.filter(a => - a.professionalId === prof.id && (isToday || (parseInt(a.id) + daySeed) % 2 === 0) - ); + const visibleAppointments = appointments.filter(a => a.professionalId === prof.id); return (
@@ -140,8 +142,8 @@ export default function Agenda() {
+ )} {showModal && (
setShowModal(false)}> @@ -204,14 +207,15 @@ export default function Agenda() {
- +
@@ -225,7 +229,7 @@ export default function Agenda() {
-
diff --git a/src/components/Team.tsx b/src/components/Team.tsx index 5f557da..f046b4c 100644 --- a/src/components/Team.tsx +++ b/src/components/Team.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Plus, X, Clock, Edit, Trash2, Star, AlertTriangle } from 'lucide-react'; interface Professional { @@ -7,21 +7,6 @@ interface Professional { services: string[]; schedule: Record; rating: number; totalClients: number; } -const initialTeam: Professional[] = [ - { - id: '1', name: 'Carlos Silva', role: 'Barbeiro Senior', phone: '(11) 98888-1111', - email: 'carlos@demo.com', commission: 40, services: ['Corte Masculino', 'Barba Completa', 'Combo Corte + Barba', 'Corte Infantil', 'Design de Sobrancelha'], - schedule: { seg: '09:00-19:00', ter: '09:00-19:00', qua: '09:00-19:00', qui: '09:00-19:00', sex: '09:00-19:00', sab: '09:00-19:00' }, - rating: 4.8, totalClients: 89 - }, - { - id: '2', name: 'Rafael Santos', role: 'Barbeiro', phone: '(11) 98888-2222', - email: 'rafael@demo.com', commission: 35, services: ['Corte Masculino', 'Barba Completa', 'Combo Corte + Barba'], - schedule: { seg: '10:00-20:00', ter: '10:00-20:00', qua: '10:00-20:00', qui: '10:00-20:00', sex: '10:00-20:00', sab: '10:00-20:00' }, - rating: 4.6, totalClients: 65 - }, -]; - const emptyProfessional: Professional = { id: '', name: '', role: '', phone: '', email: '', commission: 30, services: [], schedule: { seg: '09:00-18:00', ter: '09:00-18:00', qua: '09:00-18:00', qui: '09:00-18:00', sex: '09:00-18:00', sab: '09:00-18:00' }, @@ -29,15 +14,35 @@ const emptyProfessional: Professional = { }; export default function Team() { - const [team, setTeam] = useState(initialTeam); + const [team, setTeam] = useState([]); const [showModal, setShowModal] = useState(false); const [editingProf, setEditingProf] = useState(null); const [formData, setFormData] = useState(emptyProfessional); const [deleteConfirm, setDeleteConfirm] = useState(null); + const [loading, setLoading] = useState(true); + + const fetchTeam = async () => { + try { + const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium'; + const res = await fetch(`/api/tenant/team?slug=${slug}`); + if (res.ok) { + const data = await res.json(); + setTeam(data); + } + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchTeam(); + }, []); const openAdd = () => { setEditingProf(null); - setFormData({ ...emptyProfessional, id: Date.now().toString() }); + setFormData({ ...emptyProfessional }); setShowModal(true); }; @@ -47,22 +52,41 @@ export default function Team() { setShowModal(true); }; - const handleSave = () => { + const handleSave = async () => { if (!formData.name.trim()) return; - if (editingProf) { - setTeam(prev => prev.map(p => p.id === editingProf.id ? { ...formData } : p)); - } else { - setTeam(prev => [...prev, { ...formData }]); + const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium'; + + const method = editingProf ? 'PUT' : 'POST'; + const payload = { ...formData, slug }; + + try { + const res = await fetch('/api/tenant/team', { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + if (res.ok) { + fetchTeam(); + setShowModal(false); + setEditingProf(null); + } + } catch (e) { + console.error(e); } - setShowModal(false); - setEditingProf(null); }; - const handleDelete = (id: string) => { - setTeam(prev => prev.filter(p => p.id !== id)); - setDeleteConfirm(null); + const handleDelete = async (id: string) => { + try { + await fetch(`/api/tenant/team?id=${id}`, { method: 'DELETE' }); + setTeam(prev => prev.filter(p => p.id !== id)); + setDeleteConfirm(null); + } catch (e) { + console.error(e); + } }; + if (loading) return
Carregando equipe...
; + return (
@@ -82,7 +106,7 @@ export default function Team() { }}>

Excluir {p.name}?

-

Essa ação não pode ser desfeita.

+

Essa ação removerá o profissional de toda a agenda.

@@ -138,10 +162,10 @@ export default function Team() { Horário de Trabalho
- {Object.entries(p.schedule).map(([day, hours]) => ( + {Object.entries(p.schedule || {}).map(([day, hours]) => (
{day} - {hours} + {hours as string}
))}