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
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m36s
Details
This commit is contained in:
parent
0c7ffe6b44
commit
39c06c03d7
|
|
@ -56,7 +56,11 @@ export async function GET(req: NextRequest) {
|
|||
|
||||
export async function POST(req: NextRequest) {
|
||||
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 });
|
||||
|
||||
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));
|
||||
const endTime = dateObj.toISOString().substr(11, 8);
|
||||
|
||||
const targetDate = date || new Date().toISOString().split('T')[0];
|
||||
const initialStatus = status || 'confirmed';
|
||||
|
||||
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]
|
||||
`INSERT INTO appointments (
|
||||
tenant_id, professional_id, client_name, client_phone, client_email,
|
||||
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]);
|
||||
|
|
@ -82,3 +112,53 @@ export async function POST(req: NextRequest) {
|
|||
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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -8,8 +8,23 @@ export default function LoginPage() {
|
|||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [isForgot, setIsForgot] = useState(false);
|
||||
const [resetEmail, setResetEmail] = useState('');
|
||||
const [resetSent, setResetSent] = useState(false);
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
|
@ -64,35 +79,69 @@ export default function LoginPage() {
|
|||
{/* Lado Direito - Formulário */}
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '40px' }}>
|
||||
<div style={{ width: '100%', maxWidth: '400px', animation: 'slideUp 0.5s ease' }}>
|
||||
<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>
|
||||
{!isForgot ? (
|
||||
<>
|
||||
<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' }}>
|
||||
<div className="form-group" style={{ margin: 0 }}>
|
||||
<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"
|
||||
value={email} onChange={e => setEmail(e.target.value)} />
|
||||
</div>
|
||||
<div className="form-group" style={{ margin: 0 }}>
|
||||
<div className="flex-between">
|
||||
<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>
|
||||
</div>
|
||||
<input type="password" required className="form-input" placeholder="••••••••"
|
||||
value={password} onChange={e => setPassword(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 ? 'Entrando...' : <><ArrowRight size={18} /> Entrar no Sistema</>}
|
||||
</button>
|
||||
</form>
|
||||
<form onSubmit={handleLogin} 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</label>
|
||||
<input type="email" required className="form-input" placeholder="seu@email.com"
|
||||
value={email} onChange={e => setEmail(e.target.value)} />
|
||||
</div>
|
||||
<div className="form-group" style={{ margin: 0 }}>
|
||||
<div className="flex-between">
|
||||
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Lock size={14} /> Senha</label>
|
||||
<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>
|
||||
<input type="password" required className="form-input" placeholder="••••••••"
|
||||
value={password} onChange={e => setPassword(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 ? 'Entrando...' : <><ArrowRight size={18} /> Entrar no Sistema</>}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<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>
|
||||
</p>
|
||||
<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>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -56,26 +56,35 @@ export default function Agenda() {
|
|||
// 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 [newEmail, setNewEmail] = useState('');
|
||||
const [newService, setNewService] = useState('');
|
||||
const [newProf, setNewProf] = useState('');
|
||||
const [newDate, setNewDate] = useState(today.toISOString().split('T')[0]);
|
||||
const [newTime, setNewTime] = useState('10:00');
|
||||
const [newNotes, setNewNotes] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (professionals.length > 0 && !newProf) {
|
||||
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 changeDate = (days: number) => {
|
||||
const newDate = new Date(currentDate);
|
||||
newDate.setDate(newDate.getDate() + days);
|
||||
setCurrentDate(newDate);
|
||||
const newDateObj = new Date(currentDate);
|
||||
newDateObj.setDate(newDateObj.getDate() + days);
|
||||
setCurrentDate(newDateObj);
|
||||
setNewDate(newDateObj.toISOString().split('T')[0]);
|
||||
};
|
||||
|
||||
const goToToday = () => {
|
||||
setCurrentDate(new Date());
|
||||
const todayObj = new Date();
|
||||
setCurrentDate(todayObj);
|
||||
setNewDate(todayObj.toISOString().split('T')[0]);
|
||||
};
|
||||
|
||||
if (loading && professionals.length === 0) {
|
||||
|
|
@ -200,7 +209,7 @@ export default function Agenda() {
|
|||
</div>
|
||||
<div className="form-group">
|
||||
<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 className="form-group">
|
||||
|
|
@ -208,7 +217,7 @@ export default function Agenda() {
|
|||
<select className="form-select" value={newService} onChange={e => setNewService(e.target.value)}>
|
||||
<option value="">Selecione um serviço...</option>
|
||||
{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)
|
||||
</option>
|
||||
))}
|
||||
|
|
@ -225,7 +234,7 @@ export default function Agenda() {
|
|||
<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]} readOnly />
|
||||
<input className="form-input" type="date" value={newDate} onChange={e => setNewDate(e.target.value)} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<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 className="form-group">
|
||||
<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 className="modal-footer">
|
||||
<button className="btn btn-secondary" onClick={() => setShowModal(false)}>Cancelar</button>
|
||||
<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+)/);
|
||||
const price = priceMatch ? parseInt(priceMatch[1]) : 50;
|
||||
const durMatch = newService.match(/\((\d+)min\)/);
|
||||
const duration = durMatch ? parseInt(durMatch[1]) : 30;
|
||||
const selectedSvc = services.find(s => s.id === newService);
|
||||
|
||||
const serviceName = selectedSvc ? selectedSvc.name : 'Serviço Personalizado';
|
||||
const price = selectedSvc ? Number(selectedSvc.price) : 50;
|
||||
const duration = selectedSvc ? Number(selectedSvc.duration) : 30;
|
||||
|
||||
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
|
||||
|
||||
|
|
@ -257,11 +266,16 @@ export default function Agenda() {
|
|||
slug,
|
||||
professionalId: newProf,
|
||||
client: newClient || 'Cliente Avulso',
|
||||
phone: newPhone,
|
||||
email: newEmail,
|
||||
service: serviceName,
|
||||
serviceId: newService || null,
|
||||
startHour: h,
|
||||
startMin: m,
|
||||
duration,
|
||||
price
|
||||
price,
|
||||
date: newDate,
|
||||
notes: newNotes,
|
||||
})
|
||||
});
|
||||
if (res.ok) {
|
||||
|
|
@ -269,6 +283,8 @@ export default function Agenda() {
|
|||
setShowModal(false);
|
||||
setNewClient('');
|
||||
setNewPhone('');
|
||||
setNewEmail('');
|
||||
setNewNotes('');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
|
|||
|
|
@ -21,11 +21,24 @@ export default function Topbar({ title, subtitle, onMenuClick, onNavigate }: Top
|
|||
|
||||
const [showNotifications, setShowNotifications] = useState(false);
|
||||
const [showProfile, setShowProfile] = useState(false);
|
||||
const [notifications, setNotifications] = useState(mockNotifications);
|
||||
const [notifications, setNotifications] = useState<any[]>([]);
|
||||
|
||||
const notifRef = 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(() => {
|
||||
const savedName = localStorage.getItem('agendapro_tenant_name');
|
||||
if (savedName) {
|
||||
|
|
@ -36,12 +49,19 @@ export default function Topbar({ title, subtitle, onMenuClick, onNavigate }: Top
|
|||
const savedEmail = localStorage.getItem('agendapro_email');
|
||||
if (savedEmail) setEmail(savedEmail);
|
||||
|
||||
fetchNotifications();
|
||||
|
||||
const interval = setInterval(fetchNotifications, 15000); // Auto-refresh every 15s
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (notifRef.current && !notifRef.current.contains(e.target as Node)) setShowNotifications(false);
|
||||
if (profileRef.current && !profileRef.current.contains(e.target as Node)) setShowProfile(false);
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const playNotificationSound = () => {
|
||||
|
|
@ -69,7 +89,6 @@ export default function Topbar({ title, subtitle, onMenuClick, onNavigate }: Top
|
|||
|
||||
const handleNotificationClick = () => {
|
||||
if (!showNotifications) {
|
||||
// Simulate receiving a new notification and playing sound the first time they open
|
||||
const unreadCount = notifications.filter(n => !n.read).length;
|
||||
if (unreadCount > 0) {
|
||||
playNotificationSound();
|
||||
|
|
@ -84,8 +103,30 @@ export default function Topbar({ title, subtitle, onMenuClick, onNavigate }: Top
|
|||
setShowNotifications(false);
|
||||
};
|
||||
|
||||
const markAllAsRead = () => {
|
||||
const markAllAsRead = async () => {
|
||||
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
|
||||
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 = () => {
|
||||
|
|
@ -144,7 +185,7 @@ export default function Topbar({ title, subtitle, onMenuClick, onNavigate }: Top
|
|||
</button>
|
||||
)}
|
||||
{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
|
||||
</button>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue