forlogic-core 1.7.7 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 🧱 Projeto atual
2
2
 
3
- **Schema padrão:** `central`
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 Adicionais
619
+ #### Exemplos Práticos Completos
578
620
 
579
- **MultiSelect com Busca:**
621
+ **Exemplo 1: MultiSelect de Objetivos Estratégicos**
580
622
 
581
623
  ```typescript
582
- {
583
- name: 'categories',
584
- type: 'custom',
585
- component: MultiSelectField,
586
- componentProps: {
587
- options: categories,
588
- searchable: true
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
- **Rich Text Editor:**
688
+ **Exemplo 2: Select de Usuários do Qualiex**
594
689
 
595
690
  ```typescript
596
- {
597
- name: 'content',
598
- type: 'custom',
599
- component: RichTextEditor,
600
- componentProps: {
601
- toolbar: ['bold', 'italic', 'link']
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
- ### 🏢 Sistema de Gestores de Locais (Qualiex)
1641
+ ### 🏢 Places - Hierarquia de Locais com Gerenciamento de Acessos
1397
1642
 
1398
- Componentes reutilizáveis para gerenciamento de gestores e membros de locais/sublocais integrados com a API Qualiex.
1643
+ Sistema completo para visualização hierárquica de locais e gerenciamento configurável de acessos (gestores e membros).
1399
1644
 
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
1645
+ #### 📦 Importações
1405
1646
 
1406
1647
  ```typescript
1407
- import {
1408
- PlaceManagerButton,
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
- #### 🔧 Configuração Inicial (OBRIGATÓRIO)
1654
+ // Types
1655
+ import type { Place, PlacesListProps, PlaceCardProps, ManageAccessModalProps } from 'forlogic-core/places';
1421
1656
 
1422
- ##### **Passo 1: Criar a Tabela no Supabase**
1657
+ // Ou importação completa
1658
+ import { PlacesList, placeService } from 'forlogic-core';
1659
+ ```
1423
1660
 
1424
- A biblioteca espera uma tabela com a seguinte estrutura:
1661
+ #### 📋 Tipos de Dados
1425
1662
 
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
- );
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
- -- Í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);
1676
+ #### 🎯 Uso Básico - Apenas Visualização
1445
1677
 
1446
- -- RLS Policies (exemplo - ajustar conforme necessidade)
1447
- ALTER TABLE public.place_managers ENABLE ROW LEVEL SECURITY;
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
- CREATE POLICY "Users view own alias managers"
1450
- ON public.place_managers FOR SELECT
1451
- USING (((SELECT auth.jwt()) ->> 'alias'::text) = alias);
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
- CREATE POLICY "Users manage own alias managers"
1454
- ON public.place_managers FOR ALL
1455
- USING (((SELECT auth.jwt()) ->> 'alias'::text) = alias);
1694
+ return <PlacesList places={places} isLoading={isLoading} />;
1695
+ }
1456
1696
  ```
1457
1697
 
1458
- > 💡 **Dica:** Você pode usar um nome de tabela e schema diferentes, basta configurar no próximo passo.
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
- ##### **Passo 2: Criar o Serviço no Seu Projeto**
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
- // 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
- });
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
- **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') |
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
- #### 🎨 Componentes
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
- ##### **PlaceManagerButton**
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
- Botão dropdown com ações de gerenciamento (trocar gestor, remover gestor).
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
- ```tsx
1492
- import { PlaceManagerButton } from 'forlogic-core';
1493
- import { placeManagerService } from '@/services/placeManagerService';
1748
+ const handleMakeMembers = async (userIds: string[], placeId: string) => {
1749
+ await myAccessService.addMembers(placeId, userIds);
1750
+ queryClient.invalidateQueries(['placeAccess']);
1751
+ };
1494
1752
 
1495
- <PlaceManagerButton
1496
- placeId="abc-123"
1497
- placeName="Matriz São Paulo"
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
- **Props:**
1757
+ const getCurrentMemberIds = (placeId: string) => {
1758
+ return accessData.filter(a => a.placeId === placeId && a.role === 'member').map(a => a.userId);
1759
+ };
1503
1760
 
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 |
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
- ##### **PlaceManagerBadge**
1780
+ #### 🎨 Componentes Individuais
1513
1781
 
1514
- Badge visual que mostra "Gestor" ou "Gestores" com contador de usuários.
1782
+ ##### PlacesList
1515
1783
 
1516
- ```tsx
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
- | 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:**
1791
+ ##### PlaceCard
1536
1792
 
1537
- - **"Gestor"** - Quando 1 gestor definido
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
- ##### **ManagerSelectionDialog**
1800
+ ##### ManageAccessModal
1543
1801
 
1544
- Diálogo completo para seleção de gestores e membros com busca integrada.
1802
+ Modal para gerenciamento de acessos ao local.
1545
1803
 
1546
- ```tsx
1547
- import { ManagerSelectionDialog } from 'forlogic-core';
1548
-
1549
- <ManagerSelectionDialog
1550
- open={showDialog}
1551
- onOpenChange={setShowDialog}
1552
- onSelectManager={(user) => handleSelectManager(user)}
1553
- onSelectMember={(user) => handleAddMember(user)}
1554
- currentManagerId={manager?.user_id}
1555
- currentMemberIds={members.map(m => m.user_id)}
1556
- placeName="Matriz São Paulo"
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
- **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) |
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
- #### 🔧 Hook: usePlaceManagers
1577
-
1578
- Hook React Query para gerenciar gestores e membros de um local.
1829
+ #### 🔧 Service - placeService
1579
1830
 
