MedIABook - Sistema de Bibliotecas Médicas
Sistema completo de gerenciamento de bibliotecas médicas e temas com busca inteligente Full-Text Search
O MedIABook é um sistema completo de gerenciamento de bibliotecas médicas que permite organizar, buscar e compartilhar conteúdo médico de forma eficiente. O sistema é composto por dois módulos principais: Viewer (para usuários) e Gerenciador (para super-administradores).
Visão Geral
Funcionalidades Principais
- Busca Inteligente: Full-Text Search com PostgreSQL usando
tsvectorpara buscas em português - Sistema de Bibliotecas: Organize temas em bibliotecas categorizadas (medicamentos, doenças, orientações, etc.)
- Favoritos: Usuários podem marcar temas como favoritos para acesso rápido
- Seções Dinâmicas: 4 tipos de seções (título, subtítulo, texto, título com conteúdo copiável)
- Sistema de Tags: Hashtags para facilitar a busca e organização
- Busca em Tempo Real: Atualização instantânea conforme o usuário digita (debounce 500ms)
- Busca Flexível: Suporte a buscas sem acentos, parciais e por relevância
Arquitetura
Estrutura de Arquivos
apps/web/
├── app/
│ ├── home/(user)/mediabook/ # Viewer (Usuário)
│ │ ├── page.tsx # Lista de temas
│ │ ├── [slug]/page.tsx # Detalhes do tema
│ │ ├── _components/ # Componentes UI
│ │ │ ├── copy-button.tsx
│ │ │ ├── favorite-button.tsx
│ │ │ ├── library-filter.tsx
│ │ │ ├── search-bar.tsx
│ │ │ ├── theme-card.tsx
│ │ │ ├── theme-viewer.tsx
│ │ │ └── themes-grid.tsx
│ │ └── _lib/
│ │ ├── components/
│ │ │ └── icon-picker.tsx # Seletor Lucide
│ │ ├── hooks/
│ │ │ └── use-debounce.ts
│ │ ├── schemas/
│ │ │ └── mediabook.schema.ts
│ │ └── server/
│ │ ├── mediabook-page.loader.ts
│ │ ├── mediabook.service.ts
│ │ └── server-actions.ts
│ │
│ └── admin/mediabook/ # Gerenciador (Admin)
│ ├── page.tsx # Painel principal
│ ├── _components/
│ │ ├── libraries-manager.tsx
│ │ ├── library-card.tsx
│ │ ├── library-form-dialog.tsx
│ │ ├── theme-card.tsx
│ │ ├── theme-form-dialog.tsx
│ │ └── themes-manager.tsx
│ └── _lib/
│ ├── schemas/
│ │ └── admin-mediabook.schema.ts
│ └── server/
│ └── admin-server-actions.ts
│
├── supabase/
│ ├── migrations/
│ │ └── 20251205022854_mediabook-complete.sql
│ ├── schemas/
│ │ └── 18-mediabook.sql
│ └── seed-mediabook.sql
│
└── public/locales/pt-br/
└── mediabook.json
Banco de Dados
O MedIABook utiliza 4 tabelas principais com Row Level Security (RLS):
Tabelas
mediabook_libraries: Bibliotecas de conteúdoid(UUID)name(TEXT)description(TEXT)icon(TEXT) - Nome do ícone Lucidecreated_at,updated_at
mediabook_themes: Temas/artigosid(UUID)title(TEXT)description(TEXT)slug(TEXT UNIQUE)library_id(UUID FK)tags(TEXT[]) - Hashtagssearch_vector(TSVECTOR) - Para Full-Text Searchcreated_at,updated_at
mediabook_sections: Seções dos temasid(UUID)theme_id(UUID FK)type(ENUM: title, subtitle, text, title_with_copyable)content(TEXT)order(INTEGER)created_at,updated_at
mediabook_favorites: Favoritos dos usuáriosuser_id(UUID FK)theme_id(UUID FK)created_at
Funções SQL
is_super_admin_mediabook()- Verifica se o usuário é super-admin (sem MFA)
- Usado nas RLS policies do gerenciador
search_mediabook_themes(query, library_ids)- Busca Full-Text Search com
tsvector - Suporta busca sem acentos usando
kit.unaccent - Retorna resultados ordenados por relevância
- Ranking: Título (A) > Tags (B) > Descrição (C) > Conteúdo (D)
- Busca Full-Text Search com
get_mediabook_themes_by_library(library_id)- Retorna temas de uma biblioteca específica
get_mediabook_theme_with_sections(slug)- Retorna tema completo com suas seções e informação de favorito
update_mediabook_theme_search_vector()- Trigger para atualizar
search_vectorautomaticamente
- Trigger para atualizar
Row Level Security (RLS)
Viewer (Usuários Autenticados):
- SELECT em todas as tabelas
- INSERT/DELETE apenas em
mediabook_favorites
Gerenciador (Super-Admin):
- Acesso completo (CRUD) em todas as tabelas
- Validação via
is_super_admin_mediabook()
Usando o Viewer
Acessando o Viewer
O Viewer está disponível em /home/mediabook para usuários autenticados:
// apps/web/app/home/(user)/mediabook/page.tsx
async function MediaBookPage() {
// Página carrega automaticamente:
// - Lista de temas
// - Bibliotecas disponíveis
// - Favoritos do usuário
}
Componentes Principais
SearchBar
Busca em tempo real com debounce de 500ms:
import { SearchBar } from './_components/search-bar';
<SearchBar
onSearch={(query) => console.log(query)}
placeholder="Buscar temas..."
/>
LibraryFilter
Filtro de bibliotecas com múltipla seleção:
import { LibraryFilter } from './_components/library-filter';
<LibraryFilter
libraries={libraries}
selectedLibraries={selected}
onFilterChange={(ids) => console.log(ids)}
/>
ThemesGrid
Grid responsivo com tabs "Todas" e "Favoritos":
import { ThemesGrid } from './_components/themes-grid';
<ThemesGrid
initialThemes={themes}
libraries={libraries}
favorites={favorites}
/>
ThemeViewer
Visualizador completo de um tema:
import { ThemeViewer } from './_components/theme-viewer';
<ThemeViewer
theme={theme}
sections={sections}
library={library}
isFavorited={true}
/>
Busca Inteligente
A busca do MedIABook utiliza PostgreSQL Full-Text Search para resultados precisos e rápidos:
- Busca sem acentos: "infeccao" encontra "Infecção Urinária"
- Busca parcial: "dor" encontra "Analgésicos para Dor"
- Busca por hashtags: "#antibiotico" encontra temas com essa tag
- Ranking por relevância: Título > Tags > Descrição > Conteúdo
Usando o Gerenciador
Acessando o Gerenciador
O Gerenciador está disponível em /admin/mediabook apenas para super-administradores:
// apps/web/app/admin/mediabook/page.tsx
import { AdminGuard } from '@kit/admin/components';
export default async function AdminMediaBookPage() {
return (
<AdminGuard>
{/* Conteúdo do gerenciador */}
</AdminGuard>
);
}
Criando uma Biblioteca
import { LibraryFormDialog } from './_components/library-form-dialog';
<LibraryFormDialog
mode="create"
onSuccess={() => console.log('Biblioteca criada')}
/>
Campos do formulário:
- Nome: Nome da biblioteca
- Descrição: Descrição opcional
- Ícone: Seletor de ícones Lucide (200+ opções)
Criando um Tema
import { ThemeFormDialog } from './_components/theme-form-dialog';
<ThemeFormDialog
mode="create"
libraries={libraries}
onSuccess={() => console.log('Tema criado')}
/>
Campos do formulário:
- Título: Título do tema
- Biblioteca: Selecione a biblioteca
- Descrição: Descrição curta
- Tags: Hashtags (ex: #antibiotico, #viral)
- Seções: Adicione seções dinamicamente
Tipos de Seções
- Título: Cabeçalho de seção
- Subtítulo: Subcabeçalho
- Texto Simples: Conteúdo de texto
- Título com Conteúdo Copiável: Título + conteúdo que pode ser copiado (ex: receitas, orientações)
Server Actions
Buscar Temas
import { searchThemesAction } from '~/app/home/(user)/mediabook/_lib/server/server-actions';
const result = await searchThemesAction({
query: 'dengue',
libraryIds: ['uuid-1', 'uuid-2']
});
Adicionar Favorito
import { addFavoriteAction } from '~/app/home/(user)/mediabook/_lib/server/server-actions';
await addFavoriteAction({ themeId: 'uuid' });
Remover Favorito
import { removeFavoriteAction } from '~/app/home/(user)/mediabook/_lib/server/server-actions';
await removeFavoriteAction({ themeId: 'uuid' });
Criar Biblioteca (Admin)
import { createLibraryAction } from '~/app/admin/mediabook/_lib/server/admin-server-actions';
await createLibraryAction({
name: 'Biblioteca de Medicamentos',
description: 'Medicamentos comuns',
icon: 'Pill'
});
Criar Tema (Admin)
import { createThemeAction } from '~/app/admin/mediabook/_lib/server/admin-server-actions';
await createThemeAction({
title: 'Dengue',
libraryId: 'uuid',
description: 'Informações sobre dengue',
tags: ['#viral', '#febre'],
sections: [
{ type: 'title', content: 'Sintomas', order: 1 },
{ type: 'text', content: 'Febre alta, dor...', order: 2 }
]
});
Hooks Personalizados
useDebounce
Hook para debounce de valores (usado na busca):
import { useDebounce } from '~/app/home/(user)/mediabook/_lib/hooks/use-debounce';
function SearchComponent() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 500);
useEffect(() => {
// Executa busca com debouncedQuery
}, [debouncedQuery]);
}
Validação com Zod
Theme Schema
import { z } from 'zod';
const ThemeSchema = z.object({
id: z.string().uuid(),
title: z.string().min(1).max(255),
description: z.string().optional(),
slug: z.string(),
library_id: z.string().uuid(),
tags: z.array(z.string()),
created_at: z.string().datetime(),
updated_at: z.string().datetime(),
});
Section Schema
const SectionSchema = z.object({
id: z.string().uuid(),
theme_id: z.string().uuid(),
type: z.enum(['title', 'subtitle', 'text', 'title_with_copyable']),
content: z.string().min(1),
order: z.number().int().positive(),
});
Internacionalização
Todas as strings da interface estão em português em public/locales/pt-br/mediabook.json:
{
"viewer": {
"title": "MedIABook",
"searchPlaceholder": "Buscar temas...",
"allLibraries": "Todas as Bibliotecas",
"favorites": "Favoritos",
"noThemesFound": "Nenhum tema encontrado",
"copy": "Copiar",
"copied": "Copiado"
},
"manager": {
"title": "Gerenciador MedIABook",
"libraries": {
"title": "Bibliotecas",
"new": "Nova Biblioteca",
"edit": "Editar Biblioteca",
"delete": "Excluir Biblioteca"
},
"themes": {
"title": "Temas",
"new": "Novo Tema",
"edit": "Editar Tema"
}
}
}
Performance
Full-Text Search
O sistema utiliza PostgreSQL Full-Text Search com:
- GIN Index em
search_vectorpara buscas rápidas - Triggers automáticos para atualizar
search_vector - Função
kit.unaccentpara normalizar texto (remover acentos) - Ranking por relevância usando
ts_rank_cd
Performance esperada:
- Busca em ~5-10ms para 1000+ temas
- Atualização automática de índice em <1ms
Debounce
A busca em tempo real usa debounce de 500ms para evitar requisições excessivas:
// Evita buscar a cada tecla pressionada const debouncedQuery = useDebounce(query, 500);
Segurança
Row Level Security (RLS)
Todas as tabelas têm RLS habilitado:
-- Usuários podem apenas visualizar CREATE POLICY "mediabook_themes_read" ON public.mediabook_themes FOR SELECT TO authenticated USING (true); -- Super-admins podem tudo CREATE POLICY "mediabook_themes_admin" ON public.mediabook_themes FOR ALL TO authenticated USING (public.is_super_admin_mediabook()) WITH CHECK (public.is_super_admin_mediabook());
Validação de Entrada
Todas as entradas são validadas com Zod antes de serem processadas:
const result = CreateThemeSchema.safeParse(data);
if (!result.success) {
return { error: 'Dados inválidos' };
}
Migrações
Para aplicar a migração do MedIABook:
# Resetar banco com nova migração pnpm supabase:web:reset # Ou aplicar manualmente pnpm --filter web supabase migrations up
Seed Data
Para popular o banco com dados de teste:
# Execute o seed SQL psql -h localhost -U postgres -d postgres \ -f apps/web/supabase/seed-mediabook.sql
O seed inclui:
- 5 bibliotecas exemplo (Medicamentos, Doenças, Orientações, Procedimentos, Exames)
- 8 temas exemplo (Dengue, Hipertensão, Diabetes, etc.)
- Múltiplas seções por tema
Troubleshooting
Busca não funciona
Problema: Erro 42501 (permission denied for schema kit)
Solução: Garantir que as permissões no schema kit estão corretas:
GRANT USAGE ON SCHEMA kit TO authenticated, service_role; GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA kit TO authenticated, service_role;
Favoritos não atualizam
Problema: Botão de favoritos não atualiza sem refresh
Solução: Adicionar revalidatePath nas server actions:
revalidatePath('/home/mediabook');
Busca lenta
Problema: Buscas demoram muito
Solução: Verificar se o índice GIN foi criado:
-- Verificar índices SELECT indexname FROM pg_indexes WHERE tablename = 'mediabook_themes'; -- Deve incluir: idx_mediabook_themes_search
Exemplos Completos
Exemplo: Busca com Filtros
'use client';
import { useState, useEffect } from 'react';
import { SearchBar } from './_components/search-bar';
import { LibraryFilter } from './_components/library-filter';
import { ThemesGrid } from './_components/themes-grid';
import { searchThemesAction } from './_lib/server/server-actions';
export function MediaBookSearch({ libraries, initialThemes }) {
const [query, setQuery] = useState('');
const [selectedLibraries, setSelectedLibraries] = useState([]);
const [themes, setThemes] = useState(initialThemes);
useEffect(() => {
const search = async () => {
const result = await searchThemesAction({
query,
libraryIds: selectedLibraries
});
if (result.data) {
setThemes(result.data);
}
};
search();
}, [query, selectedLibraries]);
return (
<div>
<SearchBar onSearch={setQuery} />
<LibraryFilter
libraries={libraries}
selectedLibraries={selectedLibraries}
onFilterChange={setSelectedLibraries}
/>
<ThemesGrid themes={themes} />
</div>
);
}
Exemplo: Criar Tema Completo
const newTheme = await createThemeAction({
title: 'Infecção Urinária',
libraryId: 'uuid-biblioteca-doencas',
description: 'Informações sobre infecção urinária',
tags: ['#itu', '#infeccao', '#antibiotico'],
sections: [
{
type: 'title',
content: 'Definição',
order: 1
},
{
type: 'text',
content: 'Infecção bacteriana do trato urinário...',
order: 2
},
{
type: 'title',
content: 'Tratamento',
order: 3
},
{
type: 'title_with_copyable',
content: 'Receita\nCiprofloxacino 500mg\n1 comprimido a cada 12h por 7 dias',
order: 4
}
]
});
Estatísticas do Projeto
- Total de arquivos: 29
- Linhas de código: ~5,419
- TypeScript/TSX: 25 arquivos
- SQL: 3 arquivos
- JSON: 1 arquivo
- Tabelas: 4
- Funções SQL: 5
- Componentes React: 14 (viewer) + 7 (gerenciador)
Importação de Temas via JSON
O MedIABook suporta importação de temas via JSON, permitindo criar conteúdo em massa usando IA (ChatGPT, Claude, etc.) ou arquivos externos.
Acessando a Importação
No Gerenciador (/admin/mediabook > Temas), clique no botão "Importar".
Formato JSON para IA
Use o prompt abaixo para gerar temas compatíveis:
Gere um tema médico para o MedIABook seguindo EXATAMENTE este formato JSON:
{
"title": "Título do Tema (3-200 caracteres)",
"description": "Descrição opcional do tema",
"tags": ["tag1", "tag2", "tag3"],
"is_published": false,
"sections": [
{ "type": "TIPO", "content": "Conteúdo da seção" }
]
}
TIPOS DE SEÇÃO DISPONÍVEIS:
- "title" - Título/cabeçalho de seção
- "subtitle" - Subtítulo
- "text" - Texto explicativo
- "copyable_content" - Conteúdo copiável (receitas, orientações)
- "title_with_copyable" - Título + conteúdo copiável (separados por \n)
REGRAS:
1. O JSON deve ser válido
2. Cada seção deve ter "type" e "content"
3. Para "title_with_copyable", use \n para separar título do conteúdo
4. Tags são opcionais mas recomendadas para busca
Exemplos de JSON
Tema Único
{
"title": "Episódio Depressivo Maior - Critérios DSM-5",
"description": "Critérios diagnósticos para Episódio Depressivo Maior",
"tags": ["psiquiatria", "depressão", "dsm-5", "humor"],
"is_published": false,
"sections": [
{ "type": "title", "content": "Critérios Diagnósticos (DSM-5)" },
{ "type": "text", "content": "Cinco ou mais dos seguintes sintomas presentes durante um período de duas semanas, representando mudança em relação ao funcionamento anterior." },
{ "type": "subtitle", "content": "Sintomas Principais (pelo menos 1 obrigatório)" },
{ "type": "copyable_content", "content": "1. Humor deprimido na maior parte do dia, quase todos os dias\n2. Diminuição acentuada do interesse ou prazer em todas ou quase todas as atividades" },
{ "type": "subtitle", "content": "Sintomas Adicionais" },
{ "type": "copyable_content", "content": "3. Perda ou ganho significativo de peso\n4. Insônia ou hipersonia\n5. Agitação ou retardo psicomotor\n6. Fadiga ou perda de energia\n7. Sentimentos de inutilidade ou culpa excessiva\n8. Capacidade diminuída de pensar ou concentrar-se\n9. Pensamentos recorrentes de morte" },
{ "type": "title_with_copyable", "content": "Prescrição Inicial\nSertralina 50mg\n1 comprimido pela manhã por 30 dias\n\nRetorno em 4 semanas para avaliação" }
]
}
Múltiplos Temas (Batch)
[
{
"title": "Escala de Glasgow",
"description": "Avaliação do nível de consciência",
"tags": ["neurologia", "emergência", "escala"],
"sections": [
{ "type": "title", "content": "Abertura Ocular (1-4)" },
{ "type": "copyable_content", "content": "4 - Espontânea\n3 - Ao estímulo verbal\n2 - Ao estímulo doloroso\n1 - Ausente" },
{ "type": "title", "content": "Resposta Verbal (1-5)" },
{ "type": "copyable_content", "content": "5 - Orientada\n4 - Confusa\n3 - Palavras inapropriadas\n2 - Sons incompreensíveis\n1 - Ausente" },
{ "type": "title", "content": "Resposta Motora (1-6)" },
{ "type": "copyable_content", "content": "6 - Obedece comandos\n5 - Localiza dor\n4 - Retirada inespecífica\n3 - Flexão anormal\n2 - Extensão anormal\n1 - Ausente" }
]
},
{
"title": "CURB-65",
"description": "Escore de gravidade para pneumonia",
"tags": ["pneumologia", "pneumonia", "escore"],
"sections": [
{ "type": "title", "content": "Critérios (1 ponto cada)" },
{ "type": "copyable_content", "content": "C - Confusão mental\nU - Ureia > 50 mg/dL\nR - FR ≥ 30 irpm\nB - PAS < 90 ou PAD ≤ 60 mmHg\n65 - Idade ≥ 65 anos" },
{ "type": "title", "content": "Conduta por Pontuação" },
{ "type": "copyable_content", "content": "0-1: Tratamento ambulatorial\n2: Internação breve ou observação\n3+: Internação, considerar UTI" }
]
}
]
Prompt Completo para ChatGPT/Claude
Você é um assistente especializado em criar conteúdo médico para o MedIABook.
Crie temas no formato JSON seguindo estas regras:
ESTRUTURA DO JSON:
{
"title": "string (3-200 chars, obrigatório)",
"description": "string (opcional)",
"tags": ["array", "de", "strings"] (opcional),
"is_published": boolean (opcional, default: false),
"sections": [
{
"type": "title|subtitle|text|copyable_content|title_with_copyable",
"content": "string (obrigatório, não vazio)"
}
]
}
TIPOS DE SEÇÃO:
1. "title" - Cabeçalho principal (ex: "Definição", "Tratamento")
2. "subtitle" - Subcabeçalho (ex: "Primeira Linha", "Alternativas")
3. "text" - Texto explicativo (descrições, explicações)
4. "copyable_content" - Conteúdo para copiar (receitas, doses, critérios)
5. "title_with_copyable" - Título + conteúdo copiável separados por \n
BOAS PRÁTICAS:
- Use "title" para separar seções principais
- Use "copyable_content" para informações que o médico vai copiar
- Use "title_with_copyable" para receitas com título
- Tags devem ser relevantes para busca (especialidade, condição, medicamento)
- Inclua referências quando apropriado
EXEMPLO DE SOLICITAÇÃO:
"Crie um tema sobre Hipertensão Arterial Sistêmica incluindo definição, classificação, tratamento de primeira linha e metas terapêuticas"
RETORNE APENAS O JSON, sem explicações adicionais.
Server Action para Importação
import { importThemesAction } from '~/app/admin/mediabook/_lib/server/admin-server-actions';
// Importar múltiplos temas
const result = await importThemesAction({
library_id: 'uuid-da-biblioteca',
themes: [
{
title: 'Tema 1',
description: 'Descrição',
tags: ['tag1'],
sections: [
{ type: 'title', content: 'Conteúdo' }
]
}
]
});
// Resultado
console.log(result.data.success); // ['Tema 1']
console.log(result.data.failed); // [{ title: 'Tema X', error: 'mensagem' }]
Validação Automática
O sistema valida automaticamente:
- Título: 3-200 caracteres
- Seções: Pelo menos 1 seção com conteúdo não vazio
- Tipos: Apenas os 5 tipos permitidos
- Slug: Gerado automaticamente a partir do título (único)
Fluxo de Importação
- Cole o JSON ou faça upload de arquivo
.json - O sistema valida e mostra preview (válidos/inválidos)
- Selecione a biblioteca destino
- Clique em "Importar X tema(s)"
- Temas são criados com slug único automático
Próximos Passos
- Adicionar mais bibliotecas através do gerenciador
- Criar temas com conteúdo relevante
- Configurar permissões adicionais se necessário
- Customizar ícones das bibliotecas
- Expandir tipos de seções conforme necessidade