Skip to content

Commit 98215b1

Browse files
committed
feat: add session management features including creation, deletion, and note handling; enhance UI with modals for adding sessions and displaying notes
1 parent ac6181a commit 98215b1

22 files changed

Lines changed: 1375 additions & 231 deletions

docs/context.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ Variáveis: `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY` (servidor). Opcional no
4747
|-------|--------|
4848
| `public/data/people.yaml` | Pessoas e tópicos — versionado no git; `topicOrder` opcional (seed) |
4949
| `public/data/sessions.yaml` | Agenda seed — versionado no git |
50-
| Supabase `sessions` | Estado vivo (status, datas, notas) |
50+
| Supabase `sessions` | Estado vivo (status, datas, notas) — N sessoes por topico permitidas |
5151
| Supabase `person_preferences` | Ordem de gravacao por pessoa (`topic_order`); override editavel no painel |
5252

53-
**Status:** `scheduled`, `done`, `postponed`. **Fuso:** `America/Sao_Paulo`.
53+
**Status:** `scheduled`, `done`, `postponed`. **Fuso:** `America/Sao_Paulo`. Um topico pode ter **N sessoes**; o video so conta como concluido quando todas estao `done`.
5454

5555
---
5656

@@ -59,6 +59,8 @@ Variáveis: `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY` (servidor). Opcional no
5959
| Método | Rota | Função |
6060
|--------|------|--------|
6161
| GET | `/api/schedule` | `{ people, sessions }` — sessoes relidas do Supabase a cada request |
62+
| POST | `/api/sessions` | Cria sessao adicional para topico existente (editor) |
63+
| DELETE | `/api/sessions/:id` | Remove sessao (editor) |
6264
| PATCH | `/api/people/:personId/topic-order` | Atualiza ordem de gravacao dos topicos (editor) |
6365
| PATCH | `/api/sessions/:id` | Atualiza sessão no Supabase |
6466
| POST | `/api/sessions/swap-time` | Troca horário entre duas sessões |
@@ -68,9 +70,9 @@ Variáveis: `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY` (servidor). Opcional no
6870

6971
## UI (abas)
7072

71-
1. **Resumo** — totais e tabela por pessoa.
72-
2. **Calendário** — grade mensal, drag-and-drop, adiadas, swap de horário.
73-
3. **Por pessoa** — progresso e checklist por tópico.
73+
1. **Resumo** — totais por **topico** (catalogo) e sessoes como detalhe.
74+
2. **Calendario** — grade mensal, drag-and-drop, adiadas, swap de horario; badge de progresso por topico quando N > 1; notas por sessao.
75+
3. **Por pessoa** — progresso por topico, sub-linhas por sessao, adicionar sessao, indicador de notas.
7476

7577
---
7678

@@ -87,6 +89,8 @@ Variáveis: `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY` (servidor). Opcional no
8789
8. **`base: './'`** no Vite para GitHub Pages / raiz.
8890
9. **`yarn import`** regenera YAMLs do script legado.
8991
10. **`topicOrder`** — ordem de gravacao por pessoa: seed em `people.yaml`; override em Supabase (`cronograma_person_preferences`). Util `getTopicOrder()` / `getOrderedTopics()` em `src/lib/topicOrder.ts` (reexport em `schedule.ts`).
92+
11. **Multi-sessao por topico** — agrupamento em `src/lib/topicSessions.ts`; IDs novos com sufixo `-2`, `-3` se colidir no mesmo slot.
93+
12. **Notas por sessao** — campo `notes` no Supabase; edicao no calendario (painel Alterar sessao, fila de rascunho); leitura para visitantes; indicador com tooltip na aba Por pessoa (`src/lib/sessionNotes.ts`).
9094

9195
---
9296

@@ -104,7 +108,7 @@ Variáveis: `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY` (servidor). Opcional no
104108

105109
- Contas individuais / OAuth por usuario.
106110
- `people` / tópicos no Supabase.
107-
- Botão Discord no UI; deep link `?date=`; campo `notes` na UI; status `cancelled`.
111+
- Botão Discord no UI; deep link `?date=`; status `cancelled`.
108112
- GitHub Pages com API (só estático no workflow atual).
109113

110114
---

server/data.ts

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface Session {
3030
topicLetter: string
3131
status: SessionStatus
3232
recordedAt?: string
33+
notes?: string
3334
}
3435