1580
1831
  ```typescript
1581
- import { usePlaceManagers } from 'forlogic-core';
1582
- import { placeManagerService } from '@/services/placeManagerService';
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
- const {
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);
1842
+ **Exemplo:**
1843
+ ```typescript
1844
+ const places = await placeService.getPlaces('my-alias', 'company-123');
1596
1845
  ```
1597
1846
 
1598
- **Parâmetros:**
1847
+ ---
1599
1848
 
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`) |
1849
+ #### 🎯 Regras de Negócio
1605
1850
 
1606
- **Retorno:**
1851
+ > **⚠️ Importante:** O módulo fornece apenas a INTERFACE. A lógica de persistência deve ser implementada pelo projeto consumidor.
1607
1852
 
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 |
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
- #### 🔧 Service: PlaceManagerService
1861
+ #### 🗄️ Estrutura de Banco Sugerida (Supabase)
1624
1862
 
1625
- Classe para operações de banco de dados relacionadas a gestores de locais.
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
- ##### **Instanciação Customizada**
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
- ```typescript
1630
- import { PlaceManagerService } from 'forlogic-core';
1879
+ -- RLS Policies
1880
+ ALTER TABLE public.place_access ENABLE ROW LEVEL SECURITY;
1631
1881
 
1632
- // Configuração padrão (tabela 'place_managers' no schema 'public')
1633
- const defaultService = new PlaceManagerService();
1882
+ CREATE POLICY "Users can view place access"
1883
+ ON public.place_access FOR SELECT
1884
+ USING (true);
1634
1885
 
1635
- // Configuração customizada
1636
- const customService = new PlaceManagerService({
1637
- tableName: 'gestores_locais',
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
- ##### **Métodos Disponíveis**
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
- // Buscar gestores e membros de um local
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
- // Definir gestor (remove anterior automaticamente)
1653
- await placeManagerService.setManager(
1654
- alias: string,
1655
- placeId: string,
1656
- user: QualiexUser
1657
- );
1658
- // Retorna: void
1900
+ class PlaceAccessService {
1901
+ private supabase = getSupabaseClient();
1659
1902
 
1660
- // Adicionar membro
1661
- await placeManagerService.addMember(
1662
- alias: string,
1663
- placeId: string,
1664
- user: QualiexUser
1665
- );
1666
- // Retorna: void
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
- // Remover usuário (gestor ou membro)
1669
- await placeManagerService.removePlaceUser(
1670
- alias: string,
1671
- placeId: string,
1672
- userId: string
1673
- );
1674
- // Retorna: void
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
- #### 📘 Tipos
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
- ```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;
1950
+ if (error) throw error;
1951
+ }
1692
1952
  }
1693
1953
 
1694
- interface PlaceManagerServiceConfig {
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
- #### 🎯 Regras de Negócio
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
- 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
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
- #### 💡 Exemplo Completo
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
- ```tsx
1715
- // 1. Criar serviço (src/services/placeManagerService.ts)
1716
- import { PlaceManagerService } from 'forlogic-core';
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
- export const placeManagerService = new PlaceManagerService({
1719
- tableName: 'place_managers',
1720
- schemaName: 'public'
1721
- });
2016
+ return {
2017
+ accessData,
2018
+ setManager: setManagerMutation.mutateAsync,
2019
+ addMembers: addMembersMutation.mutateAsync,
2020
+ };
2021
+ }
2022
+ ```
1722
2023
 
1723
- // 2. Usar nos componentes (src/places/PlaceTreeItem.tsx)
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
- function PlaceTreeItem({ place }) {
1728
- return (
1729
- <div className="flex items-center gap-3">
1730
- <span>{place.name}</span>
1731
-
1732
- {/* Badge com contador */}
1733
- <PlaceManagerBadge
1734
- placeId={place.id}
1735
- userCount={place.usersIds.length}
1736
- service={placeManagerService}
1737
- />
1738
-
1739
- {/* Botão de ações */}
1740
- <PlaceManagerButton
1741
- placeId={place.id}
1742
- placeName={place.name}
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
- // 3. Uso avançado com hook (src/places/PlaceDetailsPage.tsx)
1750
- import { usePlaceManagers } from 'forlogic-core';
1751
- import { placeManagerService } from '@/services/placeManagerService';
2043
+ const { accessData, setManager, addMembers } = usePlaceAccess();
2044
+ const { data: users = [] } = useQualiexUsers();
1752
2045
 
1753
- function PlaceDetailsPage({ placeId }) {
1754
- const {
1755
- manager,
1756
- members,
1757
- setManager,
1758
- addMember,
1759
- remove,
1760
- isLoading
1761
- } = usePlaceManagers(placeId, placeManagerService);
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
- if (isLoading) return <LoadingState />;
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
- <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
- )}
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
- <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>
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
- #### 🔒 Segurança e RLS
2096
+ #### 🛠️ Troubleshooting
1798
2097
 
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.
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
- #### Troubleshooting
2108
+ #### Checklist de Implementação
1818
2109
 
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`
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
- **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
2122
+ ---
1827
2123
 
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
2124
+ #### 📚 Referências
1831
2125
 
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
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