feat: Fase 3 - Sistema de vídeo chamadas
- Integração Daily.co para videochamadas - API para criar/gerenciar salas de vídeo - API de agendamentos (CRUD) - Página de sessão com controles de vídeo/áudio - Página de agenda com calendário visual - Lista de próximos agendamentos
This commit is contained in:
125
package-lock.json
generated
125
package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.11.1",
|
"@auth/prisma-adapter": "^2.11.1",
|
||||||
|
"@daily-co/daily-js": "^0.87.0",
|
||||||
"@prisma/client": "^6.19.2",
|
"@prisma/client": "^6.19.2",
|
||||||
"@stripe/stripe-js": "^8.7.0",
|
"@stripe/stripe-js": "^8.7.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
@@ -281,6 +282,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.28.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||||
|
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||||
@@ -329,6 +339,22 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@daily-co/daily-js": {
|
||||||
|
"version": "0.87.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@daily-co/daily-js/-/daily-js-0.87.0.tgz",
|
||||||
|
"integrity": "sha512-hWHdBDvJwDeg8unz+XG9hD7xamuFi5Jmsk89ASATKL4fdTDHplpxi4PG9aaPXzBZYYLTjuxUje1K7B5uUR+gzw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@sentry/browser": "^8.33.1",
|
||||||
|
"bowser": "^2.8.1",
|
||||||
|
"dequal": "^2.0.3",
|
||||||
|
"events": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
|
||||||
@@ -1374,6 +1400,81 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@sentry-internal/browser-utils": {
|
||||||
|
"version": "8.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.55.0.tgz",
|
||||||
|
"integrity": "sha512-ROgqtQfpH/82AQIpESPqPQe0UyWywKJsmVIqi3c5Fh+zkds5LUxnssTj3yNd1x+kxaPDVB023jAP+3ibNgeNDw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sentry/core": "8.55.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sentry-internal/feedback": {
|
||||||
|
"version": "8.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.55.0.tgz",
|
||||||
|
"integrity": "sha512-cP3BD/Q6pquVQ+YL+rwCnorKuTXiS9KXW8HNKu4nmmBAyf7urjs+F6Hr1k9MXP5yQ8W3yK7jRWd09Yu6DHWOiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sentry/core": "8.55.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sentry-internal/replay": {
|
||||||
|
"version": "8.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.55.0.tgz",
|
||||||
|
"integrity": "sha512-roCDEGkORwolxBn8xAKedybY+Jlefq3xYmgN2fr3BTnsXjSYOPC7D1/mYqINBat99nDtvgFvNfRcZPiwwZ1hSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sentry-internal/browser-utils": "8.55.0",
|
||||||
|
"@sentry/core": "8.55.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sentry-internal/replay-canvas": {
|
||||||
|
"version": "8.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.55.0.tgz",
|
||||||
|
"integrity": "sha512-nIkfgRWk1091zHdu4NbocQsxZF1rv1f7bbp3tTIlZYbrH62XVZosx5iHAuZG0Zc48AETLE7K4AX9VGjvQj8i9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sentry-internal/replay": "8.55.0",
|
||||||
|
"@sentry/core": "8.55.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sentry/browser": {
|
||||||
|
"version": "8.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.55.0.tgz",
|
||||||
|
"integrity": "sha512-1A31mCEWCjaMxJt6qGUK+aDnLDcK6AwLAZnqpSchNysGni1pSn1RWSmk9TBF8qyTds5FH8B31H480uxMPUJ7Cw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sentry-internal/browser-utils": "8.55.0",
|
||||||
|
"@sentry-internal/feedback": "8.55.0",
|
||||||
|
"@sentry-internal/replay": "8.55.0",
|
||||||
|
"@sentry-internal/replay-canvas": "8.55.0",
|
||||||
|
"@sentry/core": "8.55.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sentry/core": {
|
||||||
|
"version": "8.55.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.55.0.tgz",
|
||||||
|
"integrity": "sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@standard-schema/spec": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
@@ -2602,6 +2703,12 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bowser": {
|
||||||
|
"version": "2.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz",
|
||||||
|
"integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
@@ -3033,6 +3140,15 @@
|
|||||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dequal": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/destr": {
|
"node_modules/destr": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
||||||
@@ -3760,6 +3876,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/events": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/exsolve": {
|
"node_modules/exsolve": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.11.1",
|
"@auth/prisma-adapter": "^2.11.1",
|
||||||
|
"@daily-co/daily-js": "^0.87.0",
|
||||||
"@prisma/client": "^6.19.2",
|
"@prisma/client": "^6.19.2",
|
||||||
"@stripe/stripe-js": "^8.7.0",
|
"@stripe/stripe-js": "^8.7.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
|
|||||||
109
src/app/api/appointments/route.ts
Normal file
109
src/app/api/appointments/route.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { auth } from '@/lib/auth';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
|
||||||
|
// Listar agendamentos
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const session = await auth();
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Não autenticado' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: session.user.id },
|
||||||
|
include: { children: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: 'Usuário não encontrado' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
let appointments;
|
||||||
|
|
||||||
|
if (user.role === 'therapist') {
|
||||||
|
// Terapeuta vê seus agendamentos
|
||||||
|
appointments = await prisma.appointment.findMany({
|
||||||
|
where: { therapistId: user.id },
|
||||||
|
include: {
|
||||||
|
child: { include: { parent: true } },
|
||||||
|
therapist: true,
|
||||||
|
},
|
||||||
|
orderBy: { scheduledAt: 'asc' },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Pai vê agendamentos dos filhos
|
||||||
|
const childIds = user.children.map(c => c.id);
|
||||||
|
appointments = await prisma.appointment.findMany({
|
||||||
|
where: { childId: { in: childIds } },
|
||||||
|
include: {
|
||||||
|
child: true,
|
||||||
|
therapist: true,
|
||||||
|
},
|
||||||
|
orderBy: { scheduledAt: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(appointments);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao listar agendamentos:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Erro ao buscar agendamentos' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Criar agendamento
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const session = await auth();
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Não autenticado' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar se é terapeuta
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: session.user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user || (user.role !== 'therapist' && user.role !== 'admin')) {
|
||||||
|
return NextResponse.json({ error: 'Sem permissão' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { childId, scheduledAt, duration = 50, notes } = body;
|
||||||
|
|
||||||
|
if (!childId || !scheduledAt) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'childId e scheduledAt são obrigatórios' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const appointment = await prisma.appointment.create({
|
||||||
|
data: {
|
||||||
|
childId,
|
||||||
|
therapistId: user.id,
|
||||||
|
scheduledAt: new Date(scheduledAt),
|
||||||
|
duration,
|
||||||
|
notes,
|
||||||
|
status: 'scheduled',
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
child: true,
|
||||||
|
therapist: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(appointment);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao criar agendamento:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Erro ao criar agendamento' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/app/api/daily/room/route.ts
Normal file
93
src/app/api/daily/room/route.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { auth } from '@/lib/auth';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import { createRoom, createMeetingToken, getRoom } from '@/lib/daily';
|
||||||
|
|
||||||
|
// Criar sala para um agendamento
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const session = await auth();
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Não autenticado' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { appointmentId } = body;
|
||||||
|
|
||||||
|
if (!appointmentId) {
|
||||||
|
return NextResponse.json({ error: 'appointmentId é obrigatório' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar agendamento
|
||||||
|
const appointment = await prisma.appointment.findUnique({
|
||||||
|
where: { id: appointmentId },
|
||||||
|
include: {
|
||||||
|
child: { include: { parent: true } },
|
||||||
|
therapist: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!appointment) {
|
||||||
|
return NextResponse.json({ error: 'Agendamento não encontrado' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar se o usuário é o pai ou terapeuta
|
||||||
|
const isParent = appointment.child.parentId === session.user.id;
|
||||||
|
const isTherapist = appointment.therapistId === session.user.id;
|
||||||
|
|
||||||
|
if (!isParent && !isTherapist) {
|
||||||
|
return NextResponse.json({ error: 'Sem permissão' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se já tem sala, retornar
|
||||||
|
if (appointment.roomUrl && appointment.roomName) {
|
||||||
|
const existingRoom = await getRoom(appointment.roomName);
|
||||||
|
if (existingRoom) {
|
||||||
|
const token = await createMeetingToken(appointment.roomName, {
|
||||||
|
userName: session.user.name || 'Participante',
|
||||||
|
isOwner: isTherapist,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
roomUrl: `${appointment.roomUrl}?t=${token.token}`,
|
||||||
|
roomName: appointment.roomName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Criar nova sala
|
||||||
|
const roomName = `iris-${appointmentId}`;
|
||||||
|
const room = await createRoom({
|
||||||
|
name: roomName,
|
||||||
|
expiryMinutes: 90, // Sessão de 50min + buffer
|
||||||
|
maxParticipants: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Atualizar agendamento
|
||||||
|
await prisma.appointment.update({
|
||||||
|
where: { id: appointmentId },
|
||||||
|
data: {
|
||||||
|
roomUrl: room.url,
|
||||||
|
roomName: room.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Criar token
|
||||||
|
const token = await createMeetingToken(room.name, {
|
||||||
|
userName: session.user.name || 'Participante',
|
||||||
|
isOwner: isTherapist,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
roomUrl: `${room.url}?t=${token.token}`,
|
||||||
|
roomName: room.name,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao criar sala:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Erro ao criar sala de vídeo' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
297
src/app/dashboard/agenda/page.tsx
Normal file
297
src/app/dashboard/agenda/page.tsx
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import {
|
||||||
|
Sparkles, ArrowLeft, Calendar, Clock, Video, User,
|
||||||
|
ChevronLeft, ChevronRight, Plus, Loader2
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface Appointment {
|
||||||
|
id: string;
|
||||||
|
scheduledAt: string;
|
||||||
|
duration: number;
|
||||||
|
status: string;
|
||||||
|
roomUrl: string | null;
|
||||||
|
child: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
therapist: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AgendaPage() {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const [appointments, setAppointments] = useState<Appointment[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAppointments();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchAppointments = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/appointments');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setAppointments(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao buscar agendamentos:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDaysInMonth = (date: Date) => {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth();
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
|
const lastDay = new Date(year, month + 1, 0);
|
||||||
|
const daysInMonth = lastDay.getDate();
|
||||||
|
const startingDay = firstDay.getDay();
|
||||||
|
|
||||||
|
const days = [];
|
||||||
|
|
||||||
|
// Dias do mês anterior
|
||||||
|
for (let i = 0; i < startingDay; i++) {
|
||||||
|
const prevDate = new Date(year, month, -startingDay + i + 1);
|
||||||
|
days.push({ date: prevDate, isCurrentMonth: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dias do mês atual
|
||||||
|
for (let i = 1; i <= daysInMonth; i++) {
|
||||||
|
days.push({ date: new Date(year, month, i), isCurrentMonth: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dias do próximo mês
|
||||||
|
const remainingDays = 42 - days.length;
|
||||||
|
for (let i = 1; i <= remainingDays; i++) {
|
||||||
|
days.push({ date: new Date(year, month + 1, i), isCurrentMonth: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAppointmentsForDate = (date: Date) => {
|
||||||
|
return appointments.filter(apt => {
|
||||||
|
const aptDate = new Date(apt.scheduledAt);
|
||||||
|
return aptDate.toDateString() === date.toDateString();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (dateStr: string) => {
|
||||||
|
return new Date(dateStr).toLocaleTimeString('pt-BR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isToday = (date: Date) => {
|
||||||
|
const today = new Date();
|
||||||
|
return date.toDateString() === today.toDateString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFuture = (dateStr: string) => {
|
||||||
|
return new Date(dateStr) > new Date();
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevMonth = () => {
|
||||||
|
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextMonth = () => {
|
||||||
|
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const monthYear = currentDate.toLocaleDateString('pt-BR', {
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
const days = getDaysInMonth(currentDate);
|
||||||
|
const weekDays = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
|
||||||
|
|
||||||
|
// Próximos agendamentos
|
||||||
|
const upcomingAppointments = appointments
|
||||||
|
.filter(apt => isFuture(apt.scheduledAt))
|
||||||
|
.slice(0, 5);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="bg-white border-b border-gray-200 px-4 lg:px-8 py-4">
|
||||||
|
<div className="max-w-7xl mx-auto flex items-center gap-4">
|
||||||
|
<Link href="/dashboard" className="text-gray-400 hover:text-gray-600">
|
||||||
|
<ArrowLeft className="w-6 h-6" />
|
||||||
|
</Link>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-xl gradient-rainbow flex items-center justify-center">
|
||||||
|
<Calendar className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-bold text-gray-900">Agenda</h1>
|
||||||
|
<p className="text-sm text-gray-500">Seus agendamentos</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto p-4 lg:p-8">
|
||||||
|
<div className="grid lg:grid-cols-3 gap-8">
|
||||||
|
{/* Calendar */}
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 capitalize">
|
||||||
|
{monthYear}
|
||||||
|
</h2>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={prevMonth}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-lg transition"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={nextMonth}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-lg transition"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Week days header */}
|
||||||
|
<div className="grid grid-cols-7 mb-2">
|
||||||
|
{weekDays.map(day => (
|
||||||
|
<div key={day} className="text-center text-sm font-medium text-gray-500 py-2">
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Calendar grid */}
|
||||||
|
<div className="grid grid-cols-7 gap-1">
|
||||||
|
{days.map((day, i) => {
|
||||||
|
const dayAppointments = getAppointmentsForDate(day.date);
|
||||||
|
const hasAppointments = dayAppointments.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`aspect-square p-1 ${
|
||||||
|
day.isCurrentMonth ? 'bg-white' : 'bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`h-full rounded-lg p-1 ${
|
||||||
|
isToday(day.date)
|
||||||
|
? 'bg-indigo-100 text-indigo-600'
|
||||||
|
: day.isCurrentMonth
|
||||||
|
? 'hover:bg-gray-50'
|
||||||
|
: 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className={`text-sm font-medium ${
|
||||||
|
isToday(day.date) ? 'bg-indigo-600 text-white rounded-full w-6 h-6 flex items-center justify-center' : ''
|
||||||
|
}`}>
|
||||||
|
{day.date.getDate()}
|
||||||
|
</span>
|
||||||
|
{hasAppointments && (
|
||||||
|
<div className="mt-1 space-y-0.5">
|
||||||
|
{dayAppointments.slice(0, 2).map(apt => (
|
||||||
|
<div
|
||||||
|
key={apt.id}
|
||||||
|
className="text-xs bg-indigo-500 text-white rounded px-1 py-0.5 truncate"
|
||||||
|
>
|
||||||
|
{formatTime(apt.scheduledAt)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{dayAppointments.length > 2 && (
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
+{dayAppointments.length - 2}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Upcoming appointments */}
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
||||||
|
Próximas Sessões
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex justify-center py-8">
|
||||||
|
<Loader2 className="w-8 h-8 text-indigo-500 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : upcomingAppointments.length === 0 ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Calendar className="w-12 h-12 text-gray-300 mx-auto mb-3" />
|
||||||
|
<p className="text-gray-500">Nenhuma sessão agendada</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{upcomingAppointments.map(apt => (
|
||||||
|
<div key={apt.id} className="p-4 bg-gray-50 rounded-xl">
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-900">
|
||||||
|
{apt.child.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
com {apt.therapist.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className={`text-xs px-2 py-1 rounded-full ${
|
||||||
|
apt.status === 'scheduled'
|
||||||
|
? 'bg-green-100 text-green-700'
|
||||||
|
: 'bg-gray-100 text-gray-600'
|
||||||
|
}`}>
|
||||||
|
{apt.status === 'scheduled' ? 'Agendado' : apt.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-sm text-gray-500 mb-3">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
{new Date(apt.scheduledAt).toLocaleDateString('pt-BR')}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
{formatTime(apt.scheduledAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={`/dashboard/sessao/${apt.id}`}
|
||||||
|
className="flex items-center justify-center gap-2 w-full bg-indigo-600 text-white py-2 rounded-lg text-sm font-medium hover:bg-indigo-700 transition"
|
||||||
|
>
|
||||||
|
<Video className="w-4 h-4" />
|
||||||
|
Entrar na Sessão
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
194
src/app/dashboard/sessao/[id]/page.tsx
Normal file
194
src/app/dashboard/sessao/[id]/page.tsx
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import {
|
||||||
|
Sparkles, ArrowLeft, Video, VideoOff, Mic, MicOff,
|
||||||
|
Phone, Settings, MessageCircle, Users, Loader2
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
export default function SessaoPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [roomUrl, setRoomUrl] = useState<string | null>(null);
|
||||||
|
const [callFrame, setCallFrame] = useState<any>(null);
|
||||||
|
const [videoEnabled, setVideoEnabled] = useState(true);
|
||||||
|
const [audioEnabled, setAudioEnabled] = useState(true);
|
||||||
|
|
||||||
|
const appointmentId = params.id as string;
|
||||||
|
|
||||||
|
const initializeCall = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Buscar/criar sala
|
||||||
|
const response = await fetch('/api/daily/room', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ appointmentId }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
throw new Error(data.error || 'Erro ao conectar');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { roomUrl } = await response.json();
|
||||||
|
setRoomUrl(roomUrl);
|
||||||
|
|
||||||
|
// Carregar Daily.co iframe
|
||||||
|
const DailyIframe = (await import('@daily-co/daily-js')).default;
|
||||||
|
|
||||||
|
const frame = DailyIframe.createFrame(
|
||||||
|
document.getElementById('call-container')!,
|
||||||
|
{
|
||||||
|
iframeStyle: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
border: '0',
|
||||||
|
borderRadius: '12px',
|
||||||
|
},
|
||||||
|
showLeaveButton: true,
|
||||||
|
showFullscreenButton: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await frame.join({ url: roomUrl });
|
||||||
|
setCallFrame(frame);
|
||||||
|
|
||||||
|
frame.on('left-meeting', () => {
|
||||||
|
router.push('/dashboard');
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [appointmentId, router]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initializeCall();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (callFrame) {
|
||||||
|
callFrame.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [initializeCall]);
|
||||||
|
|
||||||
|
const toggleVideo = () => {
|
||||||
|
if (callFrame) {
|
||||||
|
callFrame.setLocalVideo(!videoEnabled);
|
||||||
|
setVideoEnabled(!videoEnabled);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAudio = () => {
|
||||||
|
if (callFrame) {
|
||||||
|
callFrame.setLocalAudio(!audioEnabled);
|
||||||
|
setAudioEnabled(!audioEnabled);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const endCall = () => {
|
||||||
|
if (callFrame) {
|
||||||
|
callFrame.leave();
|
||||||
|
}
|
||||||
|
router.push('/dashboard');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Loader2 className="w-12 h-12 text-indigo-500 animate-spin mx-auto mb-4" />
|
||||||
|
<p className="text-white text-lg">Conectando à sessão...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
||||||
|
<div className="text-center max-w-md mx-auto p-8">
|
||||||
|
<div className="w-16 h-16 bg-red-500/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Video className="w-8 h-8 text-red-500" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-xl font-semibold text-white mb-2">Erro ao conectar</h1>
|
||||||
|
<p className="text-gray-400 mb-6">{error}</p>
|
||||||
|
<Link
|
||||||
|
href="/dashboard"
|
||||||
|
className="inline-flex items-center gap-2 text-indigo-400 hover:text-indigo-300"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
Voltar ao Dashboard
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="bg-gray-800 border-b border-gray-700 px-4 py-3">
|
||||||
|
<div className="flex items-center justify-between max-w-7xl mx-auto">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg gradient-rainbow flex items-center justify-center">
|
||||||
|
<Sparkles className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<span className="text-white font-semibold">Sessão de Terapia</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||||
|
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||||
|
Conectado
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Video Container */}
|
||||||
|
<div className="flex-1 p-4">
|
||||||
|
<div id="call-container" className="w-full h-full min-h-[70vh] bg-gray-800 rounded-xl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controls */}
|
||||||
|
<div className="bg-gray-800 border-t border-gray-700 px-4 py-4">
|
||||||
|
<div className="flex items-center justify-center gap-4 max-w-md mx-auto">
|
||||||
|
<button
|
||||||
|
onClick={toggleAudio}
|
||||||
|
className={`w-14 h-14 rounded-full flex items-center justify-center transition ${
|
||||||
|
audioEnabled
|
||||||
|
? 'bg-gray-700 text-white hover:bg-gray-600'
|
||||||
|
: 'bg-red-500 text-white hover:bg-red-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{audioEnabled ? <Mic className="w-6 h-6" /> : <MicOff className="w-6 h-6" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={toggleVideo}
|
||||||
|
className={`w-14 h-14 rounded-full flex items-center justify-center transition ${
|
||||||
|
videoEnabled
|
||||||
|
? 'bg-gray-700 text-white hover:bg-gray-600'
|
||||||
|
: 'bg-red-500 text-white hover:bg-red-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{videoEnabled ? <Video className="w-6 h-6" /> : <VideoOff className="w-6 h-6" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={endCall}
|
||||||
|
className="w-14 h-14 bg-red-500 text-white rounded-full flex items-center justify-center hover:bg-red-600 transition"
|
||||||
|
>
|
||||||
|
<Phone className="w-6 h-6 rotate-135" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
130
src/lib/daily.ts
Normal file
130
src/lib/daily.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
const DAILY_API_KEY = process.env.DAILY_API_KEY;
|
||||||
|
const DAILY_API_URL = 'https://api.daily.co/v1';
|
||||||
|
|
||||||
|
interface DailyRoom {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
created_at: string;
|
||||||
|
config: {
|
||||||
|
exp?: number;
|
||||||
|
nbf?: number;
|
||||||
|
max_participants?: number;
|
||||||
|
enable_chat?: boolean;
|
||||||
|
enable_knocking?: boolean;
|
||||||
|
start_video_off?: boolean;
|
||||||
|
start_audio_off?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateRoomOptions {
|
||||||
|
name?: string;
|
||||||
|
expiryMinutes?: number;
|
||||||
|
maxParticipants?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRoom(options: CreateRoomOptions = {}): Promise<DailyRoom> {
|
||||||
|
const { name, expiryMinutes = 60, maxParticipants = 4 } = options;
|
||||||
|
|
||||||
|
const roomName = name || `sessao-${Date.now()}`;
|
||||||
|
const exp = Math.floor(Date.now() / 1000) + expiryMinutes * 60;
|
||||||
|
|
||||||
|
const response = await fetch(`${DAILY_API_URL}/rooms`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${DAILY_API_KEY}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: roomName,
|
||||||
|
properties: {
|
||||||
|
exp,
|
||||||
|
max_participants: maxParticipants,
|
||||||
|
enable_chat: true,
|
||||||
|
enable_knocking: true,
|
||||||
|
start_video_off: false,
|
||||||
|
start_audio_off: false,
|
||||||
|
enable_screenshare: true,
|
||||||
|
enable_recording: 'cloud',
|
||||||
|
eject_at_room_exp: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || 'Erro ao criar sala');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRoom(roomName: string): Promise<DailyRoom | null> {
|
||||||
|
const response = await fetch(`${DAILY_API_URL}/rooms/${roomName}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${DAILY_API_KEY}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) return null;
|
||||||
|
throw new Error('Erro ao buscar sala');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteRoom(roomName: string): Promise<void> {
|
||||||
|
const response = await fetch(`${DAILY_API_URL}/rooms/${roomName}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${DAILY_API_KEY}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok && response.status !== 404) {
|
||||||
|
throw new Error('Erro ao deletar sala');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MeetingToken {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMeetingToken(
|
||||||
|
roomName: string,
|
||||||
|
options: {
|
||||||
|
userName: string;
|
||||||
|
isOwner?: boolean;
|
||||||
|
expiryMinutes?: number;
|
||||||
|
}
|
||||||
|
): Promise<MeetingToken> {
|
||||||
|
const { userName, isOwner = false, expiryMinutes = 60 } = options;
|
||||||
|
const exp = Math.floor(Date.now() / 1000) + expiryMinutes * 60;
|
||||||
|
|
||||||
|
const response = await fetch(`${DAILY_API_URL}/meeting-tokens`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${DAILY_API_KEY}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
properties: {
|
||||||
|
room_name: roomName,
|
||||||
|
user_name: userName,
|
||||||
|
is_owner: isOwner,
|
||||||
|
exp,
|
||||||
|
enable_screenshare: true,
|
||||||
|
start_video_off: false,
|
||||||
|
start_audio_off: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || 'Erro ao criar token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user