@fayz-ai/admin 0.62.0 → 0.65.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{CommandPalette-B5PMQFDG.js → CommandPalette-XDJJ5E3O.js} +3 -3
- package/dist/{CommandPalette-B5PMQFDG.js.map → CommandPalette-XDJJ5E3O.js.map} +1 -1
- package/dist/ConnectedBrandingSettings-43ZM3HCI.js +680 -0
- package/dist/ConnectedBrandingSettings-43ZM3HCI.js.map +1 -0
- package/dist/ConnectedCompanySettings-I4MALI2Q.js +197 -0
- package/dist/ConnectedCompanySettings-I4MALI2Q.js.map +1 -0
- package/dist/ConnectedModulesSettings-A2VJ5BMJ.js +563 -0
- package/dist/ConnectedModulesSettings-A2VJ5BMJ.js.map +1 -0
- package/dist/ConnectedSecuritySettings-25AHA4HJ.js +189 -0
- package/dist/ConnectedSecuritySettings-25AHA4HJ.js.map +1 -0
- package/dist/ConnectedUserProfile-E7USJRUE.js +424 -0
- package/dist/ConnectedUserProfile-E7USJRUE.js.map +1 -0
- package/dist/FieldRulesEditor-YGIPKI5T.js +129 -0
- package/dist/FieldRulesEditor-YGIPKI5T.js.map +1 -0
- package/dist/GettingStartedWidget-HMIJ3Y24.js +12 -0
- package/dist/GettingStartedWidget-HMIJ3Y24.js.map +1 -0
- package/dist/PermissionProfilesTab-3ZPRCHMZ.js +548 -0
- package/dist/PermissionProfilesTab-3ZPRCHMZ.js.map +1 -0
- package/dist/PersonTimeline-HQZT527S.js +3 -0
- package/dist/{PersonTimeline-X7JTPUWJ.js.map → PersonTimeline-HQZT527S.js.map} +1 -1
- package/dist/ProjectsSettings-7O2F27MD.js +5 -0
- package/dist/ProjectsSettings-7O2F27MD.js.map +1 -0
- package/dist/ScheduleEditor-54ELWR45.js +8 -0
- package/dist/{ScheduleEditor-L2UJXKMY.js.map → ScheduleEditor-54ELWR45.js.map} +1 -1
- package/dist/SeuSiteWidget-QLSUVC63.js +6 -0
- package/dist/SeuSiteWidget-QLSUVC63.js.map +1 -0
- package/dist/SubscriptionPage-5GE5HFJK.js +17 -0
- package/dist/{SubscriptionPage-IDVEFLZ5.js.map → SubscriptionPage-5GE5HFJK.js.map} +1 -1
- package/dist/TeamTab-6RAULFLW.js +502 -0
- package/dist/TeamTab-6RAULFLW.js.map +1 -0
- package/dist/TrialWidget-Z6CLUCU3.js +7 -0
- package/dist/TrialWidget-Z6CLUCU3.js.map +1 -0
- package/dist/app/AdminShell.d.ts.map +1 -1
- package/dist/app/admin-app.d.ts.map +1 -1
- package/dist/app/config.d.ts +14 -0
- package/dist/app/config.d.ts.map +1 -1
- package/dist/app/routing.d.ts +6 -0
- package/dist/app/routing.d.ts.map +1 -1
- package/dist/app/scaffold.d.ts.map +1 -1
- package/dist/billing/index.js +9 -4
- package/dist/chunk-2H46WDG5.js +53 -0
- package/dist/chunk-2H46WDG5.js.map +1 -0
- package/dist/chunk-3HLX7YB2.js +94 -0
- package/dist/chunk-3HLX7YB2.js.map +1 -0
- package/dist/chunk-3OUD4XBK.js +3 -0
- package/dist/chunk-3OUD4XBK.js.map +1 -0
- package/dist/{chunk-IXSY4GPK.js → chunk-5D5QLMOQ.js} +5 -3
- package/dist/chunk-5D5QLMOQ.js.map +1 -0
- package/dist/chunk-5WR62X2R.js +1081 -0
- package/dist/chunk-5WR62X2R.js.map +1 -0
- package/dist/{chunk-KPYRTOCK.js → chunk-6CQFGMEO.js} +4 -140
- package/dist/chunk-6CQFGMEO.js.map +1 -0
- package/dist/chunk-6MMSYC2I.js +116 -0
- package/dist/chunk-6MMSYC2I.js.map +1 -0
- package/dist/chunk-6QFOE3UD.js +1466 -0
- package/dist/chunk-6QFOE3UD.js.map +1 -0
- package/dist/chunk-6UUFVQ2E.js +283 -0
- package/dist/chunk-6UUFVQ2E.js.map +1 -0
- package/dist/chunk-7WNLL3F6.js +7889 -0
- package/dist/chunk-7WNLL3F6.js.map +1 -0
- package/dist/chunk-A74IHAQE.js +439 -0
- package/dist/chunk-A74IHAQE.js.map +1 -0
- package/dist/{chunk-JOBRLBIQ.js → chunk-AFA6IO23.js} +4 -3
- package/dist/chunk-AFA6IO23.js.map +1 -0
- package/dist/{chunk-P5UIGQFY.js → chunk-ALCSIN3Z.js} +5 -5
- package/dist/chunk-ALCSIN3Z.js.map +1 -0
- package/dist/chunk-AWM4AMBS.js +39 -0
- package/dist/chunk-AWM4AMBS.js.map +1 -0
- package/dist/{chunk-OQARLKZN.js → chunk-BG5Z6KLN.js} +6 -18
- package/dist/chunk-BG5Z6KLN.js.map +1 -0
- package/dist/chunk-DPNFZONC.js +19 -0
- package/dist/chunk-DPNFZONC.js.map +1 -0
- package/dist/chunk-DRCV643L.js +193 -0
- package/dist/chunk-DRCV643L.js.map +1 -0
- package/dist/chunk-E6DF3VR6.js +206 -0
- package/dist/chunk-E6DF3VR6.js.map +1 -0
- package/dist/chunk-ENHCVGXZ.js +120 -0
- package/dist/chunk-ENHCVGXZ.js.map +1 -0
- package/dist/chunk-FK6RHGI7.js +56 -0
- package/dist/chunk-FK6RHGI7.js.map +1 -0
- package/dist/chunk-G5HNA5QK.js +233 -0
- package/dist/chunk-G5HNA5QK.js.map +1 -0
- package/dist/chunk-HCFO4Z6A.js +53 -0
- package/dist/chunk-HCFO4Z6A.js.map +1 -0
- package/dist/chunk-JFIU6NQP.js +51 -0
- package/dist/chunk-JFIU6NQP.js.map +1 -0
- package/dist/chunk-JRZJBMOY.js +3 -0
- package/dist/chunk-JRZJBMOY.js.map +1 -0
- package/dist/{chunk-3YP7KW47.js → chunk-JVVOL4BB.js} +41 -512
- package/dist/chunk-JVVOL4BB.js.map +1 -0
- package/dist/chunk-KAQ3UIAA.js +415 -0
- package/dist/chunk-KAQ3UIAA.js.map +1 -0
- package/dist/{chunk-P6XUDT2P.js → chunk-KJELMULA.js} +4 -51
- package/dist/chunk-KJELMULA.js.map +1 -0
- package/dist/chunk-KLO7DKXX.js +1651 -0
- package/dist/chunk-KLO7DKXX.js.map +1 -0
- package/dist/chunk-M2CMDWUI.js +75 -0
- package/dist/chunk-M2CMDWUI.js.map +1 -0
- package/dist/chunk-PQ47MLTH.js +255 -0
- package/dist/chunk-PQ47MLTH.js.map +1 -0
- package/dist/{chunk-VX3WJLKK.js → chunk-QD7BMUDF.js} +17 -8
- package/dist/chunk-QD7BMUDF.js.map +1 -0
- package/dist/{chunk-E4MESMNR.js → chunk-QTSKCPSW.js} +7 -10
- package/dist/chunk-QTSKCPSW.js.map +1 -0
- package/dist/chunk-S56FQKPL.js +186 -0
- package/dist/chunk-S56FQKPL.js.map +1 -0
- package/dist/chunk-SGAFPNMA.js +75 -0
- package/dist/chunk-SGAFPNMA.js.map +1 -0
- package/dist/{chunk-OD7P4L3D.js → chunk-SOKCLVM4.js} +3 -3
- package/dist/{chunk-OD7P4L3D.js.map → chunk-SOKCLVM4.js.map} +1 -1
- package/dist/chunk-TL42NKTH.js +3 -0
- package/dist/chunk-TL42NKTH.js.map +1 -0
- package/dist/chunk-U6ZCO2PS.js +2964 -0
- package/dist/chunk-U6ZCO2PS.js.map +1 -0
- package/dist/chunk-VSXNROYH.js +16 -0
- package/dist/chunk-VSXNROYH.js.map +1 -0
- package/dist/chunk-WPFZCYZL.js +619 -0
- package/dist/chunk-WPFZCYZL.js.map +1 -0
- package/dist/chunk-XFTNIBDW.js +23 -0
- package/dist/chunk-XFTNIBDW.js.map +1 -0
- package/dist/chunk-Y66JDL6I.js +1020 -0
- package/dist/chunk-Y66JDL6I.js.map +1 -0
- package/dist/chunk-YEHIBPIX.js +137 -0
- package/dist/chunk-YEHIBPIX.js.map +1 -0
- package/dist/chunk-YKPPCV2Z.js +53 -0
- package/dist/chunk-YKPPCV2Z.js.map +1 -0
- package/dist/components/chat/markdown.d.ts.map +1 -1
- package/dist/components/plugins/PluginSettingsPanel.d.ts.map +1 -1
- package/dist/components/settings/SettingsPage.d.ts.map +1 -1
- package/dist/connect-XKQJOWR7.js +18 -0
- package/dist/connect-XKQJOWR7.js.map +1 -0
- package/dist/crud/CrudDetailPage.d.ts.map +1 -1
- package/dist/crud/CrudListView.d.ts.map +1 -1
- package/dist/crud/CrudPage.d.ts.map +1 -1
- package/dist/crud/RelatedListTab.d.ts.map +1 -1
- package/dist/crud/archetypes/EntityLink.d.ts +1 -1
- package/dist/crud/archetypes/EntityLink.d.ts.map +1 -1
- package/dist/crud/record-chrome.d.ts.map +1 -1
- package/dist/entity-routes-PGU7EHAD.js +4 -0
- package/dist/{entity-routes-LXOBW5IB.js.map → entity-routes-PGU7EHAD.js.map} +1 -1
- package/dist/hooks/useModuleNavigation.d.ts.map +1 -1
- package/dist/impersonation/ImpersonateDialog.d.ts.map +1 -1
- package/dist/impersonation/session.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11951 -33576
- package/dist/index.js.map +1 -1
- package/dist/lib/attention.d.ts +6 -0
- package/dist/lib/attention.d.ts.map +1 -1
- package/dist/lib/entity-routes.d.ts.map +1 -1
- package/dist/org/index.js +4 -2
- package/dist/plugins/PluginRegistryManager.d.ts.map +1 -1
- package/dist/settings/MessageTemplateRow.d.ts.map +1 -1
- package/dist/timeline/PersonTimeline.d.ts.map +1 -1
- package/dist/timeline/TimelineList.d.ts.map +1 -1
- package/dist/units/UnitRailPage.d.ts.map +1 -1
- package/dist/units/useUnitFilter.d.ts.map +1 -1
- package/package.json +5 -5
- package/dist/PersonTimeline-X7JTPUWJ.js +0 -3
- package/dist/ScheduleEditor-L2UJXKMY.js +0 -6
- package/dist/SubscriptionPage-IDVEFLZ5.js +0 -8
- package/dist/chunk-3YP7KW47.js.map +0 -1
- package/dist/chunk-E4MESMNR.js.map +0 -1
- package/dist/chunk-IXSY4GPK.js.map +0 -1
- package/dist/chunk-JOBRLBIQ.js.map +0 -1
- package/dist/chunk-KPYRTOCK.js.map +0 -1
- package/dist/chunk-OQARLKZN.js.map +0 -1
- package/dist/chunk-P5UIGQFY.js.map +0 -1
- package/dist/chunk-P6XUDT2P.js.map +0 -1
- package/dist/chunk-VX3WJLKK.js.map +0 -1
- package/dist/entity-routes-LXOBW5IB.js +0 -4
|
@@ -0,0 +1,1081 @@
|
|
|
1
|
+
import { RightRailPage } from './chunk-JFIU6NQP.js';
|
|
2
|
+
import { createCrudPage, useUnitSwitchingStore, UnitMembersTab } from './chunk-7WNLL3F6.js';
|
|
3
|
+
import { useUnitBootstrap } from './chunk-DPNFZONC.js';
|
|
4
|
+
import { useTranslation } from './chunk-YUHCC7JY.js';
|
|
5
|
+
import { toast } from './chunk-3OUD4XBK.js';
|
|
6
|
+
import { useUnitStore, fetchMemberUnits, UNIT_ACCESS_CHANGED, markWorkspaceSwitch, bindUserToUnit, unbindUserFromUnit } from './chunk-5D5QLMOQ.js';
|
|
7
|
+
import { usePermissionsStore } from './chunk-UQ2NTFQC.js';
|
|
8
|
+
import { getFayzSupabaseClient } from './chunk-KJELMULA.js';
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
import React__default from 'react';
|
|
11
|
+
import { maskCpfCnpj, recordRailFields, RECORD_GROUP, activeFacet, getSupabaseClientOptional, useDataVersion, navigateApp, writeImpersonation, readImpersonation, usePluginRuntimeOptional, getWidgetsForZone, replaceApp } from '@fayz-ai/core';
|
|
12
|
+
import { create } from 'zustand';
|
|
13
|
+
import { Input, SearchSelect, Button, Badge, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Skeleton, Dropdown, DropdownTrigger, cn, DropdownContent, DropdownItem, DropdownSeparator, PickerDialog, DropdownLabel } from '@fayz-ai/ui';
|
|
14
|
+
import { AlertTriangle, Building2, X, Plus, ChevronsUpDown, Layers, Check, Search, MapPin, Settings2, Loader2, LocateFixed, Store } from 'lucide-react';
|
|
15
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
16
|
+
|
|
17
|
+
var NO_EXTRA_ROLE = "__none__";
|
|
18
|
+
var useMemberAccessPanel = create((set) => ({
|
|
19
|
+
member: null,
|
|
20
|
+
openMember: (member) => set({ member }),
|
|
21
|
+
close: () => set({ member: null })
|
|
22
|
+
}));
|
|
23
|
+
function MemberAccessRailPage() {
|
|
24
|
+
const member = useMemberAccessPanel((s) => s.member);
|
|
25
|
+
const close = useMemberAccessPanel((s) => s.close);
|
|
26
|
+
if (!member) return null;
|
|
27
|
+
return /* @__PURE__ */ jsx(
|
|
28
|
+
MemberAccessBody,
|
|
29
|
+
{
|
|
30
|
+
userId: member.userId,
|
|
31
|
+
displayName: member.name,
|
|
32
|
+
email: member.email,
|
|
33
|
+
onClose: close
|
|
34
|
+
},
|
|
35
|
+
member.userId
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
function MemberAccessBody({
|
|
39
|
+
userId,
|
|
40
|
+
displayName,
|
|
41
|
+
email,
|
|
42
|
+
onClose
|
|
43
|
+
}) {
|
|
44
|
+
const units = useUnitStore((s) => s.units);
|
|
45
|
+
const byId = useUnitStore((s) => s.byId);
|
|
46
|
+
const profiles = usePermissionsStore((s) => s.profiles);
|
|
47
|
+
const [bindings, setBindings] = React.useState([]);
|
|
48
|
+
const [loading, setLoading] = React.useState(false);
|
|
49
|
+
const [adding, setAdding] = React.useState(false);
|
|
50
|
+
const branches = React.useMemo(() => units.filter((u) => u.isActive !== false), [units]);
|
|
51
|
+
const unbound = React.useMemo(
|
|
52
|
+
() => branches.filter((u) => !bindings.some((b) => b.locationId === u.id)),
|
|
53
|
+
[branches, bindings]
|
|
54
|
+
);
|
|
55
|
+
const reload = React.useCallback(async () => {
|
|
56
|
+
if (!userId) return;
|
|
57
|
+
setLoading(true);
|
|
58
|
+
try {
|
|
59
|
+
setBindings(await fetchMemberUnits(userId));
|
|
60
|
+
} finally {
|
|
61
|
+
setLoading(false);
|
|
62
|
+
}
|
|
63
|
+
}, [userId]);
|
|
64
|
+
const version = useDataVersion(UNIT_ACCESS_CHANGED);
|
|
65
|
+
React.useEffect(() => {
|
|
66
|
+
void reload();
|
|
67
|
+
}, [reload, version]);
|
|
68
|
+
async function add(unitId) {
|
|
69
|
+
try {
|
|
70
|
+
await bindUserToUnit(unitId, userId, null);
|
|
71
|
+
await reload();
|
|
72
|
+
setAdding(false);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
toast.error("N\xE3o deu para dar o acesso", { description: err?.message });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function setRole(unitId, role) {
|
|
78
|
+
try {
|
|
79
|
+
await bindUserToUnit(unitId, userId, role === NO_EXTRA_ROLE ? null : role);
|
|
80
|
+
await reload();
|
|
81
|
+
} catch (err) {
|
|
82
|
+
toast.error("N\xE3o deu para mudar a fun\xE7\xE3o", { description: err?.message });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function remove(unitId) {
|
|
86
|
+
try {
|
|
87
|
+
await unbindUserFromUnit(unitId, userId);
|
|
88
|
+
await reload();
|
|
89
|
+
} catch (err) {
|
|
90
|
+
toast.error("N\xE3o deu para tirar o acesso", { description: err?.message });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return /* @__PURE__ */ jsxs(RightRailPage, { title: displayName, open: true, onClose, width: "max-w-md", children: [
|
|
94
|
+
/* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 space-y-4 overflow-y-auto p-4", children: [
|
|
95
|
+
email && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: email }),
|
|
96
|
+
!loading && bindings.length === 0 && /* @__PURE__ */ jsxs("div", { className: "flex gap-3 rounded-lg border border-amber-400/40 bg-amber-400/10 p-3 text-sm", children: [
|
|
97
|
+
/* @__PURE__ */ jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" }),
|
|
98
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
99
|
+
/* @__PURE__ */ jsx("p", { children: "Sem unidade \u2014 n\xE3o v\xEA agendamentos, pedidos nem caixa de nenhuma unidade." }),
|
|
100
|
+
branches.find((u) => u.isHeadquarters) && /* @__PURE__ */ jsx(
|
|
101
|
+
Button,
|
|
102
|
+
{
|
|
103
|
+
size: "sm",
|
|
104
|
+
variant: "outline",
|
|
105
|
+
onClick: () => void add(branches.find((u) => u.isHeadquarters).id),
|
|
106
|
+
children: "Dar acesso \xE0 matriz"
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
] })
|
|
110
|
+
] }),
|
|
111
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-2", children: bindings.map((b) => {
|
|
112
|
+
const unit = byId[b.locationId];
|
|
113
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-border p-2", children: [
|
|
114
|
+
/* @__PURE__ */ jsx(Building2, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
115
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
116
|
+
/* @__PURE__ */ jsx("div", { className: "truncate text-sm", children: unit?.name ?? b.locationId }),
|
|
117
|
+
unit?.kind === "region" && /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "mt-0.5 text-[10px]", children: "regi\xE3o \u2014 alcan\xE7a as unidades abaixo" })
|
|
118
|
+
] }),
|
|
119
|
+
/* @__PURE__ */ jsxs(Select, { value: b.role ?? NO_EXTRA_ROLE, onValueChange: (v) => void setRole(b.locationId, v), children: [
|
|
120
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "h-8 w-36 text-xs", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
121
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
122
|
+
/* @__PURE__ */ jsx(SelectItem, { value: NO_EXTRA_ROLE, children: "sem fun\xE7\xE3o extra" }),
|
|
123
|
+
profiles.map((p) => /* @__PURE__ */ jsx(SelectItem, { value: p.id, children: p.name }, p.id))
|
|
124
|
+
] })
|
|
125
|
+
] }),
|
|
126
|
+
/* @__PURE__ */ jsx(Button, { size: "icon", variant: "ghost", onClick: () => void remove(b.locationId), "aria-label": "Remover", children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) })
|
|
127
|
+
] }, b.locationId);
|
|
128
|
+
}) }),
|
|
129
|
+
adding ? /* @__PURE__ */ jsxs(Select, { onValueChange: (v) => void add(v), children: [
|
|
130
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "h-9", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Escolha a unidade ou regi\xE3o" }) }),
|
|
131
|
+
/* @__PURE__ */ jsx(SelectContent, { children: unbound.map((u) => /* @__PURE__ */ jsxs(SelectItem, { value: u.id, children: [
|
|
132
|
+
u.name,
|
|
133
|
+
u.kind === "region" ? " (regi\xE3o)" : ""
|
|
134
|
+
] }, u.id)) })
|
|
135
|
+
] }) : /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: () => setAdding(true), disabled: unbound.length === 0, children: [
|
|
136
|
+
/* @__PURE__ */ jsx(Plus, { className: "mr-1.5 h-3.5 w-3.5" }),
|
|
137
|
+
"Adicionar unidade"
|
|
138
|
+
] })
|
|
139
|
+
] }),
|
|
140
|
+
/* @__PURE__ */ jsx("div", { className: "border-t border-border p-4 pr-12", children: /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
141
|
+
"As fun\xE7\xF5es por unidade ",
|
|
142
|
+
/* @__PURE__ */ jsx("strong", { children: "somam" }),
|
|
143
|
+
" \xE0 fun\xE7\xE3o da organiza\xE7\xE3o; nunca removem. Vincular algu\xE9m a uma regi\xE3o d\xE1 acesso a todas as unidades dentro dela."
|
|
144
|
+
] }) })
|
|
145
|
+
] });
|
|
146
|
+
}
|
|
147
|
+
function label(t, key, fallback) {
|
|
148
|
+
const v = t(key);
|
|
149
|
+
return !v || v === key ? fallback : v;
|
|
150
|
+
}
|
|
151
|
+
function initials(name) {
|
|
152
|
+
const parts = name.trim().split(/\s+/);
|
|
153
|
+
return ((parts[0]?.[0] ?? "") + (parts[1]?.[0] ?? "")).toUpperCase() || name.slice(0, 2).toUpperCase();
|
|
154
|
+
}
|
|
155
|
+
function UnitSwitcher({ collapsed = false, className }) {
|
|
156
|
+
useUnitBootstrap();
|
|
157
|
+
const { t } = useTranslation();
|
|
158
|
+
const unitMode = useUnitStore((s) => s.unitMode);
|
|
159
|
+
const loading = useUnitStore((s) => s.loading);
|
|
160
|
+
const units = useUnitStore((s) => s.units);
|
|
161
|
+
const tree = useUnitStore((s) => s.tree);
|
|
162
|
+
const byId = useUnitStore((s) => s.byId);
|
|
163
|
+
const myUnitIds = useUnitStore((s) => s.myUnitIds);
|
|
164
|
+
const activeUnitId = useUnitStore((s) => s.activeUnitId);
|
|
165
|
+
const beginSwitch = useUnitSwitchingStore((s) => s.begin);
|
|
166
|
+
const [query, setQuery] = React.useState("");
|
|
167
|
+
const singular = label(t, "unit.singular", "unidade");
|
|
168
|
+
const plural = label(t, "unit.plural", "unidades");
|
|
169
|
+
if (!unitMode) return null;
|
|
170
|
+
if (loading) {
|
|
171
|
+
return collapsed ? /* @__PURE__ */ jsx(Skeleton, { className: "mx-auto h-5 w-5 rounded bg-sidebar-accent" }) : /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-28 rounded-button bg-sidebar-accent" });
|
|
172
|
+
}
|
|
173
|
+
const reachable = myUnitIds.length > 0 ? myUnitIds : units.filter((u) => u.kind !== "region").map((u) => u.id);
|
|
174
|
+
const current = activeUnitId ? byId[activeUnitId] : void 0;
|
|
175
|
+
const allLabel = `${label(t, "units.all", "Todas as")} ${plural}`;
|
|
176
|
+
const currentLabel = current?.name ?? allLabel;
|
|
177
|
+
const searchable = reachable.length > 8;
|
|
178
|
+
const term = query.trim().toLowerCase();
|
|
179
|
+
const matches = term ? units.filter((u) => u.kind !== "region" && reachable.includes(u.id) && u.name.toLowerCase().includes(term)) : [];
|
|
180
|
+
if (reachable.length === 0) return null;
|
|
181
|
+
if (reachable.length === 1) {
|
|
182
|
+
const only = byId[reachable[0]];
|
|
183
|
+
if (!only) return null;
|
|
184
|
+
return collapsed ? /* @__PURE__ */ jsx("div", { className: "flex justify-center py-0.5", title: only.name, children: /* @__PURE__ */ jsx(Building2, { className: "h-3.5 w-3.5 text-sidebar-muted" }) }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-1.5 py-0.5", title: only.name, children: [
|
|
185
|
+
/* @__PURE__ */ jsx(Building2, { className: "h-3.5 w-3.5 shrink-0 text-sidebar-muted" }),
|
|
186
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate text-xs text-sidebar-muted", children: only.name })
|
|
187
|
+
] });
|
|
188
|
+
}
|
|
189
|
+
const renderNode = (node, depth) => {
|
|
190
|
+
if (node.kind === "region") {
|
|
191
|
+
return /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
192
|
+
/* @__PURE__ */ jsx(DropdownLabel, { className: "px-2 pt-2 text-[10px] uppercase tracking-wide text-muted-foreground", children: node.name }),
|
|
193
|
+
node.children.map((child) => renderNode(child, depth + 1))
|
|
194
|
+
] }, node.id);
|
|
195
|
+
}
|
|
196
|
+
if (!reachable.includes(node.id)) return null;
|
|
197
|
+
return /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
198
|
+
/* @__PURE__ */ jsxs(
|
|
199
|
+
DropdownItem,
|
|
200
|
+
{
|
|
201
|
+
onSelect: () => beginSwitch({ unitId: node.id, unitName: node.name }),
|
|
202
|
+
className: cn("flex items-center gap-2", depth > 0 && "pl-6"),
|
|
203
|
+
children: [
|
|
204
|
+
/* @__PURE__ */ jsx(Building2, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
|
|
205
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: node.name }),
|
|
206
|
+
node.isHeadquarters && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[10px] text-muted-foreground", children: label(t, "units.hq", "matriz") }),
|
|
207
|
+
activeUnitId === node.id && /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 shrink-0" })
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
),
|
|
211
|
+
node.children.map((child) => renderNode(child, depth + 1))
|
|
212
|
+
] }, node.id);
|
|
213
|
+
};
|
|
214
|
+
return /* @__PURE__ */ jsxs(Dropdown, { children: [
|
|
215
|
+
/* @__PURE__ */ jsx(DropdownTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
216
|
+
Button,
|
|
217
|
+
{
|
|
218
|
+
variant: "bare",
|
|
219
|
+
size: "bare",
|
|
220
|
+
className: cn(
|
|
221
|
+
"group flex items-center rounded-lg text-left transition-colors hover:bg-sidebar-accent/60 justify-start whitespace-normal text-[length:inherit] font-normal",
|
|
222
|
+
collapsed ? "justify-center p-1" : "w-full gap-1.5 px-1.5 py-0.5",
|
|
223
|
+
className
|
|
224
|
+
),
|
|
225
|
+
title: currentLabel,
|
|
226
|
+
"aria-label": `${singular}: ${currentLabel}`,
|
|
227
|
+
children: collapsed ? /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold text-sidebar-muted", children: current ? initials(current.name) : "\u2022\u2022" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
228
|
+
/* @__PURE__ */ jsx(Building2, { className: "h-3.5 w-3.5 shrink-0 text-sidebar-muted" }),
|
|
229
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-xs text-sidebar-muted", children: currentLabel }),
|
|
230
|
+
/* @__PURE__ */ jsx(ChevronsUpDown, { className: "h-3 w-3 shrink-0 text-sidebar-muted transition-colors group-hover:text-sidebar-foreground" })
|
|
231
|
+
] })
|
|
232
|
+
}
|
|
233
|
+
) }),
|
|
234
|
+
/* @__PURE__ */ jsxs(DropdownContent, { align: "start", className: "w-64", children: [
|
|
235
|
+
/* @__PURE__ */ jsxs(
|
|
236
|
+
DropdownItem,
|
|
237
|
+
{
|
|
238
|
+
onSelect: () => beginSwitch({ unitId: void 0, unitName: allLabel }),
|
|
239
|
+
className: "flex items-center gap-2",
|
|
240
|
+
children: [
|
|
241
|
+
/* @__PURE__ */ jsx(Layers, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
|
|
242
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: allLabel }),
|
|
243
|
+
!activeUnitId && /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 shrink-0" })
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
),
|
|
247
|
+
/* @__PURE__ */ jsx(DropdownSeparator, {}),
|
|
248
|
+
searchable && /* @__PURE__ */ jsxs("div", { className: "relative px-2 py-1.5", children: [
|
|
249
|
+
/* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-4 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
|
|
250
|
+
/* @__PURE__ */ jsx(
|
|
251
|
+
Input,
|
|
252
|
+
{
|
|
253
|
+
type: "search",
|
|
254
|
+
autoFocus: true,
|
|
255
|
+
value: query,
|
|
256
|
+
onChange: (e) => setQuery(e.target.value),
|
|
257
|
+
onKeyDown: (e) => e.stopPropagation(),
|
|
258
|
+
placeholder: label(t, "units.search", "Buscar unidade"),
|
|
259
|
+
className: "h-auto rounded-md border-border bg-background py-1 pl-7 pr-2 text-xs shadow-none focus:border-primary focus-visible:ring-0 focus-visible:ring-offset-0"
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
] }),
|
|
263
|
+
/* @__PURE__ */ jsx("div", { className: cn(searchable && "max-h-72 overflow-y-auto"), children: term ? matches.length > 0 ? matches.map((u) => /* @__PURE__ */ jsxs(
|
|
264
|
+
DropdownItem,
|
|
265
|
+
{
|
|
266
|
+
onSelect: () => beginSwitch({ unitId: u.id, unitName: u.name }),
|
|
267
|
+
className: "flex items-center gap-2",
|
|
268
|
+
children: [
|
|
269
|
+
/* @__PURE__ */ jsx(Building2, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
|
|
270
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: u.name }),
|
|
271
|
+
activeUnitId === u.id && /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 shrink-0" })
|
|
272
|
+
]
|
|
273
|
+
},
|
|
274
|
+
u.id
|
|
275
|
+
)) : /* @__PURE__ */ jsx("p", { className: "px-3 py-2 text-xs text-muted-foreground", children: label(t, "units.searchEmpty", "Nenhuma unidade com esse nome") }) : tree.map((node) => renderNode(node, 0)) })
|
|
276
|
+
] })
|
|
277
|
+
] });
|
|
278
|
+
}
|
|
279
|
+
var UNIT_DETAIL_ZONES = {
|
|
280
|
+
rooms: "unit.detail.rooms",
|
|
281
|
+
stockLocations: "unit.detail.stock-locations",
|
|
282
|
+
turnQueue: "unit.detail.turn-queue",
|
|
283
|
+
certificate: "unit.detail.certificate",
|
|
284
|
+
fiscal: "unit.detail.fiscal"
|
|
285
|
+
};
|
|
286
|
+
function zoneTab(zone) {
|
|
287
|
+
return function UnitZoneTab({ item }) {
|
|
288
|
+
const runtime = usePluginRuntimeOptional();
|
|
289
|
+
const widgets = runtime ? getWidgetsForZone(runtime, zone) : [];
|
|
290
|
+
if (!widgets.length || !item?.id) return null;
|
|
291
|
+
return /* @__PURE__ */ jsx("div", { className: "space-y-4", children: widgets.map((w) => {
|
|
292
|
+
const Widget = w.component;
|
|
293
|
+
return /* @__PURE__ */ jsx(Widget, { item, config: w.config, runtime: runtime?.context, plugin: w.plugin, widget: w }, w.id);
|
|
294
|
+
}) });
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
var LOCATION_SECTIONS = [
|
|
298
|
+
{
|
|
299
|
+
zone: UNIT_DETAIL_ZONES.rooms,
|
|
300
|
+
title: "Locais operacionais",
|
|
301
|
+
description: "Salas, cadeiras e boxes onde se atende. A lota\xE7\xE3o limita quantas pessoas a agenda marca no mesmo hor\xE1rio."
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
zone: UNIT_DETAIL_ZONES.stockLocations,
|
|
305
|
+
title: "Locais de estoque",
|
|
306
|
+
description: "Onde os produtos ficam guardados nesta unidade. Entradas, sa\xEDdas e contagens dizem de qual local."
|
|
307
|
+
}
|
|
308
|
+
];
|
|
309
|
+
function UnitLocationsTab({ item }) {
|
|
310
|
+
const runtime = usePluginRuntimeOptional();
|
|
311
|
+
if (!item?.id || !runtime) return null;
|
|
312
|
+
const sections = LOCATION_SECTIONS.map((s) => ({ ...s, widgets: getWidgetsForZone(runtime, s.zone) })).filter((s) => s.widgets.length > 0);
|
|
313
|
+
return /* @__PURE__ */ jsx("div", { className: "space-y-8", children: sections.map((s) => /* @__PURE__ */ jsxs("section", { className: "space-y-3", children: [
|
|
314
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
315
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-foreground", children: s.title }),
|
|
316
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: s.description })
|
|
317
|
+
] }),
|
|
318
|
+
s.widgets.map((w) => {
|
|
319
|
+
const Widget = w.component;
|
|
320
|
+
return /* @__PURE__ */ jsx(Widget, { item, config: w.config, runtime: runtime.context, plugin: w.plugin, widget: w }, w.id);
|
|
321
|
+
})
|
|
322
|
+
] }, s.zone)) });
|
|
323
|
+
}
|
|
324
|
+
var UNIT_DETAIL_TABS = [
|
|
325
|
+
{
|
|
326
|
+
id: "locations",
|
|
327
|
+
label: "Locais",
|
|
328
|
+
icon: "MapPin",
|
|
329
|
+
component: UnitLocationsTab,
|
|
330
|
+
aliases: ["rooms", "stock-locations"],
|
|
331
|
+
// Keyed on rooms: registry is universal, so the tab exists wherever there are units.
|
|
332
|
+
requiresWidgetZone: UNIT_DETAIL_ZONES.rooms
|
|
333
|
+
},
|
|
334
|
+
{ id: "turn-queue", label: "Lista da vez", icon: "ListOrdered", component: zoneTab(UNIT_DETAIL_ZONES.turnQueue), requiresWidgetZone: UNIT_DETAIL_ZONES.turnQueue },
|
|
335
|
+
// The archetype placeholders ("coming soon") answer nothing the V1 asked here.
|
|
336
|
+
{ id: "schedule", label: "Hours", hidden: true },
|
|
337
|
+
{ id: "stats", label: "Statistics", hidden: true },
|
|
338
|
+
{ id: "activity", label: "Activity", hidden: true }
|
|
339
|
+
];
|
|
340
|
+
function SuggestInput({ column, value, onChange, placeholder }) {
|
|
341
|
+
const search = React__default.useCallback(async (query) => {
|
|
342
|
+
const supabase = getSupabaseClientOptional();
|
|
343
|
+
if (!supabase) return [];
|
|
344
|
+
const { data } = await supabase.from("v_units").select(column).not(column, "is", null).limit(500);
|
|
345
|
+
const seen = /* @__PURE__ */ new Map();
|
|
346
|
+
for (const row of data ?? []) {
|
|
347
|
+
const v = String(row[column] ?? "").trim();
|
|
348
|
+
if (v && !seen.has(v.toLowerCase())) seen.set(v.toLowerCase(), v);
|
|
349
|
+
}
|
|
350
|
+
const q = query.trim().toLowerCase();
|
|
351
|
+
return [...seen.values()].filter((v) => !q || v.toLowerCase().includes(q)).sort((a, b) => a.localeCompare(b, "pt-BR")).map((v) => ({ id: v, label: v }));
|
|
352
|
+
}, [column]);
|
|
353
|
+
return /* @__PURE__ */ jsx(
|
|
354
|
+
SearchSelect,
|
|
355
|
+
{
|
|
356
|
+
value: value || void 0,
|
|
357
|
+
displayValue: value || void 0,
|
|
358
|
+
onChange: (id) => onChange(id || null),
|
|
359
|
+
onSearch: search,
|
|
360
|
+
minChars: 0,
|
|
361
|
+
placeholder: placeholder ?? "Selecione ou digite",
|
|
362
|
+
noResultsLabel: "Nenhum valor usado ainda",
|
|
363
|
+
searchingLabel: "Buscando\u2026",
|
|
364
|
+
clearLabel: "Limpar",
|
|
365
|
+
allowCreate: true,
|
|
366
|
+
createLabel: "Usar",
|
|
367
|
+
onCreate: (query) => onChange(query.trim() || null)
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
function suggestedTextField(field) {
|
|
372
|
+
const { column, ...rest } = field;
|
|
373
|
+
return {
|
|
374
|
+
...rest,
|
|
375
|
+
type: "custom",
|
|
376
|
+
renderInput: ({ value, onChange }) => /* @__PURE__ */ jsx(SuggestInput, { column, value: value ?? "", onChange, placeholder: rest.placeholder })
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function cnpjField(field) {
|
|
380
|
+
return {
|
|
381
|
+
...field,
|
|
382
|
+
type: "custom",
|
|
383
|
+
renderInput: ({ value, onChange }) => /* @__PURE__ */ jsx(
|
|
384
|
+
Input,
|
|
385
|
+
{
|
|
386
|
+
inputMode: "numeric",
|
|
387
|
+
value: maskCpfCnpj(String(value ?? "")),
|
|
388
|
+
placeholder: field.placeholder ?? "00.000.000/0000-00",
|
|
389
|
+
onChange: (e) => onChange(e.target.value.replace(/\D/g, "").slice(0, 14) || null)
|
|
390
|
+
}
|
|
391
|
+
)
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/components/settings/LocationsCrudPage.ts
|
|
396
|
+
var BR_TIMEZONES = [
|
|
397
|
+
{ value: "America/Noronha", label: "UTC\u22122 \xB7 Fernando de Noronha" },
|
|
398
|
+
{ value: "America/Sao_Paulo", label: "UTC\u22123 \xB7 Bras\xEDlia (SP, RJ, MG, ES, PR, SC, RS, GO, DF)" },
|
|
399
|
+
{ value: "America/Bahia", label: "UTC\u22123 \xB7 Bahia" },
|
|
400
|
+
{ value: "America/Fortaleza", label: "UTC\u22123 \xB7 Fortaleza (CE, MA, PI, RN, PB)" },
|
|
401
|
+
{ value: "America/Recife", label: "UTC\u22123 \xB7 Recife (PE)" },
|
|
402
|
+
{ value: "America/Maceio", label: "UTC\u22123 \xB7 Macei\xF3 (AL, SE)" },
|
|
403
|
+
{ value: "America/Belem", label: "UTC\u22123 \xB7 Bel\xE9m (PA, AP)" },
|
|
404
|
+
{ value: "America/Araguaina", label: "UTC\u22123 \xB7 Aragua\xEDna (TO)" },
|
|
405
|
+
{ value: "America/Cuiaba", label: "UTC\u22124 \xB7 Cuiab\xE1 (MT)" },
|
|
406
|
+
{ value: "America/Campo_Grande", label: "UTC\u22124 \xB7 Campo Grande (MS)" },
|
|
407
|
+
{ value: "America/Manaus", label: "UTC\u22124 \xB7 Manaus (AM)" },
|
|
408
|
+
{ value: "America/Porto_Velho", label: "UTC\u22124 \xB7 Porto Velho (RO)" },
|
|
409
|
+
{ value: "America/Boa_Vista", label: "UTC\u22124 \xB7 Boa Vista (RR)" },
|
|
410
|
+
{ value: "America/Rio_Branco", label: "UTC\u22125 \xB7 Rio Branco (AC)" },
|
|
411
|
+
{ value: "America/Eirunepe", label: "UTC\u22125 \xB7 Oeste do Amazonas (Eirunep\xE9)" }
|
|
412
|
+
];
|
|
413
|
+
var LOGO_BUCKET = "avatars";
|
|
414
|
+
async function uploadUnitLogo(file, row) {
|
|
415
|
+
const supabase = getSupabaseClientOptional();
|
|
416
|
+
if (!supabase) throw new Error("O armazenamento n\xE3o est\xE1 configurado.");
|
|
417
|
+
const ext = (file.name.split(".").pop() ?? "png").toLowerCase().replace(/[^a-z0-9]/g, "") || "png";
|
|
418
|
+
const path = `units/${row.id}/logo-${Date.now()}.${ext}`;
|
|
419
|
+
const { error } = await supabase.storage.from(LOGO_BUCKET).upload(path, file, { upsert: true });
|
|
420
|
+
if (error) throw error;
|
|
421
|
+
const url = supabase.storage.from(LOGO_BUCKET).getPublicUrl(path).data.publicUrl;
|
|
422
|
+
const { error: saveError } = await supabase.from("v_units").update({ logo_url: url }).eq("id", row.id);
|
|
423
|
+
if (saveError) throw saveError;
|
|
424
|
+
return url;
|
|
425
|
+
}
|
|
426
|
+
async function removeUnitLogo(row) {
|
|
427
|
+
const supabase = getSupabaseClientOptional();
|
|
428
|
+
if (!supabase) return;
|
|
429
|
+
const { error } = await supabase.from("v_units").update({ logo_url: null }).eq("id", row.id);
|
|
430
|
+
if (error) throw error;
|
|
431
|
+
}
|
|
432
|
+
var locationEntityDef = {
|
|
433
|
+
// V1 calls the screen "Empresas" (Configurações → Empresa): each row is a
|
|
434
|
+
// company — HQ or branch — and becomes a unit everywhere else.
|
|
435
|
+
name: "Empresa",
|
|
436
|
+
namePlural: "Empresas",
|
|
437
|
+
icon: "Building2",
|
|
438
|
+
layout: "location",
|
|
439
|
+
displayField: "name",
|
|
440
|
+
subtitleField: "city",
|
|
441
|
+
defaultSort: "name",
|
|
442
|
+
// Plan quantity cap — the generic CRUD (CrudFormPage/CrudPage/CrudListView)
|
|
443
|
+
// guards create/import and dims "+ Add" at the cap. Core declares 'locations'.
|
|
444
|
+
limitKey: "locations",
|
|
445
|
+
formColumns: 2,
|
|
446
|
+
facets: [activeFacet({ active: "Ativas", inactive: "Inativas", all: "Todas" })],
|
|
447
|
+
image: {
|
|
448
|
+
get: (row) => row.logoUrl ?? void 0,
|
|
449
|
+
upload: uploadUnitLogo,
|
|
450
|
+
remove: removeUnitLogo,
|
|
451
|
+
accept: "image/png,image/jpeg,image/webp,image/svg+xml"
|
|
452
|
+
},
|
|
453
|
+
// The V1 company form (Configurações → Empresa), block by block. The tabs that
|
|
454
|
+
// belong to other modules — rooms, stock locations, turn queue, certificate,
|
|
455
|
+
// fiscal issuing — are detail tabs, contributed by the plugin that owns them.
|
|
456
|
+
fieldGroups: [
|
|
457
|
+
{ id: "identity", label: "Identifica\xE7\xE3o", columns: 3, imageSlot: true },
|
|
458
|
+
{ id: "where", label: "Endere\xE7o", columns: 1 },
|
|
459
|
+
{ id: "network", label: "Classifica\xE7\xE3o na rede", description: "Alimenta os filtros dos relat\xF3rios de rede. Escolha um valor j\xE1 usado ou digite um novo.", columns: 3 },
|
|
460
|
+
{ id: "contacts", label: "Contatos", columns: 2 },
|
|
461
|
+
{ id: "channels", label: "Canais de divulga\xE7\xE3o", description: "Perfis e contatos usados nas pe\xE7as de campanha e no atendimento.", columns: 3 },
|
|
462
|
+
{ id: "settings", label: "Configura\xE7\xF5es", description: "Valem s\xF3 para esta unidade. Sem mexer, ela segue o padr\xE3o da casa.", columns: 2 },
|
|
463
|
+
{ id: "notes", label: "Observa\xE7\xF5es", columns: 1 },
|
|
464
|
+
{
|
|
465
|
+
id: "tax",
|
|
466
|
+
label: "Identidade fiscal",
|
|
467
|
+
description: "Do CNPJ desta unidade. Unidades com o mesmo CNPJ dividem estes dados.",
|
|
468
|
+
columns: 3,
|
|
469
|
+
tab: "Fiscal",
|
|
470
|
+
tabIcon: "FileText",
|
|
471
|
+
// The certificate and the issuer config are fiscal-br's; they draw here, under the CNPJ's fields.
|
|
472
|
+
detailZones: [UNIT_DETAIL_ZONES.certificate, UNIT_DETAIL_ZONES.fiscal]
|
|
473
|
+
},
|
|
474
|
+
{ ...RECORD_GROUP }
|
|
475
|
+
],
|
|
476
|
+
fields: [
|
|
477
|
+
// V1's first column: the matriz/filial badge.
|
|
478
|
+
{
|
|
479
|
+
key: "unitType",
|
|
480
|
+
label: "Tipo",
|
|
481
|
+
type: "text",
|
|
482
|
+
showInForm: false,
|
|
483
|
+
showInDetail: false,
|
|
484
|
+
renderCell: (_v, row) => {
|
|
485
|
+
const r = row;
|
|
486
|
+
return r.isHeadquarters ? "Matriz" : r.kind === "region" ? "Regi\xE3o" : "Filial";
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
{ key: "legalName", label: "Raz\xE3o social", type: "text", showInTable: true, searchable: true, group: "identity", span: 2 },
|
|
490
|
+
{ key: "name", label: "Nome fantasia", type: "text", required: true, showInTable: true, searchable: true, group: "identity", hint: "\xC9 o nome que aparece nos filtros e no seletor de unidade." },
|
|
491
|
+
{ key: "abbreviation", label: "Abrevia\xE7\xE3o", type: "text", group: "identity", placeholder: "Ex.: TIJ" },
|
|
492
|
+
{
|
|
493
|
+
...cnpjField({ key: "cnpj", label: "CNPJ", showInTable: true, searchable: true, group: "identity", hint: "Opcional. Obrigat\xF3rio para emitir nota ou receber DANFes." }),
|
|
494
|
+
renderCell: (v) => v ? maskCpfCnpj(String(v)) : "\u2014"
|
|
495
|
+
},
|
|
496
|
+
// The two columns migration 039 added. A `region` groups branches and is
|
|
497
|
+
// never somewhere anyone stands — bind a manager to it and they reach every
|
|
498
|
+
// branch under it, which is the whole reason regions exist.
|
|
499
|
+
{
|
|
500
|
+
key: "kind",
|
|
501
|
+
label: "Tipo",
|
|
502
|
+
type: "segmented",
|
|
503
|
+
defaultValue: "branch",
|
|
504
|
+
// The view reads `hq` for the headquarters, which this segmented does not
|
|
505
|
+
// offer: the matriz is the "Esta é a matriz?" toggle, as in V1.
|
|
506
|
+
showInTable: false,
|
|
507
|
+
showInDetail: false,
|
|
508
|
+
group: "identity",
|
|
509
|
+
visibleWhen: (values) => !values.isHeadquarters,
|
|
510
|
+
options: [
|
|
511
|
+
{ label: "Unidade", value: "branch", description: "Onde o atendimento acontece" },
|
|
512
|
+
{ label: "Regi\xE3o", value: "region", description: "Agrupa unidades; ningu\xE9m trabalha nela" }
|
|
513
|
+
]
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
key: "parentId",
|
|
517
|
+
label: "Regi\xE3o",
|
|
518
|
+
type: "relation",
|
|
519
|
+
showInTable: true,
|
|
520
|
+
group: "network",
|
|
521
|
+
relation: { table: "v_units", labelField: "name", filter: { kind: "region" } },
|
|
522
|
+
hint: "Deixe vazio se a unidade n\xE3o pertence a uma regi\xE3o."
|
|
523
|
+
},
|
|
524
|
+
{ key: "code", label: "C\xF3digo", type: "text", searchable: true, hint: "O c\xF3digo que a rede j\xE1 usa no papel (ex. RJ-042).", group: "identity" },
|
|
525
|
+
{ key: "address", label: "Endere\xE7o", type: "address", showInTable: false, showInDetail: false, group: "where" },
|
|
526
|
+
{ key: "phone", label: "Telefone 1", type: "phone", group: "contacts" },
|
|
527
|
+
{ key: "phone2", label: "Telefone 2", type: "phone", group: "contacts" },
|
|
528
|
+
{ key: "mobile1", label: "Celular 1", type: "phone", group: "contacts" },
|
|
529
|
+
{ key: "mobile2", label: "Celular 2", type: "phone", group: "contacts" },
|
|
530
|
+
// O endereço vem de `public.addresses` (141) e se edita no bloco próprio,
|
|
531
|
+
// igual ao da ficha da pessoa. Continua aqui para a LISTA — cidade e UF são
|
|
532
|
+
// como se reconhece uma unidade numa rede — mas não como campo solto na
|
|
533
|
+
// ficha nem no formulário: rua, número e CEP separados por outros campos
|
|
534
|
+
// não se leem como endereço, e dois lugares para escrever a mesma coisa
|
|
535
|
+
// foi o que deixou um JSON inteiro dentro do input de uma linha.
|
|
536
|
+
{ key: "city", label: "Cidade", type: "text", showInTable: true, showInForm: false, showInDetail: false, group: "identity" },
|
|
537
|
+
{ key: "state", label: "UF", type: "text", showInTable: true, showInForm: false, showInDetail: false, group: "identity" },
|
|
538
|
+
{ key: "country", label: "Pa\xEDs", type: "text", defaultValue: "BR", showInForm: false, showInDetail: false, group: "identity" },
|
|
539
|
+
{ key: "postalCode", label: "CEP", type: "text", showInForm: false, showInDetail: false, group: "identity" },
|
|
540
|
+
// Servem a uma coisa só: oferecer "a unidade mais perto de mim" a quem
|
|
541
|
+
// entra pela primeira vez e alcança várias. Editam-se no bloco de endereço
|
|
542
|
+
// (142) — coordenada é a posição de UM endereço, e separada dele a
|
|
543
|
+
// primeira mudança de rua deixa o ponteiro na loja antiga. Continuam
|
|
544
|
+
// declaradas porque a view as publica e o `geo` as lê.
|
|
545
|
+
{ key: "latitude", label: "Latitude", type: "number", showInForm: false, showInDetail: false, group: "identity" },
|
|
546
|
+
{ key: "longitude", label: "Longitude", type: "number", showInForm: false, showInDetail: false, group: "identity" },
|
|
547
|
+
{ key: "email", label: "E-mail 1", type: "email", group: "contacts" },
|
|
548
|
+
{ key: "email2", label: "E-mail 2", type: "email", group: "contacts" },
|
|
549
|
+
{ key: "isHeadquarters", label: "Esta \xE9 a matriz?", type: "boolean", showInTable: true, group: "identity", inlineToggle: true },
|
|
550
|
+
{
|
|
551
|
+
key: "coordinatorPersonId",
|
|
552
|
+
label: "Coordenador(a)",
|
|
553
|
+
type: "relation",
|
|
554
|
+
group: "network",
|
|
555
|
+
relation: { table: "people", labelField: "name" }
|
|
556
|
+
},
|
|
557
|
+
suggestedTextField({ key: "storeType", column: "store_type", label: "Tipo de loja", group: "network", placeholder: "Ex.: Rua, Shopping" }),
|
|
558
|
+
suggestedTextField({ key: "storeCategory", column: "store_category", label: "Categoria de loja", group: "network", placeholder: "Ex.: A, B, C" }),
|
|
559
|
+
{ key: "instagramHandle", label: "Instagram (@)", type: "text", group: "channels", placeholder: "@suaempresa" },
|
|
560
|
+
{ key: "facebookHandle", label: "Facebook", type: "text", group: "channels", placeholder: "/suaempresa" },
|
|
561
|
+
{ key: "tiktokHandle", label: "TikTok", type: "text", group: "channels", placeholder: "@suaempresa" },
|
|
562
|
+
{ key: "website", label: "Site", type: "url", group: "channels", placeholder: "www.suaempresa.com" },
|
|
563
|
+
{ key: "whatsappNumber", label: "WhatsApp (divulga\xE7\xE3o)", type: "phone", group: "channels" },
|
|
564
|
+
{ key: "marketingPhone", label: "Telefone de marketing", type: "phone", group: "channels" },
|
|
565
|
+
{ key: "timezone", label: "Fuso hor\xE1rio", type: "select", group: "settings", span: 2, options: BR_TIMEZONES, showInDetail: false },
|
|
566
|
+
{ key: "autoDdd", label: "DDD autom\xE1tico", type: "boolean", inlineToggle: true, group: "settings", hint: "Telefone digitado sem DDD recebe o desta unidade.", showInDetail: false },
|
|
567
|
+
{ key: "daylightSaving", label: "Hor\xE1rio de ver\xE3o", type: "boolean", inlineToggle: true, group: "settings", showInDetail: false },
|
|
568
|
+
{ key: "onlineBookingEnabled", label: "Agendamento online", type: "boolean", inlineToggle: true, group: "settings", hint: "A unidade aparece para quem agenda pela internet.", showInDetail: false },
|
|
569
|
+
{ key: "requireOpenCashSession", label: "Receber s\xF3 com caixinha aberto", type: "boolean", inlineToggle: true, group: "settings", hint: "Maquininha e Balc\xE3o recusam a cobran\xE7a de quem n\xE3o abriu o pr\xF3prio caixinha nesta unidade.", showInDetail: false },
|
|
570
|
+
{ key: "municipalRegistration", label: "Inscri\xE7\xE3o municipal", type: "text", group: "tax", hint: "No padr\xE3o nacional da NFS-e \xE9 opcional. Na d\xFAvida, deixe em branco." },
|
|
571
|
+
{ key: "stateRegistration", label: "Inscri\xE7\xE3o estadual", type: "text", group: "tax", placeholder: "ISENTO se for o caso" },
|
|
572
|
+
{ key: "taxRegime", label: "Regime tribut\xE1rio", type: "select", group: "tax", options: [
|
|
573
|
+
{ label: "Simples Nacional", value: "simples_nacional" },
|
|
574
|
+
{ label: "Simples Nacional \u2014 excesso", value: "simples_nacional_excesso" },
|
|
575
|
+
{ label: "Regime normal (Lucro Presumido / Real)", value: "regime_normal" }
|
|
576
|
+
] },
|
|
577
|
+
{ key: "cnae", label: "CNAE principal", type: "text", group: "tax", placeholder: "0000-0/00" },
|
|
578
|
+
{ key: "ibgeCityCode", label: "C\xF3digo IBGE do munic\xEDpio", type: "text", group: "tax", hint: "A NFS-e \xE9 roteada pela prefeitura deste c\xF3digo." },
|
|
579
|
+
{ key: "fiscalEmail", label: "E-mail fiscal", type: "email", group: "tax", hint: "Para onde a prefeitura e a SEFAZ mandam retorno." },
|
|
580
|
+
{ key: "logoUrl", label: "Logo", type: "image", showInForm: false, showInDetail: false, showInTable: false },
|
|
581
|
+
// O rail padrão de todo cadastro. Sem `unit`: esta ficha É a unidade.
|
|
582
|
+
...recordRailFields({ tagsTable: "v_units", activeLabel: "Ativa", unit: false })
|
|
583
|
+
],
|
|
584
|
+
data: {
|
|
585
|
+
// `public.locations` is gone: units live in schema `app` and `v_units` is the
|
|
586
|
+
// public bridge. It reads every field below and, since 140, writes them
|
|
587
|
+
// too: an INSTEAD OF trigger folds the address back into `app.units`
|
|
588
|
+
// metadata and puts a `kind` outside hq|branch into `legacy_kind`.
|
|
589
|
+
table: "v_units",
|
|
590
|
+
tenantScoped: true,
|
|
591
|
+
searchColumns: ["name", "legal_name", "cnpj", "city", "state", "code"],
|
|
592
|
+
addressOwnerType: "unit",
|
|
593
|
+
// camelCase→snake does not split a digit off: `email2` would look for column `email2`.
|
|
594
|
+
columnMap: { phone_2: "phone2", mobile_1: "mobile1", mobile_2: "mobile2", email_2: "email2" },
|
|
595
|
+
defaults: { kind: "branch" },
|
|
596
|
+
// The tree is never narrowed by the unit you are standing in: the switcher
|
|
597
|
+
// has to be able to name a unit whose data you cannot see, and so does the
|
|
598
|
+
// "Unidade: Ipanema" line on a record.
|
|
599
|
+
unitScoped: false
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
locationEntityDef.detailTabs = UNIT_DETAIL_TABS;
|
|
603
|
+
var LIST_COLUMNS = ["unitType", "legalName", "name", "cnpj", "city", "state", "isActive"];
|
|
604
|
+
locationEntityDef.fields = locationEntityDef.fields.map((f) => ({ ...f, showInTable: LIST_COLUMNS.includes(f.key) }));
|
|
605
|
+
var LocationsCrudPage = createCrudPage(locationEntityDef);
|
|
606
|
+
LocationsCrudPage.__crudBasePath = "/settings/locations";
|
|
607
|
+
function UnitsSettingsPage() {
|
|
608
|
+
useUnitBootstrap();
|
|
609
|
+
return /* @__PURE__ */ jsx(LocationsCrudPage, {});
|
|
610
|
+
}
|
|
611
|
+
var useUnitPanel = create((set) => ({
|
|
612
|
+
unitId: null,
|
|
613
|
+
openUnit: (id) => set({ unitId: id }),
|
|
614
|
+
close: () => set({ unitId: null })
|
|
615
|
+
}));
|
|
616
|
+
function UnitRailPage() {
|
|
617
|
+
const unitId = useUnitPanel((s) => s.unitId);
|
|
618
|
+
const close = useUnitPanel((s) => s.close);
|
|
619
|
+
const byId = useUnitStore((s) => s.byId);
|
|
620
|
+
const unitMode = useUnitStore((s) => s.unitMode);
|
|
621
|
+
const unit = unitId ? byId[unitId] : void 0;
|
|
622
|
+
const region = unit?.parentId ? byId[unit.parentId] : void 0;
|
|
623
|
+
const isRegion = unit?.kind === "region";
|
|
624
|
+
if (!unit) return null;
|
|
625
|
+
const address = [unit.address, unit.city, unit.state].filter(Boolean).join(", ");
|
|
626
|
+
return /* @__PURE__ */ jsx(RightRailPage, { title: unit.name, open: true, onClose: close, children: /* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 space-y-4 overflow-y-auto p-4", children: [
|
|
627
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
628
|
+
/* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-muted", children: /* @__PURE__ */ jsx(Building2, { className: "h-5 w-5 text-muted-foreground" }) }),
|
|
629
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
630
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
|
|
631
|
+
isRegion && /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "text-[10px]", children: "regi\xE3o" }),
|
|
632
|
+
unit.isHeadquarters && /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-[10px]", children: "matriz" }),
|
|
633
|
+
unit.isActive === false && /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "text-[10px]", children: "inativa" }),
|
|
634
|
+
unit.code && /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "text-[10px]", children: unit.code }),
|
|
635
|
+
region && /* @__PURE__ */ jsxs(Badge, { variant: "outline", className: "text-[10px]", children: [
|
|
636
|
+
"em ",
|
|
637
|
+
region.name
|
|
638
|
+
] })
|
|
639
|
+
] }),
|
|
640
|
+
address && /* @__PURE__ */ jsxs("p", { className: "mt-1.5 flex items-start gap-1.5 text-xs text-muted-foreground", children: [
|
|
641
|
+
/* @__PURE__ */ jsx(MapPin, { className: "mt-0.5 h-3 w-3 shrink-0" }),
|
|
642
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0", children: address })
|
|
643
|
+
] }),
|
|
644
|
+
!unitMode && /* @__PURE__ */ jsx("p", { className: "mt-2 text-xs text-muted-foreground", children: "A gest\xE3o por unidades est\xE1 desligada \u2014 estes v\xEDnculos ainda n\xE3o restringem nada." })
|
|
645
|
+
] })
|
|
646
|
+
] }),
|
|
647
|
+
/* @__PURE__ */ jsx(UnitMembersTab, { item: unit }),
|
|
648
|
+
/* @__PURE__ */ jsxs(
|
|
649
|
+
Button,
|
|
650
|
+
{
|
|
651
|
+
variant: "outline",
|
|
652
|
+
size: "sm",
|
|
653
|
+
className: "w-full",
|
|
654
|
+
onClick: () => {
|
|
655
|
+
close();
|
|
656
|
+
navigateApp(`#/settings/locations/${unit.id}`);
|
|
657
|
+
},
|
|
658
|
+
children: [
|
|
659
|
+
/* @__PURE__ */ jsx(Settings2, { className: "mr-1.5 h-3.5 w-3.5" }),
|
|
660
|
+
"Abrir a ficha da ",
|
|
661
|
+
isRegion ? "regi\xE3o" : "unidade"
|
|
662
|
+
]
|
|
663
|
+
}
|
|
664
|
+
)
|
|
665
|
+
] }) });
|
|
666
|
+
}
|
|
667
|
+
function MemberUnitsCell({
|
|
668
|
+
userId,
|
|
669
|
+
onManage
|
|
670
|
+
}) {
|
|
671
|
+
const byId = useUnitStore((s) => s.byId);
|
|
672
|
+
const unitMode = useUnitStore((s) => s.unitMode);
|
|
673
|
+
const openUnit = useUnitPanel((s) => s.openUnit);
|
|
674
|
+
const [unitIds, setUnitIds] = React.useState(null);
|
|
675
|
+
const version = useDataVersion(UNIT_ACCESS_CHANGED);
|
|
676
|
+
React.useEffect(() => {
|
|
677
|
+
if (!userId) {
|
|
678
|
+
setUnitIds([]);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
let alive = true;
|
|
682
|
+
void fetchMemberUnits(userId).then((b) => {
|
|
683
|
+
if (alive) setUnitIds(b.map((x) => x.locationId));
|
|
684
|
+
});
|
|
685
|
+
return () => {
|
|
686
|
+
alive = false;
|
|
687
|
+
};
|
|
688
|
+
}, [userId, version]);
|
|
689
|
+
if (!unitMode || !userId) return /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "\u2014" });
|
|
690
|
+
if (unitIds === null) return /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "\u2026" });
|
|
691
|
+
if (unitIds.length === 0) {
|
|
692
|
+
return /* @__PURE__ */ jsxs(
|
|
693
|
+
Button,
|
|
694
|
+
{
|
|
695
|
+
variant: "bare",
|
|
696
|
+
size: "bare",
|
|
697
|
+
type: "button",
|
|
698
|
+
onClick: onManage,
|
|
699
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-1.5 py-1 text-xs text-amber-600 transition-colors hover:bg-amber-400/10 dark:text-amber-400 font-normal",
|
|
700
|
+
title: "Sem unidade \u2014 esta pessoa n\xE3o v\xEA dados de nenhuma unidade",
|
|
701
|
+
children: [
|
|
702
|
+
/* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5" }),
|
|
703
|
+
"sem unidade"
|
|
704
|
+
]
|
|
705
|
+
}
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
const shownIds = unitIds.slice(0, 2);
|
|
709
|
+
const rest = unitIds.length - shownIds.length;
|
|
710
|
+
const allNames = unitIds.map((id) => byId[id]?.name ?? "\u2014");
|
|
711
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1", title: allNames.join(" \xB7 "), children: [
|
|
712
|
+
shownIds.map((id) => /* @__PURE__ */ jsx(Button, { variant: "bare", size: "bare", type: "button", onClick: () => openUnit(id), className: "focus:outline-none text-[length:inherit] font-normal", children: /* @__PURE__ */ jsxs(Badge, { variant: "secondary", className: "gap-1 text-[10px] transition-colors hover:bg-muted", children: [
|
|
713
|
+
/* @__PURE__ */ jsx(Building2, { className: "h-3 w-3" }),
|
|
714
|
+
byId[id]?.name ?? "\u2014"
|
|
715
|
+
] }) }, id)),
|
|
716
|
+
rest > 0 && /* @__PURE__ */ jsx(Button, { variant: "bare", size: "bare", type: "button", onClick: onManage, className: "focus:outline-none text-[length:inherit] font-normal", children: /* @__PURE__ */ jsxs(Badge, { variant: "outline", className: "text-[10px] transition-colors hover:bg-muted", children: [
|
|
717
|
+
"+",
|
|
718
|
+
rest
|
|
719
|
+
] }) })
|
|
720
|
+
] });
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// src/units/geo.ts
|
|
724
|
+
var TIMEOUT_MS = 8e3;
|
|
725
|
+
function distanceKm(aLat, aLon, bLat, bLon) {
|
|
726
|
+
const R = 6371;
|
|
727
|
+
const toRad = (d) => d * Math.PI / 180;
|
|
728
|
+
const dLat = toRad(bLat - aLat);
|
|
729
|
+
const dLon = toRad(bLon - aLon);
|
|
730
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(aLat)) * Math.cos(toRad(bLat)) * Math.sin(dLon / 2) ** 2;
|
|
731
|
+
return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)));
|
|
732
|
+
}
|
|
733
|
+
function canOfferNearest(units, reachable) {
|
|
734
|
+
if (typeof navigator === "undefined" || !navigator.geolocation) return false;
|
|
735
|
+
const withCoords = units.filter(
|
|
736
|
+
(u) => reachable.includes(u.id) && u.latitude != null && u.longitude != null
|
|
737
|
+
);
|
|
738
|
+
return withCoords.length > 1;
|
|
739
|
+
}
|
|
740
|
+
async function findNearestUnit(units, reachable) {
|
|
741
|
+
const candidates = units.filter(
|
|
742
|
+
(u) => reachable.includes(u.id) && u.latitude != null && u.longitude != null
|
|
743
|
+
);
|
|
744
|
+
if (candidates.length === 0) return void 0;
|
|
745
|
+
if (typeof navigator === "undefined" || !navigator.geolocation) return void 0;
|
|
746
|
+
const position = await new Promise((resolve) => {
|
|
747
|
+
let settled = false;
|
|
748
|
+
const done = (p) => {
|
|
749
|
+
if (settled) return;
|
|
750
|
+
settled = true;
|
|
751
|
+
resolve(p);
|
|
752
|
+
};
|
|
753
|
+
const timer = setTimeout(() => done(void 0), TIMEOUT_MS);
|
|
754
|
+
try {
|
|
755
|
+
navigator.geolocation.getCurrentPosition(
|
|
756
|
+
(p) => {
|
|
757
|
+
clearTimeout(timer);
|
|
758
|
+
done(p);
|
|
759
|
+
},
|
|
760
|
+
() => {
|
|
761
|
+
clearTimeout(timer);
|
|
762
|
+
done(void 0);
|
|
763
|
+
},
|
|
764
|
+
// Precisão grosseira de propósito: a pergunta é em qual bairro, e o modo
|
|
765
|
+
// preciso liga o GPS, gasta bateria e demora mais para responder o mesmo.
|
|
766
|
+
{ enableHighAccuracy: false, timeout: TIMEOUT_MS, maximumAge: 5 * 60 * 1e3 }
|
|
767
|
+
);
|
|
768
|
+
} catch {
|
|
769
|
+
clearTimeout(timer);
|
|
770
|
+
done(void 0);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
if (!position) return void 0;
|
|
774
|
+
let best;
|
|
775
|
+
for (const u of candidates) {
|
|
776
|
+
const km = distanceKm(
|
|
777
|
+
position.coords.latitude,
|
|
778
|
+
position.coords.longitude,
|
|
779
|
+
Number(u.latitude),
|
|
780
|
+
Number(u.longitude)
|
|
781
|
+
);
|
|
782
|
+
if (!best || km < best.km) best = { unitId: u.id, km };
|
|
783
|
+
}
|
|
784
|
+
return best;
|
|
785
|
+
}
|
|
786
|
+
var HOLD_MS = 900;
|
|
787
|
+
function UnitSwitching({ unitName, isAll, onDone }) {
|
|
788
|
+
const { t } = useTranslation();
|
|
789
|
+
const tr = (key, fallback) => {
|
|
790
|
+
const v = t(key);
|
|
791
|
+
return !v || v === key ? fallback : v;
|
|
792
|
+
};
|
|
793
|
+
const doneRef = React.useRef(onDone);
|
|
794
|
+
doneRef.current = onDone;
|
|
795
|
+
React.useEffect(() => {
|
|
796
|
+
const id = window.setTimeout(() => doneRef.current(), HOLD_MS);
|
|
797
|
+
return () => window.clearTimeout(id);
|
|
798
|
+
}, []);
|
|
799
|
+
const Icon = isAll ? Layers : Store;
|
|
800
|
+
return /* @__PURE__ */ jsx(
|
|
801
|
+
"div",
|
|
802
|
+
{
|
|
803
|
+
role: "status",
|
|
804
|
+
"aria-live": "polite",
|
|
805
|
+
"aria-label": `${tr("unit.switching.eyebrow", "Mudando para")} ${unitName}`,
|
|
806
|
+
className: "saas-switch-veil fixed inset-0 z-[140] flex items-center justify-center bg-background px-6",
|
|
807
|
+
children: /* @__PURE__ */ jsxs("div", { className: "saas-switch-rise flex w-full max-w-sm flex-col items-center", children: [
|
|
808
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex h-16 w-16 items-center justify-center", children: [
|
|
809
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "saas-switch-pulse absolute inset-0 rounded-2xl bg-primary/25" }),
|
|
810
|
+
/* @__PURE__ */ jsx("span", { className: "relative flex h-14 w-14 items-center justify-center rounded-2xl bg-primary text-primary-foreground shadow-sm", children: /* @__PURE__ */ jsx(Icon, { className: "h-6 w-6", strokeWidth: 2 }) })
|
|
811
|
+
] }),
|
|
812
|
+
/* @__PURE__ */ jsx("p", { className: "mt-6 text-xs font-medium uppercase tracking-[0.14em] text-muted-foreground", children: tr("unit.switching.eyebrow", "Mudando para") }),
|
|
813
|
+
/* @__PURE__ */ jsx("h1", { className: "mt-1.5 text-pretty text-center text-2xl font-semibold leading-tight tracking-tight text-foreground sm:text-3xl", children: unitName }),
|
|
814
|
+
/* @__PURE__ */ jsxs("p", { className: "mt-4 flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
815
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "saas-switch-dot h-2 w-2 rounded-full bg-primary" }),
|
|
816
|
+
tr("unit.switching.loading", "Carregando os dados desta unidade")
|
|
817
|
+
] })
|
|
818
|
+
] })
|
|
819
|
+
}
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
function UnitSwitchingHost() {
|
|
823
|
+
const target = useUnitSwitchingStore((s) => s.target);
|
|
824
|
+
if (!target) return null;
|
|
825
|
+
return /* @__PURE__ */ jsx(
|
|
826
|
+
UnitSwitching,
|
|
827
|
+
{
|
|
828
|
+
unitName: target.unitName,
|
|
829
|
+
isAll: target.unitId === void 0,
|
|
830
|
+
onDone: () => window.location.reload()
|
|
831
|
+
}
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
var useUnitPickerStore = create((set) => ({
|
|
835
|
+
open: false,
|
|
836
|
+
setOpen: (open) => set({ open })
|
|
837
|
+
}));
|
|
838
|
+
function openUnitPicker() {
|
|
839
|
+
useUnitPickerStore.getState().setOpen(true);
|
|
840
|
+
}
|
|
841
|
+
var ALL_UNITS = "__all__";
|
|
842
|
+
function UnitPickerDialog() {
|
|
843
|
+
const open = useUnitPickerStore((s) => s.open);
|
|
844
|
+
const setOpen = useUnitPickerStore((s) => s.setOpen);
|
|
845
|
+
const unitMode = useUnitStore((s) => s.unitMode);
|
|
846
|
+
const units = useUnitStore((s) => s.units);
|
|
847
|
+
const tree = useUnitStore((s) => s.tree);
|
|
848
|
+
const byId = useUnitStore((s) => s.byId);
|
|
849
|
+
const myUnitIds = useUnitStore((s) => s.myUnitIds);
|
|
850
|
+
const activeUnitId = useUnitStore((s) => s.activeUnitId);
|
|
851
|
+
const beginSwitch = useUnitSwitchingStore((s) => s.begin);
|
|
852
|
+
const reachable = React.useMemo(
|
|
853
|
+
() => myUnitIds.length > 0 ? myUnitIds : units.filter((u) => u.kind !== "region").map((u) => u.id),
|
|
854
|
+
[myUnitIds, units]
|
|
855
|
+
);
|
|
856
|
+
const groups = React.useMemo(() => {
|
|
857
|
+
const out = [];
|
|
858
|
+
if (reachable.length > 1) {
|
|
859
|
+
out.push({ id: "_all", options: [{ id: ALL_UNITS, label: "Todas as unidades" }] });
|
|
860
|
+
}
|
|
861
|
+
const collect = (nodes, bucket) => {
|
|
862
|
+
for (const n of nodes) {
|
|
863
|
+
if (n.kind !== "region" && reachable.includes(n.id)) {
|
|
864
|
+
bucket.push({
|
|
865
|
+
id: n.id,
|
|
866
|
+
label: n.name,
|
|
867
|
+
sublabel: n.city ?? null,
|
|
868
|
+
hint: n.isHeadquarters ? "matriz" : null,
|
|
869
|
+
keywords: [n.code ?? "", n.city ?? ""].filter(Boolean)
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
collect(n.children, bucket);
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
for (const root of tree) {
|
|
876
|
+
if (root.kind === "region") {
|
|
877
|
+
const options = [];
|
|
878
|
+
collect(root.children, options);
|
|
879
|
+
if (options.length > 0) out.push({ id: root.id, label: root.name, options });
|
|
880
|
+
} else {
|
|
881
|
+
const options = [];
|
|
882
|
+
collect([root], options);
|
|
883
|
+
if (options.length > 0) out.push({ id: root.id, options });
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return out;
|
|
887
|
+
}, [tree, reachable]);
|
|
888
|
+
if (!unitMode) return null;
|
|
889
|
+
return /* @__PURE__ */ jsx(
|
|
890
|
+
PickerDialog,
|
|
891
|
+
{
|
|
892
|
+
open,
|
|
893
|
+
onOpenChange: setOpen,
|
|
894
|
+
title: "Trocar de unidade",
|
|
895
|
+
description: "Digite para buscar, use \u2191 \u2193 para navegar.",
|
|
896
|
+
searchPlaceholder: "Buscar unidade\u2026",
|
|
897
|
+
emptyLabel: "Nenhuma unidade com esse nome.",
|
|
898
|
+
searchThreshold: 6,
|
|
899
|
+
groups,
|
|
900
|
+
value: activeUnitId ?? ALL_UNITS,
|
|
901
|
+
onSelect: (id) => {
|
|
902
|
+
if (id === ALL_UNITS) {
|
|
903
|
+
if (activeUnitId !== void 0) beginSwitch({ unitId: void 0, unitName: "Todas as unidades" });
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (id !== activeUnitId) beginSwitch({ unitId: id, unitName: byId[id]?.name ?? "" });
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
var MENU_LIST_CAP = 5;
|
|
912
|
+
function UnitMenuSection() {
|
|
913
|
+
useUnitBootstrap();
|
|
914
|
+
const [locating, setLocating] = React.useState(false);
|
|
915
|
+
const unitMode = useUnitStore((s) => s.unitMode);
|
|
916
|
+
const units = useUnitStore((s) => s.units);
|
|
917
|
+
const tree = useUnitStore((s) => s.tree);
|
|
918
|
+
const byId = useUnitStore((s) => s.byId);
|
|
919
|
+
const myUnitIds = useUnitStore((s) => s.myUnitIds);
|
|
920
|
+
const activeUnitId = useUnitStore((s) => s.activeUnitId);
|
|
921
|
+
const needsChoice = useUnitStore((s) => s.needsChoice);
|
|
922
|
+
const beginSwitch = useUnitSwitchingStore((s) => s.begin);
|
|
923
|
+
if (!unitMode) return null;
|
|
924
|
+
const reachable = myUnitIds.length > 0 ? myUnitIds : units.filter((u) => u.kind !== "region").map((u) => u.id);
|
|
925
|
+
if (reachable.length === 0) return null;
|
|
926
|
+
const current = activeUnitId ? byId[activeUnitId] : void 0;
|
|
927
|
+
const currentLabel = current?.name ?? "Todas as unidades";
|
|
928
|
+
const nearestItem = needsChoice && canOfferNearest(units, reachable) && // O último recurso, e o único que pede alguma coisa. A entrada já tentou o
|
|
929
|
+
// hábito desta máquina, o desta pessoa e a aritmética de "só existe uma".
|
|
930
|
+
/* @__PURE__ */ jsxs(
|
|
931
|
+
DropdownItem,
|
|
932
|
+
{
|
|
933
|
+
disabled: locating,
|
|
934
|
+
onClick: async (e) => {
|
|
935
|
+
e.preventDefault();
|
|
936
|
+
setLocating(true);
|
|
937
|
+
const nearest = await findNearestUnit(units, reachable);
|
|
938
|
+
setLocating(false);
|
|
939
|
+
if (nearest) beginSwitch({ unitId: nearest.unitId, unitName: byId[nearest.unitId]?.name ?? "" });
|
|
940
|
+
},
|
|
941
|
+
className: "flex items-center gap-2",
|
|
942
|
+
children: [
|
|
943
|
+
locating ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 shrink-0 animate-spin text-muted-foreground" }) : /* @__PURE__ */ jsx(LocateFixed, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
944
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: locating ? "Procurando\u2026" : "A unidade mais perto de mim" })
|
|
945
|
+
]
|
|
946
|
+
}
|
|
947
|
+
);
|
|
948
|
+
if (reachable.length > MENU_LIST_CAP) {
|
|
949
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
950
|
+
/* @__PURE__ */ jsx(DropdownSeparator, {}),
|
|
951
|
+
/* @__PURE__ */ jsx(DropdownLabel, { children: "Unidades" }),
|
|
952
|
+
/* @__PURE__ */ jsxs(DropdownItem, { onClick: () => openUnitPicker(), className: "flex items-center gap-2", title: currentLabel, children: [
|
|
953
|
+
current ? /* @__PURE__ */ jsx(Building2, { className: "h-4 w-4 shrink-0 text-muted-foreground" }) : /* @__PURE__ */ jsx(Layers, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
954
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate font-medium", children: currentLabel }),
|
|
955
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-[10px] tabular-nums text-muted-foreground", children: reachable.length }),
|
|
956
|
+
/* @__PURE__ */ jsx(ChevronsUpDown, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" })
|
|
957
|
+
] }),
|
|
958
|
+
nearestItem
|
|
959
|
+
] });
|
|
960
|
+
}
|
|
961
|
+
const renderNode = (node, depth) => {
|
|
962
|
+
if (node.kind === "region") {
|
|
963
|
+
const visibleChildren = node.children.filter((c) => c.kind === "region" || reachable.includes(c.id));
|
|
964
|
+
if (visibleChildren.length === 0) return null;
|
|
965
|
+
return /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
966
|
+
/* @__PURE__ */ jsx(DropdownLabel, { className: "px-2 pt-2 text-[10px] uppercase tracking-wide text-muted-foreground", children: node.name }),
|
|
967
|
+
visibleChildren.map((child) => renderNode(child, depth + 1))
|
|
968
|
+
] }, node.id);
|
|
969
|
+
}
|
|
970
|
+
if (!reachable.includes(node.id)) return null;
|
|
971
|
+
return /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
972
|
+
/* @__PURE__ */ jsxs(
|
|
973
|
+
DropdownItem,
|
|
974
|
+
{
|
|
975
|
+
onClick: () => beginSwitch({ unitId: node.id, unitName: node.name }),
|
|
976
|
+
className: cn("flex items-center gap-2", depth > 0 && "pl-6"),
|
|
977
|
+
children: [
|
|
978
|
+
/* @__PURE__ */ jsx(Building2, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
979
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: node.name }),
|
|
980
|
+
node.isHeadquarters && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[10px] text-muted-foreground", children: "matriz" }),
|
|
981
|
+
activeUnitId === node.id && /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 shrink-0" })
|
|
982
|
+
]
|
|
983
|
+
}
|
|
984
|
+
),
|
|
985
|
+
node.children.map((child) => renderNode(child, depth + 1))
|
|
986
|
+
] }, node.id);
|
|
987
|
+
};
|
|
988
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
989
|
+
/* @__PURE__ */ jsx(DropdownSeparator, {}),
|
|
990
|
+
/* @__PURE__ */ jsx(DropdownLabel, { children: "Unidades" }),
|
|
991
|
+
reachable.length > 1 && /* @__PURE__ */ jsxs(DropdownItem, { onClick: () => beginSwitch({ unitId: void 0, unitName: "Todas as unidades" }), className: "flex items-center gap-2", children: [
|
|
992
|
+
/* @__PURE__ */ jsx(Layers, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
993
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: "Todas as unidades" }),
|
|
994
|
+
!activeUnitId && /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 shrink-0" })
|
|
995
|
+
] }),
|
|
996
|
+
nearestItem,
|
|
997
|
+
tree.map((node) => renderNode(node, 0))
|
|
998
|
+
] });
|
|
999
|
+
}
|
|
1000
|
+
function UnitIndicator({
|
|
1001
|
+
collapsed = false,
|
|
1002
|
+
inline = false
|
|
1003
|
+
}) {
|
|
1004
|
+
useUnitBootstrap();
|
|
1005
|
+
const unitMode = useUnitStore((s) => s.unitMode);
|
|
1006
|
+
const byId = useUnitStore((s) => s.byId);
|
|
1007
|
+
const myUnitIds = useUnitStore((s) => s.myUnitIds);
|
|
1008
|
+
const units = useUnitStore((s) => s.units);
|
|
1009
|
+
const activeUnitId = useUnitStore((s) => s.activeUnitId);
|
|
1010
|
+
if (!unitMode) return null;
|
|
1011
|
+
const reachable = myUnitIds.length > 0 ? myUnitIds : units.filter((u) => u.kind !== "region").map((u) => u.id);
|
|
1012
|
+
if (reachable.length === 0) return null;
|
|
1013
|
+
const current = activeUnitId ? byId[activeUnitId] : void 0;
|
|
1014
|
+
const text = current?.name ?? (reachable.length > 1 ? "Todas as unidades" : byId[reachable[0]]?.name ?? "");
|
|
1015
|
+
if (!text) return null;
|
|
1016
|
+
if (collapsed) {
|
|
1017
|
+
return /* @__PURE__ */ jsx("div", { className: "flex justify-center pt-0.5", title: text, children: /* @__PURE__ */ jsx(Building2, { className: "h-3 w-3 text-sidebar-muted" }) });
|
|
1018
|
+
}
|
|
1019
|
+
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1020
|
+
/* @__PURE__ */ jsx(Building2, { className: "h-3 w-3 shrink-0 text-sidebar-muted/70" }),
|
|
1021
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate text-[11px] leading-none text-sidebar-muted/90", children: text })
|
|
1022
|
+
] });
|
|
1023
|
+
if (inline) {
|
|
1024
|
+
return /* @__PURE__ */ jsx("span", { className: "mt-0.5 flex min-w-0 items-center gap-1", title: text, children: body });
|
|
1025
|
+
}
|
|
1026
|
+
return /* @__PURE__ */ jsx("div", { className: "flex min-w-0 items-center gap-1.5 pl-12 pr-1.5 pb-1", title: text, children: body });
|
|
1027
|
+
}
|
|
1028
|
+
var FALLBACK_TTL_SECONDS = 30 * 60;
|
|
1029
|
+
function reloadTo(path) {
|
|
1030
|
+
replaceApp(path);
|
|
1031
|
+
window.location.reload();
|
|
1032
|
+
}
|
|
1033
|
+
async function startImpersonation(input) {
|
|
1034
|
+
const supabase = getFayzSupabaseClient();
|
|
1035
|
+
const { data: current } = await supabase.auth.getSession();
|
|
1036
|
+
const origin = current.session;
|
|
1037
|
+
if (!origin?.refresh_token) throw new Error("Sua sess\xE3o expirou. Entre de novo.");
|
|
1038
|
+
const { data, error } = await supabase.functions.invoke("platform-admin", {
|
|
1039
|
+
body: { verb: "member.impersonate", params: { tenant_id: input.tenantId, user_id: input.userId, reason: input.reason } }
|
|
1040
|
+
});
|
|
1041
|
+
if (error) {
|
|
1042
|
+
const body = await error.context?.json?.().catch(() => null);
|
|
1043
|
+
throw new Error(body?.error ?? error.message);
|
|
1044
|
+
}
|
|
1045
|
+
const { token_hash, target, expires_in } = data;
|
|
1046
|
+
const now = Date.now();
|
|
1047
|
+
const person = { userId: target.user_id, email: target.email, name: target.name, role: target.role };
|
|
1048
|
+
writeImpersonation({
|
|
1049
|
+
tenantId: input.tenantId,
|
|
1050
|
+
target: person,
|
|
1051
|
+
startedAt: now,
|
|
1052
|
+
expiresAt: now + (expires_in ?? FALLBACK_TTL_SECONDS) * 1e3,
|
|
1053
|
+
returnTo: input.returnTo,
|
|
1054
|
+
origin: { access_token: origin.access_token, refresh_token: origin.refresh_token }
|
|
1055
|
+
});
|
|
1056
|
+
const { error: verifyErr } = await supabase.auth.verifyOtp({ type: "magiclink", token_hash });
|
|
1057
|
+
if (verifyErr) {
|
|
1058
|
+
writeImpersonation(null);
|
|
1059
|
+
await supabase.auth.setSession({ access_token: origin.access_token, refresh_token: origin.refresh_token });
|
|
1060
|
+
throw new Error(verifyErr.message);
|
|
1061
|
+
}
|
|
1062
|
+
markWorkspaceSwitch(input.tenantId);
|
|
1063
|
+
reloadTo("/");
|
|
1064
|
+
}
|
|
1065
|
+
async function stopImpersonation() {
|
|
1066
|
+
const state = readImpersonation();
|
|
1067
|
+
if (!state) return;
|
|
1068
|
+
const supabase = getFayzSupabaseClient();
|
|
1069
|
+
try {
|
|
1070
|
+
await supabase.auth.signOut({ scope: "local" });
|
|
1071
|
+
} catch {
|
|
1072
|
+
}
|
|
1073
|
+
writeImpersonation(null);
|
|
1074
|
+
const { error } = await supabase.auth.setSession(state.origin);
|
|
1075
|
+
markWorkspaceSwitch(state.tenantId);
|
|
1076
|
+
reloadTo(error ? "/login" : state.returnTo ?? "/settings/team");
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
export { MemberAccessRailPage, MemberUnitsCell, UNIT_DETAIL_ZONES, UnitIndicator, UnitMenuSection, UnitPickerDialog, UnitRailPage, UnitSwitcher, UnitSwitchingHost, UnitsSettingsPage, canOfferNearest, findNearestUnit, startImpersonation, stopImpersonation, useMemberAccessPanel };
|
|
1080
|
+
//# sourceMappingURL=chunk-5WR62X2R.js.map
|
|
1081
|
+
//# sourceMappingURL=chunk-5WR62X2R.js.map
|