fix(agenda): token Google malformado (state) + cores reais das agendas

- 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 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-06-09 17:37:28 +02:00
parent 4586b3bae5
commit eed0f615c8
2 changed files with 40 additions and 5 deletions
+23 -3
View File
@@ -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);
}
+17 -2
View File
@@ -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<string>(mappedEvents.map(e => e.backgroundColor).filter(Boolean));
const corPorOrigem: Record<string, string> = {};
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);