diff --git a/backend/Dockerfile b/backend/Dockerfile index 22112d5..a559606 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,24 +1,8 @@ -FROM node:20-slim AS builder +FROM node:20-slim WORKDIR /app -COPY package*.json baileys-7.0.0-rc.9.tgz ./ -RUN npm ci +COPY package*.json ./ +RUN npm install --omit=dev && npm cache clean --force COPY . . -RUN npx prisma generate -RUN npm run build - -FROM node:20-slim AS runner -RUN apt-get update -y && apt-get install -y openssl ca-certificates && rm -rf /var/lib/apt/lists/* -WORKDIR /app -COPY package*.json baileys-7.0.0-rc.9.tgz ./ -RUN npm ci --omit=dev && npm cache clean --force -COPY --from=builder /app/dist ./dist -COPY --from=builder /app/prisma ./prisma -COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma -COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma -RUN ln -s /app/plugins /plugins && \ - ln -s /app /app/backend && \ - ln -s /app/dist /app/src - -EXPOSE 8008 +EXPOSE 8018 ENV NODE_ENV=production -CMD ["node", "dist/server.js"] +CMD ["node", "server.js"] diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..ae4eccf --- /dev/null +++ b/backend/package.json @@ -0,0 +1,18 @@ +{ + "name": "scoreodonto-api", + "version": "1.0.0", + "type": "module", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "googleapis": "^171.4.0", + "jsonwebtoken": "^9.0.3", + "pg": "^8.11.3", + "ioredis": "^5.4.1" + } +} diff --git a/backend/postgres-adapter.js b/backend/postgres-adapter.js new file mode 100644 index 0000000..37329da --- /dev/null +++ b/backend/postgres-adapter.js @@ -0,0 +1,143 @@ +import pg from 'pg'; +const { Pool } = pg; + +// Simple query translator from MySQL dialect to PostgreSQL +function translateQuery(query, params) { + if (typeof query !== 'string') return { sql: query, pgParams: params }; + + let sql = query; + let pgParams = params ? [...params] : []; + + // Remove or convert backticks + sql = sql.replace(/`([^`]+)`/g, '"$1"'); + + // INSERT INTO table SET ? + if (sql.match(/INSERT\s+INTO\s+([^\s]+)\s+SET\s+\?/i)) { + const table = sql.match(/INSERT\s+INTO\s+([^\s]+)\s+SET\s+\?/i)[1].replace(/"/g, ''); + const dataObj = pgParams[0]; + if (dataObj && typeof dataObj === 'object') { + const keys = Object.keys(dataObj); + const cols = keys.map(k => `"${k}"`).join(', '); + const valuesPlaceholder = keys.map((_, idx) => `$${idx + 1}`).join(', '); + sql = `INSERT INTO "${table}" (${cols}) VALUES (${valuesPlaceholder})`; + pgParams = keys.map(k => dataObj[k]); + } + } + + // UPDATE table SET ? WHERE ... + if (sql.match(/UPDATE\s+([^\s]+)\s+SET\s+\?\s+WHERE\s+(.+)/i)) { + const match = sql.match(/UPDATE\s+([^\s]+)\s+SET\s+\?\s+WHERE\s+(.+)/i); + const table = match[1].replace(/"/g, ''); + const whereClause = match[2]; + const dataObj = pgParams[0]; + const extraParams = pgParams.slice(1); + + if (dataObj && typeof dataObj === 'object') { + const keys = Object.keys(dataObj); + let paramIdx = 1; + const setClause = keys.map(k => `"${k}" = $${paramIdx++}`).join(', '); + + let postgresWhere = whereClause; + let tempIdx = paramIdx; + postgresWhere = postgresWhere.replace(/\?/g, () => `$${tempIdx++}`); + + sql = `UPDATE "${table}" SET ${setClause} WHERE ${postgresWhere}`; + pgParams = [...keys.map(k => dataObj[k]), ...extraParams]; + } + } + + // Translate standard '?' to '$1', '$2', etc. + let count = 1; + sql = sql.replace(/\?/g, () => `$${count++}`); + + // ON DUPLICATE KEY UPDATE -> ON CONFLICT + if (sql.match(/ON\s+DUPLICATE\s+KEY\s+UPDATE/i)) { + if (sql.toLowerCase().includes('settings')) { + sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE\s+data\s*=\s*VALUES\(data\)|ON\s+DUPLICATE\s+KEY\s+UPDATE\s+data\s*=\s*\$4|ON\s+DUPLICATE\s+KEY\s+UPDATE\s+data\s*=\s*EXCLUDED\.data/i, 'ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data'); + } else if (sql.toLowerCase().includes('google_tokens')) { + sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (owner_id) DO UPDATE SET refresh_token = EXCLUDED.refresh_token, access_token = EXCLUDED.access_token, expiry_date = EXCLUDED.expiry_date'); + } else if (sql.toLowerCase().includes('usuarios')) { + sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome, cro = EXCLUDED.cro, cro_uf = EXCLUDED.cro_uf, celular = EXCLUDED.celular, especialidade = EXCLUDED.especialidade'); + } else if (sql.toLowerCase().includes('vinculos')) { + sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (usuario_id, clinica_id) DO UPDATE SET role = EXCLUDED.role'); + } else if (sql.toLowerCase().includes('dentistas_horarios')) { + sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (id) DO UPDATE SET dia_semana = EXCLUDED.dia_semana, hora_inicio = EXCLUDED.hora_inicio, hora_fim = EXCLUDED.hora_fim, ativo = EXCLUDED.ativo'); + } else if (sql.toLowerCase().includes('dentistas')) { + sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome, especialidade = EXCLUDED.especialidade'); + } else if (sql.toLowerCase().includes('procedimentos_valores_planos')) { + sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (id) DO UPDATE SET valor = EXCLUDED.valor'); + } else if (sql.toLowerCase().includes('procedimentos')) { + sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome, "especialidadeId" = EXCLUDED."especialidadeId", "especialidadeNome" = EXCLUDED."especialidadeNome", "valorParticular" = EXCLUDED."valorParticular", "codigoInterno" = EXCLUDED."codigoInterno", categoria = EXCLUDED.categoria, tipo_regiao = EXCLUDED.tipo_regiao, exige_face = EXCLUDED.exige_face, codigo = EXCLUDED.codigo'); + } + } + + // DATE_FORMAT -> TO_CHAR + sql = sql.replace(/DATE_FORMAT\(([^,]+),\s*['"]%Y-%m['"]\)/gi, "TO_CHAR($1, 'YYYY-MM')"); + // DATE_SUB -> CURRENT_DATE - INTERVAL + sql = sql.replace(/DATE_SUB\(CURDATE\(\),\s*INTERVAL\s+(\d+)\s+MONTH\)/gi, "CURRENT_DATE - INTERVAL '$1 MONTH'"); + + // SET FOREIGN_KEY_CHECKS + if (sql.match(/SET\s+FOREIGN_KEY_CHECKS\s*=\s*\d+/i)) { + sql = "SELECT 1"; + } + + return { sql, pgParams }; +} + +class PostgresConnection { + constructor(client) { + this.client = client; + } + + async query(query, params) { + const { sql, pgParams } = translateQuery(query, params); + const res = await this.client.query(sql, pgParams); + return [res.rows, res.fields]; + } + + async beginTransaction() { + await this.client.query('BEGIN'); + } + + async commit() { + await this.client.query('COMMIT'); + } + + async rollback() { + await this.client.query('ROLLBACK'); + } + + release() { + this.client.release(); + } +} + +class PostgresPool { + constructor(config) { + // Convert connection string from postgresql:// to pool config + const connectionString = process.env.DATABASE_URL; + this.pool = new Pool({ + connectionString: connectionString || `postgresql://${config.user}:${config.password}@${config.host}:5432/${config.database}`, + ssl: connectionString && connectionString.includes('localhost') ? false : { rejectUnauthorized: false } + }); + } + + async query(query, params) { + const { sql, pgParams } = translateQuery(query, params); + const res = await this.pool.query(sql, pgParams); + return [res.rows, res.fields]; + } + + async getConnection() { + const client = await this.pool.connect(); + return new PostgresConnection(client); + } + + async end() { + await this.pool.end(); + } +} + +export default { + createPool: (config) => new PostgresPool(config) +}; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma new file mode 100644 index 0000000..c2329ee --- /dev/null +++ b/backend/prisma/schema.prisma @@ -0,0 +1,68 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Beneficiario { + id String @id @default(uuid()) + nome String + identificacao String @unique + guias GuiaOdontologica[] + gtoBuilders GtoBuilder[] + createdAt DateTime @default(now()) +} + +model GuiaOdontologica { + id String @id @default(uuid()) + numeroGuiaPrestador String + numeroGuiaOperadora String? + dataSolicitacao DateTime + tipoTratamento String + status StatusGuia + beneficiarioId String + beneficiario Beneficiario @relation(fields: [beneficiarioId], references: [id]) + createdAt DateTime @default(now()) +} + +model GtoBuilder { + id String @id @default(uuid()) + pacienteId String + paciente Beneficiario @relation(fields: [pacienteId], references: [id]) + dentistaId String + credenciadaId String + status String @default("RASCUNHO") // RASCUNHO | FINALIZADA + total Float @default(0) + items GtoItem[] + createdAt DateTime @default(now()) +} + +model GtoItem { + id String @id @default(uuid()) + builderId String + builder GtoBuilder @relation(fields: [builderId], references: [id]) + procedimentoId String + codigoTUSS String + descricao String + dente String? + face String? + quantidade Int @default(1) + valorUnitario Float + valorTotal Float + observacao String? + anexosCount Int @default(0) +} + +enum StatusGuia { + EM_ANALISE + AUTORIZADO + AUTORIZADO_PARCIAL + NEGADO + CANCELADO +} diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..cb085c5 --- /dev/null +++ b/backend/server.js @@ -0,0 +1,1578 @@ +import 'dotenv/config'; +import express from 'express'; +import mysql from './postgres-adapter.js'; +import Redis from 'ioredis'; +import cors from 'cors'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +import { google } from 'googleapis'; +import jwt from 'jsonwebtoken'; + +const JWT_SECRET = process.env.JWT_SECRET || 'scoreodonto_secret_key_2026'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const app = express(); +app.use(express.json()); +app.use(cors()); + +// --- HEALTH CHECK --- +app.get('/api/ping', (req, res) => res.json({ status: 'ok' })); + +// ============================================================================= +// SECURITY MIDDLEWARE: Tenant Guard +// Validates that the authenticated user truly belongs to the clinica_id +// found in: req.params.clinicaId | req.body.clinica_id | req.query.clinicaId +// ============================================================================= +async function tenantGuard(req, res, next) { + const authHeader = req.headers['authorization']; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ success: false, message: 'TOKEN DE AUTENTICAÇÃO NÃO ENVIADO.' }); + } + + let payload; + try { + payload = jwt.verify(authHeader.split(' ')[1], JWT_SECRET); + } catch { + return res.status(401).json({ success: false, message: 'TOKEN INVÁLIDO OU EXPIRADO. FAÇA LOGIN NOVAMENTE.' }); + } + + // Extract clinica_id from any possible location in the request + const clinicaId = + req.params.clinicaId || + req.body?.clinica_id || + req.query?.clinicaId || + req.body?.clinicaId; + + if (clinicaId) { + // Verify that the authenticated user has a real link to this clinic + const [vinculos] = await pool.query( + 'SELECT id FROM vinculos WHERE usuario_id = ? AND clinica_id = ?', + [payload.userId, clinicaId] + ); + if (vinculos.length === 0) { + console.warn(`[TENANT VIOLATION] User ${payload.userId} tried to access clinica ${clinicaId}`); + return res.status(403).json({ success: false, message: 'ACESSO NEGADO: VOCÊ NÃO TEM VÍNCULO COM ESTA UNIDADE.' }); + } + } + + // Attach verified user info to the request for downstream use + req.authUser = payload; + next(); +} + +// Helper: add Authorization header to all backend fetch calls +export function getAuthHeaders() { + const token = typeof localStorage !== 'undefined' + ? localStorage.getItem('SCOREODONTO_AUTH_TOKEN') + : null; + return token ? { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' }; +} + +// --- CLINICAS --- +app.put('/api/clinicas/:id/color', async (req, res) => { + const { cor } = req.body; + try { + await pool.query('UPDATE clinicas SET cor = ? WHERE id = ?', [cor, req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- LOGIN ROUTE --- +app.post('/api/login', async (req, res) => { + const { email, password } = req.body; + console.log(`[LOGIN ATTEMPT] Email: ${email}`); + try { + const [users] = await pool.query('SELECT id, nome, email FROM usuarios WHERE email = ? AND senha = ?', [email, password]); + if (users.length === 0) { + console.log(`[LOGIN FAILED] Invalid credentials for: ${email}`); + return res.status(401).json({ success: false, message: 'CREDENCIAIS INVÁLIDAS' }); + } + + const user = users[0]; + console.log(`[LOGIN AUTHENTICATED] User ID: ${user.id}`); + + const [workspaces] = await pool.query(` + SELECT v.clinica_id as id, c.nome_fantasia as nome, c.cor, v.role + FROM vinculos v + JOIN clinicas c ON v.clinica_id = c.id + WHERE v.usuario_id = ? + `, [user.id]); + + if (workspaces.length === 0) { + console.log(`[LOGIN FORBIDDEN] No workspaces found for User ID: ${user.id}`); + return res.status(403).json({ success: false, message: 'USUÁRIO SEM VÍNCULO COM CLÍNICAS' }); + } + + console.log(`[LOGIN SUCCESS] User: ${user.email}, Workspaces: ${workspaces.length}`); + // Generate a real JWT with userId embedded — this is validated by tenantGuard + const token = jwt.sign( + { userId: user.id, email: user.email }, + JWT_SECRET, + { expiresIn: '12h' } + ); + res.json({ + success: true, + token, + user: { + id: user.id, + nome: user.nome, + email: user.email + }, + workspaces: workspaces + }); + } catch (err) { + console.error(`[LOGIN ERROR]: ${err.message}`); + res.status(500).json({ success: false, error: err.message }); + } +}); + +const dbConfig = { + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'sis_odonto', + charset: 'utf8mb4' +}; + +let pool; +let redis; + +try { + const DRAGONFLY_URL = process.env.DRAGONFLY_URL || 'redis://localhost:6379'; + redis = new Redis(DRAGONFLY_URL); + redis.on('connect', () => console.log('Connected to DragonflyDB (Redis)')); + redis.on('error', (err) => console.error('DragonflyDB Connection Error:', err.message)); +} catch (e) { + console.error('Failed to initialize DragonflyDB:', e.message); +} + +async function connect() { + pool = await mysql.createPool(dbConfig); + console.log('Connected to PostgreSQL (via adapter)'); + // initTables is bypassed in production PostgreSQL in favor of migration schemas + // await initTables(); // Ensure tables are initialized AFTER pool is ready +} +connect(); + +const PORT = process.env.PORT || 3005; + +// --- GOOGLE OAUTH2 SETUP --- +const createOAuth2Client = () => new google.auth.OAuth2( + process.env.GOOGLE_CLIENT_ID, + process.env.GOOGLE_CLIENT_SECRET, + process.env.GOOGLE_REDIRECT_URI || `http://localhost:${PORT}/api/oauth2callback` +); + +app.listen(PORT, '0.0.0.0', () => console.log(`Server v2 running on http://0.0.0.0:${PORT}`)); + +// --- GOOGLE AUTH ROUTES --- +app.get('/api/auth/google/url', (req, res) => { + const { dentistId } = req.query; + const oauth2Client = createOAuth2Client(); + const url = oauth2Client.generateAuthUrl({ + access_type: 'offline', + scope: [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/calendar.readonly' + ], + state: dentistId || 'admin', + prompt: 'consent' + }); + res.json({ url }); +}); + +app.get('/api/oauth2callback', async (req, res) => { + const { code, state } = req.query; + if (!code) return res.status(400).send('Código não fornecido'); + + try { + const oauth2Client = createOAuth2Client(); + const { tokens } = await oauth2Client.getToken(code); + + // Se não veio dentistId no state, tentamos pegar o email do usuario + let ownerId = state || 'admin'; + + if (ownerId === 'admin') { + oauth2Client.setCredentials(tokens); + const oauth2 = google.oauth2({ version: 'v2', auth: oauth2Client }); + const { data } = await oauth2.userinfo.get(); + ownerId = data.email; + } + + // Salva tokens no MySQL + await pool.query(` + INSERT INTO google_tokens (owner_id, refresh_token, access_token, expiry_date) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + refresh_token = VALUES(refresh_token), + access_token = VALUES(access_token), + expiry_date = VALUES(expiry_date) + `, [ownerId, tokens.refresh_token, tokens.access_token, tokens.expiry_date]); + + res.send(` +
+

Agenda Conectada!

+

O Google Agenda foi integrado com sucesso.

+

Você já pode fechar esta janela.

+ +
+ `); + } catch (err) { + console.error('OAuth Error:', err.message); + res.status(500).send('Erro na autorização com o Google.'); + } +}); + +app.get('/api/auth/google/status', async (req, res) => { + try { + const [rows] = await pool.query('SELECT owner_id, updated_at FROM google_tokens'); + res.json(rows); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.delete('/api/auth/google/:ownerId', async (req, res) => { + try { + await pool.query('DELETE FROM google_tokens WHERE owner_id = ?', [req.params.ownerId]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- SETTINGS --- +app.get('/api/settings', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM settings WHERE id = "main"'); + res.json(rows[0] ? JSON.parse(rows[0].data) : {}); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/settings', async (req, res) => { + try { + const data = JSON.stringify(req.body); + await pool.query('INSERT INTO settings (id, category, data) VALUES ("main", "general", ?) ON DUPLICATE KEY UPDATE data = ?', [data, data]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- MAGIC LINK & DENTIST SELF-REGISTRATION --- +app.post('/api/dentistas/magic-link', async (req, res) => { + const { clinicaId, dentistName } = req.body; + try { + const token = Math.random().toString(36).substring(2, 15); + // Em um sistema real, salvaríamos esse token em uma tabela de convites + const magicLink = `http://localhost:3000/#/cadastro-dentista?token=${token}&clinica=${clinicaId}&nome=${encodeURIComponent(dentistName)}`; + res.json({ success: true, link: magicLink }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/dentistas/register', async (req, res) => { + const { id, nome, email, senha, cro, cro_uf, celular, especialidade, clinicaId } = req.body; + try { + await pool.query('SET FOREIGN_KEY_CHECKS = 0'); + // 1. Create User + await pool.query(` + INSERT INTO usuarios (id, nome, email, senha, cro, cro_uf, celular, especialidade) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + nome = VALUES(nome), cro = VALUES(cro), cro_uf = VALUES(cro_uf), + celular = VALUES(celular), especialidade = VALUES(especialidade) + `, [id, nome, email, senha, cro, cro_uf, celular, especialidade]); + + // 2. Create Link to clinic + const vinculoId = `v_${id}_${clinicaId}`; + await pool.query(` + INSERT INTO vinculos (id, usuario_id, clinica_id, role) + VALUES (?, ?, ?, 'dentista') + ON DUPLICATE KEY UPDATE role = 'dentista' + `, [vinculoId, id, clinicaId]); + + // 3. Create Dentist record for agenda + await pool.query(` + INSERT INTO dentistas (id, clinica_id, nome, email, telefone, especialidade, corAgenda) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + nome = VALUES(nome), especialidade = VALUES(especialidade) + `, [id, clinicaId, nome, email, celular, especialidade, '#2563eb']); + + await pool.query('SET FOREIGN_KEY_CHECKS = 1'); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.get('/api/google/events', async (req, res) => { + try { + // 1. Check for connected accounts in google_tokens + const [tokenRows] = await pool.query('SELECT * FROM google_tokens'); + const allEvents = []; + const now = new Date(); + const timeMin = now.toISOString(); + + for (const tokenData of tokenRows) { + try { + const oauth2Client = createOAuth2Client(); + oauth2Client.setCredentials({ + refresh_token: tokenData.refresh_token, + access_token: tokenData.access_token, + expiry_date: tokenData.expiry_date + }); + + const calendar = google.calendar({ version: 'v3', auth: oauth2Client }); + const response = await calendar.events.list({ + calendarId: 'primary', + timeMin: timeMin, + singleEvents: true, + orderBy: 'startTime', + maxResults: 100 + }); + + if (response.data.items) { + // Try to resolve owner name + let ownerName = tokenData.owner_id; + const [dent] = await pool.query('SELECT nome FROM dentistas WHERE id = ?', [tokenData.owner_id]); + if (dent[0]) ownerName = dent[0].nome; + + const mapped = response.data.items.map(item => ({ + id: `google_${item.id}`, + title: `[${ownerName}] ${item.summary || '(Sem Título)'}`, + start: item.start.dateTime || item.start.date, + end: item.end.dateTime || item.end.date, + isGoogle: true, + owner: ownerName, + backgroundColor: '#3b82f6', + borderColor: '#3b82f6' + })); + allEvents.push(...mapped); + } + } catch (err) { + console.error(`Error fetching calendar for ${tokenData.owner_id}:`, err.message); + } + } + + // 2. Legacy Support: Check Settings for Public Calendars (API KEY) + const [rows] = await pool.query('SELECT data FROM settings WHERE id = "main"'); + if (rows[0]) { + const settings = JSON.parse(rows[0].data); + const apiKey = settings.googleApiKey; + if (apiKey) { + const legacyIds = []; + if (settings.clinicCalendarId) legacyIds.push({ id: settings.clinicCalendarId, owner: 'CLÍNICA' }); + // We only add dentists that DON'T have a token yet to avoid duplication + if (settings.googleCalendarIds) { + for (const [dentistId, calId] of Object.entries(settings.googleCalendarIds)) { + if (calId && !tokenRows.find(t => t.owner_id === dentistId)) { + const [dName] = await pool.query('SELECT nome FROM dentistas WHERE id = ?', [dentistId]); + legacyIds.push({ id: calId, owner: dName[0]?.nome || 'DENTISTA' }); + } + } + } + + for (const { id, owner } of legacyIds) { + try { + const cleanId = id.trim().toLowerCase(); + const response = await fetch(`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(cleanId)}/events?key=${apiKey}&timeMin=${timeMin}&singleEvents=true&orderBy=startTime`); + const data = await response.json(); + if (data.items) { + const mapped = data.items.map(item => ({ + id: `google_legacy_${item.id}`, + title: `[${owner}] ${item.summary || '(Sem Título)'}`, + start: item.start.dateTime || item.start.date, + end: item.end.dateTime || item.end.date, + isGoogle: true, + owner: owner, + backgroundColor: owner === 'CLÍNICA' ? '#10b981' : '#6b7280', + borderColor: owner === 'CLÍNICA' ? '#10b981' : '#6b7280' + })); + allEvents.push(...mapped); + } + } catch (err) { console.error(`Legacy API Error for ${id}:`, err.message); } + } + } + } + + res.json(allEvents); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- DASHBOARD STATS --- +app.get('/api/dashboard/stats', async (req, res) => { + try { + const cacheKey = 'dashboard:stats'; + if (redis) { + const cachedStats = await redis.get(cacheKey); + if (cachedStats) { + console.log('[CACHE HIT] Serving dashboard stats from DragonflyDB'); + return res.json(JSON.parse(cachedStats)); + } + } + + const [pacientes] = await pool.query('SELECT COUNT(*) as count FROM pacientes'); + const [agendamentos] = await pool.query('SELECT COUNT(*) as count FROM agendamentos'); + const [leads] = await pool.query('SELECT COUNT(*) as count FROM leads'); + const [faturamento] = await pool.query('SELECT COALESCE(SUM(valor), 0) as total FROM financeiro WHERE tipo = "RECEITA" OR tipo IS NULL'); + const [pendente] = await pool.query('SELECT COALESCE(SUM(valor), 0) as total FROM financeiro WHERE tipo = "DESPESA"'); + + // Growth data (last 6 months) + // PostgreSQL DATE_FORMAT converted dynamically via adapter, but adapter fallback handles date operations perfectly. + const [growth] = await pool.query(` + SELECT + DATE_FORMAT(dataVencimento, '%Y-%m') as mes, + SUM(CASE WHEN tipo = 'RECEITA' OR tipo IS NULL THEN valor ELSE 0 END) as receita, + SUM(CASE WHEN tipo = 'DESPESA' THEN valor ELSE 0 END) as despesa + FROM financeiro + WHERE dataVencimento >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH) + GROUP BY mes + ORDER BY mes ASC + `); + + // Recent appointments with patient and dentist names + const [recent] = await pool.query(` + SELECT a.*, d.nome as dentistaNome + FROM agendamentos a + LEFT JOIN dentistas d ON a.dentistaId = d.id + ORDER BY a.start_time DESC + LIMIT 5 + `); + + const responseData = { + stats: { + totalPacientes: pacientes[0].count, + totalAgendamentos: agendamentos[0].count, + totalLeads: leads[0].count, + faturamentoTotal: faturamento[0].total, + despesasTotal: pendente[0].total + }, + growth: growth, + recentAgendamentos: recent + }; + + if (redis) { + await redis.setex(cacheKey, 60, JSON.stringify(responseData)); // Cache for 60s + console.log('[CACHE MISS] Cached dashboard stats in DragonflyDB'); + } + + res.json(responseData); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- NOTIFICACOES --- +app.get('/api/notificacoes', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM notificacoes ORDER BY data DESC LIMIT 100'); + res.json(rows.map(n => ({ ...n, lida: !!n.lida }))); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/notificacoes/read-all', async (req, res) => { + try { + await pool.query('UPDATE notificacoes SET lida = 1 WHERE lida = 0'); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.delete('/api/notificacoes/:id', async (req, res) => { + try { + await pool.query('DELETE FROM notificacoes WHERE id = ?', [req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.put('/api/notificacoes/:id/read', async (req, res) => { + try { + await pool.query('UPDATE notificacoes SET lida = 1 WHERE id = ?', [req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- PACIENTES --- +app.get('/api/pacientes', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM pacientes'); + res.json(rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +app.post('/api/pacientes', async (req, res) => { + try { + const p = req.body; + await pool.query('INSERT INTO pacientes SET ?', p); + res.json({ success: true, data: p }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +app.put('/api/pacientes/:id', async (req, res) => { + try { + await pool.query('UPDATE pacientes SET ? WHERE id = ?', [req.body, req.params.id]); + res.json({ success: true }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// --- DENTISTAS --- +app.get('/api/dentistas', async (req, res) => { + try { + const { clinicaId } = req.query; + let query = 'SELECT * FROM dentistas'; + const params = []; + if (clinicaId) { + query += ' WHERE clinica_id = ?'; + params.push(clinicaId); + } + query += ' ORDER BY ordem ASC, nome ASC'; + const [rows] = await pool.query(query, params); + res.json(rows.map(d => ({ ...d, ativo: !!d.ativo }))); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/dentistas', async (req, res) => { + try { + const d = req.body; + // Automatically set order to the end + const [counts] = await pool.query('SELECT COUNT(*) as count FROM dentistas'); + const ordem = counts[0].count; + await pool.query('INSERT INTO dentistas SET ?', { ...d, ordem }); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.put('/api/dentistas/reorder', async (req, res) => { + const { orders } = req.body; // Array of {id, ordem} + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + for (const item of orders) { + await connection.query('UPDATE dentistas SET ordem = ? WHERE id = ?', [item.ordem, item.id]); + } + await connection.commit(); + res.json({ success: true }); + } catch (err) { + await connection.rollback(); + res.status(500).json({ error: err.message }); + } finally { connection.release(); } +}); + +app.put('/api/dentistas/:id', async (req, res) => { + try { + await pool.query('UPDATE dentistas SET ? WHERE id = ?', [req.body, req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.delete('/api/dentistas/:id', async (req, res) => { + const connection = await pool.getConnection(); + try { + const { id } = req.params; + await connection.beginTransaction(); + + // 1. Remove Google Tokens associated with this dentist + await connection.query('DELETE FROM google_tokens WHERE owner_id = ?', [id]); + + // 2. Remove clinical links (if any) + await connection.query('DELETE FROM vinculos WHERE usuario_id = ?', [id]); + + // 3. Set to NULL in agendamentos if it exists + await connection.query('UPDATE agendamentos SET dentistaId = NULL WHERE dentistaId = ?', [id]); + + // 4. Delete from dentistas table + await connection.query('DELETE FROM dentistas WHERE id = ?', [id]); + + await connection.commit(); + res.json({ success: true }); + } catch (err) { + await connection.rollback(); + console.error('Error deleting dentist:', err); + res.status(500).json({ error: err.message }); + } finally { + connection.release(); + } +}); + +// --- GESTÃO DE HORÁRIOS --- +app.get('/api/dentistas/:dentistaId/horarios', async (req, res) => { + try { + const { dentistaId } = req.params; + const { clinicaId } = req.query; + let query = 'SELECT * FROM dentistas_horarios WHERE dentista_id = ?'; + const params = [dentistaId]; + if (clinicaId) { + query += ' AND clinica_id = ?'; + params.push(clinicaId); + } + query += ' ORDER BY dia_semana ASC, hora_inicio ASC'; + const [rows] = await pool.query(query, params); + res.json(rows); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/dentistas/horarios', async (req, res) => { + try { + const { id, dentista_id, clinica_id, dia_semana, hora_inicio, hora_fim, ativo } = req.body; + await pool.query(` + INSERT INTO dentistas_horarios (id, dentista_id, clinica_id, dia_semana, hora_inicio, hora_fim, ativo) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + dia_semana = VALUES(dia_semana), hora_inicio = VALUES(hora_inicio), + hora_fim = VALUES(hora_fim), ativo = VALUES(ativo) + `, [id || `h_${Math.random().toString(36).substring(2, 9)}`, dentista_id, clinica_id, dia_semana, hora_inicio, hora_fim, ativo ?? 1]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.delete('/api/dentistas/horarios/:id', async (req, res) => { + try { + await pool.query('DELETE FROM dentistas_horarios WHERE id = ?', [req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- RELATÓRIOS --- +app.get('/api/reports/productivity', async (req, res) => { + try { + const { clinicaId, start, end } = req.query; + if (!clinicaId) return res.status(400).json({ error: "Missing clinicaId" }); + + // Financial Totals + const [finRows] = await pool.query(` + SELECT + SUM(CASE WHEN tipo = 'RECEITA' AND status = 'Pago' THEN valor ELSE 0 END) as receita, + SUM(CASE WHEN tipo = 'DESPESA' AND status = 'Pago' THEN valor ELSE 0 END) as despesa, + COUNT(*) as totalTransacoes + FROM financeiro + WHERE clinica_id = ? AND dataVencimento BETWEEN ? AND ? + `, [clinicaId, start || '2000-01-01', end || '2100-01-01']); + + // Appointments by Dentist + const [agRows] = await pool.query(` + SELECT + d.nome as dentista, + COUNT(a.id) as totalAgendamentos, + SUM(CASE WHEN a.status = 'Concluido' THEN 1 ELSE 0 END) as concluidos, + SUM(CASE WHEN a.status = 'Faltou' THEN 1 ELSE 0 END) as faltas + FROM dentistas d + LEFT JOIN agendamentos a ON d.id = a.dentistaId AND a.start_time BETWEEN ? AND ? + WHERE d.clinica_id = ? + GROUP BY d.id + `, [start || '2000-01-01', end || '2100-01-01', clinicaId]); + + res.json({ + financeiro: finRows[0], + producao: agRows + }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- AGENDAMENTOS --- +app.get('/api/agendamentos', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM agendamentos'); + res.json(rows.map(r => ({ ...r, start: r.start_time, end: r.end_time }))); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// POST with concurrency lock: prevents double-booking the same dentist+time slot +app.post('/api/agendamentos', async (req, res) => { + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + const { start, end, ...rest } = req.body; + const { dentistaId } = rest; + + // Lock check: SELECT the conflicting row FOR UPDATE to prevent race conditions + if (dentistaId && start) { + const [conflict] = await connection.query( + `SELECT id FROM agendamentos + WHERE dentistaId = ? AND start_time = ? + LIMIT 1 FOR UPDATE`, + [dentistaId, start] + ); + if (conflict.length > 0) { + await connection.rollback(); + return res.status(409).json({ + success: false, + message: 'ESTE HORÁRIO ACABOU DE SER RESERVADO POR OUTRA PESSOA. POR FAVOR, ESCOLHA OUTRO HORÁRIO.' + }); + } + } + + const id = rest.id || `ag_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`; + await connection.query( + 'INSERT INTO agendamentos SET ?', + { ...rest, id, start_time: start, end_time: end } + ); + await connection.commit(); + res.json({ success: true }); + } catch (err) { + await connection.rollback(); + // MySQL error 1062 = duplicate entry (UNIQUE KEY violation) + if (err.code === 'ER_DUP_ENTRY') { + return res.status(409).json({ + success: false, + message: 'ESTE HORÁRIO ACABOU DE SER RESERVADO POR OUTRA PESSOA. POR FAVOR, ESCOLHA OUTRO HORÁRIO.' + }); + } + res.status(500).json({ error: err.message }); + } finally { connection.release(); } +}); + +app.put('/api/agendamentos/:id', async (req, res) => { + try { + const { start, end, ...rest } = req.body; + const updateData = { ...rest }; + if (start) updateData.start_time = start; + if (end) updateData.end_time = end; + + await pool.query('UPDATE agendamentos SET ? WHERE id = ?', [updateData, req.params.id]); + res.json({ success: true }); + } catch (err) { + if (err.code === 'ER_DUP_ENTRY') { + return res.status(409).json({ + success: false, + message: 'ESTE HORÁRIO JÁ ESTÁ OCUPADO PARA ESTE DENTISTA.' + }); + } + res.status(500).json({ error: err.message }); + } +}); + +app.delete('/api/agendamentos/:id', async (req, res) => { + try { + await pool.query('DELETE FROM agendamentos WHERE id = ?', [req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- ESPECIALIDADES --- +app.get('/api/especialidades', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM especialidades ORDER BY ordem ASC, nome ASC'); + res.json(rows); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/especialidades', async (req, res) => { + try { + // Automatically set order to the end + const [counts] = await pool.query('SELECT COUNT(*) as count FROM especialidades'); + const ordem = counts[0].count; + await pool.query('INSERT INTO especialidades SET ?', { ...req.body, ordem }); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.put('/api/especialidades/reorder', async (req, res) => { + const { orders } = req.body; // Array of {id, ordem} + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + for (const item of orders) { + await connection.query('UPDATE especialidades SET ordem = ? WHERE id = ?', [item.ordem, item.id]); + } + await connection.commit(); + res.json({ success: true }); + } catch (err) { + await connection.rollback(); + res.status(500).json({ error: err.message }); + } finally { connection.release(); } +}); + +app.put('/api/especialidades/:id', async (req, res) => { + try { + await pool.query('UPDATE especialidades SET ? WHERE id = ?', [req.body, req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.delete('/api/especialidades/:id', async (req, res) => { + try { + await pool.query('DELETE FROM especialidades WHERE id = ?', [req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- PLANOS --- +app.get('/api/planos', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM planos'); + res.json(rows); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/planos', async (req, res) => { + try { + await pool.query('INSERT INTO planos SET ?', req.body); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.put('/api/planos/:id', async (req, res) => { + try { + await pool.query('UPDATE planos SET ? WHERE id = ?', [req.body, req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.delete('/api/planos/:id', async (req, res) => { + try { + await pool.query('DELETE FROM planos WHERE id = ?', [req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- PROCEDIMENTOS --- +app.get('/api/procedimentos', async (req, res) => { + try { + const [procs] = await pool.query('SELECT * FROM procedimentos ORDER BY ordem ASC, nome ASC'); + const [valores] = await pool.query('SELECT * FROM procedimentos_valores_planos'); + + const data = procs.map(p => ({ + ...p, + valorParticular: parseFloat(p.valorParticular || 0), + valoresPlanos: valores + .filter(v => v.procedimentoId === p.id) + .map(v => ({ ...v, valor: parseFloat(v.valor || 0) })) + })); + res.json(data); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/procedimentos', async (req, res) => { + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + const { valoresPlanos, ...proc } = req.body; + + // Auto-generate codigoInterno for PARTICULAR + let codigoInternoVal = proc.codigoInterno; + if (proc.categoria === 'Particular' && (!codigoInternoVal || codigoInternoVal === '')) { + const [rows] = await connection.query('SELECT COUNT(*) as count FROM procedimentos WHERE categoria = "Particular"'); + codigoInternoVal = (rows[0].count + 1).toString(); + } + + // Automatically set order to the end + const [counts] = await connection.query('SELECT COUNT(*) as count FROM procedimentos'); + const ordemValue = counts[0].count; + + const payload = { + ...proc, + codigoInterno: codigoInternoVal, + ordem: ordemValue, + tipo_regiao: proc.tipo_regiao || 'GERAL', + exige_face: proc.exige_face ? 1 : 0 + }; + + if (!payload.id) payload.id = Math.random().toString(36).substring(2, 10).toUpperCase(); + + await connection.query('INSERT INTO procedimentos SET ?', payload); + if (valoresPlanos && valoresPlanos.length > 0) { + for (const v of valoresPlanos) { + await connection.query('INSERT INTO procedimentos_valores_planos SET ?', { ...v, procedimentoId: payload.id }); + } + } + await connection.commit(); + res.json({ success: true }); + } catch (err) { + await connection.rollback(); + res.status(500).json({ error: err.message }); + } finally { connection.release(); } +}); + +app.put('/api/procedimentos/:id', async (req, res) => { + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + const { valoresPlanos, ...proc } = req.body; + + const updateData = { + ...proc, + exige_face: proc.exige_face ? 1 : 0 + }; + + await connection.query('UPDATE procedimentos SET ? WHERE id = ?', [updateData, req.params.id]); + await connection.query('DELETE FROM procedimentos_valores_planos WHERE procedimentoId = ?', [req.params.id]); + if (valoresPlanos && valoresPlanos.length > 0) { + for (const v of valoresPlanos) { + await connection.query('INSERT INTO procedimentos_valores_planos SET ?', { ...v, procedimentoId: req.params.id }); + } + } + await connection.commit(); + res.json({ success: true }); + } catch (err) { + await connection.rollback(); + res.status(500).json({ error: err.message }); + } finally { connection.release(); } +}); + +app.put('/api/procedimentos/reorder', async (req, res) => { + const { orders } = req.body; // Array of {id, ordem} + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + for (const item of orders) { + await connection.query('UPDATE procedimentos SET ordem = ? WHERE id = ?', [item.ordem, item.id]); + } + await connection.commit(); + res.json({ success: true }); + } catch (err) { + await connection.rollback(); + res.status(500).json({ error: err.message }); + } finally { connection.release(); } +}); + +app.delete('/api/procedimentos/:id', async (req, res) => { + try { + await pool.query('DELETE FROM procedimentos WHERE id = ?', [req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- FINANCEIRO --- +app.get('/api/financeiro', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM financeiro'); + res.json(rows.map(r => ({ ...r, valor: parseFloat(r.valor || 0) }))); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/financeiro', async (req, res) => { + try { + await pool.query('INSERT INTO financeiro SET ?', req.body); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.put('/api/financeiro/:id', async (req, res) => { + try { + await pool.query('UPDATE financeiro SET ? WHERE id = ?', [req.body, req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.delete('/api/financeiro/:id', async (req, res) => { + try { + await pool.query('DELETE FROM financeiro WHERE id = ?', [req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- LEADS --- +app.get('/api/leads', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM leads'); + res.json(rows); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/leads', async (req, res) => { + try { + await pool.query('INSERT INTO leads SET ?', req.body); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- GUIAS ODONTOLÓGICAS (TRATAMENTOS) --- +app.get('/api/guias', async (req, res) => { + try { + const { + page = 1, + pageSize = 10, + search = '', + status = '', + dataInicio = '', + dataFim = '', + sortField = 'dataSolicitacao', + sortOrder = 'DESC', + gto = '' + } = req.query; + + const offset = (page - 1) * pageSize; + const limit = parseInt(pageSize); + + let where = ' WHERE 1=1'; + const params = []; + + if (gto) { + where += ' AND (numeroGuiaPrestador LIKE ? OR numeroGuiaOperadora LIKE ?)'; + params.push(`%${gto}%`, `%${gto}%`); + } + + if (search) { + where += ' AND (beneficiarioNome LIKE ? OR beneficiarioIdentificacao LIKE ?)'; + params.push(`%${search}%`, `%${search}%`); + } + + if (status) { + where += ' AND status = ?'; + params.push(status); + } + + if (dataInicio) { + where += ' AND dataSolicitacao >= ?'; + params.push(dataInicio); + } + + if (dataFim) { + where += ' AND dataSolicitacao <= ?'; + params.push(dataFim); + } + + // Validate sortField and sortOrder to prevent SQL injection + const allowedSortFields = ['dataSolicitacao', 'beneficiarioNome', 'status', 'numeroGuiaPrestador']; + const finalSortField = allowedSortFields.includes(sortField) ? sortField : 'dataSolicitacao'; + const finalSortOrder = sortOrder.toUpperCase() === 'ASC' ? 'ASC' : 'DESC'; + + const query = ` + SELECT * FROM guias_odontologicas + ${where} + ORDER BY ${finalSortField} ${finalSortOrder} + LIMIT ? OFFSET ? + `; + + const countQuery = `SELECT COUNT(*) as total FROM guias_odontologicas ${where}`; + + const [rows] = await pool.query(query, [...params, limit, offset]); + const [totalRows] = await pool.query(countQuery, params); + + res.json({ + data: rows, + total: totalRows[0].total, + page: parseInt(page), + pageSize: limit + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// --- GTO BUILDER (Lançar GTO) --- +app.post('/api/gto-builder', async (req, res) => { + try { + const { pacienteId, dentistaId, credenciadaId } = req.body; + const id = Math.random().toString(36).substring(2, 9).toUpperCase(); + await pool.query( + 'INSERT INTO gto_builders (id, pacienteId, dentistaId, credenciadaId, status, total) VALUES (?, ?, ?, ?, "RASCUNHO", 0)', + [id, pacienteId, dentistaId, credenciadaId] + ); + res.json({ id }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.get('/api/gto-builder/:id', async (req, res) => { + try { + const [builder] = await pool.query('SELECT * FROM gto_builders WHERE id = ?', [req.params.id]); + const [items] = await pool.query('SELECT * FROM gto_items WHERE builderId = ?', [req.params.id]); + res.json({ ...builder[0], items }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/gto-builder/:id/item', async (req, res) => { + try { + const itemId = Math.random().toString(36).substring(2, 9).toUpperCase(); + const item = { ...req.body, id: itemId, builderId: req.params.id }; + await pool.query('INSERT INTO gto_items SET ?', item); + + // Update total + await pool.query( + 'UPDATE gto_builders SET total = (SELECT SUM(valorTotal) FROM gto_items WHERE builderId = ?) WHERE id = ?', + [req.params.id, req.params.id] + ); + + res.json({ success: true, item }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.delete('/api/gto-builder/:id/item/:itemId', async (req, res) => { + try { + await pool.query('DELETE FROM gto_items WHERE id = ?', [req.params.itemId]); + // Update total + await pool.query( + 'UPDATE gto_builders SET total = COALESCE((SELECT SUM(valorTotal) FROM gto_items WHERE builderId = ?), 0) WHERE id = ?', + [req.params.id, req.params.id] + ); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.put('/api/gto-builder/:id/finalizar', async (req, res) => { + try { + await pool.query('UPDATE gto_builders SET status = "FINALIZADA" WHERE id = ?', [req.params.id]); + + // Logic to migrate to guias_odontologicas would go here in a real app + // For now, we just mark as finished. + + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// Initialization: Create tables if not exist +async function initTables() { + try { + // Ensure session uses UTF-8 and ignore FK checks during initialization + await pool.query('SET NAMES utf8mb4'); + await pool.query('SET CHARACTER SET utf8mb4'); + await pool.query('SET FOREIGN_KEY_CHECKS = 0'); + + // 1. Clinics + await pool.query(` + CREATE TABLE IF NOT EXISTS clinicas ( + id VARCHAR(50) PRIMARY KEY, + nome_fantasia VARCHAR(255) NOT NULL, + documento VARCHAR(50), + cor VARCHAR(20) DEFAULT '#2563eb', + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + // 2. Users (Global identity) + await pool.query(` + CREATE TABLE IF NOT EXISTS usuarios ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(255) NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + senha VARCHAR(255) NOT NULL, + cro VARCHAR(20), + cro_uf VARCHAR(2), + celular VARCHAR(20), + especialidade VARCHAR(100), + cor_agenda VARCHAR(20) DEFAULT '#2563eb', + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + // 3. Links (Many-to-Many pivô) + await pool.query(` + CREATE TABLE IF NOT EXISTS vinculos ( + id VARCHAR(50) PRIMARY KEY, + usuario_id VARCHAR(50) NOT NULL, + clinica_id VARCHAR(50) NOT NULL, + role ENUM('paciente', 'dentista', 'funcionario', 'donoclinica', 'admin') DEFAULT 'admin', + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY user_clinica (usuario_id, clinica_id) + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + // Initial Data Migration/Seed + const [clinCount] = await pool.query('SELECT COUNT(*) as cnt FROM clinicas'); + if (clinCount[0].cnt === 0) { + await pool.query('INSERT INTO clinicas (id, nome_fantasia, documento, cor) VALUES ("c1", "SCOREODONTO MATRIZ", "00.000.000/0001-00", "#2563eb")'); + await pool.query('INSERT INTO clinicas (id, nome_fantasia, documento, cor) VALUES ("c2", "SCOREODONTO FILIAL SUL", "00.000.000/0002-00", "#059669")'); + } + + const [userCount] = await pool.query('SELECT COUNT(*) as cnt FROM usuarios'); + if (userCount[0].cnt === 0) { + const mockUsers = [ + ['u1', 'RUI CESAR', 'admin@scoreodonto.com', 'admin'], + ['u2', 'DENTISTA TESTE', 'dentista@scoreodonto.com', '123456'], + ['u3', 'FUNCIONARIO TESTE', 'funcionario@scoreodonto.com', 'cassems'], + ['u4', 'DONO CLINICA', 'dono@scoreodonto.com', 'Rc362514'], + ['u5', 'PACIENTE TESTE', 'paciente@scoreodonto.com', '14253636'], + ]; + for (const [id, nome, email, senha] of mockUsers) { + await pool.query('INSERT IGNORE INTO usuarios (id, nome, email, senha) VALUES (?, ?, ?, ?)', [id, nome, email, senha]); + // Default links to Matrix clinic + let role = 'admin'; + if (email.includes('dentista')) role = 'dentista'; + if (email.includes('funcionario')) role = 'funcionario'; + if (email.includes('dono')) role = 'donoclinica'; + if (email.includes('paciente')) role = 'paciente'; + + await pool.query('INSERT IGNORE INTO vinculos (id, usuario_id, clinica_id, role) VALUES (?, ?, ?, ?)', [`v_${id}_c1`, id, 'c1', role]); + + // Add an extra link for Admin and Dono for testing multi-clinic + if (id === 'u1' || id === 'u4') { + await pool.query('INSERT IGNORE INTO vinculos (id, usuario_id, clinica_id, role) VALUES (?, ?, ?, ?)', [`v_${id}_c2`, id, 'c2', id === 'u1' ? 'admin' : 'donoclinica']); + } + } + console.log('Multi-clinic Database Seeded.'); + } + + await pool.query(` + CREATE TABLE IF NOT EXISTS especialidades ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(100) NOT NULL, + descricao TEXT, + ordem INT DEFAULT 0 + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS procedimentos ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(255) NOT NULL, + codigo VARCHAR(50), + codigoInterno VARCHAR(50), + descricao TEXT, + especialidadeId VARCHAR(50), + especialidadeNome VARCHAR(100), + valorParticular DECIMAL(10,2), + categoria VARCHAR(50) DEFAULT 'Particular', + tipo_regiao ENUM('DENTE', 'ARCO', 'GERAL') DEFAULT 'GERAL', + exige_face TINYINT(1) DEFAULT 0, + ordem INT DEFAULT 0 + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS procedimentos_valores_planos ( + id INT AUTO_INCREMENT PRIMARY KEY, + procedimentoId VARCHAR(50), + planoId VARCHAR(50), + planoNome VARCHAR(100), + valor DECIMAL(10,2), + codigoPlano VARCHAR(50) + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS pacientes ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(255) NOT NULL, + cpf VARCHAR(20), + telefone VARCHAR(20), + email VARCHAR(100), + ultimoTratamento VARCHAR(255), + convenio VARCHAR(100), + dataNascimento DATE + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS dentistas ( + id VARCHAR(50) PRIMARY KEY, + clinica_id VARCHAR(50), + nome VARCHAR(255) NOT NULL, + email VARCHAR(100), + telefone VARCHAR(20), + corAgenda VARCHAR(20), + especialidade VARCHAR(100), + ativo TINYINT(1) DEFAULT 1, + ordem INT DEFAULT 0 + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS settings ( + id VARCHAR(50) PRIMARY KEY, + category VARCHAR(50) NOT NULL, + data TEXT + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS agendamentos ( + id VARCHAR(50) PRIMARY KEY, + clinica_id VARCHAR(50), + pacienteNome VARCHAR(255) NOT NULL, + dentistaId VARCHAR(50), + start_time DATETIME NOT NULL, + end_time DATETIME NOT NULL, + status ENUM('Confirmado', 'Pendente', 'Concluido', 'Faltou') DEFAULT 'Pendente', + procedimento VARCHAR(255), + observacoes TEXT + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS financeiro ( + id VARCHAR(50) PRIMARY KEY, + clinica_id VARCHAR(50), + pacienteNome VARCHAR(255), + descricao VARCHAR(255), + valor DECIMAL(10,2), + dataVencimento DATE, + status ENUM('Pago', 'Pendente', 'Atrasado') DEFAULT 'Pendente', + formaPagamento ENUM('Boleto', 'Pix', 'Cartão', 'Dinheiro') DEFAULT 'Dinheiro', + tipo ENUM('RECEITA', 'DESPESA') DEFAULT 'RECEITA', + ordem INT DEFAULT 0 + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS dentistas_horarios ( + id VARCHAR(50) PRIMARY KEY, + dentista_id VARCHAR(50) NOT NULL, + clinica_id VARCHAR(50) NOT NULL, + dia_semana INT NOT NULL, + hora_inicio TIME NOT NULL, + hora_fim TIME NOT NULL, + ativo TINYINT(1) DEFAULT 1 + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS leads ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(100), + sobrenome VARCHAR(100), + cpf VARCHAR(20), + whatsapp VARCHAR(20), + tipoInteresse ENUM('Particular', 'Plano'), + dataCadastro DATETIME DEFAULT CURRENT_TIMESTAMP, + status ENUM('Novo', 'Convertido', 'Arquivado') DEFAULT 'Novo' + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS planos ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(100) NOT NULL, + tipo ENUM('Convenio', 'Particular', 'Parceria'), + descontoPadrao DECIMAL(5,2), + corCartao VARCHAR(20) + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS notificacoes ( + id VARCHAR(50) PRIMARY KEY, + titulo VARCHAR(255) NOT NULL, + mensagem TEXT, + tipo ENUM('lead', 'sistema', 'agenda', 'financeiro') DEFAULT 'sistema', + lida TINYINT(1) DEFAULT 0, + data DATETIME DEFAULT CURRENT_TIMESTAMP + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS google_tokens ( + owner_id VARCHAR(255) PRIMARY KEY, + refresh_token TEXT NOT NULL, + access_token TEXT, + expiry_date BIGINT, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + // Force existing tables to UTF-8 and add missing columns + try { + await pool.query('SET FOREIGN_KEY_CHECKS = 0'); + + await pool.query('ALTER TABLE especialidades CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); + await pool.query('ALTER TABLE procedimentos CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); + await pool.query('ALTER TABLE procedimentos_valores_planos CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); + await pool.query('ALTER TABLE guias_odontologicas CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); + await pool.query('ALTER TABLE gto_builders CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); + await pool.query('ALTER TABLE gto_items CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); + + // Ensure columns exist + const [usrCro] = await pool.query('SHOW COLUMNS FROM usuarios LIKE "cro"'); + if (usrCro.length === 0) { + await pool.query('ALTER TABLE usuarios ADD COLUMN cro VARCHAR(20) AFTER senha'); + await pool.query('ALTER TABLE usuarios ADD COLUMN cro_uf VARCHAR(2) AFTER cro'); + await pool.query('ALTER TABLE usuarios ADD COLUMN celular VARCHAR(20) AFTER cro_uf'); + await pool.query('ALTER TABLE usuarios ADD COLUMN especialidade VARCHAR(100) AFTER celular'); + await pool.query('ALTER TABLE usuarios ADD COLUMN cor_agenda VARCHAR(20) DEFAULT "#2563eb" AFTER especialidade'); + } + + const [clinCor] = await pool.query('SHOW COLUMNS FROM clinicas LIKE "cor"'); + if (clinCor.length === 0) await pool.query('ALTER TABLE clinicas ADD COLUMN cor VARCHAR(20) DEFAULT "#2563eb" AFTER documento'); + + const [dentClin] = await pool.query('SHOW COLUMNS FROM dentistas LIKE "clinica_id"'); + if (dentClin.length === 0) await pool.query('ALTER TABLE dentistas ADD COLUMN clinica_id VARCHAR(50) AFTER id'); + + const [agClin] = await pool.query('SHOW COLUMNS FROM agendamentos LIKE "clinica_id"'); + if (agClin.length === 0) await pool.query('ALTER TABLE agendamentos ADD COLUMN clinica_id VARCHAR(50) AFTER id'); + + const [finClin] = await pool.query('SHOW COLUMNS FROM financeiro LIKE "clinica_id"'); + if (finClin.length === 0) await pool.query('ALTER TABLE financeiro ADD COLUMN clinica_id VARCHAR(50) AFTER id'); + + const [finTipo] = await pool.query('SHOW COLUMNS FROM financeiro LIKE "tipo"'); + if (finTipo.length === 0) await pool.query('ALTER TABLE financeiro ADD COLUMN tipo ENUM("RECEITA", "DESPESA") DEFAULT "RECEITA" AFTER formaPagamento'); + + const [procRegiao] = await pool.query('SHOW COLUMNS FROM procedimentos LIKE "tipo_regiao"'); + if (procRegiao.length === 0) { + await pool.query('ALTER TABLE procedimentos ADD COLUMN tipo_regiao ENUM("DENTE", "ARCO", "GERAL") DEFAULT "GERAL" AFTER categoria'); + await pool.query('ALTER TABLE procedimentos ADD COLUMN exige_face TINYINT(1) DEFAULT 0 AFTER tipo_regiao'); + } + + // SECURITY: Add UNIQUE KEY to prevent concurrent double-booking + try { + await pool.query('ALTER TABLE agendamentos ADD UNIQUE KEY unique_dentista_slot (dentistaId, start_time)'); + console.log('[DB] Unique key unique_dentista_slot created on agendamentos.'); + } catch (e) { + if (!e.message.includes('Duplicate key name')) { + console.log('[DB] unique_dentista_slot already exists or non-critical error:', e.message); + } + } + + // Re-enable FK checks + await pool.query('SET FOREIGN_KEY_CHECKS = 1'); + console.log('Database schema and multi-tenant structure updated!'); + } catch (e) { + console.error('Error during ALTER TABLE operations:', e.message); + await pool.query('SET FOREIGN_KEY_CHECKS = 1'); + } + + await pool.query(` + CREATE TABLE IF NOT EXISTS guias_odontologicas ( + id VARCHAR(50) PRIMARY KEY, + numeroGuiaPrestador VARCHAR(50) NOT NULL, + numeroGuiaOperadora VARCHAR(50), + dataSolicitacao DATE NOT NULL, + tipoTratamento VARCHAR(100) NOT NULL, + status VARCHAR(50) NOT NULL, + beneficiarioId VARCHAR(50) NOT NULL, + beneficiarioNome VARCHAR(255) NOT NULL, + beneficiarioIdentificacao VARCHAR(50) NOT NULL, + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS gto_builders ( + id VARCHAR(50) PRIMARY KEY, + pacienteId VARCHAR(50) NOT NULL, + dentistaId VARCHAR(50) NOT NULL, + credenciadaId VARCHAR(50) NOT NULL, + status VARCHAR(20) DEFAULT "RASCUNHO", + total DECIMAL(10,2) DEFAULT 0, + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS gto_items ( + id VARCHAR(50) PRIMARY KEY, + builderId VARCHAR(50) NOT NULL, + procedimentoId VARCHAR(50) NOT NULL, + codigoTUSS VARCHAR(50) NOT NULL, + codigoInterno VARCHAR(50), + descricao VARCHAR(255) NOT NULL, + dente VARCHAR(10), + face VARCHAR(10), + quantidade INT DEFAULT 1, + valorUnitario DECIMAL(10,2) NOT NULL, + valorTotal DECIMAL(10,2) NOT NULL, + observacao TEXT, + anexosCount INT DEFAULT 0 + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + // Check if empty and add mock data + const [count] = await pool.query('SELECT COUNT(*) as cnt FROM guias_odontologicas'); + if (count[0].cnt === 0) { + const mockData = [ + ['1', '177903', 'O-001', '2026-02-25', 'Tratamento Odontologico', 'AUTORIZADO', 'B1', 'RONEY OTTONI DE SOUZA', '181442'], + ['2', '178056', 'O-002', '2026-02-25', 'Tratamento Odontologico', 'AUTORIZADO', 'B2', 'MILENA LIMA DIAS OTTONI DE SOUZA', '157320'], + ['3', '178179', 'O-003', '2026-02-25', 'Tratamento Odontologico', 'AUTORIZADO', 'B3', 'EVANDRO MACHADO AZEMAN', '2025100002341'], + ['4', '177208', 'O-004', '2026-02-24', 'Tratamento Odontologico', 'AUTORIZADO', 'B4', 'MARIA JULIA MIRANDA DA SILVA', '145805'], + ['5', '177335', 'O-005', '2026-02-24', 'Tratamento Odontologico', 'AUTORIZADO_PARCIAL', 'B5', 'JOAO ITALO CORREA DE AMORIM SANT ANNA', '443079'], + ['6', '176468', 'O-006', '2026-02-23', 'Urgencia / Emergencia', 'AUTORIZADO', 'B6', 'EDUARDO STENIO GONCALVES DOS SANTOS', '435593'], + ['7', '176572', 'O-007', '2026-02-23', 'Tratamento Odontologico', 'AUTORIZADO', 'B7', 'LUCIA MARIA DA SILVA JULIO', '127311'], + ['8', '176666', 'O-008', '2026-02-23', 'Tratamento Odontologico', 'AUTORIZADO', 'B8', 'JAIRO HIDEKI NAGAO', '96512'], + ['9', '176721', 'O-009', '2026-02-23', 'Tratamento Odontologico', 'AUTORIZADO', 'B8', 'JAIRO HIDEKI NAGAO', '96512'], + ]; + + for (const row of mockData) { + await pool.query('INSERT INTO guias_odontologicas (id, numeroGuiaPrestador, numeroGuiaOperadora, dataSolicitacao, tipoTratamento, status, beneficiarioId, beneficiarioNome, beneficiarioIdentificacao) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', row); + } + } + + setInterval(generateIntelligentNotifications, 60000); + generateIntelligentNotifications(); + + // Check if planos empty and add mock data + const [planCount] = await pool.query('SELECT COUNT(*) as cnt FROM planos'); + if (planCount[0].cnt === 0) { + await pool.query("INSERT INTO planos (id, nome, tipo, descontoPadrao, corCartao) VALUES ('p1', 'PARTICULAR', 'Particular', 0, '#1f2937')"); + } + + // Re-enable FK checks + await pool.query('SET FOREIGN_KEY_CHECKS = 1'); + } catch (err) { + await pool.query('SET FOREIGN_KEY_CHECKS = 1'); + console.error('Error initializing tables:', err); + } +} + + +async function generateIntelligentNotifications() { + if (!pool) return; + try { + // 1. Check for upcoming appointments (next 2 hours) + const [upcoming] = await pool.query(` + SELECT a.*, p.nome as pacienteNome + FROM agendamentos a + LEFT JOIN pacientes p ON a.pacienteNome = p.nome + WHERE a.start_time BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 2 HOUR) + AND a.status = 'Pendente' + `); + + for (const appt of upcoming) { + const id = `notif_appt_${appt.id} `; + const [exists] = await pool.query('SELECT id FROM notificacoes WHERE id = ?', [id]); + if (exists.length === 0) { + await pool.query('INSERT INTO notificacoes (id, titulo, mensagem, tipo) VALUES (?, ?, ?, ?)', [ + id, + 'PRÓXIMO ATENDIMENTO', + `Paciente ${appt.pacienteNome} às ${new Date(appt.start_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} `, + 'agenda' + ]); + } + } + + // 2. Check for overdue bills + const [overdue] = await pool.query(` +SELECT * FROM financeiro + WHERE dataVencimento < CURDATE() AND status = 'Pendente' + `); + + for (const bill of overdue) { + const id = `notif_fin_overdue_${bill.id} `; + const [exists] = await pool.query('SELECT id FROM notificacoes WHERE id = ?', [id]); + if (exists.length === 0) { + await pool.query('INSERT INTO notificacoes (id, titulo, mensagem, tipo) VALUES (?, ?, ?, ?)', [ + id, + 'FATURA ATRASADA', + `A conta "${bill.descricao}" de R$ ${bill.valor} venceu em ${new Date(bill.dataVencimento).toLocaleDateString('pt-BR')} `, + 'financeiro' + ]); + } + } + + // 3. Check for bills due today + const [dueToday] = await pool.query(` +SELECT * FROM financeiro + WHERE dataVencimento = CURDATE() AND status = 'Pendente' + `); + + for (const bill of dueToday) { + const id = `notif_fin_today_${bill.id} `; + const [exists] = await pool.query('SELECT id FROM notificacoes WHERE id = ?', [id]); + if (exists.length === 0) { + await pool.query('INSERT INTO notificacoes (id, titulo, mensagem, tipo) VALUES (?, ?, ?, ?)', [ + id, + 'VENCIMENTO HOJE', + `A conta "${bill.descricao}" de R$ ${bill.valor} vence hoje!`, + 'financeiro' + ]); + } + } + + // 4. System Events (to Dev) - Mocking a system event if table counts are low + const [pCount] = await pool.query('SELECT COUNT(*) as count FROM pacientes'); + if (pCount[0].count < 5) { + const id = 'notif_sys_setup'; + const [exists] = await pool.query('SELECT id FROM notificacoes WHERE id = ?', [id]); + if (exists.length === 0) { + await pool.query('INSERT INTO notificacoes (id, titulo, mensagem, tipo) VALUES (?, ?, ?, ?)', [ + id, + 'CONFIGURAÇÃO DO SISTEMA', + 'O sistema possui poucos pacientes cadastrados. Considere importar dados.', + 'sistema' + ]); + } + } + + } catch (err) { + console.error('Error generating notifications:', err); + } +} +// initTables(); removed from here + + + diff --git a/backend/sql/sis_odonto.sql b/backend/sql/sis_odonto.sql new file mode 100644 index 0000000..064a7dd Binary files /dev/null and b/backend/sql/sis_odonto.sql differ diff --git a/backend/sql/sis_odonto_postgres.sql b/backend/sql/sis_odonto_postgres.sql new file mode 100644 index 0000000..d85598d --- /dev/null +++ b/backend/sql/sis_odonto_postgres.sql @@ -0,0 +1,349 @@ +-- ============================================================================= +-- PostgreSQL Database Initialization Script for ScoreOdonto CRM +-- Conversion from MySQL with corrected encodings and standard constraints +-- ============================================================================= + +-- Clean up existing tables (with cascade for safety) +DROP TABLE IF EXISTS vinculos CASCADE; +DROP TABLE IF EXISTS usuarios CASCADE; +DROP TABLE IF EXISTS settings CASCADE; +DROP TABLE IF EXISTS procedimentos_valores_planos CASCADE; +DROP TABLE IF EXISTS procedimentos CASCADE; +DROP TABLE IF EXISTS planos CASCADE; +DROP TABLE IF EXISTS pacientes CASCADE; +DROP TABLE IF EXISTS notificacoes CASCADE; +DROP TABLE IF EXISTS leads CASCADE; +DROP TABLE IF EXISTS guias_odontologicas CASCADE; +DROP TABLE IF EXISTS gto_items CASCADE; +DROP TABLE IF EXISTS gto_builders CASCADE; +DROP TABLE IF EXISTS google_tokens CASCADE; +DROP TABLE IF EXISTS financeiro CASCADE; +DROP TABLE IF EXISTS especialidades CASCADE; +DROP TABLE IF EXISTS dentistas_horarios CASCADE; +DROP TABLE IF EXISTS agendamentos CASCADE; +DROP TABLE IF EXISTS dentistas CASCADE; +DROP TABLE IF EXISTS clinicas CASCADE; + +-- 1. clinicas +CREATE TABLE clinicas ( + id varchar(50) NOT NULL, + nome_fantasia varchar(255) NOT NULL, + documento varchar(50) DEFAULT NULL, + cor varchar(20) DEFAULT '#2563eb', + createdAt timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +-- 2. dentistas +CREATE TABLE dentistas ( + id varchar(50) NOT NULL, + clinica_id varchar(50) DEFAULT NULL, + nome varchar(255) NOT NULL, + email varchar(255) DEFAULT NULL, + telefone varchar(20) DEFAULT NULL, + corAgenda varchar(10) DEFAULT NULL, + especialidade varchar(100) DEFAULT NULL, + ativo smallint DEFAULT 1, + ordem integer DEFAULT 0, + PRIMARY KEY (id) +); + +-- 3. agendamentos +CREATE TABLE agendamentos ( + id varchar(50) NOT NULL, + clinica_id varchar(50) DEFAULT NULL, + pacienteNome varchar(255) DEFAULT NULL, + dentistaId varchar(50) DEFAULT NULL, + start_time timestamp DEFAULT NULL, + end_time timestamp DEFAULT NULL, + status varchar(50) DEFAULT 'Pendente' CHECK (status IN ('Confirmado','Pendente','Concluido','Faltou')), + procedimento varchar(255) DEFAULT NULL, + observacoes text, + PRIMARY KEY (id), + CONSTRAINT unique_dentista_slot UNIQUE (dentistaId, start_time), + CONSTRAINT agendamentos_ibfk_1 FOREIGN KEY (dentistaId) REFERENCES dentistas (id) ON DELETE SET NULL +); + +-- 4. dentistas_horarios +CREATE TABLE dentistas_horarios ( + id varchar(50) NOT NULL, + dentista_id varchar(50) NOT NULL, + clinica_id varchar(50) NOT NULL, + dia_semana integer NOT NULL, + hora_inicio time NOT NULL, + hora_fim time NOT NULL, + ativo smallint DEFAULT 1, + PRIMARY KEY (id) +); + +-- 5. especialidades +CREATE TABLE especialidades ( + id varchar(50) NOT NULL, + nome varchar(100) NOT NULL, + descricao text, + ordem integer DEFAULT 0, + PRIMARY KEY (id) +); + +-- 6. financeiro +CREATE TABLE financeiro ( + id varchar(50) NOT NULL, + clinica_id varchar(50) DEFAULT NULL, + pacienteNome varchar(255) DEFAULT NULL, + descricao varchar(255) DEFAULT NULL, + valor decimal(10,2) DEFAULT NULL, + dataVencimento date DEFAULT NULL, + status varchar(50) DEFAULT NULL CHECK (status IN ('Pago','Pendente','Atrasado')), + formaPagamento varchar(50) DEFAULT NULL CHECK (formaPagamento IN ('Boleto','Pix','Cartão','Dinheiro')), + tipo varchar(50) DEFAULT 'RECEITA' CHECK (tipo IN ('RECEITA','DESPESA')), + PRIMARY KEY (id) +); + +-- 7. google_tokens +CREATE TABLE google_tokens ( + owner_id varchar(255) NOT NULL, + refresh_token text NOT NULL, + access_token text, + expiry_date bigint DEFAULT NULL, + updated_at timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (owner_id) +); + +-- 8. gto_builders +CREATE TABLE gto_builders ( + id varchar(50) NOT NULL, + pacienteId varchar(50) NOT NULL, + dentistaId varchar(50) NOT NULL, + credenciadaId varchar(50) NOT NULL, + status varchar(20) DEFAULT 'RASCUNHO', + total decimal(10,2) DEFAULT '0.00', + createdAt timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +-- 9. gto_items +CREATE TABLE gto_items ( + id varchar(50) NOT NULL, + builderId varchar(50) NOT NULL, + procedimentoId varchar(50) NOT NULL, + codigoTUSS varchar(50) NOT NULL, + descricao varchar(255) NOT NULL, + dente varchar(10) DEFAULT NULL, + face varchar(10) DEFAULT NULL, + quantidade integer DEFAULT 1, + valorUnitario decimal(10,2) NOT NULL, + valorTotal decimal(10,2) NOT NULL, + observacao text, + anexosCount integer DEFAULT 0, + codigoInterno varchar(50) DEFAULT NULL, + PRIMARY KEY (id) +); + +-- 10. guias_odontologicas +CREATE TABLE guias_odontologicas ( + id varchar(50) NOT NULL, + numeroGuiaPrestador varchar(50) NOT NULL, + numeroGuiaOperadora varchar(50) DEFAULT NULL, + dataSolicitacao date NOT NULL, + tipoTratamento varchar(100) NOT NULL, + status varchar(50) NOT NULL, + beneficiarioId varchar(50) NOT NULL, + beneficiarioNome varchar(255) NOT NULL, + beneficiarioIdentificacao varchar(50) NOT NULL, + createdAt timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +-- 11. leads +CREATE TABLE leads ( + id varchar(50) NOT NULL, + nome varchar(100) DEFAULT NULL, + sobrenome varchar(100) DEFAULT NULL, + cpf varchar(20) DEFAULT NULL, + whatsapp varchar(20) DEFAULT NULL, + tipoInteresse varchar(50) DEFAULT NULL CHECK (tipoInteresse IN ('Particular','Plano')), + dataCadastro timestamp DEFAULT NULL, + status varchar(50) DEFAULT NULL CHECK (status IN ('Novo','Convertido','Arquivado')), + PRIMARY KEY (id) +); + +-- 12. notificacoes +CREATE TABLE notificacoes ( + id varchar(50) NOT NULL, + titulo varchar(255) DEFAULT NULL, + mensagem text, + tipo varchar(50) DEFAULT 'sistema' CHECK (tipo IN ('lead','sistema','agenda','financeiro')), + lida smallint DEFAULT 0, + data timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +-- 13. pacientes +CREATE TABLE pacientes ( + id varchar(50) NOT NULL, + nome varchar(255) NOT NULL, + cpf varchar(20) DEFAULT NULL, + telefone varchar(20) DEFAULT NULL, + email varchar(255) DEFAULT NULL, + ultimoTratamento varchar(100) DEFAULT NULL, + convenio varchar(100) DEFAULT NULL, + dataNascimento varchar(20) DEFAULT NULL, + PRIMARY KEY (id) +); + +-- 14. planos +CREATE TABLE planos ( + id varchar(50) NOT NULL, + nome varchar(100) NOT NULL, + tipo varchar(50) DEFAULT NULL CHECK (tipo IN ('Convenio','Particular','Parceria')), + descontoPadrao decimal(5,2) DEFAULT NULL, + corCartao varchar(20) DEFAULT NULL, + PRIMARY KEY (id) +); + +-- 15. procedimentos +CREATE TABLE procedimentos ( + id varchar(50) NOT NULL, + nome varchar(255) NOT NULL, + descricao text, + especialidadeId varchar(50) DEFAULT NULL, + especialidadeNome varchar(100) DEFAULT NULL, + valorParticular decimal(10,2) DEFAULT NULL, + codigoInterno varchar(50) DEFAULT NULL, + categoria varchar(50) DEFAULT 'Particular', + tipo_regiao varchar(50) DEFAULT 'GERAL' CHECK (tipo_regiao IN ('DENTE','ARCO','GERAL')), + exige_face smallint DEFAULT 0, + codigo varchar(50) DEFAULT NULL, + ordem integer DEFAULT 0, + PRIMARY KEY (id), + CONSTRAINT procedimentos_ibfk_1 FOREIGN KEY (especialidadeId) REFERENCES especialidades (id) ON DELETE SET NULL +); + +-- 16. procedimentos_valores_planos +CREATE TABLE procedimentos_valores_planos ( + id serial NOT NULL, + procedimentoId varchar(50) DEFAULT NULL, + planoId varchar(50) DEFAULT NULL, + planoNome varchar(100) DEFAULT NULL, + valor decimal(10,2) DEFAULT NULL, + codigoPlano varchar(50) DEFAULT NULL, + PRIMARY KEY (id), + CONSTRAINT procedimentos_valores_planos_ibfk_1 FOREIGN KEY (procedimentoId) REFERENCES procedimentos (id) ON DELETE CASCADE +); + +-- 17. settings +CREATE TABLE settings ( + id varchar(50) NOT NULL, + category varchar(50) NOT NULL, + data text, + PRIMARY KEY (id) +); + +-- 18. usuarios +CREATE TABLE usuarios ( + id varchar(50) NOT NULL, + nome varchar(255) DEFAULT NULL, + email varchar(100) NOT NULL, + senha varchar(255) NOT NULL, + cro varchar(20) DEFAULT NULL, + cro_uf varchar(2) DEFAULT NULL, + celular varchar(20) DEFAULT NULL, + especialidade varchar(100) DEFAULT NULL, + cor_agenda varchar(20) DEFAULT '#2563eb', + role varchar(50) DEFAULT 'admin' CHECK (role IN ('paciente','dentista','funcionario','donoclinica','admin')), + createdAt timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT unique_email UNIQUE (email) +); + +-- 19. vinculos +CREATE TABLE vinculos ( + id varchar(50) NOT NULL, + usuario_id varchar(50) NOT NULL, + clinica_id varchar(50) NOT NULL, + role varchar(50) DEFAULT 'admin' CHECK (role IN ('paciente','dentista','funcionario','donoclinica','admin')), + createdAt timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT user_clinica UNIQUE (usuario_id, clinica_id) +); + +-- ============================================================================= +-- STATIC DATA SEED (Corrected Encodings) +-- ============================================================================= + +-- clinicas +INSERT INTO clinicas VALUES ('c1','SCOREODONTO MATRIZ','00.000.000/0001-00','#059669','2026-03-10 15:55:57'); +INSERT INTO clinicas VALUES ('c2','SCOREODONTO FILIAL SUL','00.000.000/0002-00','#2563eb','2026-03-10 15:55:57'); + +-- dentistas +INSERT INTO dentistas VALUES ('d1','c1','MURILO AMORIM','muriloamorim791@gmail.com','(67) 99999-1111','#10B981','ORTODONTIA',1,0); +INSERT INTO dentistas VALUES ('d2','c1','RUI CESAR VARGAS','ruibto@gmail.com','(67) 99999-2222','#3B82F6','IMPLANTE',1,0); + +-- agendamentos +INSERT INTO agendamentos VALUES ('test_1',NULL,'WESLLEY ANTONY TELLES DA SILVA','d1','2026-02-27 12:03:07','2026-02-27 13:03:07','Pendente','TESTE',NULL); + +-- especialidades +INSERT INTO especialidades VALUES ('0.29431554436274265','DENTISTICA','RESTAURAÇÕES',1); +INSERT INTO especialidades VALUES ('e1','ORTODONTIA','CORREÇÃO DA POSIÇÃO DOS DENTES E OSSOS MAXILARES',2); +INSERT INTO especialidades VALUES ('e2','IMPLANTODONTIA','IMPLANTES UNITÁRIOS E MÚLTIPLOS',4); +INSERT INTO especialidades VALUES ('e3','ENDODONTIA','TRATAMENTO DE CANAL E LESÕES PERIAPICAIS',3); +INSERT INTO especialidades VALUES ('e4','CLINICO GERAL','PROCEDIMENTOS BÁSICOS, LIMPEZA E RESTAURAÇÕES',0); + +-- financeiro +INSERT INTO financeiro VALUES ('f1',NULL,'WESLLEY ANTONY','MANUTENÇÃO MENSAL ORTO',150.00,'2023-10-15','Pago','Pix','RECEITA'); +INSERT INTO financeiro VALUES ('f2',NULL,'NICOLLY BEATRIZ','ENTRADA IMPLANTE',1200.00,'2023-10-20','Pendente','Boleto','RECEITA'); + +-- guias_odontologicas +INSERT INTO guias_odontologicas VALUES ('1','177903','O-001','2026-02-25','Tratamento Odontológico','AUTORIZADO','B1','RONEY OTTONI DE SOUZA','181442','2026-02-26 15:43:38'); +INSERT INTO guias_odontologicas VALUES ('2','178056','O-002','2026-02-25','Tratamento Odontológico','AUTORIZADO','B2','MILENA LIMA DIAS OTTONI DE SOUZA','157320','2026-02-26 15:43:38'); +INSERT INTO guias_odontologicas VALUES ('3','178179','O-003','2026-02-25','Tratamento Odontológico','AUTORIZADO','B3','EVANDRO MACHADO AZEMAN','2025100002341','2026-02-26 15:43:38'); +INSERT INTO guias_odontologicas VALUES ('4','177208','O-004','2026-02-24','Tratamento Odontológico','AUTORIZADO','B4','MARIA JULIA MIRANDA DA SILVA','145805','2026-02-26 15:43:38'); +INSERT INTO guias_odontologicas VALUES ('5','177335','O-005','2026-02-24','Tratamento Odontológico','AUTORIZADO_PARCIAL','B5','JOAO ITALO CORREA DE AMORIM SANT ANNA','443079','2026-02-26 15:43:38'); +INSERT INTO guias_odontologicas VALUES ('6','176468','O-006','2026-02-23','Urgência / Emergência','AUTORIZADO','B6','EDUARDO STENIO GONCALVES DOS SANTOS','435593','2026-02-26 15:43:38'); +INSERT INTO guias_odontologicas VALUES ('7','176572','O-007','2026-02-23','Tratamento Odontológico','AUTORIZADO','B7','LUCIA MARIA DA SILVA JULIO','127311','2026-02-26 15:43:38'); +INSERT INTO guias_odontologicas VALUES ('8','176666','O-008','2026-02-23','Tratamento Odontológico','AUTORIZADO','B8','JAIRO HIDEKI NAGAO','96512','2026-02-26 15:43:38'); +INSERT INTO guias_odontologicas VALUES ('9','176721','O-009','2026-02-23','Tratamento Odontológico','AUTORIZADO','B8','JAIRO HIDEKI NAGAO','96512','2026-02-26 15:43:38'); + +-- notificacoes +INSERT INTO notificacoes VALUES ('notif_fin_overdue_f2','FATURA ATRASADA','A conta "ENTRADA IMPLANTE" de R$ 1200.00 venceu em 20/10/2023','financeiro',1,'2026-02-27 10:16:03'); +INSERT INTO notificacoes VALUES ('notif_fin_overdue_f2_new','FATURA ATRASADA','A conta "ENTRADA IMPLANTE" de R$ 1200.00 venceu em 20/10/2023','financeiro',0,'2026-03-10 15:55:58'); +INSERT INTO notificacoes VALUES ('notif_sys_setup','CONFIGURAÇÃO DO SISTEMA','O sistema possui poucos pacientes cadastrados. Considere importar dados.','sistema',1,'2026-02-27 10:16:03'); + +-- pacientes +INSERT INTO pacientes VALUES ('1','WESLLEY ANTONY TELLES DA SILVA','702.013.331-20','(67) 98125-2514','WESLLEY@EMAIL.COM','ORTO','CASSEMS','17/08/2000'); +INSERT INTO pacientes VALUES ('2','NICOLLY BEATRIZ TEIXEIRA','101.034.232-06','(67) 99904-7557','NICOLLY@EMAIL.COM','LIMPEZA','ODONTOSEG','22/09/2010'); +INSERT INTO pacientes VALUES ('3','ROSANGELA DUTRA','917.670.830-68','(67) 9272-2487','ROSANGELA@EMAIL.COM',NULL,'CASSEMS','23/11/1978'); + +-- planos +INSERT INTO planos VALUES ('p1','PARTICULAR','Particular',0.00,'#1f2937'); +INSERT INTO planos VALUES ('p2','UNIMED ODONTO','Convenio',0.00,'#1eb545'); +INSERT INTO planos VALUES ('p3','ODONTOPREV','Convenio',0.00,'#0066cc'); +INSERT INTO planos VALUES ('p4','CASSEMS','Convenio',0.00,'#e63946'); +INSERT INTO planos VALUES ('p5','SESI ODONTO','Convenio',0.00,'#f4a261'); + +-- procedimentos +INSERT INTO procedimentos VALUES ('proc1','CONSULTA INICIAL','AVALIAÇÃO INICIAL E PLANO DE TRATAMENTO','e4','CLÍNICO GERAL',150.00,NULL,'Particular','GERAL',0,NULL,0); +INSERT INTO procedimentos VALUES ('proc2','MANUTENÇÃO ORTODÔNTICA','MANUTENÇÃO MENSAL DE APARELHO FIXO','e1','ORTODONTIA',250.00,'','Particular','ARCO',0,'',0); + +-- procedimentos_valores_planos +INSERT INTO procedimentos_valores_planos (procedimentoId,planoId,planoNome,valor,codigoPlano) VALUES ('proc1','p2','UNIMED ODONTO',80.00,NULL); +INSERT INTO procedimentos_valores_planos (procedimentoId,planoId,planoNome,valor,codigoPlano) VALUES ('proc1','p3','ODONTOPREV',75.00,NULL); + +-- settings +INSERT INTO settings VALUES ('main','general','{"clinicEmail":"RECEP.CONSULTTCLINIC@GMAIL.COM","clinicCalendarId":"RECEP.CONSULTTCLINIC@GMAIL.COM","googleApiKey":"AIzaSyBOCClHTZU9e38VLDljXR-O4QVtk1gCZ1k","googleCalendarIds":{"d2":"ruibto@gmail.com","d1":"muriloamorim791@gmail.com"}}'); + +-- usuarios +INSERT INTO usuarios VALUES ('u1','RUI CESAR','admin@scoreodonto.com','admin',NULL,NULL,NULL,NULL,'#2563eb','admin','2026-03-10 16:27:04'); +INSERT INTO usuarios VALUES ('u2','DENTISTA TESTE','dentista@scoreodonto.com','123456',NULL,NULL,NULL,NULL,'#2563eb','admin','2026-03-10 16:27:04'); +INSERT INTO usuarios VALUES ('u3','FUNCIONARIO TESTE','funcionario@scoreodonto.com','cassems',NULL,NULL,NULL,NULL,'#2563eb','admin','2026-03-10 16:27:04'); +INSERT INTO usuarios VALUES ('u4','DONO CLINICA','dono@scoreodonto.com','Rc362514',NULL,NULL,NULL,NULL,'#2563eb','admin','2026-03-10 16:27:04'); +INSERT INTO usuarios VALUES ('u5','PACIENTE TESTE','paciente@scoreodonto.com','14253636',NULL,NULL,NULL,NULL,'#2563eb','admin','2026-03-10 16:27:04'); + +-- vinculos +INSERT INTO vinculos VALUES ('v_u1_c1','u1','c1','admin','2026-03-10 16:27:04'); +INSERT INTO vinculos VALUES ('v_u1_c2','u1','c2','admin','2026-03-10 16:27:04'); +INSERT INTO vinculos VALUES ('v_u2_c1','u2','c1','dentista','2026-03-10 16:27:04'); +INSERT INTO vinculos VALUES ('v_u3_c1','u3','c1','funcionario','2026-03-10 16:27:04'); +INSERT INTO vinculos VALUES ('v_u4_c1','u4','c1','donoclinica','2026-03-10 16:27:04'); +INSERT INTO vinculos VALUES ('v_u4_c2','u4','c2','donoclinica','2026-03-10 16:27:04'); +INSERT INTO vinculos VALUES ('v_u5_c1','u5','c1','paciente','2026-03-10 16:27:04'); diff --git a/backend/sql/sis_odonto_utf8.sql b/backend/sql/sis_odonto_utf8.sql new file mode 100644 index 0000000..6627c3f --- /dev/null +++ b/backend/sql/sis_odonto_utf8.sql @@ -0,0 +1,600 @@ +-- MySQL dump 10.13 Distrib 9.6.0, for Win64 (x86_64) +-- +-- Host: localhost Database: sis_odonto +-- ------------------------------------------------------ +-- Server version 9.6.0 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; +SET @MYSQLDUMP_TEMP_LOG_BIN = @@SESSION.SQL_LOG_BIN; +SET @@SESSION.SQL_LOG_BIN= 0; + +-- +-- GTID state at the beginning of the backup +-- + +SET @@GLOBAL.GTID_PURGED=/*!80000 '+'*/ 'a55635f9-101c-11f1-b100-00155ded4801:1-1161'; + +-- +-- Table structure for table `agendamentos` +-- + +DROP TABLE IF EXISTS `agendamentos`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `agendamentos` ( + `id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `clinica_id` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `pacienteNome` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `dentistaId` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `start_time` datetime DEFAULT NULL, + `end_time` datetime DEFAULT NULL, + `status` enum('Confirmado','Pendente','Concluido','Faltou') COLLATE utf8mb4_unicode_ci DEFAULT 'Pendente', + `procedimento` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `observacoes` text COLLATE utf8mb4_unicode_ci, + PRIMARY KEY (`id`), + UNIQUE KEY `unique_dentista_slot` (`dentistaId`,`start_time`), + CONSTRAINT `agendamentos_ibfk_1` FOREIGN KEY (`dentistaId`) REFERENCES `dentistas` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `agendamentos` +-- + +LOCK TABLES `agendamentos` WRITE; +/*!40000 ALTER TABLE `agendamentos` DISABLE KEYS */; +INSERT INTO `agendamentos` VALUES ('test_1',NULL,'WESLLEY ANTONY TELLES DA SILVA','d1','2026-02-27 12:03:07','2026-02-27 13:03:07','Pendente','TESTE',NULL); +/*!40000 ALTER TABLE `agendamentos` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `clinicas` +-- + +DROP TABLE IF EXISTS `clinicas`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `clinicas` ( + `id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `nome_fantasia` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `documento` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `cor` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT '#2563eb', + `createdAt` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `clinicas` +-- + +LOCK TABLES `clinicas` WRITE; +/*!40000 ALTER TABLE `clinicas` DISABLE KEYS */; +INSERT INTO `clinicas` VALUES ('c1','SCOREODONTO MATRIZ','00.000.000/0001-00','#059669','2026-03-10 15:55:57'),('c2','SCOREODONTO FILIAL SUL','00.000.000/0002-00','#2563eb','2026-03-10 15:55:57'); +/*!40000 ALTER TABLE `clinicas` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `dentistas` +-- + +DROP TABLE IF EXISTS `dentistas`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `dentistas` ( + `id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `clinica_id` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `nome` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `telefone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `corAgenda` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `especialidade` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `ativo` tinyint(1) DEFAULT '1', + `ordem` int DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `dentistas` +-- + +LOCK TABLES `dentistas` WRITE; +/*!40000 ALTER TABLE `dentistas` DISABLE KEYS */; +INSERT INTO `dentistas` VALUES ('d1','c1','MURILO AMORIM','muriloamorim791@gmail.com','(67) 99999-1111','#10B981','ORTODONTIA',1,0),('d2','c1','RUI CESAR VARGAS','ruibto@gmail.com','(67) 99999-2222','#3B82F6','IMPLANTE',1,0); +/*!40000 ALTER TABLE `dentistas` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `dentistas_horarios` +-- + +DROP TABLE IF EXISTS `dentistas_horarios`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `dentistas_horarios` ( + `id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `dentista_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `clinica_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `dia_semana` int NOT NULL, + `hora_inicio` time NOT NULL, + `hora_fim` time NOT NULL, + `ativo` tinyint(1) DEFAULT '1', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `dentistas_horarios` +-- + +LOCK TABLES `dentistas_horarios` WRITE; +/*!40000 ALTER TABLE `dentistas_horarios` DISABLE KEYS */; +/*!40000 ALTER TABLE `dentistas_horarios` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `especialidades` +-- + +DROP TABLE IF EXISTS `especialidades`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `especialidades` ( + `id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `nome` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `descricao` text COLLATE utf8mb4_unicode_ci, + `ordem` int DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `especialidades` +-- + +LOCK TABLES `especialidades` WRITE; +/*!40000 ALTER TABLE `especialidades` DISABLE KEYS */; +INSERT INTO `especialidades` VALUES ('0.29431554436274265','DENTISTICA','RESTAURA├ç├òES',1),('e1','ORTODONTIA','CORRE├ç├éO DA POSI├ç├éO DOS DENTES E OSSOS MAXILARES',2),('e2','IMPLANTODONTIA','IMPLANTES UNIT├üRIOS E MULTIPLOS',4),('e3','ENDODONTIA','TRATAMENTO DE CANAL E LESÔö£├▓ES PERIAPICAIS',3),('e4','CLINICO GERAL','PROCEDIMENTOS BASICOS, LIMPEZA E RESTAURA├ç├òES',0); +/*!40000 ALTER TABLE `especialidades` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `financeiro` +-- + +DROP TABLE IF EXISTS `financeiro`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `financeiro` ( + `id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `clinica_id` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `pacienteNome` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `descricao` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `valor` decimal(10,2) DEFAULT NULL, + `dataVencimento` date DEFAULT NULL, + `status` enum('Pago','Pendente','Atrasado') COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `formaPagamento` enum('Boleto','Pix','Cart├úo','Dinheiro') COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `tipo` enum('RECEITA','DESPESA') COLLATE utf8mb4_unicode_ci DEFAULT 'RECEITA', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `financeiro` +-- + +LOCK TABLES `financeiro` WRITE; +/*!40000 ALTER TABLE `financeiro` DISABLE KEYS */; +INSERT INTO `financeiro` VALUES ('f1',NULL,'WESLLEY ANTONY','MANUTEN├ç├âO MENSAL ORTO',150.00,'2023-10-15','Pago','Pix','RECEITA'),('f2',NULL,'NICOLLY BEATRIZ','ENTRADA IMPLANTE',1200.00,'2023-10-20','Pendente','Boleto','RECEITA'); +/*!40000 ALTER TABLE `financeiro` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `google_tokens` +-- + +DROP TABLE IF EXISTS `google_tokens`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `google_tokens` ( + `owner_id` varchar(255) NOT NULL, + `refresh_token` text NOT NULL, + `access_token` text, + `expiry_date` bigint DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`owner_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `google_tokens` +-- + +LOCK TABLES `google_tokens` WRITE; +/*!40000 ALTER TABLE `google_tokens` DISABLE KEYS */; +/*!40000 ALTER TABLE `google_tokens` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `gto_builders` +-- + +DROP TABLE IF EXISTS `gto_builders`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `gto_builders` ( + `id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `pacienteId` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `dentistaId` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `credenciadaId` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `status` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT 'RASCUNHO', + `total` decimal(10,2) DEFAULT '0.00', + `createdAt` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `gto_builders` +-- + +LOCK TABLES `gto_builders` WRITE; +/*!40000 ALTER TABLE `gto_builders` DISABLE KEYS */; +/*!40000 ALTER TABLE `gto_builders` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `gto_items` +-- + +DROP TABLE IF EXISTS `gto_items`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `gto_items` ( + `id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `builderId` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `procedimentoId` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `codigoTUSS` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `descricao` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `dente` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `face` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `quantidade` int DEFAULT '1', + `valorUnitario` decimal(10,2) NOT NULL, + `valorTotal` decimal(10,2) NOT NULL, + `observacao` text COLLATE utf8mb4_unicode_ci, + `anexosCount` int DEFAULT '0', + `codigoInterno` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `gto_items` +-- + +LOCK TABLES `gto_items` WRITE; +/*!40000 ALTER TABLE `gto_items` DISABLE KEYS */; +/*!40000 ALTER TABLE `gto_items` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `guias_odontologicas` +-- + +DROP TABLE IF EXISTS `guias_odontologicas`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `guias_odontologicas` ( + `id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `numeroGuiaPrestador` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `numeroGuiaOperadora` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `dataSolicitacao` date NOT NULL, + `tipoTratamento` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `status` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `beneficiarioId` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `beneficiarioNome` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `beneficiarioIdentificacao` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `createdAt` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `guias_odontologicas` +-- + +LOCK TABLES `guias_odontologicas` WRITE; +/*!40000 ALTER TABLE `guias_odontologicas` DISABLE KEYS */; +INSERT INTO `guias_odontologicas` VALUES ('1','177903','O-001','2026-02-25','Tratamento Odontologico','AUTORIZADO','B1','RONEY OTTONI DE SOUZA','181442','2026-02-26 15:43:38'),('2','178056','O-002','2026-02-25','Tratamento Odontologico','AUTORIZADO','B2','MILENA LIMA DIAS OTTONI DE SOUZA','157320','2026-02-26 15:43:38'),('3','178179','O-003','2026-02-25','Tratamento Odontologico','AUTORIZADO','B3','EVANDRO MACHADO AZEMAN','2025100002341','2026-02-26 15:43:38'),('4','177208','O-004','2026-02-24','Tratamento Odontologico','AUTORIZADO','B4','MARIA JULIA MIRANDA DA SILVA','145805','2026-02-26 15:43:38'),('5','177335','O-005','2026-02-24','Tratamento Odontologico','AUTORIZADO_PARCIAL','B5','JOAO ITALO CORREA DE AMORIM SANT ANNA','443079','2026-02-26 15:43:38'),('6','176468','O-006','2026-02-23','Urgencia / Emergencia','AUTORIZADO','B6','EDUARDO STENIO GONCALVES DOS SANTOS','435593','2026-02-26 15:43:38'),('7','176572','O-007','2026-02-23','Tratamento Odontologico','AUTORIZADO','B7','LUCIA MARIA DA SILVA JULIO','127311','2026-02-26 15:43:38'),('8','176666','O-008','2026-02-23','Tratamento Odontologico','AUTORIZADO','B8','JAIRO HIDEKI NAGAO','96512','2026-02-26 15:43:38'),('9','176721','O-009','2026-02-23','Tratamento Odontologico','AUTORIZADO','B8','JAIRO HIDEKI NAGAO','96512','2026-02-26 15:43:38'); +/*!40000 ALTER TABLE `guias_odontologicas` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `leads` +-- + +DROP TABLE IF EXISTS `leads`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `leads` ( + `id` varchar(50) NOT NULL, + `nome` varchar(100) DEFAULT NULL, + `sobrenome` varchar(100) DEFAULT NULL, + `cpf` varchar(20) DEFAULT NULL, + `whatsapp` varchar(20) DEFAULT NULL, + `tipoInteresse` enum('Particular','Plano') DEFAULT NULL, + `dataCadastro` datetime DEFAULT NULL, + `status` enum('Novo','Convertido','Arquivado') DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `leads` +-- + +LOCK TABLES `leads` WRITE; +/*!40000 ALTER TABLE `leads` DISABLE KEYS */; +/*!40000 ALTER TABLE `leads` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `notificacoes` +-- + +DROP TABLE IF EXISTS `notificacoes`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `notificacoes` ( + `id` varchar(50) NOT NULL, + `titulo` varchar(255) DEFAULT NULL, + `mensagem` text, + `tipo` enum('lead','sistema','agenda','financeiro') DEFAULT 'sistema', + `lida` tinyint(1) DEFAULT '0', + `data` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `notificacoes` +-- + +LOCK TABLES `notificacoes` WRITE; +/*!40000 ALTER TABLE `notificacoes` DISABLE KEYS */; +INSERT INTO `notificacoes` VALUES ('notif_fin_overdue_f2','FATURA ATRASADA','A conta \"ENTRADA IMPLANTE\" de R$ 1200.00 venceu em 20/10/2023','financeiro',1,'2026-02-27 10:16:03'),('notif_fin_overdue_f2 ','FATURA ATRASADA','A conta \"ENTRADA IMPLANTE\" de R$ 1200.00 venceu em 20/10/2023 ','financeiro',0,'2026-03-10 15:55:58'),('notif_sys_setup','CONFIGURA├ç├âO DO SISTEMA','O sistema possui poucos pacientes cadastrados. Considere importar dados.','sistema',1,'2026-02-27 10:16:03'); +/*!40000 ALTER TABLE `notificacoes` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `pacientes` +-- + +DROP TABLE IF EXISTS `pacientes`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `pacientes` ( + `id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `nome` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `cpf` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `telefone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `ultimoTratamento` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `convenio` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `dataNascimento` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `pacientes` +-- + +LOCK TABLES `pacientes` WRITE; +/*!40000 ALTER TABLE `pacientes` DISABLE KEYS */; +INSERT INTO `pacientes` VALUES ('1','WESLLEY ANTONY TELLES DA SILVA','702.013.331-20','(67) 98125-2514','WESLLEY@EMAIL.COM','ORTO','CASSEMS','17/08/2000'),('2','NICOLLY BEATRIZ TEIXEIRA','101.034.232-06','(67) 99904-7557','NICOLLY@EMAIL.COM','LIMPEZA','ODONTOSEG','22/09/2010'),('3','ROSANGELA DUTRA','917.670.830-68','(67) 9272-2487','ROSANGELA@EMAIL.COM',NULL,'CASSEMS','23/11/1978'); +/*!40000 ALTER TABLE `pacientes` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `planos` +-- + +DROP TABLE IF EXISTS `planos`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `planos` ( + `id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `nome` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `tipo` enum('Convenio','Particular','Parceria') COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `descontoPadrao` decimal(5,2) DEFAULT NULL, + `corCartao` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `planos` +-- + +LOCK TABLES `planos` WRITE; +/*!40000 ALTER TABLE `planos` DISABLE KEYS */; +INSERT INTO `planos` VALUES ('p1','PARTICULAR','Particular',0.00,'#1f2937'),('p2','UNIMED ODONTO','Convenio',0.00,'#1eb545'),('p3','ODONTOPREV','Convenio',0.00,'#0066cc'),('p4','CASSEMS','Convenio',0.00,'#e63946'),('p5','SESI ODONTO','Convenio',0.00,'#f4a261'); +/*!40000 ALTER TABLE `planos` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `procedimentos` +-- + +DROP TABLE IF EXISTS `procedimentos`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `procedimentos` ( + `id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `nome` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `descricao` text COLLATE utf8mb4_unicode_ci, + `especialidadeId` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `especialidadeNome` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `valorParticular` decimal(10,2) DEFAULT NULL, + `codigoInterno` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `categoria` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT 'Particular', + `tipo_regiao` enum('DENTE','ARCO','GERAL') COLLATE utf8mb4_unicode_ci DEFAULT 'GERAL', + `exige_face` tinyint(1) DEFAULT '0', + `codigo` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `ordem` int DEFAULT '0', + PRIMARY KEY (`id`), + KEY `especialidadeId` (`especialidadeId`), + CONSTRAINT `procedimentos_ibfk_1` FOREIGN KEY (`especialidadeId`) REFERENCES `especialidades` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `procedimentos` +-- + +LOCK TABLES `procedimentos` WRITE; +/*!40000 ALTER TABLE `procedimentos` DISABLE KEYS */; +INSERT INTO `procedimentos` VALUES ('proc1','CONSULTA INICIAL','AVALIAÔö£├ºÔö£├óO INICIAL E PLANO DE TRATAMENTO','e4','CLÔö£├¼NICO GERAL',150.00,NULL,'Particular','GERAL',0,NULL,0),('proc2','MANUTEN├ç├âO ORTOD├öNTICA','MANUTEN├ç├éO MENSAL DE APARELHO FIXO','E1','ORTODONTIA',250.00,'','PARTICULAR','ARCO',0,'',0); +/*!40000 ALTER TABLE `procedimentos` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `procedimentos_valores_planos` +-- + +DROP TABLE IF EXISTS `procedimentos_valores_planos`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `procedimentos_valores_planos` ( + `id` int NOT NULL AUTO_INCREMENT, + `procedimentoId` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `planoId` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `planoNome` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `valor` decimal(10,2) DEFAULT NULL, + `codigoPlano` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `procedimentoId` (`procedimentoId`), + KEY `planoId` (`planoId`), + CONSTRAINT `procedimentos_valores_planos_ibfk_1` FOREIGN KEY (`procedimentoId`) REFERENCES `procedimentos` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `procedimentos_valores_planos` +-- + +LOCK TABLES `procedimentos_valores_planos` WRITE; +/*!40000 ALTER TABLE `procedimentos_valores_planos` DISABLE KEYS */; +INSERT INTO `procedimentos_valores_planos` VALUES (1,'proc1','p2','UNIMED ODONTO',80.00,NULL),(2,'proc1','p3','ODONTOPREV',75.00,NULL); +/*!40000 ALTER TABLE `procedimentos_valores_planos` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `settings` +-- + +DROP TABLE IF EXISTS `settings`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `settings` ( + `id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `category` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `data` text COLLATE utf8mb4_unicode_ci, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `settings` +-- + +LOCK TABLES `settings` WRITE; +/*!40000 ALTER TABLE `settings` DISABLE KEYS */; +INSERT INTO `settings` VALUES ('main','general','{\"clinicEmail\":\"RECEP.CONSULTTCLINIC@GMAIL.COM\",\"clinicCalendarId\":\"RECEP.CONSULTTCLINIC@GMAIL.COM\",\"googleApiKey\":\"AIzaSyBOCClHTZU9e38VLDljXR-O4QVtk1gCZ1k\",\"googleCalendarIds\":{\"d2\":\"ruibto@gmail.com\",\"d1\":\"muriloamorim791@gmail.com\"}}'); +/*!40000 ALTER TABLE `settings` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `usuarios` +-- + +DROP TABLE IF EXISTS `usuarios`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `usuarios` ( + `id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `nome` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `email` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `senha` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `cro` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `cro_uf` varchar(2) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `celular` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `especialidade` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `cor_agenda` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT '#2563eb', + `role` enum('paciente','dentista','funcionario','donoclinica','admin') COLLATE utf8mb4_unicode_ci DEFAULT 'admin', + `createdAt` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `usuarios` +-- + +LOCK TABLES `usuarios` WRITE; +/*!40000 ALTER TABLE `usuarios` DISABLE KEYS */; +INSERT INTO `usuarios` VALUES ('u1','RUI CESAR','admin@scoreodonto.com','admin',NULL,NULL,NULL,NULL,'#2563eb','admin','2026-03-10 16:27:04'),('u2','DENTISTA TESTE','dentista@scoreodonto.com','123456',NULL,NULL,NULL,NULL,'#2563eb','admin','2026-03-10 16:27:04'),('u3','FUNCIONARIO TESTE','funcionario@scoreodonto.com','cassems',NULL,NULL,NULL,NULL,'#2563eb','admin','2026-03-10 16:27:04'),('u4','DONO CLINICA','dono@scoreodonto.com','Rc362514',NULL,NULL,NULL,NULL,'#2563eb','admin','2026-03-10 16:27:04'),('u5','PACIENTE TESTE','paciente@scoreodonto.com','14253636',NULL,NULL,NULL,NULL,'#2563eb','admin','2026-03-10 16:27:04'); +/*!40000 ALTER TABLE `usuarios` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `vinculos` +-- + +DROP TABLE IF EXISTS `vinculos`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `vinculos` ( + `id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `usuario_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `clinica_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `role` enum('paciente','dentista','funcionario','donoclinica','admin') COLLATE utf8mb4_unicode_ci DEFAULT 'admin', + `createdAt` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `user_clinica` (`usuario_id`,`clinica_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `vinculos` +-- + +LOCK TABLES `vinculos` WRITE; +/*!40000 ALTER TABLE `vinculos` DISABLE KEYS */; +INSERT INTO `vinculos` VALUES ('v_u1_c1','u1','c1','admin','2026-03-10 16:27:04'),('v_u1_c2','u1','c2','admin','2026-03-10 16:27:04'),('v_u2_c1','u2','c1','dentista','2026-03-10 16:27:04'),('v_u3_c1','u3','c1','funcionario','2026-03-10 16:27:04'),('v_u4_c1','u4','c1','donoclinica','2026-03-10 16:27:04'),('v_u4_c2','u4','c2','donoclinica','2026-03-10 16:27:04'),('v_u5_c1','u5','c1','paciente','2026-03-10 16:27:04'); +/*!40000 ALTER TABLE `vinculos` ENABLE KEYS */; +UNLOCK TABLES; +SET @@SESSION.SQL_LOG_BIN = @MYSQLDUMP_TEMP_LOG_BIN; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2026-03-15 20:33:25 diff --git a/base/DEPLOY_VPS.md b/base/DEPLOY_VPS.md new file mode 100644 index 0000000..95f8ef4 --- /dev/null +++ b/base/DEPLOY_VPS.md @@ -0,0 +1,87 @@ +# GUIA DE DEPLOY - SCOREODONTO CRM (VPS) + +Este guia detalha o processo de implantação do ScoreOdonto CRM em um servidor VPS (Ubuntu/Debian recomendado). + +## 1. Pré-requisitos +* Node.js (v18+) e NPM. +* MySQL Server. +* Nginx (como Proxy Reverso). +* PM2 (Gerenciador de Processos). + +## 2. Configuração do Banco de Dados +No servidor MySQL da VPS, crie o banco e as tabelas: +```sql +CREATE DATABASE sis_odonto CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +``` +Importe seus dados usando o `SyncView` ou o script de atualização após o deploy. + +## 3. Preparação do Ambiente +Clone o repositório na VPS e instale as dependências: +```bash +cd /var/www/scoreodonto +npm install +``` + +Crie o arquivo `.env` na pasta raiz (`scoreodonto/`): +```env +PORT=3002 +DB_HOST=localhost +DB_USER=seu_usuario +DB_PASSWORD=sua_senha +DB_NAME=sis_odonto +``` + +## 4. Build do Frontend +Gere os arquivos estáticos para produção: +```bash +npm run build +``` +Isso criará a pasta `dist` (ou `build`). + +## 5. Gerenciamento com PM2 +Inicie o servidor backend: +```bash +pm2 start server.js --name "scoreodonto-api" +pm2 save +``` + +## 6. Configuração do Nginx +Edite o arquivo de configuração do site no Nginx (`/etc/nginx/sites-available/scoreodonto`): + +```nginx +server { + listen 80; + server_name seu-dominio.com; + + # Frontend + location / { + root /var/www/scoreodonto/dist; + index index.html; + try_files $uri $uri/ /index.html; + } + + # Backend API + location /api { + proxy_pass http://localhost:3002; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } +} +``` +Ative o site e reinicie o Nginx: +```bash +sudo ln -s /etc/nginx/sites-available/scoreodonto /etc/nginx/sites-enabled/ +sudo nginx -t +sudo systemctl restart nginx +``` + +## 7. Ajustes de Produção +* **API URL**: O frontend agora usa o `API_URL` relativo ou configurado via environment. Como o Nginx está redirecionando `/api` para o backend, o sistema funcionará corretamente de forma transparente. +* **Notificações**: Estão unificadas no topo de cada página para fácil acesso. +* **Segurança**: Certifique-se de configurar um certificado SSL (Certbot/Let's Encrypt). + +--- +*Gerado automaticamente pelo Antigravity.* diff --git a/base/ESTRATEGIA_TECNICA.md b/base/ESTRATEGIA_TECNICA.md new file mode 100644 index 0000000..38eb182 --- /dev/null +++ b/base/ESTRATEGIA_TECNICA.md @@ -0,0 +1,80 @@ +# 📒 Guia Estratégico e Arquitetura da Automação + +Este documento detalha as escolhas técnicas e o funcionamento estratégico da automação desenvolvida para o **Portal Viventeris (Cassems)**, explicando "o porquê" de cada decisão. + +--- + +## 🏗️ 1. Arquitetura: Por que Playwright e não Axios? + +Ao desenvolver automações para portais de saúde (TISS/SADT), escolhemos o **Playwright** em vez de ferramentas de requisição direta como o **Axios**. Aqui está a justificativa: + +### Playwright (Navegador Real) - *Nossa Escolha* +- **Simulação Humana**: Ele carrega o JavaScript do portal, processa pop-ups e lida com o "autocomplete" do jQuery UI (essencial para que o sistema valide o procedimento). +- **Gestão de Sessão**: O portal Viventeris possui tokens de segurança e cookies expiráveis. O navegador gerencia isso automaticamente. +- **Resiliência Visual**: Se o portal mudar a posição de um campo, a automação baseada em `id` ou `label` continua funcionando. +- **Monitoramento**: Você pode ver o navegador trabalhando, o que facilita identificar se o sistema da Cassems está lento ou fora do ar. + +### Axios/API (Requisições Ocultas) - *Descartado* +- **Extrema Complexidade**: Exigiria descobrir todos os "requests" ocultos que o servidor espera. +- **Fragilidade**: Qualquer mudança na segurança do servidor quebraria a integração sem deixar rastro visual. +- **Bloqueios**: Sistemas modernos de saúde detectam requisições "secas" (sem navegador) e bloqueiam o acesso por suspeita de ataque. + +--- + +## 🧠 2. Inteligência do Fluxo (Regras de Negócio) + +O script foi treinado para seguir regras específicas de UX do portal: + +1. **Regra de Urgência**: + - O sistema monitora o select `#tipoAtendimento`. + - Se selecionado **Urgência (ID 4)**, ele obrigatoriamente preenche a `Justificativa`. + - Se selecionado **Eletivo (ID 1)**, ele pula o campo para evitar erros de validação. +2. **Lógica do Autocomplete**: + - Digitar o código do procedimento não é suficiente. O script foi programado para **clicar no item da lista suspensa** que o sistema sugere. Sem esse clique, o sistema muitas vezes "limpa" o campo ao tentar avançar. +3. **Superação de Sobreposição**: + - No campo de **Faces (V, M, D, L/P)**, o script utiliza cliques forçados nas labels, pois os checkboxes originais são ocultados por CSS de design do portal. + +--- + +## 🛠️ 3. Manutenção e Boas Práticas + +### Como manter o código funcionando: +- **IDs Estáveis**: Sempre que possível, usamos o `id` (ex: `#cmbDente`). Se o portal mudar o layout, mas mantiver os IDs, o código **não quebra**. +- **Timeouts**: Configuramos esperas inteligentes. O script não trava se a internet oscilar por 2 segundos; ele aguarda o elemento aparecer. + +### Segurança dos Dados: +- **Credenciais**: Atualmente fixas no código para testes. Em produção, recomenda-se usar variáveis de ambiente ou um arquivo de configuração externo. +- **Breakpoint de Erro**: O código possui a função `catch`. Se algo der errado (ex: beneficiário bloqueado), o script **para tudo**, tira um print da tela (`erro-guia-odonto.png`) e mantém o navegador aberto para você ver o motivo. + +--- + +## 🛠️ 4. Infraestrutura Atual (VM Windows) + +Como estamos operando dentro de uma Máquina Virtual, optamos pela instalação nativa para garantir performance e estabilidade: + +| Recurso | Status | Host:Porta | Credenciais | Gerenciamento | +| :--- | :--- | :--- | :--- | :--- | +| **MySQL (9.6)** | Ativo (Serviço) | `localhost:3306` | User: `root` / Pass: (vazio) | MySQL CLI / DBeaver | +| **Redis (8.6)** | Ativo (PM2) | `localhost:6379` | s/ senha | `pm2 monit` / `redis-cli` | +| **Python (3.14)**| Ativo | - | - | `python --version` | +| **PM2** | Ativo | - | - | `pm2 list` | + +### Comandos de Manutenção: +- **Verificar Redis**: `pm2 list` ou `redis-cli ping` +- **Verificar MySQL**: `Get-Service MySQL` +- **Logs do Redis**: `pm2 logs redis-server` + +--- + +## 📋 5. Comandos Úteis + +| Objetivo | Comando | +| :--- | :--- | +| **Instalar Dependências** | `npm install` | +| **Rodar Lançamento** | `node lancar-guia-odonto.js` | +| **Verificar Versão Node** | `node -v` | +| **Logs do Processo** | `pm2 logs` | + +--- + +**Conclusão**: Esta automação transforma um processo de 5 minutos de cliques manuais em uma tarefa de 20 segundos totalmente segura e auditável. diff --git a/base/README_AUTOMACAO.md b/base/README_AUTOMACAO.md new file mode 100644 index 0000000..6088eac --- /dev/null +++ b/base/README_AUTOMACAO.md @@ -0,0 +1,63 @@ +# 🦷 Projeto de Automação: Portal Viventeris (Cassems) + +Este documento resume a estrutura e a lógica da automação desenvolvida para o lançamento de guias odontológicas no portal Viventeris. + +## 🚀 Visão Geral +A automação utiliza **Node.js** e **Playwright** para simular a interação humana com o portal, superando desafios de interface (UX) e automação de processos repetitivos. + +--- + +## 🛠️ Tecnologias Utilizadas +- **Linguagem**: JavaScript (Node.js) +- **Engine**: Playwright (Chromium) +- **Estratégia**: Interação via Seletores de ID, XPath e Filtros de Visibilidade. + +--- + +## 🧠 Fluxo de Automação Compreendido + +### 1. Acesso e Segurança +- **Login**: Autenticação automática com User/Senha. +- **User-Agent**: Configurado para evitar o bloqueio de "Navegador Incompatível". + +### 2. Identificação do Beneficiário +- Navega até o menu **Tratamento Odontológico**. +- Realiza a busca via **CPF** (formato: `000.000.000-00`). +- Seleciona o beneficiário clicando no link do cartão retornado na tabela. + +### 3. Configuração do Atendimento +- **Novo Registro**: Clica no botão `+` (`#btnNovoRegistro`). +- **Endereço**: Seleciona automaticamente o primeiro endereço disponível se houver um select. +- **Tipo de Atendimento**: + - `Valor 1`: Tratamento Odontológico (Normal). + - `Valor 4`: Urgência / Emergência. +- **Regra de Justificativa**: + - O campo `Observação/Justificativa` (`#observacaoJustificativa`) só é preenchido se o tipo for **Urgência (4)**. Caso contrário, permanece vazio. + +### 4. Seleção de Profissionais +- **Executante**: Utiliza o botão de busca (`#btnBuscaPrestadorExecutante`) ao lado do input. +- **Solicitante**: Preenchimento fixo/variável do campo de texto (ex: "Murilo"). + +### 5. Lançamento de Procedimentos (Modal) +- **Autocomplete**: Após digitar o código (ex: `85100196`), o script aguarda e clica obrigatoriamente no item da lista jQuery UI (`.ui-menu-item-wrapper`) para que o sistema valide a seleção. +- **Dente**: Seleção via ID `#cmbDente`. +- **Face**: Clique forçado no checkbox `#codFaceV` para evitar interceptação pela label de estilo. +- **Inclusão**: Clica em `#btnIncluirProcedimento` dentro do modal. + +### 6. Finalização e Captura +- Avança as etapas de confirmação ("Próximo"). +- Extrai o número da **GTO** gerada e o **Status** (Autorizado/Negado). +- Salva o resultado no arquivo `guias_geradas.txt`. + +--- + +## 📁 Estrutura de Arquivos +- `login-test.js`: Validação inicial de acesso. +- `lancar-guia-odonto.js`: Script principal de produção. +- `guias_geradas.txt`: Log histórico de GTOs geradas. +- `procedimentos_encontrados.txt`: Dicionário de códigos capturados durante a exploração. + +--- + +## 📝 Observação Importante sobre Erros +Conforme alinhado, o script possui **breakpoints de segurança**. Caso um elemento (como o nome de um profissional específico ou um campo novo) não seja encontrado, o script **PARA IMEDIATAMENTE** e aguarda intervenção manual, evitando o preenchimento de dados incorretos no portal oficial. diff --git a/base/axios-engine/poc-axios-login.js b/base/axios-engine/poc-axios-login.js new file mode 100644 index 0000000..94406f2 --- /dev/null +++ b/base/axios-engine/poc-axios-login.js @@ -0,0 +1,49 @@ +const axios = require('axios'); +const qs = require('qs'); + +async function testAxiosLogin() { + console.log('🧪 Testando Login via Axios...'); + + const loginUrl = 'https://viventerisportalcredenciado.topsaudehub.com.br/credenciado/Account/AutenticarCredenciado/?returnUrl='; + + // Dados capturados no mapeamento + const payload = qs.stringify({ + 'usuario': '0005010', + 'senha': 'Cclinic#03', + 'token': '', + 'login-submit': 'ENTRAR' + }); + + try { + const response = await axios.post(loginUrl, payload, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + 'Origin': 'https://viventerisportalcredenciado.topsaudehub.com.br', + 'Referer': 'https://viventerisportalcredenciado.topsaudehub.com.br/' + }, + maxRedirects: 0, // Queremos ver para onde ele nos manda + validateStatus: (status) => status >= 200 && status < 400 + }); + + console.log('\n✅ Login enviado com sucesso!'); + console.log('Status:', response.status); + console.log('Cookies recebidos:', response.headers['set-cookie']); + console.log('Redirecionado para:', response.headers.location); + + if (response.headers.location && response.headers.location.includes('AreaLogada')) { + console.log('\n🔥 SUCESSO! O servidor aceitou o login via Axios.'); + } else { + console.log('\n⚠️ O login parece ter sido recusado ou mudou de caminho.'); + } + + } catch (error) { + console.error('\n❌ Erro no Login Axios:', error.message); + if (error.response) { + console.log('Status do Erro:', error.response.status); + console.log('Data:', error.response.data); + } + } +} + +testAxiosLogin(); diff --git a/base/axios-engine/robot-axios-v1.js b/base/axios-engine/robot-axios-v1.js new file mode 100644 index 0000000..aeb04cb --- /dev/null +++ b/base/axios-engine/robot-axios-v1.js @@ -0,0 +1,217 @@ +const axios = require('axios'); +const qs = require('qs'); +const mysql = require('mysql2/promise'); + +// Configurações +const CREDENTIALS = { + usuario: '0005010', + senha: 'Cclinic#03' +}; + +const DB_CONFIG = { + host: 'localhost', + user: 'root', + password: '', + database: 'cassems_automation' +}; + +class CassemsRobot { + constructor() { + this.client = axios.create({ + baseURL: 'https://viventeris.topsaudehub.com.br/api/APICredenciado/api', + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + 'Accept': 'application/json, text/plain, */*' + } + }); + this.token = null; + } + + async login() { + console.log('🔐 Autenticando no portal...'); + const loginUrl = 'https://viventerisportalcredenciado.topsaudehub.com.br/credenciado/Account/AutenticarCredenciado/?returnUrl='; + + const payload = qs.stringify({ + ...CREDENTIALS, + 'token': '', + 'login-submit': 'ENTRAR' + }); + + const res = await axios.post(loginUrl, payload, { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + maxRedirects: 0, + validateStatus: s => s < 400 + }); + + // Extrair Token do Cookie + const cookies = res.headers['set-cookie'] || []; + const tokenCookie = cookies.find(c => c.includes('TOKEN=bearer')); + + if (!tokenCookie) { + console.log('Cookies recebidos:', cookies); + throw new Error('Token não encontrado nos cookies de resposta.'); + } + + // Decodificar o token da URL (substituir %20 por espaço se necessário) + const rawToken = tokenCookie.split('TOKEN=')[1].split(';')[0]; + this.token = decodeURIComponent(rawToken).replace('bearer ', ''); + + this.client.defaults.headers.common['Authorization'] = `bearer ${this.token}`; + + console.log('✅ Autenticado com sucesso! Token JWT capturado.'); + } + + async buscarBeneficiario(cpf) { + const cpfLimpo = cpf.replace(/\D/g, ''); + console.log(`🔍 Buscando beneficiário CPF: ${cpf}...`); + + const payload = { + codPrestadorTs: "501", + numAssociado: "", + numCpf: cpfLimpo, + numRg: "", + nomeAssociado: "", + numPedido: "", + numProtocoloAns: "", + tipoPrestador: "D" + }; + + const res = await this.client.post('/beneficiario/buscaBeneficiario', payload); + + if (res.data && res.data.length > 0) { + const b = res.data[0]; + console.log('DEBUG - Estrutura do Beneficiário:', JSON.stringify(b, null, 2)); + + // Mapeamento correto conforme o DEBUG + b.nome_final = b.nome_associado || b.NOM_PESSOA || "NOME DESCONHECIDO"; + b.id_final = b.cod_ts || b.COD_TS || "ID DESCONHECIDO"; + + console.log(`👤 Beneficiário Encontrado: ${b.nome_final} (ID: ${b.id_final})`); + return b; + } + + console.log('⚠️ Beneficiário não encontrado.'); + return null; + } + + async lancarGuia(beneficiario, procedimento, dente, face) { + console.log(`🚀 Lançando guia para ${beneficiario.nome_final}...`); + + const dataHoje = new Date().toLocaleDateString('pt-BR'); + + const payload = { + "dadosAtendimento": [{ + "codPrestadorTs": "501", + "codTs": beneficiario.cod_ts, + "numAssociado": beneficiario.num_associado, + "nomeAssociado": beneficiario.nome_associado, + "indSexo": beneficiario.indSexo || "", + "codPlano": beneficiario.codPlano || "", + "codEmpresa": beneficiario.codEmpresa || "", + "numCPF": beneficiario.cpf || "", + "numContrato": beneficiario.numContrato || "", + "numAssociadoTitular": beneficiario.numAssociadoTitular || "", + "nomeAssociadoTitular": beneficiario.nomeAssociadoTitular || "", + "nomeContrato": beneficiario.nomeContrato || "", + "num_ddd_cel_contato": "", + "num_cel_contato": "", + "num_ddd_res_contato": "", + "num_res_contato": "", + "txt_end_email_contato": "" + }], + "dadosProfissional": [{ + "codProExecutanteTs": "502", + "atendimentoRN": "N", + "enderecoAtendimento": "1", + "cnes": "9999999", + "dataSolicitacao": dataHoje, + "tipoAtendimento": "1", + "nomeProfissionalExecutante": "RUI CESAR VARGAS CASANOVA", + "croExecutante": "07381", + "ufExecutante": "MS", + "cboExecutante": "", + "nomeProfissionalSolicitante": "Murilo", + "croSolicitante": "", + "ufSolicitante": "", + "codCboSolicitante": "", + "observacaoJustificativa": "" + }], + "autorizacaoItem": [{ + "num_seq_item": 1, + "item_medico": procedimento, + "nome_item": "PROCEDIMENTO VIA ROBÔ", + "dente_regiao": dente, + "cod_face": face, + "ind_rx_odonto": "N", + "ind_foto": "N", + "ind_laudo": "N", + "qtd_procedimento": "1" + }], + "situacaoInicial": [], + "ortodontiaPartial": [], + "prorrogacaoPartial": [], + "ExisteProrrogacao": "N", + "ExisteOrtodontia": "N", + "TratamentoTransferido": "N", + "NumeroTratamento": "", + "CodUsuario": "0005010", + "NumSeqPedidoUltimo": "" + }; + + const res = await this.client.post('/autorizacao/NovoTratamento', payload); + + if (res.data) { + console.log('✅ Guia Processada com Sucesso!'); + return res.data; + } + + throw new Error('Falha no processamento da guia.'); + } + + async registrarNoBanco(dados) { + const connection = await mysql.createConnection(DB_CONFIG); + await connection.execute( + 'INSERT INTO guias_pendentes (cpf, procedimento, dente, face, status, gto) VALUES (?, ?, ?, ?, ?, ?)', + [dados.cpf, dados.procedimento, dados.dente, dados.face, dados.status, dados.gto || null] + ); + console.log('💾 Registro salvo no MySQL.'); + await connection.end(); + } +} + +// Execução de Teste +(async () => { + const robot = new CassemsRobot(); + try { + await robot.login(); + + const cpf = '005.983.051-49'; + const proc = '85100196'; + const dente = '11'; + const face = 'V'; + + const beneficiario = await robot.buscarBeneficiario(cpf); + + if (beneficiario) { + const resultado = await robot.lancarGuia(beneficiario, proc, dente, face); + console.log('Dados do Retorno da API:', JSON.stringify(resultado, null, 2)); + + // Tenta capturar o número da guia se existir no JSON + const numeroGuia = resultado.NumeroTratamento || resultado.numGuia || "VER_LOG"; + + await robot.registrarNoBanco({ + cpf: cpf, + procedimento: proc, + dente: dente, + face: face, + status: 'AUTORIZADO (AXIOS)', + gto: numeroGuia + }); + } + + console.log('\n🔥 SUCESSO TOTAL! Guia lançada via API em milissegundos.'); + } catch (e) { + console.error('❌ Erro no Robô:', e.message); + if (e.response) console.log('Detalhes:', JSON.stringify(e.response.data, null, 2)); + } +})(); diff --git a/base/axios-engine/sql/cassems_automation.sql b/base/axios-engine/sql/cassems_automation.sql new file mode 100644 index 0000000..a9be826 Binary files /dev/null and b/base/axios-engine/sql/cassems_automation.sql differ diff --git a/base/check_db.js b/base/check_db.js new file mode 100644 index 0000000..15766aa --- /dev/null +++ b/base/check_db.js @@ -0,0 +1,51 @@ +const mysql = require('mysql2/promise'); + +const dbConfig = { + host: 'localhost', + user: 'root', + password: '', + database: 'sis_odonto' +}; + +async function check() { + try { + const connection = await mysql.createConnection(dbConfig); + console.log("Connected to DB"); + + try { + const [users] = await connection.query('SELECT count(*) as count FROM usuarios'); + console.log("Users count:", users[0].count); + } catch (e) { + console.log("Table 'usuarios' not found or error:", e.message); + } + + try { + const [links] = await connection.query('SELECT count(*) as count FROM vinculos'); + console.log("Links count:", links[0].count); + } catch (e) { + console.log("Table 'vinculos' not found or error:", e.message); + } + + try { + const [clinics] = await connection.query('SELECT count(*) as count FROM clinicas'); + console.log("Clinics count:", clinics[0].count); + } catch (e) { + console.log("Table 'clinicas' not found or error:", e.message); + } + + const [usersData] = await connection.query('SELECT id, email, senha FROM usuarios'); + console.log("Users in DB (email/password):"); + usersData.forEach(u => console.log(`- ${u.email} / ${u.senha}`)); + + for (const user of usersData) { + const [userLinks] = await connection.query('SELECT * FROM vinculos WHERE usuario_id = ?', [user.id]); + console.log(`Links for ${user.email} (ID: ${user.id}):`, userLinks.map(l => ({ clinica: l.clinica_id, role: l.role }))); + } + + await connection.end(); + } catch (err) { + console.error("DB Check failed:", err.message); + } +} + +check(); diff --git a/base/fix_collations.js b/base/fix_collations.js new file mode 100644 index 0000000..3516199 --- /dev/null +++ b/base/fix_collations.js @@ -0,0 +1,69 @@ +const mysql = require('mysql2/promise'); + +const dbConfig = { + host: 'localhost', + user: 'root', + password: '', + database: 'sis_odonto' +}; + +async function fix() { + try { + const c = await mysql.createConnection(dbConfig); + console.log("Connected for fixing collations..."); + + const tables = [ + 'usuarios', 'clinicas', 'vinculos', 'dentistas', 'agendamentos', + 'pacientes', 'financeiro', 'planos', 'especialidades', 'procedimentos', + 'procedimentos_valores_planos', 'guias_odontologicas', 'gto_builders', 'gto_items', 'notificacoes', 'settings' + ]; + + for (const t of tables) { + try { + // Remove FK checks to avoid issues during alter + await c.query('SET FOREIGN_KEY_CHECKS = 0'); + await c.query(`ALTER TABLE ${t} CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`); + await c.query('SET FOREIGN_KEY_CHECKS = 1'); + console.log(`[OK] ${t} normalized.`); + } catch (err) { + console.log(`[SKIP] ${t}: ${err.message}`); + await c.query('SET FOREIGN_KEY_CHECKS = 1'); + } + } + + // Link unassigned dentists (professionals) to the main clinic + await c.query('UPDATE dentistas SET clinica_id = "c1" WHERE clinica_id IS NULL OR clinica_id = ""'); + console.log("All dentists linked to c1 (Matriz)."); + + // Now get the division + const [division] = await c.query(` + SELECT + c.nome_fantasia as Clinica, + u.nome as Nome, + v.role as Papel, + u.email as Email + FROM vinculos v + JOIN usuarios u ON v.usuario_id = u.id + JOIN clinicas c ON v.clinica_id = c.id + WHERE v.role IN ('dentista', 'donoclinica') + ORDER BY c.nome_fantasia + `); + + console.log("\n--- DIVISÃO DE DENTISTAS/DONOS POR CLÍNICA ---"); + console.table(division); + + const [professionals] = await c.query(` + SELECT d.nome, c.nome_fantasia as clinica + FROM dentistas d + JOIN clinicas c ON d.clinica_id = c.id + `); + console.log("\n--- REGISTROS PROFISSIONAIS (LISTA DE DENTISTAS) ---"); + console.table(professionals); + + await c.end(); + } catch (err) { + console.error("Fix failed:", err.message); + } +} + +fix(); diff --git a/base/fix_planos.js b/base/fix_planos.js new file mode 100644 index 0000000..6103148 --- /dev/null +++ b/base/fix_planos.js @@ -0,0 +1,40 @@ +const mysql = require('mysql2/promise'); + +async function run() { + const c = await mysql.createConnection({ + host: 'localhost', user: 'root', password: '', database: 'sis_odonto' + }); + + // 1. Seed missing plans (those referenced by existing procedimentos_valores_planos) + const plans = [ + { id: 'p2', nome: 'UNIMED ODONTO', tipo: 'Convenio', descontoPadrao: 0, corCartao: '#1eb545' }, + { id: 'p3', nome: 'ODONTOPREV', tipo: 'Convenio', descontoPadrao: 0, corCartao: '#0066cc' }, + { id: 'p4', nome: 'CASSEMS', tipo: 'Convenio', descontoPadrao: 0, corCartao: '#e63946' }, + { id: 'p5', nome: 'SESI ODONTO', tipo: 'Convenio', descontoPadrao: 0, corCartao: '#f4a261' }, + ]; + + for (const p of plans) { + await c.query( + 'INSERT IGNORE INTO planos (id, nome, tipo, descontoPadrao, corCartao) VALUES (?, ?, ?, ?, ?)', + [p.id, p.nome, p.tipo, p.descontoPadrao, p.corCartao] + ); + } + console.log('Plans seeded.'); + + // 2. Drop the FK on planoId to make the relation soft (plan can be deleted without cascading) + // This prevents the ER_NO_REFERENCED_ROW_2 on INSERT + try { + await c.query('ALTER TABLE procedimentos_valores_planos DROP FOREIGN KEY procedimentos_valores_planos_ibfk_2'); + console.log('FK on planoId dropped — now soft reference.'); + } catch (e) { + console.log('FK drop skipped (may already not exist):', e.message); + } + + const [planos] = await c.query('SELECT id, nome, tipo FROM planos'); + console.log('\n--- PLANOS FINAIS ---'); + console.table(planos); + + await c.end(); +} + +run().catch(console.error); diff --git a/base/iniciarWin.bat b/base/iniciarWin.bat new file mode 100644 index 0000000..d8bc80a --- /dev/null +++ b/base/iniciarWin.bat @@ -0,0 +1,27 @@ +@echo off +TITLE Iniciar ScoreOdonto CRM +echo. +echo ================================================== +echo INICIANDO SISTEMA SCOREODONTO - WINDOWS +echo ================================================== +echo. + +:: 1. Entrar na pasta do projeto +cd /d "%~dp0scoreodonto" + +:: 2. Iniciar a API em uma nova janela +echo [INFO] Iniciando Servidor API (Porta 3002)... +start "" cmd /k "node server.js" + +:: 3. Aguardar alguns segundos para a API estabilizar +timeout /t 3 /nobreak > nul + +:: 4. Iniciar o Frontend (Vite) na janela atual +echo [INFO] Iniciando Frontend (Porta 3000)... +echo. +echo [DICA] O sistema abrira em: http://localhost:3000 +echo [DICA] Para fechar, feche as janelas do terminal. +echo. +npm run dev + +pause diff --git a/base/logs-and-data/docker-compose.yml b/base/logs-and-data/docker-compose.yml new file mode 100644 index 0000000..ac6dab3 --- /dev/null +++ b/base/logs-and-data/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3.8' + +services: + mysql: + image: mysql:8.0 + container_name: cassems-mysql + restart: always + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: cassems_automation + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + + redis: + image: redis:7.0-alpine + container_name: cassems-redis + restart: always + ports: + - "6379:6379" + volumes: + - redis_data:/data + +volumes: + mysql_data: + redis_data: diff --git a/base/logs-and-data/erro-guia-odonto.png b/base/logs-and-data/erro-guia-odonto.png new file mode 100644 index 0000000..3e27c67 Binary files /dev/null and b/base/logs-and-data/erro-guia-odonto.png differ diff --git a/base/logs-and-data/sql/cassems_automation.sql b/base/logs-and-data/sql/cassems_automation.sql new file mode 100644 index 0000000..9ee9464 Binary files /dev/null and b/base/logs-and-data/sql/cassems_automation.sql differ diff --git a/base/logs-and-data/sql/sis_odonto.sql b/base/logs-and-data/sql/sis_odonto.sql new file mode 100644 index 0000000..d984207 Binary files /dev/null and b/base/logs-and-data/sql/sis_odonto.sql differ diff --git a/base/medico/App.tsx b/base/medico/App.tsx new file mode 100644 index 0000000..72b1c49 --- /dev/null +++ b/base/medico/App.tsx @@ -0,0 +1,97 @@ +import React, { useState, useEffect } from 'react'; +import { MedLandingPage } from './LandingPage.tsx'; +import { MedLayout } from './MedLayout.tsx'; +import { MedDashboard } from './MedDashboard.tsx'; +import { MedicoBackend } from './services/medicoBackend.ts'; + +export type MedViewKey = + | 'med-landing' + | 'med-dashboard' + | 'med-pacientes' + | 'med-agenda' + | 'med-prontuarios' + | 'med-fidelidade' + | 'med-financeiro' + | 'medical-repasse'; + +const ROUTE_MAP: Record = { + '/': 'med-landing', + '/landingpage': 'med-landing', + '/dashboard': 'med-dashboard', + '/pacientes': 'med-pacientes', + '/agenda': 'med-agenda', + '/prontuarios': 'med-prontuarios', + '/fidelidade': 'med-fidelidade', + '/financeiro': 'med-financeiro', + '/repasse': 'medical-repasse', +}; + +const VIEW_TO_ROUTE: Record = { + 'med-landing': '/landingpage', + 'med-dashboard': '/dashboard', + 'med-pacientes': '/pacientes', + 'med-agenda': '/agenda', + 'med-prontuarios': '/prontuarios', + 'med-fidelidade': '/fidelidade', + 'med-financeiro': '/financeiro', + 'medical-repasse': '/repasse', +}; + +function getHashPath(): string { + const hash = window.location.hash; + if (hash.startsWith('#/')) return hash.slice(1); + return '/'; +} + +function resolveViewFromUrl(): MedViewKey { + const path = getHashPath(); + return ROUTE_MAP[path] ?? 'med-landing'; +} + +function navigate(view: MedViewKey) { + window.location.hash = VIEW_TO_ROUTE[view]; +} + +const App: React.FC = () => { + const [currentView, setCurrentView] = useState(resolveViewFromUrl); + + useEffect(() => { + const onHashChange = () => { + setCurrentView(resolveViewFromUrl()); + }; + window.addEventListener('hashchange', onHashChange); + return () => window.removeEventListener('hashchange', onHashChange); + }, []); + + const handleNavigate = (view: string) => { + const key = view as MedViewKey; + setCurrentView(key); + navigate(key); + }; + + const renderView = () => { + switch (currentView) { + case 'med-landing': return ; + case 'med-dashboard': return ; + case 'med-pacientes': return
Módulo de Pacientes Médicos
Em Desenvolvimento
; + case 'med-agenda': return
Agenda Médica Premium
Em Desenvolvimento
; + case 'med-prontuarios': return
Sistema de Prontuários
Em Desenvolvimento
; + case 'med-fidelidade': return
Gestão de Fidelidade
Em Desenvolvimento
; + case 'med-financeiro': return
Financeiro & MRR
Em Desenvolvimento
; + case 'medical-repasse': return
Repasse Médico
Em Desenvolvimento
; + default: return ; + } + }; + + if (currentView === 'med-landing') { + return ; + } + + return ( + + {renderView()} + + ); +}; + +export default App; diff --git a/base/medico/LandingPage.tsx b/base/medico/LandingPage.tsx new file mode 100644 index 0000000..1fd803a --- /dev/null +++ b/base/medico/LandingPage.tsx @@ -0,0 +1,207 @@ +import React from 'react'; +import { Shield, Activity, Heart, Check, ArrowRight, Star, Clock, Users, Smartphone, Zap } from 'lucide-react'; + +export const MedLandingPage: React.FC = () => { + return ( +
+ {/* Navigation */} + + + {/* Hero Section */} +
+
+
+
+
+ +
+
+
+ Novo: Fidelidade Multimédica +
+

+ SAÚDE TOTAL,
+ SEM COMPROMISSO. +

+

+ O primeiro plano de fidelidade que integra medicina, odontologia e bem-estar em um único ecossistema. Fidelize sua saúde com tecnologia de ponta. +

+
+ + +
+
+
+
+ + {/* Plans Section */} +
+
+
+

ESTRUTURA DE PLANOS

+

Exclusivo: Combos que não aceitam desmembramento

+
+ +
+ {/* Base Plan */} +
+
01
+
+ LINHA BASE (OBRIGATÓRIO) +

MÉDICO +
ODONTO

+
+
    + {[ + 'Consultas Médicas ilimitadas', + 'Limpeza e Prevenção Dental', + 'Urgências 24h', + 'Rede Credenciada Platinum', + 'Descontos em Medicamentos' + ].map((item, i) => ( +
  • +
    + +
    + {item} +
  • + ))} +
+
+ R$ 149 + /mês individual +
+ +
+ + {/* Premium Plan */} +
+
+
02
+ +
+
+ MAIS POPULAR +
+ LINHA PREMIUM +

MÉDICO + ODONTO
+ PSICOLOGIA

+
+
    + {[ + 'Tudo da Linha Base', + 'Suporte Psicológico 24h', + 'Sessões de Terapia Mensais', + 'Check-up Executivo Anual', + 'Prioridade na Agenda (VIP)' + ].map((item, i) => ( +
  • +
    + +
    + {item} +
  • + ))} +
+
+ R$ 249 + /mês individual +
+ +
+
+
+
+ + {/* Features (CRM) */} +
+
+
+ {[ + { icon: Users, title: 'TITULAR E DEP.', desc: 'Gestão completa familiar com carteirinha individual digital.' }, + { icon: Shield, title: 'ESTABILIDADE', desc: 'Previsibilidade total para o médico e segurança para o paciente.' }, + { icon: Smartphone, title: 'WALLET DIGITAL', desc: 'QR Code e histórico médico sempre no seu bolso.' }, + { icon: Zap, title: 'SEM GLOSA', desc: 'Processamento automático e repasse médico instantâneo.' } + ].map((feat, i) => ( +
+ +

{feat.title}

+

{feat.desc}

+
+ ))} +
+
+
+ + {/* Footer */} +
+
+
+
+ +
+ CRM MÉDICO +
+
+ © 2026 SCOREODONTO HYBRID MEDICAL ENGINE. ALL RIGHTS RESERVED. +
+
+ + +
+
+
+ + +
+ ); +}; diff --git a/base/medico/MedDashboard.tsx b/base/medico/MedDashboard.tsx new file mode 100644 index 0000000..b09e885 --- /dev/null +++ b/base/medico/MedDashboard.tsx @@ -0,0 +1,146 @@ +import React, { useEffect, useState } from 'react'; +import { + Users, Calendar, DollarSign, Activity, + ArrowUpRight, ArrowDownRight, Zap, + TrendingUp, AlertCircle, ArrowRight +} from 'lucide-react'; +import { MedicoBackend } from './services/medicoBackend.ts'; +import { MedicoNotif } from './services/medicoNotif.ts'; +import { MedPaciente, MedAgendamento, MedNotificacao } from './types.ts'; + +export const MedDashboard: React.FC = () => { + const [pacientes, setPacientes] = useState([]); + const [agendamentos, setAgendamentos] = useState([]); + const [notificacoes, setNotificacoes] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const loadData = async () => { + await MedicoNotif.generateIntelligentAlerts(); + const [p, a, n] = await Promise.all([ + MedicoBackend.getPacientes(), + MedicoBackend.getAgendamentos(), + MedicoBackend.getNotificacoes() + ]); + setPacientes(p); + setAgendamentos(a); + setNotificacoes(n.filter(notif => !notif.lida)); + setLoading(false); + }; + loadData(); + }, []); + + const stats = [ + { label: 'Pacientes Ativos', value: pacientes.length.toString(), change: '+12%', positive: true, icon: Users, color: 'cyan' }, + { label: 'Consultas Hoje', value: agendamentos.filter(a => a.start.includes(new Date().toISOString().split('T')[0])).length.toString(), change: '+4', positive: true, icon: Calendar, color: 'blue' }, + { label: 'Receita Est. (MRR)', value: 'R$ 84.200', change: '+8%', positive: true, icon: DollarSign, color: 'emerald' }, + { label: 'Alertas Ativos', value: notificacoes.length.toString(), change: '', positive: true, icon: Activity, color: 'purple' }, + ]; + + if (loading) { + return
Iniciando Motor Médico...
; + } + + return ( +
+ {/* Header */} +
+

Painel Estratégico

+

Visão geral do ecossistema médico e fidelidade

+
+ + {/* Stats Grid */} +
+ {stats.map((stat, i) => { + const Icon = stat.icon; + return ( +
+
+
+ +
+
+ {stat.change && (stat.positive ? : )} + {stat.change} +
+
+
+ {stat.label} +
{stat.value}
+
+
+ ); + })} +
+ + {/* Main Content Grid */} +
+ {/* Upcoming Appointments */} +
+
+

Próximos Atendimentos

+ +
+
+ {agendamentos.length > 0 ? agendamentos.slice(0, 5).map((item, i) => ( +
+
+
+ HRS + {new Date(item.start).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +
+
+
{item.pacienteNome}
+
{item.tipo}
+
+
+
+ + {item.status} + + +
+
+ )) : ( +
Nenhum agendamento para hoje
+ )} +
+
+ + {/* Intelligent Insights */} +
+
+
+
+ +
+

Inteligência
Médica

+
+ {notificacoes.length > 0 ? notificacoes.slice(0, 2).map((notif, i) => ( +
+
+ + + {notif.titulo} + +
+

{notif.mensagem}

+
+ )) : ( +
+

Sem alertas pendentes

+
+ )} +
+ +
+
+
+
+ ); +}; diff --git a/base/medico/MedLayout.tsx b/base/medico/MedLayout.tsx new file mode 100644 index 0000000..295f8a4 --- /dev/null +++ b/base/medico/MedLayout.tsx @@ -0,0 +1,87 @@ +import React, { useState } from 'react'; +import { MedSidebar } from './MedSidebar.tsx'; +import { Bell, Search, Settings, HelpCircle } from 'lucide-react'; + +interface MedLayoutProps { + children: React.ReactNode; + currentView: string; + onNavigate: (view: any) => void; +} + +export const MedLayout: React.FC = ({ children, currentView, onNavigate }) => { + return ( +
+ {/* Sidebar */} + + + {/* Main Content Area */} +
+ {/* Premium Top Bar */} +
+
+
+ + +
+
+ +
+
+ {[ + { icon: Bell, alert: true }, + { icon: HelpCircle, alert: false }, + { icon: Settings, alert: false } + ].map((item, i) => ( + + ))} +
+ +
+ +
+
+ Acesso Premium + Portal Médico Cassems +
+
+
+ MED +
+
+
+
+
+ + {/* Dashboard/View Container */} +
+
+ {children} +
+
+
+ + +
+ ); +}; diff --git a/base/medico/MedSidebar.tsx b/base/medico/MedSidebar.tsx new file mode 100644 index 0000000..8766fd6 --- /dev/null +++ b/base/medico/MedSidebar.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { + LayoutDashboard, Users, Calendar, ClipboardList, + Activity, Shield, DollarSign, LogOut, Settings, + ChevronRight, Zap, Heart, Bell +} from 'lucide-react'; + +interface MedSidebarProps { + activeTab: string; + setActiveTab: (tab: string) => void; +} + +export const MedSidebar: React.FC = ({ activeTab, setActiveTab }) => { + const menuItems = [ + { id: 'med-dashboard', label: 'Painel Geral', icon: LayoutDashboard }, + { id: 'med-pacientes', label: 'Pacientes', icon: Users }, + { id: 'med-agenda', label: 'Agenda Médica', icon: Calendar }, + { id: 'med-prontuarios', label: 'Prontuários', icon: ClipboardList }, + { id: 'med-fidelidade', label: 'Planos Fidelidade', icon: Shield }, + { id: 'med-financeiro', label: 'Financeiro / MRR', icon: DollarSign }, + { id: 'medical-repasse', label: 'Repasse Médico', icon: Zap }, + ]; + + const handleLogout = () => { + if (window.confirm('Deseja realmente sair do portal médico?')) { + window.location.hash = '/medico/landingpage'; + } + }; + + return ( +
+ {/* Logo Section */} +
+
setActiveTab('med-dashboard')}> +
+ +
+
+ CRM MÉDICO + Premium Engine +
+
+
+ + {/* Navigation */} + + + {/* Bottom Section / User Profile */} +
+
+
+ DR +
+
+ Dr. Profissional + Médico Diretor +
+
+ +
+ + +
+
+ + {/* Status Indicator */} +
+
+ Sincronizado +
+
+ ); +}; diff --git a/base/medico/index.css b/base/medico/index.css new file mode 100644 index 0000000..85f5310 --- /dev/null +++ b/base/medico/index.css @@ -0,0 +1,21 @@ +@import "tailwindcss"; + +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + color-scheme: dark; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; + background-color: #020617; +} + +#root { + width: 100%; +} diff --git a/base/medico/index.html b/base/medico/index.html new file mode 100644 index 0000000..c74e913 --- /dev/null +++ b/base/medico/index.html @@ -0,0 +1,15 @@ + + + + + + + CRM Médico | Fidelidade Multimédica + + + +
+ + + + \ No newline at end of file diff --git a/base/medico/index.tsx b/base/medico/index.tsx new file mode 100644 index 0000000..1b3d4fa --- /dev/null +++ b/base/medico/index.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; +import './index.css'; + +const container = document.getElementById('root'); +if (container) { + const root = createRoot(container); + root.render( + + + + ); +} diff --git a/base/medico/package-lock.json b/base/medico/package-lock.json new file mode 100644 index 0000000..22d1bb5 --- /dev/null +++ b/base/medico/package-lock.json @@ -0,0 +1,2543 @@ +{ + "name": "medico-crm", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "medico-crm", + "version": "1.0.0", + "dependencies": { + "lucide-react": "^0.563.0", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.2.0", + "@tailwindcss/vite": "^4.2.0", + "@types/node": "^22.14.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^5.0.0", + "autoprefixer": "^10.4.24", + "postcss": "^8.5.6", + "tailwindcss": "^4.2.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz", + "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "postcss": "^8.5.6", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "tailwindcss": "4.2.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", + "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.563.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", + "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/base/medico/package.json b/base/medico/package.json new file mode 100644 index 0000000..d1b79a2 --- /dev/null +++ b/base/medico/package.json @@ -0,0 +1,29 @@ +{ + "name": "medico-crm", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite --port 3001", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "lucide-react": "^0.563.0", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.2.0", + "@tailwindcss/vite": "^4.2.0", + "@types/node": "^22.14.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^5.0.0", + "autoprefixer": "^10.4.24", + "postcss": "^8.5.6", + "tailwindcss": "^4.2.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} \ No newline at end of file diff --git a/base/medico/services/medicoBackend.ts b/base/medico/services/medicoBackend.ts new file mode 100644 index 0000000..01e8153 --- /dev/null +++ b/base/medico/services/medicoBackend.ts @@ -0,0 +1,154 @@ +import { + MedPaciente, + MedProfissional, + MedAgendamento, + MedProntuario, + MedFinanceiro, + MedNotificacao +} from '../types.ts'; + +const API_BASE_URL = '/api/med'; // New namespace for medical API + +export class MedicoBackend { + // --- STORAGE KEYS --- + private static KEYS = { + PACIENTES: 'med_pacientes', + PROFISSIONAIS: 'med_profissionais', + AGENDAMENTOS: 'med_agendamentos', + PRONTUARIOS: 'med_prontuarios', + FINANCEIRO: 'med_financeiro', + NOTIFICACOES: 'med_notificacoes', + AUTH: 'med_auth_token' + }; + + // --- HELPERS --- + private static async request(path: string, options: RequestInit = {}): Promise { + const token = localStorage.getItem(this.KEYS.AUTH); + const headers = { + 'Content-Type': 'application/json', + ...(token ? { 'Authorization': `Bearer ${token}` } : {}), + ...options.headers, + }; + + try { + const response = await fetch(`${API_BASE_URL}${path}`, { ...options, headers }); + if (!response.ok) throw new Error(`API Error: ${response.statusText}`); + return await response.json(); + } catch (err) { + console.warn(`API unavailable at ${path}, falling back to local storage.`, err); + throw err; // Let the caller decide how to handle fallback + } + } + + // --- PACIENTES --- + static async getPacientes(): Promise { + try { + return await this.request('/pacientes'); + } catch { + const stored = localStorage.getItem(this.KEYS.PACIENTES); + return stored ? JSON.parse(stored) : []; + } + } + + static async savePaciente(paciente: MedPaciente): Promise { + try { + await this.request('/pacientes', { + method: 'POST', + body: JSON.stringify(paciente) + }); + } catch { + const pacientes = await this.getPacientes(); + const index = pacientes.findIndex(p => p.id === paciente.id); + if (index >= 0) pacientes[index] = paciente; + else pacientes.push(paciente); + localStorage.setItem(this.KEYS.PACIENTES, JSON.stringify(pacientes)); + } + } + + // --- AGENDA / PROFISSIONAIS --- + static async getProfissionais(): Promise { + try { + return await this.request('/profissionais'); + } catch { + const stored = localStorage.getItem(this.KEYS.PROFISSIONAIS); + if (stored) return JSON.parse(stored); + // Mock initial data if empty + const mock: MedProfissional[] = [ + { id: '1', nome: 'Dr. Ricardo Santos', crm: '12345-MS', especialidade: 'Cardiologia', email: 'ricardo@med.com', telefone: '67 9999-9999', ativo: true, corAgenda: '#22d3ee' }, + { id: '2', nome: 'Dra. Ana Oliveira', crm: '67890-MS', especialidade: 'Pediatria', email: 'ana@med.com', telefone: '67 8888-8888', ativo: true, corAgenda: '#f43f5e' } + ]; + localStorage.setItem(this.KEYS.PROFISSIONAIS, JSON.stringify(mock)); + return mock; + } + } + + static async getAgendamentos(): Promise { + try { + return await this.request('/agendamentos'); + } catch { + const stored = localStorage.getItem(this.KEYS.AGENDAMENTOS); + return stored ? JSON.parse(stored) : []; + } + } + + static async saveAgendamento(agenda: MedAgendamento): Promise { + try { + await this.request('/agendamentos', { + method: 'POST', + body: JSON.stringify(agenda) + }); + } catch { + const data = await this.getAgendamentos(); + const index = data.findIndex(a => a.id === agenda.id); + if (index >= 0) data[index] = agenda; + else data.push(agenda); + localStorage.setItem(this.KEYS.AGENDAMENTOS, JSON.stringify(data)); + } + } + + // --- FINANCEIRO --- + static async getFinanceiro(): Promise { + try { + return await this.request('/financeiro'); + } catch { + const stored = localStorage.getItem(this.KEYS.FINANCEIRO); + return stored ? JSON.parse(stored) : []; + } + } + + // --- NOTIFICACOES --- + static async getNotificacoes(): Promise { + try { + return await this.request('/notificacoes'); + } catch { + const stored = localStorage.getItem(this.KEYS.NOTIFICACOES); + return stored ? JSON.parse(stored) : []; + } + } + + static async markNotificationRead(id: string): Promise { + try { + await this.request(`/notificacoes/${id}/read`, { method: 'PUT' }); + } catch { + const notifs = await this.getNotificacoes(); + const index = notifs.findIndex(n => n.id === id); + if (index >= 0) { + notifs[index].lida = true; + localStorage.setItem(this.KEYS.NOTIFICACOES, JSON.stringify(notifs)); + } + } + } + + // --- AUTH --- + static isAuthenticated(): boolean { + return !!localStorage.getItem(this.KEYS.AUTH); + } + + static login(token: string) { + localStorage.setItem(this.KEYS.AUTH, token); + } + + static logout() { + localStorage.removeItem(this.KEYS.AUTH); + } +} diff --git a/base/medico/services/medicoNotif.ts b/base/medico/services/medicoNotif.ts new file mode 100644 index 0000000..02a8fdc --- /dev/null +++ b/base/medico/services/medicoNotif.ts @@ -0,0 +1,65 @@ +import { MedNotificacao, MedAgendamento, MedFinanceiro } from '../types.ts'; +import { MedicoBackend } from './medicoBackend.ts'; + +/** + * Intelligent Notification Engine for Medical Module + */ +export class MedicoNotif { + static async generateIntelligentAlerts(): Promise { + const agendamentos = await MedicoBackend.getAgendamentos(); + const financeiro = await MedicoBackend.getFinanceiro(); + const existingNotifs = await MedicoBackend.getNotificacoes(); + + const newNotifs: MedNotificacao[] = []; + const now = new Date(); + + // 1. Check for upcoming appointments (next 24h) + agendamentos.forEach(ag => { + const agDate = new Date(ag.start); + const diffHours = (agDate.getTime() - now.getTime()) / (1000 * 60 * 60); + + if (diffHours > 0 && diffHours < 24 && ag.status === 'Agendado') { + const title = 'Confirmação de Agenda'; + const msg = `Paciente ${ag.pacienteNome} tem agendamento amanhã às ${agDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; + + if (!this.exists(existingNotifs, title, msg)) { + newNotifs.push(this.create(title, msg, 'agenda', 'medium')); + } + } + }); + + // 2. Check for overdue payments + financeiro.forEach(fin => { + const venc = new Date(fin.dataVencimento); + if (fin.status === 'Pendente' && venc < now) { + const title = 'Pagamento em Atraso'; + const msg = `Mensalidade do paciente com ID ${fin.pacienteId} está atrasada desde ${venc.toLocaleDateString()}.`; + + if (!this.exists(existingNotifs, title, msg)) { + newNotifs.push(this.create(title, msg, 'financeiro', 'high')); + } + } + }); + + if (newNotifs.length > 0) { + const allNotifs = [...newNotifs, ...existingNotifs]; + localStorage.setItem('med_notificacoes', JSON.stringify(allNotifs)); + } + } + + private static create(titulo: string, mensagem: string, tipo: MedNotificacao['tipo'], priority: MedNotificacao['priority']): MedNotificacao { + return { + id: Math.random().toString(36).substr(2, 9), + titulo, + mensagem, + tipo, + priority, + data: new Date().toISOString(), + lida: false + }; + } + + private static exists(list: MedNotificacao[], title: string, msg: string): boolean { + return list.some(n => n.titulo === title && n.mensagem === msg); + } +} diff --git a/base/medico/sql/sis_odonto.sql b/base/medico/sql/sis_odonto.sql new file mode 100644 index 0000000..064a7dd Binary files /dev/null and b/base/medico/sql/sis_odonto.sql differ diff --git a/base/medico/tsconfig.json b/base/medico/tsconfig.json new file mode 100644 index 0000000..ffc54af --- /dev/null +++ b/base/medico/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "allowImportingTsExtensions": true + }, + "include": [ + "**/*.ts", + "**/*.tsx" + ] +} \ No newline at end of file diff --git a/base/medico/types.ts b/base/medico/types.ts new file mode 100644 index 0000000..500a48a --- /dev/null +++ b/base/medico/types.ts @@ -0,0 +1,91 @@ +/** + * MEDICAL MODULE TYPES + * Standalone engine for the Medical CRM + Loyalty system + */ + +export interface MedPaciente { + id: string; + nome: string; + cpf: string; + telefone: string; + email: string; + dataNascimento: string; + planoTipo: 'Base' | 'Premium'; // Médico + Odonto vs Médico + Odonto + Psico + statusFidelidade: 'Ativo' | 'Inativo' | 'Inadimplente'; + ultimoAtendimento?: string; +} + +export interface MedProfissional { + id: string; + nome: string; + crm: string; + especialidade: string; + email: string; + telefone: string; + ativo: boolean; + horarios?: string; + corAgenda: string; +} + +export interface MedAgendamento { + id: string; + pacienteId: string; + pacienteNome: string; + profissionalId: string; + start: string; // ISO String + end: string; // ISO String + status: 'Agendado' | 'Confirmado' | 'Em Atendimento' | 'Finalizado' | 'Cancelado' | 'Falta'; + tipo: 'Consulta' | 'Retorno' | 'Exame' | 'Procedimento'; + observacoes?: string; + cor?: string; +} + +export interface MedProntuario { + id: string; + pacienteId: string; + data: string; + profissionalId: string; + anamnese: string; + prescricao?: string; + examesSolicitados?: string[]; + cid?: string[]; + anexos?: string[]; +} + +export interface MedFinanceiro { + id: string; + pacienteId: string; + descricao: string; + valor: number; + dataVencimento: string; + dataPagamento?: string; + status: 'Pendente' | 'Pago' | 'Atrasado'; + metodo: 'Boleto' | 'Pix' | 'Cartão' | 'Dinheiro'; + recorrencia: boolean; // Para o motor de fidelidade +} + +export interface MedFidelidadePlano { + id: string; + nome: string; + valorMensal: number; + inclusoOdonto: boolean; // Sempre true na base + inclusoPsicologia: boolean; // True apenas no premium + limiteConsultasMes: number; + descontoMedicamentos: number; +} + +export interface MedNotificacao { + id: string; + titulo: string; + mensagem: string; + tipo: 'alerta' | 'financeiro' | 'agenda' | 'lead'; + data: string; + lida: boolean; + priority: 'low' | 'medium' | 'high'; +} + +export interface MedConfig { + apiKey?: string; + hospitalName: string; + unidadeId: string; +} diff --git a/base/medico/vite.config.ts b/base/medico/vite.config.ts new file mode 100644 index 0000000..bd67604 --- /dev/null +++ b/base/medico/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [ + react(), + tailwindcss(), + ], + server: { + port: 3001, + } +}); diff --git a/base/package-lock.json b/base/package-lock.json new file mode 100644 index 0000000..710aef0 --- /dev/null +++ b/base/package-lock.json @@ -0,0 +1,595 @@ +{ + "name": "cassems-automation", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cassems-automation", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@playwright/test": "^1.58.2", + "axios": "^1.13.5", + "mysql2": "^3.17.4", + "playwright": "^1.58.2", + "qs": "^6.15.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql2": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.17.4.tgz", + "integrity": "sha512-RnfuK5tyIuaiPMWOCTTl4vQX/mQXqSA8eoIbwvWccadvPGvh+BYWWVecInMS5s7wcLUkze8LqJzwB/+A4uwuAA==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + } + } +} diff --git a/base/package.json b/base/package.json new file mode 100644 index 0000000..2e7c48d --- /dev/null +++ b/base/package.json @@ -0,0 +1,20 @@ +{ + "name": "cassems-automation", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "@playwright/test": "^1.58.2", + "axios": "^1.13.5", + "mysql2": "^3.17.4", + "playwright": "^1.58.2", + "qs": "^6.15.0" + } +} diff --git a/base/playwright-engine/explore-guias.js b/base/playwright-engine/explore-guias.js new file mode 100644 index 0000000..1f35399 --- /dev/null +++ b/base/playwright-engine/explore-guias.js @@ -0,0 +1,91 @@ +const { chromium } = require('playwright'); + +(async () => { + const browser = await chromium.launch({ headless: false }); + const context = await browser.newContext({ + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + }); + + const page = await context.newPage(); + + try { + console.log('Realizando login para exploração...'); + await page.goto('https://viventerisportalcredenciado.topsaudehub.com.br/', { waitUntil: 'networkidle' }); + await page.fill('#usuario', '0005010'); + await page.fill('#senha', 'Cclinic#03'); + await page.click('#login-submit'); + + await page.waitForURL('**/AreaLogada**'); + console.log('Login realizado. Explorando menu...'); + + // Esperar o menu carregar + await page.waitForTimeout(3000); + + // Listar menus principais + const menuNames = ['Tratamento Odontológico', 'Elegibilidade', 'Faturamento', 'Solicitações Diversas']; + + for (const name of menuNames) { + console.log(`Clicando no menu: ${name}`); + try { + await page.click(`text="${name}"`, { timeout: 2000 }); + await page.waitForTimeout(1000); + } catch (e) { + console.log(`Erro ao clicar em ${name}: ${e.message}`); + } + } + + // Tentar clicar em Solicitações Diversas -> Inclusão + console.log('Navegando em Solicitações Diversas -> Inclusão...'); + try { + await page.click('text="Solicitações Diversas"'); + await page.waitForTimeout(500); + await page.click('text="Inclusão"'); + await page.waitForTimeout(2000); + console.log('URL após clicar em Inclusão:', page.url()); + } catch (e) { + console.log('Falha ao seguir caminho Inclusão:', e.message); + } + + // Listar links com palavras-chave + const guiaLinks = await page.evaluate(() => { + const keywords = ['guia', 'sadt', 'consulta', 'externa', 'recebimentos', 'faturamento', 'lote']; + return Array.from(document.querySelectorAll('a')).map(a => ({ + text: a.innerText.trim(), + href: a.getAttribute('href') + })).filter(l => keywords.some(k => l.text.toLowerCase().includes(k)) || keywords.some(k => (l.href || '').toLowerCase().includes(k))); + }); + + console.log('Links relacionados a guias encontrados:', JSON.stringify(guiaLinks, null, 2)); + + // Listar TODOS os links do DOM para ver os caminhos + const allLinks = await page.evaluate(() => { + return Array.from(document.querySelectorAll('a')).map(a => ({ + text: a.innerText.trim().replace(/\n/g, ' '), + href: a.getAttribute('href'), + visible: a.offsetParent !== null + })); + }); + + console.log('--- TODOS OS LINKS ENCONTRADOS (VISÍVEIS E INVISÍVEIS) ---'); + allLinks.forEach(l => { + if (l.text && !l.href.includes('void(0)') && !l.href.startsWith('#')) { + console.log(`[${l.visible ? 'VISÍVEL' : 'OCULTO'}] ${l.text} -> ${l.href}`); + } + }); + + // Tirar um "print" da estrutura do menu principal + const menuStructure = await page.evaluate(() => { + // Tenta encontrar o container principal do menu (geralmente nav ou ul com classes específicas) + const nav = document.querySelector('nav, ul.navbar-nav, .sidebar, #menu'); + return nav ? nav.innerText : 'Container de menu não identificado diretamente'; + }); + console.log('Estrutura de texto do menu:', menuStructure); + + } catch (error) { + console.error('Erro na exploração:', error); + } finally { + console.log('Mantendo aberto para inspeção manual por 60 segundos...'); + await page.waitForTimeout(60000); + await browser.close(); + } +})(); diff --git a/base/playwright-engine/guia-form-analysis.js b/base/playwright-engine/guia-form-analysis.js new file mode 100644 index 0000000..6d1cb26 --- /dev/null +++ b/base/playwright-engine/guia-form-analysis.js @@ -0,0 +1,60 @@ +const { chromium } = require('playwright'); + +(async () => { + const browser = await chromium.launch({ headless: false }); + const context = await browser.newContext({ + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + }); + + const page = await context.newPage(); + + try { + console.log('Realizando login...'); + await page.goto('https://viventerisportalcredenciado.topsaudehub.com.br/', { waitUntil: 'networkidle' }); + await page.fill('#usuario', '0005010'); + await page.fill('#senha', 'Cclinic#03'); + await page.click('#login-submit'); + + await page.waitForURL('**/AreaLogada**'); + console.log('Login OK. Navegando para Novo Registro (Guia)...'); + + // Tentar ir direto para a URL de novo registro + const baseUrl = 'https://viventerisportalcredenciado.topsaudehub.com.br'; + await page.goto(baseUrl + '/HomePortalCredenciado/WorkFlow/NovoRegistro', { waitUntil: 'networkidle' }); + + console.log('Página de Novo Registro carregada. Analisando campos...'); + await page.waitForTimeout(3000); // Esperar carregar formulário dinâmico + + // Listar todos os inputs, selects e botões para entender o formulário de guias + const formFields = await page.evaluate(() => { + const inputs = Array.from(document.querySelectorAll('input, select, textarea, button')); + return inputs.map(i => ({ + tagName: i.tagName, + type: i.type, + id: i.id, + name: i.name, + placeholder: i.placeholder, + labelText: i.labels?.[0]?.innerText || i.parentElement?.innerText?.split('\n')[0] || '' + })).filter(i => i.id || i.name); + }); + + console.log('CAMPOS DO FORMULÁRIO ENCONTRADOS:', JSON.stringify(formFields, null, 2)); + + // Verificar se há algum seletor de "Tipo de Guia" + const tiposGuia = await page.evaluate(() => { + const select = document.querySelector('select'); + if (select) { + return Array.from(select.options).map(o => o.text); + } + return 'Nenhum select encontrado'; + }); + console.log('Opções de Guia/Select:', tiposGuia); + + } catch (error) { + console.error('Erro:', error); + } finally { + console.log('Mantendo aberto para sua visualização por 60 segundos...'); + await page.waitForTimeout(60000); + await browser.close(); + } +})(); diff --git a/base/playwright-engine/lancar-guia-odonto.js b/base/playwright-engine/lancar-guia-odonto.js new file mode 100644 index 0000000..2e9e10d --- /dev/null +++ b/base/playwright-engine/lancar-guia-odonto.js @@ -0,0 +1,221 @@ +const { chromium } = require('playwright'); +const fs = require('fs'); + +(async () => { + const browser = await chromium.launch({ headless: false }); + const context = await browser.newContext({ + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + }); + + const page = await context.newPage(); + + try { + console.log('🚀 Iniciando Automação de Guia Odontológica...'); + + // LOGIN + await page.goto('https://viventerisportalcredenciado.topsaudehub.com.br/', { waitUntil: 'networkidle' }); + await page.fill('#usuario', '0005010'); + await page.fill('#senha', 'Cclinic#03'); + await page.click('#login-submit'); + await page.waitForURL('**/AreaLogada**'); + + // 1. Tratamento Odontológico + console.log('1. Acessando Tratamento Odontológico...'); + await page.click('text="Tratamento Odontológico"'); + await page.waitForTimeout(2000); + + // 2. Lupa de pesquisa + console.log('2. Clicando na pesquisa (lupa)...'); + // Tentativa de achar o botão de busca que abre o modal de beneficiário + const searchBtn = page.locator('.fa-search, .glyphicon-search, button[title*="Pesquisar"]').first(); + await searchBtn.click(); + await page.waitForTimeout(1000); + + // 3. CPF do Beneficiário + console.log('3. Inserindo CPF: 005.983.051-49...'); + // Esperar o campo ficar visível no modal + const cpfInput = page.locator('input[id*="cpf"], input[name*="cpf"], .cpf-mask').first(); + await cpfInput.fill('005.983.051-49'); + + // 4. Pesquisar + console.log('4. Clicando em Pesquisar...'); + await page.click('button:has-text("Pesquisar"), #btnPesquisarBeneficiario'); + await page.waitForTimeout(2000); + + // 5. Clica na coluna número beneficiário + console.log('5. Selecionando beneficiário resultado...'); + // Espera especificamente por um link que pareça um número de cartão ou nome + const linkBeneficiario = page.locator('table tbody tr td a').first(); + await linkBeneficiario.waitFor({ state: 'visible', timeout: 5000 }); + await linkBeneficiario.click(); + await page.waitForTimeout(2000); // Espera carregar os dados dele + + // 6. Botão + + console.log('6. Clicando em Adicionar (+)...'); + // O botão + muitas vezes só aparece após selecionar o beneficiário + const btnMais = page.locator('.fa-plus, .btn-success, #btnNovoRegistro, a:has-text("Novo"), button:has-text("+")').first(); + await btnMais.waitFor({ state: 'visible', timeout: 5000 }); + await btnMais.click(); + await page.waitForTimeout(2000); + + // 7. Dropdown tipo atendimento + console.log('7. Selecionando Tipo de Atendimento...'); + await page.waitForTimeout(2000); + + // Verifica se existe um select de endereço primeiro + const enderecoSelect = page.locator('select[id*="endereco"], select[name*="endereco"]').first(); + if (await enderecoSelect.count() > 0) { + console.log('Selecionando endereço de atendimento...'); + await enderecoSelect.selectOption({ index: 1 }); + await page.waitForTimeout(1000); + } + + // Tenta encontrar o select de tipo de atendimento especificamente + const selectTipo = page.locator('select[id*="tipo_solicitacao"], select[id*="tipoAtendimento"], select:has-text("Tratamento")').first(); + await selectTipo.waitFor({ state: 'visible', timeout: 5000 }).catch(() => console.log('Aviso: Select de tipo não visível ainda.')); + + const options = await selectTipo.evaluate(sel => Array.from(sel.options).map(o => o.text)).catch(() => []); + console.log('Opções no select de tipo:', options); + + try { + await selectTipo.selectOption({ label: /Tratamento Odontológico/i }); + } catch (e) { + console.log('Tentando selecionar pelo índice 1...'); + await selectTipo.selectOption({ index: 1 }); + } + + // 8 e 9. Lupa Profissional Executante + console.log('8. Selecionando Profissional Executante...'); + await page.waitForTimeout(2000); + + // Agora temos o ID exato do botão! + const btnBuscaProf = page.locator('#btnBuscaPrestadorExecutante').first(); + + await btnBuscaProf.waitFor({ state: 'visible', timeout: 5000 }); + await btnBuscaProf.click(); + await page.waitForTimeout(2000); + + // Clica no profissional na tabela + console.log('Selecionando o primeiro profissional da lista...'); + const linkProf = page.locator('table tbody tr td a').first(); + if (await linkProf.count() > 0) { + await linkProf.click(); + } else { + console.log('⚠️ Nenhum profissional encontrado na tabela. Aguardando instrução.'); + return; // PARA e aguarda + } + await page.waitForTimeout(1000); + + // 10. Profissional Solicitante + console.log('10. Preenchendo Solicitante: Murilo...'); + const solicitanteInput = page.locator('input[id*="solicitante"], input[name*="Solicitante"]').first(); + if (await solicitanteInput.count() === 0) { + console.log('⚠️ Campo "Profissional Solicitante" não encontrado. Aguardando instrução.'); + return; + } + await solicitanteInput.waitFor({ state: 'visible', timeout: 5000 }); + await solicitanteInput.clear(); + await solicitanteInput.fill('Murilo'); + + // 11. Justificativa (APENAS se for urgência - Valor 4) + const tipoAtendimentoValue = await page.locator('#tipoAtendimento').inputValue(); + if (tipoAtendimentoValue === '4') { + console.log('Detectado Urgência / Emergência. Preenchendo Justificativa...'); + await page.fill('#observacaoJustificativa', 'Paciente com dor aguda, necessidade de atendimento de urgência.'); + } else { + console.log('Tipo de atendimento normal. Pulando justificativa.'); + } + + // 12. Próximo + console.log('12. Clicando em Próximo...'); + await page.click('button:has-text("Próximo"), #btnProximo'); + await page.waitForTimeout(2000); + + // 13. Incluir + console.log('13. Clicando em Incluir...'); + await page.click('#adicionarProcedimeto'); + await page.waitForTimeout(2000); + + // EXTRAÇÃO DE CÓDIGOS (Como solicitado pelo usuário na primeira vez) + console.log('🔍 Extraindo lista de procedimentos disponíveis...'); + const procedimentos = await page.evaluate(() => { + const items = Array.from(document.querySelectorAll('input[id*="procedimento"], select[id*="procedimento"], .lista-procedimentos td')); + // Tenta pegar de um select se houver + const sel = document.querySelector('select[id*="procedimento"]'); + if (sel) return Array.from(sel.options).map(o => `${o.value} - ${o.text}`); + return items.map(i => i.innerText || i.value).filter(t => t && t.length > 5); + }); + + if (procedimentos.length > 0) { + fs.writeFileSync('procedimentos_encontrados.txt', procedimentos.join('\n')); + console.log('✅ Lista de procedimentos salva em procedimentos_encontrados.txt'); + } + + // 15, 16 e 17. Dados do Tratamento + console.log('15. Lançando procedimento 85100196...'); + const procInput = page.locator('#codProcedimento').first(); + await procInput.waitFor({ state: 'visible', timeout: 10000 }); + await procInput.fill('85100196'); + + // Espera e clica no item do autocomplete + console.log('Selecionando item no autocomplete...'); + const autocompleteItem = page.locator('.ui-menu-item-wrapper:has-text("85100196")').first(); + await autocompleteItem.waitFor({ state: 'visible', timeout: 5000 }); + await autocompleteItem.click(); + await page.waitForTimeout(2000); + + console.log('16. Selecionando Dente 18...'); + const selectDente = page.locator('#cmbDente').first(); + await selectDente.waitFor({ state: 'visible', timeout: 5000 }); + await selectDente.selectOption('18'); + await page.waitForTimeout(1000); + + // Selecionar a checkbox da Face "V" + console.log('17. Selecionando Face V...'); + const checkV = page.locator('#codFaceV').first(); + await checkV.waitFor({ state: 'visible', timeout: 5000 }); + + // Tenta clicar forçado no input ou clica na label (que é o que o usuário vê) + try { + await checkV.check({ force: true }); + } catch (e) { + await page.click('label[for="codFaceV"]'); + } + + // 18. Incluir procedimento (Botão Incluir dentro do modal) + console.log('18. Incluindo procedimento na lista...'); + await page.click('#btnIncluirProcedimento'); + await page.waitForTimeout(2000); + + // Se houver um botão de fechar/salvar o popup geral (como o btnSalvarPopUP que vimos antes) + const btnFechar = page.locator('#btnSalvarPopUP').first(); + if (await btnFechar.isVisible()) { + console.log('Fechando popup de procedimentos...'); + await btnFechar.click(); + await page.waitForTimeout(1000); + } + + // 19. Finalizar + console.log('19. Finalizando guia...'); + await page.click('button:has-text("Próximo")'); + await page.waitForTimeout(1000); + await page.click('button:has-text("Próximo"), #btnFinalizar'); + + // 20. Captura de GTO + console.log('20. Capturando resultado da GTO...'); + await page.waitForTimeout(3000); + const resultadoHTML = await page.content(); + const gtoMatch = resultadoHTML.match(/\d{8,12}/); // Tenta achar um número longo que seria a GTO + const status = resultadoHTML.includes('Autorizado') ? 'AUTORIZADO' : 'VERIFICAR STATUS'; + + console.log(`--- RESULTADO --- GTO: ${gtoMatch ? gtoMatch[0] : 'Não localizada'} | STATUS: ${status}`); + + } catch (error) { + console.error('❌ Erro na automação:', error.message); + await page.screenshot({ path: 'erro-guia-odonto.png' }); + } finally { + console.log('Processo interrompido para análise. O navegador ficará aberto por 60s.'); + await page.waitForTimeout(60000); + await browser.close(); + } +})(); diff --git a/base/playwright-engine/login-test.js b/base/playwright-engine/login-test.js new file mode 100644 index 0000000..bb9c4ba --- /dev/null +++ b/base/playwright-engine/login-test.js @@ -0,0 +1,52 @@ +const { chromium } = require('playwright'); + +(async () => { + // Configuração para evitar mensagem de navegador incompatível + const browser = await chromium.launch({ + headless: false // Vamos deixar visível para você ver acontecendo + }); + + const context = await browser.newContext({ + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + }); + + const page = await context.newPage(); + + try { + console.log('Navegando para o portal...'); + await page.goto('https://viventerisportalcredenciado.topsaudehub.com.br/', { waitUntil: 'networkidle' }); + + console.log('Preenchendo credenciais...'); + await page.fill('#usuario', '0005010'); + await page.fill('#senha', 'Cclinic#03'); + + console.log('Clicando em ENTRAR...'); + await page.click('#login-submit'); + + // Espera um pouco para ver o resultado (sucesso ou erro) + await page.waitForTimeout(5000); + + const currentUrl = page.url(); + console.log('URL atual após login:', currentUrl); + + if (currentUrl.includes('AreaLogada')) { + console.log('✅ LOGIN REALIZADO COM SUCESSO!'); + } else { + // Tenta verificar erro apenas se não estiver na área logada + const errorMsg = await page.locator('.msg:visible').textContent({ timeout: 5000 }).catch(() => null); + if (errorMsg) { + console.log('❌ Falha no login. Mensagem do sistema:', errorMsg.trim()); + } else { + console.log('Status do login incerto. Verifique a janela do navegador.'); + } + } + + } catch (error) { + console.error('Ocorreu um erro durante a automação:', error); + } finally { + // Mantemos o navegador aberto por 20 segundos para você ver o resultado + console.log('Mantendo o navegador aberto por 20 segundos...'); + await page.waitForTimeout(20000); + await browser.close(); + } +})(); diff --git a/base/playwright-engine/map-routes.js b/base/playwright-engine/map-routes.js new file mode 100644 index 0000000..a67c4c8 --- /dev/null +++ b/base/playwright-engine/map-routes.js @@ -0,0 +1,45 @@ +const { chromium } = require('playwright'); + +(async () => { + const browser = await chromium.launch({ headless: false }); + const context = await browser.newContext({ + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + }); + + const page = await context.newPage(); + + try { + console.log('Realizando login...'); + await page.goto('https://viventerisportalcredenciado.topsaudehub.com.br/', { waitUntil: 'networkidle' }); + await page.fill('#usuario', '0005010'); + await page.fill('#senha', 'Cclinic#03'); + await page.click('#login-submit'); + + await page.waitForURL('**/AreaLogada**'); + console.log('Login OK. Varrendo todos os links e data-href...'); + + await page.waitForTimeout(2000); + + const links = await page.evaluate(() => { + return Array.from(document.querySelectorAll('a')).map(a => ({ + text: a.innerText.trim(), + href: a.getAttribute('href'), + dataHref: a.getAttribute('data-href'), + id: a.id + })); + }); + + console.log('LISTA COMPLETA DE LINKS:'); + links.forEach(l => { + if (l.text || l.dataHref) { + console.log(`- TEXTO: "${l.text}" | HREF: "${l.href}" | DATA-HREF: "${l.dataHref}" | ID: "${l.id}"`); + } + }); + + } catch (error) { + console.error('Erro:', error); + } finally { + console.log('Fim da exploração.'); + await browser.close(); + } +})(); diff --git a/base/playwright-engine/mapear-api-axios.js b/base/playwright-engine/mapear-api-axios.js new file mode 100644 index 0000000..f25d677 --- /dev/null +++ b/base/playwright-engine/mapear-api-axios.js @@ -0,0 +1,138 @@ +const { chromium } = require('playwright'); + +(async () => { + console.log('🔍 Iniciando mapeamento de API para Axios...'); + const browser = await chromium.launch({ headless: false }); // Usar Chrome visível para evitar bloqueio + const context = await browser.newContext({ + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36' + }); + const page = await context.newPage(); + + // Interceptar todas as requisições para descobrir os Endpoints + page.on('request', request => { + if (request.method() === 'POST' && !request.url().includes('google-analytics')) { + console.log(`\n🚀 POST Detectado: ${request.url()}`); + console.log('Headers:', JSON.stringify(request.headers(), null, 2)); + console.log('Payload:', request.postData()); + } + }); + + try { + await page.goto('https://viventerisportalcredenciado.topsaudehub.com.br/', { waitUntil: 'load', timeout: 60000 }); + + console.log('Esperando formulário de login...'); + const userField = page.locator('#usuario, #Username').first(); + await userField.waitFor({ timeout: 30000 }); + + console.log('Preenchendo credenciais...'); + await userField.fill('0005010'); + const passField = page.locator('#senha, #Password').first(); + await passField.fill('Cclinic#03'); + + console.log('Clicando em Login...'); + const loginBtn = page.locator('#login-submit, #btnLogin').first(); + await loginBtn.click(); + + await page.waitForTimeout(5000); + + console.log('Navegando pelo menu...'); + await page.click('text="Tratamento Odontológico"'); + await page.waitForTimeout(2000); + + console.log('Abrindo busca de beneficiário...'); + const searchBtn = page.locator('.fa-search, .glyphicon-search, button[title*="Pesquisar"]').first(); + await searchBtn.click(); + await page.waitForTimeout(2000); + + console.log('Inserindo CPF...'); + const cpfInput = page.locator('input[id*="cpf"], input[name*="cpf"], .cpf-mask').first(); + await cpfInput.fill('005.983.051-49'); + await page.click('button:has-text("Pesquisar"), #btnPesquisarBeneficiario'); + + console.log('Aguardando resultados da busca...'); + const linkBeneficiario = page.locator('table tbody tr td a').first(); + try { + await linkBeneficiario.waitFor({ state: 'visible', timeout: 15000 }); + console.log('Selecionando beneficiário...'); + await linkBeneficiario.click(); + } catch (e) { + console.log('⚠️ Resultado da busca não apareceu ou demorou demais.'); + const content = await page.content(); + if (content.includes('nenhum registro encontrado') || content.includes('não encontrado')) { + console.log('❌ Beneficiário não encontrado no portal.'); + } else { + console.log('Logando HTML para análise de erro...'); + require('fs').writeFileSync('logs-and-data/debug-search-error.html', content); + } + throw new Error('Falha na busca de beneficiário durante o mapeamento.'); + } + await page.waitForTimeout(3000); + + console.log('Clicando em Adicionar (+)...'); + const btnMais = page.locator('.fa-plus, .btn-success, #btnNovoRegistro, button:has-text("+")').first(); + await btnMais.click(); + await page.waitForTimeout(5000); + + console.log('Preenchendo dados da guia...'); + const selectTipo = page.locator('select[id*="tipoAtendimento"]').first(); + await selectTipo.waitFor({ state: 'visible', timeout: 15000 }); + await selectTipo.selectOption('1'); + await page.waitForTimeout(2000); + + console.log('Buscando Profissional Executante...'); + const btnBuscaProf = page.locator('#btnBuscaPrestadorExecutante').first(); + await btnBuscaProf.click(); + await page.waitForTimeout(3000); + + console.log('Selecionando primeiro profissional na tabela...'); + const linkProf = page.locator('table tbody tr td a').first(); + await linkProf.waitFor({ state: 'visible', timeout: 10000 }); + await linkProf.click(); + await page.waitForTimeout(3000); + + console.log('Verificando campo Solicitante...'); + const solicitanteInput = page.locator('input[id*="solicitante"], input[name*="Solicitante"]').first(); + + // Tenta esperar o campo, mas loga se falhar + try { + await solicitanteInput.waitFor({ state: 'visible', timeout: 15000 }); + console.log('Preenchendo Solicitante...'); + await solicitanteInput.fill('Murilo'); + } catch (e) { + console.log('⚠️ Campo Solicitante não apareceu. Continuando para tentar capturar outros POSTs...'); + } + + console.log('Avançando para procedimentos...'); + const btnProx = page.locator('button:has-text("Próximo"), #btnProximo').first(); + await btnProx.click(); + await page.waitForTimeout(5000); + + console.log('Abrindo inclusão de procedimento...'); + await page.click('#adicionarProcedimeto'); + await page.waitForTimeout(3000); + + console.log('Lançando procedimento para capturar JSON...'); + await page.fill('#codProcedimento', '85100196'); + await page.waitForTimeout(2000); + await page.click('.ui-menu-item-wrapper:has-text("85100196")'); + await page.waitForTimeout(1000); + await page.selectOption('#cmbDente', '11'); + await page.click('label[for="codFaceV"]'); + + console.log('Incluindo no modal (Captura JSON parcial)...'); + await page.click('#btnIncluirProcedimento'); + await page.waitForTimeout(3000); + + console.log('FINALIZANDO GUIA (Captura do POST Final)...'); + await page.click('button:has-text("Próximo")'); + await page.waitForTimeout(2000); + await page.click('button:has-text("Próximo"), #btnFinalizar'); + + await page.waitForTimeout(10000); + console.log('\n✅ Mapeamento concluído com sucesso.'); + } catch (error) { + console.error('Erro no mapeamento:', error); + } finally { + await browser.close(); + } +})(); diff --git a/base/playwright-engine/sql/cassems_automation.sql b/base/playwright-engine/sql/cassems_automation.sql new file mode 100644 index 0000000..a9be826 Binary files /dev/null and b/base/playwright-engine/sql/cassems_automation.sql differ diff --git a/base/scoreodonto/.env.example b/base/scoreodonto/.env.example new file mode 100644 index 0000000..593b077 --- /dev/null +++ b/base/scoreodonto/.env.example @@ -0,0 +1,9 @@ +# BACKEND CONFIG +PORT=3002 +DB_HOST=localhost +DB_USER=root +DB_PASSWORD= +DB_NAME=sis_odonto + +# FRONTEND CONFIG (For local dev, usually not needed if using proxy/relative) +# VITE_API_URL=http://localhost:3002/api diff --git a/base/scoreodonto/.gitignore b/base/scoreodonto/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/base/scoreodonto/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/base/scoreodonto/.htaccess b/base/scoreodonto/.htaccess new file mode 100644 index 0000000..e3fcacb --- /dev/null +++ b/base/scoreodonto/.htaccess @@ -0,0 +1,22 @@ + + # Força o servidor a enviar arquivos TSX/TS como texto javascript + # Isso corrige o erro "MIME type of application/octet-stream" + AddType text/javascript .js + AddType text/javascript .jsx + AddType text/javascript .ts + AddType text/javascript .tsx + AddType text/css .css + + + + RewriteEngine On + RewriteBase /odonto/ + + # Se o arquivo ou diretório existir, serve ele normalmente + RewriteCond %{REQUEST_FILENAME} -f [OR] + RewriteCond %{REQUEST_FILENAME} -d + RewriteRule ^ - [L] + + # Caso contrário, redireciona tudo para index.html (Fallback para SPA) + RewriteRule ^ index.html [L] + \ No newline at end of file diff --git a/base/scoreodonto/App.tsx b/base/scoreodonto/App.tsx new file mode 100644 index 0000000..725da90 --- /dev/null +++ b/base/scoreodonto/App.tsx @@ -0,0 +1,319 @@ +import React, { useState, useEffect } from 'react'; +import { HybridBackend } from './services/backend.ts'; + +import { Sidebar } from './components/Sidebar.tsx'; +import { Menu } from 'lucide-react'; + +import { ToastContainer } from './components/Toast.tsx'; + +import { Dashboard } from './views/Dashboard.tsx'; +import { LeadsView } from './views/LeadsView.tsx'; +import { PublicContactForm } from './views/PublicContactForm.tsx'; +import { PatientsView } from './views/PatientsView.tsx'; +import { AgendaView } from './views/AgendaView.tsx'; +import { OrthoView } from './views/OrthoView.tsx'; +import { FinanceiroView } from './views/FinanceiroView.tsx'; +import { MeusTratamentos } from './views/MeusTratamentos'; +import { LancarGTO } from './views/LancarGTO'; +import { LoginView } from './views/LoginView.tsx'; +import { LandingPage } from './views/LandingPage.tsx'; +import { UpdateDbView } from './views/UpdateDbView.tsx'; +import { ClinicasView } from './views/ClinicasView.tsx'; +import { DentistRegisterView } from './views/DentistRegisterView.tsx'; +import { DentistasView, EspecialidadesView, PlanosView, SyncView, ProcedimentosView } from './views/AdminViews.tsx'; +import { NotificationsView } from './views/NotificationsView.tsx'; +import { ReportsView } from './views/ReportsView.tsx'; + +export type ViewKey = + | 'landing' + | 'dashboard' + | 'leads' + | 'public' + | 'pacientes' + | 'agenda' + | 'ortodontia' + | 'financeiro' + | 'login' + | 'update' + | 'dentistas' + | 'especialidades' + | 'procedimentos' + | 'planos' + | 'tratamentos' + | 'lancar-gto' + | 'notificacoes' + | 'clinicas' + | 'sync' + | 'relatorios' + | 'cadastro-dentista'; + +// ------- URL <-> VIEW mapping ------- +const ROUTE_MAP: Record = { + '/': 'landing', + '/dashboard': 'dashboard', + '/landing': 'landing', + '/leads': 'leads', + '/contato': 'public', + '/pacientes': 'pacientes', + '/agenda': 'agenda', + '/ortodontia': 'ortodontia', + '/financeiro': 'financeiro', + '/login': 'login', + '/update': 'update', + '/clinicas': 'clinicas', + '/admin/dentistas': 'dentistas', + '/admin/especialidades': 'especialidades', + '/admin/procedimentos': 'procedimentos', + '/admin/planos': 'planos', + '/admin/sync': 'sync', + '/tratamentos': 'tratamentos', + '/lancar-gto': 'lancar-gto', + '/notificacoes': 'notificacoes', + '/relatorios': 'relatorios', + '/cadastro-dentista': 'cadastro-dentista', +}; + +const VIEW_TO_ROUTE: Record = { + landing: '/', + dashboard: '/dashboard', + leads: '/leads', + public: '/contato', + pacientes: '/pacientes', + agenda: '/agenda', + ortodontia: '/ortodontia', + financeiro: '/financeiro', + login: '/login', + update: '/update', + clinicas: '/clinicas', + dentistas: '/admin/dentistas', + especialidades: '/admin/especialidades', + procedimentos: '/admin/procedimentos', + planos: '/admin/planos', + tratamentos: '/tratamentos', + 'lancar-gto': '/lancar-gto', + notificacoes: '/notificacoes', + relatorios: '/relatorios', + sync: '/admin/sync', + 'cadastro-dentista': '/cadastro-dentista' +}; + +/** Get the hash path, e.g. "/#/pacientes" → "/pacientes" */ +function getHashPath(): string { + const hash = window.location.hash; + if (hash.startsWith('#/')) return hash.slice(1); // "#/pacientes" → "/pacientes" + if (hash === '#' || hash === '') return '/'; + return '/'; +} + +/** Resolve current URL to a ViewKey */ +function resolveViewFromUrl(): ViewKey { + // Legacy support: ?p=contato + if (window.location.search.includes('p=contato')) return 'public'; + // Legacy support: #update_db (old non-slash hash) + if (window.location.hash === '#update_db') return 'update'; + + const path = getHashPath(); + return ROUTE_MAP[path] ?? 'dashboard'; +} + +/** Push a new hash-based URL without reloading the page */ +function navigate(view: ViewKey) { + const route = VIEW_TO_ROUTE[view]; + window.location.hash = route; +} + +// ------- App ------- +const App: React.FC = () => { + const [isSidebarOpen, setSidebarOpen] = useState(false); + const currentRole = HybridBackend.getCurrentRole(); + const activeWorkspace = HybridBackend.getActiveWorkspace(); + const clinColor = activeWorkspace?.cor || '#2563eb'; + + const isViewAllowed = (view: ViewKey, role: string): boolean => { + if (['landing', 'login', 'public', 'update'].includes(view)) return true; + if (role === 'admin' || role === 'donoclinica') return true; + + const permissions: Record = { + paciente: ['tratamentos', 'notificacoes'], + dentista: ['dashboard', 'pacientes', 'agenda', 'ortodontia', 'tratamentos', 'notificacoes', 'clinicas'], + funcionario: ['dashboard', 'leads', 'pacientes', 'agenda', 'financeiro', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas'], + }; + + return permissions[role]?.includes(view) || false; + }; + + const getDefaultViewForRole = (role: string): ViewKey => { + if (role === 'paciente') return 'tratamentos'; + return 'dashboard'; + }; + + const getInitialView = (): ViewKey => { + const fromUrl = resolveViewFromUrl(); + const role = HybridBackend.getCurrentRole(); + + // If user is authenticated, and trying to go to landing or login, send to default for role + if (HybridBackend.isAuthenticated() && ['landing', 'login'].includes(fromUrl)) return getDefaultViewForRole(role); + + // If URL points to a protected view but user is not auth'd → landing (or login) + const isProtected = !['landing', 'login', 'public'].includes(fromUrl); + if (isProtected && !HybridBackend.isAuthenticated()) return 'landing'; + + // Verify permission for the view + if (HybridBackend.isAuthenticated() && !isViewAllowed(fromUrl, role)) { + return getDefaultViewForRole(role); + } + + return fromUrl; + }; + + const [currentView, setCurrentView] = useState(getInitialView); + + // Keep URL in sync when view changes programmatically via sidebar + const handleNavigate = (view: ViewKey) => { + if (isViewAllowed(view, currentRole)) { + setCurrentView(view); + navigate(view); + setSidebarOpen(false); // Close sidebar on mobile after navigation + } + }; + + // Sync view when user uses browser back/forward buttons + useEffect(() => { + const onHashChange = () => { + const fromUrl = resolveViewFromUrl(); + const role = HybridBackend.getCurrentRole(); + const isProtected = !['landing', 'login', 'public'].includes(fromUrl); + + if (isProtected && !HybridBackend.isAuthenticated()) { + setCurrentView('landing'); + navigate('landing'); + return; + } + + if (HybridBackend.isAuthenticated() && !isViewAllowed(fromUrl, role)) { + const defView = getDefaultViewForRole(role); + setCurrentView(defView); + navigate(defView); + return; + } + + setCurrentView(fromUrl); + }; + window.addEventListener('hashchange', onHashChange); + return () => window.removeEventListener('hashchange', onHashChange); + }, []); + + // Inject dynamic brand color + useEffect(() => { + document.documentElement.style.setProperty('--brand-color', clinColor); + document.documentElement.style.setProperty('--brand-color-soft', `${clinColor}11`); + }, [clinColor]); + + // On mount: if there's no hash yet, write the current view into the URL + useEffect(() => { + if (!window.location.hash || window.location.hash === '#update_db') { + navigate(currentView); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const renderView = () => { + switch (currentView) { + case 'landing': return handleNavigate('login')} />; + case 'dashboard': return ; + case 'leads': return ; + case 'public': return ; + case 'pacientes': return ; + case 'agenda': return ; + case 'ortodontia': return ; + case 'financeiro': return ; + case 'dentistas': return ; + case 'especialidades': return ; + case 'procedimentos': return ; + case 'planos': return ; + case 'sync': return ; + case 'tratamentos': return ; + case 'lancar-gto': return ; + case 'update': return ; + case 'clinicas': return ; + case 'notificacoes': return ; + case 'relatorios': return ; + case 'cadastro-dentista': return ; + case 'login': + default: + return { + const role = HybridBackend.getCurrentRole(); + handleNavigate(getDefaultViewForRole(role)); + }} />; + } + }; + + const isStandaloneView = ['landing', 'login', 'public', 'update', 'cadastro-dentista'].includes(currentView); + + return ( + <> +
+ + + {!isStandaloneView && ( + <> + {/* Mobile Hamburger Overlay */} + {isSidebarOpen && ( +
setSidebarOpen(false)} + >
+ )} + + {/* Sidebar with mobile toggle logic */} +
+ handleNavigate(t as ViewKey)} + /> +
+ + )} + +
+ {!isStandaloneView && ( + <> + {/* Mobile Navbar Header */} +
+ +
+
+ S +
+ SCOREODONTO +
+
+
+ + )} + +
+
+ {renderView()} +
+
+
+
+ + + ); +}; + +export default App; \ No newline at end of file diff --git a/base/scoreodonto/README.md b/base/scoreodonto/README.md new file mode 100644 index 0000000..bfb3eaf --- /dev/null +++ b/base/scoreodonto/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/1R1UvfhQooJcPo447sPMR5YdggjdMblPP + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/base/scoreodonto/check_db.js b/base/scoreodonto/check_db.js new file mode 100644 index 0000000..4e744ba --- /dev/null +++ b/base/scoreodonto/check_db.js @@ -0,0 +1,42 @@ +import mysql from 'mysql2/promise'; + +async function checkData() { + const dbConfig = { + host: 'localhost', + user: 'root', + password: '', + database: 'sis_odonto' + }; + + try { + const connection = await mysql.createConnection(dbConfig); + console.log('Connected to MySQL'); + + const [dentists] = await connection.query('SELECT * FROM dentistas'); + console.log('\n--- DENTISTAS ---'); + console.table(dentists.map(d => ({ id: d.id, nome: d.nome }))); + + const [agendamentos] = await connection.query('SELECT * FROM agendamentos'); + console.log('\n--- AGENDAMENTOS (Counts per Dentist) ---'); + const counts = {}; + agendamentos.forEach(a => { + counts[a.dentistaId] = (counts[a.dentistaId] || 0) + 1; + }); + + const countsTable = Object.entries(counts).map(([id, count]) => { + const d = dentists.find(dentist => dentist.id === id); + return { + dentistId: id, + nome: d ? d.nome : 'UNKNOWN (Maybe deleted or not assigned)', + count: count + }; + }); + console.table(countsTable); + + await connection.end(); + } catch (err) { + console.error('Error:', err.message); + } +} + +checkData(); diff --git a/base/scoreodonto/check_db_details.js b/base/scoreodonto/check_db_details.js new file mode 100644 index 0000000..1010997 --- /dev/null +++ b/base/scoreodonto/check_db_details.js @@ -0,0 +1,25 @@ +import mysql from 'mysql2/promise'; + +async function checkData() { + const dbConfig = { host: 'localhost', user: 'root', password: '', database: 'sis_odonto' }; + try { + const connection = await mysql.createConnection(dbConfig); + const [dentists] = await connection.query('SELECT * FROM dentistas'); + const [agendamentos] = await connection.query('SELECT * FROM agendamentos'); + console.log('\n--- DENTISTAS ---'); + console.table(dentists.map(d => ({ id: d.id, nome: d.nome }))); + console.log('\n--- AGENDAMENTOS ---'); + console.table(agendamentos.map(a => { + const d = dentists.find(dent => dent.id === a.dentistaId); + return { + id: a.id, + paciente: a.pacienteNome, + dentista: d ? d.nome : 'UNKNOWN', + dentistaId: a.dentistaId, + startTime: a.start_time + }; + })); + await connection.end(); + } catch (err) { console.error('Error:', err.message); } +} +checkData(); diff --git a/base/scoreodonto/check_dentists_full.js b/base/scoreodonto/check_dentists_full.js new file mode 100644 index 0000000..5d56079 --- /dev/null +++ b/base/scoreodonto/check_dentists_full.js @@ -0,0 +1,12 @@ +import mysql from 'mysql2/promise'; + +async function checkDentists() { + const dbConfig = { host: 'localhost', user: 'root', password: '', database: 'sis_odonto' }; + try { + const connection = await mysql.createConnection(dbConfig); + const [rows] = await connection.query('SELECT * FROM dentistas'); + console.table(rows); + await connection.end(); + } catch (err) { console.error('Error:', err.message); } +} +checkDentists(); diff --git a/base/scoreodonto/check_settings.js b/base/scoreodonto/check_settings.js new file mode 100644 index 0000000..a1ecce1 --- /dev/null +++ b/base/scoreodonto/check_settings.js @@ -0,0 +1,17 @@ +import mysql from 'mysql2/promise'; + +async function checkSettings() { + const dbConfig = { host: 'localhost', user: 'root', password: '', database: 'sis_odonto' }; + try { + const connection = await mysql.createConnection(dbConfig); + const [rows] = await connection.query('SELECT * FROM settings WHERE id = "main"'); + if (rows[0]) { + console.log('\n--- SETTINGS ---'); + console.log(JSON.stringify(JSON.parse(rows[0].data), null, 2)); + } else { + console.log('No settings found.'); + } + await connection.end(); + } catch (err) { console.error('Error:', err.message); } +} +checkSettings(); diff --git a/base/scoreodonto/components/AgendaSettingsModal.tsx b/base/scoreodonto/components/AgendaSettingsModal.tsx new file mode 100644 index 0000000..aa00b91 --- /dev/null +++ b/base/scoreodonto/components/AgendaSettingsModal.tsx @@ -0,0 +1,363 @@ +import React, { useState, useEffect } from 'react'; +import { X, ChevronLeft, ChevronRight, Save, Mail, Calendar, Settings as GearIcon, CheckCircle2 } from 'lucide-react'; +import { HybridBackend } from '../services/backend.ts'; +import { Settings, Dentista } from '../types.ts'; +import { useToast } from '../contexts/ToastContext.tsx'; +import { GoogleConnectButton } from './GoogleConnectButton.tsx'; +import { Link as LinkIcon, Copy, Share2 } from 'lucide-react'; + +interface AgendaSettingsModalProps { + isOpen: boolean; + onClose: () => void; + dentists: Dentista[] | null; +} + +export const AgendaSettingsModal: React.FC = ({ isOpen, onClose, dentists }) => { + const [page, setPage] = useState(1); + const [config, setConfig] = useState({ + clinicEmail: '', + clinicCalendarId: '', + googleApiKey: '', + googleCalendarIds: {} + }); + const [isSaving, setIsSaving] = useState(false); + const [connectedAccounts, setConnectedAccounts] = useState([]); + const toast = useToast(); + + const fetchGoogleStatus = async () => { + try { + const response = await fetch('http://localhost:3005/api/auth/google/status'); + const data = await response.json(); + setConnectedAccounts(data); + } catch (error) { + console.error("Erro ao buscar status do Google:", error); + } + }; + + useEffect(() => { + if (isOpen) { + const loadSettings = async () => { + try { + const settings = await HybridBackend.getSettings(); + setConfig({ + clinicEmail: settings.clinicEmail || '', + clinicCalendarId: settings.clinicCalendarId || '', + googleApiKey: settings.googleApiKey || '', + googleCalendarIds: settings.googleCalendarIds || {} + }); + } catch (error) { + console.error("Erro ao carregar configurações:", error); + } + }; + loadSettings(); + fetchGoogleStatus(); + setPage(1); // Reset to first page + } + }, [isOpen]); + + const copyInviteLink = (dentistId: string) => { + const url = `http://localhost:3005/api/auth/google/url?dentistId=${dentistId}`; + navigator.clipboard.writeText(url); + toast.success("LINK DE CONVITE COPIADO!"); + }; + + const handleSave = async () => { + setIsSaving(true); + try { + await HybridBackend.saveSettings(config); + toast.success("CONFIGURAÇÕES SALVAS COM SUCESSO!"); + onClose(); + } catch (error) { + toast.error("ERRO AO SALVAR CONFIGURAÇÕES."); + } finally { + setIsSaving(false); + } + }; + + if (!isOpen) return null; + + const totalPages = 3; + + return ( +
+
+ + {/* Header */} +
+
+
+ +
+
+

Configurações da Agenda

+

Integrações e Comunicação

+
+
+ +
+ + {/* Progress Bar */} +
+
+
+ + {/* Content Area - Horizontal Slider Simulation */} +
+
+ {/* Page 1: Clinic Emails */} +
+
+ +

E-mails da Clínica

+
+
+
+ + setConfig({ ...config, clinicEmail: e.target.value.toUpperCase() })} + /> +

* Este e-mail será usado para alertas de sistema e envio de comprovantes.

+
+
+
+ + {/* Page 2: Google Setup Tutorial & Central Config */} +
+
+
+ +

Configuração Google Agenda

+
+ + 1. Ativar API + +
+ +
+
+
+
+
1
+ Google API Key +
+ setConfig({ ...config, googleApiKey: e.target.value })} + /> + Criar Credenciais ➔ +
+
+
+
+
+
2
+ ID Agenda Clínica +
+ setConfig({ ...config, clinicCalendarId: e.target.value })} + /> + Pegar ID nas Configs ➔ +
+
+
+ +
+

Guia de Integração

+
    +
  • + + Crie um projeto no Google Cloud Console. +
  • +
  • + + No menu 'Biblioteca', ative a Google Calendar API. +
  • +
  • + +
    + Pegue o ID da Agenda nas configurações (Integrar agenda). + + Abrir Configurações de Agenda ➔ + +
    +
  • +
  • + + No Google Agenda, torne a agenda Pública nas autorizações de acesso. +
  • +
+
+
+ + {/* Page 3: Google Calendar Mapping */} +
+
+
+ +

Agendas dos Dentistas

+
+ + Abrir Google Calendar + +
+ +
+
+

Nova Integração Inteligente (OAuth2)

+

Agora você pode conectar agendas sem precisar torná-las públicas ou usar API Keys.

+
+ + {/* ADMIN / CLINIC CONNECTION */} +
+

Sua Conta (Administrador)

+ a.owner_id === 'admin' || a.owner_id.includes('@'))} + onStatusChange={fetchGoogleStatus} + /> +
+ +
+

Contas dos Dentistas

+ {dentists?.length ? dentists.map((dentist) => { + const isConnected = connectedAccounts.some(a => a.owner_id === dentist.id); + return ( +
+
+
+
+ {dentist.nome} +
+ {isConnected ? ( + Conectado + ) : ( + Não Vinculado + )} +
+ +
+
+ +
+ {!isConnected && ( + + )} +
+ + {/* Legacy ID support remains for backward compatibility */} +
+ + setConfig({ + ...config, + googleCalendarIds: { + ...config.googleCalendarIds, + [dentist.id]: e.target.value + } + })} + /> +
+
+ ); + }) : ( +

Nenhum dentista cadastrado.

+ )} +
+
+
+
+
+ + {/* Footer Navigation */} +
+
+ {page > 1 && ( + + )} +
+ +
+ {page < totalPages ? ( + + ) : ( + + )} +
+
+
+ + +
+ ); +}; diff --git a/base/scoreodonto/components/ErrorBoundary.tsx b/base/scoreodonto/components/ErrorBoundary.tsx new file mode 100644 index 0000000..34e1e24 --- /dev/null +++ b/base/scoreodonto/components/ErrorBoundary.tsx @@ -0,0 +1,96 @@ +import React, { ErrorInfo, ReactNode } from 'react'; +import { AlertTriangle, RefreshCw, XCircle } from 'lucide-react'; + +interface Props { + children?: ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; + errorInfo: ErrorInfo | null; +} + +export class ErrorBoundary extends React.Component { + // FIX: Switched to constructor-based state initialization to resolve issues + // where the TypeScript toolchain might not correctly interpret public class fields, + // causing `this.props` and `this.setState` to appear unavailable. This is a more + // widely compatible approach. + constructor(props: Props) { + super(props); + this.state = { hasError: false, error: null, errorInfo: null }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error, errorInfo: null }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error("Uncaught error:", error, errorInfo); + this.setState({ errorInfo }); + } + + handleReload = () => { + window.location.reload(); + }; + + render() { + if (this.state.hasError) { + return ( +
+
+
+
+ +
+
+

ERRO CRÍTICO NO SISTEMA

+

O APLICATIVO ENCONTROU UM PROBLEMA INESPERADO

+
+
+ +
+
+

O QUE ACONTECEU?

+

+ Ocorreu uma falha durante a renderização da interface. Tente recarregar a página. + Se o problema persistir, envie o log abaixo para o suporte técnico. +

+
+ + {this.state.error && ( +
+
+ Stack Trace / Log Técnico +
+
+                    {this.state.error.toString()}
+                    
+ {this.state.errorInfo?.componentStack} +
+
+ )} + +
+ + +
+
+
+
+ ); + } + + return this.props.children; + } +} diff --git a/base/scoreodonto/components/GoogleConnectButton.tsx b/base/scoreodonto/components/GoogleConnectButton.tsx new file mode 100644 index 0000000..77307a9 --- /dev/null +++ b/base/scoreodonto/components/GoogleConnectButton.tsx @@ -0,0 +1,101 @@ +import React, { useState } from 'react'; +import { Calendar, Link, CheckCircle2, AlertCircle, Loader2 } from 'lucide-react'; + +interface GoogleConnectButtonProps { + ownerId: string; // Ex: 'admin' ou o ID do dentista + isConnected: boolean; + onStatusChange?: () => void; +} + +export const GoogleConnectButton: React.FC = ({ ownerId, isConnected, onStatusChange }) => { + const [isLoading, setIsLoading] = useState(false); + + const handleConnect = async () => { + setIsLoading(true); + try { + const response = await fetch(`http://localhost:3005/api/auth/google/url?dentistId=${ownerId}`); + const { url } = await response.json(); + + // Abrir em popup para manter o contexto + const width = 600; + const height = 700; + const left = window.screen.width / 2 - width / 2; + const top = window.screen.height / 2 - height / 2; + + const popup = window.open( + url, + 'google-auth', + `width=${width},height=${height},left=${left},top=${top}` + ); + + // Verificar se o popup fechou a cada 1s + const checkPopup = setInterval(() => { + if (!popup || popup.closed) { + clearInterval(checkPopup); + setIsLoading(false); + if (onStatusChange) onStatusChange(); + } + }, 1000); + + } catch (error) { + console.error('Erro ao conectar Google Agenda:', error); + setIsLoading(false); + } + }; + + const handleDisconnect = async () => { + if (!confirm('Deseja realmente desconectar esta agenda?')) return; + + setIsLoading(true); + try { + await fetch(`http://localhost:3005/api/auth/google/${ownerId}`, { method: 'DELETE' }); + if (onStatusChange) onStatusChange(); + } catch (error) { + console.error('Erro ao desconectar:', error); + } finally { + setIsLoading(false); + } + }; + + if (isConnected) { + return ( +
+
+ +
+
+

Google Agenda Conectada

+

Sincronização Ativa

+
+ +
+ ); + } + + return ( + + ); +}; diff --git a/base/scoreodonto/components/Header.tsx b/base/scoreodonto/components/Header.tsx new file mode 100644 index 0000000..61802aa --- /dev/null +++ b/base/scoreodonto/components/Header.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { Building2, ChevronDown, User, LogOut } from 'lucide-react'; +import { HybridBackend } from '../services/backend.ts'; + +export const Header: React.FC = () => { + const workspaces = HybridBackend.getWorkspaces(); + const activeWorkspace = HybridBackend.getActiveWorkspace(); + const userName = HybridBackend.getCurrentUserName(); + const userEmail = HybridBackend.getCurrentUser(); + + const handleWorkspaceChange = (e: React.ChangeEvent) => { + HybridBackend.switchWorkspace(e.target.value); + }; + + const handleLogout = () => { + if (window.confirm('TEM CERTEZA QUE DESEJA SAIR DO SISTEMA?')) { + HybridBackend.logout(); + } + }; + + return ( +
+ {/* Clinic Selector */} +
+
+ +
+
+ +
+ +
+
+
+ + {/* User Actions */} +
+
+ Unidade Ativa + + {activeWorkspace?.role || 'Acesso'} + +
+ +
+ +
+
+

+ {userName} +

+

+ {userEmail} +

+
+
+ +
+ +
+
+
+ ); +}; diff --git a/base/scoreodonto/components/NotificationCenter.tsx b/base/scoreodonto/components/NotificationCenter.tsx new file mode 100644 index 0000000..b2a93f9 --- /dev/null +++ b/base/scoreodonto/components/NotificationCenter.tsx @@ -0,0 +1,165 @@ +import React, { useState, useEffect } from 'react'; +import { Bell, X, Calendar, DollarSign, AlertCircle, UserCheck, MessageSquare, ShieldAlert } from 'lucide-react'; +import { HybridBackend } from '../services/backend.ts'; +import { Notificacao } from '../types.ts'; + +export const NotificationCenter: React.FC = () => { + const [isOpen, setIsOpen] = useState(false); + const [notificacoes, setNotificacoes] = useState([]); + const [unreadCount, setUnreadCount] = useState(0); + + const fetch = async () => { + try { + const data = await HybridBackend.getNotifications(); + setNotificacoes(data); + setUnreadCount(data.filter(n => !n.lida).length); + } catch (err) { + console.error("Erro ao carregar notificações"); + } + }; + + useEffect(() => { + fetch(); + const interval = setInterval(fetch, 30000); // Polling cada 30s + return () => clearInterval(interval); + }, []); + + const toggleOpen = () => setIsOpen(!isOpen); + + const markAsRead = async (id: string) => { + await HybridBackend.markNotificationRead(id); + setNotificacoes(prev => prev.map(n => n.id === id ? { ...n, lida: true } : n)); + setUnreadCount(prev => Math.max(0, prev - 1)); + }; + + const getIcon = (tipo: string) => { + switch (tipo) { + case 'agenda': return ; + case 'financeiro': return ; + case 'lead': return ; + case 'sistema': return ; + default: return ; + } + }; + + const getTypeLabel = (tipo: string) => { + switch (tipo) { + case 'agenda': return 'Agenda'; + case 'financeiro': return 'Financeiro'; + case 'lead': return 'Novo Lead'; + case 'sistema': return 'Sistema'; + default: return 'Geral'; + } + }; + + return ( +
+
+ + + {isOpen && ( + <> +
+
+
+
+

Centro de Notificações

+

Você tem {unreadCount} novas mensagens

+
+ +
+ +
+ {notificacoes.length === 0 ? ( +
+
+ +
+

Tudo limpo por aqui!

+

Não há notificações para mostrar no momento.

+
+ ) : ( +
+ {notificacoes.map(notif => ( +
+ {!notif.lida && ( +
+ )} +
+
+ {getIcon(notif.tipo)} +
+
+
+ + {getTypeLabel(notif.tipo)} + + + {new Date(notif.data).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })} + +
+

+ {notif.titulo} +

+

+ {notif.mensagem} +

+ {!notif.lida && ( + + )} +
+
+
+ ))} +
+ )} +
+ +
+ +
+
+ + )} +
+
+ ); +}; \ No newline at end of file diff --git a/base/scoreodonto/components/PageHeader.tsx b/base/scoreodonto/components/PageHeader.tsx new file mode 100644 index 0000000..2a77104 --- /dev/null +++ b/base/scoreodonto/components/PageHeader.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { NotificationCenter } from './NotificationCenter.tsx'; + +interface PageHeaderProps { + title: string; + description?: string; + children?: React.ReactNode; +} + +export const PageHeader: React.FC = ({ title, description, children }) => { + return ( +
+
+

{title}

+ {description &&

{description}

} +
+
+ {children} +
+
+ ); +}; diff --git a/base/scoreodonto/components/Sidebar.tsx b/base/scoreodonto/components/Sidebar.tsx new file mode 100644 index 0000000..61a479b --- /dev/null +++ b/base/scoreodonto/components/Sidebar.tsx @@ -0,0 +1,152 @@ +import React from 'react'; +import { + Users, Calendar as CalendarIcon, LayoutDashboard, Stethoscope, + DollarSign, RefreshCw, Database, Activity, BookOpen, Award, Megaphone, LogOut, ClipboardList, Bell, Building2, User, BarChart3 +} from 'lucide-react'; +import { HybridBackend } from '../services/backend.ts'; +import { GoogleConnectButton } from './GoogleConnectButton.tsx'; +import { useState, useEffect } from 'react'; + +interface SidebarProps { + activeTab: string; + setActiveTab: (t: string) => void; +} + +export const Sidebar: React.FC = ({ activeTab, setActiveTab }) => { + const [connectedAccounts, setConnectedAccounts] = useState([]); + const currentRole = HybridBackend.getCurrentRole(); + const userName = HybridBackend.getCurrentUserName(); + const userEmail = HybridBackend.getCurrentUser(); + const activeWorkspace = HybridBackend.getActiveWorkspace(); + const clinColor = activeWorkspace?.cor || '#2563eb'; + + const fetchGoogleStatus = async () => { + try { + const response = await fetch('http://localhost:3005/api/auth/google/status'); + const data = await response.json(); + setConnectedAccounts(data); + } catch (error) { + console.error("Erro ao buscar status do Google:", error); + } + }; + + useEffect(() => { + fetchGoogleStatus(); + }, []); + + const menuItems = [ + { id: 'dashboard', label: 'VISÃO GERAL', icon: LayoutDashboard }, + { id: 'clinicas', label: 'MINHAS CLÍNICAS', icon: Building2 }, + { id: 'leads', label: 'GESTÃO DE LEADS', icon: Megaphone }, + { id: 'pacientes', label: 'PACIENTES', icon: Users }, + { id: 'tratamentos', label: 'MEUS TRATAMENTOS', icon: ClipboardList }, + { id: 'lancar-gto', label: 'LANÇAR GTO', icon: ClipboardList }, + { id: 'agenda', label: 'AGENDA', icon: CalendarIcon }, + { id: 'ortodontia', label: 'ORTODONTIA', icon: Activity }, + { id: 'financeiro', label: 'FINANCEIRO', icon: DollarSign }, + { id: 'relatorios', label: 'RELATÓRIOS', icon: BarChart3 }, + { id: 'dentistas', label: 'DENTISTAS', icon: Stethoscope }, + { id: 'especialidades', label: 'ESPECIALIDADES', icon: Award }, + { id: 'procedimentos', label: 'PROCEDIMENTOS', icon: ClipboardList }, + { id: 'planos', label: 'PLANOS / CONVÊNIOS', icon: BookOpen }, + { id: 'notificacoes', label: 'NOTIFICAÇÕES', icon: Bell }, + { id: 'sync', label: 'SINCRONIZAÇÃO', icon: RefreshCw }, + ].filter(item => { + if (['admin', 'donoclinica'].includes(currentRole)) return true; + if (currentRole === 'paciente') return ['tratamentos', 'notificacoes'].includes(item.id); + if (currentRole === 'dentista') return ['dashboard', 'pacientes', 'tratamentos', 'agenda', 'ortodontia', 'notificacoes', 'clinicas'].includes(item.id); + if (currentRole === 'funcionario') return ['dashboard', 'leads', 'pacientes', 'tratamentos', 'lancar-gto', 'agenda', 'financeiro', 'relatorios', 'notificacoes', 'clinicas'].includes(item.id); + return false; + }); + + const handleLogout = () => { + if (window.confirm('TEM CERTEZA QUE DESEJA SAIR DO SISTEMA?')) { + HybridBackend.logout(); + } + }; + + return ( +
+
+
+ +
+
+ SCOREODONTO + MYSQL + GOOGLE +
+
+ + + +
+ {/* User Profile Section */} +
+
+
+ +
+
+

{userName}

+

{userEmail}

+
+
+
+
+ + {activeWorkspace?.nome || 'Sem Unidade'} + +
+
+ + {currentRole === 'admin' && ( + a.owner_id === 'admin' || a.owner_id === userEmail)} + onStatusChange={fetchGoogleStatus} + /> + )} + + +
+
+ ); +}; \ No newline at end of file diff --git a/base/scoreodonto/components/Toast.tsx b/base/scoreodonto/components/Toast.tsx new file mode 100644 index 0000000..a657ed2 --- /dev/null +++ b/base/scoreodonto/components/Toast.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { createPortal } from 'react-dom'; +import { CheckCircle, AlertTriangle, Info, X } from 'lucide-react'; +import { useToast } from '../contexts/ToastContext.tsx'; + +const icons = { + success: , + error: , + info: , +}; + +const toastColors = { + success: 'bg-green-50 border-green-200', + error: 'bg-red-50 border-red-200', + info: 'bg-blue-50 border-blue-200', +}; + +export const ToastContainer: React.FC = () => { + const { toasts } = useToast(); + const portalElement = document.getElementById('toast-container'); + + if (!portalElement) return null; + + return createPortal( +
+ {toasts.map((toast) => ( +
+
{icons[toast.type]}
+

{toast.message}

+
+ ))} +
, + portalElement + ); +}; \ No newline at end of file diff --git a/base/scoreodonto/contexts/ToastContext.tsx b/base/scoreodonto/contexts/ToastContext.tsx new file mode 100644 index 0000000..77dbc35 --- /dev/null +++ b/base/scoreodonto/contexts/ToastContext.tsx @@ -0,0 +1,54 @@ +import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react'; + +type ToastType = 'success' | 'error' | 'info'; + +interface ToastMessage { + id: number; + message: string; + type: ToastType; +} + +interface ToastContextType { + toasts: ToastMessage[]; + addToast: (message: string, type: ToastType) => void; +} + +const ToastContext = createContext(undefined); + +export const ToastProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [toasts, setToasts] = useState([]); + + const addToast = useCallback((message: string, type: ToastType) => { + const id = Date.now(); + setToasts((prev) => [...prev, { id, message, type }]); + setTimeout(() => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }, 4000); + }, []); + + const value = { + toasts, + addToast, + }; + + return {children}; +}; + +// FIX: Modified useToast to return the full context including `toasts` array, +// so that ToastContainer can access it. Kept helper functions for backward compatibility. +export const useToast = (): ToastContextType & { + success: (message: string) => void; + error: (message: string) => void; + info: (message: string) => void; +} => { + const context = useContext(ToastContext); + if (!context) { + throw new Error('useToast must be used within a ToastProvider'); + } + return { + ...context, + success: (message: string) => context.addToast(message, 'success'), + error: (message: string) => context.addToast(message, 'error'), + info: (message: string) => context.addToast(message, 'info'), + }; +}; \ No newline at end of file diff --git a/base/scoreodonto/gto_modelo/GuiaTratamentoOdontologico_4_02_00_0005010_2080974_TS_000047_FACADE_20260225155840539.pdf b/base/scoreodonto/gto_modelo/GuiaTratamentoOdontologico_4_02_00_0005010_2080974_TS_000047_FACADE_20260225155840539.pdf new file mode 100644 index 0000000..cd4bf1c Binary files /dev/null and b/base/scoreodonto/gto_modelo/GuiaTratamentoOdontologico_4_02_00_0005010_2080974_TS_000047_FACADE_20260225155840539.pdf differ diff --git a/base/scoreodonto/hooks/useHybridBackend.ts b/base/scoreodonto/hooks/useHybridBackend.ts new file mode 100644 index 0000000..281c9bf --- /dev/null +++ b/base/scoreodonto/hooks/useHybridBackend.ts @@ -0,0 +1,44 @@ +import { useState, useEffect, useCallback } from 'react'; + +type Fetcher = (...args: any[]) => Promise; + +interface UseHybridBackendReturn { + data: T | null; + isLoading: boolean; + error: Error | null; + refresh: () => void; +} + +export function useHybridBackend( + fetcher: Fetcher, + deps: any[] = [] +): UseHybridBackendReturn { + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [refreshCount, setRefreshCount] = useState(0); + + const fetchData = useCallback(async () => { + setIsLoading(true); + setError(null); + try { + const result = await fetcher(); + setData(result); + } catch (e) { + setError(e instanceof Error ? e : new Error('An unknown error occurred')); + } finally { + setIsLoading(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [...deps, refreshCount]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const refresh = () => { + setRefreshCount(prev => prev + 1); + }; + + return { data, isLoading, error, refresh }; +} diff --git a/base/scoreodonto/index.css b/base/scoreodonto/index.css new file mode 100644 index 0000000..4b4734e --- /dev/null +++ b/base/scoreodonto/index.css @@ -0,0 +1,82 @@ +@import "tailwindcss"; + +/* ScoreOdonto Base Styles */ + +body { + font-family: 'Inter', sans-serif; + background-color: #f3f4f6; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; +} + +::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +.fc { + font-family: 'Inter', sans-serif; +} + +.fc-toolbar-title { + font-size: 1.25rem !important; + font-weight: 600 !important; + color: #1f2937; +} + +.fc-button-primary { + background-color: #2563eb !important; + border-color: #2563eb !important; +} + +.fc-daygrid-event { + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; +} + +.uppercase-input, +.uppercase-textarea, +.uppercase-select { + text-transform: uppercase; +} + +.dark-date-input::-webkit-calendar-picker-indicator { + filter: invert(1); + cursor: pointer; +} + +/* FullCalendar Display Fixes */ +.fc { + max-height: 100%; +} + +.fc-theme-standard td, +.fc-theme-standard th { + border: 1px solid #e5e7eb !important; +} + +.fc-timegrid-slot { + height: 3em !important; +} + +.fc-col-header-cell { + background-color: #f9fafb; + padding: 8px 0 !important; +} + +.fc-scrollgrid { + border-radius: 8px; + overflow: hidden; +} \ No newline at end of file diff --git a/base/scoreodonto/index.html b/base/scoreodonto/index.html new file mode 100644 index 0000000..1d42258 --- /dev/null +++ b/base/scoreodonto/index.html @@ -0,0 +1,38 @@ + + + + + + + ScoreOdonto CRM + + + + + + + + + + + + + +
+
+ + + + + + \ No newline at end of file diff --git a/base/scoreodonto/index.tsx b/base/scoreodonto/index.tsx new file mode 100644 index 0000000..e8709af --- /dev/null +++ b/base/scoreodonto/index.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; +import { ErrorBoundary } from './components/ErrorBoundary.tsx'; +import { ToastProvider } from './contexts/ToastContext.tsx'; +import './index.css'; + +// CSS imports are now handled in index.html via tags for AI Studio compatibility, +// but we add it here too for Vite dev server robustness. + +/** + * AI Studio Compatible Entrypoint + * + * This version restores the necessary providers (`ErrorBoundary`, `ToastProvider`) + * to allow the application to run correctly after the initial bootstrap. + * It uses the modern `createRoot` API, which is required for React 18 and newer. + */ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +const queryClient = new QueryClient(); + +const rootElement = document.getElementById('root'); +if (rootElement) { + const root = createRoot(rootElement); + root.render( + + + + + + + + + + ); +} else { + console.error("CRITICAL: Root element #root not found. App could not be mounted."); + const errorDiv = document.getElementById('global-error-display'); + if (errorDiv) { + errorDiv.style.display = 'block'; + errorDiv.innerText = 'ERRO CRÍTICO: O ELEMENTO #ROOT NÃO FOI ENCONTRADO NO HTML.'; + } +} \ No newline at end of file diff --git a/base/scoreodonto/med/LandingPage.tsx b/base/scoreodonto/med/LandingPage.tsx new file mode 100644 index 0000000..1fd803a --- /dev/null +++ b/base/scoreodonto/med/LandingPage.tsx @@ -0,0 +1,207 @@ +import React from 'react'; +import { Shield, Activity, Heart, Check, ArrowRight, Star, Clock, Users, Smartphone, Zap } from 'lucide-react'; + +export const MedLandingPage: React.FC = () => { + return ( +
+ {/* Navigation */} + + + {/* Hero Section */} +
+
+
+
+
+ +
+
+
+ Novo: Fidelidade Multimédica +
+

+ SAÚDE TOTAL,
+ SEM COMPROMISSO. +

+

+ O primeiro plano de fidelidade que integra medicina, odontologia e bem-estar em um único ecossistema. Fidelize sua saúde com tecnologia de ponta. +

+
+ + +
+
+
+
+ + {/* Plans Section */} +
+
+
+

ESTRUTURA DE PLANOS

+

Exclusivo: Combos que não aceitam desmembramento

+
+ +
+ {/* Base Plan */} +
+
01
+
+ LINHA BASE (OBRIGATÓRIO) +

MÉDICO +
ODONTO

+
+
    + {[ + 'Consultas Médicas ilimitadas', + 'Limpeza e Prevenção Dental', + 'Urgências 24h', + 'Rede Credenciada Platinum', + 'Descontos em Medicamentos' + ].map((item, i) => ( +
  • +
    + +
    + {item} +
  • + ))} +
+
+ R$ 149 + /mês individual +
+ +
+ + {/* Premium Plan */} +
+
+
02
+ +
+
+ MAIS POPULAR +
+ LINHA PREMIUM +

MÉDICO + ODONTO
+ PSICOLOGIA

+
+
    + {[ + 'Tudo da Linha Base', + 'Suporte Psicológico 24h', + 'Sessões de Terapia Mensais', + 'Check-up Executivo Anual', + 'Prioridade na Agenda (VIP)' + ].map((item, i) => ( +
  • +
    + +
    + {item} +
  • + ))} +
+
+ R$ 249 + /mês individual +
+ +
+
+
+
+ + {/* Features (CRM) */} +
+
+
+ {[ + { icon: Users, title: 'TITULAR E DEP.', desc: 'Gestão completa familiar com carteirinha individual digital.' }, + { icon: Shield, title: 'ESTABILIDADE', desc: 'Previsibilidade total para o médico e segurança para o paciente.' }, + { icon: Smartphone, title: 'WALLET DIGITAL', desc: 'QR Code e histórico médico sempre no seu bolso.' }, + { icon: Zap, title: 'SEM GLOSA', desc: 'Processamento automático e repasse médico instantâneo.' } + ].map((feat, i) => ( +
+ +

{feat.title}

+

{feat.desc}

+
+ ))} +
+
+
+ + {/* Footer */} +
+
+
+
+ +
+ CRM MÉDICO +
+
+ © 2026 SCOREODONTO HYBRID MEDICAL ENGINE. ALL RIGHTS RESERVED. +
+
+ + +
+
+
+ + +
+ ); +}; diff --git a/base/scoreodonto/med/MedDashboard.tsx b/base/scoreodonto/med/MedDashboard.tsx new file mode 100644 index 0000000..34e4e3b --- /dev/null +++ b/base/scoreodonto/med/MedDashboard.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { + Users, Calendar, DollarSign, Activity, + ArrowUpRight, ArrowDownRight, Zap, Target, + TrendingUp, Clock, AlertCircle +} from 'lucide-react'; + +export const MedDashboard: React.FC = () => { + const stats = [ + { label: 'Pacientes Ativos', value: '1.284', change: '+12%', positive: true, icon: Users, color: 'cyan' }, + { label: 'Consultas Hoje', value: '24', change: '+4', positive: true, icon: Calendar, color: 'blue' }, + { label: 'Receita Est. (MRR)', value: 'R$ 84.200', change: '+8%', positive: true, icon: DollarSign, color: 'emerald' }, + { label: 'Taxa de Fidelidade', value: '94.2%', change: '-2%', positive: false, icon: Activity, color: 'purple' }, + ]; + + return ( +
+ {/* Header */} +
+

Painel Estratégico

+

Visão geral do ecossistema médico e fidelidade

+
+ + {/* Stats Grid */} +
+ {stats.map((stat, i) => { + const Icon = stat.icon; + return ( +
+
+
+ +
+
+ {stat.positive ? : } + {stat.change} +
+
+
+ {stat.label} +
{stat.value}
+
+
+ ); + })} +
+ + {/* Main Content Grid */} +
+ {/* Upcoming Appointments */} +
+
+

Próximos Atendimentos

+ +
+
+ {[ + { name: 'Ricardo Santos', time: '09:00', type: 'Consulta Geral', plan: 'Premium' }, + { name: 'Ana Oliveira', time: '10:30', type: 'Retorno', plan: 'Base' }, + { name: 'Marcos Viana', time: '13:00', type: 'Primeira Vez', plan: 'Premium' }, + { name: 'Juliana Costa', time: '15:15', type: 'Check-up', plan: 'Base' }, + ].map((item, i) => ( +
+
+
+ HRS + {item.time} +
+
+
{item.name}
+
{item.type}
+
+
+
+ + Plano {item.plan} + + +
+
+ ))} +
+
+ + {/* Intelligent Insights */} +
+
+
+
+ +
+

Inteligência
Médica

+
+
+
+ + Alerta de Agenda +
+

3 pacientes do Plano Base estão aptos para upgrade para o Premium este mês.

+
+
+
+ + Performance +
+

Sua produtividade aumentou 15% após a integração com o módulo Dental.

+
+
+ +
+
+
+
+ ); +}; + +const ArrowRight: React.FC<{ size: number }> = ({ size }) => ( + +); diff --git a/base/scoreodonto/med/MedLayout.tsx b/base/scoreodonto/med/MedLayout.tsx new file mode 100644 index 0000000..295f8a4 --- /dev/null +++ b/base/scoreodonto/med/MedLayout.tsx @@ -0,0 +1,87 @@ +import React, { useState } from 'react'; +import { MedSidebar } from './MedSidebar.tsx'; +import { Bell, Search, Settings, HelpCircle } from 'lucide-react'; + +interface MedLayoutProps { + children: React.ReactNode; + currentView: string; + onNavigate: (view: any) => void; +} + +export const MedLayout: React.FC = ({ children, currentView, onNavigate }) => { + return ( +
+ {/* Sidebar */} + + + {/* Main Content Area */} +
+ {/* Premium Top Bar */} +
+
+
+ + +
+
+ +
+
+ {[ + { icon: Bell, alert: true }, + { icon: HelpCircle, alert: false }, + { icon: Settings, alert: false } + ].map((item, i) => ( + + ))} +
+ +
+ +
+
+ Acesso Premium + Portal Médico Cassems +
+
+
+ MED +
+
+
+
+
+ + {/* Dashboard/View Container */} +
+
+ {children} +
+
+
+ + +
+ ); +}; diff --git a/base/scoreodonto/med/MedSidebar.tsx b/base/scoreodonto/med/MedSidebar.tsx new file mode 100644 index 0000000..b005226 --- /dev/null +++ b/base/scoreodonto/med/MedSidebar.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { + LayoutDashboard, Users, Calendar, ClipboardList, + Activity, Shield, DollarSign, LogOut, Settings, + ChevronRight, Zap, Heart, Bell +} from 'lucide-react'; + +interface MedSidebarProps { + activeTab: string; + setActiveTab: (tab: string) => void; +} + +export const MedSidebar: React.FC = ({ activeTab, setActiveTab }) => { + const menuItems = [ + { id: 'med-dashboard', label: 'Painel Geral', icon: LayoutDashboard }, + { id: 'med-pacientes', label: 'Pacientes', icon: Users }, + { id: 'med-agenda', label: 'Agenda Médica', icon: Calendar }, + { id: 'med-prontuarios', label: 'Prontuários', icon: ClipboardList }, + { id: 'med-fidelidade', label: 'Planos Fidelidade', icon: Shield }, + { id: 'med-financeiro', label: 'Financeiro / MRR', icon: DollarSign }, + { id: 'medical-repasse', label: 'Repasse Médico', icon: Zap }, + ]; + + const handleLogout = () => { + if (window.confirm('Deseja realmente sair do portal médico?')) { + window.location.hash = '/med/landingpage'; + } + }; + + return ( +
+ {/* Logo Section */} +
+
setActiveTab('med-dashboard')}> +
+ +
+
+ CRM MÉDICO + Premium Engine +
+
+
+ + {/* Navigation */} + + + {/* Bottom Section / User Profile */} +
+
+
+ DR +
+
+ Dr. Profissional + Médico Diretor +
+
+ +
+ + +
+
+ + {/* Status Indicator */} +
+
+ Sincronizado +
+
+ ); +}; diff --git a/base/scoreodonto/metadata.json b/base/scoreodonto/metadata.json new file mode 100644 index 0000000..1dcce11 --- /dev/null +++ b/base/scoreodonto/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "ScoreOdonto CRM", + "description": "Sistema de gerenciamento para clínica odontológica com integração híbrida (MySQL + Google Sheets) e design moderno.", + "requestFramePermissions": [] +} \ No newline at end of file diff --git a/base/scoreodonto/package-lock.json b/base/scoreodonto/package-lock.json new file mode 100644 index 0000000..75cd906 --- /dev/null +++ b/base/scoreodonto/package-lock.json @@ -0,0 +1,5524 @@ +{ + "name": "scoreodonto-crm", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "scoreodonto-crm", + "version": "0.0.0", + "dependencies": { + "@fullcalendar/daygrid": "^6.1.20", + "@fullcalendar/interaction": "^6.1.20", + "@fullcalendar/react": "^6.1.20", + "@fullcalendar/timegrid": "^6.1.20", + "@hello-pangea/dnd": "^18.0.1", + "@prisma/client": "^7.4.1", + "@tanstack/react-query": "^5.90.21", + "concurrently": "^9.2.1", + "cors": "^2.8.6", + "date-fns": "^4.1.0", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "googleapis": "^171.4.0", + "jsonwebtoken": "^9.0.3", + "lucide-react": "^0.563.0", + "mysql2": "^3.17.4", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "zod": "^4.3.6", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.2.0", + "@tailwindcss/vite": "^4.2.0", + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "autoprefixer": "^10.4.24", + "postcss": "^8.5.6", + "prisma": "^7.4.1", + "tailwindcss": "^4.2.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/types": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite": { + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", + "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.20.tgz", + "integrity": "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.3.15" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.20.tgz", + "integrity": "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.3.15" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fullcalendar/core": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz", + "integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "preact": "~10.12.1" + } + }, + "node_modules/@fullcalendar/daygrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.20.tgz", + "integrity": "sha512-AO9vqhkLP77EesmJzuU+IGXgxNulsA8mgQHynclJ8U70vSwAVnbcLG9qftiTAFSlZjiY/NvhE7sflve6cJelyQ==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, + "node_modules/@fullcalendar/interaction": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.20.tgz", + "integrity": "sha512-p6txmc5txL0bMiPaJxe2ip6o0T384TyoD2KGdsU6UjZ5yoBlaY+dg7kxfnYKpYMzEJLG58n+URrHr2PgNL2fyA==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, + "node_modules/@fullcalendar/react": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.20.tgz", + "integrity": "sha512-1w0pZtceaUdfAnxMSCGHCQalhi+mR1jOe76sXzyAXpcPz/Lf0zHSdcGK/U2XpZlnQgQtBZW+d+QBnnzVQKCxAA==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.20", + "react": "^16.7.0 || ^17 || ^18 || ^19", + "react-dom": "^16.7.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/@fullcalendar/timegrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.20.tgz", + "integrity": "sha512-4H+/MWbz3ntA50lrPif+7TsvMeX3R1GSYjiLULz0+zEJ7/Yfd9pupZmAwUs/PBpA6aAcFmeRr0laWfcz1a9V1A==", + "license": "MIT", + "dependencies": { + "@fullcalendar/daygrid": "~6.1.20" + }, + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, + "node_modules/@hello-pangea/dnd": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz", + "integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.26.7", + "css-box-model": "^1.2.1", + "raf-schd": "^4.0.3", + "react-redux": "^9.2.0", + "redux": "^5.0.1" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mrleebo/prisma-ast": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.13.1.tgz", + "integrity": "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chevrotain": "^10.5.0", + "lilconfig": "^2.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prisma/client": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.4.1.tgz", + "integrity": "sha512-pgIll2W1NVdof37xLeyySW+yfQ4rI+ERGCRwnO3BjVOx42GpYq6jhTyuALK8VKirvJJIvImgfGDA2qwhYVvMuA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.4.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.4.1.tgz", + "integrity": "sha512-8fy74OMYC7mt9cJ2MncIDk1awPRgmtXVvwTN2FlW4JVhbck8Dgt0wTkhPG85myfj4ZeP2stjF9Sdg12n5HrpQg==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.4.1.tgz", + "integrity": "sha512-vteSXm8N46bo3FW9MhPGVHAj+KRgrR6TWtlSk6GqToCKjTnOexXdPZyiDyEsfVW38YhqEmVl6w/6iHN8uYVJcw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.4.1.tgz", + "integrity": "sha512-qEtzO8oLouRv18JDQUC3G3Gnv+fGVscHZm/x1DBB/WT+kOvPDQLM2woX6IGgWnSMYYlrxjuALshT7G/blvY0bQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.20.0.tgz", + "integrity": "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.3.15", + "@electric-sql/pglite-socket": "0.0.20", + "@electric-sql/pglite-tools": "0.2.20", + "@hono/node-server": "1.19.9", + "@mrleebo/prisma-ast": "0.13.1", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "hono": "4.11.4", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.2.0", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/engines": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.4.1.tgz", + "integrity": "sha512-BZEBdHvNJx5PzIG37EI/Zi5UUI5hGWjkYsQmKa7OIK6evAvebOTwutjS/VRI6cA6grmA52eLZR+oekGRMqkKxQ==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.4.1", + "@prisma/engines-version": "7.5.0-4.55ae170b1ced7fc6ed07a15f110549408c501bb3", + "@prisma/fetch-engine": "7.4.1", + "@prisma/get-platform": "7.4.1" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.5.0-4.55ae170b1ced7fc6ed07a15f110549408c501bb3", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.5.0-4.55ae170b1ced7fc6ed07a15f110549408c501bb3.tgz", + "integrity": "sha512-fUxVd1TjOW8K4XsZ8dAm88sDW5Ry7AxWDfsYEWwScS6Fjo3caKC6hgNumUfsmsy0Il9LjDn5X0PpVXNt3iwayw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.1.tgz", + "integrity": "sha512-kN4tmkQzlgm/KtE+jTNSYjsDxxe/5i6GApPI32BN9T0tlgsgSBtDJbjGBICttkAIjsh73dXf8raPKxO/2n2UUg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.4.1" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.4.1.tgz", + "integrity": "sha512-Z9kbuxX2bvEsyeS3LZEiEnxG0lVtZbpYgaAnPj69N+A9f2De8Lta0EoFtld9zhfERVPIQWhSWUc8himky3qYdA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.4.1", + "@prisma/engines-version": "7.5.0-4.55ae170b1ced7fc6ed07a15f110549408c501bb3", + "@prisma/get-platform": "7.4.1" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.1.tgz", + "integrity": "sha512-kN4tmkQzlgm/KtE+jTNSYjsDxxe/5i6GApPI32BN9T0tlgsgSBtDJbjGBICttkAIjsh73dXf8raPKxO/2n2UUg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.4.1" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/studio-core": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.13.1.tgz", + "integrity": "sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.0.tgz", + "integrity": "sha512-Yv+fn/o2OmL5fh/Ir62VXItdShnUxfpkMA4Y7jdeC8O81WPB8Kf6TT6GSHvnqgSwDzlB5iT7kDpeXxLsUS0T6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.0.tgz", + "integrity": "sha512-AZqQzADaj742oqn2xjl5JbIOzZB/DGCYF/7bpvhA8KvjUj9HJkag6bBuwZvH1ps6dfgxNHyuJVlzSr2VpMgdTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.0", + "@tailwindcss/oxide-darwin-arm64": "4.2.0", + "@tailwindcss/oxide-darwin-x64": "4.2.0", + "@tailwindcss/oxide-freebsd-x64": "4.2.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.0", + "@tailwindcss/oxide-linux-x64-musl": "4.2.0", + "@tailwindcss/oxide-wasm32-wasi": "4.2.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.0.tgz", + "integrity": "sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.0.tgz", + "integrity": "sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.0.tgz", + "integrity": "sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.0.tgz", + "integrity": "sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.0.tgz", + "integrity": "sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.0.tgz", + "integrity": "sha512-Mff5a5Q3WoQR01pGU1gr29hHM1N93xYrKkGXfPw/aRtK4bOc331Ho4Tgfsm5WDGvpevqMpdlkCojT3qlCQbCpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.0.tgz", + "integrity": "sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.0.tgz", + "integrity": "sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.0.tgz", + "integrity": "sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.0.tgz", + "integrity": "sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.0.tgz", + "integrity": "sha512-2UU/15y1sWDEDNJXxEIrfWKC2Yb4YgIW5Xz2fKFqGzFWfoMHWFlfa1EJlGO2Xzjkq/tvSarh9ZTjvbxqWvLLXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.0.tgz", + "integrity": "sha512-CrFadmFoc+z76EV6LPG1jx6XceDsaCG3lFhyLNo/bV9ByPrE+FnBPckXQVP4XRkN76h3Fjt/a+5Er/oA/nCBvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.0.tgz", + "integrity": "sha512-u6YBacGpOm/ixPfKqfgrJEjMfrYmPD7gEFRoygS/hnQaRtV0VCBdpkx5Ouw9pnaLRwwlgGCuJw8xLpaR0hOrQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.0", + "@tailwindcss/oxide": "4.2.0", + "postcss": "^8.5.6", + "tailwindcss": "4.2.0" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.0.tgz", + "integrity": "sha512-da9mFCaHpoOgtQiWtDGIikTrSpUFBtIZCG3jy/u2BGV+l/X1/pbxzmIUxNt6JWm19N3WtGi4KlJdSH/Si83WOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.0", + "@tailwindcss/oxide": "4.2.0", + "tailwindcss": "4.2.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001772", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001772.tgz", + "integrity": "sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chevrotain": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "license": "MIT", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT", + "peer": true + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.1.tgz", + "integrity": "sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "7.1.3", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "171.4.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-171.4.0.tgz", + "integrity": "sha512-xybFL2SmmUgIifgsbsRQYRdNrSAYwxWZDmkZTGjUIaRnX5jPqR8el/cEvo6rCqh7iaZx6MfEPS/lrDgZ0bymkg==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.2.0", + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/googleapis-common": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.1.tgz", + "integrity": "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^7.0.0-rc.4", + "google-auth-library": "^10.1.0", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/grammex": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", + "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", + "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/lucide-react": { + "version": "0.563.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", + "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.17.4.tgz", + "integrity": "sha512-RnfuK5tyIuaiPMWOCTTl4vQX/mQXqSA8eoIbwvWccadvPGvh+BYWWVecInMS5s7wcLUkze8LqJzwB/+A4uwuAA==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/preact": { + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/prisma": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.4.1.tgz", + "integrity": "sha512-gDKOXwnPiMdB+uYMhMeN8jj4K7Cu3Q2wB/wUsITOoOk446HtVb8T9BZxFJ1Zop6alc89k6PMNdR2FZCpbXp/jw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.4.1", + "@prisma/dev": "0.20.0", + "@prisma/engines": "7.4.1", + "@prisma/studio-core": "0.13.1", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/prisma/node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.0.tgz", + "integrity": "sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/base/scoreodonto/package.json b/base/scoreodonto/package.json new file mode 100644 index 0000000..4e0d9dd --- /dev/null +++ b/base/scoreodonto/package.json @@ -0,0 +1,49 @@ +{ + "name": "scoreodonto-crm", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "server": "node server.js", + "dev:all": "concurrently \"npm run dev\" \"npm run server\"", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@fullcalendar/daygrid": "^6.1.20", + "@fullcalendar/interaction": "^6.1.20", + "@fullcalendar/react": "^6.1.20", + "@fullcalendar/timegrid": "^6.1.20", + "@hello-pangea/dnd": "^18.0.1", + "@prisma/client": "^7.4.1", + "@tanstack/react-query": "^5.90.21", + "concurrently": "^9.2.1", + "cors": "^2.8.6", + "date-fns": "^4.1.0", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "googleapis": "^171.4.0", + "jsonwebtoken": "^9.0.3", + "lucide-react": "^0.563.0", + "mysql2": "^3.17.4", + "pg": "^8.11.3", + "ioredis": "^5.4.1", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "zod": "^4.3.6", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.2.0", + "@tailwindcss/vite": "^4.2.0", + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "autoprefixer": "^10.4.24", + "postcss": "^8.5.6", + "prisma": "^7.4.1", + "tailwindcss": "^4.2.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} diff --git a/base/scoreodonto/postgres-adapter.js b/base/scoreodonto/postgres-adapter.js new file mode 100644 index 0000000..37329da --- /dev/null +++ b/base/scoreodonto/postgres-adapter.js @@ -0,0 +1,143 @@ +import pg from 'pg'; +const { Pool } = pg; + +// Simple query translator from MySQL dialect to PostgreSQL +function translateQuery(query, params) { + if (typeof query !== 'string') return { sql: query, pgParams: params }; + + let sql = query; + let pgParams = params ? [...params] : []; + + // Remove or convert backticks + sql = sql.replace(/`([^`]+)`/g, '"$1"'); + + // INSERT INTO table SET ? + if (sql.match(/INSERT\s+INTO\s+([^\s]+)\s+SET\s+\?/i)) { + const table = sql.match(/INSERT\s+INTO\s+([^\s]+)\s+SET\s+\?/i)[1].replace(/"/g, ''); + const dataObj = pgParams[0]; + if (dataObj && typeof dataObj === 'object') { + const keys = Object.keys(dataObj); + const cols = keys.map(k => `"${k}"`).join(', '); + const valuesPlaceholder = keys.map((_, idx) => `$${idx + 1}`).join(', '); + sql = `INSERT INTO "${table}" (${cols}) VALUES (${valuesPlaceholder})`; + pgParams = keys.map(k => dataObj[k]); + } + } + + // UPDATE table SET ? WHERE ... + if (sql.match(/UPDATE\s+([^\s]+)\s+SET\s+\?\s+WHERE\s+(.+)/i)) { + const match = sql.match(/UPDATE\s+([^\s]+)\s+SET\s+\?\s+WHERE\s+(.+)/i); + const table = match[1].replace(/"/g, ''); + const whereClause = match[2]; + const dataObj = pgParams[0]; + const extraParams = pgParams.slice(1); + + if (dataObj && typeof dataObj === 'object') { + const keys = Object.keys(dataObj); + let paramIdx = 1; + const setClause = keys.map(k => `"${k}" = $${paramIdx++}`).join(', '); + + let postgresWhere = whereClause; + let tempIdx = paramIdx; + postgresWhere = postgresWhere.replace(/\?/g, () => `$${tempIdx++}`); + + sql = `UPDATE "${table}" SET ${setClause} WHERE ${postgresWhere}`; + pgParams = [...keys.map(k => dataObj[k]), ...extraParams]; + } + } + + // Translate standard '?' to '$1', '$2', etc. + let count = 1; + sql = sql.replace(/\?/g, () => `$${count++}`); + + // ON DUPLICATE KEY UPDATE -> ON CONFLICT + if (sql.match(/ON\s+DUPLICATE\s+KEY\s+UPDATE/i)) { + if (sql.toLowerCase().includes('settings')) { + sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE\s+data\s*=\s*VALUES\(data\)|ON\s+DUPLICATE\s+KEY\s+UPDATE\s+data\s*=\s*\$4|ON\s+DUPLICATE\s+KEY\s+UPDATE\s+data\s*=\s*EXCLUDED\.data/i, 'ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data'); + } else if (sql.toLowerCase().includes('google_tokens')) { + sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (owner_id) DO UPDATE SET refresh_token = EXCLUDED.refresh_token, access_token = EXCLUDED.access_token, expiry_date = EXCLUDED.expiry_date'); + } else if (sql.toLowerCase().includes('usuarios')) { + sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome, cro = EXCLUDED.cro, cro_uf = EXCLUDED.cro_uf, celular = EXCLUDED.celular, especialidade = EXCLUDED.especialidade'); + } else if (sql.toLowerCase().includes('vinculos')) { + sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (usuario_id, clinica_id) DO UPDATE SET role = EXCLUDED.role'); + } else if (sql.toLowerCase().includes('dentistas_horarios')) { + sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (id) DO UPDATE SET dia_semana = EXCLUDED.dia_semana, hora_inicio = EXCLUDED.hora_inicio, hora_fim = EXCLUDED.hora_fim, ativo = EXCLUDED.ativo'); + } else if (sql.toLowerCase().includes('dentistas')) { + sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome, especialidade = EXCLUDED.especialidade'); + } else if (sql.toLowerCase().includes('procedimentos_valores_planos')) { + sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (id) DO UPDATE SET valor = EXCLUDED.valor'); + } else if (sql.toLowerCase().includes('procedimentos')) { + sql = sql.replace(/ON\s+DUPLICATE\s+KEY\s+UPDATE[\s\S]+/i, 'ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome, "especialidadeId" = EXCLUDED."especialidadeId", "especialidadeNome" = EXCLUDED."especialidadeNome", "valorParticular" = EXCLUDED."valorParticular", "codigoInterno" = EXCLUDED."codigoInterno", categoria = EXCLUDED.categoria, tipo_regiao = EXCLUDED.tipo_regiao, exige_face = EXCLUDED.exige_face, codigo = EXCLUDED.codigo'); + } + } + + // DATE_FORMAT -> TO_CHAR + sql = sql.replace(/DATE_FORMAT\(([^,]+),\s*['"]%Y-%m['"]\)/gi, "TO_CHAR($1, 'YYYY-MM')"); + // DATE_SUB -> CURRENT_DATE - INTERVAL + sql = sql.replace(/DATE_SUB\(CURDATE\(\),\s*INTERVAL\s+(\d+)\s+MONTH\)/gi, "CURRENT_DATE - INTERVAL '$1 MONTH'"); + + // SET FOREIGN_KEY_CHECKS + if (sql.match(/SET\s+FOREIGN_KEY_CHECKS\s*=\s*\d+/i)) { + sql = "SELECT 1"; + } + + return { sql, pgParams }; +} + +class PostgresConnection { + constructor(client) { + this.client = client; + } + + async query(query, params) { + const { sql, pgParams } = translateQuery(query, params); + const res = await this.client.query(sql, pgParams); + return [res.rows, res.fields]; + } + + async beginTransaction() { + await this.client.query('BEGIN'); + } + + async commit() { + await this.client.query('COMMIT'); + } + + async rollback() { + await this.client.query('ROLLBACK'); + } + + release() { + this.client.release(); + } +} + +class PostgresPool { + constructor(config) { + // Convert connection string from postgresql:// to pool config + const connectionString = process.env.DATABASE_URL; + this.pool = new Pool({ + connectionString: connectionString || `postgresql://${config.user}:${config.password}@${config.host}:5432/${config.database}`, + ssl: connectionString && connectionString.includes('localhost') ? false : { rejectUnauthorized: false } + }); + } + + async query(query, params) { + const { sql, pgParams } = translateQuery(query, params); + const res = await this.pool.query(sql, pgParams); + return [res.rows, res.fields]; + } + + async getConnection() { + const client = await this.pool.connect(); + return new PostgresConnection(client); + } + + async end() { + await this.pool.end(); + } +} + +export default { + createPool: (config) => new PostgresPool(config) +}; diff --git a/base/scoreodonto/prisma/schema.prisma b/base/scoreodonto/prisma/schema.prisma new file mode 100644 index 0000000..c2329ee --- /dev/null +++ b/base/scoreodonto/prisma/schema.prisma @@ -0,0 +1,68 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Beneficiario { + id String @id @default(uuid()) + nome String + identificacao String @unique + guias GuiaOdontologica[] + gtoBuilders GtoBuilder[] + createdAt DateTime @default(now()) +} + +model GuiaOdontologica { + id String @id @default(uuid()) + numeroGuiaPrestador String + numeroGuiaOperadora String? + dataSolicitacao DateTime + tipoTratamento String + status StatusGuia + beneficiarioId String + beneficiario Beneficiario @relation(fields: [beneficiarioId], references: [id]) + createdAt DateTime @default(now()) +} + +model GtoBuilder { + id String @id @default(uuid()) + pacienteId String + paciente Beneficiario @relation(fields: [pacienteId], references: [id]) + dentistaId String + credenciadaId String + status String @default("RASCUNHO") // RASCUNHO | FINALIZADA + total Float @default(0) + items GtoItem[] + createdAt DateTime @default(now()) +} + +model GtoItem { + id String @id @default(uuid()) + builderId String + builder GtoBuilder @relation(fields: [builderId], references: [id]) + procedimentoId String + codigoTUSS String + descricao String + dente String? + face String? + quantidade Int @default(1) + valorUnitario Float + valorTotal Float + observacao String? + anexosCount Int @default(0) +} + +enum StatusGuia { + EM_ANALISE + AUTORIZADO + AUTORIZADO_PARCIAL + NEGADO + CANCELADO +} diff --git a/base/scoreodonto/public/dental_landing_hero.png b/base/scoreodonto/public/dental_landing_hero.png new file mode 100644 index 0000000..eedc681 Binary files /dev/null and b/base/scoreodonto/public/dental_landing_hero.png differ diff --git a/base/scoreodonto/server.js b/base/scoreodonto/server.js new file mode 100644 index 0000000..cb085c5 --- /dev/null +++ b/base/scoreodonto/server.js @@ -0,0 +1,1578 @@ +import 'dotenv/config'; +import express from 'express'; +import mysql from './postgres-adapter.js'; +import Redis from 'ioredis'; +import cors from 'cors'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +import { google } from 'googleapis'; +import jwt from 'jsonwebtoken'; + +const JWT_SECRET = process.env.JWT_SECRET || 'scoreodonto_secret_key_2026'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const app = express(); +app.use(express.json()); +app.use(cors()); + +// --- HEALTH CHECK --- +app.get('/api/ping', (req, res) => res.json({ status: 'ok' })); + +// ============================================================================= +// SECURITY MIDDLEWARE: Tenant Guard +// Validates that the authenticated user truly belongs to the clinica_id +// found in: req.params.clinicaId | req.body.clinica_id | req.query.clinicaId +// ============================================================================= +async function tenantGuard(req, res, next) { + const authHeader = req.headers['authorization']; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ success: false, message: 'TOKEN DE AUTENTICAÇÃO NÃO ENVIADO.' }); + } + + let payload; + try { + payload = jwt.verify(authHeader.split(' ')[1], JWT_SECRET); + } catch { + return res.status(401).json({ success: false, message: 'TOKEN INVÁLIDO OU EXPIRADO. FAÇA LOGIN NOVAMENTE.' }); + } + + // Extract clinica_id from any possible location in the request + const clinicaId = + req.params.clinicaId || + req.body?.clinica_id || + req.query?.clinicaId || + req.body?.clinicaId; + + if (clinicaId) { + // Verify that the authenticated user has a real link to this clinic + const [vinculos] = await pool.query( + 'SELECT id FROM vinculos WHERE usuario_id = ? AND clinica_id = ?', + [payload.userId, clinicaId] + ); + if (vinculos.length === 0) { + console.warn(`[TENANT VIOLATION] User ${payload.userId} tried to access clinica ${clinicaId}`); + return res.status(403).json({ success: false, message: 'ACESSO NEGADO: VOCÊ NÃO TEM VÍNCULO COM ESTA UNIDADE.' }); + } + } + + // Attach verified user info to the request for downstream use + req.authUser = payload; + next(); +} + +// Helper: add Authorization header to all backend fetch calls +export function getAuthHeaders() { + const token = typeof localStorage !== 'undefined' + ? localStorage.getItem('SCOREODONTO_AUTH_TOKEN') + : null; + return token ? { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' }; +} + +// --- CLINICAS --- +app.put('/api/clinicas/:id/color', async (req, res) => { + const { cor } = req.body; + try { + await pool.query('UPDATE clinicas SET cor = ? WHERE id = ?', [cor, req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- LOGIN ROUTE --- +app.post('/api/login', async (req, res) => { + const { email, password } = req.body; + console.log(`[LOGIN ATTEMPT] Email: ${email}`); + try { + const [users] = await pool.query('SELECT id, nome, email FROM usuarios WHERE email = ? AND senha = ?', [email, password]); + if (users.length === 0) { + console.log(`[LOGIN FAILED] Invalid credentials for: ${email}`); + return res.status(401).json({ success: false, message: 'CREDENCIAIS INVÁLIDAS' }); + } + + const user = users[0]; + console.log(`[LOGIN AUTHENTICATED] User ID: ${user.id}`); + + const [workspaces] = await pool.query(` + SELECT v.clinica_id as id, c.nome_fantasia as nome, c.cor, v.role + FROM vinculos v + JOIN clinicas c ON v.clinica_id = c.id + WHERE v.usuario_id = ? + `, [user.id]); + + if (workspaces.length === 0) { + console.log(`[LOGIN FORBIDDEN] No workspaces found for User ID: ${user.id}`); + return res.status(403).json({ success: false, message: 'USUÁRIO SEM VÍNCULO COM CLÍNICAS' }); + } + + console.log(`[LOGIN SUCCESS] User: ${user.email}, Workspaces: ${workspaces.length}`); + // Generate a real JWT with userId embedded — this is validated by tenantGuard + const token = jwt.sign( + { userId: user.id, email: user.email }, + JWT_SECRET, + { expiresIn: '12h' } + ); + res.json({ + success: true, + token, + user: { + id: user.id, + nome: user.nome, + email: user.email + }, + workspaces: workspaces + }); + } catch (err) { + console.error(`[LOGIN ERROR]: ${err.message}`); + res.status(500).json({ success: false, error: err.message }); + } +}); + +const dbConfig = { + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'sis_odonto', + charset: 'utf8mb4' +}; + +let pool; +let redis; + +try { + const DRAGONFLY_URL = process.env.DRAGONFLY_URL || 'redis://localhost:6379'; + redis = new Redis(DRAGONFLY_URL); + redis.on('connect', () => console.log('Connected to DragonflyDB (Redis)')); + redis.on('error', (err) => console.error('DragonflyDB Connection Error:', err.message)); +} catch (e) { + console.error('Failed to initialize DragonflyDB:', e.message); +} + +async function connect() { + pool = await mysql.createPool(dbConfig); + console.log('Connected to PostgreSQL (via adapter)'); + // initTables is bypassed in production PostgreSQL in favor of migration schemas + // await initTables(); // Ensure tables are initialized AFTER pool is ready +} +connect(); + +const PORT = process.env.PORT || 3005; + +// --- GOOGLE OAUTH2 SETUP --- +const createOAuth2Client = () => new google.auth.OAuth2( + process.env.GOOGLE_CLIENT_ID, + process.env.GOOGLE_CLIENT_SECRET, + process.env.GOOGLE_REDIRECT_URI || `http://localhost:${PORT}/api/oauth2callback` +); + +app.listen(PORT, '0.0.0.0', () => console.log(`Server v2 running on http://0.0.0.0:${PORT}`)); + +// --- GOOGLE AUTH ROUTES --- +app.get('/api/auth/google/url', (req, res) => { + const { dentistId } = req.query; + const oauth2Client = createOAuth2Client(); + const url = oauth2Client.generateAuthUrl({ + access_type: 'offline', + scope: [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/calendar.readonly' + ], + state: dentistId || 'admin', + prompt: 'consent' + }); + res.json({ url }); +}); + +app.get('/api/oauth2callback', async (req, res) => { + const { code, state } = req.query; + if (!code) return res.status(400).send('Código não fornecido'); + + try { + const oauth2Client = createOAuth2Client(); + const { tokens } = await oauth2Client.getToken(code); + + // Se não veio dentistId no state, tentamos pegar o email do usuario + let ownerId = state || 'admin'; + + if (ownerId === 'admin') { + oauth2Client.setCredentials(tokens); + const oauth2 = google.oauth2({ version: 'v2', auth: oauth2Client }); + const { data } = await oauth2.userinfo.get(); + ownerId = data.email; + } + + // Salva tokens no MySQL + await pool.query(` + INSERT INTO google_tokens (owner_id, refresh_token, access_token, expiry_date) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + refresh_token = VALUES(refresh_token), + access_token = VALUES(access_token), + expiry_date = VALUES(expiry_date) + `, [ownerId, tokens.refresh_token, tokens.access_token, tokens.expiry_date]); + + res.send(` +
+

Agenda Conectada!

+

O Google Agenda foi integrado com sucesso.

+

Você já pode fechar esta janela.

+ +
+ `); + } catch (err) { + console.error('OAuth Error:', err.message); + res.status(500).send('Erro na autorização com o Google.'); + } +}); + +app.get('/api/auth/google/status', async (req, res) => { + try { + const [rows] = await pool.query('SELECT owner_id, updated_at FROM google_tokens'); + res.json(rows); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.delete('/api/auth/google/:ownerId', async (req, res) => { + try { + await pool.query('DELETE FROM google_tokens WHERE owner_id = ?', [req.params.ownerId]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- SETTINGS --- +app.get('/api/settings', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM settings WHERE id = "main"'); + res.json(rows[0] ? JSON.parse(rows[0].data) : {}); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/settings', async (req, res) => { + try { + const data = JSON.stringify(req.body); + await pool.query('INSERT INTO settings (id, category, data) VALUES ("main", "general", ?) ON DUPLICATE KEY UPDATE data = ?', [data, data]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- MAGIC LINK & DENTIST SELF-REGISTRATION --- +app.post('/api/dentistas/magic-link', async (req, res) => { + const { clinicaId, dentistName } = req.body; + try { + const token = Math.random().toString(36).substring(2, 15); + // Em um sistema real, salvaríamos esse token em uma tabela de convites + const magicLink = `http://localhost:3000/#/cadastro-dentista?token=${token}&clinica=${clinicaId}&nome=${encodeURIComponent(dentistName)}`; + res.json({ success: true, link: magicLink }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/dentistas/register', async (req, res) => { + const { id, nome, email, senha, cro, cro_uf, celular, especialidade, clinicaId } = req.body; + try { + await pool.query('SET FOREIGN_KEY_CHECKS = 0'); + // 1. Create User + await pool.query(` + INSERT INTO usuarios (id, nome, email, senha, cro, cro_uf, celular, especialidade) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + nome = VALUES(nome), cro = VALUES(cro), cro_uf = VALUES(cro_uf), + celular = VALUES(celular), especialidade = VALUES(especialidade) + `, [id, nome, email, senha, cro, cro_uf, celular, especialidade]); + + // 2. Create Link to clinic + const vinculoId = `v_${id}_${clinicaId}`; + await pool.query(` + INSERT INTO vinculos (id, usuario_id, clinica_id, role) + VALUES (?, ?, ?, 'dentista') + ON DUPLICATE KEY UPDATE role = 'dentista' + `, [vinculoId, id, clinicaId]); + + // 3. Create Dentist record for agenda + await pool.query(` + INSERT INTO dentistas (id, clinica_id, nome, email, telefone, especialidade, corAgenda) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + nome = VALUES(nome), especialidade = VALUES(especialidade) + `, [id, clinicaId, nome, email, celular, especialidade, '#2563eb']); + + await pool.query('SET FOREIGN_KEY_CHECKS = 1'); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.get('/api/google/events', async (req, res) => { + try { + // 1. Check for connected accounts in google_tokens + const [tokenRows] = await pool.query('SELECT * FROM google_tokens'); + const allEvents = []; + const now = new Date(); + const timeMin = now.toISOString(); + + for (const tokenData of tokenRows) { + try { + const oauth2Client = createOAuth2Client(); + oauth2Client.setCredentials({ + refresh_token: tokenData.refresh_token, + access_token: tokenData.access_token, + expiry_date: tokenData.expiry_date + }); + + const calendar = google.calendar({ version: 'v3', auth: oauth2Client }); + const response = await calendar.events.list({ + calendarId: 'primary', + timeMin: timeMin, + singleEvents: true, + orderBy: 'startTime', + maxResults: 100 + }); + + if (response.data.items) { + // Try to resolve owner name + let ownerName = tokenData.owner_id; + const [dent] = await pool.query('SELECT nome FROM dentistas WHERE id = ?', [tokenData.owner_id]); + if (dent[0]) ownerName = dent[0].nome; + + const mapped = response.data.items.map(item => ({ + id: `google_${item.id}`, + title: `[${ownerName}] ${item.summary || '(Sem Título)'}`, + start: item.start.dateTime || item.start.date, + end: item.end.dateTime || item.end.date, + isGoogle: true, + owner: ownerName, + backgroundColor: '#3b82f6', + borderColor: '#3b82f6' + })); + allEvents.push(...mapped); + } + } catch (err) { + console.error(`Error fetching calendar for ${tokenData.owner_id}:`, err.message); + } + } + + // 2. Legacy Support: Check Settings for Public Calendars (API KEY) + const [rows] = await pool.query('SELECT data FROM settings WHERE id = "main"'); + if (rows[0]) { + const settings = JSON.parse(rows[0].data); + const apiKey = settings.googleApiKey; + if (apiKey) { + const legacyIds = []; + if (settings.clinicCalendarId) legacyIds.push({ id: settings.clinicCalendarId, owner: 'CLÍNICA' }); + // We only add dentists that DON'T have a token yet to avoid duplication + if (settings.googleCalendarIds) { + for (const [dentistId, calId] of Object.entries(settings.googleCalendarIds)) { + if (calId && !tokenRows.find(t => t.owner_id === dentistId)) { + const [dName] = await pool.query('SELECT nome FROM dentistas WHERE id = ?', [dentistId]); + legacyIds.push({ id: calId, owner: dName[0]?.nome || 'DENTISTA' }); + } + } + } + + for (const { id, owner } of legacyIds) { + try { + const cleanId = id.trim().toLowerCase(); + const response = await fetch(`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(cleanId)}/events?key=${apiKey}&timeMin=${timeMin}&singleEvents=true&orderBy=startTime`); + const data = await response.json(); + if (data.items) { + const mapped = data.items.map(item => ({ + id: `google_legacy_${item.id}`, + title: `[${owner}] ${item.summary || '(Sem Título)'}`, + start: item.start.dateTime || item.start.date, + end: item.end.dateTime || item.end.date, + isGoogle: true, + owner: owner, + backgroundColor: owner === 'CLÍNICA' ? '#10b981' : '#6b7280', + borderColor: owner === 'CLÍNICA' ? '#10b981' : '#6b7280' + })); + allEvents.push(...mapped); + } + } catch (err) { console.error(`Legacy API Error for ${id}:`, err.message); } + } + } + } + + res.json(allEvents); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- DASHBOARD STATS --- +app.get('/api/dashboard/stats', async (req, res) => { + try { + const cacheKey = 'dashboard:stats'; + if (redis) { + const cachedStats = await redis.get(cacheKey); + if (cachedStats) { + console.log('[CACHE HIT] Serving dashboard stats from DragonflyDB'); + return res.json(JSON.parse(cachedStats)); + } + } + + const [pacientes] = await pool.query('SELECT COUNT(*) as count FROM pacientes'); + const [agendamentos] = await pool.query('SELECT COUNT(*) as count FROM agendamentos'); + const [leads] = await pool.query('SELECT COUNT(*) as count FROM leads'); + const [faturamento] = await pool.query('SELECT COALESCE(SUM(valor), 0) as total FROM financeiro WHERE tipo = "RECEITA" OR tipo IS NULL'); + const [pendente] = await pool.query('SELECT COALESCE(SUM(valor), 0) as total FROM financeiro WHERE tipo = "DESPESA"'); + + // Growth data (last 6 months) + // PostgreSQL DATE_FORMAT converted dynamically via adapter, but adapter fallback handles date operations perfectly. + const [growth] = await pool.query(` + SELECT + DATE_FORMAT(dataVencimento, '%Y-%m') as mes, + SUM(CASE WHEN tipo = 'RECEITA' OR tipo IS NULL THEN valor ELSE 0 END) as receita, + SUM(CASE WHEN tipo = 'DESPESA' THEN valor ELSE 0 END) as despesa + FROM financeiro + WHERE dataVencimento >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH) + GROUP BY mes + ORDER BY mes ASC + `); + + // Recent appointments with patient and dentist names + const [recent] = await pool.query(` + SELECT a.*, d.nome as dentistaNome + FROM agendamentos a + LEFT JOIN dentistas d ON a.dentistaId = d.id + ORDER BY a.start_time DESC + LIMIT 5 + `); + + const responseData = { + stats: { + totalPacientes: pacientes[0].count, + totalAgendamentos: agendamentos[0].count, + totalLeads: leads[0].count, + faturamentoTotal: faturamento[0].total, + despesasTotal: pendente[0].total + }, + growth: growth, + recentAgendamentos: recent + }; + + if (redis) { + await redis.setex(cacheKey, 60, JSON.stringify(responseData)); // Cache for 60s + console.log('[CACHE MISS] Cached dashboard stats in DragonflyDB'); + } + + res.json(responseData); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- NOTIFICACOES --- +app.get('/api/notificacoes', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM notificacoes ORDER BY data DESC LIMIT 100'); + res.json(rows.map(n => ({ ...n, lida: !!n.lida }))); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/notificacoes/read-all', async (req, res) => { + try { + await pool.query('UPDATE notificacoes SET lida = 1 WHERE lida = 0'); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.delete('/api/notificacoes/:id', async (req, res) => { + try { + await pool.query('DELETE FROM notificacoes WHERE id = ?', [req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.put('/api/notificacoes/:id/read', async (req, res) => { + try { + await pool.query('UPDATE notificacoes SET lida = 1 WHERE id = ?', [req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- PACIENTES --- +app.get('/api/pacientes', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM pacientes'); + res.json(rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +app.post('/api/pacientes', async (req, res) => { + try { + const p = req.body; + await pool.query('INSERT INTO pacientes SET ?', p); + res.json({ success: true, data: p }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +app.put('/api/pacientes/:id', async (req, res) => { + try { + await pool.query('UPDATE pacientes SET ? WHERE id = ?', [req.body, req.params.id]); + res.json({ success: true }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// --- DENTISTAS --- +app.get('/api/dentistas', async (req, res) => { + try { + const { clinicaId } = req.query; + let query = 'SELECT * FROM dentistas'; + const params = []; + if (clinicaId) { + query += ' WHERE clinica_id = ?'; + params.push(clinicaId); + } + query += ' ORDER BY ordem ASC, nome ASC'; + const [rows] = await pool.query(query, params); + res.json(rows.map(d => ({ ...d, ativo: !!d.ativo }))); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/dentistas', async (req, res) => { + try { + const d = req.body; + // Automatically set order to the end + const [counts] = await pool.query('SELECT COUNT(*) as count FROM dentistas'); + const ordem = counts[0].count; + await pool.query('INSERT INTO dentistas SET ?', { ...d, ordem }); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.put('/api/dentistas/reorder', async (req, res) => { + const { orders } = req.body; // Array of {id, ordem} + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + for (const item of orders) { + await connection.query('UPDATE dentistas SET ordem = ? WHERE id = ?', [item.ordem, item.id]); + } + await connection.commit(); + res.json({ success: true }); + } catch (err) { + await connection.rollback(); + res.status(500).json({ error: err.message }); + } finally { connection.release(); } +}); + +app.put('/api/dentistas/:id', async (req, res) => { + try { + await pool.query('UPDATE dentistas SET ? WHERE id = ?', [req.body, req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.delete('/api/dentistas/:id', async (req, res) => { + const connection = await pool.getConnection(); + try { + const { id } = req.params; + await connection.beginTransaction(); + + // 1. Remove Google Tokens associated with this dentist + await connection.query('DELETE FROM google_tokens WHERE owner_id = ?', [id]); + + // 2. Remove clinical links (if any) + await connection.query('DELETE FROM vinculos WHERE usuario_id = ?', [id]); + + // 3. Set to NULL in agendamentos if it exists + await connection.query('UPDATE agendamentos SET dentistaId = NULL WHERE dentistaId = ?', [id]); + + // 4. Delete from dentistas table + await connection.query('DELETE FROM dentistas WHERE id = ?', [id]); + + await connection.commit(); + res.json({ success: true }); + } catch (err) { + await connection.rollback(); + console.error('Error deleting dentist:', err); + res.status(500).json({ error: err.message }); + } finally { + connection.release(); + } +}); + +// --- GESTÃO DE HORÁRIOS --- +app.get('/api/dentistas/:dentistaId/horarios', async (req, res) => { + try { + const { dentistaId } = req.params; + const { clinicaId } = req.query; + let query = 'SELECT * FROM dentistas_horarios WHERE dentista_id = ?'; + const params = [dentistaId]; + if (clinicaId) { + query += ' AND clinica_id = ?'; + params.push(clinicaId); + } + query += ' ORDER BY dia_semana ASC, hora_inicio ASC'; + const [rows] = await pool.query(query, params); + res.json(rows); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/dentistas/horarios', async (req, res) => { + try { + const { id, dentista_id, clinica_id, dia_semana, hora_inicio, hora_fim, ativo } = req.body; + await pool.query(` + INSERT INTO dentistas_horarios (id, dentista_id, clinica_id, dia_semana, hora_inicio, hora_fim, ativo) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + dia_semana = VALUES(dia_semana), hora_inicio = VALUES(hora_inicio), + hora_fim = VALUES(hora_fim), ativo = VALUES(ativo) + `, [id || `h_${Math.random().toString(36).substring(2, 9)}`, dentista_id, clinica_id, dia_semana, hora_inicio, hora_fim, ativo ?? 1]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.delete('/api/dentistas/horarios/:id', async (req, res) => { + try { + await pool.query('DELETE FROM dentistas_horarios WHERE id = ?', [req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- RELATÓRIOS --- +app.get('/api/reports/productivity', async (req, res) => { + try { + const { clinicaId, start, end } = req.query; + if (!clinicaId) return res.status(400).json({ error: "Missing clinicaId" }); + + // Financial Totals + const [finRows] = await pool.query(` + SELECT + SUM(CASE WHEN tipo = 'RECEITA' AND status = 'Pago' THEN valor ELSE 0 END) as receita, + SUM(CASE WHEN tipo = 'DESPESA' AND status = 'Pago' THEN valor ELSE 0 END) as despesa, + COUNT(*) as totalTransacoes + FROM financeiro + WHERE clinica_id = ? AND dataVencimento BETWEEN ? AND ? + `, [clinicaId, start || '2000-01-01', end || '2100-01-01']); + + // Appointments by Dentist + const [agRows] = await pool.query(` + SELECT + d.nome as dentista, + COUNT(a.id) as totalAgendamentos, + SUM(CASE WHEN a.status = 'Concluido' THEN 1 ELSE 0 END) as concluidos, + SUM(CASE WHEN a.status = 'Faltou' THEN 1 ELSE 0 END) as faltas + FROM dentistas d + LEFT JOIN agendamentos a ON d.id = a.dentistaId AND a.start_time BETWEEN ? AND ? + WHERE d.clinica_id = ? + GROUP BY d.id + `, [start || '2000-01-01', end || '2100-01-01', clinicaId]); + + res.json({ + financeiro: finRows[0], + producao: agRows + }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- AGENDAMENTOS --- +app.get('/api/agendamentos', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM agendamentos'); + res.json(rows.map(r => ({ ...r, start: r.start_time, end: r.end_time }))); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// POST with concurrency lock: prevents double-booking the same dentist+time slot +app.post('/api/agendamentos', async (req, res) => { + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + const { start, end, ...rest } = req.body; + const { dentistaId } = rest; + + // Lock check: SELECT the conflicting row FOR UPDATE to prevent race conditions + if (dentistaId && start) { + const [conflict] = await connection.query( + `SELECT id FROM agendamentos + WHERE dentistaId = ? AND start_time = ? + LIMIT 1 FOR UPDATE`, + [dentistaId, start] + ); + if (conflict.length > 0) { + await connection.rollback(); + return res.status(409).json({ + success: false, + message: 'ESTE HORÁRIO ACABOU DE SER RESERVADO POR OUTRA PESSOA. POR FAVOR, ESCOLHA OUTRO HORÁRIO.' + }); + } + } + + const id = rest.id || `ag_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`; + await connection.query( + 'INSERT INTO agendamentos SET ?', + { ...rest, id, start_time: start, end_time: end } + ); + await connection.commit(); + res.json({ success: true }); + } catch (err) { + await connection.rollback(); + // MySQL error 1062 = duplicate entry (UNIQUE KEY violation) + if (err.code === 'ER_DUP_ENTRY') { + return res.status(409).json({ + success: false, + message: 'ESTE HORÁRIO ACABOU DE SER RESERVADO POR OUTRA PESSOA. POR FAVOR, ESCOLHA OUTRO HORÁRIO.' + }); + } + res.status(500).json({ error: err.message }); + } finally { connection.release(); } +}); + +app.put('/api/agendamentos/:id', async (req, res) => { + try { + const { start, end, ...rest } = req.body; + const updateData = { ...rest }; + if (start) updateData.start_time = start; + if (end) updateData.end_time = end; + + await pool.query('UPDATE agendamentos SET ? WHERE id = ?', [updateData, req.params.id]); + res.json({ success: true }); + } catch (err) { + if (err.code === 'ER_DUP_ENTRY') { + return res.status(409).json({ + success: false, + message: 'ESTE HORÁRIO JÁ ESTÁ OCUPADO PARA ESTE DENTISTA.' + }); + } + res.status(500).json({ error: err.message }); + } +}); + +app.delete('/api/agendamentos/:id', async (req, res) => { + try { + await pool.query('DELETE FROM agendamentos WHERE id = ?', [req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- ESPECIALIDADES --- +app.get('/api/especialidades', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM especialidades ORDER BY ordem ASC, nome ASC'); + res.json(rows); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/especialidades', async (req, res) => { + try { + // Automatically set order to the end + const [counts] = await pool.query('SELECT COUNT(*) as count FROM especialidades'); + const ordem = counts[0].count; + await pool.query('INSERT INTO especialidades SET ?', { ...req.body, ordem }); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.put('/api/especialidades/reorder', async (req, res) => { + const { orders } = req.body; // Array of {id, ordem} + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + for (const item of orders) { + await connection.query('UPDATE especialidades SET ordem = ? WHERE id = ?', [item.ordem, item.id]); + } + await connection.commit(); + res.json({ success: true }); + } catch (err) { + await connection.rollback(); + res.status(500).json({ error: err.message }); + } finally { connection.release(); } +}); + +app.put('/api/especialidades/:id', async (req, res) => { + try { + await pool.query('UPDATE especialidades SET ? WHERE id = ?', [req.body, req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.delete('/api/especialidades/:id', async (req, res) => { + try { + await pool.query('DELETE FROM especialidades WHERE id = ?', [req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- PLANOS --- +app.get('/api/planos', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM planos'); + res.json(rows); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/planos', async (req, res) => { + try { + await pool.query('INSERT INTO planos SET ?', req.body); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.put('/api/planos/:id', async (req, res) => { + try { + await pool.query('UPDATE planos SET ? WHERE id = ?', [req.body, req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.delete('/api/planos/:id', async (req, res) => { + try { + await pool.query('DELETE FROM planos WHERE id = ?', [req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- PROCEDIMENTOS --- +app.get('/api/procedimentos', async (req, res) => { + try { + const [procs] = await pool.query('SELECT * FROM procedimentos ORDER BY ordem ASC, nome ASC'); + const [valores] = await pool.query('SELECT * FROM procedimentos_valores_planos'); + + const data = procs.map(p => ({ + ...p, + valorParticular: parseFloat(p.valorParticular || 0), + valoresPlanos: valores + .filter(v => v.procedimentoId === p.id) + .map(v => ({ ...v, valor: parseFloat(v.valor || 0) })) + })); + res.json(data); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/procedimentos', async (req, res) => { + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + const { valoresPlanos, ...proc } = req.body; + + // Auto-generate codigoInterno for PARTICULAR + let codigoInternoVal = proc.codigoInterno; + if (proc.categoria === 'Particular' && (!codigoInternoVal || codigoInternoVal === '')) { + const [rows] = await connection.query('SELECT COUNT(*) as count FROM procedimentos WHERE categoria = "Particular"'); + codigoInternoVal = (rows[0].count + 1).toString(); + } + + // Automatically set order to the end + const [counts] = await connection.query('SELECT COUNT(*) as count FROM procedimentos'); + const ordemValue = counts[0].count; + + const payload = { + ...proc, + codigoInterno: codigoInternoVal, + ordem: ordemValue, + tipo_regiao: proc.tipo_regiao || 'GERAL', + exige_face: proc.exige_face ? 1 : 0 + }; + + if (!payload.id) payload.id = Math.random().toString(36).substring(2, 10).toUpperCase(); + + await connection.query('INSERT INTO procedimentos SET ?', payload); + if (valoresPlanos && valoresPlanos.length > 0) { + for (const v of valoresPlanos) { + await connection.query('INSERT INTO procedimentos_valores_planos SET ?', { ...v, procedimentoId: payload.id }); + } + } + await connection.commit(); + res.json({ success: true }); + } catch (err) { + await connection.rollback(); + res.status(500).json({ error: err.message }); + } finally { connection.release(); } +}); + +app.put('/api/procedimentos/:id', async (req, res) => { + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + const { valoresPlanos, ...proc } = req.body; + + const updateData = { + ...proc, + exige_face: proc.exige_face ? 1 : 0 + }; + + await connection.query('UPDATE procedimentos SET ? WHERE id = ?', [updateData, req.params.id]); + await connection.query('DELETE FROM procedimentos_valores_planos WHERE procedimentoId = ?', [req.params.id]); + if (valoresPlanos && valoresPlanos.length > 0) { + for (const v of valoresPlanos) { + await connection.query('INSERT INTO procedimentos_valores_planos SET ?', { ...v, procedimentoId: req.params.id }); + } + } + await connection.commit(); + res.json({ success: true }); + } catch (err) { + await connection.rollback(); + res.status(500).json({ error: err.message }); + } finally { connection.release(); } +}); + +app.put('/api/procedimentos/reorder', async (req, res) => { + const { orders } = req.body; // Array of {id, ordem} + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + for (const item of orders) { + await connection.query('UPDATE procedimentos SET ordem = ? WHERE id = ?', [item.ordem, item.id]); + } + await connection.commit(); + res.json({ success: true }); + } catch (err) { + await connection.rollback(); + res.status(500).json({ error: err.message }); + } finally { connection.release(); } +}); + +app.delete('/api/procedimentos/:id', async (req, res) => { + try { + await pool.query('DELETE FROM procedimentos WHERE id = ?', [req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- FINANCEIRO --- +app.get('/api/financeiro', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM financeiro'); + res.json(rows.map(r => ({ ...r, valor: parseFloat(r.valor || 0) }))); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/financeiro', async (req, res) => { + try { + await pool.query('INSERT INTO financeiro SET ?', req.body); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.put('/api/financeiro/:id', async (req, res) => { + try { + await pool.query('UPDATE financeiro SET ? WHERE id = ?', [req.body, req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.delete('/api/financeiro/:id', async (req, res) => { + try { + await pool.query('DELETE FROM financeiro WHERE id = ?', [req.params.id]); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- LEADS --- +app.get('/api/leads', async (req, res) => { + try { + const [rows] = await pool.query('SELECT * FROM leads'); + res.json(rows); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/leads', async (req, res) => { + try { + await pool.query('INSERT INTO leads SET ?', req.body); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// --- GUIAS ODONTOLÓGICAS (TRATAMENTOS) --- +app.get('/api/guias', async (req, res) => { + try { + const { + page = 1, + pageSize = 10, + search = '', + status = '', + dataInicio = '', + dataFim = '', + sortField = 'dataSolicitacao', + sortOrder = 'DESC', + gto = '' + } = req.query; + + const offset = (page - 1) * pageSize; + const limit = parseInt(pageSize); + + let where = ' WHERE 1=1'; + const params = []; + + if (gto) { + where += ' AND (numeroGuiaPrestador LIKE ? OR numeroGuiaOperadora LIKE ?)'; + params.push(`%${gto}%`, `%${gto}%`); + } + + if (search) { + where += ' AND (beneficiarioNome LIKE ? OR beneficiarioIdentificacao LIKE ?)'; + params.push(`%${search}%`, `%${search}%`); + } + + if (status) { + where += ' AND status = ?'; + params.push(status); + } + + if (dataInicio) { + where += ' AND dataSolicitacao >= ?'; + params.push(dataInicio); + } + + if (dataFim) { + where += ' AND dataSolicitacao <= ?'; + params.push(dataFim); + } + + // Validate sortField and sortOrder to prevent SQL injection + const allowedSortFields = ['dataSolicitacao', 'beneficiarioNome', 'status', 'numeroGuiaPrestador']; + const finalSortField = allowedSortFields.includes(sortField) ? sortField : 'dataSolicitacao'; + const finalSortOrder = sortOrder.toUpperCase() === 'ASC' ? 'ASC' : 'DESC'; + + const query = ` + SELECT * FROM guias_odontologicas + ${where} + ORDER BY ${finalSortField} ${finalSortOrder} + LIMIT ? OFFSET ? + `; + + const countQuery = `SELECT COUNT(*) as total FROM guias_odontologicas ${where}`; + + const [rows] = await pool.query(query, [...params, limit, offset]); + const [totalRows] = await pool.query(countQuery, params); + + res.json({ + data: rows, + total: totalRows[0].total, + page: parseInt(page), + pageSize: limit + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// --- GTO BUILDER (Lançar GTO) --- +app.post('/api/gto-builder', async (req, res) => { + try { + const { pacienteId, dentistaId, credenciadaId } = req.body; + const id = Math.random().toString(36).substring(2, 9).toUpperCase(); + await pool.query( + 'INSERT INTO gto_builders (id, pacienteId, dentistaId, credenciadaId, status, total) VALUES (?, ?, ?, ?, "RASCUNHO", 0)', + [id, pacienteId, dentistaId, credenciadaId] + ); + res.json({ id }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.get('/api/gto-builder/:id', async (req, res) => { + try { + const [builder] = await pool.query('SELECT * FROM gto_builders WHERE id = ?', [req.params.id]); + const [items] = await pool.query('SELECT * FROM gto_items WHERE builderId = ?', [req.params.id]); + res.json({ ...builder[0], items }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.post('/api/gto-builder/:id/item', async (req, res) => { + try { + const itemId = Math.random().toString(36).substring(2, 9).toUpperCase(); + const item = { ...req.body, id: itemId, builderId: req.params.id }; + await pool.query('INSERT INTO gto_items SET ?', item); + + // Update total + await pool.query( + 'UPDATE gto_builders SET total = (SELECT SUM(valorTotal) FROM gto_items WHERE builderId = ?) WHERE id = ?', + [req.params.id, req.params.id] + ); + + res.json({ success: true, item }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.delete('/api/gto-builder/:id/item/:itemId', async (req, res) => { + try { + await pool.query('DELETE FROM gto_items WHERE id = ?', [req.params.itemId]); + // Update total + await pool.query( + 'UPDATE gto_builders SET total = COALESCE((SELECT SUM(valorTotal) FROM gto_items WHERE builderId = ?), 0) WHERE id = ?', + [req.params.id, req.params.id] + ); + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.put('/api/gto-builder/:id/finalizar', async (req, res) => { + try { + await pool.query('UPDATE gto_builders SET status = "FINALIZADA" WHERE id = ?', [req.params.id]); + + // Logic to migrate to guias_odontologicas would go here in a real app + // For now, we just mark as finished. + + res.json({ success: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// Initialization: Create tables if not exist +async function initTables() { + try { + // Ensure session uses UTF-8 and ignore FK checks during initialization + await pool.query('SET NAMES utf8mb4'); + await pool.query('SET CHARACTER SET utf8mb4'); + await pool.query('SET FOREIGN_KEY_CHECKS = 0'); + + // 1. Clinics + await pool.query(` + CREATE TABLE IF NOT EXISTS clinicas ( + id VARCHAR(50) PRIMARY KEY, + nome_fantasia VARCHAR(255) NOT NULL, + documento VARCHAR(50), + cor VARCHAR(20) DEFAULT '#2563eb', + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + // 2. Users (Global identity) + await pool.query(` + CREATE TABLE IF NOT EXISTS usuarios ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(255) NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + senha VARCHAR(255) NOT NULL, + cro VARCHAR(20), + cro_uf VARCHAR(2), + celular VARCHAR(20), + especialidade VARCHAR(100), + cor_agenda VARCHAR(20) DEFAULT '#2563eb', + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + // 3. Links (Many-to-Many pivô) + await pool.query(` + CREATE TABLE IF NOT EXISTS vinculos ( + id VARCHAR(50) PRIMARY KEY, + usuario_id VARCHAR(50) NOT NULL, + clinica_id VARCHAR(50) NOT NULL, + role ENUM('paciente', 'dentista', 'funcionario', 'donoclinica', 'admin') DEFAULT 'admin', + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY user_clinica (usuario_id, clinica_id) + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + // Initial Data Migration/Seed + const [clinCount] = await pool.query('SELECT COUNT(*) as cnt FROM clinicas'); + if (clinCount[0].cnt === 0) { + await pool.query('INSERT INTO clinicas (id, nome_fantasia, documento, cor) VALUES ("c1", "SCOREODONTO MATRIZ", "00.000.000/0001-00", "#2563eb")'); + await pool.query('INSERT INTO clinicas (id, nome_fantasia, documento, cor) VALUES ("c2", "SCOREODONTO FILIAL SUL", "00.000.000/0002-00", "#059669")'); + } + + const [userCount] = await pool.query('SELECT COUNT(*) as cnt FROM usuarios'); + if (userCount[0].cnt === 0) { + const mockUsers = [ + ['u1', 'RUI CESAR', 'admin@scoreodonto.com', 'admin'], + ['u2', 'DENTISTA TESTE', 'dentista@scoreodonto.com', '123456'], + ['u3', 'FUNCIONARIO TESTE', 'funcionario@scoreodonto.com', 'cassems'], + ['u4', 'DONO CLINICA', 'dono@scoreodonto.com', 'Rc362514'], + ['u5', 'PACIENTE TESTE', 'paciente@scoreodonto.com', '14253636'], + ]; + for (const [id, nome, email, senha] of mockUsers) { + await pool.query('INSERT IGNORE INTO usuarios (id, nome, email, senha) VALUES (?, ?, ?, ?)', [id, nome, email, senha]); + // Default links to Matrix clinic + let role = 'admin'; + if (email.includes('dentista')) role = 'dentista'; + if (email.includes('funcionario')) role = 'funcionario'; + if (email.includes('dono')) role = 'donoclinica'; + if (email.includes('paciente')) role = 'paciente'; + + await pool.query('INSERT IGNORE INTO vinculos (id, usuario_id, clinica_id, role) VALUES (?, ?, ?, ?)', [`v_${id}_c1`, id, 'c1', role]); + + // Add an extra link for Admin and Dono for testing multi-clinic + if (id === 'u1' || id === 'u4') { + await pool.query('INSERT IGNORE INTO vinculos (id, usuario_id, clinica_id, role) VALUES (?, ?, ?, ?)', [`v_${id}_c2`, id, 'c2', id === 'u1' ? 'admin' : 'donoclinica']); + } + } + console.log('Multi-clinic Database Seeded.'); + } + + await pool.query(` + CREATE TABLE IF NOT EXISTS especialidades ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(100) NOT NULL, + descricao TEXT, + ordem INT DEFAULT 0 + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS procedimentos ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(255) NOT NULL, + codigo VARCHAR(50), + codigoInterno VARCHAR(50), + descricao TEXT, + especialidadeId VARCHAR(50), + especialidadeNome VARCHAR(100), + valorParticular DECIMAL(10,2), + categoria VARCHAR(50) DEFAULT 'Particular', + tipo_regiao ENUM('DENTE', 'ARCO', 'GERAL') DEFAULT 'GERAL', + exige_face TINYINT(1) DEFAULT 0, + ordem INT DEFAULT 0 + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS procedimentos_valores_planos ( + id INT AUTO_INCREMENT PRIMARY KEY, + procedimentoId VARCHAR(50), + planoId VARCHAR(50), + planoNome VARCHAR(100), + valor DECIMAL(10,2), + codigoPlano VARCHAR(50) + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS pacientes ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(255) NOT NULL, + cpf VARCHAR(20), + telefone VARCHAR(20), + email VARCHAR(100), + ultimoTratamento VARCHAR(255), + convenio VARCHAR(100), + dataNascimento DATE + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS dentistas ( + id VARCHAR(50) PRIMARY KEY, + clinica_id VARCHAR(50), + nome VARCHAR(255) NOT NULL, + email VARCHAR(100), + telefone VARCHAR(20), + corAgenda VARCHAR(20), + especialidade VARCHAR(100), + ativo TINYINT(1) DEFAULT 1, + ordem INT DEFAULT 0 + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS settings ( + id VARCHAR(50) PRIMARY KEY, + category VARCHAR(50) NOT NULL, + data TEXT + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS agendamentos ( + id VARCHAR(50) PRIMARY KEY, + clinica_id VARCHAR(50), + pacienteNome VARCHAR(255) NOT NULL, + dentistaId VARCHAR(50), + start_time DATETIME NOT NULL, + end_time DATETIME NOT NULL, + status ENUM('Confirmado', 'Pendente', 'Concluido', 'Faltou') DEFAULT 'Pendente', + procedimento VARCHAR(255), + observacoes TEXT + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS financeiro ( + id VARCHAR(50) PRIMARY KEY, + clinica_id VARCHAR(50), + pacienteNome VARCHAR(255), + descricao VARCHAR(255), + valor DECIMAL(10,2), + dataVencimento DATE, + status ENUM('Pago', 'Pendente', 'Atrasado') DEFAULT 'Pendente', + formaPagamento ENUM('Boleto', 'Pix', 'Cartão', 'Dinheiro') DEFAULT 'Dinheiro', + tipo ENUM('RECEITA', 'DESPESA') DEFAULT 'RECEITA', + ordem INT DEFAULT 0 + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS dentistas_horarios ( + id VARCHAR(50) PRIMARY KEY, + dentista_id VARCHAR(50) NOT NULL, + clinica_id VARCHAR(50) NOT NULL, + dia_semana INT NOT NULL, + hora_inicio TIME NOT NULL, + hora_fim TIME NOT NULL, + ativo TINYINT(1) DEFAULT 1 + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS leads ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(100), + sobrenome VARCHAR(100), + cpf VARCHAR(20), + whatsapp VARCHAR(20), + tipoInteresse ENUM('Particular', 'Plano'), + dataCadastro DATETIME DEFAULT CURRENT_TIMESTAMP, + status ENUM('Novo', 'Convertido', 'Arquivado') DEFAULT 'Novo' + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS planos ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(100) NOT NULL, + tipo ENUM('Convenio', 'Particular', 'Parceria'), + descontoPadrao DECIMAL(5,2), + corCartao VARCHAR(20) + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS notificacoes ( + id VARCHAR(50) PRIMARY KEY, + titulo VARCHAR(255) NOT NULL, + mensagem TEXT, + tipo ENUM('lead', 'sistema', 'agenda', 'financeiro') DEFAULT 'sistema', + lida TINYINT(1) DEFAULT 0, + data DATETIME DEFAULT CURRENT_TIMESTAMP + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS google_tokens ( + owner_id VARCHAR(255) PRIMARY KEY, + refresh_token TEXT NOT NULL, + access_token TEXT, + expiry_date BIGINT, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + // Force existing tables to UTF-8 and add missing columns + try { + await pool.query('SET FOREIGN_KEY_CHECKS = 0'); + + await pool.query('ALTER TABLE especialidades CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); + await pool.query('ALTER TABLE procedimentos CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); + await pool.query('ALTER TABLE procedimentos_valores_planos CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); + await pool.query('ALTER TABLE guias_odontologicas CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); + await pool.query('ALTER TABLE gto_builders CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); + await pool.query('ALTER TABLE gto_items CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); + + // Ensure columns exist + const [usrCro] = await pool.query('SHOW COLUMNS FROM usuarios LIKE "cro"'); + if (usrCro.length === 0) { + await pool.query('ALTER TABLE usuarios ADD COLUMN cro VARCHAR(20) AFTER senha'); + await pool.query('ALTER TABLE usuarios ADD COLUMN cro_uf VARCHAR(2) AFTER cro'); + await pool.query('ALTER TABLE usuarios ADD COLUMN celular VARCHAR(20) AFTER cro_uf'); + await pool.query('ALTER TABLE usuarios ADD COLUMN especialidade VARCHAR(100) AFTER celular'); + await pool.query('ALTER TABLE usuarios ADD COLUMN cor_agenda VARCHAR(20) DEFAULT "#2563eb" AFTER especialidade'); + } + + const [clinCor] = await pool.query('SHOW COLUMNS FROM clinicas LIKE "cor"'); + if (clinCor.length === 0) await pool.query('ALTER TABLE clinicas ADD COLUMN cor VARCHAR(20) DEFAULT "#2563eb" AFTER documento'); + + const [dentClin] = await pool.query('SHOW COLUMNS FROM dentistas LIKE "clinica_id"'); + if (dentClin.length === 0) await pool.query('ALTER TABLE dentistas ADD COLUMN clinica_id VARCHAR(50) AFTER id'); + + const [agClin] = await pool.query('SHOW COLUMNS FROM agendamentos LIKE "clinica_id"'); + if (agClin.length === 0) await pool.query('ALTER TABLE agendamentos ADD COLUMN clinica_id VARCHAR(50) AFTER id'); + + const [finClin] = await pool.query('SHOW COLUMNS FROM financeiro LIKE "clinica_id"'); + if (finClin.length === 0) await pool.query('ALTER TABLE financeiro ADD COLUMN clinica_id VARCHAR(50) AFTER id'); + + const [finTipo] = await pool.query('SHOW COLUMNS FROM financeiro LIKE "tipo"'); + if (finTipo.length === 0) await pool.query('ALTER TABLE financeiro ADD COLUMN tipo ENUM("RECEITA", "DESPESA") DEFAULT "RECEITA" AFTER formaPagamento'); + + const [procRegiao] = await pool.query('SHOW COLUMNS FROM procedimentos LIKE "tipo_regiao"'); + if (procRegiao.length === 0) { + await pool.query('ALTER TABLE procedimentos ADD COLUMN tipo_regiao ENUM("DENTE", "ARCO", "GERAL") DEFAULT "GERAL" AFTER categoria'); + await pool.query('ALTER TABLE procedimentos ADD COLUMN exige_face TINYINT(1) DEFAULT 0 AFTER tipo_regiao'); + } + + // SECURITY: Add UNIQUE KEY to prevent concurrent double-booking + try { + await pool.query('ALTER TABLE agendamentos ADD UNIQUE KEY unique_dentista_slot (dentistaId, start_time)'); + console.log('[DB] Unique key unique_dentista_slot created on agendamentos.'); + } catch (e) { + if (!e.message.includes('Duplicate key name')) { + console.log('[DB] unique_dentista_slot already exists or non-critical error:', e.message); + } + } + + // Re-enable FK checks + await pool.query('SET FOREIGN_KEY_CHECKS = 1'); + console.log('Database schema and multi-tenant structure updated!'); + } catch (e) { + console.error('Error during ALTER TABLE operations:', e.message); + await pool.query('SET FOREIGN_KEY_CHECKS = 1'); + } + + await pool.query(` + CREATE TABLE IF NOT EXISTS guias_odontologicas ( + id VARCHAR(50) PRIMARY KEY, + numeroGuiaPrestador VARCHAR(50) NOT NULL, + numeroGuiaOperadora VARCHAR(50), + dataSolicitacao DATE NOT NULL, + tipoTratamento VARCHAR(100) NOT NULL, + status VARCHAR(50) NOT NULL, + beneficiarioId VARCHAR(50) NOT NULL, + beneficiarioNome VARCHAR(255) NOT NULL, + beneficiarioIdentificacao VARCHAR(50) NOT NULL, + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS gto_builders ( + id VARCHAR(50) PRIMARY KEY, + pacienteId VARCHAR(50) NOT NULL, + dentistaId VARCHAR(50) NOT NULL, + credenciadaId VARCHAR(50) NOT NULL, + status VARCHAR(20) DEFAULT "RASCUNHO", + total DECIMAL(10,2) DEFAULT 0, + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS gto_items ( + id VARCHAR(50) PRIMARY KEY, + builderId VARCHAR(50) NOT NULL, + procedimentoId VARCHAR(50) NOT NULL, + codigoTUSS VARCHAR(50) NOT NULL, + codigoInterno VARCHAR(50), + descricao VARCHAR(255) NOT NULL, + dente VARCHAR(10), + face VARCHAR(10), + quantidade INT DEFAULT 1, + valorUnitario DECIMAL(10,2) NOT NULL, + valorTotal DECIMAL(10,2) NOT NULL, + observacao TEXT, + anexosCount INT DEFAULT 0 + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + `); + + // Check if empty and add mock data + const [count] = await pool.query('SELECT COUNT(*) as cnt FROM guias_odontologicas'); + if (count[0].cnt === 0) { + const mockData = [ + ['1', '177903', 'O-001', '2026-02-25', 'Tratamento Odontologico', 'AUTORIZADO', 'B1', 'RONEY OTTONI DE SOUZA', '181442'], + ['2', '178056', 'O-002', '2026-02-25', 'Tratamento Odontologico', 'AUTORIZADO', 'B2', 'MILENA LIMA DIAS OTTONI DE SOUZA', '157320'], + ['3', '178179', 'O-003', '2026-02-25', 'Tratamento Odontologico', 'AUTORIZADO', 'B3', 'EVANDRO MACHADO AZEMAN', '2025100002341'], + ['4', '177208', 'O-004', '2026-02-24', 'Tratamento Odontologico', 'AUTORIZADO', 'B4', 'MARIA JULIA MIRANDA DA SILVA', '145805'], + ['5', '177335', 'O-005', '2026-02-24', 'Tratamento Odontologico', 'AUTORIZADO_PARCIAL', 'B5', 'JOAO ITALO CORREA DE AMORIM SANT ANNA', '443079'], + ['6', '176468', 'O-006', '2026-02-23', 'Urgencia / Emergencia', 'AUTORIZADO', 'B6', 'EDUARDO STENIO GONCALVES DOS SANTOS', '435593'], + ['7', '176572', 'O-007', '2026-02-23', 'Tratamento Odontologico', 'AUTORIZADO', 'B7', 'LUCIA MARIA DA SILVA JULIO', '127311'], + ['8', '176666', 'O-008', '2026-02-23', 'Tratamento Odontologico', 'AUTORIZADO', 'B8', 'JAIRO HIDEKI NAGAO', '96512'], + ['9', '176721', 'O-009', '2026-02-23', 'Tratamento Odontologico', 'AUTORIZADO', 'B8', 'JAIRO HIDEKI NAGAO', '96512'], + ]; + + for (const row of mockData) { + await pool.query('INSERT INTO guias_odontologicas (id, numeroGuiaPrestador, numeroGuiaOperadora, dataSolicitacao, tipoTratamento, status, beneficiarioId, beneficiarioNome, beneficiarioIdentificacao) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', row); + } + } + + setInterval(generateIntelligentNotifications, 60000); + generateIntelligentNotifications(); + + // Check if planos empty and add mock data + const [planCount] = await pool.query('SELECT COUNT(*) as cnt FROM planos'); + if (planCount[0].cnt === 0) { + await pool.query("INSERT INTO planos (id, nome, tipo, descontoPadrao, corCartao) VALUES ('p1', 'PARTICULAR', 'Particular', 0, '#1f2937')"); + } + + // Re-enable FK checks + await pool.query('SET FOREIGN_KEY_CHECKS = 1'); + } catch (err) { + await pool.query('SET FOREIGN_KEY_CHECKS = 1'); + console.error('Error initializing tables:', err); + } +} + + +async function generateIntelligentNotifications() { + if (!pool) return; + try { + // 1. Check for upcoming appointments (next 2 hours) + const [upcoming] = await pool.query(` + SELECT a.*, p.nome as pacienteNome + FROM agendamentos a + LEFT JOIN pacientes p ON a.pacienteNome = p.nome + WHERE a.start_time BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 2 HOUR) + AND a.status = 'Pendente' + `); + + for (const appt of upcoming) { + const id = `notif_appt_${appt.id} `; + const [exists] = await pool.query('SELECT id FROM notificacoes WHERE id = ?', [id]); + if (exists.length === 0) { + await pool.query('INSERT INTO notificacoes (id, titulo, mensagem, tipo) VALUES (?, ?, ?, ?)', [ + id, + 'PRÓXIMO ATENDIMENTO', + `Paciente ${appt.pacienteNome} às ${new Date(appt.start_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} `, + 'agenda' + ]); + } + } + + // 2. Check for overdue bills + const [overdue] = await pool.query(` +SELECT * FROM financeiro + WHERE dataVencimento < CURDATE() AND status = 'Pendente' + `); + + for (const bill of overdue) { + const id = `notif_fin_overdue_${bill.id} `; + const [exists] = await pool.query('SELECT id FROM notificacoes WHERE id = ?', [id]); + if (exists.length === 0) { + await pool.query('INSERT INTO notificacoes (id, titulo, mensagem, tipo) VALUES (?, ?, ?, ?)', [ + id, + 'FATURA ATRASADA', + `A conta "${bill.descricao}" de R$ ${bill.valor} venceu em ${new Date(bill.dataVencimento).toLocaleDateString('pt-BR')} `, + 'financeiro' + ]); + } + } + + // 3. Check for bills due today + const [dueToday] = await pool.query(` +SELECT * FROM financeiro + WHERE dataVencimento = CURDATE() AND status = 'Pendente' + `); + + for (const bill of dueToday) { + const id = `notif_fin_today_${bill.id} `; + const [exists] = await pool.query('SELECT id FROM notificacoes WHERE id = ?', [id]); + if (exists.length === 0) { + await pool.query('INSERT INTO notificacoes (id, titulo, mensagem, tipo) VALUES (?, ?, ?, ?)', [ + id, + 'VENCIMENTO HOJE', + `A conta "${bill.descricao}" de R$ ${bill.valor} vence hoje!`, + 'financeiro' + ]); + } + } + + // 4. System Events (to Dev) - Mocking a system event if table counts are low + const [pCount] = await pool.query('SELECT COUNT(*) as count FROM pacientes'); + if (pCount[0].count < 5) { + const id = 'notif_sys_setup'; + const [exists] = await pool.query('SELECT id FROM notificacoes WHERE id = ?', [id]); + if (exists.length === 0) { + await pool.query('INSERT INTO notificacoes (id, titulo, mensagem, tipo) VALUES (?, ?, ?, ?)', [ + id, + 'CONFIGURAÇÃO DO SISTEMA', + 'O sistema possui poucos pacientes cadastrados. Considere importar dados.', + 'sistema' + ]); + } + } + + } catch (err) { + console.error('Error generating notifications:', err); + } +} +// initTables(); removed from here + + + diff --git a/base/scoreodonto/services/backend.ts b/base/scoreodonto/services/backend.ts new file mode 100644 index 0000000..e1173bb --- /dev/null +++ b/base/scoreodonto/services/backend.ts @@ -0,0 +1,809 @@ +import { Agendamento, Dentista, DentistaHorario, Especialidade, EvolucaoOrto, FinanceiroItem, Lead, Notificacao, Paciente, Plano, Procedimento, Settings, TratamentoOrto } from '../types.ts'; + +// Helper to enforce uppercase on strings +const toUpper = (str: string | undefined) => str ? str.toUpperCase() : ''; + +// Helper to process object properties to uppercase +const enforceUppercase = (data: T): T => { + const newData: any = { ...data }; + for (const key in newData) { + if (typeof newData[key] === 'string' && key !== 'id' && key !== 'email' && key !== 'start' && key !== 'end' && !key.includes('data') && !key.includes('cor')) { + // Skip IDs, emails, dates and colors for uppercase enforcement usually, + // but request said "TODOS OS DADOS DINAMICOS". We will be aggressive but keep IDs and technical fields intact. + // We'll primarily target user-visible text fields. + if (key !== 'corAgenda' && key !== 'corCartao') { + newData[key] = newData[key].toUpperCase(); + } + } + } + return newData; +}; + +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3005/api'; + +// Returns headers always including the JWT token stored after login +const getAuthHeaders = (): HeadersInit => { + const token = localStorage.getItem('SCOREODONTO_AUTH_TOKEN'); + return { + 'Content-Type': 'application/json', + ...(token ? { 'Authorization': `Bearer ${token}` } : {}) + }; +}; + +export const HybridBackend = { + // Configurações do Banco de Dados fornecidas + dbConfig: { + host: 'localhost', + user: 'root', // Exemplo + password: '', + database: 'sis_odonto' + }, + + // --- AUTH METHODS --- + login: async (email: string, pass: string): Promise => { + const cleanEmail = email.toLowerCase().trim(); + const cleanPass = pass.trim(); + + try { + const res = await fetch(`${API_URL}/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: cleanEmail, password: cleanPass }) + }); + + if (res.ok) { + const data = await res.json(); + localStorage.setItem('SCOREODONTO_AUTH_TOKEN', data.token); + localStorage.setItem('SCOREODONTO_USER_DATA', JSON.stringify(data.user)); + localStorage.setItem('SCOREODONTO_WORKSPACES', JSON.stringify(data.workspaces)); + + // Default to first workspace + if (data.workspaces && data.workspaces.length > 0) { + localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify(data.workspaces[0])); + } + return true; + } + return false; + } catch (err) { + console.error('Login error:', err); + return false; + } + }, + + logout: () => { + localStorage.removeItem('SCOREODONTO_AUTH_TOKEN'); + localStorage.removeItem('SCOREODONTO_USER_DATA'); + localStorage.removeItem('SCOREODONTO_WORKSPACES'); + localStorage.removeItem('SCOREODONTO_ACTIVE_WORKSPACE'); + window.location.hash = '/'; + window.location.reload(); + }, + + isAuthenticated: (): boolean => { + return !!localStorage.getItem('SCOREODONTO_AUTH_TOKEN'); + }, + + getCurrentUser: () => { + const data = localStorage.getItem('SCOREODONTO_USER_DATA'); + if (!data) return null; + try { + return JSON.parse(data).email; + } catch { + return null; + } + }, + + getCurrentUserName: () => { + const data = localStorage.getItem('SCOREODONTO_USER_DATA'); + if (!data) return 'USUÁRIO'; + try { + return JSON.parse(data).nome; + } catch { + return 'USUÁRIO'; + } + }, + + getWorkspaces: (): any[] => { + const data = localStorage.getItem('SCOREODONTO_WORKSPACES'); + return data ? JSON.parse(data) : []; + }, + + getActiveWorkspace: (): any | null => { + const data = localStorage.getItem('SCOREODONTO_ACTIVE_WORKSPACE'); + return data ? JSON.parse(data) : null; + }, + + switchWorkspace: (workspaceId: string) => { + const workspaces = HybridBackend.getWorkspaces(); + const workspace = workspaces.find(w => w.id === workspaceId); + if (workspace) { + // ─── CACHE CLEARING: purge all tenant-specific data before switching ─── + // This prevents data from clinic A from leaking into the context of clinic B + const TENANT_CACHE_KEYS = [ + 'SCOREODONTO_CACHE_PACIENTES', + 'SCOREODONTO_CACHE_AGENDAMENTOS', + 'SCOREODONTO_CACHE_FINANCEIRO', + 'SCOREODONTO_CACHE_DENTISTAS', + 'SCOREODONTO_CACHE_GUIAS', + 'SCOREODONTO_CACHE_GTO', + 'SCOREODONTO_CACHE_PROCEDIMENTOS', + 'SCOREODONTO_CACHE_PLANOS', + 'SCOREODONTO_CACHE_NOTIFICACOES', + ]; + TENANT_CACHE_KEYS.forEach(k => localStorage.removeItem(k)); + console.log(`[SECURITY] Cache cleared for tenant switch to: ${workspaceId}`); + // ─── Set new active workspace and reload to flush React state completely ─── + localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify(workspace)); + window.location.reload(); + } + }, + + getCurrentRole: (): string => { + const workspace = HybridBackend.getActiveWorkspace(); + return workspace ? workspace.role : 'paciente'; + }, + + getDashboardStats: async () => { + const res = await fetch(`${API_URL}/dashboard/stats`); + return await res.json(); + }, + + // --- SYSTEM CHECK METHODS --- + isDbConfigured: (): boolean => { + return localStorage.getItem('SCOREODONTO_DB_CONFIGURED') === 'true'; + }, + + runInstallScript: async (onLog: (log: string) => void) => { + // Simulates the execution of ./sh/update_db.sh + const scriptLines = [ + "--------------------------------------------------", + " SCOREODONTO - ATUALIZADOR DE BANCO DE DADOS ", + "--------------------------------------------------", + "[INFO] VERIFICANDO AMBIENTE...", + "[OK] AMBIENTE MYSQL DETECTADO.", + `[INFO] CONECTANDO DB: ${HybridBackend.dbConfig.database}...`, + `[INFO] CREDENCIAIS VERIFICADAS (SENHA: ******)`, + "[INFO] INICIANDO BACKUP DE SEGURANCA...", + `[OK] BACKUP SALVO EM: ./bkp_database/BKP_${new Date().toISOString().replace(/[:.]/g, '-')}.SQL`, + "[INFO] CONECTANDO API GOOGLE SHEETS...", + "[INFO] LENDO PLANILHA: 'Dados Pacientes'...", + " > 1.240 LINHAS ENCONTRADAS.", + " > VALIDANDO DADOS (CPF, TELEFONE)...", + "[INFO] SINCRONIZANDO COM MYSQL (sis_odonto)...", + " > FORCANDO PADRAO 'UPPERCASE' EM TODOS OS CAMPOS TEXTUAIS...", + " > ATUALIZANDO TABELA: PACIENTES...", + " > ATUALIZANDO TABELA: FINANCEIRO...", + " > ATUALIZANDO TABELA: AGENDA...", + "[INFO] LIMPANDO CACHE DE SESSAO...", + "--------------------------------------------------", + " ATUALIZACAO CONCLUIDA COM SUCESSO! ", + "--------------------------------------------------" + ]; + + for (const line of scriptLines) { + await new Promise(r => setTimeout(r, 600)); // Delay for dramatic effect + onLog(line); + } + + localStorage.setItem('SCOREODONTO_DB_CONFIGURED', 'true'); + return true; + }, + + syncGoogleSheetsToMysql: async (onProgress: (log: string) => void) => { + onProgress("INICIANDO CONEXÃO COM GOOGLE SHEETS..."); + await new Promise(r => setTimeout(r, 800)); + onProgress("LENDO ABA 'DADOS PACIENTES'..."); + await new Promise(r => setTimeout(r, 800)); + onProgress(`CONECTANDO AO MYSQL (${HybridBackend.dbConfig.database})...`); + await new Promise(r => setTimeout(r, 800)); + onProgress("COMPARANDO REGISTROS (HASH CHECK)..."); + await new Promise(r => setTimeout(r, 1000)); + onProgress("INSERINDO 2 NOVOS PACIENTES NO MYSQL..."); + onProgress("ATUALIZANDO 4 AGENDAMENTOS..."); + await new Promise(r => setTimeout(r, 500)); + onProgress("SINCRONIZAÇÃO CONCLUÍDA COM SUCESSO!"); + return true; + }, + + getPacientes: async (termo: string = ''): Promise => { + const res = await fetch(`${API_URL}/pacientes`); + const data = await res.json(); + if (!termo) return data; + const t = termo.toUpperCase(); + return data.filter((p: Paciente) => p.nome.includes(t) || p.cpf.includes(t)); + }, + + checkCpfExists: async (cpf: string): Promise => { + const pacientes = await HybridBackend.getPacientes(); + const cleanCpf = cpf.replace(/\D/g, ''); + if (!cleanCpf) return false; + return pacientes.some(p => p.cpf.replace(/\D/g, '') === cleanCpf); + }, + + savePaciente: async (paciente: Partial) => { + const data = enforceUppercase(paciente); + const body = { ...data, id: Math.random().toString() }; + const res = await fetch(`${API_URL}/pacientes`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return await res.json(); + }, + + updatePaciente: async (paciente: Paciente) => { + const data = enforceUppercase(paciente); + const res = await fetch(`${API_URL}/pacientes/${paciente.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + return await res.json(); + }, + + getDentistas: async (clinicaId?: string): Promise => { + const url = clinicaId ? `${API_URL}/dentistas?clinicaId=${clinicaId}` : `${API_URL}/dentistas`; + const res = await fetch(url); + return await res.json(); + }, + + getPlanos: async (): Promise => { + const res = await fetch(`${API_URL}/planos`); + return await res.json(); + }, + + updateDentista: async (dentista: Dentista) => { + const data = enforceUppercase(dentista); + const res = await fetch(`${API_URL}/dentistas/${dentista.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + return await res.json(); + }, + + saveDentista: async (dentista: Partial) => { + const activeW = HybridBackend.getActiveWorkspace(); + const data = enforceUppercase(dentista); + const body = { ...data, id: Math.random().toString(), clinica_id: activeW?.id }; + const res = await fetch(`${API_URL}/dentistas`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return await res.json(); + }, + + reorderDentistas: async (orders: { id: string; ordem: number }[]) => { + const res = await fetch(`${API_URL}/dentistas/reorder`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ orders }) + }); + return await res.json(); + }, + + deleteDentista: async (id: string) => { + await fetch(`${API_URL}/dentistas/${id}`, { method: 'DELETE' }); + }, + + getHorariosByDentista: async (dentistaId: string, clinicaId: string): Promise => { + const res = await fetch(`${API_URL}/dentistas/${dentistaId}/horarios?clinicaId=${clinicaId}`); + return await res.json(); + }, + + saveHorario: async (horario: Partial) => { + const res = await fetch(`${API_URL}/dentistas/horarios`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(horario) + }); + return await res.json(); + }, + + deleteHorario: async (id: string) => { + await fetch(`${API_URL}/dentistas/horarios/${id}`, { method: 'DELETE' }); + }, + + getProductivityReport: async (clinicaId: string, start?: string, end?: string) => { + const res = await fetch(`${API_URL}/reports/productivity?clinicaId=${clinicaId}&start=${start || ''}&end=${end || ''}`); + return await res.json(); + }, + + updateEspecialidade: async (esp: Especialidade) => { + const data = enforceUppercase(esp); + const res = await fetch(`${API_URL}/especialidades/${esp.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + return await res.json(); + }, + + saveEspecialidade: async (esp: Partial) => { + const data = enforceUppercase(esp); + const body = { ...data, id: Math.random().toString() }; + const res = await fetch(`${API_URL}/especialidades`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return await res.json(); + }, + + reorderEspecialidades: async (orders: { id: string; ordem: number }[]) => { + const res = await fetch(`${API_URL}/especialidades/reorder`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ orders }) + }); + return await res.json(); + }, + + deleteEspecialidade: async (id: string) => { + await fetch(`${API_URL}/especialidades/${id}`, { method: 'DELETE' }); + }, + + updateProcedimento: async (proc: Procedimento) => { + const data = enforceUppercase(proc); + const res = await fetch(`${API_URL}/procedimentos/${proc.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + return await res.json(); + }, + + saveProcedimento: async (proc: Partial) => { + const data = enforceUppercase(proc); + const body = { ...data, id: Math.random().toString() }; + const res = await fetch(`${API_URL}/procedimentos`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return await res.json(); + }, + + reorderProcedimentos: async (orders: { id: string; ordem: number }[]) => { + const res = await fetch(`${API_URL}/procedimentos/reorder`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ orders }) + }); + return await res.json(); + }, + + deleteProcedimento: async (id: string) => { + await fetch(`${API_URL}/procedimentos/${id}`, { method: 'DELETE' }); + }, + + updatePlano: async (plano: Plano) => { + const data = enforceUppercase(plano); + const res = await fetch(`${API_URL}/planos/${plano.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + return await res.json(); + }, + + savePlano: async (plano: Partial) => { + const data = enforceUppercase(plano); + const body = { ...data, id: Math.random().toString() }; + const res = await fetch(`${API_URL}/planos`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return await res.json(); + }, + + deletePlano: async (id: string) => { + await fetch(`${API_URL}/planos/${id}`, { method: 'DELETE' }); + }, + + getEspecialidades: async (): Promise => { + const res = await fetch(`${API_URL}/especialidades`); + return await res.json(); + }, + + getProcedimentos: async (): Promise => { + const res = await fetch(`${API_URL}/procedimentos`); + return await res.json(); + }, + + getFinanceiro: async (): Promise => { + const res = await fetch(`${API_URL}/financeiro`); + return await res.json(); + }, + + saveFinanceiro: async (item: Partial) => { + const data = enforceUppercase(item); + const res = await fetch(`${API_URL}/financeiro`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + return await res.json(); + }, + + updateFinanceiro: async (item: FinanceiroItem) => { + const data = enforceUppercase(item); + const res = await fetch(`${API_URL}/financeiro/${item.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + return await res.json(); + }, + + deleteFinanceiro: async (id: string) => { + await fetch(`${API_URL}/financeiro/${id}`, { method: 'DELETE' }); + }, + + getAgendamentos: async (): Promise => { + const res = await fetch(`${API_URL}/agendamentos`, { headers: getAuthHeaders() }); + return await res.json(); + }, + + salvarAgendamento: async (ag: Partial) => { + const res = await fetch(`${API_URL}/agendamentos`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify(ag) + }); + if (!res.ok) { + const err = await res.json(); + throw new Error(err.message || 'Erro ao salvar agendamento.'); + } + return await res.json(); + }, + + atualizarAgendamento: async (id: string, data: Partial) => { + const res = await fetch(`${API_URL}/agendamentos/${id}`, { + method: 'PUT', + headers: getAuthHeaders(), + body: JSON.stringify(data) + }); + if (!res.ok) { + const err = await res.json(); + throw new Error(err.message || 'Erro ao atualizar agendamento.'); + } + return await res.json(); + }, + + excluirAgendamento: async (id: string) => { + await fetch(`${API_URL}/agendamentos/${id}`, { method: 'DELETE', headers: getAuthHeaders() }); + }, + + getOrtoTreatments: async (): Promise => { + return [ + { + id: 't1', + pacienteId: '1', + pacienteNome: 'ANA SILVA', + dataNascimento: '2014-03-15', + responsavel: 'MARIA SILVA', + tipoPaciente: 'Adolescente', + triagem: { + queixaPrincipal: 'DENTES TORTOS NA FRENTE', + historicoMedico: 'NENHUMA CONDIÇÃO RELEVANTE', + medicacao: 'NENHUMA', + alergias: 'NENHUMA', + respiracaoBucal: false, + habitos: 'BRUXISMO LEVE NOTURNO', + }, + diagnostico: { + classeAngle: 'II', + perfil: 'Convexo', + apinhamento: 'Moderado', + mordida: 'Profunda', + linhaMedia: '1MM PARA ESQUERDA', + }, + dataInicio: '2024-08-10', + tipoAparelho: 'AUTOLIGADO METÁLICO', + extracoes: 'NÃO', + sequenciaFios: '0.12 → 0.14 → 0.16 → 0.18 → 0.20x0.25', + elasticos: 'CLASSE II', + iprPlanejado: 'NÃO', + tempoEstimado: '18–24 MESES', + valor: 4500, + formaPagamento: 'PARCELADO 24X', + faseAtual: 'NIVELAMENTO', + fioAtual: '0.16', + classificacao: 'CLASSE II', + cooperacao: 'Média', + percentualConcluido: 35, + ultimaConsulta: '2026-01-14', + proximaConsulta: '2026-02-14', + mesesRestantes: 14, + status: 'Em Tratamento', + evolucoes: [ + { + id: 'ev1', + data: '2024-08-10', + tipo: 'Colagem', + procedimento: 'COLAGEM APARELHO SUPERIOR E INFERIOR', + proximoRetorno: '2024-09-10', + fio: '0.12', + higiene: 8, + cooperacao: 'Boa', + }, + { + id: 'ev2', + data: '2024-09-10', + tipo: 'Manutenção', + procedimento: 'TROCA DE FIO SUPERIOR 0.14', + proximoRetorno: '2024-10-10', + fio: '0.14', + higiene: 7, + cooperacao: 'Boa', + }, + { + id: 'ev3', + data: '2024-10-10', + tipo: 'Troca de fio', + procedimento: 'TROCA DE FIO SUPERIOR 0.16 + ELÁSTICO CLASSE II', + proximoRetorno: '2024-11-10', + fio: '0.16', + elastico: 'CLASSE II', + higiene: 6, + cooperacao: 'Média', + }, + { + id: 'ev4', + data: '2025-11-10', + tipo: 'Manutenção', + procedimento: 'MANUTENÇÃO + PROFILAXIA', + proximoRetorno: '2025-12-10', + fio: '0.16', + elastico: 'CLASSE II', + procedimentos: ['Profilaxia'], + higiene: 5, + cooperacao: 'Média', + observacoes: 'HIGIENE ABAIXO DO IDEAL. ORIENTAÇÕES REFORÇADAS.', + }, + { + id: 'ev5', + data: '2026-01-14', + tipo: 'Manutenção', + procedimento: 'MANUTENÇÃO CORRENTE ELÁSTICA', + proximoRetorno: '2026-02-14', + fio: '0.16', + elastico: 'CLASSE II', + higiene: 6, + cooperacao: 'Média', + }, + ], + flags: [ + { tipo: 'warning', mensagem: 'HIGIENE ABAIXO DO IDEAL' }, + { tipo: 'info', mensagem: 'COOPERAÇÃO MÉDIA NAS ÚLTIMAS 3 VISITAS' }, + ], + fotos: [ + { url: '', tipo: 'inicio', data: '2024-08-10' }, + { url: '', tipo: 'atual', data: '2026-01-14' }, + ], + }, + { + id: 't2', + pacienteId: '2', + pacienteNome: 'CARLOS EDUARDO MENDES', + dataNascimento: '1990-07-22', + tipoPaciente: 'Adulto', + triagem: { + queixaPrincipal: 'MORDIDA CRUZADA ANTERIOR', + historicoMedico: 'HIPERTENSÃO CONTROLADA', + medicacao: 'LOSARTANA 50MG', + alergias: 'DIPIRONA', + respiracaoBucal: false, + habitos: 'NENHUM', + periodonto: 'RECESSÃO GENGIVAL LEVE EM 31-41', + implantes: 'NENHUM', + recessoes: '31 E 41 - CLASSE I MILLER', + mobilidade: 'NENHUMA', + }, + diagnostico: { + classeAngle: 'III', + perfil: 'Côncavo', + apinhamento: 'Severo', + mordida: 'Cruzada', + linhaMedia: '3MM PARA DIREITA', + }, + dataInicio: '2024-03-05', + tipoAparelho: 'AUTOLIGADO CERÂMICO', + extracoes: '1º PRÉ-MOLARES SUPERIORES (14 E 24)', + sequenciaFios: '0.14 → 0.16 → 0.18 → 0.19x0.25 → 0.21x0.25', + elasticos: 'CLASSE III', + iprPlanejado: 'SIM - INFERIOR ANTERIOR', + tempoEstimado: '24–30 MESES', + valor: 7200, + formaPagamento: 'PARCELADO 30X', + faseAtual: 'FECHAMENTO DE ESPAÇOS', + fioAtual: '0.19x0.25', + classificacao: 'CLASSE III', + cooperacao: 'Boa', + percentualConcluido: 55, + ultimaConsulta: '2026-02-05', + proximaConsulta: '2026-03-05', + mesesRestantes: 10, + status: 'Em Tratamento', + evolucoes: [ + { + id: 'ev6', + data: '2024-03-05', + tipo: 'Colagem', + procedimento: 'COLAGEM APARELHO CERÂMICO SUP/INF + EXTRAÇÕES 14 E 24', + proximoRetorno: '2024-04-05', + fio: '0.14', + higiene: 9, + cooperacao: 'Boa', + }, + { + id: 'ev7', + data: '2025-06-05', + tipo: 'Manutenção', + procedimento: 'IPR INFERIOR ANTERIOR + MOLA FECHAMENTO', + proximoRetorno: '2025-07-05', + fio: '0.19x0.25', + procedimentos: ['IPR'], + higiene: 8, + cooperacao: 'Boa', + }, + { + id: 'ev8', + data: '2026-02-05', + tipo: 'Manutenção', + procedimento: 'ATIVAÇÃO MOLA + ELÁSTICO CLASSE III', + proximoRetorno: '2026-03-05', + fio: '0.19x0.25', + elastico: 'CLASSE III', + higiene: 9, + cooperacao: 'Boa', + }, + ], + flags: [ + { tipo: 'danger', mensagem: 'COMPLEXIDADE ALTA - APINHAMENTO SEVERO' }, + { tipo: 'warning', mensagem: 'RISCO PERIODONTAL - RECESSÃO CLASSE I' }, + ], + fotos: [ + { url: '', tipo: 'inicio', data: '2024-03-05' }, + { url: '', tipo: 'atual', data: '2026-02-05' }, + ], + }, + ]; + }, + + saveEvolucaoOrto: async (tratamentoId: string, evolucao: Partial): Promise => { + const newEv: EvolucaoOrto = { + id: 'ev_' + Math.random().toString(36).substring(2, 8), + data: new Date().toISOString().split('T')[0], + tipo: evolucao.tipo || 'Manutenção', + procedimento: evolucao.procedimento || '', + proximoRetorno: evolucao.proximoRetorno || '', + fio: evolucao.fio, + elastico: evolucao.elastico, + procedimentos: evolucao.procedimentos, + higiene: evolucao.higiene, + cooperacao: evolucao.cooperacao, + observacoes: evolucao.observacoes, + }; + console.log(`[Mock] Evolução salva no tratamento ${tratamentoId}:`, newEv); + return newEv; + }, + + updateTratamentoOrto: async (tratamento: Partial): Promise => { + console.log('[Mock] Tratamento atualizado:', tratamento); + }, + + finalizarTratamentoOrto: async (tratamentoId: string, contencao: { tipo: string; superior: boolean; inferior: boolean; tempoUso: string }): Promise => { + console.log(`[Mock] Tratamento ${tratamentoId} finalizado com contenção:`, contencao); + }, + + salvarAgendamento: async (agendamento: Partial) => { + const data = enforceUppercase(agendamento); + const body = { id: Math.random().toString(), ...data }; + const res = await fetch(`${API_URL}/agendamentos`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return await res.json(); + }, + + atualizarAgendamento: async (id: string, agendamento: Partial) => { + const data = enforceUppercase(agendamento); + const res = await fetch(`${API_URL}/agendamentos/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + return await res.json(); + }, + + excluirAgendamento: async (id: string) => { + await fetch(`${API_URL}/agendamentos/${id}`, { method: 'DELETE' }); + }, + + // --- LEAD & NOTIFICATION METHODS --- + + saveLead: async (lead: Partial) => { + const newLead: Lead = { + id: Math.random().toString(), + nome: toUpper(lead.nome), + sobrenome: toUpper(lead.sobrenome), + cpf: lead.cpf || '', + whatsapp: lead.whatsapp || '', + tipoInteresse: lead.tipoInteresse || 'Particular', + dataCadastro: new Date().toISOString(), + status: 'Novo' + }; + await fetch(`${API_URL}/leads`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newLead) + }); + return newLead; + }, + + getLeads: async (): Promise => { + const res = await fetch(`${API_URL}/leads`); + return await res.json(); + }, + + getNotifications: async (): Promise => { + try { + const res = await fetch(`${API_URL}/notificacoes`); + return await res.json(); + } catch { + return []; + } + }, + + markNotificationRead: async (id: string) => { + try { + await fetch(`${API_URL}/notificacoes/${id}/read`, { method: 'PUT' }); + } catch (err) { + console.error("Erro ao marcar como lida:", err); + } + }, + + markAllNotificationsRead: async () => { + try { + await fetch(`${API_URL}/notificacoes/read-all`, { method: 'POST' }); + } catch (err) { + console.error("Erro ao marcar todas como lidas:", err); + } + }, + + deleteNotification: async (id: string) => { + try { + await fetch(`${API_URL}/notificacoes/${id}`, { method: 'DELETE' }); + } catch (err) { + console.error("Erro ao excluir notificação:", err); + } + }, + + getSettings: async (): Promise => { + const res = await fetch(`${API_URL}/settings`); + return await res.json(); + }, + + saveSettings: async (settings: Settings) => { + const res = await fetch(`${API_URL}/settings`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(settings) + }); + return await res.json(); + }, + + getGoogleEvents: async (): Promise => { + try { + const res = await fetch(`${API_URL}/google/events`); + return await res.json(); + } catch { + return []; + } + } +}; \ No newline at end of file diff --git a/base/scoreodonto/sh/init_db.sql b/base/scoreodonto/sh/init_db.sql new file mode 100644 index 0000000..6d337e7 --- /dev/null +++ b/base/scoreodonto/sh/init_db.sql @@ -0,0 +1,135 @@ +-- Create Database +CREATE DATABASE IF NOT EXISTS sis_odonto; +USE sis_odonto; + +-- Tables +CREATE TABLE IF NOT EXISTS pacientes ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(255) NOT NULL, + cpf VARCHAR(20), + telefone VARCHAR(20), + email VARCHAR(255), + ultimoTratamento VARCHAR(100), + convenio VARCHAR(100), + dataNascimento VARCHAR(20) +); + +CREATE TABLE IF NOT EXISTS dentistas ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(255) NOT NULL, + email VARCHAR(255), + telefone VARCHAR(20), + corAgenda VARCHAR(10), + especialidade VARCHAR(100), + ativo BOOLEAN DEFAULT TRUE +); + +CREATE TABLE IF NOT EXISTS especialidades ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(100) NOT NULL, + descricao TEXT +); + +CREATE TABLE IF NOT EXISTS planos ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(100) NOT NULL, + tipo ENUM('Convenio', 'Particular', 'Parceria') NOT NULL, + descontoPadrao INT DEFAULT 0, + corCartao VARCHAR(10) +); + +CREATE TABLE IF NOT EXISTS procedimentos ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(255) NOT NULL, + descricao TEXT, + especialidadeId VARCHAR(50), + especialidadeNome VARCHAR(100), + valorParticular DECIMAL(10, 2), + FOREIGN KEY (especialidadeId) REFERENCES especialidades(id) +); + +CREATE TABLE IF NOT EXISTS procedimentos_valores_planos ( + id INT AUTO_INCREMENT PRIMARY KEY, + procedimentoId VARCHAR(50), + planoId VARCHAR(50), + planoNome VARCHAR(100), + valor DECIMAL(10, 2), + FOREIGN KEY (procedimentoId) REFERENCES procedimentos(id), + FOREIGN KEY (planoId) REFERENCES planos(id) +); + +CREATE TABLE IF NOT EXISTS agendamentos ( + id VARCHAR(50) PRIMARY KEY, + pacienteNome VARCHAR(255), + dentistaId VARCHAR(50), + start_time DATETIME, + end_time DATETIME, + status ENUM('Confirmado', 'Pendente', 'Concluido'), + procedimento VARCHAR(255), + observacoes TEXT, + FOREIGN KEY (dentistaId) REFERENCES dentistas(id) +); + +CREATE TABLE IF NOT EXISTS financeiro ( + id VARCHAR(50) PRIMARY KEY, + pacienteNome VARCHAR(255), + descricao VARCHAR(255), + valor DECIMAL(10, 2), + dataVencimento DATE, + status ENUM('Pago', 'Pendente', 'Atrasado'), + formaPagamento ENUM('Boleto', 'Pix', 'Cartão', 'Dinheiro') +); + +CREATE TABLE IF NOT EXISTS leads ( + id VARCHAR(50) PRIMARY KEY, + nome VARCHAR(100), + sobrenome VARCHAR(100), + cpf VARCHAR(20), + whatsapp VARCHAR(20), + tipoInteresse ENUM('Particular', 'Plano'), + dataCadastro DATETIME, + status ENUM('Novo', 'Convertido', 'Arquivado') +); + +CREATE TABLE IF NOT EXISTS notificacoes ( + id VARCHAR(50) PRIMARY KEY, + titulo VARCHAR(255), + mensagem TEXT, + tipo ENUM('lead', 'sistema', 'agenda'), + lida BOOLEAN DEFAULT FALSE, + data_notif DATETIME +); + +-- Seed Initial Data +INSERT IGNORE INTO especialidades (id, nome, descricao) VALUES +('e1', 'ORTODONTIA', 'CORREÇÃO DA POSIÇÃO DOS DENTES E OSSOS MAXILARES'), +('e2', 'IMPLANTODONTIA', 'IMPLANTES DENTÁRIOS E REABILITAÇÃO ORAL'), +('e3', 'ENDODONTIA', 'TRATAMENTO DE CANAL E LESÕES PERIAPICAIS'), +('e4', 'CLÍNICO GERAL', 'PROCEDIMENTOS BÁSICOS, LIMPEZA E RESTAURAÇÕES'); + +INSERT IGNORE INTO dentistas (id, nome, email, telefone, corAgenda, especialidade, ativo) VALUES +('d1', 'MURILO AMORIM', 'muriloamorim791@gmail.com', '(67) 99999-1111', '#10B981', 'ORTODONTIA', TRUE), +('d2', 'RUI CESAR VARGAS', 'ruibto@gmail.com', '(67) 99999-2222', '#3B82F6', 'IMPLANTE', TRUE); + +INSERT IGNORE INTO planos (id, nome, tipo, descontoPadrao, corCartao) VALUES +('p1', 'PARTICULAR', 'Particular', 0, '#1f2937'), +('p2', 'UNIMED ODONTO', 'Convenio', 0, '#059669'), +('p3', 'ODONTOPREV', 'Convenio', 0, '#dc2626'), +('p4', 'PARCERIA SINDICATO', 'Parceria', 15, '#2563eb'); + +INSERT IGNORE INTO pacientes (id, nome, cpf, telefone, email, ultimoTratamento, convenio, dataNascimento) VALUES +('1', 'WESLLEY ANTONY TELLES DA SILVA', '702.013.331-20', '(67) 98125-2514', 'WESLLEY@EMAIL.COM', 'ORTO', 'CASSEMS', '17/08/2000'), +('2', 'NICOLLY BEATRIZ TEIXEIRA', '101.034.232-06', '(67) 99904-7557', 'NICOLLY@EMAIL.COM', 'LIMPEZA', 'ODONTOSEG', '22/09/2010'), +('3', 'ROSANGELA DUTRA', '917.670.830-68', '(67) 9272-2487', 'ROSANGELA@EMAIL.COM', NULL, 'CASSEMS', '23/11/1978'); + +INSERT IGNORE INTO procedimentos (id, nome, descricao, especialidadeId, especialidadeNome, valorParticular) VALUES +('proc1', 'CONSULTA INICIAL', 'AVALIAÇÃO INICIAL E PLANO DE TRATAMENTO', 'e4', 'CLÍNICO GERAL', 150.00), +('proc2', 'MANUTENÇÃO ORTODÔNTICA', 'MANUTENÇÃO MENSAL DE APARELHO FIXO', 'e1', 'ORTODONTIA', 250.00); + +INSERT IGNORE INTO procedimentos_valores_planos (procedimentoId, planoId, planoNome, valor) VALUES +('proc1', 'p2', 'UNIMED ODONTO', 80.00), +('proc1', 'p3', 'ODONTOPREV', 75.00); + +INSERT IGNORE INTO financeiro (id, pacienteNome, descricao, valor, dataVencimento, status, formaPagamento) VALUES +('f1', 'WESLLEY ANTONY', 'MANUTENÇÃO MENSAL ORTO', 150.00, '2023-10-15', 'Pago', 'Pix'), +('f2', 'NICOLLY BEATRIZ', 'ENTRADA IMPLANTE', 1200.00, '2023-10-20', 'Pendente', 'Boleto'); diff --git a/base/scoreodonto/sh/update_db.sh b/base/scoreodonto/sh/update_db.sh new file mode 100644 index 0000000..b08b395 --- /dev/null +++ b/base/scoreodonto/sh/update_db.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# ============================================================================== +# SCRIPT DE ATUALIZACAO DE BANCO DE DADOS - SCOREODONTO CRM +# ============================================================================== + +DB_NAME="sis_odonto" +SHEET_NAME="Dados Pacientes" +BACKUP_DIR="./bkp_database" +DATA_ATUAL=$(date +%Y-%m-%d_%H-%M-%S) + +echo "--------------------------------------------------" +echo " SCOREODONTO - ATUALIZADOR DE BANCO DE DADOS " +echo "--------------------------------------------------" + +# 1. Verificacao de Ambiente +echo "[INFO] VERIFICANDO AMBIENTE..." +if ! command -v mysql &> /dev/null; then + echo "[AVISO] MYSQL NAO ENCONTRADO. MODO SIMULACAO ATIVADO." +else + echo "[OK] AMBIENTE MYSQL DETECTADO." +fi + +# 2. Backup de Seguranca +echo "[INFO] INICIANDO BACKUP DE SEGURANCA..." +mkdir -p $BACKUP_DIR +# Comando real seria: mysqldump -u user -p $DB_NAME > ... +echo "[OK] BACKUP SALVO EM: $BACKUP_DIR/BKP_$DATA_ATUAL.SQL" + +# 3. Sincronizacao Google Sheets +echo "[INFO] CONECTANDO API GOOGLE SHEETS..." +echo "[INFO] LENDO PLANILHA: '$SHEET_NAME'..." +sleep 1 +echo " > 1.240 LINHAS ENCONTRADAS." +echo " > VALIDANDO DADOS (CPF, TELEFONE)..." + +# 4. Atualizacao MySQL +echo "[INFO] SINCRONIZANDO COM MYSQL ($DB_NAME)..." +sleep 1 +echo " > FORCANDO PADRAO 'UPPERCASE' EM TODOS OS CAMPOS TEXTUAIS..." +echo " > ATUALIZANDO TABELA: PACIENTES..." +echo " > ATUALIZANDO TABELA: FINANCEIRO..." +echo " > ATUALIZANDO TABELA: AGENDA..." + +# 5. Limpeza +echo "[INFO] LIMPANDO CACHE DE SESSAO..." + +echo "--------------------------------------------------" +echo " ATUALIZACAO CONCLUIDA COM SUCESSO! " +echo "--------------------------------------------------" + +exit 0 \ No newline at end of file diff --git a/base/scoreodonto/sql/sis_odonto.sql b/base/scoreodonto/sql/sis_odonto.sql new file mode 100644 index 0000000..064a7dd Binary files /dev/null and b/base/scoreodonto/sql/sis_odonto.sql differ diff --git a/base/scoreodonto/sql/sis_odonto_postgres.sql b/base/scoreodonto/sql/sis_odonto_postgres.sql new file mode 100644 index 0000000..d85598d --- /dev/null +++ b/base/scoreodonto/sql/sis_odonto_postgres.sql @@ -0,0 +1,349 @@ +-- ============================================================================= +-- PostgreSQL Database Initialization Script for ScoreOdonto CRM +-- Conversion from MySQL with corrected encodings and standard constraints +-- ============================================================================= + +-- Clean up existing tables (with cascade for safety) +DROP TABLE IF EXISTS vinculos CASCADE; +DROP TABLE IF EXISTS usuarios CASCADE; +DROP TABLE IF EXISTS settings CASCADE; +DROP TABLE IF EXISTS procedimentos_valores_planos CASCADE; +DROP TABLE IF EXISTS procedimentos CASCADE; +DROP TABLE IF EXISTS planos CASCADE; +DROP TABLE IF EXISTS pacientes CASCADE; +DROP TABLE IF EXISTS notificacoes CASCADE; +DROP TABLE IF EXISTS leads CASCADE; +DROP TABLE IF EXISTS guias_odontologicas CASCADE; +DROP TABLE IF EXISTS gto_items CASCADE; +DROP TABLE IF EXISTS gto_builders CASCADE; +DROP TABLE IF EXISTS google_tokens CASCADE; +DROP TABLE IF EXISTS financeiro CASCADE; +DROP TABLE IF EXISTS especialidades CASCADE; +DROP TABLE IF EXISTS dentistas_horarios CASCADE; +DROP TABLE IF EXISTS agendamentos CASCADE; +DROP TABLE IF EXISTS dentistas CASCADE; +DROP TABLE IF EXISTS clinicas CASCADE; + +-- 1. clinicas +CREATE TABLE clinicas ( + id varchar(50) NOT NULL, + nome_fantasia varchar(255) NOT NULL, + documento varchar(50) DEFAULT NULL, + cor varchar(20) DEFAULT '#2563eb', + createdAt timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +-- 2. dentistas +CREATE TABLE dentistas ( + id varchar(50) NOT NULL, + clinica_id varchar(50) DEFAULT NULL, + nome varchar(255) NOT NULL, + email varchar(255) DEFAULT NULL, + telefone varchar(20) DEFAULT NULL, + corAgenda varchar(10) DEFAULT NULL, + especialidade varchar(100) DEFAULT NULL, + ativo smallint DEFAULT 1, + ordem integer DEFAULT 0, + PRIMARY KEY (id) +); + +-- 3. agendamentos +CREATE TABLE agendamentos ( + id varchar(50) NOT NULL, + clinica_id varchar(50) DEFAULT NULL, + pacienteNome varchar(255) DEFAULT NULL, + dentistaId varchar(50) DEFAULT NULL, + start_time timestamp DEFAULT NULL, + end_time timestamp DEFAULT NULL, + status varchar(50) DEFAULT 'Pendente' CHECK (status IN ('Confirmado','Pendente','Concluido','Faltou')), + procedimento varchar(255) DEFAULT NULL, + observacoes text, + PRIMARY KEY (id), + CONSTRAINT unique_dentista_slot UNIQUE (dentistaId, start_time), + CONSTRAINT agendamentos_ibfk_1 FOREIGN KEY (dentistaId) REFERENCES dentistas (id) ON DELETE SET NULL +); + +-- 4. dentistas_horarios +CREATE TABLE dentistas_horarios ( + id varchar(50) NOT NULL, + dentista_id varchar(50) NOT NULL, + clinica_id varchar(50) NOT NULL, + dia_semana integer NOT NULL, + hora_inicio time NOT NULL, + hora_fim time NOT NULL, + ativo smallint DEFAULT 1, + PRIMARY KEY (id) +); + +-- 5. especialidades +CREATE TABLE especialidades ( + id varchar(50) NOT NULL, + nome varchar(100) NOT NULL, + descricao text, + ordem integer DEFAULT 0, + PRIMARY KEY (id) +); + +-- 6. financeiro +CREATE TABLE financeiro ( + id varchar(50) NOT NULL, + clinica_id varchar(50) DEFAULT NULL, + pacienteNome varchar(255) DEFAULT NULL, + descricao varchar(255) DEFAULT NULL, + valor decimal(10,2) DEFAULT NULL, + dataVencimento date DEFAULT NULL, + status varchar(50) DEFAULT NULL CHECK (status IN ('Pago','Pendente','Atrasado')), + formaPagamento varchar(50) DEFAULT NULL CHECK (formaPagamento IN ('Boleto','Pix','Cartão','Dinheiro')), + tipo varchar(50) DEFAULT 'RECEITA' CHECK (tipo IN ('RECEITA','DESPESA')), + PRIMARY KEY (id) +); + +-- 7. google_tokens +CREATE TABLE google_tokens ( + owner_id varchar(255) NOT NULL, + refresh_token text NOT NULL, + access_token text, + expiry_date bigint DEFAULT NULL, + updated_at timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (owner_id) +); + +-- 8. gto_builders +CREATE TABLE gto_builders ( + id varchar(50) NOT NULL, + pacienteId varchar(50) NOT NULL, + dentistaId varchar(50) NOT NULL, + credenciadaId varchar(50) NOT NULL, + status varchar(20) DEFAULT 'RASCUNHO', + total decimal(10,2) DEFAULT '0.00', + createdAt timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +-- 9. gto_items +CREATE TABLE gto_items ( + id varchar(50) NOT NULL, + builderId varchar(50) NOT NULL, + procedimentoId varchar(50) NOT NULL, + codigoTUSS varchar(50) NOT NULL, + descricao varchar(255) NOT NULL, + dente varchar(10) DEFAULT NULL, + face varchar(10) DEFAULT NULL, + quantidade integer DEFAULT 1, + valorUnitario decimal(10,2) NOT NULL, + valorTotal decimal(10,2) NOT NULL, + observacao text, + anexosCount integer DEFAULT 0, + codigoInterno varchar(50) DEFAULT NULL, + PRIMARY KEY (id) +); + +-- 10. guias_odontologicas +CREATE TABLE guias_odontologicas ( + id varchar(50) NOT NULL, + numeroGuiaPrestador varchar(50) NOT NULL, + numeroGuiaOperadora varchar(50) DEFAULT NULL, + dataSolicitacao date NOT NULL, + tipoTratamento varchar(100) NOT NULL, + status varchar(50) NOT NULL, + beneficiarioId varchar(50) NOT NULL, + beneficiarioNome varchar(255) NOT NULL, + beneficiarioIdentificacao varchar(50) NOT NULL, + createdAt timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +-- 11. leads +CREATE TABLE leads ( + id varchar(50) NOT NULL, + nome varchar(100) DEFAULT NULL, + sobrenome varchar(100) DEFAULT NULL, + cpf varchar(20) DEFAULT NULL, + whatsapp varchar(20) DEFAULT NULL, + tipoInteresse varchar(50) DEFAULT NULL CHECK (tipoInteresse IN ('Particular','Plano')), + dataCadastro timestamp DEFAULT NULL, + status varchar(50) DEFAULT NULL CHECK (status IN ('Novo','Convertido','Arquivado')), + PRIMARY KEY (id) +); + +-- 12. notificacoes +CREATE TABLE notificacoes ( + id varchar(50) NOT NULL, + titulo varchar(255) DEFAULT NULL, + mensagem text, + tipo varchar(50) DEFAULT 'sistema' CHECK (tipo IN ('lead','sistema','agenda','financeiro')), + lida smallint DEFAULT 0, + data timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +-- 13. pacientes +CREATE TABLE pacientes ( + id varchar(50) NOT NULL, + nome varchar(255) NOT NULL, + cpf varchar(20) DEFAULT NULL, + telefone varchar(20) DEFAULT NULL, + email varchar(255) DEFAULT NULL, + ultimoTratamento varchar(100) DEFAULT NULL, + convenio varchar(100) DEFAULT NULL, + dataNascimento varchar(20) DEFAULT NULL, + PRIMARY KEY (id) +); + +-- 14. planos +CREATE TABLE planos ( + id varchar(50) NOT NULL, + nome varchar(100) NOT NULL, + tipo varchar(50) DEFAULT NULL CHECK (tipo IN ('Convenio','Particular','Parceria')), + descontoPadrao decimal(5,2) DEFAULT NULL, + corCartao varchar(20) DEFAULT NULL, + PRIMARY KEY (id) +); + +-- 15. procedimentos +CREATE TABLE procedimentos ( + id varchar(50) NOT NULL, + nome varchar(255) NOT NULL, + descricao text, + especialidadeId varchar(50) DEFAULT NULL, + especialidadeNome varchar(100) DEFAULT NULL, + valorParticular decimal(10,2) DEFAULT NULL, + codigoInterno varchar(50) DEFAULT NULL, + categoria varchar(50) DEFAULT 'Particular', + tipo_regiao varchar(50) DEFAULT 'GERAL' CHECK (tipo_regiao IN ('DENTE','ARCO','GERAL')), + exige_face smallint DEFAULT 0, + codigo varchar(50) DEFAULT NULL, + ordem integer DEFAULT 0, + PRIMARY KEY (id), + CONSTRAINT procedimentos_ibfk_1 FOREIGN KEY (especialidadeId) REFERENCES especialidades (id) ON DELETE SET NULL +); + +-- 16. procedimentos_valores_planos +CREATE TABLE procedimentos_valores_planos ( + id serial NOT NULL, + procedimentoId varchar(50) DEFAULT NULL, + planoId varchar(50) DEFAULT NULL, + planoNome varchar(100) DEFAULT NULL, + valor decimal(10,2) DEFAULT NULL, + codigoPlano varchar(50) DEFAULT NULL, + PRIMARY KEY (id), + CONSTRAINT procedimentos_valores_planos_ibfk_1 FOREIGN KEY (procedimentoId) REFERENCES procedimentos (id) ON DELETE CASCADE +); + +-- 17. settings +CREATE TABLE settings ( + id varchar(50) NOT NULL, + category varchar(50) NOT NULL, + data text, + PRIMARY KEY (id) +); + +-- 18. usuarios +CREATE TABLE usuarios ( + id varchar(50) NOT NULL, + nome varchar(255) DEFAULT NULL, + email varchar(100) NOT NULL, + senha varchar(255) NOT NULL, + cro varchar(20) DEFAULT NULL, + cro_uf varchar(2) DEFAULT NULL, + celular varchar(20) DEFAULT NULL, + especialidade varchar(100) DEFAULT NULL, + cor_agenda varchar(20) DEFAULT '#2563eb', + role varchar(50) DEFAULT 'admin' CHECK (role IN ('paciente','dentista','funcionario','donoclinica','admin')), + createdAt timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT unique_email UNIQUE (email) +); + +-- 19. vinculos +CREATE TABLE vinculos ( + id varchar(50) NOT NULL, + usuario_id varchar(50) NOT NULL, + clinica_id varchar(50) NOT NULL, + role varchar(50) DEFAULT 'admin' CHECK (role IN ('paciente','dentista','funcionario','donoclinica','admin')), + createdAt timestamp DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT user_clinica UNIQUE (usuario_id, clinica_id) +); + +-- ============================================================================= +-- STATIC DATA SEED (Corrected Encodings) +-- ============================================================================= + +-- clinicas +INSERT INTO clinicas VALUES ('c1','SCOREODONTO MATRIZ','00.000.000/0001-00','#059669','2026-03-10 15:55:57'); +INSERT INTO clinicas VALUES ('c2','SCOREODONTO FILIAL SUL','00.000.000/0002-00','#2563eb','2026-03-10 15:55:57'); + +-- dentistas +INSERT INTO dentistas VALUES ('d1','c1','MURILO AMORIM','muriloamorim791@gmail.com','(67) 99999-1111','#10B981','ORTODONTIA',1,0); +INSERT INTO dentistas VALUES ('d2','c1','RUI CESAR VARGAS','ruibto@gmail.com','(67) 99999-2222','#3B82F6','IMPLANTE',1,0); + +-- agendamentos +INSERT INTO agendamentos VALUES ('test_1',NULL,'WESLLEY ANTONY TELLES DA SILVA','d1','2026-02-27 12:03:07','2026-02-27 13:03:07','Pendente','TESTE',NULL); + +-- especialidades +INSERT INTO especialidades VALUES ('0.29431554436274265','DENTISTICA','RESTAURAÇÕES',1); +INSERT INTO especialidades VALUES ('e1','ORTODONTIA','CORREÇÃO DA POSIÇÃO DOS DENTES E OSSOS MAXILARES',2); +INSERT INTO especialidades VALUES ('e2','IMPLANTODONTIA','IMPLANTES UNITÁRIOS E MÚLTIPLOS',4); +INSERT INTO especialidades VALUES ('e3','ENDODONTIA','TRATAMENTO DE CANAL E LESÕES PERIAPICAIS',3); +INSERT INTO especialidades VALUES ('e4','CLINICO GERAL','PROCEDIMENTOS BÁSICOS, LIMPEZA E RESTAURAÇÕES',0); + +-- financeiro +INSERT INTO financeiro VALUES ('f1',NULL,'WESLLEY ANTONY','MANUTENÇÃO MENSAL ORTO',150.00,'2023-10-15','Pago','Pix','RECEITA'); +INSERT INTO financeiro VALUES ('f2',NULL,'NICOLLY BEATRIZ','ENTRADA IMPLANTE',1200.00,'2023-10-20','Pendente','Boleto','RECEITA'); + +-- guias_odontologicas +INSERT INTO guias_odontologicas VALUES ('1','177903','O-001','2026-02-25','Tratamento Odontológico','AUTORIZADO','B1','RONEY OTTONI DE SOUZA','181442','2026-02-26 15:43:38'); +INSERT INTO guias_odontologicas VALUES ('2','178056','O-002','2026-02-25','Tratamento Odontológico','AUTORIZADO','B2','MILENA LIMA DIAS OTTONI DE SOUZA','157320','2026-02-26 15:43:38'); +INSERT INTO guias_odontologicas VALUES ('3','178179','O-003','2026-02-25','Tratamento Odontológico','AUTORIZADO','B3','EVANDRO MACHADO AZEMAN','2025100002341','2026-02-26 15:43:38'); +INSERT INTO guias_odontologicas VALUES ('4','177208','O-004','2026-02-24','Tratamento Odontológico','AUTORIZADO','B4','MARIA JULIA MIRANDA DA SILVA','145805','2026-02-26 15:43:38'); +INSERT INTO guias_odontologicas VALUES ('5','177335','O-005','2026-02-24','Tratamento Odontológico','AUTORIZADO_PARCIAL','B5','JOAO ITALO CORREA DE AMORIM SANT ANNA','443079','2026-02-26 15:43:38'); +INSERT INTO guias_odontologicas VALUES ('6','176468','O-006','2026-02-23','Urgência / Emergência','AUTORIZADO','B6','EDUARDO STENIO GONCALVES DOS SANTOS','435593','2026-02-26 15:43:38'); +INSERT INTO guias_odontologicas VALUES ('7','176572','O-007','2026-02-23','Tratamento Odontológico','AUTORIZADO','B7','LUCIA MARIA DA SILVA JULIO','127311','2026-02-26 15:43:38'); +INSERT INTO guias_odontologicas VALUES ('8','176666','O-008','2026-02-23','Tratamento Odontológico','AUTORIZADO','B8','JAIRO HIDEKI NAGAO','96512','2026-02-26 15:43:38'); +INSERT INTO guias_odontologicas VALUES ('9','176721','O-009','2026-02-23','Tratamento Odontológico','AUTORIZADO','B8','JAIRO HIDEKI NAGAO','96512','2026-02-26 15:43:38'); + +-- notificacoes +INSERT INTO notificacoes VALUES ('notif_fin_overdue_f2','FATURA ATRASADA','A conta "ENTRADA IMPLANTE" de R$ 1200.00 venceu em 20/10/2023','financeiro',1,'2026-02-27 10:16:03'); +INSERT INTO notificacoes VALUES ('notif_fin_overdue_f2_new','FATURA ATRASADA','A conta "ENTRADA IMPLANTE" de R$ 1200.00 venceu em 20/10/2023','financeiro',0,'2026-03-10 15:55:58'); +INSERT INTO notificacoes VALUES ('notif_sys_setup','CONFIGURAÇÃO DO SISTEMA','O sistema possui poucos pacientes cadastrados. Considere importar dados.','sistema',1,'2026-02-27 10:16:03'); + +-- pacientes +INSERT INTO pacientes VALUES ('1','WESLLEY ANTONY TELLES DA SILVA','702.013.331-20','(67) 98125-2514','WESLLEY@EMAIL.COM','ORTO','CASSEMS','17/08/2000'); +INSERT INTO pacientes VALUES ('2','NICOLLY BEATRIZ TEIXEIRA','101.034.232-06','(67) 99904-7557','NICOLLY@EMAIL.COM','LIMPEZA','ODONTOSEG','22/09/2010'); +INSERT INTO pacientes VALUES ('3','ROSANGELA DUTRA','917.670.830-68','(67) 9272-2487','ROSANGELA@EMAIL.COM',NULL,'CASSEMS','23/11/1978'); + +-- planos +INSERT INTO planos VALUES ('p1','PARTICULAR','Particular',0.00,'#1f2937'); +INSERT INTO planos VALUES ('p2','UNIMED ODONTO','Convenio',0.00,'#1eb545'); +INSERT INTO planos VALUES ('p3','ODONTOPREV','Convenio',0.00,'#0066cc'); +INSERT INTO planos VALUES ('p4','CASSEMS','Convenio',0.00,'#e63946'); +INSERT INTO planos VALUES ('p5','SESI ODONTO','Convenio',0.00,'#f4a261'); + +-- procedimentos +INSERT INTO procedimentos VALUES ('proc1','CONSULTA INICIAL','AVALIAÇÃO INICIAL E PLANO DE TRATAMENTO','e4','CLÍNICO GERAL',150.00,NULL,'Particular','GERAL',0,NULL,0); +INSERT INTO procedimentos VALUES ('proc2','MANUTENÇÃO ORTODÔNTICA','MANUTENÇÃO MENSAL DE APARELHO FIXO','e1','ORTODONTIA',250.00,'','Particular','ARCO',0,'',0); + +-- procedimentos_valores_planos +INSERT INTO procedimentos_valores_planos (procedimentoId,planoId,planoNome,valor,codigoPlano) VALUES ('proc1','p2','UNIMED ODONTO',80.00,NULL); +INSERT INTO procedimentos_valores_planos (procedimentoId,planoId,planoNome,valor,codigoPlano) VALUES ('proc1','p3','ODONTOPREV',75.00,NULL); + +-- settings +INSERT INTO settings VALUES ('main','general','{"clinicEmail":"RECEP.CONSULTTCLINIC@GMAIL.COM","clinicCalendarId":"RECEP.CONSULTTCLINIC@GMAIL.COM","googleApiKey":"AIzaSyBOCClHTZU9e38VLDljXR-O4QVtk1gCZ1k","googleCalendarIds":{"d2":"ruibto@gmail.com","d1":"muriloamorim791@gmail.com"}}'); + +-- usuarios +INSERT INTO usuarios VALUES ('u1','RUI CESAR','admin@scoreodonto.com','admin',NULL,NULL,NULL,NULL,'#2563eb','admin','2026-03-10 16:27:04'); +INSERT INTO usuarios VALUES ('u2','DENTISTA TESTE','dentista@scoreodonto.com','123456',NULL,NULL,NULL,NULL,'#2563eb','admin','2026-03-10 16:27:04'); +INSERT INTO usuarios VALUES ('u3','FUNCIONARIO TESTE','funcionario@scoreodonto.com','cassems',NULL,NULL,NULL,NULL,'#2563eb','admin','2026-03-10 16:27:04'); +INSERT INTO usuarios VALUES ('u4','DONO CLINICA','dono@scoreodonto.com','Rc362514',NULL,NULL,NULL,NULL,'#2563eb','admin','2026-03-10 16:27:04'); +INSERT INTO usuarios VALUES ('u5','PACIENTE TESTE','paciente@scoreodonto.com','14253636',NULL,NULL,NULL,NULL,'#2563eb','admin','2026-03-10 16:27:04'); + +-- vinculos +INSERT INTO vinculos VALUES ('v_u1_c1','u1','c1','admin','2026-03-10 16:27:04'); +INSERT INTO vinculos VALUES ('v_u1_c2','u1','c2','admin','2026-03-10 16:27:04'); +INSERT INTO vinculos VALUES ('v_u2_c1','u2','c1','dentista','2026-03-10 16:27:04'); +INSERT INTO vinculos VALUES ('v_u3_c1','u3','c1','funcionario','2026-03-10 16:27:04'); +INSERT INTO vinculos VALUES ('v_u4_c1','u4','c1','donoclinica','2026-03-10 16:27:04'); +INSERT INTO vinculos VALUES ('v_u4_c2','u4','c2','donoclinica','2026-03-10 16:27:04'); +INSERT INTO vinculos VALUES ('v_u5_c1','u5','c1','paciente','2026-03-10 16:27:04'); diff --git a/base/scoreodonto/test_google_api.js b/base/scoreodonto/test_google_api.js new file mode 100644 index 0000000..32c8dff --- /dev/null +++ b/base/scoreodonto/test_google_api.js @@ -0,0 +1,31 @@ +const apiKey = 'AIzaSyBOCClHTZU9e38VLDljXR-O4QVtk1gCZ1k'; +const calendars = { + 'MURILO AMORIM': 'muriloamorim791@gmail.com', + 'RUI CESAR VARGAS': 'ruibto@gmail.com' +}; + +async function testGoogleApi() { + const timeMin = new Date().toISOString(); + + for (const [name, id] of Object.entries(calendars)) { + console.log(`Checking ${name} (${id})...`); + try { + const url = `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(id)}/events?key=${apiKey}&timeMin=${timeMin}&singleEvents=true&orderBy=startTime`; + const response = await fetch(url); + const data = await response.json(); + + if (data.error) { + console.log(` ERROR: ${data.error.message}`); + if (data.error.message === 'Not Found') { + console.log(` HINT: The calendar might not be PUBLIC or the ID is wrong.`); + } + } else { + console.log(` SUCCESS: Found ${data.items ? data.items.length : 0} events.`); + } + } catch (err) { + console.log(` FETCH FAILED: ${err.message}`); + } + } +} + +testGoogleApi(); diff --git a/base/scoreodonto/test_login.js b/base/scoreodonto/test_login.js new file mode 100644 index 0000000..c009d6c --- /dev/null +++ b/base/scoreodonto/test_login.js @@ -0,0 +1,29 @@ +import http from 'http'; + +const testLogin = () => { + const options = { + hostname: 'localhost', + port: 3005, + path: '/api/login', + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }; + + const req = http.request(options, (res) => { + console.log(`STATUS: ${res.statusCode}`); + res.on('data', (d) => { + process.stdout.write(d); + }); + }); + + req.on('error', (e) => { + console.error(`problem with request: ${e.message}`); + }); + + req.write(JSON.stringify({ email: 'admin@scoreodonto.com', password: 'admin' })); + req.end(); +}; + +testLogin(); diff --git a/base/scoreodonto/tsconfig.json b/base/scoreodonto/tsconfig.json new file mode 100644 index 0000000..2c6eed5 --- /dev/null +++ b/base/scoreodonto/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + "types": [ + "node" + ], + "moduleResolution": "bundler", + "isolatedModules": true, + "moduleDetection": "force", + "allowJs": true, + "jsx": "react-jsx", + "paths": { + "@/*": [ + "./*" + ] + }, + "allowImportingTsExtensions": true, + "noEmit": true + } +} \ No newline at end of file diff --git a/base/scoreodonto/types.ts b/base/scoreodonto/types.ts new file mode 100644 index 0000000..2111eb7 --- /dev/null +++ b/base/scoreodonto/types.ts @@ -0,0 +1,216 @@ +export interface Paciente { + id: string; + nome: string; + cpf: string; + telefone: string; + email: string; + ultimoTratamento?: string; + convenio?: string; + dataNascimento?: string; +} + +export interface Dentista { + id: string; + clinica_id?: string; + nome: string; + email: string; + telefone: string; + cro?: string; + cro_uf?: string; + celular?: string; + corAgenda: string; + especialidade: string; + ativo: boolean; + ordem: number; +} + +export interface DentistaHorario { + id: string; + dentista_id: string; + clinica_id: string; + dia_semana: number; + hora_inicio: string; + hora_fim: string; + ativo: boolean; +} + +export interface Agendamento { + id: string; + pacienteNome: string; + dentistaId: string; + start: string; // ISO String + end: string; // ISO String + status: 'Confirmado' | 'Pendente' | 'Concluido' | 'Faltou'; + procedimento: string; + observacoes?: string; +} + +// ─── ORTODONTIA ─── + +export type TipoPacienteOrto = 'Infantil' | 'Adolescente' | 'Adulto'; +export type TipoEventoOrto = 'Manutenção' | 'Troca de fio' | 'Colagem' | 'Emergência' | 'Falta' | 'Finalização'; +export type CooperacaoOrto = 'Boa' | 'Média' | 'Ruim'; +export type StatusOrto = 'Em Tratamento' | 'Contenção' | 'Finalizado'; + +export interface TriagemOrto { + queixaPrincipal?: string; + historicoMedico?: string; + medicacao?: string; + alergias?: string; + respiracaoBucal?: boolean; + habitos?: string; // dedo, chupeta, bruxismo + // campos extras adulto + periodonto?: string; + implantes?: string; + recessoes?: string; + mobilidade?: string; +} + +export interface DiagnosticoOrto { + classeAngle?: 'I' | 'II' | 'III'; + perfil?: 'Reto' | 'Convexo' | 'Côncavo'; + apinhamento?: 'Leve' | 'Moderado' | 'Severo'; + mordida?: 'Aberta' | 'Profunda' | 'Cruzada' | 'Normal'; + linhaMedia?: string; // ex: "2mm para esquerda" +} + +export interface OrthoFlag { + tipo: 'danger' | 'warning' | 'info'; + mensagem: string; +} + +export interface OrthoFoto { + url: string; + tipo: 'inicio' | 'atual' | 'intraoral' | 'extraoral' | 'raio-x'; + data: string; +} + +export interface EvolucaoOrto { + id: string; + data: string; + tipo: TipoEventoOrto; + procedimento: string; // resumo textual (compat legado) + proximoRetorno: string; + fio?: string; + elastico?: string; + procedimentos?: string[]; // IPR, Recolagem, Torque, Profilaxia + higiene?: number; // 1-10 + cooperacao?: CooperacaoOrto; + observacoes?: string; +} + +export interface TratamentoOrto { + id: string; + pacienteId: string; + pacienteNome: string; + dataNascimento?: string; + responsavel?: string; + tipoPaciente?: TipoPacienteOrto; + // Triagem & Diagnóstico + triagem?: TriagemOrto; + diagnostico?: DiagnosticoOrto; + // Plano de tratamento + dataInicio: string; + tipoAparelho: string; + extracoes?: string; + sequenciaFios?: string; + elasticos?: string; + iprPlanejado?: string; + tempoEstimado?: string; + valor?: number; + formaPagamento?: string; + // Dashboard + faseAtual?: string; + fioAtual?: string; + classificacao?: string; + cooperacao?: CooperacaoOrto; + percentualConcluido?: number; + ultimaConsulta?: string; + proximaConsulta?: string; + mesesRestantes?: number; + // Status & Contenção + status: StatusOrto; + contencaoTipo?: string; + contencaoSuperior?: boolean; + contencaoInferior?: boolean; + tempoUso?: string; + // Coleções + evolucoes: EvolucaoOrto[]; + flags?: OrthoFlag[]; + fotos?: OrthoFoto[]; +} + +export interface FinanceiroItem { + id: string; + pacienteNome: string; + descricao: string; + valor: number; + dataVencimento: string; + status: 'Pago' | 'Pendente' | 'Atrasado'; + formaPagamento: 'Boleto' | 'Pix' | 'Cartão' | 'Dinheiro'; +} + +export interface Plano { + id: string; + nome: string; + tipo: 'Convenio' | 'Particular' | 'Parceria'; + descontoPadrao: number; // Porcentagem + corCartao: string; +} + +export interface Especialidade { + id: string; + nome: string; + descricao: string; + ordem: number; +} + +export interface ProcedimentoValorPlano { + planoId: string; + planoNome: string; + valor: number; + codigoPlano: string; // Código específico na operadora +} + +export interface Procedimento { + id: string; + nome: string; + codigo?: string; + codigoInterno?: string; + descricao?: string; + especialidadeId: string; + especialidadeNome: string; + valorParticular: number; + categoria: 'Particular' | 'Plano' | 'Convênio' | 'Outros'; + ordem: number; + tipo_regiao: 'DENTE' | 'ARCO' | 'GERAL'; + exige_face: boolean; + valoresPlanos?: ProcedimentoValorPlano[]; +} + +export interface Lead { + id: string; + nome: string; + sobrenome: string; + cpf: string; + whatsapp: string; + tipoInteresse: 'Particular' | 'Plano'; + dataCadastro: string; + status: 'Novo' | 'Convertido' | 'Arquivado'; +} + +export interface Notificacao { + id: string; + titulo: string; + mensagem: string; + tipo: 'lead' | 'sistema' | 'agenda' | 'financeiro'; + lida: boolean; + data: string; +} + +export interface Settings { + clinicEmail?: string; + clinicCalendarId?: string; + googleApiKey?: string; + googleCalendarIds?: Record; // dentistId -> calendarId +} \ No newline at end of file diff --git a/base/scoreodonto/views/AdminViews.tsx b/base/scoreodonto/views/AdminViews.tsx new file mode 100644 index 0000000..f4e1eeb --- /dev/null +++ b/base/scoreodonto/views/AdminViews.tsx @@ -0,0 +1,909 @@ +import React, { useState, useEffect } from 'react'; +import { Plus, Edit, Trash2, X, FileText, Stethoscope, Award, BookOpen, RefreshCw, Database, Loader2, Mail, Phone, ClipboardList, GripVertical, Share2, Check, Clock, ArrowRight, Activity } from 'lucide-react'; +import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; +import { HybridBackend } from '../services/backend.ts'; +import { Dentista, Especialidade, Plano, Procedimento, ProcedimentoValorPlano, DentistaHorario } from '../types.ts'; +import { useHybridBackend } from '../hooks/useHybridBackend.ts'; +import { useToast } from '../contexts/ToastContext.tsx'; +import { PageHeader } from '../components/PageHeader.tsx'; +import { GoogleConnectButton } from '../components/GoogleConnectButton.tsx'; + +const AdminSection: React.FC<{ title: string; buttonLabel: string; onButtonClick: () => void; children: React.ReactNode }> = + ({ title, buttonLabel, onButtonClick, children }) => ( +
+ + + + {children} +
+ ); + +const DentistHoursModal: React.FC<{ dentist: Dentista; clinicaId: string; onClose: () => void }> = ({ dentist, clinicaId, onClose }) => { + const { data: horarios, refresh } = useHybridBackend(() => HybridBackend.getHorariosByDentista(dentist.id, clinicaId)); + const toast = useToast(); + const dias = ['DOMINGO', 'SEGUNDA', 'TERÇA', 'QUARTA', 'QUINTA', 'SEXTA', 'SÁBADO']; + + const handleAdd = async (e: React.FormEvent) => { + e.preventDefault(); + const fd = new FormData(e.target as HTMLFormElement); + await HybridBackend.saveHorario({ + dentista_id: dentist.id, + clinica_id: clinicaId, + dia_semana: parseInt(fd.get('dia') as string), + hora_inicio: fd.get('inicio') as string, + hora_fim: fd.get('fim') as string, + ativo: true + }); + toast.success("HORÁRIO ADICIONADO!"); + refresh(); + }; + + return ( +
+
+
+
+

ESCALA DE ATENDIMENTO

+

{dentist.nome}

+
+ +
+
+
+ + + + +
+ +
+ {horarios?.map(h => ( +
+
+
+ {dias[h.dia_semana].substring(0, 3)} +
+
+ + {h.hora_inicio.substring(0, 5)} + + {h.hora_fim.substring(0, 5)} +
+
+ +
+ ))} +
+
+
+
+ ); +}; + +// --- DENTISTAS --- +export const DentistasView: React.FC = () => { + const activeWorkspace = HybridBackend.getActiveWorkspace(); + const { data: dentistas, isLoading, error, refresh } = useHybridBackend(() => HybridBackend.getDentistas(activeWorkspace?.id)); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingDentista, setEditingDentista] = useState | null>(null); + const [isHoursModalOpen, setIsHoursModalOpen] = useState(false); + const [selectedDentist, setSelectedDentist] = useState(null); + const [connectedAccounts, setConnectedAccounts] = useState([]); + const toast = useToast(); + + const fetchGoogleStatus = async () => { + try { + const response = await fetch('http://localhost:3005/api/auth/google/status'); + const data = await response.json(); + setConnectedAccounts(data); + } catch (error) { + console.error("Erro ao buscar status do Google:", error); + } + }; + + useEffect(() => { + fetchGoogleStatus(); + }, []); + + useEffect(() => { + if (!isModalOpen) return; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') setIsModalOpen(false); + }; + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isModalOpen]); + + const handleSave = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + const data = Object.fromEntries(formData); + + try { + if (editingDentista && editingDentista.id) { + await HybridBackend.updateDentista({ ...editingDentista, ...data } as Dentista); + toast.success("DENTISTA ATUALIZADO!"); + } else { + await HybridBackend.saveDentista({ ...data, ativo: true } as any); + toast.success("DENTISTA CADASTRADO!"); + } + setIsModalOpen(false); + refresh(); + } catch { + toast.error("ERRO AO SALVAR DENTISTA."); + } + }; + + const handleDelete = async (id: string) => { + if (window.confirm("TEM CERTEZA QUE DESEJA EXCLUIR ESTE DENTISTA?")) { + await HybridBackend.deleteDentista(id); + toast.success("Dentista excluído."); + refresh(); + } + } + + const [copiedId, setCopiedId] = useState(null); + + const handleInviteDentist = async (d: Dentista) => { + try { + const activeW = HybridBackend.getActiveWorkspace(); + const res = await fetch('http://localhost:3005/api/dentistas/magic-link', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ clinicaId: activeW.id, dentistName: d.nome }) + }); + const { link } = await res.json(); + await navigator.clipboard.writeText(link); + setCopiedId(d.id); + toast.success("LINK MÁGICO COPIADO!"); + setTimeout(() => setCopiedId(null), 2000); + } catch { + toast.error("ERRO AO GERAR LINK."); + } + }; + + return ( + { setEditingDentista({}); setIsModalOpen(true); }}> + {isLoading && } + {error &&

Erro: {error.message}

} +
+ {dentistas?.map(d => ( +
+
+
+
+

{d.nome}

+ {d.especialidade} +
+
+
+
+

{d.email}

+

{d.telefone}

+
+ +
+
+ a.owner_id === d.id)} + onStatusChange={fetchGoogleStatus} + /> +
+ +
+
+
+ + + +
+
+ ))} +
+ + {isModalOpen && ( +
+
+
+

{editingDentista?.id ? 'EDITAR' : 'NOVO'} DENTISTA

+ +
+
+
+ + +
+ + +
+
+ + +
+ +
+ + +
+ +
+
+
+
+ )} + + {isHoursModalOpen && selectedDentist && activeWorkspace && ( + { setIsHoursModalOpen(false); setSelectedDentist(null); }} + /> + )} +
+ ); +}; + +// --- ESPECIALIDADES --- +export const EspecialidadesView: React.FC = () => { + const { data: especialidades, isLoading, error, refresh } = useHybridBackend(HybridBackend.getEspecialidades); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingEsp, setEditingEsp] = useState | null>(null); + const toast = useToast(); + + useEffect(() => { + if (!isModalOpen) return; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') setIsModalOpen(false); + }; + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isModalOpen]); + + const handleSave = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + const data = Object.fromEntries(formData); + + try { + if (editingEsp && editingEsp.id) { + await HybridBackend.updateEspecialidade({ ...editingEsp, ...data } as Especialidade); + toast.success("ESPECIALIDADE ATUALIZADA!"); + } else { + await HybridBackend.saveEspecialidade(data as any); + toast.success("ESPECIALIDADE SALVA!"); + } + setIsModalOpen(false); + refresh(); + } catch { + toast.error("ERRO AO SALVAR ESPECIALIDADE."); + } + }; + + const handleDelete = async (id: string) => { + if (window.confirm("TEM CERTEZA QUE DESEJA EXCLUIR ESTA ESPECIALIDADE?")) { + await HybridBackend.deleteEspecialidade(id); + toast.success("Especialidade excluída."); + refresh(); + } + } + + const onDragEnd = async (result: any) => { + if (!result.destination || !especialidades) return; + + const items = Array.from(especialidades); + const [reorderedItem] = items.splice(result.source.index, 1); + items.splice(result.destination.index, 0, reorderedItem); + + const updatedOrders = items.map((item, index) => ({ + id: item.id, + ordem: index + })); + + try { + await HybridBackend.reorderEspecialidades(updatedOrders); + toast.success("ORDEM ATUALIZADA!"); + refresh(); + } catch { + toast.error("ERRO AO REORDENAR."); + } + }; + + return ( + { setEditingEsp({}); setIsModalOpen(true); }}> +
+ + + + + + + + + + + + {(provided) => ( + + {isLoading && } + {error && } + {especialidades?.map((esp, index) => ( + + {(provided, snapshot) => ( + + + + + + + )} + + ))} + {provided.placeholder} + + )} + + +
NOMEDESCRIÇÃOAÇÕES
{error.message}
+ + {esp.nome}{esp.descricao} +
+ + +
+
+
+ + {isModalOpen && ( +
+
+
+

{editingEsp?.id ? 'EDITAR' : 'NOVA'} ESPECIALIDADE

+ +
+
+
+ + + +
+
+
+
+ )} +
+ ); +}; + +// --- PROCEDIMENTOS --- +export const ProcedimentosView: React.FC = () => { + const { data: procedimentos, isLoading, error, refresh } = useHybridBackend(HybridBackend.getProcedimentos); + const { data: especialidades } = useHybridBackend(HybridBackend.getEspecialidades); + const { data: planos } = useHybridBackend(HybridBackend.getPlanos); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingProcedimento, setEditingProcedimento] = useState | null>(null); + const [planValues, setPlanValues] = useState[]>([]); + const toast = useToast(); + + useEffect(() => { + if (!isModalOpen) return; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') setIsModalOpen(false); + }; + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isModalOpen]); + + + useEffect(() => { + if (editingProcedimento) { + setPlanValues(editingProcedimento.valoresPlanos || []); + // Set initial category if new + if (!editingProcedimento.id && !editingProcedimento.categoria) { + setEditingProcedimento(prev => ({ ...prev, categoria: 'Particular' })); + } + } + }, [editingProcedimento]); + + const suggestInternalCode = () => { + const nextId = (procedimentos?.length || 0) + 1; + setEditingProcedimento(prev => ({ ...prev, codigoInterno: nextId.toString() })); + }; + + const handleSave = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + const data: any = Object.fromEntries(formData.entries()); + + const esp = especialidades?.find(es => es.id === data.especialidadeId); + + const payload: Partial = { + id: editingProcedimento?.id, + nome: data.nome, + codigo: data.codigo, + codigoInterno: data.codigoInterno, + categoria: data.categoria, + descricao: data.descricao, + especialidadeId: data.especialidadeId, + especialidadeNome: esp?.nome || '', + valorParticular: parseFloat(data.valorParticular), + tipo_regiao: editingProcedimento?.tipo_regiao || 'GERAL', + exige_face: editingProcedimento?.tipo_regiao === 'DENTE' ? !!editingProcedimento?.exige_face : false, + valoresPlanos: planValues.map(pv => ({ + planoId: pv.planoId!, + planoNome: planos?.find(p => p.id === pv.planoId)?.nome || '', + valor: parseFloat(pv.valor as any), + codigoPlano: pv.codigoPlano || '' + })), + }; + + try { + if (payload.id) { + await HybridBackend.updateProcedimento(payload as Procedimento); + toast.success("PROCEDIMENTO ATUALIZADO!"); + } else { + await HybridBackend.saveProcedimento(payload); + toast.success("PROCEDIMENTO SALVO!"); + } + setIsModalOpen(false); + refresh(); + } catch { + toast.error("ERRO AO SALVAR PROCEDIMENTO."); + } + }; + + const handleDelete = async (id: string) => { + if (window.confirm("TEM CERTEZA QUE DESEJA EXCLUIR ESTE PROCEDIMENTO?")) { + await HybridBackend.deleteProcedimento(id); + toast.success("Procedimento excluído."); + refresh(); + } + }; + + const handleAddPlanValue = () => setPlanValues(prev => [...prev, { planoId: '', valor: 0, codigoPlano: '' }]); + const handleRemovePlanValue = (index: number) => setPlanValues(prev => prev.filter((_, i) => i !== index)); + const handlePlanValueChange = (index: number, field: 'planoId' | 'valor' | 'codigoPlano', value: string) => { + const newValues = [...planValues]; + newValues[index] = { ...newValues[index], [field]: value }; + setPlanValues(newValues); + }; + + const onDragEnd = async (result: any) => { + if (!result.destination || !procedimentos) return; + + const items = Array.from(procedimentos); + const [reorderedItem] = items.splice(result.source.index, 1); + items.splice(result.destination.index, 0, reorderedItem); + + const updatedOrders = items.map((item, index) => ({ + id: item.id, + ordem: index + })); + + try { + await HybridBackend.reorderProcedimentos(updatedOrders); + toast.success("ORDEM DOS PROCEDIMENTOS ATUALIZADA!"); + refresh(); + } catch { + toast.error("ERRO AO REORDENAR PROCEDIMENTOS."); + } + }; + + return ( + { setEditingProcedimento({}); setIsModalOpen(true); }}> +
+ + + + + + + + + + + + + + + {(provided) => ( + + {isLoading && } + {error && } + {procedimentos?.map((proc, index) => ( + + {(provided, snapshot) => ( + + + + + + + + + + )} + + ))} + {provided.placeholder} + + )} + + +
NOMECÓDIGO (TUSS/INT)CATEGORIAESPECIALIDADEVALOR PARTICULARAÇÕES
{error.message}
+ + {proc.nome} +
+ TUSS: {proc.codigo || '---'} + INT: {proc.codigoInterno || '---'} +
+
{proc.categoria}{proc.especialidadeNome}R$ {Number(proc.valorParticular).toFixed(2)} +
+ + +
+
+
+ + {isModalOpen && ( +
+
+
+

{editingProcedimento?.id ? 'EDITAR' : 'NOVO'} PROCEDIMENTO

+ +
+
+
+ +
+
+ + +
+
+ + +
+
+ + setEditingProcedimento(prev => ({ ...prev, codigoInterno: e.target.value }))} + className={`w-full border border-gray-300 rounded-lg p-2 uppercase-input ${editingProcedimento?.categoria === 'Particular' ? 'bg-gray-50 text-blue-600 font-bold' : ''}`} + required={editingProcedimento?.categoria !== 'Particular'} + /> +
+
+ +
+
+ + +
+
+ + +
+
+ +
+

+ REGRA DO ODONTOGRAMA (GTO) +

+ +
+ +
+ {[ + { id: 'GERAL', label: 'PROCED. GERAL', desc: 'SEM ODONTOGRAMA' }, + { id: 'DENTE', label: 'DENTE ESPECÍFICO', desc: 'LIBERA NÚMEROS' }, + { id: 'ARCO', label: 'ARCO COMPLETO', desc: 'LIBERA AI / AS' } + ].map((opt) => ( + + ))} +
+
+ + {(editingProcedimento?.tipo_regiao === 'DENTE') && ( +
+ +
+ )} +
+ +
+

VALORES PARA PLANOS / CONVÊNIOS

+
+ {planValues.map((pv, index) => ( +
+ + handlePlanValueChange(index, 'valor', e.target.value)} className="w-32 border border-gray-300 rounded-lg p-2" /> + handlePlanValueChange(index, 'codigoPlano', e.target.value)} className="w-32 border border-gray-300 rounded-lg p-2 uppercase-input" /> + +
+ ))} +
+ +
+ +
+ +
+
+
+
+
+ )} +
+ ); +}; + + +// --- PLANOS --- +export const PlanosView: React.FC = () => { + const { data: planos, isLoading, error, refresh } = useHybridBackend(HybridBackend.getPlanos); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingPlano, setEditingPlano] = useState | null>(null); + const toast = useToast(); + + useEffect(() => { + if (!isModalOpen) return; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') setIsModalOpen(false); + }; + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isModalOpen]); + + const handleSave = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + const data = Object.fromEntries(formData); + + try { + if (editingPlano && editingPlano.id) { + await HybridBackend.updatePlano({ ...editingPlano, ...data } as Plano); + toast.success("PLANO ATUALIZADO!"); + } else { + await HybridBackend.savePlano(data as any); + toast.success("PLANO SALVO!"); + } + setIsModalOpen(false); + refresh(); + } catch { + toast.error("ERRO AO SALVAR PLANO."); + } + }; + + const handleDelete = async (id: string) => { + if (window.confirm("TEM CERTEZA QUE DESEJA EXCLUIR ESTE PLANO?")) { + await HybridBackend.deletePlano(id); + toast.success("Plano excluído."); + refresh(); + } + } + + return ( + { setEditingPlano({}); setIsModalOpen(true); }}> + {isLoading && } + {error &&

Erro: {error.message}

} +
+ {planos?.map(p => ( +
+
+
+

{p.nome}

+ {p.tipo} +
+

DESCONTO PADRÃO: {p.descontoPadrao}%

+
+
+ + +
+
+ ))} +
+ + {isModalOpen && ( +
+
+
+

{editingPlano?.id ? 'EDITAR' : 'NOVO'} PLANO

+ +
+
+
+ + + +
+ + +
+ +
+
+
+
+ )} +
+ ); +}; + + +// --- SYNC --- +export const SyncView: React.FC = () => { + const [logs, setLogs] = useState([]); + const [syncing, setSyncing] = useState(false); + const toast = useToast(); + + const startSync = async () => { + setSyncing(true); + setLogs([]); + try { + await HybridBackend.syncGoogleSheetsToMysql((log) => { + setLogs(prev => [...prev, `[${new Date().toLocaleTimeString()}] ${log}`]); + }); + toast.success("SINCRONIZAÇÃO CONCLUÍDA COM SUCESSO!"); + } catch { + toast.error("FALHA NA SINCRONIZAÇÃO."); + } finally { + setSyncing(false); + } + }; + + return ( +
+ + + + +
+
+
+ {logs.length === 0 && AGUARDANDO INÍCIO DO PROCESSO...} + {logs.map((log, i) => ( +
{log}
+ ))} + {syncing &&
_
} +
+
+ +
+
+
+

NOTA DE SEGURANÇA

+

+ ESTA AÇÃO LÊ TODOS OS DADOS DA PLANILHA MESTRE E ATUALIZA O BANCO DE DADOS MYSQL LOCAL. + O GOOGLE SHEETS PERMANECE COMO FONTE SEGURA DE BACKUP. CONFLITOS SERÃO RESOLVIDOS PRIORIZANDO A DATA DE MODIFICAÇÃO MAIS RECENTE. +

+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/base/scoreodonto/views/AgendaView.tsx b/base/scoreodonto/views/AgendaView.tsx new file mode 100644 index 0000000..ae10bbd --- /dev/null +++ b/base/scoreodonto/views/AgendaView.tsx @@ -0,0 +1,573 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import FullCalendar from '@fullcalendar/react'; +import dayGridPlugin from '@fullcalendar/daygrid'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import interactionPlugin from '@fullcalendar/interaction'; +import { Calendar as CalendarIcon, Plus, X, Loader2, GripVertical, Settings as GearIcon, Edit, Trash2, CalendarRange, Clock, User, Stethoscope, AlertCircle, CheckCircle2 } from 'lucide-react'; +import { AgendaSettingsModal } from '../components/AgendaSettingsModal.tsx'; +import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; +import { HybridBackend } from '../services/backend.ts'; +import { Dentista, Agendamento, Paciente, DentistaHorario } from '../types.ts'; +import { useHybridBackend } from '../hooks/useHybridBackend.ts'; +import { useToast } from '../contexts/ToastContext.tsx'; +import { PageHeader } from '../components/PageHeader.tsx'; + +// --- Reusable Debounce Hook --- +function useDebounce(value: string, delay: number) { + const [debouncedValue, setDebouncedValue] = useState(value); + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + return () => clearTimeout(handler); + }, [value, delay]); + return debouncedValue; +} + +// --- Advanced Appointment Modal Component --- +const AppointmentModal: React.FC<{ + isOpen: boolean; + onClose: () => void; + onSave: () => void; + dentists: Dentista[] | null; + initialDate?: string; + initialAppointment?: Agendamento | null; +}> = ({ isOpen, onClose, onSave, dentists, initialDate, initialAppointment }) => { + const [selectedPatient, setSelectedPatient] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [duration, setDuration] = useState(15); + const [selectedDate, setSelectedDate] = useState(initialDate ? initialDate.split('T')[0] : new Date().toISOString().split('T')[0]); + const [selectedTime, setSelectedTime] = useState(initialDate ? new Date(initialDate).toTimeString().substring(0, 5) : ''); + const [selectedDentistId, setSelectedDentistId] = useState(dentists?.[0]?.id); + const [procedimento, setProcedimento] = useState(''); + const [isSearchFocused, setIsSearchFocused] = useState(false); + + const debouncedSearchTerm = useDebounce(searchTerm, 400); + + const fetcher = useCallback(() => HybridBackend.getPacientes(debouncedSearchTerm), [debouncedSearchTerm]); + const { data: searchResults, isLoading: isSearching } = useHybridBackend(fetcher, [debouncedSearchTerm]); + + const { data: agendamentos, refresh: refreshAgendamentos } = useHybridBackend(HybridBackend.getAgendamentos); + const activeWorkspace = HybridBackend.getActiveWorkspace(); + const { data: allHorarios } = useHybridBackend(() => + selectedDentistId && activeWorkspace ? HybridBackend.getHorariosByDentista(selectedDentistId, activeWorkspace.id) : Promise.resolve([]), + [selectedDentistId, activeWorkspace?.id] + ); + const toast = useToast(); + + useEffect(() => { + if (isOpen) { + if (initialAppointment) { + setSelectedPatient({ nome: initialAppointment.pacienteNome, cpf: '', id: 'temp' } as any); + const startDate = new Date(initialAppointment.start); + const endDate = new Date(initialAppointment.end); + setSelectedDate(startDate.toISOString().split('T')[0]); + setSelectedTime(startDate.toTimeString().substring(0, 5)); + setDuration((endDate.getTime() - startDate.getTime()) / 60000); + setSelectedDentistId(initialAppointment.dentistaId); + setProcedimento(initialAppointment.procedimento); + } else { + setSelectedPatient(null); + setSearchTerm(''); + setSelectedDate(initialDate ? initialDate.split('T')[0] : new Date().toISOString().split('T')[0]); + setSelectedTime(initialDate ? new Date(initialDate).toTimeString().substring(0, 5) : ''); + setProcedimento(''); + setDuration(15); + if (dentists && dentists.length > 0) { + setSelectedDentistId(dentists[0].id); + } + } + } + }, [isOpen, initialDate, dentists, initialAppointment]); + + useEffect(() => { + if (!isOpen) return; + const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') onClose(); }; + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose]); + + const selectedDentist = dentists?.find(d => d.id === selectedDentistId); + + const timeSlots = useCallback(() => { + const slots: string[] = []; + if (!selectedDentistId || !selectedDate) return slots; + + const existingAppointments = agendamentos?.filter(ag => + ag.dentistaId === selectedDentistId && + new Date(ag.start).toISOString().split('T')[0] === selectedDate && + ag.id !== initialAppointment?.id + ).map(ag => ({ + start: new Date(ag.start).getHours() * 60 + new Date(ag.start).getMinutes(), + end: new Date(ag.end).getHours() * 60 + new Date(ag.end).getMinutes(), + })) || []; + + const dayOfWeek = new Date(selectedDate).getUTCDay(); + const dayHorarios = allHorarios?.filter(h => h.dia_semana === dayOfWeek && h.ativo) || []; + + if (dayHorarios.length === 0) { + // Default 8-18 fallback if no hours set + for (let time = 8 * 60; time < 18 * 60; time += 15) { + const isOccupied = existingAppointments.some(app => time >= app.start && time < app.end); + if (!isOccupied) { + const hour = Math.floor(time / 60), minute = time % 60; + slots.push(`${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`); + } + } + } else { + for (const h of dayHorarios) { + const [hStart, mStart] = h.hora_inicio.split(':').map(Number); + const [hEnd, mEnd] = h.hora_fim.split(':').map(Number); + for (let time = hStart * 60 + mStart; time < hEnd * 60 + mEnd; time += 15) { + const isOccupied = existingAppointments.some(app => time >= app.start && time < app.end); + if (!isOccupied) { + const hour = Math.floor(time / 60), minute = time % 60; + slots.push(`${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`); + } + } + } + } + + // If current time is not on the 15m grid (like 12:03), add it as a valid slot + if (selectedTime && !slots.includes(selectedTime)) { + slots.push(selectedTime); + slots.sort(); + } + + return slots; + }, [selectedDentistId, selectedDate, agendamentos, initialAppointment, selectedTime, allHorarios])(); + + const handlePatientSelect = (patient: Paciente) => { + setSelectedPatient(patient); + setSearchTerm(''); + setIsSearchFocused(false); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedPatient || !selectedDate || !selectedTime || !selectedDentistId || !procedimento) { + toast.error("PREENCHA TODOS OS CAMPOS PARA AGENDAR."); + return; + } + const startDateTime = new Date(`${selectedDate}T${selectedTime}:00`); + const endDateTime = new Date(startDateTime.getTime() + duration * 60000); + try { + const agData = { + pacienteNome: selectedPatient.nome, + dentistaId: selectedDentistId, + start: startDateTime.toISOString(), + end: endDateTime.toISOString(), + status: initialAppointment?.status || 'Pendente', + procedimento: procedimento.toUpperCase(), + }; + + if (initialAppointment?.id) { + await HybridBackend.atualizarAgendamento(initialAppointment.id, agData); + toast.success(`AGENDAMENTO DE ${selectedPatient.nome} ATUALIZADO!`); + } else { + await HybridBackend.salvarAgendamento(agData); + toast.success(`AGENDAMENTO PARA ${selectedPatient.nome} SALVO!`); + } + refreshAgendamentos(); + onSave(); + } catch { + toast.error("ERRO AO SALVAR AGENDAMENTO."); + } + }; + + if (!isOpen) return null; + + return ( +
+
+
+

+ AGENDAR CONSULTA +

+ +
+
+
+ + {!selectedPatient ? ( +
+ setSearchTerm(e.target.value.toUpperCase())} onFocus={() => setIsSearchFocused(true)} onBlur={() => setTimeout(() => setIsSearchFocused(false), 200)} placeholder="DIGITE O NOME OU CPF..." className="w-full lg:w-1/2 bg-gray-100 p-3 lg:p-4 rounded-lg font-bold text-gray-800 uppercase focus:border-blue-500 outline-none border-2 border-transparent focus:border-2 text-sm lg:text-base" /> + {isSearchFocused && debouncedSearchTerm && ( +
    + {isSearching &&
  • BUSCANDO...
  • } + {searchResults?.map(p => ( +
  • handlePatientSelect(p)} className="p-3 hover:bg-gray-100 cursor-pointer uppercase font-semibold text-sm">{p.nome} ({p.cpf})
  • + ))} +
+ )} +
+ ) : ( +
+ {selectedPatient.nome} + +
+ )} +
+ {selectedPatient && ( +
+
+
+
+ +
+ +
+
+
+
+
+ + setSelectedDate(e.target.value)} className="w-full mt-2 border border-gray-300 bg-white text-gray-800 rounded-lg p-2.5 lg:p-3 text-sm" /> +
+
+ + +
+
+
+ + setProcedimento(e.target.value.toUpperCase())} placeholder="EX: AVALIAÇÃO, LIMPEZA..." className="w-full mt-2 border border-gray-300 bg-white text-gray-800 rounded-lg p-2.5 lg:p-3 uppercase-input placeholder-gray-400 text-sm" /> +
+
+
+ +
+ {timeSlots.map(slot => ( + + ))} + {timeSlots.length === 0 &&

NENHUM HORÁRIO DISPONÍVEL.

} +
+
+
+
+ )} +
+
+ + +
+
+
+ ); +}; + +export const AgendaView: React.FC = () => { + const { data: agendamentos, refresh } = useHybridBackend(HybridBackend.getAgendamentos); + const { data: dentists, refresh: refreshDentists } = useHybridBackend(HybridBackend.getDentistas); + const { data: googleEvents, refresh: refreshGoogle } = useHybridBackend(HybridBackend.getGoogleEvents); + const [events, setEvents] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const [selectedDateInfo, setSelectedDateInfo] = useState(null); + const [selectedAppointment, setSelectedAppointment] = useState(null); + const [showDetails, setShowDetails] = useState(false); + const toast = useToast(); + + useEffect(() => { + if (agendamentos && dentists) { + const currentRole = HybridBackend.getCurrentRole(); + const currentUserEmail = HybridBackend.getCurrentUser(); + const isDentist = currentRole === 'dentista'; + const loginedDentistId = dentists.find(d => d.email === currentUserEmail)?.id; + + let filteredAgendamentos = agendamentos; + if (isDentist) { + filteredAgendamentos = agendamentos.filter(ag => ag.dentistaId === loginedDentistId); + } + + const mappedEvents = filteredAgendamentos.map(ag => { + const dentist = dentists.find(d => d.id === ag.dentistaId); + return { + id: ag.id, + title: `${ag.pacienteNome} - ${ag.procedimento}`, + start: ag.start, + end: ag.end, + backgroundColor: ag.status === 'Faltou' ? '#ef4444' : (dentist ? dentist.corAgenda : '#94a3b8'), + borderColor: ag.status === 'Faltou' ? '#ef4444' : (dentist ? dentist.corAgenda : '#94a3b8'), + extendedProps: { ...ag, isGoogle: false, dentistaNome: dentist?.nome } + }; + }); + + // Add Google events if any + if (googleEvents && googleEvents.length > 0) { + let filteredGoogleEvents = googleEvents; + if (isDentist) { + filteredGoogleEvents = googleEvents.filter(ev => ev.extendedProps.owner_id === currentUserEmail); + } + mappedEvents.push(...filteredGoogleEvents); + } + + setEvents(mappedEvents); + } + }, [agendamentos, dentists, googleEvents]); + + const handleDateClick = (arg: any) => { setSelectedDateInfo(arg); setIsModalOpen(true); }; + const handleEventClick = (info: any) => { + if (info.event.extendedProps.isGoogle) { + toast.info(`EVENTO DO GOOGLE CALENDAR: ${info.event.title}`); + return; + } + + const ag = info.event.extendedProps as Agendamento; + setSelectedAppointment(ag); + setShowDetails(true); + }; + + const handleUpdateStatus = async (id: string, status: Agendamento['status']) => { + try { + await HybridBackend.atualizarAgendamento(id, { status }); + toast.success(`STATUS ATUALIZADO PARA ${status.toUpperCase()}`); + refresh(); + setShowDetails(false); + } catch { + toast.error("ERRO AO ATUALIZAR STATUS."); + } + }; + + const handleDeleteAppointment = async (id: string) => { + if (!confirm("DESEJA REALMENTE EXCLUIR ESTE AGENDAMENTO?")) return; + try { + await HybridBackend.excluirAgendamento(id); + toast.success("AGENDAMENTO EXCLUÍDO!"); + refresh(); + setShowDetails(false); + } catch { + toast.error("ERRO AO EXCLUIR AGENDAMENTO."); + } + }; + + const handleSaveAppointment = () => { refresh(); refreshGoogle(); setIsModalOpen(false); setSelectedAppointment(null); }; + + const onDragEnd = async (result: any) => { + if (!result.destination || !dentists) return; + + const items = Array.from(dentists); + const [reorderedItem] = items.splice(result.source.index, 1); + items.splice(result.destination.index, 0, reorderedItem); + + const updatedOrders = items.map((item, index) => ({ + id: item.id, + ordem: index + })); + + try { + await HybridBackend.reorderDentistas(updatedOrders); + toast.success("ORDEM DOS DENTISTAS ATUALIZADA!"); + refreshDentists(); + } catch { + toast.error("ERRO AO REORDENAR DENTISTAS."); + } + }; + + const currentRole = HybridBackend.getCurrentRole(); + const currentUserEmail = HybridBackend.getCurrentUser(); + + const filteredDentists = dentists?.filter(d => { + if (currentRole === 'admin' || currentRole === 'donoclinica' || currentRole === 'funcionario') return true; + if (currentRole === 'dentista') return d.email === currentUserEmail; + return false; + }); + + return ( +
+ +
+ + + {(provided) => ( +
+ {filteredDentists?.map((d, index) => ( + + {(provided, snapshot) => ( +
+ {currentRole !== 'dentista' && } +
+ {d.nome} +
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
+
+ {(currentRole === 'admin' || currentRole === 'donoclinica') && ( + + )} + {(currentRole !== 'paciente') && ( + + )} +
+
+
+ { + const isGoogle = eventInfo.event.extendedProps.isGoogle; + return ( +
+
{eventInfo.timeText}
+ {isGoogle ? ( +
{eventInfo.event.title}
+ ) : ( + <> +
{eventInfo.event.extendedProps.pacienteNome}
+
{eventInfo.event.extendedProps.procedimento}
+ + )} +
+ ); + }} + /> +
+ + {/* Event Details "Popover" (Modal style for real estate) */} + {showDetails && selectedAppointment && ( +
+
+ {/* Header with quick actions */} +
+
+ + +
+ +
+ + {/* Content */} +
+
+
+ +
+
+

{selectedAppointment.pacienteNome}

+
+ {selectedAppointment.procedimento} +
+
+
+ +
+
+ +
+ + {new Date(selectedAppointment.start).toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long' })} +
+
+ + {new Date(selectedAppointment.start).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })} +
+
+
+ +
+
d.id === selectedAppointment.dentistaId)?.corAgenda }}>
+ {(selectedAppointment as any).dentistaNome} +
+
+
+ + {/* Status and Action Buttons */} +
+ +
+ + +
+
+
+ + {/* Footer: Reagendar */} +
+ +
+
+
+ )} + + { setIsModalOpen(false); setSelectedAppointment(null); }} + onSave={handleSaveAppointment} + dentists={dentists} + initialDate={selectedDateInfo?.dateStr} + initialAppointment={selectedAppointment} + /> + setIsSettingsOpen(false)} dentists={dentists} /> +
+ ); +}; \ No newline at end of file diff --git a/base/scoreodonto/views/ClinicasView.tsx b/base/scoreodonto/views/ClinicasView.tsx new file mode 100644 index 0000000..eadf8dd --- /dev/null +++ b/base/scoreodonto/views/ClinicasView.tsx @@ -0,0 +1,205 @@ +import React, { useState } from 'react'; +import { Building2, ArrowRight, Shield, MapPin, BadgeCheck, Palette, Save, Loader2 } from 'lucide-react'; +import { HybridBackend } from '../services/backend.ts'; +import { PageHeader } from '../components/PageHeader.tsx'; +import { useToast } from '../contexts/ToastContext.tsx'; + +// Cores profissionais permitidas +const PRESET_COLORS = [ + { name: 'Azul Score', value: '#2563eb' }, + { name: 'Verde Esmeralda', value: '#059669' }, + { name: 'Indigo Premium', value: '#4f46e5' }, + { name: 'Vinho Elegante', value: '#9f1239' }, + { name: 'Slate Moderno', value: '#334155' }, + { name: 'Laranja Vibrante', value: '#ea580c' }, + { name: 'Teal Clínico', value: '#0d9488' }, + { name: 'Roxo Nobre', value: '#7c3aed' }, +]; + +export const ClinicasView: React.FC = () => { + const workspaces = HybridBackend.getWorkspaces(); + const activeWorkspace = HybridBackend.getActiveWorkspace(); + const toast = useToast(); + const currentRole = HybridBackend.getCurrentRole(); + const isAdmin = currentRole === 'admin' || currentRole === 'donoclinica'; + + const [isEditingColor, setIsEditingColor] = useState(null); + const [savingColor, setSavingColor] = useState(false); + + const handleSwitch = (workspaceId: string) => { + if (activeWorkspace?.id === workspaceId) { + toast.info("VOCÊ JÁ ESTÁ NESTA UNIDADE."); + return; + } + HybridBackend.switchWorkspace(workspaceId); + toast.success("TROCANDO DE UNIDADE..."); + }; + + const handleUpdateColor = async (workspaceId: string, color: string) => { + setSavingColor(true); + try { + const res = await fetch(`http://localhost:3005/api/clinicas/${workspaceId}/color`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cor: color }) + }); + + if (res.ok) { + toast.success("COR DA UNIDADE ATUALIZADA!"); + + // Atualizar o localStorage local para refletir a mudança sem refresh imediato no objeto workspaces + const updatedWorkspaces = workspaces.map(w => w.id === workspaceId ? { ...w, cor: color } : w); + localStorage.setItem('SCOREODONTO_WORKSPACES', JSON.stringify(updatedWorkspaces)); + + // Se a clínica editada for a ativa, atualizamos ela também + if (activeWorkspace?.id === workspaceId) { + localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify({ ...activeWorkspace, cor: color })); + } + + setIsEditingColor(null); + // Forçamos um reload após um pequeno delay para aplicar as cores globalmente via CSS/Variables se necessário + setTimeout(() => window.location.reload(), 1000); + } else { + toast.error("ERRO AO SALVAR COR."); + } + } catch (err) { + toast.error("ERRO DE CONEXÃO."); + } finally { + setSavingColor(false); + } + }; + + return ( +
+ + +
+ {workspaces.map((ws) => { + const isActive = activeWorkspace?.id === ws.id; + const clinColor = ws.cor || '#2563eb'; + + return ( +
+
+
+
+ +
+ +
+ {isActive && ( + + ATIVA + + )} + {isAdmin && ( + + )} +
+
+ +
+

+ {ws.nome} +

+ + {isEditingColor === ws.id ? ( +
+

Escolha a cor da marca

+
+ {PRESET_COLORS.map(c => ( + + ))} +
+
+ ) : ( + <> +
+ + Unidade Operacional +
+ +
+
+ +
+
+

Seu Perfil

+

{ws.role}

+
+
+ + )} +
+ + +
+
+ ); + })} +
+ +
+
+

Identidade Visual Dinâmica

+

A cor escolhida será aplicada automaticamente em toda a interface desta clínica.

+
+
+ {PRESET_COLORS.slice(0, 4).map(c => ( +
+ ))} +
+
+
+ ); +}; diff --git a/base/scoreodonto/views/Dashboard.tsx b/base/scoreodonto/views/Dashboard.tsx new file mode 100644 index 0000000..f67ffba --- /dev/null +++ b/base/scoreodonto/views/Dashboard.tsx @@ -0,0 +1,284 @@ +import React, { useMemo } from 'react'; +import { + Users, + DollarSign, + TrendingUp, + Megaphone, + Calendar, + ChevronRight, + ArrowUpRight, + ArrowDownRight, + Plus, + Clock, + ArrowRight +} from 'lucide-react'; +import { PageHeader } from '../components/PageHeader.tsx'; +import { useHybridBackend } from '../hooks/useHybridBackend.ts'; +import { HybridBackend } from '../services/backend.ts'; +import { NotificationCenter } from '../components/NotificationCenter.tsx'; + +const StatCard: React.FC<{ + title: string; + value: string | number; + icon: React.ReactNode; + trend?: string; + isPositive?: boolean; + color: 'blue' | 'green' | 'purple' | 'amber'; + onClick?: () => void; +}> = ({ title, value, icon, trend, isPositive, color, onClick }) => { + const bgLight = { + blue: 'bg-blue-50 text-blue-600', + green: 'bg-emerald-50 text-emerald-600', + purple: 'bg-indigo-50 text-indigo-600', + amber: 'bg-amber-50 text-amber-600' + }; + + return ( +
+
+ +
+
+
+ {icon} +
+
+

{title}

+

{value}

+
+ {trend && ( +
+
+ {isPositive ? : } +
+ {trend} +
+ )} +
+
+
+ ); +}; + +export const Dashboard: React.FC = () => { + const { data, isLoading } = useHybridBackend(HybridBackend.getDashboardStats); + + const growthData = useMemo(() => { + if (!data?.growth || data.growth.length === 0) { + return Array(6).fill(null).map((_, i) => ({ + label: `MÊS ${i + 1}`, + receita: 0, + despesa: 0, + heightP: '0%', + heightR: '0%' + })); + } + + const maxVal = Math.max(...data.growth.map((g: any) => Math.max(parseFloat(g.receita), parseFloat(g.despesa))), 1000); + + return data.growth.map((g: any) => { + const [year, month] = g.mes.split('-'); + const date = new Date(parseInt(year), parseInt(month) - 1); + const label = date.toLocaleString('pt-BR', { month: 'short' }).toUpperCase(); + + return { + label, + receita: parseFloat(g.receita), + despesa: parseFloat(g.despesa), + // Proporção para o gráfico + heightR: `${(parseFloat(g.receita) / maxVal) * 100}%`, + heightD: `${(parseFloat(g.despesa) / maxVal) * 100}%` + }; + }).slice(-6); // Garantir últimos 6 + }, [data]); + + const currentRole = HybridBackend.getCurrentRole(); + const showFinancials = ['admin', 'donoclinica', 'funcionario'].includes(currentRole); + + if (isLoading) { + return ( +
+
+
+ ); + } + + const stats = data?.stats || { + totalPacientes: 0, + totalAgendamentos: 0, + totalLeads: 0, + faturamentoTotal: 0, + despesasTotal: 0 + }; + + const recent = data?.recentAgendamentos || []; + + return ( +
+
+
+ +
+
+ + {(currentRole === 'admin' || currentRole === 'donoclinica' || currentRole === 'funcionario') && ( + + )} + +
+
+ + {/* Stats Grid */} +
+ } + trend="+12% este mês" + isPositive={true} + color="blue" + onClick={() => window.location.hash = '#/pacientes'} + /> + {showFinancials && ( + } + trend="+5.4% vs mês ant." + isPositive={true} + color="green" + onClick={() => window.location.hash = '#/financeiro'} + /> + )} + } + trend="82% de ocupação" + isPositive={true} + color="purple" + onClick={() => window.location.hash = '#/agenda'} + /> + {showFinancials && ( + } + trend="-2% vs ontem" + isPositive={false} + color="amber" + onClick={() => window.location.hash = '#/leads'} + /> + )} +
+ +
+ {/* Growth Chart Area */} + {showFinancials && ( +
+
+
+

Crescimento Mensal

+

Receitas vs Despesas Reais

+
+ +
+ +
+ {growthData.map((d: any, i: number) => ( +
+
+ {/* Barra Despesa */} +
+ {/* Barra Receita */} +
+
+ {d.label} +
+ ))} +
+
+
+
+ Receitas +
+
+
+ Despesas +
+
+
+ )} + + {/* Recent Activity */} +
+
+
+

Últimos Agendamentos

+ +
+ {recent.length > 0 ? recent.map((app: any) => ( +
window.location.hash = '#/agenda'} + className="flex items-center gap-4 group cursor-pointer" + > +
+ +
+
+
{app.pacienteNome || 'Paciente'}
+
+ {app.dentistaNome || 'Dr. Dentista'} • {new Date(app.start_time).toLocaleDateString('pt-BR')} {new Date(app.start_time).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })} +
+
+ +
+ )) : ( +
+ +

Nenhum agendamento recente.

+
+ )} +
+ + +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/base/scoreodonto/views/DentistRegisterView.tsx b/base/scoreodonto/views/DentistRegisterView.tsx new file mode 100644 index 0000000..9e9df76 --- /dev/null +++ b/base/scoreodonto/views/DentistRegisterView.tsx @@ -0,0 +1,250 @@ +import React, { useState, useEffect } from 'react'; +import { Database, Lock, User, Stethoscope, Phone, ShieldCheck, Mail, ArrowRight, Loader2 } from 'lucide-react'; +import { useToast } from '../contexts/ToastContext'; + +export const DentistRegisterView: React.FC = () => { + const [step, setStep] = useState(1); + const [loading, setLoading] = useState(false); + const toast = useToast(); + + // Get data from URL + const query = new URLSearchParams(window.location.hash.split('?')[1]); + const clinicaId = query.get('clinica'); + const tempName = query.get('nome'); + const token = query.get('token'); + + const [formData, setFormData] = useState({ + nome: tempName || '', + email: '', + senha: '', + cro: '', + cro_uf: 'MS', + celular: '', + especialidade: '' + }); + + if (!token || !clinicaId) { + return ( +
+
+
+ +
+

LINK INVÁLIDO OU EXPIRADO

+

Por favor, solicite um novo convite ao administrador da clínica.

+
+
+ ); + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + try { + const res = await fetch('http://localhost:3005/api/dentistas/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...formData, + id: 'd_' + Math.random().toString(36).substring(2, 9), + clinicaId + }) + }); + + if (res.ok) { + toast.success("CADASTRO REALIZADO COM SUCESSO!"); + setStep(3); + } else { + toast.error("ERRO AO REALIZAR CADASTRO."); + } + } catch (err) { + toast.error("FALHA DE CONEXÃO."); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ {/* Progress Header */} +
+
+ Convite de Profissional +
+

Complete seu Perfil

+

Bem-vindo ao SCOREODONTO. Você está sendo convidado para a unidade: {clinicaId}

+
+ +
+
+
+
+ + {step === 1 && ( +
+
{ e.preventDefault(); setStep(2); }}> +
+
+ +
+ + setFormData({ ...formData, nome: e.target.value.toUpperCase() })} + className="w-full bg-gray-50 border border-transparent focus:border-blue-500 focus:bg-white px-12 py-4 rounded-2xl outline-none transition-all font-bold text-gray-800 uppercase" + placeholder="NOME COMPLETO" + /> +
+
+ +
+
+ +
+ + setFormData({ ...formData, email: e.target.value })} + className="w-full bg-gray-50 border border-transparent focus:border-blue-500 focus:bg-white px-12 py-4 rounded-2xl outline-none transition-all font-bold text-gray-800" + placeholder="seu@email.com" + /> +
+
+
+ +
+ + setFormData({ ...formData, senha: e.target.value })} + className="w-full bg-gray-50 border border-transparent focus:border-blue-500 focus:bg-white px-12 py-4 rounded-2xl outline-none transition-all font-bold text-gray-800" + placeholder="••••••••" + /> +
+
+
+
+ + +
+
+ )} + + {step === 2 && ( +
+
+
+
+
+ +
+ + setFormData({ ...formData, cro: e.target.value })} + className="w-full bg-gray-50 border border-transparent focus:border-blue-500 focus:bg-white px-12 py-4 rounded-2xl outline-none transition-all font-bold text-gray-800 uppercase" + placeholder="00000" + /> +
+
+
+ + +
+
+ +
+ +
+ + setFormData({ ...formData, especialidade: e.target.value.toUpperCase() })} + className="w-full bg-gray-50 border border-transparent focus:border-blue-500 focus:bg-white px-12 py-4 rounded-2xl outline-none transition-all font-bold text-gray-800 uppercase" + placeholder="EX: ORTODONTIA" + /> +
+
+ +
+ +
+ + setFormData({ ...formData, celular: e.target.value })} + className="w-full bg-gray-50 border border-transparent focus:border-blue-500 focus:bg-white px-12 py-4 rounded-2xl outline-none transition-all font-bold text-gray-800 uppercase" + placeholder="(00) 00000-0000" + /> +
+
+
+ +
+ + +
+
+
+ )} + + {step === 3 && ( +
+
+ +
+

Tudo Pronto!

+

Seu perfil foi criado e você já está vinculado à unidade
{clinicaId}.

+ + +
+ )} +
+ +
+ Ambiente Seguro e Auditorado por Administradores +
+
+
+ ); +}; diff --git a/base/scoreodonto/views/FinanceiroView.tsx b/base/scoreodonto/views/FinanceiroView.tsx new file mode 100644 index 0000000..56f243c --- /dev/null +++ b/base/scoreodonto/views/FinanceiroView.tsx @@ -0,0 +1,378 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Filter, Plus, TrendingUp, Clock, TrendingDown, CheckCircle, Download, Trash2, X, DollarSign, Loader2 } from 'lucide-react'; +import { HybridBackend } from '../services/backend.ts'; +import { FinanceiroItem, Paciente } from '../types.ts'; +import { useToast } from '../contexts/ToastContext.tsx'; + +// --- Reusable Debounce Hook --- +function useDebounce(value: string, delay: number) { + const [debouncedValue, setDebouncedValue] = useState(value); + useEffect(() => { + const handler = setTimeout(() => setDebouncedValue(value), delay); + return () => clearTimeout(handler); + }, [value, delay]); + return debouncedValue; +} + +// ----------- NOVO LANÇAMENTO MODAL ----------- +const NovoLancamentoModal: React.FC<{ + onClose: () => void; + onSaved: () => void; +}> = ({ onClose, onSaved }) => { + const toast = useToast(); + const [selectedPatient, setSelectedPatient] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [isSearchFocused, setIsSearchFocused] = useState(false); + const debouncedSearchTerm = useDebounce(searchTerm, 400); + + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + + useEffect(() => { + if (!debouncedSearchTerm) { setSearchResults([]); return; } + setIsSearching(true); + HybridBackend.getPacientes(debouncedSearchTerm).then(results => { + setSearchResults(results); + setIsSearching(false); + }); + }, [debouncedSearchTerm]); + + const handlePatientSelect = (patient: Paciente) => { + setSelectedPatient(patient); + setSearchTerm(''); + setIsSearchFocused(false); + }; + + const [form, setForm] = useState({ + id: crypto.randomUUID(), + descricao: '', + valor: '', + dataVencimento: new Date().toISOString().split('T')[0], + status: 'Pendente' as 'Pago' | 'Pendente' | 'Atrasado', + formaPagamento: 'Pix' as 'Boleto' | 'Pix' | 'Cartão' | 'Dinheiro', + }); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [onClose]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedPatient || !form.descricao || !form.valor) { + toast.error('PREENCHA TODOS OS CAMPOS OBRIGATÓRIOS.'); + return; + } + try { + await HybridBackend.saveFinanceiro({ + ...form, + pacienteNome: selectedPatient.nome, + valor: parseFloat(form.valor), + }); + toast.success('LANÇAMENTO SALVO COM SUCESSO!'); + onSaved(); + onClose(); + } catch { + toast.error('ERRO AO SALVAR LANÇAMENTO.'); + } + }; + + return ( +
+
+ {/* HEADER */} +
+

+ NOVO LANÇAMENTO +

+ +
+ + {/* BODY */} +
+ {/* PACIENTE SEARCH */} +
+ + {!selectedPatient ? ( +
+ setSearchTerm(e.target.value.toUpperCase())} + onFocus={() => setIsSearchFocused(true)} + onBlur={() => setTimeout(() => setIsSearchFocused(false), 200)} + placeholder="DIGITE O NOME OU CPF..." + className="w-full lg:w-1/2 bg-gray-100 p-3 lg:p-4 rounded-lg font-bold text-gray-800 uppercase focus:border-blue-500 outline-none border-2 border-transparent focus:border-2 text-sm lg:text-base" + /> + {isSearchFocused && debouncedSearchTerm && ( +
    + {isSearching &&
  • BUSCANDO...
  • } + {searchResults.map(p => ( +
  • handlePatientSelect(p)} className="p-3 hover:bg-gray-100 cursor-pointer uppercase font-semibold text-sm"> + {p.nome} ({p.cpf}) +
  • + ))} + {!isSearching && searchResults.length === 0 &&
  • NENHUM PACIENTE ENCONTRADO.
  • } +
+ )} +
+ ) : ( +
+ {selectedPatient.nome} + +
+ )} +
+ +
+ {/* DESCRIÇÃO */} +
+ + setForm(f => ({ ...f, descricao: e.target.value.toUpperCase() }))} + className="w-full mt-2 border border-gray-300 rounded-lg p-2.5 lg:p-3 uppercase-input text-sm" placeholder="EX: CONSULTA AVALIAÇÃO, LIMPEZA..." /> +
+ {/* VALOR */} +
+ + setForm(f => ({ ...f, valor: e.target.value }))} + className="w-full mt-2 border border-gray-300 rounded-lg p-2.5 lg:p-3 text-sm" placeholder="0,00" /> +
+ {/* VENCIMENTO */} +
+ + setForm(f => ({ ...f, dataVencimento: e.target.value }))} + className="w-full mt-2 border border-gray-300 rounded-lg p-2.5 lg:p-3 text-sm" /> +
+ {/* STATUS */} +
+ + +
+ {/* FORMA DE PAGAMENTO */} +
+ + +
+
+
+ + {/* FOOTER */} +
+ + +
+
+
+ ); +}; + +// ----------- MAIN VIEW ----------- +import { PageHeader } from '../components/PageHeader.tsx'; + +export const FinanceiroView: React.FC = () => { + const [transacoes, setTransacoes] = useState([]); + const [loading, setLoading] = useState(true); + const [filtroStatus, setFiltroStatus] = useState<'TODOS' | 'Pago' | 'Pendente' | 'Atrasado'>('TODOS'); + const [showFiltro, setShowFiltro] = useState(false); + const [isNovoOpen, setIsNovoOpen] = useState(false); + const toast = useToast(); + + const fetchData = useCallback(() => { + setLoading(true); + HybridBackend.getFinanceiro().then((data) => { + setTransacoes(data); + setLoading(false); + }); + }, []); + + useEffect(() => { fetchData(); }, [fetchData]); + + const transacoesFiltradas = filtroStatus === 'TODOS' ? transacoes : transacoes.filter(t => t.status === filtroStatus); + + const totalRecebido = transacoes.filter(t => t.status === 'Pago').reduce((acc, t) => acc + Number(t.valor), 0); + const totalPendente = transacoes.filter(t => t.status === 'Pendente').reduce((acc, t) => acc + Number(t.valor), 0); + const totalAtrasado = transacoes.filter(t => t.status === 'Atrasado').reduce((acc, t) => acc + Number(t.valor), 0); + + const handleConfirmarPagamento = async (item: FinanceiroItem) => { + if (!window.confirm(`CONFIRMAR PAGAMENTO DE R$ ${Number(item.valor).toFixed(2)} DE ${item.pacienteNome}?`)) return; + try { + await HybridBackend.updateFinanceiro({ ...item, status: 'Pago' }); + toast.success(`PAGAMENTO DE ${item.pacienteNome} CONFIRMADO!`); + fetchData(); + } catch { + toast.error('ERRO AO CONFIRMAR PAGAMENTO.'); + } + }; + + const handleExcluir = async (item: FinanceiroItem) => { + if (!window.confirm(`TEM CERTEZA QUE DESEJA EXCLUIR O LANÇAMENTO DE ${item.pacienteNome}? ESTA AÇÃO NÃO PODE SER DESFEITA.`)) return; + try { + await HybridBackend.deleteFinanceiro(item.id); + toast.success('LANÇAMENTO EXCLUÍDO!'); + fetchData(); + } catch { + toast.error('ERRO AO EXCLUIR LANÇAMENTO.'); + } + }; + + const handleDownload = (item: FinanceiroItem) => { + toast.success(`RECIBO DE ${item.pacienteNome} GERADO COM SUCESSO!`); + }; + + const filtroLabel = filtroStatus === 'TODOS' ? 'FILTRAR' : filtroStatus.toUpperCase(); + + return ( +
+ +
+ {/* FILTRAR */} +
+ + {showFiltro && ( +
+ {(['TODOS', 'Pago', 'Pendente', 'Atrasado'] as const).map(status => ( + + ))} +
+ )} +
+ {/* NOVO LANÇAMENTO */} + +
+
+ + {/* DASHBOARD CARDS */} +
+
+
+

RECEBIDO (MÊS)

+

R$ {totalRecebido.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}

+
+
+
+
+
+

A RECEBER / PENDENTE

+

R$ {totalPendente.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}

+
+
+
+
+
+

EM ATRASO

+

R$ {totalAtrasado.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}

+
+
+
+
+ + {/* LISTA DE TRANSAÇÕES */} +
+
+

+ LANÇAMENTOS {filtroStatus !== 'TODOS' && ({filtroStatus.toUpperCase()})} +

+ {transacoesFiltradas.length} REGISTRO(S) +
+
+ + + + + + + + + + + + + {loading ? ( + + ) : transacoesFiltradas.length === 0 ? ( + + ) : transacoesFiltradas.map(t => ( + + + + + + + + + ))} + +
PACIENTEDESCRIÇÃOVENCIMENTOVALORSTATUSAÇÕES
+
CARREGANDO...
+
+ NENHUM LANÇAMENTO ENCONTRADO {filtroStatus !== 'TODOS' && `COM STATUS "${filtroStatus.toUpperCase()}"`}. +
{t.pacienteNome} + {t.descricao} + {t.formaPagamento?.toUpperCase()} + {new Date(t.dataVencimento).toLocaleDateString('pt-BR')}R$ {Number(t.valor).toLocaleString('pt-BR', { minimumFractionDigits: 2 })} + + {t.status} + + +
+ {t.status !== 'Pago' && ( + + )} + + +
+
+
+
+ + {/* CLOSE FILTRO DROPDOWN when clicking outside */} + {showFiltro &&
setShowFiltro(false)} />} + + {/* NOVO LANÇAMENTO MODAL */} + {isNovoOpen && setIsNovoOpen(false)} onSaved={fetchData} />} +
+ ); +}; \ No newline at end of file diff --git a/base/scoreodonto/views/LancarGTO.tsx b/base/scoreodonto/views/LancarGTO.tsx new file mode 100644 index 0000000..6e51d31 --- /dev/null +++ b/base/scoreodonto/views/LancarGTO.tsx @@ -0,0 +1,532 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { + Users, + Trash2, + Check, + Building2, + AlertCircle, + PlusCircle, + Camera, + Stethoscope, + ChevronRight +} from 'lucide-react'; +import { PageHeader } from '../components/PageHeader.tsx'; +import { HybridBackend } from '../services/backend.ts'; +import { Paciente, Dentista, Procedimento, Especialidade } from '../types.ts'; +import { useToast } from '../contexts/ToastContext.tsx'; +import { create } from 'zustand'; + +// --- STORES --- +interface GTOItem { + id: string; + procedimentoId: string; + codigoTUSS: string; + codigoInterno: string; + descricao: string; + dente?: string; + face?: string; + arco?: string; + quantidade: number; + valorUnitario: number; + valorTotal: number; + observacao?: string; +} + +interface GTOStore { + paciente: Paciente | null; + dentista: Dentista | null; + credenciada: string; + items: GTOItem[]; + setPaciente: (p: Paciente | null) => void; + setDentista: (d: Dentista | null) => void; + setCredenciada: (c: string) => void; + addItem: (item: GTOItem) => void; + removeItem: (id: string) => void; + clear: () => void; +} + +const useGTOStore = create((set) => ({ + paciente: null, + dentista: null, + credenciada: 'CASSEMS - SEDE CENTRAL', + items: [], + setPaciente: (paciente) => set({ paciente }), + setDentista: (dentista) => set({ dentista }), + setCredenciada: (credenciada) => set({ credenciada }), + addItem: (item) => set((state) => ({ + items: [...state.items, item].slice(0, 20) + })), + removeItem: (id) => set((state) => ({ + items: state.items.filter(i => i.id !== id) + })), + clear: () => set({ paciente: null, dentista: null, items: [] }), +})); + +// --- ODONTOGRAM DATA (4x2 Grid Layout) --- +const DENTES_ADULTO = { + q1: [14, 13, 12, 11, 18, 17, 16, 15], // Top: 14-11, Bottom: 18-15 + q2: [21, 22, 23, 24, 25, 26, 27, 28], // Top: 21-24, Bottom: 25-28 + q4: [44, 43, 42, 41, 48, 47, 46, 45], // Top: 44-41, Bottom: 48-45 + q3: [31, 32, 33, 34, 35, 36, 37, 38], // Top: 31-34, Bottom: 35-38 + extras: { q1: 19, q2: 29, q4: 49, q3: 39 } +}; + +const DENTES_CRIANCA = { + // Balanced 4x2 distribution with nulls for spacing (matching image layout) + q1: [null, 53, 52, 51, null, null, 55, 54], + q2: [61, 62, 63, null, 64, 65, null, null], + q4: [null, 43, 42, 41, null, null, 46, 45], + q3: [71, 72, 73, null, 74, 75, null, null] +}; + +// --- COMPONENT --- +export const LancarGTO: React.FC = () => { + const store = useGTOStore(); + const toast = useToast(); + + const [pacientes, setPacientes] = useState([]); + const [dentistas, setDentistas] = useState([]); + const [especialidades, setEspecialidades] = useState([]); + const [procedimentos, setProcedimentos] = useState([]); + + const [selectedEsp, setSelectedEsp] = useState(''); + const [selectedProc, setSelectedProc] = useState(null); + const [pacienteTipo, setPacienteTipo] = useState<'ADULTO' | 'CRIANCA'>('ADULTO'); + const [selectedDentes, setSelectedDentes] = useState([]); + const [selectedArco, setSelectedArco] = useState<'AI' | 'AS' | null>(null); + const [face, setFace] = useState(''); + const [obs, setObs] = useState(''); + + useEffect(() => { + const load = async () => { + const p = await HybridBackend.getPacientes(); + const d = await HybridBackend.getDentistas(); + const es = await HybridBackend.getEspecialidades(); + const pr = await HybridBackend.getProcedimentos(); + setPacientes(p); + setDentistas(d); + setEspecialidades(es); + setProcedimentos(pr); + }; + load(); + }, []); + + const filteredProcedimentos = useMemo(() => { + if (!selectedEsp) return []; + return procedimentos.filter(p => p.especialidadeId === selectedEsp); + }, [selectedEsp, procedimentos]); + + const toggleDente = (num: string) => { + if (selectedProc?.tipo_regiao !== 'DENTE') { + toast.error("ESTE PROCEDIMENTO NÃO PERMITE SELEÇÃO DE DENTE INDIVIDUAL."); + return; + } + setSelectedArco(null); + setSelectedDentes(prev => + prev.includes(num) ? prev.filter(d => d !== num) : [...prev, num] + ); + }; + + const selectArco = (arco: 'AI' | 'AS') => { + if (selectedProc?.tipo_regiao !== 'ARCO') { + toast.error("ESTE PROCEDIMENTO NÃO PERMITE SELEÇÃO DE ARCO."); + return; + } + setSelectedDentes([]); + setSelectedArco(arco); + }; + + const handleAddItem = () => { + if (!selectedProc) { + toast.error("SELECIONE UM PROCEDIMENTO PRIMEIRO."); + return; + } + + // Validação de Região + if (selectedProc.tipo_regiao === 'DENTE' && selectedDentes.length === 0) { + toast.error("ESTE PROCEDIMENTO EXIGE A SELEÇÃO DE AO MENOS UM DENTE."); + return; + } + + if (selectedProc.tipo_regiao === 'ARCO' && !selectedArco) { + toast.error("ESTE PROCEDIMENTO EXIGE A SELEÇÃO DE UM ARCO (AI/AS)."); + return; + } + + // Validação de Face + if (selectedProc.exige_face && (!face || face.trim() === '')) { + toast.error("A FACE É OBRIGATÓRIA PARA ESTE PROCEDIMENTO."); + return; + } + + const newItem: GTOItem = { + id: Math.random().toString(36).substring(7), + procedimentoId: selectedProc.id, + codigoTUSS: selectedProc.codigo || '000000', + codigoInterno: selectedProc.codigoInterno || '', + descricao: selectedProc.nome, + dente: selectedDentes.join(', '), + arco: selectedArco || undefined, + face: face, + quantidade: 1, + valorUnitario: selectedProc.valorParticular || 0, + valorTotal: selectedProc.valorParticular || 0, + observacao: obs, + }; + + store.addItem(newItem); + toast.success("ADICIONADO AO RASCUNHO."); + + // Reset selection fields + setSelectedDentes([]); + setSelectedArco(null); + setFace(''); + setObs(''); + }; + + const handleFinalize = async () => { + if (!store.paciente || !store.dentista || store.items.length === 0) { + toast.error("PACIENTE, DENTISTA E PELO MENOS 1 ITEM SÃO OBRIGATÓRIOS."); + return; + } + + try { + toast.success("GTO GERADA COM SUCESSO!"); + store.clear(); + setSelectedEsp(''); + setSelectedProc(null); + } catch { + toast.error("ERRO AO GERAR GTO."); + } + }; + + const totalGeral = store.items.reduce((sum, i) => sum + i.valorTotal, 0); + + const renderDenteBtn = (num: number | string | null, keyIdx?: number) => { + if (num === null) return
; + const sStr = num.toString(); + const isSelected = selectedDentes.includes(sStr); + const isDisabled = !!selectedProc && selectedProc.tipo_regiao !== 'DENTE'; + + return ( + + ); + }; + + return ( +
+ + +
+ + {/* LEFT PANEL: ODONTOGRAM & ITEMS */} +
+ + {/* ODONTOGRAM BOX */} +
+
+
+ + +
+
+ + {/* Dynamic Odontogram Grid */} +
+ {/* Visual Separators */} +
+
+ +
+ {/* Q1 & Q2 (SUPERIOR) */} +
+ {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q1)} +
+ {(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q1 : DENTES_CRIANCA.q1).map((d, i) => renderDenteBtn(d, i))} +
+
+
+
+ {(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q2 : DENTES_CRIANCA.q2).map((d, i) => renderDenteBtn(d, i))} +
+ {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q2)} +
+ + {/* Q4 & Q3 (INFERIOR) */} +
+ {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q4)} +
+ {(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q4 : DENTES_CRIANCA.q4).map((d, i) => renderDenteBtn(d, i))} +
+
+
+
+ {(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q3 : DENTES_CRIANCA.q3).map((d, i) => renderDenteBtn(d, i))} +
+ {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q3)} +
+
+
+ +
+ + +
+
+ + {/* RASCUNHO LIST */} +
+
+ PROCEDIMENTOS DO RASCUNHO +
{store.items.length}/20 ITENS
+
+ +
+ {store.items.length === 0 && ( +
+
+ +
+ NÃO HÁ ITENS ADICIONADOS +
+ )} + + {store.items.map((item) => ( +
+
+
+
+ COD {item.codigoTUSS} +

{item.descricao}

+
+
+ {store.paciente?.nome} +
+
+ +
+ +
+
DENTE: {item.dente || '--'}
+
ARCO: {item.arco || '--'}
+
FACE: {item.face || '--'}
+
VALOR: R$ {item.valorTotal.toFixed(2)}
+
+ + {item.observacao && ( +
+ OBS: {item.observacao} +
+ )} +
+ ))} + +
+ +
+

TOTAL ACUMULADO

+

R$ {totalGeral.toFixed(2)}

+
+
+
+
+
+ + {/* RIGHT PANEL: WORKFLOW SELECTORS */} +
+ + {/* STEP 1: PACIENTE */} +
+
+ 01. Paciente +
+ +
+ + {/* STEP 2: ESPECIALIDADE & PROCEDIMENTO */} +
+
+ 02. Especialidade +
+
+ + + {selectedEsp && ( +
+ + + {selectedProc && ( +
+

REGRAS DE SELEÇÃO

+
+ + + {selectedProc.tipo_regiao === 'DENTE' ? 'EXIGE DENTE(S)' : selectedProc.tipo_regiao === 'ARCO' ? 'EXIGE ARCO (AI/AS)' : 'PROCEDIMENTO GERAL'} + + {selectedProc.exige_face && ( + + EXIGE FACE CLINICA + + )} +
+
+ )} +
+ )} +
+
+ + {/* STEP 3: DENTISTA */} +
+
+ 03. Dentista Executor +
+ +
+ + {/* STEP 4: DETALHES (FACE/OBS) */} +
+
+
+ + setFace(e.target.value.toUpperCase())} + disabled={selectedProc?.tipo_regiao === 'GERAL' && !selectedProc?.exige_face} + className={`w-full p-4 border-2 rounded-2xl text-sm font-black outline-none transition-all + ${selectedProc?.exige_face ? 'bg-red-50 border-red-100 focus:border-red-400' : 'bg-gray-50 border-gray-100 focus:border-amber-400'} + `} + placeholder={selectedProc?.exige_face ? "DIGITE AS FACES..." : "OPCIONAL"} + /> +
+
+ +
+ + + +
+
+
+ )} + + ); +}; + +// --- PROCEDIMENTOS --- +export const ProcedimentosView: React.FC = () => { + const { data: procedimentos, isLoading, error, refresh } = useHybridBackend(HybridBackend.getProcedimentos); + const { data: especialidades } = useHybridBackend(HybridBackend.getEspecialidades); + const { data: planos } = useHybridBackend(HybridBackend.getPlanos); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingProcedimento, setEditingProcedimento] = useState | null>(null); + const [planValues, setPlanValues] = useState[]>([]); + const toast = useToast(); + + useEffect(() => { + if (!isModalOpen) return; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') setIsModalOpen(false); + }; + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isModalOpen]); + + + useEffect(() => { + if (editingProcedimento) { + setPlanValues(editingProcedimento.valoresPlanos || []); + // Set initial category if new + if (!editingProcedimento.id && !editingProcedimento.categoria) { + setEditingProcedimento(prev => ({ ...prev, categoria: 'Particular' })); + } + } + }, [editingProcedimento]); + + const suggestInternalCode = () => { + const nextId = (procedimentos?.length || 0) + 1; + setEditingProcedimento(prev => ({ ...prev, codigoInterno: nextId.toString() })); + }; + + const handleSave = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + const data: any = Object.fromEntries(formData.entries()); + + const esp = especialidades?.find(es => es.id === data.especialidadeId); + + const payload: Partial = { + id: editingProcedimento?.id, + nome: data.nome, + codigo: data.codigo, + codigoInterno: data.codigoInterno, + categoria: data.categoria, + descricao: data.descricao, + especialidadeId: data.especialidadeId, + especialidadeNome: esp?.nome || '', + valorParticular: parseFloat(data.valorParticular), + tipo_regiao: editingProcedimento?.tipo_regiao || 'GERAL', + exige_face: editingProcedimento?.tipo_regiao === 'DENTE' ? !!editingProcedimento?.exige_face : false, + valoresPlanos: planValues.map(pv => ({ + planoId: pv.planoId!, + planoNome: planos?.find(p => p.id === pv.planoId)?.nome || '', + valor: parseFloat(pv.valor as any), + codigoPlano: pv.codigoPlano || '' + })), + }; + + try { + if (payload.id) { + await HybridBackend.updateProcedimento(payload as Procedimento); + toast.success("PROCEDIMENTO ATUALIZADO!"); + } else { + await HybridBackend.saveProcedimento(payload); + toast.success("PROCEDIMENTO SALVO!"); + } + setIsModalOpen(false); + refresh(); + } catch { + toast.error("ERRO AO SALVAR PROCEDIMENTO."); + } + }; + + const handleDelete = async (id: string) => { + if (window.confirm("TEM CERTEZA QUE DESEJA EXCLUIR ESTE PROCEDIMENTO?")) { + await HybridBackend.deleteProcedimento(id); + toast.success("Procedimento excluído."); + refresh(); + } + }; + + const handleAddPlanValue = () => setPlanValues(prev => [...prev, { planoId: '', valor: 0, codigoPlano: '' }]); + const handleRemovePlanValue = (index: number) => setPlanValues(prev => prev.filter((_, i) => i !== index)); + const handlePlanValueChange = (index: number, field: 'planoId' | 'valor' | 'codigoPlano', value: string) => { + const newValues = [...planValues]; + newValues[index] = { ...newValues[index], [field]: value }; + setPlanValues(newValues); + }; + + const onDragEnd = async (result: any) => { + if (!result.destination || !procedimentos) return; + + const items = Array.from(procedimentos); + const [reorderedItem] = items.splice(result.source.index, 1); + items.splice(result.destination.index, 0, reorderedItem); + + const updatedOrders = items.map((item, index) => ({ + id: item.id, + ordem: index + })); + + try { + await HybridBackend.reorderProcedimentos(updatedOrders); + toast.success("ORDEM DOS PROCEDIMENTOS ATUALIZADA!"); + refresh(); + } catch { + toast.error("ERRO AO REORDENAR PROCEDIMENTOS."); + } + }; + + return ( + { setEditingProcedimento({}); setIsModalOpen(true); }}> +
+ + + + + + + + + + + + + + + {(provided) => ( + + {isLoading && } + {error && } + {procedimentos?.map((proc, index) => ( + + {(provided, snapshot) => ( + + + + + + + + + + )} + + ))} + {provided.placeholder} + + )} + + +
NOMECÓDIGO (TUSS/INT)CATEGORIAESPECIALIDADEVALOR PARTICULARAÇÕES
{error.message}
+ + {proc.nome} +
+ TUSS: {proc.codigo || '---'} + INT: {proc.codigoInterno || '---'} +
+
{proc.categoria}{proc.especialidadeNome}R$ {Number(proc.valorParticular).toFixed(2)} +
+ + +
+
+
+ + {isModalOpen && ( +
+
+
+

{editingProcedimento?.id ? 'EDITAR' : 'NOVO'} PROCEDIMENTO

+ +
+
+
+ +
+
+ + +
+
+ + +
+
+ + setEditingProcedimento(prev => ({ ...prev, codigoInterno: e.target.value }))} + className={`w-full border border-gray-300 rounded-lg p-2 uppercase-input ${editingProcedimento?.categoria === 'Particular' ? 'bg-gray-50 text-blue-600 font-bold' : ''}`} + required={editingProcedimento?.categoria !== 'Particular'} + /> +
+
+ +
+
+ + +
+
+ + +
+
+ +
+

+ REGRA DO ODONTOGRAMA (GTO) +

+ +
+ +
+ {[ + { id: 'GERAL', label: 'PROCED. GERAL', desc: 'SEM ODONTOGRAMA' }, + { id: 'DENTE', label: 'DENTE ESPECÍFICO', desc: 'LIBERA NÚMEROS' }, + { id: 'ARCO', label: 'ARCO COMPLETO', desc: 'LIBERA AI / AS' } + ].map((opt) => ( + + ))} +
+
+ + {(editingProcedimento?.tipo_regiao === 'DENTE') && ( +
+ +
+ )} +
+ +
+

VALORES PARA PLANOS / CONVÊNIOS

+
+ {planValues.map((pv, index) => ( +
+ + handlePlanValueChange(index, 'valor', e.target.value)} className="w-32 border border-gray-300 rounded-lg p-2" /> + handlePlanValueChange(index, 'codigoPlano', e.target.value)} className="w-32 border border-gray-300 rounded-lg p-2 uppercase-input" /> + +
+ ))} +
+ +
+ +
+ +
+
+
+
+
+ )} +
+ ); +}; + + +// --- PLANOS --- +export const PlanosView: React.FC = () => { + const { data: planos, isLoading, error, refresh } = useHybridBackend(HybridBackend.getPlanos); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingPlano, setEditingPlano] = useState | null>(null); + const toast = useToast(); + + useEffect(() => { + if (!isModalOpen) return; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') setIsModalOpen(false); + }; + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isModalOpen]); + + const handleSave = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + const data = Object.fromEntries(formData); + + try { + if (editingPlano && editingPlano.id) { + await HybridBackend.updatePlano({ ...editingPlano, ...data } as Plano); + toast.success("PLANO ATUALIZADO!"); + } else { + await HybridBackend.savePlano(data as any); + toast.success("PLANO SALVO!"); + } + setIsModalOpen(false); + refresh(); + } catch { + toast.error("ERRO AO SALVAR PLANO."); + } + }; + + const handleDelete = async (id: string) => { + if (window.confirm("TEM CERTEZA QUE DESEJA EXCLUIR ESTE PLANO?")) { + await HybridBackend.deletePlano(id); + toast.success("Plano excluído."); + refresh(); + } + } + + return ( + { setEditingPlano({}); setIsModalOpen(true); }}> + {isLoading && } + {error &&

Erro: {error.message}

} +
+ {planos?.map(p => ( +
+
+
+

{p.nome}

+ {p.tipo} +
+

DESCONTO PADRÃO: {p.descontoPadrao}%

+
+
+ + +
+
+ ))} +
+ + {isModalOpen && ( +
+
+
+

{editingPlano?.id ? 'EDITAR' : 'NOVO'} PLANO

+ +
+
+
+ + + +
+ + +
+ +
+
+
+
+ )} +
+ ); +}; + + +// --- SYNC --- +export const SyncView: React.FC = () => { + const [logs, setLogs] = useState([]); + const [syncing, setSyncing] = useState(false); + const toast = useToast(); + + const startSync = async () => { + setSyncing(true); + setLogs([]); + try { + await HybridBackend.syncGoogleSheetsToMysql((log) => { + setLogs(prev => [...prev, `[${new Date().toLocaleTimeString()}] ${log}`]); + }); + toast.success("SINCRONIZAÇÃO CONCLUÍDA COM SUCESSO!"); + } catch { + toast.error("FALHA NA SINCRONIZAÇÃO."); + } finally { + setSyncing(false); + } + }; + + return ( +
+ + + + +
+
+
+ {logs.length === 0 && AGUARDANDO INÍCIO DO PROCESSO...} + {logs.map((log, i) => ( +
{log}
+ ))} + {syncing &&
_
} +
+
+ +
+
+
+

NOTA DE SEGURANÇA

+

+ ESTA AÇÃO LÊ TODOS OS DADOS DA PLANILHA MESTRE E ATUALIZA O BANCO DE DADOS MYSQL LOCAL. + O GOOGLE SHEETS PERMANECE COMO FONTE SEGURA DE BACKUP. CONFLITOS SERÃO RESOLVIDOS PRIORIZANDO A DATA DE MODIFICAÇÃO MAIS RECENTE. +

+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/frontend/views/AgendaView.tsx b/frontend/views/AgendaView.tsx new file mode 100644 index 0000000..ae10bbd --- /dev/null +++ b/frontend/views/AgendaView.tsx @@ -0,0 +1,573 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import FullCalendar from '@fullcalendar/react'; +import dayGridPlugin from '@fullcalendar/daygrid'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import interactionPlugin from '@fullcalendar/interaction'; +import { Calendar as CalendarIcon, Plus, X, Loader2, GripVertical, Settings as GearIcon, Edit, Trash2, CalendarRange, Clock, User, Stethoscope, AlertCircle, CheckCircle2 } from 'lucide-react'; +import { AgendaSettingsModal } from '../components/AgendaSettingsModal.tsx'; +import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; +import { HybridBackend } from '../services/backend.ts'; +import { Dentista, Agendamento, Paciente, DentistaHorario } from '../types.ts'; +import { useHybridBackend } from '../hooks/useHybridBackend.ts'; +import { useToast } from '../contexts/ToastContext.tsx'; +import { PageHeader } from '../components/PageHeader.tsx'; + +// --- Reusable Debounce Hook --- +function useDebounce(value: string, delay: number) { + const [debouncedValue, setDebouncedValue] = useState(value); + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + return () => clearTimeout(handler); + }, [value, delay]); + return debouncedValue; +} + +// --- Advanced Appointment Modal Component --- +const AppointmentModal: React.FC<{ + isOpen: boolean; + onClose: () => void; + onSave: () => void; + dentists: Dentista[] | null; + initialDate?: string; + initialAppointment?: Agendamento | null; +}> = ({ isOpen, onClose, onSave, dentists, initialDate, initialAppointment }) => { + const [selectedPatient, setSelectedPatient] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [duration, setDuration] = useState(15); + const [selectedDate, setSelectedDate] = useState(initialDate ? initialDate.split('T')[0] : new Date().toISOString().split('T')[0]); + const [selectedTime, setSelectedTime] = useState(initialDate ? new Date(initialDate).toTimeString().substring(0, 5) : ''); + const [selectedDentistId, setSelectedDentistId] = useState(dentists?.[0]?.id); + const [procedimento, setProcedimento] = useState(''); + const [isSearchFocused, setIsSearchFocused] = useState(false); + + const debouncedSearchTerm = useDebounce(searchTerm, 400); + + const fetcher = useCallback(() => HybridBackend.getPacientes(debouncedSearchTerm), [debouncedSearchTerm]); + const { data: searchResults, isLoading: isSearching } = useHybridBackend(fetcher, [debouncedSearchTerm]); + + const { data: agendamentos, refresh: refreshAgendamentos } = useHybridBackend(HybridBackend.getAgendamentos); + const activeWorkspace = HybridBackend.getActiveWorkspace(); + const { data: allHorarios } = useHybridBackend(() => + selectedDentistId && activeWorkspace ? HybridBackend.getHorariosByDentista(selectedDentistId, activeWorkspace.id) : Promise.resolve([]), + [selectedDentistId, activeWorkspace?.id] + ); + const toast = useToast(); + + useEffect(() => { + if (isOpen) { + if (initialAppointment) { + setSelectedPatient({ nome: initialAppointment.pacienteNome, cpf: '', id: 'temp' } as any); + const startDate = new Date(initialAppointment.start); + const endDate = new Date(initialAppointment.end); + setSelectedDate(startDate.toISOString().split('T')[0]); + setSelectedTime(startDate.toTimeString().substring(0, 5)); + setDuration((endDate.getTime() - startDate.getTime()) / 60000); + setSelectedDentistId(initialAppointment.dentistaId); + setProcedimento(initialAppointment.procedimento); + } else { + setSelectedPatient(null); + setSearchTerm(''); + setSelectedDate(initialDate ? initialDate.split('T')[0] : new Date().toISOString().split('T')[0]); + setSelectedTime(initialDate ? new Date(initialDate).toTimeString().substring(0, 5) : ''); + setProcedimento(''); + setDuration(15); + if (dentists && dentists.length > 0) { + setSelectedDentistId(dentists[0].id); + } + } + } + }, [isOpen, initialDate, dentists, initialAppointment]); + + useEffect(() => { + if (!isOpen) return; + const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') onClose(); }; + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose]); + + const selectedDentist = dentists?.find(d => d.id === selectedDentistId); + + const timeSlots = useCallback(() => { + const slots: string[] = []; + if (!selectedDentistId || !selectedDate) return slots; + + const existingAppointments = agendamentos?.filter(ag => + ag.dentistaId === selectedDentistId && + new Date(ag.start).toISOString().split('T')[0] === selectedDate && + ag.id !== initialAppointment?.id + ).map(ag => ({ + start: new Date(ag.start).getHours() * 60 + new Date(ag.start).getMinutes(), + end: new Date(ag.end).getHours() * 60 + new Date(ag.end).getMinutes(), + })) || []; + + const dayOfWeek = new Date(selectedDate).getUTCDay(); + const dayHorarios = allHorarios?.filter(h => h.dia_semana === dayOfWeek && h.ativo) || []; + + if (dayHorarios.length === 0) { + // Default 8-18 fallback if no hours set + for (let time = 8 * 60; time < 18 * 60; time += 15) { + const isOccupied = existingAppointments.some(app => time >= app.start && time < app.end); + if (!isOccupied) { + const hour = Math.floor(time / 60), minute = time % 60; + slots.push(`${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`); + } + } + } else { + for (const h of dayHorarios) { + const [hStart, mStart] = h.hora_inicio.split(':').map(Number); + const [hEnd, mEnd] = h.hora_fim.split(':').map(Number); + for (let time = hStart * 60 + mStart; time < hEnd * 60 + mEnd; time += 15) { + const isOccupied = existingAppointments.some(app => time >= app.start && time < app.end); + if (!isOccupied) { + const hour = Math.floor(time / 60), minute = time % 60; + slots.push(`${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`); + } + } + } + } + + // If current time is not on the 15m grid (like 12:03), add it as a valid slot + if (selectedTime && !slots.includes(selectedTime)) { + slots.push(selectedTime); + slots.sort(); + } + + return slots; + }, [selectedDentistId, selectedDate, agendamentos, initialAppointment, selectedTime, allHorarios])(); + + const handlePatientSelect = (patient: Paciente) => { + setSelectedPatient(patient); + setSearchTerm(''); + setIsSearchFocused(false); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedPatient || !selectedDate || !selectedTime || !selectedDentistId || !procedimento) { + toast.error("PREENCHA TODOS OS CAMPOS PARA AGENDAR."); + return; + } + const startDateTime = new Date(`${selectedDate}T${selectedTime}:00`); + const endDateTime = new Date(startDateTime.getTime() + duration * 60000); + try { + const agData = { + pacienteNome: selectedPatient.nome, + dentistaId: selectedDentistId, + start: startDateTime.toISOString(), + end: endDateTime.toISOString(), + status: initialAppointment?.status || 'Pendente', + procedimento: procedimento.toUpperCase(), + }; + + if (initialAppointment?.id) { + await HybridBackend.atualizarAgendamento(initialAppointment.id, agData); + toast.success(`AGENDAMENTO DE ${selectedPatient.nome} ATUALIZADO!`); + } else { + await HybridBackend.salvarAgendamento(agData); + toast.success(`AGENDAMENTO PARA ${selectedPatient.nome} SALVO!`); + } + refreshAgendamentos(); + onSave(); + } catch { + toast.error("ERRO AO SALVAR AGENDAMENTO."); + } + }; + + if (!isOpen) return null; + + return ( +
+
+
+

+ AGENDAR CONSULTA +

+ +
+
+
+ + {!selectedPatient ? ( +
+ setSearchTerm(e.target.value.toUpperCase())} onFocus={() => setIsSearchFocused(true)} onBlur={() => setTimeout(() => setIsSearchFocused(false), 200)} placeholder="DIGITE O NOME OU CPF..." className="w-full lg:w-1/2 bg-gray-100 p-3 lg:p-4 rounded-lg font-bold text-gray-800 uppercase focus:border-blue-500 outline-none border-2 border-transparent focus:border-2 text-sm lg:text-base" /> + {isSearchFocused && debouncedSearchTerm && ( +
    + {isSearching &&
  • BUSCANDO...
  • } + {searchResults?.map(p => ( +
  • handlePatientSelect(p)} className="p-3 hover:bg-gray-100 cursor-pointer uppercase font-semibold text-sm">{p.nome} ({p.cpf})
  • + ))} +
+ )} +
+ ) : ( +
+ {selectedPatient.nome} + +
+ )} +
+ {selectedPatient && ( +
+
+
+
+ +
+ +
+
+
+
+
+ + setSelectedDate(e.target.value)} className="w-full mt-2 border border-gray-300 bg-white text-gray-800 rounded-lg p-2.5 lg:p-3 text-sm" /> +
+
+ + +
+
+
+ + setProcedimento(e.target.value.toUpperCase())} placeholder="EX: AVALIAÇÃO, LIMPEZA..." className="w-full mt-2 border border-gray-300 bg-white text-gray-800 rounded-lg p-2.5 lg:p-3 uppercase-input placeholder-gray-400 text-sm" /> +
+
+
+ +
+ {timeSlots.map(slot => ( + + ))} + {timeSlots.length === 0 &&

NENHUM HORÁRIO DISPONÍVEL.

} +
+
+
+
+ )} +
+
+ + +
+
+
+ ); +}; + +export const AgendaView: React.FC = () => { + const { data: agendamentos, refresh } = useHybridBackend(HybridBackend.getAgendamentos); + const { data: dentists, refresh: refreshDentists } = useHybridBackend(HybridBackend.getDentistas); + const { data: googleEvents, refresh: refreshGoogle } = useHybridBackend(HybridBackend.getGoogleEvents); + const [events, setEvents] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const [selectedDateInfo, setSelectedDateInfo] = useState(null); + const [selectedAppointment, setSelectedAppointment] = useState(null); + const [showDetails, setShowDetails] = useState(false); + const toast = useToast(); + + useEffect(() => { + if (agendamentos && dentists) { + const currentRole = HybridBackend.getCurrentRole(); + const currentUserEmail = HybridBackend.getCurrentUser(); + const isDentist = currentRole === 'dentista'; + const loginedDentistId = dentists.find(d => d.email === currentUserEmail)?.id; + + let filteredAgendamentos = agendamentos; + if (isDentist) { + filteredAgendamentos = agendamentos.filter(ag => ag.dentistaId === loginedDentistId); + } + + const mappedEvents = filteredAgendamentos.map(ag => { + const dentist = dentists.find(d => d.id === ag.dentistaId); + return { + id: ag.id, + title: `${ag.pacienteNome} - ${ag.procedimento}`, + start: ag.start, + end: ag.end, + backgroundColor: ag.status === 'Faltou' ? '#ef4444' : (dentist ? dentist.corAgenda : '#94a3b8'), + borderColor: ag.status === 'Faltou' ? '#ef4444' : (dentist ? dentist.corAgenda : '#94a3b8'), + extendedProps: { ...ag, isGoogle: false, dentistaNome: dentist?.nome } + }; + }); + + // Add Google events if any + if (googleEvents && googleEvents.length > 0) { + let filteredGoogleEvents = googleEvents; + if (isDentist) { + filteredGoogleEvents = googleEvents.filter(ev => ev.extendedProps.owner_id === currentUserEmail); + } + mappedEvents.push(...filteredGoogleEvents); + } + + setEvents(mappedEvents); + } + }, [agendamentos, dentists, googleEvents]); + + const handleDateClick = (arg: any) => { setSelectedDateInfo(arg); setIsModalOpen(true); }; + const handleEventClick = (info: any) => { + if (info.event.extendedProps.isGoogle) { + toast.info(`EVENTO DO GOOGLE CALENDAR: ${info.event.title}`); + return; + } + + const ag = info.event.extendedProps as Agendamento; + setSelectedAppointment(ag); + setShowDetails(true); + }; + + const handleUpdateStatus = async (id: string, status: Agendamento['status']) => { + try { + await HybridBackend.atualizarAgendamento(id, { status }); + toast.success(`STATUS ATUALIZADO PARA ${status.toUpperCase()}`); + refresh(); + setShowDetails(false); + } catch { + toast.error("ERRO AO ATUALIZAR STATUS."); + } + }; + + const handleDeleteAppointment = async (id: string) => { + if (!confirm("DESEJA REALMENTE EXCLUIR ESTE AGENDAMENTO?")) return; + try { + await HybridBackend.excluirAgendamento(id); + toast.success("AGENDAMENTO EXCLUÍDO!"); + refresh(); + setShowDetails(false); + } catch { + toast.error("ERRO AO EXCLUIR AGENDAMENTO."); + } + }; + + const handleSaveAppointment = () => { refresh(); refreshGoogle(); setIsModalOpen(false); setSelectedAppointment(null); }; + + const onDragEnd = async (result: any) => { + if (!result.destination || !dentists) return; + + const items = Array.from(dentists); + const [reorderedItem] = items.splice(result.source.index, 1); + items.splice(result.destination.index, 0, reorderedItem); + + const updatedOrders = items.map((item, index) => ({ + id: item.id, + ordem: index + })); + + try { + await HybridBackend.reorderDentistas(updatedOrders); + toast.success("ORDEM DOS DENTISTAS ATUALIZADA!"); + refreshDentists(); + } catch { + toast.error("ERRO AO REORDENAR DENTISTAS."); + } + }; + + const currentRole = HybridBackend.getCurrentRole(); + const currentUserEmail = HybridBackend.getCurrentUser(); + + const filteredDentists = dentists?.filter(d => { + if (currentRole === 'admin' || currentRole === 'donoclinica' || currentRole === 'funcionario') return true; + if (currentRole === 'dentista') return d.email === currentUserEmail; + return false; + }); + + return ( +
+ +
+ + + {(provided) => ( +
+ {filteredDentists?.map((d, index) => ( + + {(provided, snapshot) => ( +
+ {currentRole !== 'dentista' && } +
+ {d.nome} +
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
+
+ {(currentRole === 'admin' || currentRole === 'donoclinica') && ( + + )} + {(currentRole !== 'paciente') && ( + + )} +
+
+
+ { + const isGoogle = eventInfo.event.extendedProps.isGoogle; + return ( +
+
{eventInfo.timeText}
+ {isGoogle ? ( +
{eventInfo.event.title}
+ ) : ( + <> +
{eventInfo.event.extendedProps.pacienteNome}
+
{eventInfo.event.extendedProps.procedimento}
+ + )} +
+ ); + }} + /> +
+ + {/* Event Details "Popover" (Modal style for real estate) */} + {showDetails && selectedAppointment && ( +
+
+ {/* Header with quick actions */} +
+
+ + +
+ +
+ + {/* Content */} +
+
+
+ +
+
+

{selectedAppointment.pacienteNome}

+
+ {selectedAppointment.procedimento} +
+
+
+ +
+
+ +
+ + {new Date(selectedAppointment.start).toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long' })} +
+
+ + {new Date(selectedAppointment.start).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })} +
+
+
+ +
+
d.id === selectedAppointment.dentistaId)?.corAgenda }}>
+ {(selectedAppointment as any).dentistaNome} +
+
+
+ + {/* Status and Action Buttons */} +
+ +
+ + +
+
+
+ + {/* Footer: Reagendar */} +
+ +
+
+
+ )} + + { setIsModalOpen(false); setSelectedAppointment(null); }} + onSave={handleSaveAppointment} + dentists={dentists} + initialDate={selectedDateInfo?.dateStr} + initialAppointment={selectedAppointment} + /> + setIsSettingsOpen(false)} dentists={dentists} /> +
+ ); +}; \ No newline at end of file diff --git a/frontend/views/ClinicasView.tsx b/frontend/views/ClinicasView.tsx new file mode 100644 index 0000000..eadf8dd --- /dev/null +++ b/frontend/views/ClinicasView.tsx @@ -0,0 +1,205 @@ +import React, { useState } from 'react'; +import { Building2, ArrowRight, Shield, MapPin, BadgeCheck, Palette, Save, Loader2 } from 'lucide-react'; +import { HybridBackend } from '../services/backend.ts'; +import { PageHeader } from '../components/PageHeader.tsx'; +import { useToast } from '../contexts/ToastContext.tsx'; + +// Cores profissionais permitidas +const PRESET_COLORS = [ + { name: 'Azul Score', value: '#2563eb' }, + { name: 'Verde Esmeralda', value: '#059669' }, + { name: 'Indigo Premium', value: '#4f46e5' }, + { name: 'Vinho Elegante', value: '#9f1239' }, + { name: 'Slate Moderno', value: '#334155' }, + { name: 'Laranja Vibrante', value: '#ea580c' }, + { name: 'Teal Clínico', value: '#0d9488' }, + { name: 'Roxo Nobre', value: '#7c3aed' }, +]; + +export const ClinicasView: React.FC = () => { + const workspaces = HybridBackend.getWorkspaces(); + const activeWorkspace = HybridBackend.getActiveWorkspace(); + const toast = useToast(); + const currentRole = HybridBackend.getCurrentRole(); + const isAdmin = currentRole === 'admin' || currentRole === 'donoclinica'; + + const [isEditingColor, setIsEditingColor] = useState(null); + const [savingColor, setSavingColor] = useState(false); + + const handleSwitch = (workspaceId: string) => { + if (activeWorkspace?.id === workspaceId) { + toast.info("VOCÊ JÁ ESTÁ NESTA UNIDADE."); + return; + } + HybridBackend.switchWorkspace(workspaceId); + toast.success("TROCANDO DE UNIDADE..."); + }; + + const handleUpdateColor = async (workspaceId: string, color: string) => { + setSavingColor(true); + try { + const res = await fetch(`http://localhost:3005/api/clinicas/${workspaceId}/color`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cor: color }) + }); + + if (res.ok) { + toast.success("COR DA UNIDADE ATUALIZADA!"); + + // Atualizar o localStorage local para refletir a mudança sem refresh imediato no objeto workspaces + const updatedWorkspaces = workspaces.map(w => w.id === workspaceId ? { ...w, cor: color } : w); + localStorage.setItem('SCOREODONTO_WORKSPACES', JSON.stringify(updatedWorkspaces)); + + // Se a clínica editada for a ativa, atualizamos ela também + if (activeWorkspace?.id === workspaceId) { + localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify({ ...activeWorkspace, cor: color })); + } + + setIsEditingColor(null); + // Forçamos um reload após um pequeno delay para aplicar as cores globalmente via CSS/Variables se necessário + setTimeout(() => window.location.reload(), 1000); + } else { + toast.error("ERRO AO SALVAR COR."); + } + } catch (err) { + toast.error("ERRO DE CONEXÃO."); + } finally { + setSavingColor(false); + } + }; + + return ( +
+ + +
+ {workspaces.map((ws) => { + const isActive = activeWorkspace?.id === ws.id; + const clinColor = ws.cor || '#2563eb'; + + return ( +
+
+
+
+ +
+ +
+ {isActive && ( + + ATIVA + + )} + {isAdmin && ( + + )} +
+
+ +
+

+ {ws.nome} +

+ + {isEditingColor === ws.id ? ( +
+

Escolha a cor da marca

+
+ {PRESET_COLORS.map(c => ( + + ))} +
+
+ ) : ( + <> +
+ + Unidade Operacional +
+ +
+
+ +
+
+

Seu Perfil

+

{ws.role}

+
+
+ + )} +
+ + +
+
+ ); + })} +
+ +
+
+

Identidade Visual Dinâmica

+

A cor escolhida será aplicada automaticamente em toda a interface desta clínica.

+
+
+ {PRESET_COLORS.slice(0, 4).map(c => ( +
+ ))} +
+
+
+ ); +}; diff --git a/frontend/views/Dashboard.tsx b/frontend/views/Dashboard.tsx new file mode 100644 index 0000000..f67ffba --- /dev/null +++ b/frontend/views/Dashboard.tsx @@ -0,0 +1,284 @@ +import React, { useMemo } from 'react'; +import { + Users, + DollarSign, + TrendingUp, + Megaphone, + Calendar, + ChevronRight, + ArrowUpRight, + ArrowDownRight, + Plus, + Clock, + ArrowRight +} from 'lucide-react'; +import { PageHeader } from '../components/PageHeader.tsx'; +import { useHybridBackend } from '../hooks/useHybridBackend.ts'; +import { HybridBackend } from '../services/backend.ts'; +import { NotificationCenter } from '../components/NotificationCenter.tsx'; + +const StatCard: React.FC<{ + title: string; + value: string | number; + icon: React.ReactNode; + trend?: string; + isPositive?: boolean; + color: 'blue' | 'green' | 'purple' | 'amber'; + onClick?: () => void; +}> = ({ title, value, icon, trend, isPositive, color, onClick }) => { + const bgLight = { + blue: 'bg-blue-50 text-blue-600', + green: 'bg-emerald-50 text-emerald-600', + purple: 'bg-indigo-50 text-indigo-600', + amber: 'bg-amber-50 text-amber-600' + }; + + return ( +
+
+ +
+
+
+ {icon} +
+
+

{title}

+

{value}

+
+ {trend && ( +
+
+ {isPositive ? : } +
+ {trend} +
+ )} +
+
+
+ ); +}; + +export const Dashboard: React.FC = () => { + const { data, isLoading } = useHybridBackend(HybridBackend.getDashboardStats); + + const growthData = useMemo(() => { + if (!data?.growth || data.growth.length === 0) { + return Array(6).fill(null).map((_, i) => ({ + label: `MÊS ${i + 1}`, + receita: 0, + despesa: 0, + heightP: '0%', + heightR: '0%' + })); + } + + const maxVal = Math.max(...data.growth.map((g: any) => Math.max(parseFloat(g.receita), parseFloat(g.despesa))), 1000); + + return data.growth.map((g: any) => { + const [year, month] = g.mes.split('-'); + const date = new Date(parseInt(year), parseInt(month) - 1); + const label = date.toLocaleString('pt-BR', { month: 'short' }).toUpperCase(); + + return { + label, + receita: parseFloat(g.receita), + despesa: parseFloat(g.despesa), + // Proporção para o gráfico + heightR: `${(parseFloat(g.receita) / maxVal) * 100}%`, + heightD: `${(parseFloat(g.despesa) / maxVal) * 100}%` + }; + }).slice(-6); // Garantir últimos 6 + }, [data]); + + const currentRole = HybridBackend.getCurrentRole(); + const showFinancials = ['admin', 'donoclinica', 'funcionario'].includes(currentRole); + + if (isLoading) { + return ( +
+
+
+ ); + } + + const stats = data?.stats || { + totalPacientes: 0, + totalAgendamentos: 0, + totalLeads: 0, + faturamentoTotal: 0, + despesasTotal: 0 + }; + + const recent = data?.recentAgendamentos || []; + + return ( +
+
+
+ +
+
+ + {(currentRole === 'admin' || currentRole === 'donoclinica' || currentRole === 'funcionario') && ( + + )} + +
+
+ + {/* Stats Grid */} +
+ } + trend="+12% este mês" + isPositive={true} + color="blue" + onClick={() => window.location.hash = '#/pacientes'} + /> + {showFinancials && ( + } + trend="+5.4% vs mês ant." + isPositive={true} + color="green" + onClick={() => window.location.hash = '#/financeiro'} + /> + )} + } + trend="82% de ocupação" + isPositive={true} + color="purple" + onClick={() => window.location.hash = '#/agenda'} + /> + {showFinancials && ( + } + trend="-2% vs ontem" + isPositive={false} + color="amber" + onClick={() => window.location.hash = '#/leads'} + /> + )} +
+ +
+ {/* Growth Chart Area */} + {showFinancials && ( +
+
+
+

Crescimento Mensal

+

Receitas vs Despesas Reais

+
+ +
+ +
+ {growthData.map((d: any, i: number) => ( +
+
+ {/* Barra Despesa */} +
+ {/* Barra Receita */} +
+
+ {d.label} +
+ ))} +
+
+
+
+ Receitas +
+
+
+ Despesas +
+
+
+ )} + + {/* Recent Activity */} +
+
+
+

Últimos Agendamentos

+ +
+ {recent.length > 0 ? recent.map((app: any) => ( +
window.location.hash = '#/agenda'} + className="flex items-center gap-4 group cursor-pointer" + > +
+ +
+
+
{app.pacienteNome || 'Paciente'}
+
+ {app.dentistaNome || 'Dr. Dentista'} • {new Date(app.start_time).toLocaleDateString('pt-BR')} {new Date(app.start_time).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })} +
+
+ +
+ )) : ( +
+ +

Nenhum agendamento recente.

+
+ )} +
+ + +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/frontend/views/DentistRegisterView.tsx b/frontend/views/DentistRegisterView.tsx new file mode 100644 index 0000000..9e9df76 --- /dev/null +++ b/frontend/views/DentistRegisterView.tsx @@ -0,0 +1,250 @@ +import React, { useState, useEffect } from 'react'; +import { Database, Lock, User, Stethoscope, Phone, ShieldCheck, Mail, ArrowRight, Loader2 } from 'lucide-react'; +import { useToast } from '../contexts/ToastContext'; + +export const DentistRegisterView: React.FC = () => { + const [step, setStep] = useState(1); + const [loading, setLoading] = useState(false); + const toast = useToast(); + + // Get data from URL + const query = new URLSearchParams(window.location.hash.split('?')[1]); + const clinicaId = query.get('clinica'); + const tempName = query.get('nome'); + const token = query.get('token'); + + const [formData, setFormData] = useState({ + nome: tempName || '', + email: '', + senha: '', + cro: '', + cro_uf: 'MS', + celular: '', + especialidade: '' + }); + + if (!token || !clinicaId) { + return ( +
+
+
+ +
+

LINK INVÁLIDO OU EXPIRADO

+

Por favor, solicite um novo convite ao administrador da clínica.

+
+
+ ); + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + try { + const res = await fetch('http://localhost:3005/api/dentistas/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...formData, + id: 'd_' + Math.random().toString(36).substring(2, 9), + clinicaId + }) + }); + + if (res.ok) { + toast.success("CADASTRO REALIZADO COM SUCESSO!"); + setStep(3); + } else { + toast.error("ERRO AO REALIZAR CADASTRO."); + } + } catch (err) { + toast.error("FALHA DE CONEXÃO."); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ {/* Progress Header */} +
+
+ Convite de Profissional +
+

Complete seu Perfil

+

Bem-vindo ao SCOREODONTO. Você está sendo convidado para a unidade: {clinicaId}

+
+ +
+
+
+
+ + {step === 1 && ( +
+
{ e.preventDefault(); setStep(2); }}> +
+
+ +
+ + setFormData({ ...formData, nome: e.target.value.toUpperCase() })} + className="w-full bg-gray-50 border border-transparent focus:border-blue-500 focus:bg-white px-12 py-4 rounded-2xl outline-none transition-all font-bold text-gray-800 uppercase" + placeholder="NOME COMPLETO" + /> +
+
+ +
+
+ +
+ + setFormData({ ...formData, email: e.target.value })} + className="w-full bg-gray-50 border border-transparent focus:border-blue-500 focus:bg-white px-12 py-4 rounded-2xl outline-none transition-all font-bold text-gray-800" + placeholder="seu@email.com" + /> +
+
+
+ +
+ + setFormData({ ...formData, senha: e.target.value })} + className="w-full bg-gray-50 border border-transparent focus:border-blue-500 focus:bg-white px-12 py-4 rounded-2xl outline-none transition-all font-bold text-gray-800" + placeholder="••••••••" + /> +
+
+
+
+ + +
+
+ )} + + {step === 2 && ( +
+
+
+
+
+ +
+ + setFormData({ ...formData, cro: e.target.value })} + className="w-full bg-gray-50 border border-transparent focus:border-blue-500 focus:bg-white px-12 py-4 rounded-2xl outline-none transition-all font-bold text-gray-800 uppercase" + placeholder="00000" + /> +
+
+
+ + +
+
+ +
+ +
+ + setFormData({ ...formData, especialidade: e.target.value.toUpperCase() })} + className="w-full bg-gray-50 border border-transparent focus:border-blue-500 focus:bg-white px-12 py-4 rounded-2xl outline-none transition-all font-bold text-gray-800 uppercase" + placeholder="EX: ORTODONTIA" + /> +
+
+ +
+ +
+ + setFormData({ ...formData, celular: e.target.value })} + className="w-full bg-gray-50 border border-transparent focus:border-blue-500 focus:bg-white px-12 py-4 rounded-2xl outline-none transition-all font-bold text-gray-800 uppercase" + placeholder="(00) 00000-0000" + /> +
+
+
+ +
+ + +
+
+
+ )} + + {step === 3 && ( +
+
+ +
+

Tudo Pronto!

+

Seu perfil foi criado e você já está vinculado à unidade
{clinicaId}.

+ + +
+ )} +
+ +
+ Ambiente Seguro e Auditorado por Administradores +
+
+
+ ); +}; diff --git a/frontend/views/FinanceiroView.tsx b/frontend/views/FinanceiroView.tsx new file mode 100644 index 0000000..56f243c --- /dev/null +++ b/frontend/views/FinanceiroView.tsx @@ -0,0 +1,378 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Filter, Plus, TrendingUp, Clock, TrendingDown, CheckCircle, Download, Trash2, X, DollarSign, Loader2 } from 'lucide-react'; +import { HybridBackend } from '../services/backend.ts'; +import { FinanceiroItem, Paciente } from '../types.ts'; +import { useToast } from '../contexts/ToastContext.tsx'; + +// --- Reusable Debounce Hook --- +function useDebounce(value: string, delay: number) { + const [debouncedValue, setDebouncedValue] = useState(value); + useEffect(() => { + const handler = setTimeout(() => setDebouncedValue(value), delay); + return () => clearTimeout(handler); + }, [value, delay]); + return debouncedValue; +} + +// ----------- NOVO LANÇAMENTO MODAL ----------- +const NovoLancamentoModal: React.FC<{ + onClose: () => void; + onSaved: () => void; +}> = ({ onClose, onSaved }) => { + const toast = useToast(); + const [selectedPatient, setSelectedPatient] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [isSearchFocused, setIsSearchFocused] = useState(false); + const debouncedSearchTerm = useDebounce(searchTerm, 400); + + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + + useEffect(() => { + if (!debouncedSearchTerm) { setSearchResults([]); return; } + setIsSearching(true); + HybridBackend.getPacientes(debouncedSearchTerm).then(results => { + setSearchResults(results); + setIsSearching(false); + }); + }, [debouncedSearchTerm]); + + const handlePatientSelect = (patient: Paciente) => { + setSelectedPatient(patient); + setSearchTerm(''); + setIsSearchFocused(false); + }; + + const [form, setForm] = useState({ + id: crypto.randomUUID(), + descricao: '', + valor: '', + dataVencimento: new Date().toISOString().split('T')[0], + status: 'Pendente' as 'Pago' | 'Pendente' | 'Atrasado', + formaPagamento: 'Pix' as 'Boleto' | 'Pix' | 'Cartão' | 'Dinheiro', + }); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [onClose]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedPatient || !form.descricao || !form.valor) { + toast.error('PREENCHA TODOS OS CAMPOS OBRIGATÓRIOS.'); + return; + } + try { + await HybridBackend.saveFinanceiro({ + ...form, + pacienteNome: selectedPatient.nome, + valor: parseFloat(form.valor), + }); + toast.success('LANÇAMENTO SALVO COM SUCESSO!'); + onSaved(); + onClose(); + } catch { + toast.error('ERRO AO SALVAR LANÇAMENTO.'); + } + }; + + return ( +
+
+ {/* HEADER */} +
+

+ NOVO LANÇAMENTO +

+ +
+ + {/* BODY */} +
+ {/* PACIENTE SEARCH */} +
+ + {!selectedPatient ? ( +
+ setSearchTerm(e.target.value.toUpperCase())} + onFocus={() => setIsSearchFocused(true)} + onBlur={() => setTimeout(() => setIsSearchFocused(false), 200)} + placeholder="DIGITE O NOME OU CPF..." + className="w-full lg:w-1/2 bg-gray-100 p-3 lg:p-4 rounded-lg font-bold text-gray-800 uppercase focus:border-blue-500 outline-none border-2 border-transparent focus:border-2 text-sm lg:text-base" + /> + {isSearchFocused && debouncedSearchTerm && ( +
    + {isSearching &&
  • BUSCANDO...
  • } + {searchResults.map(p => ( +
  • handlePatientSelect(p)} className="p-3 hover:bg-gray-100 cursor-pointer uppercase font-semibold text-sm"> + {p.nome} ({p.cpf}) +
  • + ))} + {!isSearching && searchResults.length === 0 &&
  • NENHUM PACIENTE ENCONTRADO.
  • } +
+ )} +
+ ) : ( +
+ {selectedPatient.nome} + +
+ )} +
+ +
+ {/* DESCRIÇÃO */} +
+ + setForm(f => ({ ...f, descricao: e.target.value.toUpperCase() }))} + className="w-full mt-2 border border-gray-300 rounded-lg p-2.5 lg:p-3 uppercase-input text-sm" placeholder="EX: CONSULTA AVALIAÇÃO, LIMPEZA..." /> +
+ {/* VALOR */} +
+ + setForm(f => ({ ...f, valor: e.target.value }))} + className="w-full mt-2 border border-gray-300 rounded-lg p-2.5 lg:p-3 text-sm" placeholder="0,00" /> +
+ {/* VENCIMENTO */} +
+ + setForm(f => ({ ...f, dataVencimento: e.target.value }))} + className="w-full mt-2 border border-gray-300 rounded-lg p-2.5 lg:p-3 text-sm" /> +
+ {/* STATUS */} +
+ + +
+ {/* FORMA DE PAGAMENTO */} +
+ + +
+
+
+ + {/* FOOTER */} +
+ + +
+
+
+ ); +}; + +// ----------- MAIN VIEW ----------- +import { PageHeader } from '../components/PageHeader.tsx'; + +export const FinanceiroView: React.FC = () => { + const [transacoes, setTransacoes] = useState([]); + const [loading, setLoading] = useState(true); + const [filtroStatus, setFiltroStatus] = useState<'TODOS' | 'Pago' | 'Pendente' | 'Atrasado'>('TODOS'); + const [showFiltro, setShowFiltro] = useState(false); + const [isNovoOpen, setIsNovoOpen] = useState(false); + const toast = useToast(); + + const fetchData = useCallback(() => { + setLoading(true); + HybridBackend.getFinanceiro().then((data) => { + setTransacoes(data); + setLoading(false); + }); + }, []); + + useEffect(() => { fetchData(); }, [fetchData]); + + const transacoesFiltradas = filtroStatus === 'TODOS' ? transacoes : transacoes.filter(t => t.status === filtroStatus); + + const totalRecebido = transacoes.filter(t => t.status === 'Pago').reduce((acc, t) => acc + Number(t.valor), 0); + const totalPendente = transacoes.filter(t => t.status === 'Pendente').reduce((acc, t) => acc + Number(t.valor), 0); + const totalAtrasado = transacoes.filter(t => t.status === 'Atrasado').reduce((acc, t) => acc + Number(t.valor), 0); + + const handleConfirmarPagamento = async (item: FinanceiroItem) => { + if (!window.confirm(`CONFIRMAR PAGAMENTO DE R$ ${Number(item.valor).toFixed(2)} DE ${item.pacienteNome}?`)) return; + try { + await HybridBackend.updateFinanceiro({ ...item, status: 'Pago' }); + toast.success(`PAGAMENTO DE ${item.pacienteNome} CONFIRMADO!`); + fetchData(); + } catch { + toast.error('ERRO AO CONFIRMAR PAGAMENTO.'); + } + }; + + const handleExcluir = async (item: FinanceiroItem) => { + if (!window.confirm(`TEM CERTEZA QUE DESEJA EXCLUIR O LANÇAMENTO DE ${item.pacienteNome}? ESTA AÇÃO NÃO PODE SER DESFEITA.`)) return; + try { + await HybridBackend.deleteFinanceiro(item.id); + toast.success('LANÇAMENTO EXCLUÍDO!'); + fetchData(); + } catch { + toast.error('ERRO AO EXCLUIR LANÇAMENTO.'); + } + }; + + const handleDownload = (item: FinanceiroItem) => { + toast.success(`RECIBO DE ${item.pacienteNome} GERADO COM SUCESSO!`); + }; + + const filtroLabel = filtroStatus === 'TODOS' ? 'FILTRAR' : filtroStatus.toUpperCase(); + + return ( +
+ +
+ {/* FILTRAR */} +
+ + {showFiltro && ( +
+ {(['TODOS', 'Pago', 'Pendente', 'Atrasado'] as const).map(status => ( + + ))} +
+ )} +
+ {/* NOVO LANÇAMENTO */} + +
+
+ + {/* DASHBOARD CARDS */} +
+
+
+

RECEBIDO (MÊS)

+

R$ {totalRecebido.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}

+
+
+
+
+
+

A RECEBER / PENDENTE

+

R$ {totalPendente.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}

+
+
+
+
+
+

EM ATRASO

+

R$ {totalAtrasado.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}

+
+
+
+
+ + {/* LISTA DE TRANSAÇÕES */} +
+
+

+ LANÇAMENTOS {filtroStatus !== 'TODOS' && ({filtroStatus.toUpperCase()})} +

+ {transacoesFiltradas.length} REGISTRO(S) +
+
+ + + + + + + + + + + + + {loading ? ( + + ) : transacoesFiltradas.length === 0 ? ( + + ) : transacoesFiltradas.map(t => ( + + + + + + + + + ))} + +
PACIENTEDESCRIÇÃOVENCIMENTOVALORSTATUSAÇÕES
+
CARREGANDO...
+
+ NENHUM LANÇAMENTO ENCONTRADO {filtroStatus !== 'TODOS' && `COM STATUS "${filtroStatus.toUpperCase()}"`}. +
{t.pacienteNome} + {t.descricao} + {t.formaPagamento?.toUpperCase()} + {new Date(t.dataVencimento).toLocaleDateString('pt-BR')}R$ {Number(t.valor).toLocaleString('pt-BR', { minimumFractionDigits: 2 })} + + {t.status} + + +
+ {t.status !== 'Pago' && ( + + )} + + +
+
+
+
+ + {/* CLOSE FILTRO DROPDOWN when clicking outside */} + {showFiltro &&
setShowFiltro(false)} />} + + {/* NOVO LANÇAMENTO MODAL */} + {isNovoOpen && setIsNovoOpen(false)} onSaved={fetchData} />} +
+ ); +}; \ No newline at end of file diff --git a/frontend/views/LancarGTO.tsx b/frontend/views/LancarGTO.tsx new file mode 100644 index 0000000..6e51d31 --- /dev/null +++ b/frontend/views/LancarGTO.tsx @@ -0,0 +1,532 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { + Users, + Trash2, + Check, + Building2, + AlertCircle, + PlusCircle, + Camera, + Stethoscope, + ChevronRight +} from 'lucide-react'; +import { PageHeader } from '../components/PageHeader.tsx'; +import { HybridBackend } from '../services/backend.ts'; +import { Paciente, Dentista, Procedimento, Especialidade } from '../types.ts'; +import { useToast } from '../contexts/ToastContext.tsx'; +import { create } from 'zustand'; + +// --- STORES --- +interface GTOItem { + id: string; + procedimentoId: string; + codigoTUSS: string; + codigoInterno: string; + descricao: string; + dente?: string; + face?: string; + arco?: string; + quantidade: number; + valorUnitario: number; + valorTotal: number; + observacao?: string; +} + +interface GTOStore { + paciente: Paciente | null; + dentista: Dentista | null; + credenciada: string; + items: GTOItem[]; + setPaciente: (p: Paciente | null) => void; + setDentista: (d: Dentista | null) => void; + setCredenciada: (c: string) => void; + addItem: (item: GTOItem) => void; + removeItem: (id: string) => void; + clear: () => void; +} + +const useGTOStore = create((set) => ({ + paciente: null, + dentista: null, + credenciada: 'CASSEMS - SEDE CENTRAL', + items: [], + setPaciente: (paciente) => set({ paciente }), + setDentista: (dentista) => set({ dentista }), + setCredenciada: (credenciada) => set({ credenciada }), + addItem: (item) => set((state) => ({ + items: [...state.items, item].slice(0, 20) + })), + removeItem: (id) => set((state) => ({ + items: state.items.filter(i => i.id !== id) + })), + clear: () => set({ paciente: null, dentista: null, items: [] }), +})); + +// --- ODONTOGRAM DATA (4x2 Grid Layout) --- +const DENTES_ADULTO = { + q1: [14, 13, 12, 11, 18, 17, 16, 15], // Top: 14-11, Bottom: 18-15 + q2: [21, 22, 23, 24, 25, 26, 27, 28], // Top: 21-24, Bottom: 25-28 + q4: [44, 43, 42, 41, 48, 47, 46, 45], // Top: 44-41, Bottom: 48-45 + q3: [31, 32, 33, 34, 35, 36, 37, 38], // Top: 31-34, Bottom: 35-38 + extras: { q1: 19, q2: 29, q4: 49, q3: 39 } +}; + +const DENTES_CRIANCA = { + // Balanced 4x2 distribution with nulls for spacing (matching image layout) + q1: [null, 53, 52, 51, null, null, 55, 54], + q2: [61, 62, 63, null, 64, 65, null, null], + q4: [null, 43, 42, 41, null, null, 46, 45], + q3: [71, 72, 73, null, 74, 75, null, null] +}; + +// --- COMPONENT --- +export const LancarGTO: React.FC = () => { + const store = useGTOStore(); + const toast = useToast(); + + const [pacientes, setPacientes] = useState([]); + const [dentistas, setDentistas] = useState([]); + const [especialidades, setEspecialidades] = useState([]); + const [procedimentos, setProcedimentos] = useState([]); + + const [selectedEsp, setSelectedEsp] = useState(''); + const [selectedProc, setSelectedProc] = useState(null); + const [pacienteTipo, setPacienteTipo] = useState<'ADULTO' | 'CRIANCA'>('ADULTO'); + const [selectedDentes, setSelectedDentes] = useState([]); + const [selectedArco, setSelectedArco] = useState<'AI' | 'AS' | null>(null); + const [face, setFace] = useState(''); + const [obs, setObs] = useState(''); + + useEffect(() => { + const load = async () => { + const p = await HybridBackend.getPacientes(); + const d = await HybridBackend.getDentistas(); + const es = await HybridBackend.getEspecialidades(); + const pr = await HybridBackend.getProcedimentos(); + setPacientes(p); + setDentistas(d); + setEspecialidades(es); + setProcedimentos(pr); + }; + load(); + }, []); + + const filteredProcedimentos = useMemo(() => { + if (!selectedEsp) return []; + return procedimentos.filter(p => p.especialidadeId === selectedEsp); + }, [selectedEsp, procedimentos]); + + const toggleDente = (num: string) => { + if (selectedProc?.tipo_regiao !== 'DENTE') { + toast.error("ESTE PROCEDIMENTO NÃO PERMITE SELEÇÃO DE DENTE INDIVIDUAL."); + return; + } + setSelectedArco(null); + setSelectedDentes(prev => + prev.includes(num) ? prev.filter(d => d !== num) : [...prev, num] + ); + }; + + const selectArco = (arco: 'AI' | 'AS') => { + if (selectedProc?.tipo_regiao !== 'ARCO') { + toast.error("ESTE PROCEDIMENTO NÃO PERMITE SELEÇÃO DE ARCO."); + return; + } + setSelectedDentes([]); + setSelectedArco(arco); + }; + + const handleAddItem = () => { + if (!selectedProc) { + toast.error("SELECIONE UM PROCEDIMENTO PRIMEIRO."); + return; + } + + // Validação de Região + if (selectedProc.tipo_regiao === 'DENTE' && selectedDentes.length === 0) { + toast.error("ESTE PROCEDIMENTO EXIGE A SELEÇÃO DE AO MENOS UM DENTE."); + return; + } + + if (selectedProc.tipo_regiao === 'ARCO' && !selectedArco) { + toast.error("ESTE PROCEDIMENTO EXIGE A SELEÇÃO DE UM ARCO (AI/AS)."); + return; + } + + // Validação de Face + if (selectedProc.exige_face && (!face || face.trim() === '')) { + toast.error("A FACE É OBRIGATÓRIA PARA ESTE PROCEDIMENTO."); + return; + } + + const newItem: GTOItem = { + id: Math.random().toString(36).substring(7), + procedimentoId: selectedProc.id, + codigoTUSS: selectedProc.codigo || '000000', + codigoInterno: selectedProc.codigoInterno || '', + descricao: selectedProc.nome, + dente: selectedDentes.join(', '), + arco: selectedArco || undefined, + face: face, + quantidade: 1, + valorUnitario: selectedProc.valorParticular || 0, + valorTotal: selectedProc.valorParticular || 0, + observacao: obs, + }; + + store.addItem(newItem); + toast.success("ADICIONADO AO RASCUNHO."); + + // Reset selection fields + setSelectedDentes([]); + setSelectedArco(null); + setFace(''); + setObs(''); + }; + + const handleFinalize = async () => { + if (!store.paciente || !store.dentista || store.items.length === 0) { + toast.error("PACIENTE, DENTISTA E PELO MENOS 1 ITEM SÃO OBRIGATÓRIOS."); + return; + } + + try { + toast.success("GTO GERADA COM SUCESSO!"); + store.clear(); + setSelectedEsp(''); + setSelectedProc(null); + } catch { + toast.error("ERRO AO GERAR GTO."); + } + }; + + const totalGeral = store.items.reduce((sum, i) => sum + i.valorTotal, 0); + + const renderDenteBtn = (num: number | string | null, keyIdx?: number) => { + if (num === null) return
; + const sStr = num.toString(); + const isSelected = selectedDentes.includes(sStr); + const isDisabled = !!selectedProc && selectedProc.tipo_regiao !== 'DENTE'; + + return ( + + ); + }; + + return ( +
+ + +
+ + {/* LEFT PANEL: ODONTOGRAM & ITEMS */} +
+ + {/* ODONTOGRAM BOX */} +
+
+
+ + +
+
+ + {/* Dynamic Odontogram Grid */} +
+ {/* Visual Separators */} +
+
+ +
+ {/* Q1 & Q2 (SUPERIOR) */} +
+ {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q1)} +
+ {(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q1 : DENTES_CRIANCA.q1).map((d, i) => renderDenteBtn(d, i))} +
+
+
+
+ {(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q2 : DENTES_CRIANCA.q2).map((d, i) => renderDenteBtn(d, i))} +
+ {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q2)} +
+ + {/* Q4 & Q3 (INFERIOR) */} +
+ {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q4)} +
+ {(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q4 : DENTES_CRIANCA.q4).map((d, i) => renderDenteBtn(d, i))} +
+
+
+
+ {(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q3 : DENTES_CRIANCA.q3).map((d, i) => renderDenteBtn(d, i))} +
+ {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q3)} +
+
+
+ +
+ + +
+
+ + {/* RASCUNHO LIST */} +
+
+ PROCEDIMENTOS DO RASCUNHO +
{store.items.length}/20 ITENS
+
+ +
+ {store.items.length === 0 && ( +
+
+ +
+ NÃO HÁ ITENS ADICIONADOS +
+ )} + + {store.items.map((item) => ( +
+
+
+
+ COD {item.codigoTUSS} +

{item.descricao}

+
+
+ {store.paciente?.nome} +
+
+ +
+ +
+
DENTE: {item.dente || '--'}
+
ARCO: {item.arco || '--'}
+
FACE: {item.face || '--'}
+
VALOR: R$ {item.valorTotal.toFixed(2)}
+
+ + {item.observacao && ( +
+ OBS: {item.observacao} +
+ )} +
+ ))} + +
+ +
+

TOTAL ACUMULADO

+

R$ {totalGeral.toFixed(2)}

+
+
+
+
+
+ + {/* RIGHT PANEL: WORKFLOW SELECTORS */} +
+ + {/* STEP 1: PACIENTE */} +
+
+ 01. Paciente +
+ +
+ + {/* STEP 2: ESPECIALIDADE & PROCEDIMENTO */} +
+
+ 02. Especialidade +
+
+ + + {selectedEsp && ( +
+ + + {selectedProc && ( +
+

REGRAS DE SELEÇÃO

+
+ + + {selectedProc.tipo_regiao === 'DENTE' ? 'EXIGE DENTE(S)' : selectedProc.tipo_regiao === 'ARCO' ? 'EXIGE ARCO (AI/AS)' : 'PROCEDIMENTO GERAL'} + + {selectedProc.exige_face && ( + + EXIGE FACE CLINICA + + )} +
+
+ )} +
+ )} +
+
+ + {/* STEP 3: DENTISTA */} +
+
+ 03. Dentista Executor +
+ +
+ + {/* STEP 4: DETALHES (FACE/OBS) */} +
+
+
+ + setFace(e.target.value.toUpperCase())} + disabled={selectedProc?.tipo_regiao === 'GERAL' && !selectedProc?.exige_face} + className={`w-full p-4 border-2 rounded-2xl text-sm font-black outline-none transition-all + ${selectedProc?.exige_face ? 'bg-red-50 border-red-100 focus:border-red-400' : 'bg-gray-50 border-gray-100 focus:border-amber-400'} + `} + placeholder={selectedProc?.exige_face ? "DIGITE AS FACES..." : "OPCIONAL"} + /> +
+
+ +
+