From eed0f615c86931ce8256f4951c40314b558c7c3d Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Tue, 9 Jun 2026 17:37:28 +0200 Subject: [PATCH] fix(agenda): token Google malformado (state) + cores reais das agendas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - callback OAuth: decodifica state defensivamente (|/%7C/%257C) + salvaguarda re-split (evita owner_id colado com clinica_id nulo) - reparo do token existente da recepção (script aplicado no banco) - eventos do Google agora usam a cor real (colorId / cor da agenda); paleta padrão do Google - frontend: eventos sem cor recebem cor NÃO usada na agenda, consistente por origem Co-Authored-By: Claude Opus 4.8 --- backend/server.js | 26 +++++++++++++++++++++++--- frontend/views/AgendaView.tsx | 19 +++++++++++++++++-- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/backend/server.js b/backend/server.js index f0b6655..fdb8675 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1052,8 +1052,16 @@ app.get('/api/oauth2callback', async (req, res) => { try { const oauth2Client = createOAuth2Client(); const { tokens } = await oauth2Client.getToken(code); - const [rawOwner, clinicaId] = (state || 'admin|').split('|'); + // Decodifica defensivamente (o '|' pode voltar como %7C / %257C no round-trip do Google), + // senão o split falha e o owner_id fica colado com a clínica (clinica_id nulo). + let rawState = String(state || 'admin|'); + for (let i = 0; i < 2 && /%[0-9a-f]{2}/i.test(rawState); i++) { + try { rawState = decodeURIComponent(rawState); } catch { break; } + } + let [rawOwner, clinicaId] = rawState.split('|'); let ownerId = rawOwner || 'admin'; + // Salvaguarda: nunca grava owner colado com a clínica. + if (ownerId.includes('|')) { const pp = ownerId.split('|'); ownerId = pp[0]; clinicaId = clinicaId || pp[1] || null; } if (ownerId === 'admin') { oauth2Client.setCredentials(tokens); const oauth2 = google.oauth2({ version: 'v2', auth: oauth2Client }); @@ -1199,6 +1207,11 @@ app.post('/api/dentistas/register', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); +// Paleta padrão de cores de EVENTO do Google Calendar (colorId 1..11). +const GOOGLE_EVENT_COLORS = { + '1': '#7986cb', '2': '#33b679', '3': '#8e24aa', '4': '#e67c73', '5': '#f6bf26', '6': '#f4511e', + '7': '#039be5', '8': '#616161', '9': '#3f51b5', '10': '#0b8043', '11': '#d50000' +}; app.get('/api/google/events', async (req, res) => { try { const { clinicaId } = req.query; @@ -1220,6 +1233,12 @@ app.get('/api/google/events', async (req, res) => { }); const calendar = google.calendar({ version: 'v3', auth: oauth2Client }); + // Cor da AGENDA (definida no Google) — 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/indisponível → frontend escolhe cor não usada */ } const response = await calendar.events.list({ calendarId: 'primary', timeMin: timeMin, @@ -1251,8 +1270,9 @@ app.get('/api/google/events', async (req, res) => { end: item.end.dateTime || item.end.date, isGoogle: true, owner: ownerName, - backgroundColor: '#3b82f6', - borderColor: '#3b82f6' + // cor do evento (colorId) > cor da agenda > null (frontend atribui cor não usada) + backgroundColor: (item.colorId && GOOGLE_EVENT_COLORS[item.colorId]) || calColor || null, + borderColor: (item.colorId && GOOGLE_EVENT_COLORS[item.colorId]) || calColor || null })); allEvents.push(...mapped); } diff --git a/frontend/views/AgendaView.tsx b/frontend/views/AgendaView.tsx index fe14817..ddfc786 100644 --- a/frontend/views/AgendaView.tsx +++ b/frontend/views/AgendaView.tsx @@ -409,13 +409,28 @@ export const AgendaView: React.FC<{ onNavigate?: (view: string) => void }> = ({ }; }); - // Add Google events if any + // Add Google events if any — com cores: usa a cor real (colorId/agenda) e, + // quando não houver, escolhe uma cor NÃO usada na agenda (consistente por origem). if (googleEvents && googleEvents.length > 0) { let filteredGoogleEvents = googleEvents; if (isDentist) { filteredGoogleEvents = googleEvents.filter(ev => ev?.extendedProps?.owner_id === currentUserEmail); } - mappedEvents.push(...filteredGoogleEvents); + const PALETTE = ['#2563eb', '#7c3aed', '#db2777', '#059669', '#d97706', '#0891b2', '#dc2626', '#4f46e5', '#0d9488', '#ca8a04', '#9333ea', '#e11d48', '#16a34a', '#f97316']; + const usadas = new Set(mappedEvents.map(e => e.backgroundColor).filter(Boolean)); + const corPorOrigem: Record = {}; + const proximaCor = () => PALETTE.find(c => !usadas.has(c)) || PALETTE[Object.keys(corPorOrigem).length % PALETTE.length]; + const coloridos = filteredGoogleEvents.map(ev => { + let cor = ev.backgroundColor || null; + if (cor) { usadas.add(cor); } + else { + const origem = ev.owner || 'google'; + if (!corPorOrigem[origem]) { const c = proximaCor(); corPorOrigem[origem] = c; usadas.add(c); } + cor = corPorOrigem[origem]; + } + return { ...ev, backgroundColor: cor, borderColor: cor }; + }); + mappedEvents.push(...coloridos); } setEvents(mappedEvents);