forlogic-core 1.7.6 → 1.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -208,7 +208,7 @@ import { Button } from 'forlogic-core'
208
208
  // Formulários
209
209
  Button, Input, Textarea, Label, Select, SelectContent,
210
210
  SelectItem, SelectTrigger, SelectValue, Checkbox, RadioGroup,
211
- RadioGroupItem, Switch
211
+ RadioGroupItem, Switch, CreatableCombobox
212
212
 
213
213
  // Layout
214
214
  Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle
@@ -598,6 +598,349 @@ const formFields: FormField[] = [
598
598
  type: 'custom',
599
599
  component: RichTextEditor,
600
600
  componentProps: {
601
+ toolbar: ['bold', 'italic', 'link']
602
+ }
603
+ }
604
+ ```
605
+
606
+ ---
607
+
608
+ ### 🎯 Campo Creatable Select
609
+
610
+ O `CreatableCombobox` permite que usuários **selecionem de opções existentes OU criem novos itens inline** sem sair do formulário.
611
+
612
+ #### Quando Usar?
613
+
614
+ - ✅ Seleção de tags, categorias, labels
615
+ - ✅ Campos onde usuários podem precisar criar valores rapidamente
616
+ - ✅ Dropdowns com centenas de opções (busca rápida)
617
+ - ✅ Single ou múltipla seleção
618
+
619
+ #### Uso Básico no BaseForm
620
+
621
+ ```typescript
622
+ import { createCrudPage } from 'forlogic-core';
623
+ import { TagService } from './tagService';
624
+
625
+ const formFields: FormSection[] = [
626
+ {
627
+ id: 'basic',
628
+ title: 'Informações',
629
+ fields: [
630
+ {
631
+ name: 'tags',
632
+ label: 'Tags',
633
+ type: 'creatable-select',
634
+ mode: 'multiple',
635
+ placeholder: 'Selecionar ou criar tags...',
636
+ options: tagsOptions, // [{ label: 'React', value: '1' }, ...]
637
+ onCreate: async (tagName) => {
638
+ // Criar tag no backend
639
+ const newTag = await TagService.create({ name: tagName });
640
+ // Atualizar lista de opções (refetch)
641
+ refetchTags();
642
+ return newTag.id; // Retorna ID para seleção automática
643
+ },
644
+ createLabel: (term) => `Criar "${term}"`,
645
+ required: true
646
+ },
647
+ {
648
+ name: 'category',
649
+ label: 'Categoria',
650
+ type: 'creatable-select',
651
+ mode: 'single',
652
+ placeholder: 'Selecionar categoria...',
653
+ options: categoriesOptions,
654
+ onCreate: async (categoryName) => {
655
+ const newCat = await CategoryService.create({ name: categoryName });
656
+ refetchCategories();
657
+ return newCat.id;
658
+ }
659
+ }
660
+ ]
661
+ }
662
+ ];
663
+ ```
664
+
665
+ #### Props do Campo
666
+
667
+ | Prop | Tipo | Padrão | Descrição |
668
+ | --------------------- | ------------------------------------------ | ----------------------- | ---------------------------------------------------- |
669
+ | `type` | `'creatable-select'` | - | Tipo do campo (obrigatório) |
670
+ | `mode` | `'single' \| 'multiple'` | `'single'` | Seleção única ou múltipla |
671
+ | `options` | `Array<{ label: string; value: string }>` | `[]` | Opções existentes para seleção |
672
+ | `onCreate` | `(searchTerm: string) => Promise<string>` | - | Callback para criar novo item (retorna ID) |
673
+ | `placeholder` | `string` | `'Selecionar...'` | Texto quando nenhum item selecionado |
674
+ | `searchPlaceholder` | `string` | `'Buscar...'` | Texto da busca interna |
675
+ | `createLabel` | `(term: string) => string` | `'Criar "{term}"'` | Função para gerar label do botão criar |
676
+ | `emptyMessage` | `string` | `'Nenhum item encontrado.'` | Mensagem quando não há opções |
677
+
678
+ #### Exemplo Completo com React Query
679
+
680
+ ```typescript
681
+ import { useQuery, useQueryClient } from '@tanstack/react-query';
682
+ import { createCrudPage } from 'forlogic-core';
683
+ import { TagService } from './tagService';
684
+
685
+ function TagsPage() {
686
+ const queryClient = useQueryClient();
687
+
688
+ // Buscar tags existentes
689
+ const { data: tags = [], isLoading } = useQuery({
690
+ queryKey: ['tags'],
691
+ queryFn: () => TagService.getAll()
692
+ });
693
+
694
+ // Converter para formato do CreatableCombobox
695
+ const tagsOptions = tags.map(tag => ({
696
+ label: tag.name,
697
+ value: tag.id
698
+ }));
699
+
700
+ const formFields: FormSection[] = [
701
+ {
702
+ id: 'article',
703
+ title: 'Artigo',
704
+ fields: [
705
+ {
706
+ name: 'title',
707
+ label: 'Título',
708
+ type: 'text',
709
+ required: true
710
+ },
711
+ {
712
+ name: 'tags',
713
+ label: 'Tags',
714
+ type: 'creatable-select',
715
+ mode: 'multiple',
716
+ options: tagsOptions,
717
+ onCreate: async (tagName) => {
718
+ // Validar nome
719
+ if (tagName.trim().length < 2) {
720
+ throw new Error('Nome deve ter ao menos 2 caracteres');
721
+ }
722
+
723
+ // Criar no backend
724
+ const newTag = await TagService.create({
725
+ name: tagName.trim()
726
+ });
727
+
728
+ // Invalidar cache para refetch automático
729
+ queryClient.invalidateQueries({ queryKey: ['tags'] });
730
+
731
+ // Retornar ID para seleção automática
732
+ return newTag.id;
733
+ },
734
+ placeholder: 'Selecionar tags...',
735
+ searchPlaceholder: 'Buscar ou criar tag...',
736
+ createLabel: (term) => `✨ Criar tag "${term}"`,
737
+ required: true
738
+ }
739
+ ]
740
+ }
741
+ ];
742
+
743
+ return createCrudPage({
744
+ // ... config do CRUD
745
+ formFields
746
+ });
747
+ }
748
+ ```
749
+
750
+ #### Uso Standalone (Fora do BaseForm)
751
+
752
+ ```typescript
753
+ import { CreatableCombobox } from 'forlogic-core/ui';
754
+ import { useState } from 'react';
755
+
756
+ function MyComponent() {
757
+ const [selectedTags, setSelectedTags] = useState<string[]>([]);
758
+
759
+ const tagsOptions = [
760
+ { label: 'React', value: '1' },
761
+ { label: 'TypeScript', value: '2' },
762
+ { label: 'Node.js', value: '3' }
763
+ ];
764
+
765
+ return (
766
+ <CreatableCombobox
767
+ mode="multiple"
768
+ value={selectedTags}
769
+ onChange={setSelectedTags}
770
+ options={tagsOptions}
771
+ onCreate={async (tagName) => {
772
+ const response = await fetch('/api/tags', {
773
+ method: 'POST',
774
+ body: JSON.stringify({ name: tagName })
775
+ });
776
+ const newTag = await response.json();
777
+ return newTag.id;
778
+ }}
779
+ placeholder="Selecionar tags..."
780
+ createLabel={(term) => `Criar "${term}"`}
781
+ />
782
+ );
783
+ }
784
+ ```
785
+
786
+ #### Interface TypeScript
787
+
788
+ ```typescript
789
+ interface CreatableComboboxProps {
790
+ // Dados
791
+ options: Array<{ label: string; value: string }>;
792
+ value?: string | string[];
793
+
794
+ // Eventos
795
+ onChange: (value: string | string[]) => void;
796
+ onCreate?: (searchTerm: string) => Promise<string | void>;
797
+
798
+ // Config
799
+ placeholder?: string;
800
+ searchPlaceholder?: string;
801
+ createLabel?: (term: string) => string;
802
+ emptyMessage?: string;
803
+
804
+ // Estado
805
+ disabled?: boolean;
806
+ error?: string;
807
+ isLoading?: boolean;
808
+
809
+ // UI
810
+ className?: string;
811
+ mode?: 'single' | 'multiple';
812
+ }
813
+ ```
814
+
815
+ #### Fluxo de Criação
816
+
817
+ ```mermaid
818
+ graph TD
819
+ A[Usuário digita 'Design'] --> B{Termo existe?}
820
+ B -->|Sim| C[Mostra opção existente]
821
+ B -->|Não| D[Mostra botão 'Criar Design']
822
+ D --> E[Usuário clica em criar]
823
+ E --> F[onCreate chamado]
824
+ F --> G{Retornou ID?}
825
+ G -->|Sim| H[Adiciona à seleção automaticamente]
826
+ G -->|Não| I[Apenas fecha dropdown]
827
+ H --> J[queryClient.invalidateQueries atualiza lista]
828
+ C --> K[Usuário seleciona]
829
+ K --> L[onChange chamado]
830
+ ```
831
+
832
+ #### Estados Visuais
833
+
834
+ **Campo Vazio (Single):**
835
+ ```
836
+ ┌────────────────────────────────┐
837
+ │ Selecionar categoria... ▼ │
838
+ └────────────────────────────────┘
839
+ ```
840
+
841
+ **Campo com Múltiplos Selecionados:**
842
+ ```
843
+ ┌────────────────────────────────┐
844
+ │ [React ✕] [TypeScript ✕] ▼ │
845
+ └────────────────────────────────┘
846
+ ```
847
+
848
+ **Dropdown com Busca (Termo Não Existe):**
849
+ ```
850
+ ┌────────────────────────────────┐
851
+ │ 🔍 design │
852
+ ├────────────────────────────────┤
853
+ │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ │
854
+ │ ┃ ✨ Criar "design" ┃ │ <- Botão azul
855
+ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ │
856
+ └────────────────────────────────┘
857
+ ```
858
+
859
+ **Dropdown com Opções Existentes:**
860
+ ```
861
+ ┌────────────────────────────────┐
862
+ │ 🔍 react │
863
+ ├────────────────────────────────┤
864
+ │ ✓ React │ <- Selecionado
865
+ │ React Native │
866
+ │ React Query │
867
+ └────────────────────────────────┘
868
+ ```
869
+
870
+ #### Boas Práticas
871
+
872
+ 1. **Sempre retornar o ID no onCreate:**
873
+ - Permite seleção automática do item criado
874
+ - Evita usuário ter que procurar o item recém-criado
875
+
876
+ 2. **Invalidar cache após criar:**
877
+ - Use `queryClient.invalidateQueries()` para refetch automático
878
+ - Garante que lista de opções está sempre atualizada
879
+
880
+ 3. **Validação no onCreate:**
881
+ - Valide o nome antes de criar
882
+ - Lance erro amigável se inválido
883
+ - Use `toast.error()` para feedback visual
884
+
885
+ 4. **Loading states:**
886
+ - Passe `isLoading` durante fetch inicial de opções
887
+ - `onCreate` já mostra loading interno automaticamente
888
+
889
+ 5. **Modo apropriado:**
890
+ - `single`: Categoria, Status, Tipo
891
+ - `multiple`: Tags, Skills, Departamentos
892
+
893
+ #### Troubleshooting
894
+
895
+ **Problema:** Opções não atualizam após criar
896
+ ```typescript
897
+ // ❌ ERRADO - Não invalida cache
898
+ onCreate: async (name) => {
899
+ await TagService.create({ name });
900
+ return newTag.id;
901
+ }
902
+
903
+ // ✅ CORRETO - Invalida cache
904
+ onCreate: async (name) => {
905
+ const newTag = await TagService.create({ name });
906
+ queryClient.invalidateQueries({ queryKey: ['tags'] });
907
+ return newTag.id;
908
+ }
909
+ ```
910
+
911
+ **Problema:** Dropdown fica transparente
912
+ ```css
913
+ /* Adicionar ao CSS global se necessário */
914
+ .popover-content {
915
+ background-color: white;
916
+ z-index: 9999;
917
+ }
918
+ ```
919
+
920
+ **Problema:** onCreate não retorna ID
921
+ ```typescript
922
+ // ✅ Sempre retornar o ID para seleção automática
923
+ onCreate: async (name) => {
924
+ const response = await TagService.create({ name });
925
+ return response.id; // <- IMPORTANTE
926
+ }
927
+ ```
928
+
929
+ ---
930
+
931
+ #### Integração com FormField Types
932
+
933
+ O tipo `'creatable-select'` está disponível para uso direto no `FormField`:
934
+
935
+ ```typescript
936
+ type FormFieldType =
937
+ | 'text'
938
+ | 'textarea'
939
+ | 'select'
940
+ | 'creatable-select' // <- Novo tipo
941
+ | 'checkbox'
942
+ | 'radio'
943
+ | 'date'
601
944
  toolbar: ['bold', 'italic', 'link'],
