app settings
This commit is contained in:
@@ -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);
|
||||
@@ -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));
|
||||
?>
|
||||
@@ -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));
|
||||
@@ -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 & 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
@@ -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.
|
||||
<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;">✓ Passwords match</span>');
|
||||
} else {
|
||||
$('#pw_match_label').html('<span style="color:#dc3545;">✗ 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;">✓ Passwords match</span>');
|
||||
} else {
|
||||
$('#reset_match_label').html('<span style="color:#dc3545;">✗ 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>
|
||||
Reference in New Issue
Block a user