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/README.md +857 -132
- package/dist/README.md +857 -132
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.esm.js +2 -2
- package/dist/index.js +2 -2
- package/package.json +1 -1
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
|
|
296
|
-
|
|
297
|
-
| `items`
|
|
298
|
-
| `getItemValue`
|
|
299
|
-
| `getItemLabel`
|
|
300
|
-
| `value`
|
|
301
|
-
| `onChange`
|
|
302
|
-
| `isLoading`
|
|
303
|
-
| `error`
|
|
304
|
-
| `placeholder`
|
|
305
|
-
| `searchPlaceholder` | `string`
|
|
306
|
-
| `emptyMessage`
|
|
307
|
-
| `noResultsMessage`
|
|
308
|
-
| `sortItems`
|
|
309
|
-
| `disabled`
|
|
310
|
-
| `className`
|
|
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
|
|
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
|
|
1647
|
-
|
|
1648
|
-
| **useMemo**
|
|
1649
|
-
| **useState**
|
|
1650
|
-
| **useCallback** | Funções que são passadas como props e dependem de state | • Handlers que dependem de filtros<br>• Callbacks de child components
|
|
1651
|
-
| **useEffect**
|
|
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
|
|
2214
|
-
|
|
2215
|
-
| `CompanyId não encontrado no token` | Token não validado corretamente
|
|
2216
|
-
| `Alias da unidade é obrigatório`
|
|
2217
|
-
| `Token Qualiex não encontrado`
|
|
2218
|
-
| Places retorna array vazio `[]`
|
|
2219
|
-
| Hierarquia quebrada
|
|
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
|
|
2612
|
-
|
|
2613
|
-
| `eq`
|
|
2614
|
-
| `neq`
|
|
2615
|
-
| `gt`
|
|
2616
|
-
| `gte`
|
|
2617
|
-
| `lt`
|
|
2618
|
-
| `lte`
|
|
2619
|
-
| `in`
|
|
2620
|
-
| `contains` | Contém texto
|
|
2621
|
-
| `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
|