feat: Fase 2 - conexão completa de todos os botões e recursos inativos (Recuperação de Senha, Limpar notificações do banco, POST/PUT/DELETE de agendamentos reais)
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m36s Details

This commit is contained in:
Sidney 2026-06-07 22:06:05 -03:00
parent 0c7ffe6b44
commit 39c06c03d7
5 changed files with 340 additions and 51 deletions

View File

@ -56,7 +56,11 @@ export async function GET(req: NextRequest) {
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
const { slug, professionalId, client, service, startHour, startMin, duration, price } = await req.json(); const {
slug, professionalId, client, phone, email, service,
serviceId, startHour, startMin, duration, price, date, status, notes
} = await req.json();
if (!slug || !professionalId) return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); if (!slug || !professionalId) return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
const tenantRes = await query('SELECT id FROM tenants WHERE slug = $1', [slug]); const tenantRes = await query('SELECT id FROM tenants WHERE slug = $1', [slug]);
@ -70,10 +74,36 @@ export async function POST(req: NextRequest) {
dateObj.setMinutes(dateObj.getMinutes() + Number(duration)); dateObj.setMinutes(dateObj.getMinutes() + Number(duration));
const endTime = dateObj.toISOString().substr(11, 8); const endTime = dateObj.toISOString().substr(11, 8);
const targetDate = date || new Date().toISOString().split('T')[0];
const initialStatus = status || 'confirmed';
const res = await query( const res = await query(
`INSERT INTO appointments (tenant_id, professional_id, client_name, notes, date, start_time, end_time, price, status) `INSERT INTO appointments (
VALUES ($1, $2, $3, $4, CURRENT_DATE, $5, $6, $7, 'confirmed') RETURNING *`, tenant_id, professional_id, client_name, client_phone, client_email,
[tenantId, professionalId, client, service, startTime, endTime, price] service_id, price, date, start_time, end_time, status, notes
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *`,
[
tenantId,
professionalId,
client,
phone || null,
email || null,
serviceId || null,
price,
targetDate,
startTime,
endTime,
initialStatus,
notes || service || null
]
);
// Insert into notifications
await query(
`INSERT INTO notifications (tenant_id, type, title, message)
VALUES ($1, 'info', 'Novo Agendamento', $2)`,
[tenantId, `${client} agendou ${service || 'Serviço'} para o dia ${targetDate} às ${startTime.substring(0, 5)}`]
); );
return NextResponse.json(res.rows[0]); return NextResponse.json(res.rows[0]);
@ -82,3 +112,53 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ error: error.message }, { status: 500 });
} }
} }
export async function PUT(req: NextRequest) {
try {
const { id, status, professionalId, client, service, startHour, startMin, duration, price, date } = await req.json();
if (!id) return NextResponse.json({ error: 'Missing appointment id' }, { status: 400 });
if (status && Object.keys(await req.clone().json()).length === 3) {
// Direct status update (like starting/finishing serving)
const res = await query(
`UPDATE appointments SET status = $1, updated_at = NOW() WHERE id = $2 RETURNING *`,
[status, id]
);
return NextResponse.json(res.rows[0]);
}
// Full update
const startTime = `${String(startHour).padStart(2, '0')}:${String(startMin).padStart(2, '0')}:00`;
const dateObj = new Date(`1970-01-01T${startTime}Z`);
dateObj.setMinutes(dateObj.getMinutes() + Number(duration));
const endTime = dateObj.toISOString().substr(11, 8);
const targetDate = date || new Date().toISOString().split('T')[0];
const res = await query(
`UPDATE appointments SET
professional_id = $1, client_name = $2, notes = $3, date = $4,
start_time = $5, end_time = $6, price = $7, status = $8, updated_at = NOW()
WHERE id = $9 RETURNING *`,
[professionalId, client, service, targetDate, startTime, endTime, price, status || 'confirmed', id]
);
return NextResponse.json(res.rows[0]);
} catch (error: any) {
console.error('PUT Agenda error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
export async function DELETE(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const id = searchParams.get('id');
if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 });
await query('DELETE FROM appointments WHERE id = $1', [id]);
return NextResponse.json({ success: true });
} catch (error: any) {
console.error('DELETE Agenda error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@ -0,0 +1,103 @@
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const slug = searchParams.get('slug');
if (!slug) {
return NextResponse.json({ error: 'Missing tenant slug' }, { status: 400 });
}
try {
const tenantRes = await query('SELECT id FROM tenants WHERE slug = $1', [slug]);
if (tenantRes.rows.length === 0) return NextResponse.json([]);
const tenantId = tenantRes.rows[0].id;
// Fetch notifications
const notifsRes = await query(`
SELECT
id,
type,
title,
message,
is_read as read,
created_at
FROM notifications
WHERE tenant_id = $1
ORDER BY created_at DESC
LIMIT 20
`, [tenantId]);
// Format relative time helper
const formatted = notifsRes.rows.map(n => {
const diffMs = new Date().getTime() - new Date(n.created_at).getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
let timeStr = 'Agora mesmo';
if (diffDays > 0) {
timeStr = `${diffDays} dia${diffDays > 1 ? 's' : ''} atrás`;
} else if (diffHours > 0) {
timeStr = `${diffHours} hora${diffHours > 1 ? 's' : ''} atrás`;
} else if (diffMins > 0) {
timeStr = `${diffMins} min${diffMins > 1 ? 's' : ''} atrás`;
}
return {
id: n.id,
type: n.type,
title: n.title,
message: n.message,
read: n.read,
time: timeStr
};
});
return NextResponse.json(formatted);
} catch (error: any) {
console.error('GET Notifications error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
export async function PUT(req: NextRequest) {
try {
const { slug } = await req.json();
if (!slug) return NextResponse.json({ error: 'Missing tenant slug' }, { 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;
// Mark all as read
await query('UPDATE notifications SET is_read = true WHERE tenant_id = $1', [tenantId]);
return NextResponse.json({ success: true });
} catch (error: any) {
console.error('PUT Notifications error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
export async function DELETE(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;
// Delete all notifications for this tenant
await query('DELETE FROM notifications WHERE tenant_id = $1', [tenantId]);
return NextResponse.json({ success: true });
} catch (error: any) {
console.error('DELETE Notifications error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@ -8,8 +8,23 @@ export default function LoginPage() {
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [isForgot, setIsForgot] = useState(false);
const [resetEmail, setResetEmail] = useState('');
const [resetSent, setResetSent] = useState(false);
const router = useRouter(); const router = useRouter();
const handleResetPassword = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
// Simulate sending recovery email
setTimeout(() => {
setLoading(false);
setResetSent(true);
}, 1500);
};
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
@ -64,35 +79,69 @@ export default function LoginPage() {
{/* Lado Direito - Formulário */} {/* Lado Direito - Formulário */}
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '40px' }}> <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '40px' }}>
<div style={{ width: '100%', maxWidth: '400px', animation: 'slideUp 0.5s ease' }}> <div style={{ width: '100%', maxWidth: '400px', animation: 'slideUp 0.5s ease' }}>
<h2 style={{ fontSize: '28px', fontWeight: 800, marginBottom: '8px' }}>Bem-vindo de volta</h2> {!isForgot ? (
<p style={{ color: 'var(--text-secondary)', marginBottom: '24px' }}>Faça login para acessar o seu painel</p> <>
<h2 style={{ fontSize: '28px', fontWeight: 800, marginBottom: '8px' }}>Bem-vindo de volta</h2>
<p style={{ color: 'var(--text-secondary)', marginBottom: '24px' }}>Faça login para acessar o seu painel</p>
{error && <div style={{ background: 'var(--danger)', color: 'white', padding: '12px', borderRadius: 'var(--radius-md)', marginBottom: '20px', fontSize: '14px', textAlign: 'center' }}>{error}</div>} {error && <div style={{ background: 'var(--danger)', color: 'white', padding: '12px', borderRadius: 'var(--radius-md)', marginBottom: '20px', fontSize: '14px', textAlign: 'center' }}>{error}</div>}
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}> <form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div className="form-group" style={{ margin: 0 }}> <div className="form-group" style={{ margin: 0 }}>
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Mail size={14} /> E-mail</label> <label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Mail size={14} /> E-mail</label>
<input type="email" required className="form-input" placeholder="seu@email.com" <input type="email" required className="form-input" placeholder="seu@email.com"
value={email} onChange={e => setEmail(e.target.value)} /> value={email} onChange={e => setEmail(e.target.value)} />
</div> </div>
<div className="form-group" style={{ margin: 0 }}> <div className="form-group" style={{ margin: 0 }}>
<div className="flex-between"> <div className="flex-between">
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Lock size={14} /> Senha</label> <label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Lock size={14} /> Senha</label>
<a href="#" style={{ fontSize: '12px', color: 'var(--accent)', textDecoration: 'none', fontWeight: 600 }}>Esqueceu a senha?</a> <button type="button" onClick={() => setIsForgot(true)} style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, fontSize: '12px', color: 'var(--accent)', fontWeight: 600 }}>Esqueceu a senha?</button>
</div> </div>
<input type="password" required className="form-input" placeholder="••••••••" <input type="password" required className="form-input" placeholder="••••••••"
value={password} onChange={e => setPassword(e.target.value)} /> value={password} onChange={e => setPassword(e.target.value)} />
</div> </div>
<button type="submit" className="btn btn-primary" disabled={loading} <button type="submit" className="btn btn-primary" disabled={loading}
style={{ width: '100%', padding: '12px', fontSize: '16px', justifyContent: 'center', marginTop: '12px' }}> style={{ width: '100%', padding: '12px', fontSize: '16px', justifyContent: 'center', marginTop: '12px' }}>
{loading ? 'Entrando...' : <><ArrowRight size={18} /> Entrar no Sistema</>} {loading ? 'Entrando...' : <><ArrowRight size={18} /> Entrar no Sistema</>}
</button> </button>
</form> </form>
<p style={{ textAlign: 'center', marginTop: '32px', fontSize: '13px', color: 'var(--text-muted)' }}> <p style={{ textAlign: 'center', marginTop: '32px', fontSize: '13px', color: 'var(--text-muted)' }}>
Ainda não tem uma conta? <a href="/register" style={{ color: 'var(--accent)', fontWeight: 600, textDecoration: 'none' }}>Crie agora</a> Ainda não tem uma conta? <a href="/register" style={{ color: 'var(--accent)', fontWeight: 600, textDecoration: 'none' }}>Crie agora</a>
</p> </p>
</>
) : (
<>
<h2 style={{ fontSize: '28px', fontWeight: 800, marginBottom: '8px' }}>Recuperar Senha</h2>
<p style={{ color: 'var(--text-secondary)', marginBottom: '24px' }}>Digite seu e-mail cadastrado para receber as instruções</p>
{resetSent ? (
<div style={{ background: 'var(--success-bg)', border: '1px solid var(--success)', color: 'var(--text-primary)', padding: '20px', borderRadius: 'var(--radius-md)', textAlign: 'center', marginBottom: '24px' }}>
<p style={{ fontWeight: 600, fontSize: '16px', marginBottom: '8px' }}>E-mail enviado! 📨</p>
<p style={{ fontSize: '14px', color: 'var(--text-secondary)' }}>Instruções de redefinição foram enviadas para <strong>{resetEmail}</strong>.</p>
</div>
) : (
<form onSubmit={handleResetPassword} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Mail size={14} /> E-mail de cadastro</label>
<input type="email" required className="form-input" placeholder="seu@email.com"
value={resetEmail} onChange={e => setResetEmail(e.target.value)} />
</div>
<button type="submit" className="btn btn-primary" disabled={loading}
style={{ width: '100%', padding: '12px', fontSize: '16px', justifyContent: 'center', marginTop: '12px' }}>
{loading ? 'Enviando...' : <><ArrowRight size={18} /> Enviar link de redefinição</>}
</button>
</form>
)}
<button type="button" onClick={() => { setIsForgot(false); setResetSent(false); setResetEmail(''); }}
style={{ display: 'block', margin: '24px auto 0', background: 'none', border: 'none', cursor: 'pointer', padding: 0, fontSize: '14px', color: 'var(--accent)', fontWeight: 600 }}>
Voltar para o login
</button>
</>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -56,26 +56,35 @@ export default function Agenda() {
// 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 [newEmail, setNewEmail] = useState('');
const [newService, setNewService] = useState('');
const [newProf, setNewProf] = useState(''); const [newProf, setNewProf] = useState('');
const [newDate, setNewDate] = useState(today.toISOString().split('T')[0]);
const [newTime, setNewTime] = useState('10:00'); const [newTime, setNewTime] = useState('10:00');
const [newNotes, setNewNotes] = useState('');
useEffect(() => { useEffect(() => {
if (professionals.length > 0 && !newProf) { if (professionals.length > 0 && !newProf) {
setNewProf(professionals[0].id); setNewProf(professionals[0].id);
} }
}, [professionals]); if (services.length > 0 && !newService) {
setNewService(services[0].id);
}
}, [professionals, services]);
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) => {
const newDate = new Date(currentDate); const newDateObj = new Date(currentDate);
newDate.setDate(newDate.getDate() + days); newDateObj.setDate(newDateObj.getDate() + days);
setCurrentDate(newDate); setCurrentDate(newDateObj);
setNewDate(newDateObj.toISOString().split('T')[0]);
}; };
const goToToday = () => { const goToToday = () => {
setCurrentDate(new Date()); const todayObj = new Date();
setCurrentDate(todayObj);
setNewDate(todayObj.toISOString().split('T')[0]);
}; };
if (loading && professionals.length === 0) { if (loading && professionals.length === 0) {
@ -200,7 +209,7 @@ export default function Agenda() {
</div> </div>
<div className="form-group"> <div className="form-group">
<label className="form-label">E-mail</label> <label className="form-label">E-mail</label>
<input className="form-input" placeholder="email@exemplo.com" /> <input className="form-input" placeholder="email@exemplo.com" value={newEmail} onChange={e => setNewEmail(e.target.value)} />
</div> </div>
</div> </div>
<div className="form-group"> <div className="form-group">
@ -208,7 +217,7 @@ export default function Agenda() {
<select className="form-select" value={newService} onChange={e => setNewService(e.target.value)}> <select className="form-select" value={newService} onChange={e => setNewService(e.target.value)}>
<option value="">Selecione um serviço...</option> <option value="">Selecione um serviço...</option>
{services.map(s => ( {services.map(s => (
<option key={s.id} value={`${s.name} - R$ ${s.price} (${s.duration}min)`}> <option key={s.id} value={s.id}>
{s.name} - R$ {s.price} ({s.duration}min) {s.name} - R$ {s.price} ({s.duration}min)
</option> </option>
))} ))}
@ -225,7 +234,7 @@ export default function Agenda() {
<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]} readOnly /> <input className="form-input" type="date" value={newDate} onChange={e => setNewDate(e.target.value)} />
</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>
@ -234,18 +243,18 @@ export default function Agenda() {
</div> </div>
<div className="form-group"> <div className="form-group">
<label className="form-label">Observações</label> <label className="form-label">Observações</label>
<textarea className="form-textarea" placeholder="Alguma observação especial..." /> <textarea className="form-textarea" placeholder="Alguma observação especial..." value={newNotes} onChange={e => setNewNotes(e.target.value)} />
</div> </div>
</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={async () => { <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 selectedSvc = services.find(s => s.id === newService);
const priceMatch = newService.match(/R\$ (\d+)/);
const price = priceMatch ? parseInt(priceMatch[1]) : 50; const serviceName = selectedSvc ? selectedSvc.name : 'Serviço Personalizado';
const durMatch = newService.match(/\((\d+)min\)/); const price = selectedSvc ? Number(selectedSvc.price) : 50;
const duration = durMatch ? parseInt(durMatch[1]) : 30; const duration = selectedSvc ? Number(selectedSvc.duration) : 30;
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium'; const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
@ -257,11 +266,16 @@ export default function Agenda() {
slug, slug,
professionalId: newProf, professionalId: newProf,
client: newClient || 'Cliente Avulso', client: newClient || 'Cliente Avulso',
phone: newPhone,
email: newEmail,
service: serviceName, service: serviceName,
serviceId: newService || null,
startHour: h, startHour: h,
startMin: m, startMin: m,
duration, duration,
price price,
date: newDate,
notes: newNotes,
}) })
}); });
if (res.ok) { if (res.ok) {
@ -269,6 +283,8 @@ export default function Agenda() {
setShowModal(false); setShowModal(false);
setNewClient(''); setNewClient('');
setNewPhone(''); setNewPhone('');
setNewEmail('');
setNewNotes('');
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View File

@ -21,11 +21,24 @@ export default function Topbar({ title, subtitle, onMenuClick, onNavigate }: Top
const [showNotifications, setShowNotifications] = useState(false); const [showNotifications, setShowNotifications] = useState(false);
const [showProfile, setShowProfile] = useState(false); const [showProfile, setShowProfile] = useState(false);
const [notifications, setNotifications] = useState(mockNotifications); const [notifications, setNotifications] = useState<any[]>([]);
const notifRef = useRef<HTMLDivElement>(null); const notifRef = useRef<HTMLDivElement>(null);
const profileRef = useRef<HTMLDivElement>(null); const profileRef = useRef<HTMLDivElement>(null);
const fetchNotifications = async () => {
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
try {
const res = await fetch(`/api/tenant/notifications?slug=${slug}`);
if (res.ok) {
const data = await res.json();
setNotifications(data);
}
} catch (err) {
console.error('Error fetching notifications:', err);
}
};
useEffect(() => { useEffect(() => {
const savedName = localStorage.getItem('agendapro_tenant_name'); const savedName = localStorage.getItem('agendapro_tenant_name');
if (savedName) { if (savedName) {
@ -36,12 +49,19 @@ export default function Topbar({ title, subtitle, onMenuClick, onNavigate }: Top
const savedEmail = localStorage.getItem('agendapro_email'); const savedEmail = localStorage.getItem('agendapro_email');
if (savedEmail) setEmail(savedEmail); if (savedEmail) setEmail(savedEmail);
fetchNotifications();
const interval = setInterval(fetchNotifications, 15000); // Auto-refresh every 15s
const handleClickOutside = (e: MouseEvent) => { const handleClickOutside = (e: MouseEvent) => {
if (notifRef.current && !notifRef.current.contains(e.target as Node)) setShowNotifications(false); if (notifRef.current && !notifRef.current.contains(e.target as Node)) setShowNotifications(false);
if (profileRef.current && !profileRef.current.contains(e.target as Node)) setShowProfile(false); if (profileRef.current && !profileRef.current.contains(e.target as Node)) setShowProfile(false);
}; };
document.addEventListener('mousedown', handleClickOutside); document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside); return () => {
document.removeEventListener('mousedown', handleClickOutside);
clearInterval(interval);
};
}, []); }, []);
const playNotificationSound = () => { const playNotificationSound = () => {
@ -69,7 +89,6 @@ export default function Topbar({ title, subtitle, onMenuClick, onNavigate }: Top
const handleNotificationClick = () => { const handleNotificationClick = () => {
if (!showNotifications) { if (!showNotifications) {
// Simulate receiving a new notification and playing sound the first time they open
const unreadCount = notifications.filter(n => !n.read).length; const unreadCount = notifications.filter(n => !n.read).length;
if (unreadCount > 0) { if (unreadCount > 0) {
playNotificationSound(); playNotificationSound();
@ -84,8 +103,30 @@ export default function Topbar({ title, subtitle, onMenuClick, onNavigate }: Top
setShowNotifications(false); setShowNotifications(false);
}; };
const markAllAsRead = () => { const markAllAsRead = async () => {
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
setNotifications(notifications.map(n => ({ ...n, read: true }))); setNotifications(notifications.map(n => ({ ...n, read: true })));
try {
await fetch('/api/tenant/notifications', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug }),
});
} catch (err) {
console.error(err);
}
};
const clearAllNotifications = async () => {
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
setNotifications([]);
try {
await fetch(`/api/tenant/notifications?slug=${slug}`, {
method: 'DELETE',
});
} catch (err) {
console.error(err);
}
}; };
const logout = () => { const logout = () => {
@ -144,7 +185,7 @@ export default function Topbar({ title, subtitle, onMenuClick, onNavigate }: Top
</button> </button>
)} )}
{notifications.length > 0 && ( {notifications.length > 0 && (
<button className="btn-ghost" style={{ fontSize: '12px', padding: '4px 8px', color: 'var(--danger)' }} onClick={() => setNotifications([])}> <button className="btn-ghost" style={{ fontSize: '12px', padding: '4px 8px', color: 'var(--danger)' }} onClick={clearAllNotifications}>
Limpar Limpar
</button> </button>
)} )}