forlogic-core 1.7.1 → 1.7.3

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
@@ -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,718 @@ 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
+ ### 🔗 Enrichment de Múltiplos Campos de Usuário do Qualiex
877
+
878
+ O `forlogic-core` suporta enriquecimento automático de **múltiplos campos de ID de usuário** com dados do Qualiex (nome, email, username).
879
+
880
+ #### **Configuração Global**
881
+
882
+ Configure o enrichment uma vez na inicialização do app:
883
+
884
+ ```typescript
885
+ import { setQualiexConfig } from 'forlogic-core';
886
+
887
+ // Configuração básica (usa localStorage como fallback)
888
+ setQualiexConfig({
889
+ enableUserEnrichment: true,
890
+ enableUsersApi: true,
891
+ });
892
+
893
+ // Configuração avançada (recomendado)
894
+ setQualiexConfig({
895
+ enableUserEnrichment: true,
896
+ enableUsersApi: true,
897
+ getAccessToken: () => localStorage.getItem('qualiex_access_token'),
898
+ getCompanyId: () => localStorage.getItem('selectedUnitId'),
899
+ userNameFieldSuffix: '_name', // default
900
+ userEmailFieldSuffix: '_email', // default
901
+ userUsernameFieldSuffix: '_username', // default
902
+ });
903
+ ```
904
+
905
+ #### **Uso Básico: Lista de Campos de ID**
906
+
907
+ Use `userIdFields` para enriquecer múltiplos campos de ID automaticamente:
908
+
909
+ ```typescript
910
+ import { createSimpleService } from 'forlogic-core';
911
+
912
+ const { service, useCrudHook } = createSimpleService({
913
+ tableName: 'evaluations',
914
+ schemaName: 'performance',
915
+ entityName: 'Avaliação',
916
+ searchFields: ['title', 'description'],
917
+ enableQualiexEnrichment: true,
918
+
919
+ // ✅ Lista de campos de ID para enriquecer
920
+ userIdFields: ['target_user_id', 'evaluator_user_id'],
921
+ });
922
+ ```
923
+
924
+ **Resultado automático:**
925
+ - `target_user_id` → `target_user_name` (nome do usuário)
926
+ - `evaluator_user_id` → `evaluator_user_name` (nome do usuário)
927
+
928
+ #### **Uso Avançado: Mapeamento Customizado**
929
+
930
+ Use `userFieldsMapping` para controlar exatamente quais campos de saída criar:
931
+
932
+ ```typescript
933
+ const { service, useCrudHook } = createSimpleService({
934
+ tableName: 'evaluations',
935
+ entityName: 'Avaliação',
936
+ enableQualiexEnrichment: true,
937
+
938
+ // ✅ Mapeamento granular de campos
939
+ userFieldsMapping: [
940
+ {
941
+ idField: 'target_user_id',
942
+ nameField: 'avaliado_nome', // customizado
943
+ emailField: 'avaliado_email', // opcional
944
+ },
945
+ {
946
+ idField: 'evaluator_user_id',
947
+ nameField: 'avaliador_nome',
948
+ emailField: 'avaliador_email',
949
+ usernameField: 'avaliador_username', // opcional
950
+ },
951
+ ],
952
+ });
953
+ ```
954
+
955
+ **Resultado customizado:**
956
+ - `target_user_id` → `avaliado_nome`, `avaliado_email`
957
+ - `evaluator_user_id` → `avaliador_nome`, `avaliador_email`, `avaliador_username`
958
+
959
+ #### **Enrichment Manual (Casos Avançados)**
960
+
961
+ Para casos onde você precisa enriquecer dados fora do `createSimpleService`:
962
+
963
+ ```typescript
964
+ import { QualiexEnrichmentService } from 'forlogic-core';
965
+
966
+ // Enriquecer entidades manualmente
967
+ const enrichedData = await QualiexEnrichmentService.enrichWithUserData(entities, {
968
+ entityName: 'Avaliações',
969
+ userIdFields: ['target_user_id', 'evaluator_user_id'],
970
+ });
971
+
972
+ // Ou com mapeamento customizado
973
+ const enrichedData = await QualiexEnrichmentService.enrichWithUserData(entities, {
974
+ entityName: 'Avaliações',
975
+ userFieldsMapping: [
976
+ { idField: 'target_user_id', nameField: 'avaliado_nome' },
977
+ { idField: 'evaluator_user_id', nameField: 'avaliador_nome' },
978
+ ],
979
+ });
980
+ ```
981
+
982
+ #### **Utilitários de Derivação de Campos**
983
+
984
+ Use helpers para gerar nomes de campos de forma consistente:
985
+
986
+ ```typescript
987
+ import { deriveNameField, deriveEmailField } from 'forlogic-core';
988
+
989
+ deriveNameField('target_user_id'); // => "target_user_name"
990
+ deriveEmailField('evaluator_user_id'); // => "evaluator_user_email"
991
+ ```
992
+
993
+ #### **Retrocompatibilidade**
994
+
995
+ O comportamento legado (`id_user` → `responsible_name`) continua funcionando sem mudanças:
996
+
997
+ ```typescript
998
+ // Código antigo funciona normalmente
999
+ const { service } = createSimpleService({
1000
+ tableName: 'processes',
1001
+ entityName: 'Processo',
1002
+ enableQualiexEnrichment: true,
1003
+ // Automaticamente enriquece id_user -> responsible_name
1004
+ });
1005
+ ```
1006
+
1007
+ #### **Características**
1008
+
1009
+ ✅ **Cache inteligente**: Usuários Qualiex são cacheados por 5 minutos
1010
+ ✅ **Resiliência**: Falhas não quebram a UI (apenas logs no console)
1011
+ ✅ **Deduplicação**: IDs duplicados são buscados apenas uma vez
1012
+ ✅ **Não sobrescreve**: Campos já preenchidos não são alterados
1013
+ ✅ **Type-safe**: TypeScript valida configurações
1014
+ ✅ **Retrocompatível**: Projetos antigos continuam funcionando
1015
+
1016
+ #### **Boas Práticas**
1017
+
1018
+ ✅ **BOM**: Configurar `getAccessToken` e `getCompanyId` para evitar dependência de localStorage
1019
+
1020
+ ```typescript
1021
+ setQualiexConfig({
1022
+ enableUserEnrichment: true,
1023
+ getAccessToken: async () => {
1024
+ // Buscar token de forma segura (ex: context, Zustand, etc)
1025
+ return myAuthStore.getToken();
1026
+ },
1027
+ getCompanyId: () => myAppStore.getSelectedCompanyId(),
1028
+ });
1029
+ ```
1030
+
1031
+ ❌ **EVITAR**: Múltiplos campos apontando para o mesmo campo de saída
1032
+
1033
+ ```typescript
1034
+ // ❌ Conflito: ambos escrevem em "user_name"
1035
+ userFieldsMapping: [
1036
+ { idField: 'created_by_id', nameField: 'user_name' },
1037
+ { idField: 'updated_by_id', nameField: 'user_name' }, // Conflito!
1038
+ ]
1039
+ ```
1040
+
1041
+ ✅ **BOM**: Usar sufixos distintos
1042
+
1043
+ ```typescript
1044
+ // ✅ Campos de saída únicos
1045
+ userFieldsMapping: [
1046
+ { idField: 'created_by_id', nameField: 'created_by_name' },
1047
+ { idField: 'updated_by_id', nameField: 'updated_by_name' },
1048
+ ]
1049
+ ```
1050
+
1051
+ ---
1052
+
1053
+ ### 🏢 Sistema de Gestores de Locais (Qualiex)
1054
+
1055
+ Componentes para gerenciamento de gestores e membros de locais/sublocais integrados com a API Qualiex.
1056
+
1057
+ #### Importação
1058
+
1059
+ ```typescript
1060
+ import {
1061
+ PlaceManagerButton,
1062
+ PlaceManagerBadge,
1063
+ ManagerSelectionDialog,
1064
+ usePlaceManagers,
1065
+ PlaceManagerService,
1066
+ type PlaceManager
1067
+ } from 'forlogic-core';
1068
+ ```
1069
+
1070
+ #### Componentes
1071
+
1072
+ **PlaceManagerButton** - Botão dropdown com ações de gerenciamento:
1073
+
1074
+ ```tsx
1075
+ <PlaceManagerButton
1076
+ placeId="abc-123"
1077
+ placeName="Matriz São Paulo"
1078
+ serviceConfig={{ tableName: 'place_managers' }}
1079
+ />
1080
+ ```
1081
+
1082
+ **PlaceManagerBadge** - Badge visual com contador de gestores:
1083
+
1084
+ ```tsx
1085
+ <PlaceManagerBadge
1086
+ placeId="abc-123"
1087
+ userCount={15}
1088
+ />
1089
+ ```
1090
+
1091
+ **ManagerSelectionDialog** - Diálogo completo de seleção:
1092
+
1093
+ ```tsx
1094
+ <ManagerSelectionDialog
1095
+ open={showDialog}
1096
+ onOpenChange={setShowDialog}
1097
+ onSelectManager={(user) => console.log('Gestor:', user)}
1098
+ onSelectMember={(user) => console.log('Membro:', user)}
1099
+ currentManagerId={manager?.user_id}
1100
+ currentMemberIds={members.map(m => m.user_id)}
1101
+ placeName="Matriz São Paulo"
1102
+ />
1103
+ ```
1104
+
1105
+ #### Hook: usePlaceManagers
1106
+
1107
+ ```typescript
1108
+ const {
1109
+ managers, // Todos (gestor + membros)
1110
+ manager, // Apenas o gestor
1111
+ members, // Apenas membros
1112
+ setManager, // (user: QualiexUser) => void
1113
+ addMember, // (user: QualiexUser) => void
1114
+ remove, // (userId: string) => void
1115
+ isLoading
1116
+ } = usePlaceManagers(placeId);
1117
+ ```
1118
+
1119
+ #### Service: PlaceManagerService
1120
+
1121
+ ```typescript
1122
+ import { PlaceManagerService } from 'forlogic-core';
1123
+
1124
+ // Instância customizada
1125
+ const service = new PlaceManagerService({
1126
+ tableName: 'my_place_managers',
1127
+ schemaName: 'custom_schema'
1128
+ });
1129
+
1130
+ // Métodos disponíveis
1131
+ await service.getPlaceManagers(alias, placeId);
1132
+ await service.setManager(alias, placeId, user);
1133
+ await service.addMember(alias, placeId, user);
1134
+ await service.removePlaceUser(alias, placeId, userId);
1135
+ ```
1136
+
1137
+ > ⚠️ A configuração do banco de dados (tabelas, RLS policies) deve ser feita no projeto consumidor.
1138
+
1139
+ #### Regras de Negócio
1140
+
1141
+ 1. **Um gestor por local** - Ao definir novo gestor, o anterior é removido automaticamente
1142
+ 2. **Membros ilimitados** - Múltiplos membros podem ser adicionados
1143
+ 3. **Ordenação alfabética** - Sempre ordenado por nome
1144
+ 4. **Busca inteligente** - Filtra por nome ou email
1145
+
1146
+ #### Exemplo Completo
1147
+
1148
+ ```tsx
1149
+ function PlaceTreeItem({ place }) {
1150
+ return (
1151
+ <div className="flex items-center gap-3">
1152
+ <span>{place.name}</span>
1153
+
1154
+ <PlaceManagerBadge
1155
+ placeId={place.id}
1156
+ userCount={place.usersIds.length}
1157
+ />
1158
+
1159
+ <PlaceManagerButton
1160
+ placeId={place.id}
1161
+ placeName={place.name}
1162
+ />
1163
+ </div>
1164
+ );
1165
+ }
1166
+ ```
1167
+
1168
+ ---
1169
+
444
1170
  ### ✅ CHECKLIST (antes de implementar)
