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 { useState, useEffect } from 'react';
|
||||||
import { Plus, ChevronLeft, ChevronRight, X, Clock, User, Calendar } from 'lucide-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 hours = Array.from({ length: 12 }, (_, i) => i + 8);
|
||||||
|
|
||||||
const statusColors: Record<string, string> = {
|
const statusColors: Record<string, string> = {
|
||||||
|
|
@ -33,31 +20,43 @@ export default function Agenda() {
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const [currentDate, setCurrentDate] = useState(today);
|
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(() => {
|
useEffect(() => {
|
||||||
const activeSlug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
|
fetchAgenda();
|
||||||
const savedAppts = localStorage.getItem(`agendapro_appointments_${activeSlug}`);
|
}, [currentDate]);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// States do Modal de Novo Agendamento
|
// States do Modal de Novo Agendamento
|
||||||
const [newClient, setNewClient] = useState('');
|
const [newClient, setNewClient] = useState('');
|
||||||
const [newPhone, setNewPhone] = useState('');
|
const [newPhone, setNewPhone] = useState('');
|
||||||
const [newService, setNewService] = useState('Corte Masculino - R$ 55,00 (40min)');
|
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');
|
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 dateStr = currentDate.toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
|
||||||
|
|
||||||
const changeDate = (days: number) => {
|
const changeDate = (days: number) => {
|
||||||
|
|
@ -70,6 +69,10 @@ export default function Agenda() {
|
||||||
setCurrentDate(new Date());
|
setCurrentDate(new Date());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (loading && professionals.length === 0) {
|
||||||
|
return <div className="p-8 text-center" style={{color: 'var(--text-muted)'}}>Carregando agenda...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="slide-up">
|
<div className="slide-up">
|
||||||
<div className="flex-between mb-6">
|
<div className="flex-between mb-6">
|
||||||
|
|
@ -86,8 +89,14 @@ export default function Agenda() {
|
||||||
</div>
|
</div>
|
||||||
</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 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)' }} />
|
<div style={{ padding: '14px', borderRight: '1px solid var(--border-color)' }} />
|
||||||
{professionals.map(p => (
|
{professionals.map(p => (
|
||||||
<div key={p.id} style={{
|
<div key={p.id} style={{
|
||||||
|
|
@ -102,7 +111,7 @@ export default function Agenda() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
<div>
|
||||||
{hours.map(h => (
|
{hours.map(h => (
|
||||||
<div key={h} style={{
|
<div key={h} style={{
|
||||||
|
|
@ -116,14 +125,7 @@ export default function Agenda() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{professionals.map(prof => {
|
{professionals.map(prof => {
|
||||||
// Lógica simples para simular agendamentos diferentes em dias diferentes
|
const visibleAppointments = appointments.filter(a => a.professionalId === prof.id);
|
||||||
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)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={prof.id} style={{ position: 'relative', borderRight: '1px solid var(--border-color)' }}>
|
<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={{
|
<div key={apt.id} style={{
|
||||||
position: 'absolute', top: `${top}px`, left: '4px', right: '4px',
|
position: 'absolute', top: `${top}px`, left: '4px', right: '4px',
|
||||||
height: `${height}px`, borderRadius: 'var(--radius-sm)',
|
height: `${height}px`, borderRadius: 'var(--radius-sm)',
|
||||||
background: statusColors[apt.status],
|
background: statusColors[apt.status] || statusColors.confirmed,
|
||||||
borderLeft: `3px solid ${statusBorders[apt.status]}`,
|
borderLeft: `3px solid ${statusBorders[apt.status] || statusBorders.confirmed}`,
|
||||||
padding: isSmall ? '4px 8px' : '6px 10px', cursor: 'pointer', overflow: 'hidden',
|
padding: isSmall ? '4px 8px' : '6px 10px', cursor: 'pointer', overflow: 'hidden',
|
||||||
transition: 'all 0.2s', zIndex: 2, display: 'flex', flexDirection: isSmall ? 'row' : 'column',
|
transition: 'all 0.2s', zIndex: 2, display: 'flex', flexDirection: isSmall ? 'row' : 'column',
|
||||||
alignItems: isSmall ? 'center' : 'flex-start', gap: isSmall ? '8px' : '0'
|
alignItems: isSmall ? 'center' : 'flex-start', gap: isSmall ? '8px' : '0'
|
||||||
|
|
@ -168,6 +170,7 @@ export default function Agenda() {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{showModal && (
|
{showModal && (
|
||||||
<div className="modal-overlay" onClick={() => setShowModal(false)}>
|
<div className="modal-overlay" onClick={() => setShowModal(false)}>
|
||||||
|
|
@ -204,14 +207,15 @@ export default function Agenda() {
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Profissional</label>
|
<label className="form-label">Profissional</label>
|
||||||
<select className="form-select" value={newProf} onChange={e => setNewProf(e.target.value)}>
|
<select className="form-select" value={newProf} onChange={e => setNewProf(e.target.value)}>
|
||||||
<option value="1">Carlos Silva</option>
|
{professionals.map(p => (
|
||||||
<option value="2">Rafael Santos</option>
|
<option key={p.id} value={p.id}>{p.name}</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label"><Calendar size={14} style={{ display: 'inline', marginRight: 4 }} />Data</label>
|
<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>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label"><Clock size={14} style={{ display: 'inline', marginRight: 4 }} />Horário</label>
|
<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>
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
<button className="btn btn-secondary" onClick={() => setShowModal(false)}>Cancelar</button>
|
<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 [h, m] = newTime.split(':').map(Number);
|
||||||
const serviceName = newService.split(' - ')[0];
|
const serviceName = newService.split(' - ')[0];
|
||||||
const priceMatch = newService.match(/R\$ (\d+)/);
|
const priceMatch = newService.match(/R\$ (\d+)/);
|
||||||
|
|
@ -233,29 +237,32 @@ export default function Agenda() {
|
||||||
const durMatch = newService.match(/\((\d+)min\)/);
|
const durMatch = newService.match(/\((\d+)min\)/);
|
||||||
const duration = durMatch ? parseInt(durMatch[1]) : 30;
|
const duration = durMatch ? parseInt(durMatch[1]) : 30;
|
||||||
|
|
||||||
const newApt = {
|
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
|
||||||
id: String(Date.now()),
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/tenant/agenda', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
slug,
|
||||||
professionalId: newProf,
|
professionalId: newProf,
|
||||||
client: newClient || 'Cliente Avulso',
|
client: newClient || 'Cliente Avulso',
|
||||||
service: serviceName,
|
service: serviceName,
|
||||||
startHour: h,
|
startHour: h,
|
||||||
startMin: m,
|
startMin: m,
|
||||||
duration: duration,
|
duration,
|
||||||
status: 'confirmed',
|
price
|
||||||
price: price
|
})
|
||||||
};
|
});
|
||||||
|
if (res.ok) {
|
||||||
// Volta pra hoje pra ver o agendamento cair no grid
|
fetchAgenda();
|
||||||
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));
|
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
|
|
||||||
// Reseta form
|
|
||||||
setNewClient('');
|
setNewClient('');
|
||||||
setNewPhone('');
|
setNewPhone('');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}}>Agendar</button>
|
}}>Agendar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Plus, X, Clock, Edit, Trash2, Star, AlertTriangle } from 'lucide-react';
|
import { Plus, X, Clock, Edit, Trash2, Star, AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
interface Professional {
|
interface Professional {
|
||||||
|
|
@ -7,21 +7,6 @@ interface Professional {
|
||||||
services: string[]; schedule: Record<string, string>; rating: number; totalClients: number;
|
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 = {
|
const emptyProfessional: Professional = {
|
||||||
id: '', name: '', role: '', phone: '', email: '', commission: 30,
|
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' },
|
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() {
|
export default function Team() {
|
||||||
const [team, setTeam] = useState<Professional[]>(initialTeam);
|
const [team, setTeam] = useState<Professional[]>([]);
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [editingProf, setEditingProf] = useState<Professional | null>(null);
|
const [editingProf, setEditingProf] = useState<Professional | null>(null);
|
||||||
const [formData, setFormData] = useState<Professional>(emptyProfessional);
|
const [formData, setFormData] = useState<Professional>(emptyProfessional);
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
|
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 = () => {
|
const openAdd = () => {
|
||||||
setEditingProf(null);
|
setEditingProf(null);
|
||||||
setFormData({ ...emptyProfessional, id: Date.now().toString() });
|
setFormData({ ...emptyProfessional });
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -47,22 +52,41 @@ export default function Team() {
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = async () => {
|
||||||
if (!formData.name.trim()) return;
|
if (!formData.name.trim()) return;
|
||||||
if (editingProf) {
|
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
|
||||||
setTeam(prev => prev.map(p => p.id === editingProf.id ? { ...formData } : p));
|
|
||||||
} else {
|
const method = editingProf ? 'PUT' : 'POST';
|
||||||
setTeam(prev => [...prev, { ...formData }]);
|
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);
|
setShowModal(false);
|
||||||
setEditingProf(null);
|
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));
|
setTeam(prev => prev.filter(p => p.id !== id));
|
||||||
setDeleteConfirm(null);
|
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 (
|
return (
|
||||||
<div className="slide-up">
|
<div className="slide-up">
|
||||||
<div className="flex-between mb-6">
|
<div className="flex-between mb-6">
|
||||||
|
|
@ -82,7 +106,7 @@ export default function Team() {
|
||||||
}}>
|
}}>
|
||||||
<AlertTriangle size={36} color="var(--warning)" />
|
<AlertTriangle size={36} color="var(--warning)" />
|
||||||
<p style={{ fontSize: '15px', fontWeight: 600, textAlign: 'center' }}>Excluir {p.name}?</p>
|
<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' }}>
|
<div style={{ display: 'flex', gap: '10px' }}>
|
||||||
<button className="btn btn-danger" onClick={() => handleDelete(p.id)}>Sim, excluir</button>
|
<button className="btn btn-danger" onClick={() => handleDelete(p.id)}>Sim, excluir</button>
|
||||||
<button className="btn btn-secondary" onClick={() => setDeleteConfirm(null)}>Cancelar</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
|
<Clock size={12} style={{ display: 'inline', marginRight: '4px' }} /> Horário de Trabalho
|
||||||
</h4>
|
</h4>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '6px' }}>
|
<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' }}>
|
<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={{ color: 'var(--text-muted)', textTransform: 'capitalize' }}>{day} </span>
|
||||||
<span style={{ fontWeight: 600 }}>{hours}</span>
|
<span style={{ fontWeight: 600 }}>{hours as string}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue