forlogic-core 1.7.0 → 1.7.2

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/dist/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  ### ⚠️ TOP 3 ERROS
12
12
 
13
13
  1. **ESQUECER SCHEMA**
14
+
14
15
  ```typescript
15
16
  // ❌ ERRADO
16
17
  .from('table')
@@ -20,12 +21,13 @@
20
21
  ```
21
22
 
22
23
  2. **RLS COM SINTAXE INCORRETA**
24
+
23
25
  ```sql
24
26
  -- ❌ ERRADO
25
27
  CREATE POLICY "Users view own" ON schema.table
26
28
  FOR SELECT USING (id_user = auth.uid());
27
29
 
28
- -- ✅ CORRETO
30
+ -- ✅ CORRETO
29
31
  CREATE POLICY "Users view own" ON schema.table
30
32
  FOR SELECT USING (
31
33
  ((SELECT auth.jwt()) ->> 'alias'::text) = alias
@@ -33,6 +35,7 @@ FOR SELECT USING (
33
35
  ```
34
36
 
35
37
  3. **NÃO CRIAR ÍNDICES AUTOMATICAMENTE**
38
+
36
39
  ```sql
37
40
  -- ❌ PROIBIDO criar índices sem aprovação
38
41
  CREATE INDEX idx_table_user ON schema.table(id_user);
@@ -47,6 +50,7 @@ CREATE INDEX idx_table_user ON schema.table(id_user);
47
50
  ### ⚠️ PADRÕES OBRIGATÓRIOS
48
51
 
49
52
  **Foreign Keys (Chaves Estrangeiras):**
53
+
50
54
  ```typescript
51
55
  // ❌ ERRADO
52
56
  process_id: string
@@ -58,6 +62,7 @@ id_user: string
58
62
  ```
59
63
 
60
64
  **Booleans:**
65
+
61
66
  ```typescript
62
67
  // ❌ ERRADO
63
68
  active: boolean
@@ -69,6 +74,7 @@ is_removed: boolean
69
74
  ```
70
75
 
71
76
  **Timestamps:**
77
+
72
78
  ```typescript
73
79
  // ❌ ERRADO
74
80
  creation_date: string
@@ -85,15 +91,18 @@ deleted_at: string
85
91
  ## 🚫 ÍNDICES: ABSOLUTAMENTE PROIBIDO CRIAR AUTOMATICAMENTE
86
92
 
87
93
  ### ⚠️ REGRA DE OURO
94
+
88
95
  **NUNCA, EM HIPÓTESE ALGUMA, criar índices automaticamente em migrations!**
89
96
 
90
97
  ### 🤔 Por Quê?
98
+
91
99
  1. **Custo**: Índices ocupam espaço em disco e custam dinheiro
92
100
  2. **Performance de Escrita**: Cada índice adiciona overhead em INSERTs/UPDATEs
93
101
  3. **Otimização Prematura**: 99% dos índices criados "por precaução" nunca são usados
94
102
  4. **Manutenção**: Índices desnecessários dificultam manutenção e análise de queries
95
103
 
96
104
  ### ❌ Casos PROIBIDOS (mesmo que pareçam "boas práticas")
105
+
97
106
  ```sql
98
107
  -- ❌ PROIBIDO: Índice em FK "porque é boa prática"
99
108
  CREATE INDEX idx_subprocess_process ON subprocesses(id_process);
@@ -108,11 +117,13 @@ CREATE INDEX idx_deliverable_status ON deliverables(id_subprocess, is_completed)
108
117
  ### ✅ Quando Criar Índices?
109
118
 
110
119
  **APENAS** quando:
120
+
111
121
  1. ✅ **Solicitado explicitamente** pelo usuário
112
122
  2. ✅ **Análise de performance** comprovou necessidade (EXPLAIN ANALYZE)
113
123
  3. ✅ **Aprovação prévia** do usuário para incluir na migration
114
124
 
115
125
  ### 📋 Checklist OBRIGATÓRIO Antes de Qualquer Migration
126
+
116
127
  ```markdown
117
128
  - [ ] A migration NÃO contém NENHUM `CREATE INDEX`?
118
129
  - [ ] Se contém, o usuário solicitou EXPLICITAMENTE?
@@ -121,6 +132,7 @@ CREATE INDEX idx_deliverable_status ON deliverables(id_subprocess, is_completed)
121
132
  ```
122
133
 
123
134
  ### 🔧 Processo Correto Para Criar Índices
135
+
124
136
  1. **Usuário solicita** OU problemas de performance são detectados
125
137
  2. **Rodar EXPLAIN ANALYZE** para confirmar necessidade
126
138
  3. **Perguntar ao usuário**: "Posso criar o índice X na coluna Y? Isso vai melhorar a query Z mas adiciona overhead."
@@ -128,9 +140,10 @@ CREATE INDEX idx_deliverable_status ON deliverables(id_subprocess, is_completed)
128
140
  5. **Criar migration separada** apenas com os índices aprovados
129
141
 
130
142
  ### 📝 Template de Migration de Índices (quando aprovado)
143
+
131
144
  ```sql
132
145
  -- Migration: [TIMESTAMP]_add_performance_indexes.sql
133
- -- Aprovado em: [DATA]
146
+ -- Aprovado em: [DATA]
134
147
  -- Justificativa: [RAZÃO ESPECÍFICA]
135
148
 
136
149
  CREATE INDEX idx_processes_title ON processes.processes(title);
@@ -144,6 +157,7 @@ CREATE INDEX idx_processes_title ON processes.processes(title);
144
157
  ### ⚠️ ARQUIVOS PROIBIDOS DE CRIAR LOCALMENTE
145
158
 
146
159
  **NUNCA crie estes arquivos localmente:**
160
+
147
161
  ```typescript
148
162
  // ❌ PROIBIDO - Usar da lib
149
163
  src/lib/utils.ts // cn() já existe no forlogic-core
@@ -157,6 +171,7 @@ src/components/ui/input.tsx
157
171
  ### ✅ Imports Corretos
158
172
 
159
173
  **Utils (cn):**
174
+
160
175
  ```typescript
161
176
  // ❌ ERRADO
162
177
  import { cn } from '@/lib/utils'
@@ -166,6 +181,7 @@ import { cn } from 'forlogic-core'
166
181
  ```
167
182
 
168
183
  **Componentes UI:**
184
+
169
185
  ```typescript
170
186
  // ❌ ERRADO
171
187
  import { Dialog } from '@/components/ui/dialog'
@@ -177,6 +193,7 @@ import { Button } from 'forlogic-core'
177
193
  ```
178
194
 
179
195
  ### 📋 Checklist Antes de Criar Novo Arquivo
196
+
180
197
  ```markdown
181
198
  - [ ] Este componente/util JÁ existe no forlogic-core?
182
199
  - [ ] Verifiquei a lista de exports disponíveis abaixo?
@@ -186,15 +203,16 @@ import { Button } from 'forlogic-core'
186
203
  ### 📦 Componentes e Utils Disponíveis no forlogic-core
187
204
 
188
205
  **🎨 Componentes UI:**
206
+
189
207
  ```typescript
190
208
  // Formulários
191
- Button, Input, Textarea, Label, Select, SelectContent,
192
- SelectItem, SelectTrigger, SelectValue, Checkbox, RadioGroup,
209
+ Button, Input, Textarea, Label, Select, SelectContent,
210
+ SelectItem, SelectTrigger, SelectValue, Checkbox, RadioGroup,
193
211
  RadioGroupItem, Switch
194
212
 
195
213
  // Layout
196
214
  Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle
197
- Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader,
215
+ Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader,
198
216
  DialogTitle, DialogTrigger
199
217
  Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle
200
218
  Separator, ScrollArea
@@ -216,6 +234,7 @@ Table, TableBody, TableCell, TableHead, TableHeader, TableRow
216
234
  ```
217
235
 
218
236
  **🛠️ Utils e Hooks:**
