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/dist/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
- // ⚠️ CRÍTICO: Preservar ID no update
506
- const handleUpdate = async (item: Process) => {
507
- await handleSave({
508
- ...item,
509
- id: item.id // ⚠️ OBRIGATÓRIO para update
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
- ### 🔗 Integração Qualiex (opcional)
1226
+ ## 🎓 TUTORIAL COMPLETO: CRUD de Examples (Copy-Paste Ready)
732
1227
 
733
- **Auto-enrichment** (já configurado no BaseService):
734
- ```typescript
735
- // Automático - dados enriquecidos com nome do usuário
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
- import { QualiexUserField, QualiexResponsibleSelectField } from 'forlogic-core';
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
- // Select de usuários Qualiex
745
- <QualiexResponsibleSelectField
746
- value={form.watch('id_user')}
747
- onChange={(userId) => form.setValue('id_user', userId)}
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
- **Componentes em formulários CRUD:**
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
- // Para seleção de usuário
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
- // Para seleção de responsável
762
- {
763
- name: 'responsible_id',
764
- label: 'Responsável',
765
- type: 'single-responsible-select' as const,
766
- required: true
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
- **Componentes customizados:**
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
- Você pode criar e usar componentes customizados nos formulários para necessidades específicas:
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
- // Exemplo de campo customizado
776
- {
777
- name: 'custom_field',
778
- label: 'Campo Customizado',
779
- type: 'my-custom-component' as const,
780
- required: true
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
- > **Nota:** Componentes customizados devem ser registrados no `BaseForm.tsx` para funcionarem corretamente nos formulários CRUD.
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
- // ✅ Já configurado no BaseService automaticamente
789
- headers: { 'un-alias': 'true' }
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