forlogic-core 1.7.7 → 1.8.1
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 +835 -684
- package/dist/README.md +835 -684
- package/dist/assets/index-2WBeKMgq.css +1 -0
- package/dist/assets/index-Dg0JuUel.js +696 -0
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.esm.js +2 -2
- package/dist/index.html +19 -0
- package/dist/index.js +2 -2
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🧱 Projeto atual
|
|
2
2
|
|
|
3
|
-
**Schema padrão:** `
|
|
3
|
+
**Schema padrão:** `common`
|
|
4
4
|
|
|
5
5
|
> **Nota sobre schemas:** Temos o schema padrão, mas módulos podem usar schemas diferentes quando necessário para organização ou isolamento de dados. Por exemplo, o módulo de treinamentos usa o schema `trainings`. Você pode especificar o schema no service usando `schemaName: 'nome_do_schema'`.
|
|
6
6
|
|
|
@@ -466,6 +466,48 @@ export function DepartmentSelect(props: DepartmentSelectProps) {
|
|
|
466
466
|
|
|
467
467
|
O `BaseForm` suporta campos totalmente customizados através do tipo `'custom'`, permitindo usar **qualquer componente React** como campo de formulário.
|
|
468
468
|
|
|
469
|
+
#### ⚠️ IMPORTANTE: Como Usar Corretamente
|
|
470
|
+
|
|
471
|
+
**❌ ERRADO - Usar tipo customizado sem configurar:**
|
|
472
|
+
```typescript
|
|
473
|
+
// ❌ ISSO NÃO VAI FUNCIONAR!
|
|
474
|
+
{
|
|
475
|
+
name: 'annual_training_ids',
|
|
476
|
+
label: 'Treinamentos Anuais',
|
|
477
|
+
type: 'annual-training-multi-select', // ❌ BaseForm não conhece esse tipo
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// E passar customComponents na página:
|
|
481
|
+
<BaseForm
|
|
482
|
+
customComponents={{
|
|
483
|
+
'annual-training-multi-select': AnnualTrainingMultiSelect // ❌ Ignorado!
|
|
484
|
+
}}
|
|
485
|
+
/>
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**✅ CORRETO - Usar type: 'custom' com component:**
|
|
489
|
+
```typescript
|
|
490
|
+
import { AnnualTrainingMultiSelect } from './components/AnnualTrainingMultiSelect';
|
|
491
|
+
|
|
492
|
+
{
|
|
493
|
+
name: 'annual_training_ids',
|
|
494
|
+
label: 'Treinamentos Anuais',
|
|
495
|
+
type: 'custom', // ✅ Tipo reconhecido pelo BaseForm
|
|
496
|
+
component: AnnualTrainingMultiSelect, // ✅ Componente passado diretamente
|
|
497
|
+
required: true,
|
|
498
|
+
componentProps: { // ✅ Props adicionais (opcional)
|
|
499
|
+
placeholder: 'Selecionar treinamentos...'
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
#### 🔑 Pontos Críticos
|
|
505
|
+
|
|
506
|
+
1. **Use sempre `type: 'custom'`** - É o único tipo que o BaseForm reconhece para componentes customizados
|
|
507
|
+
2. **Passe o `component` diretamente** - Não existe prop `customComponents` no BaseForm
|
|
508
|
+
3. **Importe o componente** - Deve ser importado no arquivo onde você define os campos
|
|
509
|
+
4. **Interface obrigatória** - Seu componente DEVE aceitar: `value`, `onChange`, `disabled`, `error`
|
|
510
|
+
|
|
469
511
|
#### Interface do Componente Customizado
|
|
470
512
|
|
|
471
513
|
Todo campo customizado deve seguir esta interface:
|
|
@@ -574,35 +616,238 @@ const formFields: FormField[] = [
|
|
|
574
616
|
- ✅ **Flexibilidade** - Props extras via `componentProps`
|
|
575
617
|
- ✅ **Reatividade** - `updateField` e `onValueChange` funcionam corretamente
|
|
576
618
|
|
|
577
|
-
#### Exemplos
|
|
619
|
+
#### Exemplos Práticos Completos
|
|
578
620
|
|
|
579
|
-
**MultiSelect
|
|
621
|
+
**Exemplo 1: MultiSelect de Objetivos Estratégicos**
|
|
580
622
|
|
|
581
623
|
```typescript
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
624
|
+
// 1. Criar o componente customizado
|
|
625
|
+
// src/components/StrategicObjectivesMultiSelect.tsx
|
|
626
|
+
import { Label, Badge } from 'forlogic-core';
|
|
627
|
+
import { useStrategicObjectives } from '@/hooks/useStrategicObjectives';
|
|
628
|
+
|
|
629
|
+
interface Props {
|
|
630
|
+
value: string[];
|
|
631
|
+
onChange: (value: string[]) => void;
|
|
632
|
+
disabled?: boolean;
|
|
633
|
+
error?: string;
|
|
590
634
|
}
|
|
635
|
+
|
|
636
|
+
export function StrategicObjectivesMultiSelect({
|
|
637
|
+
value = [],
|
|
638
|
+
onChange,
|
|
639
|
+
disabled,
|
|
640
|
+
error
|
|
641
|
+
}: Props) {
|
|
642
|
+
const { data: objectives = [], isLoading } = useStrategicObjectives();
|
|
643
|
+
|
|
644
|
+
const toggleObjective = (id: string) => {
|
|
645
|
+
if (disabled) return;
|
|
646
|
+
const newValue = value.includes(id)
|
|
647
|
+
? value.filter(v => v !== id)
|
|
648
|
+
: [...value, id];
|
|
649
|
+
onChange(newValue);
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
if (isLoading) return <div>Carregando...</div>;
|
|
653
|
+
|
|
654
|
+
return (
|
|
655
|
+
<div className="space-y-2">
|
|
656
|
+
<Label>Objetivos Estratégicos</Label>
|
|
657
|
+
<div className="flex flex-wrap gap-2">
|
|
658
|
+
{objectives.map(obj => (
|
|
659
|
+
<Badge
|
|
660
|
+
key={obj.id}
|
|
661
|
+
variant={value.includes(obj.id) ? 'default' : 'outline'}
|
|
662
|
+
className="cursor-pointer"
|
|
663
|
+
onClick={() => toggleObjective(obj.id)}
|
|
664
|
+
>
|
|
665
|
+
{obj.name}
|
|
666
|
+
</Badge>
|
|
667
|
+
))}
|
|
668
|
+
</div>
|
|
669
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
670
|
+
</div>
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// 2. Usar no formulário - CORRETO ✅
|
|
675
|
+
import { StrategicObjectivesMultiSelect } from '@/components/StrategicObjectivesMultiSelect';
|
|
676
|
+
|
|
677
|
+
const formFields: FormField[] = [
|
|
678
|
+
{
|
|
679
|
+
name: 'strategic_objective_ids',
|
|
680
|
+
label: '',
|
|
681
|
+
type: 'custom', // ✅
|
|
682
|
+
component: StrategicObjectivesMultiSelect, // ✅
|
|
683
|
+
required: true,
|
|
684
|
+
}
|
|
685
|
+
];
|
|
591
686
|
```
|
|
592
687
|
|
|
593
|
-
**
|
|
688
|
+
**Exemplo 2: Select de Usuários do Qualiex**
|
|
594
689
|
|
|
595
690
|
```typescript
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
691
|
+
// 1. Componente customizado
|
|
692
|
+
// src/components/QualiexUserSelect.tsx
|
|
693
|
+
import { EntitySelect } from 'forlogic-core';
|
|
694
|
+
import { useQualiexUsers } from '@/hooks/useQualiexUsers';
|
|
695
|
+
|
|
696
|
+
interface Props {
|
|
697
|
+
value: string;
|
|
698
|
+
onChange: (value: string) => void;
|
|
699
|
+
disabled?: boolean;
|
|
700
|
+
error?: string;
|
|
701
|
+
placeId?: string; // Filtrar por local
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export function QualiexUserSelect({
|
|
705
|
+
value,
|
|
706
|
+
onChange,
|
|
707
|
+
disabled,
|
|
708
|
+
error,
|
|
709
|
+
placeId
|
|
710
|
+
}: Props) {
|
|
711
|
+
const { data: users = [], isLoading } = useQualiexUsers({ placeId });
|
|
712
|
+
|
|
713
|
+
return (
|
|
714
|
+
<div className="space-y-2">
|
|
715
|
+
<EntitySelect
|
|
716
|
+
value={value}
|
|
717
|
+
onChange={onChange}
|
|
718
|
+
items={users}
|
|
719
|
+
isLoading={isLoading}
|
|
720
|
+
disabled={disabled}
|
|
721
|
+
getItemValue={(u) => u.id}
|
|
722
|
+
getItemLabel={(u) => `${u.name} - ${u.email}`}
|
|
723
|
+
placeholder="Selecionar usuário..."
|
|
724
|
+
searchPlaceholder="Buscar usuário..."
|
|
725
|
+
/>
|
|
726
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
727
|
+
</div>
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// 2. Usar no formulário - CORRETO ✅
|
|
732
|
+
import { QualiexUserSelect } from '@/components/QualiexUserSelect';
|
|
733
|
+
|
|
734
|
+
const formFields: FormField[] = [
|
|
735
|
+
{
|
|
736
|
+
name: 'id_responsible',
|
|
737
|
+
label: 'Responsável',
|
|
738
|
+
type: 'custom', // ✅
|
|
739
|
+
component: QualiexUserSelect, // ✅
|
|
740
|
+
required: true,
|
|
741
|
+
componentProps: {
|
|
742
|
+
placeId: currentPlaceId // Props extras
|
|
743
|
+
}
|
|
602
744
|
}
|
|
745
|
+
];
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
**Exemplo 3: MultiSelect de Treinamentos Anuais**
|
|
749
|
+
|
|
750
|
+
```typescript
|
|
751
|
+
// 1. Componente customizado
|
|
752
|
+
// src/training/components/AnnualTrainingMultiSelect.tsx
|
|
753
|
+
import { Checkbox, Label } from 'forlogic-core';
|
|
754
|
+
import { useAnnualTrainings } from '@/hooks/useAnnualTrainings';
|
|
755
|
+
|
|
756
|
+
interface Props {
|
|
757
|
+
value: string[];
|
|
758
|
+
onChange: (value: string[]) => void;
|
|
759
|
+
disabled?: boolean;
|
|
760
|
+
error?: string;
|
|
761
|
+
year?: number;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
export function AnnualTrainingMultiSelect({
|
|
765
|
+
value = [],
|
|
766
|
+
onChange,
|
|
767
|
+
disabled,
|
|
768
|
+
error,
|
|
769
|
+
year
|
|
770
|
+
}: Props) {
|
|
771
|
+
const { data: trainings = [], isLoading } = useAnnualTrainings(year);
|
|
772
|
+
|
|
773
|
+
const toggleTraining = (id: string) => {
|
|
774
|
+
if (disabled) return;
|
|
775
|
+
const newValue = value.includes(id)
|
|
776
|
+
? value.filter(v => v !== id)
|
|
777
|
+
: [...value, id];
|
|
778
|
+
onChange(newValue);
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
if (isLoading) return <div>Carregando treinamentos...</div>;
|
|
782
|
+
|
|
783
|
+
return (
|
|
784
|
+
<div className="space-y-2">
|
|
785
|
+
<Label>Treinamentos Anuais {year && `(${year})`}</Label>
|
|
786
|
+
<div className="space-y-2 max-h-60 overflow-y-auto border rounded-md p-3">
|
|
787
|
+
{trainings.map(training => (
|
|
788
|
+
<div key={training.id} className="flex items-center gap-2">
|
|
789
|
+
<Checkbox
|
|
790
|
+
id={`training-${training.id}`}
|
|
791
|
+
checked={value.includes(training.id)}
|
|
792
|
+
onCheckedChange={() => toggleTraining(training.id)}
|
|
793
|
+
disabled={disabled}
|
|
794
|
+
/>
|
|
795
|
+
<Label
|
|
796
|
+
htmlFor={`training-${training.id}`}
|
|
797
|
+
className="font-normal cursor-pointer"
|
|
798
|
+
>
|
|
799
|
+
{training.title}
|
|
800
|
+
</Label>
|
|
801
|
+
</div>
|
|
802
|
+
))}
|
|
803
|
+
</div>
|
|
804
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
805
|
+
</div>
|
|
806
|
+
);
|
|
603
807
|
}
|
|
808
|
+
|
|
809
|
+
// 2. Usar no formulário - CORRETO ✅
|
|
810
|
+
import { AnnualTrainingMultiSelect } from '@/training/components/AnnualTrainingMultiSelect';
|
|
811
|
+
|
|
812
|
+
const formFields: FormField[] = [
|
|
813
|
+
{
|
|
814
|
+
name: 'annual_training_ids',
|
|
815
|
+
label: '',
|
|
816
|
+
type: 'custom', // ✅
|
|
817
|
+
component: AnnualTrainingMultiSelect, // ✅
|
|
818
|
+
required: true,
|
|
819
|
+
componentProps: {
|
|
820
|
+
year: 2025 // Props extras
|
|
821
|
+
},
|
|
822
|
+
validation: (value) => {
|
|
823
|
+
if (!value?.length) return 'Selecione pelo menos um treinamento';
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
];
|
|
604
827
|
```
|
|
605
828
|
|
|
829
|
+
#### ❌ Erros Comuns e Soluções
|
|
830
|
+
|
|
831
|
+
| Erro | Causa | Solução |
|
|
832
|
+
|------|-------|---------|
|
|
833
|
+
| Campo aparece como input text | Tipo customizado não reconhecido | Use `type: 'custom'` em vez de `type: 'meu-campo-custom'` |
|
|
834
|
+
| "customComponents is not used" | Passando prop que não existe | Remova `customComponents` e use `component` direto no field |
|
|
835
|
+
| "component is not defined" | Esqueceu de importar | Importe o componente no arquivo de fields |
|
|
836
|
+
| Campo não recebe props | componentProps ignorado | Verifique se seu componente aceita as props via `...props` |
|
|
837
|
+
| onChange não funciona | Componente não chama onChange | Certifique-se de chamar `onChange(novoValor)` |
|
|
838
|
+
|
|
839
|
+
#### 📋 Checklist de Implementação
|
|
840
|
+
|
|
841
|
+
Ao criar um campo customizado, verifique:
|
|
842
|
+
|
|
843
|
+
- [ ] Componente aceita `value`, `onChange`, `disabled`, `error`
|
|
844
|
+
- [ ] `type: 'custom'` está configurado
|
|
845
|
+
- [ ] `component: SeuComponente` está passado
|
|
846
|
+
- [ ] Componente está importado no arquivo de fields
|
|
847
|
+
- [ ] `onChange` é chamado quando valor muda
|
|
848
|
+
- [ ] `error` é exibido quando presente
|
|
849
|
+
- [ ] `disabled` desabilita interações
|
|
850
|
+
|
|
606
851
|
---
|
|
607
852
|
|
|
608
853
|
### 🎯 Campo Creatable Select
|
|
@@ -1393,400 +1638,454 @@ userFieldsMapping: [
|
|
|
1393
1638
|
|
|
1394
1639
|
---
|
|
1395
1640
|
|
|
1396
|
-
### 🏢
|
|
1641
|
+
### 🏢 Places - Hierarquia de Locais com Gerenciamento de Acessos
|
|
1397
1642
|
|
|
1398
|
-
|
|
1643
|
+
Sistema completo para visualização hierárquica de locais e gerenciamento configurável de acessos (gestores e membros).
|
|
1399
1644
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
---
|
|
1403
|
-
|
|
1404
|
-
#### 📦 Importação
|
|
1645
|
+
#### 📦 Importações
|
|
1405
1646
|
|
|
1406
1647
|
```typescript
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
PlaceManagerBadge,
|
|
1410
|
-
ManagerSelectionDialog,
|
|
1411
|
-
usePlaceManagers,
|
|
1412
|
-
PlaceManagerService,
|
|
1413
|
-
type PlaceManagerServiceConfig,
|
|
1414
|
-
type PlaceManager
|
|
1415
|
-
} from 'forlogic-core';
|
|
1416
|
-
```
|
|
1648
|
+
// Componentes
|
|
1649
|
+
import { PlacesList, PlaceCard, ManageAccessModal } from 'forlogic-core/places';
|
|
1417
1650
|
|
|
1418
|
-
|
|
1651
|
+
// Service
|
|
1652
|
+
import { placeService } from 'forlogic-core/places';
|
|
1419
1653
|
|
|
1420
|
-
|
|
1654
|
+
// Types
|
|
1655
|
+
import type { Place, PlacesListProps, PlaceCardProps, ManageAccessModalProps } from 'forlogic-core/places';
|
|
1421
1656
|
|
|
1422
|
-
|
|
1657
|
+
// Ou importação completa
|
|
1658
|
+
import { PlacesList, placeService } from 'forlogic-core';
|
|
1659
|
+
```
|
|
1423
1660
|
|
|
1424
|
-
|
|
1661
|
+
#### 📋 Tipos de Dados
|
|
1425
1662
|
|
|
1426
|
-
```
|
|
1427
|
-
|
|
1428
|
-
id
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
-
);
|
|
1663
|
+
```typescript
|
|
1664
|
+
interface Place {
|
|
1665
|
+
id: string;
|
|
1666
|
+
name: string;
|
|
1667
|
+
usersCount?: number;
|
|
1668
|
+
hasChild?: boolean;
|
|
1669
|
+
isActive?: boolean;
|
|
1670
|
+
parentId?: string;
|
|
1671
|
+
parentName?: string;
|
|
1672
|
+
subPlaces?: Place[];
|
|
1673
|
+
}
|
|
1674
|
+
```
|
|
1441
1675
|
|
|
1442
|
-
|
|
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);
|
|
1676
|
+
#### 🎯 Uso Básico - Apenas Visualização
|
|
1445
1677
|
|
|
1446
|
-
|
|
1447
|
-
|
|
1678
|
+
```typescript
|
|
1679
|
+
import { PlacesList } from 'forlogic-core';
|
|
1680
|
+
import { useAuth, placeService } from 'forlogic-core';
|
|
1681
|
+
import { useQuery } from '@tanstack/react-query';
|
|
1682
|
+
|
|
1683
|
+
function MyPlacesPage() {
|
|
1684
|
+
const { alias } = useAuth();
|
|
1685
|
+
const tokenData = TokenManager.extractTokenData();
|
|
1686
|
+
const companyId = tokenData?.companyId;
|
|
1448
1687
|
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1688
|
+
const { data: places = [], isLoading } = useQuery({
|
|
1689
|
+
queryKey: ['places', alias, companyId],
|
|
1690
|
+
queryFn: () => placeService.getPlaces(alias!, companyId!),
|
|
1691
|
+
enabled: !!alias && !!companyId,
|
|
1692
|
+
});
|
|
1452
1693
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
USING (((SELECT auth.jwt()) ->> 'alias'::text) = alias);
|
|
1694
|
+
return <PlacesList places={places} isLoading={isLoading} />;
|
|
1695
|
+
}
|
|
1456
1696
|
```
|
|
1457
1697
|
|
|
1458
|
-
>
|
|
1698
|
+
> **💡 Neste modo**, o `PlacesList` apenas renderiza a hierarquia visual. O botão "Gerenciar Acessos" não aparece.
|
|
1459
1699
|
|
|
1460
1700
|
---
|
|
1461
1701
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
Crie um arquivo de serviço customizado no seu projeto:
|
|
1702
|
+
#### 🔐 Uso Avançado - Com Gerenciamento de Acessos
|
|
1465
1703
|
|
|
1466
1704
|
```typescript
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1705
|
+
interface PlacesListProps {
|
|
1706
|
+
places: Place[];
|
|
1707
|
+
isLoading: boolean;
|
|
1708
|
+
manageAccessConfig?: {
|
|
1709
|
+
onMakeManager?: (userId: string, placeId: string) => void | Promise<void>;
|
|
1710
|
+
onMakeMembers?: (userIds: string[], placeId: string) => void | Promise<void>;
|
|
1711
|
+
getCurrentManagerId?: (placeId: string) => string | undefined;
|
|
1712
|
+
getCurrentMemberIds?: (placeId: string) => string[] | undefined;
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1474
1715
|
```
|
|
1475
1716
|
|
|
1476
|
-
**
|
|
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') |
|
|
1717
|
+
**Exemplo Completo:**
|
|
1482
1718
|
|
|
1483
|
-
|
|
1719
|
+
```typescript
|
|
1720
|
+
import { PlacesList, Card } from 'forlogic-core';
|
|
1721
|
+
import { useAuth, placeService } from 'forlogic-core';
|
|
1722
|
+
import { useQuery } from '@tanstack/react-query';
|
|
1484
1723
|
|
|
1485
|
-
|
|
1724
|
+
function MyPlacesPageWithManagement() {
|
|
1725
|
+
const { alias } = useAuth();
|
|
1726
|
+
const tokenData = TokenManager.extractTokenData();
|
|
1727
|
+
const companyId = tokenData?.companyId;
|
|
1728
|
+
|
|
1729
|
+
// Buscar places
|
|
1730
|
+
const { data: places = [], isLoading } = useQuery({
|
|
1731
|
+
queryKey: ['places', alias, companyId],
|
|
1732
|
+
queryFn: () => placeService.getPlaces(alias!, companyId!),
|
|
1733
|
+
enabled: !!alias && !!companyId,
|
|
1734
|
+
});
|
|
1486
1735
|
|
|
1487
|
-
|
|
1736
|
+
// Buscar gestores e membros atuais (implementação do módulo)
|
|
1737
|
+
const { data: accessData = [] } = useQuery({
|
|
1738
|
+
queryKey: ['placeAccess'],
|
|
1739
|
+
queryFn: () => myAccessService.getAllAccess(),
|
|
1740
|
+
});
|
|
1488
1741
|
|
|
1489
|
-
|
|
1742
|
+
// Callbacks de gerenciamento
|
|
1743
|
+
const handleMakeManager = async (userId: string, placeId: string) => {
|
|
1744
|
+
await myAccessService.setManager(placeId, userId);
|
|
1745
|
+
queryClient.invalidateQueries(['placeAccess']);
|
|
1746
|
+
};
|
|
1490
1747
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1748
|
+
const handleMakeMembers = async (userIds: string[], placeId: string) => {
|
|
1749
|
+
await myAccessService.addMembers(placeId, userIds);
|
|
1750
|
+
queryClient.invalidateQueries(['placeAccess']);
|
|
1751
|
+
};
|
|
1494
1752
|
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
service={placeManagerService} // ⚠️ OBRIGATÓRIO
|
|
1499
|
-
/>
|
|
1500
|
-
```
|
|
1753
|
+
const getCurrentManagerId = (placeId: string) => {
|
|
1754
|
+
return accessData.find(a => a.placeId === placeId && a.role === 'manager')?.userId;
|
|
1755
|
+
};
|
|
1501
1756
|
|
|
1502
|
-
|
|
1757
|
+
const getCurrentMemberIds = (placeId: string) => {
|
|
1758
|
+
return accessData.filter(a => a.placeId === placeId && a.role === 'member').map(a => a.userId);
|
|
1759
|
+
};
|
|
1503
1760
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1761
|
+
return (
|
|
1762
|
+
<Card className="p-6">
|
|
1763
|
+
<PlacesList
|
|
1764
|
+
places={places}
|
|
1765
|
+
isLoading={isLoading}
|
|
1766
|
+
manageAccessConfig={{
|
|
1767
|
+
onMakeManager: handleMakeManager,
|
|
1768
|
+
onMakeMembers: handleMakeMembers,
|
|
1769
|
+
getCurrentManagerId,
|
|
1770
|
+
getCurrentMemberIds
|
|
1771
|
+
}}
|
|
1772
|
+
/>
|
|
1773
|
+
</Card>
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
```
|
|
1509
1777
|
|
|
1510
1778
|
---
|
|
1511
1779
|
|
|
1512
|
-
|
|
1780
|
+
#### 🎨 Componentes Individuais
|
|
1513
1781
|
|
|
1514
|
-
|
|
1782
|
+
##### PlacesList
|
|
1515
1783
|
|
|
1516
|
-
|
|
1517
|
-
import { PlaceManagerBadge } from 'forlogic-core';
|
|
1518
|
-
import { placeManagerService } from '@/services/placeManagerService';
|
|
1519
|
-
|
|
1520
|
-
<PlaceManagerBadge
|
|
1521
|
-
placeId="abc-123"
|
|
1522
|
-
userCount={15}
|
|
1523
|
-
service={placeManagerService} // ⚠️ OBRIGATÓRIO
|
|
1524
|
-
/>
|
|
1525
|
-
```
|
|
1784
|
+
Componente principal que renderiza a lista hierárquica de locais.
|
|
1526
1785
|
|
|
1527
1786
|
**Props:**
|
|
1787
|
+
- `places: Place[]` - Array de locais (hierarquia já construída)
|
|
1788
|
+
- `isLoading: boolean` - Estado de carregamento
|
|
1789
|
+
- `manageAccessConfig?: object` - Callbacks de gerenciamento (opcional)
|
|
1528
1790
|
|
|
1529
|
-
|
|
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:**
|
|
1791
|
+
##### PlaceCard
|
|
1536
1792
|
|
|
1537
|
-
|
|
1538
|
-
- **"Gestores"** - Quando não há gestor definido (apenas membros)
|
|
1793
|
+
Componente individual de local com suporte a colapso de sublocais.
|
|
1539
1794
|
|
|
1540
|
-
|
|
1795
|
+
**Props:**
|
|
1796
|
+
- `place: Place` - Dados do local
|
|
1797
|
+
- `level?: number` - Nível hierárquico (controla indentação)
|
|
1798
|
+
- `manageAccessConfig?: object` - Callbacks (propagado recursivamente)
|
|
1541
1799
|
|
|
1542
|
-
#####
|
|
1800
|
+
##### ManageAccessModal
|
|
1543
1801
|
|
|
1544
|
-
|
|
1802
|
+
Modal para gerenciamento de acessos ao local.
|
|
1545
1803
|
|
|
1546
|
-
```
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
currentManagerId
|
|
1555
|
-
currentMemberIds
|
|
1556
|
-
|
|
1557
|
-
isLoading={false}
|
|
1558
|
-
/>
|
|
1804
|
+
```typescript
|
|
1805
|
+
interface ManageAccessModalProps {
|
|
1806
|
+
open: boolean;
|
|
1807
|
+
onOpenChange: (open: boolean) => void;
|
|
1808
|
+
placeId: string;
|
|
1809
|
+
placeName: string;
|
|
1810
|
+
onMakeManager?: (userId: string, placeId: string) => void | Promise<void>;
|
|
1811
|
+
onMakeMembers?: (userIds: string[], placeId: string) => void | Promise<void>;
|
|
1812
|
+
currentManagerId?: string;
|
|
1813
|
+
currentMemberIds?: string[];
|
|
1814
|
+
}
|
|
1559
1815
|
```
|
|
1560
1816
|
|
|
1561
|
-
**
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
| `currentMemberIds` | `string[]` | ❌ | IDs dos membros atuais |
|
|
1571
|
-
| `placeName` | `string` | ✅ | Nome do local (para exibição) |
|
|
1572
|
-
| `isLoading` | `boolean` | ❌ | Estado de loading externo (opcional) |
|
|
1817
|
+
**Funcionalidades:**
|
|
1818
|
+
- Lista todos os usuários do alias (via `useQualiexUsers`)
|
|
1819
|
+
- Busca por nome ou email
|
|
1820
|
+
- Ordenação alfabética
|
|
1821
|
+
- Seleção múltipla de usuários
|
|
1822
|
+
- Regras automáticas:
|
|
1823
|
+
* 1 usuário selecionado: mostra "Tornar Gestor" + "Tornar Membro"
|
|
1824
|
+
* 2+ usuários selecionados: mostra apenas "Tornar Membros"
|
|
1825
|
+
* 0 usuários selecionados: botões desabilitados
|
|
1573
1826
|
|
|
1574
1827
|
---
|
|
1575
1828
|
|
|
1576
|
-
#### 🔧
|
|
1577
|
-
|
|
1578
|
-
Hook React Query para gerenciar gestores e membros de um local.
|
|
1829
|
+
#### 🔧 Service - placeService
|
|
1579
1830
|
|
|
1580
1831
|
```typescript
|
|
1581
|
-
|
|
1582
|
-
|
|
1832
|
+
async getPlaces(alias: string, companyId: string): Promise<Place[]>
|
|
1833
|
+
```
|
|
1834
|
+
|
|
1835
|
+
Busca locais da API Qualiex e constrói hierarquia automaticamente.
|
|
1836
|
+
|
|
1837
|
+
**Características:**
|
|
1838
|
+
- Suporta renovação automática de token (via QualiexErrorInterceptor)
|
|
1839
|
+
- Constrói hierarquia baseada em `parentId`
|
|
1840
|
+
- Tratamento de erros com `errorService`
|
|
1583
1841
|
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
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);
|
|
1842
|
+
**Exemplo:**
|
|
1843
|
+
```typescript
|
|
1844
|
+
const places = await placeService.getPlaces('my-alias', 'company-123');
|
|
1596
1845
|
```
|
|
1597
1846
|
|
|
1598
|
-
|
|
1847
|
+
---
|
|
1599
1848
|
|
|
1600
|
-
|
|
1601
|
-
| --------- | --------------------- | ----------- | ------------------------------------------ |
|
|
1602
|
-
| `placeId` | `string` | ✅ | ID do local |
|
|
1603
|
-
| `service` | `PlaceManagerService` | ✅ | Instância do serviço configurado |
|
|
1604
|
-
| `enabled` | `boolean` | ❌ | Habilitar/desabilitar query (default: `true`) |
|
|
1849
|
+
#### 🎯 Regras de Negócio
|
|
1605
1850
|
|
|
1606
|
-
|
|
1851
|
+
> **⚠️ Importante:** O módulo fornece apenas a INTERFACE. A lógica de persistência deve ser implementada pelo projeto consumidor.
|
|
1607
1852
|
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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 |
|
|
1853
|
+
**Regras sugeridas:**
|
|
1854
|
+
1. **Um gestor por local** - Ao atribuir novo gestor, remover o anterior
|
|
1855
|
+
2. **Múltiplos membros por local** - Sem limite
|
|
1856
|
+
3. **Usuário não pode ser gestor e membro simultaneamente** do mesmo local
|
|
1857
|
+
4. **Validar permissões** antes de salvar
|
|
1620
1858
|
|
|
1621
1859
|
---
|
|
1622
1860
|
|
|
1623
|
-
####
|
|
1861
|
+
#### 🗄️ Estrutura de Banco Sugerida (Supabase)
|
|
1624
1862
|
|
|
1625
|
-
|
|
1863
|
+
```sql
|
|
1864
|
+
CREATE TABLE IF NOT EXISTS public.place_access (
|
|
1865
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1866
|
+
place_id text NOT NULL,
|
|
1867
|
+
user_id text NOT NULL,
|
|
1868
|
+
user_name text NOT NULL,
|
|
1869
|
+
user_email text NOT NULL,
|
|
1870
|
+
role text NOT NULL CHECK (role IN ('manager', 'member')),
|
|
1871
|
+
created_at timestamptz DEFAULT now(),
|
|
1872
|
+
updated_at timestamptz DEFAULT now(),
|
|
1873
|
+
UNIQUE (place_id, user_id)
|
|
1874
|
+
);
|
|
1626
1875
|
|
|
1627
|
-
|
|
1876
|
+
CREATE INDEX idx_place_access_place ON public.place_access(place_id);
|
|
1877
|
+
CREATE INDEX idx_place_access_user ON public.place_access(user_id);
|
|
1628
1878
|
|
|
1629
|
-
|
|
1630
|
-
|
|
1879
|
+
-- RLS Policies
|
|
1880
|
+
ALTER TABLE public.place_access ENABLE ROW LEVEL SECURITY;
|
|
1631
1881
|
|
|
1632
|
-
|
|
1633
|
-
|
|
1882
|
+
CREATE POLICY "Users can view place access"
|
|
1883
|
+
ON public.place_access FOR SELECT
|
|
1884
|
+
USING (true);
|
|
1634
1885
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
schemaName: 'gestao'
|
|
1639
|
-
});
|
|
1886
|
+
CREATE POLICY "Authenticated users can manage place access"
|
|
1887
|
+
ON public.place_access FOR ALL
|
|
1888
|
+
USING (auth.role() = 'authenticated');
|
|
1640
1889
|
```
|
|
1641
1890
|
|
|
1642
|
-
|
|
1891
|
+
---
|
|
1892
|
+
|
|
1893
|
+
#### 💡 Exemplo Completo de Implementação
|
|
1894
|
+
|
|
1895
|
+
**1. Service de Acesso (`src/places/services/PlaceAccessService.ts`):**
|
|
1643
1896
|
|
|
1644
1897
|
```typescript
|
|
1645
|
-
|
|
1646
|
-
const managers = await placeManagerService.getPlaceManagers(
|
|
1647
|
-
alias: string,
|
|
1648
|
-
placeId: string
|
|
1649
|
-
);
|
|
1650
|
-
// Retorna: PlaceManager[]
|
|
1898
|
+
import { getSupabaseClient } from 'forlogic-core';
|
|
1651
1899
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
alias: string,
|
|
1655
|
-
placeId: string,
|
|
1656
|
-
user: QualiexUser
|
|
1657
|
-
);
|
|
1658
|
-
// Retorna: void
|
|
1900
|
+
class PlaceAccessService {
|
|
1901
|
+
private supabase = getSupabaseClient();
|
|
1659
1902
|
|
|
1660
|
-
|
|
1661
|
-
await
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
);
|
|
1666
|
-
|
|
1903
|
+
async getAllAccess() {
|
|
1904
|
+
const { data, error } = await this.supabase
|
|
1905
|
+
.from('place_access')
|
|
1906
|
+
.select('*');
|
|
1907
|
+
|
|
1908
|
+
if (error) throw error;
|
|
1909
|
+
return data;
|
|
1910
|
+
}
|
|
1667
1911
|
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
)
|
|
1674
|
-
|
|
1675
|
-
|
|
1912
|
+
async setManager(placeId: string, userId: string, userName: string, userEmail: string) {
|
|
1913
|
+
// Remover gestor anterior
|
|
1914
|
+
await this.supabase
|
|
1915
|
+
.from('place_access')
|
|
1916
|
+
.delete()
|
|
1917
|
+
.eq('place_id', placeId)
|
|
1918
|
+
.eq('role', 'manager');
|
|
1919
|
+
|
|
1920
|
+
// Adicionar novo gestor
|
|
1921
|
+
const { error } = await this.supabase
|
|
1922
|
+
.from('place_access')
|
|
1923
|
+
.insert({
|
|
1924
|
+
place_id: placeId,
|
|
1925
|
+
user_id: userId,
|
|
1926
|
+
user_name: userName,
|
|
1927
|
+
user_email: userEmail,
|
|
1928
|
+
role: 'manager'
|
|
1929
|
+
});
|
|
1676
1930
|
|
|
1677
|
-
|
|
1931
|
+
if (error) throw error;
|
|
1932
|
+
}
|
|
1678
1933
|
|
|
1679
|
-
|
|
1934
|
+
async addMembers(
|
|
1935
|
+
placeId: string,
|
|
1936
|
+
users: Array<{userId: string, userName: string, userEmail: string}>
|
|
1937
|
+
) {
|
|
1938
|
+
const { error } = await this.supabase
|
|
1939
|
+
.from('place_access')
|
|
1940
|
+
.upsert(
|
|
1941
|
+
users.map(user => ({
|
|
1942
|
+
place_id: placeId,
|
|
1943
|
+
user_id: user.userId,
|
|
1944
|
+
user_name: user.userName,
|
|
1945
|
+
user_email: user.userEmail,
|
|
1946
|
+
role: 'member'
|
|
1947
|
+
}))
|
|
1948
|
+
);
|
|
1680
1949
|
|
|
1681
|
-
|
|
1682
|
-
|
|
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;
|
|
1950
|
+
if (error) throw error;
|
|
1951
|
+
}
|
|
1692
1952
|
}
|
|
1693
1953
|
|
|
1694
|
-
|
|
1695
|
-
tableName?: string; // Default: 'place_managers'
|
|
1696
|
-
schemaName?: string; // Default: 'public'
|
|
1697
|
-
}
|
|
1954
|
+
export const placeAccessService = new PlaceAccessService();
|
|
1698
1955
|
```
|
|
1699
1956
|
|
|
1700
|
-
|
|
1957
|
+
**2. Hook Customizado (`src/places/hooks/usePlaceAccess.ts`):**
|
|
1701
1958
|
|
|
1702
|
-
|
|
1959
|
+
```typescript
|
|
1960
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
1961
|
+
import { placeAccessService } from '../services/PlaceAccessService';
|
|
1962
|
+
import { useToast } from 'forlogic-core';
|
|
1963
|
+
import type { QualiexUser } from 'forlogic-core';
|
|
1703
1964
|
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
4. ✅ **Busca inteligente**: Filtra por nome ou email do usuário
|
|
1708
|
-
5. ✅ **Integração Qualiex**: Utiliza `useQualiexUsers` para listar usuários
|
|
1965
|
+
export function usePlaceAccess() {
|
|
1966
|
+
const queryClient = useQueryClient();
|
|
1967
|
+
const { toast } = useToast();
|
|
1709
1968
|
|
|
1710
|
-
|
|
1969
|
+
const { data: accessData = [] } = useQuery({
|
|
1970
|
+
queryKey: ['placeAccess'],
|
|
1971
|
+
queryFn: () => placeAccessService.getAllAccess(),
|
|
1972
|
+
});
|
|
1711
1973
|
|
|
1712
|
-
|
|
1974
|
+
const setManagerMutation = useMutation({
|
|
1975
|
+
mutationFn: ({ placeId, user }: { placeId: string; user: QualiexUser }) =>
|
|
1976
|
+
placeAccessService.setManager(
|
|
1977
|
+
placeId,
|
|
1978
|
+
user.userId!,
|
|
1979
|
+
user.userName!,
|
|
1980
|
+
user.userEmail!
|
|
1981
|
+
),
|
|
1982
|
+
onSuccess: () => {
|
|
1983
|
+
queryClient.invalidateQueries(['placeAccess']);
|
|
1984
|
+
toast({ title: 'Gestor definido com sucesso' });
|
|
1985
|
+
},
|
|
1986
|
+
onError: () => {
|
|
1987
|
+
toast({
|
|
1988
|
+
title: 'Erro ao definir gestor',
|
|
1989
|
+
variant: 'destructive'
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
});
|
|
1713
1993
|
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1994
|
+
const addMembersMutation = useMutation({
|
|
1995
|
+
mutationFn: ({ placeId, users }: { placeId: string; users: QualiexUser[] }) =>
|
|
1996
|
+
placeAccessService.addMembers(
|
|
1997
|
+
placeId,
|
|
1998
|
+
users.map(u => ({
|
|
1999
|
+
userId: u.userId!,
|
|
2000
|
+
userName: u.userName!,
|
|
2001
|
+
userEmail: u.userEmail!
|
|
2002
|
+
}))
|
|
2003
|
+
),
|
|
2004
|
+
onSuccess: () => {
|
|
2005
|
+
queryClient.invalidateQueries(['placeAccess']);
|
|
2006
|
+
toast({ title: 'Membros adicionados com sucesso' });
|
|
2007
|
+
},
|
|
2008
|
+
onError: () => {
|
|
2009
|
+
toast({
|
|
2010
|
+
title: 'Erro ao adicionar membros',
|
|
2011
|
+
variant: 'destructive'
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
});
|
|
1717
2015
|
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
2016
|
+
return {
|
|
2017
|
+
accessData,
|
|
2018
|
+
setManager: setManagerMutation.mutateAsync,
|
|
2019
|
+
addMembers: addMembersMutation.mutateAsync,
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
```
|
|
1722
2023
|
|
|
1723
|
-
|
|
1724
|
-
import { PlaceManagerButton, PlaceManagerBadge } from 'forlogic-core';
|
|
1725
|
-
import { placeManagerService } from '@/services/placeManagerService';
|
|
2024
|
+
**3. Página Final (`src/places/PlacesPage.tsx`):**
|
|
1726
2025
|
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
service={placeManagerService}
|
|
1744
|
-
/>
|
|
1745
|
-
</div>
|
|
1746
|
-
);
|
|
1747
|
-
}
|
|
2026
|
+
```typescript
|
|
2027
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2028
|
+
import { useAuth, TokenManager, placeService, PlacesList, Card } from 'forlogic-core';
|
|
2029
|
+
import { usePlaceAccess } from './hooks/usePlaceAccess';
|
|
2030
|
+
import { useQualiexUsers } from 'forlogic-core';
|
|
2031
|
+
|
|
2032
|
+
export function PlacesPage() {
|
|
2033
|
+
const { alias } = useAuth();
|
|
2034
|
+
const tokenData = TokenManager.extractTokenData();
|
|
2035
|
+
const companyId = tokenData?.companyId;
|
|
2036
|
+
|
|
2037
|
+
const { data: places = [], isLoading } = useQuery({
|
|
2038
|
+
queryKey: ['places', alias, companyId],
|
|
2039
|
+
queryFn: () => placeService.getPlaces(alias!, companyId!),
|
|
2040
|
+
enabled: !!alias && !!companyId,
|
|
2041
|
+
});
|
|
1748
2042
|
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
import { placeManagerService } from '@/services/placeManagerService';
|
|
2043
|
+
const { accessData, setManager, addMembers } = usePlaceAccess();
|
|
2044
|
+
const { data: users = [] } = useQualiexUsers();
|
|
1752
2045
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
2046
|
+
const handleMakeManager = async (userId: string, placeId: string) => {
|
|
2047
|
+
const user = users.find(u => u.userId === userId);
|
|
2048
|
+
if (user) {
|
|
2049
|
+
await setManager({ placeId, user });
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
|
|
2053
|
+
const handleMakeMembers = async (userIds: string[], placeId: string) => {
|
|
2054
|
+
const selectedUsers = users.filter(u => userIds.includes(u.userId!));
|
|
2055
|
+
await addMembers({ placeId, users: selectedUsers });
|
|
2056
|
+
};
|
|
2057
|
+
|
|
2058
|
+
const getCurrentManagerId = (placeId: string) => {
|
|
2059
|
+
return accessData.find(a => a.place_id === placeId && a.role === 'manager')?.user_id;
|
|
2060
|
+
};
|
|
1762
2061
|
|
|
1763
|
-
|
|
2062
|
+
const getCurrentMemberIds = (placeId: string) => {
|
|
2063
|
+
return accessData
|
|
2064
|
+
.filter(a => a.place_id === placeId && a.role === 'member')
|
|
2065
|
+
.map(a => a.user_id);
|
|
2066
|
+
};
|
|
1764
2067
|
|
|
1765
2068
|
return (
|
|
1766
|
-
<div>
|
|
1767
|
-
<
|
|
1768
|
-
|
|
1769
|
-
<
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
</Button>
|
|
1774
|
-
</div>
|
|
1775
|
-
) : (
|
|
1776
|
-
<p>Nenhum gestor definido</p>
|
|
1777
|
-
)}
|
|
2069
|
+
<div className="container mx-auto py-6 space-y-6">
|
|
2070
|
+
<div className="flex flex-col gap-2">
|
|
2071
|
+
<h1 className="text-3xl font-bold tracking-tight">Locais</h1>
|
|
2072
|
+
<p className="text-muted-foreground">
|
|
2073
|
+
Visualização hierárquica de locais e gestão de acessos
|
|
2074
|
+
</p>
|
|
2075
|
+
</div>
|
|
1778
2076
|
|
|
1779
|
-
<
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
2077
|
+
<Card className="p-6">
|
|
2078
|
+
<PlacesList
|
|
2079
|
+
places={places}
|
|
2080
|
+
isLoading={isLoading}
|
|
2081
|
+
manageAccessConfig={{
|
|
2082
|
+
onMakeManager: handleMakeManager,
|
|
2083
|
+
onMakeMembers: handleMakeMembers,
|
|
2084
|
+
getCurrentManagerId,
|
|
2085
|
+
getCurrentMemberIds
|
|
2086
|
+
}}
|
|
2087
|
+
/>
|
|
2088
|
+
</Card>
|
|
1790
2089
|
</div>
|
|
1791
2090
|
);
|
|
1792
2091
|
}
|
|
@@ -1794,45 +2093,41 @@ function PlaceDetailsPage({ placeId }) {
|
|
|
1794
2093
|
|
|
1795
2094
|
---
|
|
1796
2095
|
|
|
1797
|
-
####
|
|
2096
|
+
#### 🛠️ Troubleshooting
|
|
1798
2097
|
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
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.
|
|
2098
|
+
| Problema | Causa | Solução |
|
|
2099
|
+
|----------|-------|---------|
|
|
2100
|
+
| Botão "Gerenciar Acessos" não aparece | `manageAccessConfig` não fornecido | Passar objeto `manageAccessConfig` para `PlacesList` |
|
|
2101
|
+
| Lista de usuários vazia no modal | `useQualiexUsers` não retorna dados | Verificar token e configuração de alias |
|
|
2102
|
+
| Erro ao salvar gestor/membro | Callbacks não implementados | Implementar `onMakeManager` e `onMakeMembers` |
|
|
2103
|
+
| Hierarquia não exibe sublocais | Dados sem `parentId` correto | Verificar estrutura retornada pela API Qualiex |
|
|
2104
|
+
| Modal não abre | Estado `open` não controlado | Verificar se `ActionMenu` está funcionando |
|
|
1814
2105
|
|
|
1815
2106
|
---
|
|
1816
2107
|
|
|
1817
|
-
####
|
|
2108
|
+
#### ✅ Checklist de Implementação
|
|
1818
2109
|
|
|
1819
|
-
|
|
1820
|
-
-
|
|
1821
|
-
-
|
|
2110
|
+
- [ ] Importar componentes do `forlogic-core/places`
|
|
2111
|
+
- [ ] Criar tabela `place_access` no Supabase (ou equivalente)
|
|
2112
|
+
- [ ] Implementar `PlaceAccessService` com métodos CRUD
|
|
2113
|
+
- [ ] Criar hook `usePlaceAccess` com React Query
|
|
2114
|
+
- [ ] Configurar `manageAccessConfig` na página
|
|
2115
|
+
- [ ] Testar seleção de gestor (apenas 1 usuário)
|
|
2116
|
+
- [ ] Testar seleção de membros (múltiplos usuários)
|
|
2117
|
+
- [ ] Implementar regra de "remover gestor anterior"
|
|
2118
|
+
- [ ] Adicionar toast de sucesso/erro
|
|
2119
|
+
- [ ] Testar busca de usuários no modal
|
|
2120
|
+
- [ ] Validar ordenação alfabética
|
|
1822
2121
|
|
|
1823
|
-
|
|
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
|
|
2122
|
+
---
|
|
1827
2123
|
|
|
1828
|
-
|
|
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
|
|
2124
|
+
#### 📚 Referências
|
|
1831
2125
|
|
|
1832
|
-
**
|
|
1833
|
-
-
|
|
1834
|
-
-
|
|
1835
|
-
-
|
|
2126
|
+
- **Componentes:** `lib/places/components/`
|
|
2127
|
+
- **Service:** `lib/places/services/PlaceService.ts`
|
|
2128
|
+
- **Types:** `lib/places/types.ts`
|
|
2129
|
+
- **Exports:** `lib/places/index.ts`
|
|
2130
|
+
- **Exemplo completo:** `src/places/PlacesPage.tsx`
|
|
1836
2131
|
|
|
1837
2132
|
---
|
|
1838
2133
|
|
|
@@ -4390,339 +4685,6 @@ headers: { 'un-alias': 'true' }
|
|
|
4390
4685
|
|
|
4391
4686
|
---
|
|
4392
4687
|
|
|
4393
|
-
## 📍 PLACES - Locais e Sublocais
|
|
4394
|
-
|
|
4395
|
-
O módulo **Places** permite gerenciar a hierarquia de locais e sublocais da organização, integrando com a API Qualiex.
|
|
4396
|
-
|
|
4397
|
-
### 🔌 Imports Disponíveis
|
|
4398
|
-
|
|
4399
|
-
```typescript
|
|
4400
|
-
// Tipos
|
|
4401
|
-
import type { Place, SubPlace } from 'forlogic-core';
|
|
4402
|
-
|
|
4403
|
-
// Serviço
|
|
4404
|
-
import { placeService, PlaceService } from 'forlogic-core';
|
|
4405
|
-
|
|
4406
|
-
// Componente de Página Pronta
|
|
4407
|
-
import { PlacesPage } from 'forlogic-core';
|
|
4408
|
-
```
|
|
4409
|
-
|
|
4410
|
-
### 📋 Estrutura dos Dados
|
|
4411
|
-
|
|
4412
|
-
```typescript
|
|
4413
|
-
interface Place {
|
|
4414
|
-
id: string;
|
|
4415
|
-
placeId: string; // ID único do local no Qualiex
|
|
4416
|
-
name: string; // Nome do local
|
|
4417
|
-
companyId: string; // ID da empresa
|
|
4418
|
-
usersIds: string[]; // Array de IDs de usuários vinculados
|
|
4419
|
-
subPlaces?: SubPlace[]; // Sublocais (hierarquia)
|
|
4420
|
-
parentId?: string | null; // ID do local pai (se for sublocalizado)
|
|
4421
|
-
isActive: boolean; // Status do local
|
|
4422
|
-
createdAt: string;
|
|
4423
|
-
updatedAt: string;
|
|
4424
|
-
}
|
|
4425
|
-
|
|
4426
|
-
interface SubPlace {
|
|
4427
|
-
id: string;
|
|
4428
|
-
placeId: string;
|
|
4429
|
-
name: string;
|
|
4430
|
-
parentId: string;
|
|
4431
|
-
usersIds: string[];
|
|
4432
|
-
isActive: boolean;
|
|
4433
|
-
subPlaces?: SubPlace[]; // Recursivo - permite múltiplos níveis
|
|
4434
|
-
}
|
|
4435
|
-
```
|
|
4436
|
-
|
|
4437
|
-
### 🎯 Como Obter Places
|
|
4438
|
-
|
|
4439
|
-
#### Método 1 (Recomendado): Hook com React Query
|
|
4440
|
-
|
|
4441
|
-
```typescript
|
|
4442
|
-
import { useQuery } from '@tanstack/react-query';
|
|
4443
|
-
import { useAuth, placeService } from 'forlogic-core';
|
|
4444
|
-
|
|
4445
|
-
function MyComponent() {
|
|
4446
|
-
const { alias } = useAuth();
|
|
4447
|
-
|
|
4448
|
-
const { data: places = [], isLoading, error } = useQuery({
|
|
4449
|
-
queryKey: ['places', alias],
|
|
4450
|
-
queryFn: () => placeService.getPlaces(alias),
|
|
4451
|
-
enabled: !!alias,
|
|
4452
|
-
staleTime: 5 * 60 * 1000 // Cache de 5 minutos
|
|
4453
|
-
});
|
|
4454
|
-
|
|
4455
|
-
if (isLoading) return <LoadingState />;
|
|
4456
|
-
if (error) return <div>Erro ao carregar locais</div>;
|
|
4457
|
-
|
|
4458
|
-
return (
|
|
4459
|
-
<div>
|
|
4460
|
-
{places.map(place => (
|
|
4461
|
-
<div key={place.id}>{place.name}</div>
|
|
4462
|
-
))}
|
|
4463
|
-
</div>
|
|
4464
|
-
);
|
|
4465
|
-
}
|
|
4466
|
-
```
|
|
4467
|
-
|
|
4468
|
-
#### Método 2: Hook Customizado Reutilizável
|
|
4469
|
-
|
|
4470
|
-
```typescript
|
|
4471
|
-
// src/hooks/usePlaces.ts
|
|
4472
|
-
import { useQuery } from '@tanstack/react-query';
|
|
4473
|
-
import { useAuth, placeService } from 'forlogic-core';
|
|
4474
|
-
|
|
4475
|
-
export function usePlaces() {
|
|
4476
|
-
const { alias } = useAuth();
|
|
4477
|
-
|
|
4478
|
-
return useQuery({
|
|
4479
|
-
queryKey: ['places', alias],
|
|
4480
|
-
queryFn: () => placeService.getPlaces(alias),
|
|
4481
|
-
enabled: !!alias,
|
|
4482
|
-
staleTime: 5 * 60 * 1000 // Cache de 5 minutos
|
|
4483
|
-
});
|
|
4484
|
-
}
|
|
4485
|
-
|
|
4486
|
-
// Usar no componente
|
|
4487
|
-
const { data: places = [], isLoading } = usePlaces();
|
|
4488
|
-
```
|
|
4489
|
-
|
|
4490
|
-
#### Método 3: Chamada Direta (Service)
|
|
4491
|
-
|
|
4492
|
-
```typescript
|
|
4493
|
-
// Para casos especiais (não recomendado para components)
|
|
4494
|
-
const places = await placeService.getPlaces('my-alias');
|
|
4495
|
-
```
|
|
4496
|
-
|
|
4497
|
-
### 🏠 Usando PlacesPage Pronta
|
|
4498
|
-
|
|
4499
|
-
```typescript
|
|
4500
|
-
// App.tsx ou routes
|
|
4501
|
-
import { PlacesPage } from 'forlogic-core';
|
|
4502
|
-
|
|
4503
|
-
<Route path="/places" element={<PlacesPage />} />
|
|
4504
|
-
```
|
|
4505
|
-
|
|
4506
|
-
### 🔗 Integrando Places em Módulos CRUD
|
|
4507
|
-
|
|
4508
|
-
#### Cenário A: PlaceSelect em Formulários
|
|
4509
|
-
|
|
4510
|
-
```typescript
|
|
4511
|
-
// src/components/PlaceSelect.tsx
|
|
4512
|
-
import { EntitySelect } from 'forlogic-core';
|
|
4513
|
-
import { usePlaces } from '@/hooks/usePlaces';
|
|
4514
|
-
import { useMemo } from 'react';
|
|
4515
|
-
|
|
4516
|
-
export function PlaceSelect({ value, onChange, disabled }: {
|
|
4517
|
-
value?: string;
|
|
4518
|
-
onChange: (value: string) => void;
|
|
4519
|
-
disabled?: boolean;
|
|
4520
|
-
}) {
|
|
4521
|
-
const { data: places = [], isLoading } = usePlaces();
|
|
4522
|
-
|
|
4523
|
-
// Achatar hierarquia para o select
|
|
4524
|
-
const flatPlaces = useMemo(() => {
|
|
4525
|
-
const flatten = (items: Place[], level = 0): any[] => {
|
|
4526
|
-
return items.flatMap(place => [
|
|
4527
|
-
{ ...place, level },
|
|
4528
|
-
...flatten(place.subPlaces || [], level + 1)
|
|
4529
|
-
]);
|
|
4530
|
-
};
|
|
4531
|
-
return flatten(places);
|
|
4532
|
-
}, [places]);
|
|
4533
|
-
|
|
4534
|
-
return (
|
|
4535
|
-
<EntitySelect
|
|
4536
|
-
value={value}
|
|
4537
|
-
onChange={onChange}
|
|
4538
|
-
items={flatPlaces}
|
|
4539
|
-
isLoading={isLoading}
|
|
4540
|
-
getItemValue={(p) => p.placeId}
|
|
4541
|
-
getItemLabel={(p) => `${' '.repeat(p.level)}${p.name}`}
|
|
4542
|
-
disabled={disabled}
|
|
4543
|
-
placeholder="Selecionar local"
|
|
4544
|
-
/>
|
|
4545
|
-
);
|
|
4546
|
-
}
|
|
4547
|
-
|
|
4548
|
-
// Usar no CRUD config
|
|
4549
|
-
{
|
|
4550
|
-
key: 'place_id',
|
|
4551
|
-
label: 'Local',
|
|
4552
|
-
type: 'custom',
|
|
4553
|
-
component: PlaceSelect,
|
|
4554
|
-
required: true
|
|
4555
|
-
}
|
|
4556
|
-
```
|
|
4557
|
-
|
|
4558
|
-
#### Cenário B: Filtrar Dados por Place
|
|
4559
|
-
|
|
4560
|
-
```typescript
|
|
4561
|
-
// Service com filtro de placeId
|
|
4562
|
-
const { service, useCrudHook } = createSimpleService({
|
|
4563
|
-
tableName: 'my_table',
|
|
4564
|
-
schemaName: 'central',
|
|
4565
|
-
additionalFilters: [
|
|
4566
|
-
{ field: 'place_id', operator: 'eq', value: selectedPlaceId }
|
|
4567
|
-
]
|
|
4568
|
-
});
|
|
4569
|
-
```
|
|
4570
|
-
|
|
4571
|
-
#### Cenário C: Exibir Nome do Local em Tabelas
|
|
4572
|
-
|
|
4573
|
-
```typescript
|
|
4574
|
-
// Hook para buscar nome do place
|
|
4575
|
-
function usePlaceName(placeId: string) {
|
|
4576
|
-
const { data: places = [] } = usePlaces();
|
|
4577
|
-
|
|
4578
|
-
return useMemo(() => {
|
|
4579
|
-
const findPlace = (items: Place[]): Place | undefined => {
|
|
4580
|
-
for (const place of items) {
|
|
4581
|
-
if (place.placeId === placeId) return place;
|
|
4582
|
-
if (place.subPlaces) {
|
|
4583
|
-
const found = findPlace(place.subPlaces);
|
|
4584
|
-
if (found) return found;
|
|
4585
|
-
}
|
|
4586
|
-
}
|
|
4587
|
-
};
|
|
4588
|
-
return findPlace(places)?.name || 'Local não encontrado';
|
|
4589
|
-
}, [places, placeId]);
|
|
4590
|
-
}
|
|
4591
|
-
|
|
4592
|
-
// Usar na coluna da tabela
|
|
4593
|
-
{
|
|
4594
|
-
key: 'place_id',
|
|
4595
|
-
header: 'Local',
|
|
4596
|
-
render: (item) => {
|
|
4597
|
-
const placeName = usePlaceName(item.place_id);
|
|
4598
|
-
return <span>{placeName}</span>;
|
|
4599
|
-
}
|
|
4600
|
-
}
|
|
4601
|
-
```
|
|
4602
|
-
|
|
4603
|
-
### 🔑 Acessando placeId/placeName dos Tokens
|
|
4604
|
-
|
|
4605
|
-
**⚠️ IMPORTANTE:** `placeId` e `placeName` **NÃO** vêm diretamente dos tokens JWT. Eles são obtidos da **API Qualiex**.
|
|
4606
|
-
|
|
4607
|
-
```typescript
|
|
4608
|
-
// ❌ ERRADO - Não existe no token
|
|
4609
|
-
const { placeId } = useAuth(); // undefined
|
|
4610
|
-
|
|
4611
|
-
// ✅ CORRETO - Buscar da API Qualiex
|
|
4612
|
-
const { data: places } = usePlaces();
|
|
4613
|
-
const userPlace = places.find(p => p.usersIds.includes(userId));
|
|
4614
|
-
const placeId = userPlace?.placeId;
|
|
4615
|
-
const placeName = userPlace?.name;
|
|
4616
|
-
```
|
|
4617
|
-
|
|
4618
|
-
**Fluxo de dados:**
|
|
4619
|
-
|
|
4620
|
-
1. Token JWT contém `alias` e `companyId`
|
|
4621
|
-
2. `placeService.getPlaces(alias)` busca os Places da API Qualiex
|
|
4622
|
-
3. Cada `Place` contém `usersIds` (array de IDs de usuários)
|
|
4623
|
-
4. Relacionar usuário logado com seu Place através de `usersIds`
|
|
4624
|
-
|
|
4625
|
-
### 🌳 Navegação Hierárquica (Tree View)
|
|
4626
|
-
|
|
4627
|
-
```typescript
|
|
4628
|
-
function PlaceTree({ places, level = 0 }: {
|
|
4629
|
-
places: Place[];
|
|
4630
|
-
level?: number;
|
|
4631
|
-
}) {
|
|
4632
|
-
return (
|
|
4633
|
-
<div>
|
|
4634
|
-
{places.map(place => (
|
|
4635
|
-
<div key={place.id}>
|
|
4636
|
-
<div style={{ paddingLeft: `${level * 20}px` }}>
|
|
4637
|
-
📍 {place.name} ({place.usersIds.length} usuários)
|
|
4638
|
-
{!place.isActive && <Badge variant="secondary">Inativo</Badge>}
|
|
4639
|
-
</div>
|
|
4640
|
-
{place.subPlaces && place.subPlaces.length > 0 && (
|
|
4641
|
-
<PlaceTree places={place.subPlaces} level={level + 1} />
|
|
4642
|
-
)}
|
|
4643
|
-
</div>
|
|
4644
|
-
))}
|
|
4645
|
-
</div>
|
|
4646
|
-
);
|
|
4647
|
-
}
|
|
4648
|
-
```
|
|
4649
|
-
|
|
4650
|
-
### 🛠️ Troubleshooting
|
|
4651
|
-
|
|
4652
|
-
| Erro | Causa | Solução |
|
|
4653
|
-
| ----------------------------------- | ------------------------------------- | ---------------------------------------------- |
|
|
4654
|
-
| `CompanyId não encontrado no token` | Token não validado corretamente | Verificar edge function `validate-token` |
|
|
4655
|
-
| `Alias da unidade é obrigatório` | `alias` não disponível no `useAuth()` | Aguardar carregamento do auth com `isLoading` |
|
|
4656
|
-
| `Token Qualiex não encontrado` | Variável de ambiente faltando | Verificar `VITE_QUALIEX_API_URL` no `.env` |
|
|
4657
|
-
| Places retorna array vazio `[]` | Empresa sem places cadastrados | Verificar cadastro no Qualiex admin |
|
|
4658
|
-
| Hierarquia quebrada | `parentId` incorreto nos dados | Verificar integridade dos dados na API Qualiex |
|
|
4659
|
-
|
|
4660
|
-
### 📦 Exemplo Completo: Dashboard por Local
|
|
4661
|
-
|
|
4662
|
-
```typescript
|
|
4663
|
-
import { usePlaces } from '@/hooks/usePlaces';
|
|
4664
|
-
import { Card, CardHeader, CardTitle, CardContent } from 'forlogic-core';
|
|
4665
|
-
|
|
4666
|
-
function PlacesDashboard() {
|
|
4667
|
-
const { data: places = [], isLoading } = usePlaces();
|
|
4668
|
-
const { data: metrics = [] } = useQuery({
|
|
4669
|
-
queryKey: ['metrics'],
|
|
4670
|
-
queryFn: fetchMetrics
|
|
4671
|
-
});
|
|
4672
|
-
|
|
4673
|
-
if (isLoading) return <LoadingState />;
|
|
4674
|
-
|
|
4675
|
-
return (
|
|
4676
|
-
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
4677
|
-
{places.map(place => {
|
|
4678
|
-
const placeMetrics = metrics.filter(m => m.place_id === place.placeId);
|
|
4679
|
-
|
|
4680
|
-
return (
|
|
4681
|
-
<Card key={place.id}>
|
|
4682
|
-
<CardHeader>
|
|
4683
|
-
<CardTitle>{place.name}</CardTitle>
|
|
4684
|
-
</CardHeader>
|
|
4685
|
-
<CardContent>
|
|
4686
|
-
<div className="space-y-2">
|
|
4687
|
-
<p className="text-sm">
|
|
4688
|
-
👥 Usuários: <strong>{place.usersIds.length}</strong>
|
|
4689
|
-
</p>
|
|
4690
|
-
<p className="text-sm">
|
|
4691
|
-
📊 Registros: <strong>{placeMetrics.length}</strong>
|
|
4692
|
-
</p>
|
|
4693
|
-
<p className="text-sm">
|
|
4694
|
-
📍 Sublocais: <strong>{place.subPlaces?.length || 0}</strong>
|
|
4695
|
-
</p>
|
|
4696
|
-
</div>
|
|
4697
|
-
</CardContent>
|
|
4698
|
-
</Card>
|
|
4699
|
-
);
|
|
4700
|
-
})}
|
|
4701
|
-
</div>
|
|
4702
|
-
);
|
|
4703
|
-
}
|
|
4704
|
-
```
|
|
4705
|
-
|
|
4706
|
-
### ✅ Checklist de Implementação
|
|
4707
|
-
|
|
4708
|
-
- [ ] `VITE_QUALIEX_API_URL` configurada no `.env`
|
|
4709
|
-
- [ ] Edge function `validate-token` retorna `company_id` corretamente
|
|
4710
|
-
- [ ] `alias` disponível no `useAuth()`
|
|
4711
|
-
- [ ] Hook `usePlaces()` criado e testado
|
|
4712
|
-
- [ ] `PlaceSelect` component criado (se necessário)
|
|
4713
|
-
- [ ] Tratamento de erro quando places vazio
|
|
4714
|
-
- [ ] Cache configurado no React Query (`staleTime`)
|
|
4715
|
-
- [ ] Hierarquia renderizada corretamente (se usar tree view)
|
|
4716
|
-
|
|
4717
|
-
### 📚 Referências
|
|
4718
|
-
|
|
4719
|
-
- **Tipos:** `lib/qualiex/places/types.ts`
|
|
4720
|
-
- **Service:** `lib/qualiex/places/PlaceService.ts`
|
|
4721
|
-
- **Componente:** `lib/qualiex/places/PlacesPage.tsx`
|
|
4722
|
-
- **Exports:** `lib/modular.ts` e `lib/exports/integrations.ts`
|
|
4723
|
-
- **Token Manager:** `lib/auth/services/TokenManager.ts`
|
|
4724
|
-
|
|
4725
|
-
---
|
|
4726
4688
|
|
|
4727
4689
|
## 🗃️ MIGRATIONS + RLS
|
|
4728
4690
|
|
|
@@ -5246,6 +5208,195 @@ const columns: CrudColumn<MyEntity>[] = [
|
|
|
5246
5208
|
|
|
5247
5209
|
---
|
|
5248
5210
|
|
|
5211
|
+
## 📧 ENVIO DE EMAILS
|
|
5212
|
+
|
|
5213
|
+
O `forlogic-core` fornece um serviço padronizado para envio de emails usando templates cadastrados no banco de dados.
|
|
5214
|
+
|
|
5215
|
+
### **Como Funciona**
|
|
5216
|
+
|
|
5217
|
+
1. **Template no banco**: Cadastre templates na tabela `common.email_templates` com variáveis usando `{{variableName}}`
|
|
5218
|
+
2. **Envio via código**: Use `emailService.sendEmail()` passando o código do template e as variáveis
|
|
5219
|
+
3. **Edge Function**: Automaticamente busca o template, substitui as variáveis e envia via SMTP
|
|
5220
|
+
4. **Logs**: Registra todos os envios em `common.email_logs`
|
|
5221
|
+
|
|
5222
|
+
### **Uso Básico**
|
|
5223
|
+
|
|
5224
|
+
```typescript
|
|
5225
|
+
import { emailService } from 'forlogic-core';
|
|
5226
|
+
|
|
5227
|
+
// Enviar email de boas-vindas
|
|
5228
|
+
await emailService.sendEmail({
|
|
5229
|
+
templateCode: 'WELCOME',
|
|
5230
|
+
to: 'usuario@exemplo.com',
|
|
5231
|
+
variables: {
|
|
5232
|
+
userName: 'João Silva',
|
|
5233
|
+
activationLink: 'https://app.com/activate/abc123',
|
|
5234
|
+
companyName: 'Minha Empresa'
|
|
5235
|
+
}
|
|
5236
|
+
});
|
|
5237
|
+
|
|
5238
|
+
// Enviar para múltiplos destinatários
|
|
5239
|
+
await emailService.sendEmail({
|
|
5240
|
+
templateCode: 'NOTIFICATION',
|
|
5241
|
+
to: ['usuario1@exemplo.com', 'usuario2@exemplo.com'],
|
|
5242
|
+
variables: {
|
|
5243
|
+
title: 'Novo documento disponível',
|
|
5244
|
+
message: 'Um documento foi compartilhado com você',
|
|
5245
|
+
actionUrl: 'https://app.com/docs/123',
|
|
5246
|
+
actionLabel: 'Ver Documento'
|
|
5247
|
+
},
|
|
5248
|
+
cc: ['gestor@exemplo.com'],
|
|
5249
|
+
replyTo: 'suporte@empresa.com'
|
|
5250
|
+
});
|
|
5251
|
+
```
|
|
5252
|
+
|
|
5253
|
+
### **Parâmetros do emailService.sendEmail()**
|
|
5254
|
+
|
|
5255
|
+
```typescript
|
|
5256
|
+
interface SendEmailParams {
|
|
5257
|
+
templateCode: string; // Código do template no banco (ex: 'WELCOME')
|
|
5258
|
+
to: string | string[]; // Email(s) do(s) destinatário(s)
|
|
5259
|
+
variables: Record<string, any>; // Variáveis para substituir no template
|
|
5260
|
+
cc?: string[]; // Emails em cópia (opcional)
|
|
5261
|
+
bcc?: string[]; // Emails em cópia oculta (opcional)
|
|
5262
|
+
replyTo?: string; // Email para resposta (opcional)
|
|
5263
|
+
}
|
|
5264
|
+
```
|
|
5265
|
+
|
|
5266
|
+
### **Criando Templates no Banco**
|
|
5267
|
+
|
|
5268
|
+
Os templates devem ser cadastrados na tabela `common.email_templates`:
|
|
5269
|
+
|
|
5270
|
+
```sql
|
|
5271
|
+
INSERT INTO common.email_templates (
|
|
5272
|
+
alias,
|
|
5273
|
+
template_code,
|
|
5274
|
+
template_name,
|
|
5275
|
+
subject,
|
|
5276
|
+
html_body,
|
|
5277
|
+
is_active
|
|
5278
|
+
) VALUES (
|
|
5279
|
+
'minha_empresa',
|
|
5280
|
+
'WELCOME',
|
|
5281
|
+
'Email de Boas-vindas',
|
|
5282
|
+
'Bem-vindo, {{userName}}!',
|
|
5283
|
+
'<h1>Olá, {{userName}}!</h1>
|
|
5284
|
+
<p>Bem-vindo à {{companyName}}!</p>
|
|
5285
|
+
<a href="{{activationLink}}">Ativar Conta</a>',
|
|
5286
|
+
true
|
|
5287
|
+
);
|
|
5288
|
+
```
|
|
5289
|
+
|
|
5290
|
+
### **Variáveis nos Templates**
|
|
5291
|
+
|
|
5292
|
+
Use a sintaxe `{{variableName}}` tanto no subject quanto no html_body:
|
|
5293
|
+
|
|
5294
|
+
```html
|
|
5295
|
+
<!-- Exemplo de template HTML -->
|
|
5296
|
+
<div style="font-family: Arial, sans-serif;">
|
|
5297
|
+
<h1>Olá, {{userName}}!</h1>
|
|
5298
|
+
<p>{{message}}</p>
|
|
5299
|
+
|
|
5300
|
+
{{#if actionUrl}}
|
|
5301
|
+
<a href="{{actionUrl}}" style="background: #0066cc; color: white; padding: 10px 20px;">
|
|
5302
|
+
{{actionLabel}}
|
|
5303
|
+
</a>
|
|
5304
|
+
{{/if}}
|
|
5305
|
+
|
|
5306
|
+
<p>Atenciosamente,<br>{{companyName}}</p>
|
|
5307
|
+
</div>
|
|
5308
|
+
```
|
|
5309
|
+
|
|
5310
|
+
### **Tratamento de Erros**
|
|
5311
|
+
|
|
5312
|
+
```typescript
|
|
5313
|
+
try {
|
|
5314
|
+
await emailService.sendEmail({
|
|
5315
|
+
templateCode: 'NOTIFICATION',
|
|
5316
|
+
to: 'usuario@exemplo.com',
|
|
5317
|
+
variables: { title: 'Teste', message: 'Mensagem' }
|
|
5318
|
+
});
|
|
5319
|
+
|
|
5320
|
+
toast.success('Email enviado com sucesso!');
|
|
5321
|
+
} catch (error) {
|
|
5322
|
+
console.error('Erro ao enviar email:', error);
|
|
5323
|
+
toast.error('Erro ao enviar email. Verifique as configurações SMTP.');
|
|
5324
|
+
}
|
|
5325
|
+
```
|
|
5326
|
+
|
|
5327
|
+
### **Requisitos**
|
|
5328
|
+
|
|
5329
|
+
1. **Edge Function**: A edge function `send-email` deve estar configurada
|
|
5330
|
+
2. **Secrets SMTP**: Configure as secrets no Supabase:
|
|
5331
|
+
- `SMTP_HOST` - Servidor SMTP (ex: smtp.gmail.com)
|
|
5332
|
+
- `SMTP_PORT` - Porta SMTP (ex: 587)
|
|
5333
|
+
- `SMTP_USER` - Usuário SMTP
|
|
5334
|
+
- `SMTP_PASSWORD` - Senha SMTP
|
|
5335
|
+
- `SMTP_FROM` - Email remetente (ex: noreply@empresa.com)
|
|
5336
|
+
|
|
5337
|
+
3. **Tabelas**:
|
|
5338
|
+
- `common.email_templates` - Armazena os templates
|
|
5339
|
+
- `common.email_logs` - Registra os envios
|
|
5340
|
+
|
|
5341
|
+
### **Exemplo Completo: Notificação de Mudança de Liderança**
|
|
5342
|
+
|
|
5343
|
+
```typescript
|
|
5344
|
+
import { emailService } from 'forlogic-core';
|
|
5345
|
+
import { toast } from 'sonner';
|
|
5346
|
+
|
|
5347
|
+
async function notifyLeadershipChange(
|
|
5348
|
+
employeeEmail: string,
|
|
5349
|
+
employeeName: string,
|
|
5350
|
+
newLeaderName: string,
|
|
5351
|
+
oldLeaderName?: string
|
|
5352
|
+
) {
|
|
5353
|
+
try {
|
|
5354
|
+
await emailService.sendEmail({
|
|
5355
|
+
templateCode: 'LEADERSHIP_CHANGE',
|
|
5356
|
+
to: employeeEmail,
|
|
5357
|
+
variables: {
|
|
5358
|
+
employeeName,
|
|
5359
|
+
newLeaderName,
|
|
5360
|
+
oldLeaderName: oldLeaderName || 'Nenhum líder anterior',
|
|
5361
|
+
companyName: 'ForLogic',
|
|
5362
|
+
changeDate: new Date().toLocaleDateString('pt-BR')
|
|
5363
|
+
}
|
|
5364
|
+
});
|
|
5365
|
+
|
|
5366
|
+
console.log(`Email de mudança de liderança enviado para ${employeeName}`);
|
|
5367
|
+
} catch (error) {
|
|
5368
|
+
console.error('Erro ao enviar email de liderança:', error);
|
|
5369
|
+
throw error;
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5372
|
+
|
|
5373
|
+
// Uso
|
|
5374
|
+
await notifyLeadershipChange(
|
|
5375
|
+
'joao@empresa.com',
|
|
5376
|
+
'João Silva',
|
|
5377
|
+
'Maria Santos',
|
|
5378
|
+
'Pedro Costa'
|
|
5379
|
+
);
|
|
5380
|
+
```
|
|
5381
|
+
|
|
5382
|
+
### **Boas Práticas**
|
|
5383
|
+
|
|
5384
|
+
**✅ FAZER:**
|
|
5385
|
+
- Cadastrar todos os templates no banco com `is_active = true`
|
|
5386
|
+
- Usar nomes de variáveis descritivos (ex: `userName`, `actionUrl`)
|
|
5387
|
+
- Tratar erros de envio com try/catch
|
|
5388
|
+
- Validar emails antes de enviar
|
|
5389
|
+
- Logar envios importantes para auditoria
|
|
5390
|
+
|
|
5391
|
+
**❌ NÃO FAZER:**
|
|
5392
|
+
- Hardcodar templates no código
|
|
5393
|
+
- Enviar sem validar variáveis obrigatórias
|
|
5394
|
+
- Ignorar erros de envio
|
|
5395
|
+
- Usar variáveis não declaradas no template
|
|
5396
|
+
- Enviar emails sem confirmação do usuário (quando necessário)
|
|
5397
|
+
|
|
5398
|
+
---
|
|
5399
|
+
|
|
5249
5400
|
## 📚 REFERÊNCIA RÁPIDA
|
|
5250
5401
|
|
|
5251
5402
|
### Imports Essenciais
|