237
+
219
238
  ```typescript
220
239
  // Utils
221
240
  cn // Merge classes Tailwind
@@ -246,6 +265,7 @@ errorService // Service de erros
246
265
  ### 🔍 Como Verificar Exports Disponíveis
247
266
 
248
267
  Se não tiver certeza se algo existe no forlogic-core:
268
+
249
269
  1. Abra `node_modules/forlogic-core/package.json`
250
270
  2. Verifique os exports disponíveis
251
271
  3. Em caso de dúvida, pergunte ao usuário antes de criar localmente
@@ -259,6 +279,7 @@ Se não tiver certeza se algo existe no forlogic-core:
259
279
  **Quando usar?**
260
280
 
261
281
  Use o `EntitySelect` para criar dropdowns com busca e ordenação para qualquer entidade do sistema:
282
+
262
283
  - ✅ Seleção de processos, departamentos, categorias, etc
263
284
  - ✅ Busca em tempo real
264
285
  - ✅ Ordenação alfabética automática
@@ -292,22 +313,22 @@ function MyForm() {
292
313
 
293
314
  **Props do EntitySelect:**
294
315
 
295
- | Prop | Tipo | Obrigatório | Descrição |
296
- |------|------|-------------|-----------|
297
- | `items` | `T[]` | ✅ | Array de itens para seleção |
298
- | `getItemValue` | `(item: T) => string` | ✅ | Função que retorna o valor único do item (ex: `id`) |
299
- | `getItemLabel` | `(item: T) => string` | ✅ | Função que retorna o texto exibido |
300
- | `value` | `string` | ❌ | Valor selecionado |
301
- | `onChange` | `(value: string) => void` | ❌ | Callback quando valor muda |
302
- | `isLoading` | `boolean` | ❌ | Mostra skeleton durante carregamento |
303
- | `error` | `Error \| null` | ❌ | Mostra mensagem de erro |
304
- | `placeholder` | `string` | ❌ | Placeholder do select (padrão: "Selecionar...") |
305
- | `searchPlaceholder` | `string` | ❌ | Placeholder da busca (padrão: "Buscar...") |
306
- | `emptyMessage` | `string` | ❌ | Mensagem quando não há itens |
307
- | `noResultsMessage` | `string` | ❌ | Mensagem quando busca não encontra resultados |
308
- | `sortItems` | `(a: T, b: T) => number` | ❌ | Função customizada de ordenação (padrão: alfabético) |
309
- | `disabled` | `boolean` | ❌ | Desabilita o select |
310
- | `className` | `string` | ❌ | Classes CSS adicionais |
316
+ | Prop | Tipo | Obrigatório | Descrição |
317
+ | ------------------- | ------------------------- | ----------- | ---------------------------------------------------- |
318
+ | `items` | `T[]` | ✅ | Array de itens para seleção |
319
+ | `getItemValue` | `(item: T) => string` | ✅ | Função que retorna o valor único do item (ex: `id`) |
320
+ | `getItemLabel` | `(item: T) => string` | ✅ | Função que retorna o texto exibido |
321
+ | `value` | `string` | ❌ | Valor selecionado |
322
+ | `onChange` | `(value: string) => void` | ❌ | Callback quando valor muda |
323
+ | `isLoading` | `boolean` | ❌ | Mostra skeleton durante carregamento |
324
+ | `error` | `Error \| null` | ❌ | Mostra mensagem de erro |
325
+ | `placeholder` | `string` | ❌ | Placeholder do select (padrão: "Selecionar...") |
326
+ | `searchPlaceholder` | `string` | ❌ | Placeholder da busca (padrão: "Buscar...") |
327
+ | `emptyMessage` | `string` | ❌ | Mensagem quando não há itens |
328
+ | `noResultsMessage` | `string` | ❌ | Mensagem quando busca não encontra resultados |
329
+ | `sortItems` | `(a: T, b: T) => number` | ❌ | Função customizada de ordenação (padrão: alfabético) |
330
+ | `disabled` | `boolean` | ❌ | Desabilita o select |
331
+ | `className` | `string` | ❌ | Classes CSS adicionais |
311
332
 
312
333
  **Ordenação Customizada:**
313
334
 
@@ -375,7 +396,7 @@ import { useProcesses } from '@/processes/processService';
375
396
 
376
397
  export function ProcessSelect({ value, onChange, disabled }: any) {
377
398
  const { data: processes = [], isLoading, error } = useProcesses();
378
-
399
+
379
400
  return (
380
401
  <EntitySelect
381
402
  value={value}
@@ -421,7 +442,7 @@ interface DepartmentSelectProps {
421
442
 
422
443
  export function DepartmentSelect(props: DepartmentSelectProps) {
423
444
  const { data: departments = [], isLoading, error } = useDepartments();
424
-
445
+
425
446
  return (
426
447
  <EntitySelect
427
448
  {...props}
@@ -441,13 +462,541 @@ export function DepartmentSelect(props: DepartmentSelectProps) {
441
462
 
442
463
  ---
443
464
 
465
+ ### 🎨 Campos Customizados no BaseForm
466
+
467
+ O `BaseForm` suporta campos totalmente customizados através do tipo `'custom'`, permitindo usar **qualquer componente React** como campo de formulário.
468
+
469
+ #### Interface do Componente Customizado
470
+
471
+ Todo campo customizado deve seguir esta interface:
472
+
473
+ ```typescript
474
+ interface CustomFieldProps {
475
+ value: any; // Valor atual do campo
476
+ onChange: (value: any) => void; // Função para atualizar o valor
477
+ disabled?: boolean; // Se o campo está desabilitado
478
+ error?: string; // Mensagem de erro de validação
479
+ [key: string]: any; // Props adicionais via componentProps
480
+ }
481
+ ```
482
+
483
+ #### Exemplo Completo: TagsField
484
+
485
+ **1. Criar o Componente Customizado:**
486
+
487
+ ```typescript
488
+ // src/components/TagsField.tsx
489
+ import { Label, Badge } from 'forlogic-core';
490
+ import { X } from 'lucide-react';
491
+
492
+ interface TagsFieldProps {
493
+ value: string[];
494
+ onChange: (value: string[]) => void;
495
+ disabled?: boolean;
496
+ error?: string;
497
+ label?: string;
498
+ availableTags?: Array<{ id: string; name: string; color: string }>;
499
+ }
500
+
501
+ export function TagsField({
502
+ value = [],
503
+ onChange,
504
+ disabled = false,
505
+ error,
506
+ label = 'Tags',
507
+ availableTags = []
508
+ }: TagsFieldProps) {
509
+ const handleToggleTag = (tagId: string) => {
510
+ if (disabled) return;
511
+
512
+ const newValue = value.includes(tagId)
513
+ ? value.filter(id => id !== tagId)
514
+ : [...value, tagId];
515
+
516
+ onChange(newValue);
517
+ };
518
+
519
+ return (
520
+ <div className="space-y-2">
521
+ <Label>{label}</Label>
522
+ <div className="flex flex-wrap gap-2">
523
+ {availableTags.map(tag => {
524
+ const isSelected = value.includes(tag.id);
525
+ return (
526
+ <Badge
527
+ key={tag.id}
528
+ variant={isSelected ? 'default' : 'outline'}
529
+ className="cursor-pointer"
530
+ style={isSelected ? { backgroundColor: tag.color } : {}}
531
+ onClick={() => handleToggleTag(tag.id)}
532
+ >
533
+ {tag.name}
534
+ {isSelected && <X className="ml-1 h-3 w-3" />}
535
+ </Badge>
536
+ );
537
+ })}
538
+ </div>
539
+ {error && <p className="text-sm text-red-500">{error}</p>}
540
+ </div>
541
+ );
542
+ }
543
+ ```
544
+
545
+ **2. Usar no FormField:**
546
+
547
+ ```typescript
548
+ import { TagsField } from './components/TagsField';
549
+
550
+ const formFields: FormField[] = [
551
+ {
552
+ name: 'tag_ids',
553
+ label: '',
554
+ type: 'custom',
555
+ component: TagsField,
556
+ componentProps: {
557
+ label: 'Tags do Artigo',
558
+ availableTags: [
559
+ { id: '1', name: 'Urgente', color: '#ef4444' },
560
+ { id: '2', name: 'Importante', color: '#f59e0b' }
561
+ ]
562
+ },
563
+ validation: (value) => {
564
+ if (!value?.length) return 'Selecione ao menos uma tag';
565
+ }
566
+ }
567
+ ];
568
+ ```
569
+
570
+ #### Características
571
+
572
+ - ✅ **Integração Total** - `value`, `onChange`, `disabled`, `error` gerenciados automaticamente
573
+ - ✅ **Validação** - Funciona normalmente com `validation`
574
+ - ✅ **Flexibilidade** - Props extras via `componentProps`
575
+ - ✅ **Reatividade** - `updateField` e `onValueChange` funcionam corretamente
576
+
577
+ #### Exemplos Adicionais
578
+
579
+ **MultiSelect com Busca:**
580
+
581
+ ```typescript
582
+ {
583
+ name: 'categories',
584
+ type: 'custom',
585
+ component: MultiSelectField,
586
+ componentProps: {
587
+ options: categories,
588
+ searchable: true
589
+ }
590
+ }
591
+ ```
592
+
593
+ **Rich Text Editor:**
594
+
595
+ ```typescript
596
+ {
597
+ name: 'content',
598
+ type: 'custom',
599
+ component: RichTextEditor,
600
+ componentProps: {
601
+ toolbar: ['bold', 'italic', 'link'],
602
+ minHeight: 200
603
+ }
604
+ }
605
+ ```
606
+
607
+ #### Considerações de Segurança
608
+
609
+ - ⚠️ Componentes customizados devem ser **confiáveis** (não executar código arbitrário)
610
+ - ⚠️ Validar todas as props recebidas
611
+ - ⚠️ Sanitizar inputs antes de enviar ao backend
612
+
613
+ ---
614
+
615
+ ### 🔘 ActionButton - Botão de Ações para Tabelas
616
+
617
+ Componente otimizado para menus de ação em linhas de tabela, com design discreto e consistente.
618
+
619
+ #### Características
620
+
621
+ - ✅ **Fundo azul claro** (bg-primary/10) com destaque visual
622
+ - ✅ **Tamanho compacto** (28px altura)
623
+ - ✅ **Ícone padrão** (três pontos verticais)
624
+ - ✅ **Hover suave** (primary/20)
625
+ - ✅ **Integração** com `DropdownMenu`
626
+ ...
627
+ #### Estilo Visual
628
+
629
+ **Default (padrão):**
630
+ - Fundo: `bg-primary/10` (azul claro)
631
+ - Texto: `text-primary` (azul)
632
+ - Borda: `border-primary/30` (azul médio)
633
+ - Hover: `hover:bg-primary/20` (azul mais intenso)
634
+ - Altura: `h-7` (28px)
635
+
636
+ **Comparação com Button normal:**
637
+
638
+ ```typescript
639
+ // ❌ EVITAR - Botão normal muito grande
640
+ <Button size="sm">
641
+ <EllipsisVertical />
642
+ </Button>
643
+
644
+ // ✅ RECOMENDADO - ActionButton compacto
645
+ <ActionButton />
646
+ ```
647
+
648
+ #### Quando Usar?
649
+
650
+ | Cenário | Use |
651
+ |---------|-----|
652
+ | Menu de ações em tabelas | ✅ `ActionButton` |
653
+ | Ações inline (sem dropdown) | ✅ `Button` normal |
654
+ | Formulários | ✅ `Button` normal |
655
+ | Header de página | ✅ `Button` normal |
656
+
657
+ #### Integração com CRUD
658
+
659
+ O sistema CRUD usa automaticamente o `ActionButton` através do componente `TableRowActions`:
660
+
661
+ ```typescript
662
+ // Gerado automaticamente pelo createCrudPage()
663
+ <TableRowActions
664
+ onEdit={() => handleEdit(item)}
665
+ onDelete={() => handleDelete(item)}
666
+ onToggleStatus={() => handleToggle(item)}
667
+ isActive={item.is_active}
668
+ />
669
+ ```
670
+
671
+ #### Props
672
+
673
+ ```typescript
674
+ interface ActionButtonProps {
675
+ variant?: 'default' | 'ghost'; // Estilo do botão
676
+ children?: React.ReactNode; // Ícone customizado
677
+ onClick?: () => void; // Handler de click
678
+ disabled?: boolean; // Desabilitar botão
679
+ className?: string; // Classes adicionais
680
+ }
681
+ ```
682
+
683
+ #### Acessibilidade
684
+
685
+ ```typescript
686
+ <ActionButton
687
+ aria-label="Abrir menu de ações"
688
+ onClick={() => console.log('clicked')}
689
+ />
690
+ ```
691
+
692
+ ---
693
+
694
+ ### 📄 Títulos de Página com 3 Linhas
695
+
696
+ O hook `usePageMetadata` suporta até **3 linhas de informação** no header através dos campos `supertitle`, `title` e `subtitle`.
697
+
698
+ #### Estrutura Visual
699
+
700
+ ```
701
+ [supertitle - text-xs, cinza claro] ← Contexto/Breadcrumb
702
+ [title - text-lg, bold, preto] [Badge] ← Título principal
703
+ [subtitle - text-sm, cinza] ← Metadados/Descrição
704
+ ```
705
+
706
+ #### Exemplo Completo (3 Linhas)
707
+
708
+ ```typescript
709
+ import { usePageMetadata } from 'forlogic-core';
710
+
711
+ function KRDetailsPage() {
712
+ usePageMetadata({
713
+ supertitle: 'OKR1 Tracionar o trabalho de processos melhorando a eficácia',
714
+ title: 'KR1 50% dos indicadores de empreendimento e áreas de negócio atingindo a meta',
715
+ subtitle: '25Q4 • Ativo • 01/10/2025 - 31/12/2025 • Laura Alves Nunes Oliveira'
716
+ });
717
+
718
+ return <div>Conteúdo da página...</div>;
719
+ }
720
+ ```
721
+
722
+ **Resultado visual:**
723
+
724
+ ```
725
+ OKR1 Tracionar o trabalho de processos melhorando a eficácia
726
+ KR1 50% dos indicadores de empreendimento e áreas de negócio atingindo a meta [Módulo]
727
+ 25Q4 • Ativo • 01/10/2025 - 31/12/2025 • Laura Alves Nunes Oliveira
728
+ ```
729
+
730
+ ---
731
+
732
+ #### Exemplo com 2 Linhas
733
+
734
+ ```typescript
735
+ usePageMetadata({
736
+ title: 'Gestão de Processos',
737
+ subtitle: 'Visualize e gerencie todos os processos da organização'
738
+ });
739
+ ```
740
+
741
+ **Resultado visual:**
742
+
743
+ ```
744
+ Gestão de Processos [Módulo]
745
+ Visualize e gerencie todos os processos da organização
746
+ ```
747
+
748
+ ---
749
+
750
+ #### Exemplo com Dados Dinâmicos
751
+
752
+ ```typescript
753
+ import { useParams } from 'react-router-dom';
754
+ import { usePageMetadata } from 'forlogic-core';
755
+
756
+ function ProcessDetailPage() {
757
+ const { id } = useParams();
758
+ const { data: process } = useProcess(id);
759
+ const { data: okr } = useOKR(process?.okr_id);
760
+
761
+ usePageMetadata({
762
+ supertitle: okr?.name || 'Carregando...',
763
+ title: process?.name || 'Carregando...',
764
+ subtitle: [
765
+ process?.quarter,
766
+ process?.status,
767
+ `${process?.start_date} - ${process?.end_date}`,
768
+ process?.responsible_name
769
+ ].filter(Boolean).join(' • ')
770
+ });
771
+
772
+ return <div>...</div>;
773
+ }
774
+ ```
775
+
776
+ ---
777
+
778
+ #### Quando Usar Cada Campo?
779
+
780
+ | Campo | Uso | Exemplo |
781
+ | ------------ | -------------------------------------------- | --------------------------------------------------------------- |
782
+ | `supertitle` | Contexto/hierarquia acima do item atual | "OKR1 Melhorar eficácia", "Departamento → Setor" |
783
+ | `title` | Nome/identificação principal do item | "KR1 50% dos indicadores atingindo meta", "Gestão de Processos" |
784
+ | `subtitle` | Metadados, descrição, informações adicionais | "25Q4 • Ativo • 01/10/2025", "Visualize todos os processos" |
785
+
786
+ ---
787
+
788
+ #### Características
789
+
790
+ ✅ **Opcional** - Todos os campos são opcionais
791
+ ✅ **Truncate automático** - Textos longos são cortados com `...`
792
+ ✅ **Backward compatible** - Projetos antigos continuam funcionando
793
+ ✅ **Renderização condicional** - Linhas vazias não são exibidas
794
+ ✅ **Type-safe** - TypeScript valida os tipos automaticamente
795
+
796
+ ---
797
+
798
+ #### Boas Práticas
799
+
800
+ **✅ BOM - Hierarquia clara:**
801
+
802
+ ```typescript
803
+ usePageMetadata({
804
+ supertitle: 'Contexto maior (departamento, categoria, OKR)',
805
+ title: 'Item específico atual',
806
+ subtitle: 'Metadados (datas, status, responsável)'
807
+ });
808
+ ```
809
+
810
+ **❌ EVITAR - Textos muito longos:**
811
+
812
+ ```typescript
813
+ // Textos serão truncados, use resumos curtos
814
+ supertitle: 'Este é um texto extremamente longo que será cortado...'
815
+ ```
816
+
817
+ **✅ BOM - Separadores visuais no subtitle:**
818
+
819
+ ```typescript
820
+ subtitle: '25Q4 • Ativo • 01/10/2025 - 31/12/2025 • João Silva'
821
+ // ^^^^^ Use • (bullet) para separar informações
822
+ ```
823
+
824
+ **✅ BOM - Dados dinâmicos com fallback:**
825
+
826
+ ```typescript
827
+ supertitle: okr?.name || undefined // Não exibe se não houver dados
828
+ title: process?.name || 'Carregando...'
829
+ ```
830
+
831
+ ---
832
+
833
+ #### Exemplo Real: Sistema de OKRs
834
+
835
+ ```typescript
836
+ import { useParams } from 'react-router-dom';
837
+ import { usePageMetadata } from 'forlogic-core';
838
+ import { useOKR, useKR } from './hooks';
839
+
840
+ function KRDetailsPage() {
841
+ const { krId } = useParams();
842
+ const { data: kr, isLoading } = useKR(krId);
843
+ const { data: okr } = useOKR(kr?.okr_id);
844
+
845
+ usePageMetadata({
846
+ supertitle: okr?.name,
847
+ title: kr?.name || 'Carregando...',
848
+ subtitle: kr ? [
849
+ kr.quarter,
850
+ kr.status,
851
+ `${kr.start_date} - ${kr.end_date}`,
852
+ kr.responsible_name
853
+ ].filter(Boolean).join(' • ') : undefined
854
+ });
855
+
856
+ if (isLoading) return <div>Carregando...</div>;
857
+
858
+ return (
859
+ <div className="p-6">
860
+ {/* Conteúdo da página de KR */}
861
+ </div>
862
+ );
863
+ }
864
+ ```
865
+
866
+ **Resultado visual:**
867
+
868
+ ```
869
+ OKR1 Tracionar o trabalho de processos melhorando a eficácia
870
+ KR1 50% dos indicadores de empreendimento e áreas de negócio atingindo a meta [Módulo]
871
+ 25Q4 • Ativo • 01/10/2025 - 31/12/2025 • Laura Alves Nunes Oliveira
872
+ ```
873
+
874
+ ---
875
+
876
+ ### 🏢 Sistema de Gestores de Locais (Qualiex)
877
+
878
+ Componentes para gerenciamento de gestores e membros de locais/sublocais integrados com a API Qualiex.
879
+
880
+ #### Importação
881
+
882
+ ```typescript
883
+ import {
884
+ PlaceManagerButton,
885
+ PlaceManagerBadge,
886
+ ManagerSelectionDialog,
887
+ usePlaceManagers,
888
+ PlaceManagerService,
889
+ type PlaceManager
890
+ } from 'forlogic-core';
891
+ ```
892
+
893
+ #### Componentes
894
+
895
+ **PlaceManagerButton** - Botão dropdown com ações de gerenciamento:
896
+
897
+ ```tsx
898
+ <PlaceManagerButton
899
+ placeId="abc-123"
900
+ placeName="Matriz São Paulo"
901
+ serviceConfig={{ tableName: 'place_managers' }}
902
+ />
903
+ ```
904
+
905
+ **PlaceManagerBadge** - Badge visual com contador de gestores:
906
+
907
+ ```tsx
908
+ <PlaceManagerBadge
909
+ placeId="abc-123"
910
+ userCount={15}
911
+ />
912
+ ```
913
+
914
+ **ManagerSelectionDialog** - Diálogo completo de seleção:
915
+
916
+ ```tsx
917
+ <ManagerSelectionDialog
918
+ open={showDialog}
919
+ onOpenChange={setShowDialog}
920
+ onSelectManager={(user) => console.log('Gestor:', user)}
921
+ onSelectMember={(user) => console.log('Membro:', user)}
922
+ currentManagerId={manager?.user_id}
923
+ currentMemberIds={members.map(m => m.user_id)}
924
+ placeName="Matriz São Paulo"
925
+ />
926
+ ```
927
+
928
+ #### Hook: usePlaceManagers
929
+
930
+ ```typescript
931
+ const {
932
+ managers, // Todos (gestor + membros)
933
+ manager, // Apenas o gestor
934
+ members, // Apenas membros
935
+ setManager, // (user: QualiexUser) => void
936
+ addMember, // (user: QualiexUser) => void
937
+ remove, // (userId: string) => void
938
+ isLoading
939
+ } = usePlaceManagers(placeId);
940
+ ```
941
+
942
+ #### Service: PlaceManagerService
943
+
944
+ ```typescript
945
+ import { PlaceManagerService } from 'forlogic-core';
946
+
947
+ // Instância customizada
948
+ const service = new PlaceManagerService({
949
+ tableName: 'my_place_managers',
950
+ schemaName: 'custom_schema'
951
+ });
952
+
953
+ // Métodos disponíveis
954
+ await service.getPlaceManagers(alias, placeId);
955
+ await service.setManager(alias, placeId, user);
956
+ await service.addMember(alias, placeId, user);
957
+ await service.removePlaceUser(alias, placeId, userId);
958
+ ```
959
+
960
+ > ⚠️ A configuração do banco de dados (tabelas, RLS policies) deve ser feita no projeto consumidor.
961
+
962
+ #### Regras de Negócio
963
+
964
+ 1. **Um gestor por local** - Ao definir novo gestor, o anterior é removido automaticamente
965
+ 2. **Membros ilimitados** - Múltiplos membros podem ser adicionados
966
+ 3. **Ordenação alfabética** - Sempre ordenado por nome
967
+ 4. **Busca inteligente** - Filtra por nome ou email
968
+
969
+ #### Exemplo Completo
970
+
971
+ ```tsx
972
+ function PlaceTreeItem({ place }) {
973
+ return (
974
+ <div className="flex items-center gap-3">
975
+ <span>{place.name}</span>
976
+
977
+ <PlaceManagerBadge
978
+ placeId={place.id}
979
+ userCount={place.usersIds.length}
980
+ />
981
+
982
+ <PlaceManagerButton
983
+ placeId={place.id}
984
+ placeName={place.name}
985
+ />
986
+ </div>
987
+ );
988
+ }
989
+ ```
990
+
991
+ ---
992
+
444
993
  ### ✅ CHECKLIST (antes de implementar)
445
994
 
446
995
  - [ ] Schema `schema` especificado em queries e service?
447
996
  - [ ] RLS usando extração JWT `((SELECT auth.jwt()) ->> 'alias'::text) = alias`?
448
997
  - [ ] **Nomenclatura correta**: `id_<entity>`, `is_<bool>`, `<action>_at`?
449
998
  - [ ] **Migration SEM índices** (ou aprovados pelo usuário)?
450
- - [ ] **Imports do forlogic-core** (não criar utils.ts ou ui/* localmente)?
999
+ - [ ] **Imports do forlogic-core** (não criar utils.ts ou ui/\* localmente)?
451
1000
  - [ ] Preservar `item.id` no update?
452
1001
  - [ ] Config gerado com `useMemo()`?
453
1002
  - [ ] `<Outlet />` no componente pai?
@@ -468,10 +1017,10 @@ graph TD
468
1017
  E --> F[CrudTable<br/>Tabela]
469
1018
  E --> G[BaseForm<br/>Formulário]
470
1019
  E --> H[BulkActionBar<br/>Ações em Massa]
471
-
1020
+
472
1021
  I[(Supabase DB)] -.->|RLS + Soft Delete| B
473
1022
  J[Qualiex API] -.->|responsible_name| B
474
-
1023
+
475
1024
  style A fill:#e1f5ff
476
1025
  style B fill:#fff4e1
477
1026
  style C fill:#ffe1f5
@@ -589,7 +1138,7 @@ import { usePageMetadataContext } from 'forlogic-core';
589
1138
 
590
1139
  export default function MyPage() {
591
1140
  const { setMetadata } = usePageMetadataContext();
592
-
1141
+
593
1142
  useEffect(() => {
594
1143
  setMetadata({
595
1144
  title: 'Meus Itens',
@@ -610,11 +1159,13 @@ export const SEARCH_CONFIG = {
610
1159
  ```
