@intranefr/superbackend 1.4.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +6 -1
- package/README.md +5 -5
- package/index.js +23 -5
- package/package.json +5 -2
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/error-tracking/browser/package.json +4 -3
- package/sdk/error-tracking/browser/src/embed.js +29 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/controllers/admin.controller.js +139 -1
- package/src/controllers/adminHeadless.controller.js +82 -0
- package/src/controllers/adminMigration.controller.js +5 -1
- package/src/controllers/adminScripts.controller.js +229 -0
- 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/orgAdmin.controller.js +286 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware.js +119 -0
- package/src/models/HeadlessModelDefinition.js +10 -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 +2 -0
- package/src/routes/adminHeadless.routes.js +6 -0
- package/src/routes/adminScripts.routes.js +21 -0
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +29 -0
- package/src/routes/llmUi.routes.js +26 -0
- package/src/routes/orgAdmin.routes.js +5 -0
- package/src/routes/uiComponentsPublic.routes.js +9 -0
- package/src/services/consoleOverride.service.js +291 -0
- package/src/services/email.service.js +17 -1
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- 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 +312 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- package/src/services/webhook.service.js +2 -2
- package/src/services/workflow.service.js +1 -1
- package/src/utils/encryption.js +5 -3
- package/views/admin-coolify-deploy.ejs +1 -1
- package/views/admin-dashboard-home.ejs +1 -1
- package/views/admin-dashboard.ejs +1 -1
- package/views/admin-errors.ejs +2 -2
- package/views/admin-global-settings.ejs +3 -3
- package/views/admin-headless.ejs +294 -24
- package/views/admin-json-configs.ejs +8 -1
- package/views/admin-llm.ejs +2 -2
- package/views/admin-organizations.ejs +365 -9
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-seo-config.ejs +1 -1
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-test.ejs +3 -3
- package/views/admin-ui-components.ejs +709 -0
- package/views/admin-users.ejs +440 -4
- package/views/admin-webhooks.ejs +1 -1
- package/views/admin-workflows.ejs +1 -1
- package/views/partials/admin-assets-script.ejs +3 -3
- package/views/partials/dashboard/nav-items.ejs +3 -0
- package/views/partials/dashboard/palette.ejs +1 -1
|
@@ -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>
|
|
@@ -275,6 +277,78 @@
|
|
|
275
277
|
</div>
|
|
276
278
|
</div>
|
|
277
279
|
|
|
280
|
+
<!-- Create Organization Modal -->
|
|
281
|
+
<div id="modal-create-org" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden items-center justify-center z-50">
|
|
282
|
+
<div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
|
|
283
|
+
<div class="px-6 py-4 border-b border-gray-200">
|
|
284
|
+
<h3 class="text-lg font-medium text-gray-900">Create New Organization</h3>
|
|
285
|
+
</div>
|
|
286
|
+
<div class="px-6 py-4">
|
|
287
|
+
<div class="space-y-4">
|
|
288
|
+
<div>
|
|
289
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Name *</label>
|
|
290
|
+
<input id="create-org-name" type="text" class="w-full border rounded px-3 py-2" placeholder="Organization name" maxlength="100">
|
|
291
|
+
<div class="text-xs text-gray-500 mt-1">2-100 characters. Slug will be auto-generated.</div>
|
|
292
|
+
</div>
|
|
293
|
+
<div>
|
|
294
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
|
295
|
+
<textarea id="create-org-description" class="w-full border rounded px-3 py-2" rows="3" placeholder="Optional description" maxlength="500"></textarea>
|
|
296
|
+
<div class="text-xs text-gray-500 mt-1">Maximum 500 characters.</div>
|
|
297
|
+
</div>
|
|
298
|
+
<div>
|
|
299
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Owner User ID</label>
|
|
300
|
+
<input id="create-org-owner" type="text" class="w-full border rounded px-3 py-2" placeholder="Leave empty to use first admin">
|
|
301
|
+
<div class="text-xs text-gray-500 mt-1">Optional. Leave empty to assign to first admin user.</div>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
<div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
|
|
306
|
+
<button id="btn-create-org-cancel" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Cancel</button>
|
|
307
|
+
<button id="btn-create-org-submit" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">Create</button>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<!-- Edit Organization Modal -->
|
|
313
|
+
<div id="modal-edit-org" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden items-center justify-center z-50">
|
|
314
|
+
<div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
|
|
315
|
+
<div class="px-6 py-4 border-b border-gray-200">
|
|
316
|
+
<h3 class="text-lg font-medium text-gray-900">Edit Organization</h3>
|
|
317
|
+
</div>
|
|
318
|
+
<div class="px-6 py-4">
|
|
319
|
+
<div class="space-y-4">
|
|
320
|
+
<input id="edit-org-id" type="hidden">
|
|
321
|
+
<div>
|
|
322
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Name *</label>
|
|
323
|
+
<input id="edit-org-name" type="text" class="w-full border rounded px-3 py-2" placeholder="Organization name" maxlength="100">
|
|
324
|
+
<div class="text-xs text-gray-500 mt-1">2-100 characters.</div>
|
|
325
|
+
</div>
|
|
326
|
+
<div>
|
|
327
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
|
328
|
+
<textarea id="edit-org-description" class="w-full border rounded px-3 py-2" rows="3" placeholder="Optional description" maxlength="500"></textarea>
|
|
329
|
+
<div class="text-xs text-gray-500 mt-1">Maximum 500 characters.</div>
|
|
330
|
+
</div>
|
|
331
|
+
<div>
|
|
332
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Owner User ID</label>
|
|
333
|
+
<input id="edit-org-owner" type="text" class="w-full border rounded px-3 py-2" placeholder="User ID">
|
|
334
|
+
<div class="text-xs text-gray-500 mt-1">Current owner will be replaced.</div>
|
|
335
|
+
</div>
|
|
336
|
+
<div>
|
|
337
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
|
338
|
+
<select id="edit-org-status" class="w-full border rounded px-3 py-2">
|
|
339
|
+
<option value="active">Active</option>
|
|
340
|
+
<option value="disabled">Disabled</option>
|
|
341
|
+
</select>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
<div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
|
|
346
|
+
<button id="btn-edit-org-cancel" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Cancel</button>
|
|
347
|
+
<button id="btn-edit-org-submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Save Changes</button>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
278
352
|
<!-- Toast Container -->
|
|
279
353
|
<div id="toast-container" class="fixed top-4 right-4 space-y-2 z-50"></div>
|
|
280
354
|
|
|
@@ -400,7 +474,10 @@
|
|
|
400
474
|
limit,
|
|
401
475
|
offset: state.orgs.offset,
|
|
402
476
|
})}`;
|
|
403
|
-
const res = await fetch(url, {
|
|
477
|
+
const res = await fetch(url, {
|
|
478
|
+
headers: { 'Accept': 'application/json' },
|
|
479
|
+
credentials: 'same-origin'
|
|
480
|
+
});
|
|
404
481
|
const data = await res.json();
|
|
405
482
|
|
|
406
483
|
if (!res.ok) {
|
|
@@ -434,7 +511,7 @@
|
|
|
434
511
|
if (orgs.length === 0) {
|
|
435
512
|
tbody.innerHTML = `
|
|
436
513
|
<tr>
|
|
437
|
-
<td class="px-4 py-6 text-sm text-gray-600" colspan="
|
|
514
|
+
<td class="px-4 py-6 text-sm text-gray-600" colspan="4">No organizations found.</td>
|
|
438
515
|
</tr>
|
|
439
516
|
`;
|
|
440
517
|
return;
|
|
@@ -445,13 +522,29 @@
|
|
|
445
522
|
const rowClass = isSelected ? 'bg-blue-50' : 'bg-white';
|
|
446
523
|
|
|
447
524
|
return `
|
|
448
|
-
<tr class="${rowClass} hover:bg-gray-50
|
|
449
|
-
<td class="px-4 py-3 text-sm text-gray-900">
|
|
525
|
+
<tr class="${rowClass} hover:bg-gray-50" data-org-id="${escapeHtml(o?._id)}">
|
|
526
|
+
<td class="px-4 py-3 text-sm text-gray-900 cursor-pointer" onclick="selectOrg('${escapeHtml(o?._id)}')">
|
|
450
527
|
<div class="font-medium">${escapeHtml(o?.name)}</div>
|
|
451
528
|
<div class="text-xs text-gray-500">${escapeHtml(o?.ownerUserId)}</div>
|
|
452
529
|
</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"
|
|
530
|
+
<td class="px-4 py-3 text-sm text-gray-700 cursor-pointer" onclick="selectOrg('${escapeHtml(o?._id)}')">${escapeHtml(o?.slug)}</td>
|
|
531
|
+
<td class="px-4 py-3 text-sm text-gray-700 cursor-pointer" onclick="selectOrg('${escapeHtml(o?._id)}')">
|
|
532
|
+
<span class="px-2 py-1 text-xs rounded-full ${
|
|
533
|
+
o?.status === 'active'
|
|
534
|
+
? 'bg-green-100 text-green-800'
|
|
535
|
+
: 'bg-red-100 text-red-800'
|
|
536
|
+
}">
|
|
537
|
+
${escapeHtml(o?.status)}
|
|
538
|
+
</span>
|
|
539
|
+
</td>
|
|
540
|
+
<td class="px-4 py-3 text-sm text-gray-700 whitespace-nowrap">
|
|
541
|
+
<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>
|
|
542
|
+
${o?.status === 'active'
|
|
543
|
+
? `<button class="text-orange-600 hover:text-orange-800 mr-2" data-disable="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}">Disable</button>`
|
|
544
|
+
: `<button class="text-green-600 hover:text-green-800 mr-2" data-enable="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}">Enable</button>`
|
|
545
|
+
}
|
|
546
|
+
<button class="text-red-600 hover:text-red-800" data-delete="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}">Delete</button>
|
|
547
|
+
</td>
|
|
455
548
|
</tr>
|
|
456
549
|
`;
|
|
457
550
|
}).join('');
|
|
@@ -511,7 +604,10 @@
|
|
|
511
604
|
}
|
|
512
605
|
|
|
513
606
|
try {
|
|
514
|
-
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}`, {
|
|
607
|
+
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}`, {
|
|
608
|
+
headers: { 'Accept': 'application/json' },
|
|
609
|
+
credentials: 'same-origin'
|
|
610
|
+
});
|
|
515
611
|
const data = await res.json();
|
|
516
612
|
|
|
517
613
|
if (!res.ok) {
|
|
@@ -562,7 +658,10 @@
|
|
|
562
658
|
limit: state.members.limit,
|
|
563
659
|
offset: state.members.offset,
|
|
564
660
|
})}`;
|
|
565
|
-
const res = await fetch(url, {
|
|
661
|
+
const res = await fetch(url, {
|
|
662
|
+
headers: { 'Accept': 'application/json' },
|
|
663
|
+
credentials: 'same-origin'
|
|
664
|
+
});
|
|
566
665
|
const data = await res.json();
|
|
567
666
|
|
|
568
667
|
if (!res.ok) {
|
|
@@ -655,6 +754,7 @@
|
|
|
655
754
|
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/members/${encodeURIComponent(memberId)}`, {
|
|
656
755
|
method: 'PATCH',
|
|
657
756
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
757
|
+
credentials: 'same-origin',
|
|
658
758
|
body: JSON.stringify({ role }),
|
|
659
759
|
});
|
|
660
760
|
const data = await res.json();
|
|
@@ -680,6 +780,7 @@
|
|
|
680
780
|
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/members/${encodeURIComponent(memberId)}`, {
|
|
681
781
|
method: 'DELETE',
|
|
682
782
|
headers: { 'Accept': 'application/json' },
|
|
783
|
+
credentials: 'same-origin'
|
|
683
784
|
});
|
|
684
785
|
const data = await res.json();
|
|
685
786
|
if (!res.ok) {
|
|
@@ -722,7 +823,10 @@
|
|
|
722
823
|
limit: state.invites.limit,
|
|
723
824
|
offset: state.invites.offset,
|
|
724
825
|
})}`;
|
|
725
|
-
const res = await fetch(url, {
|
|
826
|
+
const res = await fetch(url, {
|
|
827
|
+
headers: { 'Accept': 'application/json' },
|
|
828
|
+
credentials: 'same-origin'
|
|
829
|
+
});
|
|
726
830
|
const data = await res.json();
|
|
727
831
|
|
|
728
832
|
if (!res.ok) {
|
|
@@ -804,6 +908,7 @@
|
|
|
804
908
|
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/invites/${encodeURIComponent(inviteId)}`, {
|
|
805
909
|
method: 'DELETE',
|
|
806
910
|
headers: { 'Accept': 'application/json' },
|
|
911
|
+
credentials: 'same-origin'
|
|
807
912
|
});
|
|
808
913
|
const data = await res.json();
|
|
809
914
|
if (!res.ok) {
|
|
@@ -828,6 +933,7 @@
|
|
|
828
933
|
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/invites/${encodeURIComponent(inviteId)}/resend`, {
|
|
829
934
|
method: 'POST',
|
|
830
935
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
936
|
+
credentials: 'same-origin',
|
|
831
937
|
body: JSON.stringify({}),
|
|
832
938
|
});
|
|
833
939
|
const data = await res.json();
|
|
@@ -866,6 +972,7 @@
|
|
|
866
972
|
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/invites`, {
|
|
867
973
|
method: 'POST',
|
|
868
974
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
975
|
+
credentials: 'same-origin',
|
|
869
976
|
body: JSON.stringify({ email, role, expiresInDays }),
|
|
870
977
|
});
|
|
871
978
|
const data = await res.json();
|
|
@@ -911,6 +1018,39 @@
|
|
|
911
1018
|
const nextOrgsBtn = document.getElementById('btn-orgs-next');
|
|
912
1019
|
if (nextOrgsBtn) nextOrgsBtn.onclick = () => { state.orgs.offset = Math.max(0, state.orgs.offset + state.orgs.limit); loadOrgs(); };
|
|
913
1020
|
|
|
1021
|
+
// CRUD event handlers
|
|
1022
|
+
const createOrgBtn = document.getElementById('btn-create-org');
|
|
1023
|
+
if (createOrgBtn) createOrgBtn.onclick = () => openCreateOrgModal();
|
|
1024
|
+
|
|
1025
|
+
const createOrgCancelBtn = document.getElementById('btn-create-org-cancel');
|
|
1026
|
+
if (createOrgCancelBtn) createOrgCancelBtn.onclick = () => closeCreateOrgModal();
|
|
1027
|
+
|
|
1028
|
+
const createOrgSubmitBtn = document.getElementById('btn-create-org-submit');
|
|
1029
|
+
if (createOrgSubmitBtn) createOrgSubmitBtn.onclick = () => createOrganization();
|
|
1030
|
+
|
|
1031
|
+
const editOrgCancelBtn = document.getElementById('btn-edit-org-cancel');
|
|
1032
|
+
if (editOrgCancelBtn) editOrgCancelBtn.onclick = () => closeEditOrgModal();
|
|
1033
|
+
|
|
1034
|
+
const editOrgSubmitBtn = document.getElementById('btn-edit-org-submit');
|
|
1035
|
+
if (editOrgSubmitBtn) editOrgSubmitBtn.onclick = () => updateOrganization();
|
|
1036
|
+
|
|
1037
|
+
// Organization action buttons (will be added dynamically when table loads)
|
|
1038
|
+
document.addEventListener('click', (e) => {
|
|
1039
|
+
if (e.target.matches('[data-edit]')) {
|
|
1040
|
+
const btn = e.target;
|
|
1041
|
+
openEditOrgModal(btn.dataset.edit, btn.dataset.name, btn.dataset.description, btn.dataset.owner, btn.dataset.status);
|
|
1042
|
+
} else if (e.target.matches('[data-disable]')) {
|
|
1043
|
+
const btn = e.target;
|
|
1044
|
+
disableOrganization(btn.dataset.disable, btn.dataset.name);
|
|
1045
|
+
} else if (e.target.matches('[data-enable]')) {
|
|
1046
|
+
const btn = e.target;
|
|
1047
|
+
enableOrganization(btn.dataset.enable, btn.dataset.name);
|
|
1048
|
+
} else if (e.target.matches('[data-delete]')) {
|
|
1049
|
+
const btn = e.target;
|
|
1050
|
+
deleteOrganization(btn.dataset.delete, btn.dataset.name);
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
|
|
914
1054
|
const selectedRefreshBtn = document.getElementById('btn-selected-refresh');
|
|
915
1055
|
if (selectedRefreshBtn) selectedRefreshBtn.onclick = () => Promise.all([loadSelectedOrg(), loadMembers(), loadInvites()]);
|
|
916
1056
|
|
|
@@ -969,6 +1109,222 @@
|
|
|
969
1109
|
setSelectedControlsEnabled(false);
|
|
970
1110
|
}
|
|
971
1111
|
|
|
1112
|
+
// Modal functions
|
|
1113
|
+
function openCreateOrgModal() {
|
|
1114
|
+
document.getElementById('modal-create-org').classList.remove('hidden');
|
|
1115
|
+
document.getElementById('modal-create-org').classList.add('flex');
|
|
1116
|
+
// Clear form
|
|
1117
|
+
document.getElementById('create-org-name').value = '';
|
|
1118
|
+
document.getElementById('create-org-description').value = '';
|
|
1119
|
+
document.getElementById('create-org-owner').value = '';
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function closeCreateOrgModal() {
|
|
1123
|
+
document.getElementById('modal-create-org').classList.add('hidden');
|
|
1124
|
+
document.getElementById('modal-create-org').classList.remove('flex');
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function openEditOrgModal(orgId, name, description, owner, status) {
|
|
1128
|
+
document.getElementById('modal-edit-org').classList.remove('hidden');
|
|
1129
|
+
document.getElementById('modal-edit-org').classList.add('flex');
|
|
1130
|
+
// Populate form
|
|
1131
|
+
document.getElementById('edit-org-id').value = orgId;
|
|
1132
|
+
document.getElementById('edit-org-name').value = name || '';
|
|
1133
|
+
document.getElementById('edit-org-description').value = description || '';
|
|
1134
|
+
document.getElementById('edit-org-owner').value = owner || '';
|
|
1135
|
+
document.getElementById('edit-org-status').value = status || 'active';
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function closeEditOrgModal() {
|
|
1139
|
+
document.getElementById('modal-edit-org').classList.add('hidden');
|
|
1140
|
+
document.getElementById('modal-edit-org').classList.remove('flex');
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// CRUD operations
|
|
1144
|
+
async function createOrganization() {
|
|
1145
|
+
const name = document.getElementById('create-org-name').value.trim();
|
|
1146
|
+
const description = document.getElementById('create-org-description').value.trim();
|
|
1147
|
+
const ownerUserId = document.getElementById('create-org-owner').value.trim();
|
|
1148
|
+
|
|
1149
|
+
if (!name) {
|
|
1150
|
+
showToast('Organization name is required', 'error');
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (name.length < 2) {
|
|
1155
|
+
showToast('Name must be at least 2 characters', 'error');
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
try {
|
|
1160
|
+
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}`, {
|
|
1161
|
+
method: 'POST',
|
|
1162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1163
|
+
credentials: 'same-origin',
|
|
1164
|
+
body: JSON.stringify({
|
|
1165
|
+
name,
|
|
1166
|
+
description: description || undefined,
|
|
1167
|
+
ownerUserId: ownerUserId || undefined
|
|
1168
|
+
}),
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
const data = await res.json();
|
|
1172
|
+
if (!res.ok) {
|
|
1173
|
+
showToast(data?.error || 'Failed to create organization', 'error');
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
showToast('Organization created successfully', 'success');
|
|
1178
|
+
closeCreateOrgModal();
|
|
1179
|
+
await loadOrgs();
|
|
1180
|
+
} catch (e) {
|
|
1181
|
+
showToast(e.message || 'Failed to create organization', 'error');
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
async function updateOrganization() {
|
|
1186
|
+
const orgId = document.getElementById('edit-org-id').value;
|
|
1187
|
+
const name = document.getElementById('edit-org-name').value.trim();
|
|
1188
|
+
const description = document.getElementById('edit-org-description').value.trim();
|
|
1189
|
+
const ownerUserId = document.getElementById('edit-org-owner').value.trim();
|
|
1190
|
+
const status = document.getElementById('edit-org-status').value;
|
|
1191
|
+
|
|
1192
|
+
if (!name) {
|
|
1193
|
+
showToast('Organization name is required', 'error');
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
if (name.length < 2) {
|
|
1198
|
+
showToast('Name must be at least 2 characters', 'error');
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
try {
|
|
1203
|
+
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}`, {
|
|
1204
|
+
method: 'PUT',
|
|
1205
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1206
|
+
credentials: 'same-origin',
|
|
1207
|
+
body: JSON.stringify({
|
|
1208
|
+
name,
|
|
1209
|
+
description: description || undefined,
|
|
1210
|
+
ownerUserId: ownerUserId || undefined,
|
|
1211
|
+
status
|
|
1212
|
+
}),
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
const data = await res.json();
|
|
1216
|
+
if (!res.ok) {
|
|
1217
|
+
showToast(data?.error || 'Failed to update organization', 'error');
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
showToast('Organization updated successfully', 'success');
|
|
1222
|
+
closeEditOrgModal();
|
|
1223
|
+
await loadOrgs();
|
|
1224
|
+
if (state.orgs.selectedOrgId === orgId) {
|
|
1225
|
+
await loadSelectedOrg();
|
|
1226
|
+
}
|
|
1227
|
+
} catch (e) {
|
|
1228
|
+
showToast(e.message || 'Failed to update organization', 'error');
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
async function disableOrganization(orgId, orgName) {
|
|
1233
|
+
if (!confirm(`Are you sure you want to disable "${orgName}"?`)) return;
|
|
1234
|
+
|
|
1235
|
+
try {
|
|
1236
|
+
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/disable`, {
|
|
1237
|
+
method: 'PATCH',
|
|
1238
|
+
credentials: 'same-origin'
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
const data = await res.json();
|
|
1242
|
+
if (!res.ok) {
|
|
1243
|
+
showToast(data?.error || 'Failed to disable organization', 'error');
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
showToast('Organization disabled successfully', 'success');
|
|
1248
|
+
await loadOrgs();
|
|
1249
|
+
if (state.orgs.selectedOrgId === orgId) {
|
|
1250
|
+
await loadSelectedOrg();
|
|
1251
|
+
}
|
|
1252
|
+
} catch (e) {
|
|
1253
|
+
showToast(e.message || 'Failed to disable organization', 'error');
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
async function enableOrganization(orgId, orgName) {
|
|
1258
|
+
if (!confirm(`Are you sure you want to enable "${orgName}"?`)) return;
|
|
1259
|
+
|
|
1260
|
+
try {
|
|
1261
|
+
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/enable`, {
|
|
1262
|
+
method: 'PATCH',
|
|
1263
|
+
credentials: 'same-origin'
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
const data = await res.json();
|
|
1267
|
+
if (!res.ok) {
|
|
1268
|
+
showToast(data?.error || 'Failed to enable organization', 'error');
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
showToast('Organization enabled successfully', 'success');
|
|
1273
|
+
await loadOrgs();
|
|
1274
|
+
if (state.orgs.selectedOrgId === orgId) {
|
|
1275
|
+
await loadSelectedOrg();
|
|
1276
|
+
}
|
|
1277
|
+
} catch (e) {
|
|
1278
|
+
showToast(e.message || 'Failed to enable organization', 'error');
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
async function deleteOrganization(orgId, orgName) {
|
|
1283
|
+
const confirm1 = confirm(`Are you sure you want to delete "${orgName}"?`);
|
|
1284
|
+
if (!confirm1) return;
|
|
1285
|
+
|
|
1286
|
+
const confirm2 = confirm(
|
|
1287
|
+
'WARNING: This will permanently delete the organization and ALL its data including:\n' +
|
|
1288
|
+
'• All organization members\n' +
|
|
1289
|
+
'• All pending invites\n' +
|
|
1290
|
+
'• All associated assets and files\n' +
|
|
1291
|
+
'• All notifications and activity\n\n' +
|
|
1292
|
+
'This action cannot be undone. Continue?'
|
|
1293
|
+
);
|
|
1294
|
+
if (!confirm2) return;
|
|
1295
|
+
|
|
1296
|
+
try {
|
|
1297
|
+
showToast('Deleting organization and cleaning up data...', 'success');
|
|
1298
|
+
|
|
1299
|
+
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}`, {
|
|
1300
|
+
method: 'DELETE',
|
|
1301
|
+
credentials: 'same-origin'
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
const data = await res.json();
|
|
1305
|
+
if (!res.ok) {
|
|
1306
|
+
showToast(data?.error || 'Failed to delete organization', 'error');
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
showToast(`Organization "${orgName}" deleted permanently`, 'success');
|
|
1311
|
+
|
|
1312
|
+
// Clear selection if this org was selected
|
|
1313
|
+
if (state.orgs.selectedOrgId === orgId) {
|
|
1314
|
+
state.orgs.selectedOrgId = null;
|
|
1315
|
+
setSelectedControlsEnabled(false);
|
|
1316
|
+
document.getElementById('selected-org-subtitle').textContent = 'Select an org from the list';
|
|
1317
|
+
document.getElementById('kpi-members').textContent = '-';
|
|
1318
|
+
document.getElementById('kpi-invites').textContent = '-';
|
|
1319
|
+
document.getElementById('kpi-org-status').textContent = '-';
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
await loadOrgs();
|
|
1323
|
+
} catch (e) {
|
|
1324
|
+
showToast(e.message || 'Failed to delete organization', 'error');
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
972
1328
|
bindEvents();
|
|
973
1329
|
loadOrgs();
|
|
974
1330
|
</script>
|