Files
temetro/backend/src/routes/meetings.ts
T
Khalid Abdi 913a217e1d feat(meetings): live invite, scheduling calendar, Discord-style redesign
- Live invite: ring a clinic member into a room (socket call:invite + a bell
  notification); the invitee gets a toast with a Join action. Notifications of
  type "meeting" deep-link to /messages/meetings?room=, which auto-joins.
- Scheduling: new scheduled_meetings table + /api/meetings/events (list mine,
  create, delete); a Calendar tab on the Meetings page with a month picker
  (meeting-day dots), the day's agenda, and a Schedule-meeting dialog
  (title/date/time/participants).
- Redesign: rounded control bar with tooltips (mic/cam/screen/invite + separated
  red Leave), speaking ring on tiles (Web Audio), and live room-occupancy counts
  via call:presence broadcasts. Migration 0025.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 02:07:41 +03:00

99 lines
2.6 KiB
TypeScript

import { Router } from "express";
import { z } from "zod";
import { HttpError } from "../lib/http-error.js";
import { requireAuth, requireOrg } from "../middleware/auth.js";
import * as meetings from "../services/meetings.js";
export const meetingsRouter = Router();
// Staff meeting rooms (Discord-style voice/video channels), scoped to the active
// clinic. Any clinic member can list, create, and join rooms — calls are
// staff-to-staff. The live call (media + participants) is handled over Socket.io
// (see src/realtime.ts); these endpoints only manage the persistent room list.
meetingsRouter.use(requireAuth, requireOrg);
meetingsRouter.get("/", async (req, res, next) => {
try {
res.json(await meetings.listRooms(req.organizationId!));
} catch (err) {
next(err);
}
});
const createSchema = z.object({
name: z.string().trim().min(1).max(80),
});
meetingsRouter.post("/", async (req, res, next) => {
try {
const { name } = createSchema.parse(req.body);
const room = await meetings.createRoom(
req.organizationId!,
name,
req.user!.id,
);
res.status(201).json(room);
} catch (err) {
next(err);
}
});
// --- Scheduled meetings (calendar) -----------------------------------------
meetingsRouter.get("/events", async (req, res, next) => {
try {
res.json(await meetings.listMeetingEvents(req.organizationId!, req.user!.id));
} catch (err) {
next(err);
}
});
const eventSchema = z.object({
title: z.string().trim().min(1).max(120),
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
time: z.string().regex(/^\d{2}:\d{2}$/),
participants: z.array(z.string()).max(50).default([]),
});
meetingsRouter.post("/events", async (req, res, next) => {
try {
const input = eventSchema.parse(req.body);
const event = await meetings.createMeetingEvent(
req.organizationId!,
req.user!.id,
input,
);
res.status(201).json(event);
} catch (err) {
next(err);
}
});
meetingsRouter.delete("/events/:id", async (req, res, next) => {
try {
const ok = await meetings.deleteMeetingEvent(
req.organizationId!,
req.user!.id,
String(req.params.id ?? ""),
);
if (!ok) throw new HttpError(404, "Meeting not found.");
res.status(204).end();
} catch (err) {
next(err);
}
});
meetingsRouter.delete("/:id", async (req, res, next) => {
try {
const ok = await meetings.deleteRoom(
req.organizationId!,
String(req.params.id ?? ""),
);
if (!ok) throw new HttpError(404, "Room not found.");
res.status(204).end();
} catch (err) {
next(err);
}
});