mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-11 13:49:03 +00:00
5e2ab9bfa9
Show org-linked device and user groups via team_id, with shortcuts to Devices → Groups and one-click group creation scoped to the organization.
818 lines
39 KiB
JavaScript
818 lines
39 KiB
JavaScript
/**
|
|
* BetterDesk Console — Organization Detail Page JavaScript
|
|
*
|
|
* Handles org detail view with tabs: Users, Devices, Invitations, Settings, Device Groups.
|
|
* Uses Modal.show() for form inputs, i18n for all strings.
|
|
*/
|
|
'use strict';
|
|
|
|
(function () {
|
|
const orgId = document.querySelector('.org-detail-page')?.dataset.orgId;
|
|
if (!orgId) return;
|
|
|
|
const API = `/api/panel/org/${orgId}`;
|
|
|
|
// i18n helper — falls back to key
|
|
function t(key) {
|
|
return (typeof _ === 'function') ? _(`organizations.${key}`) || key : key;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Helpers
|
|
// -----------------------------------------------------------------------
|
|
async function api(method, path, body) {
|
|
const csrfToken = window.BetterDesk?.csrfToken || '';
|
|
const opts = {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
};
|
|
if (csrfToken) opts.headers['x-csrf-token'] = csrfToken;
|
|
if (body) opts.body = JSON.stringify(body);
|
|
const res = await fetch(API + path, opts);
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
throw new Error(err.error || 'Request failed');
|
|
}
|
|
if (res.status === 204) return null;
|
|
return res.json();
|
|
}
|
|
|
|
function toast(msg, type = 'info') {
|
|
if (typeof Notifications !== 'undefined' && Notifications[type]) Notifications[type](msg);
|
|
else if (window.showToast) window.showToast(msg, type);
|
|
else console.log(`[${type}]`, msg);
|
|
}
|
|
|
|
function escHtml(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s || '';
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function formatDate(d) {
|
|
if (!d) return '—';
|
|
return new Date(d).toLocaleDateString(undefined, {
|
|
year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
// Roles ordered lowest → highest, matching the org membership modal.
|
|
// (Member/Operator/Admin/Owner). Backend value `user` is preserved for
|
|
// compatibility but the label is standardized to "Member" via i18n
|
|
// to avoid vocabulary collision with the server-wide "Viewer" role.
|
|
function roleOptions(selected) {
|
|
const roles = ['user', 'operator', 'admin', 'owner'];
|
|
return roles.map(r =>
|
|
`<option value="${r}" ${r === selected ? 'selected' : ''}>${t('role_' + r)}</option>`
|
|
).join('');
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Tabs
|
|
// -----------------------------------------------------------------------
|
|
document.querySelectorAll('.org-tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
document.querySelectorAll('.org-tab').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.org-tab-content').forEach(c => c.style.display = 'none');
|
|
tab.classList.add('active');
|
|
document.getElementById(`tab-${tab.dataset.tab}`).style.display = 'block';
|
|
if (tab.dataset.tab === 'device-groups') loadDeviceGroups();
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Load org header
|
|
// -----------------------------------------------------------------------
|
|
let currentOrg = {};
|
|
|
|
async function loadOrgHeader() {
|
|
try {
|
|
const org = await api('GET', '');
|
|
currentOrg = org;
|
|
document.getElementById('org-detail-name').textContent = org.name;
|
|
document.getElementById('org-detail-slug').textContent = org.slug;
|
|
} catch (err) {
|
|
toast(t('loading_failed'), 'error');
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Edit org (header button)
|
|
// -----------------------------------------------------------------------
|
|
document.getElementById('org-edit-btn')?.addEventListener('click', () => {
|
|
Modal.show({
|
|
title: t('edit'),
|
|
content: `
|
|
<div class="form-group">
|
|
<label class="form-label">${escHtml(t('name'))}</label>
|
|
<input type="text" id="modal-org-name" class="form-input" value="${escHtml(currentOrg.name || '')}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">${escHtml(t('slug'))}</label>
|
|
<input type="text" id="modal-org-slug" class="form-input" value="${escHtml(currentOrg.slug || '')}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">${escHtml(t('logo_url'))}</label>
|
|
<input type="text" id="modal-org-logo" class="form-input" value="${escHtml(currentOrg.logo_url || '')}">
|
|
</div>
|
|
`,
|
|
buttons: [
|
|
{ label: _('actions.cancel') || 'Cancel', class: 'btn-secondary', onClick: () => Modal.close() },
|
|
{
|
|
label: _('actions.save') || 'Save', class: 'btn-primary', onClick: async () => {
|
|
const name = document.getElementById('modal-org-name')?.value?.trim();
|
|
const slug = document.getElementById('modal-org-slug')?.value?.trim();
|
|
const logo_url = document.getElementById('modal-org-logo')?.value?.trim();
|
|
if (!name || !slug) return;
|
|
try {
|
|
await api('PUT', '', { name, slug, logo_url });
|
|
Modal.close();
|
|
toast(t('edit'), 'success');
|
|
loadOrgHeader();
|
|
} catch (err) { toast(err.message, 'error'); }
|
|
}
|
|
}
|
|
]
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Users tab
|
|
// -----------------------------------------------------------------------
|
|
async function loadUsers() {
|
|
try {
|
|
const data = await api('GET', '/users');
|
|
const users = data.users || [];
|
|
document.getElementById('users-count').textContent = users.length;
|
|
const container = document.getElementById('users-table');
|
|
|
|
if (users.length === 0) {
|
|
container.innerHTML = `<p class="text-muted">${escHtml(t('no_users'))}</p>`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>${escHtml(t('username'))}</th>
|
|
<th>${escHtml(t('display_name'))}</th>
|
|
<th>${escHtml(t('email'))}</th>
|
|
<th>${escHtml(t('role'))}</th>
|
|
<th>${escHtml(t('last_login'))}</th>
|
|
<th>${escHtml(t('actions'))}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${users.map(u => `
|
|
<tr>
|
|
<td><strong>${escHtml(u.username)}</strong></td>
|
|
<td>${escHtml(u.display_name)}</td>
|
|
<td>${escHtml(u.email)}</td>
|
|
<td><span class="role-badge role-${String(u.role || '').replace(/[^a-z0-9_-]/gi, '')}">${escHtml(t('role_' + u.role) || u.role)}</span></td>
|
|
<td>${formatDate(u.last_login)}</td>
|
|
<td>
|
|
<button class="btn btn-icon btn-sm user-role-btn" data-user-id="${escHtml(u.id)}" data-role="${escHtml(u.role)}" title="${escHtml(t('edit_role'))}">
|
|
<span class="material-icons">edit</span>
|
|
</button>
|
|
<button class="btn btn-icon btn-sm user-remove-btn" data-user-id="${escHtml(u.id)}" title="${escHtml(_('common.delete') || 'Remove')}">
|
|
<span class="material-icons">person_remove</span>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>`;
|
|
|
|
container.querySelectorAll('.user-remove-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => deleteUser(btn.dataset.userId));
|
|
});
|
|
container.querySelectorAll('.user-role-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => editUserRole(btn.dataset.userId, btn.dataset.role));
|
|
});
|
|
} catch (err) {
|
|
toast(t('loading_failed'), 'error');
|
|
}
|
|
}
|
|
|
|
// Add User modal — supports both creating new org user or linking existing server user
|
|
document.getElementById('add-user-btn')?.addEventListener('click', async () => {
|
|
// Fetch available users first
|
|
let availableUsers = [];
|
|
try {
|
|
const resp = await api('GET', '/available-users');
|
|
availableUsers = resp.users || [];
|
|
} catch (err) {
|
|
console.error('Failed to load available users:', err);
|
|
}
|
|
|
|
const hasAvailableUsers = availableUsers.length > 0;
|
|
const userOptions = availableUsers.map(u =>
|
|
`<option value="${escHtml(u.id)}">${escHtml(u.username)} (${escHtml(u.role)})</option>`
|
|
).join('');
|
|
|
|
Modal.show({
|
|
title: t('add_user'),
|
|
content: `
|
|
<div class="modal-tabs">
|
|
<button type="button" class="modal-tab active" data-tab="create">${escHtml(t('create_new_user'))}</button>
|
|
<button type="button" class="modal-tab" data-tab="existing">${escHtml(t('add_existing_user'))}${!hasAvailableUsers ? ' (' + escHtml(t('none_available')) + ')' : ''}</button>
|
|
</div>
|
|
<div id="modal-tab-create" class="modal-tab-content" style="display:block">
|
|
<div class="form-group">
|
|
<label class="form-label">${escHtml(t('username'))}</label>
|
|
<input type="text" id="modal-user-name" class="form-input" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">${escHtml(t('password'))}</label>
|
|
<input type="password" id="modal-user-pass" class="form-input" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">${escHtml(t('role'))}</label>
|
|
<select id="modal-user-role" class="form-input">${roleOptions('user')}</select>
|
|
</div>
|
|
</div>
|
|
<div id="modal-tab-existing" class="modal-tab-content" style="display:none">
|
|
${hasAvailableUsers ? `
|
|
<div class="form-group">
|
|
<label class="form-label">${escHtml(t('select_user'))}</label>
|
|
<select id="modal-existing-user" class="form-input">
|
|
<option value="">— ${escHtml(t('select_user'))} —</option>
|
|
${userOptions}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">${escHtml(t('org_role'))}</label>
|
|
<select id="modal-existing-role" class="form-input">${roleOptions('user')}</select>
|
|
</div>
|
|
<p class="text-muted text-sm">${escHtml(t('link_user_hint'))}</p>
|
|
` : `
|
|
<p class="text-muted">${escHtml(t('no_available_users'))}</p>
|
|
`}
|
|
</div>
|
|
`,
|
|
buttons: [
|
|
{ label: _('actions.cancel') || 'Cancel', class: 'btn-secondary', onClick: () => Modal.close() },
|
|
{
|
|
label: t('add_user'), class: 'btn-primary', icon: 'person_add', onClick: async () => {
|
|
const activeTab = document.querySelector('.modal-tab.active')?.dataset.tab || 'create';
|
|
|
|
if (activeTab === 'create') {
|
|
// Create new org user
|
|
const username = document.getElementById('modal-user-name')?.value?.trim();
|
|
const password = document.getElementById('modal-user-pass')?.value;
|
|
const role = document.getElementById('modal-user-role')?.value || 'user';
|
|
if (!username || !password) return;
|
|
try {
|
|
await api('POST', '/users', { username, password, role });
|
|
Modal.close();
|
|
toast(t('user_added'), 'success');
|
|
loadUsers();
|
|
} catch (err) { toast(err.message, 'error'); }
|
|
} else {
|
|
// Link existing server user
|
|
const userId = document.getElementById('modal-existing-user')?.value;
|
|
const role = document.getElementById('modal-existing-role')?.value || 'user';
|
|
if (!userId) {
|
|
toast(t('select_user'), 'warning');
|
|
return;
|
|
}
|
|
try {
|
|
await api('POST', '/members', { user_id: parseInt(userId, 10), role });
|
|
Modal.close();
|
|
toast(t('user_linked'), 'success');
|
|
loadUsers();
|
|
} catch (err) { toast(err.message, 'error'); }
|
|
}
|
|
}
|
|
}
|
|
],
|
|
onOpen: () => {
|
|
// Tab switching
|
|
document.querySelectorAll('.modal-tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
document.querySelectorAll('.modal-tab').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.modal-tab-content').forEach(c => c.style.display = 'none');
|
|
tab.classList.add('active');
|
|
document.getElementById(`modal-tab-${tab.dataset.tab}`).style.display = 'block';
|
|
});
|
|
});
|
|
setTimeout(() => document.getElementById('modal-user-name')?.focus(), 50);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Edit user role modal
|
|
async function editUserRole(uid, currentRole) {
|
|
Modal.show({
|
|
title: t('edit_role'),
|
|
content: `
|
|
<div class="form-group">
|
|
<label class="form-label">${escHtml(t('role'))}</label>
|
|
<select id="modal-edit-role" class="form-input">${roleOptions(currentRole)}</select>
|
|
</div>
|
|
`,
|
|
buttons: [
|
|
{ label: _('actions.cancel') || 'Cancel', class: 'btn-secondary', onClick: () => Modal.close() },
|
|
{
|
|
label: _('actions.save') || 'Save', class: 'btn-primary', onClick: async () => {
|
|
const role = document.getElementById('modal-edit-role')?.value;
|
|
if (!role) return;
|
|
try {
|
|
await api('PUT', `/users/${uid}`, { role });
|
|
Modal.close();
|
|
toast(t('user_updated'), 'success');
|
|
loadUsers();
|
|
} catch (err) { toast(err.message, 'error'); }
|
|
}
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Devices tab
|
|
// -----------------------------------------------------------------------
|
|
async function loadDevices() {
|
|
try {
|
|
const data = await api('GET', '/devices');
|
|
const devices = data.devices || [];
|
|
document.getElementById('devices-count').textContent = devices.length;
|
|
const container = document.getElementById('devices-table');
|
|
|
|
if (devices.length === 0) {
|
|
container.innerHTML = `<p class="text-muted">${escHtml(t('no_devices'))}</p>`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>${escHtml(t('device_id'))}</th>
|
|
<th>${escHtml(t('department'))}</th>
|
|
<th>${escHtml(t('building'))}</th>
|
|
<th>${escHtml(t('location'))}</th>
|
|
<th>${escHtml(t('assigned_user'))}</th>
|
|
<th>${escHtml(t('actions'))}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${devices.map(d => `
|
|
<tr>
|
|
<td><a href="/devices/${escHtml(d.device_id)}">${escHtml(d.device_id)}</a></td>
|
|
<td>${escHtml(d.department)}</td>
|
|
<td>${escHtml(d.building)}</td>
|
|
<td>${escHtml(d.location)}</td>
|
|
<td>${escHtml(d.assigned_user_id)}</td>
|
|
<td>
|
|
<button class="btn btn-icon btn-sm btn-danger device-unassign-btn" data-device-id="${escHtml(d.device_id)}" title="${escHtml(_('common.delete') || 'Unassign')}">
|
|
<span class="material-icons">link_off</span>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>`;
|
|
|
|
container.querySelectorAll('.device-unassign-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => unassignDevice(btn.dataset.deviceId));
|
|
});
|
|
} catch (err) {
|
|
toast(t('loading_failed'), 'error');
|
|
}
|
|
}
|
|
|
|
// Assign Device modal
|
|
document.getElementById('assign-device-btn')?.addEventListener('click', () => {
|
|
Modal.show({
|
|
title: t('assign_device'),
|
|
content: `
|
|
<div class="form-group">
|
|
<label class="form-label">${escHtml(t('device_id'))}</label>
|
|
<input type="text" id="modal-device-id" class="form-input" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">${escHtml(t('department'))}</label>
|
|
<input type="text" id="modal-device-dept" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">${escHtml(t('building'))}</label>
|
|
<input type="text" id="modal-device-building" class="form-input">
|
|
</div>
|
|
`,
|
|
buttons: [
|
|
{ label: _('actions.cancel') || 'Cancel', class: 'btn-secondary', onClick: () => Modal.close() },
|
|
{
|
|
label: t('assign_device'), class: 'btn-primary', icon: 'add_circle', onClick: async () => {
|
|
const device_id = document.getElementById('modal-device-id')?.value?.trim();
|
|
const department = document.getElementById('modal-device-dept')?.value?.trim() || '';
|
|
const building = document.getElementById('modal-device-building')?.value?.trim() || '';
|
|
if (!device_id) return;
|
|
try {
|
|
await api('POST', '/devices', { device_id, department, building });
|
|
Modal.close();
|
|
toast(t('device_assigned'), 'success');
|
|
loadDevices();
|
|
} catch (err) { toast(err.message, 'error'); }
|
|
}
|
|
}
|
|
],
|
|
onOpen: () => setTimeout(() => document.getElementById('modal-device-id')?.focus(), 50)
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Invitations tab
|
|
// -----------------------------------------------------------------------
|
|
async function loadInvitations() {
|
|
try {
|
|
const data = await api('GET', '/invitations');
|
|
const invs = data.invitations || [];
|
|
document.getElementById('invitations-count').textContent = invs.length;
|
|
const container = document.getElementById('invitations-table');
|
|
|
|
if (invs.length === 0) {
|
|
container.innerHTML = `<p class="text-muted">${escHtml(t('no_invitations'))}</p>`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>${escHtml(t('token'))}</th>
|
|
<th>${escHtml(t('email'))}</th>
|
|
<th>${escHtml(t('role'))}</th>
|
|
<th>${escHtml(t('expires'))}</th>
|
|
<th>${escHtml(t('used'))}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${invs.map(inv => `
|
|
<tr>
|
|
<td><code>${escHtml((inv.token || '').substring(0, 16) + '...')}</code></td>
|
|
<td>${escHtml(inv.email)}</td>
|
|
<td><span class="role-badge role-${inv.role}">${escHtml(t('role_' + inv.role) || inv.role)}</span></td>
|
|
<td>${formatDate(inv.expires_at)}</td>
|
|
<td>${inv.used_at ? formatDate(inv.used_at) : '—'}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>`;
|
|
} catch (err) {
|
|
toast(t('loading_failed'), 'error');
|
|
}
|
|
}
|
|
|
|
// Create Invitation modal
|
|
document.getElementById('create-invite-btn')?.addEventListener('click', () => {
|
|
Modal.show({
|
|
title: t('create_invitation'),
|
|
content: `
|
|
<div class="form-group">
|
|
<label class="form-label">${escHtml(t('email'))}</label>
|
|
<input type="email" id="modal-invite-email" class="form-input" placeholder="optional">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">${escHtml(t('role'))}</label>
|
|
<select id="modal-invite-role" class="form-input">${roleOptions('user')}</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">${escHtml(t('expires_hours'))}</label>
|
|
<input type="number" id="modal-invite-hours" class="form-input" value="72" min="1" max="720">
|
|
</div>
|
|
`,
|
|
buttons: [
|
|
{ label: _('actions.cancel') || 'Cancel', class: 'btn-secondary', onClick: () => Modal.close() },
|
|
{
|
|
label: t('create_invitation'), class: 'btn-primary', icon: 'link', onClick: async () => {
|
|
const email = document.getElementById('modal-invite-email')?.value?.trim() || '';
|
|
const role = document.getElementById('modal-invite-role')?.value || 'user';
|
|
const hours = parseInt(document.getElementById('modal-invite-hours')?.value) || 72;
|
|
try {
|
|
const inv = await api('POST', '/invite', { email, role, expires_in_hours: hours });
|
|
Modal.close();
|
|
toast(t('invitation_created'), 'success');
|
|
loadInvitations();
|
|
} catch (err) { toast(err.message, 'error'); }
|
|
}
|
|
}
|
|
],
|
|
onOpen: () => setTimeout(() => document.getElementById('modal-invite-email')?.focus(), 50)
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Settings tab
|
|
// -----------------------------------------------------------------------
|
|
async function loadSettings() {
|
|
try {
|
|
const data = await api('GET', '/settings');
|
|
const settings = data.settings || [];
|
|
const container = document.getElementById('settings-container');
|
|
|
|
container.innerHTML = `
|
|
<div class="org-settings-form">
|
|
<div class="form-group">
|
|
<label>${escHtml(t('connection_policy'))}</label>
|
|
<select id="setting-connection-policy" class="form-input">
|
|
<option value="unattended">${escHtml(t('policy_unattended'))}</option>
|
|
<option value="attended">${escHtml(t('policy_attended'))}</option>
|
|
<option value="ask_always">${escHtml(t('policy_ask'))}</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>${escHtml(t('allow_file_transfer'))}</label>
|
|
<select id="setting-allow-file-transfer" class="form-input">
|
|
<option value="true">${_('common.yes') || 'Yes'}</option>
|
|
<option value="false">${_('common.no') || 'No'}</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>${escHtml(t('allow_clipboard'))}</label>
|
|
<select id="setting-allow-clipboard" class="form-input">
|
|
<option value="true">${_('common.yes') || 'Yes'}</option>
|
|
<option value="false">${_('common.no') || 'No'}</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>${escHtml(t('max_session_duration'))}</label>
|
|
<input type="number" id="setting-max-session" class="form-input" value="120" min="0" />
|
|
</div>
|
|
<button class="btn btn-primary" id="save-settings-btn">
|
|
<span class="material-icons">save</span> ${escHtml(t('save'))}
|
|
</button>
|
|
</div>
|
|
<div class="org-settings-raw">
|
|
<h3>${escHtml(t('raw_settings'))}</h3>
|
|
<table class="data-table">
|
|
<thead><tr><th>${escHtml(t('key'))}</th><th>${escHtml(t('value'))}</th></tr></thead>
|
|
<tbody>
|
|
${settings.map(s => `
|
|
<tr><td><code>${escHtml(s.key)}</code></td><td>${escHtml(s.value)}</td></tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>`;
|
|
|
|
// Apply current settings to form
|
|
settings.forEach(s => {
|
|
const el = document.getElementById(`setting-${s.key.replace(/_/g, '-')}`);
|
|
if (el) el.value = s.value;
|
|
});
|
|
|
|
document.getElementById('save-settings-btn')?.addEventListener('click', async () => {
|
|
const saveBtn = document.getElementById('save-settings-btn');
|
|
const originalContent = saveBtn.innerHTML;
|
|
|
|
// Visual feedback: disable button, show spinner
|
|
saveBtn.disabled = true;
|
|
saveBtn.innerHTML = '<span class="material-icons rotating">sync</span> ' + (t('saving') || 'Saving...');
|
|
|
|
const settingsToSave = [
|
|
{ key: 'connection_policy', value: document.getElementById('setting-connection-policy')?.value || 'unattended' },
|
|
{ key: 'allow_file_transfer', value: document.getElementById('setting-allow-file-transfer')?.value || 'true' },
|
|
{ key: 'allow_clipboard', value: document.getElementById('setting-allow-clipboard')?.value || 'true' },
|
|
{ key: 'max_session_duration_min', value: document.getElementById('setting-max-session')?.value || '120' },
|
|
];
|
|
|
|
console.log('[Org Settings] Saving:', settingsToSave);
|
|
|
|
try {
|
|
for (const s of settingsToSave) {
|
|
console.log('[Org Settings] PUT', s.key, '=', s.value);
|
|
const result = await api('PUT', '/settings', s);
|
|
console.log('[Org Settings] Result:', result);
|
|
}
|
|
toast(t('settings_saved') || 'Settings saved', 'success');
|
|
// Reload to confirm persistence
|
|
await loadSettings();
|
|
} catch (err) {
|
|
console.error('[Org Settings] Save error:', err);
|
|
toast(err.message || 'Failed to save settings', 'error');
|
|
} finally {
|
|
// Restore button
|
|
saveBtn.disabled = false;
|
|
saveBtn.innerHTML = originalContent;
|
|
}
|
|
});
|
|
} catch (err) {
|
|
toast(t('loading_failed'), 'error');
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Device groups linked to this organization (team_id)
|
|
// -----------------------------------------------------------------------
|
|
function groupTypeLabel(group) {
|
|
if ((group.source_type || 'manual') === 'tag' && group.tag_filter) {
|
|
return `${t('group_type_tag')}: ${group.tag_filter}`;
|
|
}
|
|
return t('group_type_manual');
|
|
}
|
|
|
|
function renderOrgGroupsList(groups, emptyKey) {
|
|
if (!groups.length) {
|
|
return `<p class="form-hint org-groups-empty">${escHtml(t(emptyKey))}</p>`;
|
|
}
|
|
return `
|
|
<table class="data-table org-groups-table">
|
|
<thead>
|
|
<tr>
|
|
<th>${escHtml(t('group_name'))}</th>
|
|
<th>${escHtml(t('group_type'))}</th>
|
|
<th>${escHtml(t('group_members'))}</th>
|
|
<th>${escHtml(t('group_allowed_user_groups'))}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${groups.map(group => `
|
|
<tr>
|
|
<td>
|
|
<strong>${escHtml(group.name || group.guid)}</strong>
|
|
${group.note ? `<div class="text-muted org-group-note">${escHtml(group.note)}</div>` : ''}
|
|
</td>
|
|
<td>${escHtml(groupTypeLabel(group))}</td>
|
|
<td>${escHtml(String(group.member_count ?? 0))}</td>
|
|
<td>${escHtml(
|
|
(group.allowed_user_groups || [])
|
|
.map(g => g.name || g.guid)
|
|
.filter(Boolean)
|
|
.join(', ') || '—'
|
|
)}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>`;
|
|
}
|
|
|
|
async function loadDeviceGroups() {
|
|
try {
|
|
const data = await api('GET', '/device-groups');
|
|
const container = document.getElementById('device-groups-container');
|
|
if (!container) return;
|
|
|
|
const deviceGroups = data.device_groups || [];
|
|
const userGroups = data.user_groups || [];
|
|
|
|
container.innerHTML = `
|
|
<p class="form-hint">${escHtml(t('device_groups_intro'))}</p>
|
|
<p class="form-hint org-team-id-hint">
|
|
<code>${escHtml(t('org_team_id'))}: ${escHtml(orgId)}</code>
|
|
</p>
|
|
<div class="org-section-actions">
|
|
<button class="btn btn-primary btn-sm" id="create-org-device-group-btn">
|
|
<span class="material-icons">add</span> ${escHtml(t('create_org_device_group'))}
|
|
</button>
|
|
</div>
|
|
${renderOrgGroupsList(deviceGroups, 'no_org_device_groups')}
|
|
<h3 class="org-groups-subtitle">${escHtml(t('org_user_groups_title'))}</h3>
|
|
<p class="form-hint">${escHtml(t('org_user_groups_intro'))}</p>
|
|
${userGroups.length ? `
|
|
<table class="data-table org-groups-table">
|
|
<thead>
|
|
<tr>
|
|
<th>${escHtml(t('group_name'))}</th>
|
|
<th>${escHtml(_('users.members') || 'Members')}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${userGroups.map(group => `
|
|
<tr>
|
|
<td>
|
|
<strong>${escHtml(group.name || group.guid)}</strong>
|
|
${group.note ? `<div class="text-muted org-group-note">${escHtml(group.note)}</div>` : ''}
|
|
</td>
|
|
<td>${escHtml(String(group.member_count ?? 0))}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
` : `<p class="form-hint org-groups-empty">${escHtml(t('no_org_user_groups'))}</p>`}
|
|
`;
|
|
|
|
document.getElementById('create-org-device-group-btn')?.addEventListener('click', () => {
|
|
const defaultName = currentOrg.name
|
|
? `${currentOrg.name} — ${t('group_devices_suffix')}`
|
|
: t('create_org_device_group');
|
|
Modal.show({
|
|
title: t('create_org_device_group'),
|
|
content: `
|
|
<div class="form-group">
|
|
<label class="form-label">${escHtml(t('group_name'))}</label>
|
|
<input type="text" id="modal-org-dg-name" class="form-input" value="${escHtml(defaultName)}" maxlength="80">
|
|
</div>
|
|
<p class="form-hint">${escHtml(t('link_team_id_hint'))}</p>
|
|
`,
|
|
buttons: [
|
|
{ label: _('actions.cancel') || 'Cancel', class: 'btn-secondary', onClick: () => Modal.close() },
|
|
{
|
|
label: _('actions.create') || 'Create',
|
|
class: 'btn-primary',
|
|
onClick: async () => {
|
|
const name = document.getElementById('modal-org-dg-name')?.value?.trim();
|
|
if (!name) {
|
|
toast(t('group_name_required'), 'error');
|
|
return;
|
|
}
|
|
try {
|
|
const csrfToken = window.BetterDesk?.csrfToken || '';
|
|
const res = await fetch('/api/device-groups', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(csrfToken ? { 'x-csrf-token': csrfToken } : {})
|
|
},
|
|
body: JSON.stringify({
|
|
name,
|
|
team_id: orgId,
|
|
source_type: 'manual'
|
|
})
|
|
});
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
throw new Error(err.error || 'Request failed');
|
|
}
|
|
Modal.close();
|
|
toast(t('org_device_group_created'), 'success');
|
|
loadDeviceGroups();
|
|
} catch (err) {
|
|
toast(err.message || t('loading_failed'), 'error');
|
|
}
|
|
}
|
|
}
|
|
]
|
|
});
|
|
});
|
|
} catch (err) {
|
|
toast(t('loading_failed'), 'error');
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Delete org
|
|
// -----------------------------------------------------------------------
|
|
document.getElementById('org-delete-btn')?.addEventListener('click', async () => {
|
|
const confirmed = await Modal.confirm({
|
|
title: _('common.delete') || 'Delete',
|
|
message: t('confirm_delete_org'),
|
|
danger: true,
|
|
confirmIcon: 'delete'
|
|
});
|
|
if (!confirmed) return;
|
|
try {
|
|
await api('DELETE', '');
|
|
toast(t('org_deleted'), 'success');
|
|
window.location.href = '/organizations';
|
|
} catch (err) {
|
|
toast(err.message, 'error');
|
|
}
|
|
});
|
|
|
|
async function deleteUser(uid) {
|
|
const confirmed = await Modal.confirm({
|
|
title: _('common.delete') || 'Remove',
|
|
message: t('confirm_remove_user'),
|
|
danger: true,
|
|
confirmIcon: 'person_remove'
|
|
});
|
|
if (!confirmed) return;
|
|
try {
|
|
await api('DELETE', `/users/${uid}`);
|
|
toast(t('user_removed'), 'success');
|
|
loadUsers();
|
|
} catch (err) {
|
|
toast(err.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function unassignDevice(did) {
|
|
const confirmed = await Modal.confirm({
|
|
title: _('common.delete') || 'Unassign',
|
|
message: t('confirm_unassign_device'),
|
|
danger: true,
|
|
confirmIcon: 'link_off'
|
|
});
|
|
if (!confirmed) return;
|
|
try {
|
|
await api('DELETE', `/devices/${did}`);
|
|
toast(t('device_unassigned'), 'success');
|
|
loadDevices();
|
|
} catch (err) {
|
|
toast(err.message, 'error');
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Init
|
|
// -----------------------------------------------------------------------
|
|
loadOrgHeader();
|
|
loadUsers();
|
|
loadDevices();
|
|
loadInvitations();
|
|
loadSettings();
|
|
loadDeviceGroups();
|
|
|
|
})();
|