/* ============================================================ KODILA — Šifrant: Uporabniki ============================================================ */ const ROLES = [ { value: 'administrator', label: 'Administrator' }, { value: 'zaposleni', label: 'Zaposleni' }, ]; const ROLE_LABEL = { administrator: 'Administrator', zaposleni: 'Zaposleni' }; const STATUS_LABEL = { active: 'Aktiven', inactive: 'Nedejaven' }; function userNameRenderer(p) { const initials = (p.value || '').split(' ').map(s => s[0]).filter(Boolean).join('').slice(0, 2).toUpperCase(); return `
${initials || '?'} ${p.value}
`; } function userRoleRenderer(p) { const cls = p.value === 'administrator' ? 'brand' : ''; return `${ROLE_LABEL[p.value] || p.value}`; } function userStatusRenderer(p) { const label = STATUS_LABEL[p.value] || p.value; const cls = (window.STATUS_MAP[label] || 'default'); const c = cls === 'default' ? '' : cls; return `${label}`; } function userLastLoginRenderer(p) { return `${p.value ? fmtDate(p.value.slice(0, 10)) : 'Še ni prijavljen'}`; } const USER_COLS = [ { field: 'id', headerName: 'ID', width: 70, cellClass: 'cell-mono', pinned: 'left' }, { field: 'fullName', headerName: 'Ime in priimek', flex: 2, minWidth: 220, cellRenderer: userNameRenderer }, { field: 'email', headerName: 'E-pošta', minWidth: 200 }, { field: 'role', headerName: 'Vloga', width: 150, cellRenderer: userRoleRenderer }, { field: 'groupName', headerName: 'Skupina', minWidth: 150, valueFormatter: p => p.value || 'Brez skupine' }, { field: 'department', headerName: 'Oddelek', minWidth: 160, valueFormatter: p => p.value || '—' }, { field: 'status', headerName: 'Status', width: 130, cellRenderer: userStatusRenderer }, { field: 'lastLoginAt', headerName: 'Zadnja prijava', width: 160, cellRenderer: userLastLoginRenderer }, ]; const EMPTY_USER = { id: null, fullName: '', email: '', username: '', role: 'zaposleni', groupId: null, department: '', status: 'active', password: '' }; const ERROR_MESSAGES = { email: { invalid: 'Vnesite veljaven e-poštni naslov.', taken: 'Ta e-poštni naslov je že v uporabi.' }, fullName: { required: 'Ime in priimek je obvezno.' }, role: { invalid: 'Izberite veljavno vlogo.' }, groupId: { invalid: 'Izbrana skupina ni veljavna.' }, password: { min_length: 'Geslo mora vsebovati vsaj 8 znakov.', min_upper: 'Geslo mora vsebovati vsaj 2 veliki črki.', min_digits: 'Geslo mora vsebovati vsaj 2 števki.', min_special: 'Geslo mora vsebovati vsaj 2 posebna znaka.', }, }; function passwordHint(policy) { if (!policy) return 'Najmanj 8 znakov, vsaj 2 veliki črki, 2 števki in 2 posebna znaka.'; return `Najmanj ${policy.minLength} znakov, vsaj ${policy.minUpper} veliki črki, ${policy.minDigits} števki in ${policy.minSpecial} posebna znaka.`; } /* --- Vnosni obrazec (drawer) --- */ function UserForm({ initial, currentUser, passwordPolicy, groups, onSave, onToggleStatus, onClose }) { const isEdit = !!(initial && initial.id); const [f, setF] = useState(initial && initial.id ? { ...EMPTY_USER, ...initial, username: initial.username || '', department: initial.department || '', groupId: initial.groupId ?? null, password: '' } : { ...EMPTY_USER }); const [changePassword, setChangePassword] = useState(!isEdit); const [errors, setErrors] = useState({}); const [saving, setSaving] = useState(false); const set = (k, v) => setF(s => ({ ...s, [k]: v })); const isSelf = isEdit && currentUser && currentUser.id === f.id; const save = async () => { setSaving(true); const payload = { fullName: f.fullName.trim(), email: f.email.trim(), username: f.username.trim() || null, role: f.role, groupId: f.role === 'administrator' ? null : f.groupId, department: f.department.trim() || null, }; if (!isEdit || changePassword) { payload.password = f.password; } const ok = await onSave(f.id, payload, isEdit, setErrors); setSaving(false); if (ok) onClose(); }; const fieldError = (field) => { const codes = errors[field]; if (!codes || !codes.length) return null; const map = ERROR_MESSAGES[field] || {}; return codes.map(c => map[c] || c).join(' '); }; return (
{ if (e.target.classList.contains('overlay')) onClose(); }}>
e.stopPropagation()}>
{isEdit ? 'Uredi uporabnika' : 'Nov uporabnik'}
{isEdit ? f.email : 'Račun za prijavo v intranet'}
Osnovni podatki
set('fullName', e.target.value)} placeholder="npr. Marko Kuhar" style={fieldError('fullName') ? { borderColor: 'var(--danger)' } : null} /> {fieldError('fullName') && {fieldError('fullName')}}
set('email', e.target.value)} placeholder="ime.priimek@kodila.si" style={fieldError('email') ? { borderColor: 'var(--danger)' } : null} /> {fieldError('email') && {fieldError('email')}}
set('username', e.target.value)} placeholder="mkuhar" />
Vloga in oddelek
set('department', e.target.value)} placeholder="npr. Trgovina MS" />
{f.role === 'administrator' && Administratorji so samodejno v skupini "Administrator".} {fieldError('groupId') && {fieldError('groupId')}}
Geslo
{isEdit && (
)} {(!isEdit || changePassword) && (
set('password', e.target.value)} autoComplete="new-password" style={fieldError('password') ? { borderColor: 'var(--danger)' } : null} /> {passwordHint(passwordPolicy)} {fieldError('password') && {fieldError('password')}}
)} {isEdit && ( <>
Status
{isSelf ? Lastnega računa ni mogoče deaktivirati. : ( )}
)}
* obvezna polja
); } function Uporabniki({ currentUser }) { const toast = useToast(); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [editing, setEditing] = useState(null); const [passwordPolicy, setPasswordPolicy] = useState(null); const [groups, setGroups] = useState([]); const load = async () => { setLoading(true); setError(''); const { ok, data } = await api('/api/users'); if (ok) { setRows(data.users); } else { setError('Seznama uporabnikov ni bilo mogoče naložiti.'); } setLoading(false); }; useEffect(() => { load(); api('/api/auth/password-policy').then(({ ok, data }) => { if (ok) setPasswordPolicy(data); }); api('/api/groups').then(({ ok, data }) => { if (ok) setGroups(data.groups); }); }, []); const save = async (id, payload, isEdit, setErrors) => { const { ok, data } = isEdit ? await api(`/api/users/${id}`, { method: 'PUT', body: JSON.stringify(payload) }) : await api('/api/users', { method: 'POST', body: JSON.stringify(payload) }); if (!ok) { setErrors(data.errors || {}); return false; } setRows(rs => isEdit ? rs.map(r => r.id === data.user.id ? data.user : r) : [data.user, ...rs]); toast(isEdit ? 'Uporabnik posodobljen' : 'Uporabnik dodan: ' + data.user.fullName, 'check'); return true; }; const toggleStatus = async (id, newStatus, onDone) => { const action = newStatus === 'inactive' ? 'deactivate' : 'reactivate'; const { ok, data } = await api(`/api/users/${id}/${action}`, { method: 'POST' }); if (!ok) { toast(data.error === 'cannot_deactivate_self' ? 'Lastnega računa ni mogoče deaktivirati.' : 'Napaka pri spremembi statusa.', 'alert'); return; } setRows(rs => rs.map(r => r.id === data.user.id ? data.user : r)); onDone(data.user.status); toast(newStatus === 'inactive' ? 'Uporabnik deaktiviran' : 'Uporabnik aktiviran', 'check'); }; return (
{error &&
{error}
} d.id} selectable exportName="kodila-uporabniki" exportTitle="Šifrant uporabnikov" searchPlaceholder="Išči po imenu, e-pošti, oddelku…" onRowOpen={(d) => setEditing(d)} actions={ } /> {editing !== null && ( setEditing(null)} /> )}
); } Object.assign(window, { Uporabniki });