3536
interface SessionRow {
@@ -49,6 +50,8 @@ const SESSIONS_YAML = path.join(ROOT, 'public', 'data', 'sessions.yaml')
4950
let people: Person[] = []
5051
let sessions: Session[] = []
5152

53+
const TZ = 'America/Sao_Paulo'
54+
5255
function rowToSession(row: SessionRow): Session {
5356
const scheduledAt =
5457
typeof row.scheduled_at === 'string'
@@ -61,6 +64,7 @@ function rowToSession(row: SessionRow): Session {
6164
topicLetter: row.topic_letter,
6265
status: row.status,
6366
recordedAt: row.recorded_at ?? undefined,
67+
notes: row.notes?.trim() ? row.notes : undefined,
6468
}
6569
}
6670

@@ -71,11 +75,41 @@ function sessionToRow(session: Session): SessionRow {
7175
person_id: session.personId,
7276
topic_letter: session.topicLetter,
7377
status: session.status,
74-
notes: '',
78+
notes: session.notes?.trim() ? session.notes : '',
7579
recorded_at: session.recordedAt?.trim() ? session.recordedAt : null,
7680
}
7781
}
7882

83+
function sessionIdBase(scheduledAt: string, personId: string, topicLetter: string): string {
84+
const parts = new Intl.DateTimeFormat('en-CA', {
85+
timeZone: TZ,
86+
year: 'numeric',
87+
month: '2-digit',
88+
day: '2-digit',
89+
hour: 'numeric',
90+
minute: 'numeric',
91+
hour12: false,
92+
}).formatToParts(new Date(scheduledAt))
93+
const get = (t: string) => parts.find((p) => p.type === t)?.value ?? ''
94+
const date = `${get('year')}-${get('month')}-${get('day')}`
95+
const hour = get('hour').padStart(2, '0')
96+
return `${date}-${hour}-${personId}-${topicLetter}`
97+
}
98+
99+
export function generateSessionId(
100+
scheduledAt: string,
101+
personId: string,
102+
topicLetter: string,
103+
existingIds: Iterable<string>,
104+
): string {
105+
const ids = new Set(existingIds)
106+
const base = sessionIdBase(scheduledAt, personId, topicLetter)
107+
if (!ids.has(base)) return base
108+
let n = 2
109+
while (ids.has(`${base}-${n}`)) n++
110+
return `${base}-${n}`
111+
}
112+
79113
interface PreferenceRow {
80114
person_id: string
81115
topic_order: string[]
@@ -204,7 +238,13 @@ export function findSession(id: string): Session | undefined {
204238

205239
export async function updateSession(
206240
id: string,
207-
patch: { status?: SessionStatus; scheduledAt?: string; recordedAt?: string },
241+
patch: {
242+
status?: SessionStatus
243+
scheduledAt?: string
244+
recordedAt?: string
245+
notes?: string
246+
topicLetter?: string
247+
},
208248
): Promise<Session | null> {
209249
const idx = sessions.findIndex((s) => s.id === id)
210250
if (idx === -1) return null
@@ -215,6 +255,17 @@ export async function updateSession(
215255
if (patch.recordedAt !== undefined) {
216256
session.recordedAt = patch.recordedAt.trim() ? patch.recordedAt : undefined
217257
}
258+
if (patch.notes !== undefined) {
259+
session.notes = patch.notes.trim() ? patch.notes : undefined
260+
}
261+
if (patch.topicLetter !== undefined) {
262+
const letter = patch.topicLetter.trim().toLowerCase()
263+
const person = people.find((p) => p.id === session.personId)
264+
if (!person?.topics.some((t) => t.letter === letter)) {
265+
throw new Error('Topico invalido para esta pessoa')
266+
}
267+
session.topicLetter = letter
268+
}
218269

219270
const { error } = await supabase.from(tables.sessions).update(sessionToRow(session)).eq('id', id)
220271
if (error) throw new Error(formatSupabaseError('[data] Falha ao atualizar sessao', error))
@@ -235,6 +286,8 @@ export type SessionPatch = {
235286
status?: SessionStatus
236287
scheduledAt?: string
237288
recordedAt?: string
289+
notes?: string
290+
topicLetter?: string
238291
}
239292

240293
export async function applySessionPatches(
@@ -300,3 +353,61 @@ export async function updatePersonTopicOrder(
300353
people[idx] = updated
301354
return updated
302355
}
356+
357+
export async function deleteSession(id: string): Promise<boolean> {
358+
const idx = sessions.findIndex((s) => s.id === id)
359+
if (idx === -1) return false
360+
361+
const { error } = await supabase.from(tables.sessions).delete().eq('id', id)
362+
if (error) throw new Error(formatSupabaseError('[data] Falha ao remover sessao', error))
363+
364+
sessions.splice(idx, 1)
365+
console.log(`[data] Sessao removida: ${id}`)
366+
return true
367+
}
368+
369+
export interface CreateSessionInput {
370+
personId: string
371+
topicLetter: string
372+
scheduledAt: string
373+
status?: SessionStatus
374+
}
375+
376+
export async function createSession(input: CreateSessionInput): Promise<Session> {
377+
const person = people.find((p) => p.id === input.personId)
378+
if (!person) throw new Error('Pessoa nao encontrada')
379+
380+
const letter = input.topicLetter.trim().toLowerCase()
381+
if (!person.topics.some((t) => t.letter === letter)) {
382+
throw new Error('Topico invalido para esta pessoa')
383+
}
384+
385+
const scheduledAt = input.scheduledAt?.trim()
386+
if (!scheduledAt) throw new Error('scheduledAt e obrigatorio')
387+
388+
const status: SessionStatus =
389+
input.status === 'done' || input.status === 'postponed' ? input.status : 'scheduled'
390+
391+
const id = generateSessionId(
392+
scheduledAt,
393+
input.personId,
394+
letter,
395+
sessions.map((s) => s.id),
396+
)
397+
398+
const session: Session = {
399+
id,
400+
scheduledAt,
401+
personId: input.personId,
402+
topicLetter: letter,
403+
status,
404+
}
405+
406+
const { error } = await supabase.from(tables.sessions).insert(sessionToRow(session))
407+
if (error) throw new Error(formatSupabaseError('[data] Falha ao criar sessao', error))
408+
409+
sessions.push(session)
410+
sessions.sort((a, b) => a.scheduledAt.localeCompare(b.scheduledAt))
411+
console.log(`[data] Sessao criada: ${id} (${input.personId}/${letter})`)
412+
return session
413+
}

server/index.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import {
1212
resetSessionsFromYaml,
1313
applySessionPatches,
1414
updatePersonTopicOrder,
15+
createSession,
16+
deleteSession,
17+
type CreateSessionInput,
1518
type SessionPatch,
1619
} from './data.js'
1720
import { handleAuthMe, handleLogin, requireEditor } from './auth.js'
@@ -52,8 +55,8 @@ app.get('/api/schedule', async (_req, res) => {
5255
app.patch('/api/sessions/:id', requireEditor, async (req, res) => {
5356
try {
5457
const id = String(req.params.id)
55-
const { status, scheduledAt, recordedAt } = req.body as SessionPatch
56-
const session = await updateSession(id, { status, scheduledAt, recordedAt })
58+
const { status, scheduledAt, recordedAt, notes, topicLetter } = req.body as SessionPatch
59+
const session = await updateSession(id, { status, scheduledAt, recordedAt, notes, topicLetter })
5760
if (!session) {
5861
return res.status(404).json({ error: 'Session not found' })
5962
}
@@ -64,6 +67,38 @@ app.patch('/api/sessions/:id', requireEditor, async (req, res) => {
6467
}
6568
})
6669

70+
app.post('/api/sessions', requireEditor, async (req, res) => {
71+
try {
72+
const { personId, topicLetter, scheduledAt, status } = req.body as CreateSessionInput
73+
if (!personId || !topicLetter || !scheduledAt) {
74+
return res.status(400).json({ error: 'personId, topicLetter e scheduledAt sao obrigatorios' })
75+
}
76+
const session = await createSession({ personId, topicLetter, scheduledAt, status })
77+
res.status(201).json({ session })
78+
} catch (e) {
79+
console.error(e)
80+
const msg = String(e)
81+
if (msg.includes('nao encontrada') || msg.includes('invalido')) {
82+
return res.status(400).json({ error: msg })
83+
}
84+
res.status(500).json({ error: msg })
85+
}
86+
})
87+
88+
app.delete('/api/sessions/:id', requireEditor, async (req, res) => {
89+
try {
90+
const id = String(req.params.id)
91+
const removed = await deleteSession(id)
92+
if (!removed) {
93+
return res.status(404).json({ error: 'Session not found' })
94+
}
95+
res.status(200).json({ ok: true })
96+
} catch (e) {
97+
console.error(e)
98+
res.status(500).json({ error: String(e) })
99+
}
100+
})
101+
67102
app.post('/api/sessions/apply-batch', requireEditor, async (req, res) => {
68103
if (isRateLimited(req, 'apply-batch', BATCH_LIMIT.max, BATCH_LIMIT.windowMs)) {
69104
return res.status(429).json({ error: 'Muitas alteracoes em pouco tempo. Aguarde um momento.' })

0 commit comments

Comments
 (0)