@intranefr/superbackend 1.4.4 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +5 -0
- package/README.md +11 -0
- package/index.js +39 -1
- package/package.json +11 -3
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +111 -5
- package/src/controllers/adminBlockDefinitions.controller.js +127 -0
- package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
- package/src/controllers/adminCache.controller.js +342 -0
- package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
- package/src/controllers/adminCrons.controller.js +388 -0
- package/src/controllers/adminDbBrowser.controller.js +124 -0
- package/src/controllers/adminEjsVirtual.controller.js +13 -3
- package/src/controllers/adminHeadless.controller.js +91 -2
- package/src/controllers/adminHealthChecks.controller.js +570 -0
- package/src/controllers/adminI18n.controller.js +51 -29
- package/src/controllers/adminLlm.controller.js +126 -2
- package/src/controllers/adminPages.controller.js +720 -0
- package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
- package/src/controllers/adminProxy.controller.js +113 -0
- package/src/controllers/adminRateLimits.controller.js +138 -0
- package/src/controllers/adminRbac.controller.js +803 -0
- package/src/controllers/adminScripts.controller.js +320 -0
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/adminTerminals.controller.js +39 -0
- package/src/controllers/adminUiComponents.controller.js +315 -0
- package/src/controllers/adminUiComponentsAi.controller.js +34 -0
- package/src/controllers/blogAdmin.controller.js +279 -0
- package/src/controllers/blogAiAdmin.controller.js +224 -0
- package/src/controllers/blogAutomationAdmin.controller.js +141 -0
- package/src/controllers/blogInternal.controller.js +26 -0
- package/src/controllers/blogPublic.controller.js +89 -0
- package/src/controllers/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +366 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware/internalCronAuth.js +29 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +879 -56
- package/src/models/BlockDefinition.js +27 -0
- package/src/models/BlogAutomationLock.js +14 -0
- package/src/models/BlogAutomationRun.js +39 -0
- package/src/models/BlogPost.js +42 -0
- package/src/models/CacheEntry.js +26 -0
- package/src/models/ConsoleEntry.js +32 -0
- package/src/models/ConsoleLog.js +23 -0
- package/src/models/ContextBlockDefinition.js +33 -0
- package/src/models/CronExecution.js +47 -0
- package/src/models/CronJob.js +70 -0
- package/src/models/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/HeadlessModelDefinition.js +10 -0
- package/src/models/HealthAutoHealAttempt.js +57 -0
- package/src/models/HealthCheck.js +132 -0
- package/src/models/HealthCheckRun.js +51 -0
- package/src/models/HealthIncident.js +49 -0
- package/src/models/Page.js +95 -0
- package/src/models/PageCollection.js +42 -0
- package/src/models/ProxyEntry.js +66 -0
- package/src/models/RateLimitCounter.js +19 -0
- package/src/models/RateLimitMetricBucket.js +20 -0
- package/src/models/RbacGrant.js +25 -0
- package/src/models/RbacGroup.js +16 -0
- package/src/models/RbacGroupMember.js +13 -0
- package/src/models/RbacGroupRole.js +13 -0
- package/src/models/RbacRole.js +25 -0
- package/src/models/RbacUserRole.js +13 -0
- package/src/models/ScriptDefinition.js +42 -0
- package/src/models/ScriptRun.js +22 -0
- package/src/models/UiComponent.js +29 -0
- package/src/models/UiComponentProject.js +26 -0
- package/src/models/UiComponentProjectComponent.js +18 -0
- package/src/routes/admin.routes.js +1 -0
- package/src/routes/adminBlog.routes.js +21 -0
- package/src/routes/adminBlogAi.routes.js +16 -0
- package/src/routes/adminBlogAutomation.routes.js +27 -0
- package/src/routes/adminCache.routes.js +20 -0
- package/src/routes/adminConsoleManager.routes.js +302 -0
- package/src/routes/adminCrons.routes.js +25 -0
- package/src/routes/adminDbBrowser.routes.js +65 -0
- package/src/routes/adminEjsVirtual.routes.js +2 -1
- package/src/routes/adminHeadless.routes.js +8 -1
- package/src/routes/adminHealthChecks.routes.js +28 -0
- package/src/routes/adminI18n.routes.js +4 -3
- package/src/routes/adminLlm.routes.js +4 -2
- package/src/routes/adminPages.routes.js +55 -0
- package/src/routes/adminProxy.routes.js +15 -0
- package/src/routes/adminRateLimits.routes.js +17 -0
- package/src/routes/adminRbac.routes.js +38 -0
- package/src/routes/adminScripts.routes.js +21 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +30 -0
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/fileManager.routes.js +62 -0
- package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
- package/src/routes/healthChecksPublic.routes.js +9 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +6 -0
- package/src/routes/pages.routes.js +123 -0
- package/src/routes/proxy.routes.js +46 -0
- package/src/routes/rbac.routes.js +47 -0
- package/src/routes/uiComponentsPublic.routes.js +9 -0
- package/src/routes/webhook.routes.js +2 -1
- package/src/routes/workflows.routes.js +4 -0
- package/src/services/blockDefinitionsAi.service.js +247 -0
- package/src/services/blog.service.js +99 -0
- package/src/services/blogAutomation.service.js +978 -0
- package/src/services/blogCronsBootstrap.service.js +184 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +700 -0
- package/src/services/consoleOverride.service.js +6 -1
- package/src/services/cronScheduler.service.js +350 -0
- package/src/services/dbBrowser.service.js +536 -0
- package/src/services/ejsVirtual.service.js +102 -32
- package/src/services/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- package/src/services/healthChecks.service.js +650 -0
- package/src/services/healthChecksBootstrap.service.js +109 -0
- package/src/services/healthChecksScheduler.service.js +106 -0
- package/src/services/llmDefaults.service.js +190 -0
- package/src/services/migrationAssets/s3.js +2 -2
- package/src/services/pages.service.js +602 -0
- package/src/services/pagesContext.service.js +331 -0
- package/src/services/pagesContextBlocksAi.service.js +349 -0
- package/src/services/proxy.service.js +535 -0
- package/src/services/rateLimiter.service.js +623 -0
- package/src/services/rbac.service.js +212 -0
- package/src/services/scriptsRunner.service.js +259 -0
- package/src/services/terminals.service.js +152 -0
- package/src/services/terminalsWs.service.js +100 -0
- package/src/services/uiComponentsAi.service.js +299 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- package/src/services/workflow.service.js +23 -8
- package/src/utils/orgRoles.js +14 -0
- package/src/utils/rbac/engine.js +60 -0
- package/src/utils/rbac/rightsRegistry.js +29 -0
- package/views/admin-blog-automation.ejs +877 -0
- package/views/admin-blog-edit.ejs +542 -0
- package/views/admin-blog.ejs +399 -0
- package/views/admin-cache.ejs +681 -0
- package/views/admin-console-manager.ejs +680 -0
- package/views/admin-crons.ejs +645 -0
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-headless.ejs +294 -24
- package/views/admin-health-checks.ejs +725 -0
- package/views/admin-i18n.ejs +59 -5
- package/views/admin-llm.ejs +99 -1
- package/views/admin-organizations.ejs +528 -10
- package/views/admin-pages.ejs +2424 -0
- package/views/admin-proxy.ejs +491 -0
- package/views/admin-rate-limiter.ejs +625 -0
- package/views/admin-rbac.ejs +1331 -0
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-ui-components.ejs +741 -0
- package/views/admin-users.ejs +261 -4
- package/views/admin-workflows.ejs +7 -7
- package/views/file-manager.ejs +866 -0
- package/views/pages/blocks/contact.ejs +27 -0
- package/views/pages/blocks/cta.ejs +18 -0
- package/views/pages/blocks/faq.ejs +20 -0
- package/views/pages/blocks/features.ejs +19 -0
- package/views/pages/blocks/hero.ejs +13 -0
- package/views/pages/blocks/html.ejs +5 -0
- package/views/pages/blocks/image.ejs +14 -0
- package/views/pages/blocks/testimonials.ejs +26 -0
- package/views/pages/blocks/text.ejs +10 -0
- package/views/pages/layouts/default.ejs +51 -0
- package/views/pages/layouts/minimal.ejs +42 -0
- package/views/pages/layouts/sidebar.ejs +54 -0
- package/views/pages/partials/footer.ejs +13 -0
- package/views/pages/partials/header.ejs +12 -0
- package/views/pages/partials/sidebar.ejs +8 -0
- package/views/pages/runtime/page.ejs +10 -0
- package/views/pages/templates/article.ejs +20 -0
- package/views/pages/templates/default.ejs +12 -0
- package/views/pages/templates/landing.ejs +14 -0
- package/views/pages/templates/listing.ejs +15 -0
- package/views/partials/admin-image-upload-modal.ejs +221 -0
- package/views/partials/dashboard/nav-items.ejs +14 -0
- package/views/partials/llm-provider-model-picker.ejs +183 -0
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
<p class="text-sm text-gray-600 mt-1">Browse orgs, members and invites</p>
|
|
34
34
|
</div>
|
|
35
35
|
<div class="flex items-center gap-4">
|
|
36
|
+
<button id="btn-create-org" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 font-medium">Create Organization</button>
|
|
36
37
|
</div>
|
|
37
38
|
</div>
|
|
38
39
|
</div>
|
|
@@ -90,6 +91,7 @@
|
|
|
90
91
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
|
91
92
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Slug</th>
|
|
92
93
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
94
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
93
95
|
</tr>
|
|
94
96
|
</thead>
|
|
95
97
|
<tbody id="orgs-tbody" class="bg-white divide-y divide-gray-200"></tbody>
|
|
@@ -140,6 +142,30 @@
|
|
|
140
142
|
<button id="btn-members-refresh" class="bg-gray-100 text-gray-800 px-3 py-2 rounded hover:bg-gray-200" disabled>Refresh</button>
|
|
141
143
|
</div>
|
|
142
144
|
|
|
145
|
+
<!-- Assign User Section -->
|
|
146
|
+
<div class="bg-gray-50 rounded-lg border p-4 mt-4">
|
|
147
|
+
<h4 class="font-semibold text-gray-900">Assign User to Organization</h4>
|
|
148
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mt-3">
|
|
149
|
+
<div>
|
|
150
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">User *</label>
|
|
151
|
+
<select id="assign-user-select" class="w-full border rounded px-3 py-2">
|
|
152
|
+
<option value="">Loading users...</option>
|
|
153
|
+
</select>
|
|
154
|
+
</div>
|
|
155
|
+
<div>
|
|
156
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
|
|
157
|
+
<select id="assign-user-role" class="w-full border rounded px-3 py-2">
|
|
158
|
+
<option value="member">member</option>
|
|
159
|
+
<option value="admin">admin</option>
|
|
160
|
+
<option value="viewer">viewer</option>
|
|
161
|
+
</select>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="flex items-end">
|
|
164
|
+
<button id="btn-assign-user" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" disabled>Assign User</button>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
143
169
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mt-4">
|
|
144
170
|
<div>
|
|
145
171
|
<label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
|
|
@@ -275,6 +301,78 @@
|
|
|
275
301
|
</div>
|
|
276
302
|
</div>
|
|
277
303
|
|
|
304
|
+
<!-- Create Organization Modal -->
|
|
305
|
+
<div id="modal-create-org" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden items-center justify-center z-50">
|
|
306
|
+
<div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
|
|
307
|
+
<div class="px-6 py-4 border-b border-gray-200">
|
|
308
|
+
<h3 class="text-lg font-medium text-gray-900">Create New Organization</h3>
|
|
309
|
+
</div>
|
|
310
|
+
<div class="px-6 py-4">
|
|
311
|
+
<div class="space-y-4">
|
|
312
|
+
<div>
|
|
313
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Name *</label>
|
|
314
|
+
<input id="create-org-name" type="text" class="w-full border rounded px-3 py-2" placeholder="Organization name" maxlength="100">
|
|
315
|
+
<div class="text-xs text-gray-500 mt-1">2-100 characters. Slug will be auto-generated.</div>
|
|
316
|
+
</div>
|
|
317
|
+
<div>
|
|
318
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
|
319
|
+
<textarea id="create-org-description" class="w-full border rounded px-3 py-2" rows="3" placeholder="Optional description" maxlength="500"></textarea>
|
|
320
|
+
<div class="text-xs text-gray-500 mt-1">Maximum 500 characters.</div>
|
|
321
|
+
</div>
|
|
322
|
+
<div>
|
|
323
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Owner User ID</label>
|
|
324
|
+
<input id="create-org-owner" type="text" class="w-full border rounded px-3 py-2" placeholder="Leave empty to use first admin">
|
|
325
|
+
<div class="text-xs text-gray-500 mt-1">Optional. Leave empty to assign to first admin user.</div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
|
|
330
|
+
<button id="btn-create-org-cancel" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Cancel</button>
|
|
331
|
+
<button id="btn-create-org-submit" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">Create</button>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<!-- Edit Organization Modal -->
|
|
337
|
+
<div id="modal-edit-org" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden items-center justify-center z-50">
|
|
338
|
+
<div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
|
|
339
|
+
<div class="px-6 py-4 border-b border-gray-200">
|
|
340
|
+
<h3 class="text-lg font-medium text-gray-900">Edit Organization</h3>
|
|
341
|
+
</div>
|
|
342
|
+
<div class="px-6 py-4">
|
|
343
|
+
<div class="space-y-4">
|
|
344
|
+
<input id="edit-org-id" type="hidden">
|
|
345
|
+
<div>
|
|
346
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Name *</label>
|
|
347
|
+
<input id="edit-org-name" type="text" class="w-full border rounded px-3 py-2" placeholder="Organization name" maxlength="100">
|
|
348
|
+
<div class="text-xs text-gray-500 mt-1">2-100 characters.</div>
|
|
349
|
+
</div>
|
|
350
|
+
<div>
|
|
351
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
|
352
|
+
<textarea id="edit-org-description" class="w-full border rounded px-3 py-2" rows="3" placeholder="Optional description" maxlength="500"></textarea>
|
|
353
|
+
<div class="text-xs text-gray-500 mt-1">Maximum 500 characters.</div>
|
|
354
|
+
</div>
|
|
355
|
+
<div>
|
|
356
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Owner User ID</label>
|
|
357
|
+
<input id="edit-org-owner" type="text" class="w-full border rounded px-3 py-2" placeholder="User ID">
|
|
358
|
+
<div class="text-xs text-gray-500 mt-1">Current owner will be replaced.</div>
|
|
359
|
+
</div>
|
|
360
|
+
<div>
|
|
361
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
|
362
|
+
<select id="edit-org-status" class="w-full border rounded px-3 py-2">
|
|
363
|
+
<option value="active">Active</option>
|
|
364
|
+
<option value="disabled">Disabled</option>
|
|
365
|
+
</select>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
<div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
|
|
370
|
+
<button id="btn-edit-org-cancel" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Cancel</button>
|
|
371
|
+
<button id="btn-edit-org-submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Save Changes</button>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
278
376
|
<!-- Toast Container -->
|
|
279
377
|
<div id="toast-container" class="fixed top-4 right-4 space-y-2 z-50"></div>
|
|
280
378
|
|
|
@@ -379,8 +477,138 @@
|
|
|
379
477
|
orgs: { offset: 0, limit: 25, total: 0, selectedOrgId: null },
|
|
380
478
|
members: { offset: 0, limit: 50, total: 0 },
|
|
381
479
|
invites: { offset: 0, limit: 50, total: 0 },
|
|
480
|
+
availableUsers: [],
|
|
382
481
|
};
|
|
383
482
|
|
|
483
|
+
async function loadAvailableUsers() {
|
|
484
|
+
|
|
485
|
+
console.log('Loading available users...');
|
|
486
|
+
|
|
487
|
+
const orgId = state.orgs.selectedOrgId;
|
|
488
|
+
if (!orgId) {
|
|
489
|
+
console.log('No org selected');
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
// Load all users
|
|
495
|
+
const usersRes = await fetch(`${API_BASE}/api/admin/users`, {
|
|
496
|
+
headers: { 'Accept': 'application/json' },
|
|
497
|
+
credentials: 'same-origin'
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
const usersData = await usersRes.json();
|
|
501
|
+
|
|
502
|
+
if (!usersRes.ok) {
|
|
503
|
+
console.error('Failed to load users:', usersData?.error);
|
|
504
|
+
const select = document.getElementById('assign-user-select');
|
|
505
|
+
if (select) {
|
|
506
|
+
select.innerHTML = `<option value="">Error: ${usersData?.error || 'Unknown error'}</option>`;
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}else{
|
|
510
|
+
console.log('Loaded users:', usersData);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Load current org members
|
|
514
|
+
const membersRes = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/members?limit=500`, {
|
|
515
|
+
headers: { 'Accept': 'application/json' },
|
|
516
|
+
credentials: 'same-origin'
|
|
517
|
+
});
|
|
518
|
+
const membersData = await membersRes.json();
|
|
519
|
+
|
|
520
|
+
if (!membersRes.ok) {
|
|
521
|
+
console.error('Failed to load members:', membersData?.error);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const allUsers = Array.isArray(usersData?.users) ? usersData.users : [];
|
|
526
|
+
const currentMembers = Array.isArray(membersData?.members) ? membersData.members : [];
|
|
527
|
+
const memberUserIds = new Set(currentMembers.map(m => String(m.userId)));
|
|
528
|
+
|
|
529
|
+
// Filter out users who are already members
|
|
530
|
+
const availableUsers = allUsers.filter(user => !memberUserIds.has(String(user._id)));
|
|
531
|
+
state.availableUsers = availableUsers;
|
|
532
|
+
|
|
533
|
+
// Update the select dropdown
|
|
534
|
+
const select = document.getElementById('assign-user-select');
|
|
535
|
+
if (select) {
|
|
536
|
+
select.innerHTML = '<option value="">Select a user...</option>';
|
|
537
|
+
availableUsers.forEach(user => {
|
|
538
|
+
const option = document.createElement('option');
|
|
539
|
+
option.value = user._id;
|
|
540
|
+
option.textContent = `${user.name || 'No name'} (${user.email})`;
|
|
541
|
+
select.appendChild(option);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Enable/disable assign button based on availability
|
|
546
|
+
const assignBtn = document.getElementById('btn-assign-user');
|
|
547
|
+
if (assignBtn) {
|
|
548
|
+
assignBtn.disabled = availableUsers.length === 0;
|
|
549
|
+
if (availableUsers.length === 0) {
|
|
550
|
+
select.innerHTML = '<option value="">No available users</option>';
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
} catch (e) {
|
|
554
|
+
console.error('Failed to load available users:', e);
|
|
555
|
+
const select = document.getElementById('assign-user-select');
|
|
556
|
+
if (select) {
|
|
557
|
+
select.innerHTML = '<option value="">Failed to load users</option>';
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function assignUserToOrg() {
|
|
563
|
+
const orgId = state.orgs.selectedOrgId;
|
|
564
|
+
const userId = document.getElementById('assign-user-select')?.value;
|
|
565
|
+
const role = document.getElementById('assign-user-role')?.value;
|
|
566
|
+
|
|
567
|
+
if (!orgId) {
|
|
568
|
+
showToast('No organization selected', 'error');
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (!userId) {
|
|
573
|
+
showToast('Please select a user', 'error');
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!role) {
|
|
578
|
+
showToast('Please select a role', 'error');
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const userName = state.availableUsers.find(u => u._id === userId)?.name || userId;
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/members`, {
|
|
586
|
+
method: 'POST',
|
|
587
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
588
|
+
credentials: 'same-origin',
|
|
589
|
+
body: JSON.stringify({ userId, role }),
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const data = await res.json();
|
|
593
|
+
|
|
594
|
+
if (!res.ok) {
|
|
595
|
+
showToast(data?.error || 'Failed to assign user', 'error');
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
showToast(data?.message || 'User assigned successfully', 'success');
|
|
600
|
+
|
|
601
|
+
// Reset form
|
|
602
|
+
document.getElementById('assign-user-select').value = '';
|
|
603
|
+
document.getElementById('assign-user-role').value = 'member';
|
|
604
|
+
|
|
605
|
+
// Reload members list and available users
|
|
606
|
+
await Promise.all([loadMembers(), loadAvailableUsers()]);
|
|
607
|
+
} catch (e) {
|
|
608
|
+
showToast(e.message || 'Failed to assign user', 'error');
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
384
612
|
async function loadOrgs() {
|
|
385
613
|
const q = document.getElementById('orgs-q')?.value?.trim();
|
|
386
614
|
const status = document.getElementById('orgs-status')?.value?.trim();
|
|
@@ -400,7 +628,10 @@
|
|
|
400
628
|
limit,
|
|
401
629
|
offset: state.orgs.offset,
|
|
402
630
|
})}`;
|
|
403
|
-
const res = await fetch(url, {
|
|
631
|
+
const res = await fetch(url, {
|
|
632
|
+
headers: { 'Accept': 'application/json' },
|
|
633
|
+
credentials: 'same-origin'
|
|
634
|
+
});
|
|
404
635
|
const data = await res.json();
|
|
405
636
|
|
|
406
637
|
if (!res.ok) {
|
|
@@ -434,7 +665,7 @@
|
|
|
434
665
|
if (orgs.length === 0) {
|
|
435
666
|
tbody.innerHTML = `
|
|
436
667
|
<tr>
|
|
437
|
-
<td class="px-4 py-6 text-sm text-gray-600" colspan="
|
|
668
|
+
<td class="px-4 py-6 text-sm text-gray-600" colspan="4">No organizations found.</td>
|
|
438
669
|
</tr>
|
|
439
670
|
`;
|
|
440
671
|
return;
|
|
@@ -445,13 +676,30 @@
|
|
|
445
676
|
const rowClass = isSelected ? 'bg-blue-50' : 'bg-white';
|
|
446
677
|
|
|
447
678
|
return `
|
|
448
|
-
<tr class="${rowClass} hover:bg-gray-50
|
|
449
|
-
<td class="px-4 py-3 text-sm text-gray-900">
|
|
679
|
+
<tr class="${rowClass} hover:bg-gray-50" data-org-id="${escapeHtml(o?._id)}">
|
|
680
|
+
<td class="px-4 py-3 text-sm text-gray-900 cursor-pointer" onclick="selectOrg('${escapeHtml(o?._id)}')">
|
|
450
681
|
<div class="font-medium">${escapeHtml(o?.name)}</div>
|
|
451
682
|
<div class="text-xs text-gray-500">${escapeHtml(o?.ownerUserId)}</div>
|
|
452
683
|
</td>
|
|
453
|
-
<td class="px-4 py-3 text-sm text-gray-700">${escapeHtml(o?.slug)}</td>
|
|
454
|
-
<td class="px-4 py-3 text-sm text-gray-700"
|
|
684
|
+
<td class="px-4 py-3 text-sm text-gray-700 cursor-pointer" onclick="selectOrg('${escapeHtml(o?._id)}')">${escapeHtml(o?.slug)}</td>
|
|
685
|
+
<td class="px-4 py-3 text-sm text-gray-700 cursor-pointer" onclick="selectOrg('${escapeHtml(o?._id)}')">
|
|
686
|
+
<span class="px-2 py-1 text-xs rounded-full ${
|
|
687
|
+
o?.status === 'active'
|
|
688
|
+
? 'bg-green-100 text-green-800'
|
|
689
|
+
: 'bg-red-100 text-red-800'
|
|
690
|
+
}">
|
|
691
|
+
${escapeHtml(o?.status)}
|
|
692
|
+
</span>
|
|
693
|
+
</td>
|
|
694
|
+
<td class="px-4 py-3 text-sm text-gray-700 whitespace-nowrap">
|
|
695
|
+
<button class="text-blue-600 hover:text-blue-800 mr-2" data-edit="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}" data-description="${escapeHtml(o?.description || '')}" data-owner="${escapeHtml(o?.ownerUserId)}" data-status="${escapeHtml(o?.status)}">Edit</button>
|
|
696
|
+
<button class="text-gray-600 hover:text-gray-800 mr-2" data-copy="${escapeHtml(o?._id)}" title="Copy orgId">Copy ID</button>
|
|
697
|
+
${o?.status === 'active'
|
|
698
|
+
? `<button class="text-orange-600 hover:text-orange-800 mr-2" data-disable="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}">Disable</button>`
|
|
699
|
+
: `<button class="text-green-600 hover:text-green-800 mr-2" data-enable="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}">Enable</button>`
|
|
700
|
+
}
|
|
701
|
+
<button class="text-red-600 hover:text-red-800" data-delete="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}">Delete</button>
|
|
702
|
+
</td>
|
|
455
703
|
</tr>
|
|
456
704
|
`;
|
|
457
705
|
}).join('');
|
|
@@ -474,6 +722,7 @@
|
|
|
474
722
|
const ids = [
|
|
475
723
|
'btn-selected-refresh',
|
|
476
724
|
'btn-members-refresh',
|
|
725
|
+
'btn-assign-user',
|
|
477
726
|
'btn-members-apply',
|
|
478
727
|
'btn-members-reset',
|
|
479
728
|
'btn-members-prev',
|
|
@@ -498,7 +747,7 @@
|
|
|
498
747
|
state.invites.offset = 0;
|
|
499
748
|
|
|
500
749
|
setSelectedControlsEnabled(true);
|
|
501
|
-
await Promise.all([loadSelectedOrg(), loadMembers(), loadInvites()]);
|
|
750
|
+
await Promise.all([loadSelectedOrg(), loadMembers(), loadInvites(), loadAvailableUsers()]);
|
|
502
751
|
await loadOrgs();
|
|
503
752
|
}
|
|
504
753
|
|
|
@@ -511,7 +760,10 @@
|
|
|
511
760
|
}
|
|
512
761
|
|
|
513
762
|
try {
|
|
514
|
-
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}`, {
|
|
763
|
+
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}`, {
|
|
764
|
+
headers: { 'Accept': 'application/json' },
|
|
765
|
+
credentials: 'same-origin'
|
|
766
|
+
});
|
|
515
767
|
const data = await res.json();
|
|
516
768
|
|
|
517
769
|
if (!res.ok) {
|
|
@@ -562,7 +814,10 @@
|
|
|
562
814
|
limit: state.members.limit,
|
|
563
815
|
offset: state.members.offset,
|
|
564
816
|
})}`;
|
|
565
|
-
const res = await fetch(url, {
|
|
817
|
+
const res = await fetch(url, {
|
|
818
|
+
headers: { 'Accept': 'application/json' },
|
|
819
|
+
credentials: 'same-origin'
|
|
820
|
+
});
|
|
566
821
|
const data = await res.json();
|
|
567
822
|
|
|
568
823
|
if (!res.ok) {
|
|
@@ -655,6 +910,7 @@
|
|
|
655
910
|
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/members/${encodeURIComponent(memberId)}`, {
|
|
656
911
|
method: 'PATCH',
|
|
657
912
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
913
|
+
credentials: 'same-origin',
|
|
658
914
|
body: JSON.stringify({ role }),
|
|
659
915
|
});
|
|
660
916
|
const data = await res.json();
|
|
@@ -680,6 +936,7 @@
|
|
|
680
936
|
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/members/${encodeURIComponent(memberId)}`, {
|
|
681
937
|
method: 'DELETE',
|
|
682
938
|
headers: { 'Accept': 'application/json' },
|
|
939
|
+
credentials: 'same-origin'
|
|
683
940
|
});
|
|
684
941
|
const data = await res.json();
|
|
685
942
|
if (!res.ok) {
|
|
@@ -722,7 +979,10 @@
|
|
|
722
979
|
limit: state.invites.limit,
|
|
723
980
|
offset: state.invites.offset,
|
|
724
981
|
})}`;
|
|
725
|
-
const res = await fetch(url, {
|
|
982
|
+
const res = await fetch(url, {
|
|
983
|
+
headers: { 'Accept': 'application/json' },
|
|
984
|
+
credentials: 'same-origin'
|
|
985
|
+
});
|
|
726
986
|
const data = await res.json();
|
|
727
987
|
|
|
728
988
|
if (!res.ok) {
|
|
@@ -804,6 +1064,7 @@
|
|
|
804
1064
|
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/invites/${encodeURIComponent(inviteId)}`, {
|
|
805
1065
|
method: 'DELETE',
|
|
806
1066
|
headers: { 'Accept': 'application/json' },
|
|
1067
|
+
credentials: 'same-origin'
|
|
807
1068
|
});
|
|
808
1069
|
const data = await res.json();
|
|
809
1070
|
if (!res.ok) {
|
|
@@ -828,6 +1089,7 @@
|
|
|
828
1089
|
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/invites/${encodeURIComponent(inviteId)}/resend`, {
|
|
829
1090
|
method: 'POST',
|
|
830
1091
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
1092
|
+
credentials: 'same-origin',
|
|
831
1093
|
body: JSON.stringify({}),
|
|
832
1094
|
});
|
|
833
1095
|
const data = await res.json();
|
|
@@ -866,6 +1128,7 @@
|
|
|
866
1128
|
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/invites`, {
|
|
867
1129
|
method: 'POST',
|
|
868
1130
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
1131
|
+
credentials: 'same-origin',
|
|
869
1132
|
body: JSON.stringify({ email, role, expiresInDays }),
|
|
870
1133
|
});
|
|
871
1134
|
const data = await res.json();
|
|
@@ -911,12 +1174,51 @@
|
|
|
911
1174
|
const nextOrgsBtn = document.getElementById('btn-orgs-next');
|
|
912
1175
|
if (nextOrgsBtn) nextOrgsBtn.onclick = () => { state.orgs.offset = Math.max(0, state.orgs.offset + state.orgs.limit); loadOrgs(); };
|
|
913
1176
|
|
|
1177
|
+
// CRUD event handlers
|
|
1178
|
+
const createOrgBtn = document.getElementById('btn-create-org');
|
|
1179
|
+
if (createOrgBtn) createOrgBtn.onclick = () => openCreateOrgModal();
|
|
1180
|
+
|
|
1181
|
+
const createOrgCancelBtn = document.getElementById('btn-create-org-cancel');
|
|
1182
|
+
if (createOrgCancelBtn) createOrgCancelBtn.onclick = () => closeCreateOrgModal();
|
|
1183
|
+
|
|
1184
|
+
const createOrgSubmitBtn = document.getElementById('btn-create-org-submit');
|
|
1185
|
+
if (createOrgSubmitBtn) createOrgSubmitBtn.onclick = () => createOrganization();
|
|
1186
|
+
|
|
1187
|
+
const editOrgCancelBtn = document.getElementById('btn-edit-org-cancel');
|
|
1188
|
+
if (editOrgCancelBtn) editOrgCancelBtn.onclick = () => closeEditOrgModal();
|
|
1189
|
+
|
|
1190
|
+
const editOrgSubmitBtn = document.getElementById('btn-edit-org-submit');
|
|
1191
|
+
if (editOrgSubmitBtn) editOrgSubmitBtn.onclick = () => updateOrganization();
|
|
1192
|
+
|
|
1193
|
+
// Organization action buttons (will be added dynamically when table loads)
|
|
1194
|
+
document.addEventListener('click', (e) => {
|
|
1195
|
+
if (e.target.matches('[data-edit]')) {
|
|
1196
|
+
const btn = e.target;
|
|
1197
|
+
openEditOrgModal(btn.dataset.edit, btn.dataset.name, btn.dataset.description, btn.dataset.owner, btn.dataset.status);
|
|
1198
|
+
} else if (e.target.matches('[data-copy]')) {
|
|
1199
|
+
const btn = e.target;
|
|
1200
|
+
copyText(btn.dataset.copy);
|
|
1201
|
+
} else if (e.target.matches('[data-disable]')) {
|
|
1202
|
+
const btn = e.target;
|
|
1203
|
+
disableOrganization(btn.dataset.disable, btn.dataset.name);
|
|
1204
|
+
} else if (e.target.matches('[data-enable]')) {
|
|
1205
|
+
const btn = e.target;
|
|
1206
|
+
enableOrganization(btn.dataset.enable, btn.dataset.name);
|
|
1207
|
+
} else if (e.target.matches('[data-delete]')) {
|
|
1208
|
+
const btn = e.target;
|
|
1209
|
+
deleteOrganization(btn.dataset.delete, btn.dataset.name);
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
|
|
914
1213
|
const selectedRefreshBtn = document.getElementById('btn-selected-refresh');
|
|
915
1214
|
if (selectedRefreshBtn) selectedRefreshBtn.onclick = () => Promise.all([loadSelectedOrg(), loadMembers(), loadInvites()]);
|
|
916
1215
|
|
|
917
1216
|
const membersRefreshBtn = document.getElementById('btn-members-refresh');
|
|
918
1217
|
if (membersRefreshBtn) membersRefreshBtn.onclick = () => loadMembers();
|
|
919
1218
|
|
|
1219
|
+
const assignUserBtn = document.getElementById('btn-assign-user');
|
|
1220
|
+
if (assignUserBtn) assignUserBtn.onclick = () => assignUserToOrg();
|
|
1221
|
+
|
|
920
1222
|
const membersApplyBtn = document.getElementById('btn-members-apply');
|
|
921
1223
|
if (membersApplyBtn) membersApplyBtn.onclick = () => { state.members.offset = 0; loadMembers(); };
|
|
922
1224
|
|
|
@@ -969,6 +1271,222 @@
|
|
|
969
1271
|
setSelectedControlsEnabled(false);
|
|
970
1272
|
}
|
|
971
1273
|
|
|
1274
|
+
// Modal functions
|
|
1275
|
+
function openCreateOrgModal() {
|
|
1276
|
+
document.getElementById('modal-create-org').classList.remove('hidden');
|
|
1277
|
+
document.getElementById('modal-create-org').classList.add('flex');
|
|
1278
|
+
// Clear form
|
|
1279
|
+
document.getElementById('create-org-name').value = '';
|
|
1280
|
+
document.getElementById('create-org-description').value = '';
|
|
1281
|
+
document.getElementById('create-org-owner').value = '';
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function closeCreateOrgModal() {
|
|
1285
|
+
document.getElementById('modal-create-org').classList.add('hidden');
|
|
1286
|
+
document.getElementById('modal-create-org').classList.remove('flex');
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function openEditOrgModal(orgId, name, description, owner, status) {
|
|
1290
|
+
document.getElementById('modal-edit-org').classList.remove('hidden');
|
|
1291
|
+
document.getElementById('modal-edit-org').classList.add('flex');
|
|
1292
|
+
// Populate form
|
|
1293
|
+
document.getElementById('edit-org-id').value = orgId;
|
|
1294
|
+
document.getElementById('edit-org-name').value = name || '';
|
|
1295
|
+
document.getElementById('edit-org-description').value = description || '';
|
|
1296
|
+
document.getElementById('edit-org-owner').value = owner || '';
|
|
1297
|
+
document.getElementById('edit-org-status').value = status || 'active';
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function closeEditOrgModal() {
|
|
1301
|
+
document.getElementById('modal-edit-org').classList.add('hidden');
|
|
1302
|
+
document.getElementById('modal-edit-org').classList.remove('flex');
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// CRUD operations
|
|
1306
|
+
async function createOrganization() {
|
|
1307
|
+
const name = document.getElementById('create-org-name').value.trim();
|
|
1308
|
+
const description = document.getElementById('create-org-description').value.trim();
|
|
1309
|
+
const ownerUserId = document.getElementById('create-org-owner').value.trim();
|
|
1310
|
+
|
|
1311
|
+
if (!name) {
|
|
1312
|
+
showToast('Organization name is required', 'error');
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (name.length < 2) {
|
|
1317
|
+
showToast('Name must be at least 2 characters', 'error');
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
try {
|
|
1322
|
+
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}`, {
|
|
1323
|
+
method: 'POST',
|
|
1324
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1325
|
+
credentials: 'same-origin',
|
|
1326
|
+
body: JSON.stringify({
|
|
1327
|
+
name,
|
|
1328
|
+
description: description || undefined,
|
|
1329
|
+
ownerUserId: ownerUserId || undefined
|
|
1330
|
+
}),
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
const data = await res.json();
|
|
1334
|
+
if (!res.ok) {
|
|
1335
|
+
showToast(data?.error || 'Failed to create organization', 'error');
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
showToast('Organization created successfully', 'success');
|
|
1340
|
+
closeCreateOrgModal();
|
|
1341
|
+
await loadOrgs();
|
|
1342
|
+
} catch (e) {
|
|
1343
|
+
showToast(e.message || 'Failed to create organization', 'error');
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
async function updateOrganization() {
|
|
1348
|
+
const orgId = document.getElementById('edit-org-id').value;
|
|
1349
|
+
const name = document.getElementById('edit-org-name').value.trim();
|
|
1350
|
+
const description = document.getElementById('edit-org-description').value.trim();
|
|
1351
|
+
const ownerUserId = document.getElementById('edit-org-owner').value.trim();
|
|
1352
|
+
const status = document.getElementById('edit-org-status').value;
|
|
1353
|
+
|
|
1354
|
+
if (!name) {
|
|
1355
|
+
showToast('Organization name is required', 'error');
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (name.length < 2) {
|
|
1360
|
+
showToast('Name must be at least 2 characters', 'error');
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
try {
|
|
1365
|
+
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}`, {
|
|
1366
|
+
method: 'PUT',
|
|
1367
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1368
|
+
credentials: 'same-origin',
|
|
1369
|
+
body: JSON.stringify({
|
|
1370
|
+
name,
|
|
1371
|
+
description: description || undefined,
|
|
1372
|
+
ownerUserId: ownerUserId || undefined,
|
|
1373
|
+
status
|
|
1374
|
+
}),
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
const data = await res.json();
|
|
1378
|
+
if (!res.ok) {
|
|
1379
|
+
showToast(data?.error || 'Failed to update organization', 'error');
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
showToast('Organization updated successfully', 'success');
|
|
1384
|
+
closeEditOrgModal();
|
|
1385
|
+
await loadOrgs();
|
|
1386
|
+
if (state.orgs.selectedOrgId === orgId) {
|
|
1387
|
+
await loadSelectedOrg();
|
|
1388
|
+
}
|
|
1389
|
+
} catch (e) {
|
|
1390
|
+
showToast(e.message || 'Failed to update organization', 'error');
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
async function disableOrganization(orgId, orgName) {
|
|
1395
|
+
if (!confirm(`Are you sure you want to disable "${orgName}"?`)) return;
|
|
1396
|
+
|
|
1397
|
+
try {
|
|
1398
|
+
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/disable`, {
|
|
1399
|
+
method: 'PATCH',
|
|
1400
|
+
credentials: 'same-origin'
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
const data = await res.json();
|
|
1404
|
+
if (!res.ok) {
|
|
1405
|
+
showToast(data?.error || 'Failed to disable organization', 'error');
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
showToast('Organization disabled successfully', 'success');
|
|
1410
|
+
await loadOrgs();
|
|
1411
|
+
if (state.orgs.selectedOrgId === orgId) {
|
|
1412
|
+
await loadSelectedOrg();
|
|
1413
|
+
}
|
|
1414
|
+
} catch (e) {
|
|
1415
|
+
showToast(e.message || 'Failed to disable organization', 'error');
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
async function enableOrganization(orgId, orgName) {
|
|
1420
|
+
if (!confirm(`Are you sure you want to enable "${orgName}"?`)) return;
|
|
1421
|
+
|
|
1422
|
+
try {
|
|
1423
|
+
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/enable`, {
|
|
1424
|
+
method: 'PATCH',
|
|
1425
|
+
credentials: 'same-origin'
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
const data = await res.json();
|
|
1429
|
+
if (!res.ok) {
|
|
1430
|
+
showToast(data?.error || 'Failed to enable organization', 'error');
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
showToast('Organization enabled successfully', 'success');
|
|
1435
|
+
await loadOrgs();
|
|
1436
|
+
if (state.orgs.selectedOrgId === orgId) {
|
|
1437
|
+
await loadSelectedOrg();
|
|
1438
|
+
}
|
|
1439
|
+
} catch (e) {
|
|
1440
|
+
showToast(e.message || 'Failed to enable organization', 'error');
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
async function deleteOrganization(orgId, orgName) {
|
|
1445
|
+
const confirm1 = confirm(`Are you sure you want to delete "${orgName}"?`);
|
|
1446
|
+
if (!confirm1) return;
|
|
1447
|
+
|
|
1448
|
+
const confirm2 = confirm(
|
|
1449
|
+
'WARNING: This will permanently delete the organization and ALL its data including:\n' +
|
|
1450
|
+
'• All organization members\n' +
|
|
1451
|
+
'• All pending invites\n' +
|
|
1452
|
+
'• All associated assets and files\n' +
|
|
1453
|
+
'• All notifications and activity\n\n' +
|
|
1454
|
+
'This action cannot be undone. Continue?'
|
|
1455
|
+
);
|
|
1456
|
+
if (!confirm2) return;
|
|
1457
|
+
|
|
1458
|
+
try {
|
|
1459
|
+
showToast('Deleting organization and cleaning up data...', 'success');
|
|
1460
|
+
|
|
1461
|
+
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}`, {
|
|
1462
|
+
method: 'DELETE',
|
|
1463
|
+
credentials: 'same-origin'
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
const data = await res.json();
|
|
1467
|
+
if (!res.ok) {
|
|
1468
|
+
showToast(data?.error || 'Failed to delete organization', 'error');
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
showToast(`Organization "${orgName}" deleted permanently`, 'success');
|
|
1473
|
+
|
|
1474
|
+
// Clear selection if this org was selected
|
|
1475
|
+
if (state.orgs.selectedOrgId === orgId) {
|
|
1476
|
+
state.orgs.selectedOrgId = null;
|
|
1477
|
+
setSelectedControlsEnabled(false);
|
|
1478
|
+
document.getElementById('selected-org-subtitle').textContent = 'Select an org from the list';
|
|
1479
|
+
document.getElementById('kpi-members').textContent = '-';
|
|
1480
|
+
document.getElementById('kpi-invites').textContent = '-';
|
|
1481
|
+
document.getElementById('kpi-org-status').textContent = '-';
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
await loadOrgs();
|
|
1485
|
+
} catch (e) {
|
|
1486
|
+
showToast(e.message || 'Failed to delete organization', 'error');
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
972
1490
|
bindEvents();
|
|
973
1491
|
loadOrgs();
|
|
974
1492
|
</script>
|