Autosave: 20260503-065836
This commit is contained in:
parent
abc4505db8
commit
1554df04a2
5
accounting.php
Normal file
5
accounting.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'accounting');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
activate.php
Normal file
5
activate.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'activate');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
backups.php
Normal file
5
backups.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'backups');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
cash_registers.php
Normal file
5
cash_registers.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'cash_registers');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
cashflow_report.php
Normal file
5
cashflow_report.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'cashflow_report');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
categories.php
Normal file
5
categories.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'categories');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
copy_outlet_data.php
Normal file
5
copy_outlet_data.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'copy_outlet_data');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
customer_display_settings.php
Normal file
5
customer_display_settings.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'customer_display_settings');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
customer_statement.php
Normal file
5
customer_statement.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'customer_statement');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
customers.php
Normal file
5
customers.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'customers');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
dashboard.php
Normal file
5
dashboard.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'dashboard');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
devices.php
Normal file
5
devices.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'devices');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
expense_categories.php
Normal file
5
expense_categories.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'expense_categories');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
expense_report.php
Normal file
5
expense_report.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'expense_report');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
expenses.php
Normal file
5
expenses.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'expenses');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
expiry_report.php
Normal file
5
expiry_report.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'expiry_report');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
hr_attendance.php
Normal file
5
hr_attendance.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'hr_attendance');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
hr_departments.php
Normal file
5
hr_departments.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'hr_departments');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
hr_employees.php
Normal file
5
hr_employees.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'hr_employees');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
hr_payroll.php
Normal file
5
hr_payroll.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'hr_payroll');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
194
includes/page_routes.php
Normal file
194
includes/page_routes.php
Normal file
@ -0,0 +1,194 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
if (!function_exists('page_entrypoint_map')) {
|
||||
function page_entrypoint_map(): array {
|
||||
static $map = [
|
||||
'activate' => 'activate.php',
|
||||
'dashboard' => 'dashboard.php',
|
||||
'pos' => 'pos.php',
|
||||
'sales' => 'sales.php',
|
||||
'sales_returns' => 'sales_returns.php',
|
||||
'purchases' => 'purchases.php',
|
||||
'purchase_returns' => 'purchase_returns.php',
|
||||
'quotations' => 'quotations.php',
|
||||
'lpos' => 'lpos.php',
|
||||
'accounting' => 'accounting.php',
|
||||
'expense_categories' => 'expense_categories.php',
|
||||
'expenses' => 'expenses.php',
|
||||
'expense_report' => 'expense_report.php',
|
||||
'items' => 'items.php',
|
||||
'categories' => 'categories.php',
|
||||
'units' => 'units.php',
|
||||
'customers' => 'customers.php',
|
||||
'suppliers' => 'suppliers.php',
|
||||
'customer_statement' => 'customer_statement.php',
|
||||
'supplier_statement' => 'supplier_statement.php',
|
||||
'cashflow_report' => 'cashflow_report.php',
|
||||
'expiry_report' => 'expiry_report.php',
|
||||
'low_stock_report' => 'low_stock_report.php',
|
||||
'loyalty_history' => 'loyalty_history.php',
|
||||
'payment_methods' => 'payment_methods.php',
|
||||
'settings' => 'settings.php',
|
||||
'devices' => 'devices.php',
|
||||
'hr_departments' => 'hr_departments.php',
|
||||
'hr_employees' => 'hr_employees.php',
|
||||
'hr_attendance' => 'hr_attendance.php',
|
||||
'hr_payroll' => 'hr_payroll.php',
|
||||
'role_groups' => 'role_groups.php',
|
||||
'users' => 'users.php',
|
||||
'scale_devices' => 'scale_devices.php',
|
||||
'customer_display_settings' => 'customer_display_settings.php',
|
||||
'backups' => 'backups.php',
|
||||
'logs' => 'logs.php',
|
||||
'cash_registers' => 'cash_registers.php',
|
||||
'register_sessions' => 'register_sessions.php',
|
||||
'outlets' => 'outlets.php',
|
||||
'my_profile' => 'my_profile.php',
|
||||
'copy_outlet_data' => 'copy_outlet_data.php',
|
||||
];
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('page_entry_script')) {
|
||||
function page_entry_script(string $page): ?string {
|
||||
$page = strtolower(trim($page));
|
||||
if ($page === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$map = page_entrypoint_map();
|
||||
return $map[$page] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('page_from_entry_script')) {
|
||||
function page_from_entry_script(?string $scriptName = null): ?string {
|
||||
static $reverse = null;
|
||||
if ($reverse === null) {
|
||||
$reverse = array_flip(page_entrypoint_map());
|
||||
}
|
||||
|
||||
$scriptName = strtolower(trim((string)($scriptName ?? basename((string)($_SERVER['SCRIPT_NAME'] ?? '')))));
|
||||
if ($scriptName === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $reverse[$scriptName] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('page_url')) {
|
||||
function page_url(string $page = 'dashboard', array $params = []): string {
|
||||
$targetScript = page_entry_script($page);
|
||||
$query = $params;
|
||||
|
||||
if ($targetScript === null) {
|
||||
$targetScript = 'index.php';
|
||||
if ($page !== '' && strtolower($page) !== 'dashboard') {
|
||||
$query = ['page' => $page] + $query;
|
||||
}
|
||||
}
|
||||
|
||||
$queryString = http_build_query($query);
|
||||
return $targetScript . ($queryString !== '' ? '?' . $queryString : '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('page_normalize_url')) {
|
||||
function page_normalize_url(string $url): string {
|
||||
$url = trim($url);
|
||||
if ($url === '') {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$parts = parse_url($url);
|
||||
if ($parts === false) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$query = [];
|
||||
if (!empty($parts['query'])) {
|
||||
parse_str($parts['query'], $query);
|
||||
}
|
||||
|
||||
$page = isset($query['page']) ? strtolower(trim((string)$query['page'])) : '';
|
||||
if ($page === '') {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$targetScript = page_entry_script($page);
|
||||
if ($targetScript === null) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$path = (string)($parts['path'] ?? '');
|
||||
$basename = strtolower(trim($path !== '' ? basename($path) : ''));
|
||||
$knownRoutePage = $basename !== '' ? page_from_entry_script($basename) : null;
|
||||
|
||||
if ($path !== '' && str_starts_with($path, '/')) {
|
||||
$targetPath = '/' . $targetScript;
|
||||
} else {
|
||||
$targetPath = $targetScript;
|
||||
}
|
||||
|
||||
if ($basename === '' || $basename === 'index.php' || $knownRoutePage !== null) {
|
||||
$parts['path'] = $targetPath;
|
||||
unset($query['page']);
|
||||
} else {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$normalized = (string)($parts['path'] ?? $targetPath);
|
||||
if (!empty($query)) {
|
||||
$normalized .= '?' . http_build_query($query);
|
||||
}
|
||||
if (isset($parts['fragment']) && $parts['fragment'] !== '') {
|
||||
$normalized .= '#' . $parts['fragment'];
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('page_request_is_ajax')) {
|
||||
function page_request_is_ajax(): bool {
|
||||
return strtolower((string)($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '')) === 'xmlhttprequest';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('page_redirect_legacy_url')) {
|
||||
function page_redirect_legacy_url(): void {
|
||||
if (PHP_SAPI === 'cli') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) !== 'GET') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($_GET['page']) || page_request_is_ajax()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$requestUri = (string)($_SERVER['REQUEST_URI'] ?? '');
|
||||
if ($requestUri === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$normalized = page_normalize_url($requestUri);
|
||||
if ($normalized !== '' && $normalized !== $requestUri) {
|
||||
header('Location: ' . $normalized, true, 302);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('set_route_page_context')) {
|
||||
function set_route_page_context(string $page): void {
|
||||
$_GET['page'] = $page;
|
||||
$_REQUEST['page'] = $page;
|
||||
}
|
||||
}
|
||||
5
items.php
Normal file
5
items.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'items');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
logs.php
Normal file
5
logs.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'logs');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
low_stock_report.php
Normal file
5
low_stock_report.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'low_stock_report');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
loyalty_history.php
Normal file
5
loyalty_history.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'loyalty_history');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
lpos.php
Normal file
5
lpos.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'lpos');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
my_profile.php
Normal file
5
my_profile.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'my_profile');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
outlets.php
Normal file
5
outlets.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'outlets');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
22
page_bootstrap.php
Normal file
22
page_bootstrap.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/page_routes.php';
|
||||
|
||||
if (!defined('APP_PAGE')) {
|
||||
http_response_code(500);
|
||||
exit('Route bootstrap is missing APP_PAGE.');
|
||||
}
|
||||
|
||||
if (PHP_SAPI !== 'cli' && strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'GET' && !page_request_is_ajax() && isset($_GET['page'])) {
|
||||
$requestUri = (string)($_SERVER['REQUEST_URI'] ?? '');
|
||||
$normalized = page_normalize_url($requestUri);
|
||||
if ($normalized !== '' && $normalized !== $requestUri) {
|
||||
header('Location: ' . $normalized, true, 302);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
define('APP_ROUTE_BOOTSTRAP', true);
|
||||
set_route_page_context(APP_PAGE);
|
||||
require __DIR__ . '/index.php';
|
||||
@ -11,7 +11,7 @@
|
||||
<i class="bi bi-plus-lg"></i> <span data-en="Manual Entry" data-ar="قيد يدوي">Manual Entry</span>
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<a href="index.php?page=accounting" class="btn <?= !isset($_GET['view']) ? 'btn-primary' : 'btn-outline-primary' ?>" data-en="Journal" data-ar="اليومية">Journal</a>
|
||||
<a href="<?= htmlspecialchars(page_url('accounting')) ?>" class="btn <?= !isset($_GET['view']) ? 'btn-primary' : 'btn-outline-primary' ?>" data-en="Journal" data-ar="اليومية">Journal</a>
|
||||
<a href="index.php?page=accounting&view=coa" class="btn <?= isset($_GET['view']) && $_GET['view'] === 'coa' ? 'btn-primary' : 'btn-outline-primary' ?>" data-en="Chart of Accounts" data-ar="شجرة الحسابات">Chart of Accounts</a>
|
||||
<a href="index.php?page=accounting&view=trial_balance" class="btn <?= isset($_GET['view']) && $_GET['view'] === 'trial_balance' ? 'btn-primary' : 'btn-outline-primary' ?>" data-en="Trial Balance" data-ar="ميزان المراجعة">Trial Balance</a>
|
||||
<a href="index.php?page=accounting&view=profit_loss" class="btn <?= isset($_GET['view']) && $_GET['view'] === 'profit_loss' ? 'btn-primary' : 'btn-outline-primary' ?>" data-en="P&L" data-ar="الأرباح">P&L</a>
|
||||
|
||||
@ -55,7 +55,7 @@
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Start Copy/Sync</button>
|
||||
<a href="index.php?page=settings" class="btn btn-secondary">Back</a>
|
||||
<a href="<?= htmlspecialchars(page_url('settings')) ?>" class="btn btn-secondary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
173
pages/role_groups_view.php
Normal file
173
pages/role_groups_view.php
Normal file
@ -0,0 +1,173 @@
|
||||
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center border-0 flex-wrap gap-3">
|
||||
<div>
|
||||
<h5 class="m-0 fw-bold text-primary" data-en="Role Groups" data-ar="مجموعات الأدوار">Role Groups</h5>
|
||||
<p class="text-muted small mb-0" data-en="Manage access levels and permissions" data-ar="إدارة مستويات الوصول والصلاحيات">Manage access levels and permissions</p>
|
||||
</div>
|
||||
<button class="btn btn-primary rounded-pill px-4" data-bs-toggle="modal" data-bs-target="#addRoleGroupModal">
|
||||
<i class="bi bi-shield-plus me-1"></i>
|
||||
<span data-en="Create New Group" data-ar="إنشاء مجموعة جديدة">Create New Group</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4" data-en="Group Name" data-ar="اسم المجموعة">Group Name</th>
|
||||
<th data-en="Created Date" data-ar="تاريخ الإنشاء">Created Date</th>
|
||||
<th data-en="Status" data-ar="الحالة">Status</th>
|
||||
<th data-en="Actions" data-ar="الإجراءات" class="text-end pe-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($data['role_groups'] as $group): ?>
|
||||
<tr>
|
||||
<td class="ps-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle bg-primary bg-opacity-10 p-2 me-3 text-primary">
|
||||
<i class="bi bi-shield-check"></i>
|
||||
</div>
|
||||
<span class="fw-semibold text-dark"><?= htmlspecialchars((string)$group['name']) ?></span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-muted small">
|
||||
<?= !empty($group['created_at']) ? htmlspecialchars(date('M d, Y', strtotime((string)$group['created_at']))) : '-' ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge rounded-pill bg-success bg-opacity-10 text-success px-3">Active</span>
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
<div class="dropdown d-inline-block">
|
||||
<button class="btn btn-light btn-sm rounded-circle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-three-dots-vertical"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
|
||||
<?php if (can('users_edit')): ?>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#editRoleGroupModal<?= $group['id'] ?>">
|
||||
<i class="bi bi-pencil me-2 text-primary"></i> Edit Permissions
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if (can('users_delete')): ?>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form method="POST" onsubmit="return confirm('Delete this role group? This cannot be undone.')">
|
||||
<input type="hidden" name="id" value="<?= $group['id'] ?>">
|
||||
<button type="submit" name="delete_role_group" class="dropdown-item text-danger">
|
||||
<i class="bi bi-trash me-2"></i> Delete Group
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="editRoleGroupModal<?= $group['id'] ?>" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content border-0 shadow text-start">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title fw-bold" data-en="Edit Role Group" data-ar="تعديل مجموعة الأدوار">Edit Role Group</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="id" value="<?= $group['id'] ?>">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold" data-en="Group Name" data-ar="اسم المجموعة">Group Name</label>
|
||||
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars((string)$group['name']) ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label fw-semibold mb-0" data-en="Permissions" data-ar="الصلاحيات">Permissions</label>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-xs btn-outline-primary py-0 px-2 small select-all-btn" data-modal="#editRoleGroupModal<?= $group['id'] ?>">Select All</button>
|
||||
<button type="button" class="btn btn-xs btn-outline-secondary py-0 px-2 small deselect-all-btn" data-modal="#editRoleGroupModal<?= $group['id'] ?>">Deselect All</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 p-2 bg-light rounded d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<span class="small fw-bold me-2">Global Actions:</span>
|
||||
<div class="d-flex gap-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input select-all-action" type="checkbox" data-action="view" id="selectAllView<?= $group['id'] ?>">
|
||||
<label class="form-check-label small" for="selectAllView<?= $group['id'] ?>">View</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input select-all-action" type="checkbox" data-action="add" id="selectAllAdd<?= $group['id'] ?>">
|
||||
<label class="form-check-label small" for="selectAllAdd<?= $group['id'] ?>">Add</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input select-all-action" type="checkbox" data-action="edit" id="selectAllEdit<?= $group['id'] ?>">
|
||||
<label class="form-check-label small" for="selectAllEdit<?= $group['id'] ?>" data-en="Edit" data-ar="تعديل">Edit</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input select-all-action" type="checkbox" data-action="delete" id="selectAllDelete<?= $group['id'] ?>">
|
||||
<label class="form-check-label small" for="selectAllDelete<?= $group['id'] ?>" data-en="Delete" data-ar="حذف">Delete</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row overflow-auto pe-2" style="max-height: 500px;">
|
||||
<?php
|
||||
$stmtP = db()->prepare("SELECT permission FROM role_permissions WHERE role_id = ?");
|
||||
$stmtP->execute([$group['id']]);
|
||||
$perms = $stmtP->fetchAll(PDO::FETCH_COLUMN);
|
||||
foreach ($permission_groups as $group_name => $modules): ?>
|
||||
<div class="permission-group-container col-12 mb-4">
|
||||
<div class="mt-3 mb-2 bg-secondary bg-opacity-10 p-2 d-flex justify-content-between align-items-center rounded border-start border-primary border-3">
|
||||
<span class="fw-bold text-uppercase small text-primary"><?= htmlspecialchars((string)$group_name) ?></span>
|
||||
<div class="form-check mb-0">
|
||||
<input class="form-check-input select-all-group" type="checkbox" id="group_<?= $group['id'] ?>_<?= strtolower(str_replace(' ', '_', $group_name)) ?>">
|
||||
<label class="form-check-label small fw-bold" for="group_<?= $group['id'] ?>_<?= strtolower(str_replace(' ', '_', $group_name)) ?>">Group All</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<?php foreach ($modules as $m => $label): ?>
|
||||
<div class="col-md-6 mb-2 border-bottom pb-2 module-row">
|
||||
<div class="small fw-bold mb-2 text-dark border-start border-2 ps-2 border-info d-flex justify-content-between align-items-center">
|
||||
<span><?= htmlspecialchars((string)$label) ?></span>
|
||||
<div class="form-check mb-0">
|
||||
<input class="form-check-input select-all-row" type="checkbox" id="row_all_<?= $group['id'] ?>_<?= $m ?>">
|
||||
<label class="form-check-label smaller text-muted mb-0 ms-1" style="font-size: 0.7rem;" for="row_all_<?= $group['id'] ?>_<?= $m ?>">Select All</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-3 flex-wrap ps-2">
|
||||
<?php foreach (['view', 'add', 'edit', 'delete'] as $a):
|
||||
$p = $m . '_' . $a;
|
||||
?>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input perm-check" type="checkbox" name="permissions[]" value="<?= $p ?>" data-action="<?= $a ?>" id="perm_<?= $group['id'] ?>_<?= $p ?>" <?= in_array($p, (array)$perms, true) ? 'checked' : '' ?>>
|
||||
<label class="form-check-label small" for="perm_<?= $group['id'] ?>_<?= $p ?>"><?= ucfirst($a) ?></label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light rounded-pill px-3" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
||||
<button type="submit" name="edit_role_group" class="btn btn-primary rounded-pill px-4" data-en="Update" data-ar="تحديث">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($data['role_groups'])): ?>
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-4 text-muted" data-en="No role groups found" data-ar="لا توجد مجموعات أدوار">No role groups found</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php require 'pages/users_role_permissions_script.php'; ?>
|
||||
138
pages/sales_purchases_handlers.php
Normal file
138
pages/sales_purchases_handlers.php
Normal file
@ -0,0 +1,138 @@
|
||||
<?php
|
||||
// Shared Sales/Purchases secondary handlers extracted from index.php.
|
||||
|
||||
if (!function_exists('sales_purchases_target_page')) {
|
||||
function sales_purchases_target_page(string $type): string {
|
||||
return $type === 'purchase' ? 'purchases' : 'sales';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['delete_invoice'])) {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$type = ($page === 'purchases') ? 'purchase' : 'sale';
|
||||
$table = ($type === 'purchase') ? 'purchases' : 'invoices';
|
||||
$item_table = ($type === 'purchase') ? 'purchase_items' : 'invoice_items';
|
||||
$fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
|
||||
|
||||
db()->prepare("DELETE FROM $table WHERE id = ?")->execute([$id]);
|
||||
db()->prepare("DELETE FROM $item_table WHERE $fk_col = ?")->execute([$id]);
|
||||
redirectWithMessage(($type === 'purchase' ? 'Purchase' : 'Invoice') . ' deleted!', page_url(sales_purchases_target_page($type)));
|
||||
}
|
||||
|
||||
if (isset($_POST['convert_to_invoice'])) {
|
||||
$db = db();
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
$quot_id = (int)($_POST['quotation_id'] ?? 0);
|
||||
|
||||
$stmt = $db->prepare("SELECT * FROM quotations WHERE id = ?");
|
||||
$stmt->execute([$quot_id]);
|
||||
$quot = $stmt->fetch();
|
||||
|
||||
if (!$quot) {
|
||||
throw new Exception('Quotation not found.');
|
||||
}
|
||||
if (($quot['status'] ?? '') === 'converted') {
|
||||
throw new Exception('Quotation already converted.');
|
||||
}
|
||||
|
||||
$stmtItems = $db->prepare("SELECT * FROM quotation_items WHERE quotation_id = ?");
|
||||
$stmtItems->execute([$quot_id]);
|
||||
$qItems = $stmtItems->fetchAll();
|
||||
|
||||
$inv_date = date('Y-m-d');
|
||||
$stmtInv = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount, outlet_id) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0, ?)");
|
||||
$stmtInv->execute([$quot['customer_id'], $inv_date, $quot['total_amount'], $quot['vat_amount'], $quot['total_with_vat'], current_outlet_id()]);
|
||||
$inv_id = $db->lastInsertId();
|
||||
|
||||
$items_for_journal = [];
|
||||
foreach ($qItems as $item) {
|
||||
$lineVatAmount = line_item_vat_amount($db, $item);
|
||||
$db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")
|
||||
->execute([$inv_id, $item['item_id'], $item['quantity'], $item['unit_price'], $lineVatAmount, $item['total_price']]);
|
||||
|
||||
update_stock($item['item_id'], -$item['quantity']);
|
||||
$items_for_journal[] = ['id' => $item['item_id'], 'qty' => $item['quantity']];
|
||||
}
|
||||
|
||||
$db->prepare("UPDATE quotations SET status = 'converted' WHERE id = ?")->execute([$quot_id]);
|
||||
recordSaleJournal($inv_id, $quot['total_with_vat'], $inv_date, $items_for_journal, $quot['vat_amount']);
|
||||
|
||||
$db->commit();
|
||||
redirectWithMessage("Quotation converted to Invoice #$inv_id successfully!", page_url('sales'));
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
$message = 'Error: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['convert_lpo_to_purchase'])) {
|
||||
$db = db();
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
$lpo_id = (int)($_POST['lpo_id'] ?? 0);
|
||||
|
||||
$stmt = $db->prepare("SELECT * FROM lpos WHERE id = ?");
|
||||
$stmt->execute([$lpo_id]);
|
||||
$lpo = $stmt->fetch();
|
||||
|
||||
if (!$lpo) {
|
||||
throw new Exception('LPO not found.');
|
||||
}
|
||||
if (($lpo['status'] ?? '') === 'converted') {
|
||||
throw new Exception('LPO already converted.');
|
||||
}
|
||||
|
||||
$stmtItems = $db->prepare("SELECT * FROM lpo_items WHERE lpo_id = ?");
|
||||
$stmtItems->execute([$lpo_id]);
|
||||
$lItems = $stmtItems->fetchAll();
|
||||
|
||||
$pur_date = date('Y-m-d');
|
||||
$stmtPur = $db->prepare("INSERT INTO purchases (supplier_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount, outlet_id) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0, ?)");
|
||||
$stmtPur->execute([$lpo['supplier_id'], $pur_date, $lpo['total_amount'], $lpo['vat_amount'], $lpo['total_with_vat'], current_outlet_id()]);
|
||||
$pur_id = $db->lastInsertId();
|
||||
|
||||
foreach ($lItems as $item) {
|
||||
$db->prepare("INSERT INTO purchase_items (purchase_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")
|
||||
->execute([$pur_id, $item['item_id'], $item['quantity'], $item['unit_price'], $item['vat_amount'], $item['total_amount']]);
|
||||
|
||||
update_stock($item['item_id'], $item['quantity']);
|
||||
}
|
||||
|
||||
$db->prepare("UPDATE lpos SET status = 'converted' WHERE id = ?")->execute([$lpo_id]);
|
||||
|
||||
$db->commit();
|
||||
redirectWithMessage("LPO converted to Purchase Invoice #$pur_id successfully!", page_url('purchases'));
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
$message = 'Error: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['record_payment'])) {
|
||||
$id = (int)($_POST['invoice_id'] ?? 0);
|
||||
$amount = (float)($_POST['amount'] ?? 0);
|
||||
$date = !empty($_POST['payment_date']) ? (string)$_POST['payment_date'] : date('Y-m-d');
|
||||
$method = $_POST['payment_method'] ?? 'Cash';
|
||||
$type = ($page === 'purchases') ? 'purchase' : 'sale';
|
||||
$table = ($type === 'purchase') ? 'purchases' : 'invoices';
|
||||
$payment_table = ($type === 'purchase') ? 'purchase_payments' : 'payments';
|
||||
$fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
|
||||
|
||||
$db = db();
|
||||
$db->prepare("INSERT INTO $payment_table ($fk_col, amount, payment_date, payment_method, notes) VALUES (?, ?, ?, ?, ?)")
|
||||
->execute([$id, $amount, $date, $method, $_POST['notes'] ?? '']);
|
||||
$pay_id = $db->lastInsertId();
|
||||
$db->prepare("UPDATE $table SET paid_amount = paid_amount + ?, status = IF(paid_amount + ? >= total_with_vat, 'paid', 'partially_paid') WHERE id = ?")
|
||||
->execute([$amount, $amount, $id]);
|
||||
|
||||
if ($type === 'sale') {
|
||||
recordPaymentReceivedJournal((int)$pay_id, $amount, $date, $method);
|
||||
} else {
|
||||
recordPaymentMadeJournal((int)$pay_id, $amount, $date, $method);
|
||||
}
|
||||
|
||||
$_SESSION['trigger_receipt_modal'] = true;
|
||||
$_SESSION['show_receipt_id'] = $pay_id;
|
||||
redirectWithMessage('Payment recorded!', page_url(sales_purchases_target_page($type)));
|
||||
}
|
||||
6
pages/sales_purchases_page_script.php
Normal file
6
pages/sales_purchases_page_script.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
// Sales/Purchases page-specific JavaScript bundle extracted from index.php.
|
||||
require __DIR__ . '/sales_purchases_payment_receipt_script.php';
|
||||
require __DIR__ . '/sales_purchases_invoice_form_helpers.php';
|
||||
require __DIR__ . '/sales_purchases_print_script.php';
|
||||
require __DIR__ . '/sales_purchases_invoice_actions_script.php';
|
||||
@ -27,7 +27,7 @@
|
||||
.modal.show .modal-body { padding: 0 !important; margin: 0 !important; background: white !important; }
|
||||
|
||||
@page {
|
||||
size: auto;
|
||||
size: A4 portrait;
|
||||
margin: 5mm;
|
||||
}
|
||||
|
||||
|
||||
@ -89,7 +89,7 @@
|
||||
$_SESSION['show_invoice_id'] = (int)$inv_id;
|
||||
$_SESSION['show_invoice_page'] = ($type === 'purchase') ? 'purchases' : 'sales';
|
||||
$msg = ($type === 'purchase' ? "Purchase" : "Invoice") . " #$inv_id created!";
|
||||
redirectWithMessage($msg, "index.php?page=" . ($type === 'purchase' ? 'purchases' : 'sales'));
|
||||
redirectWithMessage($msg, page_url($type === 'purchase' ? 'purchases' : 'sales'));
|
||||
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
|
||||
}
|
||||
|
||||
@ -174,6 +174,6 @@
|
||||
|
||||
$db->commit();
|
||||
$msg = ($type === 'purchase' ? "Purchase" : "Invoice") . " updated successfully!";
|
||||
redirectWithMessage($msg, "index.php?page=" . ($type === 'purchase' ? 'purchases' : 'sales'));
|
||||
redirectWithMessage($msg, page_url($type === 'purchase' ? 'purchases' : 'sales'));
|
||||
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
|
||||
}
|
||||
|
||||
@ -53,8 +53,7 @@
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="bg-light p-3 rounded mb-4 d-print-none">
|
||||
<form method="GET" class="documents-filter">
|
||||
<input type="hidden" name="page" value="<?= $page ?>">
|
||||
<form method="GET" action="<?= htmlspecialchars(page_url($page)) ?>" class="documents-filter">
|
||||
<input type="hidden" name="limit" value="<?= (int)($_GET['limit'] ?? 20) ?>">
|
||||
<div class="documents-filter__field documents-filter__field--search">
|
||||
<label class="form-label small fw-bold" data-en="Search" data-ar="بحث">Search</label>
|
||||
@ -93,7 +92,7 @@
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="window.print()">
|
||||
<i class="bi bi-printer"></i> <span data-en="Print" data-ar="طباعة">Print</span>
|
||||
</button>
|
||||
<a href="index.php?page=<?= $page ?>" class="btn btn-outline-secondary btn-sm">
|
||||
<a href="<?= htmlspecialchars(page_url($page)) ?>" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-x-circle"></i> <span data-en="Clear" data-ar="مسح">Clear</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
157
pages/settings_save_logic.php
Normal file
157
pages/settings_save_logic.php
Normal file
@ -0,0 +1,157 @@
|
||||
<?php
|
||||
if (isset($_POST['update_settings'])) {
|
||||
if (can('settings_view')) {
|
||||
$db = db();
|
||||
$settings = isset($_POST['settings']) && is_array($_POST['settings']) ? $_POST['settings'] : [];
|
||||
|
||||
$textLimits = [
|
||||
'company_name' => 190,
|
||||
'ctr_no' => 120,
|
||||
'vat_number' => 120,
|
||||
'company_phone' => 60,
|
||||
'company_email' => 190,
|
||||
'company_address' => 500,
|
||||
'license_app_name' => 190,
|
||||
'smtp_host' => 190,
|
||||
'smtp_user' => 190,
|
||||
'smtp_pass' => 255,
|
||||
'smtp_from_email' => 190,
|
||||
'smtp_from_name' => 190,
|
||||
'smtp_reply_to' => 190,
|
||||
'wablas_api_url' => 255,
|
||||
'wablas_token' => 500,
|
||||
'wablas_security_key' => 500,
|
||||
'wablas_sender' => 190,
|
||||
'wablas_invoice_numbers' => 1500,
|
||||
'wablas_invoice_template' => 3000,
|
||||
'wablas_daily_summary_numbers' => 1500,
|
||||
'wablas_daily_summary_template' => 3000,
|
||||
];
|
||||
|
||||
foreach ($textLimits as $key => $limit) {
|
||||
if (array_key_exists($key, $settings)) {
|
||||
$settings[$key] = substr(trim((string)$settings[$key]), 0, $limit);
|
||||
}
|
||||
}
|
||||
|
||||
$licenseAppName = trim((string)($settings['license_app_name'] ?? ''));
|
||||
$settings['license_app_name'] = $licenseAppName !== '' ? substr($licenseAppName, 0, 190) : '';
|
||||
|
||||
$licenseAppSlug = trim((string)($settings['license_app_slug'] ?? ''));
|
||||
$settings['license_app_slug'] = $licenseAppSlug !== '' ? LicenseService::sanitizeAppSlug($licenseAppSlug, true) : '';
|
||||
|
||||
$timezone = trim((string)($settings['timezone'] ?? ''));
|
||||
$settings['timezone'] = in_array($timezone, DateTimeZone::listIdentifiers(), true)
|
||||
? $timezone
|
||||
: date_default_timezone_get();
|
||||
|
||||
$settings['allow_zero_stock_sell'] = (($settings['allow_zero_stock_sell'] ?? '1') === '0') ? '0' : '1';
|
||||
$settings['loyalty_enabled'] = (($settings['loyalty_enabled'] ?? '0') === '1') ? '1' : '0';
|
||||
$settings['smtp_enabled'] = (($settings['smtp_enabled'] ?? '0') === '1') ? '1' : '0';
|
||||
$settings['wablas_enabled'] = (($settings['wablas_enabled'] ?? '0') === '1') ? '1' : '0';
|
||||
$settings['wablas_invoice_enabled'] = (($settings['wablas_invoice_enabled'] ?? '0') === '1') ? '1' : '0';
|
||||
$settings['wablas_daily_summary_enabled'] = (($settings['wablas_daily_summary_enabled'] ?? '0') === '1') ? '1' : '0';
|
||||
|
||||
$settings['weight_barcode_mode'] = in_array(($settings['weight_barcode_mode'] ?? 'weight'), ['weight', 'price'], true)
|
||||
? (string)$settings['weight_barcode_mode']
|
||||
: 'weight';
|
||||
|
||||
$prefixStart = (int)($settings['weight_barcode_prefix_start'] ?? 20);
|
||||
$prefixEnd = (int)($settings['weight_barcode_prefix_end'] ?? 29);
|
||||
if ($prefixStart < 20 || $prefixStart > 29) {
|
||||
$prefixStart = 20;
|
||||
}
|
||||
if ($prefixEnd < 20 || $prefixEnd > 29) {
|
||||
$prefixEnd = 29;
|
||||
}
|
||||
if ($prefixStart > $prefixEnd) {
|
||||
[$prefixStart, $prefixEnd] = [$prefixEnd, $prefixStart];
|
||||
}
|
||||
$settings['weight_barcode_prefix_start'] = (string)$prefixStart;
|
||||
$settings['weight_barcode_prefix_end'] = (string)$prefixEnd;
|
||||
|
||||
$smtpPortRaw = trim((string)($settings['smtp_port'] ?? ''));
|
||||
if ($smtpPortRaw === '') {
|
||||
$settings['smtp_port'] = '';
|
||||
} else {
|
||||
$smtpPort = (int)$smtpPortRaw;
|
||||
if ($smtpPort < 1 || $smtpPort > 65535) {
|
||||
$smtpPort = 587;
|
||||
}
|
||||
$settings['smtp_port'] = (string)$smtpPort;
|
||||
}
|
||||
|
||||
$settings['smtp_secure'] = in_array(($settings['smtp_secure'] ?? 'tls'), ['tls', 'ssl', 'none'], true)
|
||||
? (string)$settings['smtp_secure']
|
||||
: 'tls';
|
||||
|
||||
$wablasApiUrl = trim((string)($settings['wablas_api_url'] ?? ''));
|
||||
$settings['wablas_api_url'] = $wablasApiUrl !== '' ? substr(rtrim($wablasApiUrl, '/'), 0, 255) : '';
|
||||
|
||||
$wablasCountryCode = preg_replace('/[^0-9+]/', '', (string)($settings['wablas_default_country_code'] ?? ''));
|
||||
$settings['wablas_default_country_code'] = substr($wablasCountryCode, 0, 8);
|
||||
|
||||
$timeFields = [
|
||||
'wablas_invoice_time' => '',
|
||||
'wablas_daily_summary_time' => '20:00',
|
||||
];
|
||||
foreach ($timeFields as $key => $fallback) {
|
||||
$timeValue = trim((string)($settings[$key] ?? ''));
|
||||
if ($timeValue === '') {
|
||||
$settings[$key] = $fallback;
|
||||
continue;
|
||||
}
|
||||
$settings[$key] = preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/', $timeValue) ? $timeValue : $fallback;
|
||||
}
|
||||
|
||||
foreach (['wablas_invoice_numbers', 'wablas_daily_summary_numbers'] as $numbersKey) {
|
||||
$numbersRaw = str_replace(["\r\n", "\r"], "\n", (string)($settings[$numbersKey] ?? ''));
|
||||
$parts = preg_split('/[\n,;]+/', $numbersRaw) ?: [];
|
||||
$normalizedNumbers = [];
|
||||
foreach ($parts as $part) {
|
||||
$phone = preg_replace('/[^0-9+]/', '', trim((string)$part));
|
||||
if ($phone !== '') {
|
||||
$normalizedNumbers[$phone] = true;
|
||||
}
|
||||
}
|
||||
$settings[$numbersKey] = implode("\n", array_slice(array_keys($normalizedNumbers), 0, 50));
|
||||
}
|
||||
|
||||
foreach (['wablas_invoice_template' => 3000, 'wablas_daily_summary_template' => 3000] as $templateKey => $limit) {
|
||||
$template = str_replace(["\r\n", "\r"], "\n", (string)($settings[$templateKey] ?? ''));
|
||||
$settings[$templateKey] = substr(trim($template), 0, $limit);
|
||||
}
|
||||
|
||||
foreach ($settings as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
continue;
|
||||
}
|
||||
$value = (string)$value;
|
||||
$stmt = $db->prepare("INSERT INTO settings (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?");
|
||||
$stmt->execute([$key, $value, $value]);
|
||||
}
|
||||
|
||||
$files = ['company_logo', 'favicon', 'manager_signature', 'display_slide_1', 'display_slide_2', 'display_slide_3'];
|
||||
foreach ($files as $file_key) {
|
||||
if (isset($_FILES[$file_key]) && $_FILES[$file_key]['error'] === 0) {
|
||||
$ext = pathinfo($_FILES[$file_key]['name'], PATHINFO_EXTENSION);
|
||||
$filename = 'uploads/' . $file_key . '_' . time() . '.' . $ext;
|
||||
if (!is_dir('uploads')) {
|
||||
mkdir('uploads', 0777, true);
|
||||
}
|
||||
if (move_uploaded_file($_FILES[$file_key]['tmp_name'], $filename)) {
|
||||
$stmt = $db->prepare("INSERT INTO settings (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?");
|
||||
$stmt->execute([$file_key, $filename, $filename]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$allowedTabs = ['company', 'system', 'branding', 'integrations'];
|
||||
$activeTab = strtolower(trim((string)($_POST['settings_active_tab'] ?? 'company')));
|
||||
if (!in_array($activeTab, $allowedTabs, true)) {
|
||||
$activeTab = 'company';
|
||||
}
|
||||
|
||||
redirectWithMessage('Settings updated successfully!', page_url('settings', ['tab' => $activeTab]));
|
||||
}
|
||||
}
|
||||
666
pages/settings_view.php
Normal file
666
pages/settings_view.php
Normal file
@ -0,0 +1,666 @@
|
||||
<?php
|
||||
$licenseIdentity = LicenseService::getClientIdentity();
|
||||
$licenseSourceLabels = [
|
||||
'settings' => 'Saved in settings',
|
||||
'environment' => 'Environment variable',
|
||||
'derived' => 'Derived from app name',
|
||||
'default' => 'Built-in fallback',
|
||||
];
|
||||
|
||||
$savedLicenseAppName = trim((string)($data['settings']['license_app_name'] ?? ''));
|
||||
$savedLicenseAppSlug = trim((string)($data['settings']['license_app_slug'] ?? ''));
|
||||
$licenseAppNameInput = $savedLicenseAppName;
|
||||
$licenseAppSlugInput = $savedLicenseAppSlug;
|
||||
|
||||
$settingsTabs = [
|
||||
'company' => ['label' => 'Company', 'label_ar' => 'الشركة', 'icon' => 'bi-building'],
|
||||
'system' => ['label' => 'System', 'label_ar' => 'النظام', 'icon' => 'bi-sliders'],
|
||||
'branding' => ['label' => 'Branding', 'label_ar' => 'الهوية البصرية', 'icon' => 'bi-palette'],
|
||||
'integrations' => ['label' => 'Integrations', 'label_ar' => 'التكاملات', 'icon' => 'bi-plug'],
|
||||
];
|
||||
|
||||
$requestedSettingsTab = strtolower(trim((string)($_GET['tab'] ?? ($_POST['settings_active_tab'] ?? 'company'))));
|
||||
$activeSettingsTab = array_key_exists($requestedSettingsTab, $settingsTabs) ? $requestedSettingsTab : 'company';
|
||||
$currentTimezone = (string)($data['settings']['timezone'] ?? date_default_timezone_get());
|
||||
$timezoneIdentifiers = DateTimeZone::listIdentifiers();
|
||||
$smtpConfigured = !empty($data['settings']['smtp_host']) && !empty($data['settings']['smtp_user']);
|
||||
$wablasConfigured = !empty($data['settings']['wablas_api_url']) && !empty($data['settings']['wablas_token']) && !empty($data['settings']['wablas_security_key']);
|
||||
?>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-xxl-10">
|
||||
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
|
||||
<div class="card-header bg-white py-3 border-bottom-0 d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3">
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<div class="rounded-circle bg-primary bg-opacity-10 p-3 text-primary">
|
||||
<i class="bi bi-building-gear fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="h5 m-0 fw-bold text-dark" data-en="Company Profile & Settings" data-ar="ملف الشركة والإعدادات">Company Profile & Settings</h1>
|
||||
<p class="text-muted small mb-0" data-en="Manage your business identity, operations, branding, and integrations" data-ar="إدارة هوية شركتك وعملياتها وهويتها البصرية والتكاملات">Manage your business identity, operations, branding, and integrations</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a href="<?= htmlspecialchars(page_url('copy_outlet_data')) ?>" class="btn btn-outline-primary btn-sm rounded-pill px-3">
|
||||
<i class="bi bi-arrow-repeat me-1"></i>
|
||||
<span data-en="Sync Outlets" data-ar="مزامنة الفروع">Sync Outlets</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="alert alert-light border rounded-4 d-flex flex-column flex-xl-row justify-content-between align-items-xl-center gap-3 mb-4 shadow-sm">
|
||||
<div>
|
||||
<div class="fw-semibold text-dark mb-1" data-en="One place for business, system, branding, and integration settings" data-ar="مكان واحد لإعدادات الشركة والنظام والهوية البصرية والتكاملات">One place for business, system, branding, and integration settings</div>
|
||||
<div class="small text-muted" data-en="Tabs keep the page shorter and preserve the selected section after you save." data-ar="التبويبات تجعل الصفحة أقصر وتحافظ على التبويب المحدد بعد الحفظ.">Tabs keep the page shorter and preserve the selected section after you save.</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2 small">
|
||||
<span class="badge rounded-pill text-bg-light border px-3 py-2">
|
||||
<i class="bi bi-envelope me-1"></i>
|
||||
<?= $smtpConfigured ? 'SMTP ready' : 'SMTP draft' ?>
|
||||
</span>
|
||||
<span class="badge rounded-pill text-bg-light border px-3 py-2">
|
||||
<i class="bi bi-whatsapp me-1"></i>
|
||||
<?= $wablasConfigured ? 'Wablas ready' : 'Wablas draft' ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="settings_active_tab" id="settings-active-tab" value="<?= htmlspecialchars($activeSettingsTab) ?>">
|
||||
|
||||
<div class="bg-light rounded-4 p-2 mb-4 shadow-sm">
|
||||
<ul class="nav nav-pills nav-fill flex-column flex-lg-row gap-2" id="settingsTabs" role="tablist">
|
||||
<?php foreach ($settingsTabs as $tabKey => $tabMeta): ?>
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
<button
|
||||
class="nav-link rounded-3 text-start text-lg-center px-3 py-3 <?= $activeSettingsTab === $tabKey ? 'active shadow-sm' : 'text-dark' ?>"
|
||||
id="settings-tab-<?= htmlspecialchars($tabKey) ?>"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#settings-pane-<?= htmlspecialchars($tabKey) ?>"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="settings-pane-<?= htmlspecialchars($tabKey) ?>"
|
||||
aria-selected="<?= $activeSettingsTab === $tabKey ? 'true' : 'false' ?>"
|
||||
data-tab-key="<?= htmlspecialchars($tabKey) ?>"
|
||||
>
|
||||
<span class="d-inline-flex align-items-center gap-2 fw-semibold">
|
||||
<i class="bi <?= htmlspecialchars($tabMeta['icon']) ?>"></i>
|
||||
<span data-en="<?= htmlspecialchars($tabMeta['label']) ?>" data-ar="<?= htmlspecialchars($tabMeta['label_ar']) ?>"><?= $lang === 'ar' ? $tabMeta['label_ar'] : $tabMeta['label'] ?></span>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="settingsTabsContent">
|
||||
<div class="tab-pane fade <?= $activeSettingsTab === 'company' ? 'show active' : '' ?>" id="settings-pane-company" role="tabpanel" aria-labelledby="settings-tab-company">
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card border-0 shadow-sm h-100 rounded-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<div class="rounded-circle bg-primary bg-opacity-10 text-primary p-2"><i class="bi bi-building"></i></div>
|
||||
<div>
|
||||
<h2 class="h6 fw-bold mb-0" data-en="Company Details" data-ar="تفاصيل الشركة">Company Details</h2>
|
||||
<p class="text-muted small mb-0" data-en="Legal and tax identity used across documents" data-ar="الهوية القانونية والضريبية المستخدمة في المستندات">Legal and tax identity used across documents</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Company Name" data-ar="اسم الشركة">Company Name</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0"><i class="bi bi-building"></i></span>
|
||||
<input type="text" name="settings[company_name]" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($data['settings']['company_name'] ?? '') ?>" placeholder="e.g. Tech Solutions LLC">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="CTR No (Commercial Registration)" data-ar="رقم السجل التجاري">CTR No (Commercial Registration)</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0"><i class="bi bi-file-text"></i></span>
|
||||
<input type="text" name="settings[ctr_no]" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($data['settings']['ctr_no'] ?? '') ?>" placeholder="e.g. 1234567">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="VAT Number" data-ar="الرقم الضريبي">VAT Number</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0"><i class="bi bi-receipt"></i></span>
|
||||
<input type="text" name="settings[vat_number]" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?>" placeholder="e.g. OM123456789">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card border-0 shadow-sm h-100 rounded-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<div class="rounded-circle bg-primary bg-opacity-10 text-primary p-2"><i class="bi bi-telephone"></i></div>
|
||||
<div>
|
||||
<h2 class="h6 fw-bold mb-0" data-en="Contact Information" data-ar="معلومات الاتصال">Contact Information</h2>
|
||||
<p class="text-muted small mb-0" data-en="Shown in invoices, receipts, and customer-facing documents" data-ar="تظهر في الفواتير والإيصالات والمستندات الموجهة للعملاء">Shown in invoices, receipts, and customer-facing documents</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Phone Number" data-ar="رقم الهاتف">Phone Number</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0"><i class="bi bi-phone"></i></span>
|
||||
<input type="text" name="settings[company_phone]" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($data['settings']['company_phone'] ?? '') ?>" placeholder="+968 9999 9999">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Email Address" data-ar="البريد الإلكتروني">Email Address</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0"><i class="bi bi-envelope"></i></span>
|
||||
<input type="email" name="settings[company_email]" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($data['settings']['company_email'] ?? '') ?>" placeholder="info@example.com">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Physical Address" data-ar="العنوان الفعلي">Physical Address</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0"><i class="bi bi-geo-alt"></i></span>
|
||||
<textarea name="settings[company_address]" class="form-control border-start-0 ps-0" rows="5" placeholder="Street, Building, City..."><?= htmlspecialchars($data['settings']['company_address'] ?? '') ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade <?= $activeSettingsTab === 'system' ? 'show active' : '' ?>" id="settings-pane-system" role="tabpanel" aria-labelledby="settings-tab-system">
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card border-0 shadow-sm h-100 rounded-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<div class="rounded-circle bg-primary bg-opacity-10 text-primary p-2"><i class="bi bi-sliders"></i></div>
|
||||
<div>
|
||||
<h2 class="h6 fw-bold mb-0" data-en="System Configuration" data-ar="تكوين النظام">System Configuration</h2>
|
||||
<p class="text-muted small mb-0" data-en="Operational rules that affect inventory and barcode behavior" data-ar="قواعد التشغيل التي تؤثر على المخزون وسلوك الباركود">Operational rules that affect inventory and barcode behavior</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="System Timezone" data-ar="المنطقة الزمنية للنظام">System Timezone</label>
|
||||
<select name="settings[timezone]" class="form-select">
|
||||
<?php foreach ($timezoneIdentifiers as $tz): ?>
|
||||
<option value="<?= htmlspecialchars($tz) ?>" <?= $tz === $currentTimezone ? 'selected' : '' ?>><?= htmlspecialchars($tz) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Stock Policy" data-ar="سياسة المخزون">Stock Policy</label>
|
||||
<select name="settings[allow_zero_stock_sell]" class="form-select">
|
||||
<option value="0" <?= ($data['settings']['allow_zero_stock_sell'] ?? '1') === '0' ? 'selected' : '' ?> data-en="Prevent selling out of stock" data-ar="منع البيع عند نفاذ المخزون">Prevent selling out of stock</option>
|
||||
<option value="1" <?= ($data['settings']['allow_zero_stock_sell'] ?? '1') === '1' ? 'selected' : '' ?> data-en="Allow selling out of stock" data-ar="السماح بالبيع عند نفاذ المخزون">Allow selling out of stock</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Scale Barcode Mode" data-ar="وضع باركود الميزان">Scale Barcode Mode</label>
|
||||
<select name="settings[weight_barcode_mode]" class="form-select">
|
||||
<option value="weight" <?= ($data['settings']['weight_barcode_mode'] ?? 'weight') === 'weight' ? 'selected' : '' ?> data-en="Use embedded weight" data-ar="استخدام الوزن">Use embedded weight</option>
|
||||
<option value="price" <?= ($data['settings']['weight_barcode_mode'] ?? '') === 'price' ? 'selected' : '' ?> data-en="Use embedded price" data-ar="استخدام السعر">Use embedded price</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Scale Prefix From" data-ar="بادئة الميزان من">Scale Prefix From</label>
|
||||
<input type="number" min="20" max="29" name="settings[weight_barcode_prefix_start]" class="form-control" value="<?= htmlspecialchars($data['settings']['weight_barcode_prefix_start'] ?? '20') ?>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Scale Prefix To" data-ar="بادئة الميزان إلى">Scale Prefix To</label>
|
||||
<input type="number" min="20" max="29" name="settings[weight_barcode_prefix_end]" class="form-control" value="<?= htmlspecialchars($data['settings']['weight_barcode_prefix_end'] ?? '29') ?>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-text" data-en="13-digit scale barcode format: 2-digit prefix + 5-digit item code + 5-digit value + 1 check digit. Full 13-digit scale barcodes are reserved and cannot be saved on items or imported." data-ar="صيغة باركود الميزان 13 رقمًا: بادئة من رقمين + كود صنف من 5 أرقام + قيمة من 5 أرقام + رقم تحقق. الباركود الكامل 13 رقمًا محجوز ولا يمكن حفظه أو استيراده كصنف.">13-digit scale barcode format: 2-digit prefix + 5-digit item code + 5-digit value + 1 check digit. Full 13-digit scale barcodes are reserved and cannot be saved on items or imported.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card border-0 shadow-sm h-100 rounded-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-3">
|
||||
<div>
|
||||
<h2 class="h6 fw-bold mb-1" data-en="License App Identity" data-ar="هوية ترخيص التطبيق">License App Identity</h2>
|
||||
<p class="text-muted small mb-0" data-en="Stable product identity used by the central license manager" data-ar="هوية منتج ثابتة يستخدمها مدير التراخيص المركزي">Stable product identity used by the central license manager</p>
|
||||
</div>
|
||||
<span class="badge rounded-pill text-bg-light border">
|
||||
<span data-en="Name source" data-ar="مصدر الاسم">Name source</span>: <?= htmlspecialchars($licenseSourceLabels[$licenseIdentity['app_name_source']] ?? 'Built-in fallback') ?>
|
||||
·
|
||||
<span data-en="Slug source" data-ar="مصدر المعرّف">Slug source</span>: <?= htmlspecialchars($licenseSourceLabels[$licenseIdentity['app_slug_source']] ?? 'Built-in fallback') ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="alert alert-info border-0 shadow-sm small mb-3">
|
||||
<strong data-en="Use one stable identity per product." data-ar="استخدم هوية ثابتة لكل منتج.">Use one stable identity per product.</strong>
|
||||
<span data-en="Change the slug only when you intentionally rename or split a product." data-ar="غيّر المعرّف فقط عندما تقصد إعادة تسمية المنتج أو تقسيمه.">Change the slug only when you intentionally rename or split a product.</span>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="App Name for Licensing" data-ar="اسم التطبيق للترخيص">App Name for Licensing</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0"><i class="bi bi-window"></i></span>
|
||||
<input type="text" id="license-app-name" name="settings[license_app_name]" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($licenseAppNameInput) ?>" placeholder="<?= htmlspecialchars((string)$licenseIdentity['app_name']) ?>">
|
||||
</div>
|
||||
<div class="form-text" data-en="Leave blank only if you want to fall back to environment/default values." data-ar="اتركه فارغًا فقط إذا كنت تريد الرجوع إلى قيم البيئة أو القيم الافتراضية.">Leave blank only if you want to fall back to environment/default values.</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Stable App Slug" data-ar="المعرّف الثابت للتطبيق">Stable App Slug</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0"><i class="bi bi-tag"></i></span>
|
||||
<input type="text" id="license-app-slug" name="settings[license_app_slug]" data-explicit="<?= $savedLicenseAppSlug !== '' ? '1' : '0' ?>" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($licenseAppSlugInput) ?>" placeholder="<?= htmlspecialchars((string)$licenseIdentity['app_slug']) ?>">
|
||||
<button type="button" class="btn btn-outline-secondary" id="suggest-license-slug" data-en="Suggest" data-ar="اقتراح">Suggest</button>
|
||||
</div>
|
||||
<div class="form-text" data-en="Use one slug per product, not per customer or installation." data-ar="استخدم معرّفًا واحدًا لكل منتج، وليس لكل عميل أو تثبيت.">Use one slug per product, not per customer or installation.</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-text">
|
||||
<span data-en="Current effective identity:" data-ar="هوية التطبيق الفعالة حاليًا:">Current effective identity:</span>
|
||||
<code><?= htmlspecialchars((string)$licenseIdentity['app_name']) ?></code>
|
||||
·
|
||||
<code><?= htmlspecialchars((string)$licenseIdentity['app_slug']) ?></code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm rounded-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<div class="rounded-circle bg-primary bg-opacity-10 text-primary p-2"><i class="bi bi-award"></i></div>
|
||||
<div>
|
||||
<h2 class="h6 fw-bold mb-0" data-en="Loyalty Program" data-ar="برنامج الولاء">Loyalty Program</h2>
|
||||
<p class="text-muted small mb-0" data-en="Control how customers earn and redeem loyalty points" data-ar="التحكم في كيفية كسب العملاء لنقاط الولاء واستردادها">Control how customers earn and redeem loyalty points</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Loyalty Status" data-ar="حالة الولاء">Loyalty Status</label>
|
||||
<select name="settings[loyalty_enabled]" class="form-select">
|
||||
<option value="0" <?= ($data['settings']['loyalty_enabled'] ?? '0') === '0' ? 'selected' : '' ?> data-en="Disabled" data-ar="معطل">Disabled</option>
|
||||
<option value="1" <?= ($data['settings']['loyalty_enabled'] ?? '0') === '1' ? 'selected' : '' ?> data-en="Active" data-ar="نشط">Active</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Earning Rule (Points/1 OMR)" data-ar="قاعدة الكسب (نقاط/1 ريال)">Earning Rule (Points/1 OMR)</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0"><i class="bi bi-arrow-up-circle"></i></span>
|
||||
<input type="number" step="0.01" name="settings[loyalty_points_per_unit]" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($data['settings']['loyalty_points_per_unit'] ?? '1') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Redemption Rule (Points/1 OMR)" data-ar="قاعدة الاسترداد (نقاط/1 ريال)">Redemption Rule (Points/1 OMR)</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0"><i class="bi bi-arrow-down-circle"></i></span>
|
||||
<input type="number" step="0.01" name="settings[loyalty_redeem_points_per_unit]" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($data['settings']['loyalty_redeem_points_per_unit'] ?? '100') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade <?= $activeSettingsTab === 'branding' ? 'show active' : '' ?>" id="settings-pane-branding" role="tabpanel" aria-labelledby="settings-tab-branding">
|
||||
<div class="card border-0 shadow-sm rounded-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<div class="rounded-circle bg-primary bg-opacity-10 text-primary p-2"><i class="bi bi-palette"></i></div>
|
||||
<div>
|
||||
<h2 class="h6 fw-bold mb-0" data-en="Visual Identity" data-ar="الهوية البصرية">Visual Identity</h2>
|
||||
<p class="text-muted small mb-0" data-en="Upload the brand assets used across receipts, browser tabs, and approvals" data-ar="قم برفع الأصول البصرية المستخدمة في الإيصالات وعلامات المتصفح والاعتمادات">Upload the brand assets used across receipts, browser tabs, and approvals</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border rounded-4 bg-light text-center p-3">
|
||||
<label class="form-label fw-semibold mb-2" data-en="Company Logo" data-ar="شعار الشركة">Company Logo</label>
|
||||
<div class="mb-3 d-flex justify-content-center align-items-center" style="height: 110px;">
|
||||
<?php if (!empty($data['settings']['company_logo'])): ?>
|
||||
<img src="<?= htmlspecialchars($data['settings']['company_logo']) ?>?v=<?= time() ?>" alt="Company logo" class="img-fluid" style="max-height: 90px;">
|
||||
<?php else: ?>
|
||||
<i class="bi bi-image text-muted fs-1"></i>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<input type="file" name="company_logo" class="form-control form-control-sm" accept="image/*">
|
||||
<div class="form-text mt-2">PNG or SVG recommended.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border rounded-4 bg-light text-center p-3">
|
||||
<label class="form-label fw-semibold mb-2" data-en="Website Favicon" data-ar="أيقونة الموقع">Website Favicon</label>
|
||||
<div class="mb-3 d-flex justify-content-center align-items-center" style="height: 110px;">
|
||||
<?php if (!empty($data['settings']['favicon'])): ?>
|
||||
<img src="<?= htmlspecialchars($data['settings']['favicon']) ?>?v=<?= time() ?>" alt="Favicon" class="img-fluid" style="max-height: 36px;">
|
||||
<?php else: ?>
|
||||
<i class="bi bi-globe text-muted fs-1"></i>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<input type="file" name="favicon" class="form-control form-control-sm" accept="image/*,.ico">
|
||||
<div class="form-text mt-2">Square icon works best.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border rounded-4 bg-light text-center p-3">
|
||||
<label class="form-label fw-semibold mb-2" data-en="Manager Signature" data-ar="توقيع المدير">Manager Signature</label>
|
||||
<div class="mb-3 d-flex justify-content-center align-items-center" style="height: 110px;">
|
||||
<?php if (!empty($data['settings']['manager_signature'])): ?>
|
||||
<img src="<?= htmlspecialchars($data['settings']['manager_signature']) ?>?v=<?= time() ?>" alt="Manager signature" class="img-fluid" style="max-height: 90px;">
|
||||
<?php else: ?>
|
||||
<i class="bi bi-pen text-muted fs-1"></i>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<input type="file" name="manager_signature" class="form-control form-control-sm" accept="image/*">
|
||||
<div class="form-text mt-2">Used on approvals and printouts.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade <?= $activeSettingsTab === 'integrations' ? 'show active' : '' ?>" id="settings-pane-integrations" role="tabpanel" aria-labelledby="settings-tab-integrations">
|
||||
<div class="row g-4">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning border-0 shadow-sm rounded-4 mb-0">
|
||||
<div class="fw-semibold mb-1" data-en="Integration profiles are now grouped here" data-ar="تم تجميع ملفات التكامل هنا">Integration profiles are now grouped here</div>
|
||||
<div class="small mb-0" data-en="SMTP settings are saved from the admin UI for integration workflows, while the built-in MailService still remains the app’s email engine. Wablas connection, security key, recipients, templates, and schedule fields are now grouped here for WhatsApp automation flows." data-ar="يتم حفظ إعدادات SMTP من واجهة الإدارة لسير عمل التكامل، بينما يظل MailService المدمج هو محرك البريد في التطبيق. كما تم تجميع اتصال Wablas ومفتاح الأمان والمستلمين والقوالب وحقول الجدولة هنا لتدفقات أتمتة واتساب.">SMTP settings are saved from the admin UI for integration workflows, while the built-in MailService still remains the app’s email engine. Wablas connection, security key, recipients, templates, and schedule fields are now grouped here for WhatsApp automation flows.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card border-0 shadow-sm h-100 rounded-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="rounded-circle bg-primary bg-opacity-10 text-primary p-2"><i class="bi bi-envelope"></i></div>
|
||||
<div>
|
||||
<h2 class="h6 fw-bold mb-0" data-en="SMTP Email" data-ar="بريد SMTP">SMTP Email</h2>
|
||||
<p class="text-muted small mb-0" data-en="Store the outgoing email profile used by your business" data-ar="احفظ ملف البريد الصادر المستخدم في عملك">Store the outgoing email profile used by your business</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge rounded-pill <?= ($data['settings']['smtp_enabled'] ?? '0') === '1' ? 'bg-success bg-opacity-10 text-success' : 'text-bg-light border' ?> px-3 py-2">
|
||||
<?= ($data['settings']['smtp_enabled'] ?? '0') === '1' ? 'Enabled' : 'Disabled' ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Status" data-ar="الحالة">Status</label>
|
||||
<select name="settings[smtp_enabled]" class="form-select">
|
||||
<option value="0" <?= ($data['settings']['smtp_enabled'] ?? '0') === '0' ? 'selected' : '' ?>>Disabled</option>
|
||||
<option value="1" <?= ($data['settings']['smtp_enabled'] ?? '0') === '1' ? 'selected' : '' ?>>Enabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="SMTP Host" data-ar="خادم SMTP">SMTP Host</label>
|
||||
<input type="text" name="settings[smtp_host]" class="form-control" value="<?= htmlspecialchars($data['settings']['smtp_host'] ?? '') ?>" placeholder="smtp.office365.com">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Port" data-ar="المنفذ">Port</label>
|
||||
<input type="number" min="1" max="65535" name="settings[smtp_port]" class="form-control" value="<?= htmlspecialchars($data['settings']['smtp_port'] ?? '') ?>" placeholder="587">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Security" data-ar="التشفير">Security</label>
|
||||
<select name="settings[smtp_secure]" class="form-select">
|
||||
<?php $smtpSecure = $data['settings']['smtp_secure'] ?? 'tls'; ?>
|
||||
<option value="tls" <?= $smtpSecure === 'tls' ? 'selected' : '' ?>>TLS</option>
|
||||
<option value="ssl" <?= $smtpSecure === 'ssl' ? 'selected' : '' ?>>SSL</option>
|
||||
<option value="none" <?= $smtpSecure === 'none' ? 'selected' : '' ?>>None</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Username" data-ar="اسم المستخدم">Username</label>
|
||||
<input type="text" name="settings[smtp_user]" class="form-control" value="<?= htmlspecialchars($data['settings']['smtp_user'] ?? '') ?>" placeholder="mailer@example.com">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Password" data-ar="كلمة المرور">Password</label>
|
||||
<input type="password" name="settings[smtp_pass]" class="form-control" value="<?= htmlspecialchars($data['settings']['smtp_pass'] ?? '') ?>" autocomplete="new-password" placeholder="SMTP password">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="From Email" data-ar="بريد الإرسال">From Email</label>
|
||||
<input type="email" name="settings[smtp_from_email]" class="form-control" value="<?= htmlspecialchars($data['settings']['smtp_from_email'] ?? '') ?>" placeholder="no-reply@example.com">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="From Name" data-ar="اسم المرسل">From Name</label>
|
||||
<input type="text" name="settings[smtp_from_name]" class="form-control" value="<?= htmlspecialchars($data['settings']['smtp_from_name'] ?? '') ?>" placeholder="Your Company">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Reply-To Email" data-ar="الرد إلى">Reply-To Email</label>
|
||||
<input type="email" name="settings[smtp_reply_to]" class="form-control" value="<?= htmlspecialchars($data['settings']['smtp_reply_to'] ?? '') ?>" placeholder="support@example.com">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card border-0 shadow-sm h-100 rounded-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="rounded-circle bg-success bg-opacity-10 text-success p-2"><i class="bi bi-whatsapp"></i></div>
|
||||
<div>
|
||||
<h2 class="h6 fw-bold mb-0" data-en="Wablas WhatsApp Gateway" data-ar="بوابة واتساب Wablas">Wablas WhatsApp Gateway</h2>
|
||||
<p class="text-muted small mb-0" data-en="Keep the WhatsApp channel profile ready for alerts and automations" data-ar="جهّز ملف قناة واتساب للتنبيهات والأتمتة">Keep the WhatsApp channel profile ready for alerts and automations</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge rounded-pill <?= ($data['settings']['wablas_enabled'] ?? '0') === '1' ? 'bg-success bg-opacity-10 text-success' : 'text-bg-light border' ?> px-3 py-2">
|
||||
<?= ($data['settings']['wablas_enabled'] ?? '0') === '1' ? 'Enabled' : 'Disabled' ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Status" data-ar="الحالة">Status</label>
|
||||
<select name="settings[wablas_enabled]" class="form-select">
|
||||
<option value="0" <?= ($data['settings']['wablas_enabled'] ?? '0') === '0' ? 'selected' : '' ?>>Disabled</option>
|
||||
<option value="1" <?= ($data['settings']['wablas_enabled'] ?? '0') === '1' ? 'selected' : '' ?>>Enabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="API URL" data-ar="رابط API">API URL</label>
|
||||
<input type="url" name="settings[wablas_api_url]" class="form-control" value="<?= htmlspecialchars($data['settings']['wablas_api_url'] ?? '') ?>" placeholder="https://your-wablas-endpoint">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="API Token" data-ar="رمز API">API Token</label>
|
||||
<input type="password" name="settings[wablas_token]" class="form-control" value="<?= htmlspecialchars($data['settings']['wablas_token'] ?? '') ?>" autocomplete="new-password" placeholder="Wablas access token">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Security Key" data-ar="مفتاح الأمان">Security Key</label>
|
||||
<input type="password" name="settings[wablas_security_key]" class="form-control" value="<?= htmlspecialchars($data['settings']['wablas_security_key'] ?? '') ?>" autocomplete="new-password" placeholder="Wablas security key">
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Sender / Device / Phone" data-ar="المرسل / الجهاز / الهاتف">Sender / Device / Phone</label>
|
||||
<input type="text" name="settings[wablas_sender]" class="form-control" value="<?= htmlspecialchars($data['settings']['wablas_sender'] ?? '') ?>" placeholder="Instance, device, or WhatsApp number">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Default Country Code" data-ar="رمز الدولة الافتراضي">Default Country Code</label>
|
||||
<input type="text" name="settings[wablas_default_country_code]" class="form-control" value="<?= htmlspecialchars($data['settings']['wablas_default_country_code'] ?? '') ?>" placeholder="+968">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-text" data-en="Use the API URL, token, and security key from your Wablas dashboard. This profile will be ready for WhatsApp notifications and future automation hooks." data-ar="استخدم رابط API والرمز ومفتاح الأمان من لوحة تحكم Wablas. سيكون هذا الملف جاهزًا لإشعارات واتساب وخطافات الأتمتة المستقبلية.">Use the API URL, token, and security key from your Wablas dashboard. This profile will be ready for WhatsApp notifications and future automation hooks.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm rounded-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-4">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="rounded-circle bg-dark bg-opacity-10 text-dark p-2"><i class="bi bi-chat-dots"></i></div>
|
||||
<div>
|
||||
<h2 class="h6 fw-bold mb-0" data-en="WhatsApp Templates & Schedule" data-ar="قوالب واتساب والجدولة">WhatsApp Templates & Schedule</h2>
|
||||
<p class="text-muted small mb-0" data-en="Prepare invoice and daily summary messages with fixed recipients and a saved send time" data-ar="جهّز رسائل الفاتورة والملخص اليومي مع مستلمين ثابتين ووقت إرسال محفوظ">Prepare invoice and daily summary messages with fixed recipients and a saved send time</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge rounded-pill text-bg-light border px-3 py-2">Ready for automation</span>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="border rounded-4 p-4 h-100 bg-light bg-opacity-50">
|
||||
<div class="d-flex align-items-start justify-content-between gap-2 mb-3">
|
||||
<div>
|
||||
<h3 class="h6 fw-bold mb-1" data-en="Invoice Message" data-ar="رسالة الفاتورة">Invoice Message</h3>
|
||||
<p class="text-muted small mb-0" data-en="Choose who receives invoice WhatsApp messages and keep a reusable template" data-ar="اختر من يستلم رسائل الفاتورة عبر واتساب واحتفظ بقالب قابل لإعادة الاستخدام">Choose who receives invoice WhatsApp messages and keep a reusable template</p>
|
||||
</div>
|
||||
<span class="badge rounded-pill <?= ($data['settings']['wablas_invoice_enabled'] ?? '0') === '1' ? 'bg-success bg-opacity-10 text-success' : 'text-bg-light border' ?> px-3 py-2">
|
||||
<?= ($data['settings']['wablas_invoice_enabled'] ?? '0') === '1' ? 'Enabled' : 'Disabled' ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Status" data-ar="الحالة">Status</label>
|
||||
<select name="settings[wablas_invoice_enabled]" class="form-select">
|
||||
<option value="0" <?= ($data['settings']['wablas_invoice_enabled'] ?? '0') === '0' ? 'selected' : '' ?>>Disabled</option>
|
||||
<option value="1" <?= ($data['settings']['wablas_invoice_enabled'] ?? '0') === '1' ? 'selected' : '' ?>>Enabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Preferred Send Time" data-ar="وقت الإرسال المفضل">Preferred Send Time</label>
|
||||
<input type="time" name="settings[wablas_invoice_time]" class="form-control" value="<?= htmlspecialchars($data['settings']['wablas_invoice_time'] ?? '') ?>">
|
||||
<div class="form-text" data-en="Optional. Leave blank if invoices should go out immediately when the invoice workflow is triggered." data-ar="اختياري. اتركه فارغًا إذا كان يجب إرسال الفواتير فور تشغيل سير عمل الفاتورة.">Optional. Leave blank if invoices should go out immediately when the invoice workflow is triggered.</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Recipient Numbers" data-ar="أرقام المستلمين">Recipient Numbers</label>
|
||||
<textarea name="settings[wablas_invoice_numbers]" class="form-control" rows="3" placeholder="+96890000000 +96891111111"><?= htmlspecialchars($data['settings']['wablas_invoice_numbers'] ?? '') ?></textarea>
|
||||
<div class="form-text" data-en="Use one number per line or separate numbers with commas. Include the country code." data-ar="استخدم رقمًا واحدًا في كل سطر أو افصل الأرقام بفواصل. ضمّن رمز الدولة.">Use one number per line or separate numbers with commas. Include the country code.</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Invoice Template" data-ar="قالب الفاتورة">Invoice Template</label>
|
||||
<textarea name="settings[wablas_invoice_template]" class="form-control" rows="6" placeholder="Hello {customer_name}, your invoice {invoice_no} total is {grand_total}. View: {invoice_url}"><?= htmlspecialchars($data['settings']['wablas_invoice_template'] ?? '') ?></textarea>
|
||||
<div class="form-text" data-en="Placeholders: {invoice_no}, {customer_name}, {grand_total}, {invoice_url}, {company_name}" data-ar="المتغيرات: {invoice_no} و {customer_name} و {grand_total} و {invoice_url} و {company_name}">Placeholders: <code>{invoice_no}</code> <code>{customer_name}</code> <code>{grand_total}</code> <code>{invoice_url}</code> <code>{company_name}</code></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="border rounded-4 p-4 h-100 bg-light bg-opacity-50">
|
||||
<div class="d-flex align-items-start justify-content-between gap-2 mb-3">
|
||||
<div>
|
||||
<h3 class="h6 fw-bold mb-1" data-en="Daily Summary Report" data-ar="تقرير الملخص اليومي">Daily Summary Report</h3>
|
||||
<p class="text-muted small mb-0" data-en="Store the WhatsApp recap template, target numbers, and preferred report time" data-ar="احفظ قالب ملخص واتساب والأرقام المستهدفة ووقت التقرير المفضل">Store the WhatsApp recap template, target numbers, and preferred report time</p>
|
||||
</div>
|
||||
<span class="badge rounded-pill <?= ($data['settings']['wablas_daily_summary_enabled'] ?? '0') === '1' ? 'bg-success bg-opacity-10 text-success' : 'text-bg-light border' ?> px-3 py-2">
|
||||
<?= ($data['settings']['wablas_daily_summary_enabled'] ?? '0') === '1' ? 'Enabled' : 'Disabled' ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Status" data-ar="الحالة">Status</label>
|
||||
<select name="settings[wablas_daily_summary_enabled]" class="form-select">
|
||||
<option value="0" <?= ($data['settings']['wablas_daily_summary_enabled'] ?? '0') === '0' ? 'selected' : '' ?>>Disabled</option>
|
||||
<option value="1" <?= ($data['settings']['wablas_daily_summary_enabled'] ?? '0') === '1' ? 'selected' : '' ?>>Enabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Send Time" data-ar="وقت الإرسال">Send Time</label>
|
||||
<input type="time" name="settings[wablas_daily_summary_time]" class="form-control" value="<?= htmlspecialchars($data['settings']['wablas_daily_summary_time'] ?? '20:00') ?>">
|
||||
<div class="form-text" data-en="Set the daily report delivery time. Default is 20:00 if left empty." data-ar="حدد وقت إرسال التقرير اليومي. الافتراضي هو 20:00 إذا تُرك الحقل فارغًا.">Set the daily report delivery time. Default is 20:00 if left empty.</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Recipient Numbers" data-ar="أرقام المستلمين">Recipient Numbers</label>
|
||||
<textarea name="settings[wablas_daily_summary_numbers]" class="form-control" rows="3" placeholder="+96890000000 +96891111111"><?= htmlspecialchars($data['settings']['wablas_daily_summary_numbers'] ?? '') ?></textarea>
|
||||
<div class="form-text" data-en="Use one number per line or separate numbers with commas. Include the country code." data-ar="استخدم رقمًا واحدًا في كل سطر أو افصل الأرقام بفواصل. ضمّن رمز الدولة.">Use one number per line or separate numbers with commas. Include the country code.</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Daily Summary Template" data-ar="قالب الملخص اليومي">Daily Summary Template</label>
|
||||
<textarea name="settings[wablas_daily_summary_template]" class="form-control" rows="6" placeholder="Daily summary {report_date}: invoices {invoice_count}, sales {sales_total}, cash {cash_total}, card {card_total}"><?= htmlspecialchars($data['settings']['wablas_daily_summary_template'] ?? '') ?></textarea>
|
||||
<div class="form-text" data-en="Placeholders: {report_date}, {invoice_count}, {sales_total}, {cash_total}, {card_total}, {company_name}" data-ar="المتغيرات: {report_date} و {invoice_count} و {sales_total} و {cash_total} و {card_total} و {company_name}">Placeholders: <code>{report_date}</code> <code>{invoice_count}</code> <code>{sales_total}</code> <code>{cash_total}</code> <code>{card_total}</code> <code>{company_name}</code></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-light border rounded-4 mt-4 mb-0 small">
|
||||
<div class="fw-semibold mb-1" data-en="Scheduling note" data-ar="ملاحظة الجدولة">Scheduling note</div>
|
||||
<div class="text-muted mb-0" data-en="These fields now store the Wablas connection profile, recipients, templates, and preferred times. If you want, I can wire the actual send action / cron job next so the invoice and daily summary messages are dispatched automatically." data-ar="تحفظ هذه الحقول الآن ملف اتصال Wablas والمستلمين والقوالب والأوقات المفضلة. إذا رغبت، يمكنني توصيل إجراء الإرسال الفعلي / مهمة cron بعد ذلك لإرسال رسائل الفاتورة والملخص اليومي تلقائيًا.">These fields now store the Wablas connection profile, recipients, templates, and preferred times. If you want, I can wire the actual send action / cron job next so the invoice and daily summary messages are dispatched automatically.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column flex-md-row justify-content-end gap-2 pt-4 mt-4 border-top">
|
||||
<button type="submit" name="update_settings" class="btn btn-primary btn-lg rounded-pill px-5 shadow-sm align-self-md-end">
|
||||
<i class="bi bi-check-lg me-2"></i>
|
||||
<span data-en="Save All Changes" data-ar="حفظ جميع التغييرات">Save All Changes</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var nameInput = document.getElementById('license-app-name');
|
||||
var slugInput = document.getElementById('license-app-slug');
|
||||
var suggestButton = document.getElementById('suggest-license-slug');
|
||||
var activeTabInput = document.getElementById('settings-active-tab');
|
||||
var tabButtons = document.querySelectorAll('#settingsTabs [data-bs-toggle="tab"]');
|
||||
|
||||
if (nameInput && slugInput && suggestButton) {
|
||||
var slugWasExplicit = slugInput.dataset.explicit === '1';
|
||||
var slugify = function (value) {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
};
|
||||
|
||||
nameInput.addEventListener('input', function () {
|
||||
if (!slugWasExplicit || slugInput.value.trim() === '') {
|
||||
slugInput.value = slugify(nameInput.value);
|
||||
}
|
||||
});
|
||||
|
||||
slugInput.addEventListener('input', function () {
|
||||
slugWasExplicit = slugInput.value.trim() !== '';
|
||||
slugInput.dataset.explicit = slugWasExplicit ? '1' : '0';
|
||||
});
|
||||
|
||||
suggestButton.addEventListener('click', function () {
|
||||
slugInput.value = slugify(nameInput.value);
|
||||
slugWasExplicit = slugInput.value.trim() !== '';
|
||||
slugInput.dataset.explicit = slugWasExplicit ? '1' : '0';
|
||||
slugInput.focus();
|
||||
});
|
||||
}
|
||||
|
||||
tabButtons.forEach(function (button) {
|
||||
button.addEventListener('shown.bs.tab', function (event) {
|
||||
var tabKey = event.target.getAttribute('data-tab-key') || 'company';
|
||||
if (activeTabInput) {
|
||||
activeTabInput.value = tabKey;
|
||||
}
|
||||
try {
|
||||
var currentUrl = new URL(window.location.href);
|
||||
currentUrl.searchParams.set('tab', tabKey);
|
||||
window.history.replaceState({}, '', currentUrl.toString());
|
||||
} catch (error) {
|
||||
// Ignore URL update errors and keep the tab interaction working.
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var activeButton = document.querySelector('#settingsTabs .nav-link.active');
|
||||
if (activeButton && activeTabInput) {
|
||||
activeTabInput.value = activeButton.getAttribute('data-tab-key') || 'company';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
5
payment_methods.php
Normal file
5
payment_methods.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'payment_methods');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
pos.php
Normal file
5
pos.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'pos');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
purchase_returns.php
Normal file
5
purchase_returns.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'purchase_returns');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
purchases.php
Normal file
5
purchases.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'purchases');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
quotations.php
Normal file
5
quotations.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'quotations');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
register_sessions.php
Normal file
5
register_sessions.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'register_sessions');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
role_groups.php
Normal file
5
role_groups.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'role_groups');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
sales.php
Normal file
5
sales.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'sales');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
sales_returns.php
Normal file
5
sales_returns.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'sales_returns');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
scale_devices.php
Normal file
5
scale_devices.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'scale_devices');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
session_cookie.txt
Normal file
5
session_cookie.txt
Normal file
@ -0,0 +1,5 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
127.0.0.1 FALSE / FALSE 0 PHPSESSID 57sm980t3j7up8jglofb3bqhml
|
||||
5
settings.php
Normal file
5
settings.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'settings');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
supplier_statement.php
Normal file
5
supplier_statement.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'supplier_statement');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
suppliers.php
Normal file
5
suppliers.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'suppliers');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
5
units.php
Normal file
5
units.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('APP_PAGE', 'units');
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
Loading…
x
Reference in New Issue
Block a user