611
1160
 
612
1161
  **Quando aumentar o delay:**
1162
+
613
1163
  - ✅ Muitos usuários simultâneos (reduz carga no servidor)
614
1164
  - ✅ Campos de busca muito amplos (muitos registros)
615
1165
  - ✅ Backend com rate limiting
616
1166
 
617
1167
  **Quando reduzir o delay:**
1168
+
618
1169
  - ✅ Poucos registros (resposta instantânea)
619
1170
  - ✅ Busca crítica para UX (feedback imediato)
620
1171
 
@@ -664,16 +1215,128 @@ graph LR
664
1215
  D --> E[useCrud lê URL]
665
1216
  E --> F[Supabase Query]
666
1217
  F --> G[Resultados filtrados]
667
-
1218
+
668
1219
  style C fill:#d4f4dd
669
1220
  style F fill:#ffd4d4
670
1221
  ```
671
1222
 
672
1223
  ---
673
1224
 
1225
+ ## 🎯 ARQUITETURA DE 3 NÍVEIS - CRUD COMPONENTS
1226
+
1227
+ O sistema CRUD oferece **3 níveis de abstração**:
1228
+
1229
+ ### **📊 Quando Usar Cada Nível?**
1230
+
1231
+ | Cenário | Nível | Componentes |
1232
+ | -------------------------- | ------------------ | ------------------------- |
1233
+ | CRUD padrão | 🟩 Página Completa | `createCrudPage()` |
1234
+ | CRUD customizado | 🔶 Compostos | `CrudTable` + `FilterBar` |
1235
+ | Dashboard/Tabelas não-CRUD | 🔷 Primitivos | `TablePrimitive` |
1236
+ | Kanban/Grid customizado | 🔷 Primitivos | `CardsPrimitive` |
1237
+
1238
+ ### **🔷 NÍVEL 1 - Primitivos (Controle Total)**
1239
+
1240
+ Componentes puros **sem lógica CRUD**:
1241
+
1242
+ ```typescript
1243
+ import {
1244
+ TablePrimitive, // Tabela reutilizável
1245
+ ActionMenuPrimitive, // Menu de ações
1246
+ PaginationPrimitive, // Paginação manual
1247
+ FilterBarPrimitive, // Barra de filtros
1248
+ CardsPrimitive // Grid de cards
1249
+ } from 'forlogic-core';
1250
+ ```
1251
+
1252
+ **Exemplo:**
1253
+
1254
+ ```typescript
1255
+ <TablePrimitive
1256
+ data={reports}
1257
+ columns={[
1258
+ { key: 'date', header: 'Data', sortable: true },
1259
+ { key: 'revenue', header: 'Receita' }
1260
+ ]}
1261
+ onSort={handleSort}
1262
+ renderActions={(item) => (
1263
+ <ActionMenuPrimitive customActions={[...]} />
1264
+ )}
1265
+ />
1266
+ ```
1267
+
1268
+ ### **🔶 NÍVEL 2 - Compostos (CRUD Integrado)**
1269
+
1270
+ Primitivos + lógica CRUD:
1271
+
1272
+ ```typescript
1273
+ import {
1274
+ CrudTable, // Tabela com CRUD
1275
+ CrudCards, // Cards com CRUD
1276
+ CrudPagination, // Paginação integrada
1277
+ FilterBar, // Filtros integrados
1278
+ BulkActionBar // Ações em massa
1279
+ } from 'forlogic-core';
1280
+ ```
1281
+
1282
+ **Exemplo:**
1283
+
1284
+ ```typescript
1285
+ <FilterBar
1286
+ searchValue={manager.searchTerm}
1287
+ onSearchChange={manager.handleSearch}
1288
+ customFilters={[<StatusSelect />]}
1289
+ />
1290
+
1291
+ <CrudTable
1292
+ manager={manager}
1293
+ columns={customColumns}
1294
+ renderActions={(item) => <CustomActions item={item} />}
1295
+ />
1296
+
1297
+ <CrudPagination manager={manager} />
1298
+ ```
1299
+
1300
+ ### **🟩 NÍVEL 3 - Página Completa (Geração Automática)**
1301
+
1302
+ API de alto nível:
1303
+
1304
+ ```typescript
1305
+ import {
1306
+ createCrudPage, // Gera página completa
1307
+ generateCrudConfig, // Config automática
1308
+ createSimpleService // Service + Hook
1309
+ } from 'forlogic-core';
1310
+ ```
1311
+
1312
+ **Exemplo:**
1313
+
1314
+ ```typescript
1315
+ const config = useMemo(() =>
1316
+ generateCrudConfig<Product>({
1317
+ entity: 'produto',
1318
+ columns: [
1319
+ { key: 'name', label: 'Nome', required: true },
1320
+ { key: 'price', label: 'Preço', type: 'number' }
1321
+ ]
1322
+ }),
1323
+ []);
1324
+
1325
+ return createCrudPage({ config, crud: manager, saveHandler: manager.save });
1326
+ ```
1327
+
1328
+ ### **🧪 Página de Demonstração**
1329
+
1330
+ Acesse **`/demo-crud`** para ver todos os componentes em ação com exemplos práticos.
1331
+
1332
+ ---
1333
+
1334
+ ---
1335
+
674
1336
  ## 🚀 QUICK START - Criar CRUD Completo
675
1337
 
676
1338
  ### **1️⃣ Type**
1339
+
677
1340
  ```typescript