602
945
  minHeight: 200
603
946
  }
@@ -1052,9 +1395,13 @@ userFieldsMapping: [
1052
1395
 
1053
1396
  ### 🏢 Sistema de Gestores de Locais (Qualiex)
1054
1397
 
1055
- Componentes para gerenciamento de gestores e membros de locais/sublocais integrados com a API Qualiex.
1398
+ Componentes reutilizáveis para gerenciamento de gestores e membros de locais/sublocais integrados com a API Qualiex.
1056
1399
 
1057
- #### Importação
1400
+ > **⚠️ IMPORTANTE:** Esta funcionalidade requer configuração de tabela no banco de dados do projeto consumidor. A biblioteca fornece os componentes, mas **você deve criar e configurar a tabela e o serviço** no seu projeto.
1401
+
1402
+ ---
1403
+
1404
+ #### 📦 Importação
1058
1405
 
1059
1406
  ```typescript
1060
1407
  import {
@@ -1063,110 +1410,432 @@ import {
1063
1410
  ManagerSelectionDialog,
1064
1411
  usePlaceManagers,
1065
1412
  PlaceManagerService,
1413
+ type PlaceManagerServiceConfig,
1066
1414
  type PlaceManager
1067
1415
  } from 'forlogic-core';
1068
1416
  ```
1069
1417
 
1070
- #### Componentes
1418
+ ---
1419
+
1420
+ #### 🔧 Configuração Inicial (OBRIGATÓRIO)
1421
+
1422
+ ##### **Passo 1: Criar a Tabela no Supabase**
1423
+
1424
+ A biblioteca espera uma tabela com a seguinte estrutura:
1425
+
1426
+ ```sql
1427
+ CREATE TABLE IF NOT EXISTS public.place_managers (
1428
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
1429
+ alias text NOT NULL,
1430
+ place_id text NOT NULL,
1431
+ user_id text NOT NULL,
1432
+ user_name text NOT NULL,
1433
+ user_email text NOT NULL,
1434
+ role text NOT NULL CHECK (role IN ('manager', 'member')),
1435
+ created_at timestamptz DEFAULT now(),
1436
+ updated_at timestamptz DEFAULT now(),
1437
+
1438
+ -- Constraint: Um usuário não pode ter múltiplas funções no mesmo local
1439
+ UNIQUE (alias, place_id, user_id)
1440
+ );
1441
+
1442
+ -- Índices para performance
1443
+ CREATE INDEX idx_place_managers_alias_place ON public.place_managers(alias, place_id);
1444
+ CREATE INDEX idx_place_managers_user ON public.place_managers(user_id);
1445
+
1446
+ -- RLS Policies (exemplo - ajustar conforme necessidade)
1447
+ ALTER TABLE public.place_managers ENABLE ROW LEVEL SECURITY;
1448
+
1449
+ CREATE POLICY "Users view own alias managers"
1450
+ ON public.place_managers FOR SELECT
1451
+ USING (((SELECT auth.jwt()) ->> 'alias'::text) = alias);
1452
+
1453
+ CREATE POLICY "Users manage own alias managers"
1454
+ ON public.place_managers FOR ALL
1455
+ USING (((SELECT auth.jwt()) ->> 'alias'::text) = alias);
1456
+ ```
1457
+
1458
+ > 💡 **Dica:** Você pode usar um nome de tabela e schema diferentes, basta configurar no próximo passo.
1459
+
1460
+ ---
1461
+
1462
+ ##### **Passo 2: Criar o Serviço no Seu Projeto**
1463
+
1464
+ Crie um arquivo de serviço customizado no seu projeto:
1465
+
1466
+ ```typescript
1467
+ // src/services/placeManagerService.ts
1468
+ import { PlaceManagerService } from 'forlogic-core';
1469
+
1470
+ export const placeManagerService = new PlaceManagerService({
1471
+ tableName: 'place_managers', // 🔧 Nome da sua tabela
1472
+ schemaName: 'public' // 🔧 Nome do schema
1473
+ });
1474
+ ```
1475
+
1476
+ **Configurações disponíveis:**
1477
+
1478
+ | Propriedade | Tipo | Padrão | Descrição |
1479
+ | ------------ | -------- | ------------------ | ------------------------------ |
1480
+ | `tableName` | `string` | `'place_managers'` | Nome da tabela no Supabase |
1481
+ | `schemaName` | `string` | `'public'` | Nome do schema (geralmente 'public') |
1482
+
1483
+ ---
1071
1484
 
1072
- **PlaceManagerButton** - Botão dropdown com ações de gerenciamento:
1485
+ #### 🎨 Componentes
1486
+
1487
+ ##### **PlaceManagerButton**
1488
+
1489
+ Botão dropdown com ações de gerenciamento (trocar gestor, remover gestor).
1073
1490
 
1074
1491
  ```tsx
1492
+ import { PlaceManagerButton } from 'forlogic-core';
1493
+ import { placeManagerService } from '@/services/placeManagerService';
1494
+
1075
1495
  <PlaceManagerButton
1076
1496
  placeId="abc-123"
1077
1497
  placeName="Matriz São Paulo"
1078
- serviceConfig={{ tableName: 'place_managers' }}
1498
+ service={placeManagerService} // ⚠️ OBRIGATÓRIO
1079
1499
  />
1080
1500
  ```
1081
1501
 
1082
- **PlaceManagerBadge** - Badge visual com contador de gestores:
1502
+ **Props:**
1503
+
1504
+ | Prop | Tipo | Obrigatório | Descrição |
1505
+ | ----------- | ---------------------- | ----------- | -------------------------------- |
1506
+ | `placeId` | `string` | ✅ | ID do local |
1507
+ | `placeName` | `string` | ✅ | Nome do local (para exibição) |
1508
+ | `service` | `PlaceManagerService` | ✅ | Instância do serviço configurado |
1509
+
1510
+ ---
1511
+
1512
+ ##### **PlaceManagerBadge**
1513
+
1514
+ Badge visual que mostra "Gestor" ou "Gestores" com contador de usuários.
1083
1515
 
1084
1516
  ```tsx
1517
+ import { PlaceManagerBadge } from 'forlogic-core';
1518
+ import { placeManagerService } from '@/services/placeManagerService';
1519
+
1085
1520
  <PlaceManagerBadge
1086
1521
  placeId="abc-123"
1087
1522
  userCount={15}
1523
+ service={placeManagerService} // ⚠️ OBRIGATÓRIO
1088
1524
  />
1089
1525
  ```
1090
1526
 
1091
- **ManagerSelectionDialog** - Diálogo completo de seleção:
1527
+ **Props:**
1528
+
1529
+ | Prop | Tipo | Obrigatório | Descrição |
1530
+ | ----------- | --------------------- | ----------- | -------------------------------- |
1531
+ | `placeId` | `string` | ✅ | ID do local |
1532
+ | `userCount` | `number` | ✅ | Total de usuários do local |
1533
+ | `service` | `PlaceManagerService` | ✅ | Instância do serviço configurado |
1534
+
1535
+ **Estados visuais:**
1536
+
1537
+ - **"Gestor"** - Quando há 1 gestor definido
1538
+ - **"Gestores"** - Quando não há gestor definido (apenas membros)
1539
+
1540
+ ---
1541
+
1542
+ ##### **ManagerSelectionDialog**
1543
+
1544
+ Diálogo completo para seleção de gestores e membros com busca integrada.
1092
1545
 
1093
1546
  ```tsx
1547
+ import { ManagerSelectionDialog } from 'forlogic-core';
1548
+
1094
1549
  <ManagerSelectionDialog
1095
1550
  open={showDialog}
1096
1551
  onOpenChange={setShowDialog}
1097
- onSelectManager={(user) => console.log('Gestor:', user)}
1098
- onSelectMember={(user) => console.log('Membro:', user)}
1552
+ onSelectManager={(user) => handleSelectManager(user)}
1553
+ onSelectMember={(user) => handleAddMember(user)}
1099
1554
  currentManagerId={manager?.user_id}
1100
1555
  currentMemberIds={members.map(m => m.user_id)}
1101
1556
  placeName="Matriz São Paulo"
1557
+ isLoading={false}
1102
1558
  />
1103
1559
  ```
1104
1560
 
1105
- #### Hook: usePlaceManagers
1561
+ **Props:**
1562
+
1563
+ | Prop | Tipo | Obrigatório | Descrição |
1564
+ | ------------------ | ----------------------------- | ----------- | ------------------------------------- |
1565
+ | `open` | `boolean` | ✅ | Controla visibilidade |
1566
+ | `onOpenChange` | `(open: boolean) => void` | ✅ | Callback de mudança de estado |
1567
+ | `onSelectManager` | `(user: QualiexUser) => void` | ✅ | Callback ao selecionar gestor |
1568
+ | `onSelectMember` | `(user: QualiexUser) => void` | ✅ | Callback ao adicionar membro |
1569
+ | `currentManagerId` | `string` | ❌ | ID do gestor atual |
1570
+ | `currentMemberIds` | `string[]` | ❌ | IDs dos membros atuais |
1571
+ | `placeName` | `string` | ✅ | Nome do local (para exibição) |
1572
+ | `isLoading` | `boolean` | ❌ | Estado de loading externo (opcional) |
1573
+
1574
+ ---
1575
+
1576
+ #### 🔧 Hook: usePlaceManagers
1577
+
1578
+ Hook React Query para gerenciar gestores e membros de um local.
1106
1579
 
1107
1580
  ```typescript
1581
+ import { usePlaceManagers } from 'forlogic-core';
1582
+ import { placeManagerService } from '@/services/placeManagerService';
1583
+
1108
1584
  const {
1109
- managers, // Todos (gestor + membros)
1110
- manager, // Apenas o gestor
1111
- members, // Apenas membros
1112
- setManager, // (user: QualiexUser) => void
1113
- addMember, // (user: QualiexUser) => void
1114
- remove, // (userId: string) => void
1115
- isLoading
1116
- } = usePlaceManagers(placeId);
1117
- ```
1585
+ managers, // PlaceManager[] - Todos (gestor + membros)
1586
+ manager, // PlaceManager | undefined - Apenas o gestor
1587
+ members, // PlaceManager[] - Apenas membros
1588
+ isLoading, // boolean - Carregando dados iniciais
1589
+ setManager, // (user: QualiexUser) => void
1590
+ addMember, // (user: QualiexUser) => void
1591
+ remove, // (userId: string) => void
1592
+ isSettingManager, // boolean - Carregando mutação de gestor
1593
+ isAddingMember, // boolean - Carregando mutação de membro
1594
+ isRemoving // boolean - Carregando mutação de remoção
1595
+ } = usePlaceManagers(placeId, placeManagerService);
1596
+ ```
1597
+
1598
+ **Parâmetros:**
1599
+
1600
+ | Parâmetro | Tipo | Obrigatório | Descrição |
1601
+ | --------- | --------------------- | ----------- | ------------------------------------------ |
1602
+ | `placeId` | `string` | ✅ | ID do local |
1603
+ | `service` | `PlaceManagerService` | ✅ | Instância do serviço configurado |
1604
+ | `enabled` | `boolean` | ❌ | Habilitar/desabilitar query (default: `true`) |
1605
+
1606
+ **Retorno:**
1607
+
1608
+ | Propriedade | Tipo | Descrição |
1609
+ | ------------------ | ----------------------------- | ---------------------------------------------- |
1610
+ | `managers` | `PlaceManager[]` | Todos os usuários (gestor + membros) |
1611
+ | `manager` | `PlaceManager \| undefined` | Apenas o gestor (se existir) |
1612
+ | `members` | `PlaceManager[]` | Apenas membros (sem o gestor) |
1613
+ | `isLoading` | `boolean` | Carregando dados iniciais |
1614
+ | `setManager` | `(user: QualiexUser) => void` | Define novo gestor (remove anterior) |
1615
+ | `addMember` | `(user: QualiexUser) => void` | Adiciona membro ao local |
1616
+ | `remove` | `(userId: string) => void` | Remove usuário (gestor ou membro) |
1617
+ | `isSettingManager` | `boolean` | Estado da mutação de definir gestor |
1618
+ | `isAddingMember` | `boolean` | Estado da mutação de adicionar membro |
1619
+ | `isRemoving` | `boolean` | Estado da mutação de remover |
1620
+
1621
+ ---
1622
+
1623
+ #### 🔧 Service: PlaceManagerService
1118
1624
 
1119
- #### Service: PlaceManagerService
1625
+ Classe para operações de banco de dados relacionadas a gestores de locais.
1626
+
1627
+ ##### **Instanciação Customizada**
1120
1628
 
1121
1629
  ```typescript
