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:
+23
-3
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user