678
1341
  // src/processes/process.ts
679
1342
  export interface Process {
@@ -692,12 +1355,13 @@ export type ProcessUpdate = Partial<ProcessInsert>;
692
1355
  ```
693
1356
 
694
1357
  ### **2️⃣ Service**
1358
+
695
1359
  ```typescript
696
1360
  // src/processes/processService.ts
697
1361
  import { createSimpleService } from 'forlogic-core';
698
1362
  import { Process, ProcessInsert, ProcessUpdate } from './process';
699
1363
 
700
- export const { service: processService, useCrudHook: useProcesses } =
1364
+ export const { service: processService, useCrudHook: useProcesses } =
701
1365
  createSimpleService<Process, ProcessInsert, ProcessUpdate>({
702
1366
  tableName: 'processes',
703
1367
  entityName: 'processo',
@@ -708,6 +1372,7 @@ export const { service: processService, useCrudHook: useProcesses } =
708
1372
  ```
709
1373
 
710
1374
  ### **3️⃣ Save Handler (Integrado ao useCrud)**
1375
+
711
1376
  ```typescript
712
1377
  // src/processes/ProcessesPage.tsx
713
1378
 
@@ -731,6 +1396,7 @@ export default function ProcessesPage() {
731
1396
  ```
732
1397
 
733
1398
  ### **4️⃣ Config (com useMemo)**
1399
+
734
1400
  ```typescript
735
1401
  // src/processes/ProcessesPage.tsx
736
1402
  import { useMemo } from 'react';
@@ -740,12 +1406,12 @@ export default function ProcessesPage() {
740
1406
  const crud = useProcesses();
741
1407
 
742
1408
  // ⚠️ OBRIGATÓRIO useMemo para evitar re-renders
743
- const config = useMemo(() =>
1409
+ const config = useMemo(() =>
744
1410
  generateCrudConfig<Process>({
745
1411
  entity: 'processo',
746
1412
  columns: [
747
1413
  { key: 'title', label: 'Título', type: 'text', required: true },
748
- { key: 'status', label: 'Status', type: 'select',
1414
+ { key: 'status', label: 'Status', type: 'select',
749
1415
  options: [
750
1416
  { value: 'draft', label: 'Rascunho' },
751
1417
  { value: 'active', label: 'Ativo' },
@@ -754,7 +1420,7 @@ export default function ProcessesPage() {
754
1420
  },
755
1421
  { key: 'description', label: 'Descrição', type: 'textarea' }
756
1422
  ]
757
- }),
1423
+ }),
758
1424
  []);
759
1425
 
760
1426
  return createCrudPage({
@@ -766,6 +1432,7 @@ export default function ProcessesPage() {
766
1432
  ```
767
1433
 
768
1434
  ### **5️⃣ Page + Outlet (preservar estado)**
1435
+
769
1436
  ```typescript
770
1437
  // src/App.tsx
771
1438
  import { Outlet } from 'react-router-dom';
@@ -805,16 +1472,17 @@ npm install forlogic-core@latest
805
1472
  ### **1️⃣ Migração de Tipos (Breaking Change)**
806
1473
 
807
1474
  #### **❌ ANTES (Versão Antiga):**
1475
+
808
1476
  ```typescript
809
- import {
810
- ContentEntity,
811
- VisualEntity,
812
- UserRelatedEntity,
1477
+ import {
1478
+ ContentEntity,
1479
+ VisualEntity,
1480
+ UserRelatedEntity,
813
1481
  ActivableEntity,
814
- FormEntity
1482
+ FormEntity
815
1483
  } from 'forlogic-core';
816
1484
 
817
- export interface Example extends
1485
+ export interface Example extends
818
1486
  ContentEntity,
819
1487
  VisualEntity,
820
1488
  UserRelatedEntity,
@@ -839,6 +1507,7 @@ export interface UpdateExamplePayload extends Partial<CreateExamplePayload> {
839
1507
  ```
840
1508
 
841
1509
  #### **✅ DEPOIS (Nova API):**
1510
+
842
1511
  ```typescript
843
1512
  import { BaseEntity } from 'forlogic-core';
844
1513
 
@@ -855,7 +1524,7 @@ export interface Example extends BaseEntity {
855
1524
  }
856
1525
 
857
1526
  export type CreateExamplePayload = Omit<
858
- Example,
1527
+ Example,
859
1528
  keyof BaseEntity | 'responsible_name'
860
1529
  >;
861
1530
 
@@ -863,6 +1532,7 @@ export type UpdateExamplePayload = Partial<CreateExamplePayload>;
863
1532
  ```
864
1533
 
865
1534
  **📝 Mudanças:**
1535
+
866
1536
  - ❌ **REMOVIDO**: Helper interfaces (`ContentEntity`, `VisualEntity`, etc)
867
1537
  - ✅ **NOVO**: Apenas `BaseEntity` + campos explícitos
868
1538
  - ✅ **NOVO**: `is_actived` agora é campo padrão de `BaseEntity`
@@ -873,6 +1543,7 @@ export type UpdateExamplePayload = Partial<CreateExamplePayload>;
873
1543
  ### **2️⃣ Migração de Save Handler (Breaking Change)**
874
1544
 
875
1545
  #### **❌ ANTES (createSimpleSaveHandler):**
1546
+
876
1547
  ```typescript
877
1548
  import { createSimpleSaveHandler, useAuth } from 'forlogic-core';
878
1549
 
@@ -895,6 +1566,7 @@ const handleSave = createSimpleSaveHandler(
895
1566
  ```
896
1567
 
897
1568
  #### **✅ DEPOIS (manager.save):**
1569
+
898
1570
  ```typescript
899
1571
  const handleSave = (data: any) => {
900
1572
  manager.save(data, (d) => ({
@@ -910,6 +1582,7 @@ const handleSave = (data: any) => {
910
1582
  ```
911
1583
 
912
1584
  **📝 Mudanças:**
1585
+
913
1586
  - ❌ **REMOVIDO**: `createSimpleSaveHandler`
914
1587
  - ❌ **REMOVIDO**: Import de `useAuth` para pegar `alias`
915
1588
  - ✅ **NOVO**: `manager.save(data, transform)`
@@ -920,6 +1593,7 @@ const handleSave = (data: any) => {
920
1593
  ### **3️⃣ Migração de Campos de Formulário**
921
1594
 
922
1595
  #### **❌ ANTES (Tipos Múltiplos):**
1596
+
923
1597
  ```typescript
924
1598
  {
925
1599
  name: 'id_user',
@@ -930,6 +1604,7 @@ const handleSave = (data: any) => {
930
1604
  ```
931
1605
 
932
1606
  #### **✅ DEPOIS (Tipo Unificado):**
1607
+
933
1608
  ```typescript
934
1609
  {
935
1610
  name: 'id_user',
@@ -941,6 +1616,7 @@ const handleSave = (data: any) => {
941
1616
  ```
942
1617
 
943
1618
  **📝 Mudanças:**
1619
+
944
1620
  - ❌ **REMOVIDO**: `'simple-qualiex-user-field'`, `'single-responsible-select'`
945
1621
  - ✅ **NOVO**: Apenas `'user-select'` com parâmetro `mode`
946
1622
 
@@ -949,12 +1625,14 @@ const handleSave = (data: any) => {
949
1625
  ### **4️⃣ Migração de Imports**
950
1626
 
951
1627
  #### **❌ ANTES:**
1628
+
952
1629
  ```typescript
953
1630
  import { createSimpleSaveHandler } from 'forlogic-core';
954
1631
  import { ContentEntity, VisualEntity, ... } from 'forlogic-core';
955
1632
  ```
956
1633
 
957
1634
  #### **✅ DEPOIS:**
1635
+
958
1636
  ```typescript
959
1637
  // createSimpleSaveHandler removido (usar manager.save)
960
1638
  import { BaseEntity } from 'forlogic-core';
@@ -962,6 +1640,7 @@ import { handleExternalLink } from 'forlogic-core'; // NOVO helper
962
1640
  ```
963
1641
 
964
1642
  **📝 Mudanças:**
1643
+
965
1644
  - ❌ **REMOVIDO**: `createSimpleSaveHandler`
966
1645
  - ❌ **REMOVIDO**: Helper interfaces de tipos
967
1646
  - ✅ **NOVO**: `handleExternalLink` (helper de links externos)
@@ -971,11 +1650,12 @@ import { handleExternalLink } from 'forlogic-core'; // NOVO helper
971
1650
  ### **5️⃣ Migração de Filtros Customizados**
972
1651
 
973
1652
  #### **❌ ANTES (Filtro Frontend):**
1653
+
974
1654
  ```typescript
975
1655
  const [statusFilter, setStatusFilter] = useState('active');
976
1656
 
977
1657
  const filteredEntities = useMemo(() => {
978
- return manager.entities.filter(e =>
1658
+ return manager.entities.filter(e =>
979
1659
  statusFilter === 'all' ? true : e.is_actived
980
1660
  );
981
1661
  }, [manager.entities, statusFilter]);
@@ -989,6 +1669,7 @@ const filteredManager = useMemo(() => ({
989
1669
  ```
990
1670
 
991
1671
  #### **✅ DEPOIS (Filtro Backend - Recomendado):**
1672
+
992
1673
  ```typescript
993
1674
  const [statusFilter, setStatusFilter] = useState<boolean | 'all'>(true);
994
1675
 
@@ -1002,6 +1683,7 @@ const CrudPage = createCrudPage({ manager, config, onSave });
1002
1683
  ```
1003
1684
 
1004
1685
  **📝 Mudanças:**
1686
+
1005
1687
  - ✅ **RECOMENDADO**: Filtro aplicado no backend (melhor performance)
1006
1688
  - ✅ **NOVO**: Hook aceita `additionalFilters` como parâmetro
1007
1689
  - ❌ **EVITAR**: Filtro frontend (só para casos complexos)
@@ -1013,6 +1695,7 @@ const CrudPage = createCrudPage({ manager, config, onSave });
1013
1695
  Use este checklist para validar que seu projeto foi migrado corretamente:
1014
1696
 
1015
1697
  #### **Types (`example.ts`):**
1698
+
1016
1699
  - [ ] Removido imports de helper interfaces (`ContentEntity`, `VisualEntity`, etc)
1017
1700
  - [ ] Interface principal agora estende apenas `BaseEntity`
1018
1701
  - [ ] Campos explícitos declarados na interface
@@ -1022,9 +1705,11 @@ Use este checklist para validar que seu projeto foi migrado corretamente:
1022
1705
  - [ ] Removido interfaces/types não usados (`ExampleFilters`, `ExampleSortField`, `ExampleInsert`, `ExampleUpdate`)
1023
1706
 
1024
1707
  #### **Service (`ExampleService.ts`):**
1708
+
1025
1709
  - [ ] Nenhuma mudança necessária (API permanece igual)
1026
1710
 
1027
1711
  #### **Page (`ExamplesPage.tsx`):**
1712
+
1028
1713
  - [ ] Removido import de `createSimpleSaveHandler`
1029
1714
  - [ ] Removido import de `useAuth` (se usado apenas para alias)
1030
1715
  - [ ] Substituído `createSimpleSaveHandler` por `manager.save()`
@@ -1033,6 +1718,7 @@ Use este checklist para validar que seu projeto foi migrado corretamente:
1033
1718
  - [ ] Substituído lógica de links externos por `handleExternalLink` helper
1034
1719
 
1035
1720
  #### **Testes:**
1721
+
1036
1722
  - [ ] Build sem erros TypeScript (`npm run build`)
1037
1723
  - [ ] Página carrega sem erros
1038
1724
  - [ ] Criar novo item funciona (alias injetado corretamente)
@@ -1053,14 +1739,17 @@ Use este checklist para validar que seu projeto foi migrado corretamente:
1053
1739
  ### **❓ Problemas na Migração?**
1054
1740
 
1055
1741
  #### **Erro: "Property 'save' does not exist on type..."**
1742
+
1056
1743
  - ✅ **Solução**: Atualize `forlogic-core` para versão mais recente
1057
1744
  - ✅ **Comando**: `npm install forlogic-core@latest`
1058
1745
 
1059
1746
  #### **Erro: "Cannot find module 'ContentEntity'"**
1747
+
1060
1748
  - ✅ **Solução**: Remova imports de helper interfaces e use `BaseEntity`
1061
1749
  - ✅ **Ver**: Seção "1️⃣ Migração de Tipos" acima
1062
1750
 
1063
1751
  #### **Erro: "alias is required but not provided"**
1752
+
1064
1753
  - ✅ **Solução**: Use `manager.save()` ao invés de `createEntity` direto
1065
1754
  - ✅ **Ver**: Seção "2️⃣ Migração de Save Handler" acima
1066
1755
 
@@ -1137,15 +1826,18 @@ const config: CrudPageConfig<Process> = {
1137
1826
  ### **Comportamento**
1138
1827
 
1139
1828
  **Desktop (Tabela):**
1829
+
1140
1830
  - Checkbox na primeira coluna (50px de largura)
1141
1831
  - Checkbox no header para "Selecionar Todos"
1142
1832
  - Click na linha NÃO abre o form quando bulk actions está ativo (evita edição acidental)
1143
1833
 
1144
1834
  **Mobile (Cards):**
1835
+
1145
1836
  - Checkbox no canto superior esquerdo de cada card
1146
1837
  - Mesmo comportamento de seleção que desktop
1147
1838
 
1148
1839
  **Barra de Ações:**
1840
+
1149
1841
  - Aparece automaticamente quando há itens selecionados
1150
1842
  - Mostra quantidade de itens selecionados
1151
1843
  - Botão "Limpar" para deselecionar todos
@@ -1231,25 +1923,25 @@ Este tutorial mostra como criar um CRUD completo usando o módulo **Examples** c
1231
1923
 
1232
1924
  ```typescript
1233
1925
  // ============= EXAMPLE MODULE TYPES =============
1234
- import {
1926
+ import {
1235
1927
  ContentEntity, // title, description
1236
1928
  VisualEntity, // color, icon_name
1237
1929
  UserRelatedEntity, // id_user, responsible_name
1238
1930
  ActivableEntity, // is_actived
1239
1931
  FormEntity, // url_field, date_field
1240
- FilterState,
1241
- EntitySortField
1932
+ FilterState,
1933
+ EntitySortField
1242
1934
  } from 'forlogic-core';
1243
1935
 
1244
1936
  /**
1245
1937
  * Example - Entidade completa de exemplo
1246
- *
1938
+ *
1247
1939
  * ✅ Campos Customizados:
1248
1940
  * - title, description (conteúdo)
1249
1941
  * - color, icon_name (visual)
1250
1942
  * - id_user, responsible_name (usuário - enriquecido via Qualiex)
1251
1943
  * - url_field, date_field (formulário)
1252
- *
1944
+ *
1253
1945
  * 🔒 Campos Herdados de BaseEntity (automáticos):
1254
1946
  * - id: string
1255
1947
  * - alias: string
@@ -1272,20 +1964,20 @@ export interface Example extends BaseEntity {
1272
1964
 
1273
1965
  /**
1274
1966
  * CreateExamplePayload - Dados para CRIAR novo registro
1275
- *
1967
+ *
1276
1968
  * ⚠️ IMPORTANTE:
1277
1969
  * - Campo `alias` é injetado AUTOMATICAMENTE pelo manager.save()
1278
1970
  * - Campos opcionais devem ter `| null`
1279
1971
  * - NÃO incluir id, created_at, updated_at (gerados automaticamente)
1280
1972
  */
1281
1973
  export type CreateExamplePayload = Omit<
1282
- Example,
1974
+ Example,
1283
1975
  keyof BaseEntity | 'responsible_name'
1284
1976
  >;
1285
1977
 
1286
1978
  /**
1287
1979
  * UpdateExamplePayload - Dados para ATUALIZAR registro existente
1288
- *
1980
+ *
1289
1981
  * 📝 Pattern:
1290
1982
  * - Todos os campos são opcionais (Partial)
1291
1983
  */
@@ -1293,6 +1985,7 @@ export type UpdateExamplePayload = Partial<CreateExamplePayload>;
1293
1985
  ```
1294
1986
 
1295
1987
  **📖 Explicação Detalhada:**
1988
+
1296
1989
  - **Composição de Interfaces:** Ao invés de redefinir campos, herda de interfaces prontas da lib
1297
1990
  - **`alias` no CreatePayload:** RLS do Supabase precisa desse campo para funcionar
1298
1991
  - **`Partial<>` no UpdatePayload:** Permite updates parciais (só manda os campos que mudaram)
@@ -1310,7 +2003,7 @@ import type { Example, CreateExamplePayload, UpdateExamplePayload } from './exam
1310
2003
 
1311
2004
  /**
1312
2005
  * ExampleService - Service CRUD completo gerado automaticamente
1313
- *
2006
+ *
1314
2007
  * ✅ O que é gerado:
1315
2008
  * - service.getAll(params)
1316
2009
  * - service.getById(id)
@@ -1318,13 +2011,13 @@ import type { Example, CreateExamplePayload, UpdateExamplePayload } from './exam
1318
2011
  * - service.update(id, data)
1319
2012
  * - service.delete(id)
1320
2013
  * - useCrudHook() - Hook React Query integrado
1321
- *
2014
+ *
1322
2015
  * 🔧 Configuração:
1323
2016
  * - tableName: Nome da tabela no Supabase (schema: central)
1324
2017
  * - entityName: Nome legível para toasts ("Exemplo criado com sucesso")
1325
2018
  * - searchFields: Campos que serão pesquisados pelo filtro de busca
1326
2019
  * - enableQualiexEnrichment: true → adiciona responsible_name automaticamente
1327
- *
2020
+ *
1328
2021
  * 📊 Estrutura de Tabela Esperada (Supabase):
1329
2022
  * CREATE TABLE central.examples (
1330
2023
  * id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -1342,7 +2035,7 @@ import type { Example, CreateExamplePayload, UpdateExamplePayload } from './exam
1342
2035
  * date_field DATE
1343
2036
  * );
1344
2037
  */
1345
- export const { service: ExampleService, useCrudHook: useExamplesCrud } =
2038
+ export const { service: ExampleService, useCrudHook: useExamplesCrud } =
1346
2039
  createSimpleService<Example, CreateExamplePayload, UpdateExamplePayload>({
1347
2040
  tableName: 'examples', // 🗃️ Tabela no Supabase
1348
2041
  entityName: 'Exemplo', // 📣 Nome para mensagens
@@ -1353,6 +2046,7 @@ export const { service: ExampleService, useCrudHook: useExamplesCrud } =
1353
2046
  ```
1354
2047
 
1355
2048
  **📖 Explicação Detalhada:**
2049
+
1356
2050
  - **Uma linha, tudo pronto:** `createSimpleService` gera todo o boilerplate
1357
2051
  - **Soft delete automático:** `deleteEntity()` marca `is_removed = true`, não deleta fisicamente
1358
2052
  - **RLS automático:** Filtra por `alias` automaticamente
@@ -1396,9 +2090,9 @@ import { useState, useMemo } from 'react';
1396
2090
  ```typescript
1397
2091
  /**
1398
2092
  * 📝 CONFIGURAÇÃO DO FORMULÁRIO
1399
- *
2093
+ *
1400
2094
  * Organizado em seções (formSections) com campos (fields).
1401
- *
2095
+ *
1402
2096
  * Tipos de campos suportados:
1403
2097
  * - 'text' - Input de texto simples
1404
2098
  * - 'email' - Input de email com validação
@@ -1489,6 +2183,7 @@ const formSections = [{
1489
2183
  Ver código completo no arquivo `src/examples/ExamplesPage.tsx` do projeto.
1490
2184
 
1491
2185
  **Estrutura básica:**
2186
+
1492
2187
  1. Hooks no topo
1493
2188
  2. Estados de filtros com `useState`
1494
2189
  3. Estados derivados com `useMemo`
@@ -1572,6 +2267,7 @@ const filteredManager = useMemo(() => ({
1572
2267
  ```
1573
2268
 
1574
2269
  **📖 Explicação:**
2270
+
1575
2271
  - **`useState`**: Armazena valor selecionado no filtro
1576
2272
  - **`useMemo` (filteredEntities)**: Evita re-filtrar a cada render
1577
2273
  - **`useMemo` (filteredManager)**: Evita re-criar objeto manager
@@ -1598,8 +2294,8 @@ const filteredManager = useMemo(() => ({
1598
2294
  }), [manager, filteredEntities]);
1599
2295
 
1600
2296
  const DepartmentFilter = () => (
1601
- <select
1602
- value={deptFilter}
2297
+ <select
2298
+ value={deptFilter}
1603
2299
  onChange={(e) => setDeptFilter(e.target.value)}
1604
2300
  className="px-3 py-2 border rounded-md"
1605
2301
  >
@@ -1624,7 +2320,7 @@ const [dateRange, setDateRange] = useState<{ from?: Date; to?: Date }>({});
1624
2320
 
1625
2321
  const filteredEntities = useMemo(() => {
1626
2322
  if (!dateRange.from && !dateRange.to) return manager.entities;
1627
-
2323
+
1628
2324
  return manager.entities.filter(e => {
1629
2325
  const itemDate = parseISO(e.created_at);
1630
2326
  if (dateRange.from && isBefore(itemDate, dateRange.from)) return false;
@@ -1643,12 +2339,12 @@ const filteredManager = useMemo(() => ({
1643
2339
 
1644
2340
  ## 🪝 HOOKS REACT NO CRUD
1645
2341
 
1646
- | Hook | Quando Usar | Exemplo no CRUD | ⚠️ Evitar |
1647
- |------|-------------|-----------------|-----------|
1648
- | **useMemo** | Cálculos pesados que dependem de props/state | • Configuração de colunas<br>• Filtros derivados<br>• Manager customizado | Valores simples (strings, números) |
1649
- | **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 |
1650
- | **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 |
1651
- | **useEffect** | Side effects (fetch, subscriptions) | • Fetch inicial de dados (já feito pelo manager)<br>• Sincronização externa | Cálculos ou transformações de dados |
2342
+ | Hook | Quando Usar | Exemplo no CRUD | ⚠️ Evitar |
2343
+ | --------------- | ------------------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------- |
2344
+ | **useMemo** | Cálculos pesados que dependem de props/state | • Configuração de colunas<br>• Filtros derivados<br>• Manager customizado | Valores simples (strings, números) |
2345
+ | **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 |
2346
+ | **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 |
2347
+ | **useEffect** | Side effects (fetch, subscriptions) | • Fetch inicial de dados (já feito pelo manager)<br>• Sincronização externa | Cálculos ou transformações de dados |
1652
2348
 
1653
2349
  ### **📖 Exemplos Práticos**
1654
2350
 
@@ -1672,7 +2368,7 @@ const [statusFilter, setStatusFilter] = useState('active');
1672
2368
  const statusFilter = useMemo(() => 'active', []); // Não faz sentido!
1673
2369
 
1674
2370
  // ✅ CORRETO: useMemo para estado derivado
1675
- const filteredEntities = useMemo(() =>
2371
+ const filteredEntities = useMemo(() =>
1676
2372
  manager.entities.filter(e => e.is_actived),
1677
2373
  [manager.entities]
1678
2374
  );
@@ -1703,15 +2399,15 @@ import { cn } from 'forlogic-core';
1703
2399
  ```typescript
1704
2400
  // ❌ SINTOMA: Formulário fecha/reabre sozinho, re-renders infinitos
1705
2401
  // ❌ CAUSA: Config recriado a cada render
1706
- const config = {
1707
- columns: exampleColumns,
1708
- formSections
2402
+ const config = {
2403
+ columns: exampleColumns,
2404
+ formSections
1709
2405
  };
1710
2406
 
1711
2407
  // ✅ SOLUÇÃO: Envolver em useMemo
1712
- const config = useMemo(() => ({
1713
- columns: exampleColumns,
1714
- formSections
2408
+ const config = useMemo(() => ({
2409
+ columns: exampleColumns,
2410
+ formSections
1715
2411
  }), []);
1716
2412
  ```
1717
2413
 
@@ -1722,15 +2418,15 @@ const config = useMemo(() => ({
1722
2418
  ```typescript
1723
2419
  // ❌ SINTOMA: TypeError: manager.createEntity is not a function
1724
2420
  // ❌ CAUSA: Passou array direto
1725
- const CrudPage = createCrudPage({
2421
+ const CrudPage = createCrudPage({
1726
2422
  manager: manager.entities, // ← Errado!
1727
- config
2423
+ config
1728
2424
  });
1729
2425
 
1730
2426
  // ✅ SOLUÇÃO: Passar manager completo
1731
- const CrudPage = createCrudPage({
2427
+ const CrudPage = createCrudPage({
1732
2428
  manager, // ← Correto!
1733
- config
2429
+ config
1734
2430
  });
1735
2431
  ```
1736
2432
 
@@ -1744,7 +2440,7 @@ const CrudPage = createCrudPage({
1744
2440
  const filteredEntities = manager.entities.filter(e => e.is_actived);
1745
2441
 
1746
2442
  // ✅ SOLUÇÃO: Envolver em useMemo
1747
- const filteredEntities = useMemo(() =>
2443
+ const filteredEntities = useMemo(() =>
1748
2444
  manager.entities.filter(e => e.is_actived),
1749
2445
  [manager.entities]
1750
2446
  );
@@ -1757,7 +2453,7 @@ const filteredEntities = useMemo(() =>
1757
2453
  ```typescript
1758
2454
  // ❌ SINTOMA: Erro de RLS no Supabase, registro não é criado
1759
2455
  // ❌ CAUSA: Usar createEntity/updateEntity direto sem alias
1760
- manager.createEntity({
2456
+ manager.createEntity({
1761
2457
  title: data.title,
1762
2458
  email: data.email
1763
2459
  // ← Falta alias!
@@ -1820,9 +2516,9 @@ export const ExamplesPage = () => {
1820
2516
  key: 'website',
1821
2517
  header: 'Site',
1822
2518
  render: (item) => (
1823
- <a
1824
- href={item.website}
1825
- target="_blank"
2519
+ <a
2520
+ href={item.website}
2521
+ target="_blank"
1826
2522
  rel="noopener noreferrer"
1827
2523
  className="text-blue-600 hover:underline"
1828
2524
  >
@@ -1899,6 +2595,7 @@ const CrudPage = createCrudPage({
1899
2595
  ### 🔗 Integração Qualiex (opcional)
1900
2596
 
1901
2597
  **Auto-enrichment** (já configurado no BaseService):
2598
+
1902
2599
  ```typescript
1903
2600
  // ✅ Automático - dados enriquecidos com nome do usuário
1904
2601
  const processes = await processService.getAll();
@@ -1906,17 +2603,19 @@ const processes = await processService.getAll();
1906
2603
  ```
1907
2604
 
1908
2605
  **Componentes prontos:**
2606
+
1909
2607
  ```typescript
1910
2608
  import { QualiexUserField, QualiexResponsibleSelectField } from 'forlogic-core';
1911
2609
 
1912
2610
  // Select de usuários Qualiex
1913
- <QualiexResponsibleSelectField
2611
+ <QualiexResponsibleSelectField
1914
2612
  value={form.watch('id_user')}
1915
2613
  onChange={(userId) => form.setValue('id_user', userId)}
1916
2614
  />
1917
2615
  ```
1918
2616
 
1919
2617
  **Componentes em formulários CRUD:**
2618
+
1920
2619
  ```typescript
1921
2620
  // Para seleção de usuário (modo unificado)
1922
2621
  {
@@ -1945,6 +2644,7 @@ Você pode criar e usar componentes customizados nos formulários para necessida
1945
2644
  > **Nota:** Componentes customizados devem ser registrados no `BaseForm.tsx` para funcionarem corretamente nos formulários CRUD.
1946
2645
 
1947
2646
  **⚠️ CRÍTICO:** Requests Qualiex exigem header `un-alias`:
2647
+
1948
2648
  ```typescript
1949
2649
  // ✅ Já configurado no BaseService automaticamente
1950
2650
  headers: { 'un-alias': 'true' }
@@ -2006,7 +2706,7 @@ import { useAuth, placeService } from 'forlogic-core';
2006
2706
 
2007
2707
  function MyComponent() {
2008
2708
  const { alias } = useAuth();
2009
-
2709
+
2010
2710
  const { data: places = [], isLoading, error } = useQuery({
2011
2711
  queryKey: ['places', alias],
2012
2712
  queryFn: () => placeService.getPlaces(alias),
@@ -2036,7 +2736,7 @@ import { useAuth, placeService } from 'forlogic-core';
2036
2736
 
2037
2737
  export function usePlaces() {
2038
2738
  const { alias } = useAuth();
2039
-
2739
+
2040
2740
  return useQuery({
2041
2741
  queryKey: ['places', alias],
2042
2742
  queryFn: () => placeService.getPlaces(alias),
@@ -2081,7 +2781,7 @@ export function PlaceSelect({ value, onChange, disabled }: {
2081
2781
  disabled?: boolean;
2082
2782
  }) {
2083
2783
  const { data: places = [], isLoading } = usePlaces();
2084
-
2784
+
2085
2785
  // Achatar hierarquia para o select
2086
2786
  const flatPlaces = useMemo(() => {
2087
2787
  const flatten = (items: Place[], level = 0): any[] => {
@@ -2092,7 +2792,7 @@ export function PlaceSelect({ value, onChange, disabled }: {
2092
2792
  };
2093
2793
  return flatten(places);
2094
2794
  }, [places]);
2095
-
2795
+
2096
2796
  return (
2097
2797
  <EntitySelect
2098
2798
  value={value}
@@ -2136,7 +2836,7 @@ const { service, useCrudHook } = createSimpleService({
2136
2836
  // Hook para buscar nome do place
2137
2837
  function usePlaceName(placeId: string) {
2138
2838
  const { data: places = [] } = usePlaces();
2139
-
2839
+
2140
2840
  return useMemo(() => {
2141
2841
  const findPlace = (items: Place[]): Place | undefined => {
2142
2842
  for (const place of items) {
@@ -2178,6 +2878,7 @@ const placeName = userPlace?.name;
2178
2878
  ```
2179
2879
 
2180
2880
  **Fluxo de dados:**
2881
+
2181
2882
  1. Token JWT contém `alias` e `companyId`
2182
2883
  2. `placeService.getPlaces(alias)` busca os Places da API Qualiex
2183
2884
  3. Cada `Place` contém `usersIds` (array de IDs de usuários)
@@ -2210,13 +2911,13 @@ function PlaceTree({ places, level = 0 }: {
2210
2911
 
2211
2912
  ### 🛠️ Troubleshooting
2212
2913
 
2213
- | Erro | Causa | Solução |
2214
- |------|-------|---------|
2215
- | `CompanyId não encontrado no token` | Token não validado corretamente | Verificar edge function `validate-token` |
2216
- | `Alias da unidade é obrigatório` | `alias` não disponível no `useAuth()` | Aguardar carregamento do auth com `isLoading` |
2217
- | `Token Qualiex não encontrado` | Variável de ambiente faltando | Verificar `VITE_QUALIEX_API_URL` no `.env` |
2218
- | Places retorna array vazio `[]` | Empresa sem places cadastrados | Verificar cadastro no Qualiex admin |
2219
- | Hierarquia quebrada | `parentId` incorreto nos dados | Verificar integridade dos dados na API Qualiex |
2914
+ | Erro | Causa | Solução |
2915
+ | ----------------------------------- | ------------------------------------- | ---------------------------------------------- |
2916
+ | `CompanyId não encontrado no token` | Token não validado corretamente | Verificar edge function `validate-token` |
2917
+ | `Alias da unidade é obrigatório` | `alias` não disponível no `useAuth()` | Aguardar carregamento do auth com `isLoading` |
2918
+ | `Token Qualiex não encontrado` | Variável de ambiente faltando | Verificar `VITE_QUALIEX_API_URL` no `.env` |
2919
+ | Places retorna array vazio `[]` | Empresa sem places cadastrados | Verificar cadastro no Qualiex admin |
2920
+ | Hierarquia quebrada | `parentId` incorreto nos dados | Verificar integridade dos dados na API Qualiex |
2220
2921
 
2221
2922
  ### 📦 Exemplo Completo: Dashboard por Local
2222
2923
 
@@ -2237,7 +2938,7 @@ function PlacesDashboard() {
2237
2938
  <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
2238
2939
  {places.map(place => {
2239
2940
  const placeMetrics = metrics.filter(m => m.place_id === place.placeId);
2240
-
2941
+
2241
2942
  return (
2242
2943
  <Card key={place.id}>
2243
2944
  <CardHeader>
@@ -2288,6 +2989,7 @@ function PlacesDashboard() {
2288
2989
  ## 🗃️ MIGRATIONS + RLS
2289
2990
 
2290
2991
  ### Template SQL Completo
2992
+
2291
2993
  ```sql
2292
2994
  -- 1️⃣ Criar tabela
2293
2995
  CREATE TABLE central.processes (
@@ -2335,10 +3037,11 @@ EXECUTE FUNCTION public.set_updated_at();
2335
3037
  ```
2336
3038
 
2337
3039
  ### ❌ Sintaxes Proibidas RLS
3040
+
2338
3041
  ```sql
2339
3042
  -- ❌ ERRADO - Sintaxes simplificadas que não funcionam
2340
3043
  id_user = auth.uid() -- ❌ Campo errado
2341
- id = auth.uid() -- ❌ Campo errado
3044
+ id = auth.uid() -- ❌ Campo errado
2342
3045
  alias = auth.uid() -- ❌ Função errada
2343
3046
 
2344
3047
  -- ✅ CORRETO - Extração completa do JWT
@@ -2356,6 +3059,7 @@ alias = auth.uid() -- ❌ Função errada
2356
3059
  5. **`= alias`**: Compara com a coluna `alias` da tabela
2357
3060
 
2358
3061
  **Estrutura do JWT:**
3062
+
2359
3063
  ```json
2360
3064
  {
2361
3065
  "sub": "user-uuid",
@@ -2366,11 +3070,13 @@ alias = auth.uid() -- ❌ Função errada
2366
3070
  ```
2367
3071
 
2368
3072
  **Fluxo de Autenticação Multi-tenant:**
3073
+
2369
3074
  ```
2370
3075
  Login → Validação Externa → JWT com alias → RLS Policy → Filtragem por empresa
2371
3076
  ```
2372
3077
 
2373
3078
  **⚠️ Erros Comuns:**
3079
+
2374
3080
  - `alias = auth.uid()` → Compara alias com UUID (tipos incompatíveis)
2375
3081
  - `id_user = auth.uid()` → Compara com usuário, não com empresa
2376
3082
  - `auth.jwt().alias` → Sintaxe JavaScript, não SQL
@@ -2380,6 +3086,7 @@ Login → Validação Externa → JWT com alias → RLS Policy → Filtragem por
2380
3086
  ## 🐛 TROUBLESHOOTING
2381
3087
 
2382
3088
  ### 1️⃣ "relation does not exist"
3089
+
2383
3090
  ```typescript
2384
3091
  // Causa: Schema ausente
2385
3092
  .from('processes') // ❌
@@ -2389,6 +3096,7 @@ Login → Validação Externa → JWT com alias → RLS Policy → Filtragem por
2389
3096
  ```
2390
3097
 
2391
3098
  ### 2️⃣ RLS retorna vazio
3099
+
2392
3100
  ```sql
2393
3101
  -- Causa: Sintaxe incorreta
2394
3102
  USING (id_user = auth.uid()) -- ❌ ERRADO
@@ -2401,6 +3109,7 @@ USING (
2401
3109
  ```
2402
3110
 
2403
3111
  ### 3️⃣ Duplicação de registros
3112
+
2404
3113
  ```typescript
2405
3114
  // Causa: ID ausente no update
2406
3115
  await service.save({ title: 'Novo' }); // ❌ Cria duplicado
@@ -2410,6 +3119,7 @@ await service.save({ id: item.id, title: 'Novo' }); // ✅
2410
3119
  ```
2411
3120
 
2412
3121
  ### 4️⃣ Página recarrega ao editar
3122
+
2413
3123
  ```typescript
2414
3124
  // Causa: Config sem useMemo
2415
3125
  const config = generateCrudConfig(...); // ❌ Re-render infinito
@@ -2419,6 +3129,7 @@ const config = useMemo(() => generateCrudConfig(...), []); // ✅
2419
3129
  ```
2420
3130
 
2421
3131
  ### 5️⃣ Estado reseta ao navegar
3132
+
2422
3133
  ```typescript
2423
3134
  // Causa: Outlet ausente
2424
3135
  <Route path="/processes" element={<ProcessesPage />} /> // ❌
@@ -2431,6 +3142,7 @@ const config = useMemo(() => generateCrudConfig(...), []); // ✅
2431
3142
  ```
2432
3143
 
2433
3144
  ### 6️⃣ Qualiex retorna 401
3145
+
2434
3146
  ```typescript
2435
3147
  // Causa: Header ausente
2436
3148
  fetch(url); // ❌
@@ -2447,6 +3159,7 @@ fetch(url, { headers: { 'un-alias': 'true' } }); // ✅
2447
3159
  ### **Por que Filtros no Backend?**
2448
3160
 
2449
3161
  Filtrar dados no **backend** é a abordagem recomendada porque:
3162
+
2450
3163
  - ✅ **Paginação correta**: Total de itens reflete os dados filtrados
2451
3164
  - ✅ **Performance**: Menos dados trafegados pela rede
2452
3165
  - ✅ **Escalabilidade**: Funciona com milhares de registros
@@ -2463,9 +3176,9 @@ Para filtros simples e estáticos, use `additionalFilters` no service:
2463
3176
  import { createSimpleService } from 'forlogic-core';
2464
3177
  import { Subprocess, SubprocessInsert, SubprocessUpdate } from './types';
2465
3178
 
2466
- export const {
2467
- service: subprocessService,
2468
- useCrudHook: baseUseCrudHook
3179
+ export const {
3180
+ service: subprocessService,
3181
+ useCrudHook: baseUseCrudHook
2469
3182
  } = createSimpleService<Subprocess, SubprocessInsert, SubprocessUpdate>({
2470
3183
  tableName: 'subprocesses',
2471
3184
  entityName: 'subprocesso',
@@ -2489,9 +3202,9 @@ import { useMemo } from 'react';
2489
3202
  import { createSimpleService } from 'forlogic-core';
2490
3203
 
2491
3204
  // Service base
2492
- export const {
2493
- service: subprocessService,
2494
- useCrudHook: baseUseCrudHook
3205
+ export const {
3206
+ service: subprocessService,
3207
+ useCrudHook: baseUseCrudHook
2495
3208
  } = createSimpleService<Subprocess, SubprocessInsert, SubprocessUpdate>({
2496
3209
  tableName: 'subprocesses',
2497
3210
  entityName: 'subprocesso',
@@ -2507,7 +3220,7 @@ export function useSubprocessesCrud(filters?: {
2507
3220
  // Transforma filtros em additionalFilters
2508
3221
  const additionalFilters = useMemo(() => {
2509
3222
  const filterList: any[] = [];
2510
-
3223
+
2511
3224
  // Filtro de status (ativo/inativo)
2512
3225
  if (filters?.is_actived !== undefined) {
2513
3226
  filterList.push({
@@ -2516,7 +3229,7 @@ export function useSubprocessesCrud(filters?: {
2516
3229
  value: filters.is_actived
2517
3230
  });
2518
3231
  }
2519
-
3232
+
2520
3233
  // Filtro de processo (incluindo "sem processo")
2521
3234
  if (filters?.id_process) {
2522
3235
  if (filters.id_process === 'none') {
@@ -2533,10 +3246,10 @@ export function useSubprocessesCrud(filters?: {
2533
3246
  });
2534
3247
  }
2535
3248
  }
2536
-
3249
+
2537
3250
  return filterList;
2538
3251
  }, [filters?.is_actived, filters?.id_process]);
2539
-
3252
+
2540
3253
  // Chama hook base com filtros dinâmicos
2541
3254
  return baseUseCrudHook({ additionalFilters });
2542
3255
  }
@@ -2551,16 +3264,16 @@ import { useSubprocessesCrud } from './SubprocessService';
2551
3264
 
2552
3265
  export default function SubprocessesPage() {
2553
3266
  const [searchParams, setSearchParams] = useSearchParams();
2554
-
3267
+
2555
3268
  // Ler filtros da URL
2556
3269
  const filters = useMemo(() => ({
2557
3270
  is_actived: searchParams.get('is_actived') === 'true',
2558
3271
  id_process: searchParams.get('id_process') || undefined
2559
3272
  }), [searchParams]);
2560
-
3273
+
2561
3274
  // Manager com filtros aplicados no backend
2562
3275
  const manager = useSubprocessesCrud(filters);
2563
-
3276
+
2564
3277
  // Config com filtros de UI
2565
3278
  const config = useMemo(() => generateCrudConfig<Subprocess>({
2566
3279
  entityName: 'Subprocesso',
@@ -2599,7 +3312,7 @@ export default function SubprocessesPage() {
2599
3312
  ],
2600
3313
  columns: [...]
2601
3314
  }), [searchParams]);
2602
-
3315
+
2603
3316
  return <CrudPage />;
2604
3317
  }
2605
3318
  ```
@@ -2608,17 +3321,17 @@ export default function SubprocessesPage() {
2608
3321
 
2609
3322
  O `BaseService` suporta os seguintes operadores em `additionalFilters`:
2610
3323
 
2611
- | Operador | Descrição | Exemplo |
2612
- |----------|-----------|---------|
2613
- | `eq` | Igual a | `{ field: 'status', operator: 'eq', value: 'active' }` |
2614
- | `neq` | Diferente de | `{ field: 'status', operator: 'neq', value: 'archived' }` |
2615
- | `gt` | Maior que | `{ field: 'price', operator: 'gt', value: 100 }` |
2616
- | `gte` | Maior ou igual | `{ field: 'price', operator: 'gte', value: 100 }` |
2617
- | `lt` | Menor que | `{ field: 'stock', operator: 'lt', value: 10 }` |
2618
- | `lte` | Menor ou igual | `{ field: 'stock', operator: 'lte', value: 10 }` |
2619
- | `in` | Em lista | `{ field: 'category', operator: 'in', value: ['A', 'B'] }` |
2620
- | `contains` | Contém texto | `{ field: 'name', operator: 'contains', value: 'test' }` |
2621
- | `is_null` | É nulo | `{ field: 'deleted_at', operator: 'is_null' }` |
3324
+ | Operador | Descrição | Exemplo |
3325
+ | ---------- | -------------- | ---------------------------------------------------------- |
3326
+ | `eq` | Igual a | `{ field: 'status', operator: 'eq', value: 'active' }` |
3327
+ | `neq` | Diferente de | `{ field: 'status', operator: 'neq', value: 'archived' }` |
3328
+ | `gt` | Maior que | `{ field: 'price', operator: 'gt', value: 100 }` |
3329
+ | `gte` | Maior ou igual | `{ field: 'price', operator: 'gte', value: 100 }` |
3330
+ | `lt` | Menor que | `{ field: 'stock', operator: 'lt', value: 10 }` |
3331
+ | `lte` | Menor ou igual | `{ field: 'stock', operator: 'lte', value: 10 }` |
3332
+ | `in` | Em lista | `{ field: 'category', operator: 'in', value: ['A', 'B'] }` |
3333
+ | `contains` | Contém texto | `{ field: 'name', operator: 'contains', value: 'test' }` |
3334
+ | `is_null` | É nulo | `{ field: 'deleted_at', operator: 'is_null' }` |
2622
3335
 
2623
3336
  ### **Exemplo Completo: Filtro de Status e Processo**
2624
3337
 
@@ -2630,7 +3343,7 @@ export function useSubprocessesCrud(filters?: {
2630
3343
  }) {
2631
3344
  const additionalFilters = useMemo(() => {
2632
3345
  const filterList: any[] = [];
2633
-
3346
+
2634
3347
  if (filters?.is_actived !== undefined) {
2635
3348
  filterList.push({
2636
3349
  field: 'is_actived',
@@ -2638,7 +3351,7 @@ export function useSubprocessesCrud(filters?: {
2638
3351
  value: filters.is_actived
2639
3352
  });
2640
3353
  }
2641
-
3354
+
2642
3355
  if (filters?.id_process) {
2643
3356
  if (filters.id_process === 'none') {
2644
3357
  filterList.push({
@@ -2653,10 +3366,10 @@ export function useSubprocessesCrud(filters?: {
2653
3366
  });
2654
3367
  }
2655
3368
  }
2656
-
3369
+
2657
3370
  return filterList;
2658
3371
  }, [filters?.is_actived, filters?.id_process]);
2659
-
3372
+
2660
3373
  return baseUseCrudHook({ additionalFilters });
2661
3374
  }
2662
3375
 
@@ -2675,10 +3388,11 @@ const manager = useSubprocessesCrud(filters);
2675
3388
  ### **⚠️ Filtros Frontend vs Backend**
2676
3389
 
2677
3390
  **❌ Filtros no Frontend (EVITAR):**
3391
+
2678
3392
  ```typescript
2679
3393
  // Problema: Paginação incorreta
2680
- const filteredEntities = useMemo(() =>
2681
- manager.entities.filter(e => e.is_actived),
3394
+ const filteredEntities = useMemo(() =>
3395
+ manager.entities.filter(e => e.is_actived),
2682
3396
  [manager.entities]
2683
3397
  );
2684
3398
 
@@ -2688,10 +3402,11 @@ const filteredEntities = useMemo(() =>
2688
3402
  ```
2689
3403
 
2690
3404
  **✅ Filtros no Backend (CORRETO):**
3405
+
2691
3406
  ```typescript
2692
3407
  // Backend retorna apenas dados filtrados
2693
- const manager = useSubprocessesCrud({
2694
- is_actived: true
3408
+ const manager = useSubprocessesCrud({
3409
+ is_actived: true
2695
3410
  });
2696
3411
 
2697
3412
  // manager.totalCount = 43 (correto!)
@@ -2706,7 +3421,9 @@ const manager = useSubprocessesCrud({
2706
3421
  O `forlogic-core` oferece três formas de definir larguras de colunas nas tabelas CRUD:
2707
3422
 
2708
3423
  ### **1️⃣ Via `className` (Recomendado)**
3424
+
2709
3425
  Use classes do Tailwind para larguras fixas ou responsivas:
3426
+
2710
3427
  ```typescript
2711
3428
  const columns = [
2712
3429
  {
@@ -2723,7 +3440,9 @@ const columns = [
2723
3440
  ```
2724
3441
 
2725
3442
  ### **2️⃣ Via `width` (Fixo em pixels)**
3443
+
2726
3444
  Especifique largura fixa diretamente:
3445
+
2727
3446
  ```typescript
2728
3447
  {
2729
3448
  key: 'order',
@@ -2734,7 +3453,9 @@ Especifique largura fixa diretamente:
2734
3453
  ```
2735
3454
 
2736
3455
  ### **3️⃣ Via `minWidth` + `weight` (Flexível)**
3456
+
2737
3457
  Para colunas que crescem proporcionalmente:
3458
+
2738
3459
  ```typescript
2739
3460
  {
2740
3461
  key: 'description',
@@ -2745,11 +3466,13 @@ Para colunas que crescem proporcionalmente:
2745
3466
  ```
2746
3467
 
2747
3468
  ### **⚠️ Importante**
3469
+
2748
3470
  - A tabela usa `table-auto` para respeitar essas configurações
2749
3471
  - Para truncar textos longos, use: `className: "max-w-[200px] truncate"`
2750
3472
  - Combine `whitespace-nowrap` com largura fixa para evitar quebras
2751
3473
 
2752
3474
  ### **📋 Exemplo Completo**
3475
+
2753
3476
  ```typescript
2754
3477
  const columns: CrudColumn<MyEntity>[] = [
2755
3478
  {
@@ -2788,13 +3511,14 @@ const columns: CrudColumn<MyEntity>[] = [
2788
3511
  ## 📚 REFERÊNCIA RÁPIDA
2789
3512
 
2790
3513
  ### Imports Essenciais
3514
+
2791
3515
  ```typescript
2792
3516
  // CRUD
2793
- import {
2794
- createSimpleService,
2795
- createCrudPage,
3517
+ import {
3518
+ createSimpleService,
3519
+ createCrudPage,
2796
3520
  generateCrudConfig,
2797
- createSimpleSaveHandler
3521
+ createSimpleSaveHandler
2798
3522
  } from 'forlogic-core';
2799
3523
 
2800
3524
  // UI
@@ -2808,6 +3532,7 @@ import { QualiexUserField, useQualiexUsers } from 'forlogic-core';
2808
3532
  ```
2809
3533
 
2810
3534
  ### Estrutura de Arquivos
3535
+
2811
3536
  ```
2812
3537
  src/
2813
3538
  ├── processes/
@@ -2820,4 +3545,4 @@ src/
2820
3545
 
2821
3546
  ## 📝 Licença
2822
3547
 
2823
- MIT License - ForLogic © 2025
3548
+ MIT License - ForLogic © 2025