Files
wms-app/app/setting/profile.php
T

647 lines
28 KiB
PHP

<?php
require_once __DIR__ . '/../session.php';
require '../config.php';
require '../include_header.php';
?>
<body>
<!-- zxcvbn JS: client-side password strength scoring, no server round-trip -->
<script src="https://cdn.jsdelivr.net/npm/zxcvbn@4.4.2/dist/zxcvbn.js"></script>
<?php require '../include_topbar.php'; ?>
<?php require '../include_setting_sidebar.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<!-- Page header -->
<div class="row">
<div class="col-12">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-6 gap-3">
<div>
<h1 class="fs-3 mb-1">Profile</h1>
<p class="mb-0">Manage your account details</p>
</div>
</div>
</div>
</div>
<div class="row g-5">
<!-- LEFT: Avatar card -->
<div class="col-lg-3 col-12">
<div class="card">
<div class="card-body p-6 text-center">
<div class="position-relative d-inline-block mb-4">
<img id="avatar_preview"
src="<?php echo $server_url?>assets/images/avatar-1.jpg"
alt="Profile picture"
class="rounded-circle border"
style="width:96px;height:96px;object-fit:cover;">
<label for="profile_picture"
class="position-absolute bottom-0 end-0 btn btn-sm btn-primary rounded-circle p-0 d-flex align-items-center justify-content-center"
style="width:26px;height:26px;cursor:pointer;" title="Change photo">
<i class="ti ti-camera" style="font-size:12px;line-height:1;"></i>
</label>
<input type="file" id="profile_picture" accept="image/*" class="d-none">
</div>
<h5 class="mb-0" id="display_name">—</h5>
<p class="text-muted small mb-0" id="display_username">—</p>
<p class="text-muted small mt-1 mb-0" id="display_email">—</p>
</div>
</div>
</div>
<!-- RIGHT -->
<div class="col-lg-9 col-12">
<!-- Personal information -->
<div class="card mb-5" id="profileForm">
<div class="card-body p-6">
<h2 class="fs-5 mb-4">Personal Information</h2>
<div class="row">
<div class="col-md-6 mb-3">
<label for="name" class="form-label">First Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="name" placeholder="First name" required>
</div>
<div class="col-md-6 mb-3">
<label for="surname" class="form-label">Last Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="surname" placeholder="Last name" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="email" class="form-label">Email <span class="text-danger">*</span></label>
<input type="email" class="form-control" id="email" placeholder="your@email.com" required>
</div>
<div class="col-md-6 mb-3">
<label for="phone" class="form-label">Phone</label>
<input type="text" class="form-control" id="phone" placeholder="e.g. 0812345678">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="country" class="form-label">Country</label>
<select class="form-select" id="country">
<option value="">— Select country —</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Username</label>
<input type="text" class="form-control" id="username_display" disabled>
<div class="form-text">Username cannot be changed</div>
</div>
</div>
<div class="row">
<div class="col-12 mb-3">
<label for="address" class="form-label">Address</label>
<textarea class="form-control" id="address" rows="2" placeholder="Enter your address"></textarea>
</div>
</div>
<div class="d-flex gap-2 mt-2">
<button class="btn btn-primary" onclick="save_profile();">
<i class="ti ti-device-floppy me-1"></i>Save Changes
</button>
</div>
</div>
</div>
<!-- Change password -->
<div class="card" id="passwordForm">
<div class="card-body p-6">
<h2 class="fs-5 mb-1">Change Password</h2>
<p class="text-muted small mb-4">Minimum required strength: <strong>Strong (3/4)</strong></p>
<div class="row">
<div class="col-md-6 mb-3">
<label for="current_password" class="form-label">
Current Password <span class="text-danger">*</span>
</label>
<div class="input-group">
<input type="password" class="form-control" id="current_password"
placeholder="Enter current password" oninput="update_pw_button()">
<button class="btn btn-outline-secondary toggle-pw" type="button" data-target="current_password">
<i class="ti ti-eye"></i>
</button>
</div>
<div class="form-text">
<a href="javascript:;" class="link-warning" onclick="open_forgot_modal();">
<i class="ti ti-help-circle me-1"></i>Forgot your current password?
</a>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="new_password" class="form-label">
New Password <span class="text-danger">*</span>
</label>
<div class="input-group">
<input type="password" class="form-control" id="new_password"
placeholder="Enter new password" oninput="on_new_password_input(this.value)">
<button class="btn btn-outline-secondary toggle-pw" type="button" data-target="new_password">
<i class="ti ti-eye"></i>
</button>
</div>
<div class="mt-2">
<div class="progress" style="height:5px;">
<div id="pw_strength_bar" class="progress-bar"
style="width:0%;transition:width .25s,background-color .25s;border-radius:4px;"></div>
</div>
<div class="d-flex justify-content-between align-items-start mt-1 gap-2">
<small id="pw_strength_label" class="fw-semibold" style="white-space:nowrap;">—</small>
<small id="pw_feedback" class="text-muted text-end"></small>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="confirm_password" class="form-label">
Confirm Password <span class="text-danger">*</span>
</label>
<div class="input-group">
<input type="password" class="form-control" id="confirm_password"
placeholder="Repeat new password" oninput="check_confirm_match()">
<button class="btn btn-outline-secondary toggle-pw" type="button" data-target="confirm_password">
<i class="ti ti-eye"></i>
</button>
</div>
<small id="pw_match_label" class="mt-1 d-block"></small>
</div>
</div>
<div class="d-flex gap-2 mt-2">
<button class="btn btn-warning" id="btn_change_password"
onclick="change_password();" disabled>
<i class="ti ti-key me-1"></i>Update Password
</button>
</div>
</div>
</div>
</div><!-- /RIGHT -->
</div><!-- /row -->
</div>
<?php require '../include_ending.php'; ?>
</main>
<!-- ═══════════════════════════════════════════════
FORGOT PASSWORD MODAL
Step 1 → confirm send OTP
Step 2 → enter OTP + new password
════════════════════════════════════════════════════ -->
<div class="modal fade" id="forgotModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title">
<i class="ti ti-lock-open me-2 text-warning"></i>Reset Password
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"
onclick="reset_forgot_modal();"></button>
</div>
<!-- Step 1 -->
<div id="forgot_step1">
<div class="modal-body pt-3">
<p class="text-muted small mb-3">
An OTP will be sent to your registered email address.
</p>
<div class="alert alert-secondary py-2 px-3 small mb-0">
<i class="ti ti-mail me-1"></i>
<span id="modal_masked_email">—</span>
</div>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-secondary btn-sm"
data-bs-dismiss="modal" onclick="reset_forgot_modal();">Cancel</button>
<button type="button" class="btn btn-warning btn-sm" id="btn_send_otp"
onclick="send_reset_otp();">
<i class="ti ti-send me-1"></i>Send OTP
</button>
</div>
</div>
<!-- Step 2 -->
<div id="forgot_step2" class="d-none">
<div class="modal-body pt-3">
<p class="text-muted small mb-3">
OTP sent to <strong id="modal_masked_email2"></strong> —
Reference: <strong id="modal_reference"></strong>
</p>
<div class="mb-3">
<label class="form-label">OTP <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="reset_otp"
placeholder="6-digit OTP" maxlength="6"
oninput="this.value=this.value.replace(/\D/g,''); update_reset_button();">
<div class="form-text">
Valid for 5 minutes. &nbsp;
<a href="javascript:;" class="link-primary" onclick="send_reset_otp();">Resend OTP</a>
</div>
</div>
<div class="mb-3">
<label class="form-label">New Password <span class="text-danger">*</span></label>
<div class="input-group">
<input type="password" class="form-control" id="reset_new_password"
placeholder="Enter new password"
oninput="on_reset_password_input(this.value)">
<button class="btn btn-outline-secondary toggle-pw" type="button"
data-target="reset_new_password">
<i class="ti ti-eye"></i>
</button>
</div>
<div class="mt-2">
<div class="progress" style="height:5px;">
<div id="reset_strength_bar" class="progress-bar"
style="width:0%;transition:width .25s,background-color .25s;border-radius:4px;"></div>
</div>
<div class="d-flex justify-content-between mt-1 gap-2">
<small id="reset_strength_label" class="fw-semibold" style="white-space:nowrap;">—</small>
<small id="reset_feedback" class="text-muted text-end"></small>
</div>
</div>
</div>
<div class="mb-0">
<label class="form-label">Confirm Password <span class="text-danger">*</span></label>
<div class="input-group">
<input type="password" class="form-control" id="reset_confirm_password"
placeholder="Repeat new password"
oninput="check_reset_confirm_match()">
<button class="btn btn-outline-secondary toggle-pw" type="button"
data-target="reset_confirm_password">
<i class="ti ti-eye"></i>
</button>
</div>
<small id="reset_match_label" class="mt-1 d-block"></small>
</div>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-secondary btn-sm"
data-bs-dismiss="modal" onclick="reset_forgot_modal();">Cancel</button>
<button type="button" class="btn btn-primary btn-sm"
id="btn_reset_confirm" onclick="confirm_reset_password();" disabled>
<i class="ti ti-lock-check me-1"></i>Reset Password
</button>
</div>
</div>
</div>
</div>
</div>
<script>
// ═══════════════════════════════════════════════════════
// Shared strength config
// ═══════════════════════════════════════════════════════
const STRENGTH_LEVELS = [
{ label: 'Very weak', color: '#dc3545', pct: 20 },
{ label: 'Weak', color: '#fd7e14', pct: 40 },
{ label: 'Fair', color: '#ffc107', pct: 60 },
{ label: 'Strong', color: '#198754', pct: 80 },
{ label: 'Very strong', color: '#0d6efd', pct: 100 },
];
// ═══════════════════════════════════════════════════════
// State
// ═══════════════════════════════════════════════════════
var pw_score = -1;
var reset_pw_score = -1;
var user_email = ''; // stored on retrieve, used for modal masking
function mask_email(email) {
const at = email.indexOf('@');
if (at < 0) return email;
return email.substring(0, 2)
+ '*'.repeat(Math.max(1, at - 2))
+ email.substring(at);
}
// ═══════════════════════════════════════════════════════
// On load
// ═══════════════════════════════════════════════════════
$(async function () {
try {
await load_countries();
await retrieve_profile();
} catch(e) { console.error(e); }
});
// ═══════════════════════════════════════════════════════
// COUNTRIES — load from ISO 3166 CDN
// ═══════════════════════════════════════════════════════
function load_countries() {
return $.getJSON(
'https://cdn.jsdelivr.net/npm/world_countries_lists@latest/data/countries/en/countries.json'
).then(function(data) {
const $sel = $('#country');
// Thailand first for convenience, then alphabetical
const sorted = data.slice().sort((a, b) => a.name.localeCompare(b.name));
const th = sorted.findIndex(c => c.alpha2 === 'th');
if (th > -1) {
const [thailand] = sorted.splice(th, 1);
sorted.unshift(thailand);
}
sorted.forEach(function(c) {
$sel.append(new Option(c.name, c.name));
});
});
}
// ═══════════════════════════════════════════════════════
// PROFILE — retrieve & save
// ═══════════════════════════════════════════════════════
function retrieve_profile() {
return ajax_request({
url: '<?php echo $server_url?>setting/api/engine/retrieve_profile.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
const u = res.output;
$('#name').val(u.name);
$('#surname').val(u.surname);
$('#email').val(u.email);
$('#phone').val(u.phone);
$('#username_display').val(u.username);
$('#country').val(u.country); // select already populated by load_countries()
$('#address').val(u.address);
if (u.profile_picture) {
$('#avatar_preview').attr('src',
'<?php echo $server_url?>uploads/profile/' + u.profile_picture);
}
user_email = u.email;
refresh_display_panel(u.name, u.surname, u.username, u.email);
}
});
}
function save_profile() {
const fd = new FormData();
const file = $('#profile_picture')[0].files[0];
if (file) fd.append('profile_picture', file);
return ajax_request({
url: '<?php echo $server_url?>setting/api/engine/manage_profile.php',
autoPrepare: true,
checkRequired: 1,
action: 'update',
formData: fd,
onSuccess: function(res) {
if (res.profile_picture) {
$('#avatar_preview').attr('src',
'<?php echo $server_url?>uploads/profile/' + res.profile_picture);
}
refresh_display_panel(
$('#name').val(), $('#surname').val(),
$('#username_display').val(), $('#email').val()
);
bootbox.alert(res.message);
}
});
}
function refresh_display_panel(name, surname, username, email) {
$('#display_name').text((name + ' ' + surname).trim() || '—');
$('#display_username').text('@' + (username || '—'));
$('#display_email').text(email || '—');
}
// Live update display panel while typing
$('#name, #surname').on('input', function() {
$('#display_name').text(($('#name').val() + ' ' + $('#surname').val()).trim() || '—');
});
$('#email').on('input', function() {
$('#display_email').text($(this).val() || '—');
});
// Avatar preview
$('#profile_picture').on('change', function() {
const file = this.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => $('#avatar_preview').attr('src', e.target.result);
reader.readAsDataURL(file);
});
// ═══════════════════════════════════════════════════════
// CHANGE PASSWORD
// ═══════════════════════════════════════════════════════
function on_new_password_input(pw) {
if (!pw) { reset_strength_ui('pw'); update_pw_button(); return; }
check_strength(pw, 'pw');
}
function check_confirm_match() {
const np = $('#new_password').val();
const cp = $('#confirm_password').val();
if (!cp) { $('#pw_match_label').text(''); return; }
if (np === cp) {
$('#pw_match_label').html('<span style="color:#198754;">&#10003; Passwords match</span>');
} else {
$('#pw_match_label').html('<span style="color:#dc3545;">&#10007; Passwords do not match</span>');
}
update_pw_button();
}
function update_pw_button() {
const cur = $('#current_password').val();
const np = $('#new_password').val();
const cp = $('#confirm_password').val();
const ready = cur && np && cp && (np === cp) && pw_score >= 3;
$('#btn_change_password').prop('disabled', !ready);
}
function change_password() {
if (pw_score < 3) { bootbox.alert('Please choose a stronger password.'); return; }
return ajax_request({
url: '<?php echo $server_url?>setting/api/engine/change_password.php',
autoPrepare: false,
data: { json: JSON.stringify({
otp: document.getElementById('session-context').dataset.otp,
company_id: document.getElementById('session-context').dataset.companyId,
action: 'update',
current_password: $('#current_password').val(),
new_password: $('#new_password').val(),
confirm_password: $('#confirm_password').val(),
})},
onSuccess: function() {
bootbox.alert(
'Password changed successfully. You will be logged out now.',
function() { window.location.href = '<?php echo $server_url?>index.php'; }
);
}
});
}
// ═══════════════════════════════════════════════════════
// FORGOT PASSWORD MODAL
// ═══════════════════════════════════════════════════════
function open_forgot_modal() {
reset_forgot_modal();
$('#modal_masked_email').text(user_email ? mask_email(user_email) : '—');
$('#forgotModal').modal('show');
}
function reset_forgot_modal() {
// Reset steps
$('#forgot_step1').removeClass('d-none');
$('#forgot_step2').addClass('d-none');
// Clear fields
$('#reset_otp').val('');
$('#reset_new_password').val('');
$('#reset_confirm_password').val('');
$('#reset_match_label').text('');
reset_strength_ui('reset');
update_reset_button();
}
function send_reset_otp() {
$('#btn_send_otp').prop('disabled', true);
return ajax_request({
url: '<?php echo $server_url?>setting/api/engine/request_reset_otp.php',
autoPrepare: false,
noLoading: false,
data: { json: JSON.stringify({
otp: document.getElementById('session-context').dataset.otp,
company_id: document.getElementById('session-context').dataset.companyId,
action: 'read',
})},
onSuccess: function(res) {
// Move to step 2
$('#forgot_step1').addClass('d-none');
$('#forgot_step2').removeClass('d-none');
$('#modal_masked_email2').text(res.masked_email);
$('#modal_reference').text(res.reference);
}
}).catch(function() {
$('#btn_send_otp').prop('disabled', false);
});
}
function on_reset_password_input(pw) {
if (!pw) { reset_strength_ui('reset'); update_reset_button(); return; }
check_strength(pw, 'reset');
}
function check_reset_confirm_match() {
const np = $('#reset_new_password').val();
const cp = $('#reset_confirm_password').val();
if (!cp) { $('#reset_match_label').text(''); return; }
if (np === cp) {
$('#reset_match_label').html('<span style="color:#198754;">&#10003; Passwords match</span>');
} else {
$('#reset_match_label').html('<span style="color:#dc3545;">&#10007; Passwords do not match</span>');
}
update_reset_button();
}
function update_reset_button() {
const otp = $('#reset_otp').val();
const np = $('#reset_new_password').val();
const cp = $('#reset_confirm_password').val();
const ready = otp.length === 6 && np && cp && (np === cp) && reset_pw_score >= 3;
$('#btn_reset_confirm').prop('disabled', !ready);
}
function confirm_reset_password() {
if (reset_pw_score < 3) { bootbox.alert('Please choose a stronger password.'); return; }
return ajax_request({
url: '<?php echo $server_url?>setting/api/engine/reset_password_otp.php',
autoPrepare: false,
data: { json: JSON.stringify({
session_otp: document.getElementById('session-context').dataset.otp,
company_id: document.getElementById('session-context').dataset.companyId,
action: 'update',
otp: $('#reset_otp').val(),
new_password: $('#reset_new_password').val(),
confirm_password: $('#reset_confirm_password').val(),
})},
onSuccess: function() {
$('#forgotModal').modal('hide');
bootbox.alert(
'Password reset successfully. You will be logged out now.',
function() { window.location.href = '<?php echo $server_url?>index.php'; }
);
}
});
}
// ═══════════════════════════════════════════════════════
// SHARED: strength check (used by both change & reset)
// prefix = 'pw' | 'reset'
// ═══════════════════════════════════════════════════════
function check_strength(pw, prefix) {
const user_inputs = [
$('#name').val(), $('#surname').val(),
$('#username').val(), user_email
].filter(Boolean);
const result = zxcvbn(pw, user_inputs);
const score = result.score;
if (prefix === 'pw') pw_score = score;
if (prefix === 'reset') reset_pw_score = score;
const bar = prefix === 'pw' ? '#pw_strength_bar' : '#reset_strength_bar';
const label = prefix === 'pw' ? '#pw_strength_label' : '#reset_strength_label';
const feedback = prefix === 'pw' ? '#pw_feedback' : '#reset_feedback';
const lvl = STRENGTH_LEVELS[score];
$(bar).css({ width: lvl.pct + '%', backgroundColor: lvl.color });
$(label).text(lvl.label).css('color', lvl.color);
$(feedback).text(result.feedback.warning || result.feedback.suggestions[0] || '');
if (prefix === 'pw') { check_confirm_match(); update_pw_button(); }
if (prefix === 'reset') { check_reset_confirm_match(); update_reset_button(); }
}
function reset_strength_ui(prefix) {
if (prefix === 'pw') pw_score = -1;
if (prefix === 'reset') reset_pw_score = -1;
const bar = prefix === 'pw' ? '#pw_strength_bar' : '#reset_strength_bar';
const label = prefix === 'pw' ? '#pw_strength_label' : '#reset_strength_label';
const feedback = prefix === 'pw' ? '#pw_feedback' : '#reset_feedback';
$(bar).css({ width: '0%', backgroundColor: '' });
$(label).text('—').css('color', '');
$(feedback).text('');
}
// ═══════════════════════════════════════════════════════
// Shared: toggle show/hide password
// ═══════════════════════════════════════════════════════
$(document).on('click', '.toggle-pw', function() {
const $input = $('#' + $(this).data('target'));
const isText = $input.attr('type') === 'text';
$input.attr('type', isText ? 'password' : 'text');
$(this).find('i').toggleClass('ti-eye ti-eye-off');
});
</script>
</body>
</html>