feat: conectar Equipe e Agenda ao banco de dados PostgreSQL
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m41s
Details
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m41s
Details
This commit is contained in:
parent
04a556290e
commit
c4871262f9
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, string> = {
|
||||
|
|
@ -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<any[]>([]);
|
||||
const [professionals, setProfessionals] = useState<any[]>([]);
|
||||
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 <div className="p-8 text-center" style={{color: 'var(--text-muted)'}}>Carregando agenda...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="slide-up">
|
||||
<div className="flex-between mb-6">
|
||||
|
|
@ -86,8 +89,14 @@ export default function Agenda() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{professionals.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<h3>Sua agenda está vazia</h3>
|
||||
<p>Você precisa adicionar um profissional na aba Equipe antes de agendar clientes.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '70px repeat(2, 1fr)', borderBottom: '1px solid var(--border-color)' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: `70px repeat(${professionals.length}, 1fr)`, borderBottom: '1px solid var(--border-color)' }}>
|
||||
<div style={{ padding: '14px', borderRight: '1px solid var(--border-color)' }} />
|
||||
{professionals.map(p => (
|
||||
<div key={p.id} style={{
|
||||
|
|
@ -102,7 +111,7 @@ export default function Agenda() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '70px repeat(2, 1fr)', position: 'relative' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: `70px repeat(${professionals.length}, 1fr)`, position: 'relative' }}>
|
||||
<div>
|
||||
{hours.map(h => (
|
||||
<div key={h} style={{
|
||||
|
|
@ -116,14 +125,7 @@ export default function Agenda() {
|
|||
</div>
|
||||
|
||||
{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 (
|
||||
<div key={prof.id} style={{ position: 'relative', borderRight: '1px solid var(--border-color)' }}>
|
||||
|
|
@ -140,8 +142,8 @@ export default function Agenda() {
|
|||
<div key={apt.id} style={{
|
||||
position: 'absolute', top: `${top}px`, left: '4px', right: '4px',
|
||||
height: `${height}px`, borderRadius: 'var(--radius-sm)',
|
||||
background: statusColors[apt.status],
|
||||
borderLeft: `3px solid ${statusBorders[apt.status]}`,
|
||||
background: statusColors[apt.status] || statusColors.confirmed,
|
||||
borderLeft: `3px solid ${statusBorders[apt.status] || statusBorders.confirmed}`,
|
||||
padding: isSmall ? '4px 8px' : '6px 10px', cursor: 'pointer', overflow: 'hidden',
|
||||
transition: 'all 0.2s', zIndex: 2, display: 'flex', flexDirection: isSmall ? 'row' : 'column',
|
||||
alignItems: isSmall ? 'center' : 'flex-start', gap: isSmall ? '8px' : '0'
|
||||
|
|
@ -168,6 +170,7 @@ export default function Agenda() {
|
|||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showModal && (
|
||||
<div className="modal-overlay" onClick={() => setShowModal(false)}>
|
||||
|
|
@ -204,14 +207,15 @@ export default function Agenda() {
|
|||
<div className="form-group">
|
||||
<label className="form-label">Profissional</label>
|
||||
<select className="form-select" value={newProf} onChange={e => setNewProf(e.target.value)}>
|
||||
<option value="1">Carlos Silva</option>
|
||||
<option value="2">Rafael Santos</option>
|
||||
{professionals.map(p => (
|
||||
<option key={p.id} value={p.id}>{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="grid-2">
|
||||
<div className="form-group">
|
||||
<label className="form-label"><Calendar size={14} style={{ display: 'inline', marginRight: 4 }} />Data</label>
|
||||
<input className="form-input" type="date" defaultValue={today.toISOString().split('T')[0]} />
|
||||
<input className="form-input" type="date" defaultValue={today.toISOString().split('T')[0]} readOnly />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label"><Clock size={14} style={{ display: 'inline', marginRight: 4 }} />Horário</label>
|
||||
|
|
@ -225,7 +229,7 @@ export default function Agenda() {
|
|||
</div>
|
||||
<div className="modal-footer">
|
||||
<button className="btn btn-secondary" onClick={() => setShowModal(false)}>Cancelar</button>
|
||||
<button className="btn btn-primary" onClick={() => {
|
||||
<button className="btn btn-primary" onClick={async () => {
|
||||
const [h, m] = newTime.split(':').map(Number);
|
||||
const serviceName = newService.split(' - ')[0];
|
||||
const priceMatch = newService.match(/R\$ (\d+)/);
|
||||
|
|
@ -233,29 +237,32 @@ export default function Agenda() {
|
|||
const durMatch = newService.match(/\((\d+)min\)/);
|
||||
const duration = durMatch ? parseInt(durMatch[1]) : 30;
|
||||
|
||||
const newApt = {
|
||||
id: String(Date.now()),
|
||||
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/tenant/agenda', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
slug,
|
||||
professionalId: newProf,
|
||||
client: newClient || 'Cliente Avulso',
|
||||
service: serviceName,
|
||||
startHour: h,
|
||||
startMin: m,
|
||||
duration: duration,
|
||||
status: 'confirmed',
|
||||
price: price
|
||||
};
|
||||
|
||||
// Volta pra hoje pra ver o agendamento cair no grid
|
||||
const activeSlug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
|
||||
const updated = [...appointments, newApt];
|
||||
setCurrentDate(new Date());
|
||||
setAppointments(updated);
|
||||
localStorage.setItem(`agendapro_appointments_${activeSlug}`, JSON.stringify(updated));
|
||||
duration,
|
||||
price
|
||||
})
|
||||
});
|
||||
if (res.ok) {
|
||||
fetchAgenda();
|
||||
setShowModal(false);
|
||||
|
||||
// Reseta form
|
||||
setNewClient('');
|
||||
setNewPhone('');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}>Agendar</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<string, string>; 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<Professional[]>(initialTeam);
|
||||
const [team, setTeam] = useState<Professional[]>([]);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingProf, setEditingProf] = useState<Professional | null>(null);
|
||||
const [formData, setFormData] = useState<Professional>(emptyProfessional);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(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);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
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 <div className="p-8 text-center" style={{color: 'var(--text-muted)'}}>Carregando equipe...</div>;
|
||||
|
||||
return (
|
||||
<div className="slide-up">
|
||||
<div className="flex-between mb-6">
|
||||
|
|
@ -82,7 +106,7 @@ export default function Team() {
|
|||
}}>
|
||||
<AlertTriangle size={36} color="var(--warning)" />
|
||||
<p style={{ fontSize: '15px', fontWeight: 600, textAlign: 'center' }}>Excluir {p.name}?</p>
|
||||
<p style={{ fontSize: '13px', color: 'var(--text-secondary)', textAlign: 'center' }}>Essa ação não pode ser desfeita.</p>
|
||||
<p style={{ fontSize: '13px', color: 'var(--text-secondary)', textAlign: 'center' }}>Essa ação removerá o profissional de toda a agenda.</p>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button className="btn btn-danger" onClick={() => handleDelete(p.id)}>Sim, excluir</button>
|
||||
<button className="btn btn-secondary" onClick={() => setDeleteConfirm(null)}>Cancelar</button>
|
||||
|
|
@ -138,10 +162,10 @@ export default function Team() {
|
|||
<Clock size={12} style={{ display: 'inline', marginRight: '4px' }} /> Horário de Trabalho
|
||||
</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '6px' }}>
|
||||
{Object.entries(p.schedule).map(([day, hours]) => (
|
||||
{Object.entries(p.schedule || {}).map(([day, hours]) => (
|
||||
<div key={day} style={{ fontSize: '12px', padding: '4px 8px', background: 'var(--bg-primary)', borderRadius: '4px', textAlign: 'center' }}>
|
||||
<span style={{ color: 'var(--text-muted)', textTransform: 'capitalize' }}>{day} </span>
|
||||
<span style={{ fontWeight: 600 }}>{hours}</span>
|
||||
<span style={{ fontWeight: 600 }}>{hours as string}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue