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 +1034 -132
- package/dist/README.md +1034 -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/dist/README.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
### ⚠️ TOP 3 ERROS
|
|
12
12
|
|
|
13
13
|
1. **ESQUECER SCHEMA**
|
|
14
|
+
|
|
14
15
|
```typescript
|
|
15
16
|
// ❌ ERRADO
|
|
16
17
|
.from('table')
|
|
@@ -20,12 +21,13 @@
|
|
|
20
21
|
```
|
|
21
22
|
|
|
22
23
|
2. **RLS COM SINTAXE INCORRETA**
|
|
24
|
+
|
|
23
25
|
```sql
|
|
24
26
|
-- ❌ ERRADO
|
|
25
27
|
CREATE POLICY "Users view own" ON schema.table
|
|
26
28
|
FOR SELECT USING (id_user = auth.uid());
|
|
27
29
|
|
|
28
|
-
-- ✅ CORRETO
|
|
30
|
+
-- ✅ CORRETO
|
|
29
31
|
CREATE POLICY "Users view own" ON schema.table
|
|
30
32
|
FOR SELECT USING (
|
|
31
33
|
((SELECT auth.jwt()) ->> 'alias'::text) = alias
|
|
@@ -33,6 +35,7 @@ FOR SELECT USING (
|
|
|
33
35
|
```
|
|
34
36
|
|
|
35
37
|
3. **NÃO CRIAR ÍNDICES AUTOMATICAMENTE**
|
|
38
|
+
|
|
36
39
|
```sql
|
|
37
40
|
-- ❌ PROIBIDO criar índices sem aprovação
|
|
38
41
|
CREATE INDEX idx_table_user ON schema.table(id_user);
|
|
@@ -47,6 +50,7 @@ CREATE INDEX idx_table_user ON schema.table(id_user);
|
|
|
47
50
|
### ⚠️ PADRÕES OBRIGATÓRIOS
|
|
48
51
|
|
|
49
52
|
**Foreign Keys (Chaves Estrangeiras):**
|
|
53
|
+
|
|
50
54
|
```typescript
|
|
51
55
|
// ❌ ERRADO
|
|
52
56
|
process_id: string
|
|
@@ -58,6 +62,7 @@ id_user: string
|
|
|
58
62
|
```
|
|
59
63
|
|
|
60
64
|
**Booleans:**
|
|
65
|
+
|
|
61
66
|
```typescript
|
|
62
67
|
// ❌ ERRADO
|
|
63
68
|
active: boolean
|
|
@@ -69,6 +74,7 @@ is_removed: boolean
|
|
|
69
74
|
```
|
|
70
75
|
|
|
71
76
|
**Timestamps:**
|
|
77
|
+
|
|
72
78
|
```typescript
|
|
73
79
|
// ❌ ERRADO
|
|
74
80
|
creation_date: string
|
|
@@ -85,15 +91,18 @@ deleted_at: string
|
|
|
85
91
|
## 🚫 ÍNDICES: ABSOLUTAMENTE PROIBIDO CRIAR AUTOMATICAMENTE
|
|
86
92
|
|
|
87
93
|
### ⚠️ REGRA DE OURO
|
|
94
|
+
|
|
88
95
|
**NUNCA, EM HIPÓTESE ALGUMA, criar índices automaticamente em migrations!**
|
|
89
96
|
|
|
90
97
|
### 🤔 Por Quê?
|
|
98
|
+
|
|
91
99
|
1. **Custo**: Índices ocupam espaço em disco e custam dinheiro
|
|
92
100
|
2. **Performance de Escrita**: Cada índice adiciona overhead em INSERTs/UPDATEs
|
|
93
101
|
3. **Otimização Prematura**: 99% dos índices criados "por precaução" nunca são usados
|
|
94
102
|
4. **Manutenção**: Índices desnecessários dificultam manutenção e análise de queries
|
|
95
103
|
|
|
96
104
|
### ❌ Casos PROIBIDOS (mesmo que pareçam "boas práticas")
|
|
105
|
+
|
|
97
106
|
```sql
|
|
98
107
|
-- ❌ PROIBIDO: Índice em FK "porque é boa prática"
|
|
99
108
|
CREATE INDEX idx_subprocess_process ON subprocesses(id_process);
|
|
@@ -108,11 +117,13 @@ CREATE INDEX idx_deliverable_status ON deliverables(id_subprocess, is_completed)
|
|
|
108
117
|
### ✅ Quando Criar Índices?
|
|
109
118
|
|
|
110
119
|
**APENAS** quando:
|
|
120
|
+
|
|
111
121
|
1. ✅ **Solicitado explicitamente** pelo usuário
|
|
112
122
|
2. ✅ **Análise de performance** comprovou necessidade (EXPLAIN ANALYZE)
|
|
113
123
|
3. ✅ **Aprovação prévia** do usuário para incluir na migration
|
|
114
124
|
|
|
115
125
|
### 📋 Checklist OBRIGATÓRIO Antes de Qualquer Migration
|
|
126
|
+
|
|
116
127
|
```markdown
|
|
117
128
|
- [ ] A migration NÃO contém NENHUM `CREATE INDEX`?
|
|
118
129
|
- [ ] Se contém, o usuário solicitou EXPLICITAMENTE?
|
|
@@ -121,6 +132,7 @@ CREATE INDEX idx_deliverable_status ON deliverables(id_subprocess, is_completed)
|
|
|
121
132
|
```
|
|
122
133
|
|
|
123
134
|
### 🔧 Processo Correto Para Criar Índices
|
|
135
|
+
|
|
124
136
|
1. **Usuário solicita** OU problemas de performance são detectados
|
|
125
137
|
2. **Rodar EXPLAIN ANALYZE** para confirmar necessidade
|
|
126
138
|
3. **Perguntar ao usuário**: "Posso criar o índice X na coluna Y? Isso vai melhorar a query Z mas adiciona overhead."
|
|
@@ -128,9 +140,10 @@ CREATE INDEX idx_deliverable_status ON deliverables(id_subprocess, is_completed)
|
|
|
128
140
|
5. **Criar migration separada** apenas com os índices aprovados
|
|
129
141
|
|
|
130
142
|
### 📝 Template de Migration de Índices (quando aprovado)
|
|
143
|
+
|
|
131
144
|
```sql
|
|
132
145
|
-- Migration: [TIMESTAMP]_add_performance_indexes.sql
|
|
133
|
-
-- Aprovado em: [DATA]
|
|
146
|
+
-- Aprovado em: [DATA]
|
|
134
147
|
-- Justificativa: [RAZÃO ESPECÍFICA]
|
|
135
148
|
|
|
136
149
|
CREATE INDEX idx_processes_title ON processes.processes(title);
|
|
@@ -144,6 +157,7 @@ CREATE INDEX idx_processes_title ON processes.processes(title);
|
|
|
144
157
|
### ⚠️ ARQUIVOS PROIBIDOS DE CRIAR LOCALMENTE
|
|
145
158
|
|
|
146
159
|
**NUNCA crie estes arquivos localmente:**
|
|
160
|
+
|
|
147
161
|
```typescript
|
|
148
162
|
// ❌ PROIBIDO - Usar da lib
|
|
149
163
|
src/lib/utils.ts // cn() já existe no forlogic-core
|
|
@@ -157,6 +171,7 @@ src/components/ui/input.tsx
|
|
|
157
171
|
### ✅ Imports Corretos
|
|
158
172
|
|
|
159
173
|
**Utils (cn):**
|
|
174
|
+
|
|
160
175
|
```typescript
|
|
161
176
|
// ❌ ERRADO
|
|
162
177
|
import { cn } from '@/lib/utils'
|
|
@@ -166,6 +181,7 @@ import { cn } from 'forlogic-core'
|
|
|
166
181
|
```
|
|
167
182
|
|
|
168
183
|
**Componentes UI:**
|
|
184
|
+
|
|
169
185
|
```typescript
|
|
170
186
|
// ❌ ERRADO
|
|
171
187
|
import { Dialog } from '@/components/ui/dialog'
|
|
@@ -177,6 +193,7 @@ import { Button } from 'forlogic-core'
|
|
|
177
193
|
```
|
|
178
194
|
|
|
179
195
|
### 📋 Checklist Antes de Criar Novo Arquivo
|
|
196
|
+
|
|
180
197
|
```markdown
|
|
181
198
|
- [ ] Este componente/util JÁ existe no forlogic-core?
|
|
182
199
|
- [ ] Verifiquei a lista de exports disponíveis abaixo?
|
|
@@ -186,15 +203,16 @@ import { Button } from 'forlogic-core'
|
|
|
186
203
|
### 📦 Componentes e Utils Disponíveis no forlogic-core
|
|
187
204
|
|
|
188
205
|
**🎨 Componentes UI:**
|
|
206
|
+
|
|
189
207
|
```typescript
|
|
190
208
|
// Formulários
|
|
191
|
-
Button, Input, Textarea, Label, Select, SelectContent,
|
|
192
|
-
SelectItem, SelectTrigger, SelectValue, Checkbox, RadioGroup,
|
|
209
|
+
Button, Input, Textarea, Label, Select, SelectContent,
|
|
210
|
+
SelectItem, SelectTrigger, SelectValue, Checkbox, RadioGroup,
|
|
193
211
|
RadioGroupItem, Switch
|
|
194
212
|
|
|
195
213
|
// Layout
|
|
196
214
|
Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle
|
|
197
|
-
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader,
|
|
215
|
+
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader,
|
|
198
216
|
DialogTitle, DialogTrigger
|
|
199
217
|
Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle
|
|
200
218
|
Separator, ScrollArea
|
|
@@ -216,6 +234,7 @@ Table, TableBody, TableCell, TableHead, TableHeader, TableRow
|
|
|
216
234
|
```
|
|
217
235
|
|
|
218
236
|
**🛠️ Utils e Hooks:**
|
|
237
|
+
|
|
219
238
|
```typescript
|
|
220
239
|
// Utils
|
|
221
240
|
cn // Merge classes Tailwind
|
|
@@ -246,6 +265,7 @@ errorService // Service de erros
|
|
|
246
265
|
### 🔍 Como Verificar Exports Disponíveis
|
|
247
266
|
|
|
248
267
|
Se não tiver certeza se algo existe no forlogic-core:
|
|
268
|
+
|
|
249
269
|
1. Abra `node_modules/forlogic-core/package.json`
|
|
250
270
|
2. Verifique os exports disponíveis
|
|
251
271
|
3. Em caso de dúvida, pergunte ao usuário antes de criar localmente
|
|
@@ -259,6 +279,7 @@ Se não tiver certeza se algo existe no forlogic-core:
|
|
|
259
279
|
**Quando usar?**
|
|
260
280
|
|
|
261
281
|
Use o `EntitySelect` para criar dropdowns com busca e ordenação para qualquer entidade do sistema:
|
|
282
|
+
|
|
262
283
|
- ✅ Seleção de processos, departamentos, categorias, etc
|
|
263
284
|
- ✅ Busca em tempo real
|
|
264
285
|
- ✅ Ordenação alfabética automática
|
|
@@ -292,22 +313,22 @@ function MyForm() {
|
|
|
292
313
|
|
|
293
314
|
**Props do EntitySelect:**
|
|
294
315
|
|
|
295
|
-
| Prop
|
|
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,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
|
|
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
|
|
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**
|
|
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
|
|
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
|
|
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
|
|
2612
|
-
|
|
2613
|
-
| `eq`
|
|
2614
|
-
| `neq`
|
|
2615
|
-
| `gt`
|
|
2616
|
-
| `gte`
|
|
2617
|
-
| `lt`
|
|
2618
|
-
| `lte`
|
|
2619
|
-
| `in`
|
|
2620
|
-
| `contains` | Contém texto
|
|
2621
|
-
| `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
|