445
1171
 
446
1172
  - [ ] Schema `schema` especificado em queries e service?
447
1173
  - [ ] RLS usando extração JWT `((SELECT auth.jwt()) ->> 'alias'::text) = alias`?
448
1174
  - [ ] **Nomenclatura correta**: `id_<entity>`, `is_<bool>`, `<action>_at`?
449
1175
  - [ ] **Migration SEM índices** (ou aprovados pelo usuário)?
450
- - [ ] **Imports do forlogic-core** (não criar utils.ts ou ui/* localmente)?
1176
+ - [ ] **Imports do forlogic-core** (não criar utils.ts ou ui/\* localmente)?
451
1177
  - [ ] Preservar `item.id` no update?
452
1178
  - [ ] Config gerado com `useMemo()`?
453
1179
  - [ ] `<Outlet />` no componente pai?
@@ -468,10 +1194,10 @@ graph TD
468
1194
  E --> F[CrudTable<br/>Tabela]
469
1195
  E --> G[BaseForm<br/>Formulário]
470
1196
  E --> H[BulkActionBar<br/>Ações em Massa]
471
-
1197
+
472
1198
  I[(Supabase DB)] -.->|RLS + Soft Delete| B
473
1199
  J[Qualiex API] -.->|responsible_name| B
474
-
1200
+
475
1201
  style A fill:#e1f5ff
476
1202
  style B fill:#fff4e1
477
1203
  style C fill:#ffe1f5
@@ -589,7 +1315,7 @@ import { usePageMetadataContext } from 'forlogic-core';
589
1315
 
590
1316
  export default function MyPage() {
591
1317
  const { setMetadata } = usePageMetadataContext();
592
-
1318
+
593
1319
  useEffect(() => {
594
1320
  setMetadata({
595
1321
  title: 'Meus Itens',
@@ -610,11 +1336,13 @@ export const SEARCH_CONFIG = {
610
1336
  ```
611
1337
 
612
1338
  **Quando aumentar o delay:**
1339
+
613
1340
  - ✅ Muitos usuários simultâneos (reduz carga no servidor)
614
1341
  - ✅ Campos de busca muito amplos (muitos registros)
615
1342
  - ✅ Backend com rate limiting
616
1343
 
617
1344
  **Quando reduzir o delay:**
1345
+
618
1346
  - ✅ Poucos registros (resposta instantânea)
619
1347
  - ✅ Busca crítica para UX (feedback imediato)
620
1348
 
@@ -664,16 +1392,128 @@ graph LR
664
1392
  D --> E[useCrud lê URL]
665
1393
  E --> F[Supabase Query]
666
1394
  F --> G[Resultados filtrados]
667
-
1395
+
668
1396
  style C fill:#d4f4dd
669
1397
  style F fill:#ffd4d4
670
1398
  ```
671
1399
 
672
1400
  ---
673
1401
 
1402
+ ## 🎯 ARQUITETURA DE 3 NÍVEIS - CRUD COMPONENTS
1403
+
1404
+ O sistema CRUD oferece **3 níveis de abstração**:
1405
+
1406
+ ### **📊 Quando Usar Cada Nível?**
1407
+
1408
+ | Cenário | Nível | Componentes |
1409
+ | -------------------------- | ------------------ | ------------------------- |
1410
+ | CRUD padrão | 🟩 Página Completa | `createCrudPage()` |
1411
+ | CRUD customizado | 🔶 Compostos | `CrudTable` + `FilterBar` |
1412
+ | Dashboard/Tabelas não-CRUD | 🔷 Primitivos | `TablePrimitive` |
1413
+ | Kanban/Grid customizado | 🔷 Primitivos | `CardsPrimitive` |
1414
+
1415
+ ### **🔷 NÍVEL 1 - Primitivos (Controle Total)**
1416
+
1417
+ Componentes puros **sem lógica CRUD**:
1418
+
1419
+ ```typescript
1420
+ import {
1421
+ TablePrimitive, // Tabela reutilizável
1422
+ ActionMenuPrimitive, // Menu de ações
1423
+ PaginationPrimitive, // Paginação manual
1424
+ FilterBarPrimitive, // Barra de filtros
1425
+ CardsPrimitive // Grid de cards
1426
+ } from 'forlogic-core';
1427
+ ```
1428
+
1429
+ **Exemplo:**
1430
+
1431
+ ```typescript
1432
+ <TablePrimitive
1433
+ data={reports}
1434
+ columns={[
1435
+ { key: 'date', header: 'Data', sortable: true },
1436
+ { key: 'revenue', header: 'Receita' }
1437
+ ]}
1438
+ onSort={handleSort}
1439
+ renderActions={(item) => (
1440
+ <ActionMenuPrimitive customActions={[...]} />
1441
+ )}
1442
+ />
1443
+ ```
1444
+
1445
+ ### **🔶 NÍVEL 2 - Compostos (CRUD Integrado)**
1446
+
1447
+ Primitivos + lógica CRUD:
1448
+
1449
+ ```typescript
1450
+ import {
1451
+ CrudTable, // Tabela com CRUD
1452
+ CrudCards, // Cards com CRUD
1453
+ CrudPagination, // Paginação integrada
1454
+ FilterBar, // Filtros integrados
1455
+ BulkActionBar // Ações em massa
1456
+ } from 'forlogic-core';
1457
+ ```
1458
+
1459
+ **Exemplo:**
1460
+
1461
+ ```typescript
1462
+ <FilterBar
1463
+ searchValue={manager.searchTerm}
1464
+ onSearchChange={manager.handleSearch}
1465
+ customFilters={[<StatusSelect />]}
1466
+ />
1467
+
1468
+ <CrudTable
1469
+ manager={manager}
1470
+ columns={customColumns}
1471
+ renderActions={(item) => <CustomActions item={item} />}
1472
+ />
1473
+
1474
+ <CrudPagination manager={manager} />
1475
+ ```
1476
+
1477
+ ### **🟩 NÍVEL 3 - Página Completa (Geração Automática)**
1478
+
1479
+ API de alto nível:
1480
+
1481
+ ```typescript
1482
+ import {
1483
+ createCrudPage, // Gera página completa
1484
+ generateCrudConfig, // Config automática
1485
+ createSimpleService // Service + Hook
1486
+ } from 'forlogic-core';
1487
+ ```
1488
+
1489
+ **Exemplo:**
1490
+
1491
+ ```typescript
1492
+ const config = useMemo(() =>
1493
+ generateCrudConfig<Product>({
1494
+ entity: 'produto',
1495
+ columns: [
1496
+ { key: 'name', label: 'Nome', required: true },
1497
+ { key: 'price', label: 'Preço', type: 'number' }
1498
+ ]
1499
+ }),
1500
+ []);
1501
+
1502
+ return createCrudPage({ config, crud: manager, saveHandler: manager.save });
1503
+ ```
1504
+
1505
+ ### **🧪 Página de Demonstração**
1506
+
1507
+ Acesse **`/demo-crud`** para ver todos os componentes em ação com exemplos práticos.
1508
+
1509
+ ---
1510
+
1511
+ ---
1512
+
674
1513
  ## 🚀 QUICK START - Criar CRUD Completo
675
1514
 
676
1515
  ### **1️⃣ Type**
1516
+
677
1517
  ```typescript
678
1518
  // src/processes/process.ts
679
1519
  export interface Process {
@@ -692,12 +1532,13 @@ export type ProcessUpdate = Partial<ProcessInsert>;
692
1532
  ```
693
1533
 
694
1534
  ### **2️⃣ Service**
1535
+
695
1536
  ```typescript
696
1537
  // src/processes/processService.ts
697
1538
  import { createSimpleService } from 'forlogic-core';
698
1539
  import { Process, ProcessInsert, ProcessUpdate } from './process';
699
1540
 
700
- export const { service: processService, useCrudHook: useProcesses } =
1541
+ export const { service: processService, useCrudHook: useProcesses } =
701
1542
  createSimpleService<Process, ProcessInsert, ProcessUpdate>({
702
1543
  tableName: 'processes',
703
1544
  entityName: 'processo',
@@ -708,6 +1549,7 @@ export const { service: processService, useCrudHook: useProcesses } =
708
1549
  ```
709
1550
 
710
1551
  ### **3️⃣ Save Handler (Integrado ao useCrud)**
1552
+
711
1553
  ```typescript
712
1554
  // src/processes/ProcessesPage.tsx
713
1555
 
@@ -731,6 +1573,7 @@ export default function ProcessesPage() {
731
1573
  ```
732
1574
 
733
1575
  ### **4️⃣ Config (com useMemo)**
1576
+
734
1577
  ```typescript
735
1578
  // src/processes/ProcessesPage.tsx
736
1579
  import { useMemo } from 'react';
@@ -740,12 +1583,12 @@ export default function ProcessesPage() {
740
1583
  const crud = useProcesses();
741
1584
 
742
1585
  // ⚠️ OBRIGATÓRIO useMemo para evitar re-renders
743
- const config = useMemo(() =>
1586
+ const config = useMemo(() =>
744
1587
  generateCrudConfig<Process>({
745
1588
  entity: 'processo',
746
1589
  columns: [
747
1590
  { key: 'title', label: 'Título', type: 'text', required: true },
748
- { key: 'status', label: 'Status', type: 'select',
1591
+ { key: 'status', label: 'Status', type: 'select',
749
1592
  options: [
750
1593
  { value: 'draft', label: 'Rascunho' },
751
1594
  { value: 'active', label: 'Ativo' },
@@ -754,7 +1597,7 @@ export default function ProcessesPage() {
754
1597
  },
755
1598
  { key: 'description', label: 'Descrição', type: 'textarea' }
756
1599
  ]
757
- }),
1600
+ }),
758
1601
  []);
759
1602
 
760
1603
  return createCrudPage({
@@ -766,6 +1609,7 @@ export default function ProcessesPage() {
766
1609
  ```
767
1610
 
768
1611
  ### **5️⃣ Page + Outlet (preservar estado)**
1612
+
769
1613
  ```typescript
770
1614
  // src/App.tsx
771
1615
  import { Outlet } from 'react-router-dom';
@@ -805,16 +1649,17 @@ npm install forlogic-core@latest
805
1649
  ### **1️⃣ Migração de Tipos (Breaking Change)**
806
1650
 
807
1651
  #### **❌ ANTES (Versão Antiga):**
1652
+
808
1653
  ```typescript
809
- import {
810
- ContentEntity,
811
- VisualEntity,
812
- UserRelatedEntity,
1654
+ import {
1655
+ ContentEntity,
1656
+ VisualEntity,
1657
+ UserRelatedEntity,
813
1658
  ActivableEntity,
814
- FormEntity
1659
+ FormEntity
815
1660
  } from 'forlogic-core';
816
1661
 
817
- export interface Example extends
1662
+ export interface Example extends
818
1663
  ContentEntity,
819
1664
  VisualEntity,
820
1665
  UserRelatedEntity,
@@ -839,6 +1684,7 @@ export interface UpdateExamplePayload extends Partial<CreateExamplePayload> {
839
1684
  ```
840
1685
 
841
1686
  #### **✅ DEPOIS (Nova API):**
1687
+
842
1688
  ```typescript
843
1689
  import { BaseEntity } from 'forlogic-core';
844
1690
 
@@ -855,7 +1701,7 @@ export interface Example extends BaseEntity {
855
1701
  }
856
1702
 
857
1703
  export type CreateExamplePayload = Omit<
858
- Example,
1704
+ Example,
859
1705
  keyof BaseEntity | 'responsible_name'
860
1706
  >;
861
1707
 
@@ -863,6 +1709,7 @@ export type UpdateExamplePayload = Partial<CreateExamplePayload>;
863
1709
  ```
864
1710
 
865
1711
  **📝 Mudanças:**
1712
+
866
1713
  - ❌ **REMOVIDO**: Helper interfaces (`ContentEntity`, `VisualEntity`, etc)
867
1714
  - ✅ **NOVO**: Apenas `BaseEntity` + campos explícitos
868
1715
  - ✅ **NOVO**: `is_actived` agora é campo padrão de `BaseEntity`
@@ -873,6 +1720,7 @@ export type UpdateExamplePayload = Partial<CreateExamplePayload>;
873
1720
  ### **2️⃣ Migração de Save Handler (Breaking Change)**
874
1721
 
875
1722
  #### **❌ ANTES (createSimpleSaveHandler):**
1723
+
876
1724
  ```typescript
877
1725
  import { createSimpleSaveHandler, useAuth } from 'forlogic-core';
878
1726
 
@@ -895,6 +1743,7 @@ const handleSave = createSimpleSaveHandler(
895
1743
  ```
896
1744
 
897
1745
  #### **✅ DEPOIS (manager.save):**
1746
+
898
1747
  ```typescript
899
1748
  const handleSave = (data: any) => {
900
1749
  manager.save(data, (d) => ({
@@ -910,6 +1759,7 @@ const handleSave = (data: any) => {
910
1759
  ```
911
1760
 
912
1761
  **📝 Mudanças:**
1762
+
913
1763
  - ❌ **REMOVIDO**: `createSimpleSaveHandler`
914
1764
  - ❌ **REMOVIDO**: Import de `useAuth` para pegar `alias`
915
1765
  - ✅ **NOVO**: `manager.save(data, transform)`
@@ -920,6 +1770,7 @@ const handleSave = (data: any) => {
920
1770
  ### **3️⃣ Migração de Campos de Formulário**
921
1771
 
922
1772
  #### **❌ ANTES (Tipos Múltiplos):**
1773
+
923
1774
  ```typescript
924
1775
  {
925
1776
  name: 'id_user',
@@ -930,6 +1781,7 @@ const handleSave = (data: any) => {
930
1781
  ```
931
1782
 
932
1783
  #### **✅ DEPOIS (Tipo Unificado):**
1784
+
933
1785
  ```typescript
934
1786
  {
935
1787
  name: 'id_user',
@@ -941,6 +1793,7 @@ const handleSave = (data: any) => {
941
1793
  ```
942
1794
 
943
1795
  **📝 Mudanças:**
1796
+
944
1797
  - ❌ **REMOVIDO**: `'simple-qualiex-user-field'`, `'single-responsible-select'`
945
1798
  - ✅ **NOVO**: Apenas `'user-select'` com parâmetro `mode`
946
1799
 
@@ -949,12 +1802,14 @@ const handleSave = (data: any) => {
949
1802
  ### **4️⃣ Migração de Imports**
950
1803
 
951
1804
  #### **❌ ANTES:**
1805
+
952
1806
  ```typescript
953
1807
  import { createSimpleSaveHandler } from 'forlogic-core';
954
1808
  import { ContentEntity, VisualEntity, ... } from 'forlogic-core';
955
1809
  ```
956
1810
 
957
1811
  #### **✅ DEPOIS:**
1812
+
958
1813
  ```typescript
959
1814
  // createSimpleSaveHandler removido (usar manager.save)
960
1815
  import { BaseEntity } from 'forlogic-core';
@@ -962,6 +1817,7 @@ import { handleExternalLink } from 'forlogic-core'; // NOVO helper
962
1817
  ```
963
1818
 
964
1819
  **📝 Mudanças:**
1820
+
965
1821
  - ❌ **REMOVIDO**: `createSimpleSaveHandler`
966
1822
  - ❌ **REMOVIDO**: Helper interfaces de tipos
967
1823
  - ✅ **NOVO**: `handleExternalLink` (helper de links externos)
@@ -971,11 +1827,12 @@ import { handleExternalLink } from 'forlogic-core'; // NOVO helper
971
1827
  ### **5️⃣ Migração de Filtros Customizados**
972
1828
 
973
1829
  #### **❌ ANTES (Filtro Frontend):**
1830
+
974
1831
  ```typescript
975
1832
  const [statusFilter, setStatusFilter] = useState('active');
976
1833
 
977
1834
  const filteredEntities = useMemo(() => {
978
- return manager.entities.filter(e =>
1835
+ return manager.entities.filter(e =>
979
1836
  statusFilter === 'all' ? true : e.is_actived
980
1837
  );
981
1838
  }, [manager.entities, statusFilter]);
@@ -989,6 +1846,7 @@ const filteredManager = useMemo(() => ({
989
1846
  ```
990
1847
 
991
1848
  #### **✅ DEPOIS (Filtro Backend - Recomendado):**
1849
+
992
1850
  ```typescript
993
1851
  const [statusFilter, setStatusFilter] = useState<boolean | 'all'>(true);
994
1852
 
@@ -1002,6 +1860,7 @@ const CrudPage = createCrudPage({ manager, config, onSave });
1002
1860
  ```
1003
1861
 
1004
1862
  **📝 Mudanças:**
1863
+
1005
1864
  - ✅ **RECOMENDADO**: Filtro aplicado no backend (melhor performance)
1006
1865
  - ✅ **NOVO**: Hook aceita `additionalFilters` como parâmetro
1007
1866
  - ❌ **EVITAR**: Filtro frontend (só para casos complexos)
@@ -1013,6 +1872,7 @@ const CrudPage = createCrudPage({ manager, config, onSave });
1013
1872
  Use este checklist para validar que seu projeto foi migrado corretamente:
1014
1873
 
1015
1874
  #### **Types (`example.ts`):**
1875
+
1016
1876
  - [ ] Removido imports de helper interfaces (`ContentEntity`, `VisualEntity`, etc)
1017
1877
  - [ ] Interface principal agora estende apenas `BaseEntity`
1018
1878
  - [ ] Campos explícitos declarados na interface
@@ -1022,9 +1882,11 @@ Use este checklist para validar que seu projeto foi migrado corretamente:
1022
1882
  - [ ] Removido interfaces/types não usados (`ExampleFilters`, `ExampleSortField`, `ExampleInsert`, `ExampleUpdate`)
1023
1883
 
1024
1884
  #### **Service (`ExampleService.ts`):**
1885
+
1025
1886
  - [ ] Nenhuma mudança necessária (API permanece igual)
1026
1887
 
1027
1888
  #### **Page (`ExamplesPage.tsx`):**
1889
+
1028
1890
  - [ ] Removido import de `createSimpleSaveHandler`
1029
1891
  - [ ] Removido import de `useAuth` (se usado apenas para alias)
1030
1892
  - [ ] Substituído `createSimpleSaveHandler` por `manager.save()`
@@ -1033,6 +1895,7 @@ Use este checklist para validar que seu projeto foi migrado corretamente:
1033
1895
  - [ ] Substituído lógica de links externos por `handleExternalLink` helper
1034
1896
 
1035
1897
  #### **Testes:**
1898
+
1036
1899
  - [ ] Build sem erros TypeScript (`npm run build`)
1037
1900
  - [ ] Página carrega sem erros
1038
1901
  - [ ] Criar novo item funciona (alias injetado corretamente)
@@ -1053,14 +1916,17 @@ Use este checklist para validar que seu projeto foi migrado corretamente:
1053
1916
  ### **❓ Problemas na Migração?**
1054
1917
 
1055
1918
  #### **Erro: "Property 'save' does not exist on type..."**
1919
+
1056
1920
  - ✅ **Solução**: Atualize `forlogic-core` para versão mais recente
1057
1921
  - ✅ **Comando**: `npm install forlogic-core@latest`
1058
1922
 
1059
1923
  #### **Erro: "Cannot find module 'ContentEntity'"**
1924
+
1060
1925
  - ✅ **Solução**: Remova imports de helper interfaces e use `BaseEntity`
1061
1926
  - ✅ **Ver**: Seção "1️⃣ Migração de Tipos" acima
1062
1927
 
1063
1928
  #### **Erro: "alias is required but not provided"**
1929
+
1064
1930
  - ✅ **Solução**: Use `manager.save()` ao invés de `createEntity` direto
1065
1931
  - ✅ **Ver**: Seção "2️⃣ Migração de Save Handler" acima
1066
1932
 
@@ -1137,15 +2003,18 @@ const config: CrudPageConfig<Process> = {
1137
2003
  ### **Comportamento**
1138
2004
 
1139
2005
  **Desktop (Tabela):**
2006
+
1140
2007
  - Checkbox na primeira coluna (50px de largura)
1141
2008
  - Checkbox no header para "Selecionar Todos"
1142
2009
  - Click na linha NÃO abre o form quando bulk actions está ativo (evita edição acidental)
1143
2010
 
1144
2011
  **Mobile (Cards):**
2012
+
1145
2013
  - Checkbox no canto superior esquerdo de cada card
1146
2014
  - Mesmo comportamento de seleção que desktop
1147
2015
 
1148
2016
  **Barra de Ações:**
2017
+
1149
2018
  - Aparece automaticamente quando há itens selecionados
1150
2019
  - Mostra quantidade de itens selecionados
1151
2020
  - Botão "Limpar" para deselecionar todos
@@ -1231,25 +2100,25 @@ Este tutorial mostra como criar um CRUD completo usando o módulo **Examples** c
1231
2100
 
1232
2101
  ```typescript
1233
2102
  // ============= EXAMPLE MODULE TYPES =============
1234
- import {
2103
+ import {
1235
2104
  ContentEntity, // title, description
1236
2105
  VisualEntity, // color, icon_name
1237
2106
  UserRelatedEntity, // id_user, responsible_name
1238
2107
  ActivableEntity, // is_actived
1239
2108
  FormEntity, // url_field, date_field
1240
- FilterState,
1241
- EntitySortField
2109
+ FilterState,
2110
+ EntitySortField
1242
2111
  } from 'forlogic-core';
1243
2112
 
1244
2113
  /**
1245
2114
  * Example - Entidade completa de exemplo
1246
- *
2115
+ *
1247
2116
  * ✅ Campos Customizados:
1248
2117
  * - title, description (conteúdo)
1249
2118
  * - color, icon_name (visual)
1250
2119
  * - id_user, responsible_name (usuário - enriquecido via Qualiex)
1251
2120
  * - url_field, date_field (formulário)
1252
- *
2121
+ *
1253
2122
  * 🔒 Campos Herdados de BaseEntity (automáticos):
1254
2123
  * - id: string
1255
2124
  * - alias: string
@@ -1272,20 +2141,20 @@ export interface Example extends BaseEntity {
1272
2141
 
1273
2142
  /**
1274
2143
  * CreateExamplePayload - Dados para CRIAR novo registro
1275
- *
2144
+ *
1276
2145
  * ⚠️ IMPORTANTE:
1277
2146
  * - Campo `alias` é injetado AUTOMATICAMENTE pelo manager.save()
1278
2147
  * - Campos opcionais devem ter `| null`
1279
2148
  * - NÃO incluir id, created_at, updated_at (gerados automaticamente)
1280
2149
  */
1281
2150
  export type CreateExamplePayload = Omit<
1282
- Example,
2151
+ Example,
1283
2152
  keyof BaseEntity | 'responsible_name'
1284
2153
  >;
1285
2154
 
1286
2155
  /**
1287
2156
  * UpdateExamplePayload - Dados para ATUALIZAR registro existente
1288
- *
2157
+ *
1289
2158
  * 📝 Pattern:
1290
2159
  * - Todos os campos são opcionais (Partial)
1291
2160
  */
@@ -1293,6 +2162,7 @@ export type UpdateExamplePayload = Partial<CreateExamplePayload>;
1293
2162
  ```
1294
2163
 
1295
2164
  **📖 Explicação Detalhada:**
2165
+
1296
2166
  - **Composição de Interfaces:** Ao invés de redefinir campos, herda de interfaces prontas da lib
1297
2167
  - **`alias` no CreatePayload:** RLS do Supabase precisa desse campo para funcionar
1298
2168
  - **`Partial<>` no UpdatePayload:** Permite updates parciais (só manda os campos que mudaram)
@@ -1310,7 +2180,7 @@ import type { Example, CreateExamplePayload, UpdateExamplePayload } from './exam
1310
2180
 
1311
2181
  /**
1312
2182
  * ExampleService - Service CRUD completo gerado automaticamente
1313
- *
2183
+ *
1314
2184
  * ✅ O que é gerado:
1315
2185
  * - service.getAll(params)
1316
2186
  * - service.getById(id)
@@ -1318,13 +2188,13 @@ import type { Example, CreateExamplePayload, UpdateExamplePayload } from './exam
1318
2188
  * - service.update(id, data)
1319
2189
  * - service.delete(id)
1320
2190
  * - useCrudHook() - Hook React Query integrado
1321
- *
2191
+ *
1322
2192
  * 🔧 Configuração:
1323
2193
  * - tableName: Nome da tabela no Supabase (schema: central)
1324
2194
  * - entityName: Nome legível para toasts ("Exemplo criado com sucesso")
1325
2195
  * - searchFields: Campos que serão pesquisados pelo filtro de busca
1326
2196
  * - enableQualiexEnrichment: true → adiciona responsible_name automaticamente
1327
- *
2197
+ *
1328
2198
  * 📊 Estrutura de Tabela Esperada (Supabase):
1329
2199
  * CREATE TABLE central.examples (
1330
2200
  * id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -1342,7 +2212,7 @@ import type { Example, CreateExamplePayload, UpdateExamplePayload } from './exam
1342
2212
  * date_field DATE
1343
2213
  * );
1344
2214
  */
1345
- export const { service: ExampleService, useCrudHook: useExamplesCrud } =
2215
+ export const { service: ExampleService, useCrudHook: useExamplesCrud } =
1346
2216
  createSimpleService<Example, CreateExamplePayload, UpdateExamplePayload>({
1347
2217
  tableName: 'examples', // 🗃️ Tabela no Supabase
1348
2218
  entityName: 'Exemplo', // 📣 Nome para mensagens
@@ -1353,6 +2223,7 @@ export const { service: ExampleService, useCrudHook: useExamplesCrud } =
1353
2223
  ```
1354
2224
 
1355
2225
  **📖 Explicação Detalhada:**
2226
+
1356
2227
  - **Uma linha, tudo pronto:** `createSimpleService` gera todo o boilerplate
1357
2228
  - **Soft delete automático:** `deleteEntity()` marca `is_removed = true`, não deleta fisicamente
1358
2229
  - **RLS automático:** Filtra por `alias` automaticamente
@@ -1396,9 +2267,9 @@ import { useState, useMemo } from 'react';
1396
2267
  ```typescript
1397
2268
  /**
1398
2269
  * 📝 CONFIGURAÇÃO DO FORMULÁRIO
1399
- *
2270
+ *
1400
2271
  * Organizado em seções (formSections) com campos (fields).
1401
- *
2272
+ *
1402
2273
  * Tipos de campos suportados:
1403
2274
  * - 'text' - Input de texto simples
1404
2275
  * - 'email' - Input de email com validação
@@ -1489,6 +2360,7 @@ const formSections = [{
1489
2360
  Ver código completo no arquivo `src/examples/ExamplesPage.tsx` do projeto.
1490
2361
 
1491
2362
  **Estrutura básica:**
2363
+
1492
2364
  1. Hooks no topo
1493
2365
  2. Estados de filtros com `useState`
1494
2366
  3. Estados derivados com `useMemo`
@@ -1572,6 +2444,7 @@ const filteredManager = useMemo(() => ({
1572
2444
  ```
1573
2445
 
1574
2446
  **📖 Explicação:**
2447
+
1575
2448
  - **`useState`**: Armazena valor selecionado no filtro
1576
2449
  - **`useMemo` (filteredEntities)**: Evita re-filtrar a cada render
1577
2450
  - **`useMemo` (filteredManager)**: Evita re-criar objeto manager
@@ -1598,8 +2471,8 @@ const filteredManager = useMemo(() => ({
1598
2471
  }), [manager, filteredEntities]);
1599
2472
 
1600
2473
  const DepartmentFilter = () => (
1601
- <select
1602
- value={deptFilter}
2474
+ <select
2475
+ value={deptFilter}
1603
2476
  onChange={(e) => setDeptFilter(e.target.value)}
1604
2477
  className="px-3 py-2 border rounded-md"
1605
2478
  >
@@ -1624,7 +2497,7 @@ const [dateRange, setDateRange] = useState<{ from?: Date; to?: Date }>({});
1624
2497
 
1625
2498
  const filteredEntities = useMemo(() => {
1626
2499
  if (!dateRange.from && !dateRange.to) return manager.entities;
1627
-
2500
+
1628
2501
  return manager.entities.filter(e => {
1629
2502
  const itemDate = parseISO(e.created_at);
1630
2503
  if (dateRange.from && isBefore(itemDate, dateRange.from)) return false;
@@ -1643,12 +2516,12 @@ const filteredManager = useMemo(() => ({
1643
2516
 
1644
2517
  ## 🪝 HOOKS REACT NO CRUD
1645
2518
 
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 |
2519
+ | Hook | Quando Usar | Exemplo no CRUD | ⚠️ Evitar |
2520
+ | --------------- | ------------------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------- |
2521
+ | **useMemo** | Cálculos pesados que dependem de props/state | • Configuração de colunas<br>• Filtros derivados<br>• Manager customizado | Valores simples (strings, números) |
2522
+ | **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 |
2523
+ | **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 |
2524
+ | **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
2525
 
1653
2526
  ### **📖 Exemplos Práticos**
1654
2527
 
@@ -1672,7 +2545,7 @@ const [statusFilter, setStatusFilter] = useState('active');
1672
2545
  const statusFilter = useMemo(() => 'active', []); // Não faz sentido!
1673
2546
 
1674
2547
  // ✅ CORRETO: useMemo para estado derivado
1675
- const filteredEntities = useMemo(() =>
2548
+ const filteredEntities = useMemo(() =>
1676
2549
  manager.entities.filter(e => e.is_actived),
1677
2550
  [manager.entities]
1678
2551
  );
@@ -1703,15 +2576,15 @@ import { cn } from 'forlogic-core';
1703
2576
  ```typescript
1704
2577
  // ❌ SINTOMA: Formulário fecha/reabre sozinho, re-renders infinitos
1705
2578
  // ❌ CAUSA: Config recriado a cada render
1706
- const config = {
1707
- columns: exampleColumns,
1708
- formSections
2579
+ const config = {
2580
+ columns: exampleColumns,
2581
+ formSections
1709
2582
  };
1710
2583
 
1711
2584
  // ✅ SOLUÇÃO: Envolver em useMemo
1712
- const config = useMemo(() => ({
1713
- columns: exampleColumns,
1714
- formSections
2585
+ const config = useMemo(() => ({
2586
+ columns: exampleColumns,
2587
+ formSections
1715
2588
  }), []);
1716
2589
  ```
1717
2590
 
@@ -1722,15 +2595,15 @@ const config = useMemo(() => ({
1722
2595
  ```typescript
1723
2596
  // ❌ SINTOMA: TypeError: manager.createEntity is not a function
1724
2597
  // ❌ CAUSA: Passou array direto
1725
- const CrudPage = createCrudPage({
2598
+ const CrudPage = createCrudPage({
1726
2599
  manager: manager.entities, // ← Errado!
1727
- config
2600
+ config
1728
2601
  });
1729
2602
 
1730
2603
  // ✅ SOLUÇÃO: Passar manager completo
1731
- const CrudPage = createCrudPage({
2604
+ const CrudPage = createCrudPage({
1732
2605
  manager, // ← Correto!
1733
- config
2606
+ config
1734
2607
  });
1735
2608
  ```
1736
2609
 
@@ -1744,7 +2617,7 @@ const CrudPage = createCrudPage({
1744
2617
  const filteredEntities = manager.entities.filter(e => e.is_actived);
1745
2618
 
1746
2619
  // ✅ SOLUÇÃO: Envolver em useMemo
1747
- const filteredEntities = useMemo(() =>
2620
+ const filteredEntities = useMemo(() =>
1748
2621
  manager.entities.filter(e => e.is_actived),
1749
2622
  [manager.entities]
1750
2623
  );
@@ -1757,7 +2630,7 @@ const filteredEntities = useMemo(() =>
1757
2630
  ```typescript
1758
2631
  // ❌ SINTOMA: Erro de RLS no Supabase, registro não é criado
1759
2632
  // ❌ CAUSA: Usar createEntity/updateEntity direto sem alias
1760
- manager.createEntity({
2633
+ manager.createEntity({
1761
2634
  title: data.title,
1762
2635
  email: data.email
1763
2636
  // ← Falta alias!
@@ -1820,9 +2693,9 @@ export const ExamplesPage = () => {
1820
2693
  key: 'website',
1821
2694
  header: 'Site',
1822
2695
  render: (item) => (
1823
- <a
1824
- href={item.website}
1825
- target="_blank"
2696
+ <a
2697
+ href={item.website}
2698
+ target="_blank"
1826
2699
  rel="noopener noreferrer"
1827
2700
  className="text-blue-600 hover:underline"
1828
2701
  >
@@ -1899,6 +2772,7 @@ const CrudPage = createCrudPage({
1899
2772
  ### 🔗 Integração Qualiex (opcional)
1900
2773
 
1901
2774
  **Auto-enrichment** (já configurado no BaseService):
2775
+
1902
2776
  ```typescript
1903
2777
  // ✅ Automático - dados enriquecidos com nome do usuário
1904
2778
  const processes = await processService.getAll();
@@ -1906,17 +2780,19 @@ const processes = await processService.getAll();
1906
2780
  ```
1907
2781
 
1908
2782
  **Componentes prontos:**
2783
+
1909
2784
  ```typescript
1910
2785
  import { QualiexUserField, QualiexResponsibleSelectField } from 'forlogic-core';
1911
2786
 
1912
2787
  // Select de usuários Qualiex
1913
- <QualiexResponsibleSelectField
2788
+ <QualiexResponsibleSelectField
1914
2789
  value={form.watch('id_user')}
1915
2790
  onChange={(userId) => form.setValue('id_user', userId)}
1916
2791
  />
1917
2792
  ```
1918
2793
 
1919
2794
  **Componentes em formulários CRUD:**
2795
+
1920
2796
  ```typescript
1921
2797
  // Para seleção de usuário (modo unificado)
1922
2798
  {
@@ -1945,6 +2821,7 @@ Você pode criar e usar componentes customizados nos formulários para necessida
1945
2821
  > **Nota:** Componentes customizados devem ser registrados no `BaseForm.tsx` para funcionarem corretamente nos formulários CRUD.
1946
2822
 
1947
2823
  **⚠️ CRÍTICO:** Requests Qualiex exigem header `un-alias`:
2824
+
1948
2825
  ```typescript
1949
2826
  // ✅ Já configurado no BaseService automaticamente
1950
2827
  headers: { 'un-alias': 'true' }
@@ -2006,7 +2883,7 @@ import { useAuth, placeService } from 'forlogic-core';
2006
2883
 
2007
2884
  function MyComponent() {
2008
2885
  const { alias } = useAuth();
2009
-
2886
+
2010
2887
  const { data: places = [], isLoading, error } = useQuery({
2011
2888
  queryKey: ['places', alias],
2012
2889
  queryFn: () => placeService.getPlaces(alias),
@@ -2036,7 +2913,7 @@ import { useAuth, placeService } from 'forlogic-core';
2036
2913
 
2037
2914
  export function usePlaces() {
2038
2915
  const { alias } = useAuth();
2039
-
2916
+
2040
2917
  return useQuery({
2041
2918
  queryKey: ['places', alias],
2042
2919
  queryFn: () => placeService.getPlaces(alias),
@@ -2081,7 +2958,7 @@ export function PlaceSelect({ value, onChange, disabled }: {
2081
2958
  disabled?: boolean;
2082
2959
  }) {
2083
2960
  const { data: places = [], isLoading } = usePlaces();
2084
-
2961
+
2085
2962
  // Achatar hierarquia para o select
2086
2963
  const flatPlaces = useMemo(() => {
2087
2964
  const flatten = (items: Place[], level = 0): any[] => {
@@ -2092,7 +2969,7 @@ export function PlaceSelect({ value, onChange, disabled }: {
2092
2969
  };
2093
2970
  return flatten(places);
2094
2971
  }, [places]);
2095
-
2972
+
2096
2973
  return (
2097
2974
  <EntitySelect
2098
2975
  value={value}
@@ -2136,7 +3013,7 @@ const { service, useCrudHook } = createSimpleService({
2136
3013
  // Hook para buscar nome do place
2137
3014
  function usePlaceName(placeId: string) {
2138
3015
  const { data: places = [] } = usePlaces();
2139
-
3016
+
2140
3017
  return useMemo(() => {
2141
3018
  const findPlace = (items: Place[]): Place | undefined => {
2142
3019
  for (const place of items) {
@@ -2178,6 +3055,7 @@ const placeName = userPlace?.name;
2178
3055
  ```
2179
3056
 
2180
3057
  **Fluxo de dados:**
3058
+
2181
3059
  1. Token JWT contém `alias` e `companyId`
2182
3060
  2. `placeService.getPlaces(alias)` busca os Places da API Qualiex
2183
3061
  3. Cada `Place` contém `usersIds` (array de IDs de usuários)
@@ -2210,13 +3088,13 @@ function PlaceTree({ places, level = 0 }: {
2210
3088
 
2211
3089
  ### 🛠️ Troubleshooting
2212
3090
 
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 |
3091
+ | Erro | Causa | Solução |
3092
+ | ----------------------------------- | ------------------------------------- | ---------------------------------------------- |
3093
+ | `CompanyId não encontrado no token` | Token não validado corretamente | Verificar edge function `validate-token` |
3094
+ | `Alias da unidade é obrigatório` | `alias` não disponível no `useAuth()` | Aguardar carregamento do auth com `isLoading` |
3095
+ | `Token Qualiex não encontrado` | Variável de ambiente faltando | Verificar `VITE_QUALIEX_API_URL` no `.env` |
3096
+ | Places retorna array vazio `[]` | Empresa sem places cadastrados | Verificar cadastro no Qualiex admin |
3097
+ | Hierarquia quebrada | `parentId` incorreto nos dados | Verificar integridade dos dados na API Qualiex |
2220
3098
 
2221
3099
  ### 📦 Exemplo Completo: Dashboard por Local
2222
3100
 
@@ -2237,7 +3115,7 @@ function PlacesDashboard() {
2237
3115
  <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
2238
3116
  {places.map(place => {
2239
3117
  const placeMetrics = metrics.filter(m => m.place_id === place.placeId);
2240
-
3118
+
2241
3119
  return (
2242
3120
  <Card key={place.id}>
2243
3121
  <CardHeader>
@@ -2288,6 +3166,7 @@ function PlacesDashboard() {
2288
3166
  ## 🗃️ MIGRATIONS + RLS
2289
3167
 
2290
3168
  ### Template SQL Completo
3169
+
2291
3170
  ```sql
2292
3171
  -- 1️⃣ Criar tabela
2293
3172
  CREATE TABLE central.processes (
@@ -2335,10 +3214,11 @@ EXECUTE FUNCTION public.set_updated_at();
2335
3214
  ```
2336
3215
 
2337
3216
  ### ❌ Sintaxes Proibidas RLS
3217
+
2338
3218
  ```sql
2339
3219
  -- ❌ ERRADO - Sintaxes simplificadas que não funcionam
2340
3220
  id_user = auth.uid() -- ❌ Campo errado
2341
- id = auth.uid() -- ❌ Campo errado
3221
+ id = auth.uid() -- ❌ Campo errado
2342
3222
  alias = auth.uid() -- ❌ Função errada
2343
3223
 
2344
3224
  -- ✅ CORRETO - Extração completa do JWT
@@ -2356,6 +3236,7 @@ alias = auth.uid() -- ❌ Função errada
2356
3236
  5. **`= alias`**: Compara com a coluna `alias` da tabela
2357
3237
 
2358
3238
  **Estrutura do JWT:**
3239
+
2359
3240
  ```json
2360
3241
  {
2361
3242
  "sub": "user-uuid",
@@ -2366,11 +3247,13 @@ alias = auth.uid() -- ❌ Função errada
2366
3247
  ```
2367
3248
 
2368
3249
  **Fluxo de Autenticação Multi-tenant:**
3250
+
2369
3251
  ```
2370
3252
  Login → Validação Externa → JWT com alias → RLS Policy → Filtragem por empresa
2371
3253
  ```
2372
3254
 
2373
3255
  **⚠️ Erros Comuns:**
3256
+
2374
3257
  - `alias = auth.uid()` → Compara alias com UUID (tipos incompatíveis)
2375
3258
  - `id_user = auth.uid()` → Compara com usuário, não com empresa
2376
3259
  - `auth.jwt().alias` → Sintaxe JavaScript, não SQL
@@ -2380,6 +3263,7 @@ Login → Validação Externa → JWT com alias → RLS Policy → Filtragem por
2380
3263
  ## 🐛 TROUBLESHOOTING
2381
3264
 
2382
3265
  ### 1️⃣ "relation does not exist"
3266
+
2383
3267
  ```typescript
2384
3268
  // Causa: Schema ausente
2385
3269
  .from('processes') // ❌
@@ -2389,6 +3273,7 @@ Login → Validação Externa → JWT com alias → RLS Policy → Filtragem por
2389
3273
  ```
2390
3274
 
2391
3275
  ### 2️⃣ RLS retorna vazio
3276
+
2392
3277
  ```sql
2393
3278
  -- Causa: Sintaxe incorreta
2394
3279
  USING (id_user = auth.uid()) -- ❌ ERRADO
@@ -2401,6 +3286,7 @@ USING (
2401
3286
  ```
2402
3287
 
2403
3288
  ### 3️⃣ Duplicação de registros
3289
+
2404
3290
  ```typescript
2405
3291
  // Causa: ID ausente no update
2406
3292
  await service.save({ title: 'Novo' }); // ❌ Cria duplicado
@@ -2410,6 +3296,7 @@ await service.save({ id: item.id, title: 'Novo' }); // ✅
2410
3296
  ```
2411
3297
 
2412
3298
  ### 4️⃣ Página recarrega ao editar
3299
+
2413
3300
  ```typescript
2414
3301
  // Causa: Config sem useMemo
2415
3302
  const config = generateCrudConfig(...); // ❌ Re-render infinito
@@ -2419,6 +3306,7 @@ const config = useMemo(() => generateCrudConfig(...), []); // ✅
2419
3306
  ```
2420
3307
 
2421
3308
  ### 5️⃣ Estado reseta ao navegar
3309
+
2422
3310
  ```typescript
2423
3311
  // Causa: Outlet ausente
2424
3312
  <Route path="/processes" element={<ProcessesPage />} /> // ❌
@@ -2431,6 +3319,7 @@ const config = useMemo(() => generateCrudConfig(...), []); // ✅
2431
3319
  ```
2432
3320
 
2433
3321
  ### 6️⃣ Qualiex retorna 401
3322
+
2434
3323
  ```typescript
2435
3324
  // Causa: Header ausente
2436
3325
  fetch(url); // ❌
@@ -2447,6 +3336,7 @@ fetch(url, { headers: { 'un-alias': 'true' } }); // ✅
2447
3336
  ### **Por que Filtros no Backend?**
2448
3337
 
2449
3338
  Filtrar dados no **backend** é a abordagem recomendada porque:
3339
+
2450
3340
  - ✅ **Paginação correta**: Total de itens reflete os dados filtrados
2451
3341
  - ✅ **Performance**: Menos dados trafegados pela rede
2452
3342
  - ✅ **Escalabilidade**: Funciona com milhares de registros
@@ -2463,9 +3353,9 @@ Para filtros simples e estáticos, use `additionalFilters` no service:
2463
3353
  import { createSimpleService } from 'forlogic-core';
2464
3354
  import { Subprocess, SubprocessInsert, SubprocessUpdate } from './types';
2465
3355
 
2466
- export const {
2467
- service: subprocessService,
2468
- useCrudHook: baseUseCrudHook
3356
+ export const {
3357
+ service: subprocessService,
3358
+ useCrudHook: baseUseCrudHook
2469
3359
  } = createSimpleService<Subprocess, SubprocessInsert, SubprocessUpdate>({
2470
3360
  tableName: 'subprocesses',
2471
3361
  entityName: 'subprocesso',
@@ -2489,9 +3379,9 @@ import { useMemo } from 'react';
2489
3379
  import { createSimpleService } from 'forlogic-core';
2490
3380
 
2491
3381
  // Service base
2492
- export const {
2493
- service: subprocessService,
2494
- useCrudHook: baseUseCrudHook
3382
+ export const {
3383
+ service: subprocessService,
3384
+ useCrudHook: baseUseCrudHook
2495
3385
  } = createSimpleService<Subprocess, SubprocessInsert, SubprocessUpdate>({
2496
3386
  tableName: 'subprocesses',
2497
3387
  entityName: 'subprocesso',
@@ -2507,7 +3397,7 @@ export function useSubprocessesCrud(filters?: {
2507
3397
  // Transforma filtros em additionalFilters
2508
3398
  const additionalFilters = useMemo(() => {
2509
3399
  const filterList: any[] = [];
2510
-
3400
+
2511
3401
  // Filtro de status (ativo/inativo)
2512
3402
  if (filters?.is_actived !== undefined) {
2513
3403
  filterList.push({
@@ -2516,7 +3406,7 @@ export function useSubprocessesCrud(filters?: {
2516
3406
  value: filters.is_actived
2517
3407
  });
2518
3408
  }
2519
-
3409
+
2520
3410
  // Filtro de processo (incluindo "sem processo")
2521
3411
  if (filters?.id_process) {
2522
3412
  if (filters.id_process === 'none') {
@@ -2533,10 +3423,10 @@ export function useSubprocessesCrud(filters?: {
2533
3423
  });
2534
3424
  }
2535
3425
  }
2536
-
3426
+
2537
3427
  return filterList;
2538
3428
  }, [filters?.is_actived, filters?.id_process]);
2539
-
3429
+
2540
3430
  // Chama hook base com filtros dinâmicos
2541
3431
  return baseUseCrudHook({ additionalFilters });
2542
3432
  }
@@ -2551,16 +3441,16 @@ import { useSubprocessesCrud } from './SubprocessService';
2551
3441
 
2552
3442
  export default function SubprocessesPage() {
2553
3443
  const [searchParams, setSearchParams] = useSearchParams();
2554
-
3444
+
2555
3445
  // Ler filtros da URL
2556
3446
  const filters = useMemo(() => ({
2557
3447
  is_actived: searchParams.get('is_actived') === 'true',
2558
3448
  id_process: searchParams.get('id_process') || undefined
2559
3449
  }), [searchParams]);
2560
-
3450
+
2561
3451
  // Manager com filtros aplicados no backend
2562
3452
  const manager = useSubprocessesCrud(filters);
2563
-
3453
+
2564
3454
  // Config com filtros de UI
2565
3455
  const config = useMemo(() => generateCrudConfig<Subprocess>({
2566
3456
  entityName: 'Subprocesso',
@@ -2599,7 +3489,7 @@ export default function SubprocessesPage() {
2599
3489
  ],
2600
3490
  columns: [...]
2601
3491
  }), [searchParams]);
2602
-
3492
+
2603
3493
  return <CrudPage />;
2604
3494
  }
2605
3495
  ```
@@ -2608,17 +3498,17 @@ export default function SubprocessesPage() {
2608
3498
 
2609
3499
  O `BaseService` suporta os seguintes operadores em `additionalFilters`:
2610
3500
 
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' }` |
3501
+ | Operador | Descrição | Exemplo |
3502
+ | ---------- | -------------- | ---------------------------------------------------------- |
3503
+ | `eq` | Igual a | `{ field: 'status', operator: 'eq', value: 'active' }` |
3504
+ | `neq` | Diferente de | `{ field: 'status', operator: 'neq', value: 'archived' }` |
3505
+ | `gt` | Maior que | `{ field: 'price', operator: 'gt', value: 100 }` |
3506
+ | `gte` | Maior ou igual | `{ field: 'price', operator: 'gte', value: 100 }` |
3507
+ | `lt` | Menor que | `{ field: 'stock', operator: 'lt', value: 10 }` |
3508
+ | `lte` | Menor ou igual | `{ field: 'stock', operator: 'lte', value: 10 }` |
3509
+ | `in` | Em lista | `{ field: 'category', operator: 'in', value: ['A', 'B'] }` |
3510
+ | `contains` | Contém texto | `{ field: 'name', operator: 'contains', value: 'test' }` |
3511
+ | `is_null` | É nulo | `{ field: 'deleted_at', operator: 'is_null' }` |
2622
3512
 
2623
3513
  ### **Exemplo Completo: Filtro de Status e Processo**
2624
3514
 
@@ -2630,7 +3520,7 @@ export function useSubprocessesCrud(filters?: {
2630
3520
  }) {
2631
3521
  const additionalFilters = useMemo(() => {
2632
3522
  const filterList: any[] = [];
2633
-
3523
+
2634
3524
  if (filters?.is_actived !== undefined) {
2635
3525
  filterList.push({
2636
3526
  field: 'is_actived',
@@ -2638,7 +3528,7 @@ export function useSubprocessesCrud(filters?: {
2638
3528
  value: filters.is_actived
2639
3529
  });
2640
3530
  }
2641
-
3531
+
2642
3532
  if (filters?.id_process) {
2643
3533
  if (filters.id_process === 'none') {
2644
3534
  filterList.push({
@@ -2653,10 +3543,10 @@ export function useSubprocessesCrud(filters?: {
2653
3543
  });
2654
3544
  }
2655
3545
  }
2656
-
3546
+
2657
3547
  return filterList;
2658
3548
  }, [filters?.is_actived, filters?.id_process]);
2659
-
3549
+
2660
3550
  return baseUseCrudHook({ additionalFilters });
2661
3551
  }
2662
3552
 
@@ -2675,10 +3565,11 @@ const manager = useSubprocessesCrud(filters);
2675
3565
  ### **⚠️ Filtros Frontend vs Backend**
2676
3566
 
2677
3567
  **❌ Filtros no Frontend (EVITAR):**
3568
+
2678
3569
  ```typescript
2679
3570
  // Problema: Paginação incorreta
2680
- const filteredEntities = useMemo(() =>
2681
- manager.entities.filter(e => e.is_actived),
3571
+ const filteredEntities = useMemo(() =>
3572
+ manager.entities.filter(e => e.is_actived),
2682
3573
  [manager.entities]
2683
3574
  );
2684
3575
 
@@ -2688,10 +3579,11 @@ const filteredEntities = useMemo(() =>
2688
3579
  ```
2689
3580
 
2690
3581
  **✅ Filtros no Backend (CORRETO):**
3582
+
2691
3583
  ```typescript
2692
3584
  // Backend retorna apenas dados filtrados
2693
- const manager = useSubprocessesCrud({
2694
- is_actived: true
3585
+ const manager = useSubprocessesCrud({
3586
+ is_actived: true
2695
3587
  });
2696
3588
 
2697
3589
  // manager.totalCount = 43 (correto!)
@@ -2706,7 +3598,9 @@ const manager = useSubprocessesCrud({
2706
3598
  O `forlogic-core` oferece três formas de definir larguras de colunas nas tabelas CRUD:
2707
3599
 
2708
3600
  ### **1️⃣ Via `className` (Recomendado)**
3601
+
2709
3602
  Use classes do Tailwind para larguras fixas ou responsivas:
3603
+
2710
3604
  ```typescript
2711
3605
  const columns = [
2712
3606
  {
@@ -2723,7 +3617,9 @@ const columns = [
2723
3617
  ```
2724
3618
 
2725
3619
  ### **2️⃣ Via `width` (Fixo em pixels)**
3620
+
2726
3621
  Especifique largura fixa diretamente:
3622
+
2727
3623
  ```typescript
2728
3624
  {
2729
3625
  key: 'order',
@@ -2734,7 +3630,9 @@ Especifique largura fixa diretamente:
2734
3630
  ```
2735
3631
 
2736
3632
  ### **3️⃣ Via `minWidth` + `weight` (Flexível)**
3633
+
2737
3634
  Para colunas que crescem proporcionalmente:
3635
+
2738
3636
  ```typescript
2739
3637
  {
2740
3638
  key: 'description',
@@ -2745,11 +3643,13 @@ Para colunas que crescem proporcionalmente:
2745
3643
  ```
2746
3644
 
2747
3645
  ### **⚠️ Importante**
3646
+
2748
3647
  - A tabela usa `table-auto` para respeitar essas configurações
2749
3648
  - Para truncar textos longos, use: `className: "max-w-[200px] truncate"`
2750
3649
  - Combine `whitespace-nowrap` com largura fixa para evitar quebras
2751
3650
 
2752
3651
  ### **📋 Exemplo Completo**
3652
+
2753
3653
  ```typescript
2754
3654
  const columns: CrudColumn<MyEntity>[] = [
2755
3655
  {
@@ -2788,13 +3688,14 @@ const columns: CrudColumn<MyEntity>[] = [
2788
3688
  ## 📚 REFERÊNCIA RÁPIDA
2789
3689
 
2790
3690
  ### Imports Essenciais
3691
+
2791
3692
  ```typescript
2792
3693
  // CRUD
2793
- import {
2794
- createSimpleService,
2795
- createCrudPage,
3694
+ import {
3695
+ createSimpleService,
3696
+ createCrudPage,
2796
3697
  generateCrudConfig,
2797
- createSimpleSaveHandler
3698
+ createSimpleSaveHandler
2798
3699
  } from 'forlogic-core';
2799
3700
 
2800
3701
  // UI
@@ -2808,6 +3709,7 @@ import { QualiexUserField, useQualiexUsers } from 'forlogic-core';
2808
3709
  ```
2809
3710
 
2810
3711
  ### Estrutura de Arquivos
3712
+
2811
3713
  ```
2812
3714
  src/
2813
3715
  ├── processes/
@@ -2820,4 +3722,4 @@ src/
2820
3722
 
2821
3723
  ## 📝 Licença
2822
3724
 
2823
- MIT License - ForLogic © 2025
3725
+ MIT License - ForLogic © 2025