1122
1630
  import { PlaceManagerService } from 'forlogic-core';
1123
1631
 
1124
- // Instância customizada
1125
- const service = new PlaceManagerService({
1126
- tableName: 'my_place_managers',
1127
- schemaName: 'custom_schema'
1632
+ // Configuração padrão (tabela 'place_managers' no schema 'public')
1633
+ const defaultService = new PlaceManagerService();
1634
+
1635
+ // Configuração customizada
1636
+ const customService = new PlaceManagerService({
1637
+ tableName: 'gestores_locais',
1638
+ schemaName: 'gestao'
1128
1639
  });
1640
+ ```
1641
+
1642
+ ##### **Métodos Disponíveis**
1643
+
1644
+ ```typescript
1645
+ // Buscar gestores e membros de um local
1646
+ const managers = await placeManagerService.getPlaceManagers(
1647
+ alias: string,
1648
+ placeId: string
1649
+ );
1650
+ // Retorna: PlaceManager[]
1651
+
1652
+ // Definir gestor (remove anterior automaticamente)
1653
+ await placeManagerService.setManager(
1654
+ alias: string,
1655
+ placeId: string,
1656
+ user: QualiexUser
1657
+ );
1658
+ // Retorna: void
1659
+
1660
+ // Adicionar membro
1661
+ await placeManagerService.addMember(
1662
+ alias: string,
1663
+ placeId: string,
1664
+ user: QualiexUser
1665
+ );
1666
+ // Retorna: void
1129
1667
 
1130
- // Métodos disponíveis
1131
- await service.getPlaceManagers(alias, placeId);
1132
- await service.setManager(alias, placeId, user);
1133
- await service.addMember(alias, placeId, user);
1134
- await service.removePlaceUser(alias, placeId, userId);
1668
+ // Remover usuário (gestor ou membro)
1669
+ await placeManagerService.removePlaceUser(
1670
+ alias: string,
1671
+ placeId: string,
1672
+ userId: string
1673
+ );
1674
+ // Retorna: void
1135
1675
  ```
1136
1676
 
1137
- > ⚠️ A configuração do banco de dados (tabelas, RLS policies) deve ser feita no projeto consumidor.
1677
+ ---
1678
+
1679
+ #### 📘 Tipos
1680
+
1681
+ ```typescript
1682
+ interface PlaceManager {
1683
+ id: string;
1684
+ alias: string;
1685
+ place_id: string;
1686
+ user_id: string;
1687
+ user_name: string;
1688
+ user_email: string;
1689
+ role: 'manager' | 'member';
1690
+ created_at: string;
1691
+ updated_at: string;
1692
+ }
1693
+
1694
+ interface PlaceManagerServiceConfig {
1695
+ tableName?: string; // Default: 'place_managers'
1696
+ schemaName?: string; // Default: 'public'
1697
+ }
1698
+ ```
1138
1699
 
1139
- #### Regras de Negócio
1700
+ ---
1140
1701
 
1141
- 1. **Um gestor por local** - Ao definir novo gestor, o anterior é removido automaticamente
1142
- 2. **Membros ilimitados** - Múltiplos membros podem ser adicionados
1143
- 3. **Ordenação alfabética** - Sempre ordenado por nome
1144
- 4. **Busca inteligente** - Filtra por nome ou email
1702
+ #### 🎯 Regras de Negócio
1145
1703
 
1146
- #### Exemplo Completo
1704
+ 1. **Um gestor por local**: Ao definir novo gestor, o anterior é removido automaticamente
1705
+ 2. ✅ **Membros ilimitados**: Múltiplos membros podem ser adicionados ao mesmo local
1706
+ 3. ✅ **Ordenação alfabética**: Usuários sempre ordenados por nome
1707
+ 4. ✅ **Busca inteligente**: Filtra por nome ou email do usuário
1708
+ 5. ✅ **Integração Qualiex**: Utiliza `useQualiexUsers` para listar usuários
1709
+
1710
+ ---
1711
+
1712
+ #### 💡 Exemplo Completo
1147
1713
 
1148
1714
  ```tsx
1715
+ // 1. Criar serviço (src/services/placeManagerService.ts)
1716
+ import { PlaceManagerService } from 'forlogic-core';
1717
+
1718
+ export const placeManagerService = new PlaceManagerService({
1719
+ tableName: 'place_managers',
1720
+ schemaName: 'public'
1721
+ });
1722
+
1723
+ // 2. Usar nos componentes (src/places/PlaceTreeItem.tsx)
1724
+ import { PlaceManagerButton, PlaceManagerBadge } from 'forlogic-core';
1725
+ import { placeManagerService } from '@/services/placeManagerService';
1726
+
1149
1727
  function PlaceTreeItem({ place }) {
1150
1728
  return (
1151
1729
  <div className="flex items-center gap-3">
1152
1730
  <span>{place.name}</span>
1153
-
1154
- <PlaceManagerBadge
1155
- placeId={place.id}
1731
+
1732
+ {/* Badge com contador */}
1733
+ <PlaceManagerBadge
1734
+ placeId={place.id}
1156
1735
  userCount={place.usersIds.length}
1736
+ service={placeManagerService}
1157
1737
  />
1158
-
1159
- <PlaceManagerButton
1738
+
1739
+ {/* Botão de ações */}
1740
+ <PlaceManagerButton
1160
1741
  placeId={place.id}
1161
1742
  placeName={place.name}
1743
+ service={placeManagerService}
1162
1744
  />
1163
1745
  </div>
1164
1746
  );
1165
1747
  }
1748
+
1749
+ // 3. Uso avançado com hook (src/places/PlaceDetailsPage.tsx)
1750
+ import { usePlaceManagers } from 'forlogic-core';
1751
+ import { placeManagerService } from '@/services/placeManagerService';
1752
+
1753
+ function PlaceDetailsPage({ placeId }) {
1754
+ const {
1755
+ manager,
1756
+ members,
1757
+ setManager,
1758
+ addMember,
1759
+ remove,
1760
+ isLoading
1761
+ } = usePlaceManagers(placeId, placeManagerService);
1762
+
1763
+ if (isLoading) return <LoadingState />;
1764
+
1765
+ return (
1766
+ <div>
1767
+ <h2>Gestor Atual</h2>
1768
+ {manager ? (
1769
+ <div>
1770
+ <p>{manager.user_name}</p>
1771
+ <Button onClick={() => remove(manager.user_id)}>
1772
+ Remover Gestor
1773
+ </Button>
1774
+ </div>
1775
+ ) : (
1776
+ <p>Nenhum gestor definido</p>
1777
+ )}
1778
+
1779
+ <h2>Membros ({members.length})</h2>
1780
+ <ul>
1781
+ {members.map(member => (
1782
+ <li key={member.id}>
1783
+ {member.user_name}
1784
+ <Button onClick={() => remove(member.user_id)}>
1785
+ Remover
1786
+ </Button>
1787
+ </li>
1788
+ ))}
1789
+ </ul>
1790
+ </div>
1791
+ );
1792
+ }
1166
1793
  ```
1167
1794
 
1168
1795
  ---
1169
1796
 
1797
+ #### 🔒 Segurança e RLS
1798
+
1799
+ **Recomendações de RLS Policies:**
1800
+
1801
+ ```sql
1802
+ -- Permitir leitura para usuários do mesmo alias
1803
+ CREATE POLICY "Users view own alias managers"
1804
+ ON public.place_managers FOR SELECT
1805
+ USING (((SELECT auth.jwt()) ->> 'alias'::text) = alias);
1806
+
1807
+ -- Permitir gestão completa para usuários do mesmo alias
1808
+ CREATE POLICY "Users manage own alias managers"
1809
+ ON public.place_managers FOR ALL
1810
+ USING (((SELECT auth.jwt()) ->> 'alias'::text) = alias);
1811
+ ```
1812
+
1813
+ > ⚠️ **Importante:** Ajuste as policies conforme as regras de negócio do seu projeto.
1814
+
1815
+ ---
1816
+
1817
+ #### ❓ Troubleshooting
1818
+
1819
+ **Erro: "relation 'place_managers' does not exist"**
1820
+ - ✅ Verifique se a tabela foi criada no Supabase
1821
+ - ✅ Confirme o nome correto da tabela e schema no `PlaceManagerService`
1822
+
1823
+ **Erro: "No rows returned"**
1824
+ - ✅ Verifique as RLS policies da tabela
1825
+ - ✅ Confirme que o usuário tem permissão para ler a tabela
1826
+ - ✅ Verifique se o `alias` está correto
1827
+
1828
+ **Componente não atualiza após adicionar/remover**
1829
+ - ✅ O hook usa React Query com invalidação automática de cache
1830
+ - ✅ Verifique se está usando a mesma instância do `service` em todos os componentes
1831
+
1832
+ **Dropdown não abre ou está vazio**
1833
+ - ✅ Verifique se a integração com Qualiex está configurada
1834
+ - ✅ Confirme que `useQualiexUsers` está retornando usuários
1835
+ - ✅ Verifique console do navegador para erros de API
1836
+
1837
+ ---
1838
+
1170
1839
  ### ✅ CHECKLIST (antes de implementar)
1171
1840
 
1172
1841
  - [ ] Schema `schema` especificado em queries e service?
@@ -1766,6 +2435,327 @@ customActions: [
1766
2435
 
1767
2436
  ---
1768
2437
 
2438
+ ### **🔍 TUTORIAL COMPLETO: FILTROS E EXPORTAÇÃO**
2439
+
2440
+ Este tutorial mostra como implementar filtros customizados e botão de exportação em páginas CRUD.
2441
+
2442
+ ---
2443
+
2444
+ #### **📋 Cenário: Página de Exemplos com Filtro de Status e Exportação**
2445
+
2446
+ Vamos criar uma página que:
2447
+ 1. ✅ Filtra exemplos por status (Ativo/Inativo/Todos)
2448
+ 2. ✅ Exporta todos os dados para CSV (não apenas da página atual)
2449
+ 3. ✅ Mantém filtros aplicados ao exportar
2450
+
2451
+ ---
2452
+
2453
+ #### **Passo 1: Configurar o Service**
2454
+
2455
+ ```typescript
2456
+ // src/examples/ExampleService.ts
2457
+ import { createSimpleService } from 'forlogic-core';
2458
+ import type { Example, CreateExamplePayload, UpdateExamplePayload } from './example';
2459
+
2460
+ export const { service: ExampleService, useCrudHook: useExamplesCrud } =
2461
+ createSimpleService<Example, CreateExamplePayload, UpdateExamplePayload>({
2462
+ tableName: 'examples',
2463
+ entityName: 'Exemplo',
2464
+ searchFields: ['title', 'description'],
2465
+ schemaName: 'central',
2466
+ enableQualiexEnrichment: true
2467
+ });
2468
+ ```
2469
+
2470
+ **✅ Por que exportar o `service`?**
2471
+ - Permite chamar `ExampleService.getAll()` diretamente para exportação
2472
+ - Busca todos os registros sem depender do manager paginado
2473
+
2474
+ ---
2475
+
2476
+ #### **Passo 2: Criar Componente de Filtro**
2477
+
2478
+ ```typescript
2479
+ // src/examples/ExamplesPage.tsx
2480
+
2481
+ // 1️⃣ Estado do filtro (padrão: true = apenas Ativos)
2482
+ const [statusFilter, setStatusFilter] = useState<boolean | 'all'>(true);
2483
+
2484
+ // 2️⃣ Manager com filtro aplicado no backend
2485
+ const manager = useExamplesCrud(
2486
+ statusFilter === 'all' ? {} : { is_actived: statusFilter }
2487
+ );
2488
+
2489
+ // 3️⃣ Componente de filtro customizado
2490
+ const StatusFilter = () => (
2491
+ <EntitySelect
2492
+ value={String(statusFilter)}
2493
+ onChange={(v) => setStatusFilter(v === 'all' ? 'all' : v === 'true')}
2494
+ items={[
2495
+ { id: 'true', name: 'Ativo' },
2496
+ { id: 'false', name: 'Inativo' },
2497
+ { id: 'all', name: '[Todos]' }
2498
+ ]}
2499
+ getItemValue={(item) => item.id}
2500
+ getItemLabel={(item) => item.name}
2501
+ placeholder="Status"
2502
+ className="w-full sm:w-[180px]"
2503
+ />
2504
+ );
2505
+ ```
2506
+
2507
+ **📝 Como funciona:**
2508
+ - `statusFilter` controla o estado do filtro
2509
+ - `useExamplesCrud` recebe `{ is_actived: true }` como filtro backend
2510
+ - Backend retorna apenas registros com `is_actived = true`
2511
+ - Paginação mostra total correto (ex: "1-10 de 43 itens")
2512
+
2513
+ ---
2514
+
2515
+ #### **Passo 3: Implementar Exportação para CSV**
2516
+
2517
+ ```typescript
2518
+ // src/examples/ExamplesPage.tsx
2519
+ import { Download } from 'lucide-react';
2520
+ import { useToast, formatDatetime } from 'forlogic-core';
2521
+ import { ExampleService } from './ExampleService';
2522
+
2523
+ export const ExamplesPage = () => {
2524
+ const { toast } = useToast();
2525
+ const [statusFilter, setStatusFilter] = useState<boolean | 'all'>(true);
2526
+ const manager = useExamplesCrud(
2527
+ statusFilter === 'all' ? {} : { is_actived: statusFilter }
2528
+ );
2529
+
2530
+ // 🚀 Função de exportação (busca TODOS os itens)
2531
+ const handleExport = async () => {
2532
+ try {
2533
+ // 1️⃣ Mostrar feedback
2534
+ toast({
2535
+ title: 'Exportando...',
2536
+ description: 'Buscando todos os registros'
2537
+ });
2538
+
2539
+ // 2️⃣ Buscar TODOS os registros (limite alto)
2540
+ const filters = statusFilter === 'all' ? {} : { is_actived: statusFilter };
2541
+ const allData = await ExampleService.getAll({
2542
+ search: manager.searchTerm, // Manter busca atual
2543
+ limit: 10000, // Limite alto para buscar tudo
2544
+ page: 1,
2545
+ ...filters // Aplicar filtros de status
2546
+ });
2547
+
2548
+ // 3️⃣ Validar dados
2549
+ if (!allData.data || allData.data.length === 0) {
2550
+ toast({
2551
+ variant: 'destructive',
2552
+ title: 'Nenhum dado disponível',
2553
+ description: 'Não há dados para exportar'
2554
+ });
2555
+ return;
2556
+ }
2557
+
2558
+ // 4️⃣ Definir cabeçalhos do CSV
2559
+ const headers = [
2560
+ 'Título',
2561
+ 'Responsável',
2562
+ 'Status',
2563
+ 'Link',
2564
+ 'Descrição',
2565
+ 'Cor',
2566
+ 'Ícone',
2567
+ 'Atualizado em'
2568
+ ];
2569
+
2570
+ // 5️⃣ Mapear dados para linhas CSV
2571
+ const rows = allData.data.map((item: Example) => [
2572
+ item.title || '',
2573
+ item.responsible_name || '',
2574
+ item.is_actived ? 'Ativo' : 'Inativo',
2575
+ item.url_field || '',
2576
+ item.description || '',
2577
+ item.color || '',
2578
+ item.icon_name || '',
2579
+ formatDatetime(item.updated_at)
2580
+ ]);
2581
+
2582
+ // 6️⃣ Criar conteúdo CSV
2583
+ const csvContent = [
2584
+ headers.join(','),
2585
+ ...rows.map(row =>
2586
+ row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',')
2587
+ )
2588
+ ].join('\n');
2589
+
2590
+ // 7️⃣ Criar blob e fazer download
2591
+ const blob = new Blob(['\ufeff' + csvContent], {
2592
+ type: 'text/csv;charset=utf-8;'
2593
+ });
2594
+ const link = document.createElement('a');
2595
+ const url = URL.createObjectURL(blob);
2596
+ link.setAttribute('href', url);
2597
+ link.setAttribute('download', `exemplos_${new Date().toISOString().split('T')[0]}.csv`);
2598
+ link.style.visibility = 'hidden';
2599
+ document.body.appendChild(link);
2600
+ link.click();
2601
+ document.body.removeChild(link);
2602
+
2603
+ // 8️⃣ Feedback de sucesso
2604
+ toast({
2605
+ title: 'Exportação concluída',
2606
+ description: `${allData.data.length} registros exportados com sucesso`
2607
+ });
2608
+ } catch (error) {
2609
+ console.error('Erro ao exportar:', error);
2610
+ toast({
2611
+ variant: 'destructive',
2612
+ title: 'Erro na exportação',
2613
+ description: 'Não foi possível exportar os dados'
2614
+ });
2615
+ }
2616
+ };
2617
+
2618
+ // Restante do código...
2619
+ };
2620
+ ```
2621
+
2622
+ **⚠️ IMPORTANTE:**
2623
+ - `ExampleService.getAll()` busca TODOS os registros (não só da página)
2624
+ - Limite de 10.000 itens - ajuste se necessário
2625
+ - `\ufeff` adiciona BOM para Unicode (acentos no Excel)
2626
+ - Escape de aspas duplas: `""` dentro do CSV
2627
+
2628
+ ---
2629
+
2630
+ #### **Passo 4: Configurar `customActions` e `filters`**
2631
+
2632
+ ```typescript
2633
+ // src/examples/ExamplesPage.tsx
2634
+
2635
+ const CrudPage = createCrudPage({
2636
+ manager,
2637
+ config: {
2638
+ entityName: 'exemplo',
2639
+ entityNamePlural: 'exemplos',
2640
+ columns: exampleColumns,
2641
+ formSections,
2642
+
2643
+ // ✅ Filtros na barra superior
2644
+ filters: [
2645
+ { type: 'search' }, // Busca padrão
2646
+ { type: 'custom', component: StatusFilter } // Filtro customizado
2647
+ ],
2648
+
2649
+ // ✅ Botão de exportar na barra de ações
2650
+ customActions: [
2651
+ {
2652
+ label: 'Exportar',
2653
+ icon: Download as any,
2654
+ variant: 'outline' as const,
2655
+ action: handleExport
2656
+ }
2657
+ ],
2658
+
2659
+ enableBulkActions: true
2660
+ },
2661
+ onSave: handleSave,
2662
+ onToggleStatus: handleToggleStatus
2663
+ });
2664
+ ```
2665
+
2666
+ ---
2667
+
2668
+ #### **🎯 Tipos de Filtros Disponíveis**
2669
+
2670
+ ##### **1. Filtro de Busca (Search)**
2671
+ ```typescript
2672
+ filters: [
2673
+ { type: 'search' } // Busca automática nos searchFields do service
2674
+ ]
2675
+ ```
2676
+
2677
+ ##### **2. Filtro Select Nativo**
2678
+ ```typescript
2679
+ filters: [
2680
+ {
2681
+ type: 'select',
2682
+ placeholder: 'Status',
2683
+ value: statusFilter,
2684
+ onChange: setStatusFilter,
2685
+ options: [
2686
+ { value: 'all', label: '[Todos]' },
2687
+ { value: 'active', label: 'Ativo' },
2688
+ { value: 'inactive', label: 'Inativo' }
2689
+ ]
2690
+ }
2691
+ ]
2692
+ ```
2693
+
2694
+ ##### **3. Filtro Customizado (Recomendado)**
2695
+ ```typescript
2696
+ const StatusFilter = () => (
2697
+ <EntitySelect
2698
+ value={String(statusFilter)}
2699
+ onChange={(v) => setStatusFilter(v === 'all' ? 'all' : v === 'true')}
2700
+ items={[
2701
+ { id: 'true', name: 'Ativo' },
2702
+ { id: 'false', name: 'Inativo' },
2703
+ { id: 'all', name: '[Todos]' }
2704
+ ]}
2705
+ getItemValue={(item) => item.id}
2706
+ getItemLabel={(item) => item.name}
2707
+ placeholder="Status"
2708
+ />
2709
+ );
2710
+
2711
+ filters: [
2712
+ { type: 'custom', component: StatusFilter }
2713
+ ]
2714
+ ```
2715
+
2716
+ ---
2717
+
2718
+ #### **📊 Resultado Visual**
2719
+
2720
+ **Barra de Ações:**
2721
+ ```
2722
+ [ + Novo ] [ 🔽 Exportar ] [ 🔍 Buscar... ] [ Status: Ativo ▼ ]
2723
+ ```
2724
+
2725
+ **Ao clicar em "Exportar":**
2726
+ 1. ✅ Toast: "Exportando... Buscando todos os registros"
2727
+ 2. ✅ Download automático: `exemplos_2025-01-21.csv`
2728
+ 3. ✅ Toast: "43 registros exportados com sucesso"
2729
+
2730
+ **Conteúdo do CSV:**
2731
+ ```csv
2732
+ Título,Responsável,Status,Link,Descrição,Cor,Ícone,Atualizado em
2733
+ "Exemplo 1","João Silva","Ativo","https://...","Descrição exemplo","#3b82f6","Star","21/01/2025 14:30"
2734
+ "Exemplo 2","Maria Santos","Inativo","","Outro exemplo","#f59e0b","Heart","20/01/2025 09:15"
2735
+ ```
2736
+
2737
+ ---
2738
+
2739
+ #### **🔥 Boas Práticas**
2740
+
2741
+ **✅ FAZER:**
2742
+ - Exportar via `service.getAll()` para buscar todos os registros
2743
+ - Aplicar mesmos filtros na exportação
2744
+ - Escapar aspas duplas: `cell.replace(/"/g, '""')`
2745
+ - Adicionar BOM Unicode: `\ufeff` para acentos
2746
+ - Mostrar feedback (toast) durante exportação
2747
+ - Usar `async/await` para exportação
2748
+
2749
+ **❌ NÃO FAZER:**
2750
+ - Exportar apenas `manager.entities` (só dados da página atual)
2751
+ - Ignorar filtros aplicados ao exportar
2752
+ - Esquecer de tratar erros
2753
+ - Exportar sem feedback visual
2754
+
2755
+ ---
2756
+
2757
+ ---
2758
+
1769
2759
  ### **6️⃣ Views Customizadas (substituir Tabela/Cards)**
1770
2760
 
1771
2761
  Substitua completamente a tabela padrão por qualquer componente customizado.