686 lines
28 KiB
PHP
686 lines
28 KiB
PHP
<?php include('include/headscript.php'); ?>
|
|
<!doctype html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<!-- Required meta tags -->
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<!--favicon-->
|
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
|
|
|
<?php include('cssinclude.php'); ?>
|
|
|
|
<!-- Select2 CSS -->
|
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
|
|
|
<title>SmartTRF - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> - User Profile</title>
|
|
|
|
<style>
|
|
.profile-avatar-preview {
|
|
max-width: 100px;
|
|
width: 100px;
|
|
height: 100px;
|
|
object-fit: cover;
|
|
border-radius: 50%;
|
|
border: 1px solid #e5e7eb;
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.small-muted {
|
|
font-size: 12px;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.select2-container {
|
|
width: 100% !important;
|
|
}
|
|
|
|
.lims-loading {
|
|
font-size: 12px;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.password-note {
|
|
background: #f8fafc;
|
|
border: 1px solid #e5e7eb;
|
|
color: #475569;
|
|
border-radius: 10px;
|
|
padding: 10px 12px;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.password-error {
|
|
display: none;
|
|
background: #fff1f2;
|
|
border: 1px solid #fecdd3;
|
|
color: #be123c;
|
|
border-radius: 10px;
|
|
padding: 10px 12px;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.password-success {
|
|
display: none;
|
|
background: #f0fdf4;
|
|
border: 1px solid #bbf7d0;
|
|
color: #166534;
|
|
border-radius: 10px;
|
|
padding: 10px 12px;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<!--wrapper-->
|
|
<div class="wrapper">
|
|
<!--sidebar wrapper -->
|
|
<?php include('include/navbar.php'); ?>
|
|
<!--end sidebar wrapper -->
|
|
|
|
<!--start header -->
|
|
<?php include('include/topbar.php'); ?>
|
|
<!--end header -->
|
|
|
|
<!--start page wrapper -->
|
|
<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">
|
|
<div>
|
|
<h6 class="mb-0">Edit Profile</h6>
|
|
<small class="text-muted">Update your personal information and LIMS references.</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<form action="update-profile.php" method="POST" enctype="multipart/form-data" id="profileForm">
|
|
<div class="row g-3">
|
|
|
|
<div class="col-md-6">
|
|
<label for="first_name" class="form-label">First Name</label>
|
|
<input
|
|
type="text"
|
|
class="form-control"
|
|
id="first_name"
|
|
name="first_name"
|
|
value="<?= htmlspecialchars($nameuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
|
required>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label for="last_name" class="form-label">Last Name</label>
|
|
<input
|
|
type="text"
|
|
class="form-control"
|
|
id="last_name"
|
|
name="last_name"
|
|
value="<?= htmlspecialchars($surnameuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
|
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"
|
|
value="<?= htmlspecialchars($emailuser ?? '', ENT_QUOTES, 'UTF-8'); ?>"
|
|
required>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label for="lims_user_id" class="form-label">Accettatore</label>
|
|
<select
|
|
class="form-select"
|
|
id="lims_user_id"
|
|
name="lims_user_id"
|
|
data-current-value="<?= htmlspecialchars($lims_user_id ?? '', ENT_QUOTES, 'UTF-8'); ?>">
|
|
<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="lims_global_user_id" class="form-label">LIMS Global</label>
|
|
<select
|
|
class="form-select"
|
|
id="lims_global_user_id"
|
|
name="lims_global_user_id"
|
|
data-current-value="<?= htmlspecialchars($lims_global_user_id ?? '', ENT_QUOTES, 'UTF-8'); ?>">
|
|
<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">
|
|
<div class="lims-loading" id="limsLoadingMessage">
|
|
Loading LIMS users and acceptors...
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-12">
|
|
<label for="avatar" class="form-label">Profile Picture</label>
|
|
<input
|
|
type="file"
|
|
class="form-control"
|
|
id="avatar"
|
|
name="avatar"
|
|
accept="image/jpeg,image/png,image/webp,image/gif">
|
|
|
|
<div class="mt-3 d-flex align-items-center gap-3">
|
|
<?php if (!empty($photousername)): ?>
|
|
<img
|
|
src="../upload/users/<?= htmlspecialchars($photousername, ENT_QUOTES, 'UTF-8'); ?>"
|
|
alt="Current Avatar"
|
|
class="profile-avatar-preview"
|
|
id="avatarPreview">
|
|
<?php else: ?>
|
|
<img
|
|
src="../upload/users/profile.png"
|
|
alt="Default Avatar"
|
|
class="profile-avatar-preview"
|
|
id="avatarPreview">
|
|
<?php endif; ?>
|
|
|
|
<div>
|
|
<div class="small-muted">Current avatar</div>
|
|
<div class="small-muted">Allowed formats: JPG, PNG, WEBP, GIF.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label for="password" class="form-label">New Password</label>
|
|
<div class="input-group">
|
|
<input
|
|
type="password"
|
|
class="form-control"
|
|
id="password"
|
|
name="password"
|
|
autocomplete="new-password">
|
|
<button
|
|
class="btn btn-outline-secondary toggle-password"
|
|
type="button"
|
|
data-target="password"
|
|
aria-label="Show or hide password">
|
|
<i class="bx bx-show"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label for="password_confirm" class="form-label">Confirm New Password</label>
|
|
<div class="input-group">
|
|
<input
|
|
type="password"
|
|
class="form-control"
|
|
id="password_confirm"
|
|
name="password_confirm"
|
|
autocomplete="new-password">
|
|
<button
|
|
class="btn btn-outline-secondary toggle-password"
|
|
type="button"
|
|
data-target="password_confirm"
|
|
aria-label="Show or hide password confirmation">
|
|
<i class="bx bx-show"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-12">
|
|
<div class="password-note" id="passwordNote">
|
|
<strong>Important:</strong> leave both password fields empty to keep your current password unchanged.
|
|
Type a new password only if you want to replace it.
|
|
</div>
|
|
|
|
<div class="password-error mt-2" id="passwordError">
|
|
The two password fields do not match. Please check them before saving.
|
|
</div>
|
|
|
|
<div class="password-success mt-2" id="passwordSuccess">
|
|
The two passwords match.
|
|
</div>
|
|
</div>
|
|
|
|
<input
|
|
type="hidden"
|
|
name="iduserlogin"
|
|
value="<?= htmlspecialchars($iduserlogin ?? '', ENT_QUOTES, 'UTF-8'); ?>">
|
|
|
|
<div class="col-md-12">
|
|
<button type="submit" class="btn btn-primary" id="submitProfileButton">
|
|
Update Profile
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!--end page wrapper -->
|
|
|
|
<!--start overlay-->
|
|
<div class="overlay toggle-icon"></div>
|
|
<!--end overlay-->
|
|
|
|
<!--Start Back To Top Button-->
|
|
<a href="javaScript:;" class="back-to-top">
|
|
<i class='bx bxs-up-arrow-alt'></i>
|
|
</a>
|
|
<!--End Back To Top Button-->
|
|
|
|
<?php include('include/footer.php'); ?>
|
|
</div>
|
|
<!--end wrapper-->
|
|
|
|
<!-- search modal -->
|
|
<?php //include('include/searchmodal.php');
|
|
?>
|
|
<!-- end search modal -->
|
|
|
|
<!--start switcher-->
|
|
<?php //include('include/themeswitcher.php');
|
|
?>
|
|
<!--end switcher-->
|
|
|
|
<?php include('jsinclude.php'); ?>
|
|
|
|
<!-- Select2 JS -->
|
|
<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 profileForm = document.getElementById('profileForm');
|
|
const submitProfileButton = document.getElementById('submitProfileButton');
|
|
|
|
const passwordInput = document.getElementById('password');
|
|
const passwordConfirmInput = document.getElementById('password_confirm');
|
|
const passwordError = document.getElementById('passwordError');
|
|
const passwordSuccess = document.getElementById('passwordSuccess');
|
|
|
|
const limsUserSelect = document.getElementById('lims_user_id');
|
|
const limsGlobalUserSelect = document.getElementById('lims_global_user_id');
|
|
const limsLoadingMessage = document.getElementById('limsLoadingMessage');
|
|
const avatarInput = document.getElementById('avatar');
|
|
const avatarPreview = document.getElementById('avatarPreview');
|
|
|
|
const currentLimsUserId = String(limsUserSelect.dataset.currentValue || '');
|
|
const currentLimsGlobalUserId = String(limsGlobalUserSelect.dataset.currentValue || '');
|
|
|
|
const limsGlobalUsersEndpoint = 'get_utenti.php';
|
|
const limsAcceptorsEndpoint = 'get_customfield_values.php?field_ids=244';
|
|
|
|
function validatePasswords(showEmptySuccess = false) {
|
|
const password = passwordInput.value;
|
|
const confirmPassword = passwordConfirmInput.value;
|
|
|
|
passwordError.style.display = 'none';
|
|
passwordSuccess.style.display = 'none';
|
|
passwordInput.classList.remove('is-invalid', 'is-valid');
|
|
passwordConfirmInput.classList.remove('is-invalid', 'is-valid');
|
|
|
|
/*
|
|
* Both empty means: keep current password.
|
|
*/
|
|
if (password === '' && confirmPassword === '') {
|
|
submitProfileButton.disabled = false;
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* One field filled and the other empty is not valid.
|
|
*/
|
|
if (password === '' || confirmPassword === '') {
|
|
passwordError.textContent = 'Please fill both password fields, or leave both empty to keep the current password.';
|
|
passwordError.style.display = 'block';
|
|
passwordInput.classList.add('is-invalid');
|
|
passwordConfirmInput.classList.add('is-invalid');
|
|
submitProfileButton.disabled = true;
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Both filled but different.
|
|
*/
|
|
if (password !== confirmPassword) {
|
|
passwordError.textContent = 'The two password fields do not match. Please check them before saving.';
|
|
passwordError.style.display = 'block';
|
|
passwordInput.classList.add('is-invalid');
|
|
passwordConfirmInput.classList.add('is-invalid');
|
|
submitProfileButton.disabled = true;
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Both filled and equal.
|
|
*/
|
|
passwordInput.classList.add('is-valid');
|
|
passwordConfirmInput.classList.add('is-valid');
|
|
passwordSuccess.style.display = showEmptySuccess ? 'block' : 'block';
|
|
submitProfileButton.disabled = false;
|
|
return true;
|
|
}
|
|
|
|
passwordInput.addEventListener('input', function() {
|
|
validatePasswords();
|
|
});
|
|
|
|
passwordConfirmInput.addEventListener('input', function() {
|
|
validatePasswords();
|
|
});
|
|
|
|
profileForm.addEventListener('submit', function(event) {
|
|
if (!validatePasswords(true)) {
|
|
event.preventDefault();
|
|
|
|
if (typeof Swal !== 'undefined') {
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: 'Password mismatch',
|
|
text: 'Please check the password fields before saving.'
|
|
});
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
$('#lims_user_id').select2({
|
|
width: '100%',
|
|
placeholder: 'Select acceptor',
|
|
allowClear: true,
|
|
minimumResultsForSearch: 0
|
|
});
|
|
|
|
$('#lims_global_user_id').select2({
|
|
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 refreshSelect2() {
|
|
if (typeof $ === 'undefined' || typeof $.fn.select2 === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
destroySelect2IfActive('#lims_user_id');
|
|
destroySelect2IfActive('#lims_global_user_id');
|
|
|
|
$('#lims_user_id').select2({
|
|
width: '100%',
|
|
placeholder: 'Select acceptor',
|
|
allowClear: true,
|
|
minimumResultsForSearch: 0
|
|
});
|
|
|
|
$('#lims_global_user_id').select2({
|
|
width: '100%',
|
|
placeholder: 'Select LIMS user',
|
|
allowClear: true,
|
|
minimumResultsForSearch: 0
|
|
});
|
|
|
|
$('#lims_user_id').trigger('change');
|
|
$('#lims_global_user_id').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 setSelectValue(selectElement, value) {
|
|
selectElement.value = value || '';
|
|
|
|
if (typeof $ !== 'undefined' && $.fn.select2) {
|
|
$(selectElement).trigger('change');
|
|
}
|
|
}
|
|
|
|
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) {
|
|
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(limsGlobalUserSelect, 'Select LIMS user');
|
|
|
|
users.forEach(function(user) {
|
|
appendOption(limsGlobalUserSelect, user.id, user.text);
|
|
});
|
|
|
|
if (currentLimsGlobalUserId !== '') {
|
|
setSelectValue(limsGlobalUserSelect, currentLimsGlobalUserId);
|
|
}
|
|
}
|
|
|
|
function populateAcceptors(acceptors) {
|
|
clearSelect(limsUserSelect, 'Select acceptor');
|
|
|
|
acceptors.forEach(function(item) {
|
|
appendOption(limsUserSelect, item.id, item.text);
|
|
});
|
|
|
|
if (currentLimsUserId !== '') {
|
|
setSelectValue(limsUserSelect, currentLimsUserId);
|
|
}
|
|
}
|
|
|
|
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);
|
|
refreshSelect2();
|
|
|
|
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.';
|
|
}
|
|
|
|
if (typeof Swal !== 'undefined') {
|
|
Swal.fire({
|
|
icon: 'warning',
|
|
title: 'LIMS lists not loaded',
|
|
text: error.message || 'Unable to load LIMS dropdown values.'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
avatarInput.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);
|
|
});
|
|
document.querySelectorAll('.toggle-password').forEach(function(button) {
|
|
button.addEventListener('click', function() {
|
|
const targetId = button.dataset.target;
|
|
const input = document.getElementById(targetId);
|
|
const icon = button.querySelector('i');
|
|
|
|
if (!input) {
|
|
return;
|
|
}
|
|
|
|
if (input.type === 'password') {
|
|
input.type = 'text';
|
|
|
|
if (icon) {
|
|
icon.classList.remove('bx-show');
|
|
icon.classList.add('bx-hide');
|
|
}
|
|
} else {
|
|
input.type = 'password';
|
|
|
|
if (icon) {
|
|
icon.classList.remove('bx-hide');
|
|
icon.classList.add('bx-show');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
initSelect2();
|
|
loadLimsDropdowns();
|
|
validatePasswords();
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|