diff --git a/src/app/[slug]/agendar/page.tsx b/src/app/[slug]/agendar/page.tsx index f0c62d2..a66744b 100644 --- a/src/app/[slug]/agendar/page.tsx +++ b/src/app/[slug]/agendar/page.tsx @@ -1,29 +1,6 @@ 'use client'; import { useState, useEffect, use } from 'react'; -import { Calendar, Clock, User, Phone, Mail, ChevronLeft, ChevronRight, Check, Scissors, MapPin, Star } from 'lucide-react'; - -const establishment = { - name: 'Barbearia Premium', - type: 'barbearia', - description: 'A melhor barbearia da cidade. Cortes modernos, barbas impecáveis e ambiente premium.', - phone: '(11) 99999-1234', - address: 'Rua das Flores, 123 - Centro, São Paulo - SP', - rating: 4.9, - reviews: 127, -}; - -const services = [ - { id: '1', category: 'Cabelo', name: 'Corte Masculino', desc: 'Corte moderno com lavagem e finalização', duration: 40, price: 55 }, - { id: '2', category: 'Cabelo', name: 'Corte Infantil', desc: 'Corte para crianças até 12 anos', duration: 30, price: 40 }, - { id: '3', category: 'Barba', name: 'Barba Completa', desc: 'Aparar, modelar e hidratação', duration: 30, price: 40 }, - { id: '4', category: 'Barba', name: 'Design de Sobrancelha', desc: 'Alinhamento com navalha', duration: 15, price: 25 }, - { id: '5', category: 'Combos', name: 'Combo Corte + Barba', desc: 'Corte masculino + barba completa', duration: 60, price: 85 }, -]; - -const professionals = [ - { id: '1', name: 'Carlos Silva', role: 'Barbeiro Senior', rating: 4.8 }, - { id: '2', name: 'Rafael Santos', role: 'Barbeiro', rating: 4.6 }, -]; +import { Calendar, Clock, User, Phone, Mail, ChevronLeft, ChevronRight, Check, Scissors, MapPin, Star, Loader } from 'lucide-react'; export default function PublicBookingPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = use(params); @@ -38,41 +15,91 @@ export default function PublicBookingPage({ params }: { params: Promise<{ slug: const [clientEmail, setClientEmail] = useState(''); const [booked, setBooked] = useState(false); const [slots, setSlots] = useState([]); + const [loadingDb, setLoadingDb] = useState(true); + + const [dbServices, setDbServices] = useState([]); + const [dbProfessionals, setDbProfessionals] = useState([]); + + // Dados do tenant vindos do banco + const [establishment, setEstablishment] = useState({ + name: '', type: 'barbearia', description: '', phone: '', + address: '', rating: 5.0, reviews: 0, + }); + + // Algoritmo dinâmico de fatiamento de grade + const generateSlots = (startTimeStr: string, endTimeStr: string, intervalMin: number) => { + const generated: string[] = []; + const [sh, sm] = startTimeStr.split(':').map(Number); + const [eh, em] = endTimeStr.split(':').map(Number); + + let currentMinutes = sh * 60 + sm; + const endMinutes = eh * 60 + em; + + while (currentMinutes < endMinutes) { + const h = Math.floor(currentMinutes / 60); + const m = currentMinutes % 60; + const timeStr = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; + generated.push(timeStr); + currentMinutes += intervalMin; + } + return generated; + }; - // Carrega e gera os slots dinâmicos do barbeiro useEffect(() => { - const getCookie = (name: string): string => { - if (typeof document === 'undefined') return ''; - const value = `; ${document.cookie}`; - const parts = value.split(`; ${name}=`); - if (parts.length === 2) return parts.pop()!.split(';').shift() || ''; - return ''; - }; + const fetchData = async () => { + try { + const [profileRes, agendaRes, servicesRes] = await Promise.all([ + fetch(`/api/tenant/profile?slug=${slug}`), + fetch(`/api/tenant/agenda?slug=${slug}`), + fetch(`/api/tenant/services?slug=${slug}`) + ]); + + // Carregar perfil do tenant + if (profileRes.ok) { + const profile = await profileRes.json(); + const fullAddress = [profile.address, profile.city, profile.state].filter(Boolean).join(', '); + setEstablishment({ + name: profile.name || slug, + type: profile.businessType || 'barbearia', + description: profile.description || '', + phone: profile.phone || '', + address: fullAddress || '', + rating: 5.0, + reviews: 0, + }); + + // Gerar slots com horários do tenant + const startTime = profile.startTime || '09:00'; + const endTime = profile.endTime || '19:00'; + const interval = profile.interval || 30; + setSlots(generateSlots(startTime, endTime, interval)); + + // Aplicar cor do tema + if (profile.themeColor) { + document.documentElement.style.setProperty('--accent', profile.themeColor); + document.documentElement.style.setProperty('--accent-glow', `${profile.themeColor}26`); + } + } else { + // Fallback + setSlots(generateSlots('09:00', '19:00', 30)); + } - const start = getCookie(`agendapro_start_time_${slug}`) || localStorage.getItem(`agendapro_start_time_${slug}`) || '09:00'; - const end = getCookie(`agendapro_end_time_${slug}`) || localStorage.getItem(`agendapro_end_time_${slug}`) || '19:00'; - const interval = Number(getCookie(`agendapro_interval_${slug}`) || localStorage.getItem(`agendapro_interval_${slug}`) || '30'); - - // Algoritmo dinâmico de fatiamento de grade - const generateSlots = (startTimeStr: string, endTimeStr: string, intervalMin: number) => { - const generated = []; - let [sh, sm] = startTimeStr.split(':').map(Number); - const [eh, em] = endTimeStr.split(':').map(Number); - - let currentMinutes = sh * 60 + sm; - const endMinutes = eh * 60 + em; - - while (currentMinutes < endMinutes) { - const h = Math.floor(currentMinutes / 60); - const m = currentMinutes % 60; - const timeStr = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; - generated.push(timeStr); - currentMinutes += intervalMin; + if (agendaRes.ok) { + const data = await agendaRes.json(); + setDbProfessionals(data.professionals || []); + } + if (servicesRes.ok) { + const data = await servicesRes.json(); + setDbServices(data || []); + } + } catch (err) { + console.error('Erro ao buscar dados do banco:', err); + setSlots(generateSlots('09:00', '19:00', 30)); + } finally { + setLoadingDb(false); } - return generated; }; - - setSlots(generateSlots(start, end, interval)); + fetchData(); }, [slug]); const today = new Date(); @@ -82,8 +109,8 @@ export default function PublicBookingPage({ params }: { params: Promise<{ slug: return d; }); - const svc = services.find(s => s.id === selectedService); - const prof = professionals.find(p => p.id === selectedProfessional); + const svc = dbServices.find(s => s.id === selectedService); + const prof = dbProfessionals.find(p => p.id === selectedProfessional); const canNext = () => { if (step === 1) return !!selectedService; @@ -93,44 +120,37 @@ export default function PublicBookingPage({ params }: { params: Promise<{ slug: return false; }; - const handleBook = () => { - // 1. Monta o objeto de agendamento no formato esperado pela Agenda + const handleBook = async () => { const [startHour, startMin] = selectedTime ? selectedTime.split(':').map(Number) : [9, 0]; - const newAppointment = { - id: Date.now().toString(), - professionalId: selectedProfessional || '1', - client: clientName, - phone: clientPhone, - email: clientEmail, - service: svc?.name || 'Serviço', - price: svc?.price || 0, - duration: svc?.duration || 30, - startHour, - startMin, - status: 'pending', // Inicia pendente de aprovação! - date: selectedDate, // Data do agendamento (ex: "2026-05-31") - }; - // 2. Salva no localStorage com isolamento por slug (Tenant Isolation) - const key = `agendapro_appointments_${slug}`; - const existing = localStorage.getItem(key); - let currentList = []; - if (existing) { - currentList = JSON.parse(existing); - } else { - // Se não existia e for barbearia premium demo, pega os mockAppointments padrão - currentList = slug === 'barbearia-premium' ? [ - { id: '1', professionalId: '1', client: 'João Mendes', service: 'Corte Masculino', startHour: 10, startMin: 0, duration: 40, status: 'confirmed', price: 55, date: new Date().toISOString().split('T')[0] }, - { id: '2', professionalId: '1', client: 'Pedro Oliveira', service: 'Combo Corte + Barba', startHour: 11, startMin: 0, duration: 60, status: 'confirmed', price: 85, date: new Date().toISOString().split('T')[0] }, - { id: '3', professionalId: '2', client: 'Lucas Ferreira', service: 'Barba Completa', startHour: 14, startMin: 0, duration: 30, status: 'pending', price: 40, date: new Date().toISOString().split('T')[0] }, - { id: '4', professionalId: '2', client: 'Gabriel Souza', service: 'Corte Masculino', startHour: 10, startMin: 0, duration: 40, status: 'completed', price: 55, date: new Date().toISOString().split('T')[0] }, - { id: '5', professionalId: '1', client: 'Mateus Costa', service: 'Design de Sobrancelha', startHour: 15, startMin: 30, duration: 15, status: 'confirmed', price: 25, date: new Date().toISOString().split('T')[0] } - ] : []; + try { + const res = await fetch('/api/tenant/agenda', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + slug, + professionalId: selectedProfessional || '1', + client: clientName, + phone: clientPhone, + email: clientEmail, + service: svc?.name || 'Serviço', + price: svc?.price || 0, + duration: svc?.duration || 30, + startHour, + startMin, + status: 'pending', + date: selectedDate, + }) + }); + + if (res.ok) { + setBooked(true); + } else { + console.error('Falha ao agendar no banco.'); + } + } catch (e) { + console.error(e); } - - currentList.push(newAppointment); - localStorage.setItem(key, JSON.stringify(currentList)); - setBooked(true); }; if (booked) { @@ -223,7 +243,7 @@ export default function PublicBookingPage({ params }: { params: Promise<{ slug:

Escolha o serviço

- {services.map(s => ( + {dbServices.map(s => (
setSelectedService(s.id)} style={{ padding: '18px 20px', borderRadius: 'var(--radius-md)', cursor: 'pointer', background: selectedService === s.id ? 'var(--accent-glow)' : 'var(--bg-card)', @@ -232,13 +252,13 @@ export default function PublicBookingPage({ params }: { params: Promise<{ slug: }}>
{s.name}
-
{s.desc}
+
{s.desc || s.description || 'Serviço profissional'}
{s.duration} min
-
R$ {s.price.toFixed(2)}
+
R$ {Number(s.price).toFixed(2)}
))} @@ -251,20 +271,20 @@ export default function PublicBookingPage({ params }: { params: Promise<{ slug:

Escolha o profissional

- {professionals.map(p => ( + {dbProfessionals.map(p => (
setSelectedProfessional(p.id)} style={{ padding: '20px', borderRadius: 'var(--radius-md)', cursor: 'pointer', background: selectedProfessional === p.id ? 'var(--accent-glow)' : 'var(--bg-card)', border: `2px solid ${selectedProfessional === p.id ? 'var(--accent)' : 'var(--border-color)'}`, transition: 'all 0.2s', display: 'flex', alignItems: 'center', gap: '16px' }}> -
{p.name.split(' ').map(n => n[0]).join('')}
+
{p.name.split(' ').map((n: string) => n[0]).join('').substring(0, 2).toUpperCase()}
{p.name}
-
{p.role}
+
{p.role || 'Barbeiro/Profissional'}
- {p.rating} + {p.rating || '5.0'}
))} diff --git a/src/app/api/auth/change-password/route.ts b/src/app/api/auth/change-password/route.ts new file mode 100644 index 0000000..3e4c313 --- /dev/null +++ b/src/app/api/auth/change-password/route.ts @@ -0,0 +1,53 @@ +import { NextResponse } from 'next/server'; +import { query } from '@/lib/db'; +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; + +export async function POST(req: Request) { + try { + const { token, currentPassword, newPassword } = await req.json(); + + if (!token || !currentPassword || !newPassword) { + return NextResponse.json({ error: 'Campos obrigatórios: token, senha atual e nova senha' }, { status: 400 }); + } + + // Decode token to get userId + const secret = process.env.JWT_SECRET || 'fallback_secret_for_development'; + let decoded: any; + try { + decoded = jwt.verify(token, secret); + } catch (e) { + return NextResponse.json({ error: 'Token inválido ou expirado. Faça login novamente.' }, { status: 401 }); + } + + const userId = decoded.userId; + if (!userId) { + return NextResponse.json({ error: 'Token inválido' }, { status: 401 }); + } + + // Get current user + const userRes = await query('SELECT id, password_hash FROM users WHERE id = $1', [userId]); + if (userRes.rows.length === 0) { + return NextResponse.json({ error: 'Usuário não encontrado' }, { status: 404 }); + } + + const user = userRes.rows[0]; + + // Verify current password + const isValid = await bcrypt.compare(currentPassword, user.password_hash); + if (!isValid) { + return NextResponse.json({ error: 'Senha atual incorreta' }, { status: 401 }); + } + + // Hash new password and update + const salt = await bcrypt.genSalt(10); + const newHash = await bcrypt.hash(newPassword, salt); + + await query('UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2', [newHash, userId]); + + return NextResponse.json({ success: true, message: 'Senha alterada com sucesso' }); + } catch (error: any) { + console.error('Change password error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/tenant/dashboard/route.ts b/src/app/api/tenant/dashboard/route.ts new file mode 100644 index 0000000..83de56f --- /dev/null +++ b/src/app/api/tenant/dashboard/route.ts @@ -0,0 +1,133 @@ +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({ error: 'Tenant not found' }, { status: 404 }); + const tenantId = tenantRes.rows[0].id; + + // 1. Agendamentos de hoje + const todayApptsRes = await query(` + SELECT + a.id, + TO_CHAR(a.start_time, 'HH24:MI') as time, + COALESCE(c.name, a.client_name, 'Cliente Avulso') as client, + COALESCE(s.name, 'Serviço') as service, + COALESCE(u.name, 'Profissional') as professional, + 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 + LEFT JOIN users u ON a.professional_id = u.id + WHERE a.tenant_id = $1 AND a.date = CURRENT_DATE + ORDER BY a.start_time ASC + `, [tenantId]); + + // 2. KPIs - Agendamentos do dia + const kpiTodayRes = await query(` + SELECT + COUNT(*) as total_today, + COUNT(*) FILTER (WHERE status = 'completed') as completed_today, + COALESCE(SUM(price) FILTER (WHERE status IN ('completed', 'confirmed', 'in_progress')), 0) as revenue_today + FROM appointments + WHERE tenant_id = $1 AND date = CURRENT_DATE + `, [tenantId]); + + // 3. Clientes ativos (total) + const clientsRes = await query(` + SELECT COUNT(*) as total_clients FROM clients WHERE tenant_id = $1 AND is_active = true + `, [tenantId]); + + // 4. Agendamentos semana anterior (para comparação) + const lastWeekRes = await query(` + SELECT COUNT(*) as total_last_week + FROM appointments + WHERE tenant_id = $1 AND date BETWEEN CURRENT_DATE - INTERVAL '14 days' AND CURRENT_DATE - INTERVAL '7 days' + `, [tenantId]); + + const thisWeekRes = await query(` + SELECT COUNT(*) as total_this_week + FROM appointments + WHERE tenant_id = $1 AND date BETWEEN CURRENT_DATE - INTERVAL '7 days' AND CURRENT_DATE + `, [tenantId]); + + // 5. Desempenho semanal (últimos 7 dias - Seg a Sáb) + const weeklyRes = await query(` + SELECT + EXTRACT(DOW FROM date) as dow, + COUNT(*) as count + FROM appointments + WHERE tenant_id = $1 AND date BETWEEN CURRENT_DATE - INTERVAL '7 days' AND CURRENT_DATE + GROUP BY EXTRACT(DOW FROM date) + ORDER BY dow + `, [tenantId]); + + // 6. Próximos horários livres + const professionalsRes = await query(` + SELECT id, name FROM users WHERE tenant_id = $1 AND role = 'professional' AND is_active = true + `, [tenantId]); + + // Parse KPIs + const kpi = kpiTodayRes.rows[0]; + const totalToday = Number(kpi.total_today); + const revenueToday = Number(kpi.revenue_today); + const totalClients = Number(clientsRes.rows[0].total_clients); + const thisWeek = Number(thisWeekRes.rows[0].total_this_week); + const lastWeek = Number(lastWeekRes.rows[0].total_last_week); + const weekChange = lastWeek > 0 ? Math.round(((thisWeek - lastWeek) / lastWeek) * 100) : 0; + + // Taxa de ocupação (slots preenchidos / slots totais estimados) + const totalProfessionals = professionalsRes.rows.length || 1; + const slotsPerDay = 10 * totalProfessionals; // ~10 slots por profissional + const occupancy = slotsPerDay > 0 ? Math.min(100, Math.round((totalToday / slotsPerDay) * 100)) : 0; + + // Desempenho semanal + const dayNames = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb']; + const weeklyPerformance = [1, 2, 3, 4, 5, 6].map(dow => { + const found = weeklyRes.rows.find((r: any) => Number(r.dow) === dow); + const count = found ? Number(found.count) : 0; + const maxSlots = totalProfessionals * 10; + return { + label: dayNames[dow], + value: maxSlots > 0 ? Math.min(100, Math.round((count / maxSlots) * 100)) : 0, + count + }; + }); + + // Appointments com formatação + const appointments = todayApptsRes.rows.map((a: any) => ({ + id: a.id, + time: a.time, + client: a.client, + service: a.service, + professional: a.professional, + status: a.status, + price: Number(a.price) + })); + + return NextResponse.json({ + appointments, + kpis: { + appointmentsToday: totalToday, + revenueToday, + totalClients, + occupancy, + weekChange, + }, + weeklyPerformance, + professionals: professionalsRes.rows, + }); + } catch (error: any) { + console.error('GET Tenant Dashboard error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/tenant/profile/route.ts b/src/app/api/tenant/profile/route.ts new file mode 100644 index 0000000..a5d6fc1 --- /dev/null +++ b/src/app/api/tenant/profile/route.ts @@ -0,0 +1,53 @@ +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 res = await query(` + SELECT + t.name, t.slug, t.business_type, t.phone, t.email, + t.address, t.city, t.state, t.zip_code, t.description, + t.logo_url, t.cover_url, t.settings + FROM tenants t + WHERE t.slug = $1 AND t.is_active = true + `, [slug]); + + if (res.rows.length === 0) { + return NextResponse.json({ error: 'Estabelecimento não encontrado' }, { status: 404 }); + } + + const tenant = res.rows[0]; + const settings = tenant.settings || {}; + + return NextResponse.json({ + name: tenant.name, + slug: tenant.slug, + businessType: tenant.business_type, + phone: tenant.phone || '', + email: tenant.email || '', + address: tenant.address || '', + city: tenant.city || '', + state: tenant.state || '', + zipCode: tenant.zip_code || '', + description: tenant.description || '', + logoUrl: tenant.logo_url || '', + coverUrl: tenant.cover_url || '', + themeColor: settings.theme_color || '#6C63FF', + startTime: settings.start_time || '09:00', + endTime: settings.end_time || '19:00', + interval: settings.interval || 30, + workingDays: settings.working_days || [1, 2, 3, 4, 5, 6], + allowOnlineBooking: settings.allow_online_booking !== false, + }); + } catch (error: any) { + console.error('GET Profile error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/tenant/settings/route.ts b/src/app/api/tenant/settings/route.ts new file mode 100644 index 0000000..ddadd67 --- /dev/null +++ b/src/app/api/tenant/settings/route.ts @@ -0,0 +1,136 @@ +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 res = await query(` + SELECT + t.id, t.name, t.slug, t.business_type, t.phone, t.email, + t.address, t.city, t.state, t.zip_code, t.description, + t.logo_url, t.cover_url, t.settings, + t.subscription_status, t.is_active, + COALESCE(p.name, 'Starter') as plan_name + FROM tenants t + LEFT JOIN plans p ON t.plan_id = p.id + WHERE t.slug = $1 + `, [slug]); + + if (res.rows.length === 0) { + return NextResponse.json({ error: 'Tenant not found' }, { status: 404 }); + } + + const tenant = res.rows[0]; + const settings = tenant.settings || {}; + + return NextResponse.json({ + id: tenant.id, + name: tenant.name, + slug: tenant.slug, + businessType: tenant.business_type, + phone: tenant.phone || '', + email: tenant.email || '', + address: tenant.address || '', + city: tenant.city || '', + state: tenant.state || '', + zipCode: tenant.zip_code || '', + description: tenant.description || '', + logoUrl: tenant.logo_url || '', + coverUrl: tenant.cover_url || '', + planName: tenant.plan_name, + subscriptionStatus: tenant.subscription_status, + // Settings JSONB + themeColor: settings.theme_color || '#6C63FF', + workingDays: settings.working_days || [1, 2, 3, 4, 5, 6], + startTime: settings.start_time || '09:00', + endTime: settings.end_time || '19:00', + interval: settings.interval || 30, + allowOnlineBooking: settings.allow_online_booking !== false, + requireConfirmation: settings.require_confirmation || false, + sendReminders: settings.send_reminders !== false, + reminderMinutesBefore: settings.reminder_minutes_before || 60, + allowCancellation: settings.allow_cancellation !== false, + bookingAdvanceMin: settings.booking_advance_min || 60, + bookingAdvanceMaxDays: settings.booking_advance_max_days || 30, + cancellationLimitMin: settings.cancellation_limit_min || 120, + notifyNewAppointment: settings.notify_new_appointment !== false, + notifyCancellation: settings.notify_cancellation !== false, + notifyClientReminder: settings.notify_client_reminder !== false, + dailySummary: settings.daily_summary || false, + weeklyReport: settings.weekly_report !== false, + }); + } catch (error: any) { + console.error('GET Settings error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +export async function PUT(req: NextRequest) { + try { + const body = await req.json(); + const { slug } = body; + + if (!slug) return NextResponse.json({ error: 'Missing slug' }, { status: 400 }); + + // Build the settings JSONB object + const settingsJson = { + theme_color: body.themeColor || '#6C63FF', + working_days: body.workingDays || [1, 2, 3, 4, 5, 6], + start_time: body.startTime || '09:00', + end_time: body.endTime || '19:00', + interval: body.interval || 30, + allow_online_booking: body.allowOnlineBooking !== false, + require_confirmation: body.requireConfirmation || false, + send_reminders: body.sendReminders !== false, + reminder_minutes_before: body.reminderMinutesBefore || 60, + allow_cancellation: body.allowCancellation !== false, + booking_advance_min: body.bookingAdvanceMin || 60, + booking_advance_max_days: body.bookingAdvanceMaxDays || 30, + cancellation_limit_min: body.cancellationLimitMin || 120, + notify_new_appointment: body.notifyNewAppointment !== false, + notify_cancellation: body.notifyCancellation !== false, + notify_client_reminder: body.notifyClientReminder !== false, + daily_summary: body.dailySummary || false, + weekly_report: body.weeklyReport !== false, + }; + + const res = await query(` + UPDATE tenants SET + name = COALESCE($1, name), + business_type = COALESCE($2, business_type), + phone = $3, + email = $4, + address = $5, + city = $6, + state = $7, + zip_code = $8, + description = $9, + settings = $10, + updated_at = NOW() + WHERE slug = $11 + RETURNING id, name, slug + `, [ + body.name, body.businessType, + body.phone || null, body.email || null, + body.address || null, body.city || null, body.state || null, body.zipCode || null, + body.description || null, + JSON.stringify(settingsJson), + slug + ]); + + if (res.rows.length === 0) { + return NextResponse.json({ error: 'Tenant not found' }, { status: 404 }); + } + + return NextResponse.json({ success: true, tenant: res.rows[0] }); + } catch (error: any) { + console.error('PUT Settings error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 09f25b2..912e794 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -1,60 +1,95 @@ 'use client'; import { useState, useEffect } from 'react'; -import { Calendar, Users, DollarSign, TrendingUp, Clock, ArrowUpRight, Play, CheckSquare } from 'lucide-react'; +import { Calendar, Users, DollarSign, TrendingUp, Clock, ArrowUpRight, Play, CheckSquare, Loader } from 'lucide-react'; interface DashboardProps { onNavigate: (page: string) => void; } -const todayAppointments = [ - { id: 1, time: '10:00', client: 'João Mendes', service: 'Corte Masculino', professional: 'Carlos Silva', status: 'confirmed', price: 55 }, - { id: 2, time: '11:00', client: 'Pedro Oliveira', service: 'Combo Corte + Barba', professional: 'Carlos Silva', status: 'confirmed', price: 85 }, - { id: 3, time: '14:00', client: 'Lucas Ferreira', service: 'Barba Completa', professional: 'Rafael Santos', status: 'pending', price: 40 }, - { id: 4, time: '15:30', client: 'Mateus Costa', service: 'Corte Masculino', professional: 'Rafael Santos', status: 'confirmed', price: 55 }, - { id: 5, time: '16:00', client: 'Gabriel Souza', service: 'Design de Sobrancelha', professional: 'Carlos Silva', status: 'confirmed', price: 25 }, -]; - const statusMap: Record = { confirmed: { label: 'Confirmado', cls: 'badge-info' }, pending: { label: 'Pendente', cls: 'badge-warning' }, completed: { label: 'Concluído', cls: 'badge-success' }, cancelled: { label: 'Cancelado', cls: 'badge-danger' }, serving: { label: 'Em Atendimento', cls: 'badge-purple' }, + in_progress: { label: 'Em Atendimento', cls: 'badge-purple' }, }; export default function Dashboard({ onNavigate }: DashboardProps) { - const [appointments, setAppointments] = useState(todayAppointments); - const [currentServingId, setCurrentServingId] = useState(null); - const [isNew, setIsNew] = useState(false); + const [appointments, setAppointments] = useState([]); + const [kpis, setKpis] = useState({ appointmentsToday: 0, revenueToday: 0, totalClients: 0, occupancy: 0, weekChange: 0 }); + const [weeklyPerformance, setWeeklyPerformance] = useState([]); + const [loading, setLoading] = useState(true); + const [currentServingId, setCurrentServingId] = useState(null); useEffect(() => { - const savedName = localStorage.getItem('agendapro_tenant_name'); - if (savedName) { - setIsNew(true); - setAppointments([]); - } + const fetchDashboard = async () => { + const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium'; + try { + const res = await fetch(`/api/tenant/dashboard?slug=${slug}`); + if (res.ok) { + const data = await res.json(); + setAppointments(data.appointments || []); + setKpis(data.kpis || { appointmentsToday: 0, revenueToday: 0, totalClients: 0, occupancy: 0, weekChange: 0 }); + setWeeklyPerformance(data.weeklyPerformance || []); + } + } catch (err) { + console.error('Erro ao carregar dashboard:', err); + } finally { + setLoading(false); + } + }; + + fetchDashboard(); }, []); - const handleStartServing = (id: number) => { + const handleStartServing = async (id: string) => { setCurrentServingId(id); - setAppointments(prev => prev.map(a => a.id === id ? { ...a, status: 'serving' } : a)); + setAppointments(prev => prev.map(a => a.id === id ? { ...a, status: 'in_progress' } : a)); + + // Atualiza no banco + try { + const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium'; + await fetch('/api/tenant/agenda', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id, status: 'in_progress', slug }) + }); + } catch (err) { + console.error('Erro ao iniciar atendimento:', err); + } }; - const handleFinishServing = (id: number) => { + const handleFinishServing = async (id: string) => { setCurrentServingId(null); setAppointments(prev => prev.map(a => a.id === id ? { ...a, status: 'completed' } : a)); + + // Atualiza no banco + try { + const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium'; + await fetch('/api/tenant/agenda', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id, status: 'completed', slug }) + }); + } catch (err) { + console.error('Erro ao finalizar atendimento:', err); + } }; - const servingApt = appointments.find(a => a.id === currentServingId); - const activeQueue = appointments.filter(a => a.status !== 'completed' && a.id !== currentServingId); + const servingApt = appointments.find(a => a.id === currentServingId || a.status === 'in_progress'); + const activeQueue = appointments.filter(a => a.status !== 'completed' && a.status !== 'cancelled' && a.id !== servingApt?.id); const nextInQueue = activeQueue[0]; - // Cálculos dinâmicos de estatísticas - const appointmentsCount = isNew ? appointments.length : 24; - const completedRevenue = appointments.filter(a => a.status === 'completed' || a.status === 'serving').reduce((s, a) => s + a.price, 0); - const revenueValue = isNew ? completedRevenue : 2450; - const activeClientsCount = isNew ? appointments.length : 156; - const occupancyPercentage = isNew ? (appointments.length > 0 ? Math.min(100, Math.round((appointments.length / 8) * 100)) : 0) : 92; + const weekChangeLabel = kpis.weekChange > 0 ? `↑ ${kpis.weekChange}% vs semana anterior` : kpis.weekChange < 0 ? `↓ ${Math.abs(kpis.weekChange)}% vs semana anterior` : 'Sem dados da semana anterior'; + + if (loading) { + return ( +
+ Carregando dashboard... +
+ ); + } return (
@@ -69,6 +104,13 @@ export default function Dashboard({ onNavigate }: DashboardProps) { background: #a855f7 !important; color: white !important; } + .spin { + animation: spin 1s linear infinite; + } + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } `} {/* Painel de Fila de Atendimento */} @@ -82,7 +124,7 @@ export default function Dashboard({ onNavigate }: DashboardProps) { Atendendo agora: {servingApt.client}

- {servingApt.service} com {servingApt.professional} • R$ {servingApt.price.toFixed(2)} + {servingApt.service} com {servingApt.professional} • R$ {Number(servingApt.price).toFixed(2)}

) : nextInQueue ? ( @@ -96,8 +138,12 @@ export default function Dashboard({ onNavigate }: DashboardProps) {
) : (
-

Nenhum cliente na fila de espera

-

Todos os agendamentos de hoje foram concluídos.

+

+ {appointments.length === 0 ? 'Nenhum agendamento para hoje' : 'Todos os atendimentos do dia foram concluídos'} +

+

+ {appointments.length === 0 ? 'Acesse a Agenda para criar novos agendamentos.' : 'Parabéns! Todos os clientes foram atendidos.'} +

)}
@@ -115,7 +161,6 @@ export default function Dashboard({ onNavigate }: DashboardProps) {
- {/* Sub-painel da Fila quando alguém já está sendo atendido */} {servingApt && (
@@ -137,33 +182,33 @@ export default function Dashboard({ onNavigate }: DashboardProps) {
-
{appointmentsCount}
+
{kpis.appointmentsToday}
Agendamentos Hoje
-
{isNew ? '0% esta semana' : '↑ 12% vs semana anterior'}
+
{weekChangeLabel}
-
R$ {revenueValue.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+
R$ {kpis.revenueToday.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
Receita do Dia
-
{isNew ? '0% hoje' : '↑ 8% vs ontem'}
+
Valores confirmados
-
{activeClientsCount}
-
Clientes Ativos
-
{isNew ? '0 novos clientes' : '↑ 5 novos esta semana'}
+
{kpis.totalClients}
+
Clientes Cadastrados
+
Total ativo
-
{occupancyPercentage}%
+
{kpis.occupancy}%
Taxa de Ocupação
-
{isNew ? 'Com base na agenda' : '↑ 3% vs mês anterior'}
+
Hoje
@@ -192,15 +237,16 @@ export default function Dashboard({ onNavigate }: DashboardProps) { {appointments.map(apt => { - const isServing = apt.status === 'serving'; + const isServing = apt.status === 'in_progress'; const isCompleted = apt.status === 'completed'; + const st = statusMap[apt.status] || { label: apt.status, cls: 'badge-secondary' }; return ( {apt.time}
- {apt.client.split(' ').map(n => n[0]).join('')} + {apt.client.split(' ').map((n: string) => n[0]).join('').substring(0, 2)}
{apt.client}
@@ -208,14 +254,21 @@ export default function Dashboard({ onNavigate }: DashboardProps) { {apt.service} {apt.professional} - - {statusMap[apt.status].label} + + {st.label} - R$ {apt.price.toFixed(2)} + R$ {Number(apt.price).toFixed(2)} ); })} + {appointments.length === 0 && ( + + + Nenhum agendamento para hoje. Crie um novo na Agenda. + + + )} @@ -225,14 +278,7 @@ export default function Dashboard({ onNavigate }: DashboardProps) {

Desempenho Semanal

- {[ - { label: 'Seg', value: isNew ? 0 : 85 }, - { label: 'Ter', value: isNew ? 0 : 72 }, - { label: 'Qua', value: isNew ? 0 : 90 }, - { label: 'Qui', value: isNew ? 0 : 65 }, - { label: 'Sex', value: isNew ? 0 : 95 }, - { label: 'Sáb', value: isNew ? 0 : 100 }, - ].map((d) => ( + {weeklyPerformance.length > 0 ? weeklyPerformance.map((d: any) => (
{d.label}
@@ -243,30 +289,32 @@ export default function Dashboard({ onNavigate }: DashboardProps) { transition: 'width 1s ease' }} />
- {d.value}% + {d.count || 0}
- ))} + )) : ( +

Sem dados suficientes

+ )}
-

Próximos Horários Livres

+

Ações Rápidas

{[ - { time: '13:00 - 14:00', prof: 'Carlos Silva' }, - { time: '14:30 - 15:30', prof: 'Carlos Silva' }, - { time: '16:00 - 17:00', prof: 'Rafael Santos' }, - ].map((slot, i) => ( + { label: 'Novo Agendamento', desc: 'Agendar um novo cliente', page: 'agenda' }, + { label: 'Cadastrar Cliente', desc: 'Adicionar cliente à base', page: 'clients' }, + { label: 'Novo Serviço', desc: 'Cadastrar um novo serviço', page: 'services' }, + ].map((action, i) => (
-
{slot.time}
-
{slot.prof}
+
{action.label}
+
{action.desc}
- +
))}
diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 769494b..4e3bd32 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { Store, Clock, Globe, Bell, Palette, Shield, Save, Copy, ExternalLink, Check, Link } from 'lucide-react'; +import { Store, Clock, Globe, Bell, Palette, Shield, Save, Copy, ExternalLink, Check, Link, Loader } from 'lucide-react'; const tabs = [ { id: 'general', label: 'Geral', icon: Store }, @@ -23,107 +23,200 @@ export default function Settings() { const [activeTab, setActiveTab] = useState('general'); const [saved, setSaved] = useState(false); const [copied, setCopied] = useState(false); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); - // States Dinâmicos do Tenant Ativo (Multi-Tenant Isolation) - const [tenantName, setTenantName] = useState('Barbearia Premium'); - const [slug, setSlug] = useState('barbearia-premium'); - const [phone, setPhone] = useState('(11) 99999-1234'); - const [email, setEmail] = useState('contato@barbeariapremium.com.br'); - const [address, setAddress] = useState('Rua das Flores, 123 - Centro'); - const [city, setCity] = useState('São Paulo'); + // General + const [tenantName, setTenantName] = useState(''); + const [slug, setSlug] = useState(''); + const [businessType, setBusinessType] = useState('barbearia'); + const [phone, setPhone] = useState(''); + const [email, setEmail] = useState(''); + const [address, setAddress] = useState(''); + const [city, setCity] = useState(''); const [state, setState] = useState('SP'); - const [zipCode, setZipCode] = useState('01001-000'); - const [description, setDescription] = useState('A melhor barbearia da cidade. Cortes modernos, barbas impecáveis e ambiente premium.'); + const [zipCode, setZipCode] = useState(''); + const [description, setDescription] = useState(''); - // States para Horários de Expediente + // Hours const [startTime, setStartTime] = useState('09:00'); const [endTime, setEndTime] = useState('19:00'); const [intervalTime, setIntervalTime] = useState(30); - const [workingDays, setWorkingDays] = useState([false, true, true, true, true, true, true]); // Dom-Sab + const [workingDays, setWorkingDays] = useState([false, true, true, true, true, true, true]); - // Carrega dados dinâmicos do Tenant logado - useEffect(() => { - const activeSlug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium'; - setSlug(activeSlug); - - const savedName = localStorage.getItem('agendapro_tenant_name') || 'Barbearia Premium'; - setTenantName(savedName); - - // Carrega dados isolados deste tenant específico - const savedPhone = localStorage.getItem(`agendapro_phone_${activeSlug}`) || (activeSlug === 'barbearia-premium' ? '(11) 99999-1234' : ''); - const savedEmail = localStorage.getItem(`agendapro_email_${activeSlug}`) || (activeSlug === 'barbearia-premium' ? 'contato@barbeariapremium.com.br' : ''); - const savedAddress = localStorage.getItem(`agendapro_address_${activeSlug}`) || (activeSlug === 'barbearia-premium' ? 'Rua das Flores, 123 - Centro' : ''); - const savedCity = localStorage.getItem(`agendapro_city_${activeSlug}`) || (activeSlug === 'barbearia-premium' ? 'São Paulo' : ''); - const savedState = localStorage.getItem(`agendapro_state_${activeSlug}`) || (activeSlug === 'barbearia-premium' ? 'SP' : 'SP'); - const savedZip = localStorage.getItem(`agendapro_zip_${activeSlug}`) || (activeSlug === 'barbearia-premium' ? '01001-000' : ''); - const savedDesc = localStorage.getItem(`agendapro_desc_${activeSlug}`) || (activeSlug === 'barbearia-premium' ? 'A melhor barbearia da cidade. Cortes modernos, barbas impecáveis e ambiente premium.' : 'Novo estabelecimento pronto para agendamentos online.'); - - setPhone(savedPhone); - setEmail(savedEmail); - setAddress(savedAddress); - setCity(savedCity); - setState(savedState); - setZipCode(savedZip); - setDescription(savedDesc); - - // Horários isolados por tenant - const savedStart = localStorage.getItem(`agendapro_start_time_${activeSlug}`) || '09:00'; - const savedEnd = localStorage.getItem(`agendapro_end_time_${activeSlug}`) || '19:00'; - const savedInterval = Number(localStorage.getItem(`agendapro_interval_${activeSlug}`) || '30'); - setStartTime(savedStart); - setEndTime(savedEnd); - setIntervalTime(savedInterval); - }, []); - - // States para Reservas Online + // Booking const [allowOnline, setAllowOnline] = useState(true); const [requireConfirmation, setRequireConfirmation] = useState(false); const [autoReminders, setAutoReminders] = useState(true); const [allowCancel, setAllowCancel] = useState(true); + const [bookingAdvanceMin, setBookingAdvanceMin] = useState(60); + const [bookingAdvanceMaxDays, setBookingAdvanceMaxDays] = useState(30); + const [cancellationLimitMin, setCancellationLimitMin] = useState(120); - // States para Notificações + // Notifications const [notifyNew, setNotifyNew] = useState(true); const [notifyCancel, setNotifyCancel] = useState(true); const [notifyClient, setNotifyClient] = useState(true); const [dailySummary, setDailySummary] = useState(false); const [weeklyReport, setWeeklyReport] = useState(true); + const [reminderMinutes, setReminderMinutes] = useState(60); + + // Appearance + const [themeColor, setThemeColor] = useState('#6C63FF'); + + // Security + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmNewPassword, setConfirmNewPassword] = useState(''); + const [securityError, setSecurityError] = useState(''); + const [securitySuccess, setSecuritySuccess] = useState(''); + + // Carrega dados do banco de dados + useEffect(() => { + const fetchSettings = async () => { + const activeSlug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium'; + setSlug(activeSlug); + + try { + const res = await fetch(`/api/tenant/settings?slug=${activeSlug}`); + if (res.ok) { + const data = await res.json(); + setTenantName(data.name || ''); + setBusinessType(data.businessType || 'barbearia'); + setPhone(data.phone || ''); + setEmail(data.email || ''); + setAddress(data.address || ''); + setCity(data.city || ''); + setState(data.state || 'SP'); + setZipCode(data.zipCode || ''); + setDescription(data.description || ''); + setThemeColor(data.themeColor || '#6C63FF'); + setStartTime(data.startTime || '09:00'); + setEndTime(data.endTime || '19:00'); + setIntervalTime(data.interval || 30); + setAllowOnline(data.allowOnlineBooking !== false); + setRequireConfirmation(data.requireConfirmation || false); + setAutoReminders(data.sendReminders !== false); + setAllowCancel(data.allowCancellation !== false); + setBookingAdvanceMin(data.bookingAdvanceMin || 60); + setBookingAdvanceMaxDays(data.bookingAdvanceMaxDays || 30); + setCancellationLimitMin(data.cancellationLimitMin || 120); + setNotifyNew(data.notifyNewAppointment !== false); + setNotifyCancel(data.notifyCancellation !== false); + setNotifyClient(data.notifyClientReminder !== false); + setDailySummary(data.dailySummary || false); + setWeeklyReport(data.weeklyReport !== false); + setReminderMinutes(data.reminderMinutesBefore || 60); + + // Converte workingDays [1,2,3,4,5,6] para array booleano [dom,seg,...,sab] + const wd = data.workingDays || [1, 2, 3, 4, 5, 6]; + setWorkingDays([0, 1, 2, 3, 4, 5, 6].map(d => wd.includes(d))); + + // Aplica cor do tema + if (data.themeColor) { + document.documentElement.style.setProperty('--accent', data.themeColor); + document.documentElement.style.setProperty('--accent-glow', `${data.themeColor}26`); + } + } + } catch (err) { + console.error('Erro ao carregar configurações:', err); + } finally { + setLoading(false); + } + }; + + fetchSettings(); + }, []); const publicUrl = typeof window !== 'undefined' ? `${window.location.origin}/${slug}/agendar` : `/${slug}/agendar`; - const handleSave = () => { - // 1. Atualiza dados de sessão do tenant - localStorage.setItem('agendapro_tenant_name', tenantName); - localStorage.setItem('agendapro_tenant_slug', slug); + const handleSave = async () => { + setSaving(true); - // 2. Persiste dados com isolamento por slug (Tenant Isolation) - localStorage.setItem(`agendapro_phone_${slug}`, phone); - localStorage.setItem(`agendapro_email_${slug}`, email); - localStorage.setItem(`agendapro_address_${slug}`, address); - localStorage.setItem(`agendapro_city_${slug}`, city); - localStorage.setItem(`agendapro_state_${slug}`, state); - localStorage.setItem(`agendapro_zip_${slug}`, zipCode); - localStorage.setItem(`agendapro_desc_${slug}`, description); + // Converte workingDays boolean[] para int[] + const wdInts = workingDays.map((active, i) => active ? i : -1).filter(i => i >= 0); - // Horários isolados - localStorage.setItem(`agendapro_start_time_${slug}`, startTime); - localStorage.setItem(`agendapro_end_time_${slug}`, endTime); - localStorage.setItem(`agendapro_interval_${slug}`, String(intervalTime)); - - // Atualiza cookies dinâmicos para a página pública - document.cookie = `agendapro_tenant_slug=${slug}; path=/; max-age=2592000`; - document.cookie = `agendapro_start_time_${slug}=${startTime}; path=/; max-age=2592000`; - document.cookie = `agendapro_end_time_${slug}=${endTime}; path=/; max-age=2592000`; - document.cookie = `agendapro_interval_${slug}=${intervalTime}; path=/; max-age=2592000`; + try { + const res = await fetch('/api/tenant/settings', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + slug, + name: tenantName, + businessType, + phone, email, address, city, state, zipCode, description, + themeColor, + workingDays: wdInts, + startTime, endTime, + interval: intervalTime, + allowOnlineBooking: allowOnline, + requireConfirmation, + sendReminders: autoReminders, + reminderMinutesBefore: reminderMinutes, + allowCancellation: allowCancel, + bookingAdvanceMin, + bookingAdvanceMaxDays, + cancellationLimitMin, + notifyNewAppointment: notifyNew, + notifyCancellation: notifyCancel, + notifyClientReminder: notifyClient, + dailySummary, + weeklyReport, + }) + }); - // Dispara evento customizado para notificar sidebar e topbar sobre alteração de nome do estabelecimento - if (typeof window !== 'undefined') { - window.dispatchEvent(new Event('storage')); + if (res.ok) { + // Atualiza localStorage para manter compatibilidade + localStorage.setItem('agendapro_tenant_name', tenantName); + if (typeof window !== 'undefined') window.dispatchEvent(new Event('storage')); + + setSaved(true); + setTimeout(() => setSaved(false), 2000); + } + } catch (err) { + console.error('Erro ao salvar:', err); + } finally { + setSaving(false); + } + }; + + const handleChangePassword = async () => { + setSecurityError(''); + setSecuritySuccess(''); + + if (!currentPassword || !newPassword) { + setSecurityError('Preencha todos os campos.'); + return; + } + if (newPassword !== confirmNewPassword) { + setSecurityError('A nova senha e a confirmação não conferem.'); + return; } - setSaved(true); - setTimeout(() => setSaved(false), 2000); + try { + const token = localStorage.getItem('agendapro_token'); + const res = await fetch('/api/auth/change-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token, currentPassword, newPassword }) + }); + const data = await res.json(); + + if (res.ok) { + setSecuritySuccess('Senha alterada com sucesso!'); + setCurrentPassword(''); + setNewPassword(''); + setConfirmNewPassword(''); + } else { + setSecurityError(data.error || 'Erro ao alterar senha.'); + } + } catch (err) { + setSecurityError('Falha de conexão com o servidor.'); + } }; + if (loading) return
Carregando configurações...
; + return (
@@ -150,13 +243,14 @@ export default function Settings() {
- setBusinessType(e.target.value)}> {businessTypes.map(bt => )}
- setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-'))} /> +
@@ -181,12 +275,9 @@ export default function Settings() {
@@ -199,8 +290,8 @@ export default function Settings() {