1417 lines
56 KiB
PHP
1417 lines
56 KiB
PHP
<?php
|
|
include('include/headscript.php');
|
|
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
session_start();
|
|
}
|
|
|
|
if (!$user->hasRole('Admin') && !$user->hasRole('SuperUser')) {
|
|
header('Location: import_dashboard.php');
|
|
exit;
|
|
}
|
|
|
|
$db = DBHandlerSelect::getInstance();
|
|
$pdo = $db->getConnection();
|
|
|
|
/*
|
|
* Default values for new users.
|
|
*/
|
|
$defaultStatus = 'Active';
|
|
|
|
/*
|
|
* Avatar upload directory.
|
|
* This path assumes this page is inside /public and avatars are stored in /upload/users.
|
|
*/
|
|
$avatarUploadDir = dirname(__DIR__) . '/upload/users/';
|
|
$avatarPublicPath = '../upload/users/';
|
|
|
|
if (!is_dir($avatarUploadDir)) {
|
|
mkdir($avatarUploadDir, 0755, true);
|
|
}
|
|
|
|
/*
|
|
* Create CSRF token.
|
|
*/
|
|
if (empty($_SESSION['user_admin_csrf'])) {
|
|
$_SESSION['user_admin_csrf'] = bin2hex(random_bytes(32));
|
|
}
|
|
|
|
$csrfToken = $_SESSION['user_admin_csrf'];
|
|
|
|
function cleanInput($value)
|
|
{
|
|
return trim((string)($value ?? ''));
|
|
}
|
|
|
|
function redirectWithMessage($type, $message)
|
|
{
|
|
$_SESSION['user_admin_flash'] = [
|
|
'type' => $type,
|
|
'message' => $message
|
|
];
|
|
|
|
header('Location: ' . $_SERVER['PHP_SELF']);
|
|
exit;
|
|
}
|
|
|
|
function getEnvValue($key, $default = '')
|
|
{
|
|
if (isset($_ENV[$key]) && $_ENV[$key] !== '') {
|
|
return $_ENV[$key];
|
|
}
|
|
|
|
$value = getenv($key);
|
|
|
|
if ($value !== false && $value !== '') {
|
|
return $value;
|
|
}
|
|
|
|
return $default;
|
|
}
|
|
|
|
function uploadAvatarIfPresent($inputName, $avatarUploadDir)
|
|
{
|
|
if (
|
|
empty($_FILES[$inputName]) ||
|
|
empty($_FILES[$inputName]['name']) ||
|
|
$_FILES[$inputName]['error'] === UPLOAD_ERR_NO_FILE
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
if ($_FILES[$inputName]['error'] !== UPLOAD_ERR_OK) {
|
|
throw new Exception('Avatar upload failed.');
|
|
}
|
|
|
|
$maxSize = 3 * 1024 * 1024;
|
|
|
|
if ($_FILES[$inputName]['size'] > $maxSize) {
|
|
throw new Exception('Avatar is too large. Maximum size is 3 MB.');
|
|
}
|
|
|
|
$allowedMimeTypes = [
|
|
'image/jpeg' => 'jpg',
|
|
'image/png' => 'png',
|
|
'image/webp' => 'webp',
|
|
'image/gif' => 'gif'
|
|
];
|
|
|
|
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
|
$mimeType = $finfo->file($_FILES[$inputName]['tmp_name']);
|
|
|
|
if (!isset($allowedMimeTypes[$mimeType])) {
|
|
throw new Exception('Invalid avatar format. Allowed formats: JPG, PNG, WEBP, GIF.');
|
|
}
|
|
|
|
$extension = $allowedMimeTypes[$mimeType];
|
|
$filename = 'user_' . time() . '_' . bin2hex(random_bytes(6)) . '.' . $extension;
|
|
$destination = rtrim($avatarUploadDir, '/') . '/' . $filename;
|
|
|
|
if (!move_uploaded_file($_FILES[$inputName]['tmp_name'], $destination)) {
|
|
throw new Exception('Unable to save avatar.');
|
|
}
|
|
|
|
return $filename;
|
|
}
|
|
|
|
function isRoleAllowedForManagement(PDO $pdo, $roleId)
|
|
{
|
|
/*
|
|
* Admin role cannot be assigned from this page.
|
|
*/
|
|
$stmt = $pdo->prepare("
|
|
SELECT id
|
|
FROM auth_roles
|
|
WHERE id = :id
|
|
AND name <> 'Admin'
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([':id' => (int)$roleId]);
|
|
|
|
return (bool)$stmt->fetch(PDO::FETCH_ASSOC);
|
|
}
|
|
|
|
function isUserAdmin(PDO $pdo, $userId)
|
|
{
|
|
$stmt = $pdo->prepare("
|
|
SELECT u.id
|
|
FROM auth_users u
|
|
INNER JOIN auth_roles r ON r.id = u.role_id
|
|
WHERE u.id = :id
|
|
AND r.name = 'Admin'
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([':id' => (int)$userId]);
|
|
|
|
return (bool)$stmt->fetch(PDO::FETCH_ASSOC);
|
|
}
|
|
|
|
function smtpReadLine($socket)
|
|
{
|
|
$response = '';
|
|
|
|
while ($line = fgets($socket, 515)) {
|
|
$response .= $line;
|
|
|
|
if (isset($line[3]) && $line[3] === ' ') {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
function smtpCommand($socket, $command, array $expectedCodes)
|
|
{
|
|
if ($command !== null) {
|
|
fwrite($socket, $command . "\r\n");
|
|
}
|
|
|
|
$response = smtpReadLine($socket);
|
|
$code = (int)substr($response, 0, 3);
|
|
|
|
if (!in_array($code, $expectedCodes, true)) {
|
|
throw new Exception('SMTP error after command [' . $command . ']: ' . trim($response));
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
function buildSmartTrfWelcomeEmailHtml($firstName, $lastName, $email, $temporaryPassword, $loginUrl)
|
|
{
|
|
$fullName = trim($firstName . ' ' . $lastName);
|
|
|
|
$safeFullName = htmlspecialchars($fullName, ENT_QUOTES, 'UTF-8');
|
|
$safeEmail = htmlspecialchars($email, ENT_QUOTES, 'UTF-8');
|
|
$safePassword = htmlspecialchars($temporaryPassword, ENT_QUOTES, 'UTF-8');
|
|
$safeLoginUrl = htmlspecialchars($loginUrl, ENT_QUOTES, 'UTF-8');
|
|
|
|
return '
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Welcome to SmartTRF</title>
|
|
</head>
|
|
<body style="margin:0;padding:0;background:#f4f7fb;font-family:Arial,Helvetica,sans-serif;color:#1f2937;">
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f4f7fb;padding:30px 0;">
|
|
<tr>
|
|
<td align="center">
|
|
<table width="620" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:16px;overflow:hidden;box-shadow:0 10px 30px rgba(15,23,42,0.08);">
|
|
<tr>
|
|
<td style="background:linear-gradient(135deg,#0f172a,#1d4ed8);padding:34px 36px;">
|
|
<div style="font-size:26px;font-weight:700;color:#ffffff;letter-spacing:0.2px;">SmartTRF</div>
|
|
<div style="font-size:14px;color:#dbeafe;margin-top:6px;">Your digital workspace for sample and TRF management</div>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td style="padding:34px 36px 10px 36px;">
|
|
<h1 style="font-size:22px;line-height:1.3;margin:0 0 14px 0;color:#111827;">Welcome, ' . $safeFullName . '</h1>
|
|
<p style="font-size:15px;line-height:1.7;margin:0;color:#374151;">
|
|
Your SmartTRF account has been created. You can now access the platform using the credentials below.
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td style="padding:18px 36px;">
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;">
|
|
<tr>
|
|
<td style="padding:20px 22px;">
|
|
<div style="font-size:13px;text-transform:uppercase;letter-spacing:0.08em;color:#64748b;font-weight:700;margin-bottom:12px;">
|
|
Access credentials
|
|
</div>
|
|
|
|
<table width="100%" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td style="font-size:14px;color:#64748b;padding:7px 0;width:130px;">Email</td>
|
|
<td style="font-size:15px;color:#111827;font-weight:600;padding:7px 0;">' . $safeEmail . '</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="font-size:14px;color:#64748b;padding:7px 0;width:130px;">Temporary password</td>
|
|
<td style="font-size:16px;color:#111827;font-weight:700;padding:7px 0;font-family:Consolas,Monaco,monospace;">' . $safePassword . '</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td style="padding:6px 36px 18px 36px;">
|
|
<div style="background:#fff7ed;border:1px solid #fed7aa;border-radius:12px;padding:16px 18px;">
|
|
<div style="font-size:15px;font-weight:700;color:#9a3412;margin-bottom:5px;">Security recommendation</div>
|
|
<div style="font-size:14px;line-height:1.6;color:#7c2d12;">
|
|
This is a temporary password. For security reasons, please change it after your first login.
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td align="center" style="padding:12px 36px 34px 36px;">
|
|
<a href="' . $safeLoginUrl . '" style="display:inline-block;background:#2563eb;color:#ffffff;text-decoration:none;font-size:15px;font-weight:700;padding:14px 26px;border-radius:10px;">
|
|
Access SmartTRF
|
|
</a>
|
|
|
|
<div style="font-size:12px;line-height:1.6;color:#64748b;margin-top:18px;">
|
|
If the button does not work, copy and paste this link into your browser:<br>
|
|
<span style="color:#2563eb;">' . $safeLoginUrl . '</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td style="background:#f8fafc;padding:22px 36px;border-top:1px solid #e5e7eb;">
|
|
<div style="font-size:12px;line-height:1.6;color:#64748b;">
|
|
This message was automatically generated by SmartTRF. Please do not reply to this email.
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<div style="font-size:12px;color:#94a3b8;margin-top:18px;">
|
|
SmartTRF · C.E. Soft
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</body>
|
|
</html>';
|
|
}
|
|
|
|
function buildSmartTrfWelcomeEmailText($firstName, $lastName, $email, $temporaryPassword, $loginUrl)
|
|
{
|
|
$fullName = trim($firstName . ' ' . $lastName);
|
|
|
|
return "Welcome to SmartTRF\n\n" .
|
|
"Hello " . $fullName . ",\n\n" .
|
|
"Your SmartTRF account has been created.\n\n" .
|
|
"Access credentials:\n" .
|
|
"Email: " . $email . "\n" .
|
|
"Temporary password: " . $temporaryPassword . "\n\n" .
|
|
"For security reasons, please change this temporary password after your first login.\n\n" .
|
|
"Login URL:\n" .
|
|
$loginUrl . "\n\n" .
|
|
"This message was automatically generated by SmartTRF.";
|
|
}
|
|
|
|
function sendSmartTrfWelcomeEmail($toEmail, $firstName, $lastName, $temporaryPassword)
|
|
{
|
|
$smtpHost = getEnvValue('MAIL_HOST');
|
|
$smtpPort = (int)getEnvValue('MAIL_PORT', 465);
|
|
$smtpUsername = getEnvValue('MAIL_USERNAME');
|
|
$smtpPassword = getEnvValue('MAIL_PASSWORD');
|
|
$smtpEncryption = strtolower(getEnvValue('MAIL_ENCRYPTION', 'ssl'));
|
|
$fromAddress = getEnvValue('MAIL_FROM_ADDRESS', $smtpUsername);
|
|
$fromName = getEnvValue('MAIL_FROM_NAME', 'SmartTRF');
|
|
$loginUrl = getEnvValue('APP_URL', 'https://trf.cesoft.io/public/login');
|
|
|
|
if ($smtpHost === '' || $smtpUsername === '' || $smtpPassword === '' || $fromAddress === '') {
|
|
throw new Exception('SMTP configuration is incomplete.');
|
|
}
|
|
|
|
$subject = 'Welcome to SmartTRF - Your account has been created';
|
|
|
|
$htmlBody = buildSmartTrfWelcomeEmailHtml($firstName, $lastName, $toEmail, $temporaryPassword, $loginUrl);
|
|
$textBody = buildSmartTrfWelcomeEmailText($firstName, $lastName, $toEmail, $temporaryPassword, $loginUrl);
|
|
|
|
$boundary = 'smarttrf_' . bin2hex(random_bytes(12));
|
|
|
|
$encodedSubject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
|
|
$encodedFromName = '=?UTF-8?B?' . base64_encode($fromName) . '?=';
|
|
|
|
$headers = [];
|
|
$headers[] = 'From: ' . $encodedFromName . ' <' . $fromAddress . '>';
|
|
$headers[] = 'To: <' . $toEmail . '>';
|
|
$headers[] = 'Subject: ' . $encodedSubject;
|
|
$headers[] = 'MIME-Version: 1.0';
|
|
$headers[] = 'Content-Type: multipart/alternative; boundary="' . $boundary . '"';
|
|
$headers[] = 'Date: ' . date('r');
|
|
$headers[] = 'Message-ID: <' . bin2hex(random_bytes(16)) . '@smarttrf>';
|
|
|
|
$message = '';
|
|
$message .= '--' . $boundary . "\r\n";
|
|
$message .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
|
$message .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
|
|
$message .= $textBody . "\r\n\r\n";
|
|
|
|
$message .= '--' . $boundary . "\r\n";
|
|
$message .= "Content-Type: text/html; charset=UTF-8\r\n";
|
|
$message .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
|
|
$message .= $htmlBody . "\r\n\r\n";
|
|
$message .= '--' . $boundary . "--\r\n";
|
|
|
|
$smtpMessage = implode("\r\n", $headers) . "\r\n\r\n" . $message;
|
|
|
|
$transportHost = ($smtpEncryption === 'ssl' ? 'ssl://' : '') . $smtpHost;
|
|
|
|
$socket = stream_socket_client(
|
|
$transportHost . ':' . $smtpPort,
|
|
$errno,
|
|
$errstr,
|
|
30,
|
|
STREAM_CLIENT_CONNECT
|
|
);
|
|
|
|
if (!$socket) {
|
|
throw new Exception('SMTP connection failed: ' . $errstr . ' (' . $errno . ')');
|
|
}
|
|
|
|
stream_set_timeout($socket, 30);
|
|
|
|
try {
|
|
smtpCommand($socket, null, [220]);
|
|
smtpCommand($socket, 'EHLO ' . ($_SERVER['SERVER_NAME'] ?? 'localhost'), [250]);
|
|
|
|
if ($smtpEncryption === 'tls') {
|
|
smtpCommand($socket, 'STARTTLS', [220]);
|
|
|
|
if (!stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
|
|
throw new Exception('Unable to enable TLS encryption.');
|
|
}
|
|
|
|
smtpCommand($socket, 'EHLO ' . ($_SERVER['SERVER_NAME'] ?? 'localhost'), [250]);
|
|
}
|
|
|
|
smtpCommand($socket, 'AUTH LOGIN', [334]);
|
|
smtpCommand($socket, base64_encode($smtpUsername), [334]);
|
|
smtpCommand($socket, base64_encode($smtpPassword), [235]);
|
|
|
|
smtpCommand($socket, 'MAIL FROM:<' . $fromAddress . '>', [250]);
|
|
smtpCommand($socket, 'RCPT TO:<' . $toEmail . '>', [250, 251]);
|
|
smtpCommand($socket, 'DATA', [354]);
|
|
|
|
fwrite($socket, $smtpMessage . "\r\n.\r\n");
|
|
smtpCommand($socket, null, [250]);
|
|
|
|
smtpCommand($socket, 'QUIT', [221]);
|
|
} finally {
|
|
fclose($socket);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Load roles available for creation/edit.
|
|
* Admin is intentionally excluded.
|
|
*/
|
|
$stmtRoles = $pdo->query("
|
|
SELECT id, name, display_name
|
|
FROM auth_roles
|
|
WHERE name <> 'Admin'
|
|
ORDER BY display_name ASC, name ASC
|
|
");
|
|
$manageableRoles = $stmtRoles->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
if (empty($manageableRoles)) {
|
|
redirectWithMessage('error', 'No manageable roles found. Please create at least one non-admin role.');
|
|
}
|
|
|
|
$defaultRoleId = (int)$manageableRoles[0]['id'];
|
|
|
|
/*
|
|
* Handle form actions.
|
|
*/
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
try {
|
|
$postedToken = $_POST['csrf_token'] ?? '';
|
|
|
|
if (!hash_equals($csrfToken, $postedToken)) {
|
|
throw new Exception('Invalid security token.');
|
|
}
|
|
|
|
$action = cleanInput($_POST['action'] ?? '');
|
|
|
|
if ($action === 'create') {
|
|
$firstName = cleanInput($_POST['first_name'] ?? '');
|
|
$lastName = cleanInput($_POST['last_name'] ?? '');
|
|
$email = cleanInput($_POST['email'] ?? '');
|
|
$password = (string)($_POST['password'] ?? '');
|
|
$roleId = (int)($_POST['role_id'] ?? 0);
|
|
|
|
$limsUserId = cleanInput($_POST['lims_user_id'] ?? '');
|
|
$limsGlobalUserId = cleanInput($_POST['lims_global_user_id'] ?? '');
|
|
|
|
if ($firstName === '' || $lastName === '' || $email === '' || $password === '' || $roleId <= 0) {
|
|
throw new Exception('Please fill all required fields.');
|
|
}
|
|
|
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
throw new Exception('Invalid email address.');
|
|
}
|
|
|
|
if (!isRoleAllowedForManagement($pdo, $roleId)) {
|
|
throw new Exception('You cannot assign the Admin role from this page.');
|
|
}
|
|
|
|
$stmt = $pdo->prepare("SELECT id FROM auth_users WHERE email = :email LIMIT 1");
|
|
$stmt->execute([':email' => $email]);
|
|
|
|
if ($stmt->fetch()) {
|
|
throw new Exception('This email already exists.');
|
|
}
|
|
|
|
$avatarFilename = uploadAvatarIfPresent('avatar', $avatarUploadDir);
|
|
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
|
|
|
$sql = "
|
|
INSERT INTO auth_users
|
|
(
|
|
email,
|
|
username,
|
|
password,
|
|
first_name,
|
|
last_name,
|
|
avatar,
|
|
role_id,
|
|
status,
|
|
lims_user_id,
|
|
lims_global_user_id,
|
|
created_at,
|
|
updated_at
|
|
)
|
|
VALUES
|
|
(
|
|
:email,
|
|
NULL,
|
|
:password,
|
|
:first_name,
|
|
:last_name,
|
|
:avatar,
|
|
:role_id,
|
|
:status,
|
|
:lims_user_id,
|
|
:lims_global_user_id,
|
|
NOW(),
|
|
NOW()
|
|
)
|
|
";
|
|
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute([
|
|
':email' => $email,
|
|
':password' => $passwordHash,
|
|
':first_name' => $firstName,
|
|
':last_name' => $lastName,
|
|
':avatar' => $avatarFilename,
|
|
':role_id' => $roleId,
|
|
':status' => $defaultStatus,
|
|
':lims_user_id' => ($limsUserId !== '' ? (int)$limsUserId : null),
|
|
':lims_global_user_id' => ($limsGlobalUserId !== '' ? (int)$limsGlobalUserId : null)
|
|
]);
|
|
|
|
try {
|
|
sendSmartTrfWelcomeEmail($email, $firstName, $lastName, $password);
|
|
redirectWithMessage('success', 'User created successfully. Welcome email sent.');
|
|
} catch (Exception $mailException) {
|
|
error_log('SmartTRF welcome email error: ' . $mailException->getMessage());
|
|
redirectWithMessage('warning', 'User created successfully, but the welcome email was not sent: ' . $mailException->getMessage());
|
|
}
|
|
}
|
|
|
|
if ($action === 'update') {
|
|
$userId = (int)($_POST['user_id'] ?? 0);
|
|
$firstName = cleanInput($_POST['first_name'] ?? '');
|
|
$lastName = cleanInput($_POST['last_name'] ?? '');
|
|
$email = cleanInput($_POST['email'] ?? '');
|
|
$password = (string)($_POST['password'] ?? '');
|
|
$roleId = (int)($_POST['role_id'] ?? 0);
|
|
|
|
$limsUserId = cleanInput($_POST['lims_user_id'] ?? '');
|
|
$limsGlobalUserId = cleanInput($_POST['lims_global_user_id'] ?? '');
|
|
|
|
if ($userId <= 0) {
|
|
throw new Exception('Invalid user ID.');
|
|
}
|
|
|
|
if (isUserAdmin($pdo, $userId)) {
|
|
throw new Exception('Admin users can be viewed but cannot be edited from this page.');
|
|
}
|
|
|
|
if ($firstName === '' || $lastName === '' || $email === '' || $roleId <= 0) {
|
|
throw new Exception('Please fill all required fields.');
|
|
}
|
|
|
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
throw new Exception('Invalid email address.');
|
|
}
|
|
|
|
if (!isRoleAllowedForManagement($pdo, $roleId)) {
|
|
throw new Exception('You cannot assign the Admin role from this page.');
|
|
}
|
|
|
|
$stmt = $pdo->prepare("SELECT id FROM auth_users WHERE email = :email AND id <> :id LIMIT 1");
|
|
$stmt->execute([
|
|
':email' => $email,
|
|
':id' => $userId
|
|
]);
|
|
|
|
if ($stmt->fetch()) {
|
|
throw new Exception('This email is already used by another user.');
|
|
}
|
|
|
|
$avatarFilename = uploadAvatarIfPresent('avatar', $avatarUploadDir);
|
|
|
|
$fields = [
|
|
'email = :email',
|
|
'first_name = :first_name',
|
|
'last_name = :last_name',
|
|
'role_id = :role_id',
|
|
'lims_user_id = :lims_user_id',
|
|
'lims_global_user_id = :lims_global_user_id',
|
|
'updated_at = NOW()'
|
|
];
|
|
|
|
$params = [
|
|
':email' => $email,
|
|
':first_name' => $firstName,
|
|
':last_name' => $lastName,
|
|
':role_id' => $roleId,
|
|
':lims_user_id' => ($limsUserId !== '' ? (int)$limsUserId : null),
|
|
':lims_global_user_id' => ($limsGlobalUserId !== '' ? (int)$limsGlobalUserId : null),
|
|
':id' => $userId
|
|
];
|
|
|
|
if ($avatarFilename !== null) {
|
|
$fields[] = 'avatar = :avatar';
|
|
$params[':avatar'] = $avatarFilename;
|
|
}
|
|
|
|
if ($password !== '') {
|
|
$fields[] = 'password = :password';
|
|
$params[':password'] = password_hash($password, PASSWORD_DEFAULT);
|
|
}
|
|
|
|
$sql = "UPDATE auth_users SET " . implode(', ', $fields) . " WHERE id = :id";
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
|
|
redirectWithMessage('success', 'User updated successfully.');
|
|
}
|
|
|
|
if ($action === 'delete') {
|
|
$userId = (int)($_POST['user_id'] ?? 0);
|
|
|
|
if ($userId <= 0) {
|
|
throw new Exception('Invalid user ID.');
|
|
}
|
|
|
|
if (isset($iduserlogin) && (int)$iduserlogin === $userId) {
|
|
throw new Exception('You cannot delete your own user.');
|
|
}
|
|
|
|
if (isUserAdmin($pdo, $userId)) {
|
|
throw new Exception('Admin users can be viewed but cannot be deleted from this page.');
|
|
}
|
|
|
|
$stmt = $pdo->prepare("DELETE FROM auth_users WHERE id = :id");
|
|
$stmt->execute([':id' => $userId]);
|
|
|
|
redirectWithMessage('success', 'User deleted successfully.');
|
|
}
|
|
|
|
throw new Exception('Invalid action.');
|
|
} catch (Exception $e) {
|
|
redirectWithMessage('error', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Load users with role information.
|
|
*/
|
|
$stmt = $pdo->query("
|
|
SELECT
|
|
u.id,
|
|
u.first_name,
|
|
u.last_name,
|
|
u.email,
|
|
u.avatar,
|
|
u.status,
|
|
u.role_id,
|
|
u.lims_user_id,
|
|
u.lims_global_user_id,
|
|
u.created_at,
|
|
u.updated_at,
|
|
r.name AS role_name,
|
|
r.display_name AS role_display_name
|
|
FROM auth_users u
|
|
LEFT JOIN auth_roles r ON r.id = u.role_id
|
|
ORDER BY u.first_name ASC, u.last_name ASC, u.email ASC
|
|
");
|
|
|
|
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$flash = $_SESSION['user_admin_flash'] ?? null;
|
|
unset($_SESSION['user_admin_flash']);
|
|
?>
|
|
<!doctype html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
|
<?php include('cssinclude.php'); ?>
|
|
|
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
|
|
|
<title>User Administration - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
|
|
|
<style>
|
|
.user-avatar {
|
|
width: 42px;
|
|
height: 42px;
|
|
object-fit: cover;
|
|
border-radius: 50%;
|
|
border: 1px solid #e5e7eb;
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.user-avatar-preview {
|
|
width: 74px;
|
|
height: 74px;
|
|
object-fit: cover;
|
|
border-radius: 50%;
|
|
border: 1px solid #e5e7eb;
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.table-actions {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.modal-avatar-box {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.small-muted {
|
|
font-size: 12px;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.role-badge-admin {
|
|
background: #212529;
|
|
color: #fff;
|
|
}
|
|
|
|
.role-badge-user {
|
|
background: #0d6efd;
|
|
color: #fff;
|
|
}
|
|
|
|
.role-badge-superuser {
|
|
background: #6f42c1;
|
|
color: #fff;
|
|
}
|
|
|
|
.role-badge-default {
|
|
background: #6c757d;
|
|
color: #fff;
|
|
}
|
|
|
|
.select2-container {
|
|
width: 100% !important;
|
|
}
|
|
|
|
.lims-loading {
|
|
font-size: 12px;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.password-note {
|
|
background: #fff7ed;
|
|
border: 1px solid #fed7aa;
|
|
color: #9a3412;
|
|
border-radius: 10px;
|
|
padding: 10px 12px;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="wrapper">
|
|
<?php include('include/navbar.php'); ?>
|
|
<?php include('include/topbar.php'); ?>
|
|
|
|
<div class="page-wrapper">
|
|
<div class="page-content">
|
|
<?php include('top_stat_widget.php'); ?>
|
|
|
|
<div class="card radius-10">
|
|
<div class="card-header">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<h6 class="mb-0">User Administration</h6>
|
|
<small class="text-muted">Create, edit, change password and delete system users.</small>
|
|
</div>
|
|
<button type="button" class="btn btn-primary" id="btnAddUser">
|
|
<i class="bx bx-plus"></i> Add User
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table id="usersTable" class="table table-striped table-bordered align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 70px;">Avatar</th>
|
|
<th>First Name</th>
|
|
<th>Last Name</th>
|
|
<th>Email</th>
|
|
<th>Role</th>
|
|
<th>Accettatore</th>
|
|
<th>LIMS Global</th>
|
|
<th>Status</th>
|
|
<th style="width: 190px;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($users as $userRow): ?>
|
|
<?php
|
|
$avatar = !empty($userRow['avatar']) ? $avatarPublicPath . $userRow['avatar'] : $avatarPublicPath . 'profile.png';
|
|
$roleName = $userRow['role_name'] ?? '';
|
|
$roleDisplayName = $userRow['role_display_name'] ?: $roleName;
|
|
|
|
$isAdminUser = ($roleName === 'Admin');
|
|
|
|
$roleBadgeClass = 'role-badge-default';
|
|
|
|
if ($roleName === 'Admin') {
|
|
$roleBadgeClass = 'role-badge-admin';
|
|
} elseif ($roleName === 'User') {
|
|
$roleBadgeClass = 'role-badge-user';
|
|
} elseif ($roleName === 'SuperUser') {
|
|
$roleBadgeClass = 'role-badge-superuser';
|
|
}
|
|
|
|
$cannotDeleteSelf = isset($iduserlogin) && (int)$iduserlogin === (int)$userRow['id'];
|
|
?>
|
|
<tr>
|
|
<td>
|
|
<img src="<?= htmlspecialchars($avatar, ENT_QUOTES, 'UTF-8'); ?>" class="user-avatar" alt="Avatar">
|
|
</td>
|
|
<td><?= htmlspecialchars($userRow['first_name'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
|
|
<td><?= htmlspecialchars($userRow['last_name'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
|
|
<td><?= htmlspecialchars($userRow['email'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
|
|
<td>
|
|
<span class="badge <?= htmlspecialchars($roleBadgeClass, ENT_QUOTES, 'UTF-8'); ?>">
|
|
<?= htmlspecialchars($roleDisplayName ?? '', ENT_QUOTES, 'UTF-8'); ?>
|
|
</span>
|
|
</td>
|
|
<td
|
|
class="lims-acceptor-display"
|
|
data-lims-user-id="<?= htmlspecialchars($userRow['lims_user_id'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
|
|
<?= htmlspecialchars($userRow['lims_user_id'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
|
|
</td>
|
|
<td
|
|
class="lims-global-display"
|
|
data-lims-global-user-id="<?= htmlspecialchars($userRow['lims_global_user_id'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
|
|
<?= htmlspecialchars($userRow['lims_global_user_id'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-success">
|
|
<?= htmlspecialchars($userRow['status'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
|
|
</span>
|
|
</td>
|
|
<td class="table-actions">
|
|
<?php if ($isAdminUser): ?>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" disabled>
|
|
Admin locked
|
|
</button>
|
|
<?php else: ?>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-outline-primary btnEditUser"
|
|
data-id="<?= (int)$userRow['id']; ?>"
|
|
data-first-name="<?= htmlspecialchars($userRow['first_name'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
|
data-last-name="<?= htmlspecialchars($userRow['last_name'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
|
data-email="<?= htmlspecialchars($userRow['email'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
|
data-role-id="<?= (int)$userRow['role_id']; ?>"
|
|
data-lims-user-id="<?= htmlspecialchars($userRow['lims_user_id'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
|
data-lims-global-user-id="<?= htmlspecialchars($userRow['lims_global_user_id'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
|
data-avatar="<?= htmlspecialchars($avatar, ENT_QUOTES, 'UTF-8'); ?>">
|
|
Edit
|
|
</button>
|
|
|
|
<?php if ($cannotDeleteSelf): ?>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" disabled>
|
|
Current user
|
|
</button>
|
|
<?php else: ?>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-outline-danger btnDeleteUser"
|
|
data-id="<?= (int)$userRow['id']; ?>"
|
|
data-name="<?= htmlspecialchars(trim(($userRow['first_name'] ?? '') . ' ' . ($userRow['last_name'] ?? '')), ENT_QUOTES, 'UTF-8'); ?>">
|
|
Delete
|
|
</button>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="lims-loading mt-2" id="limsLoadingMessage">
|
|
Loading LIMS users and acceptors...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overlay toggle-icon"></div>
|
|
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
|
<?php include('include/footer.php'); ?>
|
|
</div>
|
|
|
|
<!-- Add/Edit User Modal -->
|
|
<div class="modal fade" id="userModal" tabindex="-1" aria-labelledby="userModalTitle" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<form method="POST" enctype="multipart/form-data" class="modal-content" id="userForm">
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
|
|
<input type="hidden" name="action" id="formAction" value="create">
|
|
<input type="hidden" name="user_id" id="userId" value="">
|
|
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="userModalTitle">Add User</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
|
|
<div class="modal-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label for="firstName" class="form-label">First Name *</label>
|
|
<input type="text" class="form-control" id="firstName" name="first_name" required>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label for="lastName" class="form-label">Last Name *</label>
|
|
<input type="text" class="form-control" id="lastName" name="last_name" required>
|
|
</div>
|
|
|
|
<div class="col-md-12">
|
|
<label for="email" class="form-label">Email *</label>
|
|
<input type="email" class="form-control" id="email" name="email" required>
|
|
<div class="small-muted mt-1" id="emailHelp">
|
|
When a new user is created, SmartTRF sends a welcome email with the temporary password.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-12">
|
|
<label for="roleId" class="form-label">Role *</label>
|
|
<select class="form-select" id="roleId" name="role_id" required>
|
|
<option value="">Select role</option>
|
|
<?php foreach ($manageableRoles as $role): ?>
|
|
<option value="<?= (int)$role['id']; ?>">
|
|
<?= htmlspecialchars($role['display_name'] ?: $role['name'], ENT_QUOTES, 'UTF-8'); ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<div class="small-muted mt-1">
|
|
Admin role is visible in the table but cannot be assigned from this page.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label for="limsUserId" class="form-label">Accettatore</label>
|
|
<select class="form-select" id="limsUserId" name="lims_user_id">
|
|
<option value="">Select acceptor</option>
|
|
</select>
|
|
<div class="small-muted mt-1">
|
|
Values loaded from CustomField ID 244.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label for="limsGlobalUserId" class="form-label">LIMS Global</label>
|
|
<select class="form-select" id="limsGlobalUserId" name="lims_global_user_id">
|
|
<option value="">Select LIMS user</option>
|
|
</select>
|
|
<div class="small-muted mt-1">
|
|
Values loaded from LIMS users list.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-12">
|
|
<label for="password" class="form-label" id="passwordLabel">Temporary Password *</label>
|
|
<input type="password" class="form-control" id="password" name="password">
|
|
<div class="small-muted" id="passwordHelp">
|
|
For new users, this temporary password will be sent by email. When editing, leave this field empty to keep the current password unchanged.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-12">
|
|
<div class="password-note" id="passwordSecurityNote">
|
|
<strong>Security note:</strong> the password entered during user creation is sent to the user as a temporary password. The email will recommend changing it after the first login.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-12">
|
|
<label for="avatar" class="form-label">Avatar</label>
|
|
|
|
<div class="modal-avatar-box mb-2">
|
|
<img src="<?= htmlspecialchars($avatarPublicPath . 'profile.png', ENT_QUOTES, 'UTF-8'); ?>" id="avatarPreview" class="user-avatar-preview" alt="Avatar preview">
|
|
<div>
|
|
<input type="file" class="form-control" id="avatar" name="avatar" accept="image/jpeg,image/png,image/webp,image/gif">
|
|
<div class="small-muted mt-1">Allowed formats: JPG, PNG, WEBP, GIF. Max 3 MB.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary" id="saveUserButton">Save User</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Form -->
|
|
<form method="POST" id="deleteUserForm" style="display:none;">
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
|
|
<input type="hidden" name="action" value="delete">
|
|
<input type="hidden" name="user_id" id="deleteUserId" value="">
|
|
</form>
|
|
|
|
<?php include('jsinclude.php'); ?>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const userModalElement = document.getElementById('userModal');
|
|
const userModal = new bootstrap.Modal(userModalElement);
|
|
|
|
const formAction = document.getElementById('formAction');
|
|
const userId = document.getElementById('userId');
|
|
const userModalTitle = document.getElementById('userModalTitle');
|
|
const saveUserButton = document.getElementById('saveUserButton');
|
|
|
|
const firstName = document.getElementById('firstName');
|
|
const lastName = document.getElementById('lastName');
|
|
const email = document.getElementById('email');
|
|
const roleId = document.getElementById('roleId');
|
|
const limsUserId = document.getElementById('limsUserId');
|
|
const limsGlobalUserId = document.getElementById('limsGlobalUserId');
|
|
const password = document.getElementById('password');
|
|
const passwordLabel = document.getElementById('passwordLabel');
|
|
const passwordHelp = document.getElementById('passwordHelp');
|
|
const passwordSecurityNote = document.getElementById('passwordSecurityNote');
|
|
const avatar = document.getElementById('avatar');
|
|
const avatarPreview = document.getElementById('avatarPreview');
|
|
const limsLoadingMessage = document.getElementById('limsLoadingMessage');
|
|
|
|
const defaultAvatar = "<?= htmlspecialchars($avatarPublicPath . 'profile.png', ENT_QUOTES, 'UTF-8'); ?>";
|
|
const defaultRoleId = "<?= (int)$defaultRoleId; ?>";
|
|
|
|
let limsGlobalUsersMap = {};
|
|
let limsAcceptorsMap = {};
|
|
|
|
/*
|
|
* Adjust only these endpoints if the page is moved to another folder.
|
|
*/
|
|
const limsGlobalUsersEndpoint = 'get_utenti.php';
|
|
const limsAcceptorsEndpoint = 'get_customfield_values.php?field_ids=244';
|
|
|
|
function initSelect2() {
|
|
if (typeof $ === 'undefined') {
|
|
console.error('jQuery is not loaded. Select2 cannot start.');
|
|
return;
|
|
}
|
|
|
|
if (typeof $.fn.select2 === 'undefined') {
|
|
console.error('Select2 is not loaded. Check select2.min.js include.');
|
|
return;
|
|
}
|
|
|
|
$('#roleId').select2({
|
|
dropdownParent: $('#userModal'),
|
|
width: '100%',
|
|
minimumResultsForSearch: 0,
|
|
placeholder: 'Select role'
|
|
});
|
|
|
|
$('#limsUserId').select2({
|
|
dropdownParent: $('#userModal'),
|
|
width: '100%',
|
|
placeholder: 'Select acceptor',
|
|
allowClear: true,
|
|
minimumResultsForSearch: 0
|
|
});
|
|
|
|
$('#limsGlobalUserId').select2({
|
|
dropdownParent: $('#userModal'),
|
|
width: '100%',
|
|
placeholder: 'Select LIMS user',
|
|
allowClear: true,
|
|
minimumResultsForSearch: 0
|
|
});
|
|
}
|
|
|
|
function destroySelect2IfActive(selector) {
|
|
if (typeof $ !== 'undefined' && $.fn.select2 && $(selector).hasClass('select2-hidden-accessible')) {
|
|
$(selector).select2('destroy');
|
|
}
|
|
}
|
|
|
|
function refreshLimsSelect2() {
|
|
if (typeof $ === 'undefined' || typeof $.fn.select2 === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
destroySelect2IfActive('#limsUserId');
|
|
destroySelect2IfActive('#limsGlobalUserId');
|
|
|
|
$('#limsUserId').select2({
|
|
dropdownParent: $('#userModal'),
|
|
width: '100%',
|
|
placeholder: 'Select acceptor',
|
|
allowClear: true,
|
|
minimumResultsForSearch: 0
|
|
});
|
|
|
|
$('#limsGlobalUserId').select2({
|
|
dropdownParent: $('#userModal'),
|
|
width: '100%',
|
|
placeholder: 'Select LIMS user',
|
|
allowClear: true,
|
|
minimumResultsForSearch: 0
|
|
});
|
|
|
|
$('#limsUserId').trigger('change');
|
|
$('#limsGlobalUserId').trigger('change');
|
|
}
|
|
|
|
function setSelectValue(selectElement, value) {
|
|
selectElement.value = value || '';
|
|
|
|
if (typeof $ !== 'undefined' && $.fn.select2) {
|
|
$(selectElement).trigger('change');
|
|
}
|
|
}
|
|
|
|
function clearSelect(selectElement, placeholderText) {
|
|
selectElement.innerHTML = '';
|
|
|
|
const option = document.createElement('option');
|
|
option.value = '';
|
|
option.textContent = placeholderText;
|
|
selectElement.appendChild(option);
|
|
}
|
|
|
|
function appendOption(selectElement, value, text) {
|
|
if (value === null || value === undefined || value === '') {
|
|
return;
|
|
}
|
|
|
|
const option = document.createElement('option');
|
|
option.value = String(value);
|
|
option.textContent = text || String(value);
|
|
selectElement.appendChild(option);
|
|
}
|
|
|
|
function normalizeLimsGlobalUsers(data) {
|
|
const list = Array.isArray(data) ? data : (data.value || []);
|
|
const normalized = [];
|
|
|
|
list.forEach(function(item) {
|
|
const id = item.IdUtente ?? item.id ?? item.ID ?? null;
|
|
const text = item.Nome || item.Nominativo || item.UserName || item.Email || id;
|
|
|
|
if (id !== null && id !== undefined && id !== '') {
|
|
normalized.push({
|
|
id: String(id),
|
|
text: String(text)
|
|
});
|
|
}
|
|
});
|
|
|
|
normalized.sort(function(a, b) {
|
|
return String(a.text).localeCompare(String(b.text), 'it', {
|
|
sensitivity: 'base'
|
|
});
|
|
});
|
|
|
|
return normalized;
|
|
}
|
|
|
|
function normalizeAcceptors(data) {
|
|
/*
|
|
* Expected response:
|
|
* {
|
|
* "244": [
|
|
* { ... }
|
|
* ]
|
|
* }
|
|
*/
|
|
const list = data['244'] || data[244] || [];
|
|
const normalized = [];
|
|
|
|
list.forEach(function(item) {
|
|
const id =
|
|
item.IdCustomFieldValue ??
|
|
item.IdCustomFieldsValue ??
|
|
item.IdCustomFieldValues ??
|
|
item.Id ??
|
|
item.ID ??
|
|
item.id ??
|
|
item.ValueId ??
|
|
item.value_id ??
|
|
item.Codice ??
|
|
null;
|
|
|
|
const text =
|
|
item.Value ??
|
|
item.Valore ??
|
|
item.Nome ??
|
|
item.Name ??
|
|
item.Descrizione ??
|
|
item.Description ??
|
|
item.Text ??
|
|
item.text ??
|
|
item.Label ??
|
|
item.label ??
|
|
id;
|
|
|
|
if (id !== null && id !== undefined && id !== '') {
|
|
normalized.push({
|
|
id: String(id),
|
|
text: String(text)
|
|
});
|
|
}
|
|
});
|
|
|
|
normalized.sort(function(a, b) {
|
|
return String(a.text).localeCompare(String(b.text), 'it', {
|
|
sensitivity: 'base'
|
|
});
|
|
});
|
|
|
|
return normalized;
|
|
}
|
|
|
|
function populateLimsGlobalUsers(users) {
|
|
clearSelect(limsGlobalUserId, 'Select LIMS user');
|
|
limsGlobalUsersMap = {};
|
|
|
|
users.forEach(function(user) {
|
|
limsGlobalUsersMap[user.id] = user.text;
|
|
appendOption(limsGlobalUserId, user.id, user.text);
|
|
});
|
|
}
|
|
|
|
function populateAcceptors(acceptors) {
|
|
clearSelect(limsUserId, 'Select acceptor');
|
|
limsAcceptorsMap = {};
|
|
|
|
acceptors.forEach(function(item) {
|
|
limsAcceptorsMap[item.id] = item.text;
|
|
appendOption(limsUserId, item.id, item.text);
|
|
});
|
|
}
|
|
|
|
function updateTableDisplayNames() {
|
|
document.querySelectorAll('.lims-global-display').forEach(function(cell) {
|
|
const id = String(cell.dataset.limsGlobalUserId || '');
|
|
|
|
if (id && limsGlobalUsersMap[id]) {
|
|
cell.textContent = limsGlobalUsersMap[id];
|
|
}
|
|
});
|
|
|
|
document.querySelectorAll('.lims-acceptor-display').forEach(function(cell) {
|
|
const id = String(cell.dataset.limsUserId || '');
|
|
|
|
if (id && limsAcceptorsMap[id]) {
|
|
cell.textContent = limsAcceptorsMap[id];
|
|
}
|
|
});
|
|
}
|
|
|
|
async function loadLimsDropdowns() {
|
|
try {
|
|
const [globalResponse, acceptorsResponse] = await Promise.all([
|
|
fetch(limsGlobalUsersEndpoint, {
|
|
cache: 'no-store'
|
|
}),
|
|
fetch(limsAcceptorsEndpoint, {
|
|
cache: 'no-store'
|
|
})
|
|
]);
|
|
|
|
if (!globalResponse.ok) {
|
|
throw new Error('Unable to load LIMS global users.');
|
|
}
|
|
|
|
if (!acceptorsResponse.ok) {
|
|
throw new Error('Unable to load LIMS acceptors.');
|
|
}
|
|
|
|
const globalData = await globalResponse.json();
|
|
const acceptorsData = await acceptorsResponse.json();
|
|
|
|
const globalUsers = normalizeLimsGlobalUsers(globalData);
|
|
const acceptors = normalizeAcceptors(acceptorsData);
|
|
|
|
populateLimsGlobalUsers(globalUsers);
|
|
populateAcceptors(acceptors);
|
|
updateTableDisplayNames();
|
|
refreshLimsSelect2();
|
|
|
|
if (limsLoadingMessage) {
|
|
limsLoadingMessage.textContent = 'LIMS users and acceptors loaded.';
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
|
|
if (limsLoadingMessage) {
|
|
limsLoadingMessage.textContent = 'Warning: unable to load LIMS users or acceptors.';
|
|
}
|
|
|
|
Swal.fire({
|
|
icon: 'warning',
|
|
title: 'LIMS lists not loaded',
|
|
text: error.message || 'Unable to load LIMS dropdown values.'
|
|
});
|
|
}
|
|
}
|
|
|
|
function resetModalForCreate() {
|
|
formAction.value = 'create';
|
|
userId.value = '';
|
|
|
|
userModalTitle.textContent = 'Add User';
|
|
saveUserButton.textContent = 'Create User';
|
|
|
|
firstName.value = '';
|
|
lastName.value = '';
|
|
email.value = '';
|
|
setSelectValue(roleId, defaultRoleId);
|
|
setSelectValue(limsUserId, '');
|
|
setSelectValue(limsGlobalUserId, '');
|
|
password.value = '';
|
|
avatar.value = '';
|
|
avatarPreview.src = defaultAvatar;
|
|
|
|
password.required = true;
|
|
passwordLabel.textContent = 'Temporary Password *';
|
|
passwordHelp.textContent = 'Password is required when creating a new user. It will be sent by email as a temporary password.';
|
|
passwordSecurityNote.style.display = 'block';
|
|
}
|
|
|
|
function setModalForEdit(button) {
|
|
formAction.value = 'update';
|
|
userId.value = button.dataset.id || '';
|
|
|
|
userModalTitle.textContent = 'Edit User';
|
|
saveUserButton.textContent = 'Update User';
|
|
|
|
firstName.value = button.dataset.firstName || '';
|
|
lastName.value = button.dataset.lastName || '';
|
|
email.value = button.dataset.email || '';
|
|
|
|
setSelectValue(roleId, button.dataset.roleId || '');
|
|
setSelectValue(limsUserId, button.dataset.limsUserId || '');
|
|
setSelectValue(limsGlobalUserId, button.dataset.limsGlobalUserId || '');
|
|
|
|
password.value = '';
|
|
avatar.value = '';
|
|
avatarPreview.src = button.dataset.avatar || defaultAvatar;
|
|
|
|
password.required = false;
|
|
passwordLabel.textContent = 'Password';
|
|
passwordHelp.textContent = 'Leave this field empty to keep the current password unchanged. Type a new password only if you want to replace it.';
|
|
passwordSecurityNote.style.display = 'none';
|
|
}
|
|
|
|
document.getElementById('btnAddUser').addEventListener('click', function() {
|
|
resetModalForCreate();
|
|
userModal.show();
|
|
});
|
|
|
|
document.querySelectorAll('.btnEditUser').forEach(function(button) {
|
|
button.addEventListener('click', function() {
|
|
setModalForEdit(button);
|
|
userModal.show();
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.btnDeleteUser').forEach(function(button) {
|
|
button.addEventListener('click', function() {
|
|
const id = button.dataset.id;
|
|
const name = button.dataset.name || 'this user';
|
|
|
|
Swal.fire({
|
|
title: 'Delete user?',
|
|
text: 'Are you sure you want to delete ' + name + '? This action cannot be undone.',
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Yes, delete',
|
|
cancelButtonText: 'Cancel',
|
|
confirmButtonColor: '#d33'
|
|
}).then(function(result) {
|
|
if (result.isConfirmed) {
|
|
document.getElementById('deleteUserId').value = id;
|
|
document.getElementById('deleteUserForm').submit();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
avatar.addEventListener('change', function(event) {
|
|
const file = event.target.files[0];
|
|
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = function(e) {
|
|
avatarPreview.src = e.target.result;
|
|
};
|
|
|
|
reader.readAsDataURL(file);
|
|
});
|
|
|
|
initSelect2();
|
|
|
|
if (typeof $ !== 'undefined' && $.fn.DataTable) {
|
|
$('#usersTable').DataTable({
|
|
pageLength: 25,
|
|
order: [
|
|
[1, 'asc'],
|
|
[2, 'asc']
|
|
]
|
|
});
|
|
}
|
|
|
|
loadLimsDropdowns();
|
|
|
|
<?php if ($flash): ?>
|
|
Swal.fire({
|
|
icon: '<?= htmlspecialchars($flash['type'], ENT_QUOTES, 'UTF-8'); ?>',
|
|
title: '<?= $flash['type'] === 'success' ? 'Done' : ($flash['type'] === 'warning' ? 'Warning' : 'Error'); ?>',
|
|
text: '<?= htmlspecialchars($flash['message'], ENT_QUOTES, 'UTF-8'); ?>'
|
|
});
|
|
<?php endif; ?>
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|