forlogic-core 1.7.5 → 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 +1032 -42
- package/dist/README.md +1032 -42
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.esm.js +2 -2
- package/dist/index.js +2 -2
- package/package.json +1 -1
package/dist/README.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1498
|
+
service={placeManagerService} // ⚠️ OBRIGATÓRIO
|
|
1079
1499
|
/>
|
|
1080
1500
|
```
|
|
1081
1501
|
|
|
1082
|
-
**
|
|
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
|
-
**
|
|
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) =>
|
|
1098
|
-
onSelectMember={(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
|
-
|
|
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,
|
|
1110
|
-
manager,
|
|
1111
|
-
members,
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
-
//
|
|
1131
|
-
await
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1700
|
+
---
|
|
1140
1701
|
|
|
1141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1155
|
-
|
|
1731
|
+
|
|
1732
|
+
{/* Badge com contador */}
|
|
1733
|
+
<PlaceManagerBadge
|
|
1734
|
+
placeId={place.id}
|
|
1156
1735
|
userCount={place.usersIds.length}
|
|
1736
|
+
service={placeManagerService}
|
|
1157
1737
|
/>
|
|
1158
|
-
|
|
1159
|
-
|
|
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.
|