forlogic-core 1.6.12 → 1.7.0
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 +1552 -58
- package/dist/README.md +1552 -58
- 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/assets/index-DdME9Z_4.css +0 -1
- package/dist/assets/index-zmVOoemO.js +0 -7939
- package/dist/index.html +0 -19
package/README.md
CHANGED
|
@@ -220,6 +220,7 @@ Table, TableBody, TableCell, TableHead, TableHeader, TableRow
|
|
|
220
220
|
// Utils
|
|
221
221
|
cn // Merge classes Tailwind
|
|
222
222
|
formatDate, formatDatetime // Formatação de datas
|
|
223
|
+
handleExternalLink // Helper para links externos
|
|
223
224
|
|
|
224
225
|
// Auth
|
|
225
226
|
useAuth // Hook de autenticação
|
|
@@ -237,7 +238,6 @@ TokenManager // Gerenciamento de tokens
|
|
|
237
238
|
createSimpleService // Criar service CRUD
|
|
238
239
|
createCrudPage // Criar página CRUD
|
|
239
240
|
generateCrudConfig // Gerar config CRUD
|
|
240
|
-
createSimpleSaveHandler // Handler de save
|
|
241
241
|
|
|
242
242
|
// Errors
|
|
243
243
|
errorService // Service de erros
|
|
@@ -455,6 +455,222 @@ export function DepartmentSelect(props: DepartmentSelectProps) {
|
|
|
455
455
|
|
|
456
456
|
---
|
|
457
457
|
|
|
458
|
+
## 🏗️ ARQUITETURA CRUD
|
|
459
|
+
|
|
460
|
+
O sistema CRUD do forlogic-core segue uma arquitetura em camadas que separa responsabilidades e facilita manutenção:
|
|
461
|
+
|
|
462
|
+
```mermaid
|
|
463
|
+
graph TD
|
|
464
|
+
A[types.ts<br/>Interfaces TypeScript] --> B[Service.ts<br/>createSimpleService]
|
|
465
|
+
B --> C[useCrudHook<br/>React Query]
|
|
466
|
+
C --> D[Page.tsx<br/>Componente Principal]
|
|
467
|
+
D --> E[createCrudPage<br/>Gerador de CRUD]
|
|
468
|
+
E --> F[CrudTable<br/>Tabela]
|
|
469
|
+
E --> G[BaseForm<br/>Formulário]
|
|
470
|
+
E --> H[BulkActionBar<br/>Ações em Massa]
|
|
471
|
+
|
|
472
|
+
I[(Supabase DB)] -.->|RLS + Soft Delete| B
|
|
473
|
+
J[Qualiex API] -.->|responsible_name| B
|
|
474
|
+
|
|
475
|
+
style A fill:#e1f5ff
|
|
476
|
+
style B fill:#fff4e1
|
|
477
|
+
style C fill:#ffe1f5
|
|
478
|
+
style D fill:#e1ffe1
|
|
479
|
+
style E fill:#f5e1ff
|
|
480
|
+
style F fill:#ffe1e1
|
|
481
|
+
style G fill:#ffe1e1
|
|
482
|
+
style H fill:#ffe1e1
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### **📁 Estrutura de Pastas Obrigatória**
|
|
486
|
+
|
|
487
|
+
```
|
|
488
|
+
src/
|
|
489
|
+
├── examples/ # 📁 Módulo completo
|
|
490
|
+
│ ├── example.ts # 🔷 Types (Entity + Payloads)
|
|
491
|
+
│ ├── ExampleService.ts # ⚙️ Service (createSimpleService)
|
|
492
|
+
│ ├── ExamplesPage.tsx # 🎨 Página CRUD
|
|
493
|
+
│ └── components/ # 📦 Componentes específicos (opcional)
|
|
494
|
+
│ └── ExampleSelect.tsx
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### **✅ Checklist de Implementação**
|
|
498
|
+
|
|
499
|
+
```
|
|
500
|
+
✅ OBRIGATÓRIOS:
|
|
501
|
+
□ types.ts - Definir Example extends BaseEntity com campos explícitos
|
|
502
|
+
□ types.ts - Usar Omit<> e Partial<> para CreatePayload e UpdatePayload
|
|
503
|
+
□ Service.ts - Usar createSimpleService<Example, CreatePayload, UpdatePayload>
|
|
504
|
+
□ Service.ts - Configurar tableName, entityName, searchFields
|
|
505
|
+
□ Page.tsx - Chamar useExamplesCrud() DENTRO do componente
|
|
506
|
+
□ Page.tsx - Usar useMemo() para columns, formSections, config
|
|
507
|
+
□ Page.tsx - Usar manager.save() ao invés de createSimpleSaveHandler
|
|
508
|
+
|
|
509
|
+
🔧 OPCIONAIS:
|
|
510
|
+
□ Filtros customizados (backend via additionalFilters recomendado)
|
|
511
|
+
□ onToggleStatus para ativar/desativar
|
|
512
|
+
□ Renderização customizada nas colunas
|
|
513
|
+
□ Campos de formulário customizados
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### **🔄 Fluxo de Dados**
|
|
517
|
+
|
|
518
|
+
1. **types.ts**: Define interfaces TypeScript (Entity, CreatePayload, UpdatePayload)
|
|
519
|
+
2. **Service.ts**: Gera service com `createSimpleService` (métodos CRUD + hook)
|
|
520
|
+
3. **useCrudHook**: Hook React Query que gerencia estado, cache, loading
|
|
521
|
+
4. **Page.tsx**: Componente que usa o hook e define configuração visual
|
|
522
|
+
5. **createCrudPage**: Gera componente de página completo (tabela + formulário)
|
|
523
|
+
6. **UI Components**: Renderiza CrudTable, CrudForm, BulkActionBar, etc.
|
|
524
|
+
|
|
525
|
+
### **🔐 Integrações Automáticas**
|
|
526
|
+
|
|
527
|
+
- 🔒 **RLS (Row Level Security)**: Filtra por `alias`
|
|
528
|
+
- 🗑️ **Soft Delete**: Marca `is_removed = true` ao invés de deletar
|
|
529
|
+
- 👤 **Qualiex Enrichment**: Adiciona `responsible_name` aos registros
|
|
530
|
+
- 🔍 **Busca Global**: Header com busca automática integrada ao CRUD
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## 🔍 Busca Global no Header
|
|
535
|
+
|
|
536
|
+
O `AppHeader` inclui uma barra de busca integrada que funciona automaticamente com o sistema CRUD.
|
|
537
|
+
|
|
538
|
+
### **Como Funciona:**
|
|
539
|
+
|
|
540
|
+
1. **Ativação Automática**: A busca aparece automaticamente em páginas CRUD quando `isSearchVisible = true` no AuthContext
|
|
541
|
+
2. **Debounce**: Usa delay de 500ms (configurável via `SEARCH_CONFIG.debounceDelay`) para evitar queries excessivas
|
|
542
|
+
3. **URL Sync**: Mantém o termo de busca sincronizado na URL (`?search=termo`)
|
|
543
|
+
4. **Reset de Paginação**: Ao buscar, reseta automaticamente para página 1
|
|
544
|
+
5. **Botão Refresh**: Atualiza os dados da página atual
|
|
545
|
+
|
|
546
|
+
### **Ativar Busca em uma Página:**
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
// No AuthContext ou componente pai
|
|
550
|
+
const [isSearchVisible, setIsSearchVisible] = useState(true);
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### **Integração com CRUD:**
|
|
554
|
+
|
|
555
|
+
O hook `useCrud` já lê automaticamente o parâmetro `search` da URL:
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
// lib/crud/hooks/useCrud.ts
|
|
559
|
+
const [searchParams] = useSearchParams();
|
|
560
|
+
const searchTerm = searchParams.get('search') || '';
|
|
561
|
+
|
|
562
|
+
// A busca é aplicada automaticamente nos searchFields configurados no service
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### **Configurar Campos de Busca:**
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
// src/examples/ExampleService.ts
|
|
569
|
+
export const { service, useCrudHook } = createSimpleService<Example>({
|
|
570
|
+
tableName: 'examples',
|
|
571
|
+
entityName: 'exemplo',
|
|
572
|
+
schemaName: 'central',
|
|
573
|
+
searchFields: ['title', 'description', 'tags'], // 🔍 Campos pesquisáveis
|
|
574
|
+
});
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### **Customizar Placeholder:**
|
|
578
|
+
|
|
579
|
+
O placeholder da busca se adapta automaticamente à rota. Para customizar:
|
|
580
|
+
|
|
581
|
+
```tsx
|
|
582
|
+
// AppHeader.tsx - Modificação direta (afeta todas as rotas)
|
|
583
|
+
<Input
|
|
584
|
+
placeholder={location.pathname === '/wiki' ? "Buscar artigos..." : "Buscar..."}
|
|
585
|
+
/>
|
|
586
|
+
|
|
587
|
+
// OU usar metadata (recomendado para páginas específicas)
|
|
588
|
+
import { usePageMetadataContext } from 'forlogic-core';
|
|
589
|
+
|
|
590
|
+
export default function MyPage() {
|
|
591
|
+
const { setMetadata } = usePageMetadataContext();
|
|
592
|
+
|
|
593
|
+
useEffect(() => {
|
|
594
|
+
setMetadata({
|
|
595
|
+
title: 'Meus Itens',
|
|
596
|
+
subtitle: 'Gerencie seus itens',
|
|
597
|
+
// Futura feature: searchPlaceholder: 'Buscar por nome...'
|
|
598
|
+
});
|
|
599
|
+
}, []);
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### **Configuração Avançada:**
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
// lib/config/index.ts
|
|
607
|
+
export const SEARCH_CONFIG = {
|
|
608
|
+
debounceDelay: 500, // ms - ajuste conforme necessidade
|
|
609
|
+
} as const;
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
**Quando aumentar o delay:**
|
|
613
|
+
- ✅ Muitos usuários simultâneos (reduz carga no servidor)
|
|
614
|
+
- ✅ Campos de busca muito amplos (muitos registros)
|
|
615
|
+
- ✅ Backend com rate limiting
|
|
616
|
+
|
|
617
|
+
**Quando reduzir o delay:**
|
|
618
|
+
- ✅ Poucos registros (resposta instantânea)
|
|
619
|
+
- ✅ Busca crítica para UX (feedback imediato)
|
|
620
|
+
|
|
621
|
+
### **Controle Programático:**
|
|
622
|
+
|
|
623
|
+
```typescript
|
|
624
|
+
// Limpar busca programaticamente
|
|
625
|
+
import { useSearchParams } from 'react-router-dom';
|
|
626
|
+
|
|
627
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
628
|
+
|
|
629
|
+
const clearSearch = () => {
|
|
630
|
+
const newParams = new URLSearchParams(searchParams);
|
|
631
|
+
newParams.delete('search');
|
|
632
|
+
newParams.delete('page');
|
|
633
|
+
setSearchParams(newParams);
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// Definir busca programaticamente
|
|
637
|
+
const setSearch = (term: string) => {
|
|
638
|
+
const newParams = new URLSearchParams(searchParams);
|
|
639
|
+
newParams.set('search', term);
|
|
640
|
+
newParams.set('page', '1'); // Reset para primeira página
|
|
641
|
+
setSearchParams(newParams);
|
|
642
|
+
};
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### **Refresh Manual:**
|
|
646
|
+
|
|
647
|
+
O botão de refresh chama a função `refreshData` do AuthContext:
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
// Implementar no seu AuthContext
|
|
651
|
+
const refreshData = useCallback(() => {
|
|
652
|
+
queryClient.invalidateQueries(); // Invalida cache do React Query
|
|
653
|
+
toast.success('Dados atualizados');
|
|
654
|
+
}, [queryClient]);
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### **Arquitetura da Busca:**
|
|
658
|
+
|
|
659
|
+
```mermaid
|
|
660
|
+
graph LR
|
|
661
|
+
A[Usuário digita] --> B[useState local]
|
|
662
|
+
B --> C[useDebounce 500ms]
|
|
663
|
+
C --> D[URL ?search=termo]
|
|
664
|
+
D --> E[useCrud lê URL]
|
|
665
|
+
E --> F[Supabase Query]
|
|
666
|
+
F --> G[Resultados filtrados]
|
|
667
|
+
|
|
668
|
+
style C fill:#d4f4dd
|
|
669
|
+
style F fill:#ffd4d4
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
458
674
|
## 🚀 QUICK START - Criar CRUD Completo
|
|
459
675
|
|
|
460
676
|
### **1️⃣ Type**
|
|
@@ -491,24 +707,27 @@ export const { service: processService, useCrudHook: useProcesses } =
|
|
|
491
707
|
});
|
|
492
708
|
```
|
|
493
709
|
|
|
494
|
-
### **3️⃣ Save Handler**
|
|
710
|
+
### **3️⃣ Save Handler (Integrado ao useCrud)**
|
|
495
711
|
```typescript
|
|
496
712
|
// src/processes/ProcessesPage.tsx
|
|
497
|
-
import { createSimpleSaveHandler } from 'forlogic-core';
|
|
498
|
-
import { processService } from './processService';
|
|
499
|
-
|
|
500
|
-
const handleSave = createSimpleSaveHandler({
|
|
501
|
-
service: processService,
|
|
502
|
-
entityName: 'processo'
|
|
503
|
-
});
|
|
504
713
|
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
714
|
+
export default function ProcessesPage() {
|
|
715
|
+
const manager = useProcesses();
|
|
716
|
+
|
|
717
|
+
const handleSave = (data: any) => {
|
|
718
|
+
manager.save(data, (d) => ({
|
|
719
|
+
title: d.title,
|
|
720
|
+
description: d.description || null,
|
|
721
|
+
status: d.status || 'draft'
|
|
722
|
+
}));
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// O método save() automaticamente:
|
|
726
|
+
// ✅ Detecta se é CREATE (sem id) ou UPDATE (com id)
|
|
727
|
+
// ✅ Injeta o alias no CREATE
|
|
728
|
+
// ✅ Chama createEntity ou updateEntity
|
|
729
|
+
// ✅ Fecha o modal automaticamente após sucesso
|
|
730
|
+
}
|
|
512
731
|
```
|
|
513
732
|
|
|
514
733
|
### **4️⃣ Config (com useMemo)**
|
|
@@ -571,6 +790,282 @@ function ProcessLayout() {
|
|
|
571
790
|
|
|
572
791
|
---
|
|
573
792
|
|
|
793
|
+
## 🔄 GUIA DE MIGRAÇÃO - v2.0
|
|
794
|
+
|
|
795
|
+
Se você tem projetos usando versões antigas do `forlogic-core`, siga este guia para atualizar.
|
|
796
|
+
|
|
797
|
+
### **📦 Atualizar Versão**
|
|
798
|
+
|
|
799
|
+
```bash
|
|
800
|
+
npm install forlogic-core@latest
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
---
|
|
804
|
+
|
|
805
|
+
### **1️⃣ Migração de Tipos (Breaking Change)**
|
|
806
|
+
|
|
807
|
+
#### **❌ ANTES (Versão Antiga):**
|
|
808
|
+
```typescript
|
|
809
|
+
import {
|
|
810
|
+
ContentEntity,
|
|
811
|
+
VisualEntity,
|
|
812
|
+
UserRelatedEntity,
|
|
813
|
+
ActivableEntity,
|
|
814
|
+
FormEntity
|
|
815
|
+
} from 'forlogic-core';
|
|
816
|
+
|
|
817
|
+
export interface Example extends
|
|
818
|
+
ContentEntity,
|
|
819
|
+
VisualEntity,
|
|
820
|
+
UserRelatedEntity,
|
|
821
|
+
ActivableEntity,
|
|
822
|
+
FormEntity {}
|
|
823
|
+
|
|
824
|
+
export interface CreateExamplePayload {
|
|
825
|
+
title: string;
|
|
826
|
+
description?: string | null;
|
|
827
|
+
alias: string; // ⚠️ Obrigatório manualmente
|
|
828
|
+
color?: string;
|
|
829
|
+
icon_name?: string;
|
|
830
|
+
id_user?: string | null;
|
|
831
|
+
is_actived?: boolean;
|
|
832
|
+
url_field?: string | null;
|
|
833
|
+
date_field?: string | null;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
export interface UpdateExamplePayload extends Partial<CreateExamplePayload> {
|
|
837
|
+
title: string; // Override
|
|
838
|
+
}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
#### **✅ DEPOIS (Nova API):**
|
|
842
|
+
```typescript
|
|
843
|
+
import { BaseEntity } from 'forlogic-core';
|
|
844
|
+
|
|
845
|
+
export interface Example extends BaseEntity {
|
|
846
|
+
title: string;
|
|
847
|
+
description?: string | null;
|
|
848
|
+
color?: string;
|
|
849
|
+
icon_name?: string;
|
|
850
|
+
id_user?: string | null;
|
|
851
|
+
responsible_name?: string;
|
|
852
|
+
url_field?: string | null;
|
|
853
|
+
date_field?: string | null;
|
|
854
|
+
// is_actived agora vem de BaseEntity!
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
export type CreateExamplePayload = Omit<
|
|
858
|
+
Example,
|
|
859
|
+
keyof BaseEntity | 'responsible_name'
|
|
860
|
+
>;
|
|
861
|
+
|
|
862
|
+
export type UpdateExamplePayload = Partial<CreateExamplePayload>;
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
**📝 Mudanças:**
|
|
866
|
+
- ❌ **REMOVIDO**: Helper interfaces (`ContentEntity`, `VisualEntity`, etc)
|
|
867
|
+
- ✅ **NOVO**: Apenas `BaseEntity` + campos explícitos
|
|
868
|
+
- ✅ **NOVO**: `is_actived` agora é campo padrão de `BaseEntity`
|
|
869
|
+
- ✅ **NOVO**: Use `Omit<>` e `Partial<>` para Payloads
|
|
870
|
+
|
|
871
|
+
---
|
|
872
|
+
|
|
873
|
+
### **2️⃣ Migração de Save Handler (Breaking Change)**
|
|
874
|
+
|
|
875
|
+
#### **❌ ANTES (createSimpleSaveHandler):**
|
|
876
|
+
```typescript
|
|
877
|
+
import { createSimpleSaveHandler, useAuth } from 'forlogic-core';
|
|
878
|
+
|
|
879
|
+
const { alias: currentAlias } = useAuth();
|
|
880
|
+
|
|
881
|
+
const handleSave = createSimpleSaveHandler(
|
|
882
|
+
manager,
|
|
883
|
+
// createTransform
|
|
884
|
+
(data) => ({
|
|
885
|
+
title: data.title,
|
|
886
|
+
description: data.description || null,
|
|
887
|
+
alias: currentAlias // ⚠️ Injetar manualmente
|
|
888
|
+
}),
|
|
889
|
+
// updateTransform
|
|
890
|
+
(data) => ({
|
|
891
|
+
title: data.title,
|
|
892
|
+
description: data.description || null
|
|
893
|
+
})
|
|
894
|
+
);
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
#### **✅ DEPOIS (manager.save):**
|
|
898
|
+
```typescript
|
|
899
|
+
const handleSave = (data: any) => {
|
|
900
|
+
manager.save(data, (d) => ({
|
|
901
|
+
title: d.title,
|
|
902
|
+
description: d.description || null
|
|
903
|
+
}));
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
// O método save() faz automaticamente:
|
|
907
|
+
// ✅ Detecta CREATE vs UPDATE (baseado em data.id)
|
|
908
|
+
// ✅ Injeta alias no CREATE
|
|
909
|
+
// ✅ Fecha modal após sucesso
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
**📝 Mudanças:**
|
|
913
|
+
- ❌ **REMOVIDO**: `createSimpleSaveHandler`
|
|
914
|
+
- ❌ **REMOVIDO**: Import de `useAuth` para pegar `alias`
|
|
915
|
+
- ✅ **NOVO**: `manager.save(data, transform)`
|
|
916
|
+
- ✅ **NOVO**: Alias injetado automaticamente
|
|
917
|
+
|
|
918
|
+
---
|
|
919
|
+
|
|
920
|
+
### **3️⃣ Migração de Campos de Formulário**
|
|
921
|
+
|
|
922
|
+
#### **❌ ANTES (Tipos Múltiplos):**
|
|
923
|
+
```typescript
|
|
924
|
+
{
|
|
925
|
+
name: 'id_user',
|
|
926
|
+
label: 'Responsável',
|
|
927
|
+
type: 'simple-qualiex-user-field', // OU 'single-responsible-select'
|
|
928
|
+
required: true
|
|
929
|
+
}
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
#### **✅ DEPOIS (Tipo Unificado):**
|
|
933
|
+
```typescript
|
|
934
|
+
{
|
|
935
|
+
name: 'id_user',
|
|
936
|
+
label: 'Responsável',
|
|
937
|
+
type: 'user-select',
|
|
938
|
+
mode: 'single', // OU 'multiple'
|
|
939
|
+
required: true
|
|
940
|
+
}
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
**📝 Mudanças:**
|
|
944
|
+
- ❌ **REMOVIDO**: `'simple-qualiex-user-field'`, `'single-responsible-select'`
|
|
945
|
+
- ✅ **NOVO**: Apenas `'user-select'` com parâmetro `mode`
|
|
946
|
+
|
|
947
|
+
---
|
|
948
|
+
|
|
949
|
+
### **4️⃣ Migração de Imports**
|
|
950
|
+
|
|
951
|
+
#### **❌ ANTES:**
|
|
952
|
+
```typescript
|
|
953
|
+
import { createSimpleSaveHandler } from 'forlogic-core';
|
|
954
|
+
import { ContentEntity, VisualEntity, ... } from 'forlogic-core';
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
#### **✅ DEPOIS:**
|
|
958
|
+
```typescript
|
|
959
|
+
// createSimpleSaveHandler removido (usar manager.save)
|
|
960
|
+
import { BaseEntity } from 'forlogic-core';
|
|
961
|
+
import { handleExternalLink } from 'forlogic-core'; // NOVO helper
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
**📝 Mudanças:**
|
|
965
|
+
- ❌ **REMOVIDO**: `createSimpleSaveHandler`
|
|
966
|
+
- ❌ **REMOVIDO**: Helper interfaces de tipos
|
|
967
|
+
- ✅ **NOVO**: `handleExternalLink` (helper de links externos)
|
|
968
|
+
|
|
969
|
+
---
|
|
970
|
+
|
|
971
|
+
### **5️⃣ Migração de Filtros Customizados**
|
|
972
|
+
|
|
973
|
+
#### **❌ ANTES (Filtro Frontend):**
|
|
974
|
+
```typescript
|
|
975
|
+
const [statusFilter, setStatusFilter] = useState('active');
|
|
976
|
+
|
|
977
|
+
const filteredEntities = useMemo(() => {
|
|
978
|
+
return manager.entities.filter(e =>
|
|
979
|
+
statusFilter === 'all' ? true : e.is_actived
|
|
980
|
+
);
|
|
981
|
+
}, [manager.entities, statusFilter]);
|
|
982
|
+
|
|
983
|
+
const filteredManager = useMemo(() => ({
|
|
984
|
+
...manager,
|
|
985
|
+
entities: filteredEntities
|
|
986
|
+
}), [manager, filteredEntities]);
|
|
987
|
+
|
|
988
|
+
// Passar filteredManager para createCrudPage
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
#### **✅ DEPOIS (Filtro Backend - Recomendado):**
|
|
992
|
+
```typescript
|
|
993
|
+
const [statusFilter, setStatusFilter] = useState<boolean | 'all'>(true);
|
|
994
|
+
|
|
995
|
+
// Passar filtro direto para o hook
|
|
996
|
+
const manager = useExamplesCrud(
|
|
997
|
+
statusFilter === 'all' ? {} : { is_actived: statusFilter }
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
// Passar manager original (já vem filtrado)
|
|
1001
|
+
const CrudPage = createCrudPage({ manager, config, onSave });
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
**📝 Mudanças:**
|
|
1005
|
+
- ✅ **RECOMENDADO**: Filtro aplicado no backend (melhor performance)
|
|
1006
|
+
- ✅ **NOVO**: Hook aceita `additionalFilters` como parâmetro
|
|
1007
|
+
- ❌ **EVITAR**: Filtro frontend (só para casos complexos)
|
|
1008
|
+
|
|
1009
|
+
---
|
|
1010
|
+
|
|
1011
|
+
### **6️⃣ Checklist de Migração**
|
|
1012
|
+
|
|
1013
|
+
Use este checklist para validar que seu projeto foi migrado corretamente:
|
|
1014
|
+
|
|
1015
|
+
#### **Types (`example.ts`):**
|
|
1016
|
+
- [ ] Removido imports de helper interfaces (`ContentEntity`, `VisualEntity`, etc)
|
|
1017
|
+
- [ ] Interface principal agora estende apenas `BaseEntity`
|
|
1018
|
+
- [ ] Campos explícitos declarados na interface
|
|
1019
|
+
- [ ] `CreatePayload` usa `Omit<Example, keyof BaseEntity | 'responsible_name'>`
|
|
1020
|
+
- [ ] `UpdatePayload` usa `Partial<CreatePayload>`
|
|
1021
|
+
- [ ] Removido `alias: string` do `CreatePayload`
|
|
1022
|
+
- [ ] Removido interfaces/types não usados (`ExampleFilters`, `ExampleSortField`, `ExampleInsert`, `ExampleUpdate`)
|
|
1023
|
+
|
|
1024
|
+
#### **Service (`ExampleService.ts`):**
|
|
1025
|
+
- [ ] Nenhuma mudança necessária (API permanece igual)
|
|
1026
|
+
|
|
1027
|
+
#### **Page (`ExamplesPage.tsx`):**
|
|
1028
|
+
- [ ] Removido import de `createSimpleSaveHandler`
|
|
1029
|
+
- [ ] Removido import de `useAuth` (se usado apenas para alias)
|
|
1030
|
+
- [ ] Substituído `createSimpleSaveHandler` por `manager.save()`
|
|
1031
|
+
- [ ] Campos de formulário tipo `'user-select'` ao invés de tipos antigos
|
|
1032
|
+
- [ ] Filtros usando backend (`useExamplesCrud(filters)`) quando possível
|
|
1033
|
+
- [ ] Substituído lógica de links externos por `handleExternalLink` helper
|
|
1034
|
+
|
|
1035
|
+
#### **Testes:**
|
|
1036
|
+
- [ ] Build sem erros TypeScript (`npm run build`)
|
|
1037
|
+
- [ ] Página carrega sem erros
|
|
1038
|
+
- [ ] Criar novo item funciona (alias injetado corretamente)
|
|
1039
|
+
- [ ] Editar item funciona
|
|
1040
|
+
- [ ] Deletar item funciona
|
|
1041
|
+
- [ ] Filtros funcionam corretamente
|
|
1042
|
+
- [ ] Paginação funciona
|
|
1043
|
+
- [ ] Busca funciona
|
|
1044
|
+
|
|
1045
|
+
---
|
|
1046
|
+
|
|
1047
|
+
### **7️⃣ Exemplo Completo de Migração**
|
|
1048
|
+
|
|
1049
|
+
**Ver arquivo `src/examples/ExamplesPage.tsx` do projeto para exemplo 100% atualizado.**
|
|
1050
|
+
|
|
1051
|
+
---
|
|
1052
|
+
|
|
1053
|
+
### **❓ Problemas na Migração?**
|
|
1054
|
+
|
|
1055
|
+
#### **Erro: "Property 'save' does not exist on type..."**
|
|
1056
|
+
- ✅ **Solução**: Atualize `forlogic-core` para versão mais recente
|
|
1057
|
+
- ✅ **Comando**: `npm install forlogic-core@latest`
|
|
1058
|
+
|
|
1059
|
+
#### **Erro: "Cannot find module 'ContentEntity'"**
|
|
1060
|
+
- ✅ **Solução**: Remova imports de helper interfaces e use `BaseEntity`
|
|
1061
|
+
- ✅ **Ver**: Seção "1️⃣ Migração de Tipos" acima
|
|
1062
|
+
|
|
1063
|
+
#### **Erro: "alias is required but not provided"**
|
|
1064
|
+
- ✅ **Solução**: Use `manager.save()` ao invés de `createEntity` direto
|
|
1065
|
+
- ✅ **Ver**: Seção "2️⃣ Migração de Save Handler" acima
|
|
1066
|
+
|
|
1067
|
+
---
|
|
1068
|
+
|
|
574
1069
|
## 🎯 AÇÕES EM LOTE (Bulk Actions)
|
|
575
1070
|
|
|
576
1071
|
O sistema CRUD suporta seleção múltipla e ações em lote usando checkboxes.
|
|
@@ -728,69 +1223,1068 @@ export function ProcessesPage() {
|
|
|
728
1223
|
|
|
729
1224
|
---
|
|
730
1225
|
|
|
731
|
-
|
|
1226
|
+
## 🎓 TUTORIAL COMPLETO: CRUD de Examples (Copy-Paste Ready)
|
|
732
1227
|
|
|
733
|
-
**
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const processes = await processService.getAll();
|
|
737
|
-
// processes[0].usuario_nome = "João Silva" (se enableQualiexEnrichment: true)
|
|
738
|
-
```
|
|
1228
|
+
Este tutorial mostra como criar um CRUD completo usando o módulo **Examples** como referência. É um exemplo funcional 100% que você pode copiar e adaptar.
|
|
1229
|
+
|
|
1230
|
+
### **Passo 1: Criar Types (`src/examples/example.ts`)**
|
|
739
1231
|
|
|
740
|
-
**Componentes prontos:**
|
|
741
1232
|
```typescript
|
|
742
|
-
|
|
1233
|
+
// ============= EXAMPLE MODULE TYPES =============
|
|
1234
|
+
import {
|
|
1235
|
+
ContentEntity, // title, description
|
|
1236
|
+
VisualEntity, // color, icon_name
|
|
1237
|
+
UserRelatedEntity, // id_user, responsible_name
|
|
1238
|
+
ActivableEntity, // is_actived
|
|
1239
|
+
FormEntity, // url_field, date_field
|
|
1240
|
+
FilterState,
|
|
1241
|
+
EntitySortField
|
|
1242
|
+
} from 'forlogic-core';
|
|
743
1243
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1244
|
+
/**
|
|
1245
|
+
* Example - Entidade completa de exemplo
|
|
1246
|
+
*
|
|
1247
|
+
* ✅ Campos Customizados:
|
|
1248
|
+
* - title, description (conteúdo)
|
|
1249
|
+
* - color, icon_name (visual)
|
|
1250
|
+
* - id_user, responsible_name (usuário - enriquecido via Qualiex)
|
|
1251
|
+
* - url_field, date_field (formulário)
|
|
1252
|
+
*
|
|
1253
|
+
* 🔒 Campos Herdados de BaseEntity (automáticos):
|
|
1254
|
+
* - id: string
|
|
1255
|
+
* - alias: string
|
|
1256
|
+
* - company_id: string
|
|
1257
|
+
* - is_actived: boolean
|
|
1258
|
+
* - is_removed: boolean
|
|
1259
|
+
* - created_at: string
|
|
1260
|
+
* - updated_at: string
|
|
1261
|
+
*/
|
|
1262
|
+
export interface Example extends BaseEntity {
|
|
1263
|
+
title: string;
|
|
1264
|
+
description?: string | null;
|
|
1265
|
+
color?: string;
|
|
1266
|
+
icon_name?: string;
|
|
1267
|
+
id_user?: string | null;
|
|
1268
|
+
responsible_name?: string; // Enriquecido via Qualiex
|
|
1269
|
+
url_field?: string | null;
|
|
1270
|
+
date_field?: string | null;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* CreateExamplePayload - Dados para CRIAR novo registro
|
|
1275
|
+
*
|
|
1276
|
+
* ⚠️ IMPORTANTE:
|
|
1277
|
+
* - Campo `alias` é injetado AUTOMATICAMENTE pelo manager.save()
|
|
1278
|
+
* - Campos opcionais devem ter `| null`
|
|
1279
|
+
* - NÃO incluir id, created_at, updated_at (gerados automaticamente)
|
|
1280
|
+
*/
|
|
1281
|
+
export type CreateExamplePayload = Omit<
|
|
1282
|
+
Example,
|
|
1283
|
+
keyof BaseEntity | 'responsible_name'
|
|
1284
|
+
>;
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* UpdateExamplePayload - Dados para ATUALIZAR registro existente
|
|
1288
|
+
*
|
|
1289
|
+
* 📝 Pattern:
|
|
1290
|
+
* - Todos os campos são opcionais (Partial)
|
|
1291
|
+
*/
|
|
1292
|
+
export type UpdateExamplePayload = Partial<CreateExamplePayload>;
|
|
749
1293
|
```
|
|
750
1294
|
|
|
751
|
-
|
|
1295
|
+
**📖 Explicação Detalhada:**
|
|
1296
|
+
- **Composição de Interfaces:** Ao invés de redefinir campos, herda de interfaces prontas da lib
|
|
1297
|
+
- **`alias` no CreatePayload:** RLS do Supabase precisa desse campo para funcionar
|
|
1298
|
+
- **`Partial<>` no UpdatePayload:** Permite updates parciais (só manda os campos que mudaram)
|
|
1299
|
+
- **Campos `| null`:** Importante para sincronizar com Supabase (que aceita NULL)
|
|
1300
|
+
|
|
1301
|
+
---
|
|
1302
|
+
|
|
1303
|
+
### **Passo 2: Criar Service (`src/examples/ExampleService.ts`)**
|
|
1304
|
+
|
|
752
1305
|
```typescript
|
|
753
|
-
//
|
|
754
|
-
{
|
|
755
|
-
name: 'responsible_id',
|
|
756
|
-
label: 'Responsável',
|
|
757
|
-
type: 'simple-qualiex-user-field' as const,
|
|
758
|
-
required: true
|
|
759
|
-
}
|
|
1306
|
+
// ============= SIMPLIFIED SERVICE (MIGRATED) =============
|
|
760
1307
|
|
|
761
|
-
|
|
762
|
-
{
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1308
|
+
import { createSimpleService } from 'forlogic-core';
|
|
1309
|
+
import type { Example, CreateExamplePayload, UpdateExamplePayload } from './example';
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* ExampleService - Service CRUD completo gerado automaticamente
|
|
1313
|
+
*
|
|
1314
|
+
* ✅ O que é gerado:
|
|
1315
|
+
* - service.getAll(params)
|
|
1316
|
+
* - service.getById(id)
|
|
1317
|
+
* - service.create(data)
|
|
1318
|
+
* - service.update(id, data)
|
|
1319
|
+
* - service.delete(id)
|
|
1320
|
+
* - useCrudHook() - Hook React Query integrado
|
|
1321
|
+
*
|
|
1322
|
+
* 🔧 Configuração:
|
|
1323
|
+
* - tableName: Nome da tabela no Supabase (schema: central)
|
|
1324
|
+
* - entityName: Nome legível para toasts ("Exemplo criado com sucesso")
|
|
1325
|
+
* - searchFields: Campos que serão pesquisados pelo filtro de busca
|
|
1326
|
+
* - enableQualiexEnrichment: true → adiciona responsible_name automaticamente
|
|
1327
|
+
*
|
|
1328
|
+
* 📊 Estrutura de Tabela Esperada (Supabase):
|
|
1329
|
+
* CREATE TABLE central.examples (
|
|
1330
|
+
* id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1331
|
+
* alias TEXT NOT NULL, -- ✅ Para RLS (obrigatório)
|
|
1332
|
+
* is_removed BOOLEAN DEFAULT false, -- ✅ Para soft delete (obrigatório)
|
|
1333
|
+
* created_at TIMESTAMPTZ DEFAULT now(),
|
|
1334
|
+
* updated_at TIMESTAMPTZ DEFAULT now(),
|
|
1335
|
+
* title TEXT NOT NULL,
|
|
1336
|
+
* description TEXT,
|
|
1337
|
+
* id_user TEXT,
|
|
1338
|
+
* is_actived BOOLEAN DEFAULT true,
|
|
1339
|
+
* color TEXT,
|
|
1340
|
+
* icon_name TEXT,
|
|
1341
|
+
* url_field TEXT,
|
|
1342
|
+
* date_field DATE
|
|
1343
|
+
* );
|
|
1344
|
+
*/
|
|
1345
|
+
export const { service: ExampleService, useCrudHook: useExamplesCrud } =
|
|
1346
|
+
createSimpleService<Example, CreateExamplePayload, UpdateExamplePayload>({
|
|
1347
|
+
tableName: 'examples', // 🗃️ Tabela no Supabase
|
|
1348
|
+
entityName: 'Exemplo', // 📣 Nome para mensagens
|
|
1349
|
+
searchFields: ['title'], // 🔍 Campos de busca textual
|
|
1350
|
+
schemaName: 'central', // 📂 Schema (default: 'central')
|
|
1351
|
+
enableQualiexEnrichment: true // 👤 Adiciona responsible_name
|
|
1352
|
+
});
|
|
768
1353
|
```
|
|
769
1354
|
|
|
770
|
-
|
|
1355
|
+
**📖 Explicação Detalhada:**
|
|
1356
|
+
- **Uma linha, tudo pronto:** `createSimpleService` gera todo o boilerplate
|
|
1357
|
+
- **Soft delete automático:** `deleteEntity()` marca `is_removed = true`, não deleta fisicamente
|
|
1358
|
+
- **RLS automático:** Filtra por `alias` automaticamente
|
|
1359
|
+
- **Enrichment Qualiex:** Busca o `responsible_name` na API Qualiex e adiciona aos registros
|
|
771
1360
|
|
|
772
|
-
|
|
1361
|
+
---
|
|
1362
|
+
|
|
1363
|
+
### **Passo 3: Criar Página (`src/examples/ExamplesPage.tsx`) - PARTE 1**
|
|
1364
|
+
|
|
1365
|
+
#### **Imports e Configuração de Colunas**
|
|
773
1366
|
|
|
774
1367
|
```typescript
|
|
775
|
-
//
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1368
|
+
// ============= EXAMPLES PAGE (MIGRATED TO NEW API) =============
|
|
1369
|
+
|
|
1370
|
+
import { useExamplesCrud } from './ExampleService';
|
|
1371
|
+
import {
|
|
1372
|
+
createCrudPage,
|
|
1373
|
+
createSimpleSaveHandler,
|
|
1374
|
+
formatDatetime,
|
|
1375
|
+
useAuth,
|
|
1376
|
+
type CrudColumn,
|
|
1377
|
+
EntitySelect
|
|
1378
|
+
} from 'forlogic-core';
|
|
1379
|
+
import type { Example, CreateExamplePayload, UpdateExamplePayload } from './example';
|
|
1380
|
+
import { ExternalLink, Star } from 'lucide-react';
|
|
1381
|
+
import { toast } from 'sonner';
|
|
1382
|
+
import * as LucideIcons from 'lucide-react';
|
|
1383
|
+
import { useState, useMemo } from 'react';
|
|
1384
|
+
|
|
1385
|
+
/**
|
|
1386
|
+
* ⚠️ IMPORTANTE: Importar `cn` da lib, NÃO do utils local
|
|
1387
|
+
* ❌ ERRADO: import { cn } from '@/lib/utils'
|
|
1388
|
+
* ✅ CORRETO: import { cn } from 'forlogic-core'
|
|
1389
|
+
*/
|
|
782
1390
|
```
|
|
783
1391
|
|
|
784
|
-
|
|
1392
|
+
---
|
|
1393
|
+
|
|
1394
|
+
### **Passo 4: Configuração do Formulário**
|
|
785
1395
|
|
|
786
|
-
**⚠️ CRÍTICO:** Requests Qualiex exigem header `un-alias`:
|
|
787
1396
|
```typescript
|
|
788
|
-
|
|
789
|
-
|
|
1397
|
+
/**
|
|
1398
|
+
* 📝 CONFIGURAÇÃO DO FORMULÁRIO
|
|
1399
|
+
*
|
|
1400
|
+
* Organizado em seções (formSections) com campos (fields).
|
|
1401
|
+
*
|
|
1402
|
+
* Tipos de campos suportados:
|
|
1403
|
+
* - 'text' - Input de texto simples
|
|
1404
|
+
* - 'email' - Input de email com validação
|
|
1405
|
+
* - 'textarea' - Área de texto grande
|
|
1406
|
+
* - 'select' - Dropdown com options
|
|
1407
|
+
* - 'color-picker' - Seletor de cor
|
|
1408
|
+
* - 'icon-picker' - Seletor de ícone Lucide
|
|
1409
|
+
* - 'user-select' - Seletor de usuário (com mode: 'single' | 'multiple')
|
|
1410
|
+
* - 'custom' - Campo completamente customizado
|
|
1411
|
+
* - 'group' - Agrupa campos horizontalmente
|
|
1412
|
+
*/
|
|
1413
|
+
const formSections = [{
|
|
1414
|
+
id: 'general',
|
|
1415
|
+
title: 'Informações Gerais',
|
|
1416
|
+
fields: [
|
|
1417
|
+
{
|
|
1418
|
+
// 🎯 GROUP: Agrupa campos horizontalmente
|
|
1419
|
+
type: 'group' as any,
|
|
1420
|
+
name: 'title-group',
|
|
1421
|
+
label: '',
|
|
1422
|
+
layout: 'horizontal' as const,
|
|
1423
|
+
className: 'grid grid-cols-1 sm:grid-cols-2 gap-6',
|
|
1424
|
+
fields: [
|
|
1425
|
+
{
|
|
1426
|
+
name: 'title',
|
|
1427
|
+
label: 'Título',
|
|
1428
|
+
type: 'text' as const,
|
|
1429
|
+
required: true,
|
|
1430
|
+
placeholder: 'Digite o título do exemplo',
|
|
1431
|
+
},
|
|
1432
|
+
{
|
|
1433
|
+
name: 'id_user',
|
|
1434
|
+
label: 'Responsável',
|
|
1435
|
+
type: 'user-select' as const, // 👤 Campo unificado
|
|
1436
|
+
mode: 'single',
|
|
1437
|
+
required: true,
|
|
1438
|
+
placeholder: 'Selecionar responsável',
|
|
1439
|
+
defaultValue: '',
|
|
1440
|
+
}
|
|
1441
|
+
],
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
name: 'url_field',
|
|
1445
|
+
label: 'Link',
|
|
1446
|
+
type: 'text' as const,
|
|
1447
|
+
required: false,
|
|
1448
|
+
placeholder: 'https://exemplo.com'
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
name: 'description',
|
|
1452
|
+
label: 'Descrição',
|
|
1453
|
+
type: 'textarea' as const,
|
|
1454
|
+
required: false,
|
|
1455
|
+
placeholder: 'Descrição detalhada do exemplo'
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
// 🎨 GROUP: Visual (cor + ícone)
|
|
1459
|
+
type: 'group' as any,
|
|
1460
|
+
name: 'visual-group',
|
|
1461
|
+
label: 'Visual',
|
|
1462
|
+
layout: 'horizontal' as const,
|
|
1463
|
+
className: 'grid grid-cols-1 sm:grid-cols-2 gap-6',
|
|
1464
|
+
fields: [
|
|
1465
|
+
{
|
|
1466
|
+
name: 'color',
|
|
1467
|
+
label: 'Cor',
|
|
1468
|
+
type: 'color-picker' as const, // 🎨 Color picker da lib
|
|
1469
|
+
required: false,
|
|
1470
|
+
defaultValue: '#3b82f6'
|
|
1471
|
+
},
|
|
1472
|
+
{
|
|
1473
|
+
name: 'icon_name',
|
|
1474
|
+
label: 'Ícone',
|
|
1475
|
+
type: 'icon-picker' as const, // 🔷 Icon picker da lib
|
|
1476
|
+
required: false,
|
|
1477
|
+
defaultValue: 'Star'
|
|
1478
|
+
}
|
|
1479
|
+
]
|
|
1480
|
+
}
|
|
1481
|
+
],
|
|
1482
|
+
}];
|
|
790
1483
|
```
|
|
791
1484
|
|
|
792
1485
|
---
|
|
793
1486
|
|
|
1487
|
+
### **Passo 5: Componente Principal com Filtros**
|
|
1488
|
+
|
|
1489
|
+
Ver código completo no arquivo `src/examples/ExamplesPage.tsx` do projeto.
|
|
1490
|
+
|
|
1491
|
+
**Estrutura básica:**
|
|
1492
|
+
1. Hooks no topo
|
|
1493
|
+
2. Estados de filtros com `useState`
|
|
1494
|
+
3. Estados derivados com `useMemo`
|
|
1495
|
+
4. Manager customizado com `useMemo`
|
|
1496
|
+
5. Handlers (`handleToggleStatus`, `handleSave`)
|
|
1497
|
+
6. Criar página com `createCrudPage`
|
|
1498
|
+
|
|
1499
|
+
---
|
|
1500
|
+
|
|
1501
|
+
## 🎯 PADRÕES DE FILTROS CUSTOMIZADOS
|
|
1502
|
+
|
|
1503
|
+
### **Pattern 1: Filtro de Status (Backend - Recomendado)**
|
|
1504
|
+
|
|
1505
|
+
```typescript
|
|
1506
|
+
// ✅ PADRÃO BACKEND: Filtro aplicado na query SQL (melhor performance)
|
|
1507
|
+
|
|
1508
|
+
// 1) Estado do filtro
|
|
1509
|
+
const [statusFilter, setStatusFilter] = useState<boolean | 'all'>(true);
|
|
1510
|
+
|
|
1511
|
+
// 2) Passar filtro para o hook (aplica no backend)
|
|
1512
|
+
const manager = useExamplesCrud(
|
|
1513
|
+
statusFilter === 'all' ? {} : { is_actived: statusFilter }
|
|
1514
|
+
);
|
|
1515
|
+
|
|
1516
|
+
// 3) Componente do filtro
|
|
1517
|
+
const StatusFilter = () => (
|
|
1518
|
+
<EntitySelect
|
|
1519
|
+
value={String(statusFilter)}
|
|
1520
|
+
onChange={(v) => setStatusFilter(v === 'all' ? 'all' : v === 'true')}
|
|
1521
|
+
items={[
|
|
1522
|
+
{ id: 'true', name: 'Ativo' },
|
|
1523
|
+
{ id: 'false', name: 'Inativo' },
|
|
1524
|
+
{ id: 'all', name: '[Todos]' }
|
|
1525
|
+
]}
|
|
1526
|
+
getItemValue={(item) => item.id}
|
|
1527
|
+
getItemLabel={(item) => item.name}
|
|
1528
|
+
placeholder="Status"
|
|
1529
|
+
className="w-full sm:w-[180px]"
|
|
1530
|
+
/>
|
|
1531
|
+
);
|
|
1532
|
+
|
|
1533
|
+
// 4) Usar em config.filters
|
|
1534
|
+
config: {
|
|
1535
|
+
filters: [
|
|
1536
|
+
{ type: 'search' },
|
|
1537
|
+
{ type: 'custom', component: StatusFilter }
|
|
1538
|
+
]
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// 5) Passar manager original (filtro já aplicado)
|
|
1542
|
+
const CrudPage = createCrudPage({
|
|
1543
|
+
manager, // ← Manager já vem filtrado do backend
|
|
1544
|
+
config,
|
|
1545
|
+
onSave
|
|
1546
|
+
});
|
|
1547
|
+
```
|
|
1548
|
+
|
|
1549
|
+
### **Pattern 1B: Filtro de Status (Frontend - Casos Específicos)**
|
|
1550
|
+
|
|
1551
|
+
```typescript
|
|
1552
|
+
// Use filtro frontend APENAS se:
|
|
1553
|
+
// ✅ Precisa combinar múltiplas propriedades (ex: status + tipo)
|
|
1554
|
+
// ✅ Lógica de filtro muito complexa para SQL
|
|
1555
|
+
// ❌ NÃO use para filtros simples (pior performance)
|
|
1556
|
+
|
|
1557
|
+
const [statusFilter, setStatusFilter] = useState<boolean | 'all'>(true);
|
|
1558
|
+
|
|
1559
|
+
const filteredEntities = useMemo(() => {
|
|
1560
|
+
if (statusFilter === 'all') return manager.entities;
|
|
1561
|
+
return manager.entities.filter(e => e.is_actived === statusFilter);
|
|
1562
|
+
}, [manager.entities, statusFilter]);
|
|
1563
|
+
|
|
1564
|
+
const filteredManager = useMemo(() => ({
|
|
1565
|
+
...manager,
|
|
1566
|
+
entities: filteredEntities,
|
|
1567
|
+
pagination: {
|
|
1568
|
+
...manager.pagination,
|
|
1569
|
+
totalItems: filteredEntities.length // ⚠️ Atualizar contador
|
|
1570
|
+
}
|
|
1571
|
+
}), [manager, filteredEntities]);
|
|
1572
|
+
```
|
|
1573
|
+
|
|
1574
|
+
**📖 Explicação:**
|
|
1575
|
+
- **`useState`**: Armazena valor selecionado no filtro
|
|
1576
|
+
- **`useMemo` (filteredEntities)**: Evita re-filtrar a cada render
|
|
1577
|
+
- **`useMemo` (filteredManager)**: Evita re-criar objeto manager
|
|
1578
|
+
- **`EntitySelect`**: Componente de dropdown da lib
|
|
1579
|
+
- **`filteredManager`**: Manager modificado que createCrudPage vai usar
|
|
1580
|
+
|
|
1581
|
+
---
|
|
1582
|
+
|
|
1583
|
+
### **Pattern 2: Filtro de Departamento (Select Nativo)**
|
|
1584
|
+
|
|
1585
|
+
```typescript
|
|
1586
|
+
// ✅ PADRÃO: Filtro por categoria/departamento
|
|
1587
|
+
|
|
1588
|
+
const [deptFilter, setDeptFilter] = useState<string>('all');
|
|
1589
|
+
|
|
1590
|
+
const filteredEntities = useMemo(() => {
|
|
1591
|
+
if (deptFilter === 'all') return manager.entities;
|
|
1592
|
+
return manager.entities.filter(e => e.department === deptFilter);
|
|
1593
|
+
}, [manager.entities, deptFilter]);
|
|
1594
|
+
|
|
1595
|
+
const filteredManager = useMemo(() => ({
|
|
1596
|
+
...manager,
|
|
1597
|
+
entities: filteredEntities
|
|
1598
|
+
}), [manager, filteredEntities]);
|
|
1599
|
+
|
|
1600
|
+
const DepartmentFilter = () => (
|
|
1601
|
+
<select
|
|
1602
|
+
value={deptFilter}
|
|
1603
|
+
onChange={(e) => setDeptFilter(e.target.value)}
|
|
1604
|
+
className="px-3 py-2 border rounded-md"
|
|
1605
|
+
>
|
|
1606
|
+
<option value="all">Todos Departamentos</option>
|
|
1607
|
+
<option value="HR">RH</option>
|
|
1608
|
+
<option value="IT">TI</option>
|
|
1609
|
+
<option value="Finance">Financeiro</option>
|
|
1610
|
+
</select>
|
|
1611
|
+
);
|
|
1612
|
+
```
|
|
1613
|
+
|
|
1614
|
+
---
|
|
1615
|
+
|
|
1616
|
+
### **Pattern 3: Filtro de Data Range (Date Picker)**
|
|
1617
|
+
|
|
1618
|
+
```typescript
|
|
1619
|
+
// ✅ PADRÃO: Filtro por intervalo de datas
|
|
1620
|
+
|
|
1621
|
+
import { format, isAfter, isBefore, parseISO } from 'date-fns';
|
|
1622
|
+
|
|
1623
|
+
const [dateRange, setDateRange] = useState<{ from?: Date; to?: Date }>({});
|
|
1624
|
+
|
|
1625
|
+
const filteredEntities = useMemo(() => {
|
|
1626
|
+
if (!dateRange.from && !dateRange.to) return manager.entities;
|
|
1627
|
+
|
|
1628
|
+
return manager.entities.filter(e => {
|
|
1629
|
+
const itemDate = parseISO(e.created_at);
|
|
1630
|
+
if (dateRange.from && isBefore(itemDate, dateRange.from)) return false;
|
|
1631
|
+
if (dateRange.to && isAfter(itemDate, dateRange.to)) return false;
|
|
1632
|
+
return true;
|
|
1633
|
+
});
|
|
1634
|
+
}, [manager.entities, dateRange]);
|
|
1635
|
+
|
|
1636
|
+
const filteredManager = useMemo(() => ({
|
|
1637
|
+
...manager,
|
|
1638
|
+
entities: filteredEntities
|
|
1639
|
+
}), [manager, filteredEntities]);
|
|
1640
|
+
```
|
|
1641
|
+
|
|
1642
|
+
---
|
|
1643
|
+
|
|
1644
|
+
## 🪝 HOOKS REACT NO CRUD
|
|
1645
|
+
|
|
1646
|
+
| Hook | Quando Usar | Exemplo no CRUD | ⚠️ Evitar |
|
|
1647
|
+
|------|-------------|-----------------|-----------|
|
|
1648
|
+
| **useMemo** | Cálculos pesados que dependem de props/state | • Configuração de colunas<br>• Filtros derivados<br>• Manager customizado | Valores simples (strings, números) |
|
|
1649
|
+
| **useState** | Valores que mudam via interação do usuário | • Filtros customizados<br>• Modal open/close<br>• Seleção temporária | Estados derivados de outros estados |
|
|
1650
|
+
| **useCallback** | Funções que são passadas como props e dependem de state | • Handlers que dependem de filtros<br>• Callbacks de child components | Handlers simples sem dependências |
|
|
1651
|
+
| **useEffect** | Side effects (fetch, subscriptions) | • Fetch inicial de dados (já feito pelo manager)<br>• Sincronização externa | Cálculos ou transformações de dados |
|
|
1652
|
+
|
|
1653
|
+
### **📖 Exemplos Práticos**
|
|
1654
|
+
|
|
1655
|
+
```typescript
|
|
1656
|
+
// ✅ CORRETO: useMemo para config (evita re-render do formulário)
|
|
1657
|
+
const config = useMemo(() => ({
|
|
1658
|
+
columns: [...],
|
|
1659
|
+
formSections: [...]
|
|
1660
|
+
}), []);
|
|
1661
|
+
|
|
1662
|
+
// ❌ ERRADO: sem useMemo (re-cria objeto a cada render)
|
|
1663
|
+
const config = {
|
|
1664
|
+
columns: [...],
|
|
1665
|
+
formSections: [...]
|
|
1666
|
+
};
|
|
1667
|
+
|
|
1668
|
+
// ✅ CORRETO: useState para filtro
|
|
1669
|
+
const [statusFilter, setStatusFilter] = useState('active');
|
|
1670
|
+
|
|
1671
|
+
// ❌ ERRADO: useMemo para valor que muda via interação
|
|
1672
|
+
const statusFilter = useMemo(() => 'active', []); // Não faz sentido!
|
|
1673
|
+
|
|
1674
|
+
// ✅ CORRETO: useMemo para estado derivado
|
|
1675
|
+
const filteredEntities = useMemo(() =>
|
|
1676
|
+
manager.entities.filter(e => e.is_actived),
|
|
1677
|
+
[manager.entities]
|
|
1678
|
+
);
|
|
1679
|
+
|
|
1680
|
+
// ❌ ERRADO: recalcula a cada render (lento)
|
|
1681
|
+
const filteredEntities = manager.entities.filter(e => e.is_actived);
|
|
1682
|
+
```
|
|
1683
|
+
|
|
1684
|
+
---
|
|
1685
|
+
|
|
1686
|
+
## ❌ ERROS COMUNS E SOLUÇÕES
|
|
1687
|
+
|
|
1688
|
+
### **Erro 1: Importar `cn` do lugar errado**
|
|
1689
|
+
|
|
1690
|
+
```typescript
|
|
1691
|
+
// ❌ SINTOMA: Classes CSS não aplicam
|
|
1692
|
+
// ❌ CAUSA: import { cn } from '@/lib/utils'
|
|
1693
|
+
import { cn } from '@/lib/utils';
|
|
1694
|
+
|
|
1695
|
+
// ✅ SOLUÇÃO: Importar da lib
|
|
1696
|
+
import { cn } from 'forlogic-core';
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
---
|
|
1700
|
+
|
|
1701
|
+
### **Erro 2: Esquecer `useMemo` no config**
|
|
1702
|
+
|
|
1703
|
+
```typescript
|
|
1704
|
+
// ❌ SINTOMA: Formulário fecha/reabre sozinho, re-renders infinitos
|
|
1705
|
+
// ❌ CAUSA: Config recriado a cada render
|
|
1706
|
+
const config = {
|
|
1707
|
+
columns: exampleColumns,
|
|
1708
|
+
formSections
|
|
1709
|
+
};
|
|
1710
|
+
|
|
1711
|
+
// ✅ SOLUÇÃO: Envolver em useMemo
|
|
1712
|
+
const config = useMemo(() => ({
|
|
1713
|
+
columns: exampleColumns,
|
|
1714
|
+
formSections
|
|
1715
|
+
}), []);
|
|
1716
|
+
```
|
|
1717
|
+
|
|
1718
|
+
---
|
|
1719
|
+
|
|
1720
|
+
### **Erro 3: Passar `entities` ao invés de `manager`**
|
|
1721
|
+
|
|
1722
|
+
```typescript
|
|
1723
|
+
// ❌ SINTOMA: TypeError: manager.createEntity is not a function
|
|
1724
|
+
// ❌ CAUSA: Passou array direto
|
|
1725
|
+
const CrudPage = createCrudPage({
|
|
1726
|
+
manager: manager.entities, // ← Errado!
|
|
1727
|
+
config
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
// ✅ SOLUÇÃO: Passar manager completo
|
|
1731
|
+
const CrudPage = createCrudPage({
|
|
1732
|
+
manager, // ← Correto!
|
|
1733
|
+
config
|
|
1734
|
+
});
|
|
1735
|
+
```
|
|
1736
|
+
|
|
1737
|
+
---
|
|
1738
|
+
|
|
1739
|
+
### **Erro 4: Filtro sem `useMemo`**
|
|
1740
|
+
|
|
1741
|
+
```typescript
|
|
1742
|
+
// ❌ SINTOMA: Performance ruim, travamentos
|
|
1743
|
+
// ❌ CAUSA: Recalcula filtro a cada render
|
|
1744
|
+
const filteredEntities = manager.entities.filter(e => e.is_actived);
|
|
1745
|
+
|
|
1746
|
+
// ✅ SOLUÇÃO: Envolver em useMemo
|
|
1747
|
+
const filteredEntities = useMemo(() =>
|
|
1748
|
+
manager.entities.filter(e => e.is_actived),
|
|
1749
|
+
[manager.entities]
|
|
1750
|
+
);
|
|
1751
|
+
```
|
|
1752
|
+
|
|
1753
|
+
---
|
|
1754
|
+
|
|
1755
|
+
### **Erro 5: Esquecer de usar `manager.save()`**
|
|
1756
|
+
|
|
1757
|
+
```typescript
|
|
1758
|
+
// ❌ SINTOMA: Erro de RLS no Supabase, registro não é criado
|
|
1759
|
+
// ❌ CAUSA: Usar createEntity/updateEntity direto sem alias
|
|
1760
|
+
manager.createEntity({
|
|
1761
|
+
title: data.title,
|
|
1762
|
+
email: data.email
|
|
1763
|
+
// ← Falta alias!
|
|
1764
|
+
});
|
|
1765
|
+
|
|
1766
|
+
// ✅ SOLUÇÃO: Usar manager.save() que injeta alias automaticamente
|
|
1767
|
+
const handleSave = (data: any) => {
|
|
1768
|
+
manager.save(data, (d) => ({
|
|
1769
|
+
title: d.title,
|
|
1770
|
+
email: d.email
|
|
1771
|
+
}));
|
|
1772
|
+
// O save() detecta CREATE vs UPDATE e injeta alias automaticamente
|
|
1773
|
+
};
|
|
1774
|
+
```
|
|
1775
|
+
|
|
1776
|
+
---
|
|
1777
|
+
|
|
1778
|
+
### **Erro 6: Chamar hooks fora do componente**
|
|
1779
|
+
|
|
1780
|
+
```typescript
|
|
1781
|
+
// ❌ SINTOMA: Error: Hooks can only be called inside of the body of a function component
|
|
1782
|
+
// ❌ CAUSA: Hook chamado fora do componente
|
|
1783
|
+
const manager = useExamplesCrud(); // ← Fora do componente!
|
|
1784
|
+
|
|
1785
|
+
export const ExamplesPage = () => {
|
|
1786
|
+
return <CrudPage />;
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
// ✅ SOLUÇÃO: Chamar hooks DENTRO do componente
|
|
1790
|
+
export const ExamplesPage = () => {
|
|
1791
|
+
const manager = useExamplesCrud(); // ← Dentro do componente!
|
|
1792
|
+
return <CrudPage />;
|
|
1793
|
+
};
|
|
1794
|
+
```
|
|
1795
|
+
|
|
1796
|
+
---
|
|
1797
|
+
|
|
1798
|
+
## 🎨 PERSONALIZAÇÃO AVANÇADA
|
|
1799
|
+
|
|
1800
|
+
### **1. Renderização Customizada de Colunas**
|
|
1801
|
+
|
|
1802
|
+
```typescript
|
|
1803
|
+
// Exemplo 1: Badge de status com cores
|
|
1804
|
+
{
|
|
1805
|
+
key: 'status',
|
|
1806
|
+
header: 'Status',
|
|
1807
|
+
render: (item) => (
|
|
1808
|
+
<span className={`px-2 py-1 rounded-full text-xs ${
|
|
1809
|
+
item.status === 'completed' ? 'bg-green-100 text-green-800' :
|
|
1810
|
+
item.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
|
|
1811
|
+
'bg-gray-100 text-gray-800'
|
|
1812
|
+
}`}>
|
|
1813
|
+
{item.status}
|
|
1814
|
+
</span>
|
|
1815
|
+
)
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
// Exemplo 2: Link externo
|
|
1819
|
+
{
|
|
1820
|
+
key: 'website',
|
|
1821
|
+
header: 'Site',
|
|
1822
|
+
render: (item) => (
|
|
1823
|
+
<a
|
|
1824
|
+
href={item.website}
|
|
1825
|
+
target="_blank"
|
|
1826
|
+
rel="noopener noreferrer"
|
|
1827
|
+
className="text-blue-600 hover:underline"
|
|
1828
|
+
>
|
|
1829
|
+
Visitar
|
|
1830
|
+
</a>
|
|
1831
|
+
)
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// Exemplo 3: Ícone colorido
|
|
1835
|
+
{
|
|
1836
|
+
key: 'priority',
|
|
1837
|
+
header: 'Prioridade',
|
|
1838
|
+
render: (item) => {
|
|
1839
|
+
const icons = { high: '🔴', medium: '🟡', low: '🟢' };
|
|
1840
|
+
return <span>{icons[item.priority]}</span>;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
```
|
|
1844
|
+
|
|
1845
|
+
---
|
|
1846
|
+
|
|
1847
|
+
### **2. Campos de Formulário Customizados**
|
|
1848
|
+
|
|
1849
|
+
```typescript
|
|
1850
|
+
// Exemplo: Campo customizado com EntitySelect
|
|
1851
|
+
{
|
|
1852
|
+
name: 'category_id',
|
|
1853
|
+
label: 'Categoria',
|
|
1854
|
+
type: 'custom' as const,
|
|
1855
|
+
component: (props) => {
|
|
1856
|
+
const { data: categories } = useCategoriesCrud();
|
|
1857
|
+
return (
|
|
1858
|
+
<EntitySelect
|
|
1859
|
+
{...props}
|
|
1860
|
+
items={categories}
|
|
1861
|
+
getItemValue={(item) => item.id}
|
|
1862
|
+
getItemLabel={(item) => item.name}
|
|
1863
|
+
placeholder="Selecionar categoria"
|
|
1864
|
+
/>
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
```
|
|
1869
|
+
|
|
1870
|
+
---
|
|
1871
|
+
|
|
1872
|
+
### **3. Ações Customizadas na Linha**
|
|
1873
|
+
|
|
1874
|
+
```typescript
|
|
1875
|
+
// Adicionar botão "Duplicar" nas ações da linha
|
|
1876
|
+
const customActions = (item: Example) => [
|
|
1877
|
+
{
|
|
1878
|
+
label: 'Duplicar',
|
|
1879
|
+
icon: Copy,
|
|
1880
|
+
onClick: () => {
|
|
1881
|
+
const newItem = { ...item, id: undefined, title: `${item.title} (cópia)` };
|
|
1882
|
+
manager.createEntity(newItem);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
];
|
|
1886
|
+
|
|
1887
|
+
// Passar para createCrudPage
|
|
1888
|
+
const CrudPage = createCrudPage({
|
|
1889
|
+
manager,
|
|
1890
|
+
config: {
|
|
1891
|
+
...config,
|
|
1892
|
+
customRowActions: customActions
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
```
|
|
1896
|
+
|
|
1897
|
+
---
|
|
1898
|
+
|
|
1899
|
+
### 🔗 Integração Qualiex (opcional)
|
|
1900
|
+
|
|
1901
|
+
**Auto-enrichment** (já configurado no BaseService):
|
|
1902
|
+
```typescript
|
|
1903
|
+
// ✅ Automático - dados enriquecidos com nome do usuário
|
|
1904
|
+
const processes = await processService.getAll();
|
|
1905
|
+
// processes[0].usuario_nome = "João Silva" (se enableQualiexEnrichment: true)
|
|
1906
|
+
```
|
|
1907
|
+
|
|
1908
|
+
**Componentes prontos:**
|
|
1909
|
+
```typescript
|
|
1910
|
+
import { QualiexUserField, QualiexResponsibleSelectField } from 'forlogic-core';
|
|
1911
|
+
|
|
1912
|
+
// Select de usuários Qualiex
|
|
1913
|
+
<QualiexResponsibleSelectField
|
|
1914
|
+
value={form.watch('id_user')}
|
|
1915
|
+
onChange={(userId) => form.setValue('id_user', userId)}
|
|
1916
|
+
/>
|
|
1917
|
+
```
|
|
1918
|
+
|
|
1919
|
+
**Componentes em formulários CRUD:**
|
|
1920
|
+
```typescript
|
|
1921
|
+
// Para seleção de usuário (modo unificado)
|
|
1922
|
+
{
|
|
1923
|
+
name: 'responsible_id',
|
|
1924
|
+
label: 'Responsável',
|
|
1925
|
+
type: 'user-select' as const,
|
|
1926
|
+
mode: 'single', // ou 'multiple'
|
|
1927
|
+
required: true
|
|
1928
|
+
}
|
|
1929
|
+
```
|
|
1930
|
+
|
|
1931
|
+
**Componentes customizados:**
|
|
1932
|
+
|
|
1933
|
+
Você pode criar e usar componentes customizados nos formulários para necessidades específicas:
|
|
1934
|
+
|
|
1935
|
+
```typescript
|
|
1936
|
+
// Exemplo de campo customizado
|
|
1937
|
+
{
|
|
1938
|
+
name: 'custom_field',
|
|
1939
|
+
label: 'Campo Customizado',
|
|
1940
|
+
type: 'my-custom-component' as const,
|
|
1941
|
+
required: true
|
|
1942
|
+
}
|
|
1943
|
+
```
|
|
1944
|
+
|
|
1945
|
+
> **Nota:** Componentes customizados devem ser registrados no `BaseForm.tsx` para funcionarem corretamente nos formulários CRUD.
|
|
1946
|
+
|
|
1947
|
+
**⚠️ CRÍTICO:** Requests Qualiex exigem header `un-alias`:
|
|
1948
|
+
```typescript
|
|
1949
|
+
// ✅ Já configurado no BaseService automaticamente
|
|
1950
|
+
headers: { 'un-alias': 'true' }
|
|
1951
|
+
```
|
|
1952
|
+
|
|
1953
|
+
---
|
|
1954
|
+
|
|
1955
|
+
## 📍 PLACES - Locais e Sublocais
|
|
1956
|
+
|
|
1957
|
+
O módulo **Places** permite gerenciar a hierarquia de locais e sublocais da organização, integrando com a API Qualiex.
|
|
1958
|
+
|
|
1959
|
+
### 🔌 Imports Disponíveis
|
|
1960
|
+
|
|
1961
|
+
```typescript
|
|
1962
|
+
// Tipos
|
|
1963
|
+
import type { Place, SubPlace } from 'forlogic-core';
|
|
1964
|
+
|
|
1965
|
+
// Serviço
|
|
1966
|
+
import { placeService, PlaceService } from 'forlogic-core';
|
|
1967
|
+
|
|
1968
|
+
// Componente de Página Pronta
|
|
1969
|
+
import { PlacesPage } from 'forlogic-core';
|
|
1970
|
+
```
|
|
1971
|
+
|
|
1972
|
+
### 📋 Estrutura dos Dados
|
|
1973
|
+
|
|
1974
|
+
```typescript
|
|
1975
|
+
interface Place {
|
|
1976
|
+
id: string;
|
|
1977
|
+
placeId: string; // ID único do local no Qualiex
|
|
1978
|
+
name: string; // Nome do local
|
|
1979
|
+
companyId: string; // ID da empresa
|
|
1980
|
+
usersIds: string[]; // Array de IDs de usuários vinculados
|
|
1981
|
+
subPlaces?: SubPlace[]; // Sublocais (hierarquia)
|
|
1982
|
+
parentId?: string | null; // ID do local pai (se for sublocalizado)
|
|
1983
|
+
isActive: boolean; // Status do local
|
|
1984
|
+
createdAt: string;
|
|
1985
|
+
updatedAt: string;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
interface SubPlace {
|
|
1989
|
+
id: string;
|
|
1990
|
+
placeId: string;
|
|
1991
|
+
name: string;
|
|
1992
|
+
parentId: string;
|
|
1993
|
+
usersIds: string[];
|
|
1994
|
+
isActive: boolean;
|
|
1995
|
+
subPlaces?: SubPlace[]; // Recursivo - permite múltiplos níveis
|
|
1996
|
+
}
|
|
1997
|
+
```
|
|
1998
|
+
|
|
1999
|
+
### 🎯 Como Obter Places
|
|
2000
|
+
|
|
2001
|
+
#### Método 1 (Recomendado): Hook com React Query
|
|
2002
|
+
|
|
2003
|
+
```typescript
|
|
2004
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2005
|
+
import { useAuth, placeService } from 'forlogic-core';
|
|
2006
|
+
|
|
2007
|
+
function MyComponent() {
|
|
2008
|
+
const { alias } = useAuth();
|
|
2009
|
+
|
|
2010
|
+
const { data: places = [], isLoading, error } = useQuery({
|
|
2011
|
+
queryKey: ['places', alias],
|
|
2012
|
+
queryFn: () => placeService.getPlaces(alias),
|
|
2013
|
+
enabled: !!alias,
|
|
2014
|
+
staleTime: 5 * 60 * 1000 // Cache de 5 minutos
|
|
2015
|
+
});
|
|
2016
|
+
|
|
2017
|
+
if (isLoading) return <LoadingState />;
|
|
2018
|
+
if (error) return <div>Erro ao carregar locais</div>;
|
|
2019
|
+
|
|
2020
|
+
return (
|
|
2021
|
+
<div>
|
|
2022
|
+
{places.map(place => (
|
|
2023
|
+
<div key={place.id}>{place.name}</div>
|
|
2024
|
+
))}
|
|
2025
|
+
</div>
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
```
|
|
2029
|
+
|
|
2030
|
+
#### Método 2: Hook Customizado Reutilizável
|
|
2031
|
+
|
|
2032
|
+
```typescript
|
|
2033
|
+
// src/hooks/usePlaces.ts
|
|
2034
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2035
|
+
import { useAuth, placeService } from 'forlogic-core';
|
|
2036
|
+
|
|
2037
|
+
export function usePlaces() {
|
|
2038
|
+
const { alias } = useAuth();
|
|
2039
|
+
|
|
2040
|
+
return useQuery({
|
|
2041
|
+
queryKey: ['places', alias],
|
|
2042
|
+
queryFn: () => placeService.getPlaces(alias),
|
|
2043
|
+
enabled: !!alias,
|
|
2044
|
+
staleTime: 5 * 60 * 1000 // Cache de 5 minutos
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// Usar no componente
|
|
2049
|
+
const { data: places = [], isLoading } = usePlaces();
|
|
2050
|
+
```
|
|
2051
|
+
|
|
2052
|
+
#### Método 3: Chamada Direta (Service)
|
|
2053
|
+
|
|
2054
|
+
```typescript
|
|
2055
|
+
// Para casos especiais (não recomendado para components)
|
|
2056
|
+
const places = await placeService.getPlaces('my-alias');
|
|
2057
|
+
```
|
|
2058
|
+
|
|
2059
|
+
### 🏠 Usando PlacesPage Pronta
|
|
2060
|
+
|
|
2061
|
+
```typescript
|
|
2062
|
+
// App.tsx ou routes
|
|
2063
|
+
import { PlacesPage } from 'forlogic-core';
|
|
2064
|
+
|
|
2065
|
+
<Route path="/places" element={<PlacesPage />} />
|
|
2066
|
+
```
|
|
2067
|
+
|
|
2068
|
+
### 🔗 Integrando Places em Módulos CRUD
|
|
2069
|
+
|
|
2070
|
+
#### Cenário A: PlaceSelect em Formulários
|
|
2071
|
+
|
|
2072
|
+
```typescript
|
|
2073
|
+
// src/components/PlaceSelect.tsx
|
|
2074
|
+
import { EntitySelect } from 'forlogic-core';
|
|
2075
|
+
import { usePlaces } from '@/hooks/usePlaces';
|
|
2076
|
+
import { useMemo } from 'react';
|
|
2077
|
+
|
|
2078
|
+
export function PlaceSelect({ value, onChange, disabled }: {
|
|
2079
|
+
value?: string;
|
|
2080
|
+
onChange: (value: string) => void;
|
|
2081
|
+
disabled?: boolean;
|
|
2082
|
+
}) {
|
|
2083
|
+
const { data: places = [], isLoading } = usePlaces();
|
|
2084
|
+
|
|
2085
|
+
// Achatar hierarquia para o select
|
|
2086
|
+
const flatPlaces = useMemo(() => {
|
|
2087
|
+
const flatten = (items: Place[], level = 0): any[] => {
|
|
2088
|
+
return items.flatMap(place => [
|
|
2089
|
+
{ ...place, level },
|
|
2090
|
+
...flatten(place.subPlaces || [], level + 1)
|
|
2091
|
+
]);
|
|
2092
|
+
};
|
|
2093
|
+
return flatten(places);
|
|
2094
|
+
}, [places]);
|
|
2095
|
+
|
|
2096
|
+
return (
|
|
2097
|
+
<EntitySelect
|
|
2098
|
+
value={value}
|
|
2099
|
+
onChange={onChange}
|
|
2100
|
+
items={flatPlaces}
|
|
2101
|
+
isLoading={isLoading}
|
|
2102
|
+
getItemValue={(p) => p.placeId}
|
|
2103
|
+
getItemLabel={(p) => `${' '.repeat(p.level)}${p.name}`}
|
|
2104
|
+
disabled={disabled}
|
|
2105
|
+
placeholder="Selecionar local"
|
|
2106
|
+
/>
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
// Usar no CRUD config
|
|
2111
|
+
{
|
|
2112
|
+
key: 'place_id',
|
|
2113
|
+
label: 'Local',
|
|
2114
|
+
type: 'custom',
|
|
2115
|
+
component: PlaceSelect,
|
|
2116
|
+
required: true
|
|
2117
|
+
}
|
|
2118
|
+
```
|
|
2119
|
+
|
|
2120
|
+
#### Cenário B: Filtrar Dados por Place
|
|
2121
|
+
|
|
2122
|
+
```typescript
|
|
2123
|
+
// Service com filtro de placeId
|
|
2124
|
+
const { service, useCrudHook } = createSimpleService({
|
|
2125
|
+
tableName: 'my_table',
|
|
2126
|
+
schemaName: 'central',
|
|
2127
|
+
additionalFilters: [
|
|
2128
|
+
{ field: 'place_id', operator: 'eq', value: selectedPlaceId }
|
|
2129
|
+
]
|
|
2130
|
+
});
|
|
2131
|
+
```
|
|
2132
|
+
|
|
2133
|
+
#### Cenário C: Exibir Nome do Local em Tabelas
|
|
2134
|
+
|
|
2135
|
+
```typescript
|
|
2136
|
+
// Hook para buscar nome do place
|
|
2137
|
+
function usePlaceName(placeId: string) {
|
|
2138
|
+
const { data: places = [] } = usePlaces();
|
|
2139
|
+
|
|
2140
|
+
return useMemo(() => {
|
|
2141
|
+
const findPlace = (items: Place[]): Place | undefined => {
|
|
2142
|
+
for (const place of items) {
|
|
2143
|
+
if (place.placeId === placeId) return place;
|
|
2144
|
+
if (place.subPlaces) {
|
|
2145
|
+
const found = findPlace(place.subPlaces);
|
|
2146
|
+
if (found) return found;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
return findPlace(places)?.name || 'Local não encontrado';
|
|
2151
|
+
}, [places, placeId]);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// Usar na coluna da tabela
|
|
2155
|
+
{
|
|
2156
|
+
key: 'place_id',
|
|
2157
|
+
header: 'Local',
|
|
2158
|
+
render: (item) => {
|
|
2159
|
+
const placeName = usePlaceName(item.place_id);
|
|
2160
|
+
return <span>{placeName}</span>;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
```
|
|
2164
|
+
|
|
2165
|
+
### 🔑 Acessando placeId/placeName dos Tokens
|
|
2166
|
+
|
|
2167
|
+
**⚠️ IMPORTANTE:** `placeId` e `placeName` **NÃO** vêm diretamente dos tokens JWT. Eles são obtidos da **API Qualiex**.
|
|
2168
|
+
|
|
2169
|
+
```typescript
|
|
2170
|
+
// ❌ ERRADO - Não existe no token
|
|
2171
|
+
const { placeId } = useAuth(); // undefined
|
|
2172
|
+
|
|
2173
|
+
// ✅ CORRETO - Buscar da API Qualiex
|
|
2174
|
+
const { data: places } = usePlaces();
|
|
2175
|
+
const userPlace = places.find(p => p.usersIds.includes(userId));
|
|
2176
|
+
const placeId = userPlace?.placeId;
|
|
2177
|
+
const placeName = userPlace?.name;
|
|
2178
|
+
```
|
|
2179
|
+
|
|
2180
|
+
**Fluxo de dados:**
|
|
2181
|
+
1. Token JWT contém `alias` e `companyId`
|
|
2182
|
+
2. `placeService.getPlaces(alias)` busca os Places da API Qualiex
|
|
2183
|
+
3. Cada `Place` contém `usersIds` (array de IDs de usuários)
|
|
2184
|
+
4. Relacionar usuário logado com seu Place através de `usersIds`
|
|
2185
|
+
|
|
2186
|
+
### 🌳 Navegação Hierárquica (Tree View)
|
|
2187
|
+
|
|
2188
|
+
```typescript
|
|
2189
|
+
function PlaceTree({ places, level = 0 }: {
|
|
2190
|
+
places: Place[];
|
|
2191
|
+
level?: number;
|
|
2192
|
+
}) {
|
|
2193
|
+
return (
|
|
2194
|
+
<div>
|
|
2195
|
+
{places.map(place => (
|
|
2196
|
+
<div key={place.id}>
|
|
2197
|
+
<div style={{ paddingLeft: `${level * 20}px` }}>
|
|
2198
|
+
📍 {place.name} ({place.usersIds.length} usuários)
|
|
2199
|
+
{!place.isActive && <Badge variant="secondary">Inativo</Badge>}
|
|
2200
|
+
</div>
|
|
2201
|
+
{place.subPlaces && place.subPlaces.length > 0 && (
|
|
2202
|
+
<PlaceTree places={place.subPlaces} level={level + 1} />
|
|
2203
|
+
)}
|
|
2204
|
+
</div>
|
|
2205
|
+
))}
|
|
2206
|
+
</div>
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
```
|
|
2210
|
+
|
|
2211
|
+
### 🛠️ Troubleshooting
|
|
2212
|
+
|
|
2213
|
+
| Erro | Causa | Solução |
|
|
2214
|
+
|------|-------|---------|
|
|
2215
|
+
| `CompanyId não encontrado no token` | Token não validado corretamente | Verificar edge function `validate-token` |
|
|
2216
|
+
| `Alias da unidade é obrigatório` | `alias` não disponível no `useAuth()` | Aguardar carregamento do auth com `isLoading` |
|
|
2217
|
+
| `Token Qualiex não encontrado` | Variável de ambiente faltando | Verificar `VITE_QUALIEX_API_URL` no `.env` |
|
|
2218
|
+
| Places retorna array vazio `[]` | Empresa sem places cadastrados | Verificar cadastro no Qualiex admin |
|
|
2219
|
+
| Hierarquia quebrada | `parentId` incorreto nos dados | Verificar integridade dos dados na API Qualiex |
|
|
2220
|
+
|
|
2221
|
+
### 📦 Exemplo Completo: Dashboard por Local
|
|
2222
|
+
|
|
2223
|
+
```typescript
|
|
2224
|
+
import { usePlaces } from '@/hooks/usePlaces';
|
|
2225
|
+
import { Card, CardHeader, CardTitle, CardContent } from 'forlogic-core';
|
|
2226
|
+
|
|
2227
|
+
function PlacesDashboard() {
|
|
2228
|
+
const { data: places = [], isLoading } = usePlaces();
|
|
2229
|
+
const { data: metrics = [] } = useQuery({
|
|
2230
|
+
queryKey: ['metrics'],
|
|
2231
|
+
queryFn: fetchMetrics
|
|
2232
|
+
});
|
|
2233
|
+
|
|
2234
|
+
if (isLoading) return <LoadingState />;
|
|
2235
|
+
|
|
2236
|
+
return (
|
|
2237
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
2238
|
+
{places.map(place => {
|
|
2239
|
+
const placeMetrics = metrics.filter(m => m.place_id === place.placeId);
|
|
2240
|
+
|
|
2241
|
+
return (
|
|
2242
|
+
<Card key={place.id}>
|
|
2243
|
+
<CardHeader>
|
|
2244
|
+
<CardTitle>{place.name}</CardTitle>
|
|
2245
|
+
</CardHeader>
|
|
2246
|
+
<CardContent>
|
|
2247
|
+
<div className="space-y-2">
|
|
2248
|
+
<p className="text-sm">
|
|
2249
|
+
👥 Usuários: <strong>{place.usersIds.length}</strong>
|
|
2250
|
+
</p>
|
|
2251
|
+
<p className="text-sm">
|
|
2252
|
+
📊 Registros: <strong>{placeMetrics.length}</strong>
|
|
2253
|
+
</p>
|
|
2254
|
+
<p className="text-sm">
|
|
2255
|
+
📍 Sublocais: <strong>{place.subPlaces?.length || 0}</strong>
|
|
2256
|
+
</p>
|
|
2257
|
+
</div>
|
|
2258
|
+
</CardContent>
|
|
2259
|
+
</Card>
|
|
2260
|
+
);
|
|
2261
|
+
})}
|
|
2262
|
+
</div>
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
```
|
|
2266
|
+
|
|
2267
|
+
### ✅ Checklist de Implementação
|
|
2268
|
+
|
|
2269
|
+
- [ ] `VITE_QUALIEX_API_URL` configurada no `.env`
|
|
2270
|
+
- [ ] Edge function `validate-token` retorna `company_id` corretamente
|
|
2271
|
+
- [ ] `alias` disponível no `useAuth()`
|
|
2272
|
+
- [ ] Hook `usePlaces()` criado e testado
|
|
2273
|
+
- [ ] `PlaceSelect` component criado (se necessário)
|
|
2274
|
+
- [ ] Tratamento de erro quando places vazio
|
|
2275
|
+
- [ ] Cache configurado no React Query (`staleTime`)
|
|
2276
|
+
- [ ] Hierarquia renderizada corretamente (se usar tree view)
|
|
2277
|
+
|
|
2278
|
+
### 📚 Referências
|
|
2279
|
+
|
|
2280
|
+
- **Tipos:** `lib/qualiex/places/types.ts`
|
|
2281
|
+
- **Service:** `lib/qualiex/places/PlaceService.ts`
|
|
2282
|
+
- **Componente:** `lib/qualiex/places/PlacesPage.tsx`
|
|
2283
|
+
- **Exports:** `lib/modular.ts` e `lib/exports/integrations.ts`
|
|
2284
|
+
- **Token Manager:** `lib/auth/services/TokenManager.ts`
|
|
2285
|
+
|
|
2286
|
+
---
|
|
2287
|
+
|
|
794
2288
|
## 🗃️ MIGRATIONS + RLS
|
|
795
2289
|
|
|
796
2290
|
### Template SQL Completo
|