feat(agenda): importa cor do evento Google + UI do modal pequeno
- agendamentos.cor (TEXT); import grava cor do evento (colorId/agenda) - frontend usa ag.cor (prioridade: status > cor própria > cor do dentista) - StackModal: remove backdrop-blur (vidro); mobile vira bottom-sheet de altura auto, desktop painel pequeno com max-w; botão maximizar visível no mobile; padding responsivo Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+8
-3
@@ -2020,6 +2020,9 @@ app.post('/api/agenda/importar-google', authGuard, async (req, res) => {
|
||||
const auth = await getClinicOAuth(clinicaId);
|
||||
if (!auth) return res.status(400).json({ success: false, message: 'Nenhuma conta Google conectada nesta clínica.' });
|
||||
const calendar = google.calendar({ version: 'v3', auth });
|
||||
// Cor da agenda (fallback p/ eventos sem cor própria).
|
||||
let calColor = null;
|
||||
try { const cm = await calendar.calendarList.get({ calendarId: 'primary' }); calColor = cm.data.backgroundColor || null; } catch { /* escopo */ }
|
||||
const now = Date.now();
|
||||
const timeMin = new Date(now - 7 * 86400000).toISOString();
|
||||
const timeMax = new Date(now + (Number(dias) || 60) * 86400000).toISOString();
|
||||
@@ -2034,11 +2037,12 @@ app.post('/api/agenda/importar-google', authGuard, async (req, res) => {
|
||||
const { rows: ex } = await pool.query('SELECT 1 FROM agendamentos WHERE google_event_id = $1 AND clinica_id = $2 LIMIT 1', [it.id, clinicaId]);
|
||||
if (ex.length) { pulados++; continue; }
|
||||
const end = it.end?.dateTime || it.end?.date || start;
|
||||
const cor = (it.colorId && GOOGLE_EVENT_COLORS[it.colorId]) || calColor || null;
|
||||
const id = `ag_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
||||
await pool.query(
|
||||
`INSERT INTO agendamentos (id, clinica_id, pacientenome, start_time, end_time, status, procedimento, google_event_id, created_by, created_by_nome, version)
|
||||
VALUES ($1,$2,$3,$4,$5,'agendado',$6,$7,$8,$9,1)`,
|
||||
[id, clinicaId, it.summary || 'Importado do Google', start, end, 'IMPORTADO (GOOGLE)', it.id, actor, actorNm]);
|
||||
`INSERT INTO agendamentos (id, clinica_id, pacientenome, start_time, end_time, status, procedimento, google_event_id, cor, created_by, created_by_nome, version)
|
||||
VALUES ($1,$2,$3,$4,$5,'agendado',$6,$7,$8,$9,$10,1)`,
|
||||
[id, clinicaId, it.summary || 'Importado do Google', start, end, 'IMPORTADO (GOOGLE)', it.id, cor, actor, actorNm]);
|
||||
await registrarAudit(pool, { clinicaId, entidade: 'agendamento', entidadeId: id, acao: 'criou', actorId: actor, actorNome: actorNm, detalhes: { origem: 'google', summary: it.summary } });
|
||||
criados++;
|
||||
}
|
||||
@@ -3314,6 +3318,7 @@ async function runMigrations() {
|
||||
`ALTER TABLE agendamentos ADD COLUMN IF NOT EXISTS canceled_at TIMESTAMPTZ`,
|
||||
`ALTER TABLE agendamentos ADD COLUMN IF NOT EXISTS version INTEGER DEFAULT 1`,
|
||||
`ALTER TABLE agendamentos ADD COLUMN IF NOT EXISTS google_event_id TEXT`,
|
||||
`ALTER TABLE agendamentos ADD COLUMN IF NOT EXISTS cor TEXT`,
|
||||
`CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id TEXT PRIMARY KEY,
|
||||
clinica_id TEXT,
|
||||
|
||||
@@ -38,10 +38,12 @@ export const StackModal: React.FC<{ isOpen: boolean; onClose: () => void; root:
|
||||
const canBack = stack.length > 1;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/60 z-[70] flex items-end sm:items-center justify-center backdrop-blur-sm sm:p-4">
|
||||
<div className="fixed inset-0 bg-black/40 z-[70] flex items-end sm:items-center justify-center sm:p-4">
|
||||
<div
|
||||
className={`bg-white shadow-2xl flex flex-col overflow-hidden transition-all duration-300 w-full h-dvh rounded-none ${
|
||||
fullscreen ? 'sm:w-screen sm:h-dvh sm:rounded-none' : 'sm:w-[460px] sm:h-auto sm:max-h-[88vh] sm:rounded-3xl'
|
||||
className={`bg-white shadow-2xl flex flex-col overflow-hidden transition-all duration-300 w-full ${
|
||||
fullscreen
|
||||
? 'h-dvh max-h-none rounded-none sm:w-screen'
|
||||
: 'max-h-[90vh] rounded-t-2xl sm:w-[440px] sm:max-w-[95vw] sm:h-auto sm:max-h-[88vh] sm:rounded-3xl'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 px-4 py-3 border-b border-gray-100 shrink-0">
|
||||
@@ -55,7 +57,7 @@ export const StackModal: React.FC<{ isOpen: boolean; onClose: () => void; root:
|
||||
<h3 className="font-black text-sm uppercase text-gray-800 truncate flex-1">{current.title}</h3>
|
||||
<button
|
||||
onClick={() => setFullscreen(f => !f)}
|
||||
className="hidden sm:inline-flex p-1.5 hover:bg-gray-100 rounded-lg text-gray-400 transition-colors"
|
||||
className="inline-flex p-1.5 hover:bg-gray-100 rounded-lg text-gray-400 transition-colors"
|
||||
title={fullscreen ? 'Restaurar' : 'Tela cheia'}
|
||||
>
|
||||
{fullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
|
||||
@@ -67,7 +69,7 @@ export const StackModal: React.FC<{ isOpen: boolean; onClose: () => void; root:
|
||||
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
||||
<Ctx.Provider value={{ push, pop, close }}>
|
||||
<div key={stack.length} className="animate-in slide-in-from-right-4 fade-in duration-200 p-5">
|
||||
<div key={stack.length} className="animate-in slide-in-from-right-4 fade-in duration-200 p-4 sm:p-5">
|
||||
{current.render()}
|
||||
</div>
|
||||
</Ctx.Provider>
|
||||
|
||||
@@ -409,7 +409,8 @@ export const AgendaView: React.FC<{ onNavigate?: (view: string) => void }> = ({
|
||||
const st = String(ag.status || '').toLowerCase();
|
||||
const faltou = st === 'falta' || st === 'faltou';
|
||||
const inativo = st === 'cancelado' || st === 'remarcar';
|
||||
const cor = faltou ? '#ef4444' : inativo ? '#cbd5e1' : (dentist ? dentist.corAgenda : '#94a3b8');
|
||||
// Prioridade: status (falta/cancelado) > cor própria do agendamento (ex.: importada do Google) > cor do dentista.
|
||||
const cor = faltou ? '#ef4444' : inativo ? '#cbd5e1' : ((ag as any).cor || (dentist ? dentist.corAgenda : '#94a3b8'));
|
||||
return {
|
||||
id: ag.id,
|
||||
title: `${ag.pacienteNome} - ${ag.procedimento}`,
|
||||
|
||||
Reference in New Issue
Block a user