Group users by company + allow admins to set user/admin role

- Superadmin sees users grouped by company with header rows
- Admins can now set user or admin role when creating/editing users
- Admin role change restricted to own company only
- Prevents admin from modifying superadmin roles

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 15:16:04 +02:00
parent a94d1edee0
commit b4a85a28a5
3 changed files with 102 additions and 26 deletions

14
api.php
View File

@@ -1919,9 +1919,10 @@ switch ($action) {
}
}
} elseif (!$isSA) {
// Admin luo käyttäjiä vain omaan yritykseensä -> oletusrooli user
// Admin luo käyttäjiä omaan yritykseensä — voi valita admin tai user
$myCompanyId = $_SESSION['company_id'] ?? '';
$companyRoles[$myCompanyId] = 'user';
$requestedRole = $input['company_roles'][$myCompanyId] ?? 'user';
$companyRoles[$myCompanyId] = in_array($requestedRole, ['admin', 'user']) ? $requestedRole : 'user';
}
$newUser = [
'id' => generateId(),
@@ -1982,9 +1983,12 @@ switch ($action) {
if (isset($input['company_roles']) && is_array($input['company_roles'])) {
$companyRoles = $u['company_roles'] ?? [];
foreach ($input['company_roles'] as $cid => $crole) {
if (in_array($cid, $u['companies'] ?? []) && in_array($crole, ['admin', 'user'])) {
$companyRoles[$cid] = $crole;
}
if (!in_array($cid, $u['companies'] ?? []) || !in_array($crole, ['admin', 'user'])) continue;
// Admin voi muuttaa vain oman yrityksensä rooleja
if (!$isSA && $cid !== $myCompanyId) continue;
// Admin ei voi muuttaa superadminin roolia
if (!$isSA && ($u['role'] === 'superadmin')) continue;
$companyRoles[$cid] = $crole;
}
$u['company_roles'] = $companyRoles;
}

View File

@@ -2133,6 +2133,13 @@
<label>Yritykset ja roolit</label>
<div id="user-company-checkboxes" style="display:flex;flex-direction:column;gap:0.5rem;margin-top:0.25rem;"></div>
</div>
<div class="form-group" id="admin-company-role-section" style="display:none;">
<label>Rooli yrityksessä</label>
<select id="admin-company-role-select" style="padding:6px 10px;border:2px solid #e0e0e0;border-radius:8px;font-size:0.85rem;">
<option value="user">Käyttäjä</option>
<option value="admin">Admin</option>
</select>
</div>
</div>
<div id="user-signatures-section" style="display:none;margin-top:1rem;border-top:1px solid #e5e7eb;padding-top:1rem;">
<h3 style="color:#0f3460;font-size:1rem;margin-bottom:0.75rem;">Sähköpostiallekirjoitukset</h3>

107
script.js
View File

@@ -1160,23 +1160,64 @@ async function loadUsers() {
try {
const users = await apiCall('users');
const utbody = document.getElementById('users-tbody');
utbody.innerHTML = users.map(u => `<tr>
<td><strong>${esc(u.username)}</strong></td>
<td>${esc(u.nimi)}</td>
<td>${esc(u.email || '')}</td>
<td>${u.role === 'superadmin' ? '<span class="role-badge role-superadmin">Pääkäyttäjä</span>' :
(u.company_roles || {})[currentCompany?.id] === 'admin'
? '<span class="role-badge role-admin">Admin</span>'
: '<span class="role-badge role-user">Käyttäjä</span>'}</td>
<td>${esc(u.luotu)}</td>
<td class="actions-cell">
<button onclick="editUser('${u.id}')" title="Muokkaa">&#9998;</button>
${u.id !== '${currentUser.id}' ? `<button onclick="deleteUser('${u.id}','${esc(u.username)}')" title="Poista">&#128465;</button>` : ''}
</td>
</tr>`).join('');
const isSA = currentUser?.role === 'superadmin';
if (isSA) {
// Superadmin: ryhmittele yrityskohtaisesti
const companyMap = {}; // companyId => { name, users }
const noCompany = [];
users.forEach(u => {
const comps = u.companies || [];
if (comps.length === 0) {
noCompany.push(u);
} else {
comps.forEach(cid => {
if (!companyMap[cid]) {
const comp = availableCompanies.find(c => c.id === cid);
companyMap[cid] = { name: comp?.nimi || cid, users: [] };
}
companyMap[cid].users.push(u);
});
}
});
// Järjestä yritykset nimen mukaan
const sortedCompanies = Object.entries(companyMap).sort((a, b) => a[1].name.localeCompare(b[1].name));
let html = '';
for (const [cid, group] of sortedCompanies) {
html += `<tr><td colspan="6" style="background:#e8ecf1;font-weight:700;color:#0f3460;padding:10px 12px;font-size:0.95rem;border-top:2px solid #c8d0da;">${esc(group.name)}</td></tr>`;
html += group.users.map(u => renderUserRow(u, cid)).join('');
}
if (noCompany.length) {
html += `<tr><td colspan="6" style="background:#f5f0e8;font-weight:700;color:#888;padding:10px 12px;font-size:0.95rem;border-top:2px solid #e0d8c8;">Ei yritystä</td></tr>`;
html += noCompany.map(u => renderUserRow(u, null)).join('');
}
utbody.innerHTML = html;
} else {
// Admin: flat-lista omasta yrityksestä
utbody.innerHTML = users.map(u => renderUserRow(u, currentCompany?.id)).join('');
}
} catch (e) { console.error(e); }
}
function renderUserRow(u, companyId) {
const role = u.role === 'superadmin'
? '<span class="role-badge role-superadmin">Pääkäyttäjä</span>'
: (u.company_roles || {})[companyId] === 'admin'
? '<span class="role-badge role-admin">Admin</span>'
: '<span class="role-badge role-user">Käyttäjä</span>';
return `<tr>
<td><strong>${esc(u.username)}</strong></td>
<td>${esc(u.nimi)}</td>
<td>${esc(u.email || '')}</td>
<td>${role}</td>
<td>${esc(u.luotu)}</td>
<td class="actions-cell">
<button onclick="editUser('${u.id}')" title="Muokkaa">&#9998;</button>
${u.id !== currentUser?.id ? `<button onclick="deleteUser('${u.id}','${esc(u.username)}')" title="Poista">&#128465;</button>` : ''}
</td>
</tr>`;
}
let usersCache = [];
document.getElementById('btn-add-user').addEventListener('click', () => openUserForm());
document.getElementById('user-modal-close').addEventListener('click', () => userModal.style.display = 'none');
@@ -1196,9 +1237,24 @@ function openUserForm(user = null) {
// Piilota superadmin-kenttä ellei ole superadmin
const roleGroup = document.getElementById('user-role-group');
if (roleGroup) roleGroup.style.display = currentUser?.role === 'superadmin' ? '' : 'none';
// Piilota yrityscheckboxit adminilta (näkee vain oman yrityksen)
// Piilota yrityscheckboxit adminilta (näkee vain oman yrityksen), mutta näytä yrityskohtainen rooli
const isSuperAdmin = currentUser?.role === 'superadmin';
const compSection = document.getElementById('user-company-checkboxes')?.closest('.form-group');
if (compSection) compSection.style.display = currentUser?.role === 'superadmin' ? '' : 'none';
if (compSection) compSection.style.display = isSuperAdmin ? '' : 'none';
// Admin-näkymä: yrityskohtainen rooli omalle yritykselle
const adminRoleSection = document.getElementById('admin-company-role-section');
if (adminRoleSection) {
if (!isSuperAdmin && currentUser?.company_role === 'admin') {
adminRoleSection.style.display = '';
const adminRoleSelect = document.getElementById('admin-company-role-select');
if (adminRoleSelect) {
const currentRole = (user?.company_roles || {})[currentCompany?.id] || 'user';
adminRoleSelect.value = currentRole;
}
} else {
adminRoleSection.style.display = 'none';
}
}
// Yrityscheckboxit + yrityskohtaiset roolit
const allComps = availableCompanies.length > 0 ? availableCompanies : [];
const userComps = user ? (user.companies || []) : [];
@@ -1282,12 +1338,21 @@ document.getElementById('user-form').addEventListener('submit', async (e) => {
const companies = [...document.querySelectorAll('.user-company-cb:checked')].map(cb => cb.value);
// Kerää yrityskohtaiset roolit
const company_roles = {};
document.querySelectorAll('.user-company-role').forEach(sel => {
const cid = sel.dataset.companyId;
if (companies.includes(cid)) {
company_roles[cid] = sel.value;
if (currentUser?.role === 'superadmin') {
// Superadmin: kerää kaikista yritys-dropdowneista
document.querySelectorAll('.user-company-role').forEach(sel => {
const cid = sel.dataset.companyId;
if (companies.includes(cid)) {
company_roles[cid] = sel.value;
}
});
} else {
// Admin: käytä admin-company-role-select omalle yritykselle
const adminRoleSelect = document.getElementById('admin-company-role-select');
if (adminRoleSelect && currentCompany?.id) {
company_roles[currentCompany.id] = adminRoleSelect.value;
}
});
}
// Kerää allekirjoitukset
const signatures = {};
document.querySelectorAll('.sig-textarea').forEach(ta => {