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 tsvector para 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

  1. mediabook_libraries: Bibliotecas de conteúdo

    • id (UUID)
    • name (TEXT)
    • description (TEXT)
    • icon (TEXT) - Nome do ícone Lucide
    • created_at, updated_at
  2. mediabook_themes: Temas/artigos

    • id (UUID)
    • title (TEXT)
    • description (TEXT)
    • slug (TEXT UNIQUE)
    • library_id (UUID FK)
    • tags (TEXT[]) - Hashtags
    • search_vector (TSVECTOR) - Para Full-Text Search
    • created_at, updated_at
  3. mediabook_sections: Seções dos temas

    • id (UUID)
    • theme_id (UUID FK)
    • type (ENUM: title, subtitle, text, title_with_copyable)
    • content (TEXT)
    • order (INTEGER)
    • created_at, updated_at
  4. mediabook_favorites: Favoritos dos usuários

    • user_id (UUID FK)
    • theme_id (UUID FK)
    • created_at

Funções SQL

  1. is_super_admin_mediabook()

    • Verifica se o usuário é super-admin (sem MFA)
    • Usado nas RLS policies do gerenciador
  2. 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)
  3. get_mediabook_themes_by_library(library_id)

    • Retorna temas de uma biblioteca específica
  4. get_mediabook_theme_with_sections(slug)

    • Retorna tema completo com suas seções e informação de favorito
  5. update_mediabook_theme_search_vector()

    • Trigger para atualizar search_vector automaticamente

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

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

  1. Título: Cabeçalho de seção
  2. Subtítulo: Subcabeçalho
  3. Texto Simples: Conteúdo de texto
  4. 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

O sistema utiliza PostgreSQL Full-Text Search com:

  • GIN Index em search_vector para buscas rápidas
  • Triggers automáticos para atualizar search_vector
  • Função kit.unaccent para 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

  1. Cole o JSON ou faça upload de arquivo .json
  2. O sistema valida e mostra preview (válidos/inválidos)
  3. Selecione a biblioteca destino
  4. Clique em "Importar X tema(s)"
  5. Temas são criados com slug único automático

Próximos Passos

  1. Adicionar mais bibliotecas através do gerenciador
  2. Criar temas com conteúdo relevante
  3. Configurar permissões adicionais se necessário
  4. Customizar ícones das bibliotecas
  5. Expandir tipos de seções conforme necessidade