PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
3.0.17 3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 All 85 releases
yatra / assets / dist / js / Team-M9W1qR00.js

Team-M9W1qR00.js in Yatra – Travel Booking & Tour Operator Software 3.0.17, at assets/dist/js/Team-M9W1qR00.js

3,035 lines 145.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { r as reactExports, u as useQuery, j as jsxRuntimeExports, U as Users, av as Shield, d as Mail, bK as History, l as Settings, bb as Lock, V as AlertTriangle, C as Crown, aE as CheckCircle2, X as ExternalLink, v as useQueryClient, w as useMutation, bV as UserPlus, bE as LogOut, aO as Trash2, bW as KeyRound, aH as Copy, b9 as XCircle, E as Loader2, a6 as React, bX as Unlock, aB as Check, bD as ShieldCheck } from "./react-vendor-CithbthY.js";
2 import { a as apiClient, _ as __, s as sprintf, c as brandName, u as useToast, I as Input } from "./index-Dec_weLB.js";
3 import { X as Skeleton, C as Card, d as CardContent, P as PageHeader, B as Button, f as CardHeader, g as CardTitle, h as CardDescription, S as Select, Y as Table, e as Badge, Z as Tooltip, k as ConfirmationDialog, M as Modal, _ as Pagination, A as Alert, s as Label, $ as Switch, a0 as setUserCaps } from "../../admin/dist/js/app.js";
4 const teamApi = {
5 getMeta: () => apiClient.get("/team/meta"),
6 listCapabilities: () => apiClient.get("/team/capabilities"),
7 listRoles: () => apiClient.get("/team/roles"),
8 createRole: (payload) => apiClient.post("/team/roles", payload),
9 getRole: (slug) => apiClient.get(`/team/roles/${encodeURIComponent(slug)}`),
10 updateRole: (slug, payload) => apiClient.put(
11 `/team/roles/${encodeURIComponent(slug)}`,
12 payload
13 ),
14 deleteRole: (slug, reassignTo = null) => apiClient.delete(
15 `/team/roles/${encodeURIComponent(slug)}`,
16 reassignTo ? { data: { reassign_to: reassignTo } } : void 0
17 ),
18 listUsers: () => apiClient.get("/team/users"),
19 /** WP users not yet on the Yatra team — for the "Add existing user"
20 * picker. `q` is an optional type-ahead query (search by login /
21 * email / display name). */
22 listAvailableUsers: (q = "", limit = 50) => apiClient.get("/team/users/available", {
23 params: q ? { q, limit } : { limit }
24 }),
25 /** Create a brand-new WP user + assign a Yatra role in one shot.
26 * Sister flow to invitations — invitations send an email that
27 * creates the user on accept; this one provisions immediately. */
28 createUser: (payload) => apiClient.post("/team/users/create", payload),
29 getUser: (id) => apiClient.get(`/team/users/${id}`),
30 updateUser: (id, payload) => apiClient.put(`/team/users/${id}`, payload),
31 forceLogout: (id) => apiClient.post(`/team/users/${id}/force-logout`, {}),
32 removeUser: (id) => apiClient.delete(`/team/users/${id}`),
33 /**
34 * Bulk operations on members.
35 *
36 * Response shape includes per-id success/failure so the UI can
37 * highlight rows that didn't complete (e.g. last-admin guard
38 * blocking a remove). `ok_count` is the convenience aggregate.
39 */
40 /** Curated role-creation templates served read-only from the server. */
41 listRoleTemplates: () => apiClient.get("/team/role-templates"),
42 /**
43 * Module-level settings. Currently surfaces a single forward-looking
44 * toggle: what should happen to non-admin team members' Yatra access
45 * if the operator ever turns the Team & Access module off?
46 *
47 * Returns an object (not a flat boolean) so future settings can be
48 * added without rev-bumping the response shape.
49 */
50 getSettings: () => apiClient.get("/team/settings"),
51 /** Partial update — every key is optional. Backend persists only
52 * the keys present and writes an audit row when the diff is
53 * meaningful (e.g. login_ip_allowlist normalizes to a different
54 * list of CIDRs). */
55 updateSettings: (payload) => apiClient.put("/team/settings", payload),
56 bulkUsers: (payload) => apiClient.post("/team/users/bulk", payload),
57 listInvitations: () => apiClient.get("/team/invitations"),
58 sendInvitation: (payload) => apiClient.post("/team/invitations", payload),
59 /**
60 * Revoke or delete an invitation.
61 *
62 * purge=false (default): the invitation must be `pending`. Marks
63 * it `revoked` (the magic-link token is invalidated) and keeps
64 * the row so the operator can still see it in the list.
65 * purge=true: deletes the row entirely. If the invitation was
66 * still pending, it's revoked first so the token can never be
67 * redeemed after the record is gone.
68 */
69 revokeInvitation: (id, opts = {}) => apiClient.delete(
70 `/team/invitations/${encodeURIComponent(id)}${opts.purge ? "?purge=1" : ""}`
71 ),
72 acceptInvitation: (token) => apiClient.post("/team/invitations/accept", { token }),
73 listAuditLog: (filters = {}) => apiClient.get("/team/audit-log", { params: filters }),
74 auditFacets: () => apiClient.get("/team/audit-log/facets"),
75 /**
76 * Wipe every audit-log entry. Server requires `confirm: true` in the
77 * body as a safety belt against accidental DELETEs. The wipe itself
78 * is recorded as a new audit entry so the operator who cleared
79 * history is documented.
80 */
81 clearAuditLog: () => apiClient.delete("/team/audit-log", {
82 data: { confirm: true }
83 }),
84 /**
85 * Delete a specific set of audit-log rows by id. Server caps at 500
86 * ids per request. The deletion is recorded as a new audit entry.
87 */
88 bulkDeleteAuditLog: (ids) => apiClient.post("/team/audit-log/bulk-delete", { ids })
89 };
90 const extractError = (e) => {
91 var _a, _b;
92 return ((_b = (_a = e == null ? void 0 : e.response) == null ? void 0 : _a.data) == null ? void 0 : _b.message) || (e == null ? void 0 : e.message) || __("Something went wrong.", "yatra");
93 };
94 const getInitialTab = () => {
95 if (typeof window === "undefined") return "members";
96 const tab = new URLSearchParams(window.location.search).get("tab");
97 if (tab === "roles" || tab === "invitations" || tab === "audit" || tab === "settings")
98 return tab;
99 return "members";
100 };
101 const Team = () => {
102 var _a;
103 const [activeTab, setActiveTab] = reactExports.useState(() => getInitialTab());
104 const switchTab = (next) => {
105 setActiveTab(next);
106 if (typeof window !== "undefined") {
107 const url = new URL(window.location.href);
108 url.searchParams.set("tab", next);
109 window.history.replaceState({}, "", url.toString());
110 }
111 };
112 const { data: meta, isLoading: metaLoading } = useQuery({
113 queryKey: ["team-meta"],
114 queryFn: () => teamApi.getMeta()
115 });
116 const { data: settingsData } = useQuery({
117 queryKey: ["team-settings"],
118 queryFn: () => teamApi.getSettings(),
119 initialData: () => {
120 var _a2;
121 const seed = (_a2 = window.yatraAdmin) == null ? void 0 : _a2.teamKeepAccessOnModuleDisable;
122 return {
123 data: { keep_access_on_module_disable: seed === true }
124 };
125 },
126 // Don't fire the network request until the module is actually
127 // enabled — the /team/settings endpoint requires Team & Access
128 // to be active. Initial data still serves the banner during
129 // loading + non-agency states.
130 enabled: (meta == null ? void 0 : meta.is_module_enabled) === true
131 });
132 const keepAccessOnDisable = ((_a = settingsData == null ? void 0 : settingsData.data) == null ? void 0 : _a.keep_access_on_module_disable) === true;
133 if (metaLoading) {
134 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
135 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
136 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-6 w-1/3" }),
137 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-4 w-2/3" })
138 ] }),
139 /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
140 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-b border-gray-200 dark:border-gray-700 px-4 py-3 flex gap-6", children: [0, 1, 2, 3].map((i) => /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-5 w-24" }, i)) }),
141 /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "space-y-3 pt-4", children: [0, 1, 2, 3, 4].map((i) => /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-12 w-full" }, i)) })
142 ] })
143 ] });
144 }
145 if (!meta) return null;
146 if (!meta.is_agency_active) {
147 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
148 /* @__PURE__ */ jsxRuntimeExports.jsx(
149 PageHeader,
150 {
151 description: __(
152 "Granular roles, scoped access, magic-link invitations, and a tamper-evident audit log.",
153 "yatra"
154 )
155 }
156 ),
157 /* @__PURE__ */ jsxRuntimeExports.jsx(UpgradeCard, { meta })
158 ] });
159 }
160 if (!meta.is_module_enabled) {
161 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
162 /* @__PURE__ */ jsxRuntimeExports.jsx(
163 PageHeader,
164 {
165 description: __(
166 "Granular roles, scoped access, magic-link invitations, and a tamper-evident audit log.",
167 "yatra"
168 )
169 }
170 ),
171 /* @__PURE__ */ jsxRuntimeExports.jsx(ModulePrompt, {})
172 ] });
173 }
174 const tabs = [
175 { key: "members", label: __("Members", "yatra"), icon: Users },
176 { key: "roles", label: __("Roles", "yatra"), icon: Shield },
177 { key: "invitations", label: __("Invitations", "yatra"), icon: Mail },
178 { key: "audit", label: __("Audit log", "yatra"), icon: History },
179 { key: "settings", label: __("Settings", "yatra"), icon: Settings }
180 ];
181 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
182 /* @__PURE__ */ jsxRuntimeExports.jsx(
183 PageHeader,
184 {
185 description: __(
186 "Granular roles + capability-based access for multi-staff agencies. Defense-in-depth — every action gated on the server, the UI mirrors via cap-aware controls.",
187 "yatra"
188 )
189 }
190 ),
191 keepAccessOnDisable ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-md border border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/40 p-4 flex flex-wrap items-start gap-3", children: [
192 /* @__PURE__ */ jsxRuntimeExports.jsx(Lock, { className: "w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" }),
193 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [
194 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-sm font-semibold text-blue-900 dark:text-blue-100", children: __(
195 "Your team will keep access if you ever turn off this module",
196 "yatra"
197 ) }),
198 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-blue-800 dark:text-blue-200/90 mt-0.5", children: __(
199 "You've opted in to preserve team access when the module is disabled. Turning off Team & Access in Yatra → Modules will pause its advanced features (expiry, scopes, audit log) but your team members keep their roles and current access. You can change this in Settings.",
200 "yatra"
201 ) })
202 ] }),
203 activeTab !== "settings" && /* @__PURE__ */ jsxRuntimeExports.jsx(
204 Button,
205 {
206 type: "button",
207 variant: "outline",
208 onClick: () => switchTab("settings"),
209 className: "border-blue-300 text-blue-900 hover:bg-blue-100 dark:border-blue-700 dark:text-blue-100 dark:hover:bg-blue-900",
210 children: __("Open Settings", "yatra")
211 }
212 )
213 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-md border border-red-300 bg-red-50 dark:border-red-800 dark:bg-red-950/40 p-4 flex flex-wrap items-start gap-3", children: [
214 /* @__PURE__ */ jsxRuntimeExports.jsx(AlertTriangle, { className: "w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" }),
215 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [
216 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-sm font-semibold text-red-900 dark:text-red-100", children: __(
217 "Heads up — turning off this module will revoke all team access",
218 "yatra"
219 ) }),
220 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-red-800 dark:text-red-200/90 mt-0.5", children: __(
221 "This is the default. If you ever disable Team & Access in Yatra → Modules, every Yatra role on your site (Owner, Manager, Sales Agent, plus any custom roles you built) will be removed and your team members will lose all Yatra access. Re-enabling brings back the 8 built-in roles only — custom roles and member assignments don't come back. Switch the setting ON in Settings if you'd rather keep your team's access.",
222 "yatra"
223 ) })
224 ] }),
225 activeTab !== "settings" && /* @__PURE__ */ jsxRuntimeExports.jsx(
226 Button,
227 {
228 type: "button",
229 variant: "outline",
230 onClick: () => switchTab("settings"),
231 className: "border-red-300 text-red-900 hover:bg-red-100 dark:border-red-700 dark:text-red-100 dark:hover:bg-red-900",
232 children: __("Open Settings", "yatra")
233 }
234 )
235 ] }),
236 /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
237 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-b border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ jsxRuntimeExports.jsx("nav", { className: "flex flex-wrap gap-1 px-4", children: tabs.map((tab) => {
238 const Icon = tab.icon;
239 const active = activeTab === tab.key;
240 return /* @__PURE__ */ jsxRuntimeExports.jsxs(
241 "button",
242 {
243 type: "button",
244 onClick: () => switchTab(tab.key),
245 className: `flex items-center gap-2 px-4 py-3 border-b-2 font-medium text-sm transition-colors ${active ? "border-blue-500 text-blue-600 dark:text-blue-400" : "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600"}`,
246 children: [
247 /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { className: "w-4 h-4" }),
248 tab.label
249 ]
250 },
251 tab.key
252 );
253 }) }) }),
254 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "p-6", children: [
255 activeTab === "members" && /* @__PURE__ */ jsxRuntimeExports.jsx(MembersTab, {}),
256 activeTab === "roles" && /* @__PURE__ */ jsxRuntimeExports.jsx(RolesTab, {}),
257 activeTab === "invitations" && /* @__PURE__ */ jsxRuntimeExports.jsx(InvitationsTab, {}),
258 activeTab === "audit" && /* @__PURE__ */ jsxRuntimeExports.jsx(AuditLogTab, {}),
259 activeTab === "settings" && /* @__PURE__ */ jsxRuntimeExports.jsx(SettingsTab, {})
260 ] })
261 ] })
262 ] });
263 };
264 const UpgradeCard = ({ meta }) => /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { className: "max-w-3xl", children: [
265 /* @__PURE__ */ jsxRuntimeExports.jsx(CardHeader, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-3", children: [
266 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "rounded-md bg-purple-100 p-2 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Crown, { className: "h-5 w-5" }) }),
267 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [
268 /* @__PURE__ */ jsxRuntimeExports.jsx(CardTitle, { children: __("Team & Access", "yatra") }),
269 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { className: "mt-1", children: __(
270 "Available on the Scale plan. Add staff with role-appropriate access — front desk doesn't see refund history, guides see only their destinations, accountants get financial reports without booking edits.",
271 "yatra"
272 ) })
273 ] })
274 ] }) }),
275 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-4", children: [
276 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [
277 [
278 __("8 shipped roles + custom builder", "yatra"),
279 __(
280 "Owner, Manager, Sales Agent, Front Desk, Guide, Accountant, Marketing, Auditor.",
281 "yatra"
282 )
283 ],
284 [
285 __("Per-user scope assignment", "yatra"),
286 __("Restrict by destination, activity, or trip.", "yatra")
287 ],
288 [
289 __("Magic-link invitations", "yatra"),
290 __("Invite by email — no manual user creation.", "yatra")
291 ],
292 [
293 __("180-day audit log", "yatra"),
294 __("Every sensitive action recorded, exportable to CSV.", "yatra")
295 ]
296 ].map(([title, sub]) => /* @__PURE__ */ jsxRuntimeExports.jsx(
297 "div",
298 {
299 className: "rounded-md border border-gray-200 dark:border-gray-700 p-3",
300 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-2", children: [
301 /* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" }),
302 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
303 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-medium text-gray-900 dark:text-white", children: title }),
304 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: sub })
305 ] })
306 ] })
307 },
308 title
309 )) }),
310 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 pt-2", children: [
311 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("a", { href: meta.upgrade_url, target: "_blank", rel: "noopener noreferrer", children: [
312 __("Upgrade to Scale", "yatra"),
313 /* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { className: "ml-1.5 h-4 w-4" })
314 ] }) }),
315 meta.docs_url && /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: meta.docs_url, target: "_blank", rel: "noopener noreferrer", children: __("Read the docs", "yatra") }) })
316 ] })
317 ] })
318 ] });
319 const ModulePrompt = () => /* @__PURE__ */ jsxRuntimeExports.jsx(Card, { className: "max-w-3xl", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
320 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, { className: "flex items-center gap-2", children: [
321 /* @__PURE__ */ jsxRuntimeExports.jsx(Users, { className: "h-5 w-5 text-blue-500" }),
322 __("Enable the Team & Access module", "yatra")
323 ] }),
324 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: sprintf(
325 /* translators: %s: brand name (e.g. "Yatra" or an operator's white-labeled brand) */
326 __(
327 'Toggle "Team & Access" on under %s → Modules to start configuring roles and members.',
328 "yatra"
329 ),
330 brandName()
331 ) })
332 ] }) });
333 const unixToLocalInputValue = (unixSec) => {
334 if (!unixSec || unixSec <= 0) return "";
335 const d = new Date(unixSec * 1e3);
336 const pad = (n) => n.toString().padStart(2, "0");
337 return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) + "T" + pad(d.getHours()) + ":" + pad(d.getMinutes());
338 };
339 const AccessExpiryCell = ({ user }) => {
340 if (!user.expires_at || user.expires_at <= 0) {
341 return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-gray-400", children: __("Permanent", "yatra") });
342 }
343 const now = Math.floor(Date.now() / 1e3);
344 const secondsRemaining = user.expires_at - now;
345 const daysRemaining = Math.ceil(secondsRemaining / 86400);
346 const formatted = new Date(user.expires_at * 1e3).toLocaleString();
347 if (secondsRemaining <= 0) {
348 return /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { content: formatted, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { className: "bg-red-50 text-red-700 dark:bg-red-900/30 dark:text-red-300 border border-red-200 dark:border-red-800", children: __("Expired", "yatra") }) });
349 }
350 if (daysRemaining <= 7) {
351 return /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { content: formatted, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { className: "bg-amber-50 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300 border border-amber-200 dark:border-amber-800", children: sprintf(
352 /* translators: %d: number of days */
353 __("Expires in %d day(s)", "yatra"),
354 daysRemaining
355 ) }) });
356 }
357 return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap", children: formatted });
358 };
359 const ExpiryNowHint = ({
360 expiresLocal
361 }) => {
362 const [now, setNow] = reactExports.useState(() => /* @__PURE__ */ new Date());
363 React.useEffect(() => {
364 const id = window.setInterval(() => setNow(/* @__PURE__ */ new Date()), 6e4);
365 return () => window.clearInterval(id);
366 }, []);
367 const nowFormatted = now.toLocaleString();
368 let relative = "";
369 if (expiresLocal) {
370 const target = new Date(expiresLocal).getTime();
371 const diffMs = target - now.getTime();
372 const absMin = Math.abs(Math.round(diffMs / 6e4));
373 if (diffMs < 0) {
374 relative = __("(in the past)", "yatra");
375 } else if (absMin < 60) {
376 relative = sprintf(
377 /* translators: %d: minutes from now */
378 __("in %d min", "yatra"),
379 absMin
380 );
381 } else if (absMin < 60 * 24) {
382 relative = sprintf(
383 /* translators: %d: hours from now */
384 __("in %d hour(s)", "yatra"),
385 Math.round(absMin / 60)
386 );
387 } else {
388 relative = sprintf(
389 /* translators: %d: days from now */
390 __("in %d day(s)", "yatra"),
391 Math.round(absMin / (60 * 24))
392 );
393 }
394 }
395 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-gray-500 dark:text-gray-400", children: [
396 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: sprintf(
397 /* translators: %s: formatted current local datetime */
398 __("Current time: %s", "yatra"),
399 nowFormatted
400 ) }),
401 relative && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-blue-600 dark:text-blue-400", children: sprintf(
402 /* translators: %s: relative offset like "in 3 days" */
403 __("Expires %s", "yatra"),
404 relative
405 ) })
406 ] });
407 };
408 const MembersTab = () => {
409 var _a;
410 const queryClient = useQueryClient();
411 const { showToast } = useToast();
412 const [editingId, setEditingId] = reactExports.useState(null);
413 const [pendingRemove, setPendingRemove] = reactExports.useState(null);
414 const [showAddModal, setShowAddModal] = reactExports.useState(false);
415 const [showCreateModal, setShowCreateModal] = reactExports.useState(false);
416 const [selectedIds, setSelectedIds] = reactExports.useState(/* @__PURE__ */ new Set());
417 const [pendingBulk, setPendingBulk] = reactExports.useState(null);
418 const { data, isLoading } = useQuery({
419 queryKey: ["team-users"],
420 queryFn: () => teamApi.listUsers()
421 });
422 const { data: rolesData } = useQuery({
423 queryKey: ["team-roles"],
424 queryFn: () => teamApi.listRoles()
425 });
426 const removeMutation = useMutation({
427 mutationFn: (id) => teamApi.removeUser(id),
428 onSuccess: () => {
429 queryClient.invalidateQueries({ queryKey: ["team-users"] });
430 showToast(__("Member removed.", "yatra"), "success");
431 setPendingRemove(null);
432 },
433 onError: (e) => {
434 showToast(extractError(e), "error");
435 setPendingRemove(null);
436 }
437 });
438 const forceLogoutMutation = useMutation({
439 mutationFn: (id) => teamApi.forceLogout(id),
440 onSuccess: () => showToast(__("All sessions invalidated.", "yatra"), "success"),
441 onError: (e) => showToast(extractError(e), "error")
442 });
443 const bulkMutation = useMutation({
444 mutationFn: (payload) => teamApi.bulkUsers(payload),
445 onSuccess: (res) => {
446 queryClient.invalidateQueries({ queryKey: ["team-users"] });
447 setSelectedIds(/* @__PURE__ */ new Set());
448 setPendingBulk(null);
449 if (res.data.fail_count > 0) {
450 showToast(
451 sprintf(
452 /* translators: 1: ok count, 2: fail count */
453 __("Bulk done: %1$d succeeded, %2$d failed.", "yatra"),
454 res.data.ok_count,
455 res.data.fail_count
456 ),
457 "warning"
458 );
459 } else {
460 showToast(
461 res.message || __("Bulk action complete.", "yatra"),
462 "success"
463 );
464 }
465 },
466 onError: (e) => {
467 showToast(extractError(e), "error");
468 setPendingBulk(null);
469 }
470 });
471 const users = (data == null ? void 0 : data.data) ?? [];
472 const currentUserId = (_a = window.yatraAdmin) == null ? void 0 : _a.currentUser;
473 const toggleOne = (id, checked) => {
474 setSelectedIds((prev) => {
475 const next = new Set(prev);
476 if (checked) next.add(id);
477 else next.delete(id);
478 return next;
479 });
480 };
481 const toggleAll = (checked) => {
482 if (!checked) {
483 setSelectedIds(/* @__PURE__ */ new Set());
484 return;
485 }
486 const next = /* @__PURE__ */ new Set();
487 users.forEach((u) => {
488 if (u.id !== currentUserId) next.add(u.id);
489 });
490 setSelectedIds(next);
491 };
492 const roleLabels = reactExports.useMemo(() => {
493 const map = {};
494 ((rolesData == null ? void 0 : rolesData.data) ?? []).forEach((r) => {
495 map[r.slug] = r.display_name;
496 });
497 return map;
498 }, [rolesData]);
499 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
500 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-3 flex-wrap", children: [
501 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
502 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: __("Team members", "yatra") }),
503 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: sprintf(
504 /* translators: %s: brand name */
505 __(
506 "Every WordPress user with a %s role. Add an existing WP user here, or send a magic-link invitation from the Invitations tab.",
507 "yatra"
508 ),
509 brandName()
510 ) })
511 ] }),
512 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap gap-2", children: [
513 /* @__PURE__ */ jsxRuntimeExports.jsxs(Button, { variant: "outline", onClick: () => setShowAddModal(true), children: [
514 /* @__PURE__ */ jsxRuntimeExports.jsx(UserPlus, { className: "mr-1.5 h-4 w-4" }),
515 __("Add existing WP user", "yatra")
516 ] }),
517 /* @__PURE__ */ jsxRuntimeExports.jsxs(Button, { onClick: () => setShowCreateModal(true), children: [
518 /* @__PURE__ */ jsxRuntimeExports.jsx(UserPlus, { className: "mr-1.5 h-4 w-4" }),
519 __("Create new user", "yatra")
520 ] })
521 ] })
522 ] }),
523 selectedIds.size > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-2 rounded-md border border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/30 p-3", children: [
524 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-blue-900 dark:text-blue-100 font-medium mr-auto", children: sprintf(
525 /* translators: %d: count of selected rows */
526 __("%d member(s) selected", "yatra"),
527 selectedIds.size
528 ) }),
529 /* @__PURE__ */ jsxRuntimeExports.jsxs(
530 Select,
531 {
532 "aria-label": __("Bulk: change role to", "yatra"),
533 defaultValue: "",
534 onChange: (e) => {
535 const role = e.target.value;
536 if (!role) return;
537 setPendingBulk({ action: "change_role", roleSlug: role });
538 e.target.value = "";
539 },
540 children: [
541 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: __("Change role to…", "yatra") }),
542 ((rolesData == null ? void 0 : rolesData.data) ?? []).map((r) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: r.slug, children: r.display_name }, r.slug))
543 ]
544 }
545 ),
546 /* @__PURE__ */ jsxRuntimeExports.jsxs(
547 Button,
548 {
549 type: "button",
550 variant: "outline",
551 onClick: () => setPendingBulk({ action: "force_logout" }),
552 disabled: bulkMutation.isPending,
553 children: [
554 /* @__PURE__ */ jsxRuntimeExports.jsx(LogOut, { className: "mr-1.5 h-4 w-4" }),
555 __("Force logout", "yatra")
556 ]
557 }
558 ),
559 /* @__PURE__ */ jsxRuntimeExports.jsxs(
560 Button,
561 {
562 type: "button",
563 variant: "destructive",
564 onClick: () => setPendingBulk({ action: "remove" }),
565 disabled: bulkMutation.isPending,
566 children: [
567 /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "mr-1.5 h-4 w-4" }),
568 __("Remove", "yatra")
569 ]
570 }
571 ),
572 /* @__PURE__ */ jsxRuntimeExports.jsx(
573 Button,
574 {
575 type: "button",
576 variant: "outline",
577 onClick: () => setSelectedIds(/* @__PURE__ */ new Set()),
578 children: __("Clear", "yatra")
579 }
580 )
581 ] }),
582 /* @__PURE__ */ jsxRuntimeExports.jsx(Card, { className: "overflow-visible", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "p-0 overflow-visible", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
583 Table,
584 {
585 data: users,
586 selectedItemIds: Array.from(selectedIds),
587 onSelectItem: (id, checked) => toggleOne(Number(id), checked),
588 onSelectAll: toggleAll,
589 isAllSelected: users.length > 0 && users.filter((u) => u.id !== currentUserId).every((u) => selectedIds.has(u.id)),
590 getItemId: (u) => u.id,
591 columns: [
592 {
593 key: "display_name",
594 label: __("Member", "yatra"),
595 render: (u) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [
596 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-semibold text-blue-600 dark:text-blue-400", children: (u.display_name || u.user_login).charAt(0).toUpperCase() }) }),
597 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0", children: [
598 /* @__PURE__ */ jsxRuntimeExports.jsx(
599 "button",
600 {
601 type: "button",
602 onClick: () => setEditingId(u.id),
603 className: "font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline transition-colors cursor-pointer text-left",
604 children: u.display_name || u.user_login
605 }
606 ),
607 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5 truncate max-w-md", children: u.email }),
608 u.is_wp_admin && /* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, { className: "mt-1 bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300 text-[10px]", children: [
609 /* @__PURE__ */ jsxRuntimeExports.jsx(Crown, { className: "w-2.5 h-2.5 mr-1" }),
610 __("WP Admin", "yatra")
611 ] })
612 ] })
613 ] })
614 },
615 {
616 key: "primary_role",
617 label: __("Role", "yatra"),
618 render: (u) => u.primary_role ? /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { className: "bg-indigo-50 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800", children: roleLabels[u.primary_role] || u.primary_role }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-gray-400", children: "—" })
619 },
620 {
621 key: "scope",
622 label: __("Scope", "yatra"),
623 render: (u) => u.has_scope ? /* @__PURE__ */ jsxRuntimeExports.jsx(
624 Tooltip,
625 {
626 content: /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
627 u.scopes.destinations.length > 0 && `${u.scopes.destinations.length} dest.`,
628 " ",
629 u.scopes.activities.length > 0 && `${u.scopes.activities.length} act.`,
630 " ",
631 u.scopes.trips.length > 0 && `${u.scopes.trips.length} trips`,
632 " ",
633 u.scopes.categories.length > 0 && `${u.scopes.categories.length} cat.`
634 ] }),
635 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { className: "bg-amber-50 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300", children: __("Scoped", "yatra") })
636 }
637 ) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-gray-400", children: __("Unrestricted", "yatra") })
638 },
639 {
640 key: "last_login",
641 label: __("Last login", "yatra"),
642 render: (u) => u.last_login ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap", children: new Date(u.last_login).toLocaleString() }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-gray-400", children: "—" })
643 },
644 {
645 key: "expires_at",
646 label: __("Access expires", "yatra"),
647 render: (u) => /* @__PURE__ */ jsxRuntimeExports.jsx(AccessExpiryCell, { user: u })
648 }
649 ],
650 actions: [
651 {
652 key: "edit",
653 label: __("Edit access", "yatra"),
654 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(KeyRound, { className: "w-4 h-4" }),
655 onClick: (u) => setEditingId(u.id)
656 },
657 {
658 key: "logout",
659 label: __("Force logout", "yatra"),
660 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(LogOut, { className: "w-4 h-4" }),
661 onClick: (u) => forceLogoutMutation.mutate(u.id)
662 },
663 {
664 key: "remove",
665 label: __("Remove from team", "yatra"),
666 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "w-4 h-4" }),
667 onClick: (u) => setPendingRemove(u),
668 variant: "destructive"
669 }
670 ],
671 isLoading,
672 emptyText: __("No team members yet", "yatra"),
673 emptyDescription: __(
674 "Invite a teammate from the Invitations tab to get started.",
675 "yatra"
676 )
677 }
678 ) }) }),
679 editingId !== null && /* @__PURE__ */ jsxRuntimeExports.jsx(
680 MemberEditDrawer,
681 {
682 userId: editingId,
683 onClose: () => setEditingId(null)
684 }
685 ),
686 showAddModal && /* @__PURE__ */ jsxRuntimeExports.jsx(AddMemberModal, { onClose: () => setShowAddModal(false) }),
687 showCreateModal && /* @__PURE__ */ jsxRuntimeExports.jsx(CreateUserModal, { onClose: () => setShowCreateModal(false) }),
688 /* @__PURE__ */ jsxRuntimeExports.jsx(
689 ConfirmationDialog,
690 {
691 isOpen: pendingRemove !== null,
692 onClose: () => !removeMutation.isPending && setPendingRemove(null),
693 onConfirm: () => pendingRemove && removeMutation.mutate(pendingRemove.id),
694 title: sprintf(
695 /* translators: %s: brand name */
696 __("Remove member from %s?", "yatra"),
697 brandName()
698 ),
699 description: pendingRemove ? sprintf(
700 /* translators: 1: brand name, 2: member display name, 3: brand name */
701 __(
702 'This strips %1$s role + caps + scopes from "%2$s". Their WordPress user is preserved (they keep any non-%3$s access on this site).',
703 "yatra"
704 ),
705 brandName(),
706 pendingRemove.display_name,
707 brandName()
708 ) : "",
709 confirmText: __("Remove access", "yatra"),
710 cancelText: __("Cancel", "yatra"),
711 variant: "danger",
712 isLoading: removeMutation.isPending
713 }
714 ),
715 /* @__PURE__ */ jsxRuntimeExports.jsx(
716 ConfirmationDialog,
717 {
718 isOpen: pendingBulk !== null,
719 onClose: () => !bulkMutation.isPending && setPendingBulk(null),
720 onConfirm: () => {
721 if (!pendingBulk) return;
722 const ids = Array.from(selectedIds);
723 if (pendingBulk.action === "change_role") {
724 bulkMutation.mutate({
725 action: "change_role",
726 user_ids: ids,
727 role_slug: pendingBulk.roleSlug
728 });
729 } else if (pendingBulk.action === "remove") {
730 bulkMutation.mutate({ action: "remove", user_ids: ids });
731 } else if (pendingBulk.action === "force_logout") {
732 bulkMutation.mutate({ action: "force_logout", user_ids: ids });
733 }
734 },
735 title: (() => {
736 if (!pendingBulk) return "";
737 if (pendingBulk.action === "change_role") {
738 return sprintf(
739 /* translators: %d: count */
740 __("Change role on %d member(s)?", "yatra"),
741 selectedIds.size
742 );
743 }
744 if (pendingBulk.action === "remove") {
745 return sprintf(
746 /* translators: %d: count */
747 __("Remove %d member(s) from the team?", "yatra"),
748 selectedIds.size
749 );
750 }
751 return sprintf(
752 /* translators: %d: count */
753 __("Force logout on %d member(s)?", "yatra"),
754 selectedIds.size
755 );
756 })(),
757 description: (() => {
758 var _a2;
759 if (!pendingBulk) return "";
760 if (pendingBulk.action === "change_role") {
761 const roleLabel = ((_a2 = ((rolesData == null ? void 0 : rolesData.data) ?? []).find(
762 (r) => r.slug === pendingBulk.roleSlug
763 )) == null ? void 0 : _a2.display_name) ?? pendingBulk.roleSlug;
764 return sprintf(
765 /* translators: 1: role label, 2: count */
766 __(
767 'Sets the role to "%1$s" on %2$d member(s). Existing per-user grants and scopes are preserved. The last team administrator cannot be demoted — failures are reported per-id.',
768 "yatra"
769 ),
770 roleLabel ?? "",
771 selectedIds.size
772 );
773 }
774 if (pendingBulk.action === "remove") {
775 return __(
776 "Strips role + caps + scopes from each selected member. Their WordPress user accounts stay. Last-team-admin is protected — that row will report as failed.",
777 "yatra"
778 );
779 }
780 return __(
781 "Invalidates every active session for the selected members. They will be logged out everywhere and need to re-authenticate.",
782 "yatra"
783 );
784 })(),
785 confirmText: (pendingBulk == null ? void 0 : pendingBulk.action) === "change_role" ? __("Apply role", "yatra") : (pendingBulk == null ? void 0 : pendingBulk.action) === "remove" ? __("Remove access", "yatra") : __("Force logout", "yatra"),
786 cancelText: __("Cancel", "yatra"),
787 variant: (pendingBulk == null ? void 0 : pendingBulk.action) === "remove" ? "danger" : "info",
788 isLoading: bulkMutation.isPending
789 }
790 )
791 ] });
792 };
793 const AddMemberModal = ({ onClose }) => {
794 const queryClient = useQueryClient();
795 const { showToast } = useToast();
796 const [searchQ, setSearchQ] = reactExports.useState("");
797 const [pickedId, setPickedId] = reactExports.useState(null);
798 const [roleSlug, setRoleSlug] = reactExports.useState("yatra_sales_agent");
799 const [debouncedQ, setDebouncedQ] = reactExports.useState("");
800 React.useEffect(() => {
801 const t = window.setTimeout(() => setDebouncedQ(searchQ), 250);
802 return () => window.clearTimeout(t);
803 }, [searchQ]);
804 const { data: candidatesData, isLoading: candidatesLoading } = useQuery({
805 queryKey: ["team-users-available", debouncedQ],
806 queryFn: () => teamApi.listAvailableUsers(debouncedQ, 50)
807 });
808 const { data: rolesData } = useQuery({
809 queryKey: ["team-roles"],
810 queryFn: () => teamApi.listRoles()
811 });
812 const candidates = (candidatesData == null ? void 0 : candidatesData.data) ?? [];
813 const addMutation = useMutation({
814 mutationFn: () => teamApi.updateUser(pickedId, { role_slug: roleSlug }),
815 onSuccess: () => {
816 queryClient.invalidateQueries({ queryKey: ["team-users"] });
817 queryClient.invalidateQueries({ queryKey: ["team-users-available"] });
818 showToast(__("Member added to the team.", "yatra"), "success");
819 onClose();
820 },
821 onError: (e) => showToast(extractError(e), "error")
822 });
823 return /* @__PURE__ */ jsxRuntimeExports.jsx(
824 Modal,
825 {
826 isOpen: true,
827 onClose,
828 title: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
829 /* @__PURE__ */ jsxRuntimeExports.jsx(UserPlus, { className: "w-5 h-5 text-blue-500" }),
830 __("Add existing WordPress user", "yatra")
831 ] }),
832 size: "md",
833 footer: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2", children: [
834 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", onClick: onClose, children: __("Cancel", "yatra") }),
835 /* @__PURE__ */ jsxRuntimeExports.jsx(
836 Button,
837 {
838 disabled: pickedId === null || addMutation.isPending,
839 onClick: () => addMutation.mutate(),
840 children: addMutation.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
841 /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
842 __("Adding…", "yatra")
843 ] }) : __("Add to team", "yatra")
844 }
845 )
846 ] }),
847 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
848 /* @__PURE__ */ jsxRuntimeExports.jsx(Alert, { variant: "info", title: __("Looking for someone new?", "yatra"), children: sprintf(
849 /* translators: %s: brand name */
850 __(
851 "This picker shows WordPress users who don't yet have a %s role. For people who aren't on the site at all, send them an email invitation from the Invitations tab — that creates the WP user for them.",
852 "yatra"
853 ),
854 brandName()
855 ) }),
856 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
857 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "add-member-search", children: __("Find a user", "yatra") }),
858 /* @__PURE__ */ jsxRuntimeExports.jsx(
859 Input,
860 {
861 id: "add-member-search",
862 value: searchQ,
863 onChange: (e) => {
864 setSearchQ(e.target.value);
865 setPickedId(null);
866 },
867 placeholder: __("Search by name, email, or login…", "yatra"),
868 className: "mt-1"
869 }
870 )
871 ] }),
872 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border border-gray-200 dark:border-gray-700 rounded-md max-h-72 overflow-y-auto", children: candidatesLoading ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "divide-y divide-gray-100 dark:divide-gray-800", children: [0, 1, 2, 3].map((i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 px-3 py-2.5", children: [
873 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-4 w-4 rounded-full flex-shrink-0" }),
874 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "w-9 h-9 rounded-full flex-shrink-0" }),
875 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [
876 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-3.5 w-2/5" }),
877 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-3 w-3/5" })
878 ] })
879 ] }, i)) }) : candidates.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-center py-8 px-4 text-sm text-gray-500 dark:text-gray-400", children: debouncedQ === "" ? sprintf(
880 /* translators: %s: brand name */
881 __(
882 "No available users — every WP user on this site is already a %s team member. Use the Invitations tab to add new people.",
883 "yatra"
884 ),
885 brandName()
886 ) : __("No matching users.", "yatra") }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "divide-y divide-gray-100 dark:divide-gray-800", children: candidates.map((u) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
887 "label",
888 {
889 htmlFor: `pick-${u.id}`,
890 className: `flex items-center gap-3 px-3 py-2.5 cursor-pointer transition-colors ${pickedId === u.id ? "bg-blue-50 dark:bg-blue-900/20" : "hover:bg-gray-50 dark:hover:bg-gray-800/50"}`,
891 children: [
892 /* @__PURE__ */ jsxRuntimeExports.jsx(
893 "input",
894 {
895 id: `pick-${u.id}`,
896 type: "radio",
897 name: "add-member-pick",
898 checked: pickedId === u.id,
899 onChange: () => setPickedId(u.id),
900 className: "h-4 w-4 flex-shrink-0"
901 }
902 ),
903 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-9 h-9 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-semibold text-blue-600 dark:text-blue-400", children: (u.display_name || u.login).charAt(0).toUpperCase() }) }),
904 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0 flex-1", children: [
905 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-medium text-gray-900 dark:text-white truncate", children: u.display_name || u.login }),
906 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-500 dark:text-gray-400 truncate", children: u.email })
907 ] })
908 ]
909 },
910 u.id
911 )) }) }),
912 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
913 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "add-member-role", children: sprintf(
914 /* translators: %s: brand name */
915 __("%s role", "yatra"),
916 brandName()
917 ) }),
918 /* @__PURE__ */ jsxRuntimeExports.jsx(
919 Select,
920 {
921 id: "add-member-role",
922 value: roleSlug,
923 onChange: (e) => setRoleSlug(e.target.value),
924 className: "mt-1",
925 children: ((rolesData == null ? void 0 : rolesData.data) ?? []).map((r) => /* @__PURE__ */ jsxRuntimeExports.jsxs("option", { value: r.slug, children: [
926 r.display_name,
927 " (",
928 r.capability_count,
929 " ",
930 __("caps", "yatra"),
931 ")"
932 ] }, r.slug))
933 }
934 ),
935 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mt-1 text-xs text-gray-500 dark:text-gray-400", children: __(
936 "Per-user scopes + capability overrides can be set after the member is added (3-dot menu → Edit access).",
937 "yatra"
938 ) })
939 ] })
940 ] })
941 }
942 );
943 };
944 const CreateUserModal = ({ onClose }) => {
945 const queryClient = useQueryClient();
946 const { showToast } = useToast();
947 const { data: rolesData } = useQuery({
948 queryKey: ["team-roles"],
949 queryFn: () => teamApi.listRoles()
950 });
951 const [email, setEmail] = reactExports.useState("");
952 const [firstName, setFirstName] = reactExports.useState("");
953 const [lastName, setLastName] = reactExports.useState("");
954 const [username, setUsername] = reactExports.useState("");
955 const [roleSlug, setRoleSlug] = reactExports.useState("");
956 const [pwMode, setPwMode] = reactExports.useState("reset_email");
957 const [password, setPassword] = reactExports.useState("");
958 const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
959 const passwordOk = pwMode === "reset_email" || password.length >= 8;
960 const canSubmit = emailValid && roleSlug !== "" && passwordOk;
961 const createMutation = useMutation({
962 mutationFn: () => teamApi.createUser({
963 email,
964 role_slug: roleSlug,
965 first_name: firstName || void 0,
966 last_name: lastName || void 0,
967 username: username || void 0,
968 password: pwMode === "manual" ? password : void 0,
969 send_reset_email: pwMode === "reset_email"
970 }),
971 onSuccess: () => {
972 queryClient.invalidateQueries({ queryKey: ["team-users"] });
973 showToast(__("User created and added to the team.", "yatra"), "success");
974 onClose();
975 },
976 onError: (e) => showToast(extractError(e), "error")
977 });
978 return /* @__PURE__ */ jsxRuntimeExports.jsx(
979 Modal,
980 {
981 isOpen: true,
982 onClose,
983 title: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
984 /* @__PURE__ */ jsxRuntimeExports.jsx(UserPlus, { className: "w-5 h-5 text-blue-500" }),
985 __("Create new user", "yatra")
986 ] }),
987 size: "lg",
988 footer: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2", children: [
989 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", onClick: onClose, children: __("Cancel", "yatra") }),
990 /* @__PURE__ */ jsxRuntimeExports.jsx(
991 Button,
992 {
993 disabled: !canSubmit || createMutation.isPending,
994 onClick: () => createMutation.mutate(),
995 children: createMutation.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
996 /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
997 __("Creating…", "yatra")
998 ] }) : __("Create user", "yatra")
999 }
1000 )
1001 ] }),
1002 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
1003 /* @__PURE__ */ jsxRuntimeExports.jsx(
1004 Alert,
1005 {
1006 variant: "info",
1007 title: __("Creates a new WordPress user", "yatra"),
1008 children: sprintf(
1009 /* translators: %s: brand name */
1010 __(
1011 'This provisions a brand-new WP account and attaches the chosen %s role in one step. For people who already have a WP user, use "Add existing WP user" instead. To send a magic-link invite via email (account is created on accept), use the Invitations tab.',
1012 "yatra"
1013 ),
1014 brandName()
1015 )
1016 }
1017 ),
1018 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-3", children: [
1019 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1020 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "cu-first", children: __("First name", "yatra") }),
1021 /* @__PURE__ */ jsxRuntimeExports.jsx(
1022 Input,
1023 {
1024 id: "cu-first",
1025 value: firstName,
1026 onChange: (e) => setFirstName(e.target.value),
1027 className: "mt-1"
1028 }
1029 )
1030 ] }),
1031 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1032 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "cu-last", children: __("Last name", "yatra") }),
1033 /* @__PURE__ */ jsxRuntimeExports.jsx(
1034 Input,
1035 {
1036 id: "cu-last",
1037 value: lastName,
1038 onChange: (e) => setLastName(e.target.value),
1039 className: "mt-1"
1040 }
1041 )
1042 ] })
1043 ] }),
1044 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1045 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "cu-email", children: __("Email", "yatra") }),
1046 /* @__PURE__ */ jsxRuntimeExports.jsx(
1047 Input,
1048 {
1049 id: "cu-email",
1050 type: "email",
1051 value: email,
1052 onChange: (e) => setEmail(e.target.value),
1053 placeholder: "[email protected]",
1054 className: "mt-1",
1055 required: true
1056 }
1057 ),
1058 email !== "" && !emailValid && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-red-600 mt-1", children: __("Enter a valid email address.", "yatra") })
1059 ] }),
1060 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1061 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "cu-username", children: __("Username (optional)", "yatra") }),
1062 /* @__PURE__ */ jsxRuntimeExports.jsx(
1063 Input,
1064 {
1065 id: "cu-username",
1066 value: username,
1067 onChange: (e) => setUsername(e.target.value),
1068 placeholder: __("Auto-generated from email", "yatra"),
1069 className: "mt-1"
1070 }
1071 ),
1072 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mt-1 text-xs text-gray-500 dark:text-gray-400", children: __(
1073 "WP requires unique, lowercase, no spaces. If left blank, derived from the email local-part.",
1074 "yatra"
1075 ) })
1076 ] }),
1077 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1078 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "cu-role", children: sprintf(
1079 /* translators: %s: brand name */
1080 __("%s role", "yatra"),
1081 brandName()
1082 ) }),
1083 /* @__PURE__ */ jsxRuntimeExports.jsxs(
1084 Select,
1085 {
1086 id: "cu-role",
1087 value: roleSlug,
1088 onChange: (e) => setRoleSlug(e.target.value),
1089 className: "mt-1",
1090 children: [
1091 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: __("Select a role…", "yatra") }),
1092 ((rolesData == null ? void 0 : rolesData.data) ?? []).map((r) => /* @__PURE__ */ jsxRuntimeExports.jsxs("option", { value: r.slug, children: [
1093 r.display_name,
1094 " (",
1095 r.capability_count,
1096 " ",
1097 __("caps", "yatra"),
1098 ")"
1099 ] }, r.slug))
1100 ]
1101 }
1102 )
1103 ] }),
1104 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-md border border-gray-200 dark:border-gray-700 p-3", children: [
1105 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { className: "text-sm font-medium", children: __("Password", "yatra") }),
1106 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 space-y-2", children: [
1107 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-start gap-2 cursor-pointer", children: [
1108 /* @__PURE__ */ jsxRuntimeExports.jsx(
1109 "input",
1110 {
1111 type: "radio",
1112 name: "cu-pw-mode",
1113 checked: pwMode === "reset_email",
1114 onChange: () => setPwMode("reset_email"),
1115 className: "mt-0.5"
1116 }
1117 ),
1118 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-sm", children: [
1119 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-medium text-gray-900 dark:text-white", children: __("Send reset-password email (recommended)", "yatra") }),
1120 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-500 dark:text-gray-400", children: __(
1121 "User receives a standard WP password-reset link. You never see or type their password — strongest security posture.",
1122 "yatra"
1123 ) })
1124 ] })
1125 ] }),
1126 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-start gap-2 cursor-pointer", children: [
1127 /* @__PURE__ */ jsxRuntimeExports.jsx(
1128 "input",
1129 {
1130 type: "radio",
1131 name: "cu-pw-mode",
1132 checked: pwMode === "manual",
1133 onChange: () => setPwMode("manual"),
1134 className: "mt-0.5"
1135 }
1136 ),
1137 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-sm flex-1", children: [
1138 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-medium text-gray-900 dark:text-white", children: __("Set a password now", "yatra") }),
1139 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-gray-500 dark:text-gray-400 mb-2", children: __(
1140 "You'll need to share this password with the user out-of-band (DM, in person). 8 character minimum.",
1141 "yatra"
1142 ) }),
1143 pwMode === "manual" && /* @__PURE__ */ jsxRuntimeExports.jsx(
1144 Input,
1145 {
1146 type: "password",
1147 value: password,
1148 onChange: (e) => setPassword(e.target.value),
1149 placeholder: __("At least 8 characters", "yatra"),
1150 className: "w-full"
1151 }
1152 )
1153 ] })
1154 ] })
1155 ] })
1156 ] })
1157 ] })
1158 }
1159 );
1160 };
1161 const MemberEditDrawer = ({ userId, onClose }) => {
1162 var _a;
1163 const queryClient = useQueryClient();
1164 const { showToast } = useToast();
1165 const { data, isLoading } = useQuery({
1166 queryKey: ["team-user", userId],
1167 queryFn: () => teamApi.getUser(userId)
1168 });
1169 const { data: rolesData } = useQuery({
1170 queryKey: ["team-roles"],
1171 queryFn: () => teamApi.listRoles()
1172 });
1173 const { data: capsData } = useQuery({
1174 queryKey: ["team-capabilities"],
1175 queryFn: () => teamApi.listCapabilities()
1176 });
1177 const user = data == null ? void 0 : data.data;
1178 const [roleSlug, setRoleSlug] = reactExports.useState("");
1179 const [extraGrants, setExtraGrants] = reactExports.useState([]);
1180 const [extraRevokes, setExtraRevokes] = reactExports.useState([]);
1181 const [expiresLocal, setExpiresLocal] = reactExports.useState("");
1182 const [expiryDirty, setExpiryDirty] = reactExports.useState(false);
1183 React.useEffect(() => {
1184 if (user) {
1185 setRoleSlug(user.primary_role || "");
1186 setExtraGrants(user.caps_grant);
1187 setExtraRevokes(user.caps_revoke);
1188 setExpiresLocal(unixToLocalInputValue(user.expires_at));
1189 setExpiryDirty(false);
1190 }
1191 }, [user]);
1192 const isSelfEdit = (user == null ? void 0 : user.id) === ((_a = window.yatraAdmin) == null ? void 0 : _a.currentUser);
1193 const updateMutation = useMutation({
1194 mutationFn: () => {
1195 const payload = {
1196 role_slug: roleSlug,
1197 caps_grant: extraGrants,
1198 caps_revoke: extraRevokes
1199 };
1200 if (expiryDirty) {
1201 payload.expires_at = expiresLocal ? Math.floor(new Date(expiresLocal).getTime() / 1e3) : 0;
1202 }
1203 return teamApi.updateUser(userId, payload);
1204 },
1205 onSuccess: (res) => {
1206 var _a2, _b;
1207 queryClient.invalidateQueries({ queryKey: ["team-users"] });
1208 queryClient.invalidateQueries({ queryKey: ["team-user", userId] });
1209 if (((_a2 = res.data) == null ? void 0 : _a2.id) === (((_b = window.yatraAdmin) == null ? void 0 : _b.userCaps) ? userId : -1)) {
1210 setUserCaps(res.data.effective_caps);
1211 }
1212 showToast(__("Member updated.", "yatra"), "success");
1213 onClose();
1214 },
1215 onError: (e) => showToast(extractError(e), "error")
1216 });
1217 return /* @__PURE__ */ jsxRuntimeExports.jsx(
1218 Modal,
1219 {
1220 isOpen: true,
1221 onClose,
1222 title: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
1223 /* @__PURE__ */ jsxRuntimeExports.jsx(KeyRound, { className: "w-5 h-5 text-blue-500" }),
1224 user ? user.display_name : __("Loading…", "yatra")
1225 ] }),
1226 size: "lg",
1227 hideFooter: false,
1228 footer: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2", children: [
1229 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", onClick: onClose, children: (user == null ? void 0 : user.is_wp_admin) ? __("Close", "yatra") : __("Cancel", "yatra") }),
1230 !(user == null ? void 0 : user.is_wp_admin) && /* @__PURE__ */ jsxRuntimeExports.jsx(
1231 Button,
1232 {
1233 disabled: updateMutation.isPending,
1234 onClick: () => updateMutation.mutate(),
1235 children: updateMutation.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1236 /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
1237 __("Saving…", "yatra")
1238 ] }) : __("Save changes", "yatra")
1239 }
1240 )
1241 ] }),
1242 children: isLoading || !user ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
1243 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-4 w-24" }),
1244 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-9 w-full" }),
1245 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-md border border-gray-200 dark:border-gray-700 p-3 space-y-3", children: [
1246 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-4 w-48" }),
1247 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-9 w-full" })
1248 ] }),
1249 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-2", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
1250 "div",
1251 {
1252 className: "rounded-md border border-gray-200 dark:border-gray-700 p-3 space-y-2",
1253 children: [
1254 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-4 w-32" }),
1255 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
1256 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-6 w-full" }),
1257 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-6 w-full" }),
1258 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-6 w-full" }),
1259 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-6 w-full" })
1260 ] })
1261 ]
1262 },
1263 i
1264 )) })
1265 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-4", children: user.is_wp_admin ? (
1266 // Admin-lock UI. WP administrators always pass every yatra_*
1267 // cap via the server-side admin fallback, so role / grant /
1268 // revoke / scope / expiry assignments against them would be
1269 // misleading no-ops. We show a read-only summary instead of
1270 // editable form controls, and the server rejects any write
1271 // attempts with `yatra_team_admin_locked` (409) as a defense-
1272 // in-depth check.
1273 /* @__PURE__ */ jsxRuntimeExports.jsxs(
1274 Alert,
1275 {
1276 variant: "info",
1277 title: __("WordPress administrator", "yatra"),
1278 children: [
1279 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm", children: sprintf(
1280 /* translators: %s: brand name */
1281 __(
1282 "This user is a WP administrator and always passes every %s capability check via the admin fallback. Role assignment, capability grants, revokes, scope restrictions, and access expiry cannot be enforced on them.",
1283 "yatra"
1284 ),
1285 brandName()
1286 ) }),
1287 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm mt-2", children: __(
1288 "To scope this user's Yatra access, first remove their WordPress administrator role from the standard wp-admin Users screen, then return here to assign a Yatra role.",
1289 "yatra"
1290 ) }),
1291 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-3 grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs", children: [
1292 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1293 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-gray-500 dark:text-gray-400 uppercase tracking-wide font-medium mb-0.5", children: __("Effective capabilities", "yatra") }),
1294 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-gray-900 dark:text-white font-medium", children: sprintf(
1295 /* translators: %d: count of caps */
1296 __("%d (all yatra_*)", "yatra"),
1297 user.effective_caps.length
1298 ) })
1299 ] }),
1300 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1301 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-gray-500 dark:text-gray-400 uppercase tracking-wide font-medium mb-0.5", children: __("Yatra role on record", "yatra") }),
1302 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-gray-900 dark:text-white font-medium", children: user.primary_role ? roleSlug || user.primary_role : __("None (admin fallback only)", "yatra") })
1303 ] })
1304 ] })
1305 ]
1306 }
1307 )
1308 ) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1309 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1310 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { children: sprintf(
1311 /* translators: %s: brand name */
1312 __("%s role", "yatra"),
1313 brandName()
1314 ) }),
1315 /* @__PURE__ */ jsxRuntimeExports.jsxs(
1316 Select,
1317 {
1318 value: roleSlug,
1319 onChange: (e) => setRoleSlug(e.target.value),
1320 className: "mt-1",
1321 "aria-label": __("Role", "yatra"),
1322 children: [
1323 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: sprintf(
1324 /* translators: %s: brand name */
1325 __("No %s role", "yatra"),
1326 brandName()
1327 ) }),
1328 ((rolesData == null ? void 0 : rolesData.data) ?? []).map((r) => /* @__PURE__ */ jsxRuntimeExports.jsxs("option", { value: r.slug, children: [
1329 r.display_name,
1330 " (",
1331 r.capability_count,
1332 " ",
1333 __("caps", "yatra"),
1334 ")"
1335 ] }, r.slug))
1336 ]
1337 }
1338 )
1339 ] }),
1340 !isSelfEdit && !user.is_wp_admin && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-md border border-gray-200 dark:border-gray-700 p-3 space-y-2", children: [
1341 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-3", children: [
1342 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1343 /* @__PURE__ */ jsxRuntimeExports.jsx(
1344 Label,
1345 {
1346 htmlFor: "member-expires-at",
1347 className: "text-sm font-medium",
1348 children: __("Access expires on (optional)", "yatra")
1349 }
1350 ),
1351 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: __(
1352 "Time-windowed access: caps are revoked automatically once this passes. Useful for contractors, seasonal staff, or temporary vendor access. Leave blank for permanent access.",
1353 "yatra"
1354 ) })
1355 ] }),
1356 expiresLocal && /* @__PURE__ */ jsxRuntimeExports.jsx(
1357 Button,
1358 {
1359 type: "button",
1360 variant: "outline",
1361 size: "sm",
1362 onClick: () => {
1363 setExpiresLocal("");
1364 setExpiryDirty(true);
1365 },
1366 children: __("Clear expiry", "yatra")
1367 }
1368 )
1369 ] }),
1370 /* @__PURE__ */ jsxRuntimeExports.jsx(
1371 Input,
1372 {
1373 id: "member-expires-at",
1374 type: "datetime-local",
1375 value: expiresLocal,
1376 min: unixToLocalInputValue(
1377 Math.floor(Date.now() / 1e3) + 60
1378 ),
1379 onChange: (e) => {
1380 setExpiresLocal(e.target.value);
1381 setExpiryDirty(true);
1382 },
1383 className: "w-full"
1384 }
1385 ),
1386 /* @__PURE__ */ jsxRuntimeExports.jsx(ExpiryNowHint, { expiresLocal }),
1387 user.is_expired && /* @__PURE__ */ jsxRuntimeExports.jsx(
1388 Alert,
1389 {
1390 variant: "warning",
1391 title: __("Access has already expired", "yatra"),
1392 children: __(
1393 "This member is past their expiry and currently has no access. The next hourly sweep will fully strip their role + grants. Set a new date above to extend access — or clear the expiry to make it permanent.",
1394 "yatra"
1395 )
1396 }
1397 )
1398 ] }),
1399 capsData && /* @__PURE__ */ jsxRuntimeExports.jsx(
1400 CapabilityOverridesEditor,
1401 {
1402 registry: capsData.capabilities,
1403 effective: user.effective_caps,
1404 grants: extraGrants,
1405 revokes: extraRevokes,
1406 onChangeGrants: setExtraGrants,
1407 onChangeRevokes: setExtraRevokes
1408 }
1409 )
1410 ] }) })
1411 }
1412 );
1413 };
1414 const CapabilityOverridesEditor = ({
1415 registry,
1416 effective,
1417 grants,
1418 revokes,
1419 onChangeGrants,
1420 onChangeRevokes
1421 }) => {
1422 const byCategory = reactExports.useMemo(() => {
1423 const map = {};
1424 Object.entries(registry).forEach(([cap, def]) => {
1425 if (!map[def.category]) map[def.category] = [];
1426 map[def.category].push([cap, def]);
1427 });
1428 return map;
1429 }, [registry]);
1430 const sensColor = (s) => {
1431 if (s === "critical")
1432 return "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300";
1433 if (s === "high")
1434 return "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300";
1435 if (s === "medium")
1436 return "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300";
1437 return "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300";
1438 };
1439 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1440 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-medium text-gray-900 dark:text-white mb-1", children: __("Per-user capability overrides", "yatra") }),
1441 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mb-3", children: __(
1442 "Override the role's defaults for THIS user. Grants extend access; revokes deny even when the role allows.",
1443 "yatra"
1444 ) }),
1445 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-3 max-h-80 overflow-y-auto pr-1 border border-gray-200 dark:border-gray-700 rounded-md p-2", children: Object.entries(byCategory).map(([cat, rows]) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1446 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[11px] font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 px-1 py-1", children: cat }),
1447 rows.map(([cap, def]) => {
1448 const isEffective = effective.includes(cap);
1449 const isGranted = grants.includes(cap);
1450 const isRevoked = revokes.includes(cap);
1451 return /* @__PURE__ */ jsxRuntimeExports.jsxs(
1452 "div",
1453 {
1454 className: "flex items-center justify-between gap-3 px-2 py-1.5 rounded hover:bg-gray-50 dark:hover:bg-gray-800/50",
1455 children: [
1456 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0 flex-1", children: [
1457 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
1458 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-900 dark:text-white", children: def.label }),
1459 /* @__PURE__ */ jsxRuntimeExports.jsx(
1460 "span",
1461 {
1462 className: `text-[10px] font-medium px-1.5 py-0.5 rounded ${sensColor(def.sensitivity)}`,
1463 children: def.sensitivity
1464 }
1465 )
1466 ] }),
1467 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-[11px] text-gray-400 dark:text-gray-500 font-mono", children: cap })
1468 ] }),
1469 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 flex-shrink-0", children: [
1470 isEffective && !isGranted && !isRevoked && /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { className: "bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-300 text-[10px]", children: __("via role", "yatra") }),
1471 /* @__PURE__ */ jsxRuntimeExports.jsx(
1472 Tooltip,
1473 {
1474 content: __("Grant this cap on top of the role", "yatra"),
1475 children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1476 "button",
1477 {
1478 type: "button",
1479 onClick: () => {
1480 if (isGranted) {
1481 onChangeGrants(grants.filter((c) => c !== cap));
1482 } else {
1483 onChangeGrants([...grants, cap]);
1484 onChangeRevokes(revokes.filter((c) => c !== cap));
1485 }
1486 },
1487 className: `text-[10px] px-2 py-1 rounded border transition-colors ${isGranted ? "bg-blue-600 text-white border-blue-600" : "bg-white text-gray-600 border-gray-200 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700 dark:hover:bg-gray-700"}`,
1488 children: __("Grant", "yatra")
1489 }
1490 )
1491 }
1492 ),
1493 /* @__PURE__ */ jsxRuntimeExports.jsx(
1494 Tooltip,
1495 {
1496 content: __(
1497 "Deny this cap even if the role allows",
1498 "yatra"
1499 ),
1500 children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1501 "button",
1502 {
1503 type: "button",
1504 onClick: () => {
1505 if (isRevoked) {
1506 onChangeRevokes(revokes.filter((c) => c !== cap));
1507 } else {
1508 onChangeRevokes([...revokes, cap]);
1509 onChangeGrants(grants.filter((c) => c !== cap));
1510 }
1511 },
1512 className: `text-[10px] px-2 py-1 rounded border transition-colors ${isRevoked ? "bg-red-600 text-white border-red-600" : "bg-white text-gray-600 border-gray-200 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700 dark:hover:bg-gray-700"}`,
1513 children: __("Revoke", "yatra")
1514 }
1515 )
1516 }
1517 )
1518 ] })
1519 ]
1520 },
1521 cap
1522 );
1523 })
1524 ] }, cat)) })
1525 ] });
1526 };
1527 const RolesTab = () => {
1528 const queryClient = useQueryClient();
1529 const { showToast } = useToast();
1530 const [editingRoleSlug, setEditingRoleSlug] = reactExports.useState(null);
1531 const [createSeed, setCreateSeed] = reactExports.useState(null);
1532 const [pendingDelete, setPendingDelete] = reactExports.useState(null);
1533 const { data, isLoading } = useQuery({
1534 queryKey: ["team-roles"],
1535 queryFn: () => teamApi.listRoles()
1536 });
1537 const deleteMutation = useMutation({
1538 mutationFn: (slug) => teamApi.deleteRole(slug),
1539 onSuccess: () => {
1540 queryClient.invalidateQueries({ queryKey: ["team-roles"] });
1541 showToast(__("Role deleted.", "yatra"), "success");
1542 setPendingDelete(null);
1543 },
1544 onError: (e) => {
1545 showToast(extractError(e), "error");
1546 setPendingDelete(null);
1547 }
1548 });
1549 const roles = (data == null ? void 0 : data.data) ?? [];
1550 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
1551 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-3 flex-wrap", children: [
1552 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1553 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: __("Roles", "yatra") }),
1554 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: __(
1555 "Eight shipped system roles cover most agencies. Click any role to see + edit its capabilities. Clone or create your own with the buttons here.",
1556 "yatra"
1557 ) })
1558 ] }),
1559 /* @__PURE__ */ jsxRuntimeExports.jsxs(Button, { onClick: () => setCreateSeed("new"), children: [
1560 /* @__PURE__ */ jsxRuntimeExports.jsx(Shield, { className: "mr-1.5 h-4 w-4" }),
1561 __("Create custom role", "yatra")
1562 ] })
1563 ] }),
1564 /* @__PURE__ */ jsxRuntimeExports.jsx(Card, { className: "overflow-visible", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "p-0 overflow-visible", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1565 Table,
1566 {
1567 data: roles,
1568 columns: [
1569 {
1570 key: "display_name",
1571 label: __("Role", "yatra"),
1572 render: (r) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [
1573 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-10 h-10 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Shield, { className: "w-5 h-5 text-indigo-600 dark:text-indigo-400" }) }),
1574 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0", children: [
1575 /* @__PURE__ */ jsxRuntimeExports.jsx(
1576 "button",
1577 {
1578 type: "button",
1579 onClick: () => setEditingRoleSlug(r.slug),
1580 className: "font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline transition-colors cursor-pointer text-left",
1581 children: r.display_name
1582 }
1583 ),
1584 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-xs text-gray-500 dark:text-gray-400 font-mono", children: r.slug }) })
1585 ] })
1586 ] })
1587 },
1588 {
1589 key: "is_system",
1590 label: __("Type", "yatra"),
1591 render: (r) => r.is_system ? /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { className: "bg-purple-50 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300", children: __("System", "yatra") }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { className: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300", children: __("Custom", "yatra") })
1592 },
1593 {
1594 key: "capability_count",
1595 label: __("Capabilities", "yatra"),
1596 render: (r) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
1597 Badge,
1598 {
1599 variant: "outline",
1600 className: "cursor-pointer",
1601 onClick: () => setEditingRoleSlug(r.slug),
1602 children: [
1603 r.capability_count,
1604 " ",
1605 __("caps", "yatra")
1606 ]
1607 }
1608 )
1609 },
1610 {
1611 key: "member_count",
1612 label: __("Members", "yatra"),
1613 render: (r) => /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "outline", children: r.member_count })
1614 }
1615 ],
1616 actions: [
1617 {
1618 key: "edit",
1619 label: __("View capabilities", "yatra"),
1620 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(KeyRound, { className: "w-4 h-4" }),
1621 onClick: (r) => setEditingRoleSlug(r.slug)
1622 },
1623 {
1624 key: "clone",
1625 label: __("Clone to custom role", "yatra"),
1626 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "w-4 h-4" }),
1627 onClick: (r) => setCreateSeed(`clone:${r.slug}`)
1628 },
1629 {
1630 key: "delete",
1631 label: __("Delete role", "yatra"),
1632 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "w-4 h-4" }),
1633 onClick: (r) => setPendingDelete(r),
1634 condition: (r) => !r.is_system,
1635 variant: "destructive"
1636 }
1637 ],
1638 isLoading,
1639 emptyText: __("No roles found", "yatra"),
1640 emptyDescription: __(
1641 "System roles should have populated automatically. Try toggling the module off and on.",
1642 "yatra"
1643 )
1644 }
1645 ) }) }),
1646 editingRoleSlug !== null && /* @__PURE__ */ jsxRuntimeExports.jsx(
1647 RoleEditDrawer,
1648 {
1649 slug: editingRoleSlug,
1650 onClose: () => setEditingRoleSlug(null),
1651 onClone: (slug) => {
1652 setEditingRoleSlug(null);
1653 setCreateSeed(`clone:${slug}`);
1654 }
1655 }
1656 ),
1657 createSeed !== null && /* @__PURE__ */ jsxRuntimeExports.jsx(
1658 RoleCreateDrawer,
1659 {
1660 seedSlug: createSeed.startsWith("clone:") ? createSeed.slice(6) : null,
1661 onClose: () => setCreateSeed(null)
1662 }
1663 ),
1664 /* @__PURE__ */ jsxRuntimeExports.jsx(
1665 ConfirmationDialog,
1666 {
1667 isOpen: pendingDelete !== null,
1668 onClose: () => !deleteMutation.isPending && setPendingDelete(null),
1669 onConfirm: () => pendingDelete && deleteMutation.mutate(pendingDelete.slug),
1670 title: __("Delete custom role?", "yatra"),
1671 description: pendingDelete ? __(
1672 'Members assigned to "{name}" will lose this role. Their WordPress user account is preserved.',
1673 "yatra"
1674 ).replace("{name}", pendingDelete.display_name) : "",
1675 confirmText: __("Delete role", "yatra"),
1676 cancelText: __("Cancel", "yatra"),
1677 variant: "danger",
1678 isLoading: deleteMutation.isPending
1679 }
1680 )
1681 ] });
1682 };
1683 const RoleEditDrawer = ({ slug, onClose, onClone }) => {
1684 const queryClient = useQueryClient();
1685 const { showToast } = useToast();
1686 const { data, isLoading } = useQuery({
1687 queryKey: ["team-role", slug],
1688 queryFn: () => teamApi.getRole(slug)
1689 });
1690 const { data: capsData } = useQuery({
1691 queryKey: ["team-capabilities"],
1692 queryFn: () => teamApi.listCapabilities()
1693 });
1694 const role = data == null ? void 0 : data.data;
1695 const [displayName, setDisplayName] = reactExports.useState("");
1696 const [selectedCaps, setSelectedCaps] = reactExports.useState([]);
1697 React.useEffect(() => {
1698 if (role) {
1699 setDisplayName(role.display_name);
1700 setSelectedCaps(role.capabilities);
1701 }
1702 }, [role]);
1703 const updateMutation = useMutation({
1704 mutationFn: () => teamApi.updateRole(slug, {
1705 display_name: displayName,
1706 capabilities: selectedCaps
1707 }),
1708 onSuccess: () => {
1709 queryClient.invalidateQueries({ queryKey: ["team-roles"] });
1710 queryClient.invalidateQueries({ queryKey: ["team-role", slug] });
1711 showToast(__("Role updated.", "yatra"), "success");
1712 onClose();
1713 },
1714 onError: (e) => showToast(extractError(e), "error")
1715 });
1716 const isSystem = (role == null ? void 0 : role.is_system) ?? false;
1717 return /* @__PURE__ */ jsxRuntimeExports.jsx(
1718 Modal,
1719 {
1720 isOpen: true,
1721 onClose,
1722 title: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
1723 /* @__PURE__ */ jsxRuntimeExports.jsx(Shield, { className: "w-5 h-5 text-indigo-500" }),
1724 role ? role.display_name : __("Loading…", "yatra"),
1725 isSystem && /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { className: "bg-purple-50 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300 ml-2", children: __("System role", "yatra") })
1726 ] }),
1727 size: "lg",
1728 hideFooter: false,
1729 footer: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2", children: [
1730 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", onClick: onClose, children: __("Close", "yatra") }),
1731 isSystem ? /* @__PURE__ */ jsxRuntimeExports.jsxs(Button, { onClick: () => onClone(slug), children: [
1732 /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "mr-1.5 h-4 w-4" }),
1733 __("Clone to edit", "yatra")
1734 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx(
1735 Button,
1736 {
1737 disabled: updateMutation.isPending,
1738 onClick: () => updateMutation.mutate(),
1739 children: updateMutation.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1740 /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
1741 __("Saving…", "yatra")
1742 ] }) : __("Save changes", "yatra")
1743 }
1744 )
1745 ] }),
1746 children: isLoading || !role ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
1747 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-4 w-20" }),
1748 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-9 w-full" }),
1749 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-2", children: [0, 1, 2, 3].map((i) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
1750 "div",
1751 {
1752 className: "rounded-md border border-gray-200 dark:border-gray-700 p-3 space-y-2",
1753 children: [
1754 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-4 w-28" }),
1755 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
1756 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-6 w-full" }),
1757 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-6 w-full" })
1758 ] })
1759 ]
1760 },
1761 i
1762 )) })
1763 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
1764 isSystem && /* @__PURE__ */ jsxRuntimeExports.jsx(Alert, { variant: "info", title: __("System role", "yatra"), children: sprintf(
1765 /* translators: 1: brand name, 2: brand name */
1766 __(
1767 "System roles ship with %1$s and can't be edited directly — clone to a custom role to change capabilities. This protects your team if %2$s ships new capabilities in future releases (clones won't auto-update; system roles will).",
1768 "yatra"
1769 ),
1770 brandName(),
1771 brandName()
1772 ) }),
1773 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1774 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "role-display-name", children: __("Role name", "yatra") }),
1775 /* @__PURE__ */ jsxRuntimeExports.jsx(
1776 Input,
1777 {
1778 id: "role-display-name",
1779 value: displayName,
1780 onChange: (e) => setDisplayName(e.target.value),
1781 disabled: isSystem,
1782 className: "mt-1"
1783 }
1784 ),
1785 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "mt-1 text-xs text-gray-500 dark:text-gray-400", children: [
1786 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "font-mono", children: role.slug }),
1787 " ·",
1788 " ",
1789 role.member_count,
1790 " ",
1791 role.member_count === 1 ? __("member", "yatra") : __("members", "yatra")
1792 ] })
1793 ] }),
1794 capsData && /* @__PURE__ */ jsxRuntimeExports.jsx(
1795 CapabilityMatrix,
1796 {
1797 registry: capsData.capabilities,
1798 selected: selectedCaps,
1799 onChange: setSelectedCaps,
1800 disabled: isSystem
1801 }
1802 )
1803 ] })
1804 }
1805 );
1806 };
1807 const RoleCreateDrawer = ({ seedSlug, onClose }) => {
1808 var _a;
1809 const queryClient = useQueryClient();
1810 const { showToast } = useToast();
1811 const { data: seedData } = useQuery({
1812 queryKey: ["team-role", seedSlug],
1813 queryFn: () => teamApi.getRole(seedSlug),
1814 enabled: seedSlug !== null
1815 });
1816 const { data: capsData } = useQuery({
1817 queryKey: ["team-capabilities"],
1818 queryFn: () => teamApi.listCapabilities()
1819 });
1820 const { data: templatesData } = useQuery({
1821 queryKey: ["team-role-templates"],
1822 queryFn: () => teamApi.listRoleTemplates(),
1823 enabled: seedSlug === null
1824 // only show templates for fresh creation
1825 });
1826 const [displayName, setDisplayName] = reactExports.useState("");
1827 const [selectedCaps, setSelectedCaps] = reactExports.useState([]);
1828 const [appliedTemplateId, setAppliedTemplateId] = reactExports.useState(
1829 null
1830 );
1831 React.useEffect(() => {
1832 if (seedData == null ? void 0 : seedData.data) {
1833 setDisplayName(`${seedData.data.display_name} (copy)`);
1834 setSelectedCaps(seedData.data.capabilities);
1835 }
1836 }, [seedData]);
1837 const applyTemplate = (templateId) => {
1838 const tpl = ((templatesData == null ? void 0 : templatesData.data) ?? []).find((t) => t.id === templateId);
1839 if (!tpl) return;
1840 const caps = Array.from(
1841 /* @__PURE__ */ new Set([...tpl.capabilities, "yatra_access_admin"])
1842 );
1843 setSelectedCaps(caps);
1844 setAppliedTemplateId(templateId);
1845 if (displayName.trim() === "") {
1846 setDisplayName(tpl.label);
1847 }
1848 };
1849 const createMutation = useMutation({
1850 mutationFn: () => teamApi.createRole({
1851 display_name: displayName,
1852 capabilities: selectedCaps
1853 }),
1854 onSuccess: () => {
1855 queryClient.invalidateQueries({ queryKey: ["team-roles"] });
1856 showToast(__("Custom role created.", "yatra"), "success");
1857 onClose();
1858 },
1859 onError: (e) => showToast(extractError(e), "error")
1860 });
1861 const canSave = displayName.trim() !== "" && !createMutation.isPending;
1862 return /* @__PURE__ */ jsxRuntimeExports.jsx(
1863 Modal,
1864 {
1865 isOpen: true,
1866 onClose,
1867 title: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
1868 /* @__PURE__ */ jsxRuntimeExports.jsx(Shield, { className: "w-5 h-5 text-indigo-500" }),
1869 seedSlug ? __("Clone role", "yatra") : __("Create custom role", "yatra")
1870 ] }),
1871 size: "lg",
1872 footer: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2", children: [
1873 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", onClick: onClose, children: __("Cancel", "yatra") }),
1874 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { disabled: !canSave, onClick: () => createMutation.mutate(), children: createMutation.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1875 /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
1876 __("Creating…", "yatra")
1877 ] }) : __("Create role", "yatra") })
1878 ] }),
1879 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
1880 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1881 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "new-role-name", children: __("Role name", "yatra") }),
1882 /* @__PURE__ */ jsxRuntimeExports.jsx(
1883 Input,
1884 {
1885 id: "new-role-name",
1886 value: displayName,
1887 onChange: (e) => setDisplayName(e.target.value),
1888 placeholder: __("e.g. Senior Sales Agent", "yatra"),
1889 className: "mt-1"
1890 }
1891 ),
1892 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mt-1 text-xs text-gray-500 dark:text-gray-400", children: __(
1893 "Slug auto-generated from the name (yatra_* prefix added).",
1894 "yatra"
1895 ) })
1896 ] }),
1897 seedSlug === null && (((_a = templatesData == null ? void 0 : templatesData.data) == null ? void 0 : _a.length) ?? 0) > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-md border border-gray-200 dark:border-gray-700 p-3 space-y-2", children: [
1898 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-3", children: [
1899 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1900 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { className: "text-sm font-medium", children: __("Start from a template (optional)", "yatra") }),
1901 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: __(
1902 "Pre-curated cap bundles for common archetypes. Picking one prefills the matrix below — fully editable before save.",
1903 "yatra"
1904 ) })
1905 ] }),
1906 appliedTemplateId && /* @__PURE__ */ jsxRuntimeExports.jsx(
1907 Button,
1908 {
1909 type: "button",
1910 variant: "outline",
1911 size: "sm",
1912 onClick: () => {
1913 setSelectedCaps([]);
1914 setAppliedTemplateId(null);
1915 },
1916 children: __("Clear", "yatra")
1917 }
1918 )
1919 ] }),
1920 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-2", children: ((templatesData == null ? void 0 : templatesData.data) ?? []).map((tpl) => {
1921 const active = appliedTemplateId === tpl.id;
1922 return /* @__PURE__ */ jsxRuntimeExports.jsxs(
1923 "button",
1924 {
1925 type: "button",
1926 onClick: () => applyTemplate(tpl.id),
1927 className: `text-left rounded-md border p-3 transition-colors ${active ? "border-blue-500 bg-blue-50 dark:bg-blue-900/20 dark:border-blue-400" : "border-gray-200 dark:border-gray-700 hover:border-blue-300 hover:bg-blue-50/40 dark:hover:bg-blue-900/10"}`,
1928 children: [
1929 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
1930 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-medium text-gray-900 dark:text-white", children: tpl.label }),
1931 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] uppercase tracking-wide text-gray-400", children: [
1932 tpl.capabilities.length,
1933 " ",
1934 __("caps", "yatra")
1935 ] })
1936 ] }),
1937 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: tpl.description })
1938 ]
1939 },
1940 tpl.id
1941 );
1942 }) })
1943 ] }),
1944 capsData && /* @__PURE__ */ jsxRuntimeExports.jsx(
1945 CapabilityMatrix,
1946 {
1947 registry: capsData.capabilities,
1948 selected: selectedCaps,
1949 onChange: setSelectedCaps,
1950 disabled: false
1951 }
1952 )
1953 ] })
1954 }
1955 );
1956 };
1957 const CapabilityMatrix = ({ registry, selected, onChange, disabled }) => {
1958 const byCategory = reactExports.useMemo(() => {
1959 const map = {};
1960 Object.entries(registry).forEach(([cap, def]) => {
1961 if (!map[def.category]) map[def.category] = [];
1962 map[def.category].push([cap, def]);
1963 });
1964 return map;
1965 }, [registry]);
1966 const selectedSet = reactExports.useMemo(() => new Set(selected), [selected]);
1967 const sensColor = (s) => {
1968 if (s === "critical")
1969 return "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300";
1970 if (s === "high")
1971 return "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300";
1972 if (s === "medium")
1973 return "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300";
1974 return "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300";
1975 };
1976 const toggleCap = (cap) => {
1977 if (disabled) return;
1978 if (selectedSet.has(cap)) {
1979 onChange(selected.filter((c) => c !== cap));
1980 } else {
1981 onChange([...selected, cap]);
1982 }
1983 };
1984 const toggleCategory = (caps) => {
1985 if (disabled) return;
1986 const ids = caps.map(([c]) => c);
1987 const allSelected = ids.every((id) => selectedSet.has(id));
1988 if (allSelected) {
1989 onChange(selected.filter((c) => !ids.includes(c)));
1990 } else {
1991 const next = new Set(selected);
1992 ids.forEach((c) => next.add(c));
1993 onChange(Array.from(next));
1994 }
1995 };
1996 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1997 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-2 mb-1", children: [
1998 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1999 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-medium text-gray-900 dark:text-white", children: __("Capabilities", "yatra") }),
2000 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: disabled ? __(
2001 "Read-only — system roles can't be edited. Use Clone to make an editable copy.",
2002 "yatra"
2003 ) : __(
2004 "Each capability gates a specific action. Sensitivity drives audit-log defaults.",
2005 "yatra"
2006 ) })
2007 ] }),
2008 /* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, { variant: "outline", children: [
2009 selected.length,
2010 " / ",
2011 Object.keys(registry).length
2012 ] })
2013 ] }),
2014 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-2 max-h-96 overflow-y-auto pr-1 border border-gray-200 dark:border-gray-700 rounded-md p-2", children: Object.entries(byCategory).map(([category, rows]) => {
2015 const ids = rows.map(([c]) => c);
2016 const checked = ids.filter((c) => selectedSet.has(c)).length;
2017 const allChecked = checked === ids.length;
2018 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
2019 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-2 px-1 py-1 sticky top-0 bg-white dark:bg-gray-900 z-10", children: [
2020 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[11px] font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400", children: [
2021 category,
2022 " ",
2023 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-normal text-gray-400", children: [
2024 "(",
2025 checked,
2026 "/",
2027 ids.length,
2028 ")"
2029 ] })
2030 ] }),
2031 !disabled && /* @__PURE__ */ jsxRuntimeExports.jsx(
2032 "button",
2033 {
2034 type: "button",
2035 onClick: () => toggleCategory(rows),
2036 className: "text-[11px] font-medium text-blue-600 dark:text-blue-400 hover:underline",
2037 children: allChecked ? __("Clear all", "yatra") : __("Select all", "yatra")
2038 }
2039 )
2040 ] }),
2041 rows.map(([cap, def]) => {
2042 const isChecked = selectedSet.has(cap);
2043 return /* @__PURE__ */ jsxRuntimeExports.jsxs(
2044 "label",
2045 {
2046 htmlFor: `cap-${cap}`,
2047 className: `flex items-center gap-3 px-2 py-1.5 rounded transition-colors ${disabled ? "cursor-default" : "cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50"} ${isChecked ? "bg-blue-50/40 dark:bg-blue-900/10" : ""}`,
2048 children: [
2049 /* @__PURE__ */ jsxRuntimeExports.jsx(
2050 "input",
2051 {
2052 id: `cap-${cap}`,
2053 type: "checkbox",
2054 checked: isChecked,
2055 onChange: () => toggleCap(cap),
2056 disabled,
2057 className: "h-4 w-4 rounded border-gray-300 flex-shrink-0"
2058 }
2059 ),
2060 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0 flex-1", children: [
2061 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
2062 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-900 dark:text-white", children: def.label }),
2063 /* @__PURE__ */ jsxRuntimeExports.jsx(
2064 "span",
2065 {
2066 className: `text-[10px] font-medium px-1.5 py-0.5 rounded ${sensColor(def.sensitivity)}`,
2067 children: def.sensitivity
2068 }
2069 )
2070 ] }),
2071 /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-[11px] text-gray-400 dark:text-gray-500 font-mono", children: cap })
2072 ] })
2073 ]
2074 },
2075 cap
2076 );
2077 })
2078 ] }, category);
2079 }) })
2080 ] });
2081 };
2082 const InvitationsTab = () => {
2083 const queryClient = useQueryClient();
2084 const { showToast } = useToast();
2085 const [showInviteModal, setShowInviteModal] = reactExports.useState(false);
2086 const [revealAcceptUrl, setRevealAcceptUrl] = reactExports.useState(null);
2087 const [pendingRevoke, setPendingRevoke] = reactExports.useState(
2088 null
2089 );
2090 const [revokeAlsoDelete, setRevokeAlsoDelete] = reactExports.useState(true);
2091 const [pendingDelete, setPendingDelete] = reactExports.useState(
2092 null
2093 );
2094 const { data, isLoading } = useQuery({
2095 queryKey: ["team-invitations"],
2096 queryFn: () => teamApi.listInvitations()
2097 });
2098 const revokeMutation = useMutation({
2099 mutationFn: (vars) => teamApi.revokeInvitation(vars.id, { purge: vars.purge }),
2100 onSuccess: (res) => {
2101 queryClient.invalidateQueries({ queryKey: ["team-invitations"] });
2102 queryClient.invalidateQueries({ queryKey: ["team-audit"] });
2103 showToast(res.message, "success");
2104 setPendingRevoke(null);
2105 setPendingDelete(null);
2106 },
2107 onError: (e) => {
2108 showToast(extractError(e), "error");
2109 setPendingRevoke(null);
2110 setPendingDelete(null);
2111 }
2112 });
2113 const rows = reactExports.useMemo(() => {
2114 const map = (data == null ? void 0 : data.data) ?? {};
2115 return Object.values(map);
2116 }, [data]);
2117 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
2118 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-3 flex-wrap", children: [
2119 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
2120 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: __("Invitations", "yatra") }),
2121 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: __(
2122 "Magic-link invitations expire after 72 hours by default. Tokens are stored hashed; the link is only shown once.",
2123 "yatra"
2124 ) })
2125 ] }),
2126 /* @__PURE__ */ jsxRuntimeExports.jsxs(Button, { onClick: () => setShowInviteModal(true), children: [
2127 /* @__PURE__ */ jsxRuntimeExports.jsx(UserPlus, { className: "mr-1.5 h-4 w-4" }),
2128 __("Invite member", "yatra")
2129 ] })
2130 ] }),
2131 /* @__PURE__ */ jsxRuntimeExports.jsx(Card, { className: "overflow-visible", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "p-0 overflow-visible", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2132 Table,
2133 {
2134 data: rows,
2135 columns: [
2136 {
2137 key: "email",
2138 label: __("Email", "yatra"),
2139 render: (i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
2140 /* @__PURE__ */ jsxRuntimeExports.jsx(Mail, { className: "w-4 h-4 text-gray-400" }),
2141 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-900 dark:text-white", children: i.email })
2142 ] })
2143 },
2144 {
2145 key: "role_slug",
2146 label: __("Role", "yatra"),
2147 render: (i) => /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { className: "bg-indigo-50 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300", children: i.role_slug })
2148 },
2149 {
2150 key: "status",
2151 label: __("Status", "yatra"),
2152 render: (i) => {
2153 const cls = {
2154 pending: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300",
2155 accepted: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300",
2156 revoked: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300",
2157 expired: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300"
2158 };
2159 return /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { className: cls[i.status], children: i.status });
2160 }
2161 },
2162 {
2163 key: "expires_at",
2164 label: __("Expires", "yatra"),
2165 render: (i) => /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap", children: new Date(i.expires_at * 1e3).toLocaleString() })
2166 }
2167 ],
2168 actions: [
2169 // Revoke — only meaningful for a still-pending invitation.
2170 // Opens a confirmation that lets the operator opt-in to
2171 // ALSO deleting the row (default checked — most operators
2172 // don't want orphaned `revoked` rows piling up).
2173 {
2174 key: "revoke",
2175 label: __("Revoke", "yatra"),
2176 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(XCircle, { className: "w-4 h-4" }),
2177 onClick: (i) => {
2178 setRevokeAlsoDelete(true);
2179 setPendingRevoke(i);
2180 },
2181 condition: (i) => i.status === "pending",
2182 variant: "destructive"
2183 },
2184 // Delete — for any non-pending row (revoked, accepted,
2185 // expired). Pure storage hygiene. Without this, the
2186 // table grew without bound as old invitations accumulated.
2187 {
2188 key: "delete",
2189 label: __("Delete record", "yatra"),
2190 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "w-4 h-4" }),
2191 onClick: (i) => setPendingDelete(i),
2192 condition: (i) => i.status !== "pending",
2193 variant: "destructive"
2194 }
2195 ],
2196 isLoading,
2197 emptyText: __("No invitations yet", "yatra"),
2198 emptyDescription: __(
2199 "Send an invitation to add a teammate.",
2200 "yatra"
2201 ),
2202 onCreateClick: () => setShowInviteModal(true)
2203 }
2204 ) }) }),
2205 showInviteModal && /* @__PURE__ */ jsxRuntimeExports.jsx(
2206 InvitationModal,
2207 {
2208 onClose: () => setShowInviteModal(false),
2209 onSent: (acceptUrl) => {
2210 setShowInviteModal(false);
2211 setRevealAcceptUrl(acceptUrl);
2212 }
2213 }
2214 ),
2215 /* @__PURE__ */ jsxRuntimeExports.jsx(
2216 AcceptUrlRevealDialog,
2217 {
2218 url: revealAcceptUrl,
2219 onClose: () => setRevealAcceptUrl(null)
2220 }
2221 ),
2222 pendingRevoke && /* @__PURE__ */ jsxRuntimeExports.jsx(
2223 Modal,
2224 {
2225 isOpen: true,
2226 onClose: () => {
2227 if (!revokeMutation.isPending) setPendingRevoke(null);
2228 },
2229 title: __("Revoke invitation?", "yatra"),
2230 size: "md",
2231 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
2232 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-700 dark:text-gray-300", children: sprintf(
2233 /* translators: %s: invited email address */
2234 __(
2235 "The magic-link sent to %s will stop working immediately. Anyone who already clicked the link before now has already accepted (you can confirm in the audit log).",
2236 "yatra"
2237 ),
2238 pendingRevoke.email
2239 ) }),
2240 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-start gap-2 cursor-pointer p-3 rounded-md border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/40", children: [
2241 /* @__PURE__ */ jsxRuntimeExports.jsx(
2242 "input",
2243 {
2244 type: "checkbox",
2245 checked: revokeAlsoDelete,
2246 onChange: (e) => setRevokeAlsoDelete(e.target.checked),
2247 disabled: revokeMutation.isPending,
2248 className: "mt-0.5"
2249 }
2250 ),
2251 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-sm", children: [
2252 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block font-medium text-gray-900 dark:text-white", children: __("Also delete the invitation record", "yatra") }),
2253 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: __(
2254 "Removes the row from this list. The audit log keeps a permanent trail of who was invited, by whom, and when — that's preserved separately.",
2255 "yatra"
2256 ) })
2257 ] })
2258 ] }),
2259 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2 pt-1", children: [
2260 /* @__PURE__ */ jsxRuntimeExports.jsx(
2261 Button,
2262 {
2263 variant: "outline",
2264 onClick: () => setPendingRevoke(null),
2265 disabled: revokeMutation.isPending,
2266 children: __("Cancel", "yatra")
2267 }
2268 ),
2269 /* @__PURE__ */ jsxRuntimeExports.jsxs(
2270 Button,
2271 {
2272 variant: "destructive",
2273 onClick: () => revokeMutation.mutate({
2274 id: pendingRevoke.id,
2275 purge: revokeAlsoDelete
2276 }),
2277 disabled: revokeMutation.isPending,
2278 children: [
2279 revokeMutation.isPending && /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" }),
2280 revokeAlsoDelete ? __("Revoke and delete", "yatra") : __("Revoke", "yatra")
2281 ]
2282 }
2283 )
2284 ] })
2285 ] })
2286 }
2287 ),
2288 /* @__PURE__ */ jsxRuntimeExports.jsx(
2289 ConfirmationDialog,
2290 {
2291 isOpen: pendingDelete !== null,
2292 onClose: () => {
2293 if (!revokeMutation.isPending) setPendingDelete(null);
2294 },
2295 onConfirm: () => {
2296 if (pendingDelete) {
2297 revokeMutation.mutate({ id: pendingDelete.id, purge: true });
2298 }
2299 },
2300 title: __("Delete invitation record?", "yatra"),
2301 description: pendingDelete ? sprintf(
2302 /* translators: 1: invited email address, 2: current status */
2303 __(
2304 "Permanently remove the %1$s invitation (status: %2$s) from this list. The audit log entry stays, so you can still see who was invited and when — only the live record is removed.",
2305 "yatra"
2306 ),
2307 pendingDelete.email,
2308 pendingDelete.status
2309 ) : "",
2310 confirmText: __("Delete record", "yatra"),
2311 cancelText: __("Cancel", "yatra"),
2312 variant: "danger",
2313 isLoading: revokeMutation.isPending
2314 }
2315 )
2316 ] });
2317 };
2318 const InvitationModal = ({ onClose, onSent }) => {
2319 const queryClient = useQueryClient();
2320 const { showToast } = useToast();
2321 const { data: rolesData } = useQuery({
2322 queryKey: ["team-roles"],
2323 queryFn: () => teamApi.listRoles()
2324 });
2325 const [email, setEmail] = reactExports.useState("");
2326 const [role, setRole] = reactExports.useState("yatra_sales_agent");
2327 const [expiresIn, setExpiresIn] = reactExports.useState(259200);
2328 const sendMutation = useMutation({
2329 mutationFn: () => teamApi.sendInvitation({ email, role, expires_in: expiresIn }),
2330 onSuccess: (res) => {
2331 queryClient.invalidateQueries({ queryKey: ["team-invitations"] });
2332 showToast(__("Invitation sent.", "yatra"), "success");
2333 onSent(res.data.accept_url);
2334 },
2335 onError: (e) => showToast(extractError(e), "error")
2336 });
2337 return /* @__PURE__ */ jsxRuntimeExports.jsx(
2338 Modal,
2339 {
2340 isOpen: true,
2341 onClose,
2342 title: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
2343 /* @__PURE__ */ jsxRuntimeExports.jsx(UserPlus, { className: "w-5 h-5 text-blue-500" }),
2344 __("Invite a team member", "yatra")
2345 ] }),
2346 size: "md",
2347 footer: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2", children: [
2348 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", onClick: onClose, children: __("Cancel", "yatra") }),
2349 /* @__PURE__ */ jsxRuntimeExports.jsx(
2350 Button,
2351 {
2352 disabled: email.trim() === "" || sendMutation.isPending,
2353 onClick: () => sendMutation.mutate(),
2354 children: sendMutation.isPending ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
2355 /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
2356 __("Sending…", "yatra")
2357 ] }) : __("Send invitation", "yatra")
2358 }
2359 )
2360 ] }),
2361 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
2362 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
2363 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "invite-email", children: __("Email address", "yatra") }),
2364 /* @__PURE__ */ jsxRuntimeExports.jsx(
2365 Input,
2366 {
2367 id: "invite-email",
2368 type: "email",
2369 value: email,
2370 onChange: (e) => setEmail(e.target.value),
2371 placeholder: "[email protected]",
2372 className: "mt-1"
2373 }
2374 )
2375 ] }),
2376 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
2377 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "invite-role", children: __("Role", "yatra") }),
2378 /* @__PURE__ */ jsxRuntimeExports.jsx(
2379 Select,
2380 {
2381 id: "invite-role",
2382 value: role,
2383 onChange: (e) => setRole(e.target.value),
2384 className: "mt-1",
2385 children: ((rolesData == null ? void 0 : rolesData.data) ?? []).map((r) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: r.slug, children: r.display_name }, r.slug))
2386 }
2387 )
2388 ] }),
2389 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
2390 /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { htmlFor: "invite-expiry", children: __("Link expires in", "yatra") }),
2391 /* @__PURE__ */ jsxRuntimeExports.jsxs(
2392 Select,
2393 {
2394 id: "invite-expiry",
2395 value: String(expiresIn),
2396 onChange: (e) => setExpiresIn(Number(e.target.value)),
2397 className: "mt-1",
2398 children: [
2399 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "86400", children: __("24 hours", "yatra") }),
2400 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "259200", children: __("72 hours (recommended)", "yatra") }),
2401 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "604800", children: __("7 days", "yatra") })
2402 ]
2403 }
2404 )
2405 ] }),
2406 /* @__PURE__ */ jsxRuntimeExports.jsx(Alert, { variant: "info", title: __("How it works", "yatra"), children: __(
2407 "The invitee receives an email with a magic link. Clicking it attaches the chosen role to an existing WP user with that email, or creates a new WP user + sends them a password-reset link.",
2408 "yatra"
2409 ) })
2410 ] })
2411 }
2412 );
2413 };
2414 const AcceptUrlRevealDialog = ({ url, onClose }) => {
2415 const [copied, setCopied] = reactExports.useState(false);
2416 const doCopy = async () => {
2417 if (!url) return;
2418 try {
2419 await navigator.clipboard.writeText(url);
2420 setCopied(true);
2421 window.setTimeout(() => setCopied(false), 2e3);
2422 } catch (_e) {
2423 }
2424 };
2425 if (!url) return null;
2426 return /* @__PURE__ */ jsxRuntimeExports.jsx(
2427 Modal,
2428 {
2429 isOpen: true,
2430 onClose,
2431 title: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
2432 /* @__PURE__ */ jsxRuntimeExports.jsx(ShieldCheck, { className: "w-5 h-5 text-green-500" }),
2433 __("Invitation link", "yatra")
2434 ] }),
2435 size: "md",
2436 footer: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end gap-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: onClose, children: __("Done", "yatra") }) }),
2437 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
2438 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-700 dark:text-gray-200", children: __(
2439 "The invitation email is on its way. If you'd like to share the link manually (e.g. via Slack), copy it below — it's only shown once.",
2440 "yatra"
2441 ) }),
2442 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
2443 /* @__PURE__ */ jsxRuntimeExports.jsx(
2444 Input,
2445 {
2446 value: url,
2447 readOnly: true,
2448 onFocus: (e) => e.currentTarget.select(),
2449 className: "font-mono text-xs",
2450 "aria-label": __("Accept URL", "yatra")
2451 }
2452 ),
2453 /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "outline", onClick: doCopy, children: copied ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
2454 /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "mr-1.5 h-4 w-4" }),
2455 __("Copied", "yatra")
2456 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
2457 /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "mr-1.5 h-4 w-4" }),
2458 __("Copy", "yatra")
2459 ] }) })
2460 ] }),
2461 /* @__PURE__ */ jsxRuntimeExports.jsx(
2462 Alert,
2463 {
2464 variant: "warning",
2465 title: __("Treat this link like a password", "yatra"),
2466 children: __(
2467 "Anyone with this URL can claim the invited role until it expires.",
2468 "yatra"
2469 )
2470 }
2471 )
2472 ] })
2473 }
2474 );
2475 };
2476 const AuditLogTab = () => {
2477 const queryClient = useQueryClient();
2478 const { showToast } = useToast();
2479 const [page, setPage] = reactExports.useState(1);
2480 const [actionFilter, setActionFilter] = reactExports.useState("");
2481 const [entityFilter, setEntityFilter] = reactExports.useState("");
2482 const [resultFilter, setResultFilter] = reactExports.useState("");
2483 const [selectedIds, setSelectedIds] = reactExports.useState(/* @__PURE__ */ new Set());
2484 const [confirmClear, setConfirmClear] = reactExports.useState(false);
2485 const [confirmBulk, setConfirmBulk] = reactExports.useState(false);
2486 const perPage = 50;
2487 const { data, isLoading } = useQuery({
2488 queryKey: [
2489 "team-audit-log",
2490 page,
2491 actionFilter,
2492 entityFilter,
2493 resultFilter
2494 ],
2495 queryFn: () => teamApi.listAuditLog({
2496 page,
2497 per_page: perPage,
2498 ...actionFilter ? { action: actionFilter } : {},
2499 ...entityFilter ? { entity_type: entityFilter } : {},
2500 ...resultFilter ? { result: resultFilter } : {}
2501 }),
2502 placeholderData: (prev) => prev
2503 });
2504 const { data: facets } = useQuery({
2505 queryKey: ["team-audit-facets"],
2506 queryFn: () => teamApi.auditFacets()
2507 });
2508 const rows = (data == null ? void 0 : data.data) ?? [];
2509 const total = (data == null ? void 0 : data.total) ?? 0;
2510 const totalPages = Math.max(1, Math.ceil(total / perPage));
2511 const hasFilters = !!(actionFilter || entityFilter || resultFilter);
2512 const invalidateAudit = () => {
2513 queryClient.invalidateQueries({ queryKey: ["team-audit-log"] });
2514 queryClient.invalidateQueries({ queryKey: ["team-audit-facets"] });
2515 setSelectedIds(/* @__PURE__ */ new Set());
2516 };
2517 const clearMutation = useMutation({
2518 mutationFn: () => teamApi.clearAuditLog(),
2519 onSuccess: (res) => {
2520 invalidateAudit();
2521 setConfirmClear(false);
2522 showToast(res.message, "success");
2523 },
2524 onError: (e) => {
2525 setConfirmClear(false);
2526 showToast(extractError(e), "error");
2527 }
2528 });
2529 const bulkDeleteMutation = useMutation({
2530 mutationFn: (ids) => teamApi.bulkDeleteAuditLog(ids),
2531 onSuccess: (res) => {
2532 invalidateAudit();
2533 setConfirmBulk(false);
2534 showToast(res.message, "success");
2535 },
2536 onError: (e) => {
2537 setConfirmBulk(false);
2538 showToast(extractError(e), "error");
2539 }
2540 });
2541 React.useEffect(() => {
2542 setSelectedIds(/* @__PURE__ */ new Set());
2543 }, [page, actionFilter, entityFilter, resultFilter]);
2544 const toggleOne = (id, checked) => {
2545 setSelectedIds((prev) => {
2546 const next = new Set(prev);
2547 if (checked) next.add(id);
2548 else next.delete(id);
2549 return next;
2550 });
2551 };
2552 const toggleAll = (checked) => {
2553 if (!checked) {
2554 setSelectedIds(/* @__PURE__ */ new Set());
2555 return;
2556 }
2557 setSelectedIds(new Set(rows.map((r) => r.id)));
2558 };
2559 const isAllSelected = rows.length > 0 && rows.every((r) => selectedIds.has(r.id));
2560 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
2561 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-3 flex-wrap", children: [
2562 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
2563 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: __("Audit log", "yatra") }),
2564 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: __(
2565 "Append-only event stream. High + critical sensitivity actions and every denied attempt are logged. Default retention 180 days.",
2566 "yatra"
2567 ) })
2568 ] }),
2569 total > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs(
2570 Button,
2571 {
2572 type: "button",
2573 variant: "outline",
2574 onClick: () => setConfirmClear(true),
2575 disabled: clearMutation.isPending,
2576 children: [
2577 /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "mr-1.5 h-4 w-4" }),
2578 __("Clear all", "yatra")
2579 ]
2580 }
2581 )
2582 ] }),
2583 selectedIds.size > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-2 rounded-md border border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/30 p-3", children: [
2584 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-blue-900 dark:text-blue-100 font-medium mr-auto", children: sprintf(
2585 /* translators: %d: count of selected audit-log entries */
2586 __("%d entries selected", "yatra"),
2587 selectedIds.size
2588 ) }),
2589 /* @__PURE__ */ jsxRuntimeExports.jsxs(
2590 Button,
2591 {
2592 type: "button",
2593 variant: "destructive",
2594 onClick: () => setConfirmBulk(true),
2595 disabled: bulkDeleteMutation.isPending,
2596 children: [
2597 /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "mr-1.5 h-4 w-4" }),
2598 __("Delete selected", "yatra")
2599 ]
2600 }
2601 ),
2602 /* @__PURE__ */ jsxRuntimeExports.jsx(
2603 Button,
2604 {
2605 type: "button",
2606 variant: "outline",
2607 onClick: () => setSelectedIds(/* @__PURE__ */ new Set()),
2608 children: __("Clear selection", "yatra")
2609 }
2610 )
2611 ] }),
2612 /* @__PURE__ */ jsxRuntimeExports.jsx(Card, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(CardContent, { className: "p-3", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col lg:flex-row lg:items-center gap-2", children: [
2613 /* @__PURE__ */ jsxRuntimeExports.jsxs(
2614 Select,
2615 {
2616 value: actionFilter,
2617 onChange: (e) => {
2618 setActionFilter(e.target.value);
2619 setPage(1);
2620 },
2621 "aria-label": __("Filter by action", "yatra"),
2622 className: "w-full lg:w-56",
2623 children: [
2624 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: __("All actions", "yatra") }),
2625 ((facets == null ? void 0 : facets.actions) ?? []).map((a) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: a, children: a }, a))
2626 ]
2627 }
2628 ),
2629 /* @__PURE__ */ jsxRuntimeExports.jsxs(
2630 Select,
2631 {
2632 value: entityFilter,
2633 onChange: (e) => {
2634 setEntityFilter(e.target.value);
2635 setPage(1);
2636 },
2637 "aria-label": __("Filter by entity", "yatra"),
2638 className: "w-full lg:w-48",
2639 children: [
2640 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: __("All entities", "yatra") }),
2641 ((facets == null ? void 0 : facets.entity_types) ?? []).map((e) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: e, children: e }, e))
2642 ]
2643 }
2644 ),
2645 /* @__PURE__ */ jsxRuntimeExports.jsxs(
2646 Select,
2647 {
2648 value: resultFilter,
2649 onChange: (e) => {
2650 setResultFilter(e.target.value);
2651 setPage(1);
2652 },
2653 "aria-label": __("Filter by result", "yatra"),
2654 className: "w-full lg:w-40",
2655 children: [
2656 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: __("Any result", "yatra") }),
2657 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "allowed", children: __("Allowed", "yatra") }),
2658 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "denied", children: __("Denied", "yatra") })
2659 ]
2660 }
2661 ),
2662 hasFilters && /* @__PURE__ */ jsxRuntimeExports.jsx(
2663 Button,
2664 {
2665 variant: "outline",
2666 onClick: () => {
2667 setActionFilter("");
2668 setEntityFilter("");
2669 setResultFilter("");
2670 setPage(1);
2671 },
2672 children: __("Reset", "yatra")
2673 }
2674 )
2675 ] }) }) }),
2676 /* @__PURE__ */ jsxRuntimeExports.jsx(Card, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "p-0 overflow-visible", children: [
2677 /* @__PURE__ */ jsxRuntimeExports.jsx(
2678 Table,
2679 {
2680 data: rows,
2681 selectedItemIds: Array.from(selectedIds),
2682 onSelectItem: (id, checked) => toggleOne(Number(id), checked),
2683 onSelectAll: toggleAll,
2684 isAllSelected,
2685 getItemId: (r) => r.id,
2686 columns: [
2687 {
2688 key: "occurred_at",
2689 label: __("When", "yatra"),
2690 render: (r) => /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap", children: new Date(r.occurred_at).toLocaleString() })
2691 },
2692 {
2693 key: "actor",
2694 label: __("Who", "yatra"),
2695 render: (r) => r.actor_user_id ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0", children: [
2696 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm text-gray-900 dark:text-white truncate", children: r.actor_display_name || `#${r.actor_user_id}` }),
2697 r.actor_ip && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-gray-400 font-mono", children: r.actor_ip })
2698 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { className: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300", children: __("system", "yatra") })
2699 },
2700 {
2701 key: "action",
2702 label: __("Action", "yatra"),
2703 render: (r) => /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-xs font-mono text-indigo-700 dark:text-indigo-300", children: r.action })
2704 },
2705 {
2706 key: "entity",
2707 label: __("Entity", "yatra"),
2708 render: (r) => r.entity_type ? /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs text-gray-700 dark:text-gray-300", children: [
2709 r.entity_type,
2710 r.entity_id ? ` #${r.entity_id}` : ""
2711 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-gray-400", children: "—" })
2712 },
2713 {
2714 key: "result",
2715 label: __("Result", "yatra"),
2716 render: (r) => r.result === "denied" ? /* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, { className: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300", children: [
2717 /* @__PURE__ */ jsxRuntimeExports.jsx(XCircle, { className: "mr-1 h-3 w-3" }),
2718 __("Denied", "yatra")
2719 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(Badge, { className: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300", children: [
2720 /* @__PURE__ */ jsxRuntimeExports.jsx(CheckCircle2, { className: "mr-1 h-3 w-3" }),
2721 __("Allowed", "yatra")
2722 ] })
2723 }
2724 ],
2725 actions: [],
2726 isLoading,
2727 emptyText: __("No audit events yet", "yatra"),
2728 emptyDescription: __(
2729 "High and critical sensitivity actions will appear here as they happen.",
2730 "yatra"
2731 )
2732 }
2733 ),
2734 rows.length > 0 && totalPages > 1 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-gray-200 px-4 py-3 dark:border-gray-700", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2735 Pagination,
2736 {
2737 currentPage: page,
2738 totalPages,
2739 totalItems: total,
2740 itemsPerPage: perPage,
2741 onPageChange: setPage,
2742 itemName: __("events", "yatra")
2743 }
2744 ) })
2745 ] }) }),
2746 /* @__PURE__ */ jsxRuntimeExports.jsx(
2747 ConfirmationDialog,
2748 {
2749 isOpen: confirmClear,
2750 onClose: () => !clearMutation.isPending && setConfirmClear(false),
2751 onConfirm: () => clearMutation.mutate(),
2752 title: __("Clear the entire audit log?", "yatra"),
2753 description: sprintf(
2754 /* translators: %d: count of audit-log entries about to be wiped */
2755 __(
2756 "This permanently deletes all %d audit-log entries. The clear action itself will be recorded as a new entry — so the operator who wiped history is documented. There is no undo.",
2757 "yatra"
2758 ),
2759 total
2760 ),
2761 confirmText: __("Yes, clear all", "yatra"),
2762 cancelText: __("Cancel", "yatra"),
2763 variant: "danger",
2764 isLoading: clearMutation.isPending
2765 }
2766 ),
2767 /* @__PURE__ */ jsxRuntimeExports.jsx(
2768 ConfirmationDialog,
2769 {
2770 isOpen: confirmBulk,
2771 onClose: () => !bulkDeleteMutation.isPending && setConfirmBulk(false),
2772 onConfirm: () => bulkDeleteMutation.mutate(Array.from(selectedIds)),
2773 title: sprintf(
2774 /* translators: %d: count of selected audit-log entries */
2775 __("Delete %d audit-log entries?", "yatra"),
2776 selectedIds.size
2777 ),
2778 description: __(
2779 "Permanent. The deletion itself is recorded as a new audit entry so the operator who removed history is documented.",
2780 "yatra"
2781 ),
2782 confirmText: __("Delete entries", "yatra"),
2783 cancelText: __("Cancel", "yatra"),
2784 variant: "danger",
2785 isLoading: bulkDeleteMutation.isPending
2786 }
2787 )
2788 ] });
2789 };
2790 const SettingsTab = () => {
2791 var _a, _b;
2792 const queryClient = useQueryClient();
2793 const { showToast } = useToast();
2794 const { data, isLoading } = useQuery({
2795 queryKey: ["team-settings"],
2796 queryFn: () => teamApi.getSettings()
2797 });
2798 const keepAccess = ((_a = data == null ? void 0 : data.data) == null ? void 0 : _a.keep_access_on_module_disable) === true;
2799 const serverAllowlist = ((_b = data == null ? void 0 : data.data) == null ? void 0 : _b.login_ip_allowlist) ?? "";
2800 const [allowlistDraft, setAllowlistDraft] = React.useState(serverAllowlist);
2801 React.useEffect(() => {
2802 setAllowlistDraft(serverAllowlist);
2803 }, [serverAllowlist]);
2804 const allowlistDirty = allowlistDraft.trim() !== serverAllowlist.trim();
2805 const updateMutation = useMutation({
2806 mutationFn: (next) => teamApi.updateSettings({ keep_access_on_module_disable: next }),
2807 onSuccess: (res) => {
2808 if (typeof window !== "undefined" && window.yatraAdmin) {
2809 window.yatraAdmin.teamKeepAccessOnModuleDisable = res.data.keep_access_on_module_disable === true;
2810 }
2811 queryClient.invalidateQueries({ queryKey: ["team-settings"] });
2812 queryClient.invalidateQueries({ queryKey: ["team-audit"] });
2813 showToast(res.message, "success");
2814 },
2815 onError: (e) => showToast(extractError(e), "error")
2816 });
2817 const saveAllowlist = useMutation({
2818 mutationFn: (next) => teamApi.updateSettings({ login_ip_allowlist: next }),
2819 onSuccess: (res) => {
2820 queryClient.invalidateQueries({ queryKey: ["team-settings"] });
2821 queryClient.invalidateQueries({ queryKey: ["team-audit"] });
2822 showToast(res.message, "success");
2823 setAllowlistDraft(res.data.login_ip_allowlist ?? "");
2824 },
2825 onError: (e) => showToast(extractError(e), "error")
2826 });
2827 if (isLoading) {
2828 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [
2829 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-5 w-48" }),
2830 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-20 w-full" }),
2831 /* @__PURE__ */ jsxRuntimeExports.jsx(Skeleton, { className: "h-32 w-full" })
2832 ] });
2833 }
2834 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
2835 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
2836 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: __("Team & Access settings", "yatra") }),
2837 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: __(
2838 "One setting: decide what happens to your team's access if you ever turn off this module. Every change here is recorded in the Audit log.",
2839 "yatra"
2840 ) })
2841 ] }),
2842 keepAccess ? /* @__PURE__ */ jsxRuntimeExports.jsx(
2843 Alert,
2844 {
2845 variant: "info",
2846 title: __("You've opted in to keep team access on disable", "yatra"),
2847 children: __(
2848 "This toggle is ON. If you ever turn off the Team & Access module, your team members keep their access and their Yatra roles stay on your site. This is a permissive choice — only leave it ON if you specifically want roles to survive a module-off period (for example, a brief maintenance window).",
2849 "yatra"
2850 )
2851 }
2852 ) : /* @__PURE__ */ jsxRuntimeExports.jsx(
2853 Alert,
2854 {
2855 variant: "info",
2856 title: __(
2857 "On the default — team access is revoked when the module is off",
2858 "yatra"
2859 ),
2860 children: __(
2861 "This toggle is OFF, which is the default. If you ever turn off the Team & Access module, every Yatra role on your site (Owner, Manager, Sales Agent, and any custom roles you built) will be removed, and your team members will lose all Yatra access. Re-enabling the module brings back the 8 built-in roles, but your custom roles and the original assignments do NOT come back. Flip this toggle ON if you'd rather keep your team's access when the module is off.",
2862 "yatra"
2863 )
2864 }
2865 ),
2866 /* @__PURE__ */ jsxRuntimeExports.jsx(Card, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "p-5 space-y-5", children: [
2867 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-4", children: [
2868 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-3 min-w-0", children: [
2869 keepAccess ? /* @__PURE__ */ jsxRuntimeExports.jsx(Lock, { className: "w-5 h-5 text-green-600 mt-0.5 flex-shrink-0" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Unlock, { className: "w-5 h-5 text-amber-600 mt-0.5 flex-shrink-0" }),
2870 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0", children: [
2871 /* @__PURE__ */ jsxRuntimeExports.jsxs(Label, { className: "text-base font-medium text-gray-900 dark:text-white block", children: [
2872 __(
2873 "Keep team access running when this module is turned off",
2874 "yatra"
2875 ),
2876 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ml-2 text-xs font-normal text-gray-500 dark:text-gray-400", children: __("(off by default)", "yatra") })
2877 ] }),
2878 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: keepAccess ? __(
2879 "ON. If you turn this module off later, your team members keep their roles and current access. Nothing on your site changes until you decide to switch back.",
2880 "yatra"
2881 ) : __(
2882 "OFF (default). If you turn this module off later, every Yatra role on your site will be removed and your team members will lose their Yatra access. You (the site owner) always keep your own admin access.",
2883 "yatra"
2884 ) }),
2885 updateMutation.isPending && /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-gray-400 mt-1 flex items-center gap-1.5", children: [
2886 /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
2887 __("Saving…", "yatra")
2888 ] })
2889 ] })
2890 ] }),
2891 /* @__PURE__ */ jsxRuntimeExports.jsx(
2892 Switch,
2893 {
2894 checked: keepAccess,
2895 disabled: updateMutation.isPending,
2896 onCheckedChange: (next) => updateMutation.mutate(next)
2897 }
2898 )
2899 ] }),
2900 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-3", children: [
2901 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-md border border-green-200 bg-green-50/40 dark:border-green-900 dark:bg-green-950/20 p-3", children: [
2902 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mb-1.5", children: [
2903 /* @__PURE__ */ jsxRuntimeExports.jsx(Lock, { className: "w-4 h-4 text-green-600" }),
2904 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: __("While this module stays ON", "yatra") })
2905 ] }),
2906 /* @__PURE__ */ jsxRuntimeExports.jsxs("ul", { className: "text-xs text-gray-600 dark:text-gray-400 space-y-1 list-disc pl-4", children: [
2907 /* @__PURE__ */ jsxRuntimeExports.jsx("li", { children: __("You (site owner) see everything.", "yatra") }),
2908 /* @__PURE__ */ jsxRuntimeExports.jsx("li", { children: __("Team members see only what their role allows.", "yatra") }),
2909 /* @__PURE__ */ jsxRuntimeExports.jsx("li", { children: __(
2910 "Customers and other users see nothing in the admin.",
2911 "yatra"
2912 ) }),
2913 /* @__PURE__ */ jsxRuntimeExports.jsx("li", { className: "text-gray-400 italic", children: __(
2914 "This setting doesn't apply yet — it kicks in only if you turn the module off.",
2915 "yatra"
2916 ) })
2917 ] })
2918 ] }),
2919 /* @__PURE__ */ jsxRuntimeExports.jsxs(
2920 "div",
2921 {
2922 className: `rounded-md border p-3 ${keepAccess ? "border-green-200 bg-green-50/40 dark:border-green-900 dark:bg-green-950/20" : "border-red-200 bg-red-50/40 dark:border-red-900 dark:bg-red-950/20"}`,
2923 children: [
2924 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mb-1.5", children: [
2925 keepAccess ? /* @__PURE__ */ jsxRuntimeExports.jsx(Lock, { className: "w-4 h-4 text-green-600" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(AlertTriangle, { className: "w-4 h-4 text-red-600" }),
2926 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: __("If you ever turn this module OFF", "yatra") })
2927 ] }),
2928 /* @__PURE__ */ jsxRuntimeExports.jsxs("ul", { className: "text-xs text-gray-600 dark:text-gray-400 space-y-1 list-disc pl-4", children: [
2929 /* @__PURE__ */ jsxRuntimeExports.jsx("li", { children: __("You (site owner) still see everything.", "yatra") }),
2930 keepAccess ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
2931 /* @__PURE__ */ jsxRuntimeExports.jsx("li", { className: "text-green-700 dark:text-green-300 font-medium", children: __(
2932 "Team members keep their access — based on their assigned role.",
2933 "yatra"
2934 ) }),
2935 /* @__PURE__ */ jsxRuntimeExports.jsx("li", { children: __(
2936 "Advanced features (expiry, per-user grants, scopes, audit log) pause until you turn the module back on.",
2937 "yatra"
2938 ) })
2939 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
2940 /* @__PURE__ */ jsxRuntimeExports.jsx("li", { className: "text-red-700 dark:text-red-300 font-medium", children: __(
2941 "All Yatra roles are deleted (Owner, Manager, custom roles you built — everything except the Yatra Customer role).",
2942 "yatra"
2943 ) }),
2944 /* @__PURE__ */ jsxRuntimeExports.jsx("li", { className: "text-red-700 dark:text-red-300 font-medium", children: __("Team members lose all their Yatra access.", "yatra") }),
2945 /* @__PURE__ */ jsxRuntimeExports.jsx("li", { children: __(
2946 "Re-enabling the module brings back the 8 built-in roles. Custom roles and the original member assignments don't come back automatically.",
2947 "yatra"
2948 ) })
2949 ] })
2950 ] })
2951 ]
2952 }
2953 )
2954 ] }),
2955 /* @__PURE__ */ jsxRuntimeExports.jsx(Alert, { variant: "info", title: __("Quick recap", "yatra"), children: __(
2956 "Leave the toggle OFF (default) for the security-conservative behavior — every Yatra role is removed if the module is later disabled, so no stale role-based access lingers. Flip it ON only if you specifically need team access to survive a module-off period (e.g. a brief maintenance toggle). Your audit log and member data are preserved either way.",
2957 "yatra"
2958 ) })
2959 ] }) }),
2960 /* @__PURE__ */ jsxRuntimeExports.jsxs(Card, { children: [
2961 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardHeader, { children: [
2962 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardTitle, { className: "flex items-center gap-2 text-base", children: [
2963 /* @__PURE__ */ jsxRuntimeExports.jsx(Shield, { className: "w-4 h-4 text-blue-500" }),
2964 __("Restrict staff logins by source IP", "yatra")
2965 ] }),
2966 /* @__PURE__ */ jsxRuntimeExports.jsx(CardDescription, { children: __(
2967 "Optional. Only users who hold a Yatra role are affected — WordPress administrators are always exempt and can never lock themselves out. Drop a comma- or newline-separated list of CIDRs (e.g. 203.0.113.0/24, 198.51.100.5). Empty = no restriction.",
2968 "yatra"
2969 ) })
2970 ] }),
2971 /* @__PURE__ */ jsxRuntimeExports.jsxs(CardContent, { className: "space-y-4", children: [
2972 /* @__PURE__ */ jsxRuntimeExports.jsx(
2973 "textarea",
2974 {
2975 className: "w-full font-mono text-xs px-3 py-2 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 min-h-[120px]",
2976 value: allowlistDraft,
2977 onChange: (e) => setAllowlistDraft(e.target.value),
2978 placeholder: "203.0.113.0/24\n2001:db8::/32\n198.51.100.5"
2979 }
2980 ),
2981 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-3 flex-wrap", children: [
2982 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: serverAllowlist.trim() === "" ? __(
2983 "Currently no restriction — every authenticated staff member can sign in from anywhere.",
2984 "yatra"
2985 ) : sprintf(
2986 /* translators: %d: number of CIDR entries currently active */
2987 __("%d source IP rule(s) active.", "yatra"),
2988 serverAllowlist.split(",").filter((s) => s.trim() !== "").length
2989 ) }),
2990 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
2991 allowlistDirty && /* @__PURE__ */ jsxRuntimeExports.jsx(
2992 Button,
2993 {
2994 variant: "outline",
2995 onClick: () => setAllowlistDraft(serverAllowlist),
2996 disabled: saveAllowlist.isPending,
2997 children: __("Reset", "yatra")
2998 }
2999 ),
3000 /* @__PURE__ */ jsxRuntimeExports.jsxs(
3001 Button,
3002 {
3003 onClick: () => saveAllowlist.mutate(allowlistDraft),
3004 disabled: !allowlistDirty || saveAllowlist.isPending,
3005 children: [
3006 saveAllowlist.isPending && /* @__PURE__ */ jsxRuntimeExports.jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" }),
3007 __("Save allowlist", "yatra")
3008 ]
3009 }
3010 )
3011 ] })
3012 ] }),
3013 /* @__PURE__ */ jsxRuntimeExports.jsx(
3014 Alert,
3015 {
3016 variant: serverAllowlist.trim() === "" ? "info" : "warning",
3017 title: serverAllowlist.trim() === "" ? __("Failsafe — admins are always exempt", "yatra") : __("Live restriction is in effect", "yatra"),
3018 children: serverAllowlist.trim() === "" ? __(
3019 "If you do enable a restriction here and ever lock yourself out, drop the constant `YATRA_DISABLE_LOGIN_IP_ALLOWLIST = true` into wp-config.php to bypass the gate entirely — no database access required.",
3020 "yatra"
3021 ) : __(
3022 "Logins from outside the allowlist are blocked at the password step. WordPress administrators are exempt — you can never lock yourself out as the site owner. Every block is recorded in the Audit log as `team.login_blocked_by_ip`.",
3023 "yatra"
3024 )
3025 }
3026 )
3027 ] })
3028 ] })
3029 ] });
3030 };
3031 export {
3032 Team as default
3033 };
3034 //# sourceMappingURL=Team-M9W1qR00.js.map
3035