splitting pages and final touch
This commit is contained in:
parent
b89863b4d3
commit
c53e944d52
@ -28,6 +28,53 @@ class LocalAIApi
|
|||||||
/** @var array<string,mixed>|null */
|
/** @var array<string,mixed>|null */
|
||||||
private static ?array $configCache = null;
|
private static ?array $configCache = null;
|
||||||
|
|
||||||
|
private static function debugFileLoggingEnabled(): bool
|
||||||
|
{
|
||||||
|
static $enabled = null;
|
||||||
|
if ($enabled !== null) {
|
||||||
|
return $enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidates = [
|
||||||
|
getenv('APP_FILE_DEBUG_LOGS'),
|
||||||
|
$_ENV['APP_FILE_DEBUG_LOGS'] ?? null,
|
||||||
|
$_SERVER['APP_FILE_DEBUG_LOGS'] ?? null,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($candidates as $candidate) {
|
||||||
|
if ($candidate === false || $candidate === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = strtolower(trim((string) $candidate));
|
||||||
|
if ($value === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($value, ['1', 'true', 'yes', 'on'], true)) {
|
||||||
|
$enabled = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($value, ['0', 'false', 'no', 'off'], true)) {
|
||||||
|
$enabled = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$enabled = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function debugFileLog(string $message): void
|
||||||
|
{
|
||||||
|
if (!self::debugFileLoggingEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@file_put_contents(__DIR__ . '/../debug.log', rtrim($message, "\r\n") . PHP_EOL, FILE_APPEND);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signature compatible with the OpenAI Responses API.
|
* Signature compatible with the OpenAI Responses API.
|
||||||
*
|
*
|
||||||
@ -107,8 +154,7 @@ class LocalAIApi
|
|||||||
|
|
||||||
$projectUuid = $cfg['project_uuid'];
|
$projectUuid = $cfg['project_uuid'];
|
||||||
|
|
||||||
// DEBUG LOGGING
|
self::debugFileLog(date('Y-m-d H:i:s') . " - Requesting AI. UUID: [" . ($projectUuid ?? 'NULL') . "] CFG: " . json_encode($cfg));
|
||||||
file_put_contents(__DIR__ . '/../debug.log', date('Y-m-d H:i:s') . " - Requesting AI. UUID: [" . ($projectUuid ?? 'NULL') . "] CFG: " . json_encode($cfg) . "\n", FILE_APPEND);
|
|
||||||
|
|
||||||
if (empty($projectUuid)) {
|
if (empty($projectUuid)) {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
2026-02-21 18:35:57 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"project-uuid","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
# 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 b6ihps49oas08569v0vg5i1hnb
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
# 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 2uea9bvpufvp1th7vnnvcog2as
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
# 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 f3c2q95r0m6iaptq2skotkpemo
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
# 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 il48dnd8duujsd9cbumbfbum85
|
|
||||||
138
debug.log
138
debug.log
@ -1,138 +0,0 @@
|
|||||||
[INFO] AI agent editing: index.php
|
|
||||||
2026-02-26 08:18:16 - Items case hit
|
|
||||||
2026-03-01 13:00:29 - Items case hit
|
|
||||||
2026-03-01 13:17:12 - Items case hit
|
|
||||||
2026-03-01 18:24:40 - Items case hit
|
|
||||||
2026-03-17 09:58:59 - Items case hit
|
|
||||||
2026-03-17 10:04:31 - Items case hit
|
|
||||||
2026-03-17 10:12:20 - Items case hit
|
|
||||||
2026-03-17 17:26:10 - Items case hit
|
|
||||||
2026-03-17 18:04:40 - Items case hit
|
|
||||||
2026-03-17 19:06:45 - Items case hit
|
|
||||||
2026-03-17 19:07:15 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-17 19:07:52 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-17 19:08:16 - Items case hit
|
|
||||||
2026-03-17 19:08:47 - Items case hit
|
|
||||||
2026-03-17 21:28:08 - Items case hit
|
|
||||||
2026-03-17 21:37:02 - Items case hit
|
|
||||||
2026-03-17 21:46:30 - Items case hit
|
|
||||||
2026-03-17 21:47:03 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-17 21:47:17 - Items case hit
|
|
||||||
2026-03-17 21:47:27 - Items case hit
|
|
||||||
2026-03-17 22:30:34 - Items case hit
|
|
||||||
2026-03-17 22:30:54 - Items case hit
|
|
||||||
2026-03-17 22:31:12 - Items case hit
|
|
||||||
2026-03-17 22:31:17 - Items case hit
|
|
||||||
2026-03-17 22:36:20 - Items case hit
|
|
||||||
2026-03-17 22:40:51 - Items case hit
|
|
||||||
2026-03-17 18:45:47 - Items case hit
|
|
||||||
2026-03-17 18:47:55 - Items case hit
|
|
||||||
2026-03-17 18:51:56 - Items case hit
|
|
||||||
2026-03-17 18:54:27 - Items case hit
|
|
||||||
2026-03-18 01:51:29 - Items case hit
|
|
||||||
2026-03-18 01:51:45 - Items case hit
|
|
||||||
2026-03-18 02:00:48 - Items case hit
|
|
||||||
2026-03-18 02:02:37 - Items case hit
|
|
||||||
2026-03-18 02:03:03 - Items case hit
|
|
||||||
2026-03-18 02:05:55 - Items case hit
|
|
||||||
2026-03-18 02:06:44 - Items case hit
|
|
||||||
2026-03-18 02:12:27 - Items case hit
|
|
||||||
2026-03-18 02:14:19 - Items case hit
|
|
||||||
2026-03-18 02:15:13 - Items case hit
|
|
||||||
2026-03-18 02:15:31 - Items case hit
|
|
||||||
2026-03-18 02:15:56 - Items case hit
|
|
||||||
2026-03-18 02:16:33 - Items case hit
|
|
||||||
2026-03-18 02:17:32 - Items case hit
|
|
||||||
2026-03-18 02:17:48 - Items case hit
|
|
||||||
2026-03-18 02:18:21 - Items case hit
|
|
||||||
2026-03-18 02:19:01 - Items case hit
|
|
||||||
2026-03-18 02:19:49 - Items case hit
|
|
||||||
2026-03-18 02:20:06 - Items case hit
|
|
||||||
2026-03-18 02:20:47 - Items case hit
|
|
||||||
2026-03-18 02:21:53 - Items case hit
|
|
||||||
2026-03-18 02:24:31 - Items case hit
|
|
||||||
2026-03-18 02:26:44 - Items case hit
|
|
||||||
2026-03-18 02:28:48 - Items case hit
|
|
||||||
2026-03-18 02:28:58 - Items case hit
|
|
||||||
2026-03-18 06:00:05 - Items case hit
|
|
||||||
2026-03-18 14:33:50 - Items case hit
|
|
||||||
2026-03-18 14:34:10 - Items case hit
|
|
||||||
2026-03-18 17:01:37 - Items case hit
|
|
||||||
2026-03-18 17:07:17 - Items case hit
|
|
||||||
2026-03-18 17:09:46 - Items case hit
|
|
||||||
2026-03-18 17:35:42 - Items case hit
|
|
||||||
2026-03-18 17:36:53 - Items case hit
|
|
||||||
2026-03-18 17:49:12 - Items case hit
|
|
||||||
2026-03-18 18:07:03 - Items case hit
|
|
||||||
2026-03-18 18:08:41 - Items case hit
|
|
||||||
2026-03-18 22:12:51 - Items case hit
|
|
||||||
2026-03-18 22:15:09 - Items case hit
|
|
||||||
2026-03-18 22:24:28 - Items case hit
|
|
||||||
2026-03-18 22:25:14 - Items case hit
|
|
||||||
2026-03-18 22:25:43 - Items case hit
|
|
||||||
2026-03-18 22:26:38 - Items case hit
|
|
||||||
2026-03-18 22:29:27 - Items case hit
|
|
||||||
2026-03-18 22:29:45 - Items case hit
|
|
||||||
2026-03-18 22:30:17 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-18 22:30:49 - Items case hit
|
|
||||||
2026-03-18 22:31:04 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-18 22:31:16 - Items case hit
|
|
||||||
2026-03-18 22:31:26 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-18 22:31:35 - Items case hit
|
|
||||||
2026-03-18 22:31:54 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-18 22:32:01 - Items case hit
|
|
||||||
2026-03-18 22:32:15 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-18 22:32:28 - Items case hit
|
|
||||||
2026-03-18 22:33:07 - Items case hit
|
|
||||||
2026-03-18 22:40:07 - Items case hit
|
|
||||||
2026-03-18 22:40:13 - Items case hit
|
|
||||||
2026-03-18 22:40:18 - Items case hit
|
|
||||||
2026-03-18 22:40:29 - Items case hit
|
|
||||||
2026-03-18 22:40:46 - Items case hit
|
|
||||||
2026-03-18 22:41:01 - Items case hit
|
|
||||||
2026-03-18 22:41:16 - Items case hit
|
|
||||||
2026-03-18 23:02:18 - Items case hit
|
|
||||||
2026-03-18 23:03:06 - Items case hit
|
|
||||||
2026-03-18 23:03:19 - Items case hit
|
|
||||||
2026-03-18 23:05:39 - Items case hit
|
|
||||||
2026-03-18 23:06:02 - Items case hit
|
|
||||||
2026-03-19 05:57:24 - Items case hit
|
|
||||||
2026-03-19 05:57:45 - Items case hit
|
|
||||||
2026-03-19 05:58:17 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-19 05:58:34 - Items case hit
|
|
||||||
2026-03-19 05:58:51 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-19 05:59:01 - Items case hit
|
|
||||||
2026-03-19 06:00:05 - Items case hit
|
|
||||||
2026-03-19 18:14:48 - Items case hit
|
|
||||||
2026-03-19 18:15:00 - Items case hit
|
|
||||||
2026-03-19 18:15:13 - Items case hit
|
|
||||||
2026-03-19 21:18:34 - Items case hit
|
|
||||||
2026-03-19 21:18:52 - Items case hit
|
|
||||||
2026-03-20 04:58:13 - Test write from web server
|
|
||||||
2026-03-20 09:01:08 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-20 09:01:47 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-20 21:46:17 - Items case hit
|
|
||||||
2026-03-20 21:46:35 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-20 21:46:43 - Items case hit
|
|
||||||
2026-03-20 21:46:56 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-20 21:47:03 - Items case hit
|
|
||||||
2026-03-20 21:47:15 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-20 21:47:23 - Items case hit
|
|
||||||
2026-03-20 21:56:20 - Items case hit
|
|
||||||
2026-03-20 21:56:30 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-20 21:56:39 - Items case hit
|
|
||||||
2026-03-20 21:56:47 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-03-20 21:56:51 - Items case hit
|
|
||||||
2026-05-02 17:20:57 - Items case hit
|
|
||||||
2026-05-02 17:23:36 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-05-02 17:29:22 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-05-02 17:29:31 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-05-02 17:29:39 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-05-02 17:29:45 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-05-02 17:32:00 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-05-02 17:32:02 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-05-02 17:32:10 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-05-02 17:32:11 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-05-02 17:32:54 - Requesting AI. UUID: [e1f9b5b3-fcef-4c8d-87d2-8630b1f72491] CFG: {"base_url":"https:\/\/flatlogic.com","responses_path":"\/projects\/38471\/ai-request","project_id":"38471","project_uuid":"e1f9b5b3-fcef-4c8d-87d2-8630b1f72491","project_header":"Project-UUID","default_model":"gpt-4o-mini","timeout":30,"verify_tls":true}
|
|
||||||
2026-05-02 18:41:44 - Items case hit
|
|
||||||
2026-05-03 01:36:24 - Items case hit
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'db/config.php';
|
|
||||||
require_once 'includes/accounting_helper.php';
|
|
||||||
|
|
||||||
try {
|
|
||||||
echo "Checking acc_accounts table...\n";
|
|
||||||
$accounts = db()->query("SELECT * FROM acc_accounts LIMIT 5")->fetchAll();
|
|
||||||
echo "Found " . count($accounts) . " accounts.\n";
|
|
||||||
print_r($accounts);
|
|
||||||
|
|
||||||
echo "\nTesting getAccountBalance('1100')...\n";
|
|
||||||
$balance = getAccountBalance('1100');
|
|
||||||
echo "Balance for 1100: " . $balance . "\n";
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo "Error: " . $e->getMessage() . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'db/config.php';
|
|
||||||
$whereSql = "1=1";
|
|
||||||
$params = [];
|
|
||||||
$stmt = db()->prepare("SELECT i.*, c.name_en as cat_en, c.name_ar as cat_ar, u.short_name_en as unit_en, u.short_name_ar as unit_ar, s.name as supplier_name
|
|
||||||
FROM stock_items i
|
|
||||||
LEFT JOIN stock_categories c ON i.category_id = c.id
|
|
||||||
LEFT JOIN stock_units u ON i.unit_id = u.id
|
|
||||||
LEFT JOIN suppliers s ON i.supplier_id = s.id
|
|
||||||
WHERE $whereSql
|
|
||||||
ORDER BY i.id DESC");
|
|
||||||
$stmt->execute($params);
|
|
||||||
$items = $stmt->fetchAll();
|
|
||||||
echo "Count: " . count($items) . "\n";
|
|
||||||
foreach ($items as $item) {
|
|
||||||
echo "ID: {$item['id']}, Name: {$item['name_en']}\n";
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
<?php
|
|
||||||
$file = 'index.php';
|
|
||||||
$content = file_get_contents($file);
|
|
||||||
|
|
||||||
if ($content === false) {
|
|
||||||
die("Failed to read index.php");
|
|
||||||
}
|
|
||||||
|
|
||||||
$search = <<<'EOD'
|
|
||||||
case 'accounting':
|
|
||||||
$data['journal_entries'] = db()->query("SELECT je.*,
|
|
||||||
(SELECT SUM(debit) FROM acc_ledger WHERE journal_entry_id = je.id) as total_debit
|
|
||||||
FROM acc_journal_entries je
|
|
||||||
ORDER BY je.entry_date DESC, je.id DESC LIMIT 100")->fetchAll();
|
|
||||||
EOD;
|
|
||||||
|
|
||||||
$replace = <<<'EOD'
|
|
||||||
case 'accounting':
|
|
||||||
// Pagination for Journal Entries
|
|
||||||
$currentPage = isset($_GET['p']) ? max(1, (int)$_GET['p']) : 1;
|
|
||||||
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 20;
|
|
||||||
$offset = ($currentPage - 1) * $limit;
|
|
||||||
|
|
||||||
$total_entries = db()->query("SELECT COUNT(*) FROM acc_journal_entries")->fetchColumn();
|
|
||||||
$data['total_pages'] = ceil($total_entries / $limit);
|
|
||||||
$data['current_page'] = $currentPage;
|
|
||||||
|
|
||||||
$data['journal_entries'] = db()->query("SELECT je.*,
|
|
||||||
(SELECT SUM(debit) FROM acc_ledger WHERE journal_entry_id = je.id) as total_debit
|
|
||||||
FROM acc_journal_entries je
|
|
||||||
ORDER BY je.entry_date DESC, je.id DESC LIMIT $limit OFFSET $offset")->fetchAll();
|
|
||||||
EOD;
|
|
||||||
|
|
||||||
// Normalize line endings
|
|
||||||
$content = str_replace("\r\n", "\n", $content);
|
|
||||||
$search = str_replace("\r\n", "\n", $search);
|
|
||||||
$replace = str_replace("\r\n", "\n", $replace);
|
|
||||||
|
|
||||||
if (strpos($content, $search) !== false) {
|
|
||||||
$newContent = str_replace($search, $replace, $content);
|
|
||||||
file_put_contents($file, $newContent);
|
|
||||||
echo "Successfully patched index.php\n";
|
|
||||||
} else {
|
|
||||||
echo "Could not find the code block to replace.\n";
|
|
||||||
// Debug: print a small chunk around where we expect it
|
|
||||||
$pos = strpos($content, "case 'accounting':");
|
|
||||||
if ($pos !== false) {
|
|
||||||
echo "Found case 'accounting': at position $pos. Content around it:\n";
|
|
||||||
echo substr($content, $pos, 500) . "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
$content = file_get_contents('index.php');
|
|
||||||
|
|
||||||
$search1 = '<div class="mb-3">
|
|
||||||
<label class="form-label fw-semibold" data-en="Assigned Outlet" data-ar="الفرع">Assigned Outlet</label>
|
|
||||||
<select name="outlet_id" class="form-select">
|
|
||||||
<option value="">--- Global (Admin only) ---</option>
|
|
||||||
<?php foreach (($data[\'outlets\'] ?? []) as $o): ?>
|
|
||||||
<option value="<?= $o[\'id\'] ?>" <?= ($u[\'outlet_id\'] ?? null) == $o[\'id\'] ? \'selected\' : \'\' ?>><?= htmlspecialchars($o[\'name\']) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>';
|
|
||||||
|
|
||||||
$content = str_replace($search1 . "\n " . $search1, $search1, $content);
|
|
||||||
|
|
||||||
|
|
||||||
$search2 = '<div class="mb-3">
|
|
||||||
<label class="form-label" data-en="Assigned Outlet" data-ar="الفرع">Assigned Outlet</label>
|
|
||||||
<select name="outlet_id" class="form-select">
|
|
||||||
<option value="">--- Global (Admin only) ---</option>
|
|
||||||
<?php foreach (($data[\'outlets\'] ?? []) as $o): ?>
|
|
||||||
<option value="<?= $o[\'id\'] ?>"><?= htmlspecialchars($o[\'name\']) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>';
|
|
||||||
|
|
||||||
$content = str_replace($search2 . "\n " . $search2, $search2, $content);
|
|
||||||
|
|
||||||
file_put_contents('index.php', $content);
|
|
||||||
echo "Duplicates removed.\n";
|
|
||||||
149
fix_forms.py
149
fix_forms.py
@ -1,149 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
file_path = 'index.php'
|
|
||||||
|
|
||||||
with open(file_path, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# 1. Replace <div class="row g-3"> with <div class="form-grid-3">
|
|
||||||
# We want to be careful. The user specifically mentioned "Items edit form" and "Add new items form".
|
|
||||||
# And "Apply the style to all forms in app".
|
|
||||||
# "row g-3" is the standard Bootstrap form layout class. Replacing it with "form-grid-3" (which I defined)
|
|
||||||
# should be safe for forms.
|
|
||||||
# However, "row g-3" might be used for non-form layouts (e.g. stats cards).
|
|
||||||
# I should look for inputs inside.
|
|
||||||
# But checking content inside is hard with regex.
|
|
||||||
|
|
||||||
# Let's count how many "row g-3" there are.
|
|
||||||
matches = len(re.findall(r'class=["\']row g-3["\']', content))
|
|
||||||
print(f"Found {matches} instances of 'row g-3'.")
|
|
||||||
|
|
||||||
# Strategy:
|
|
||||||
# Replace 'class="row g-3"' with 'class="form-grid-3"' IF it's inside a form or modal context.
|
|
||||||
# Or just replace all? "row g-3" implies a grid with gap 3 (1rem).
|
|
||||||
# My "form-grid-3" is a grid with gap 1rem and 3 columns.
|
|
||||||
# If "row g-3" was used for 2 columns (col-6) or 4 columns (col-3), forcing 3 columns might break layout.
|
|
||||||
# BUT the user said "Apply the style to all forms".
|
|
||||||
# Most "row g-3" in this app seem to be forms (based on previous grep).
|
|
||||||
# Non-form grids usually use "row" without "g-3" or with different gap?
|
|
||||||
# Let's assume "row g-3" is the form standard here.
|
|
||||||
|
|
||||||
# I'll define a function to replace specific occurrences if I can identify them.
|
|
||||||
# The "Edit Item" modal is around line 4964.
|
|
||||||
# The "Add Item" modal is around line 10141.
|
|
||||||
|
|
||||||
# Regex to find <div class="row g-3"> and replace it, but maybe verify context?
|
|
||||||
# I'll just do a global replace for now, but I will backup first.
|
|
||||||
# Wait, "row g-3" in a dashboard stats widget (e.g. 4 cards) would become 3 columns.
|
|
||||||
# That might be annoying.
|
|
||||||
# I should inspect if there are stats widgets using "row g-3".
|
|
||||||
|
|
||||||
# Let's look for "col-md-3" or "col-xl-3" inside "row g-3".
|
|
||||||
# If I see "col-xl-3", it's likely a 4-column layout (12/3 = 4).
|
|
||||||
# My "form-grid-3" forces 3 columns.
|
|
||||||
# So replacing it would break 4-column layouts.
|
|
||||||
# "Edit Item" uses "col-md-3", "col-md-4", "col-md-6".
|
|
||||||
# If I change the container to grid-3, the children become grid items.
|
|
||||||
# I added CSS to make children width 100%.
|
|
||||||
|
|
||||||
# To be safer, I will only replace "row g-3" if it contains "form-control" or "form-select" or "form-label" inside it (heuristic).
|
|
||||||
# This is tricky with regex.
|
|
||||||
|
|
||||||
# Alternative: Find the specific blocks for Items and change them.
|
|
||||||
# Then find other *forms*.
|
|
||||||
|
|
||||||
# Let's use a simpler approach.
|
|
||||||
# I will search for the specific lines for Edit Item and Add Item and change them.
|
|
||||||
# Then I will search for other obvious forms.
|
|
||||||
|
|
||||||
new_content = content
|
|
||||||
|
|
||||||
# 1. Edit Item Modal (around line 4975)
|
|
||||||
# Context: <div class="modal fade" id="editItemModal...
|
|
||||||
# ... <form ...> ... <div class="modal-body"> ... <div class="row g-3">
|
|
||||||
# I'll use regex to match the structure.
|
|
||||||
|
|
||||||
# Pattern for Edit/Add Item Modals (and potentially others)
|
|
||||||
# Matches <div class="row g-3"> inside a form or modal body?
|
|
||||||
# Actually, the user wants "items edit form" specifically.
|
|
||||||
|
|
||||||
# I'll replace specifically in the Item Modals first.
|
|
||||||
# "Edit Item" has id="editItemModal..."
|
|
||||||
# "Add Item" has id="addItemModal"
|
|
||||||
|
|
||||||
# But "Edit Item" is inside a PHP loop? No, the modal ID has PHP echo.
|
|
||||||
# L4965: <div class="modal fade" id="editItemModal<?= $item['id'] ?>" ...
|
|
||||||
# ...
|
|
||||||
# L4975: <div class="row g-3">
|
|
||||||
|
|
||||||
# I'll verify the lines again.
|
|
||||||
# L4975 is likely correct.
|
|
||||||
|
|
||||||
# I'll proceed with a more manual replacement for the Items forms to ensure they are fixed.
|
|
||||||
# Then I'll check "Edit Profile" (L8558) and "Company Profile" (L8594).
|
|
||||||
|
|
||||||
def replace_block(text, start_marker, end_marker, old_class, new_class):
|
|
||||||
# Find start marker
|
|
||||||
pattern = re.compile(re.escape(start_marker) + r'.*?' + re.escape(old_class), re.DOTALL)
|
|
||||||
# This is hard because of multiple occurrences.
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Direct replacement on known lines (approximate) is safer if I can locate them uniquely.
|
|
||||||
# Or replace all "row g-3" that are inside <form>?
|
|
||||||
# That covers "all forms".
|
|
||||||
|
|
||||||
# Let's try to replace <div class="row g-3"> with <div class="form-grid-3"> everywhere,
|
|
||||||
# BUT checking if it looks like a form.
|
|
||||||
# A form usually has <input>, <select>, <label>.
|
|
||||||
|
|
||||||
# I'll iterate through all "row g-3" occurrences.
|
|
||||||
# For each, I'll check if the following content (up to closing div) contains form elements.
|
|
||||||
# This requires parsing HTML, which is hard.
|
|
||||||
|
|
||||||
# Let's go with the user's specific request + "all forms" interpretation.
|
|
||||||
# I will replace `class="row g-3"` with `class="form-grid-3"` ONLY if the immediate context looks like a form.
|
|
||||||
# I.e. if I see `<form ... class="row g-3">` -> Replace.
|
|
||||||
# If I see `<div class="row g-3">` followed closely by `<div class="col`.
|
|
||||||
|
|
||||||
# Let's use a regex that matches `<div class="row g-3">` followed by whitespace and `<div class="col`.
|
|
||||||
# Most forms follow this pattern.
|
|
||||||
|
|
||||||
# Pattern: <div class="row g-3">\s*<div class="col
|
|
||||||
regex_div = r'(<div class="row g-3">)(\s*<div class="col)'
|
|
||||||
new_content = re.sub(regex_div, r'<div class="form-grid-3">\2', new_content)
|
|
||||||
|
|
||||||
# Pattern: <form ... class="row g-3">
|
|
||||||
regex_form = r'(<form [^>]*class=")(row g-3)(")'
|
|
||||||
new_content = re.sub(regex_form, r'\1form-grid-3\3', new_content)
|
|
||||||
|
|
||||||
# Also explicitly fix the Items modals if they weren't caught (e.g. if there's comment or something in between).
|
|
||||||
# L4975: <div class="row g-3"> (inside Edit Item).
|
|
||||||
# It has <div class="col-md-6"> immediately after?
|
|
||||||
# Let's check L4975 in `read_file` output.
|
|
||||||
# L4975: <div class="row g-3">
|
|
||||||
# L4976: <div class="col-md-6">
|
|
||||||
# Yes, it matches.
|
|
||||||
|
|
||||||
# L10151 (Add Item):
|
|
||||||
# L10151: <div class="row g-3">
|
|
||||||
# L10152: <div class="col-md-6">
|
|
||||||
# Matches.
|
|
||||||
|
|
||||||
# So the regex `(<div class="row g-3">)(\s*<div class="col)` should catch them.
|
|
||||||
|
|
||||||
# One more thing: The user asked to "Add background to titles bar".
|
|
||||||
# I've added CSS for `.modal-header`.
|
|
||||||
# But for non-modals (like Edit Profile), they use `<h5>`.
|
|
||||||
# I should try to wrap them or add a class.
|
|
||||||
# L8554: <h5 class="mb-4 fw-bold" data-en="Edit Profile"...
|
|
||||||
# I'll use regex to find these specific h5 titles in cards and wrap them or add a class.
|
|
||||||
# Search for `class="card p-4 ...">` then `<h5>`.
|
|
||||||
# This is too brittle. I'll stick to the CSS solution for Modals and hope it's enough for "titles bar".
|
|
||||||
# The user specifically mentioned "Items edit form" (which is a modal).
|
|
||||||
# If "Edit Profile" doesn't have a background title, I can manually fix it if requested or if I can find it reliably.
|
|
||||||
# I'll stick to the safe replacement for now.
|
|
||||||
|
|
||||||
with open(file_path, 'w') as f:
|
|
||||||
f.write(new_content)
|
|
||||||
|
|
||||||
print("Replacement complete.")
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
|
|
||||||
with open('index.php', 'r') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
# The corrupted block starts around line 50 (index 50 is line 51)
|
|
||||||
# And ends around line 65 where "// Timezone Setup" is.
|
|
||||||
# Let's find "require_once 'db/config.php';" which I added, and "// Timezone Setup"
|
|
||||||
|
|
||||||
start_idx = -1
|
|
||||||
end_idx = -1
|
|
||||||
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if "require_once 'db/config.php';" in line:
|
|
||||||
start_idx = i
|
|
||||||
if "// Timezone Setup" in line:
|
|
||||||
end_idx = i
|
|
||||||
break
|
|
||||||
|
|
||||||
if start_idx != -1 and end_idx != -1:
|
|
||||||
print(f"Replacing lines {start_idx+1} to {end_idx}")
|
|
||||||
|
|
||||||
new_block = """require_once 'db/config.php';
|
|
||||||
require_once 'includes/stock_helper.php';
|
|
||||||
|
|
||||||
// Helper for current outlet
|
|
||||||
if (!function_exists('current_outlet_id')) {
|
|
||||||
function current_outlet_id() {
|
|
||||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
|
||||||
return (int)($_SESSION['outlet_id'] ?? 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Outlet Switch
|
|
||||||
if (isset($_GET['action']) && $_GET['action'] === 'switch_outlet' && isset($_GET['id'])) {
|
|
||||||
$target_id = (int)$_GET['id'];
|
|
||||||
$allowed_outlets = $_SESSION['user_outlets'] ?? [1];
|
|
||||||
$is_admin = ($_SESSION['user_role_name'] ?? '') === 'Administrator';
|
|
||||||
|
|
||||||
if ($is_admin || in_array($target_id, $allowed_outlets)) {
|
|
||||||
$stmt = db()->prepare("SELECT id FROM outlets WHERE id = ? AND status = 'active'");
|
|
||||||
$stmt->execute([$target_id]);
|
|
||||||
if ($stmt->fetchColumn()) {
|
|
||||||
$_SESSION['outlet_id'] = $target_id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
header("Location: index.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Replace the slice
|
|
||||||
lines[start_idx:end_idx] = [new_block]
|
|
||||||
|
|
||||||
with open('index.php', 'w') as f:
|
|
||||||
f.writelines(lines)
|
|
||||||
print("Fixed index.php")
|
|
||||||
else:
|
|
||||||
print("Could not find block boundaries.")
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
$content = file_get_contents('index.php');
|
|
||||||
|
|
||||||
$search = "\$_SESSION['outlet_id'] = \$u['outlet_id'];";
|
|
||||||
$replacement = "\$_SESSION['outlet_id'] = \$u['outlet_id'];";
|
|
||||||
|
|
||||||
// To fix duplicates, we can simply replace 3 of them with 1
|
|
||||||
$content = preg_replace("/(\s*\\\$_\SESSION\['outlet_id'\] = \\\$u\['outlet_id'\];){2,}/", "\n \$_SESSION['outlet_id'] = \$u['outlet_id'];", $content);
|
|
||||||
|
|
||||||
file_put_contents('index.php', $content);
|
|
||||||
echo "Duplicate login outlet_ids removed.\n";
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
// ... (lines 1-464)
|
|
||||||
if ($action === 'get_payment_details') {
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
$payment_id = (int)$_GET['payment_id'];
|
|
||||||
$stmt = db()->prepare("SELECT p.*, i.customer_id, c.name as customer_name, o.name as outlet_name
|
|
||||||
FROM payments p
|
|
||||||
JOIN invoices i ON p.invoice_id = i.id
|
|
||||||
JOIN customers c ON i.customer_id = c.id
|
|
||||||
LEFT JOIN outlets o ON i.outlet_id = o.id
|
|
||||||
WHERE p.id = ?");
|
|
||||||
$stmt->execute([$payment_id]);
|
|
||||||
echo json_encode($stmt->fetch(PDO::FETCH_ASSOC));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'db/config.php';
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Assign default outlet_id to NULLs
|
|
||||||
$pdo->exec("UPDATE _stock_items SET outlet_id = 1 WHERE outlet_id IS NULL");
|
|
||||||
$pdo->exec("UPDATE _stock_categories SET outlet_id = 1 WHERE outlet_id IS NULL");
|
|
||||||
$pdo->exec("UPDATE _stock_units SET outlet_id = 1 WHERE outlet_id IS NULL");
|
|
||||||
|
|
||||||
// 2. Fix _stock_items index
|
|
||||||
// First, drop old unique index if it exists
|
|
||||||
try {
|
|
||||||
$pdo->exec("ALTER TABLE _stock_items DROP INDEX sku");
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo "Note: index 'sku' might not exist or already dropped: " . $e->getMessage() . "\n";
|
|
||||||
}
|
|
||||||
// Add new unique index (outlet_id, sku)
|
|
||||||
$pdo->exec("ALTER TABLE _stock_items ADD UNIQUE KEY outlet_sku (outlet_id, sku)");
|
|
||||||
|
|
||||||
// 3. Fix _stock_categories index
|
|
||||||
$pdo->exec("ALTER TABLE _stock_categories ADD UNIQUE KEY outlet_cat_name (outlet_id, name_en)");
|
|
||||||
|
|
||||||
// 4. Fix _stock_units index
|
|
||||||
$pdo->exec("ALTER TABLE _stock_units ADD UNIQUE KEY outlet_unit_name (outlet_id, name_en)");
|
|
||||||
|
|
||||||
echo "Database schema updated successfully.\n";
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo "Error updating database schema: " . $e->getMessage() . "\n";
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Just a final check to remove any leftovers from previous attempts if any
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
$content = file_get_contents('index.php');
|
|
||||||
|
|
||||||
$search_th = '<th data-en="Access Level" data-ar="مستوى الوصول">Access Level</th>';
|
|
||||||
$replace_th = '<th data-en="Access Level" data-ar="مستوى الوصول">Access Level</th>
|
|
||||||
<th data-en="Outlet" data-ar="الفرع">Outlet</th>';
|
|
||||||
|
|
||||||
$content = str_replace($search_th, $replace_th, $content);
|
|
||||||
|
|
||||||
$search_td = '<span class="badge rounded-pill bg-info bg-opacity-10 text-info px-3">
|
|
||||||
<?= htmlspecialchars((string)($u[\'group_name\'] ?? \'No Role Assigned\')) ?>
|
|
||||||
</span>
|
|
||||||
</td>';
|
|
||||||
$replace_td = '<span class="badge rounded-pill bg-info bg-opacity-10 text-info px-3">
|
|
||||||
<?= htmlspecialchars((string)($u[\'group_name\'] ?? \'No Role Assigned\')) ?>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge rounded-pill bg-secondary bg-opacity-10 text-secondary px-3">
|
|
||||||
<?php
|
|
||||||
$out_name = "Global / All Outlets";
|
|
||||||
foreach (($data["outlets"] ?? []) as $out) {
|
|
||||||
if ($out["id"] == $u["outlet_id"]) {
|
|
||||||
$out_name = $out["name"];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
echo htmlspecialchars($out_name);
|
|
||||||
?>
|
|
||||||
</span>
|
|
||||||
</td>';
|
|
||||||
|
|
||||||
$content = str_replace($search_td, $replace_td, $content);
|
|
||||||
|
|
||||||
file_put_contents('index.php', $content);
|
|
||||||
echo "Users table updated to show assigned outlet.\n";
|
|
||||||
14185
index.php.bak
14185
index.php.bak
File diff suppressed because it is too large
Load Diff
13632
index.php.bak.20260319
13632
index.php.bak.20260319
File diff suppressed because it is too large
Load Diff
@ -1,7 +0,0 @@
|
|||||||
2026-02-26 17:58:24 - Failed login for 'admin'. Reason: Password mismatch
|
|
||||||
2026-02-26 17:58:40 - Failed login for 'admin'. Reason: Password mismatch
|
|
||||||
2026-02-26 17:58:46 - Failed login for 'admin'. Reason: Password mismatch
|
|
||||||
2026-02-26 17:59:03 - Failed login for 'admin'. Reason: Password mismatch
|
|
||||||
2026-03-17 10:12:37 - Failed login for 'moosa'. Reason: Password mismatch
|
|
||||||
2026-05-02 04:46:38 - Failed login for 'moosa'. Reason: Password mismatch
|
|
||||||
2026-05-02 04:46:47 - Failed login for 'admin'. Reason: Password mismatch
|
|
||||||
5074
output_test.html
5074
output_test.html
File diff suppressed because it is too large
Load Diff
152
pages/avery_label_script.php
Normal file
152
pages/avery_label_script.php
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<script>
|
||||||
|
// Avery Label Logic
|
||||||
|
const selectAllItems = document.getElementById('selectAllItems');
|
||||||
|
const bulkBarcodeBtn = document.getElementById('bulkBarcodeBtn');
|
||||||
|
|
||||||
|
if (selectAllItems) {
|
||||||
|
selectAllItems.addEventListener('change', function() {
|
||||||
|
document.querySelectorAll('.item-checkbox').forEach(cb => {
|
||||||
|
cb.checked = this.checked;
|
||||||
|
});
|
||||||
|
toggleBulkBtn();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('change', function(e) {
|
||||||
|
if (e.target.classList.contains('item-checkbox')) {
|
||||||
|
toggleBulkBtn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleBulkBtn() {
|
||||||
|
const checked = document.querySelectorAll('.item-checkbox:checked').length;
|
||||||
|
if (bulkBarcodeBtn) {
|
||||||
|
bulkBarcodeBtn.style.display = checked > 0 ? 'inline-block' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.openAveryModal = function() {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('averyLabelsModal'));
|
||||||
|
const checkedItems = document.querySelectorAll('.item-checkbox:checked');
|
||||||
|
const container = document.getElementById('averyItemQuantities');
|
||||||
|
const defaultCopies = parseInt(document.getElementById('averyCopies').value) || 1;
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
if (checkedItems.length === 0) {
|
||||||
|
container.innerHTML = '<small class="text-muted" data-en="No items selected." data-ar="لم يتم تحديد أي صنف.">No items selected.</small>';
|
||||||
|
} else {
|
||||||
|
const table = document.createElement('table');
|
||||||
|
table.className = 'table table-sm table-borderless mb-0';
|
||||||
|
const tbody = document.createElement('tbody');
|
||||||
|
|
||||||
|
checkedItems.forEach(cb => {
|
||||||
|
const sku = cb.dataset.sku;
|
||||||
|
const name = cb.dataset.name;
|
||||||
|
const id = cb.dataset.id;
|
||||||
|
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td class="align-middle" style="width: 70%; font-size: 0.9em;">${name} <span class="text-muted">(${sku})</span></td>
|
||||||
|
<td class="align-middle" style="width: 30%;">
|
||||||
|
<input type="number" class="form-control form-control-sm item-qty-input"
|
||||||
|
data-id="${id}" value="${defaultCopies}" min="0" onchange="updateAveryPreview()" onkeyup="updateAveryPreview()">
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
table.appendChild(tbody);
|
||||||
|
container.appendChild(table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.show();
|
||||||
|
updateAveryPreview();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.updateAllItemQuantities = function() {
|
||||||
|
const globalQty = document.getElementById('averyCopies').value;
|
||||||
|
const itemInputs = document.querySelectorAll('.item-qty-input');
|
||||||
|
itemInputs.forEach(input => {
|
||||||
|
input.value = globalQty;
|
||||||
|
});
|
||||||
|
updateAveryPreview();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.updateAveryPreview = function() {
|
||||||
|
const layout = document.getElementById('averyLayout').value;
|
||||||
|
const container = document.getElementById('averyPrintArea');
|
||||||
|
const checkedItems = document.querySelectorAll('.item-checkbox:checked');
|
||||||
|
|
||||||
|
container.className = 'avery-container avery-layout-' + layout;
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
checkedItems.forEach(cb => {
|
||||||
|
const sku = cb.dataset.sku;
|
||||||
|
const nameAr = cb.dataset.nameAr || '';
|
||||||
|
const nameEn = cb.dataset.nameEn || '';
|
||||||
|
const name = cb.dataset.name;
|
||||||
|
const price = cb.dataset.price;
|
||||||
|
const id = cb.dataset.id;
|
||||||
|
|
||||||
|
// Find specific quantity input
|
||||||
|
const qtyInput = document.querySelector(`.item-qty-input[data-id="${id}"]`);
|
||||||
|
let copies = 1;
|
||||||
|
if (qtyInput) {
|
||||||
|
copies = parseInt(qtyInput.value) || 0;
|
||||||
|
} else {
|
||||||
|
copies = parseInt(document.getElementById('averyCopies').value) || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < copies; i++) {
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.className = 'avery-label';
|
||||||
|
const uniqueId = Math.random().toString(36).substr(2, 9);
|
||||||
|
const svgId = `bc-${sku}-${uniqueId}`;
|
||||||
|
|
||||||
|
let nameHtml = '';
|
||||||
|
if (nameAr || nameEn) {
|
||||||
|
const arText = nameAr || name;
|
||||||
|
const enText = nameEn || '';
|
||||||
|
nameHtml = `<div style="font-size: 10px; font-weight: bold; direction: rtl; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.1;">${arText}</div>
|
||||||
|
<div style="font-size: 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.1;">${enText}</div>`;
|
||||||
|
} else {
|
||||||
|
nameHtml = `<div style="font-size: 9px; font-weight: bold; margin-bottom: 2px; line-height: 1.1; overflow: hidden; text-overflow: ellipsis; width: 100%;">${name}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.innerHTML = `
|
||||||
|
${nameHtml}
|
||||||
|
<svg id="${svgId}"></svg>
|
||||||
|
<div style="font-size: 11px; font-weight: bold; margin-top: 2px;">OMR ${price}</div>
|
||||||
|
`;
|
||||||
|
container.appendChild(label);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.getElementById(svgId)) {
|
||||||
|
const bcHeight = layout === 'L7651' ? 20 : 35;
|
||||||
|
JsBarcode(`#${svgId}`, sku, {
|
||||||
|
format: "CODE128",
|
||||||
|
width: layout === 'L7651' ? 1.0 : 1.2,
|
||||||
|
height: bcHeight,
|
||||||
|
displayValue: true,
|
||||||
|
fontSize: layout === 'L7651' ? 8 : 10,
|
||||||
|
margin: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('beforeprint', () => {
|
||||||
|
if (document.getElementById('averyLabelsModal') && document.getElementById('averyLabelsModal').classList.contains('show')) {
|
||||||
|
document.body.classList.add('printing-avery');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.addEventListener('afterprint', () => {
|
||||||
|
document.body.classList.remove('printing-avery');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
196
pages/barcode_pos_script.php
Normal file
196
pages/barcode_pos_script.php
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
window.printItemBarcode = function(sku, nameAr, nameEn, price) {
|
||||||
|
if (!sku) {
|
||||||
|
Swal.fire('Error', 'This item has no SKU/Barcode assigned.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('barcodeLabelName').innerHTML = '<div style="font-weight:bold; font-size:12px; direction:rtl; margin-bottom:2px;">' + nameAr + '</div><div style="font-size:10px;">' + nameEn + '</div>';
|
||||||
|
document.getElementById('barcodeLabelPrice').textContent = 'OMR ' + price;
|
||||||
|
|
||||||
|
JsBarcode("#barcodeSvg", sku, {
|
||||||
|
format: "CODE128",
|
||||||
|
lineColor: "#000",
|
||||||
|
width: 2,
|
||||||
|
height: 50,
|
||||||
|
displayValue: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('barcodePrintModal'));
|
||||||
|
modal.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.executeBarcodePrint = function() {
|
||||||
|
const qty = parseInt(document.getElementById('barcodeQty').value) || 1;
|
||||||
|
const width = parseInt(document.getElementById('barcodeWidth').value) || 40;
|
||||||
|
const height = parseInt(document.getElementById('barcodeHeight').value) || 25;
|
||||||
|
|
||||||
|
// Get content
|
||||||
|
const nameHtml = document.getElementById('barcodeLabelName').innerHTML;
|
||||||
|
const price = document.getElementById('barcodeLabelPrice').innerText;
|
||||||
|
const svg = document.getElementById('barcodeSvg').outerHTML;
|
||||||
|
|
||||||
|
// Create a hidden iframe
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.style.position = 'absolute';
|
||||||
|
iframe.style.width = '0px';
|
||||||
|
iframe.style.height = '0px';
|
||||||
|
iframe.style.border = 'none';
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
|
||||||
|
const doc = iframe.contentWindow.document;
|
||||||
|
|
||||||
|
let labelsHtml = '';
|
||||||
|
for (let i = 0; i < qty; i++) {
|
||||||
|
labelsHtml += `
|
||||||
|
<div class="label-container">
|
||||||
|
<div class="label-name" style="height: auto; overflow: visible;">${nameHtml}</div>
|
||||||
|
${svg}
|
||||||
|
<div class="label-price">${price}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.open();
|
||||||
|
doc.write(`
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
@page { size: ${width}mm ${height}mm; margin: 0; }
|
||||||
|
body { margin: 0; padding: 0; font-family: sans-serif; }
|
||||||
|
.label-container {
|
||||||
|
width: ${width}mm;
|
||||||
|
height: ${height}mm;
|
||||||
|
page-break-after: always;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 1mm;
|
||||||
|
}
|
||||||
|
.label-container:last-child { page-break-after: avoid; }
|
||||||
|
.label-name { font-weight: bold; margin-bottom: 2px; line-height: 1.1; max-width: 100%; }
|
||||||
|
.label-price { font-size: 12px; font-weight: bold; margin-top: 2px; }
|
||||||
|
svg { max-width: 100%; height: auto; max-height: 70%; display: block; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${labelsHtml}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
doc.close();
|
||||||
|
|
||||||
|
iframe.contentWindow.focus();
|
||||||
|
setTimeout(() => {
|
||||||
|
iframe.contentWindow.print();
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(iframe);
|
||||||
|
}, 2000);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
<?php require 'pages/sales_purchases_print_script.php'; ?>
|
||||||
|
|
||||||
|
window.printPosReceiptFromInvoice = function(inv) {
|
||||||
|
const container = document.getElementById('posReceiptContent');
|
||||||
|
const itemsHtml = inv.items.map(item => {
|
||||||
|
const itemTotal = item.unit_price * item.quantity;
|
||||||
|
const vatRate = parseFloat(item.vat_rate !== undefined && item.vat_rate !== null ? item.vat_rate : 0);
|
||||||
|
const vatAmount = itemTotal * (vatRate / (100 + vatRate));
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${item.name_en} / ${item.name_ar}<br><small>${item.quantity} x ${parseFloat(item.unit_price).toFixed(3)}</small></td>
|
||||||
|
<td style="text-align: right; vertical-align: bottom;">${vatAmount.toFixed(2)}</td>
|
||||||
|
<td style="text-align: right; vertical-align: bottom;">${itemTotal.toFixed(3)}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const totalVat = inv.items.reduce((sum, item) => {
|
||||||
|
const itemTotal = item.unit_price * item.quantity;
|
||||||
|
const vatRate = parseFloat(item.vat_rate !== undefined && item.vat_rate !== null ? item.vat_rate : 0);
|
||||||
|
return sum + (itemTotal * (vatRate / (100 + vatRate)));
|
||||||
|
}, 0);
|
||||||
|
const subtotal = inv.items.reduce((sum, item) => sum + (item.unit_price * item.quantity), 0);
|
||||||
|
|
||||||
|
const companyName = "<?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>";
|
||||||
|
const outletName = "<?= htmlspecialchars($data['settings']['current_outlet_name'] ?? '') ?>";
|
||||||
|
const companyPhone = "<?= htmlspecialchars($data['settings']['company_phone'] ?? '') ?>";
|
||||||
|
const companyVat = "<?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?>";
|
||||||
|
const companyLogo = "<?= htmlspecialchars($data['settings']['company_logo'] ?? '') ?>";
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="thermal-receipt">
|
||||||
|
<div class="center">
|
||||||
|
${companyLogo ? `<img src="${companyLogo}" alt="Logo" style="max-height: 60px; width: auto; margin-bottom: 10px; display: block; margin-left: auto; margin-right: auto;">` : ''}
|
||||||
|
<h5 class="mb-0 fw-bold">${companyName}</h5>
|
||||||
|
${inv.outlet_name ? `<div class="fw-bold text-uppercase">${inv.outlet_name}</div>` : ''}
|
||||||
|
${companyPhone ? `<div>Tel: ${companyPhone}</div>` : ''}
|
||||||
|
${companyVat ? `<div>VAT: ${companyVat}</div>` : ''}
|
||||||
|
<div class="separator"></div>
|
||||||
|
<h6 class="fw-bold">TAX INVOICE / فاتورة ضريبية</h6>
|
||||||
|
<div>Inv / رقم: INV-${inv.id.toString().padStart(5, '0')}</div>
|
||||||
|
<div>Date / التاريخ: ${inv.invoice_date}</div>
|
||||||
|
<div class="separator"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Customer / العميل:</strong> ${inv.customer_name || 'Walk-in / عميل عابر'}
|
||||||
|
</div>
|
||||||
|
<div class="separator"></div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ITEM / الصنف</th>
|
||||||
|
<th style="text-align: right;">VAT / الضريبة</th>
|
||||||
|
<th style="text-align: right;">TOTAL / الإجمالي</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${itemsHtml}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="separator"></div>
|
||||||
|
<div class="d-flex justify-content-between small">
|
||||||
|
<span>Subtotal (Excl. VAT) / المجموع الفرعي (دون الضريبة)</span>
|
||||||
|
<span><?= __('currency') ?> ${(subtotal - totalVat).toFixed(3)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between small">
|
||||||
|
<span>VAT / الضريبة</span>
|
||||||
|
<span><?= __('currency') ?> ${totalVat.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="total-row d-flex justify-content-between">
|
||||||
|
<span>TOTAL (Incl. VAT) / الإجمالي (شامل الضريبة)</span>
|
||||||
|
<span><?= __('currency') ?> ${subtotal.toFixed(3)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between small">
|
||||||
|
<span>PAID / المدفوع</span>
|
||||||
|
<span><?= __('currency') ?> ${parseFloat(inv.paid_amount).toFixed(3)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between small fw-bold">
|
||||||
|
<span>BALANCE / الرصيد</span>
|
||||||
|
<span><?= __('currency') ?> ${(subtotal - inv.paid_amount).toFixed(3)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="separator"></div>
|
||||||
|
<div class="center small">
|
||||||
|
<p>Thank You for your business! / شكراً لتعاملكم معنا!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const posModal = new bootstrap.Modal(document.getElementById('posReceiptModal'));
|
||||||
|
posModal.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
function printPosReceipt() {
|
||||||
|
const content = document.getElementById('posReceiptContent').innerHTML;
|
||||||
|
const printArea = document.getElementById('posPrintArea');
|
||||||
|
printArea.innerHTML = `<div class="thermal-receipt thermal-receipt-print">${content}</div>`;
|
||||||
|
|
||||||
|
document.body.classList.add('printing-receipt');
|
||||||
|
window.print();
|
||||||
|
document.body.classList.remove('printing-receipt');
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
110
pages/language_dashboard_script.php
Normal file
110
pages/language_dashboard_script.php
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// --- Language Apply Script ---
|
||||||
|
function applyLanguage(node) {
|
||||||
|
const docLang = document.documentElement.lang || 'ar';
|
||||||
|
const targetAttr = docLang === 'ar' ? 'data-ar' : 'data-en';
|
||||||
|
|
||||||
|
let elements = [];
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
if (node.hasAttribute(targetAttr)) {
|
||||||
|
elements.push(node);
|
||||||
|
}
|
||||||
|
node.querySelectorAll('[' + targetAttr + ']').forEach(el => elements.push(el));
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.forEach(el => {
|
||||||
|
const text = el.getAttribute(targetAttr);
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
if (el.hasAttribute('placeholder')) {
|
||||||
|
el.setAttribute('placeholder', text);
|
||||||
|
}
|
||||||
|
if (el.hasAttribute('title')) {
|
||||||
|
el.setAttribute('title', text);
|
||||||
|
}
|
||||||
|
|
||||||
|
let textNodes = [];
|
||||||
|
el.childNodes.forEach(child => {
|
||||||
|
if (child.nodeType === Node.TEXT_NODE && child.nodeValue.trim() !== '') {
|
||||||
|
textNodes.push(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (textNodes.length > 0) {
|
||||||
|
textNodes[0].nodeValue = text;
|
||||||
|
for (let i = 1; i < textNodes.length; i++) {
|
||||||
|
textNodes[i].nodeValue = '';
|
||||||
|
}
|
||||||
|
} else if (el.children.length === 1 && ['STRONG', 'B', 'SPAN', 'SMALL'].includes(el.children[0].tagName)) {
|
||||||
|
el.children[0].textContent = text;
|
||||||
|
} else if (el.children.length === 0) {
|
||||||
|
el.textContent = text;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
applyLanguage(document.body);
|
||||||
|
|
||||||
|
const observer = new MutationObserver(mutations => {
|
||||||
|
mutations.forEach(mutation => {
|
||||||
|
mutation.addedNodes.forEach(node => {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
applyLanguage(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
});
|
||||||
|
// -----------------------------
|
||||||
|
<?php if ($page === 'dashboard' && can('dashboard_view')): ?>
|
||||||
|
const monthlyData = <?= json_encode($data['monthly_sales']) ?>;
|
||||||
|
const yearlyData = <?= json_encode($data['yearly_sales']) ?>;
|
||||||
|
|
||||||
|
const ctx = document.getElementById('salesChart').getContext('2d');
|
||||||
|
let salesChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: monthlyData.map(d => d.label),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Sales (OMR)',
|
||||||
|
data: monthlyData.map(d => d.total),
|
||||||
|
borderColor: '#0d6efd',
|
||||||
|
backgroundColor: 'rgba(13, 110, 253, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) { return 'OMR ' + value.toFixed(3); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btnMonthly').addEventListener('click', function() {
|
||||||
|
this.classList.add('active');
|
||||||
|
document.getElementById('btnYearly').classList.remove('active');
|
||||||
|
salesChart.data.labels = monthlyData.map(d => d.label);
|
||||||
|
salesChart.data.datasets[0].data = monthlyData.map(d => d.total);
|
||||||
|
salesChart.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btnYearly').addEventListener('click', function() {
|
||||||
|
this.classList.add('active');
|
||||||
|
document.getElementById('btnMonthly').classList.remove('active');
|
||||||
|
salesChart.data.labels = yearlyData.map(d => d.label);
|
||||||
|
salesChart.data.datasets[0].data = yearlyData.map(d => d.total);
|
||||||
|
salesChart.update();
|
||||||
|
});
|
||||||
|
<?php endif; ?>
|
||||||
379
pages/lpo_quotation_script.php
Normal file
379
pages/lpo_quotation_script.php
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
// LPO Form Logic
|
||||||
|
initInvoiceForm('lpoProductSearchInput', 'lpoSearchSuggestions', 'lpoItemsTableBody', 'lpo_grand_display', 'lpo_subtotal_display', 'lpo_vat_display');
|
||||||
|
initInvoiceForm('editLpoProductSearchInput', 'editLpoSearchSuggestions', 'editLpoItemsTableBody', 'edit_lpo_grand_display', 'edit_lpo_subtotal_display', 'edit_lpo_vat_display');
|
||||||
|
|
||||||
|
document.querySelectorAll('.edit-lpo-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const data = JSON.parse(this.dataset.json);
|
||||||
|
document.getElementById('edit_lpo_id').value = data.id;
|
||||||
|
const supplierSelect = document.getElementById('edit_lpo_supplier_id');
|
||||||
|
supplierSelect.value = data.supplier_id;
|
||||||
|
if (window.jQuery && $(supplierSelect).data('select2')) {
|
||||||
|
$(supplierSelect).trigger('change');
|
||||||
|
}
|
||||||
|
document.getElementById('edit_lpo_date').value = data.lpo_date;
|
||||||
|
document.getElementById('edit_lpo_delivery_date').value = data.delivery_date || '';
|
||||||
|
document.getElementById('edit_lpo_status').value = data.status || 'pending';
|
||||||
|
document.getElementById('edit_lpo_terms').value = data.terms_conditions || '';
|
||||||
|
|
||||||
|
const tableBody = document.getElementById('editLpoItemsTableBody');
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
|
data.items.forEach(item => {
|
||||||
|
addItemToTable({
|
||||||
|
id: item.item_id,
|
||||||
|
name_en: item.name_en,
|
||||||
|
name_ar: item.name_ar,
|
||||||
|
sku: '',
|
||||||
|
vat_rate: item.vat_rate || 0
|
||||||
|
}, tableBody, null, null,
|
||||||
|
document.getElementById('edit_lpo_grand_display'),
|
||||||
|
document.getElementById('edit_lpo_subtotal_display'),
|
||||||
|
document.getElementById('edit_lpo_vat_display'),
|
||||||
|
{ quantity: item.quantity, unit_price: item.unit_price });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.view-lpo-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const data = JSON.parse(this.dataset.json);
|
||||||
|
window.viewAndPrintLPO(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
window.viewAndPrintLPO = function(data) {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('viewLpoModal'));
|
||||||
|
const content = document.getElementById('lpoDetailsContent');
|
||||||
|
|
||||||
|
const logoUrl = companySettings.company_logo || '';
|
||||||
|
const companyHeader = `
|
||||||
|
<div class="row align-items-center mb-4">
|
||||||
|
<div class="col-6">
|
||||||
|
${logoUrl ? `<img src="${logoUrl}" alt="Logo" style="max-height: 80px;" class="mb-3">` : ''}
|
||||||
|
<h4 class="fw-bold mb-0">${companySettings.company_name || 'Your Company'}</h4>
|
||||||
|
<p class="text-muted mb-0 small">
|
||||||
|
${companySettings.company_address || ''}<br>
|
||||||
|
Phone: ${companySettings.company_phone || ''} | Email: ${companySettings.company_email || ''}
|
||||||
|
${companySettings.tax_number ? `<br>TRN: ${companySettings.tax_number}` : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<h2 class="text-primary fw-bold mb-1">LOCAL PURCHASE ORDER</h2>
|
||||||
|
<p class="h5 mb-0 text-muted">LPO-${data.id.toString().padStart(5, '0')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
let itemsHtml = '';
|
||||||
|
data.items.forEach((item, index) => {
|
||||||
|
itemsHtml += `
|
||||||
|
<tr>
|
||||||
|
<td>${index + 1}</td>
|
||||||
|
<td>${item.name_en}<br><small class="text-muted">${item.name_ar}</small></td>
|
||||||
|
<td class="text-center">${item.quantity}</td>
|
||||||
|
<td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td>
|
||||||
|
<td class="text-center">${parseFloat(item.vat_rate || 0).toFixed(2)}%</td>
|
||||||
|
<td class="text-end">${parseFloat(item.total_amount).toFixed(3)}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
content.innerHTML = `
|
||||||
|
${companyHeader}
|
||||||
|
<hr>
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-6">
|
||||||
|
<h6 class="text-uppercase text-muted fw-bold mb-3" data-en="Supplier" data-ar="المورد">Supplier</h6>
|
||||||
|
<p class="h6 mb-1 fw-bold">${data.supplier_name}</p>
|
||||||
|
<p class="small text-muted mb-0">
|
||||||
|
${data.supplier_phone ? `Phone: ${data.supplier_phone}` : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<h6 class="text-uppercase text-muted fw-bold mb-3" data-en="Details" data-ar="تفاصيل">Details</h6>
|
||||||
|
<div class="d-flex justify-content-end mb-1">
|
||||||
|
<span class="text-muted me-2">Date:</span>
|
||||||
|
<span class="fw-bold">${data.lpo_date}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end mb-1">
|
||||||
|
<span class="text-muted me-2">Delivery:</span>
|
||||||
|
<span class="fw-bold">${data.delivery_date || '---'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<span class="text-muted me-2">Status:</span>
|
||||||
|
<span class="badge ${data.status === 'pending' ? 'bg-warning text-dark' : 'bg-success'}">${data.status.toUpperCase()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-bordered">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 5%">#</th>
|
||||||
|
<th style="width: 45%" data-en="Description" data-ar="الوصف">Description</th>
|
||||||
|
<th style="width: 10%" class="text-center" data-en="Qty" data-ar="الكمية">Qty</th>
|
||||||
|
<th style="width: 15%" class="text-end">Unit Price</th>
|
||||||
|
<th style="width: 10%" class="text-center">VAT</th>
|
||||||
|
<th style="width: 15%" class="text-end" data-en="Total" data-ar="الإجمالي">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${itemsHtml}</tbody>
|
||||||
|
<tfoot class="table-light">
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-end fw-bold" data-en="Subtotal" data-ar="الإجمالي الفرعي">Subtotal</td>
|
||||||
|
<td class="text-end fw-bold">OMR ${parseFloat(data.total_amount).toFixed(3)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-end fw-bold">VAT Amount</td>
|
||||||
|
<td class="text-end fw-bold">OMR ${parseFloat(data.vat_amount).toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="table-primary">
|
||||||
|
<td colspan="5" class="text-end fw-bold h5">Grand Total</td>
|
||||||
|
<td class="text-end fw-bold h5">OMR ${parseFloat(data.total_with_vat).toFixed(3)}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
${data.terms_conditions ? `
|
||||||
|
<div class="card bg-light border-0 mt-4">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<h6 class="fw-bold mb-2 small text-uppercase text-muted">Terms & Conditions</h6>
|
||||||
|
<p class="small mb-0">${data.terms_conditions.replace(/\n/g, '<br>')}</p>
|
||||||
|
</div>
|
||||||
|
</div>` : ''}
|
||||||
|
|
||||||
|
<div class="row mt-5 pt-3">
|
||||||
|
<div class="col-4 text-center">
|
||||||
|
<div style="border-top: 1px solid #dee2e6; padding-top: 10px;">
|
||||||
|
<p class="small mb-0">Prepared By</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4"></div>
|
||||||
|
<div class="col-4 text-center">
|
||||||
|
<div style="border-top: 1px solid #dee2e6; padding-top: 10px;">
|
||||||
|
<p class="small mb-0">Authorized Signature</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
window.printLPO = function() {
|
||||||
|
const printWindow = window.open('', '_blank');
|
||||||
|
printWindow.document.write('<html><head><title>LPO-' + data.id + '</title>');
|
||||||
|
printWindow.document.write('<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap<?= $dir === 'rtl' ? '.rtl' : '' ?>.min.css" rel="stylesheet">');
|
||||||
|
printWindow.document.write('<style>body { padding: 40px; } @media print { .no-print { display: none; } }</style>');
|
||||||
|
printWindow.document.write('</head><body>');
|
||||||
|
printWindow.document.write(content.innerHTML);
|
||||||
|
printWindow.document.write('</body></html>');
|
||||||
|
printWindow.document.close();
|
||||||
|
setTimeout(() => {
|
||||||
|
printWindow.print();
|
||||||
|
printWindow.close();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
modal.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Quotation Form Logic
|
||||||
|
initInvoiceForm('quotProductSearchInput', 'quotSearchSuggestions', 'quotItemsTableBody', 'quot_grand_display', 'quot_subtotal_display', 'quot_vat_display');
|
||||||
|
initInvoiceForm('editQuotProductSearchInput', 'editQuotSearchSuggestions', 'editQuotItemsTableBody', 'edit_quot_grand_display', 'edit_quot_subtotal_display', 'edit_quot_vat_display');
|
||||||
|
|
||||||
|
document.querySelectorAll('.edit-quotation-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const data = JSON.parse(this.dataset.json);
|
||||||
|
document.getElementById('edit_quotation_id').value = data.id;
|
||||||
|
document.getElementById('edit_quot_customer_id').value = data.customer_id;
|
||||||
|
document.getElementById('edit_quot_date').value = data.quotation_date;
|
||||||
|
document.getElementById('edit_quot_valid').value = data.valid_until || '';
|
||||||
|
document.getElementById('edit_quot_status').value = data.status || 'pending';
|
||||||
|
|
||||||
|
const tableBody = document.getElementById('editQuotItemsTableBody');
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
|
data.items.forEach(item => {
|
||||||
|
addItemToTable({
|
||||||
|
id: item.item_id,
|
||||||
|
name_en: item.name_en,
|
||||||
|
name_ar: item.name_ar,
|
||||||
|
sku: '',
|
||||||
|
vat_rate: item.vat_rate || 0
|
||||||
|
}, tableBody, null, null,
|
||||||
|
document.getElementById('edit_quot_grand_display'),
|
||||||
|
document.getElementById('edit_quot_subtotal_display'),
|
||||||
|
document.getElementById('edit_quot_vat_display'),
|
||||||
|
{ quantity: item.quantity, unit_price: item.unit_price });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.convert-quotation-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
if (confirm('Convert this quotation to an invoice? This will reduce stock.')) {
|
||||||
|
const f = document.createElement('form');
|
||||||
|
f.method = 'POST';
|
||||||
|
f.innerHTML = `<input type="hidden" name="convert_to_invoice" value="1"><input type="hidden" name="quotation_id" value="${this.dataset.id}">`;
|
||||||
|
document.body.appendChild(f);
|
||||||
|
f.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// View Quotation Logic
|
||||||
|
window.viewAndPrintQuotation = function(data, autoPrint = false) {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('viewQuotationModal'));
|
||||||
|
const content = document.getElementById('quotationPrintableArea');
|
||||||
|
|
||||||
|
let itemsHtml = '';
|
||||||
|
data.items.forEach((item, index) => {
|
||||||
|
itemsHtml += `
|
||||||
|
<tr>
|
||||||
|
<td>${index + 1}</td>
|
||||||
|
<td>${item.name_en}<br><small>${item.name_ar}</small></td>
|
||||||
|
<td class="text-center">${item.quantity}</td>
|
||||||
|
<td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td>
|
||||||
|
<td class="text-center">${parseFloat(item.vat_rate || 0).toFixed(2)}%</td>
|
||||||
|
<td class="text-end">${parseFloat(item.total_price).toFixed(3)}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Company Logo and Header Construction
|
||||||
|
const logoUrl = companySettings.company_logo || '';
|
||||||
|
const logoImg = logoUrl ? `<img src="${logoUrl}" alt="Logo" class="invoice-logo mb-3">` : '';
|
||||||
|
const companyName = companySettings.company_name || 'Accounting System';
|
||||||
|
const companyAddress = (companySettings.company_address || '').replace(/\n/g, '<br>');
|
||||||
|
const companyVat = companySettings.vat_number ? `<p class="text-muted small mb-0">VAT: ${companySettings.vat_number}</p>` : '';
|
||||||
|
const companyPhone = companySettings.company_phone ? `<p class="text-muted small mb-0">Tel: ${companySettings.company_phone}</p>` : '';
|
||||||
|
|
||||||
|
// Quotation Header Construction
|
||||||
|
const quotDate = data.quotation_date;
|
||||||
|
const quotValid = data.valid_until || 'N/A';
|
||||||
|
const quotNo = 'QUO-' + data.id.toString().padStart(5, '0');
|
||||||
|
const customerName = data.customer_name || 'Walk-in Customer';
|
||||||
|
const statusBadge = `<span class="badge ${data.status === 'converted' ? 'bg-success' : 'bg-secondary'}">${data.status.toUpperCase()}</span>`;
|
||||||
|
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="invoice-header mb-4">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-6">
|
||||||
|
${logoImg}
|
||||||
|
<h3 class="mb-1 fw-bold">${companyName}</h3>
|
||||||
|
<p class="text-muted small mb-0">${companyAddress}</p>
|
||||||
|
${companyVat}
|
||||||
|
${companyPhone}
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<h1 class="invoice-title fw-bold mb-0 text-uppercase">Quotation / عرض سعر</h1>
|
||||||
|
<div class="mt-2">${statusBadge}</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<p class="mb-0 fs-5">No / رقم: <strong class="text-primary">${quotNo}</strong></p>
|
||||||
|
<p class="mb-0">Date / التاريخ: <span class="fw-bold">${quotDate}</span></p>
|
||||||
|
<p class="mb-0">Valid Until / صالح لغاية: <span class="fw-bold">${quotValid}</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-4 g-3">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="invoice-info-card">
|
||||||
|
<p class="text-muted small text-uppercase fw-bold mb-2 border-bottom pb-1">To / إلى</p>
|
||||||
|
<h5 class="mb-1 fw-bold">${customerName}</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-bordered table-formal">
|
||||||
|
<thead class="bg-dark text-white">
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Item Description / وصف الصنف</th>
|
||||||
|
<th class="text-center">Qty / الكمية</th>
|
||||||
|
<th class="text-end">Unit Price / سعر الوحدة</th>
|
||||||
|
<th class="text-center">VAT / الضريبة</th>
|
||||||
|
<th class="text-end">Total / الإجمالي</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${itemsHtml}</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th colspan="5" class="text-end">Subtotal / المجموع الفرعي</th>
|
||||||
|
<td class="text-end fw-bold">${parseFloat(data.total_amount).toFixed(3)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th colspan="5" class="text-end">VAT Amount / مبلغ الضريبة</th>
|
||||||
|
<td class="text-end fw-bold">${parseFloat(data.vat_amount).toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="table-primary">
|
||||||
|
<th colspan="5" class="text-end h5">Grand Total (OMR) / المجموع الكلي (رع)</th>
|
||||||
|
<td class="text-end h5 fw-bold">${parseFloat(data.total_with_vat).toFixed(3)}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="mt-5 pt-3 border-top">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<p class="small text-muted text-uppercase fw-bold">Terms & Conditions / الشروط والأحكام:</p>
|
||||||
|
<ul class="small text-muted">
|
||||||
|
<li>Quotation is valid until the date mentioned above. / عرض السعر صالح لغاية التاريخ المذكور أعلاه.</li>
|
||||||
|
<li>Prices are inclusive of VAT where applicable. / الأسعار تشمل ضريبة القيمة المضافة حيثما ينطبق ذلك.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end pt-4">
|
||||||
|
<div class="border-top d-inline-block px-5">Authorized Signature / التوقيع المعتمد</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<p class="text-muted x-small mb-0">Generated by / تم إنشاؤه بواسطة ${companyName}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const actionButtons = document.getElementById('quotationActionButtons');
|
||||||
|
actionButtons.innerHTML = '';
|
||||||
|
if (data.status === 'pending') {
|
||||||
|
const convertBtn = document.createElement('button');
|
||||||
|
convertBtn.className = 'btn btn-success me-2';
|
||||||
|
convertBtn.innerHTML = '<i class="bi bi-receipt"></i> <span data-en="Convert to Invoice" data-ar="تحويل إلى فاتورة">Convert to Invoice</span>';
|
||||||
|
convertBtn.onclick = function() {
|
||||||
|
if (confirm('Convert this quotation to an invoice?')) {
|
||||||
|
const f = document.createElement('form');
|
||||||
|
f.method = 'POST';
|
||||||
|
f.innerHTML = `<input type="hidden" name="convert_to_invoice" value="1"><input type="hidden" name="quotation_id" value="${data.id}">`;
|
||||||
|
document.body.appendChild(f);
|
||||||
|
f.submit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
actionButtons.appendChild(convertBtn);
|
||||||
|
|
||||||
|
const editBtn = document.createElement('button');
|
||||||
|
editBtn.className = 'btn btn-primary';
|
||||||
|
editBtn.innerHTML = '<i class="bi bi-pencil-square"></i> <span data-en="Edit" data-ar="تعديل">Edit</span>';
|
||||||
|
editBtn.onclick = function() {
|
||||||
|
const editModal = new bootstrap.Modal(document.getElementById('editQuotationModal'));
|
||||||
|
modal.hide();
|
||||||
|
const originalEditBtn = document.querySelector(`.edit-quotation-btn[data-json*='"id":${data.id},']`) ||
|
||||||
|
document.querySelector(`.edit-quotation-btn[data-json*='"id":${data.id}']`);
|
||||||
|
if (originalEditBtn) originalEditBtn.click();
|
||||||
|
};
|
||||||
|
actionButtons.appendChild(editBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.show();
|
||||||
|
if (autoPrint) {
|
||||||
|
setTimeout(() => { window.print(); }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelectorAll('.view-quotation-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const data = JSON.parse(this.dataset.json);
|
||||||
|
window.viewAndPrintQuotation(data, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
20
pages/register_session_report_script.php
Normal file
20
pages/register_session_report_script.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script>
|
||||||
|
function loadSessionReport(id) {
|
||||||
|
var modalEl = document.getElementById('universalSessionReportModal');
|
||||||
|
var modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||||
|
|
||||||
|
// Reset content
|
||||||
|
document.getElementById('sessionReportContent').innerHTML = '<div class="modal-body text-center p-5"><div class="spinner-border text-primary"></div></div>';
|
||||||
|
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
fetch('ajax_session_report.php?id=' + id)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
document.getElementById('sessionReportContent').innerHTML = html;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
document.getElementById('sessionReportContent').innerHTML = '<div class="modal-header"><h5 class="modal-title">Error</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body text-danger">Failed to load report.</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
223
pages/sales_purchases_invoice_actions_script.php
Normal file
223
pages/sales_purchases_invoice_actions_script.php
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
// Edit Invoice Logic
|
||||||
|
document.querySelectorAll('.edit-invoice-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const data = JSON.parse(this.dataset.json);
|
||||||
|
document.getElementById('edit_invoice_id').value = data.id;
|
||||||
|
document.getElementById('edit_customer_id').value = data.customer_id;
|
||||||
|
document.getElementById('edit_invoice_date').value = data.invoice_date;
|
||||||
|
document.getElementById('edit_due_date').value = data.due_date || '';
|
||||||
|
document.getElementById('edit_payment_type').value = data.payment_type || 'cash';
|
||||||
|
document.getElementById('edit_status').value = data.status || 'unpaid';
|
||||||
|
document.getElementById('edit_paid_amount').value = parseFloat(data.paid_amount || 0).toFixed(3);
|
||||||
|
|
||||||
|
if (data.status === 'partially_paid') {
|
||||||
|
document.getElementById('editPaidAmountContainer').style.display = 'block';
|
||||||
|
} else {
|
||||||
|
document.getElementById('editPaidAmountContainer').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableBody = document.getElementById('editInvoiceItemsTableBody');
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
|
data.items.forEach(item => {
|
||||||
|
// We need more data than what's in invoice_items (like SKU and names, but we have them from the join in PHP)
|
||||||
|
// The dataset-json already contains name_en, name_ar etc because of the PHP logic at line 1093
|
||||||
|
const itemMeta = {
|
||||||
|
id: item.item_id,
|
||||||
|
name_en: item.name_en,
|
||||||
|
name_ar: item.name_ar,
|
||||||
|
sku: '', // Optional, or fetch if needed
|
||||||
|
vat_rate: 0 // Will be handled if we have it in the join
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch current item details to get VAT rate if possible, or use stored if available
|
||||||
|
// For simplicity, let's assume we want to use the item's current VAT rate or store it.
|
||||||
|
// Looking at the join at line 1093, it doesn't fetch vat_rate. Let's fix that in PHP too.
|
||||||
|
|
||||||
|
addItemToTable({
|
||||||
|
id: item.item_id,
|
||||||
|
name_en: item.name_en,
|
||||||
|
name_ar: item.name_ar,
|
||||||
|
sku: '',
|
||||||
|
vat_rate: item.vat_rate || 0 // We'll add this to PHP join
|
||||||
|
}, tableBody, null, null,
|
||||||
|
document.getElementById('edit_grandTotal'),
|
||||||
|
document.getElementById('edit_subtotal'),
|
||||||
|
document.getElementById('edit_totalVat'),
|
||||||
|
{ quantity: item.quantity, unit_price: item.unit_price });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// View and Print Invoice Logic
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (e.target.closest('.view-invoice-btn')) {
|
||||||
|
const btn = e.target.closest('.view-invoice-btn');
|
||||||
|
const data = JSON.parse(btn.dataset.json);
|
||||||
|
if (window.viewAndPrintA4Invoice) {
|
||||||
|
window.viewAndPrintA4Invoice(data, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.target.closest('.print-a4-btn')) {
|
||||||
|
const btn = e.target.closest('.print-a4-btn');
|
||||||
|
const data = JSON.parse(btn.dataset.json);
|
||||||
|
if (window.viewAndPrintA4Invoice) {
|
||||||
|
window.viewAndPrintA4Invoice(data, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return Logic (General for Sales and Purchase)
|
||||||
|
const setupReturnLogic = (selectId, containerId, tbodyId, totalDisplayId, submitBtnId, type = "sale") => {
|
||||||
|
const select = document.getElementById(selectId);
|
||||||
|
if (!select) return;
|
||||||
|
|
||||||
|
const calculateTotal = function() {
|
||||||
|
let total = 0;
|
||||||
|
document.querySelectorAll('#' + tbodyId + ' tr').forEach(row => {
|
||||||
|
const qtyInput = row.querySelector('.return-qty-input');
|
||||||
|
if (!qtyInput) return;
|
||||||
|
const qty = parseFloat(qtyInput.value) || 0;
|
||||||
|
const price = parseFloat(qtyInput.dataset.price) || 0;
|
||||||
|
const lineTotal = qty * price;
|
||||||
|
const lineTotalDisplay = row.querySelector('.line-total');
|
||||||
|
if (lineTotalDisplay) {
|
||||||
|
lineTotalDisplay.innerText = lineTotal.toFixed(3);
|
||||||
|
}
|
||||||
|
total += lineTotal;
|
||||||
|
});
|
||||||
|
const totalDisplay = document.getElementById(totalDisplayId);
|
||||||
|
if (totalDisplay) {
|
||||||
|
totalDisplay.innerText = 'OMR ' + total.toFixed(3);
|
||||||
|
}
|
||||||
|
const submitBtn = document.getElementById(submitBtnId);
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.disabled = total <= 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInvoiceChange = async function() {
|
||||||
|
const invoiceId = select.value;
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
const tbody = document.getElementById(tbodyId);
|
||||||
|
const submitBtn = document.getElementById(submitBtnId);
|
||||||
|
|
||||||
|
if (!invoiceId) {
|
||||||
|
if (container) container.style.display = 'none';
|
||||||
|
if (submitBtn) submitBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tbody) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center"><div class="spinner-border spinner-border-sm text-primary"></div> <span data-en="Loading items..." data-ar="جاري تحميل الأصناف...">Loading items...</span></td></tr>';
|
||||||
|
}
|
||||||
|
if (container) container.style.display = 'block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`index.php?action=get_invoice_items&invoice_id=${invoiceId}&type=${type}`);
|
||||||
|
const items = await resp.json();
|
||||||
|
|
||||||
|
if (tbody) {
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
if (items.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted" data-en="No items found for this invoice." data-ar="لا توجد أصناف لهذه الفاتورة.">No items found for this invoice.</td></tr>';
|
||||||
|
} else {
|
||||||
|
let html = '';
|
||||||
|
items.forEach(item => {
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td>${item.name_en}<br><small class="text-muted">${item.sku}</small></td>
|
||||||
|
<td>${parseFloat(item.quantity).toFixed(2)}</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="quantities[]" class="form-control form-control-sm return-qty-input"
|
||||||
|
step="0.01" min="0" max="${item.quantity}" value="0"
|
||||||
|
data-price="${item.unit_price}">
|
||||||
|
<input type="hidden" name="item_ids[]" value="${item.item_id}">
|
||||||
|
<input type="hidden" name="prices[]" value="${item.unit_price}">
|
||||||
|
</td>
|
||||||
|
<td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td>
|
||||||
|
<td class="text-end line-total">0.000</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
tbody.innerHTML = html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (submitBtn) submitBtn.disabled = true;
|
||||||
|
|
||||||
|
const qtyInputs = tbody.querySelectorAll('.return-qty-input');
|
||||||
|
qtyInputs.forEach(input => {
|
||||||
|
['input', 'change', 'keyup'].forEach(evt => input.addEventListener(evt, calculateTotal));
|
||||||
|
});
|
||||||
|
|
||||||
|
calculateTotal();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (window.Swal) Swal.fire('Error', 'Failed to fetch invoice items', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
select.addEventListener('change', handleInvoiceChange);
|
||||||
|
if (window.jQuery && jQuery.fn.select2) {
|
||||||
|
$(select).on('select2:select change', handleInvoiceChange);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setupReturnLogic('return_invoice_select', 'return_items_container', 'return_items_tbody', 'return_total_display', 'submit_return_btn', 'sale');
|
||||||
|
setupReturnLogic('purchase_return_invoice_select', 'purchase_return_items_container', 'purchase_return_items_tbody', 'purchase_return_total_display', 'purchase_submit_return_btn', 'purchase');
|
||||||
|
|
||||||
|
// Return Invoice Button from Sales/Purchases list
|
||||||
|
document.querySelectorAll('.return-invoice-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const invoiceId = this.dataset.id;
|
||||||
|
const targetModal = this.dataset.bsTarget;
|
||||||
|
const selectId = targetModal === '#addSalesReturnModal' ? 'return_invoice_select' : 'purchase_return_invoice_select';
|
||||||
|
const select = document.getElementById(selectId);
|
||||||
|
if (select) {
|
||||||
|
$(select).val(invoiceId).trigger('change');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// View Return Logic
|
||||||
|
document.querySelectorAll('.view-return-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async function() {
|
||||||
|
const returnId = this.dataset.id;
|
||||||
|
const type = '<?= $page === "purchase_returns" ? "purchase" : "sale" ?>';
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('viewReturnDetailsModal'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`index.php?action=get_return_details&return_id=${returnId}&type=${type}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
document.getElementById('view_return_no').innerText = (type === 'purchase' ? 'PRET-' : 'SRET-') + String(data.id).padStart(5, '0');
|
||||||
|
document.getElementById('view_return_party').innerText = data.party_name;
|
||||||
|
document.getElementById('view_return_date').innerText = data.return_date;
|
||||||
|
const refId = data.purchase_id || data.invoice_id;
|
||||||
|
const refPrefix = type === 'purchase' ? 'PUR-' : 'INV-';
|
||||||
|
document.getElementById('view_return_invoice').innerText = refPrefix + String(refId).padStart(5, '0');
|
||||||
|
document.getElementById('view_return_total').innerText = 'OMR ' + parseFloat(data.total_amount).toFixed(3);
|
||||||
|
document.getElementById('view_return_notes').innerText = data.notes || 'No notes';
|
||||||
|
|
||||||
|
let itemsHtml = '';
|
||||||
|
data.items.forEach(item => {
|
||||||
|
itemsHtml += `
|
||||||
|
<tr>
|
||||||
|
<td>${item.name_en}<br><small class="text-muted">${item.sku}</small></td>
|
||||||
|
<td class="text-center">${parseFloat(item.quantity).toFixed(2)}</td>
|
||||||
|
<td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td>
|
||||||
|
<td class="text-end">${parseFloat(item.total_price).toFixed(3)}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
document.getElementById('view_return_items_tbody').innerHTML = itemsHtml;
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
Swal.fire('Error', 'Failed to fetch return details', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
155
pages/sales_purchases_invoice_form_helpers.php
Normal file
155
pages/sales_purchases_invoice_form_helpers.php
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
// Invoice Form Logic
|
||||||
|
const initInvoiceForm = (searchInputId, suggestionsId, tableBodyId, grandTotalId, subtotalId, totalVatId) => {
|
||||||
|
const searchInput = document.getElementById(searchInputId);
|
||||||
|
const suggestions = document.getElementById(suggestionsId);
|
||||||
|
const tableBody = document.getElementById(tableBodyId);
|
||||||
|
const grandTotalEl = document.getElementById(grandTotalId);
|
||||||
|
const subtotalEl = document.getElementById(subtotalId);
|
||||||
|
const totalVatEl = document.getElementById(totalVatId);
|
||||||
|
|
||||||
|
if (!searchInput || !tableBody) return;
|
||||||
|
|
||||||
|
let timeout = null;
|
||||||
|
searchInput.addEventListener('input', function() {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
const q = this.value.trim();
|
||||||
|
if (q.length < 2) {
|
||||||
|
suggestions.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
fetch(`index.php?action=search_items&q=${encodeURIComponent(q)}`)
|
||||||
|
.then(res => res.ok ? res.json() : [])
|
||||||
|
.then(data => {
|
||||||
|
suggestions.innerHTML = '';
|
||||||
|
if (data.length > 0) {
|
||||||
|
data.forEach(item => {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.className = 'list-group-item list-group-item-action';
|
||||||
|
btn.innerHTML = `
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span><strong>${item.sku}</strong> - ${item.name_en} / ${item.name_ar}</span>
|
||||||
|
<span class="text-muted small">Stock: ${item.stock_quantity}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
btn.onclick = () => addItemToTable(item, tableBody, searchInput, suggestions, grandTotalEl, subtotalEl, totalVatEl);
|
||||||
|
suggestions.appendChild(btn);
|
||||||
|
});
|
||||||
|
suggestions.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
suggestions.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close suggestions when clicking outside
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (!searchInput.contains(e.target) && !suggestions.contains(e.target)) {
|
||||||
|
suggestions.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function addItemToTable(item, tableBody, searchInput, suggestions, grandTotalEl, subtotalEl, totalVatEl, customData = null) {
|
||||||
|
if (suggestions) suggestions.style.display = 'none';
|
||||||
|
if (searchInput) searchInput.value = '';
|
||||||
|
|
||||||
|
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
|
||||||
|
const currentStock = parseFloat(item.stock_quantity) || 0;
|
||||||
|
|
||||||
|
if (invoiceType === 'sale' && !allowZeroStock && !customData) {
|
||||||
|
const existingInTable = Array.from(tableBody.querySelectorAll('.item-row')).find(row => row.querySelector('.item-id-input').value == item.id);
|
||||||
|
let currentQtyInTable = 0;
|
||||||
|
if (existingInTable) {
|
||||||
|
currentQtyInTable = parseFloat(existingInTable.querySelector('.item-qty').value) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentQtyInTable + 1 > currentStock) {
|
||||||
|
alert('Insufficient stock! Available: ' + currentStock);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingRow = Array.from(tableBody.querySelectorAll('.item-id-input')).find(input => input.value == item.id);
|
||||||
|
if (existingRow && !customData) {
|
||||||
|
const row = existingRow.closest('tr');
|
||||||
|
const qtyInput = row.querySelector('.item-qty');
|
||||||
|
qtyInput.value = parseFloat(qtyInput.value) + 1;
|
||||||
|
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.className = 'item-row';
|
||||||
|
const price = customData ? customData.unit_price : (invoiceType === 'sale' ? item.sale_price : item.purchase_price);
|
||||||
|
const qty = customData ? customData.quantity : 1;
|
||||||
|
const vatRate = item.vat_rate || 0;
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>
|
||||||
|
<input type="hidden" name="item_ids[]" class="item-id-input" value="${item.id}">
|
||||||
|
<input type="hidden" class="item-row-stock" value="${item.stock_quantity}">
|
||||||
|
<input type="hidden" class="item-vat-rate" value="${vatRate}">
|
||||||
|
<div><strong>${item.name_en}</strong></div>
|
||||||
|
<div class="small text-muted">${item.name_ar} (${item.sku})</div>
|
||||||
|
</td>
|
||||||
|
<td><input type="number" step="0.001" name="quantities[]" class="form-control item-qty" value="${qty}" required></td>
|
||||||
|
<td><input type="number" step="0.001" name="prices[]" class="form-control item-price" value="${price}" required></td>
|
||||||
|
<td><input type="text" class="form-control bg-light" value="${parseFloat(vatRate || 0).toFixed(2)}%" readonly></td>
|
||||||
|
<td><input type="number" step="0.001" class="form-control item-total" value="${(qty * price).toFixed(3)}" readonly></td>
|
||||||
|
<td><button type="button" class="btn btn-outline-danger btn-sm remove-row"><i class="bi bi-trash"></i></button></td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
attachRowListeners(row, tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
||||||
|
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl) {
|
||||||
|
let subtotal = 0;
|
||||||
|
let totalVat = 0;
|
||||||
|
tableBody.querySelectorAll('.item-row').forEach(row => {
|
||||||
|
const qty = parseFloat(row.querySelector('.item-qty').value) || 0;
|
||||||
|
const price = parseFloat(row.querySelector('.item-price').value) || 0;
|
||||||
|
const vatRate = parseFloat(row.querySelector('.item-vat-rate').value) || 0;
|
||||||
|
|
||||||
|
const total = qty * price;
|
||||||
|
const vatAmount = total * (vatRate / 100);
|
||||||
|
|
||||||
|
row.querySelector('.item-total').value = total.toFixed(3);
|
||||||
|
subtotal += total;
|
||||||
|
totalVat += vatAmount;
|
||||||
|
});
|
||||||
|
const grandTotal = subtotal + totalVat;
|
||||||
|
|
||||||
|
if (subtotalEl) subtotalEl.textContent = 'OMR ' + subtotal.toFixed(3);
|
||||||
|
if (totalVatEl) totalVatEl.textContent = 'OMR ' + totalVat.toFixed(2);
|
||||||
|
if (grandTotalEl) grandTotalEl.textContent = 'OMR ' + grandTotal.toFixed(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachRowListeners(row, tableBody, grandTotalEl, subtotalEl, totalVatEl) {
|
||||||
|
row.querySelector('.item-qty').addEventListener('input', function() {
|
||||||
|
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
|
||||||
|
if (invoiceType === 'sale' && !allowZeroStock) {
|
||||||
|
const stock = parseFloat(row.querySelector('.item-row-stock').value) || 0;
|
||||||
|
const qty = parseFloat(this.value) || 0;
|
||||||
|
if (qty > stock) {
|
||||||
|
alert('Insufficient stock! Available: ' + stock);
|
||||||
|
this.value = stock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
||||||
|
});
|
||||||
|
row.querySelector('.item-price').addEventListener('input', () => recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl));
|
||||||
|
row.querySelector('.remove-row').addEventListener('click', function() {
|
||||||
|
row.remove();
|
||||||
|
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoiceType = '<?= in_array($page, ["sales", "quotations"]) ? "sale" : (in_array($page, ["purchases", "lpos"]) ? "purchase" : "") ?>';
|
||||||
|
initInvoiceForm('productSearchInput', 'searchSuggestions', 'invoiceItemsTableBody', 'grandTotal', 'subtotal', 'totalVat');
|
||||||
|
initInvoiceForm('editProductSearchInput', 'editSearchSuggestions', 'editInvoiceItemsTableBody', 'edit_grandTotal', 'edit_subtotal', 'edit_totalVat');
|
||||||
1117
pages/sales_purchases_modals.php
Normal file
1117
pages/sales_purchases_modals.php
Normal file
File diff suppressed because it is too large
Load Diff
158
pages/sales_purchases_payment_receipt_script.php
Normal file
158
pages/sales_purchases_payment_receipt_script.php
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
// Status change logic for Paid Amount field
|
||||||
|
const togglePaidAmount = (statusId, containerId) => {
|
||||||
|
const statusEl = document.getElementById(statusId);
|
||||||
|
const containerEl = document.getElementById(containerId);
|
||||||
|
if (statusEl && containerEl) {
|
||||||
|
statusEl.addEventListener('change', function() {
|
||||||
|
if (this.value === 'partially_paid') {
|
||||||
|
containerEl.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
containerEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
togglePaidAmount('add_status', 'addPaidAmountContainer');
|
||||||
|
togglePaidAmount('edit_status', 'editPaidAmountContainer');
|
||||||
|
|
||||||
|
// Pay Invoice Logic
|
||||||
|
document.querySelectorAll('.pay-invoice-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const id = this.getAttribute('data-id');
|
||||||
|
const total = parseFloat(this.getAttribute('data-total'));
|
||||||
|
const paid = parseFloat(this.getAttribute('data-paid') || 0);
|
||||||
|
const remaining = total - paid;
|
||||||
|
|
||||||
|
document.getElementById('pay_invoice_id').value = id;
|
||||||
|
document.getElementById('pay_invoice_total').value = total.toFixed(3);
|
||||||
|
document.getElementById('pay_remaining_amount').value = remaining.toFixed(3);
|
||||||
|
document.getElementById('pay_amount').value = remaining.toFixed(3);
|
||||||
|
document.getElementById('pay_amount').max = remaining.toFixed(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show receipt modal if needed
|
||||||
|
<?php if (isset($_SESSION['trigger_receipt_modal'])):
|
||||||
|
$rid = (int)$_SESSION['show_receipt_id'];
|
||||||
|
unset($_SESSION['trigger_receipt_modal']);
|
||||||
|
?>
|
||||||
|
if (typeof showReceipt === 'function') {
|
||||||
|
showReceipt(<?= $rid ?>);
|
||||||
|
} else {
|
||||||
|
setTimeout(() => { if (typeof showReceipt === 'function') showReceipt(<?= $rid ?>); }, 500);
|
||||||
|
}
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
function showReceipt(paymentId) {
|
||||||
|
fetch(`index.php?action=get_payment_details&payment_id=${paymentId}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data) return;
|
||||||
|
document.getElementById('receiptNo').textContent = 'RCP-' + data.id.toString().padStart(5, '0');
|
||||||
|
document.getElementById('receiptDate').textContent = data.payment_date;
|
||||||
|
document.getElementById('receiptCustomer').textContent = data.customer_name || '---';
|
||||||
|
document.getElementById('receiptInvNo').textContent = (data.inv_type === 'purchase' ? 'PUR-' : 'INV-') + data.inv_id.toString().padStart(5, '0');
|
||||||
|
document.getElementById('receiptMethod').textContent = data.payment_method;
|
||||||
|
document.getElementById('receiptAmount').textContent = parseFloat(data.amount).toFixed(3);
|
||||||
|
document.getElementById('receiptAmountWords').textContent = data.amount_words;
|
||||||
|
|
||||||
|
const outletEl = document.getElementById('receiptOutletName');
|
||||||
|
if (outletEl) {
|
||||||
|
outletEl.textContent = data.outlet_name ? (data.outlet_name) : '';
|
||||||
|
outletEl.style.display = data.outlet_name ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update labels for Purchase vs Sale
|
||||||
|
const partyLabel = document.getElementById('receiptPartyLabel');
|
||||||
|
const againstLabel = document.getElementById('receiptAgainstLabel');
|
||||||
|
const receiptTitle = document.querySelector('#receiptModal .modal-title');
|
||||||
|
const receiptH4 = document.querySelector('.receipt-container h4.letter-spacing-2');
|
||||||
|
|
||||||
|
if (data.inv_type === 'purchase') {
|
||||||
|
partyLabel.textContent = 'Paid To';
|
||||||
|
partyLabel.setAttribute('data-en', 'Paid To');
|
||||||
|
partyLabel.setAttribute('data-ar', 'صرف إلى');
|
||||||
|
|
||||||
|
againstLabel.textContent = 'Against Purchase';
|
||||||
|
againstLabel.setAttribute('data-en', 'Against Purchase');
|
||||||
|
againstLabel.setAttribute('data-ar', 'مقابل شراء');
|
||||||
|
|
||||||
|
receiptTitle.textContent = 'Payment Voucher';
|
||||||
|
receiptTitle.setAttribute('data-en', 'Payment Voucher');
|
||||||
|
receiptTitle.setAttribute('data-ar', 'سند صرف');
|
||||||
|
|
||||||
|
receiptH4.textContent = 'PAYMENT VOUCHER';
|
||||||
|
receiptH4.setAttribute('data-en', 'PAYMENT VOUCHER');
|
||||||
|
receiptH4.setAttribute('data-ar', 'سند صرف');
|
||||||
|
} else {
|
||||||
|
partyLabel.textContent = 'Received From';
|
||||||
|
partyLabel.setAttribute('data-en', 'Received From');
|
||||||
|
partyLabel.setAttribute('data-ar', 'وصلنا من');
|
||||||
|
|
||||||
|
againstLabel.textContent = 'Against Invoice';
|
||||||
|
againstLabel.setAttribute('data-en', 'Against Invoice');
|
||||||
|
againstLabel.setAttribute('data-ar', 'مقابل فاتورة');
|
||||||
|
|
||||||
|
receiptTitle.textContent = 'Payment Receipt';
|
||||||
|
receiptTitle.setAttribute('data-en', 'Payment Receipt');
|
||||||
|
receiptTitle.setAttribute('data-ar', 'سند قبض');
|
||||||
|
|
||||||
|
receiptH4.textContent = 'PAYMENT RECEIPT';
|
||||||
|
receiptH4.setAttribute('data-en', 'PAYMENT RECEIPT');
|
||||||
|
receiptH4.setAttribute('data-ar', 'سند قبض');
|
||||||
|
}
|
||||||
|
|
||||||
|
const notesContainer = document.getElementById('receiptNotesContainer');
|
||||||
|
if (data.notes) {
|
||||||
|
document.getElementById('receiptNotes').textContent = data.notes;
|
||||||
|
notesContainer.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
notesContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const receiptModal = new bootstrap.Modal(document.getElementById('receiptModal'));
|
||||||
|
receiptModal.show();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelectorAll('.view-payments-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const invoiceId = this.getAttribute('data-id');
|
||||||
|
const tbody = document.getElementById('paymentsTableBody');
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center" data-en="Loading..." data-ar="جاري التحميل...">Loading...</td></tr>';
|
||||||
|
|
||||||
|
fetch(`index.php?action=get_payments&invoice_id=${invoiceId}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
if (data.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center" data-en="No payments found." data-ar="لا توجد مدفوعات.">No payments found.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.forEach(p => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>RCP-${p.id.toString().padStart(5, '0')}</td>
|
||||||
|
<td>${p.payment_date}</td>
|
||||||
|
<td>${p.payment_method}</td>
|
||||||
|
<td class="text-end fw-bold">OMR ${parseFloat(p.amount).toFixed(3)}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick="showReceipt(${p.id})">
|
||||||
|
<i class="bi bi-printer"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
window.printReceipt = function() {
|
||||||
|
const content = document.getElementById('printableReceipt').innerHTML;
|
||||||
|
const originalContent = document.body.innerHTML;
|
||||||
|
document.body.innerHTML = content;
|
||||||
|
window.print();
|
||||||
|
document.body.innerHTML = originalContent;
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
181
pages/sales_purchases_print_script.php
Normal file
181
pages/sales_purchases_print_script.php
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
window.viewAndPrintA4Invoice = function(data, autoPrint = true) {
|
||||||
|
if (!data) return;
|
||||||
|
// Reuse view logic
|
||||||
|
const invoiceDisplayNo = data.document_no || data.transaction_no || ((data.type === 'purchase' ? 'PUR-' : 'INV-') + data.id.toString().padStart(5, '0'));
|
||||||
|
document.getElementById('invNumber').textContent = invoiceDisplayNo;
|
||||||
|
document.getElementById('invDate').textContent = data.invoice_date;
|
||||||
|
document.getElementById('invPaymentType').textContent = data.payment_type ? data.payment_type.toUpperCase() : 'CASH';
|
||||||
|
document.getElementById('invCustomerName').textContent = data.customer_name || '---';
|
||||||
|
|
||||||
|
const phoneEl = document.getElementById('invCustomerPhone');
|
||||||
|
const phoneContainer = document.getElementById('invCustomerPhoneContainer');
|
||||||
|
if (data.customer_phone) {
|
||||||
|
phoneEl.textContent = data.customer_phone;
|
||||||
|
phoneContainer.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
phoneContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const taxIdEl = document.getElementById('invCustomerTaxId');
|
||||||
|
const taxIdContainer = document.getElementById('invCustomerTaxIdContainer');
|
||||||
|
if (data.customer_tax_id) {
|
||||||
|
taxIdEl.textContent = data.customer_tax_id;
|
||||||
|
taxIdContainer.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
taxIdContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('invAmountInWords').textContent = data.total_in_words || '';
|
||||||
|
|
||||||
|
const invOutletEl = document.getElementById('invOutletName');
|
||||||
|
if (invOutletEl) {
|
||||||
|
invOutletEl.textContent = data.outlet_name ? (data.outlet_name) : '';
|
||||||
|
invOutletEl.style.display = data.outlet_name ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('invPartyLabel').textContent = data.type === 'sale' ? 'Bill To / فاتورة إلى' : 'Bill From / فاتورة من';
|
||||||
|
document.getElementById('invPartyLabel').setAttribute('data-en', data.type === 'sale' ? 'Bill To' : 'Bill From');
|
||||||
|
document.getElementById('invPartyLabel').setAttribute('data-ar', data.type === 'sale' ? 'فاتورة إلى' : 'فاتورة من');
|
||||||
|
document.getElementById('invoiceTypeLabel').textContent = data.type;
|
||||||
|
document.getElementById('invoiceTypeLabel').className = 'badge text-uppercase ' + (data.type === 'sale' ? 'bg-success' : 'bg-warning');
|
||||||
|
|
||||||
|
const statusLabel = document.getElementById('invoiceStatusLabel');
|
||||||
|
let statusClass = 'bg-secondary';
|
||||||
|
let statusEn = data.status ? (data.status.charAt(0).toUpperCase() + data.status.slice(1)) : '---';
|
||||||
|
if (data.status === 'paid') statusClass = 'bg-success';
|
||||||
|
else if (data.status === 'unpaid') statusClass = 'bg-danger';
|
||||||
|
else if (data.status === 'partially_paid') {
|
||||||
|
statusClass = 'bg-warning text-dark';
|
||||||
|
statusEn = 'Partially Paid';
|
||||||
|
}
|
||||||
|
|
||||||
|
statusLabel.textContent = statusEn;
|
||||||
|
statusLabel.className = 'badge text-uppercase ' + statusClass;
|
||||||
|
|
||||||
|
const body = document.getElementById('invItemsBody');
|
||||||
|
body.innerHTML = '';
|
||||||
|
if (data.items) {
|
||||||
|
data.items.forEach(item => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${item.name_en} / ${item.name_ar}</td>
|
||||||
|
<td class="text-center">${item.quantity}</td>
|
||||||
|
<td class="text-end"><small><?= __('currency') ?></small> ${parseFloat(item.unit_price).toFixed(3)}</td>
|
||||||
|
<td class="text-end">${parseFloat(item.vat_rate || 0).toFixed(2)}%</td>
|
||||||
|
<td class="text-end"><small><?= __('currency') ?></small> ${parseFloat(item.total_price).toFixed(3)}</td>
|
||||||
|
`;
|
||||||
|
body.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const vatVal = parseFloat(data.vat_amount || 0);
|
||||||
|
const totalVal = parseFloat(data.total_amount || 0);
|
||||||
|
const grandTotalValue = (parseFloat(data.total_with_vat) || (totalVal + vatVal));
|
||||||
|
|
||||||
|
if (document.getElementById('invSubtotal')) document.getElementById('invSubtotal').innerHTML = '<small><?= __('currency') ?></small> ' + (grandTotalValue - vatVal).toFixed(3);
|
||||||
|
if (document.getElementById('invVatAmount')) document.getElementById('invVatAmount').innerHTML = '<small><?= __('currency') ?></small> ' + vatVal.toFixed(2);
|
||||||
|
if (document.getElementById('invGrandTotal')) document.getElementById('invGrandTotal').innerHTML = '<small><?= __('currency') ?></small> ' + grandTotalValue.toFixed(3);
|
||||||
|
|
||||||
|
if (document.getElementById('invPaidInfo')) document.getElementById('invPaidInfo').innerHTML = '<small><?= __('currency') ?></small> ' + parseFloat(data.paid_amount || 0).toFixed(3);
|
||||||
|
const balance = grandTotalValue - parseFloat(data.paid_amount || 0);
|
||||||
|
if (document.getElementById('invBalanceInfo')) document.getElementById('invBalanceInfo').innerHTML = '<small><?= __('currency') ?></small> ' + balance.toFixed(3);
|
||||||
|
|
||||||
|
// Generate QR Code for Zakat, Tax and Customs Authority (ZATCA) style or simple formal
|
||||||
|
const companyName = <?= json_encode($data['settings']['company_name'] ?? 'Accounting System') ?>;
|
||||||
|
const vatNo = <?= json_encode($data['settings']['vat_number'] ?? '') ?>;
|
||||||
|
const qrData = `Seller: ${companyName}\nVAT: ${vatNo}\nInvoice: ${invoiceDisplayNo}\nDate: ${data.invoice_date}\nTotal: ${grandTotalValue.toFixed(3)}`;
|
||||||
|
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=${encodeURIComponent(qrData)}`;
|
||||||
|
if (document.getElementById('invQrCode')) {
|
||||||
|
document.getElementById('invQrCode').innerHTML = `<img src="${qrUrl}" alt="QR Code" style="width: 100px; height: 100px;" class="border p-1 bg-white">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('viewInvoiceModal'));
|
||||||
|
viewModal.show();
|
||||||
|
|
||||||
|
if (autoPrint) {
|
||||||
|
setTimeout(() => { window.print(); }, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`index.php?action=get_payments&invoice_id=${data.id}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(payments => {
|
||||||
|
const paymentsBody = document.getElementById('invPaymentsBody');
|
||||||
|
const paymentsSection = document.getElementById('invPaymentsSection');
|
||||||
|
if (paymentsBody) paymentsBody.innerHTML = '';
|
||||||
|
if (payments && payments.length > 0) {
|
||||||
|
if (paymentsBody) {
|
||||||
|
payments.forEach(p => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `<td>${p.payment_date}</td><td>${p.payment_method}</td><td class="text-end fw-bold">OMR ${parseFloat(p.amount).toFixed(3)}</td>`;
|
||||||
|
paymentsBody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (paymentsSection) paymentsSection.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
if (paymentsSection) paymentsSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
}).catch(err => console.error('Error fetching payments:', err));
|
||||||
|
};
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$autoInvoicePayload = null;
|
||||||
|
if (
|
||||||
|
isset($_SESSION['trigger_invoice_modal'], $_SESSION['show_invoice_id'], $_SESSION['show_invoice_page']) &&
|
||||||
|
in_array($page, ['sales', 'purchases'], true) &&
|
||||||
|
$_SESSION['show_invoice_page'] === $page
|
||||||
|
) {
|
||||||
|
$autoInvoiceId = (int)$_SESSION['show_invoice_id'];
|
||||||
|
$autoInvoiceType = $page === 'purchases' ? 'purchase' : 'sale';
|
||||||
|
unset($_SESSION['trigger_invoice_modal'], $_SESSION['show_invoice_id'], $_SESSION['show_invoice_page']);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$autoTable = $autoInvoiceType === 'purchase' ? 'purchases' : 'invoices';
|
||||||
|
$autoPartyTable = $autoInvoiceType === 'purchase' ? 'suppliers' : 'customers';
|
||||||
|
$autoPartyCol = $autoInvoiceType === 'purchase' ? 'supplier_id' : 'customer_id';
|
||||||
|
$autoTaxColumn = entity_tax_column($autoPartyTable);
|
||||||
|
$autoTaxSelect = $autoTaxColumn !== null ? "c.$autoTaxColumn AS customer_tax_id" : "'' AS customer_tax_id";
|
||||||
|
$autoOutletSelect = "'' AS outlet_name";
|
||||||
|
$autoOutletJoin = '';
|
||||||
|
|
||||||
|
if (db_column_exists($autoTable, 'outlet_id') && db_table_exists('outlets')) {
|
||||||
|
$autoOutletSelect = 'o.name AS outlet_name';
|
||||||
|
$autoOutletJoin = 'LEFT JOIN outlets o ON doc.outlet_id = o.id';
|
||||||
|
}
|
||||||
|
|
||||||
|
$autoStmt = db()->prepare("SELECT doc.*, c.name AS customer_name, c.phone AS customer_phone, $autoTaxSelect, $autoOutletSelect
|
||||||
|
FROM $autoTable doc
|
||||||
|
LEFT JOIN $autoPartyTable c ON doc.$autoPartyCol = c.id
|
||||||
|
$autoOutletJoin
|
||||||
|
WHERE doc.id = ?
|
||||||
|
LIMIT 1");
|
||||||
|
$autoStmt->execute([$autoInvoiceId]);
|
||||||
|
$autoInvoicePayload = $autoStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($autoInvoicePayload) {
|
||||||
|
$autoItemSql = $autoInvoiceType === 'purchase'
|
||||||
|
? "SELECT pi.*, i.name_en, i.name_ar, i.vat_rate FROM purchase_items pi LEFT JOIN stock_items i ON pi.item_id = i.id WHERE pi.purchase_id = ?"
|
||||||
|
: "SELECT ii.*, i.name_en, i.name_ar, i.vat_rate FROM invoice_items ii LEFT JOIN stock_items i ON ii.item_id = i.id WHERE ii.invoice_id = ?";
|
||||||
|
$autoItemsStmt = db()->prepare($autoItemSql);
|
||||||
|
$autoItemsStmt->execute([$autoInvoiceId]);
|
||||||
|
$autoInvoicePayload['items'] = $autoItemsStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
$autoInvoicePayload['type'] = $autoInvoiceType;
|
||||||
|
$autoInvoicePayload['total_with_vat'] = (float)($autoInvoicePayload['total_with_vat'] ?? (($autoInvoicePayload['total_amount'] ?? 0) + ($autoInvoicePayload['vat_amount'] ?? 0)));
|
||||||
|
$autoInvoicePayload['paid_amount'] = (float)($autoInvoicePayload['paid_amount'] ?? 0);
|
||||||
|
$autoInvoicePayload['total_in_words'] = numberToWordsOMR($autoInvoicePayload['total_with_vat']);
|
||||||
|
$autoTransactionNo = trim((string)($autoInvoicePayload['transaction_no'] ?? ''));
|
||||||
|
$autoPrefix = $autoInvoiceType === 'purchase' ? 'PUR' : 'INV';
|
||||||
|
$autoInvoicePayload['document_no'] = ($autoInvoiceType === 'sale' && $autoTransactionNo !== '')
|
||||||
|
? $autoTransactionNo
|
||||||
|
: $autoPrefix . '-' . str_pad((string)$autoInvoicePayload['id'], 5, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$autoInvoicePayload = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?php if (!empty($autoInvoicePayload)): ?>
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window.viewAndPrintA4Invoice) {
|
||||||
|
window.viewAndPrintA4Invoice(<?= json_encode($autoInvoicePayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, true);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
<?php endif; ?>
|
||||||
237
pages/sales_purchases_print_view.php
Normal file
237
pages/sales_purchases_print_view.php
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
<style>
|
||||||
|
@media print {
|
||||||
|
.no-print, .sidebar, .topbar, .btn, .modal-header, .modal-footer, .d-print-none,
|
||||||
|
.modal-backdrop { display: none !important; }
|
||||||
|
body { background: white !important; margin: 0 !important; padding: 0 !important; overflow: visible !important; }
|
||||||
|
.main-content { margin: 0 !important; padding: 0 !important; background: white !important; }
|
||||||
|
|
||||||
|
/* Hide all modals by default */
|
||||||
|
.modal { display: none !important; }
|
||||||
|
|
||||||
|
/* Show ONLY the active modal */
|
||||||
|
.modal.show {
|
||||||
|
position: absolute !important;
|
||||||
|
left: 0 !important;
|
||||||
|
top: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
display: block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
background: white !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.show .modal-dialog { max-width: 100% !important; width: 100% !important; margin: 0 !important; padding: 0 !important; }
|
||||||
|
.modal.show .modal-content { border: none !important; box-shadow: none !important; background: white !important; }
|
||||||
|
.modal.show .modal-body { padding: 0 !important; margin: 0 !important; background: white !important; }
|
||||||
|
|
||||||
|
@page {
|
||||||
|
size: auto;
|
||||||
|
margin: 5mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-printable-container {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-4 { margin-top: 1rem !important; }
|
||||||
|
.mt-5 { margin-top: 1.5rem !important; }
|
||||||
|
.mb-4 { margin-bottom: 1rem !important; }
|
||||||
|
.p-5 { padding: 1.5rem !important; }
|
||||||
|
|
||||||
|
.table-bordered th, .table-bordered td { border: 1px solid #dee2e6 !important; }
|
||||||
|
.bg-light { background-color: #f8f9fa !important; -webkit-print-color-adjust: exact; }
|
||||||
|
.text-primary { color: #0d6efd !important; -webkit-print-color-adjust: exact; }
|
||||||
|
.badge { border: 1px solid #000; color: #000 !important; }
|
||||||
|
|
||||||
|
/* Ensure the modal is the only thing visible ONLY when a modal is open */
|
||||||
|
body.modal-open:not(.printing-receipt) { visibility: hidden !important; }
|
||||||
|
body.modal-open:not(.printing-receipt) .modal.show {
|
||||||
|
visibility: visible !important;
|
||||||
|
display: block !important;
|
||||||
|
position: absolute !important;
|
||||||
|
left: 0 !important;
|
||||||
|
top: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
body.modal-open:not(.printing-receipt) .modal.show * { visibility: visible !important; }
|
||||||
|
|
||||||
|
/* Old rules that caused blank pages for nested modals */
|
||||||
|
/* body.modal-open:not(.printing-receipt) > *:not(.modal):not(.swal2-container) { display: none !important; } */
|
||||||
|
/* body.modal-open:not(.printing-receipt) .main-content { display: none !important; } */
|
||||||
|
|
||||||
|
/* POS Receipt printing specific */
|
||||||
|
body.printing-receipt .modal { display: none !important; }
|
||||||
|
body.printing-receipt .modal-backdrop { display: none !important; }
|
||||||
|
body.printing-receipt #posPrintArea {
|
||||||
|
display: block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
position: absolute !important;
|
||||||
|
left: 0 !important;
|
||||||
|
top: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
z-index: 9999 !important;
|
||||||
|
background: white !important;
|
||||||
|
}
|
||||||
|
body.printing-receipt #posPrintArea * {
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.invoice-logo { max-height: 80px; width: auto; }
|
||||||
|
.invoice-header { border-bottom: 2px solid #333; padding-bottom: 20px; }
|
||||||
|
.invoice-title { font-size: 2.5rem; color: #333; letter-spacing: 2px; }
|
||||||
|
.invoice-info-card { background: #f8f9fa; border-radius: 8px; padding: 15px; height: 100%; }
|
||||||
|
.table-formal thead th { background: #333; color: #fff; border: none; text-transform: uppercase; font-size: 0.85rem; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="modal fade" id="viewInvoiceModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content border-0 shadow">
|
||||||
|
<div class="modal-header d-print-none">
|
||||||
|
<h5 class="modal-title" data-en="View Invoice" data-ar="عرض الفاتورة">View Invoice</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-0" id="invoicePrintableArea">
|
||||||
|
<div class="p-4 invoice-printable-container">
|
||||||
|
<div class="invoice-header mb-4">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-6">
|
||||||
|
<?php
|
||||||
|
$logo = $data['settings']['company_logo'] ?? $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||||
|
if ($logo):
|
||||||
|
?>
|
||||||
|
<img src="<?= htmlspecialchars($logo) ?>" alt="Logo" class="invoice-logo mb-3">
|
||||||
|
<?php endif; ?>
|
||||||
|
<h3 class="mb-1 fw-bold"><?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?></h3>
|
||||||
|
<p class="text-muted small mb-0"><?= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?></p>
|
||||||
|
<p class="text-muted small mb-0 fw-bold text-primary" id="invOutletName" style="display:none;"></p>
|
||||||
|
<p class="text-muted small mb-0">VAT: <?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?></p>
|
||||||
|
<?php if (!empty($data['settings']['company_phone'])): ?>
|
||||||
|
<p class="text-muted small mb-0">Tel: <?= htmlspecialchars($data['settings']['company_phone']) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<h1 class="invoice-title fw-bold mb-0 text-uppercase">Tax Invoice / فاتورة ضريبية</h1>
|
||||||
|
<div class="mt-2"><span id="invoiceTypeLabel" class="badge"></span></div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<p class="mb-0 fs-5">Invoice No / رقم الفاتورة: <strong id="invNumber" class="text-primary"></strong></p>
|
||||||
|
<p class="mb-0">Date / التاريخ: <span id="invDate" class="fw-bold"></span></p>
|
||||||
|
<p class="mb-0 small">Status / الحالة: <span id="invoiceStatusLabel"></span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-4 g-3">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="invoice-info-card">
|
||||||
|
<p class="text-muted small text-uppercase fw-bold mb-2 border-bottom pb-1" id="invPartyLabel">Bill To / فاتورة إلى</p>
|
||||||
|
<h5 class="mb-1 fw-bold"><span id="invCustomerName"></span></h5>
|
||||||
|
<p class="small text-muted mb-0" id="invCustomerTaxIdContainer">VAT / الضريبة: <span id="invCustomerTaxId"></span></p>
|
||||||
|
<p class="small text-muted mb-0" id="invCustomerPhoneContainer">Phone / الهاتف: <span id="invCustomerPhone"></span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="invoice-info-card text-end">
|
||||||
|
<p class="text-muted small text-uppercase fw-bold mb-2 border-bottom pb-1">Payment Details / تفاصيل الدفع</p>
|
||||||
|
<p class="mb-1">Method / الطريقة: <strong id="invPaymentType"></strong></p>
|
||||||
|
<p class="mb-0 small text-muted">Currency / العملة: <strong>OMR / ريال عماني</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-bordered table-formal">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Item Description / وصف الصنف</th>
|
||||||
|
<th class="text-center" style="width: 80px;">Qty / الكمية</th>
|
||||||
|
<th class="text-end" style="width: 120px;">Unit Price / سعر الوحدة</th>
|
||||||
|
<th class="text-end" style="width: 100px;">VAT % / الضريبة %</th>
|
||||||
|
<th class="text-end" style="width: 150px;">Total / الإجمالي</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="invItemsBody"></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="p-3 bg-light rounded" style="min-height: 80px;">
|
||||||
|
<p class="text-muted small text-uppercase fw-bold mb-1">Amount in Words / المبلغ بالحروف</p>
|
||||||
|
<p id="invAmountInWords" class="small fw-bold mb-0"></p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<p class="text-muted small text-uppercase fw-bold mb-1">Terms & Conditions / الشروط والأحكام</p>
|
||||||
|
<ul class="small text-muted ps-3 mb-0">
|
||||||
|
<li>Goods once sold will not be taken back or exchanged.</li>
|
||||||
|
<li>Payment is due within the agreed credit period.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="text-muted">Subtotal (Excl. VAT) / المجموع الفرعي (دون الضريبة)</span>
|
||||||
|
<span id="invSubtotal" class="fw-bold text-nowrap"></span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="text-muted">VAT Amount / مبلغ الضريبة</span>
|
||||||
|
<span id="invVatAmount" class="fw-bold text-nowrap"></span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-3 border-top pt-2">
|
||||||
|
<span class="h4 fw-bold">Grand Total / المجموع الكلي</span>
|
||||||
|
<span id="invGrandTotal" class="h4 fw-bold text-primary text-nowrap"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="invPaymentsSection" class="mt-4 border-top pt-3">
|
||||||
|
<p class="text-muted small text-uppercase fw-bold mb-2">Payment Tracking / تتبع الدفع</p>
|
||||||
|
<table class="table table-sm table-bordered small mb-3">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Date / التاريخ</th>
|
||||||
|
<th>Method / الطريقة</th>
|
||||||
|
<th class="text-end">Amount / المبلغ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="invPaymentsBody"></tbody>
|
||||||
|
</table>
|
||||||
|
<div class="bg-light p-3 rounded">
|
||||||
|
<div class="d-flex justify-content-between small mb-1">
|
||||||
|
<span>Paid Amount / المبلغ المدفوع</span>
|
||||||
|
<span id="invPaidInfo" class="text-success fw-bold"></span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between small fw-bold">
|
||||||
|
<span>Balance Due / الرصيد المتبقي</span>
|
||||||
|
<span id="invBalanceInfo" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 pt-3 border-top text-center">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="border-top pt-2 mx-auto" style="width: 150px;">
|
||||||
|
<p class="small text-muted mb-0">Customer Signature / توقيع العميل</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<div id="invQrCode" class="d-flex justify-content-center"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="border-top pt-2 mx-auto" style="width: 150px;">
|
||||||
|
<p class="small text-muted mb-0">Authorized Signatory / التوقيع المعتمد</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted x-small mt-3 mb-0">Generated by <?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?> | Visit us at <?= $_SERVER['HTTP_HOST'] ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer d-print-none">
|
||||||
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Close" data-ar="إغلاق">Close</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="window.print()"><i class="bi bi-printer me-2"></i>Print Invoice</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
179
pages/sales_purchases_save_logic.php
Normal file
179
pages/sales_purchases_save_logic.php
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
<?php
|
||||||
|
// Shared Sales/Purchases create/update handlers extracted from index.php
|
||||||
|
// to reduce regression risk while preserving the existing behavior.
|
||||||
|
// Invoices
|
||||||
|
if (isset($_POST['add_invoice'])) {
|
||||||
|
$db = db();
|
||||||
|
try {
|
||||||
|
$db->beginTransaction();
|
||||||
|
$type = $_POST['type'] ?? 'sale';
|
||||||
|
$table = ($type === 'purchase') ? 'purchases' : 'invoices';
|
||||||
|
$item_table = ($type === 'purchase') ? 'purchase_items' : 'invoice_items';
|
||||||
|
$cust_supplier_col = ($type === 'purchase') ? 'supplier_id' : 'customer_id';
|
||||||
|
$fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
|
||||||
|
|
||||||
|
$cust_id = (int)$_POST['customer_id'];
|
||||||
|
$inv_date = $_POST['invoice_date'] ?: date('Y-m-d');
|
||||||
|
$due_date = $_POST['due_date'] ?: null;
|
||||||
|
$status = $_POST['status'] ?? 'pending';
|
||||||
|
$pay_type = $_POST['payment_type'] ?? 'cash';
|
||||||
|
|
||||||
|
$items = $_POST['item_ids'] ?? [];
|
||||||
|
if (empty($items)) {
|
||||||
|
throw new Exception("Please add at least one item.");
|
||||||
|
}
|
||||||
|
$qtys = $_POST['quantities'] ?? [];
|
||||||
|
$prices = $_POST['prices'] ?? [];
|
||||||
|
|
||||||
|
$total_subtotal = 0;
|
||||||
|
$total_vat = 0;
|
||||||
|
|
||||||
|
foreach ($items as $i => $item_id) {
|
||||||
|
if (!$item_id) continue;
|
||||||
|
$qty = (float)$qtys[$i];
|
||||||
|
$price = (float)$prices[$i];
|
||||||
|
$subtotal = $qty * $price;
|
||||||
|
|
||||||
|
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
|
||||||
|
$stmtVat->execute([$item_id]);
|
||||||
|
$vatRate = (float)$stmtVat->fetchColumn();
|
||||||
|
|
||||||
|
$vatAmount = $subtotal * ($vatRate / 100);
|
||||||
|
$total_subtotal += $subtotal;
|
||||||
|
$total_vat += $vatAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
$total_with_vat = $total_subtotal + $total_vat;
|
||||||
|
$paid = (float)($_POST['paid_amount'] ?? 0);
|
||||||
|
if ($status === 'paid') $paid = $total_with_vat;
|
||||||
|
|
||||||
|
$stmt = $db->prepare("INSERT INTO $table ($cust_supplier_col, invoice_date, due_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$cust_id, $inv_date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid]);
|
||||||
|
$inv_id = $db->lastInsertId();
|
||||||
|
if (db_column_exists($table, 'outlet_id')) {
|
||||||
|
$db->prepare("UPDATE $table SET outlet_id = ? WHERE id = ?")->execute([current_outlet_id(), $inv_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$items_for_journal = [];
|
||||||
|
foreach ($items as $i => $item_id) {
|
||||||
|
if (!$item_id) continue;
|
||||||
|
$qty = (float)$qtys[$i];
|
||||||
|
$price = (float)$prices[$i];
|
||||||
|
$subtotal = $qty * $price;
|
||||||
|
|
||||||
|
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
|
||||||
|
$stmtVat->execute([$item_id]);
|
||||||
|
$vatRate = (float)$stmtVat->fetchColumn();
|
||||||
|
$vatAmount = $subtotal * ($vatRate / 100);
|
||||||
|
|
||||||
|
$db->prepare("INSERT INTO $item_table ($fk_col, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$inv_id, $item_id, $qty, $price, $vatAmount, $subtotal]);
|
||||||
|
|
||||||
|
// Update stock
|
||||||
|
$change = ($type === 'sale') ? -$qty : $qty;
|
||||||
|
update_stock($item_id, $change);
|
||||||
|
$items_for_journal[] = ['id' => $item_id, 'qty' => $qty];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accounting
|
||||||
|
if ($type === 'sale') {
|
||||||
|
recordSaleJournal($inv_id, $total_with_vat, $inv_date, $items_for_journal, $total_vat);
|
||||||
|
} else {
|
||||||
|
// For purchases, you might have recordPurchaseJournal, but let's check if it exists
|
||||||
|
if (function_exists('recordPurchaseJournal')) {
|
||||||
|
recordPurchaseJournal($inv_id, $total_with_vat, $inv_date, $items_for_journal, $total_vat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->commit();
|
||||||
|
$_SESSION['trigger_invoice_modal'] = true;
|
||||||
|
$_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'));
|
||||||
|
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_POST['edit_invoice'])) {
|
||||||
|
$db = db();
|
||||||
|
try {
|
||||||
|
$db->beginTransaction();
|
||||||
|
$id = (int)$_POST['invoice_id'];
|
||||||
|
$type = ($page === 'purchases') ? 'purchase' : 'sale';
|
||||||
|
$table = ($type === 'purchase') ? 'purchases' : 'invoices';
|
||||||
|
$item_table = ($type === 'purchase') ? 'purchase_items' : 'invoice_items';
|
||||||
|
$cust_supplier_col = ($type === 'purchase') ? 'supplier_id' : 'customer_id';
|
||||||
|
$fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
|
||||||
|
|
||||||
|
$cust_id = (int)$_POST['customer_id'];
|
||||||
|
$date = $_POST['invoice_date'] ?: date('Y-m-d');
|
||||||
|
$due_date = $_POST['due_date'] ?: null;
|
||||||
|
$status = $_POST['status'] ?? 'pending';
|
||||||
|
$pay_type = $_POST['payment_type'] ?? 'cash';
|
||||||
|
|
||||||
|
$items = $_POST['item_ids'] ?? [];
|
||||||
|
$qtys = $_POST['quantities'] ?? [];
|
||||||
|
$prices = $_POST['prices'] ?? [];
|
||||||
|
|
||||||
|
$total_subtotal = 0;
|
||||||
|
$total_vat = 0;
|
||||||
|
|
||||||
|
foreach ($items as $i => $item_id) {
|
||||||
|
if (!$item_id) continue;
|
||||||
|
$qty = (float)$qtys[$i];
|
||||||
|
$price = (float)$prices[$i];
|
||||||
|
$subtotal = $qty * $price;
|
||||||
|
|
||||||
|
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
|
||||||
|
$stmtVat->execute([$item_id]);
|
||||||
|
$vatRate = (float)$stmtVat->fetchColumn();
|
||||||
|
|
||||||
|
$vatAmount = $subtotal * ($vatRate / 100);
|
||||||
|
$total_subtotal += $subtotal;
|
||||||
|
$total_vat += $vatAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
$total_with_vat = $total_subtotal + $total_vat;
|
||||||
|
$paid = (float)($_POST['paid_amount'] ?? 0);
|
||||||
|
if ($status === 'paid') $paid = $total_with_vat;
|
||||||
|
|
||||||
|
$db->prepare("UPDATE $table SET $cust_supplier_col = ?, invoice_date = ?, due_date = ?, status = ?, payment_type = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, paid_amount = ? WHERE id = ?")
|
||||||
|
->execute([$cust_id, $date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid, $id]);
|
||||||
|
if (db_column_exists($table, 'outlet_id')) {
|
||||||
|
$db->prepare("UPDATE $table SET outlet_id = COALESCE(outlet_id, ?) WHERE id = ?")->execute([current_outlet_id(), $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revert stock for old items
|
||||||
|
$stmtOld = $db->prepare("SELECT item_id, quantity FROM $item_table WHERE $fk_col = ?");
|
||||||
|
$stmtOld->execute([$id]);
|
||||||
|
$oldItems = $stmtOld->fetchAll();
|
||||||
|
foreach ($oldItems as $old) {
|
||||||
|
$change = ($type === 'sale') ? (float)$old['quantity'] : -(float)$old['quantity'];
|
||||||
|
update_stock($old['item_id'], $change);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete old items
|
||||||
|
$db->prepare("DELETE FROM $item_table WHERE $fk_col = ?")->execute([$id]);
|
||||||
|
|
||||||
|
// Insert new items and update stock
|
||||||
|
foreach ($items as $i => $item_id) {
|
||||||
|
if (!$item_id) continue;
|
||||||
|
$qty = (float)$qtys[$i];
|
||||||
|
$price = (float)$prices[$i];
|
||||||
|
$subtotal = $qty * $price;
|
||||||
|
|
||||||
|
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
|
||||||
|
$stmtVat->execute([$item_id]);
|
||||||
|
$vatRate = (float)$stmtVat->fetchColumn();
|
||||||
|
$vatAmount = $subtotal * ($vatRate / 100);
|
||||||
|
|
||||||
|
$db->prepare("INSERT INTO $item_table ($fk_col, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$id, $item_id, $qty, $price, $vatAmount, $subtotal]);
|
||||||
|
|
||||||
|
$change = ($type === 'sale') ? -$qty : $qty;
|
||||||
|
update_stock($item_id, $change);
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->commit();
|
||||||
|
$msg = ($type === 'purchase' ? "Purchase" : "Invoice") . " updated successfully!";
|
||||||
|
redirectWithMessage($msg, "index.php?page=" . ($type === 'purchase' ? 'purchases' : 'sales'));
|
||||||
|
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
|
||||||
|
}
|
||||||
35
pages/users_role_permissions_script.php
Normal file
35
pages/users_role_permissions_script.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Select All by Action (View, Add, Edit, Delete)
|
||||||
|
$('.select-all-action').on('change', function() {
|
||||||
|
const action = $(this).data('action');
|
||||||
|
const checked = $(this).is(':checked');
|
||||||
|
const modal = $(this).closest('.modal');
|
||||||
|
modal.find('.perm-check[data-action="' + action + '"]').prop('checked', checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select All by Row
|
||||||
|
$('.select-all-row').on('change', function() {
|
||||||
|
const checked = $(this).is(':checked');
|
||||||
|
$(this).closest('.module-row').find('.perm-check').prop('checked', checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select All by Group
|
||||||
|
$('.select-all-group').on('change', function() {
|
||||||
|
const checked = $(this).is(':checked');
|
||||||
|
$(this).closest('.permission-group-container').find('.perm-check, .select-all-row').prop('checked', checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select All Button
|
||||||
|
$('.select-all-btn').on('click', function() {
|
||||||
|
const modal = $($(this).data('modal'));
|
||||||
|
modal.find('.perm-check, .select-all-action, .select-all-group, .select-all-row').prop('checked', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deselect All Button
|
||||||
|
$('.deselect-all-btn').on('click', function() {
|
||||||
|
const modal = $($(this).data('modal'));
|
||||||
|
modal.find('.perm-check, .select-all-action, .select-all-group, .select-all-row').prop('checked', false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
190
post_debug.log
190
post_debug.log
@ -1,190 +0,0 @@
|
|||||||
2026-02-25 09:56:38 - POST: {"action":"translate","text":"Product 2","target":"ar"}
|
|
||||||
2026-02-25 12:57:47 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":25}]","total_amount":"25","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":7,\"qty\":1,\"price\":10,\"vat_rate\":0,\"vat_amount\":0},{\"id\":8,\"qty\":1,\"price\":15,\"vat_rate\":0,\"vat_amount\":0}]"}
|
|
||||||
2026-02-25 13:23:21 - POST: {"sync_accounting":""}
|
|
||||||
2026-02-25 13:30:19 - POST: {"action":"save_theme","theme":"nord"}
|
|
||||||
2026-02-25 13:30:28 - POST: {"action":"save_theme","theme":"forest"}
|
|
||||||
2026-02-25 13:30:36 - POST: {"action":"save_theme","theme":"citrus"}
|
|
||||||
2026-02-25 13:30:39 - POST: {"action":"save_theme","theme":"forest"}
|
|
||||||
2026-02-25 13:30:41 - POST: {"action":"save_theme","theme":"nord"}
|
|
||||||
2026-02-25 13:31:58 - POST: {"id":"","name":"Nizwa Branch","phone":"9689555","address":"Nizwa","status":"active","add_outlet":""}
|
|
||||||
2026-02-25 17:08:42 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":15.45}]","total_amount":"15.45","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":1,\"qty\":1,\"price\":0.45,\"vat_rate\":0,\"vat_amount\":0},{\"id\":8,\"qty\":1,\"price\":15,\"vat_rate\":0,\"vat_amount\":0}]"}
|
|
||||||
2026-02-25 17:09:31 - POST: {"id":"37","delete_invoice":""}
|
|
||||||
2026-02-26 02:56:35 - POST: {"import_items":""}
|
|
||||||
2026-02-26 02:57:11 - POST: {"import_items":""}
|
|
||||||
2026-02-26 03:01:00 - POST: {"import_items":""}
|
|
||||||
2026-02-26 03:01:48 - POST: {"action":"translate","text":"LAMING PINEAPPLE 454G","target":"ar"}
|
|
||||||
2026-02-26 03:05:29 - POST: {"action":"translate","text":"LAMING RED KIDNEY BEANS 425","target":"ar"}
|
|
||||||
2026-02-26 03:09:07 - POST: {"id":"62","name_en":"LAMING RED KIDNEY BEANS 425","name_ar":"LAMING RED KIDNEY BEANS 425","sku":"000023071605","category_id":"2","unit_id":"1","supplier_id":"5","sale_price":"0.25","purchase_price":"0.2","stock_quantity":"0","min_stock_level":"0","vat_rate":"0.00","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-02-26 03:09:24 - POST: {"action":"translate","text":"LAMING RED KIDNEY BEANS 425","target":"ar"}
|
|
||||||
2026-02-26 03:09:34 - POST: {"id":"62","name_en":"LAMING RED KIDNEY BEANS 425","name_ar":"\u0641\u0627\u0635\u0648\u0644\u064a\u0627\u0621 \u062d\u0645\u0631\u0627\u0621 \u0644\u0627\u0645\u064a\u0646\u062c 425","sku":"000023071605","category_id":"2","unit_id":"1","supplier_id":"5","sale_price":"0.25","purchase_price":"0.2","stock_quantity":"0","min_stock_level":"0","vat_rate":"0.00","expiry_date":"","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-02-26 03:11:08 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":1.7}]","total_amount":"1.7000000000000002","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":48,\"qty\":1,\"price\":1.3,\"vat_rate\":0,\"vat_amount\":0},{\"id\":17,\"qty\":1,\"price\":0.4,\"vat_rate\":0,\"vat_amount\":0}]"}
|
|
||||||
2026-02-26 05:17:43 - POST: {"action":"save_theme","theme":"forest"}
|
|
||||||
2026-02-26 05:17:47 - POST: {"action":"save_theme","theme":"default"}
|
|
||||||
2026-02-26 05:47:24 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":2.6}]","total_amount":"2.6","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":19,\"qty\":1,\"price\":1.2,\"vat_rate\":0,\"vat_amount\":0},{\"id\":12,\"qty\":1,\"price\":1.2,\"vat_rate\":0,\"vat_amount\":0},{\"id\":13,\"qty\":1,\"price\":0.2,\"vat_rate\":0,\"vat_amount\":0}]"}
|
|
||||||
2026-02-26 06:03:42 - POST: {"device_name":"scale 1","device_type":"scale","connection_type":"network","ip_address":"192.168.1.10","port":"9100","baud_rate":"","add_pos_device":""}
|
|
||||||
2026-02-26 08:15:07 - POST: {"id":"2","name":"Nizwa Branch","phone":"9689555","ctr_number":"5012641","vat_number":"OM1254","address":"Nizwa","status":"active","edit_outlet":""}
|
|
||||||
2026-02-26 08:15:57 - POST: {"id":"1","name":"Moosa Ali Al-Abri","phone":"99359472","ctr_number":"514899","vat_number":"OM99888","address":"AL Hamra\r\n","status":"active","edit_outlet":""}
|
|
||||||
2026-02-26 08:16:37 - POST: {"id":"1","name":"Main Branch","phone":"99359472","ctr_number":"514899","vat_number":"OM99888","address":"AL Hamra\r\n","status":"active","edit_outlet":""}
|
|
||||||
2026-02-26 08:17:31 - POST: {"name":"Counter1","add_cash_register":""}
|
|
||||||
2026-02-26 08:17:56 - POST: {"open_register":"1","register_id":"4","opening_balance":"0"}
|
|
||||||
2026-02-26 17:58:24 - POST: {"username":"admin","password":"admin123","login":""}
|
|
||||||
2026-02-26 17:58:40 - POST: {"username":"admin","password":"admin123","login":""}
|
|
||||||
2026-02-26 17:58:46 - POST: {"username":"admin","password":"admin123","login":""}
|
|
||||||
2026-02-26 17:59:03 - POST: {"username":"admin","password":"admin123","login":""}
|
|
||||||
2026-02-26 17:59:14 - POST: {"username":"admin","password":"admin","login":""}
|
|
||||||
2026-03-17 09:25:20 - POST: {"start_trial":""}
|
|
||||||
2026-03-17 09:25:46 - POST: {"username":"admin","password":"admin","login":""}
|
|
||||||
2026-03-17 09:58:21 - POST: {"license_key":"FLAT-8E07-54E9-B427","activate":""}
|
|
||||||
2026-03-17 10:12:13 - POST: {"username":"admin","password":"admin","login":""}
|
|
||||||
2026-03-17 10:12:37 - POST: {"username":"moosa","password":"moosa123","login":""}
|
|
||||||
2026-03-17 13:25:29 - POST: {"name":"Counter1","add_cash_register":""}
|
|
||||||
2026-03-17 13:25:44 - POST: {"open_register":"1","register_id":"3","opening_balance":"0"}
|
|
||||||
2026-03-17 15:07:15 - POST: {"action":"translate","text":"Food Stuff","target":"ar"}
|
|
||||||
2026-03-17 15:07:23 - POST: {"name_en":"Food Stuff","name_ar":"\u0645\u0648\u0627\u062f \u063a\u0630\u0627\u0626\u064a\u0629","add_category":""}
|
|
||||||
2026-03-17 15:07:52 - POST: {"action":"translate","text":"piece","target":"ar"}
|
|
||||||
2026-03-17 15:08:10 - POST: {"name_en":"piece","short_en":"pcs","name_ar":"\u062d\u0628\u0629","short_ar":"\u062d\u0628\u0629","add_unit":""}
|
|
||||||
2026-03-17 15:08:46 - POST: {"name_en":"Tomato","name_ar":"\u0637\u0645\u0627\u0637\u0645","category_id":"3","unit_id":"4","supplier_id":"","sku":"241604476040","sale_price":"0.25","purchase_price":"0.2","stock_quantity":"0.000","min_stock_level":"0.000","vat_rate":"0","expiry_date":"","promotion_start":"","promotion_end":"","promotion_percent":"0.00","add_item":""}
|
|
||||||
2026-03-17 15:09:17 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":0.25}]","total_amount":"0.25","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":7,\"qty\":1,\"price\":0.25,\"vat_rate\":0,\"vat_amount\":0}]"}
|
|
||||||
2026-03-17 15:09:44 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":0.25}]","total_amount":"0.25","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":7,\"qty\":1,\"price\":0.25,\"vat_rate\":0,\"vat_amount\":0}]"}
|
|
||||||
2026-03-17 15:15:26 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":0.5}]","total_amount":"0.5","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":7,\"qty\":2,\"price\":0.25,\"vat_rate\":0,\"vat_amount\":0}]"}
|
|
||||||
2026-03-17 17:47:03 - POST: {"action":"translate","text":"Kilogram","target":"ar"}
|
|
||||||
2026-03-17 17:47:11 - POST: {"name_en":"Kilogram","short_en":"Kg","name_ar":"\u0643\u064a\u0644\u0648\u063a\u0631\u0627\u0645","short_ar":"\u0643\u062c\u0645","add_unit":""}
|
|
||||||
2026-03-17 17:47:27 - POST: {"id":"7","name_en":"Tomato","name_ar":"\u0637\u0645\u0627\u0637\u0645","sku":"241604476040","category_id":"3","unit_id":"5","supplier_id":"","sale_price":"0.25","purchase_price":"0.2","vat_rate":"0.00","stock_quantity":"0","min_stock_level":"0","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-03-17 18:07:32 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":0.25}]","total_amount":"0.25","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":7,\"qty\":1,\"price\":0.25,\"vat_rate\":0,\"vat_amount\":0}]"}
|
|
||||||
2026-03-17 18:14:21 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":0.25}]","total_amount":"0.25","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":7,\"qty\":1,\"price\":0.25,\"vat_rate\":0,\"vat_amount\":0}]"}
|
|
||||||
2026-03-18 02:00:56 - POST: {"action":"save_theme","theme":"ocean"}
|
|
||||||
2026-03-18 02:01:01 - POST: {"action":"save_theme","theme":"sunset"}
|
|
||||||
2026-03-18 02:01:26 - POST: {"action":"save_theme","theme":"citrus"}
|
|
||||||
2026-03-18 02:01:29 - POST: {"action":"save_theme","theme":"default"}
|
|
||||||
2026-03-18 02:31:31 - POST: {"action":"save_theme","theme":"sunset"}
|
|
||||||
2026-03-18 02:31:34 - POST: {"action":"save_theme","theme":"ocean"}
|
|
||||||
2026-03-18 02:31:37 - POST: {"action":"save_theme","theme":"default"}
|
|
||||||
2026-03-18 02:31:40 - POST: {"action":"save_theme","theme":"sunset"}
|
|
||||||
2026-03-18 02:31:50 - POST: {"action":"save_theme","theme":"default"}
|
|
||||||
2026-03-18 06:08:50 - POST: {"type":"customer","name":"Moosa Ali Al-Abri","email":"aalabry@gmail.com","phone":"99359472","tax_id":"","balance":"0.000","add_customer":""}
|
|
||||||
2026-03-18 06:29:34 - POST: {"settings":{"company_name":"Bahjet Al-Safa Trading","company_phone":"99359472","company_email":"aalabry@gmail.com","vat_number":"OM25418","company_address":"AL Hamra\r\nOman","timezone":"Asia\/Muscat","allow_zero_stock_sell":"1","loyalty_enabled":"0","loyalty_points_per_unit":"1","loyalty_redeem_points_per_unit":"100"},"update_settings":""}
|
|
||||||
2026-03-18 06:30:25 - POST: {"type":"sale","customer_id":"7","invoice_date":"2026-03-18","due_date":"","payment_type":"cash","status":"paid","paid_amount":"0.000","item_ids":["7"],"quantities":["5"],"prices":["0.250"],"add_invoice":""}
|
|
||||||
2026-03-18 06:31:00 - POST: {"type":"sale","customer_id":"7","invoice_date":"2026-03-18","due_date":"","payment_type":"cash","status":"unpaid","paid_amount":"0.000","item_ids":["7"],"quantities":["1"],"prices":["0.250"],"add_invoice":""}
|
|
||||||
2026-03-18 06:42:39 - POST: {"type":"sale","customer_id":"7","invoice_date":"2026-03-18","due_date":"","payment_type":"cash","status":"unpaid","paid_amount":"0.000","item_ids":["7"],"quantities":["1"],"prices":["0.250"],"add_invoice":""}
|
|
||||||
2026-03-18 06:43:05 - POST: {"invoice_id":"31","customer_id":"7","invoice_date":"2026-03-18","due_date":"","payment_type":"cash","status":"paid","paid_amount":"0.000","item_ids":["7"],"quantities":["1.00"],"prices":["0.250"],"edit_invoice":""}
|
|
||||||
2026-03-18 07:19:13 - POST: {"id":"","name":"Nizwa Outlet","phone":"","address":"","status":"active","add_outlet":""}
|
|
||||||
2026-03-18 09:46:16 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":1.25}]","total_amount":"1.25","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":7,\"qty\":5,\"price\":0.25,\"vat_rate\":0,\"vat_amount\":0}]"}
|
|
||||||
2026-03-18 09:47:04 - POST: {"type":"sale","customer_id":"7","invoice_date":"2026-03-18","due_date":"","payment_type":"cash","status":"unpaid","paid_amount":"0.000","item_ids":["7"],"quantities":["1"],"prices":["0.250"],"add_invoice":""}
|
|
||||||
2026-03-18 09:53:37 - POST: {"type":"sale","customer_id":"7","invoice_date":"2026-03-18","due_date":"","payment_type":"cash","status":"unpaid","paid_amount":"0.000","item_ids":["7"],"quantities":["1"],"prices":["0.250"],"add_invoice":""}
|
|
||||||
2026-03-18 09:59:41 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":0.25}]","total_amount":"0.25","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":7,\"qty\":1,\"price\":0.25,\"vat_rate\":0,\"vat_amount\":0}]"}
|
|
||||||
2026-03-18 10:32:41 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":0.25}]","total_amount":"0.25","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":7,\"qty\":1,\"price\":0.25,\"vat_rate\":0,\"vat_amount\":0}]"}
|
|
||||||
2026-03-18 14:09:03 - POST: {"action":"save_theme","theme":"dark"}
|
|
||||||
2026-03-18 14:09:06 - POST: {"action":"save_theme","theme":"ocean"}
|
|
||||||
2026-03-18 14:09:10 - POST: {"action":"save_theme","theme":"forest"}
|
|
||||||
2026-03-18 14:09:12 - POST: {"action":"save_theme","theme":"dracula"}
|
|
||||||
2026-03-18 14:09:15 - POST: {"action":"save_theme","theme":"default"}
|
|
||||||
2026-03-18 18:14:49 - POST: {"import_items":""}
|
|
||||||
2026-03-18 18:15:31 - POST: {"import_items":""}
|
|
||||||
2026-03-18 18:24:49 - POST: {"import_items":""}
|
|
||||||
2026-03-18 18:26:54 - POST: {"import_items":""}
|
|
||||||
2026-03-18 18:29:45 - POST: {"import_items":""}
|
|
||||||
2026-03-18 18:30:17 - POST: {"action":"translate","text":"LAMING RED KIDNEY BEANS ","target":"ar"}
|
|
||||||
2026-03-18 18:30:49 - POST: {"id":"62","name_en":"LAMING RED KIDNEY BEANS ","name_ar":"\u0641\u0627\u0635\u0648\u0644\u064a\u0627\u0621 \u062d\u0645\u0631\u0627\u0621 \u0643\u064a\u062f\u0646\u064a \u0644\u0627\u0645\u064a\u0646\u063a","sku":"000023071605","category_id":"3","unit_id":"4","supplier_id":"","sale_price":"0.25","purchase_price":"0.2","stock_quantity":"0","min_stock_level":"0","vat_rate":"5","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-03-18 18:31:04 - POST: {"action":"translate","text":"LAMING PINEAPPLE 454G","target":"ar"}
|
|
||||||
2026-03-18 18:31:15 - POST: {"id":"61","name_en":"LAMING PINEAPPLE 454G","name_ar":"\u0623\u0646\u0627\u0646\u0627\u0633 \u0644\u0627\u0645\u0646\u062c 454\u063a","sku":"000023071599","category_id":"3","unit_id":"4","supplier_id":"","sale_price":"0.4","purchase_price":"0.346","stock_quantity":"0","min_stock_level":"0","vat_rate":"5","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-03-18 18:31:26 - POST: {"action":"translate","text":"\u0644\u0627\u0645\u064a\u0646\u063a \u0641\u0637\u0631 \u0637\u0627\u0632\u062c \u0643\u0627\u0645\u0644425\u063a","target":"en"}
|
|
||||||
2026-03-18 18:31:34 - POST: {"id":"60","name_en":"Laming Whole Fresh Mushroom 425g","name_ar":"\u0644\u0627\u0645\u064a\u0646\u063a \u0641\u0637\u0631 \u0637\u0627\u0632\u062c \u0643\u0627\u0645\u0644425\u063a","sku":"000023071582","category_id":"3","unit_id":"4","supplier_id":"","sale_price":"0.55","purchase_price":"0.425","stock_quantity":"0","min_stock_level":"0","vat_rate":"0.00","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-03-18 18:31:54 - POST: {"action":"translate","text":"LAMING FOUL MEDAMES 397G","target":"ar"}
|
|
||||||
2026-03-18 18:32:01 - POST: {"id":"59","name_en":"LAMING FOUL MEDAMES 397G","name_ar":"\u0641\u0648\u0644 \u0645\u062f\u0645\u0633 \u0644\u0627\u0645\u064a\u0646\u062c 397\u063a","sku":"000023071575","category_id":"3","unit_id":"4","supplier_id":"","sale_price":"0.2","purchase_price":"0.157","stock_quantity":"0","min_stock_level":"0","vat_rate":"5","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-03-18 18:32:15 - POST: {"action":"translate","text":"\u0641\u0627\u0635\u0648\u0644\u064a\u0627 \u0644\u0627\u0645\u064a\u0646\u062c227\u062c\u0631\u0627\u0645","target":"en"}
|
|
||||||
2026-03-18 18:32:28 - POST: {"id":"58","name_en":"Leming Beans 227g","name_ar":"\u0641\u0627\u0635\u0648\u0644\u064a\u0627 \u0644\u0627\u0645\u064a\u0646\u062c227\u062c\u0631\u0627\u0645","sku":"000023071568","category_id":"3","unit_id":"4","supplier_id":"","sale_price":"0.15","purchase_price":"0.111","stock_quantity":"0","min_stock_level":"0","vat_rate":"5","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-03-18 18:40:45 - POST: {"id":"62","name_en":"LAMING RED KIDNEY BEANS ","name_ar":"\u0641\u0627\u0635\u0648\u0644\u064a\u0627\u0621 \u062d\u0645\u0631\u0627\u0621 \u0643\u064a\u062f\u0646\u064a \u0644\u0627\u0645\u064a\u0646\u063a","sku":"000023071605","category_id":"3","unit_id":"4","supplier_id":"","sale_price":"0.25","purchase_price":"0.2","stock_quantity":"100","min_stock_level":"0","vat_rate":"5.00","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-03-18 18:41:00 - POST: {"id":"61","name_en":"LAMING PINEAPPLE 454G","name_ar":"\u0623\u0646\u0627\u0646\u0627\u0633 \u0644\u0627\u0645\u0646\u062c 454\u063a","sku":"000023071599","category_id":"3","unit_id":"4","supplier_id":"","sale_price":"0.4","purchase_price":"0.346","stock_quantity":"50","min_stock_level":"0","vat_rate":"5.00","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-03-19 01:57:45 - POST: {"import_items":""}
|
|
||||||
2026-03-19 01:58:17 - POST: {"action":"translate","text":"LAMING RED KIDNEY BEANS 425","target":"ar"}
|
|
||||||
2026-03-19 01:58:33 - POST: {"id":"117","name_en":"LAMING RED KIDNEY BEANS 425","name_ar":"\u0641\u0627\u0635\u0648\u0644\u064a\u0627 \u062d\u0645\u0631\u0627\u0621 \u0643\u064a\u062f\u0646\u064a \u0644\u0627\u0645\u064a\u0646\u062c 425","sku":"000023071605","category_id":"","unit_id":"","supplier_id":"","sale_price":"0.25","purchase_price":"0.2","stock_quantity":"0","min_stock_level":"0","vat_rate":"5","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-03-19 01:58:51 - POST: {"action":"translate","text":"LAMING PINEAPPLE 454G","target":"ar"}
|
|
||||||
2026-03-19 01:59:00 - POST: {"id":"116","name_en":"LAMING PINEAPPLE 454G","name_ar":"\u0623\u0646\u0627\u0646\u0627\u0633 \u0644\u0627\u0645\u064a\u0646\u063a 454 \u062c\u0631\u0627\u0645","sku":"000023071599","category_id":"","unit_id":"","supplier_id":"","sale_price":"0.4","purchase_price":"0.346","stock_quantity":"55","min_stock_level":"0","vat_rate":"5","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-03-19 06:12:40 - POST: {"close_register":"1","session_id":"11","cash_in_hand":"10","notes":""}
|
|
||||||
2026-03-19 06:27:02 - POST: {"open_register":"1","register_id":"3","opening_balance":"0"}
|
|
||||||
2026-03-19 06:27:40 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":25.4}]","total_amount":"25.4","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":18,\"qty\":1,\"price\":0.6,\"vat_rate\":0,\"vat_amount\":0},{\"id\":17,\"qty\":2,\"price\":0.4,\"vat_rate\":0,\"vat_amount\":0},{\"id\":16,\"qty\":1,\"price\":1.5,\"vat_rate\":0,\"vat_amount\":0},{\"id\":22,\"qty\":5,\"price\":1,\"vat_rate\":0,\"vat_amount\":0},{\"id\":23,\"qty\":1,\"price\":1.2,\"vat_rate\":0,\"vat_amount\":0},{\"id\":21,\"qty\":3,\"price\":1.6,\"vat_rate\":0,\"vat_amount\":0},{\"id\":20,\"qty\":1,\"price\":0.9,\"vat_rate\":0,\"vat_amount\":0},{\"id\":14,\"qty\":1,\"price\":0.2,\"vat_rate\":0,\"vat_amount\":0},{\"id\":8,\"qty\":1,\"price\":1.2,\"vat_rate\":0,\"vat_amount\":0},{\"id\":9,\"qty\":1,\"price\":0.15,\"vat_rate\":0,\"vat_amount\":0},{\"id\":15,\"qty\":1,\"price\":2.55,\"vat_rate\":0,\"vat_amount\":0},{\"id\":29,\"qty\":1,\"price\":2.5,\"vat_rate\":0,\"vat_amount\":0},{\"id\":53,\"qty\":1,\"price\":0.15,\"vat_rate\":0,\"vat_amount\":0},{\"id\":33,\"qty\":1,\"price\":1.3,\"vat_rate\":0,\"vat_amount\":0},{\"id\":70,\"qty\":1,\"price\":2.55,\"vat_rate\":0,\"vat_amount\":0}]"}
|
|
||||||
2026-03-19 06:30:20 - POST: {"close_register":"1","redirect_to":"dashboard","session_id":"12","cash_in_hand":"33","notes":""}
|
|
||||||
2026-03-19 06:38:24 - POST: {"close_register":"1","redirect_to":"dashboard","session_id":"12","cash_in_hand":"33","notes":""}
|
|
||||||
2026-03-19 06:40:40 - POST: {"open_register":"1","register_id":"3","opening_balance":"3"}
|
|
||||||
2026-03-19 06:40:49 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":0.2}]","total_amount":"0.2","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":13,\"qty\":1,\"price\":0.2,\"vat_rate\":0,\"vat_amount\":0}]"}
|
|
||||||
2026-03-19 06:41:01 - POST: {"close_register":"1","redirect_to":"dashboard","session_id":"13","cash_in_hand":"3","notes":""}
|
|
||||||
2026-03-19 07:58:11 - POST: {"open_register":"1","register_id":"3","opening_balance":"32"}
|
|
||||||
2026-03-19 07:58:27 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":11.25}]","total_amount":"11.249999999999998","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":8,\"qty\":1,\"price\":1.2,\"vat_rate\":0,\"vat_amount\":0},{\"id\":15,\"qty\":1,\"price\":2.55,\"vat_rate\":0,\"vat_amount\":0},{\"id\":21,\"qty\":2,\"price\":1.6,\"vat_rate\":0,\"vat_amount\":0},{\"id\":22,\"qty\":2,\"price\":1,\"vat_rate\":0,\"vat_amount\":0},{\"id\":30,\"qty\":1,\"price\":1.1,\"vat_rate\":0,\"vat_amount\":0},{\"id\":23,\"qty\":1,\"price\":1.2,\"vat_rate\":0,\"vat_amount\":0}]"}
|
|
||||||
2026-03-19 07:59:22 - POST: {"close_register":"1","redirect_to":"dashboard","session_id":"14","cash_in_hand":"20","notes":""}
|
|
||||||
2026-03-19 09:22:52 - POST: {"action":"save_theme","theme":"ocean"}
|
|
||||||
2026-03-19 09:22:56 - POST: {"action":"save_theme","theme":"dark"}
|
|
||||||
2026-03-19 09:22:58 - POST: {"action":"save_theme","theme":"default"}
|
|
||||||
2026-03-19 09:23:00 - POST: {"action":"save_theme","theme":"dracula"}
|
|
||||||
2026-03-19 09:23:02 - POST: {"action":"save_theme","theme":"sunset"}
|
|
||||||
2026-03-19 13:46:44 - POST: {"create_backup":""}
|
|
||||||
2026-03-19 13:48:49 - POST: {"open_register":"1","register_id":"3","opening_balance":"0"}
|
|
||||||
2026-03-20 04:59:21 - POST: {"action":"translate","text":"Apple","target":"ar"}
|
|
||||||
2026-03-20 04:59:40 - POST: {"action":"translate","text":"Apple","target":"ar"}
|
|
||||||
2026-03-20 05:00:37 - POST: {"action":"translate","text":"Apple","target":"ar"}
|
|
||||||
2026-03-20 05:01:08 - POST: {"action":"translate","text":"Apple","target":"ar"}
|
|
||||||
2026-03-20 05:01:47 - POST: {"action":"translate","text":"Apple","target":"ar"}
|
|
||||||
2026-03-20 05:25:51 - POST: {"action":"save_theme","theme":"nord"}
|
|
||||||
2026-03-20 05:25:54 - POST: {"action":"save_theme","theme":"sunset"}
|
|
||||||
2026-03-20 05:25:56 - POST: {"action":"save_theme","theme":"citrus"}
|
|
||||||
2026-03-20 05:25:59 - POST: {"action":"save_theme","theme":"dark"}
|
|
||||||
2026-03-20 05:26:01 - POST: {"action":"save_theme","theme":"ocean"}
|
|
||||||
2026-03-20 05:26:03 - POST: {"action":"save_theme","theme":"default"}
|
|
||||||
2026-03-20 17:46:35 - POST: {"action":"translate","text":"LAMING TOMATO PASTE 800G","target":"ar"}
|
|
||||||
2026-03-20 17:46:43 - POST: {"id":"57","name_en":"LAMING TOMATO PASTE 800G","name_ar":"\u0645\u0639\u062c\u0648\u0646 \u0637\u0645\u0627\u0637\u0645 \u0644\u0627\u0645\u064a\u0646\u062c 800 \u062c\u0631\u0627\u0645","sku":"000023071476","category_id":"","unit_id":"","supplier_id":"","sale_price":"0.5","purchase_price":"0.392","stock_quantity":"0","min_stock_level":"0","vat_rate":"0.00","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-03-20 17:46:56 - POST: {"action":"translate","text":"TOMATO PASTE","target":"ar"}
|
|
||||||
2026-03-20 17:47:03 - POST: {"id":"56","name_en":"TOMATO PASTE","name_ar":"\u0645\u0639\u062c\u0648\u0646 \u0627\u0644\u0637\u0645\u0627\u0637\u0645","sku":"000023071469","category_id":"","unit_id":"","supplier_id":"","sale_price":"0.2","purchase_price":"0.11","stock_quantity":"0","min_stock_level":"0","vat_rate":"0.00","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-03-20 17:47:15 - POST: {"action":"translate","text":"\u0646\u0634\u0627 400\u062c\u0645","target":"en"}
|
|
||||||
2026-03-20 17:47:23 - POST: {"id":"55","name_en":"Starch 400g","name_ar":"\u0646\u0634\u0627 400\u062c\u0645","sku":"000023028982","category_id":"","unit_id":"","supplier_id":"","sale_price":"0.35","purchase_price":"0.263","stock_quantity":"0","min_stock_level":"0","vat_rate":"0.00","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-03-20 17:56:30 - POST: {"action":"translate","text":"\u0644\u064a\u0641 \u062d\u062f\u064a\u062f 3*1","target":"en"}
|
|
||||||
2026-03-20 17:56:38 - POST: {"id":"14","name_en":"Iron Wool 3*1","name_ar":"\u0644\u064a\u0641 \u062d\u062f\u064a\u062f 3*1","sku":"000001404159","category_id":"","unit_id":"","supplier_id":"","sale_price":"0.2","purchase_price":"0.125","stock_quantity":"-1","min_stock_level":"0","vat_rate":"0.00","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-03-20 17:56:47 - POST: {"action":"translate","text":"\u0644\u064a\u0641 \u062d\u062f\u064a\u062f \u0630\u0647\u0628\u064a 3*1","target":"en"}
|
|
||||||
2026-03-20 17:56:51 - POST: {"id":"13","name_en":"Gold Iron Sponge 3*1","name_ar":"\u0644\u064a\u0641 \u062d\u062f\u064a\u062f \u0630\u0647\u0628\u064a 3*1","sku":"000001404142","category_id":"","unit_id":"","supplier_id":"","sale_price":"0.2","purchase_price":"0.125","stock_quantity":"-1","min_stock_level":"0","vat_rate":"0.00","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
|
|
||||||
2026-03-20 18:02:45 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":0.7}]","total_amount":"0.7","tax_amount":"0.023809523809523808","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":56,\"qty\":1,\"price\":0.2,\"vat_rate\":0,\"vat_amount\":0},{\"id\":62,\"qty\":2,\"price\":0.25,\"vat_rate\":5,\"vat_amount\":0.023809523809523808}]"}
|
|
||||||
2026-03-20 18:25:38 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":3.15}]","total_amount":"3.15","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":36,\"qty\":1,\"price\":1.45,\"vat_rate\":0,\"vat_amount\":0},{\"id\":18,\"qty\":1,\"price\":0.6,\"vat_rate\":0,\"vat_amount\":0},{\"id\":30,\"qty\":1,\"price\":1.1,\"vat_rate\":0,\"vat_amount\":0}]"}
|
|
||||||
2026-04-09 04:06:44 - POST: {"start_trial":""}
|
|
||||||
2026-04-09 04:06:58 - POST: {"username":"admin","password":"admin","login":""}
|
|
||||||
2026-04-09 04:08:44 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-04-09 04:46:43 - POST: {"username":"admin","password":"admin","login":""}
|
|
||||||
2026-05-01 19:22:46 - POST: {"start_trial":"1"}
|
|
||||||
2026-05-01 19:22:46 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-01 19:32:47 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 02:56:23 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 03:00:39 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 03:32:58 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 04:46:38 - POST: {"username":"moosa","password":"moosa123","login":""}
|
|
||||||
2026-05-02 04:46:47 - POST: {"username":"admin","password":"admin123","login":""}
|
|
||||||
2026-05-02 04:46:55 - POST: {"username":"admin","password":"admin","login":""}
|
|
||||||
2026-05-02 04:50:45 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 04:50:50 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 04:50:50 - POST: {"name_en":"Wallet","name_ar":"\u0645\u062d\u0641\u0638\u0629","add_payment_method":"1"}
|
|
||||||
2026-05-02 04:54:23 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 04:54:23 - POST: {"name_en":"QA Wallet","name_ar":"\u0645\u062d\u0641\u0638\u0629 \u062a\u062c\u0631\u064a\u0628\u064a\u0629","add_payment_method":"1"}
|
|
||||||
2026-05-02 04:54:32 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 04:54:39 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 04:54:45 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 04:54:49 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 04:54:54 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 04:55:00 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 04:55:44 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 04:55:52 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 04:55:52 - POST: {"name_en":"QA Wallet","name_ar":"\u0645\u062d\u0641\u0638\u0629 \u062a\u062c\u0631\u064a\u0628\u064a\u0629","add_payment_method":"1"}
|
|
||||||
2026-05-02 04:55:52 - POST: {"id":"5","name_en":"QA Wallet Updated","name_ar":"\u0645\u062d\u0641\u0638\u0629 \u0645\u062d\u062f\u062b\u0629","edit_payment_method":"1"}
|
|
||||||
2026-05-02 04:55:52 - POST: {"id":"5","delete_payment_method":"1"}
|
|
||||||
2026-05-02 04:56:04 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 04:56:12 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 04:56:19 - POST: {"username":"admin","password":"admin","login":"1"}
|
|
||||||
2026-05-02 04:56:42 - POST: {"id":"3","name_en":"Bank Transfer","name_ar":"\u062a\u062d\u0648\u064a\u0644 \u0628\u0646\u0643\u064a","edit_payment_method":""}
|
|
||||||
2026-05-02 17:20:51 - POST: {"name_en":"piece","name_ar":"\u062d\u0628\u0629","add_category":""}
|
|
||||||
2026-05-02 17:23:36 - POST: {"action":"translate","text":"piece","target":"ar"}
|
|
||||||
2026-05-02 17:29:22 - POST: {"action":"translate","text":"piece","target":"ar"}
|
|
||||||
2026-05-02 17:29:31 - POST: {"action":"translate","text":"piece","target":"ar"}
|
|
||||||
2026-05-02 17:29:44 - POST: {"name_en":"piece","name_ar":"\u062d\u0628\u0629","add_unit":""}
|
|
||||||
2026-05-02 17:32:00 - POST: {"action":"translate","text":"piece","target":"ar"}
|
|
||||||
2026-05-02 17:32:02 - POST: {"action":"translate","text":"%D9%82%D8%B7%D8%B9%D8%A9","target":"en"}
|
|
||||||
2026-05-02 17:32:10 - POST: {"action":"translate","text":"\u0642\u0637\u0639\u0629","target":"en"}
|
|
||||||
2026-05-02 17:32:11 - POST: {"action":"translate","text":"LAMING PINEAPPLE 454G","target":"ar"}
|
|
||||||
2026-05-02 17:32:54 - POST: {"action":"translate","text":"onion","target":"ar"}
|
|
||||||
2026-05-02 18:40:57 - POST: {"name":"\u0645\u062d\u0627\u0633\u0628 1","add_cash_register":""}
|
|
||||||
2026-05-02 18:41:32 - POST: {"id":"1","name":"\u0627\u0644\u0641\u0631\u0639 \u0627\u0644\u0631\u0626\u064a\u0633\u064a","phone":"","address":"Head Office","status":"active","edit_outlet":""}
|
|
||||||
2026-05-02 19:20:30 - POST: {"license_key":"BACC-F7B0-A44F-2AC2","activate":"1"}
|
|
||||||
2026-05-03 02:27:31 - POST: {"seed_default_accounts":""}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
file_path = 'index.php'
|
|
||||||
|
|
||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# Fix the syntax error
|
|
||||||
search_bad = 'echo "<option value=\"$tz\" $selected>$tz</option>";'
|
|
||||||
replace_good = 'echo "<option value=\\\
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
2026-02-25 04:49:58 - search_items call: q=on
|
|
||||||
2026-02-25 12:57:14 - search_items call: q=on
|
|
||||||
2026-02-25 17:10:03 - search_items call: q=on
|
|
||||||
2026-03-18 10:30:10 - search_items call: q=to
|
|
||||||
2026-03-18 10:30:56 - search_items call: q=to
|
|
||||||
2026-03-18 13:46:59 - search_items call: q=to
|
|
||||||
2026-03-18 13:53:33 - search_items call: q=to
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
$_POST['import_items'] = true;
|
|
||||||
$_FILES['excel_file'] = [
|
|
||||||
'name' => 'test_import.csv',
|
|
||||||
'type' => 'text/csv',
|
|
||||||
'tmp_name' => __DIR__ . '/test_import.csv',
|
|
||||||
'error' => 0,
|
|
||||||
'size' => filesize(__DIR__ . '/test_import.csv')
|
|
||||||
];
|
|
||||||
|
|
||||||
// Define db() helper if not defined (it should be in db/config.php)
|
|
||||||
require_once __DIR__ . '/db/config.php';
|
|
||||||
|
|
||||||
// Include the logic from index.php
|
|
||||||
// I'll copy the logic here to avoid including the whole index.php which might have side effects
|
|
||||||
$tmpPath = $_FILES['excel_file']['tmp_name'];
|
|
||||||
$handle = fopen($tmpPath, "r");
|
|
||||||
$firstRow = fgetcsv($handle, 0, ","); // Skip header
|
|
||||||
$count = 0; $errors = 0;
|
|
||||||
while (($data = fgetcsv($handle, 0, ",")) !== FALSE) {
|
|
||||||
if (count($data) < 2) continue;
|
|
||||||
try {
|
|
||||||
$sku = substr(trim($data[0] ?? ''), 0, 100);
|
|
||||||
$name_en = $data[1] ?? '';
|
|
||||||
$name_ar = $data[2] ?? '';
|
|
||||||
$sale_price = str_replace(',', '', $data[3] ?? 0);
|
|
||||||
$purchase_price = str_replace(',', '', $data[4] ?? 0);
|
|
||||||
|
|
||||||
if (empty($sku) && empty($name_en)) continue;
|
|
||||||
|
|
||||||
$stmt = db()->prepare("INSERT INTO stock_items (sku, name_en, name_ar, sale_price, purchase_price)
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE name_en=VALUES(name_en), name_ar=VALUES(name_ar),
|
|
||||||
sale_price=VALUES(sale_price), purchase_price=VALUES(purchase_price)");
|
|
||||||
$stmt->execute([$sku, $name_en, $name_ar, (float)$sale_price, (float)$purchase_price]);
|
|
||||||
echo "Imported: $sku\n";
|
|
||||||
$count++;
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
echo "Error: " . $e->getMessage() . "\n";
|
|
||||||
$errors++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fclose($handle);
|
|
||||||
echo "Finished. Success: $count, Errors: $errors\n";
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Mock session
|
|
||||||
session_start();
|
|
||||||
$_SESSION['user_id'] = 1;
|
|
||||||
$_SESSION['user_role_name'] = 'Administrator';
|
|
||||||
$_SESSION['user_permissions'] = 'all';
|
|
||||||
|
|
||||||
// Simulate GET request
|
|
||||||
$_GET['page'] = 'accounting';
|
|
||||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
|
||||||
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
|
|
||||||
$_SERVER['HTTPS'] = 'on';
|
|
||||||
|
|
||||||
// Capture output
|
|
||||||
ob_start();
|
|
||||||
try {
|
|
||||||
require 'index.php';
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
echo "\nFATAL ERROR: " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine() . "\n";
|
|
||||||
}
|
|
||||||
$output = ob_get_clean();
|
|
||||||
|
|
||||||
file_put_contents('output_test.html', $output);
|
|
||||||
echo "Output saved to output_test.html (" . strlen($output) . " bytes)\n";
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
<?php
|
|
||||||
$path = '/home/ubuntu/executor/.env';
|
|
||||||
$env = @parse_ini_file($path);
|
|
||||||
echo "Raw PROJECT_UUID from .env: " . ($env['PROJECT_UUID'] ?? 'MISSING') . "\n";
|
|
||||||
echo "Raw PROJECT_ID from .env: " . ($env['PROJECT_ID'] ?? 'MISSING') . "\n";
|
|
||||||
|
|
||||||
$cfg = require __DIR__ . '/ai/config.php';
|
|
||||||
echo "Final Project UUID: [" . ($cfg['project_uuid'] ?? 'MISSING') . "]\n";
|
|
||||||
echo "Final Project ID: [" . ($cfg['project_id'] ?? 'MISSING') . "]\n";
|
|
||||||
22
test_db.php
22
test_db.php
@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'db/config.php';
|
|
||||||
try {
|
|
||||||
$db = db();
|
|
||||||
echo "Testing role_groups insert...\n";
|
|
||||||
$stmt = $db->prepare("INSERT INTO role_groups (name, permissions) VALUES (?, ?)");
|
|
||||||
$stmt->execute(['Test Group', json_encode(['dashboard_view'])]);
|
|
||||||
$groupId = $db->lastInsertId();
|
|
||||||
echo "Inserted Group ID: $groupId\n";
|
|
||||||
|
|
||||||
echo "Testing users insert...\n";
|
|
||||||
$stmt = $db->prepare("INSERT INTO users (username, password, email, group_id) VALUES (?, ?, ?, ?)");
|
|
||||||
$stmt->execute(['testuser_' . time(), password_hash('password', PASSWORD_DEFAULT), 'test@example.com', $groupId]);
|
|
||||||
echo "Inserted User ID: " . $db->lastInsertId() . "\n";
|
|
||||||
|
|
||||||
echo "Cleanup...\n";
|
|
||||||
$db->prepare("DELETE FROM users WHERE username LIKE 'testuser_%'")->execute();
|
|
||||||
$db->prepare("DELETE FROM role_groups WHERE name = 'Test Group'")->execute();
|
|
||||||
echo "Done.\n";
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo "Error: " . $e->getMessage() . "\n";
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
SKU,English Name,Arabic Name,Sale Price,Cost Price
|
|
||||||
ITEM1,Product 1,منتج 1,10.00,5.00
|
|
||||||
ITEM2,Product 2,,15.00,7.00
|
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'db/config.php';
|
|
||||||
$q = 'Tomato';
|
|
||||||
$searchTerm = "%$q%";
|
|
||||||
$stmt = db()->prepare("SELECT * FROM stock_items WHERE name_en LIKE ? OR name_ar LIKE ? OR sku LIKE ? LIMIT 15");
|
|
||||||
$stmt->execute([$searchTerm, $searchTerm, $searchTerm]);
|
|
||||||
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
echo "Results for Tomato: " . count($results) . "\n";
|
|
||||||
print_r($results);
|
|
||||||
|
|
||||||
$q = 'INV-00001';
|
|
||||||
$searchTerm = "%$q%";
|
|
||||||
$stmt->execute([$searchTerm, $searchTerm, $searchTerm]);
|
|
||||||
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
echo "Results for INV-00001: " . count($results) . "\n";
|
|
||||||
Loading…
x
Reference in New Issue
Block a user