| 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 |
|