forlogic-core 1.6.11 → 1.6.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -455,6 +455,224 @@ export function DepartmentSelect(props: DepartmentSelectProps) {
455
455
 
456
456
  ---
457
457
 
458
+ ## 🏗️ ARQUITETURA CRUD
459
+
460
+ O sistema CRUD do forlogic-core segue uma arquitetura em camadas que separa responsabilidades e facilita manutenção:
461
+
462
+ ```mermaid
463
+ graph TD
464
+ A[types.ts<br/>Interfaces TypeScript] --> B[Service.ts<br/>createSimpleService]
465
+ B --> C[useCrudHook<br/>React Query]
466
+ C --> D[Page.tsx<br/>Componente Principal]
467
+ D --> E[createCrudPage<br/>Gerador de CRUD]
468
+ E --> F[CrudTable<br/>Tabela]
469
+ E --> G[CrudForm<br/>Formulário]
470
+ E --> H[BulkActionBar<br/>Ações em Massa]
471
+
472
+ I[(Supabase DB)] -.->|RLS + Soft Delete| B
473
+ J[Qualiex API] -.->|responsible_name| B
474
+
475
+ style A fill:#e1f5ff
476
+ style B fill:#fff4e1
477
+ style C fill:#ffe1f5
478
+ style D fill:#e1ffe1
479
+ style E fill:#f5e1ff
480
+ style F fill:#ffe1e1
481
+ style G fill:#ffe1e1
482
+ style H fill:#ffe1e1
483
+ ```
484
+
485
+ ### **📁 Estrutura de Pastas Obrigatória**
486
+
487
+ ```
488
+ src/
489
+ ├── examples/ # 📁 Módulo completo
490
+ │ ├── example.ts # 🔷 Types (Entity + Payloads)
491
+ │ ├── ExampleService.ts # ⚙️ Service (createSimpleService)
492
+ │ ├── ExamplesPage.tsx # 🎨 Página CRUD
493
+ │ └── components/ # 📦 Componentes específicos (opcional)
494
+ │ └── ExampleSelect.tsx
495
+ ```
496
+
497
+ ### **✅ Checklist de Implementação**
498
+
499
+ ```
500
+ ✅ OBRIGATÓRIOS:
501
+ □ types.ts - Definir Example extends ContentEntity + VisualEntity + ...
502
+ □ types.ts - CreateExamplePayload com alias: string
503
+ □ types.ts - UpdateExamplePayload extends Partial<CreateExamplePayload>
504
+ □ Service.ts - Usar createSimpleService<Example, CreatePayload, UpdatePayload>
505
+ □ Service.ts - Configurar tableName, entityName, searchFields
506
+ □ Page.tsx - Chamar useExamplesCrud() DENTRO do componente
507
+ □ Page.tsx - Usar useMemo() para columns, formSections, config
508
+ □ Page.tsx - Importar { alias } de useAuth()
509
+ □ Page.tsx - Passar alias no CreatePayload
510
+
511
+ 🔧 OPCIONAIS:
512
+ □ Filtros customizados (useState + useMemo)
513
+ □ onToggleStatus para ativar/desativar
514
+ □ Renderização customizada nas colunas
515
+ □ Campos de formulário customizados
516
+ ```
517
+
518
+ ### **🔄 Fluxo de Dados**
519
+
520
+ 1. **types.ts**: Define interfaces TypeScript (Entity, CreatePayload, UpdatePayload)
521
+ 2. **Service.ts**: Gera service com `createSimpleService` (métodos CRUD + hook)
522
+ 3. **useCrudHook**: Hook React Query que gerencia estado, cache, loading
523
+ 4. **Page.tsx**: Componente que usa o hook e define configuração visual
524
+ 5. **createCrudPage**: Gera componente de página completo (tabela + formulário)
525
+ 6. **UI Components**: Renderiza CrudTable, CrudForm, BulkActionBar, etc.
526
+
527
+ ### **🔐 Integrações Automáticas**
528
+
529
+ - 🔒 **RLS (Row Level Security)**: Filtra por `alias`
530
+ - 🗑️ **Soft Delete**: Marca `is_removed = true` ao invés de deletar
531
+ - 👤 **Qualiex Enrichment**: Adiciona `responsible_name` aos registros
532
+ - 🔍 **Busca Global**: Header com busca automática integrada ao CRUD
533
+
534
+ ---
535
+
536
+ ## 🔍 Busca Global no Header
537
+
538
+ O `AppHeader` inclui uma barra de busca integrada que funciona automaticamente com o sistema CRUD.
539
+
540
+ ### **Como Funciona:**
541
+
542
+ 1. **Ativação Automática**: A busca aparece automaticamente em páginas CRUD quando `isSearchVisible = true` no AuthContext
543
+ 2. **Debounce**: Usa delay de 500ms (configurável via `SEARCH_CONFIG.debounceDelay`) para evitar queries excessivas
544
+ 3. **URL Sync**: Mantém o termo de busca sincronizado na URL (`?search=termo`)
545
+ 4. **Reset de Paginação**: Ao buscar, reseta automaticamente para página 1
546
+ 5. **Botão Refresh**: Atualiza os dados da página atual
547
+
548
+ ### **Ativar Busca em uma Página:**
549
+
550
+ ```typescript
551
+ // No AuthContext ou componente pai
552
+ const [isSearchVisible, setIsSearchVisible] = useState(true);
553
+ ```
554
+
555
+ ### **Integração com CRUD:**
556
+
557
+ O hook `useCrud` já lê automaticamente o parâmetro `search` da URL:
558
+
559
+ ```typescript
560
+ // lib/crud/hooks/useCrud.ts
561
+ const [searchParams] = useSearchParams();
562
+ const searchTerm = searchParams.get('search') || '';
563
+
564
+ // A busca é aplicada automaticamente nos searchFields configurados no service
565
+ ```
566
+
567
+ ### **Configurar Campos de Busca:**
568
+
569
+ ```typescript
570
+ // src/examples/ExampleService.ts
571
+ export const { service, useCrudHook } = createSimpleService<Example>({
572
+ tableName: 'examples',
573
+ entityName: 'exemplo',
574
+ schemaName: 'central',
575
+ searchFields: ['title', 'description', 'tags'], // 🔍 Campos pesquisáveis
576
+ });
577
+ ```
578
+
579
+ ### **Customizar Placeholder:**
580
+
581
+ O placeholder da busca se adapta automaticamente à rota. Para customizar:
582
+
583
+ ```tsx
584
+ // AppHeader.tsx - Modificação direta (afeta todas as rotas)
585
+ <Input
586
+ placeholder={location.pathname === '/wiki' ? "Buscar artigos..." : "Buscar..."}
587
+ />
588
+
589
+ // OU usar metadata (recomendado para páginas específicas)
590
+ import { usePageMetadataContext } from 'forlogic-core';
591
+
592
+ export default function MyPage() {
593
+ const { setMetadata } = usePageMetadataContext();
594
+
595
+ useEffect(() => {
596
+ setMetadata({
597
+ title: 'Meus Itens',
598
+ subtitle: 'Gerencie seus itens',
599
+ // Futura feature: searchPlaceholder: 'Buscar por nome...'
600
+ });
601
+ }, []);
602
+ }
603
+ ```
604
+
605
+ ### **Configuração Avançada:**
606
+
607
+ ```typescript
608
+ // lib/config/index.ts
609
+ export const SEARCH_CONFIG = {
610
+ debounceDelay: 500, // ms - ajuste conforme necessidade
611
+ } as const;
612
+ ```
613
+
614
+ **Quando aumentar o delay:**
615
+ - ✅ Muitos usuários simultâneos (reduz carga no servidor)
616
+ - ✅ Campos de busca muito amplos (muitos registros)
617
+ - ✅ Backend com rate limiting
618
+
619
+ **Quando reduzir o delay:**
620
+ - ✅ Poucos registros (resposta instantânea)
621
+ - ✅ Busca crítica para UX (feedback imediato)
622
+
623
+ ### **Controle Programático:**
624
+
625
+ ```typescript
626
+ // Limpar busca programaticamente
627
+ import { useSearchParams } from 'react-router-dom';
628
+
629
+ const [searchParams, setSearchParams] = useSearchParams();
630
+
631
+ const clearSearch = () => {
632
+ const newParams = new URLSearchParams(searchParams);
633
+ newParams.delete('search');
634
+ newParams.delete('page');
635
+ setSearchParams(newParams);
636
+ };
637
+
638
+ // Definir busca programaticamente
639
+ const setSearch = (term: string) => {
640
+ const newParams = new URLSearchParams(searchParams);
641
+ newParams.set('search', term);
642
+ newParams.set('page', '1'); // Reset para primeira página
643
+ setSearchParams(newParams);
644
+ };
645
+ ```
646
+
647
+ ### **Refresh Manual:**
648
+
649
+ O botão de refresh chama a função `refreshData` do AuthContext:
650
+
651
+ ```typescript
652
+ // Implementar no seu AuthContext
653
+ const refreshData = useCallback(() => {
654
+ queryClient.invalidateQueries(); // Invalida cache do React Query
655
+ toast.success('Dados atualizados');
656
+ }, [queryClient]);
657
+ ```
658
+
659
+ ### **Arquitetura da Busca:**
660
+
661
+ ```mermaid
662
+ graph LR
663
+ A[Usuário digita] --> B[useState local]
664
+ B --> C[useDebounce 500ms]
665
+ C --> D[URL ?search=termo]
666
+ D --> E[useCrud lê URL]
667
+ E --> F[Supabase Query]
668
+ F --> G[Resultados filtrados]
669
+
670
+ style C fill:#d4f4dd
671
+ style F fill:#ffd4d4
672
+ ```
673
+
674
+ ---
675
+
458
676
  ## 🚀 QUICK START - Criar CRUD Completo
459
677
 
460
678
  ### **1️⃣ Type**
@@ -728,6 +946,676 @@ export function ProcessesPage() {
728
946
 
729
947
  ---
730
948
 
949
+ ## 🎓 TUTORIAL COMPLETO: CRUD de Examples (Copy-Paste Ready)
950
+
951
+ Este tutorial mostra como criar um CRUD completo usando o módulo **Examples** como referência. É um exemplo funcional 100% que você pode copiar e adaptar.
952
+
953
+ ### **Passo 1: Criar Types (`src/examples/example.ts`)**
954
+
955
+ ```typescript
956
+ // ============= EXAMPLE MODULE TYPES =============
957
+ import {
958
+ ContentEntity, // title, description
959
+ VisualEntity, // color, icon_name
960
+ UserRelatedEntity, // id_user, responsible_name
961
+ ActivableEntity, // is_actived
962
+ FormEntity, // url_field, date_field
963
+ FilterState,
964
+ EntitySortField
965
+ } from 'forlogic-core';
966
+
967
+ /**
968
+ * Example - Entidade completa de exemplo
969
+ *
970
+ * ✅ Composição de Interfaces:
971
+ * - ContentEntity: fornece title, description
972
+ * - VisualEntity: fornece color, icon_name
973
+ * - UserRelatedEntity: fornece id_user, responsible_name (enriquecido via Qualiex)
974
+ * - ActivableEntity: fornece is_actived (campo de status on/off)
975
+ * - FormEntity: fornece url_field, date_field
976
+ *
977
+ * 🔒 Campos Herdados de BaseEntity (automáticos):
978
+ * - id: string
979
+ * - alias: string
980
+ * - company_id: string
981
+ * - is_removed: boolean
982
+ * - created_at: string
983
+ * - updated_at: string
984
+ */
985
+ export interface Example extends
986
+ ContentEntity,
987
+ VisualEntity,
988
+ UserRelatedEntity,
989
+ ActivableEntity,
990
+ FormEntity {}
991
+
992
+ /**
993
+ * CreateExamplePayload - Dados para CRIAR novo registro
994
+ *
995
+ * ⚠️ IMPORTANTE:
996
+ * - Sempre incluir `alias: string` (obrigatório para RLS)
997
+ * - Campos opcionais devem ter `| null`
998
+ * - NÃO incluir id, created_at, updated_at (gerados automaticamente)
999
+ */
1000
+ export interface CreateExamplePayload {
1001
+ title: string; // ✅ Obrigatório
1002
+ description?: string | null; // ❌ Opcional
1003
+ alias: string; // ✅ OBRIGATÓRIO para RLS
1004
+ url_field?: string | null;
1005
+ date_field?: string | null;
1006
+ color?: string;
1007
+ icon_name?: string;
1008
+ id_user?: string | null;
1009
+ is_actived?: boolean;
1010
+ }
1011
+
1012
+ /**
1013
+ * UpdateExamplePayload - Dados para ATUALIZAR registro existente
1014
+ *
1015
+ * 📝 Pattern:
1016
+ * - Estende Partial<CreateExamplePayload> (todos os campos opcionais)
1017
+ * - MAS title é obrigatório (override)
1018
+ */
1019
+ export interface UpdateExamplePayload extends Partial<CreateExamplePayload> {
1020
+ title: string; // ✅ Override: title obrigatório mesmo no update
1021
+ }
1022
+
1023
+ export type ExampleInsert = CreateExamplePayload;
1024
+ export type ExampleUpdate = UpdateExamplePayload;
1025
+ ```
1026
+
1027
+ **📖 Explicação Detalhada:**
1028
+ - **Composição de Interfaces:** Ao invés de redefinir campos, herda de interfaces prontas da lib
1029
+ - **`alias` no CreatePayload:** RLS do Supabase precisa desse campo para funcionar
1030
+ - **`Partial<>` no UpdatePayload:** Permite updates parciais (só manda os campos que mudaram)
1031
+ - **Campos `| null`:** Importante para sincronizar com Supabase (que aceita NULL)
1032
+
1033
+ ---
1034
+
1035
+ ### **Passo 2: Criar Service (`src/examples/ExampleService.ts`)**
1036
+
1037
+ ```typescript
1038
+ // ============= SIMPLIFIED SERVICE (MIGRATED) =============
1039
+
1040
+ import { createSimpleService } from 'forlogic-core';
1041
+ import type { Example, CreateExamplePayload, UpdateExamplePayload } from './example';
1042
+
1043
+ /**
1044
+ * ExampleService - Service CRUD completo gerado automaticamente
1045
+ *
1046
+ * ✅ O que é gerado:
1047
+ * - service.getAll(params)
1048
+ * - service.getById(id)
1049
+ * - service.create(data)
1050
+ * - service.update(id, data)
1051
+ * - service.delete(id)
1052
+ * - useCrudHook() - Hook React Query integrado
1053
+ *
1054
+ * 🔧 Configuração:
1055
+ * - tableName: Nome da tabela no Supabase (schema: central)
1056
+ * - entityName: Nome legível para toasts ("Exemplo criado com sucesso")
1057
+ * - searchFields: Campos que serão pesquisados pelo filtro de busca
1058
+ * - enableQualiexEnrichment: true → adiciona responsible_name automaticamente
1059
+ *
1060
+ * 📊 Estrutura de Tabela Esperada (Supabase):
1061
+ * CREATE TABLE central.examples (
1062
+ * id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1063
+ * alias TEXT NOT NULL, -- ✅ Para RLS (obrigatório)
1064
+ * is_removed BOOLEAN DEFAULT false, -- ✅ Para soft delete (obrigatório)
1065
+ * created_at TIMESTAMPTZ DEFAULT now(),
1066
+ * updated_at TIMESTAMPTZ DEFAULT now(),
1067
+ * title TEXT NOT NULL,
1068
+ * description TEXT,
1069
+ * id_user TEXT,
1070
+ * is_actived BOOLEAN DEFAULT true,
1071
+ * color TEXT,
1072
+ * icon_name TEXT,
1073
+ * url_field TEXT,
1074
+ * date_field DATE
1075
+ * );
1076
+ */
1077
+ export const { service: ExampleService, useCrudHook: useExamplesCrud } =
1078
+ createSimpleService<Example, CreateExamplePayload, UpdateExamplePayload>({
1079
+ tableName: 'examples', // 🗃️ Tabela no Supabase
1080
+ entityName: 'Exemplo', // 📣 Nome para mensagens
1081
+ searchFields: ['title'], // 🔍 Campos de busca textual
1082
+ schemaName: 'central', // 📂 Schema (default: 'central')
1083
+ enableQualiexEnrichment: true // 👤 Adiciona responsible_name
1084
+ });
1085
+ ```
1086
+
1087
+ **📖 Explicação Detalhada:**
1088
+ - **Uma linha, tudo pronto:** `createSimpleService` gera todo o boilerplate
1089
+ - **Soft delete automático:** `deleteEntity()` marca `is_removed = true`, não deleta fisicamente
1090
+ - **RLS automático:** Filtra por `alias` automaticamente
1091
+ - **Enrichment Qualiex:** Busca o `responsible_name` na API Qualiex e adiciona aos registros
1092
+
1093
+ ---
1094
+
1095
+ ### **Passo 3: Criar Página (`src/examples/ExamplesPage.tsx`) - PARTE 1**
1096
+
1097
+ #### **Imports e Configuração de Colunas**
1098
+
1099
+ ```typescript
1100
+ // ============= EXAMPLES PAGE (MIGRATED TO NEW API) =============
1101
+
1102
+ import { useExamplesCrud } from './ExampleService';
1103
+ import {
1104
+ createCrudPage,
1105
+ createSimpleSaveHandler,
1106
+ formatDatetime,
1107
+ useAuth,
1108
+ type CrudColumn,
1109
+ EntitySelect
1110
+ } from 'forlogic-core';
1111
+ import type { Example, CreateExamplePayload, UpdateExamplePayload } from './example';
1112
+ import { ExternalLink, Star } from 'lucide-react';
1113
+ import { toast } from 'sonner';
1114
+ import * as LucideIcons from 'lucide-react';
1115
+ import { useState, useMemo } from 'react';
1116
+
1117
+ /**
1118
+ * ⚠️ IMPORTANTE: Importar `cn` da lib, NÃO do utils local
1119
+ * ❌ ERRADO: import { cn } from '@/lib/utils'
1120
+ * ✅ CORRETO: import { cn } from 'forlogic-core'
1121
+ */
1122
+ ```
1123
+
1124
+ ---
1125
+
1126
+ ### **Passo 4: Configuração do Formulário**
1127
+
1128
+ ```typescript
1129
+ /**
1130
+ * 📝 CONFIGURAÇÃO DO FORMULÁRIO
1131
+ *
1132
+ * Organizado em seções (formSections) com campos (fields).
1133
+ *
1134
+ * Tipos de campos suportados:
1135
+ * - 'text' - Input de texto simples
1136
+ * - 'email' - Input de email com validação
1137
+ * - 'textarea' - Área de texto grande
1138
+ * - 'select' - Dropdown com options
1139
+ * - 'color-picker' - Seletor de cor
1140
+ * - 'icon-picker' - Seletor de ícone Lucide
1141
+ * - 'simple-qualiex-user-field' - Seletor de usuário Qualiex
1142
+ * - 'custom' - Campo completamente customizado
1143
+ * - 'group' - Agrupa campos horizontalmente
1144
+ */
1145
+ const formSections = [{
1146
+ id: 'general',
1147
+ title: 'Informações Gerais',
1148
+ fields: [
1149
+ {
1150
+ // 🎯 GROUP: Agrupa campos horizontalmente
1151
+ type: 'group' as any,
1152
+ name: 'title-group',
1153
+ label: '',
1154
+ layout: 'horizontal' as const,
1155
+ className: 'grid grid-cols-1 sm:grid-cols-2 gap-6',
1156
+ fields: [
1157
+ {
1158
+ name: 'title',
1159
+ label: 'Título',
1160
+ type: 'text' as const,
1161
+ required: true,
1162
+ placeholder: 'Digite o título do exemplo',
1163
+ },
1164
+ {
1165
+ name: 'id_user',
1166
+ label: 'Responsável',
1167
+ type: 'simple-qualiex-user-field' as const, // 👤 Campo especial da lib
1168
+ required: true,
1169
+ placeholder: 'Selecionar responsável',
1170
+ defaultValue: '',
1171
+ }
1172
+ ],
1173
+ },
1174
+ {
1175
+ name: 'url_field',
1176
+ label: 'Link',
1177
+ type: 'text' as const,
1178
+ required: false,
1179
+ placeholder: 'https://exemplo.com'
1180
+ },
1181
+ {
1182
+ name: 'description',
1183
+ label: 'Descrição',
1184
+ type: 'textarea' as const,
1185
+ required: false,
1186
+ placeholder: 'Descrição detalhada do exemplo'
1187
+ },
1188
+ {
1189
+ // 🎨 GROUP: Visual (cor + ícone)
1190
+ type: 'group' as any,
1191
+ name: 'visual-group',
1192
+ label: 'Visual',
1193
+ layout: 'horizontal' as const,
1194
+ className: 'grid grid-cols-1 sm:grid-cols-2 gap-6',
1195
+ fields: [
1196
+ {
1197
+ name: 'color',
1198
+ label: 'Cor',
1199
+ type: 'color-picker' as const, // 🎨 Color picker da lib
1200
+ required: false,
1201
+ defaultValue: '#3b82f6'
1202
+ },
1203
+ {
1204
+ name: 'icon_name',
1205
+ label: 'Ícone',
1206
+ type: 'icon-picker' as const, // 🔷 Icon picker da lib
1207
+ required: false,
1208
+ defaultValue: 'Star'
1209
+ }
1210
+ ]
1211
+ }
1212
+ ],
1213
+ }];
1214
+ ```
1215
+
1216
+ ---
1217
+
1218
+ ### **Passo 5: Componente Principal com Filtros**
1219
+
1220
+ Ver código completo no arquivo `src/examples/ExamplesPage.tsx` do projeto.
1221
+
1222
+ **Estrutura básica:**
1223
+ 1. Hooks no topo
1224
+ 2. Estados de filtros com `useState`
1225
+ 3. Estados derivados com `useMemo`
1226
+ 4. Manager customizado com `useMemo`
1227
+ 5. Handlers (`handleToggleStatus`, `handleSave`)
1228
+ 6. Criar página com `createCrudPage`
1229
+
1230
+ ---
1231
+
1232
+ ## 🎯 PADRÕES DE FILTROS CUSTOMIZADOS
1233
+
1234
+ ### **Pattern 1: Filtro de Status (Ativo/Inativo/Todos)**
1235
+
1236
+ ```typescript
1237
+ // ✅ PADRÃO COMPLETO: Filtro + Estado Derivado + Manager Customizado
1238
+
1239
+ // 1) Estado do filtro
1240
+ const [statusFilter, setStatusFilter] = useState<'active' | 'inactive' | 'all'>('active');
1241
+
1242
+ // 2) Estado derivado (filtra entities)
1243
+ const filteredEntities = useMemo(() => {
1244
+ if (statusFilter === 'all') return manager.entities;
1245
+ return manager.entities.filter(e =>
1246
+ statusFilter === 'active' ? e.is_actived : !e.is_actived
1247
+ );
1248
+ }, [manager.entities, statusFilter]);
1249
+
1250
+ // 3) Manager customizado
1251
+ const filteredManager = useMemo(() => ({
1252
+ ...manager,
1253
+ entities: filteredEntities
1254
+ }), [manager, filteredEntities]);
1255
+
1256
+ // 4) Componente do filtro
1257
+ const StatusFilter = () => (
1258
+ <EntitySelect
1259
+ value={statusFilter}
1260
+ onChange={(value) => setStatusFilter(value as 'active' | 'inactive' | 'all')}
1261
+ items={[
1262
+ { id: 'active', name: 'Ativo' },
1263
+ { id: 'inactive', name: 'Inativo' },
1264
+ { id: 'all', name: '[Todos]' }
1265
+ ]}
1266
+ getItemValue={(item) => item.id}
1267
+ getItemLabel={(item) => item.name}
1268
+ placeholder="Status"
1269
+ />
1270
+ );
1271
+
1272
+ // 5) Usar em config.filters
1273
+ config: {
1274
+ filters: [
1275
+ { type: 'search' },
1276
+ { type: 'custom', component: StatusFilter }
1277
+ ]
1278
+ }
1279
+
1280
+ // 6) Passar filteredManager para createCrudPage
1281
+ const CrudPage = createCrudPage({
1282
+ manager: filteredManager, // ← Não manager original!
1283
+ config,
1284
+ onSave
1285
+ });
1286
+ ```
1287
+
1288
+ **📖 Explicação:**
1289
+ - **`useState`**: Armazena valor selecionado no filtro
1290
+ - **`useMemo` (filteredEntities)**: Evita re-filtrar a cada render
1291
+ - **`useMemo` (filteredManager)**: Evita re-criar objeto manager
1292
+ - **`EntitySelect`**: Componente de dropdown da lib
1293
+ - **`filteredManager`**: Manager modificado que createCrudPage vai usar
1294
+
1295
+ ---
1296
+
1297
+ ### **Pattern 2: Filtro de Departamento (Select Nativo)**
1298
+
1299
+ ```typescript
1300
+ // ✅ PADRÃO: Filtro por categoria/departamento
1301
+
1302
+ const [deptFilter, setDeptFilter] = useState<string>('all');
1303
+
1304
+ const filteredEntities = useMemo(() => {
1305
+ if (deptFilter === 'all') return manager.entities;
1306
+ return manager.entities.filter(e => e.department === deptFilter);
1307
+ }, [manager.entities, deptFilter]);
1308
+
1309
+ const filteredManager = useMemo(() => ({
1310
+ ...manager,
1311
+ entities: filteredEntities
1312
+ }), [manager, filteredEntities]);
1313
+
1314
+ const DepartmentFilter = () => (
1315
+ <select
1316
+ value={deptFilter}
1317
+ onChange={(e) => setDeptFilter(e.target.value)}
1318
+ className="px-3 py-2 border rounded-md"
1319
+ >
1320
+ <option value="all">Todos Departamentos</option>
1321
+ <option value="HR">RH</option>
1322
+ <option value="IT">TI</option>
1323
+ <option value="Finance">Financeiro</option>
1324
+ </select>
1325
+ );
1326
+ ```
1327
+
1328
+ ---
1329
+
1330
+ ### **Pattern 3: Filtro de Data Range (Date Picker)**
1331
+
1332
+ ```typescript
1333
+ // ✅ PADRÃO: Filtro por intervalo de datas
1334
+
1335
+ import { format, isAfter, isBefore, parseISO } from 'date-fns';
1336
+
1337
+ const [dateRange, setDateRange] = useState<{ from?: Date; to?: Date }>({});
1338
+
1339
+ const filteredEntities = useMemo(() => {
1340
+ if (!dateRange.from && !dateRange.to) return manager.entities;
1341
+
1342
+ return manager.entities.filter(e => {
1343
+ const itemDate = parseISO(e.created_at);
1344
+ if (dateRange.from && isBefore(itemDate, dateRange.from)) return false;
1345
+ if (dateRange.to && isAfter(itemDate, dateRange.to)) return false;
1346
+ return true;
1347
+ });
1348
+ }, [manager.entities, dateRange]);
1349
+
1350
+ const filteredManager = useMemo(() => ({
1351
+ ...manager,
1352
+ entities: filteredEntities
1353
+ }), [manager, filteredEntities]);
1354
+ ```
1355
+
1356
+ ---
1357
+
1358
+ ## 🪝 HOOKS REACT NO CRUD
1359
+
1360
+ | Hook | Quando Usar | Exemplo no CRUD | ⚠️ Evitar |
1361
+ |------|-------------|-----------------|-----------|
1362
+ | **useMemo** | Cálculos pesados que dependem de props/state | • Configuração de colunas<br>• Filtros derivados<br>• Manager customizado | Valores simples (strings, números) |
1363
+ | **useState** | Valores que mudam via interação do usuário | • Filtros customizados<br>• Modal open/close<br>• Seleção temporária | Estados derivados de outros estados |
1364
+ | **useCallback** | Funções que são passadas como props e dependem de state | • Handlers que dependem de filtros<br>• Callbacks de child components | Handlers simples sem dependências |
1365
+ | **useEffect** | Side effects (fetch, subscriptions) | • Fetch inicial de dados (já feito pelo manager)<br>• Sincronização externa | Cálculos ou transformações de dados |
1366
+
1367
+ ### **📖 Exemplos Práticos**
1368
+
1369
+ ```typescript
1370
+ // ✅ CORRETO: useMemo para config (evita re-render do formulário)
1371
+ const config = useMemo(() => ({
1372
+ columns: [...],
1373
+ formSections: [...]
1374
+ }), []);
1375
+
1376
+ // ❌ ERRADO: sem useMemo (re-cria objeto a cada render)
1377
+ const config = {
1378
+ columns: [...],
1379
+ formSections: [...]
1380
+ };
1381
+
1382
+ // ✅ CORRETO: useState para filtro
1383
+ const [statusFilter, setStatusFilter] = useState('active');
1384
+
1385
+ // ❌ ERRADO: useMemo para valor que muda via interação
1386
+ const statusFilter = useMemo(() => 'active', []); // Não faz sentido!
1387
+
1388
+ // ✅ CORRETO: useMemo para estado derivado
1389
+ const filteredEntities = useMemo(() =>
1390
+ manager.entities.filter(e => e.is_actived),
1391
+ [manager.entities]
1392
+ );
1393
+
1394
+ // ❌ ERRADO: recalcula a cada render (lento)
1395
+ const filteredEntities = manager.entities.filter(e => e.is_actived);
1396
+ ```
1397
+
1398
+ ---
1399
+
1400
+ ## ❌ ERROS COMUNS E SOLUÇÕES
1401
+
1402
+ ### **Erro 1: Importar `cn` do lugar errado**
1403
+
1404
+ ```typescript
1405
+ // ❌ SINTOMA: Classes CSS não aplicam
1406
+ // ❌ CAUSA: import { cn } from '@/lib/utils'
1407
+ import { cn } from '@/lib/utils';
1408
+
1409
+ // ✅ SOLUÇÃO: Importar da lib
1410
+ import { cn } from 'forlogic-core';
1411
+ ```
1412
+
1413
+ ---
1414
+
1415
+ ### **Erro 2: Esquecer `useMemo` no config**
1416
+
1417
+ ```typescript
1418
+ // ❌ SINTOMA: Formulário fecha/reabre sozinho, re-renders infinitos
1419
+ // ❌ CAUSA: Config recriado a cada render
1420
+ const config = {
1421
+ columns: exampleColumns,
1422
+ formSections
1423
+ };
1424
+
1425
+ // ✅ SOLUÇÃO: Envolver em useMemo
1426
+ const config = useMemo(() => ({
1427
+ columns: exampleColumns,
1428
+ formSections
1429
+ }), []);
1430
+ ```
1431
+
1432
+ ---
1433
+
1434
+ ### **Erro 3: Passar `entities` ao invés de `manager`**
1435
+
1436
+ ```typescript
1437
+ // ❌ SINTOMA: TypeError: manager.createEntity is not a function
1438
+ // ❌ CAUSA: Passou array direto
1439
+ const CrudPage = createCrudPage({
1440
+ manager: manager.entities, // ← Errado!
1441
+ config
1442
+ });
1443
+
1444
+ // ✅ SOLUÇÃO: Passar manager completo
1445
+ const CrudPage = createCrudPage({
1446
+ manager, // ← Correto!
1447
+ config
1448
+ });
1449
+ ```
1450
+
1451
+ ---
1452
+
1453
+ ### **Erro 4: Filtro sem `useMemo`**
1454
+
1455
+ ```typescript
1456
+ // ❌ SINTOMA: Performance ruim, travamentos
1457
+ // ❌ CAUSA: Recalcula filtro a cada render
1458
+ const filteredEntities = manager.entities.filter(e => e.is_actived);
1459
+
1460
+ // ✅ SOLUÇÃO: Envolver em useMemo
1461
+ const filteredEntities = useMemo(() =>
1462
+ manager.entities.filter(e => e.is_actived),
1463
+ [manager.entities]
1464
+ );
1465
+ ```
1466
+
1467
+ ---
1468
+
1469
+ ### **Erro 5: Esquecer `alias` no CREATE**
1470
+
1471
+ ```typescript
1472
+ // ❌ SINTOMA: Erro de RLS no Supabase, registro não é criado
1473
+ // ❌ CAUSA: Payload sem alias
1474
+ const handleSave = createSimpleSaveHandler(
1475
+ manager,
1476
+ (data) => ({
1477
+ title: data.title,
1478
+ email: data.email
1479
+ // ← Falta alias!
1480
+ })
1481
+ );
1482
+
1483
+ // ✅ SOLUÇÃO: Incluir alias do token
1484
+ const { alias: currentAlias } = useAuth();
1485
+
1486
+ const handleSave = createSimpleSaveHandler(
1487
+ manager,
1488
+ (data) => ({
1489
+ title: data.title,
1490
+ email: data.email,
1491
+ alias: currentAlias // ← Correto!
1492
+ })
1493
+ );
1494
+ ```
1495
+
1496
+ ---
1497
+
1498
+ ### **Erro 6: Chamar hooks fora do componente**
1499
+
1500
+ ```typescript
1501
+ // ❌ SINTOMA: Error: Hooks can only be called inside of the body of a function component
1502
+ // ❌ CAUSA: Hook chamado fora do componente
1503
+ const manager = useExamplesCrud(); // ← Fora do componente!
1504
+
1505
+ export const ExamplesPage = () => {
1506
+ return <CrudPage />;
1507
+ };
1508
+
1509
+ // ✅ SOLUÇÃO: Chamar hooks DENTRO do componente
1510
+ export const ExamplesPage = () => {
1511
+ const manager = useExamplesCrud(); // ← Dentro do componente!
1512
+ return <CrudPage />;
1513
+ };
1514
+ ```
1515
+
1516
+ ---
1517
+
1518
+ ## 🎨 PERSONALIZAÇÃO AVANÇADA
1519
+
1520
+ ### **1. Renderização Customizada de Colunas**
1521
+
1522
+ ```typescript
1523
+ // Exemplo 1: Badge de status com cores
1524
+ {
1525
+ key: 'status',
1526
+ header: 'Status',
1527
+ render: (item) => (
1528
+ <span className={`px-2 py-1 rounded-full text-xs ${
1529
+ item.status === 'completed' ? 'bg-green-100 text-green-800' :
1530
+ item.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
1531
+ 'bg-gray-100 text-gray-800'
1532
+ }`}>
1533
+ {item.status}
1534
+ </span>
1535
+ )
1536
+ }
1537
+
1538
+ // Exemplo 2: Link externo
1539
+ {
1540
+ key: 'website',
1541
+ header: 'Site',
1542
+ render: (item) => (
1543
+ <a
1544
+ href={item.website}
1545
+ target="_blank"
1546
+ rel="noopener noreferrer"
1547
+ className="text-blue-600 hover:underline"
1548
+ >
1549
+ Visitar
1550
+ </a>
1551
+ )
1552
+ }
1553
+
1554
+ // Exemplo 3: Ícone colorido
1555
+ {
1556
+ key: 'priority',
1557
+ header: 'Prioridade',
1558
+ render: (item) => {
1559
+ const icons = { high: '🔴', medium: '🟡', low: '🟢' };
1560
+ return <span>{icons[item.priority]}</span>;
1561
+ }
1562
+ }
1563
+ ```
1564
+
1565
+ ---
1566
+
1567
+ ### **2. Campos de Formulário Customizados**
1568
+
1569
+ ```typescript
1570
+ // Exemplo: Campo customizado com EntitySelect
1571
+ {
1572
+ name: 'category_id',
1573
+ label: 'Categoria',
1574
+ type: 'custom' as const,
1575
+ component: (props) => {
1576
+ const { data: categories } = useCategoriesCrud();
1577
+ return (
1578
+ <EntitySelect
1579
+ {...props}
1580
+ items={categories}
1581
+ getItemValue={(item) => item.id}
1582
+ getItemLabel={(item) => item.name}
1583
+ placeholder="Selecionar categoria"
1584
+ />
1585
+ );
1586
+ }
1587
+ }
1588
+ ```
1589
+
1590
+ ---
1591
+
1592
+ ### **3. Ações Customizadas na Linha**
1593
+
1594
+ ```typescript
1595
+ // Adicionar botão "Duplicar" nas ações da linha
1596
+ const customActions = (item: Example) => [
1597
+ {
1598
+ label: 'Duplicar',
1599
+ icon: Copy,
1600
+ onClick: () => {
1601
+ const newItem = { ...item, id: undefined, title: `${item.title} (cópia)` };
1602
+ manager.createEntity(newItem);
1603
+ }
1604
+ }
1605
+ ];
1606
+
1607
+ // Passar para createCrudPage
1608
+ const CrudPage = createCrudPage({
1609
+ manager,
1610
+ config: {
1611
+ ...config,
1612
+ customRowActions: customActions
1613
+ }
1614
+ });
1615
+ ```
1616
+
1617
+ ---
1618
+
731
1619
  ### 🔗 Integração Qualiex (opcional)
732
1620
 
733
1621
  **Auto-enrichment** (já configurado no BaseService):
@@ -791,6 +1679,339 @@ headers: { 'un-alias': 'true' }
791
1679
 
792
1680
  ---
793
1681
 
1682
+ ## 📍 PLACES - Locais e Sublocais
1683
+
1684
+ O módulo **Places** permite gerenciar a hierarquia de locais e sublocais da organização, integrando com a API Qualiex.
1685
+
1686
+ ### 🔌 Imports Disponíveis
1687
+
1688
+ ```typescript
1689
+ // Tipos
1690
+ import type { Place, SubPlace } from 'forlogic-core';
1691
+
1692
+ // Serviço
1693
+ import { placeService, PlaceService } from 'forlogic-core';
1694
+
1695
+ // Componente de Página Pronta
1696
+ import { PlacesPage } from 'forlogic-core';
1697
+ ```
1698
+
1699
+ ### 📋 Estrutura dos Dados
1700
+
1701
+ ```typescript
1702
+ interface Place {
1703
+ id: string;
1704
+ placeId: string; // ID único do local no Qualiex
1705
+ name: string; // Nome do local
1706
+ companyId: string; // ID da empresa
1707
+ usersIds: string[]; // Array de IDs de usuários vinculados
1708
+ subPlaces?: SubPlace[]; // Sublocais (hierarquia)
1709
+ parentId?: string | null; // ID do local pai (se for sublocalizado)
1710
+ isActive: boolean; // Status do local
1711
+ createdAt: string;
1712
+ updatedAt: string;
1713
+ }
1714
+
1715
+ interface SubPlace {
1716
+ id: string;
1717
+ placeId: string;
1718
+ name: string;
1719
+ parentId: string;
1720
+ usersIds: string[];
1721
+ isActive: boolean;
1722
+ subPlaces?: SubPlace[]; // Recursivo - permite múltiplos níveis
1723
+ }
1724
+ ```
1725
+
1726
+ ### 🎯 Como Obter Places
1727
+
1728
+ #### Método 1 (Recomendado): Hook com React Query
1729
+
1730
+ ```typescript
1731
+ import { useQuery } from '@tanstack/react-query';
1732
+ import { useAuth, placeService } from 'forlogic-core';
1733
+
1734
+ function MyComponent() {
1735
+ const { alias } = useAuth();
1736
+
1737
+ const { data: places = [], isLoading, error } = useQuery({
1738
+ queryKey: ['places', alias],
1739
+ queryFn: () => placeService.getPlaces(alias),
1740
+ enabled: !!alias,
1741
+ staleTime: 5 * 60 * 1000 // Cache de 5 minutos
1742
+ });
1743
+
1744
+ if (isLoading) return <LoadingState />;
1745
+ if (error) return <div>Erro ao carregar locais</div>;
1746
+
1747
+ return (
1748
+ <div>
1749
+ {places.map(place => (
1750
+ <div key={place.id}>{place.name}</div>
1751
+ ))}
1752
+ </div>
1753
+ );
1754
+ }
1755
+ ```
1756
+
1757
+ #### Método 2: Hook Customizado Reutilizável
1758
+
1759
+ ```typescript
1760
+ // src/hooks/usePlaces.ts
1761
+ import { useQuery } from '@tanstack/react-query';
1762
+ import { useAuth, placeService } from 'forlogic-core';
1763
+
1764
+ export function usePlaces() {
1765
+ const { alias } = useAuth();
1766
+
1767
+ return useQuery({
1768
+ queryKey: ['places', alias],
1769
+ queryFn: () => placeService.getPlaces(alias),
1770
+ enabled: !!alias,
1771
+ staleTime: 5 * 60 * 1000 // Cache de 5 minutos
1772
+ });
1773
+ }
1774
+
1775
+ // Usar no componente
1776
+ const { data: places = [], isLoading } = usePlaces();
1777
+ ```
1778
+
1779
+ #### Método 3: Chamada Direta (Service)
1780
+
1781
+ ```typescript
1782
+ // Para casos especiais (não recomendado para components)
1783
+ const places = await placeService.getPlaces('my-alias');
1784
+ ```
1785
+
1786
+ ### 🏠 Usando PlacesPage Pronta
1787
+
1788
+ ```typescript
1789
+ // App.tsx ou routes
1790
+ import { PlacesPage } from 'forlogic-core';
1791
+
1792
+ <Route path="/places" element={<PlacesPage />} />
1793
+ ```
1794
+
1795
+ ### 🔗 Integrando Places em Módulos CRUD
1796
+
1797
+ #### Cenário A: PlaceSelect em Formulários
1798
+
1799
+ ```typescript
1800
+ // src/components/PlaceSelect.tsx
1801
+ import { EntitySelect } from 'forlogic-core';
1802
+ import { usePlaces } from '@/hooks/usePlaces';
1803
+ import { useMemo } from 'react';
1804
+
1805
+ export function PlaceSelect({ value, onChange, disabled }: {
1806
+ value?: string;
1807
+ onChange: (value: string) => void;
1808
+ disabled?: boolean;
1809
+ }) {
1810
+ const { data: places = [], isLoading } = usePlaces();
1811
+
1812
+ // Achatar hierarquia para o select
1813
+ const flatPlaces = useMemo(() => {
1814
+ const flatten = (items: Place[], level = 0): any[] => {
1815
+ return items.flatMap(place => [
1816
+ { ...place, level },
1817
+ ...flatten(place.subPlaces || [], level + 1)
1818
+ ]);
1819
+ };
1820
+ return flatten(places);
1821
+ }, [places]);
1822
+
1823
+ return (
1824
+ <EntitySelect
1825
+ value={value}
1826
+ onChange={onChange}
1827
+ items={flatPlaces}
1828
+ isLoading={isLoading}
1829
+ getItemValue={(p) => p.placeId}
1830
+ getItemLabel={(p) => `${' '.repeat(p.level)}${p.name}`}
1831
+ disabled={disabled}
1832
+ placeholder="Selecionar local"
1833
+ />
1834
+ );
1835
+ }
1836
+
1837
+ // Usar no CRUD config
1838
+ {
1839
+ key: 'place_id',
1840
+ label: 'Local',
1841
+ type: 'custom',
1842
+ component: PlaceSelect,
1843
+ required: true
1844
+ }
1845
+ ```
1846
+
1847
+ #### Cenário B: Filtrar Dados por Place
1848
+
1849
+ ```typescript
1850
+ // Service com filtro de placeId
1851
+ const { service, useCrudHook } = createSimpleService({
1852
+ tableName: 'my_table',
1853
+ schemaName: 'central',
1854
+ additionalFilters: [
1855
+ { field: 'place_id', operator: 'eq', value: selectedPlaceId }
1856
+ ]
1857
+ });
1858
+ ```
1859
+
1860
+ #### Cenário C: Exibir Nome do Local em Tabelas
1861
+
1862
+ ```typescript
1863
+ // Hook para buscar nome do place
1864
+ function usePlaceName(placeId: string) {
1865
+ const { data: places = [] } = usePlaces();
1866
+
1867
+ return useMemo(() => {
1868
+ const findPlace = (items: Place[]): Place | undefined => {
1869
+ for (const place of items) {
1870
+ if (place.placeId === placeId) return place;
1871
+ if (place.subPlaces) {
1872
+ const found = findPlace(place.subPlaces);
1873
+ if (found) return found;
1874
+ }
1875
+ }
1876
+ };
1877
+ return findPlace(places)?.name || 'Local não encontrado';
1878
+ }, [places, placeId]);
1879
+ }
1880
+
1881
+ // Usar na coluna da tabela
1882
+ {
1883
+ key: 'place_id',
1884
+ header: 'Local',
1885
+ render: (item) => {
1886
+ const placeName = usePlaceName(item.place_id);
1887
+ return <span>{placeName}</span>;
1888
+ }
1889
+ }
1890
+ ```
1891
+
1892
+ ### 🔑 Acessando placeId/placeName dos Tokens
1893
+
1894
+ **⚠️ IMPORTANTE:** `placeId` e `placeName` **NÃO** vêm diretamente dos tokens JWT. Eles são obtidos da **API Qualiex**.
1895
+
1896
+ ```typescript
1897
+ // ❌ ERRADO - Não existe no token
1898
+ const { placeId } = useAuth(); // undefined
1899
+
1900
+ // ✅ CORRETO - Buscar da API Qualiex
1901
+ const { data: places } = usePlaces();
1902
+ const userPlace = places.find(p => p.usersIds.includes(userId));
1903
+ const placeId = userPlace?.placeId;
1904
+ const placeName = userPlace?.name;
1905
+ ```
1906
+
1907
+ **Fluxo de dados:**
1908
+ 1. Token JWT contém `alias` e `companyId`
1909
+ 2. `placeService.getPlaces(alias)` busca os Places da API Qualiex
1910
+ 3. Cada `Place` contém `usersIds` (array de IDs de usuários)
1911
+ 4. Relacionar usuário logado com seu Place através de `usersIds`
1912
+
1913
+ ### 🌳 Navegação Hierárquica (Tree View)
1914
+
1915
+ ```typescript
1916
+ function PlaceTree({ places, level = 0 }: {
1917
+ places: Place[];
1918
+ level?: number;
1919
+ }) {
1920
+ return (
1921
+ <div>
1922
+ {places.map(place => (
1923
+ <div key={place.id}>
1924
+ <div style={{ paddingLeft: `${level * 20}px` }}>
1925
+ 📍 {place.name} ({place.usersIds.length} usuários)
1926
+ {!place.isActive && <Badge variant="secondary">Inativo</Badge>}
1927
+ </div>
1928
+ {place.subPlaces && place.subPlaces.length > 0 && (
1929
+ <PlaceTree places={place.subPlaces} level={level + 1} />
1930
+ )}
1931
+ </div>
1932
+ ))}
1933
+ </div>
1934
+ );
1935
+ }
1936
+ ```
1937
+
1938
+ ### 🛠️ Troubleshooting
1939
+
1940
+ | Erro | Causa | Solução |
1941
+ |------|-------|---------|
1942
+ | `CompanyId não encontrado no token` | Token não validado corretamente | Verificar edge function `validate-token` |
1943
+ | `Alias da unidade é obrigatório` | `alias` não disponível no `useAuth()` | Aguardar carregamento do auth com `isLoading` |
1944
+ | `Token Qualiex não encontrado` | Variável de ambiente faltando | Verificar `VITE_QUALIEX_API_URL` no `.env` |
1945
+ | Places retorna array vazio `[]` | Empresa sem places cadastrados | Verificar cadastro no Qualiex admin |
1946
+ | Hierarquia quebrada | `parentId` incorreto nos dados | Verificar integridade dos dados na API Qualiex |
1947
+
1948
+ ### 📦 Exemplo Completo: Dashboard por Local
1949
+
1950
+ ```typescript
1951
+ import { usePlaces } from '@/hooks/usePlaces';
1952
+ import { Card, CardHeader, CardTitle, CardContent } from 'forlogic-core';
1953
+
1954
+ function PlacesDashboard() {
1955
+ const { data: places = [], isLoading } = usePlaces();
1956
+ const { data: metrics = [] } = useQuery({
1957
+ queryKey: ['metrics'],
1958
+ queryFn: fetchMetrics
1959
+ });
1960
+
1961
+ if (isLoading) return <LoadingState />;
1962
+
1963
+ return (
1964
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
1965
+ {places.map(place => {
1966
+ const placeMetrics = metrics.filter(m => m.place_id === place.placeId);
1967
+
1968
+ return (
1969
+ <Card key={place.id}>
1970
+ <CardHeader>
1971
+ <CardTitle>{place.name}</CardTitle>
1972
+ </CardHeader>
1973
+ <CardContent>
1974
+ <div className="space-y-2">
1975
+ <p className="text-sm">
1976
+ 👥 Usuários: <strong>{place.usersIds.length}</strong>
1977
+ </p>
1978
+ <p className="text-sm">
1979
+ 📊 Registros: <strong>{placeMetrics.length}</strong>
1980
+ </p>
1981
+ <p className="text-sm">
1982
+ 📍 Sublocais: <strong>{place.subPlaces?.length || 0}</strong>
1983
+ </p>
1984
+ </div>
1985
+ </CardContent>
1986
+ </Card>
1987
+ );
1988
+ })}
1989
+ </div>
1990
+ );
1991
+ }
1992
+ ```
1993
+
1994
+ ### ✅ Checklist de Implementação
1995
+
1996
+ - [ ] `VITE_QUALIEX_API_URL` configurada no `.env`
1997
+ - [ ] Edge function `validate-token` retorna `company_id` corretamente
1998
+ - [ ] `alias` disponível no `useAuth()`
1999
+ - [ ] Hook `usePlaces()` criado e testado
2000
+ - [ ] `PlaceSelect` component criado (se necessário)
2001
+ - [ ] Tratamento de erro quando places vazio
2002
+ - [ ] Cache configurado no React Query (`staleTime`)
2003
+ - [ ] Hierarquia renderizada corretamente (se usar tree view)
2004
+
2005
+ ### 📚 Referências
2006
+
2007
+ - **Tipos:** `lib/qualiex/places/types.ts`
2008
+ - **Service:** `lib/qualiex/places/PlaceService.ts`
2009
+ - **Componente:** `lib/qualiex/places/PlacesPage.tsx`
2010
+ - **Exports:** `lib/modular.ts` e `lib/exports/integrations.ts`
2011
+ - **Token Manager:** `lib/auth/services/TokenManager.ts`
2012
+
2013
+ ---
2014
+
794
2015
  ## 🗃️ MIGRATIONS + RLS
795
2016
 
796
2017
  ### Template SQL Completo