app settings

This commit is contained in:
Thanakorn S
2026-04-27 10:50:31 +07:00
parent 76b9b2a2cb
commit d8c55d278a
3128 changed files with 630683 additions and 138 deletions
@@ -0,0 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/PasswordManager.php';
$pm = new PasswordManager($pdo1, $include_url);
$pm->handleChange($user_id, $data);
@@ -0,0 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/PasswordManager.php';
$pm = new PasswordManager($pdo1, $include_url);
$pm->handleCheck($data);
+164
View File
@@ -0,0 +1,164 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/FileUploader.php';
// ─── Allowed upload MIME types ────────────────────────────────────────────
const ALLOWED_MIME = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
const MAX_SIZE = 2 * 1024 * 1024; // 2 MB
// ─── Helper: handle one image slot ────────────────────────────────────────
function handle_image_slot(
string $slot, // 'company_logo' | 'company_seal'
string $action, // 'keep' | 'replace' | 'remove'
string $current, // current filename from DB
string $upload_dir, // absolute path to uploads/company/
string $prefix // filename prefix 'logo_' | 'seal_'
): ?string {
// 'keep' → return current unchanged
if ($action === 'keep') return $current;
// 'remove' → delete file, return ''
if ($action === 'remove') {
if ($current && file_exists($upload_dir . $current)) {
unlink($upload_dir . $current);
}
return '';
}
// 'replace' → validate + save new file
if ($action === 'replace' && !empty($_FILES[$slot]['tmp_name'])) {
$file = $_FILES[$slot];
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException("Upload error on {$slot}: code {$file['error']}.");
}
if ($file['size'] > MAX_SIZE) {
throw new RuntimeException('File too large. Maximum size is 2 MB.');
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, ALLOWED_MIME, true)) {
throw new RuntimeException('Invalid file type. Only JPEG, PNG, GIF, WEBP allowed.');
}
// Delete old file first
if ($current && file_exists($upload_dir . $current)) {
unlink($upload_dir . $current);
}
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = $prefix . uniqid() . '.' . strtolower($ext);
if (!move_uploaded_file($file['tmp_name'], $upload_dir . $filename)) {
throw new RuntimeException("Failed to save {$slot}.");
}
return $filename;
}
return $current; // fallback
}
try {
// ── Fetch current image filenames from DB ─────────────────────────────
$sth = $pdo1->prepare(
'SELECT company_logo, company_seal FROM company_list WHERE company_id = :id LIMIT 1'
);
$sth->execute([':id' => $company_id]);
db_check($sth, $answer);
$current = $sth->fetch(PDO::FETCH_ASSOC);
if (!$current) {
$answer['message'] = 'Company not found.';
http_response_code(404);
exit(json_encode($answer));
}
// ── Upload directory ──────────────────────────────────────────────────
$upload_dir = $include_url . 'uploads/company/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}
// ── Process images ────────────────────────────────────────────────────
$logo_action = $data['logo_action'] ?? 'keep';
$seal_action = $data['seal_action'] ?? 'keep';
$new_logo = handle_image_slot('company_logo', $logo_action, $current['company_logo'] ?? '', $upload_dir, 'logo_');
$new_seal = handle_image_slot('company_seal', $seal_action, $current['company_seal'] ?? '', $upload_dir, 'seal_');
// ── Text field sanitisation ───────────────────────────────────────────
$channel = strtolower(preg_replace('/[^a-z0-9\-_]/', '', $data['channel_name'] ?? ''));
// ── UPDATE company_list ───────────────────────────────────────────────
$sth = $pdo1->prepare("
UPDATE company_list SET
channel_name = :channel_name,
company_name = :company_name,
company_name2 = :company_name2,
company_logo = :company_logo,
company_seal = :company_seal,
branch = :branch,
branch_no = :branch_no,
fiscal_year = :fiscal_year,
fx = :fx,
address = :address,
address2 = :address2,
tax_id = :tax_id,
prompt_pay = :prompt_pay,
entrepreneur = :entrepreneur,
email = :email,
phone = :phone,
fax = :fax,
website = :website,
facebook_page = :facebook_page
WHERE company_id = :company_id
");
$sth->execute([
':channel_name' => $channel,
':company_name' => trim($data['company_name'] ?? ''),
':company_name2' => trim($data['company_name2'] ?? ''),
':company_logo' => $new_logo,
':company_seal' => $new_seal,
':branch' => trim($data['branch'] ?? 'สำนักงานใหญ่'),
':branch_no' => trim($data['branch_no'] ?? ''),
':fiscal_year' => trim($data['fiscal_year'] ?? ''),
':fx' => trim($data['fx'] ?? 'thb'),
':address' => trim($data['address'] ?? ''),
':address2' => trim($data['address2'] ?? ''),
':tax_id' => trim($data['tax_id'] ?? ''),
':prompt_pay' => trim($data['prompt_pay'] ?? ''),
':entrepreneur' => trim($data['entrepreneur'] ?? ''),
':email' => trim($data['email'] ?? ''),
':phone' => trim($data['phone'] ?? ''),
':fax' => trim($data['fax'] ?? ''),
':website' => trim($data['website'] ?? ''),
':facebook_page' => trim($data['facebook_page'] ?? ''),
':company_id' => $company_id,
]);
db_check($sth, $answer);
$answer['success'] = 1;
$answer['message'] = 'Company profile saved.';
// Return new filenames only if they changed so client can refresh previews
if ($logo_action !== 'keep') $answer['company_logo'] = $new_logo;
if ($seal_action !== 'keep') $answer['company_seal'] = $new_seal;
} catch (RuntimeException $e) {
$answer['message'] = $e->getMessage();
http_response_code(422);
} catch (Exception $e) {
$answer['message'] = 'Failed to save company profile.';
http_response_code(500);
}
exit(json_encode($answer));
?>
+75
View File
@@ -0,0 +1,75 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/FileUploader.php';
try {
// ── Fetch current picture filename from DB ────────────────
$sth = $pdo1->prepare(
'SELECT profile_picture FROM user WHERE user_id = :user_id LIMIT 1'
);
$sth->execute([':user_id' => $user_id]);
$current_picture = $sth->fetchColumn() ?: '';
// ── Handle new picture upload ─────────────────────────────
$db_picture = $current_picture;
if (!empty($_FILES['profile_picture']['name'])) {
// Remove old file if exists
if (!empty($current_picture)) {
$old_path = $include_url . 'uploads/profile/' . $current_picture;
if (file_exists($old_path)) unlink($old_path);
}
$uploader = new FileUploader($include_url . 'uploads/profile/');
$errors = $uploader->upload('profile_picture');
if (!empty($errors)) {
$answer['message'] = $errors[0];
http_response_code(400);
exit(json_encode($answer));
}
$db_picture = $uploader->getUploadedFiles()[0] ?? '';
}
// ── Update user record ────────────────────────────────────
$sth = $pdo1->prepare(
"UPDATE user SET
name = :name,
surname = :surname,
email = :email,
phone = :phone,
country = :country,
address = :address,
profile_picture = :profile_picture
WHERE user_id = :user_id"
);
$sth->execute([
':name' => trim($data['name'] ?? ''),
':surname' => trim($data['surname'] ?? ''),
':email' => trim($data['email'] ?? ''),
':phone' => trim($data['phone'] ?? ''),
':country' => trim($data['country'] ?? ''),
':address' => trim($data['address'] ?? ''),
':profile_picture' => $db_picture,
':user_id' => $user_id,
]);
db_check($sth, $answer);
// ── Refresh session ───────────────────────────────────────
$_SESSION['login_name'] = trim($data['name'] ?? '');
$_SESSION['login_surname'] = trim($data['surname'] ?? '');
$answer['success'] = 1;
$answer['message'] = 'Profile updated.';
$answer['profile_picture'] = $db_picture;
} catch (Exception $e) {
$answer['message'] = 'Failed to update profile.';
http_response_code(500);
}
exit(json_encode($answer));
@@ -0,0 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/PasswordResetManager.php';
$prm = new PasswordResetManager($pdo1, $pdo2, $include_url, $SMTP, $pinkey);
$prm->handleRequestOtp($user_id, $company_id);
@@ -0,0 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/PasswordResetManager.php';
$prm = new PasswordResetManager($pdo1, $pdo2, $include_url, $SMTP, $pinkey);
$prm->handleConfirmReset($user_id, $data);
@@ -0,0 +1,42 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
try {
$sth = $pdo1->prepare("
SELECT
company_id, channel_name,
company_name, company_name2,
company_logo, company_seal,
branch, branch_no,
fiscal_year, fx,
address, address2,
tax_id, prompt_pay, entrepreneur,
email, phone, fax,
website, facebook_page
FROM company_list
WHERE company_id = :company_id
LIMIT 1
");
$sth->execute([':company_id' => $company_id]);
db_check($sth, $answer);
$company = $sth->fetch(PDO::FETCH_ASSOC);
if (!$company) {
$answer['message'] = 'Company not found.';
http_response_code(404);
exit(json_encode($answer));
}
$answer['success'] = 1;
$answer['output'] = $company;
} catch (Exception $e) {
$answer['message'] = 'Failed to load company profile.';
http_response_code(500);
}
exit(json_encode($answer));
?>
@@ -0,0 +1,33 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
try {
$sth = $pdo1->prepare(
"SELECT user_id, username, name, surname,
email, phone, country, address, profile_picture
FROM user
WHERE user_id = :user_id
LIMIT 1"
);
$sth->execute([':user_id' => $user_id]);
db_check($sth, $answer);
$user = $sth->fetch(PDO::FETCH_ASSOC);
if (!$user) {
$answer['message'] = 'User not found.';
http_response_code(404);
exit(json_encode($answer));
}
$answer['success'] = 1;
$answer['output'] = $user;
} catch (Exception $e) {
$answer['message'] = 'Failed to load profile.';
http_response_code(500);
}
exit(json_encode($answer));
+418
View File
@@ -0,0 +1,418 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?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">Company Profile</h1>
<p class="mb-0">Manage your company information, branding and tax details</p>
</div>
</div>
</div>
</div>
<div class="row g-5">
<!-- LEFT: Logo / Seal -->
<div class="col-lg-3 col-12">
<!-- Logo card -->
<div class="card mb-4">
<div class="card-body p-6 text-center">
<div class="mb-3">
<img id="logo_preview"
src="<?php echo $server_url?>assets/images/logo.svg"
alt="Company Logo"
class="rounded border p-2"
style="width:120px;height:120px;object-fit:contain;">
</div>
<p class="text-muted small mb-3">Company Logo</p>
<label class="btn btn-outline-secondary btn-sm w-100 mb-2">
<i class="ti ti-upload me-1"></i>Upload Logo
<input type="file" id="company_logo" accept="image/*" class="d-none">
</label>
<button type="button" id="remove_logo_btn"
class="btn btn-ghost-danger btn-sm w-100 d-none"
onclick="remove_image('logo')">
<i class="ti ti-trash me-1"></i>Remove Logo
</button>
</div>
</div>
<!-- Seal card -->
<div class="card">
<div class="card-body p-6 text-center">
<div class="mb-3">
<img id="seal_preview"
src="<?php echo $server_url?>assets/images/logo.svg"
alt="Company Seal"
class="rounded border p-2"
style="width:120px;height:120px;object-fit:contain;">
</div>
<p class="text-muted small mb-3">Company Seal</p>
<label class="btn btn-outline-secondary btn-sm w-100 mb-2">
<i class="ti ti-upload me-1"></i>Upload Seal
<input type="file" id="company_seal" accept="image/*" class="d-none">
</label>
<button type="button" id="remove_seal_btn"
class="btn btn-ghost-danger btn-sm w-100 d-none"
onclick="remove_image('seal')">
<i class="ti ti-trash me-1"></i>Remove Seal
</button>
</div>
</div>
</div><!-- /LEFT -->
<!-- RIGHT: Tabbed form -->
<div class="col-lg-9 col-12">
<div class="card">
<div class="card-body p-0">
<ul class="nav nav-tabs px-6 pt-4" id="companyTabs">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#tab_basic">
<i class="ti ti-building me-1"></i>Basic Info
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab_address">
<i class="ti ti-map-pin me-1"></i>Address
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab_tax">
<i class="ti ti-receipt-tax me-1"></i>Tax &amp; Finance
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab_contact">
<i class="ti ti-phone me-1"></i>Contact
</a>
</li>
</ul>
<div class="tab-content p-6">
<!-- Tab 1: Basic Info -->
<div class="tab-pane active show" id="tab_basic">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Company Name (TH)</label>
<input type="text" class="form-control" id="company_name" placeholder="ชื่อบริษัท">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Company Name (EN)</label>
<input type="text" class="form-control" id="company_name2" placeholder="Company Name (English)">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Nickname / Channel Name</label>
<input type="text" class="form-control" id="channel_name" placeholder="e.g. my-shop">
<div class="form-text">Unique identifier. Lowercase, no spaces.</div>
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Branch</label>
<input type="text" class="form-control" id="branch" placeholder="สำนักงานใหญ่">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Branch No.</label>
<input type="text" class="form-control" id="branch_no" placeholder="00000">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Fiscal Year End</label>
<select class="form-select" id="fiscal_year">
<option value="">— Select month —</option>
<?php for ($m = 1; $m <= 12; $m++): ?>
<option value="<?php echo str_pad($m, 2, '0', STR_PAD_LEFT); ?>">
<?php echo date('F', mktime(0,0,0,$m,1)); ?>
</option>
<?php endfor; ?>
</select>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Currency</label>
<select class="form-select" id="fx">
<option value="thb">THB — Thai Baht</option>
<option value="usd">USD — US Dollar</option>
<option value="eur">EUR — Euro</option>
<option value="sgd">SGD — Singapore Dollar</option>
<option value="myr">MYR — Malaysian Ringgit</option>
</select>
</div>
</div>
</div><!-- /tab_basic -->
<!-- Tab 2: Address -->
<div class="tab-pane" id="tab_address">
<div class="row">
<div class="col-12 mb-3">
<label class="form-label">Address</label>
<textarea class="form-control" id="address" rows="3"
placeholder="Full address…"></textarea>
</div>
<div class="col-12 mb-3">
<label class="form-label">Address 2 <span class="text-muted">(optional)</span></label>
<textarea class="form-control" id="address2" rows="2"
placeholder="Branch / shipping address…"></textarea>
</div>
</div>
</div><!-- /tab_address -->
<!-- Tab 3: Tax & Finance -->
<div class="tab-pane" id="tab_tax">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Tax ID (เลขประจำตัวผู้เสียภาษี)</label>
<input type="text" class="form-control" id="tax_id"
placeholder="0-0000-00000-00-0" maxlength="20">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">PromptPay Number</label>
<input type="text" class="form-control" id="prompt_pay"
placeholder="13-digit PromptPay" maxlength="13">
</div>
<div class="col-12 mb-3">
<label class="form-label">Entrepreneur / Owner Name</label>
<input type="text" class="form-control" id="entrepreneur"
placeholder="ชื่อผู้ประกอบการ">
</div>
</div>
</div><!-- /tab_tax -->
<!-- Tab 4: Contact -->
<div class="tab-pane" id="tab_contact">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Email</label>
<div class="input-group">
<span class="input-group-text"><i class="ti ti-mail"></i></span>
<input type="email" class="form-control" id="email"
placeholder="company@example.com">
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Phone</label>
<div class="input-group">
<span class="input-group-text"><i class="ti ti-phone"></i></span>
<input type="text" class="form-control" id="phone"
placeholder="02-xxx-xxxx">
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Fax</label>
<div class="input-group">
<span class="input-group-text"><i class="ti ti-printer"></i></span>
<input type="text" class="form-control" id="fax"
placeholder="02-xxx-xxxx">
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Website</label>
<div class="input-group">
<span class="input-group-text"><i class="ti ti-world"></i></span>
<input type="url" class="form-control" id="website"
placeholder="https://example.com">
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Facebook Page</label>
<div class="input-group">
<span class="input-group-text"><i class="ti ti-brand-facebook"></i></span>
<input type="url" class="form-control" id="facebook_page"
placeholder="https://facebook.com/yourpage">
</div>
</div>
</div>
</div><!-- /tab_contact -->
</div><!-- /tab-content -->
</div><!-- /card-body -->
<div class="card-footer d-flex justify-content-end gap-2">
<button class="btn btn-primary" onclick="save_company();">
<i class="ti ti-device-floppy me-1"></i>Save Changes
</button>
</div>
</div><!-- /card -->
</div><!-- /RIGHT -->
</div><!-- /row g-5 -->
</div>
<?php require '../include_ending.php'; ?>
</main>
<script>
// ═══════════════════════════════════════════════════════
// State
// ═══════════════════════════════════════════════════════
const api = '<?php echo $server_url?>setting/api/engine/';
const img_base = '<?php echo $server_url?>uploads/company/';
let _logo_action = 'keep'; // 'keep' | 'replace' | 'remove'
let _seal_action = 'keep';
// ═══════════════════════════════════════════════════════
// On load
// ═══════════════════════════════════════════════════════
$(function () {
load_company();
bind_image_input('#company_logo', '#logo_preview', '#remove_logo_btn', v => _logo_action = v);
bind_image_input('#company_seal', '#seal_preview', '#remove_seal_btn', v => _seal_action = v);
});
// ═══════════════════════════════════════════════════════
// Load
// ═══════════════════════════════════════════════════════
function load_company() {
return ajax_request({
url: api + 'retrieve_company.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function (r) {
const c = r.output;
// Basic info
$('#company_name').val(c.company_name || '');
$('#company_name2').val(c.company_name2 || '');
$('#channel_name').val(c.channel_name || '');
$('#branch').val(c.branch || '');
$('#branch_no').val(c.branch_no || '');
$('#fiscal_year').val(c.fiscal_year || '');
$('#fx').val(c.fx || 'thb');
// Address
$('#address').val(c.address || '');
$('#address2').val(c.address2 || '');
// Tax & Finance
$('#tax_id').val(c.tax_id || '');
$('#prompt_pay').val(c.prompt_pay || '');
$('#entrepreneur').val(c.entrepreneur || '');
// Contact
$('#email').val(c.email || '');
$('#phone').val(c.phone || '');
$('#fax').val(c.fax || '');
$('#website').val(c.website || '');
$('#facebook_page').val(c.facebook_page || '');
// Images
if (c.company_logo) {
$('#logo_preview').attr('src', img_base + c.company_logo);
$('#remove_logo_btn').removeClass('d-none');
}
if (c.company_seal) {
$('#seal_preview').attr('src', img_base + c.company_seal);
$('#remove_seal_btn').removeClass('d-none');
}
},
});
}
// ═══════════════════════════════════════════════════════
// Save
// ═══════════════════════════════════════════════════════
function save_company() {
const fd = new FormData();
// Only files + action flags — all text fields (.form-control / .form-select)
// are collected automatically by autoPrepare → prepare_form_data()
fd.append('logo_action', _logo_action);
fd.append('seal_action', _seal_action);
const logo_file = $('#company_logo')[0].files[0];
const seal_file = $('#company_seal')[0].files[0];
if (logo_file) fd.append('company_logo', logo_file);
if (seal_file) fd.append('company_seal', seal_file);
return ajax_request({
url: api + 'manage_company.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
formData: fd,
onSuccess: function (r) {
if (r.company_logo !== undefined) {
refresh_image_slot(
'logo', r.company_logo,
'#logo_preview', '#remove_logo_btn', '#company_logo'
);
_logo_action = 'keep';
}
if (r.company_seal !== undefined) {
refresh_image_slot(
'seal', r.company_seal,
'#seal_preview', '#remove_seal_btn', '#company_seal'
);
_seal_action = 'keep';
}
bootbox.alert('Company profile saved.');
},
});
}
// ═══════════════════════════════════════════════════════
// Image helpers
// ═══════════════════════════════════════════════════════
function bind_image_input(input_sel, preview_sel, remove_btn_sel, set_action) {
$(input_sel).on('change', function () {
const file = this.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
$(preview_sel).attr('src', e.target.result);
$(remove_btn_sel).removeClass('d-none');
set_action('replace');
};
reader.readAsDataURL(file);
});
}
function remove_image(slot) {
if (slot === 'logo') {
$('#logo_preview').attr('src', '<?php echo $server_url?>assets/images/logo.svg');
$('#company_logo').val('');
$('#remove_logo_btn').addClass('d-none');
_logo_action = 'remove';
} else {
$('#seal_preview').attr('src', '<?php echo $server_url?>assets/images/logo.svg');
$('#company_seal').val('');
$('#remove_seal_btn').addClass('d-none');
_seal_action = 'remove';
}
}
function refresh_image_slot(slot, filename, preview_sel, remove_btn_sel, input_sel) {
const fallback = '<?php echo $server_url?>assets/images/logo.svg';
const src = filename ? img_base + filename + '?t=' + Date.now() : fallback;
$(preview_sel).attr('src', src);
filename
? $(remove_btn_sel).removeClass('d-none')
: $(remove_btn_sel).addClass('d-none');
$(input_sel).val('');
}
</script>
</body>
</html>
+584 -110
View File
@@ -1,191 +1,665 @@
<?php
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php';?>
<?php require '../include_setting_sidebar.php';?>
<?php require '../include_topbar.php'; ?>
<?php require '../include_setting_sidebar.php'; ?>
<!-- MAIN CONTENT -->
<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-5 gap-3">
<div class="">
<h1 class="fs-3 mb-1">Manage Profile</h1>
<p class="mb-0">Update profile detail</p>
<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" id="userForm">
<div class="col-12">
<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>
<!-- Row 1: username / email -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" placeholder="Enter username" required>
<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="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" placeholder="Enter email" required>
<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>
<!-- Row 2: password / phone -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" placeholder="Enter password" required>
<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="Enter phone number">
<input type="text" class="form-control" id="phone" placeholder="e.g. 0812345678">
</div>
</div>
<!-- Row 3: name / surname -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" placeholder="Enter first name" required>
</div>
<div class="col-md-6 mb-3">
<label for="surname" class="form-label">Surname</label>
<input type="text" class="form-control" id="surname" placeholder="Enter last name" required>
</div>
</div>
<!-- Row 4: country / language -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="country" class="form-label">Country</label>
<input type="text" class="form-control" id="country" placeholder="Enter country">
</div>
<div class="col-md-6 mb-3">
<label for="language" class="form-label">Language</label>
<select class="form-select" id="language">
<option value="th" selected>Thai (th)</option>
<option value="en">English (en)</option>
<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>
<!-- default_company / profile_picture -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="default_company" class="form-label">Default Company ID</label>
<input type="number" class="form-control" id="default_company" placeholder="0" value="0">
<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>
<!-- address -->
<div class="mb-3">
<label for="address" class="form-label">Address</label>
<input type="text" class="form-control" id="address" placeholder="Enter address">
</div>
<!-- facebook / line_code -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="facebook" class="form-label">Facebook</label>
<input type="text" class="form-control" id="facebook" placeholder="Enter Facebook profile">
</div>
<div class="col-md-6 mb-3">
<label for="line_code" class="form-label">Line Code</label>
<input type="text" class="form-control" id="line_code" placeholder="Enter Line ID">
</div>
</div>
<!-- signature -->
<div class="mb-3">
<label for="signature" class="form-label">Signature</label>
<input type="text" class="form-control" id="signature" placeholder="Enter signature">
</div>
<!-- Buttons -->
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" onclick="manage_user();">Create</button>
<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>
</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>
<?php require "../include_ending.php";?>
<!-- ═══════════════════════════════════════════════
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>
function manage_category() {
// ═══════════════════════════════════════════════════════
// 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 },
];
return ajax_request({
url: "<?php echo $server_url?>inventory/api/engine/manage_category.php",
autoPrepare: true,
checkRequired: 1,
debugMode: 0,
action: 'manage',
onSuccess: function(res) {
window.location.href = "<?php echo $server_url?>inventory/product.php";
}
});
// ═══════════════════════════════════════════════════════
// State
// ═══════════════════════════════════════════════════════
var pw_score = -1;
var pw_debounce = null;
var pw_check_xhr = null;
var reset_pw_score = -1;
var reset_pw_debounce = null;
var reset_pw_check_xhr = null;
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);
}
function retrieve_for_edit() {
// ═══════════════════════════════════════════════════════
// On load
// ═══════════════════════════════════════════════════════
$(async function () {
try {
await load_countries();
await retrieve_profile();
} catch(e) { console.error(e); }
});
<?php if(empty($_GET["id"])){ ?>
return false;
<?php } ?>
// ═══════════════════════════════════════════════════════
// 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?>inventory/api/engine/retrieve_category.php",
url: '<?php echo $server_url?>setting/api/engine/retrieve_profile.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
$.each(res.output, function(key, item) {
$(`#${key}`).val(item);
});
$(`button[type=submit]`).text('Update');
$(`button[type=reset]`).hide();
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('Profile updated successfully.');
}
});
}
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) {
clearTimeout(pw_debounce);
if (!pw) { reset_strength_ui('pw'); update_pw_button(); return; }
pw_debounce = setTimeout(() => check_strength(pw, 'pw'), 350);
}
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() {
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');
}
$(async function() {
try {
await retrieve_for_edit();
} catch (e) {
console.log(e);
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) {
clearTimeout(reset_pw_debounce);
if (!pw) { reset_strength_ui('reset'); update_reset_button(); return; }
reset_pw_debounce = setTimeout(() => check_strength(pw, 'reset'), 350);
}
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() {
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) {
// Abort pending request
if (prefix === 'pw' && pw_check_xhr) pw_check_xhr.abort();
if (prefix === 'reset' && reset_pw_check_xhr) reset_pw_check_xhr.abort();
const xhr = $.ajax({
url: '<?php echo $server_url?>setting/api/engine/check_password.php',
type: 'POST',
dataType: 'json',
data: { json: JSON.stringify({
otp: document.getElementById('session-context').dataset.otp,
company_id: document.getElementById('session-context').dataset.companyId,
action: 'read',
password: pw,
})},
headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') },
success: function(res) {
const score = res.score ?? -1;
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';
if (score < 0) { reset_strength_ui(prefix); return; }
const lvl = STRENGTH_LEVELS[score];
$(bar).css({ width: lvl.pct + '%', backgroundColor: lvl.color });
$(label).text(lvl.label).css('color', lvl.color);
$(feedback).text(res.feedback || '');
if (prefix === 'pw') { check_confirm_match(); update_pw_button(); }
if (prefix === 'reset') { check_reset_confirm_match(); update_reset_button(); }
}
});
if (prefix === 'pw') pw_check_xhr = xhr;
if (prefix === 'reset') reset_pw_check_xhr = xhr;
}
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>