Compare commits

..

11 Commits

Author SHA1 Message Date
Ollama
0a0f0c7f4c fix: Address CodeRabbit review comments
- items/manage.php: Remove duplicate let declaration for start_date
  (partial/daterangepicker already declares it)
- header_js.php: Escape CSRF hash in JavaScript context
- tax_jurisdictions.php: Fix mismatched selector (remove_tax_jurisdictions
  -> remove_tax_jurisdiction)
2026-04-15 14:36:34 +00:00
Ollama
7a7ee4a5f3 fix: Replace remaining var declarations in gulpfile.js
- Converted 12 remaining var declarations to const
- All variables are function-scoped and never reassigned
- Complete coverage for this file now
2026-04-15 13:17:55 +00:00
Ollama
418dbc69e8 fix: Replace remaining var declarations in Views
- Changed var  to const in sales/register.php
- Changed var  to const in configs/receipt_config.php

These were missed in the initial pass.
2026-04-15 13:02:22 +00:00
Ollama
e177099be1 refactor: Replace var with let/const in remaining JS files
- Modernized gulpfile.js: 3 var declarations replaced
- Modernized app/Views/errors/html/debug.js: all var declarations replaced
- Used const for never-reassigned, let for reassigned variables
2026-04-15 12:54:24 +00:00
Ollama
513cccad69 refactor: Replace var with let/const in inline JavaScript
- Fixed CodeRabbit review: changed enable_actions and load_success
  from const to let in manage_tables.js (they are reassigned in init)
- Replaced all var declarations in inline JavaScript in Views with
  let (for reassigned) or const (for never reassigned)
- Modernized 48 additional files with inline JavaScript
2026-04-15 12:45:32 +00:00
Ollama
91137a43a2 refactor: Replace var with let/const in JavaScript files
- Replace var with let for variables that are reassigned
- Replace var with const for variables that are never reassigned
- Modernize manage_tables.js and nominatim.autocomplete.js
- Skip third-party libraries (imgpreview.full.jquery.js, clipboard.min.js)

Closes #4491
2026-04-15 12:11:51 +00:00
Ollama
905b58ca6e [Fix]: Add missing return statements to Sales Controller functions
- Fix postComplete(): Add return keyword for error redirect paths
  (lines 799, 843, 871) when duplicate invoice/work_order/quote numbers
- Fix postChangeItemNumber(): Add return statement returning JSON response
- Fix postChangeItemName(): Add return statement returning JSON response
- Fix postChangeItemDescription(): Add return statement returning JSON response

All 4 functions declared return types but were missing return statements,
causing potential runtime errors in certain code paths.

Resolves #4492
2026-04-15 06:49:12 +00:00
dependabot[bot]
609b206375 Bump lodash from 4.17.23 to 4.18.1 (#4462)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: objecttothis <17935339+objecttothis@users.noreply.github.com>
2026-04-14 01:21:43 +04:00
objecttothis
6fec2464f8 Update to CodeIgniter 4.7.2 (#4485)
- Merge Config and Core File Changes 4.6.3 > 4.6.4
- Merge Config and Core File Changes 4.6.4 > 4.7.0
- Added app\Config\WorkerMode.php
- Merge Config and Core File Changes Not previously merged
- Added app\Config\Hostnames.php
- Corrected incorrect CSS property used in invoice.php view.
- Corrected unknown CSS properties used in register.php view.
- Used shorthand CSS in debug.css
- Corrected indentation in barcode_sheet.php view.
- Corrected indentation in footer.php view.
- Corrected indentation in invoice_email.php view.
- Replaced obsolete attributes with CSS style attributes in barcode_sheet.php
- Replaced obsolete attribute in error_exception.php
- Replaced obsolete attribute in invoice_email.php
- Replaced obsolete attribute in quote_email.php
- Replaced obsolete attributes in work_order_email.php
- Fixed indentation in system_info.php
- Replaced <strong> tag outside <p> tags, which isn't allowed, with style attributes.
- Simplified js return logic and indentation fixes in tax_categories.php
- Simplified js return logic in tax_codes.php
- Simplified js return logic in tax_jurisdictions.php
- Removed unnecessary labels in manage views.
- Rewrite JavaScript function and PHP to be more readable in bar.php, hbar.php, line.php and pie.php
- Added type declarations, return types and an import to app\Config\Services
- Updated Attribute.php parameter type
- Updated Receiving_lib.php parameter type
- Updated Receivings.php parameter types and updated PHPdocs
- Updated tabular_helper.php parameter types and updated PHPdocs
- Added type declarations and corrected PHPdocs in url_helper.php
- Added return types to functions
- Revert $objectSrc value in ContentSecurityPolicy.php
- Correct return type in Customer->get_stats()
- Correct return type in Item->get_info_by_id_or_number()
- Correct misspelling in border-spacing
- Added missing css style semicolons
- Resolve operator precedence ambiguity.
- Resolve column mismatch.
- Added missing escaping in view.
- Updated requirement for PHP 8.2
- Resolve unresolved conflicts
- Added PHP 8.2 requirement to the README.md
- Fixed bugs in display of UI
- Fixed duplicated `>` in app\Views\Expenses\manage.php
- Removed excess whitespace at the end of some lines in table_filter_persistence.php
- Added missing `>` in app\Views\Expenses\manage.php
- Corrected grammar in PHPdoc in table_filter_persistence.php
- Remove bug causing `\` to be injected into the new giftcard value
- Fix bug causing DROPDOWN Attribute Values to not save correctly
- Added check for null in $normalizedItemId

- Removing < PHP 8.2 from linting and tests
- Update Linter to not include PHP 8.2 and 8.1
- Remove PHP 8.1 unit test cycle.
- Update Bug Report Template
- Update Composer files for CodeIgniter 4.7.2
- Updated INSTALL.md to reflect changes.

---------

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-14 01:05:10 +04:00
jekkos
332d8c8c69 fix: change docker image tag to master 2026-04-10 23:58:38 +02:00
objecttothis
577cf55b6a [Feature]: Case-sensitive attribute updates and CSV Import attribute deletion capability (#4384)
PSR and Readability Changes
- Removed unused import
- Corrected PHPdoc to include the correct return type
- Refactored out a function to get attribute data from the row in a CSV item import.
- refactored snake_case variables and function names to camelCase
- Refactored the naming of saveAttributeData() to better reflect the functions purpose.
- Improved PHPdocs
- Remove whitespace
- Remove unneeded comment
- Refactored abbreviated variable name for clarity
- Removed $csvHeaders as it is unused
- Corrected spacing and curly brace location
- Refactored Stock Locations validation inside general validation

Bugfixes
- Fixed bug causing attribute_id and item_id to not be properly assigned when empty() returns true.
- Fixed bug causing CSV Item import to not update barcode when changed in the import file.
- Fixed saveAttributeValue() logic causing attribute_value to be updated to a value that already exists for a different attribute_id
- Fixed bug preventing Category as dropdown functionality from working
- Fixed bug preventing barcodes from updating. in Item CSV Imports
- Corrected bug in stock_location->save_value()
- Corrected incorrect helper file references.
- Removed duplicate call to save attribute link
- Rollback transaction on failure before returning false
- Rollback transaction and return 0 on failure to save attribute link.
- Account for '0' being an acceptable TEXT or DECIMAL attributeValue.
- Corrected Business logic
- Resolved incorrect array key
- Account for 0 in column values
- Correct check empty attribute check
- Previously 0 would have been skipped even though that's a valid value for an attribute.
- Removed unused foreach loop index variables
- Corrected CodeIgniter Framework version to specific version

UnitTest Seeder and tests
- Created a seeder to automatically prepare the test database.
- Modified the Unit Test setup to properly seed the test database.
- Wrote a unit test to test deleting an attribute from an item through the CSV.
- Corrected errors in unit tests preventing them from passing. save_value() returns a bool, not the itemId
- Fix Unit Tests that were failing
- Corrected the logic in itemUpdate test
- Replaced precision test with one reflecting testing of actual value.
- This test does not test cash rounding rules. That should go into a different test.
- Correct expected value in test.
- Update app/Database/Seeds/TestDatabaseBootstrapSeeder.php
- Added check to testImportDeleteAttributeFromExistingItem
- Correct mocking of dropdowns
- Remove code depending on removed database.sql
- Removed FQN in seeder() call
- Added checks in Database seeder
- Moved the function to the attribute model where it belongs which allows testability.

Case Change Capability (CSV Import and Form)
- CSV Import and view Case Changes of `attribute_value`
- Store attribute even when just case is different.
- Add getAttributeValueByAttributeId() to assist in comparing the value
- Corrected Capitalization in File Handling Logic

CSV Import Attribute Link Deletion Capability
- Validation checks bypass magic word cells.
- Delete the attribute link for an item if the CSV contains `_DELETE_`
- Added calls to deleteOrphanedValues()
- Items CSV Import Attribute Delete
- Exclude the itemId in the check to see if the barcode number exists

Error Checking and Reporting Improvements
- Fail the import if an invalid stock location is found in the CSV
- Return false if deleteAttributeLinks fails
- Match sanitization of description field to Form submission import
- Fold errors into result and return value
- Populated $allowedStockLocations before sending it to the validation function
- Added logic to not ignore failed saveItemAttributes calls
- Add error checking to failed row insert
- Reworked &= to && logic so that it short-circuits the function call after if success is already false.
- Add transaction to storeCSVAttributeValue function to prevent deleting the attribute links before confirming the new value successfully saved.
- Modified generate_message in Db_log.php to be defensive.

Attribute Improvements
- Move ATTRIBUTE_VALUE_TYPES to the helper
- Normalize AttributeId in saveAttributeLink()
- normalize itemId in saveAttributeLink()
- Account for '0' in column values for allow_alt_description
- Remove duplicate saveAttributeValue call
- Correct return value of function
- Like other save_value() functions, the location_data variable is passed by reference.
- Unlike other save_value() functions, the location_data variable is not being updated with the primary key id.
- Added updateAttributeValue() function as part of logic fix.
- Added attribute_helper.php
- Simplified logic to store attribute values

---------

Signed-off-by: objec <objecttothis@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-09 11:13:22 +04:00
132 changed files with 2752 additions and 2401 deletions

View File

@@ -12,11 +12,11 @@ body:
attributes:
value: |
## Thanks for taking the time to fill out this bug report! 🐜
Bug reports help us identify and fix issues. Please provide as much detail as possible.
> ⚠️ **Important:** Submit a separate bug report for each problem you encounter.
>
>
> 🚫 Do not include personal identifying information such as email addresses or encryption keys.
# ─────────────────────────────────────────────────────────────────────────────
@@ -28,7 +28,7 @@ body:
label: 🐛 Bug Description
description: A clear and concise description of what the bug is.
placeholder: |
Example: When I try to print a receipt, the application crashes
Example: When I try to print a receipt, the application crashes
with an error message saying "Unable to connect to printer".
validations:
required: true
@@ -86,8 +86,7 @@ body:
- PHP 8.2
- PHP 8.1
- PHP 7.4
- PHP 7.3
- PHP 7.2
- Other
default: 0
validations:
required: true
@@ -141,7 +140,7 @@ body:
label: 📊 System Information Report
description: |
Copy and paste the system information from OSPOS:
**Navigation:** Configuration → Setup & Conf → System Info
placeholder: |
Paste the System Information Report here...
@@ -155,7 +154,7 @@ body:
label: 📜 Relevant Log Output
description: |
Please copy and paste any relevant log output.
**Log locations:**
- OSPOS logs: `writable/logs/`
- Web server logs: `/var/log/apache2/` or `/var/log/nginx/`
@@ -185,4 +184,4 @@ body:
- label: I have searched existing issues to ensure this bug has not already been reported
required: true
- label: I have provided all the information requested above
required: true
required: true

View File

@@ -155,7 +155,7 @@ jobs:
run: |
BRANCH=$(echo "${GITHUB_REF#refs/heads/}" | tr '/' '_')
if [ "$BRANCH" = "master" ]; then
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }},${{ secrets.DOCKER_USERNAME }}/opensourcepos:latest" >> $GITHUB_OUTPUT
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }},${{ secrets.DOCKER_USERNAME }}/opensourcepos:master" >> $GITHUB_OUTPUT
else
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }}" >> $GITHUB_OUTPUT
fi

View File

@@ -28,7 +28,6 @@ jobs:
fail-fast: false
matrix:
php-version:
- '8.1'
- '8.2'
- '8.3'
- '8.4'

View File

@@ -12,14 +12,6 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: PHP Lint 8.0
uses: dbfx/github-phplint/8.0@master
with:
folder-to-exclude: "! -path \"./vendor/*\" ! -path \"./folder/excluded/*\""
- name: PHP Lint 8.1
uses: dbfx/github-phplint/8.1@master
with:
folder-to-exclude: "! -path \"./vendor/*\" ! -path \"./folder/excluded/*\""
- name: PHP Lint 8.2
uses: dbfx/github-phplint/8.2@master
with:

View File

@@ -34,7 +34,6 @@ jobs:
fail-fast: false
matrix:
php-version:
- '8.1'
- '8.2'
- '8.3'
- '8.4'
@@ -119,4 +118,4 @@ jobs:
- name: Stop MariaDB
if: always()
run: docker stop mysql && docker rm mysql
run: docker stop mysql && docker rm mysql

View File

@@ -1,6 +1,6 @@
## Server Requirements
- PHP version `8.1` to `8.4` are supported, PHP version `≤7.4` is NOT supported. Please note that PHP needs to have the extensions `php-json`, `php-gd`, `php-bcmath`, `php-intl`, `php-openssl`, `php-mbstring`, `php-curl` and `php-xml` installed and enabled. An unstable master build can be downloaded in the releases section.
- PHP version `8.2` to `8.4` are supported, PHP version `≤ 8.1` is NOT supported. Please note that PHP needs to have the extensions `php-json`, `php-gd`, `php-bcmath`, `php-intl`, `php-openssl`, `php-mbstring`, `php-curl` and `php-xml` installed and enabled. An unstable master build can be downloaded in the releases section.
- MySQL `5.7` is supported, also MariaDB replacement `10.x` is supported and might offer better performance.
- Apache `2.4` is supported. Nginx should work fine too, see [wiki page here](https://github.com/opensourcepos/opensourcepos/wiki/Local-Deployment-using-LEMP).
- Raspberry PI based installations proved to work, see [wiki page here](<https://github.com/opensourcepos/opensourcepos/wiki/Installing-on-Raspberry-PI---Orange-PI-(Headless-OSPOS)>).

View File

@@ -102,7 +102,7 @@ NOTE: If you're running non-release code, please make sure you always run the la
- If you have suhosin installed and face an issue with CSRF, please make sure you read [issue #1492](https://github.com/opensourcepos/opensourcepos/issues/1492).
- PHP `≥ 8.1` is required to run this app.
- PHP `≥ 8.2` is required to run this app.
## 🏃 Keep the Machine Running

View File

@@ -55,21 +55,13 @@ class App extends BaseConfig
public string $baseURL; // Defined in the constructor
/**
* Allowed Hostnames for the Site URL.
*
* Security: This is used to validate the HTTP Host header to prevent
* Host Header Injection attacks. If the Host header doesn't match
* an entry in this list, the request will use the first allowed hostname.
*
* IMPORTANT: This MUST be configured for production deployments.
* If empty in production, the application will fail to start.
* In development, it will fall back to 'localhost' with a warning.
*
* Configure via .env file (comma-separated list):
* app.allowedHostnames = 'example.com,www.example.com'
*
* For local development:
* app.allowedHostnames = 'localhost'
* Allowed Hostnames in the Site URL other than the hostname in the baseURL.
* If you want to accept multiple Hostnames, set this.
*
* E.g.,
* When your site URL ($baseURL) is 'http://example.com/', and your site
* also accepts 'http://media.example.com/' and 'http://accounts.example.com/':
* ['media.example.com', 'accounts.example.com']
*
* @var list<string>
*/
@@ -125,7 +117,7 @@ class App extends BaseConfig
| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
|
*/
public string $permittedURIChars = 'a-z 0-9~%.:_\-=';
public string $permittedURIChars = 'a-z 0-9~%.:_\-';
/**
* --------------------------------------------------------------------------
@@ -286,12 +278,12 @@ class App extends BaseConfig
* @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/
* @see http://www.w3.org/TR/CSP/
*/
public bool $CSPEnabled = false; // TODO: Currently CSP3 tags are not supported so enabling this causes problems with script-src-elem, style-src-attr and style-src-elem
public bool $CSPEnabled = false;
public function __construct()
{
parent::__construct();
// Solution for CodeIgniter 4 limitation: arrays cannot be set from .env
// See: https://github.com/codeigniter4/CodeIgniter4/issues/7311
$envAllowedHostnames = getenv('app.allowedHostnames');
@@ -301,9 +293,9 @@ class App extends BaseConfig
static fn (string $hostname): bool => $hostname !== ''
));
}
$this->https_on = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') || (isset($_ENV['FORCE_HTTPS']) && $_ENV['FORCE_HTTPS'] == 'true');
$host = $this->getValidHost();
$this->baseURL = $this->https_on ? 'https' : 'http';
$this->baseURL .= '://' . $host . '/';
@@ -312,39 +304,39 @@ class App extends BaseConfig
/**
* Validates and returns a trusted hostname.
*
*
* Security: Prevents Host Header Injection attacks (GHSA-jchf-7hr6-h4f3)
* by validating the HTTP_HOST against a whitelist of allowed hostnames.
*
*
* In production: Fails fast if allowedHostnames is not configured.
* In development: Allows localhost fallback with an error log.
*
*
* @return string A validated hostname
* @throws \RuntimeException If allowedHostnames is not configured in production
*/
private function getValidHost(): string
{
$httpHost = $_SERVER['HTTP_HOST'] ?? 'localhost';
// Determine environment
// CodeIgniter's test bootstrap sets $_SERVER['CI_ENVIRONMENT'] = 'testing'
// Check $_SERVER first, then $_ENV, then fall back to 'production'
$environment = $_SERVER['CI_ENVIRONMENT'] ?? $_ENV['CI_ENVIRONMENT'] ?? getenv('CI_ENVIRONMENT') ?: 'production';
if (empty($this->allowedHostnames)) {
$errorMessage =
$errorMessage =
'Security: allowedHostnames is not configured. ' .
'Host header injection protection is disabled. ' .
'Set app.allowedHostnames in your .env file. ' .
'Example: app.allowedHostnames = "example.com,www.example.com" ' .
'Received Host: ' . $httpHost;
// Production: Fail explicitly to prevent silent security vulnerabilities
// Testing and development: Allow localhost fallback
if ($environment === 'production') {
throw new \RuntimeException($errorMessage);
}
log_message('error', $errorMessage . ' Using localhost fallback (development only).');
return 'localhost';
}
@@ -354,7 +346,7 @@ class App extends BaseConfig
}
// Host not in whitelist - use first configured hostname as fallback
log_message('warning',
log_message('warning',
'Security: Rejected HTTP_HOST "' . $httpHost . '" - not in allowedHostnames whitelist. ' .
'Using fallback: ' . $this->allowedHostnames[0]
);

View File

@@ -17,8 +17,6 @@ use CodeIgniter\Config\AutoloadConfig;
*
* NOTE: This class is required prior to Autoloader instantiation,
* and does not extend BaseConfig.
*
* @immutable
*/
class Autoload extends AutoloadConfig
{

View File

@@ -1,23 +1,38 @@
<?php
/*
* The environment testing is reserved for PHPUnit testing. It has special
* conditions built into the framework at various places to assist with that.
* You cant use it for your development.
*/
/*
|--------------------------------------------------------------------------
| ERROR DISPLAY
| ERROR DISPLAY
|--------------------------------------------------------------------------
*/
| In development, we want to show as many errors as possible to help
| make sure they don't make it to production. And save us hours of
| painful debugging.
*/
error_reporting(E_ALL);
ini_set('display_errors', '1');
/*
|--------------------------------------------------------------------------
| DEBUG BACKTRACES
| DEBUG BACKTRACES
|--------------------------------------------------------------------------
*/
| If true, this constant will tell the error screens to display debug
| backtraces along with the other error information. If you would
| prefer to not see this, set this value to false.
*/
defined('SHOW_DEBUG_BACKTRACE') || define('SHOW_DEBUG_BACKTRACE', true);
/*
|--------------------------------------------------------------------------
| DEBUG MODE
| DEBUG MODE
|--------------------------------------------------------------------------
*/
defined('CI_DEBUG') || define('CI_DEBUG', true);
| Debug mode is an experimental flag that can allow changes throughout
| the system. It's not widely used currently, and may not survive
| release of the framework.
*/
defined('CI_DEBUG') || define('CI_DEBUG', true);

View File

@@ -6,6 +6,22 @@ use CodeIgniter\Config\BaseConfig;
class CURLRequest extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* CURLRequest Share Connection Options
* --------------------------------------------------------------------------
*
* Share connection options between requests.
*
* @var list<int>
*
* @see https://www.php.net/manual/en/curl.constants.php#constant.curl-lock-data-connect
*/
public array $shareConnectionOptions = [
CURL_LOCK_DATA_CONNECT,
CURL_LOCK_DATA_DNS,
];
/**
* --------------------------------------------------------------------------
* CURLRequest Share Options

View File

@@ -3,6 +3,7 @@
namespace Config;
use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Cache\Handlers\ApcuHandler;
use CodeIgniter\Cache\Handlers\DummyHandler;
use CodeIgniter\Cache\Handlers\FileHandler;
use CodeIgniter\Cache\Handlers\MemcachedHandler;
@@ -78,7 +79,7 @@ class Cache extends BaseConfig
* Your file storage preferences can be specified below, if you are using
* the File driver.
*
* @var array<string, int|string|null>
* @var array{storePath?: string, mode?: int}
*/
public array $file = [
'storePath' => WRITEPATH . 'cache/',
@@ -95,7 +96,7 @@ class Cache extends BaseConfig
*
* @see https://codeigniter.com/user_guide/libraries/caching.html#memcached
*
* @var array<string, bool|int|string>
* @var array{host?: string, port?: int, weight?: int, raw?: bool}
*/
public array $memcached = [
'host' => '127.0.0.1',
@@ -108,17 +109,28 @@ class Cache extends BaseConfig
* -------------------------------------------------------------------------
* Redis settings
* -------------------------------------------------------------------------
*
* Your Redis server can be specified below, if you are using
* the Redis or Predis drivers.
*
* @var array<string, int|string|null>
* @var array{
* host?: string,
* password?: string|null,
* port?: int,
* timeout?: int,
* async?: bool,
* persistent?: bool,
* database?: int
* }
*/
public array $redis = [
'host' => '127.0.0.1',
'password' => null,
'port' => 6379,
'timeout' => 0,
'database' => 0,
'host' => '127.0.0.1',
'password' => null,
'port' => 6379,
'timeout' => 0,
'async' => false, // specific to Predis and ignored by the native Redis extension
'persistent' => false,
'database' => 0,
];
/**
@@ -132,6 +144,7 @@ class Cache extends BaseConfig
* @var array<string, class-string<CacheInterface>>
*/
public array $validHandlers = [
'apcu' => ApcuHandler::class,
'dummy' => DummyHandler::class,
'file' => FileHandler::class,
'memcached' => MemcachedHandler::class,
@@ -158,4 +171,28 @@ class Cache extends BaseConfig
* @var bool|list<string>
*/
public $cacheQueryString = false;
/**
* --------------------------------------------------------------------------
* Web Page Caching: Cache Status Codes
* --------------------------------------------------------------------------
*
* HTTP status codes that are allowed to be cached. Only responses with
* these status codes will be cached by the PageCache filter.
*
* Default: [] - Cache all status codes (backward compatible)
*
* Recommended: [200] - Only cache successful responses
*
* You can also use status codes like:
* [200, 404, 410] - Cache successful responses and specific error codes
* [200, 201, 202, 203, 204] - All 2xx successful responses
*
* WARNING: Using [] may cache temporary error pages (404, 500, etc).
* Consider restricting to [200] for production applications to avoid
* caching errors that should be temporary.
*
* @var list<int>
*/
public array $cacheStatusCodes = [];
}

View File

@@ -30,6 +30,11 @@ class ContentSecurityPolicy extends BaseConfig
*/
public ?string $reportURI = null;
/**
* Specifies a reporting endpoint to which violation reports ought to be sent.
*/
public ?string $reportTo = null;
/**
* Instructs user agents to rewrite URL schemes, changing
* HTTP to HTTPS. This directive is for websites with
@@ -38,12 +43,12 @@ class ContentSecurityPolicy extends BaseConfig
public bool $upgradeInsecureRequests = false;
// -------------------------------------------------------------------------
// Sources allowed
// CSP DIRECTIVES SETTINGS
// NOTE: once you set a policy to 'none', it cannot be further restricted
// -------------------------------------------------------------------------
/**
* Will default to self if not overridden
* Will default to `'self'` if not overridden
*
* @var list<string>|string|null
*/
@@ -64,6 +69,21 @@ class ContentSecurityPolicy extends BaseConfig
'www.google.com www.gstatic.com'
];
/**
* Specifies valid sources for JavaScript <script> elements.
*
* @var list<string>|string
*/
public array|string $scriptSrcElem = 'self';
/**
* Specifies valid sources for JavaScript inline event
* handlers and JavaScript URLs.
*
* @var list<string>|string
*/
public array|string $scriptSrcAttr = 'self';
/**
* Lists allowed stylesheets' URLs.
*
@@ -76,6 +96,21 @@ class ContentSecurityPolicy extends BaseConfig
'https://fonts.googleapis.com',
];
/**
* Specifies valid sources for stylesheets <link> elements.
*
* @var list<string>|string
*/
public array|string $styleSrcElem = 'self';
/**
* Specifies valid sources for stylesheets inline
* style attributes and `<style>` elements.
*
* @var list<string>|string
*/
public array|string $styleSrcAttr = 'self';
/**
* Defines the origins from which images can be loaded.
*
@@ -169,6 +204,11 @@ class ContentSecurityPolicy extends BaseConfig
*/
public $manifestSrc;
/**
* @var list<string>|string
*/
public array|string $workerSrc = [];
/**
* Limits the kinds of plugins a page may invoke.
*
@@ -184,17 +224,17 @@ class ContentSecurityPolicy extends BaseConfig
public $sandbox;
/**
* Nonce tag for style
* Nonce placeholder for style tags.
*/
public string $styleNonceTag = '{csp-style-nonce}';
/**
* Nonce tag for script
* Nonce placeholder for script tags.
*/
public string $scriptNonceTag = '{csp-script-nonce}';
/**
* Replace nonce tag automatically
* Replace nonce tag automatically?
*/
public bool $autoNonce = true;
}

View File

@@ -85,7 +85,7 @@ class Cookie extends BaseConfig
* (empty string) means default SameSite attribute set by browsers (`Lax`)
* will be set on cookies. If set to `None`, `$secure` must also be set.
*
* @phpstan-var 'None'|'Lax'|'Strict'|''
* @var ''|'Lax'|'None'|'Strict'
*/
public string $samesite = 'Lax';

View File

@@ -42,6 +42,8 @@ class Database extends Config
'strictOn' => false,
'failover' => [],
'port' => 3306,
'numberNative' => false,
'foundRows' => false,
'dateFormat' => [
'date' => 'Y-m-d',
'datetime' => 'Y-m-d H:i:s',
@@ -55,26 +57,27 @@ class Database extends Config
* @var array<string, mixed>
*/
public array $tests = [
'DSN' => '',
'hostname' => 'localhost',
'username' => 'admin',
'password' => 'pointofsale',
'database' => 'ospos',
'DBDriver' => 'MySQLi',
'DBPrefix' => 'ospos_',
'pConnect' => false,
'DBDebug' => (ENVIRONMENT !== 'production'),
'charset' => 'utf8mb4',
'DBCollat' => 'utf8mb4_general_ci',
'swapPre' => '',
'encrypt' => false,
'compress' => false,
'strictOn' => false,
'failover' => [],
'port' => 3306,
'foreignKeys' => true,
'busyTimeout' => 1000,
'dateFormat' => [
'DSN' => '',
'hostname' => 'localhost',
'username' => 'admin',
'password' => 'pointofsale',
'database' => 'ospos',
'DBDriver' => 'MySQLi',
'DBPrefix' => 'ospos_',
'pConnect' => false,
'DBDebug' => (ENVIRONMENT !== 'production'),
'charset' => 'utf8mb4',
'DBCollat' => 'utf8mb4_general_ci',
'swapPre' => '',
'encrypt' => false,
'compress' => false,
'strictOn' => false,
'failover' => [],
'port' => 3306,
'foreignKeys' => true,
'busyTimeout' => 1000,
'synchronous' => null,
'dateFormat' => [
'date' => 'Y-m-d',
'datetime' => 'Y-m-d H:i:s',
'time' => 'H:i:s',

View File

@@ -2,9 +2,6 @@
namespace Config;
/**
* @immutable
*/
class DocTypes
{
/**

View File

@@ -30,6 +30,11 @@ class Email extends BaseConfig
*/
public string $SMTPHost = 'mail.mxserver.com';
/**
* Which SMTP authentication method to use: login, plain
*/
public string $SMTPAuthMethod = 'login';
/**
* SMTP Username
*/

View File

@@ -23,6 +23,23 @@ class Encryption extends BaseConfig
*/
public string $key = '';
/**
* --------------------------------------------------------------------------
* Previous Encryption Keys
* --------------------------------------------------------------------------
*
* When rotating encryption keys, add old keys here to maintain ability
* to decrypt data encrypted with previous keys. Encryption always uses
* the current $key. Decryption tries current key first, then falls back
* to previous keys if decryption fails.
*
* In .env file, use comma-separated string:
* encryption.previousKeys = hex2bin:9be8c64fcea509867...,hex2bin:3f5a1d8e9c2b7a4f6...
*
* @var list<string>|string
*/
public array|string $previousKeys = '';
/**
* --------------------------------------------------------------------------
* Encryption Driver to Use

View File

@@ -65,7 +65,10 @@ class Filters extends BaseFilters
* List of filter aliases that are always
* applied before and after every request.
*
* @var array<string, array<string, array<string, string>>>|array<string, list<string>>
* @var array{
* before: array<string, array{except: list<string>|string}>|list<string>,
* after: array<string, array{except: list<string>|string}>|list<string>
* }
*/
public array $globals = [
'before' => [
@@ -100,7 +103,7 @@ class Filters extends BaseFilters
* before or after URI patterns.
*
* Example:
* isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
* 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
*
* @var array<string, array<string, list<string>>>
*/

View File

@@ -61,4 +61,13 @@ class Format extends BaseConfig
'application/xml' => 0,
'text/xml' => 0,
];
/**
* --------------------------------------------------------------------------
* Maximum depth for JSON encoding.
* --------------------------------------------------------------------------
*
* This value determines how deep the JSON encoder will traverse nested structures.
*/
public int $jsonEncodeDepth = 512;
}

40
app/Config/Hostnames.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
namespace Config;
class Hostnames
{
// List of known two-part TLDs for subdomain extraction
public const TWO_PART_TLDS = [
'co.uk', 'org.uk', 'gov.uk', 'ac.uk', 'sch.uk', 'ltd.uk', 'plc.uk',
'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'asn.au', 'id.au',
'co.jp', 'ac.jp', 'go.jp', 'or.jp', 'ne.jp', 'gr.jp',
'co.nz', 'org.nz', 'govt.nz', 'ac.nz', 'net.nz', 'geek.nz', 'maori.nz', 'school.nz',
'co.in', 'net.in', 'org.in', 'ind.in', 'ac.in', 'gov.in', 'res.in',
'com.cn', 'net.cn', 'org.cn', 'gov.cn', 'edu.cn',
'com.sg', 'net.sg', 'org.sg', 'gov.sg', 'edu.sg', 'per.sg',
'co.za', 'org.za', 'gov.za', 'ac.za', 'net.za',
'co.kr', 'or.kr', 'go.kr', 'ac.kr', 'ne.kr', 'pe.kr',
'co.th', 'or.th', 'go.th', 'ac.th', 'net.th', 'in.th',
'com.my', 'net.my', 'org.my', 'edu.my', 'gov.my', 'mil.my', 'name.my',
'com.mx', 'org.mx', 'net.mx', 'edu.mx', 'gob.mx',
'com.br', 'net.br', 'org.br', 'gov.br', 'edu.br', 'art.br', 'eng.br',
'co.il', 'org.il', 'ac.il', 'gov.il', 'net.il', 'muni.il',
'co.id', 'or.id', 'ac.id', 'go.id', 'net.id', 'web.id', 'my.id',
'com.hk', 'edu.hk', 'gov.hk', 'idv.hk', 'net.hk', 'org.hk',
'com.tw', 'net.tw', 'org.tw', 'edu.tw', 'gov.tw', 'idv.tw',
'com.sa', 'net.sa', 'org.sa', 'gov.sa', 'edu.sa', 'sch.sa', 'med.sa',
'co.ae', 'net.ae', 'org.ae', 'gov.ae', 'ac.ae', 'sch.ae',
'com.tr', 'net.tr', 'org.tr', 'gov.tr', 'edu.tr', 'av.tr', 'gen.tr',
'co.ke', 'or.ke', 'go.ke', 'ac.ke', 'sc.ke', 'me.ke', 'mobi.ke', 'info.ke',
'com.ng', 'org.ng', 'gov.ng', 'edu.ng', 'net.ng', 'sch.ng', 'name.ng',
'com.pk', 'net.pk', 'org.pk', 'gov.pk', 'edu.pk', 'fam.pk',
'com.eg', 'edu.eg', 'gov.eg', 'org.eg', 'net.eg',
'com.cy', 'net.cy', 'org.cy', 'gov.cy', 'ac.cy',
'com.lk', 'org.lk', 'edu.lk', 'gov.lk', 'net.lk', 'int.lk',
'com.bd', 'net.bd', 'org.bd', 'ac.bd', 'gov.bd', 'mil.bd',
'com.ar', 'net.ar', 'org.ar', 'gov.ar', 'edu.ar', 'mil.ar',
'gob.cl', 'com.pl', 'net.pl', 'org.pl', 'gov.pl', 'edu.pl',
'co.ir', 'ac.ir', 'org.ir', 'id.ir', 'gov.ir', 'sch.ir', 'net.ir',
];
}

View File

@@ -16,6 +16,8 @@ class Images extends BaseConfig
/**
* The path to the image library.
* Required for ImageMagick, GraphicsMagick, or NetPBM.
*
* @deprecated 4.7.0 No longer used.
*/
public string $libraryPath = '/usr/local/bin/convert';

View File

@@ -4,6 +4,7 @@ namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Log\Handlers\FileHandler;
use CodeIgniter\Log\Handlers\HandlerInterface;
class Logger extends BaseConfig
{
@@ -73,7 +74,7 @@ class Logger extends BaseConfig
* Handlers are executed in the order defined in this array, starting with
* the handler on top and continuing down.
*
* @var array<class-string, array<string, int|list<string>|string>>
* @var array<class-string<HandlerInterface>, array<string, int|list<string>|string>>
*/
public array $handlers = [
/*

View File

@@ -47,4 +47,19 @@ class Migrations extends BaseConfig
* - Y_m_d_His_
*/
public string $timestampFormat = 'YmdHis_';
/**
* --------------------------------------------------------------------------
* Enable/Disable Migration Lock
* --------------------------------------------------------------------------
*
* Locking is disabled by default.
*
* When enabled, it will prevent multiple migration processes
* from running at the same time by using a lock mechanism.
*
* This is useful in production environments to avoid conflicts
* or race conditions during concurrent deployments.
*/
public bool $lock = false;
}

View File

@@ -3,8 +3,6 @@
namespace Config;
/**
* Mimes
*
* This file contains an array of mime types. It is used by the
* Upload class to help identify allowed file types.
*
@@ -15,8 +13,6 @@ namespace Config;
*
* When working with mime types, please make sure you have the ´fileinfo´
* extension enabled to reliably detect the media types.
*
* @immutable
*/
class Mimes
{
@@ -482,13 +478,16 @@ class Mimes
'application/sla',
'application/vnd.ms-pki.stl',
'application/x-navistyle',
'model/stl',
'application/octet-stream',
],
];
/**
* Attempts to determine the best mime type for the given file extension.
*
* @return string|null The mime type found, or none if unable to determine.
* @param string $extension
* @return array|string|null The mime type found, or none if unable to determine.
*/
public static function guessTypeFromExtension(string $extension): array|string|null
{
@@ -524,7 +523,7 @@ class Mimes
}
// Reverse check the mime type list if no extension was proposed.
// This search is order sensitive!
// This search is order-sensitive!
foreach (static::$mimes as $ext => $types) {
if (in_array($type, (array) $types, true)) {
return $ext;

View File

@@ -9,8 +9,6 @@ use CodeIgniter\Modules\Modules as BaseModules;
*
* NOTE: This class is required prior to Autoloader instantiation,
* and does not extend BaseConfig.
*
* @immutable
*/
class Modules extends BaseModules
{

View File

@@ -8,7 +8,7 @@ namespace Config;
* NOTE: This class does not extend BaseConfig for performance reasons.
* So you cannot replace the property values with Environment Variables.
*
* @immutable
* WARNING: Do not use these options when running the app in the Worker Mode.
*/
class Optimize
{

View File

@@ -15,8 +15,6 @@ namespace Config;
*
* NOTE: This class is required prior to Autoloader instantiation,
* and does not extend BaseConfig.
*
* @immutable
*/
class Paths
{
@@ -77,4 +75,16 @@ class Paths
* is used when no value is provided to `Services::renderer()`.
*/
public string $viewDirectory = __DIR__ . '/../Views';
/**
* ---------------------------------------------------------------
* ENVIRONMENT DIRECTORY NAME
* ---------------------------------------------------------------
*
* This variable must contain the name of the directory where
* the .env file is located.
* Please consider security implications when changing this
* value - the directory should not be publicly accessible.
*/
public string $envDirectory = __DIR__ . '/../../';
}

View File

@@ -96,6 +96,15 @@ class Routing extends BaseRouting
*/
public bool $autoRoute = true;
/**
* If TRUE, the system will look for attributes on controller
* class and methods that can run before and after the
* controller/method.
*
* If FALSE, will ignore any attributes.
*/
public bool $useControllerAttributes = true;
/**
* For Defined Routes.
* If TRUE, will enable the use of the 'prioritize' option

View File

@@ -13,9 +13,9 @@ class Security extends BaseConfig
*
* Protection Method for Cross Site Request Forgery protection.
*
* @var string|false 'cookie', 'session', or false
* @var string 'cookie' or 'session'
*/
public string|false $csrfProtection = 'session';
public string $csrfProtection = 'session';
/**
* --------------------------------------------------------------------------

View File

@@ -2,6 +2,7 @@
namespace Config;
use App\Libraries\MY_Language;
use Locale;
use HTMLPurifier;
use HTMLPurifier_Config;
@@ -38,9 +39,11 @@ class Services extends BaseService
/**
* Responsible for loading the language string translations.
*
* @param string|null $locale
* @param bool $getShared
* @return MY_Language
*/
public static function language(?string $locale = null, bool $getShared = true)
public static function language(?string $locale = null, bool $getShared = true): MY_Language
{
if ($getShared) {
return static::getSharedInstance('language', $locale)->setLocale($locale);
@@ -55,12 +58,12 @@ class Services extends BaseService
// Use '?:' for empty string check
$locale = $locale ?: $requestLocale;
return new \App\Libraries\MY_Language($locale);
return new MY_Language($locale);
}
private static $htmlPurifier;
private static HTMLPurifier $htmlPurifier;
public static function htmlPurifier($getShared = true)
public static function htmlPurifier($getShared = true): object
{
if ($getShared) {
return static::getSharedInstance('htmlPurifier');

View File

@@ -3,10 +3,10 @@
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Session\Handlers\BaseHandler;
use CodeIgniter\Session\Handlers\DatabaseHandler;
use CodeIgniter\Session\Handlers\FileHandler;
use Config\Database;
class Session extends BaseConfig
{
@@ -139,7 +139,7 @@ class Session extends BaseConfig
$this->driver = FileHandler::class;
$this->savePath = WRITEPATH . 'session';
}
} catch (\CodeIgniter\Database\Exceptions\DatabaseException $e) {
} catch (DatabaseException $e) {
$this->driver = FileHandler::class;
$this->savePath = WRITEPATH . 'session';
}

View File

@@ -119,4 +119,29 @@ class Toolbar extends BaseConfig
public array $watchedExtensions = [
'php', 'css', 'js', 'html', 'svg', 'json', 'env',
];
/**
* --------------------------------------------------------------------------
* Ignored HTTP Headers
* --------------------------------------------------------------------------
*
* CodeIgniter Debug Toolbar normally injects HTML and JavaScript into every
* HTML response. This is correct for full page loads, but it breaks requests
* that expect only a clean HTML fragment.
*
* Libraries like HTMX, Unpoly, and Hotwire (Turbo) update parts of the page or
* manage navigation on the client side. Injecting the Debug Toolbar into their
* responses can cause invalid HTML, duplicated scripts, or JavaScript errors
* (such as infinite loops or "Maximum call stack size exceeded").
*
* Any request containing one of the following headers is treated as a
* client-managed or partial request, and the Debug Toolbar injection is skipped.
*
* @var array<string, string|null>
*/
public array $disableOnHeaders = [
'X-Requested-With' => 'xmlhttprequest', // AJAX requests
'HX-Request' => 'true', // HTMX requests
'X-Up-Version' => null, // Unpoly partial requests
];
}

View File

@@ -230,9 +230,13 @@ class UserAgents extends BaseConfig
*/
public array $robots = [
'googlebot' => 'Googlebot',
'google-pagerenderer' => 'Google Page Renderer',
'google-read-aloud' => 'Google Read Aloud',
'google-safety' => 'Google Safety Bot',
'msnbot' => 'MSNBot',
'baiduspider' => 'Baiduspider',
'bingbot' => 'Bing',
'bingpreview' => 'BingPreview',
'slurp' => 'Inktomi Slurp',
'yahoo' => 'Yahoo',
'ask jeeves' => 'Ask Jeeves',
@@ -248,5 +252,11 @@ class UserAgents extends BaseConfig
'ia_archiver' => 'Alexa Crawler',
'MJ12bot' => 'Majestic-12',
'Uptimebot' => 'Uptimebot',
'duckduckbot' => 'DuckDuckBot',
'sogou' => 'Sogou Spider',
'exabot' => 'Exabot',
'bot' => 'Generic Bot',
'crawler' => 'Generic Crawler',
'spider' => 'Generic Spider',
];
}

View File

@@ -59,4 +59,21 @@ class View extends BaseView
* @var list<class-string<ViewDecoratorInterface>>
*/
public array $decorators = [];
/**
* Subdirectory within app/Views for namespaced view overrides.
*
* Namespaced views will be searched in:
*
* app/Views/{$appOverridesFolder}/{Namespace}/{view_path}.{php|html...}
*
* This allows application-level overrides for package or module views
* without modifying vendor source files.
*
* Examples:
* 'overrides' -> app/Views/overrides/Example/Blog/post/card.php
* 'vendor' -> app/Views/vendor/Example/Blog/post/card.php
* '' -> app/Views/Example/Blog/post/card.php (direct mapping)
*/
public string $appOverridesFolder = 'overrides';
}

62
app/Config/WorkerMode.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
namespace Config;
/**
* This configuration controls how CodeIgniter behaves when running
* in worker mode (with FrankenPHP).
*/
class WorkerMode
{
/**
* Persistent Services
*
* List of service names that should persist across requests.
* These services will NOT be reset between requests.
*
* Services not in this list will be reset for each request to prevent
* state leakage.
*
* Recommended persistent services:
* - `autoloader`: PSR-4 autoloading configuration
* - `locator`: File locator
* - `exceptions`: Exception handler
* - `commands`: CLI commands registry
* - `codeigniter`: Main application instance
* - `superglobals`: Superglobals wrapper
* - `routes`: Router configuration
* - `cache`: Cache instance
*
* @var list<string>
*/
public array $persistentServices = [
'autoloader',
'locator',
'exceptions',
'commands',
'codeigniter',
'superglobals',
'routes',
'cache',
];
/**
* Reset Event Listeners
*
* List of event names whose listeners should be removed between requests.
* Use this if you register event listeners inside other event callbacks
* (rather than at the top level of Config/Events.php), which would cause
* them to accumulate across requests in worker mode.
*
* @var list<string>
*/
public array $resetEventListeners = [];
/**
* Force Garbage Collection
*
* Whether to force garbage collection after each request.
* Helps prevent memory leaks at a small performance cost.
*/
public bool $forceGarbageCollection = true;
}

View File

@@ -132,7 +132,7 @@ class Attributes extends Secure_Controller
$definition_name = $definition_data['definition_name'];
if ($this->attribute->save_definition($definition_data, $definition_id)) {
if ($this->attribute->saveDefinition($definition_data, $definition_id)) {
// New definition
if ($definition_id == NO_DEFINITION_ID) {
$definition_values = json_decode(html_entity_decode($this->request->getPost('definition_values')));

View File

@@ -3,56 +3,46 @@
namespace App\Controllers;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\CLIRequest;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;
/**
* Class BaseController
*
* BaseController provides a convenient place for loading components
* and performing functions that are needed by all your controllers.
* Extend this class in any new controllers:
* class Home extends BaseController
*
* For security be sure to declare any new methods as protected or private.
* Extend this class in any new controllers:
* ```
* class Home extends BaseController
* ```
*
* For security, be sure to declare any new methods as protected or private.
*/
abstract class BaseController extends Controller
{
/**
* Instance of the main Request object.
*
* @var CLIRequest|IncomingRequest
*/
protected $request;
/**
* An array of helpers to be loaded automatically upon
* class instantiation. These helpers will be available
* to all other controllers that extend BaseController.
*
* @var list<string>
*/
protected $helpers = [];
/**
* Be sure to declare properties for any property fetch you initialized.
* The creation of dynamic property is deprecated in PHP 8.2.
*/
// protected $session;
/**
* @param RequestInterface $request
* @param ResponseInterface $response
* @param LoggerInterface $logger
* @return void
*/
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger): void
{
// Do Not Edit This Line
// Load here all helpers you want to be available in your controllers that extend BaseController.
// Caution: Do not put the this below the parent::initController() call below.
// $this->helpers = ['form', 'url'];
// Caution: Do not edit this line.
parent::initController($request, $response, $logger);
// Preload any models, libraries, etc, here.
// E.g.: $this->session = service('session');
// $this->session = service('session');
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Controllers;
use App\Libraries\Barcode_lib;
use App\Libraries\Image_lib;
use App\Libraries\Mailchimp_lib;
use App\Libraries\Receiving_lib;
use App\Libraries\Sale_lib;
@@ -251,10 +250,6 @@ class Config extends Secure_Controller
$data['image_allowed_types'] = array_combine($image_allowed_types, $image_allowed_types);
$data['selected_image_allowed_types'] = explode(',', $this->config['image_allowed_types']);
$exif_fields = ['Make', 'Model', 'Orientation', 'Copyright', 'Software', 'DateTime', 'GPS'];
$data['exif_fields'] = array_combine($exif_fields, $exif_fields);
$data['selected_exif_fields'] = array_filter(explode(',', $this->config['exif_fields_to_keep'] ?? ''));
// Integrations Related fields
$data['mailchimp'] = [];
@@ -360,15 +355,6 @@ class Config extends Secure_Controller
$file->move(FCPATH . 'uploads/', $file_info['raw_name'] . '.' . $file_info['file_ext'], true);
$exif_fields_to_keep = array_filter(explode(',', $this->appconfig->get_value('exif_fields_to_keep', 'Copyright,Orientation,Software')));
if (!empty($exif_fields_to_keep)) {
$image_lib = new Image_lib();
$filepath = FCPATH . 'uploads/' . $file_info['raw_name'] . '.' . $file_info['file_ext'];
if (!$image_lib->stripEXIF($filepath, $exif_fields_to_keep)) {
log_message('warning', 'EXIF stripping failed for: ' . $filepath);
}
}
return ($file_info);
}
@@ -381,7 +367,7 @@ class Config extends Secure_Controller
*/
public function postSaveGeneral(): ResponseInterface
{
$batch_save_data = [
$batchSaveData = [
'theme' => $this->request->getPost('theme'),
'login_form' => $this->request->getPost('login_form'),
'default_sales_discount_type' => $this->request->getPost('default_sales_discount_type') != null,
@@ -396,8 +382,7 @@ class Config extends Secure_Controller
'image_max_width' => $this->request->getPost('image_max_width', FILTER_SANITIZE_NUMBER_INT),
'image_max_height' => $this->request->getPost('image_max_height', FILTER_SANITIZE_NUMBER_INT),
'image_max_size' => $this->request->getPost('image_max_size', FILTER_SANITIZE_NUMBER_INT),
'image_allowed_types' => implode(',', $this->request->getPost('image_allowed_types') ?? []),
'exif_fields_to_keep' => implode(',', $this->request->getPost('exif_fields_to_keep') ?? []),
'image_allowed_types' => implode(',', $this->request->getPost('image_allowed_types')),
'gcaptcha_enable' => $this->request->getPost('gcaptcha_enable') != null,
'gcaptcha_secret_key' => $this->request->getPost('gcaptcha_secret_key'),
'gcaptcha_site_key' => $this->request->getPost('gcaptcha_site_key'),
@@ -413,19 +398,19 @@ class Config extends Secure_Controller
$this->module->set_show_office_group($this->request->getPost('show_office_group') != null);
if ($batch_save_data['category_dropdown'] == 1) {
$definition_data['definition_name'] = 'ospos_category';
$definition_data['definition_flags'] = 0;
$definition_data['definition_type'] = 'DROPDOWN';
$definition_data['definition_id'] = CATEGORY_DEFINITION_ID;
$definition_data['deleted'] = 0;
if ($batchSaveData['category_dropdown']) {
$definitionData['definition_name'] = 'ospos_category';
$definitionData['definition_flags'] = 0;
$definitionData['definition_type'] = 'DROPDOWN';
$definitionData['definition_id'] = CATEGORY_DEFINITION_ID;
$definitionData['deleted'] = 0;
$this->attribute->save_definition($definition_data, CATEGORY_DEFINITION_ID);
} elseif ($batch_save_data['category_dropdown'] == NO_DEFINITION_ID) {
$this->attribute->saveDefinition($definitionData, CATEGORY_DEFINITION_ID);
} elseif ($batchSaveData['category_dropdown'] == NO_DEFINITION_ID) {
$this->attribute->deleteDefinition(CATEGORY_DEFINITION_ID);
}
$success = $this->appconfig->batch_save($batch_save_data);
$success = $this->appconfig->batch_save($batchSaveData);
return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]);
}

View File

@@ -35,12 +35,12 @@ class Home extends Secure_Controller
}
/**
* Load "change employee password" form
* Load the "change employee password" form
*
* @param int $employeeId
* @return ResponseInterface|string
* @noinspection PhpUnused
*/
public function getChangePassword(int $employeeId = NEW_ENTRY)
public function getChangePassword(int $employeeId = NEW_ENTRY): ResponseInterface|string
{
$loggedInEmployee = $this->employee->get_logged_in_employee_info();
$currentPersonId = $loggedInEmployee->person_id;

View File

@@ -3,10 +3,7 @@
namespace App\Controllers;
use App\Libraries\Barcode_lib;
use App\Libraries\Image_lib;
use App\Libraries\Item_lib;
use App\Models\Appconfig;
use App\Models\Attribute;
use App\Models\Inventory;
use App\Models\Item;
@@ -16,7 +13,6 @@ use App\Models\Item_taxes;
use App\Models\Stock_location;
use App\Models\Supplier;
use App\Models\Tax_category;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Images\Handlers\BaseHandler;
use CodeIgniter\HTTP\DownloadResponse;
@@ -41,7 +37,6 @@ class Items extends Secure_Controller
private Stock_location $stock_location;
private Supplier $supplier;
private Tax_category $tax_category;
private Appconfig $appconfig;
private array $config;
@@ -65,7 +60,6 @@ class Items extends Secure_Controller
$this->stock_location = model(Stock_location::class);
$this->supplier = model(Supplier::class);
$this->tax_category = model(Tax_category::class);
$this->appconfig = model(Appconfig::class);
$this->config = config(OSPOS::class)->settings;
}
@@ -77,7 +71,7 @@ class Items extends Secure_Controller
$this->session->set('allow_temp_items', 0);
$data['table_headers'] = get_items_manage_table_headers();
// Restore stock_location from URL or session
$stockLocation = $this->request->getGet('stock_location', FILTER_SANITIZE_NUMBER_INT);
$data['stock_location'] = $stockLocation
@@ -512,7 +506,7 @@ class Items extends Secure_Controller
$data['definition_names'] = $this->attribute->get_definition_names();
foreach ($data['definition_values'] as $definition_id => $definition_value) {
$attribute_value = $this->attribute->get_attribute_value($item_id, $definition_id);
$attribute_value = $this->attribute->getAttributeValue($item_id, $definition_id);
$attribute_id = (empty($attribute_value) || empty($attribute_value->attribute_id)) ? null : $attribute_value->attribute_id;
$values = &$data['definition_values'][$definition_id];
$values['attribute_id'] = $attribute_id;
@@ -548,7 +542,7 @@ class Items extends Secure_Controller
$data['definition_names'] = $this->attribute->get_definition_names();
foreach ($data['definition_values'] as $definition_id => $definition_value) {
$attribute_value = $this->attribute->get_attribute_value($item_id, $definition_id);
$attribute_value = $this->attribute->getAttributeValue($item_id, $definition_id);
$attribute_id = (empty($attribute_value) || empty($attribute_value->attribute_id)) ? null : $attribute_value->attribute_id;
$values = &$data['definition_values'][$definition_id];
$values['attribute_id'] = $attribute_id;
@@ -717,7 +711,7 @@ class Items extends Secure_Controller
$item_quantity = $this->item_quantity->get_item_quantity($item_id, $location['location_id']);
if ($item_quantity->quantity != $updated_quantity || $new_item) {
$success &= $this->item_quantity->save_value($location_detail, $item_id, $location['location_id']);
$success = $success && $this->item_quantity->save_value($location_detail, $item_id, $location['location_id']);
$inv_data = [
'trans_date' => date('Y-m-d H:i:s'),
@@ -728,10 +722,10 @@ class Items extends Secure_Controller
'trans_inventory' => $updated_quantity - $item_quantity->quantity
];
$success &= $this->inventory->insert($inv_data, false);
$success = $success && $this->inventory->insert($inv_data, false);
}
}
$this->saveItemAttributes($item_id);
$success = $success && $this->saveItemAttributes($item_id);
if ($success && $upload_success) {
$message = lang('Items.successful_' . ($new_item ? 'adding' : 'updating')) . ' ' . $item_data['name'];
@@ -781,7 +775,7 @@ class Items extends Secure_Controller
$filename = $file->getClientName();
$info = pathinfo($filename);
// Sanitize filename to remove problematic characters like spaces
$sanitized_name = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $info['filename']);
@@ -792,16 +786,6 @@ class Items extends Secure_Controller
];
$file->move(FCPATH . 'uploads/item_pics/', $file_info['raw_name'] . '.' . $file_info['file_ext'], true);
$exif_fields_to_keep = array_filter(explode(',', $this->appconfig->get_value('exif_fields_to_keep', 'Copyright,Orientation,Software')));
if (!empty($exif_fields_to_keep)) {
$image_lib = new Image_lib();
$filepath = FCPATH . 'uploads/item_pics/' . $file_info['raw_name'] . '.' . $file_info['file_ext'];
if (!$image_lib->stripEXIF($filepath, $exif_fields_to_keep)) {
log_message('warning', 'EXIF stripping failed for: ' . $filepath);
}
}
return ($file_info);
}
@@ -954,7 +938,7 @@ class Items extends Secure_Controller
*/
public function getGenerateCsvFile(): DownloadResponse
{
helper('importfile_helper');
helper('importfile');
$name = 'import_items.csv';
$allowed_locations = $this->stock_location->get_allowed_locations();
$allowed_attributes = $this->attribute->get_definition_names();
@@ -973,14 +957,13 @@ class Items extends Secure_Controller
}
/**
* Imports items from CSV formatted file.
* Imports items from a CSV formatted file.
* @return ResponseInterface
* @throws ReflectionException
* @noinspection PhpUnused
*/
public function postImportCsvFile(): ResponseInterface
{
helper('importfile_helper');
helper('importfile');
try {
if ($_FILES['file_path']['error'] !== UPLOAD_ERR_OK) {
return $this->response->setJSON(['success' => false, 'message' => lang('Items.csv_import_failed')]);
@@ -989,33 +972,33 @@ class Items extends Secure_Controller
set_time_limit(240);
$failCodes = [];
$csv_rows = get_csv_file($_FILES['file_path']['tmp_name']);
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
$allowed_stock_locations = $this->stock_location->get_allowed_locations();
$attribute_definition_names = $this->attribute->get_definition_names();
$csvRows = get_csv_file($_FILES['file_path']['tmp_name']);
$employeeId = $this->employee->get_logged_in_employee_info()->person_id;
$allowedStockLocations = $this->stock_location->get_allowed_locations();
$attributeDefinitionNames = $this->attribute->get_definition_names();
unset($attribute_definition_names[NEW_ENTRY]); // Removes the common_none_selected_text from the array
unset($attributeDefinitionNames[NEW_ENTRY]); // Removes the common_none_selected_text from the array
$attribute_data = [];
$attributeData = [];
foreach ($attribute_definition_names as $definition_name) {
$attribute_data[$definition_name] = $this->attribute->get_definition_by_name($definition_name)[0];
foreach ($attributeDefinitionNames as $definitionName) {
$attributeData[$definitionName] = $this->attribute->get_definition_by_name($definitionName)[0];
if ($attribute_data[$definition_name]['definition_type'] === DROPDOWN) {
$attribute_data[$definition_name]['dropdown_values'] = $this->attribute->get_definition_values($attribute_data[$definition_name]['definition_id']);
if ($attributeData[$definitionName]['definition_type'] === DROPDOWN) {
$attributeData[$definitionName]['dropdown_values'] = $this->attribute->get_definition_values($attributeData[$definitionName]['definition_id']);
}
}
$db = db_connect();
$db->transBegin(); // TODO: This section needs to be reworked so that the data array is being created then passed to the Item model because $db doesn't exist in the controller without being instantiated, but database operations should be restricted to the model
foreach ($csv_rows as $key => $row) {
$is_failed_row = false;
$item_id = (int)$row['Id'];
$is_update = ($item_id > 0);
$item_data = [
'item_id' => $item_id,
foreach ($csvRows as $key => $row) {
$isFailedRow = false;
$itemId = (int)$row['Id'];
$isUpdate = ($itemId > 0);
$itemData = [
'item_id' => $itemId,
'name' => $row['Item Name'],
'description' => $row['Description'],
'description' => filter_var($row['Description'], FILTER_SANITIZE_FULL_SPECIAL_CHARS),
'category' => $row['Category'],
'cost_price' => $row['Cost Price'],
'unit_price' => $row['Unit Price'],
@@ -1025,25 +1008,26 @@ class Items extends Secure_Controller
'pic_filename' => $row['Image']
];
if (!empty($row['supplier ID'])) {
$item_data['supplier_id'] = $this->supplier->exists($row['Supplier ID']) ? $row['Supplier ID'] : null;
if (!empty($row['Supplier ID'])) {
$itemData['supplier_id'] = $this->supplier->exists($row['Supplier ID']) ? $row['Supplier ID'] : null;
}
if ($is_update) {
$item_data['allow_alt_description'] = empty($row['Allow Alt Description']) ? null : $row['Allow Alt Description'];
$item_data['is_serialized'] = empty($row['Item has Serial Number']) ? null : $row['Item has Serial Number'];
if ($isUpdate) {
$itemData['allow_alt_description'] = $row['Allow Alt Description'] === '' ? null : $row['Allow Alt Description'];
$itemData['is_serialized'] = $row['Item has Serial Number'] === '' ? null : $row['Item has Serial Number'];
} else {
$item_data['allow_alt_description'] = empty($row['Allow Alt Description']) ? '0' : '1';
$item_data['is_serialized'] = empty($row['Item has Serial Number']) ? '0' : '1';
$itemData['allow_alt_description'] = $row['Allow Alt Description'] === '' ? '0' : '1';
$itemData['is_serialized'] = $row['Item has Serial Number'] === '' ? '0' : '1';
}
if (!empty($row['Barcode']) && !$is_update) {
$item_data['item_number'] = $row['Barcode'];
$is_failed_row = $this->item->item_number_exists($item_data['item_number']);
if (!empty($row['Barcode'])) {
$itemData['item_number'] = $row['Barcode'];
$isFailedRow = $this->item->item_number_exists($itemData['item_number'], $itemId);
}
if (!$is_failed_row) {
$invalidLocations = $this->validateCSVStockLocations($row, $allowedStockLocations);
if (!$isFailedRow) {
$allowedStockLocations = $this->stock_location->get_allowed_locations();
$isFailedRow = $this->validateCSVData($row, $itemData, $allowedStockLocations, $attributeDefinitionNames, $attributeData);
if (!empty($invalidLocations)) {
$isFailedRow = true;
log_message('error', 'CSV import: Invalid stock location(s) found: ' . implode(', ', $invalidLocations));
@@ -1051,28 +1035,35 @@ class Items extends Secure_Controller
}
// Remove false, null, '' and empty strings but keep 0
$item_data = array_filter($item_data, function ($value) {
$itemData = array_filter($itemData, function ($value) {
return $value !== null && strlen($value);
});
if (!$is_failed_row && $this->item->save_value($item_data, $item_id)) {
$this->save_tax_data($row, $item_data);
$this->save_inventory_quantities($row, $item_data, $allowed_stock_locations, $employee_id);
$is_failed_row = $this->save_attribute_data($row, $item_data, $attribute_data); // TODO: $is_failed_row never gets used after this.
if (!$isFailedRow && $this->item->save_value($itemData, $itemId)) {
$this->save_tax_data($row, $itemData);
$this->save_inventory_quantities($row, $itemData, $allowedStockLocations, $employeeId);
$csvAttributeValues = $this->extractAttributeData($row);
$isFailedRow = !$this->attribute->saveCSVRowAttributeData($csvAttributeValues, $itemData, $attributeData);
if ($isFailedRow) {
$failedRow = $key + 2;
$failCodes[] = $failedRow;
log_message('error', "CSV Item import failed on line $failedRow while saving attributes.");
continue;
}
if ($is_update) {
$item_data = array_merge($item_data, get_object_vars($this->item->get_info_by_id_or_number($item_id)));
if ($isUpdate) {
$itemData = array_merge($itemData, get_object_vars($this->item->get_info_by_id_or_number($itemId)));
}
} else {
$failed_row = $key + 2;
$failCodes[] = $failed_row;
log_message('error', "CSV Item import failed on line $failed_row. This item was not imported.");
$failedRow = $key + 2;
$failCodes[] = $failedRow;
log_message('error', "CSV Item import failed on line $failedRow. This item was not imported.");
}
unset($csv_rows[$key]);
unset($csvRows[$key]);
}
$csv_rows = null;
$csvRows = null;
if (count($failCodes) > 0) {
$message = lang('Items.csv_import_partially_failed', [count($failCodes), implode(', ', $failCodes)]);
@@ -1080,6 +1071,7 @@ class Items extends Secure_Controller
return $this->response->setJSON(['success' => false, 'message' => $message]);
} else {
$db->transCommit();
$this->attribute->deleteOrphanedValues();
return $this->response->setJSON(['success' => true, 'message' => lang('Items.csv_import_success')]);
}
@@ -1093,6 +1085,20 @@ class Items extends Secure_Controller
}
private function extractAttributeData(array $row): array
{
$attributeData = [];
foreach ($row as $key => $value) {
if (str_starts_with($key, 'attribute_')) {
$definitionName = substr($key, 10);
$attributeData[$definitionName] = $value;
}
}
return $attributeData;
}
/**
* Validates that stock location columns in CSV row are valid locations
*
@@ -1121,87 +1127,99 @@ class Items extends Secure_Controller
* Checks the entire line of data in an import file for errors
*
* @param array $row
* @param array $item_data
* @param array $allowed_locations
* @param array $definition_names
* @param array $attribute_data
* @param array $itemData
* @param array $allowedStockLocations
* @param array $definitionNames
* @param array $attributeData
* @return bool Returns false if all data checks out and true when there is an error in the data
*/
private function data_error_check(array $row, array $item_data, array $allowed_locations, array $definition_names, array $attribute_data): bool // TODO: Long function and large number of parameters in the declaration... perhaps refactoring is needed
private function validateCSVData(array $row, array $itemData, array $allowedStockLocations, array $definitionNames, array $attributeData): bool // TODO: Long function and large number of parameters in the declaration... perhaps refactoring is needed
{
$item_id = $row['Id'];
$is_update = (bool)$item_id;
$itemId = $row['Id'];
$isUpdate = (bool)$itemId;
// Check for empty required fields
$check_for_empty = [
'name' => $item_data['name'],
'category' => $item_data['category'],
'unit_price' => $item_data['unit_price']
$valuesToCheckForEmpty = [
'name' => $itemData['name'],
'category' => $itemData['category'],
'unit_price' => $itemData['unit_price']
];
foreach ($check_for_empty as $key => $val) {
if (empty($val) && !$is_update) {
foreach ($valuesToCheckForEmpty as $key => $value) {
if (($value === null || $value === '') && !$isUpdate) {
log_message('error', "Empty required value in $key.");
return true;
}
}
if (!$is_update) {
$item_data['cost_price'] = empty($item_data['cost_price']) ? 0 : $item_data['cost_price']; // Allow for zero wholesale price
if (!$isUpdate) {
$itemData['cost_price'] = empty($itemData['cost_price']) ? 0 : $itemData['cost_price']; // Allow for zero wholesale price
} else {
if (!$this->item->exists($item_id)) {
log_message('error', "non-existent item_id: '$item_id' when either existing item_id or no item_id is required.");
if (!$this->item->exists($itemId)) {
log_message('error', "non-existent item_id: '$itemId' when either existing item_id or no item_id is required.");
return true;
}
}
// Build array of fields to check for numerics
$check_for_numeric_values = [
'cost_price' => $item_data['cost_price'],
'unit_price' => $item_data['unit_price'],
'reorder_level' => $item_data['reorder_level'],
$valuesToCheckForNumeric = [
'cost_price' => $itemData['cost_price'],
'unit_price' => $itemData['unit_price'],
'reorder_level' => $itemData['reorder_level'],
'supplier_id' => $row['Supplier ID'],
'Tax 1 Percent' => $row['Tax 1 Percent'],
'Tax 2 Percent' => $row['Tax 2 Percent']
];
foreach ($allowed_locations as $location_name) {
$check_for_numeric_values[] = $row["location_$location_name"];
foreach ($allowedStockLocations as $location_name) {
$valuesToCheckForNumeric[] = $row["location_$location_name"];
}
// Check for non-numeric values which require numeric
foreach ($check_for_numeric_values as $key => $value) {
foreach ($valuesToCheckForNumeric as $key => $value) {
if (!is_numeric($value) && !empty($value)) {
log_message('error', "non-numeric: '$value' for '$key' when numeric is required");
return true;
}
}
// Check stock locations
$invalidLocations = $this->validateCSVStockLocations($row, $allowedStockLocations);
if (!empty($invalidLocations)) {
log_message('error', 'CSV import: Invalid stock location(s) found: ' . implode(', ', $invalidLocations));
return true;
}
// Check Attribute Data
foreach ($definition_names as $definition_name) {
if (!empty($row["attribute_$definition_name"])) {
$definition_type = $attribute_data[$definition_name]['definition_type'];
$attribute_value = $row["attribute_$definition_name"];
foreach ($definitionNames as $definitionName) {
$attributeColumn = "attribute_$definitionName";
if (array_key_exists($attributeColumn, $row) && $row[$attributeColumn] != '') {
$definitionType = $attributeData[$definitionName]['definition_type'];
$attributeValue = $row[$attributeColumn];
switch ($definition_type) {
if (strcasecmp($attributeValue, '_DELETE_') === 0) {
continue;
}
switch ($definitionType) {
case DROPDOWN:
$dropdown_values = $attribute_data[$definition_name]['dropdown_values'];
$dropdown_values[] = '';
$dropdownValues = $attributeData[$definitionName]['dropdown_values'];
$dropdownValues[] = '';
if (!empty($attribute_value) && !in_array($attribute_value, $dropdown_values)) {
log_message('error', "Value: '$attribute_value' is not an acceptable DROPDOWN value");
if (!empty($attributeValue) && !in_array($attributeValue, $dropdownValues)) {
log_message('error', "Value: '$attributeValue' is not an acceptable DROPDOWN value");
return true;
}
break;
case DECIMAL:
if (!is_numeric($attribute_value) && !empty($attribute_value)) {
log_message('error', "'$attribute_value' is not an acceptable DECIMAL value");
if (!is_numeric($attributeValue) && !empty($attributeValue)) {
log_message('error', "'$attributeValue' is not an acceptable DECIMAL value");
return true;
}
break;
case DATE:
if (!valid_date($attribute_value) && !empty($attribute_value)) {
log_message('error', "'$attribute_value' is not an acceptable DATE value. The value must match the set locale.");
if (!valid_date($attributeValue) && !empty($attributeValue)) {
log_message('error', "'$attributeValue' is not an acceptable DATE value. The value must match the set locale.");
return true;
}
break;
@@ -1212,59 +1230,6 @@ class Items extends Secure_Controller
return false;
}
/**
* Saves attribute data found in the CSV import.
*
* @param array $row
* @param array $item_data
* @param array $definitions
* @return bool
*/
private function save_attribute_data(array $row, array $item_data, array $definitions): bool
{
foreach ($definitions as $definition) {
$attribute_name = $definition['definition_name'];
$attribute_value = $row["attribute_$attribute_name"];
// Create attribute value
if (!empty($attribute_value) || $attribute_value === '0') {
if ($definition['definition_type'] === CHECKBOX) {
$checkbox_is_unchecked = (strcasecmp($attribute_value, 'false') === 0 || $attribute_value === '0');
$attribute_value = $checkbox_is_unchecked ? '0' : '1';
$attribute_id = $this->store_attribute_value($attribute_value, $definition, $item_data['item_id']);
} elseif (!empty($attribute_value)) {
$attribute_id = $this->store_attribute_value($attribute_value, $definition, $item_data['item_id']);
} else {
return true;
}
if (!$attribute_id) {
return true;
}
}
}
return false;
}
/**
* Saves the attribute_value and attribute_link if necessary
*/
private function store_attribute_value(string $value, array $attribute_data, int $item_id)
{
$attribute_id = $this->attribute->attributeValueExists($value, $attribute_data['definition_type']);
$this->attribute->deleteAttributeLinks($item_id, $attribute_data['definition_id']);
if (!$attribute_id) {
$attribute_id = $this->attribute->saveAttributeValue($value, $attribute_data['definition_id'], $item_id, false, $attribute_data['definition_type']);
} elseif (!$this->attribute->saveAttributeLink($item_id, $attribute_data['definition_id'], $attribute_id)) {
return false;
}
return $attribute_id;
}
/**
* Saves inventory quantities for the row in the appropriate stock locations.
*
@@ -1358,10 +1323,11 @@ class Items extends Secure_Controller
* Saves item attributes for a given item.
*
* @param int $itemId The item for which attributes need to be saved to.
* @return void
* @return bool Returns true when item attributes are successfully saved and false on error.
*/
public function saveItemAttributes(int $itemId): void
public function saveItemAttributes(int $itemId): bool
{
$success = true;
$attributeLinks = $this->request->getPost('attribute_links') ?? [];
$attributeIds = $this->request->getPost('attribute_ids');
@@ -1373,16 +1339,18 @@ class Items extends Secure_Controller
switch ($definitionType) {
case DROPDOWN:
$attributeId = $attributeValue;
$success = $success && $this->attribute->saveAttributeLink($itemId, $definitionId, $attributeId);
break;
case DECIMAL:
$attributeValue = parse_decimals($attributeValue);
// Fall through to save the attribute value
// no break
default:
$attributeId = $this->attribute->saveAttributeValue($attributeValue, $definitionId, $itemId, $attributeIds[$definitionId], $definitionType);
$success = $success && ($attributeId > 0);
break;
}
$this->attribute->saveAttributeLink($itemId, $definitionId, $attributeId);
}
return $success && $this->attribute->deleteOrphanedValues();
}
}

View File

@@ -190,11 +190,11 @@ class Receivings extends Secure_Controller
/**
* Edit line item in current receiving. Used in app/Views/receivings/receiving.php
*
* @param string|int|null $item_id
* @param int|string|null $item_id
* @return string
* @noinspection PhpUnused
*/
public function postEditItem($item_id): string
public function postEditItem(int|string|null $item_id): string
{
$data = [];
@@ -242,7 +242,7 @@ class Receivings extends Secure_Controller
}
$receiving_info = $this->receiving->get_info($receiving_id)->getRowArray();
$current_employee_id = $this->employee->get_logged_in_employee_info()->person_id;
$can_assign_employee = $this->employee->has_grant('employees', $current_employee_id);
@@ -280,8 +280,10 @@ class Receivings extends Secure_Controller
}
/**
* @throws ReflectionException
* @param int $receiving_id
* @param bool $update_inventory
* @return ResponseInterface
* @throws ReflectionException
*/
public function postDelete(int $receiving_id = -1, bool $update_inventory = true): ResponseInterface
{

View File

@@ -425,7 +425,7 @@ class Sales extends Secure_Controller
$new_giftcard_value = $giftcard->get_giftcard_value($giftcard_num) - $this->sale_lib->get_amount_due();
$new_giftcard_value = max($new_giftcard_value, 0);
$this->sale_lib->set_giftcard_remainder($new_giftcard_value);
$new_giftcard_value = str_replace('$', '\$', to_currency($new_giftcard_value));
$new_giftcard_value = to_currency($new_giftcard_value);
$data['warning'] = lang('Giftcards.remaining_balance', [$giftcard_num, $new_giftcard_value]);
$amount_tendered = min($this->sale_lib->get_amount_due(), $giftcard->get_giftcard_value($giftcard_num));
@@ -796,7 +796,7 @@ class Sales extends Secure_Controller
if ($sale_id == NEW_ENTRY && $this->sale->check_invoice_number_exists($invoice_number)) {
$data['error'] = lang('Sales.invoice_number_duplicate', [$invoice_number]);
$this->_reload($data);
return $this->_reload($data);
} else {
$data['invoice_number'] = $invoice_number;
$data['sale_status'] = COMPLETED;
@@ -817,6 +817,7 @@ class Sales extends Secure_Controller
if ($data['sale_id_num'] == NEW_ENTRY) {
$data['error_message'] = lang('Sales.transaction_failed');
return $this->_reload($data);
} else {
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
$this->sale_lib->clear_all();
@@ -840,7 +841,7 @@ class Sales extends Secure_Controller
if ($sale_id == NEW_ENTRY && $this->sale->check_work_order_number_exists($work_order_number)) {
$data['error'] = lang('Sales.work_order_number_duplicate');
$this->_reload($data);
return $this->_reload($data);
} else {
$data['work_order_number'] = $work_order_number;
$data['sale_status'] = SUSPENDED;
@@ -868,7 +869,7 @@ class Sales extends Secure_Controller
if ($sale_id == NEW_ENTRY && $this->sale->check_quote_number_exists($quote_number)) {
$data['error'] = lang('Sales.quote_number_duplicate');
$this->_reload($data);
return $this->_reload($data);
} else {
$data['quote_number'] = $quote_number;
$data['sale_status'] = SUSPENDED;
@@ -900,6 +901,7 @@ class Sales extends Secure_Controller
if ($data['sale_id_num'] == NEW_ENTRY) {
$data['error_message'] = lang('Sales.transaction_failed');
return $this->_reload($data);
} else {
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
$this->sale_lib->clear_all();
@@ -1693,10 +1695,11 @@ class Sales extends Secure_Controller
$this->item->update_item_number($item_id, $item_number);
$cart = $this->sale_lib->get_cart();
$x = $this->search_cart_for_item_id($item_id, $cart);
if ($x != null) {
if ($x !== null) {
$cart[$x]['item_number'] = $item_number;
}
$this->sale_lib->set_cart($cart);
return $this->response->setJSON(['success' => true]);
}
/**
@@ -1715,11 +1718,12 @@ class Sales extends Secure_Controller
$cart = $this->sale_lib->get_cart();
$x = $this->search_cart_for_item_id($item_id, $cart);
if ($x != null) {
if ($x !== null) {
$cart[$x]['name'] = $name;
}
$this->sale_lib->set_cart($cart);
return $this->response->setJSON(['success' => true]);
}
/**
@@ -1738,11 +1742,12 @@ class Sales extends Secure_Controller
$cart = $this->sale_lib->get_cart();
$x = $this->search_cart_for_item_id($item_id, $cart);
if ($x != null) {
if ($x !== null) {
$cart[$x]['description'] = $description;
}
$this->sale_lib->set_cart($cart);
return $this->response->setJSON(['success' => true]);
}
/**

View File

@@ -20,7 +20,7 @@ class Migration_database_optimizations extends Migration
$attribute = model(Attribute::class);
$attribute->delete_orphaned_values();
$attribute->deleteOrphanedValues();
$this->migrate_duplicate_attribute_values(DECIMAL);
$this->migrate_duplicate_attribute_values(DATE);

View File

@@ -1,49 +0,0 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use Config\Database;
class MigrationEXIFStrippingOptions extends Migration
{
/**
* Perform a migration step.
*/
public function up(): void
{
log_message('info', 'Migrating EXIF Stripping Options');
$db = Database::connect();
$configs = [
[
'key' => 'exif_fields_to_keep',
'value' => 'Copyright,Orientation,Software'
]
];
foreach ($configs as $config) {
$existing = $db->table('app_config')
->where('key', $config['key'])
->get()
->getRow();
if ($existing === null) {
$db->table('app_config')->insert($config);
}
}
}
/**
* Revert a migration step.
*/
public function down(): void
{
$db = Database::connect();
$db->table('app_config')
->where('key', 'exif_fields_to_keep')
->delete();
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
use Config\Database;
class TestDatabaseBootstrapSeeder extends Seeder
{
public function run(): void
{
if (ENVIRONMENT !== 'testing') {
throw new \RuntimeException('TestDatabaseBootstrapSeeder can only run in the testing environment.');
}
$config = config('Database');
$group = $config->tests;
$dbName = $group['database'];
if ($dbName === '' || !str_contains(strtolower($dbName), 'test')) {
throw new \RuntimeException("Refusing to reset non-test database: {$dbName}");
}
$serverConn = Database::connect([
'hostname' => $group['hostname'],
'username' => $group['username'],
'password' => $group['password'],
'DBDriver' => $group['DBDriver'],
'database' => null,
'charset' => $group['charset'] ?? 'utf8mb4',
'DBCollat' => $group['DBCollat'] ?? 'utf8mb4_general_ci',
], false);
$serverConn->query("DROP DATABASE IF EXISTS `{$dbName}`");
$serverConn->query("CREATE DATABASE IF NOT EXISTS `{$dbName}`");
}
}

View File

@@ -36,21 +36,26 @@ class Db_log
private function generate_message(): string
{
$db = Database::connect();
$last_query = $db->getLastQuery();
$affected_rows = $db->affectedRows();
$execution_time = $this->convert_time($last_query->getDuration());
$lastQuery = $db->getLastQuery();
if ($lastQuery === null) {
return '';
}
$affectedRows = $db->affectedRows();
$executionTime = $this->convert_time($lastQuery->getDuration());
$message = '*** Query: ' . date('Y-m-d H:i:s T') . ' *******************'
. "\n" . $last_query->getQuery()
. "\n Affected rows: $affected_rows"
. "\n Execution Time: " . $execution_time['time'] . ' ' . $execution_time['unit'];
. "\n" . $lastQuery->getQuery()
. "\n Affected rows: $affectedRows"
. "\n Execution Time: " . $executionTime['time'] . ' ' . $executionTime['unit'];
$long_query = ($execution_time['unit'] === 's') && ($execution_time['time'] > 0.5);
if ($long_query) {
$longQuery = ($executionTime['unit'] === 's') && ($executionTime['time'] > 0.5);
if ($longQuery) {
$message .= ' [LONG RUNNING QUERY]';
}
return $this->config->db_log_only_long && !$long_query ? '' : $message;
return $this->config->db_log_only_long && !$longQuery ? '' : $message;
}
/**

View File

@@ -0,0 +1,35 @@
<?php
/**
* Translates the attribute type to the corresponding database column name.
*
* Maps attribute type constants to their corresponding attribute_values table columns.
* Defaults to 'attribute_value' for TEXT, DROPDOWN and CHECKBOX attribute types.
*
* @param string $input The attribute type constant (DATE, DECIMAL, etc.)
* @return string The database column name for storing this attribute type
*/
function getAttributeDataType(string $input): string
{
$columnMap = [
DATE => 'attribute_date',
DECIMAL => 'attribute_decimal',
];
return $columnMap[$input] ?? 'attribute_value';
}
/**
* Validates that the provided data type is an allowed attribute value type.
*
* @param string $dataType
* @return void
*/
function validateAttributeValueType(string $dataType): void
{
$attributeValueTypes = ['attribute_value', 'attribute_decimal', 'attribute_date'];
if (!in_array($dataType, $attributeValueTypes, true)) {
throw new InvalidArgumentException('Invalid data type');
}
}

View File

@@ -1,10 +1,10 @@
<?php
/**
* @param array $stock_locations
* @param array $attributes
* @return string
*/
function generate_import_items_csv(array $stock_locations, array $attributes): string
{
$csv_headers = pack('CCC', 0xef, 0xbb, 0xbf); // Encode the Byte-Order Mark (BOM) so that UTF-8 File headers display properly in Microsoft Excel

View File

@@ -94,4 +94,3 @@ function remove_backup(): void
@unlink($backup_path);
log_message('info', "Removed $backup_path");
}

View File

@@ -5,6 +5,7 @@ use App\Models\Employee;
use App\Models\Item_taxes;
use App\Models\Tax_category;
use CodeIgniter\Database\ResultInterface;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\Session\Session;
use Config\OSPOS;
use Config\Services;
@@ -577,8 +578,8 @@ function item_kit_headers(): array
['item_kit_number' => lang('Item_kits.item_kit_number')],
['name' => lang('Item_kits.name')],
['description' => lang('Item_kits.description')],
['total_cost_price' => lang('Items.cost_price'), 'sortable' => FALSE],
['total_unit_price' => lang('Items.unit_price'), 'sortable' => FALSE]
['total_cost_price' => lang('Items.cost_price'), 'sortable' => false],
['total_unit_price' => lang('Items.unit_price'), 'sortable' => false]
];
}
@@ -654,7 +655,7 @@ function expand_attribute_values(array $definition_names, array $row): array
foreach ($definition_names as $definition_id => $definitionInfo) {
if (isset($indexed_values[$definition_id])) {
$raw_value = $indexed_values[$definition_id];
// Format DECIMAL attributes according to locale
if (is_array($definitionInfo) && isset($definitionInfo['type']) && $definitionInfo['type'] === DECIMAL) {
$attribute_values["$definition_id"] = to_decimals($raw_value);
@@ -742,7 +743,7 @@ function get_expense_category_manage_table_headers(): string
}
/**
* Gets the html data row for the expenses category
* Gets the html data row for the expense category
*/
function get_expense_category_data_row(object $expense_category): array
{
@@ -841,7 +842,7 @@ function get_expenses_data_last_row(object $expense): array
}
/**
* Get the expenses payments summary
* Get the expense payments summary
*/
function get_expenses_manage_payments_summary(array $payments, ResultInterface $expenses): string // TODO: $expenses is passed but never used.
{
@@ -933,22 +934,22 @@ function get_controller(): string
}
/**
* Restores filter values from URL query string.
*
* @param CodeIgniter\HTTP\IncomingRequest $request The request object
* Restores filter values from the URL query string.
*
* @param IncomingRequest $request The request object
* @return array Array with 'start_date', 'end_date', and 'selected_filters' keys
*/
function restoreTableFilters($request): array
function restoreTableFilters(IncomingRequest $request): array
{
$startDate = $request->getGet('start_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$endDate = $request->getGet('end_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$urlFilters = $request->getGet('filters', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
return array_filter([
'start_date' => $startDate ?: null,
'end_date' => $endDate ?: null,
'selected_filters' => $urlFilters ?? []
], function($value) {
], function ($value) {
return $value !== null && $value !== [];
});
}

View File

@@ -7,7 +7,7 @@ if (!function_exists('base64url_encode')) {
* @param string $data
* @return string
*/
function base64url_encode($data)
function base64url_encode(string $data): string
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
@@ -20,7 +20,7 @@ if (!function_exists('base64url_decode')) {
* @param string $data
* @return string|false
*/
function base64url_decode($data)
function base64url_decode(string $data): false|string
{
$remainder = strlen($data) % 4;
if ($remainder) {
@@ -28,4 +28,4 @@ if (!function_exists('base64url_decode')) {
}
return base64_decode(strtr($data, '-_', '+/'));
}
}
}

View File

@@ -329,6 +329,4 @@ return [
"wholesale_markup" => "",
"work_order_enable" => "Work Order Support",
"work_order_format" => "Work Order Format",
"exif_fields_to_keep" => "EXIF Fields to Keep",
"exif_fields_to_keep_tooltip" => "Select EXIF fields to preserve in uploaded images. Fields not selected will be removed. Leave empty to disable EXIF stripping. Keeps beneficial metadata while removing privacy-sensitive data like GPS location.",
];

View File

@@ -1,220 +0,0 @@
<?php
namespace App\Libraries;
use lsolesen\pel\PelIfd;
use lsolesen\pel\PelJpeg;
use lsolesen\pel\PelTag;
class Image_lib
{
private array $exif_to_pel_tags = [
'Make' => PelTag::MAKE,
'Model' => PelTag::MODEL,
'Orientation' => PelTag::ORIENTATION,
'Copyright' => PelTag::COPYRIGHT,
'Software' => PelTag::SOFTWARE,
'DateTime' => PelTag::DATE_TIME,
];
public function stripEXIF(string $filepath, array $fields_to_keep = []): bool
{
if (!file_exists($filepath)) {
return false;
}
$mimetype = mime_content_type($filepath);
$allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($mimetype, $allowed_types)) {
return false;
}
if ($mimetype === 'image/jpeg' || $mimetype === 'image/jpg') {
return $this->stripExifJpeg($filepath, $fields_to_keep);
}
if ($mimetype === 'image/png') {
return $this->stripExifPng($filepath);
}
if ($mimetype === 'image/gif') {
return $this->stripExifGif($filepath);
}
if ($mimetype === 'image/webp') {
return $this->stripExifWebp($filepath);
}
return false;
}
private function stripExifJpeg(string $filepath, array $fields_to_keep = []): bool
{
try {
$data = file_get_contents($filepath);
if ($data === false) {
return false;
}
$jpeg = new PelJpeg($data);
$exif = $jpeg->getExif();
if ($exif === null) {
return true;
}
$tiff = $exif->getTiff();
if ($tiff === null) {
return true;
}
$ifd0 = $tiff->getIfd();
if ($ifd0 !== null) {
$this->removeExifFields($ifd0, $fields_to_keep);
$subIfd = $ifd0->getSubIfd(PelTag::EXIF_IFD_POINTER);
if ($subIfd !== null) {
$this->removeExifFields($subIfd, $fields_to_keep);
}
if (!in_array('GPS', $fields_to_keep)) {
$ifd0->removeEntry(PelTag::GPS_INFO_IFD_POINTER);
}
}
$jpeg->saveFile($filepath);
return true;
} catch (\Exception $e) {
return $this->stripExifFallback($filepath);
}
}
private function removeExifFields(PelIfd $ifd, array $fields_to_keep): void
{
$tags_to_remove = array_diff(array_keys($this->exif_to_pel_tags), $fields_to_keep);
foreach ($tags_to_remove as $field_name) {
$pel_tag = $this->exif_to_pel_tags[$field_name];
$entry = $ifd->getEntry($pel_tag);
if ($entry !== null) {
$ifd->removeEntry($pel_tag);
}
}
}
private function stripExifPng(string $filepath): bool
{
$image = @imagecreatefrompng($filepath);
if ($image === false) {
return false;
}
imagealphablending($image, false);
imagesavealpha($image, true);
$result = imagepng($image, $filepath, 9);
imagedestroy($image);
return $result;
}
private function stripExifGif(string $filepath): bool
{
$content = file_get_contents($filepath);
if ($content === false) {
return false;
}
if (strpos($content, "\x21\xF9\x04") !== false || strpos($content, 'NETSCAPE2.0') !== false) {
return true;
}
$image = @imagecreatefromgif($filepath);
if ($image === false) {
return false;
}
$result = imagegif($image, $filepath);
imagedestroy($image);
return $result;
}
private function stripExifWebp(string $filepath): bool
{
if (!function_exists('imagecreatefromwebp')) {
return false;
}
$image = @imagecreatefromwebp($filepath);
if ($image === false) {
return false;
}
$result = imagewebp($image, $filepath, 100);
imagedestroy($image);
return $result;
}
private function stripExifFallback(string $filepath): bool
{
$content = file_get_contents($filepath);
if ($content === false) {
return false;
}
$markers = [];
$offset = 0;
while ($offset < strlen($content)) {
if ($offset + 4 > strlen($content)) {
break;
}
$marker = ord($content[$offset + 1]);
if (ord($content[$offset]) !== 0xFF) {
break;
}
if ($marker >= 0xE0 && $marker <= 0xEF) {
$marker_len = ord($content[$offset + 2]) * 256 + ord($content[$offset + 3]);
$markers[] = [$offset, $marker_len + 2];
$offset += $marker_len + 2;
} elseif ($marker === 0xD8 || $marker === 0xD9) {
$offset += 2;
} elseif ($marker === 0x00 || $marker === 0xD0 || $marker === 0xD1 || $marker === 0xD2 || $marker === 0xD3 || $marker === 0xD4 || $marker === 0xD5 || $marker === 0xD6 || $marker === 0xD7) {
$offset += 2;
} elseif ($marker === 0x01) {
$offset += 2;
} else {
if ($offset + 4 > strlen($content)) {
break;
}
$marker_len = ord($content[$offset + 2]) * 256 + ord($content[$offset + 3]);
$offset += $marker_len + 2;
}
}
if (empty($markers)) {
return true;
}
$marker_names = [];
foreach ($markers as $marker_info) {
$marker_byte = ord($content[$marker_info[0] + 1]);
$marker_names[] = 'APP' . ($marker_byte - 0xE0);
}
log_message('warning', "stripExifFallback: Removing all APP markers from {$filepath}: " . implode(', ', $marker_names));
$new_content = $content;
foreach (array_reverse($markers) as $marker_info) {
$new_content = substr_replace($new_content, '', $marker_info[0], $marker_info[1]);
}
return file_put_contents($filepath, $new_content) !== false;
}
}

View File

@@ -6,8 +6,7 @@ use CodeIgniter\Language\Language;
class MY_Language extends Language
{
public function getLine(string $line, array $args = [])
public function getLine(string $line, array $args = []): array|string
{
// If no file is given, just parse the line
if (! str_contains($line, '.')) {
@@ -20,7 +19,7 @@ class MY_Language extends Language
$output = $this->getTranslationOutput($this->locale, $file, $parsedLine);
if ($output === NULL && strpos($this->locale, '-')) {
if ($output === null && strpos($this->locale, '-')) {
[$locale] = explode('-', $this->locale, 2);
[$file, $parsedLine] = $this->parseLine($line, $locale);
@@ -29,7 +28,7 @@ class MY_Language extends Language
}
// If still not found, try English
if ($output === NULL || $output === "") {
if ($output === null || $output === "") {
[$file, $parsedLine] = $this->parseLine($line, 'en');
$output = $this->getTranslationOutput('en', $file, $parsedLine);

View File

@@ -394,7 +394,7 @@ class Receiving_lib
/**
* @param $line int|string The item_number or item_id of the item to be removed from the receiving.
*/
public function delete_item($line): void
public function delete_item(int|string $line): void
{
$items = $this->get_cart();
unset($items[$line]);

View File

@@ -9,6 +9,7 @@ use CodeIgniter\Model;
use CodeIgniter\Database\RawSql;
use Config\OSPOS;
use DateTime;
use InvalidArgumentException;
use stdClass;
use ReflectionClass;
@@ -498,7 +499,7 @@ class Attribute extends Model
}
$this->delete_orphaned_links($definition_id);
$this->delete_orphaned_values();
$this->deleteOrphanedValues();
return $success;
}
@@ -517,7 +518,6 @@ class Attribute extends Model
if (!$one_attribute_id) {
$one_attribute_id = $this->saveAttributeValue('1', $definition_id, false, false, CHECKBOX);
$one_attribute_id = $this->saveAttributeValue('1', $definition_id, false, false, CHECKBOX);
}
return [$zero_attribute_id, $one_attribute_id];
@@ -526,43 +526,43 @@ class Attribute extends Model
/**
* Inserts or updates a definition
*/
public function save_definition(array &$definition_data, int $definition_id = NO_DEFINITION_ID): bool
public function saveDefinition(array &$definitionData, int $definitionId = NO_DEFINITION_ID): bool
{
$this->db->transStart();
// Definition doesn't exist
if ($definition_id === NO_DEFINITION_ID || !$this->exists($definition_id)) {
if ($this->exists($definition_id, true)) {
$success = $this->undelete($definition_id);
// Insert definition
if ($definitionId === NO_DEFINITION_ID || !$this->exists($definitionId)) {
if ($this->exists($definitionId, true)) {
$success = $this->undelete($definitionId);
} else {
$builder = $this->db->table('attribute_definitions');
$success = $builder->insert($definition_data);
$definition_data['definition_id'] = $this->db->insertID();
$success = $builder->insert($definitionData);
$definitionData['definition_id'] = $definitionId !== CATEGORY_DEFINITION_ID ? $this->db->insertID() : $definitionId;
}
}
// Definition already exists
// Update definition
else {
$builder = $this->db->table('attribute_definitions');
$builder->select('definition_type');
$builder->where('definition_id', $definition_id);
$builder->where('definition_id', $definitionId);
$builder->where('deleted', ACTIVE);
$query = $builder->get();
$row = $query->getRow();
$from_definition_type = $row->definition_type;
$to_definition_type = $definition_data['definition_type'];
$to_definition_type = $definitionData['definition_type'];
// Update the definition values
$builder->where('definition_id', $definition_id);
// Update definition values
$builder->where('definition_id', $definitionId);
$success = $builder->update($definitionData);
$definitionData['definition_id'] = $definitionId;
$success = $builder->update($definition_data);
$definition_data['definition_id'] = $definition_id;
if ($from_definition_type !== $to_definition_type) {
if (!$this->convert_definition_data($definition_id, $from_definition_type, $to_definition_type)) {
return false;
}
if ($from_definition_type !== $to_definition_type
&& !$this->convert_definition_data($definitionId, $from_definition_type, $to_definition_type)) {
$this->db->transRollback();
return false;
}
}
@@ -575,10 +575,10 @@ class Attribute extends Model
/**
* @param string $definition_name
* @param $definition_type
* @param string|bool $definition_type
* @return array
*/
public function get_definition_by_name(string $definition_name, $definition_type = false): array
public function get_definition_by_name(string $definition_name, string|bool $definition_type = false): array
{
$builder = $this->db->table('attribute_definitions');
$builder->where('definition_name', $definition_name);
@@ -601,24 +601,48 @@ class Attribute extends Model
*/
public function saveAttributeLink(int $itemId, int $definitionId, int $attributeId): bool
{
$normalizedItemId = empty($itemId) ? null : $itemId;
$normalizedAttributeId = empty($attributeId) ? null : $attributeId;
$this->db->transStart();
$definitionType = $this->getAttributeInfo($definitionId)->definition_type ?? '';
$builder = $this->db->table('attribute_links');
if ($this->attributeLinkExists($itemId, $definitionId)) {
$builder->set(['attribute_id' => $attributeId]);
if ($definitionType === DROPDOWN && $normalizedItemId === null) {
$builder->where('item_id', $normalizedItemId);
$builder->where('definition_id', $definitionId);
$builder->where('item_id', $itemId);
$builder->where('attribute_id', $normalizedAttributeId);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
$builder->update();
$dropdownAttributeLinkExists = $builder->countAllResults(false) !== 0;
if (!$dropdownAttributeLinkExists) {
$data = [
'attribute_id' => $normalizedAttributeId,
'item_id' => $normalizedItemId,
'definition_id' => $definitionId
];
$builder->insert($data);
}
} else {
$data = [
'attribute_id' => $attributeId,
'item_id' => $itemId,
'definition_id' => $definitionId
];
$builder->insert($data);
if ($this->attributeLinkExists($normalizedItemId, $definitionId)) {
$builder->set(['attribute_id' => $normalizedAttributeId]);
$builder->where('definition_id', $definitionId);
$builder->where('item_id', $normalizedItemId);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
$builder->update();
} else {
$data = [
'attribute_id' => $normalizedAttributeId,
'item_id' => $normalizedItemId,
'definition_id' => $definitionId
];
$builder->insert($data);
}
}
$this->db->transComplete();
@@ -627,24 +651,28 @@ class Attribute extends Model
}
/**
* @param int $item_id
* @param int|bool $definition_id
* @return bool
* Deletes attribute links for a given item and optionally a given definition. Does not delete links where sale_id
* or receiving_id has a value. If a definitionId is not provided, deletes all attribute links for the item that do
* not have a sale_id or receiving_id value.
*
* @param int $itemId The item ID to delete links for.
* @param int|bool $definitionId The definition ID to delete links for. (optional)
* @return bool true if successful, false otherwise
*/
public function deleteAttributeLinks(int $item_id, int|bool $definition_id = false): bool
public function deleteAttributeLinks(int $itemId, int|bool $definitionId = false): bool
{
$delete_data = ['item_id' => $item_id];
$deleteData = ['item_id' => $itemId];
// Exclude rows where sale_id or receiving_id has a value
$builder = $this->db->table('attribute_links');
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
if (!empty($definition_id)) {
$delete_data += ['definition_id' => $definition_id];
if (!empty($definitionId)) {
$deleteData += ['definition_id' => $definitionId];
}
return $builder->delete($delete_data);
return $builder->delete($deleteData);
}
/**
@@ -703,7 +731,7 @@ class Attribute extends Model
* @param int $definition_id
* @return object|null
*/
public function get_attribute_value(int $item_id, int $definition_id): ?object
public function getAttributeValue(int $item_id, int $definition_id): ?object
{
$builder = $this->db->table('attribute_values');
$builder->join('attribute_links', 'attribute_links.attribute_id = attribute_values.attribute_id');
@@ -720,6 +748,31 @@ class Attribute extends Model
return $this->getEmptyObject('attribute_values');
}
/**
* Gets a single attribute value by attribute ID.
*
* @param int $attributeId The attribute ID to look up
* @param string $dataType The column name to retrieve (attribute_value, attribute_date, or attribute_decimal)
* @return string|float|null The attribute value. Note: MySQL returns values as follows:
* - attribute_value (TEXT): string
* - attribute_date (DATE): string in 'Y-m-d' format
* - attribute_decimal (DECIMAL): string or float depending on CodeIgniter configuration
* Returns null if the attribute_id is not found.
*/
public function getAttributeValueByAttributeId(int $attributeId, string $dataType): string|float|null
{
helper('attribute');
validateAttributeValueType($dataType);
$builder = $this->db->table('attribute_values');
$builder->select($dataType);
$builder->where('attribute_id', $attributeId);
$builder->limit(1);
$row = $builder->get()->getRow();
return $row ? $row->$dataType : null;
}
/**
* Initializes an empty object based on database definitions
* @param string $table_name
@@ -806,67 +859,155 @@ class Attribute extends Model
}
/**
* @param string $attribute_value
* @param int $definition_id
* @param $item_id
* @param $attribute_id
* @param string $definition_type
* @return int
* Saves an attribute value and creates an attribute link between the attribute value and item if necessary.
* If the attribute value already exists, it will simply create a link to the existing attribute value.
* If the attribute value exists but only has capitalization differences, it will update the existing attribute
* value to match the new capitalization.
* @param string $attributeValue The attribute value to be saved.
* @param int $definitionId The ID of the attribute definition this value is associated with.
* @param int|bool $itemId The ID of the item to link this attribute value to. If false, NULL will be inserted into
* the database for that itemId indicating it is a dropdown value and not linked to a specific item.
* @param int|bool $attributeId The ID of the attribute value if it already exists and is being updated. If false,
* a new attribute value will be created.
* @param string $definitionType The type of the attribute definition which will dictate which column the attribute
* value is saved to.
* @return int The attribute ID of the saved attribute value.
*/
public function saveAttributeValue(string $attribute_value, int $definition_id, int|bool $item_id = false, int|bool $attribute_id = false, string $definition_type = DROPDOWN): int
public function saveAttributeValue(string $attributeValue, int $definitionId, int|bool $itemId = false, int|bool $attributeId = false, string $definitionType = DROPDOWN): int
{
$config = config(OSPOS::class)->settings;
helper('attribute');
$dataType = getAttributeDataType($definitionType);
if ($definitionType === DATE) {
$config = config(OSPOS::class)->settings;
$date = DateTime::createFromFormat($config['dateformat'], $attributeValue);
if ($date !== false) {
$attributeValue = $date->format('Y-m-d');
}
}
$this->db->transStart();
switch ($definition_type) {
case DATE:
$data_type = 'date';
$attribute_date_value = DateTime::createFromFormat($config['dateformat'], $attribute_value);
$attribute_value = $attribute_date_value->format('Y-m-d');
break;
case DECIMAL:
$data_type = 'decimal';
break;
default:
$data_type = 'value';
break;
}
$existingAttributeId = $this->attributeValueExists($attributeValue, $definitionType);
// New Attribute
if (empty($attribute_id) || empty($item_id) || $attribute_id == -1) {
$attribute_id = $this->attributeValueExists($attribute_value, $definition_type);
// Update
if ($existingAttributeId) {
$attributeId = $existingAttributeId;
$storedValue = $this->getAttributeValueByAttributeId($attributeId, $dataType);
if (!$attribute_id) {
$builder = $this->db->table('attribute_values');
$builder->set(["attribute_$data_type" => $attribute_value]);
$builder->insert();
$attribute_id = $this->db->insertID();
if ($dataType === 'attribute_value'
&& is_string($storedValue)
&& strcasecmp($storedValue, $attributeValue) === 0
&& $storedValue !== $attributeValue
) {
$this->updateAttributeValue($attributeId, $dataType, $attributeValue);
}
$data = [
'attribute_id' => empty($attribute_id) ? null : $attribute_id,
'item_id' => empty($item_id) ? null : $item_id,
'definition_id' => $definition_id
];
$builder = $this->db->table('attribute_links');
$builder->set($data);
$builder->insert();
}
// Existing Attribute
else {
} else {
// Insert
$builder = $this->db->table('attribute_values');
$builder->set(["attribute_$data_type" => $attribute_value]);
$builder->where('attribute_id', $attribute_id);
$builder->update();
$builder->set([$dataType => $attributeValue]);
$builder->insert();
$attributeId = $this->db->insertID();
}
if (!empty($definitionId)) {
$success = $this->saveAttributeLink($itemId, $definitionId, $attributeId);
if (!$success) {
$this->db->transRollback();
return 0;
}
}
$this->db->transComplete();
return $attribute_id;
return $attributeId;
}
/**
* Saves attribute data found in one row of a CSV import file. Loops through all attribute definitions and checks
* if there is data for that attribute in the row. If there is, it saves the attribute value and link to the item.
*
* @param array $attributeValues Attribute name/value pairs from one row of the CSV import file
* @param array $itemData Contains data for the item being imported/updated from the CSV file.
* @param array $definitions Contains all attribute definitions in the system.
* @return bool Returns true if all attribute data saves correctly and false if there is an error saving any of
* the attribute data.
*/
public function saveCSVRowAttributeData(array $attributeValues, array $itemData, array $definitions): bool
{
helper('attribute');
foreach ($definitions as $definition) {
$attributeName = $definition['definition_name'];
$attributeValue = $attributeValues[$attributeName] ?? null;
if (isset($attributeValue) && strcasecmp($attributeValue, '_DELETE_') === 0) {
if (!$this->deleteAttributeLinks($itemData['item_id'], $definition['definition_id'])) {
return false;
}
continue;
}
// Create attribute value
if (!empty($attributeValue) || $attributeValue === '0') {
if ($definition['definition_type'] === CHECKBOX) {
$checkbox_is_unchecked = (strcasecmp($attributeValue, 'false') === 0 || $attributeValue === '0');
$attributeValue = $checkbox_is_unchecked ? '0' : '1';
$attribute_id = $this->storeCSVAttributeValue($attributeValue, $definition, $itemData['item_id']);
} elseif (!empty($attributeValue) || $attributeValue === '0') {
$attribute_id = $this->storeCSVAttributeValue($attributeValue, $definition, $itemData['item_id']);
} else {
return false;
}
if (!$attribute_id) {
return false;
}
}
}
return true;
}
/**
* Saves the attribute_value and attribute_link in a CSV file if necessary
*
* @param string $value
* @param array $attributeData
* @param int $itemId
* @return bool|int
*/
private function storeCSVAttributeValue(string $value, array $attributeData, int $itemId): bool|int
{
$this->db->transStart();
$attributeId = $this->attributeValueExists($value, $attributeData['definition_type']);
$this->deleteAttributeLinks($itemId, $attributeData['definition_id']);
if (!$attributeId) {
$attributeId = $this->saveAttributeValue($value, $attributeData['definition_id'], $itemId, false, $attributeData['definition_type']);
} else {
helper('attribute');
$dataType = getAttributeDataType($attributeData['definition_type']);
$storedValue = $this->getAttributeValueByAttributeId($attributeId, $dataType);
// Update the attribute value if only the case has changed and only for text values.
if ($dataType === 'attribute_value'
&& is_string($storedValue)
&& strcasecmp($storedValue, $value) === 0
&& $storedValue !== $value) {
$attributeId = $this->saveAttributeValue($value, $attributeData['definition_id'], $itemId, $attributeId, $attributeData['definition_type']);
} elseif (!$this->saveAttributeLink($itemId, $attributeData['definition_id'], $attributeId)) {
return false;
}
}
$this->db->transComplete();
if (!$this->db->transStatus()) {
return false;
}
return $attributeId;
}
/**
@@ -899,15 +1040,14 @@ class Attribute extends Model
return $builder->update(['deleted' => DELETED]);
}
/**
* Deletes attribute links by definition ID
*
* @param int|array $definition_id
*/
/**
* Deletes attribute links by definition ID
*
* @param int|array $definition_id
*/
public function deleteAttributeLinksByDefinitionId(int|array $definition_id): void
{
if(!is_array($definition_id))
{
if (!is_array($definition_id)) {
$definition_id = [$definition_id];
}
@@ -951,7 +1091,7 @@ class Attribute extends Model
*
* @return boolean true is returned if the delete was successful or false if there were any failures
*/
public function delete_orphaned_values(): bool
public function deleteOrphanedValues(): bool
{
$subquery = $this->db->table('attribute_links')
->distinct()
@@ -1039,7 +1179,7 @@ class Attribute extends Model
*
* @param int $definitionId
* @param int $attributeId
* @return \CodeIgniter\Database\BaseBuilder
* @return void
*/
private function deleteAttributeLinksByDefinitionIdAndAttributeId(int $definitionId, int $attributeId): void
{
@@ -1050,4 +1190,41 @@ class Attribute extends Model
$builder->where('attribute_id', $attributeId);
$builder->delete();
}
/**
* Updates the attribute_value, attribute_date, or attribute_decimal column in the attribute_values table based on
* the provided data type for a specific attribute ID.
*
* @param int $attributeId
* @param string $dataType
* @param mixed $attributeValue
* @return void
*/
private function updateAttributeValue(int $attributeId, string $dataType, mixed $attributeValue): void
{
helper('attribute');
validateAttributeValueType($dataType);
// Update the attribute_values table
$builder = $this->db->table('attribute_values');
$builder->set([$dataType => $attributeValue]);
$builder->where('attribute_id', $attributeId);
$builder->update();
// Check if this attribute_id is linked to definition_id = -1 (category dropdown) using COUNT
$linkBuilder = $this->db->table('attribute_links');
$linkBuilder->selectCount('attribute_id', 'cnt');
$linkBuilder->where('attribute_id', $attributeId);
$linkBuilder->where('definition_id', CATEGORY_DEFINITION_ID);
$countRow = $linkBuilder->get()->getRow();
$isCategoryDropdownAttribute = $countRow && $countRow->cnt > 0;
// Update the items.category column to match new capitalization.
if ($isCategoryDropdownAttribute) {
$itemsBuilder = $this->db->table('items');
$itemsBuilder->set(['category' => $attributeValue]);
$itemsBuilder->where('category', $attributeValue);
$itemsBuilder->update();
}
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use CodeIgniter\Database\ResultInterface;
use Config\OSPOS;
use stdClass;
/**
* Customer class
@@ -128,7 +129,7 @@ class Customer extends Person
/**
* Gets stats about a particular customer
*/
public function get_stats(int $customer_id)
public function get_stats(int $customer_id): ?stdClass
{
$db_prefix = $this->db->getPrefix();
$totals_decimals = totals_decimals();

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use CodeIgniter\Database\ResultInterface;
use CodeIgniter\Session\Session;
use stdClass;
/**
* Employee class
@@ -407,7 +408,7 @@ class Employee extends Person
/**
* Gets information about the currently logged in employee.
*/
public function get_logged_in_employee_info()
public function get_logged_in_employee_info(): float|false|array|int|string|stdClass|null
{
if ($this->is_logged_in()) {
return $this->get_info($this->session->get('person_id'));

View File

@@ -352,7 +352,7 @@ class Item extends Model
/**
* Gets information about a particular item by item id or number
*/
public function get_info_by_id_or_number(string $item_id, bool $include_deleted = true)
public function get_info_by_id_or_number(string $item_id, bool $include_deleted = true): stdClass|string
{
$builder = $this->db->table('items');
$builder->groupStart();
@@ -547,9 +547,9 @@ class Item extends Model
public function get_search_suggestion_format(?string $seed = null): string
{
$config = config(OSPOS::class)->settings;
$suggestionsFirstColumn = $this->suggestionColumnIsAllowed($config['suggestions_first_column'])
? $config['suggestions_first_column']
? $config['suggestions_first_column']
: 'name';
$seed .= ',' . $suggestionsFirstColumn;
@@ -573,14 +573,14 @@ class Item extends Model
$config = config(OSPOS::class)->settings;
$label = '';
$label1 = $this->suggestionColumnIsAllowed($config['suggestions_first_column'])
? $config['suggestions_first_column']
$label1 = $this->suggestionColumnIsAllowed($config['suggestions_first_column'])
? $config['suggestions_first_column']
: 'name';
$label2 = $this->suggestionColumnIsAllowed($config['suggestions_second_column'])
? $config['suggestions_second_column']
$label2 = $this->suggestionColumnIsAllowed($config['suggestions_second_column'])
? $config['suggestions_second_column']
: '';
$label3 = $this->suggestionColumnIsAllowed($config['suggestions_third_column'])
? $config['suggestions_third_column']
$label3 = $this->suggestionColumnIsAllowed($config['suggestions_third_column'])
? $config['suggestions_third_column']
: '';
$this->format_result_numbers($result_row);

View File

@@ -185,6 +185,7 @@ class Stock_location extends Model
$builder = $this->db->table('stock_locations');
$builder->insert($location_data_to_save);
$location_id = $this->db->insertID();
$location_data['location_id'] = $location_id;
$this->_insert_new_permission('items', $location_id, $location_name); // TODO: need to refactor out the hungarian notation.
$this->_insert_new_permission('sales', $location_id, $location_name);

View File

@@ -102,12 +102,12 @@
<script type="text/javascript">
// Validation and submit handling
$(document).ready(function() {
var values = [];
var definition_id = <?= esc($definition_id, 'js') ?>;
var is_new = definition_id == 0;
const values = [];
const definition_id = <?= esc($definition_id, 'js') ?>;
const is_new = definition_id == 0;
var disable_definition_types = function() {
var definition_type = $("#definition_type option:selected").text();
const disable_definition_types = function() {
const definition_type = $("#definition_type option:selected").text();
if (definition_type == "DATE" || (definition_type == "GROUP" && !is_new) || definition_type == "DECIMAL") {
$('#definition_type').prop("disabled", true);
@@ -121,7 +121,7 @@
}
disable_definition_types();
var disable_category_dropdown = function() {
const disable_category_dropdown = function() {
if (definition_id == -1) {
$('#definition_name').prop("disabled", true);
$('#definition_type').prop("disabled", true);
@@ -131,11 +131,11 @@
}
disable_category_dropdown();
var show_hide_fields = function(event) {
var is_dropdown = $('#definition_type').val() !== '1';
var is_decimal = $('#definition_type').val() !== '2';
var is_no_group = $('#definition_type').val() !== '0';
var is_category_dropdown = definition_id == -1;
const show_hide_fields = function(event) {
const is_dropdown = $('#definition_type').val() !== '1';
const is_decimal = $('#definition_type').val() !== '2';
const is_no_group = $('#definition_type').val() !== '0';
const is_category_dropdown = definition_id == -1;
$('#definition_value, #definition_list_group').parents('.form-group').toggleClass('hidden', is_dropdown);
$('#definition_unit').parents('.form-group').toggleClass('hidden', is_decimal);
@@ -150,12 +150,12 @@
show_hide_fields();
$('.selectpicker').each(function() {
var $selectpicker = $(this);
const $selectpicker = $(this);
$.fn.selectpicker.call($selectpicker, $selectpicker.data());
});
var remove_attribute_value = function() {
var value = $(this).parents("li").text();
const remove_attribute_value = function() {
const value = $(this).parents("li").text();
if (is_new) {
values.splice($.inArray(value, values), 1);
@@ -168,8 +168,8 @@
$(this).parents("li").remove();
};
var add_attribute_value = function(value) {
var is_event = typeof(value) !== 'string';
const add_attribute_value = function(value) {
const is_event = typeof(value) !== 'string';
if ($("#definition_value").val().match(/(\||_)/g) != null) {
return;
@@ -206,7 +206,7 @@
}
});
var definition_values = <?= json_encode(array_values($definition_values)) ?>;
const definition_values = <?= json_encode(array_values($definition_values)) ?>;
$.each(definition_values, function(index, element) {
add_attribute_value(element);
});

View File

@@ -104,7 +104,7 @@
(function() {
<?= view('partial/datepicker_locale', ['format' => dateformat_bootstrap($config['dateformat'])]) ?>
var enable_delete = function() {
const enable_delete = function() {
$('.remove_attribute_btn').click(function() {
$(this).parents('.form-group').remove();
});
@@ -113,7 +113,7 @@
enable_delete();
$("input[name*='attribute_links']").change(function() {
var definition_id = $(this).data('definition-id');
const definition_id = $(this).data('definition-id');
$("input[name='attribute_ids[" + definition_id + "]']").val('');
}).autocomplete({
source: function(request, response) {
@@ -129,11 +129,11 @@
delay: 10
});
var definition_values = function() {
var result = {};
const definition_values = function() {
const result = {};
$("[name*='attribute_links'").each(function() {
var definition_id = $(this).data('definition-id');
var element = $(this);
const definition_id = $(this).data('definition-id');
const element = $(this);
// For checkboxes, use the visible checkbox, not the hidden input
if (element.attr('type') === 'hidden' && element.siblings('input[type="checkbox"]').length > 0) {
@@ -151,9 +151,9 @@
return result;
};
var refresh = function() {
var definition_id = $("#definition_name option:selected").val();
var attribute_values = definition_values();
const refresh = function() {
const definition_id = $("#definition_name option:selected").val();
let attribute_values = definition_values();
attribute_values[definition_id] = '';
$('#attributes').load('<?= "items/attributes/$item_id" ?>', {
'definition_ids': JSON.stringify(attribute_values)

View File

@@ -11,34 +11,31 @@ $barcode_lib = new Barcode_lib();
<!doctype html>
<html lang="<?= current_language_code() ?>">
<head>
<meta charset="utf-8">
<title><?= lang('Items.generate_barcodes') ?></title>
<link rel="stylesheet" href="<?= base_url() ?>css/barcode_font.css">
<style>
.barcode svg {
height: <?= $barcode_config['barcode_height'] ?>px;
width: <?= $barcode_config['barcode_width'] ?>px;
}
</style>
</head>
<body class=<?= 'font_' . $barcode_lib->get_font_name($barcode_config['barcode_font']) ?> style="font-size: <?= $barcode_config['barcode_font_size'] ?>px;">
<table cellspacing="<?= $barcode_config['barcode_page_cellspacing'] ?>" width="<?= $barcode_config['barcode_page_width'] . '%' ?>">
<tr>
<?php
$count = 0;
foreach ($items as $item) {
if ($count % $barcode_config['barcode_num_in_row'] == 0 && $count != 0) {
echo '</tr><tr>';
}
echo '<td>' . $barcode_lib->display_barcode($item, $barcode_config) . '</td>';
$count++;
<head>
<meta charset="utf-8">
<title><?= lang('Items.generate_barcodes') ?></title>
<link rel="stylesheet" href="<?= base_url() ?>css/barcode_font.css">
<style>
.barcode svg {
height: <?= $barcode_config['barcode_height'] ?>px;
width: <?= $barcode_config['barcode_width'] ?>px;
}
?>
</tr>
</table>
</body>
</style>
</head>
<body class=<?= 'font_' . $barcode_lib->get_font_name($barcode_config['barcode_font']) ?> style="font-size: <?= $barcode_config['barcode_font_size'] ?>px;">
<table style="border-spacing: <?= $barcode_config['barcode_page_cellspacing'] ?>; width: <?= $barcode_config['barcode_page_width'] ?>%;">
<tr>
<?php
$count = 0;
foreach ($items as $item) {
if ($count % $barcode_config['barcode_num_in_row'] == 0 && $count != 0) {
echo '</tr><tr>';
}
echo '<td>' . $barcode_lib->display_barcode($item, $barcode_config) . '</td>';
$count++;
}
?>
</tr>
</table>
</body>
</html>

View File

@@ -308,7 +308,7 @@
);
});
var submit_form = function() {
const submit_form = function() {
$(this).ajaxSubmit({
success: function(response) {
dialog_support.hide();

View File

@@ -47,7 +47,7 @@
<?= view('partial/print_receipt', ['print_after_sale' => false, 'selected_printer' => 'takings_printer']) ?>
<div id="title_bar" class="print_hide btn-toolbar">
<button onclick="javascript:printdoc()" class="btn btn-info btn-sm pull-right">
<button onclick="printdoc()" class="btn btn-info btn-sm pull-right">
<span class="glyphicon glyphicon-print">&nbsp;</span><?= lang('Common.print') ?>
</button>
<button class="btn btn-info btn-sm pull-right modal-dlg" data-btn-submit="<?= lang('Common.submit') ?>" data-href="<?= "$controller_name/view" ?>" title="<?= lang(ucfirst($controller_name) . ".new") ?>">

View File

@@ -139,7 +139,7 @@
<script type="text/javascript">
// Validation and submit handling
$(document).ready(function() {
var check_protocol = function() {
const check_protocol = function() {
if ($('#protocol').val() == 'sendmail') {
$('#mailpath').prop('disabled', false);
$('#smtp_host, #smtp_user, #smtp_pass, #smtp_port, #smtp_timeout, #smtp_crypto').prop('disabled', true);

View File

@@ -3,8 +3,6 @@
* @var array $themes
* @var array $image_allowed_types
* @var array $selected_image_allowed_types
* @var array $exif_fields
* @var array $selected_exif_fields
* @var bool $show_office_group
* @var string $controller_name
* @var array $config
@@ -270,26 +268,6 @@
</div>
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('Config.exif_fields_to_keep'), 'exif_fields_to_keep', ['class' => 'control-label col-xs-2']) ?>
<div class="col-xs-4">
<?= form_multiselect([
'name' => 'exif_fields_to_keep[]',
'options' => $exif_fields,
'selected' => $selected_exif_fields,
'id' => 'exif_fields_to_keep',
'class' => 'selectpicker show-menu-arrow',
'data-none-selected-text' => lang('Common.none_selected_text'),
'data-selected-text-format' => 'count > 1',
'data-style' => 'btn-default btn-sm',
'data-width' => '100%'
]) ?>
<label class="control-label">
<span class="glyphicon glyphicon-info-sign" data-toggle="tooltip" data-placement="right" title="<?= lang('Config.exif_fields_to_keep_tooltip') ?>"></span>
</label>
</div>
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('Config.gcaptcha_enable'), 'gcaptcha_enable', ['class' => 'control-label col-xs-2']) ?>
<div class="col-xs-1">
@@ -489,8 +467,8 @@
<script type="text/javascript">
// Validation and submit handling
$(document).ready(function() {
var enable_disable_gcaptcha_enable = (function() {
var gcaptcha_enable = $("#gcaptcha_enable").is(":checked");
const enable_disable_gcaptcha_enable = (function() {
const gcaptcha_enable = $("#gcaptcha_enable").is(":checked");
if (gcaptcha_enable) {
$("#gcaptcha_site_key, #gcaptcha_secret_key").prop("disabled", !gcaptcha_enable).addClass("required");
$("#config_gcaptcha_site_key, #config_gcaptcha_secret_key").addClass("required");

View File

@@ -198,9 +198,9 @@
<script type="text/javascript">
// Validation and submit handling
$(document).ready(function() {
var enable_disable_invoice_enable = (function() {
var invoice_enabled = $("#invoice_enable").is(":checked");
var work_order_enabled = $("#work_order_enable").is(":checked");
const enable_disable_invoice_enable = (function() {
const invoice_enabled = $("#invoice_enable").is(":checked");
const work_order_enabled = $("#work_order_enable").is(":checked");
$("#sales_invoice_format, #recv_invoice_format, #invoice_default_comments, #invoice_email_message, select[name='invoice_type'], #sales_quote_format, select[name='line_sequence'], #last_used_invoice_number, #last_used_quote_number, #quote_default_comments, #work_order_enable, #work_order_format, #last_used_work_order_number").prop("disabled", !invoice_enabled);
if (invoice_enabled) {
$("#work_order_format, #last_used_work_order_number").prop("disabled", !work_order_enabled);
@@ -210,9 +210,9 @@
return arguments.callee;
})();
var enable_disable_work_order_enable = (function() {
var work_order_enabled = $("#work_order_enable").is(":checked");
var invoice_enabled = $("#invoice_enable").is(":checked");
const enable_disable_work_order_enable = (function() {
const work_order_enabled = $("#work_order_enable").is(":checked");
const invoice_enabled = $("#invoice_enable").is(":checked");
if (invoice_enabled) {
$("#work_order_format, #last_used_work_order_number").prop("disabled", !work_order_enabled);
}

View File

@@ -292,7 +292,7 @@
$('span').tooltip();
$('#currency_symbol, #thousands_separator, #currency_code').change(function() {
var data = {
const data = {
number_locale: $('#number_locale').val()
};
data['save_number_locale'] = $("input[name='save_number_locale']").val();
@@ -336,7 +336,7 @@
}
},
dataFilter: function(data) {
var response = JSON.parse(data);
const response = JSON.parse(data);
$("input[name='save_number_locale']").val(response.save_number_locale);
$('#number_locale_example').text(response.number_locale_example);
$('#currency_symbol').val(response.currency_symbol);

View File

@@ -338,9 +338,9 @@
// Validation and submit handling
$(document).ready(function() {
if (window.localStorage && window.jsPrintSetup) {
var printers = (jsPrintSetup.getPrintersList() && jsPrintSetup.getPrintersList().split(',')) || [];
const printers = (jsPrintSetup.getPrintersList() && jsPrintSetup.getPrintersList().split(',')) || [];
$('#receipt_printer, #invoice_printer, #takings_printer').each(function() {
var $this = $(this)
const $this = $(this)
$(printers).each(function(key, value) {
$this.append($('<option>', {
value: value
@@ -360,7 +360,7 @@
});
}
var dialog_confirmed = window.jsPrintSetup;
const dialog_confirmed = window.jsPrintSetup;
$('#receipt_config_form').validate($.extend(form_support.handler, {
submitHandler: function(form) {

View File

@@ -43,8 +43,8 @@
// Validation and submit handling
$(document).ready(function() {
var enable_disable_customer_reward_enable = (function() {
var customer_reward_enable = $("#customer_reward_enable").is(":checked");
const enable_disable_customer_reward_enable = (function() {
const customer_reward_enable = $("#customer_reward_enable").is(":checked");
$("input[name*='customer_reward']:not(input[name=customer_reward_enable])").prop("disabled", !customer_reward_enable);
$("input[name*='reward_points_']:not(input[name=customer_reward_enable])").prop("disabled", !customer_reward_enable);
if (customer_reward_enable) {
@@ -57,9 +57,9 @@
$("#customer_reward_enable").change(enable_disable_customer_reward_enable);
var table_count = <?= sizeof($customer_rewards) ?>;
let table_count = <?= sizeof($customer_rewards) ?>;
var hide_show_remove = function() {
const hide_show_remove = function() {
if ($("input[name*='customer_rewards']:enabled").length > 1) {
$(".remove_customer_rewards").show();
} else {
@@ -67,27 +67,27 @@
}
};
var add_customer_reward = function() {
var id = $(this).parent().find('input').attr('id');
const add_customer_reward = function() {
let id = $(this).parent().find('input').attr('id');
id = id.replace(/.*?_(\d+)$/g, "$1");
var previous_id = 'customer_reward_' + id;
var previous_id_next = 'reward_points_' + id;
var block = $(this).parent().clone(true);
var new_block = block.insertAfter($(this).parent());
var new_block_id = 'customer_reward_' + ++id;
var new_block_id_next = 'reward_points_' + id;
const previous_id = 'customer_reward_' + id;
const previous_id_next = 'reward_points_' + id;
const block = $(this).parent().clone(true);
const new_block = block.insertAfter($(this).parent());
const new_block_id = 'customer_reward_' + ++id;
const new_block_id_next = 'reward_points_' + id;
$(new_block).find('label').html("<?= lang('Config.customer_reward') ?> " + ++table_count).attr('for', new_block_id).attr('class', 'control-label col-xs-2');
$(new_block).find("input[id='" + previous_id + "']").attr('id', new_block_id).removeAttr('disabled').attr('name', new_block_id).attr('class', 'form-control input-sm').val('');
$(new_block).find("input[id='" + previous_id_next + "']").attr('id', new_block_id_next).removeAttr('disabled').attr('name', new_block_id_next).attr('class', 'form-control input-sm').val('');
hide_show_remove();
};
var remove_customer_reward = function() {
const remove_customer_reward = function() {
$(this).parent().remove();
hide_show_remove();
};
var init_add_remove_tables = function() {
const init_add_remove_tables = function() {
$('.add_customer_reward').click(add_customer_reward);
$('.remove_customer_reward').click(remove_customer_reward);
hide_show_remove();
@@ -96,10 +96,10 @@
};
init_add_remove_tables();
var duplicate_found = false;
const duplicate_found = false;
// Run validator once for all fields
$.validator.addMethod('customer_reward', function(value, element) {
var value_count = 0;
let value_count = 0;
$("input[name*='customer_reward']:not(input[name=customer_reward_enable])").each(function() {
value_count = $(this).val() == value ? value_count + 1 : value_count;
});

View File

@@ -29,9 +29,9 @@
<script type="text/javascript">
// Validation and submit handling
$(document).ready(function() {
var location_count = <?= sizeof($stock_locations) ?>;
let location_count = <?= sizeof($stock_locations) ?>;
var hide_show_remove = function() {
const hide_show_remove = function() {
if ($("input[name*='stock_location']:enabled").length > 1) {
$(".remove_stock_location").show();
} else {
@@ -39,31 +39,31 @@
}
};
var add_stock_location = function() {
var block = $(this).parent().clone(true);
var new_block = block.insertAfter($(this).parent());
var new_block_id = 'stock_location[]';
const add_stock_location = function() {
const block = $(this).parent().clone(true);
const new_block = block.insertAfter($(this).parent());
const new_block_id = 'stock_location[]';
$(new_block).find('label').html("<?= lang('Config.stock_location') ?> " + ++location_count).attr('for', new_block_id).attr('class', 'control-label col-xs-2');
$(new_block).find('input').attr('id', new_block_id).removeAttr('disabled').attr('name', new_block_id).attr('class', 'form-control input-sm').val('');
hide_show_remove();
};
var remove_stock_location = function() {
const remove_stock_location = function() {
$(this).parent().remove();
hide_show_remove();
};
var init_add_remove_locations = function() {
const init_add_remove_locations = function() {
$('.add_stock_location').click(add_stock_location);
$('.remove_stock_location').click(remove_stock_location);
hide_show_remove();
};
init_add_remove_locations();
var duplicate_found = false;
const duplicate_found = false;
// Run validator once for all fields
$.validator.addMethod('stock_location', function(value, element) {
var value_count = 0;
let value_count = 0;
$("input[name*='stock_location']").each(function() {
value_count = $(this).val() == value ? value_count + 1 : value_count;
});

View File

@@ -25,11 +25,9 @@ use Config\OSPOS;
<div class="container">
<div class="row">
<div class="col-sm-2" style="text-align: left;"><br>
<strong>
<p style="min-height: 14.7em;">General Info</p>
<p style="min-height: 10.5em;">User Setup</p><br>
<p>Permissions</p>
</strong>
<p style="min-height: 14.7em; font-weight: bold;">General Info</p>
<p style="min-height: 10.5em; font-weight: bold;">User Setup</p><br>
<p style="font-weight: bold;">Permissions</p>
</div>
<div class="col-sm-8" id="issuetemplate" style="text-align: left;"><br>
<?= lang('Config.ospos_info') . ':' ?>
@@ -38,14 +36,14 @@ use Config\OSPOS;
<div id="TimeError"></div>
Extensions & Modules:<br>
<?php
echo "&#187; GD: ", extension_loaded('gd') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br>';
echo "&#187; BC Math: ", extension_loaded('bcmath') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red">Disabled &#x2717</span>', '<br>';
echo "&#187; INTL: ", extension_loaded('intl') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red">Disabled &#x2717</span>', '<br>';
echo "&#187; OpenSSL: ", extension_loaded('openssl') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red">Disabled &#x2717</span>', '<br>';
echo "&#187; MBString: ", extension_loaded('mbstring') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red">Disabled &#x2717</span>', '<br>';
echo "&#187; Curl: ", extension_loaded('curl') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red">Disabled &#x2717</span>', '<br>';
echo "&#187; Json: ", extension_loaded('json') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red">Disabled &#x2717</span>', '<br><br>';
echo "&#187; Xml: ", extension_loaded('xml') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red">Disabled &#x2717</span>', '<br><br>';
echo "&#187; GD: ", extension_loaded('gd') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br>';
echo "&#187; BC Math: ", extension_loaded('bcmath') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br>';
echo "&#187; INTL: ", extension_loaded('intl') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br>';
echo "&#187; OpenSSL: ", extension_loaded('openssl') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br>';
echo "&#187; MBString: ", extension_loaded('mbstring') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br>';
echo "&#187; Curl: ", extension_loaded('curl') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br>';
echo "&#187; Json: ", extension_loaded('json') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br><br>';
echo "&#187; Xml: ", extension_loaded('xml') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br><br>';
?>
User Configuration:<br>
.Browser:
@@ -200,7 +198,7 @@ use Config\OSPOS;
<div style="text-align: center;">
<a class="copy" data-clipboard-action="copy" data-clipboard-target="#issuetemplate">Copy Info</a> | <a href="https://github.com/opensourcepos/opensourcepos/issues/new" target="_blank"> <?= lang('Config.report_an_issue') ?></a>
<script type="text/javascript">
var clipboard = new ClipboardJS('.copy');
const clipboard = new ClipboardJS('.copy');
clipboard.on('success', function(e) {
document.getSelection().removeAllRanges();

View File

@@ -43,8 +43,8 @@
// Validation and submit handling
$(document).ready(function() {
var enable_disable_dinner_table_enable = (function() {
var dinner_table_enable = $("#dinner_table_enable").is(":checked");
const enable_disable_dinner_table_enable = (function() {
const dinner_table_enable = $("#dinner_table_enable").is(":checked");
$("input[name*='dinner_table']:not(input[name=dinner_table_enable])").prop("disabled", !dinner_table_enable);
if (dinner_table_enable) {
$(".add_dinner_table, .remove_dinner_table").show();
@@ -56,9 +56,9 @@
$("#dinner_table_enable").change(enable_disable_dinner_table_enable);
var table_count = <?= sizeof($dinner_tables) ?>;
let table_count = <?= sizeof($dinner_tables) ?>;
var hide_show_remove = function() {
const hide_show_remove = function() {
if ($("input[name*='dinner_tables']:enabled").length > 1) {
$(".remove_dinner_tables").show();
} else {
@@ -66,23 +66,23 @@
}
};
var add_dinner_table = function() {
var id = $(this).parent().find('input').attr('id');
const add_dinner_table = function() {
let id = $(this).parent().find('input').attr('id');
id = id.replace(/.*?_(\d+)$/g, "$1");
var block = $(this).parent().clone(true);
var new_block = block.insertAfter($(this).parent());
var new_block_id = 'dinner_table_' + ++id;
const block = $(this).parent().clone(true);
const new_block = block.insertAfter($(this).parent());
const new_block_id = 'dinner_table_' + ++id;
$(new_block).find('label').html("<?= lang('Config.dinner_table') ?> " + ++table_count).attr('for', new_block_id).attr('class', 'control-label col-xs-2');
$(new_block).find('input').attr('id', new_block_id).removeAttr('disabled').attr('name', new_block_id).attr('class', 'form-control input-sm').val('');
hide_show_remove();
};
var remove_dinner_table = function() {
const remove_dinner_table = function() {
$(this).parent().remove();
hide_show_remove();
};
var init_add_remove_tables = function() {
const init_add_remove_tables = function() {
$('.add_dinner_table').click(add_dinner_table);
$('.remove_dinner_table').click(remove_dinner_table);
hide_show_remove();
@@ -91,10 +91,10 @@
};
init_add_remove_tables();
var duplicate_found = false;
const duplicate_found = false;
// Run validator once for all fields
$.validator.addMethod('dinner_table', function(value, element) {
var value_count = 0;
let value_count = 0;
$("input[name*='dinner_table']:not(input[name=dinner_table_enable])").each(function() {
value_count = $(this).val() == value ? value_count + 1 : value_count;
});

View File

@@ -143,8 +143,8 @@
<script type="text/javascript">
// Validation and submit handling
$(document).ready(function() {
var enable_disable_use_destination_based_tax = (function() {
var use_destination_based_tax = $("#use_destination_based_tax").is(":checked");
const enable_disable_use_destination_based_tax = (function() {
const use_destination_based_tax = $("#use_destination_based_tax").is(":checked");
$("select[name='default_tax_code']").prop("disabled", !use_destination_based_tax);
$("select[name='default_tax_category']").prop("disabled", !use_destination_based_tax);
$("select[name='default_tax_jurisdiction']").prop("disabled", !use_destination_based_tax);

View File

@@ -453,7 +453,7 @@
}
});
var fill_value = function(event, ui) {
const fill_value = function(event, ui) {
event.preventDefault();
$("input[name='sales_tax_code_id']").val(ui.item.value);
$("input[name='sales_tax_code_name']").val(ui.item.label);

View File

@@ -167,10 +167,10 @@
});
$.validator.addMethod('module', function(value, element) {
var result = $('#permission_list input').is(':checked');
let result = $('#permission_list input').is(':checked');
$('.module').each(function(index, element) {
var parent = $(element).parent();
var checked = $(element).is(':checked');
const parent = $(element);
const checked = $(element).is(':checked');
if ($('ul', parent).length > 0 && result) {
result &= !checked || (checked && $('ul > li > input:checked', parent).length > 0);
}
@@ -179,10 +179,10 @@
}, "<?= lang('Employees.subpermission_required') ?>");
$('ul#permission_list > li > input.module').each(function() {
var $this = $(this);
const $this = $(this);
$('ul > li > input,select', $this.parent()).each(function() {
var $that = $(this);
var updateInputs = function(checked) {
const $that = $(this);
const updateInputs = function(checked) {
$that.prop('disabled', !checked);
!checked && $that.prop('checked', false);
}

View File

@@ -57,7 +57,7 @@ if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) {
$args = implode(', ', array_map(static fn ($value): string => match (true) {
is_object($value) => 'Object(' . $value::class . ')',
is_array($value) => $value !== [] ? '[...]' : '[]',
$value === null => 'null', // Return the lowercased version
$value === null => 'null', // return the lowercased version
default => var_export($value, true),
}, array_values($error['args'] ?? [])));

View File

@@ -3,7 +3,7 @@
--main-text-color: #555;
--dark-text-color: #222;
--light-text-color: #c7c7c7;
--brand-primary-color: #E06E3F;
--brand-primary-color: #DC4814;
--light-bg-color: #ededee;
--dark-bg-color: #404040;
}
@@ -71,7 +71,7 @@ p.lead {
text-align: center;
padding: calc(4px + 0.2083vw);
width: 100%;
margin-top: -2.14rem;
top: 0;
position: fixed;
}
@@ -101,11 +101,9 @@ p.lead {
}
.tabs {
list-style: none;
list-style-position: inside;
margin: 0;
list-style: none inside none;
padding: 0;
margin-bottom: -1px;
margin: 0 0 -1px;
}
.tabs li {
display: inline;

View File

@@ -1,17 +1,17 @@
var tabLinks = new Array();
var contentDivs = new Array();
const tabLinks = new Array();
const contentDivs = new Array();
function init()
{
// Grab the tab links and content divs from the page
var tabListItems = document.getElementById('tabs').childNodes;
const tabListItems = document.getElementById('tabs').childNodes;
console.log(tabListItems);
for (var i = 0; i < tabListItems.length; i ++)
for (let i = 0; i < tabListItems.length; i ++)
{
if (tabListItems[i].nodeName == "LI")
{
var tabLink = getFirstChildWithTagName(tabListItems[i], 'A');
var id = getHash(tabLink.getAttribute('href'));
const tabLink = getFirstChildWithTagName(tabListItems[i], 'A');
const id = getHash(tabLink.getAttribute('href'));
tabLinks[id] = tabLink;
contentDivs[id] = document.getElementById(id);
}
@@ -19,9 +19,9 @@ function init()
// Assign onclick events to the tab links, and
// highlight the first tab
var i = 0;
let i = 0;
for (var id in tabLinks)
for (const id in tabLinks)
{
tabLinks[id].onclick = showTab;
tabLinks[id].onfocus = function () {
@@ -35,26 +35,26 @@ function init()
}
// Hide all content divs except the first
var i = 0;
let j = 0;
for (var id in contentDivs)
for (const id in contentDivs)
{
if (i != 0)
if (j != 0)
{
console.log(contentDivs[id]);
contentDivs[id].className = 'content hide';
}
i ++;
j ++;
}
}
function showTab()
{
var selectedId = getHash(this.getAttribute('href'));
const selectedId = getHash(this.getAttribute('href'));
// Highlight the selected tab, and dim all others.
// Also show the selected content div, and hide all others.
for (var id in contentDivs)
for (const id in contentDivs)
{
if (id == selectedId)
{
@@ -74,7 +74,7 @@ function showTab()
function getFirstChildWithTagName(element, tagName)
{
for (var i = 0; i < element.childNodes.length; i ++)
for (let i = 0; i < element.childNodes.length; i ++)
{
if (element.childNodes[i].nodeName == tagName)
{
@@ -85,28 +85,29 @@ function getFirstChildWithTagName(element, tagName)
function getHash(url)
{
var hashPos = url.lastIndexOf('#');
const hashPos = url.lastIndexOf('#');
return url.substring(hashPos + 1);
}
function toggle(elem)
{
elem = document.getElementById(elem);
let disp;
if (elem.style && elem.style['display'])
{
// Only works with the "style" attr
var disp = elem.style['display'];
disp = elem.style['display'];
}
else if (elem.currentStyle)
{
// For MSIE, naturally
var disp = elem.currentStyle['display'];
disp = elem.currentStyle['display'];
}
else if (window.getComputedStyle)
{
// For most other browsers
var disp = document.defaultView.getComputedStyle(elem, null).getPropertyValue('display');
disp = document.defaultView.getComputedStyle(elem, null).getPropertyValue('display');
}
// Toggle the state of the "display" style

View File

@@ -1,6 +1,5 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><?= lang('Errors.badRequest') ?></title>
@@ -16,7 +15,6 @@
left: 50%;
margin-left: -73px;
}
body {
height: 100%;
background: #fafafa;
@@ -24,7 +22,6 @@
color: #777;
font-weight: 300;
}
h1 {
font-weight: lighter;
letter-spacing: normal;
@@ -33,7 +30,6 @@
margin-bottom: 0;
color: #222;
}
.wrap {
max-width: 1024px;
margin: 5rem auto;
@@ -44,12 +40,10 @@
border-radius: 0.5rem;
position: relative;
}
pre {
white-space: normal;
margin-top: 1.5rem;
}
code {
background: #fafafa;
border: 1px solid #efefef;
@@ -57,11 +51,9 @@
border-radius: 5px;
display: block;
}
p {
margin-top: 1.5rem;
}
.footer {
margin-top: 2rem;
border-top: 1px solid #efefef;
@@ -69,7 +61,6 @@
font-size: 85%;
color: #999;
}
a:active,
a:link,
a:visited {
@@ -77,19 +68,17 @@
}
</style>
</head>
<body>
<div class="wrap">
<h1>400</h1>
<div class="wrap">
<h1>400</h1>
<p>
<?php if (ENVIRONMENT !== 'production') : ?>
<?= nl2br(esc($message)) ?>
<?php else : ?>
<?= lang('Errors.sorryBadRequest') ?>
<?php endif; ?>
</p>
</div>
<p>
<?php if (ENVIRONMENT !== 'production') : ?>
<?= nl2br(esc($message)) ?>
<?php else : ?>
<?= lang('Errors.sorryBadRequest') ?>
<?php endif; ?>
</p>
</div>
</body>
</html>

View File

@@ -1,10 +1,4 @@
<?php
/**
* @var string $message
*/
?>
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">

View File

@@ -14,421 +14,424 @@ $errorId = uniqid('error', true);
?>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<title><?= esc($title) ?></title>
<style>
<?= preg_replace('#[\r\n\t ]+#', ' ', file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.css')) ?>
</style>
<title><?= esc($title) ?></title>
<style>
<?= preg_replace('#[\r\n\t ]+#', ' ', file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.css')) ?>
</style>
<script type="text/javascript">
<?= file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.js') ?>
</script>
</head>
<body onload="init()">
<script type="text/javascript">
<?= file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.js') ?>
</script>
</head>
<body onload="init()">
<!-- Header -->
<div class="header">
<div class="environment">
Displayed at <?= esc(date('H:i:sa')) ?> &mdash;
PHP: <?= esc(PHP_VERSION) ?> &mdash;
CodeIgniter: <?= esc(CodeIgniter::CI_VERSION) ?> --
Environment: <?= ENVIRONMENT ?>
<!-- Header -->
<div class="header">
<div class="environment">
Displayed at <?= esc(date('H:i:s')) ?> &mdash;
PHP: <?= esc(PHP_VERSION) ?> &mdash;
CodeIgniter: <?= esc(CodeIgniter::CI_VERSION) ?> --
Environment: <?= ENVIRONMENT ?>
</div>
<div class="container">
<h1><?= esc($title), esc($exception->getCode() ? ' #' . $exception->getCode() : '') ?></h1>
<p>
<?= nl2br(esc($exception->getMessage())) ?>
<a href="https://www.duckduckgo.com/?q=<?= urlencode($title . ' ' . preg_replace('#\'.*\'|".*"#Us', '', $exception->getMessage())) ?>"
rel="noreferrer" target="_blank">search &rarr;</a>
</p>
</div>
</div>
<!-- Source -->
<div class="container">
<h1><?= esc($title), esc($exception->getCode() ? ' #' . $exception->getCode() : '') ?></h1>
<p>
<?= nl2br(esc($exception->getMessage())) ?>
<a href="https://www.duckduckgo.com/?q=<?= urlencode($title . ' ' . preg_replace('#\'.*\'|".*"#Us', '', $exception->getMessage())) ?>"
rel="noreferrer" target="_blank">search &rarr;</a>
</p>
<p><b><?= esc(clean_path($file)) ?></b> at line <b><?= esc($line) ?></b></p>
<?php if (is_file($file)) : ?>
<div class="source">
<?= static::highlightFile($file, $line, 15); ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- Source -->
<div class="container">
<p><b><?= esc(clean_path($file)) ?></b> at line <b><?= esc($line) ?></b></p>
<?php if (is_file($file)) : ?>
<div class="source">
<?= static::highlightFile($file, $line, 15); ?>
</div>
<?php endif; ?>
</div>
<div class="container">
<?php
$last = $exception;
while ($prevException = $last->getPrevious()) {
$last = $prevException;
?>
<pre>
Caused by:
<?= esc($prevException::class), esc($prevException->getCode() ? ' #' . $prevException->getCode() : '') ?>
<?= nl2br(esc($prevException->getMessage())) ?>
<a href="https://www.duckduckgo.com/?q=<?= urlencode($prevException::class . ' ' . preg_replace('#\'.*\'|".*"#Us', '', $prevException->getMessage())) ?>" rel="noreferrer" target="_blank">search &rarr;</a>
<?= esc(clean_path($prevException->getFile()) . ':' . $prevException->getLine()) ?>
</pre>
<?php } ?>
</div>
<?php if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) : ?>
<div class="container">
<ul class="tabs" id="tabs">
<li><a href="#backtrace">Backtrace</a></li>
<li><a href="#server">Server</a></li>
<li><a href="#request">Request</a></li>
<li><a href="#response">Response</a></li>
<li><a href="#files">Files</a></li>
<li><a href="#memory">Memory</a></li>
</ul>
<div class="tab-content">
<!-- Backtrace -->
<div class="content" id="backtrace">
<ol class="trace">
<?php foreach ($trace as $index => $row) : ?>
<li>
<p>
<!-- Trace info -->
<?php if (isset($row['file']) && is_file($row['file'])) : ?>
<?php
if (isset($row['function']) && in_array($row['function'], ['include', 'include_once', 'require', 'require_once'], true)) {
echo esc($row['function'] . ' ' . clean_path($row['file']));
} else {
echo esc(clean_path($row['file']) . ' : ' . $row['line']);
}
?>
<?php else: ?>
{PHP internal code}
<?php endif; ?>
<!-- Class/Method -->
<?php if (isset($row['class'])) : ?>
&nbsp;&nbsp;&mdash;&nbsp;&nbsp;<?= esc($row['class'] . $row['type'] . $row['function']) ?>
<?php if (! empty($row['args'])) : ?>
<?php $argsId = $errorId . 'args' . $index ?>
( <a href="#" onclick="return toggle('<?= esc($argsId, 'attr') ?>');">arguments</a> )
<div class="args" id="<?= esc($argsId, 'attr') ?>">
<table cellspacing="0">
<?php
$params = null;
// Reflection by name is not available for closure function
if (! str_ends_with($row['function'], '}')) {
$mirror = isset($row['class']) ? new ReflectionMethod($row['class'], $row['function']) : new ReflectionFunction($row['function']);
$params = $mirror->getParameters();
}
foreach ($row['args'] as $key => $value) : ?>
<tr>
<td><code><?= esc(isset($params[$key]) ? '$' . $params[$key]->name : "#{$key}") ?></code></td>
<td><pre><?= esc(print_r($value, true)) ?></pre></td>
</tr>
<?php endforeach ?>
</table>
</div>
<?php else : ?>
()
<?php endif; ?>
<?php endif; ?>
<?php if (! isset($row['class']) && isset($row['function'])) : ?>
&nbsp;&nbsp;&mdash;&nbsp;&nbsp; <?= esc($row['function']) ?>()
<?php endif; ?>
</p>
<!-- Source? -->
<?php if (isset($row['file']) && is_file($row['file']) && isset($row['class'])) : ?>
<div class="source">
<?= static::highlightFile($row['file'], $row['line']) ?>
</div>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ol>
</div>
<!-- Server -->
<div class="content" id="server">
<?php foreach (['_SERVER', '_SESSION'] as $var) : ?>
<?php
if (empty($GLOBALS[$var]) || ! is_array($GLOBALS[$var])) {
continue;
} ?>
<h3>$<?= esc($var) ?></h3>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($GLOBALS[$var] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach ?>
<!-- Constants -->
<?php $constants = get_defined_constants(true); ?>
<?php if (! empty($constants['user'])) : ?>
<h3>Constants</h3>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($constants['user'] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Request -->
<div class="content" id="request">
<?php $request = service('request'); ?>
<table>
<tbody>
<tr>
<td style="width: 10em">Path</td>
<td><?= esc($request->getUri()) ?></td>
</tr>
<tr>
<td>HTTP Method</td>
<td><?= esc($request->getMethod()) ?></td>
</tr>
<tr>
<td>IP Address</td>
<td><?= esc($request->getIPAddress()) ?></td>
</tr>
<tr>
<td style="width: 10em">Is AJAX Request?</td>
<td><?= $request->isAJAX() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>Is CLI Request?</td>
<td><?= $request->isCLI() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>Is Secure Request?</td>
<td><?= $request->isSecure() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>User Agent</td>
<td><?= esc($request->getUserAgent()->getAgentString()) ?></td>
</tr>
</tbody>
</table>
<?php $empty = true; ?>
<?php foreach (['_GET', '_POST', '_COOKIE'] as $var) : ?>
<?php
if (empty($GLOBALS[$var]) || ! is_array($GLOBALS[$var])) {
continue;
} ?>
<?php $empty = false; ?>
<h3>$<?= esc($var) ?></h3>
<table style="width: 100%">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($GLOBALS[$var] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach ?>
<?php if ($empty) : ?>
<div class="alert">
No $_GET, $_POST, or $_COOKIE Information to show.
</div>
<?php endif; ?>
<?php $headers = $request->headers(); ?>
<?php if (! empty($headers)) : ?>
<h3>Headers</h3>
<table>
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($headers as $name => $value) : ?>
<tr>
<td><?= esc($name, 'html') ?></td>
<td>
<?php
if ($value instanceof Header) {
echo esc($value->getValueLine(), 'html');
} else {
foreach ($value as $i => $header) {
echo ' ('. $i+1 . ') ' . esc($header->getValueLine(), 'html');
}
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Response -->
<div class="container">
<?php
$response = service('response');
$response->setStatusCode(http_response_code());
?>
<div class="content" id="response">
<table>
<tr>
<td style="width: 15em">Response Status</td>
<td><?= esc($response->getStatusCode() . ' - ' . $response->getReasonPhrase()) ?></td>
</tr>
</table>
$last = $exception;
<?php $headers = $response->headers(); ?>
<?php if (! empty($headers)) : ?>
<h3>Headers</h3>
while ($prevException = $last->getPrevious()) {
$last = $prevException;
?>
<pre>
Caused by:
<?= esc($prevException::class), esc($prevException->getCode() ? ' #' . $prevException->getCode() : '') ?>
<?= nl2br(esc($prevException->getMessage())) ?>
<a href="https://www.duckduckgo.com/?q=<?= urlencode($prevException::class . ' ' . preg_replace('#\'.*\'|".*"#Us', '', $prevException->getMessage())) ?>"
rel="noreferrer" target="_blank">search &rarr;</a>
<?= esc(clean_path($prevException->getFile()) . ':' . $prevException->getLine()) ?>
</pre>
<?php
}
?>
</div>
<?php if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) : ?>
<div class="container">
<ul class="tabs" id="tabs">
<li><a href="#backtrace">Backtrace</a></li>
<li><a href="#server">Server</a></li>
<li><a href="#request">Request</a></li>
<li><a href="#response">Response</a></li>
<li><a href="#files">Files</a></li>
<li><a href="#memory">Memory</a></li>
</ul>
<div class="tab-content">
<!-- Backtrace -->
<div class="content" id="backtrace">
<ol class="trace">
<?php foreach ($trace as $index => $row) : ?>
<li>
<p>
<!-- Trace info -->
<?php if (isset($row['file']) && is_file($row['file'])) : ?>
<?php
if (isset($row['function']) && in_array($row['function'], ['include', 'include_once', 'require', 'require_once'], true)) {
echo esc($row['function'] . ' ' . clean_path($row['file']));
} else {
echo esc(clean_path($row['file']) . ' : ' . $row['line']);
}
?>
<?php else: ?>
{PHP internal code}
<?php endif; ?>
<!-- Class/Method -->
<?php if (isset($row['class'])) : ?>
&nbsp;&nbsp;&mdash;&nbsp;&nbsp;<?= esc($row['class'] . $row['type'] . $row['function']) ?>
<?php if (! empty($row['args'])) : ?>
<?php $argsId = $errorId . 'args' . $index ?>
( <a href="#" onclick="return toggle('<?= esc($argsId, 'attr') ?>');">arguments</a> )
<div class="args" id="<?= esc($argsId, 'attr') ?>">
<table style="border-spacing: 0;">
<?php
$params = null;
// Reflection by name is not available for closure function
if (! str_ends_with($row['function'], '}')) {
$mirror = isset($row['class']) ? new ReflectionMethod($row['class'], $row['function']) : new ReflectionFunction($row['function']);
$params = $mirror->getParameters();
}
foreach ($row['args'] as $key => $value) : ?>
<tr>
<td><code><?= esc(isset($params[$key]) ? '$' . $params[$key]->name : "#{$key}") ?></code></td>
<td><pre><?= esc(print_r($value, true)) ?></pre></td>
</tr>
<?php endforeach ?>
</table>
</div>
<?php else : ?>
()
<?php endif; ?>
<?php endif; ?>
<?php if (! isset($row['class']) && isset($row['function'])) : ?>
&nbsp;&nbsp;&mdash;&nbsp;&nbsp; <?= esc($row['function']) ?>()
<?php endif; ?>
</p>
<!-- Source? -->
<?php if (isset($row['file']) && is_file($row['file']) && isset($row['class'])) : ?>
<div class="source">
<?= static::highlightFile($row['file'], $row['line']) ?>
</div>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ol>
</div>
<!-- Server -->
<div class="content" id="server">
<?php foreach (['_SERVER', '_SESSION'] as $var) : ?>
<?php
if (empty($GLOBALS[$var]) || ! is_array($GLOBALS[$var])) {
continue;
} ?>
<h3>$<?= esc($var) ?></h3>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($GLOBALS[$var] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach ?>
<!-- Constants -->
<?php $constants = get_defined_constants(true); ?>
<?php if (! empty($constants['user'])) : ?>
<h3>Constants</h3>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($constants['user'] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Request -->
<div class="content" id="request">
<?php $request = service('request'); ?>
<table>
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($headers as $name => $value) : ?>
<tr>
<td><?= esc($name, 'html') ?></td>
<td>
<?php
if ($value instanceof Header) {
echo esc($response->getHeaderLine($name), 'html');
} else {
foreach ($value as $i => $header) {
echo ' ('. $i+1 . ') ' . esc($header->getValueLine(), 'html');
}
}
?>
</td>
<td style="width: 10em">Path</td>
<td><?= esc($request->getUri()) ?></td>
</tr>
<?php endforeach; ?>
<tr>
<td>HTTP Method</td>
<td><?= esc($request->getMethod()) ?></td>
</tr>
<tr>
<td>IP Address</td>
<td><?= esc($request->getIPAddress()) ?></td>
</tr>
<tr>
<td style="width: 10em">Is AJAX Request?</td>
<td><?= $request->isAJAX() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>Is CLI Request?</td>
<td><?= $request->isCLI() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>Is Secure Request?</td>
<td><?= $request->isSecure() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>User Agent</td>
<td><?= esc($request->getUserAgent()->getAgentString()) ?></td>
</tr>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Files -->
<div class="content" id="files">
<?php $files = get_included_files(); ?>
<?php $empty = true; ?>
<?php foreach (['_GET', '_POST', '_COOKIE'] as $var) : ?>
<?php
if (empty($GLOBALS[$var]) || ! is_array($GLOBALS[$var])) {
continue;
} ?>
<ol>
<?php foreach ($files as $file) :?>
<li><?= esc(clean_path($file)) ?></li>
<?php endforeach ?>
</ol>
</div>
<?php $empty = false; ?>
<!-- Memory -->
<div class="content" id="memory">
<h3>$<?= esc($var) ?></h3>
<table>
<tbody>
<table style="width: 100%">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($GLOBALS[$var] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach ?>
<?php if ($empty) : ?>
<div class="alert">
No $_GET, $_POST, or $_COOKIE Information to show.
</div>
<?php endif; ?>
<?php $headers = $request->headers(); ?>
<?php if (! empty($headers)) : ?>
<h3>Headers</h3>
<table>
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($headers as $name => $value) : ?>
<tr>
<td><?= esc($name, 'html') ?></td>
<td>
<?php
if ($value instanceof Header) {
echo esc($value->getValueLine(), 'html');
} else {
foreach ($value as $i => $header) {
echo ' (' . ($i + 1) . ') ' . esc($header->getValueLine(), 'html');
}
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Response -->
<?php
$response = service('response');
$response->setStatusCode(http_response_code());
?>
<div class="content" id="response">
<table>
<tr>
<td>Memory Usage</td>
<td><?= esc(static::describeMemory(memory_get_usage(true))) ?></td>
<td style="width: 15em">Response Status</td>
<td><?= esc($response->getStatusCode() . ' - ' . $response->getReasonPhrase()) ?></td>
</tr>
<tr>
<td style="width: 12em">Peak Memory Usage:</td>
<td><?= esc(static::describeMemory(memory_get_peak_usage(true))) ?></td>
</tr>
<tr>
<td>Memory Limit:</td>
<td><?= esc(ini_get('memory_limit')) ?></td>
</tr>
</tbody>
</table>
</table>
</div>
<?php $headers = $response->headers(); ?>
<?php if (! empty($headers)) : ?>
<h3>Headers</h3>
</div> <!-- /tab-content -->
<table>
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($headers as $name => $value) : ?>
<tr>
<td><?= esc($name, 'html') ?></td>
<td>
<?php
if ($value instanceof Header) {
echo esc($response->getHeaderLine($name), 'html');
} else {
foreach ($value as $i => $header) {
echo ' (' . ($i + 1) . ') ' . esc($header->getValueLine(), 'html');
}
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div> <!-- /container -->
<?php endif; ?>
<?php endif; ?>
</div>
</body>
<!-- Files -->
<div class="content" id="files">
<?php $files = get_included_files(); ?>
<ol>
<?php foreach ($files as $file) :?>
<li><?= esc(clean_path($file)) ?></li>
<?php endforeach ?>
</ol>
</div>
<!-- Memory -->
<div class="content" id="memory">
<table>
<tbody>
<tr>
<td>Memory Usage</td>
<td><?= esc(static::describeMemory(memory_get_usage(true))) ?></td>
</tr>
<tr>
<td style="width: 12em">Peak Memory Usage:</td>
<td><?= esc(static::describeMemory(memory_get_peak_usage(true))) ?></td>
</tr>
<tr>
<td>Memory Limit:</td>
<td><?= esc(ini_get('memory_limit')) ?></td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /tab-content -->
</div> <!-- /container -->
<?php endif; ?>
</body>
</html>

View File

@@ -49,13 +49,13 @@
});
});
</script
<?= view('partial/table_filter_persistence') ?>>
</script>
<?= view('partial/table_filter_persistence') ?>
<?= view('partial/print_receipt', ['print_after_sale' => false, 'selected_printer' => 'takings_printer']) ?>
<div id="title_bar" class="print_hide btn-toolbar">
<button onclick="javascript:printdoc()" class="btn btn-info btn-sm pull-right">
<button onclick="printdoc()" class="btn btn-info btn-sm pull-right">
<span class="glyphicon glyphicon-print">&nbsp;</span><?= lang('Common.print') ?>
</button>
<button class="btn btn-info btn-sm pull-right modal-dlg" data-btn-submit="<?= lang('Common.submit') ?>" data-href="<?= "$controller_name/view" ?>" title="<?= lang(ucfirst($controller_name) . '.new') ?>">

View File

@@ -78,7 +78,7 @@
!$(this).val() && $(this).val('');
});
var fill_value = function(event, ui) {
const fill_value = function(event, ui) {
event.preventDefault();
$(this).val((ui.item ? ui.item.label : ""));
$("input[name='person_id']").val(ui.item.value);

View File

@@ -235,7 +235,7 @@
}
});
var fill_value = function(event, ui) {
const fill_value = function(event, ui) {
event.preventDefault();
$("input[name='kit_item_id']").val(ui.item.value);
$("input[name='item_name']").val(DOMPurify.sanitize(ui.item.label));

View File

@@ -467,7 +467,7 @@
!$(this).val() && $(this).val('');
});
var fill_tax_category_value = function(event, ui) {
const fill_tax_category_value = function(event, ui) {
event.preventDefault();
$("input[name='tax_category_id']").val(ui.item.value);
$("input[name='tax_category']").val(ui.item.label);
@@ -483,7 +483,7 @@
focus: fill_tax_category_value
});
var fill_low_sell_value = function(event, ui) {
const fill_low_sell_value = function(event, ui) {
event.preventDefault();
$("input[name='low_sell_item_id']").val(ui.item.value);
$("input[name='low_sell_item_name']").val(ui.item.label);
@@ -517,7 +517,7 @@
return value.match(/(\||_)/g) == null;
}, "<?= lang('Attributes.attribute_value_invalid_chars') ?>");
var init_validation = function() {
const init_validation = function() {
$('#item_form').validate($.extend({
submitHandler: function(form, event) { // Event is not used as a parameter here
$(form).ajaxSubmit({

View File

@@ -178,10 +178,10 @@
delay: 10
});
var confirm_message = false;
let confirm_message = false;
$('#tax_percent_name_2, #tax_name_2').prop('disabled', true),
$('#tax_percent_name_1, #tax_name_1').blur(function() {
var disabled = !($('#tax_percent_name_1').val() + $('#tax_name_1').val());
const disabled = !($('#tax_percent_name_1').val() + $('#tax_name_1').val());
$('#tax_percent_name_2, #tax_name_2').prop('disabled', disabled);
confirm_message = disabled ? '' : "<?= lang('Items.confirm_bulk_edit_wipe_taxes') ?>";
});

View File

@@ -115,27 +115,27 @@ use App\Models\Inventory;
});
function display_stock(location_id) {
var item_quantities = <?= json_encode(esc($item_quantities, 'raw')) ?>;
const item_quantities = <?= json_encode(esc($item_quantities, 'raw')) ?>;
document.getElementById("quantity").value = parseFloat(item_quantities[location_id]).toFixed(<?= quantity_decimals() ?>);
var inventory_data = <?= json_encode(esc($inventory_array, 'raw')) ?>;
var employee_data = <?= json_encode(esc($employee_name, 'raw')) ?>;
const inventory_data = <?= json_encode(esc($inventory_array, 'raw')) ?>;
const employee_data = <?= json_encode(esc($employee_name, 'raw')) ?>;
var table = document.getElementById("inventory_result");
const table = document.getElementById("inventory_result");
// Remove old query from tbody
var rowCount = table.rows.length;
for (var index = rowCount; index > 0; index--) {
const rowCount = table.rows.length;
for (let index = rowCount; index > 0; index--) {
table.deleteRow(index - 1);
}
// Add new query to tbody
for (var index = 0; index < inventory_data.length; index++) {
var data = inventory_data[index];
for (let index = 0; index < inventory_data.length; index++) {
const data = inventory_data[index];
if (data['trans_location'] == location_id) {
var tr = document.createElement('tr');
const tr = document.createElement('tr');
var td = document.createElement('td');
let td = document.createElement('td');
td.appendChild(document.createTextNode(data['trans_date']));
tr.appendChild(td);

View File

@@ -136,7 +136,7 @@
});
function fill_quantity(val) {
var item_quantities = <?= json_encode(esc($item_quantities, 'raw')) ?>;
const item_quantities = <?= json_encode(esc($item_quantities, 'raw')) ?>;
document.getElementById('quantity').value = parseFloat(item_quantities[val]).toFixed(<?= quantity_decimals() ?>);
}
</script>

View File

@@ -30,7 +30,7 @@ use App\Models\Employee;
// Set the beginning of time as starting date
$('#daterangepicker').data('daterangepicker').setStartDate("<?= date($config['dateformat'], mktime(0, 0, 0, 01, 01, 2010)) ?>");
// Update the hidden inputs with the selected dates before submitting the search data
var start_date = "<?= date('Y-m-d', mktime(0, 0, 0, 01, 01, 2010)) ?>";
start_date = "<?= date('Y-m-d', mktime(0, 0, 0, 01, 01, 2010)) ?>";
// Override dates from server if provided
<?php if (isset($start_date) && $start_date): ?>

View File

@@ -3,7 +3,7 @@ use Config\OSPOS;
$config = config(OSPOS::class)->settings; ?>
var pickerconfig = function(config) {
const pickerconfig = function(config) {
return $.extend({
format: "<?= $this->data["format"] ?? dateformat_bootstrap($config['dateformat']) . ' ' . dateformat_bootstrap($config['timeformat'])?>",
<?php

View File

@@ -6,8 +6,8 @@
<?php if (empty($config['date_or_time_format'])) { ?>
$('#daterangepicker').css("width", "180");
var start_date = "<?= date('Y-m-d') ?>";
var end_date = "<?= date('Y-m-d') ?>";
let start_date = "<?= date('Y-m-d') ?>";
let end_date = "<?= date('Y-m-d') ?>";
$('#daterangepicker').daterangepicker({
"ranges": {
@@ -112,8 +112,8 @@
});
<?php } else { ?>
$('#daterangepicker').css("width", "305");
var start_date = "<?= date('Y-m-d H:i:s', mktime(0, 0, 0, date("m"), date("d"), date("Y"))) ?>";
var end_date = "<?= date('Y-m-d H:i:s', mktime(23, 59, 59, date("m"), date("d"), date("Y"))) ?>";
let start_date = "<?= date('Y-m-d H:i:s', mktime(0, 0, 0, date("m"), date("d"), date("Y"))) ?>";
let end_date = "<?= date('Y-m-d H:i:s', mktime(23, 59, 59, date("m"), date("d"), date("Y"))) ?>";
$('#daterangepicker').daterangepicker({
"ranges": {
"<?= lang('Datepicker.today') ?>": [

View File

@@ -4,21 +4,20 @@ use Config\OSPOS;
?>
</div>
</div>
</div>
<div id="footer">
<div class="jumbotron push-spaces">
<strong>
<?= lang('Common.copyrights', [date('Y')]) ?> ·
<a href="https://opensourcepos.org" target="_blank"><?= lang('Common.website') ?></a> ·
<?= esc(config('App')->application_version) ?> -
<a target="_blank" href="https://github.com/opensourcepos/opensourcepos/commit/<?= esc(config(OSPOS::class)->commit_sha1) ?>">
<?= esc(substr(config(OSPOS::class)->commit_sha1, 0, 6)); ?>
</a>
</strong>.
<div id="footer">
<div class="jumbotron push-spaces">
<strong>
<?= lang('Common.copyrights', [date('Y')]) ?> ·
<a href="https://opensourcepos.org" target="_blank"><?= lang('Common.website') ?></a> ·
<?= esc(config('App')->application_version) ?> -
<a target="_blank" href="https://github.com/opensourcepos/opensourcepos/commit/<?= esc(config(OSPOS::class)->commit_sha1) ?>">
<?= esc(substr(config(OSPOS::class)->commit_sha1, 0, 6)); ?>
</a>
</strong>.
</div>
</div>
</div>
</body>
</body>
</html>

View File

@@ -6,14 +6,14 @@
<script type="text/javascript">
// Live clock
var clock_tick = function clock_tick() {
const clock_tick = function clock_tick() {
setInterval('update_clock();', 1000);
}
// Start the clock immediately
clock_tick();
var update_clock = function update_clock() {
const update_clock = function update_clock() {
document.getElementById('liveclock').innerHTML = moment().format("<?= dateformat_momentjs($config['dateformat'] . ' ' . $config['timeformat']) ?>");
}
@@ -32,11 +32,11 @@
}
});
var csrf_token = function() {
return "<?= csrf_hash() ?>";
const csrf_token = function() {
return "<?= esc(csrf_hash(), 'js') ?>";
};
var csrf_form_base = function() {
const csrf_form_base = function() {
return {
<?= esc(config('Security')->tokenName, 'js') ?>: function() {
return csrf_token()
@@ -44,14 +44,14 @@
}
};
var setup_csrf_token = function() {
const setup_csrf_token = function() {
$('input[name="<?= esc(config('Security')->tokenName, 'js') ?>"]').val(csrf_token());
};
var ajax = $.ajax;
const ajax = $.ajax;
$.ajax = function() {
var args = arguments[0];
let args = arguments[0];
if (args['type'] && args['type'].toLowerCase() == 'post' && csrf_token()) {
if (typeof args['data'] === 'string') {
args['data'] += '&' + $.param(csrf_form_base());
@@ -80,7 +80,7 @@
});
});
var submit = $.fn.submit;
const submit = $.fn.submit;
$.fn.submit = function() {
setup_csrf_token();

View File

@@ -1,7 +1,7 @@
<script type="text/javascript">
(function(lang, $) {
var lines = {
const lines = {
'common_submit': "<?= lang('Common.submit') ?>",
'common_close': "<?= lang('Common.close') ?>"
};

View File

@@ -29,11 +29,11 @@
jsPrintSetup.setOption('footerStrRight', '');
<?php } ?>
var printers = jsPrintSetup.getPrintersList().split(',');
const printers = jsPrintSetup.getPrintersList().split(',');
// Get right printer here..
for (var index in printers) {
var default_ticket_printer = window.localStorage && localStorage['<?= esc($selected_printer, 'js') ?>'];
var selected_printer = printers[index];
for (const index in printers) {
const default_ticket_printer = window.localStorage && localStorage['<?= esc($selected_printer, 'js') ?>'];
const selected_printer = printers[index];
if (selected_printer == default_ticket_printer) {
// Select Epson label printer
jsPrintSetup.setPrinter(selected_printer);

View File

@@ -1,12 +1,12 @@
<?php
/**
* Table Filter Persistence
*
* This partial updates the URL when filters change, allowing users to
*
* This partially updates the URL when filters change, allowing users to
* share/bookmark filtered views and maintain state on back navigation.
*
*
* Filter restoration from URL is handled server-side in the controller.
*
*
* @param array $options Additional filter options
* - 'additional_params': Array of additional parameter names to track (e.g., ['stock_location'])
* - 'filter_select_id': Filter multiselect element ID (default: 'filters')
@@ -18,12 +18,12 @@ $filter_select_id = $options['filter_select_id'] ?? 'filters';
<script type="text/javascript">
$(document).ready(function() {
var additional_params = <?= json_encode($additional_params) ?>;
var filter_select_id = '<?= esc($filter_select_id) ?>';
const additional_params = <?= json_encode($additional_params) ?>;
const filter_select_id = '<?= esc($filter_select_id) ?>';
function update_url() {
var params = new URLSearchParams();
const params = new URLSearchParams();
// Add dates
if (typeof start_date !== 'undefined') {
params.set('start_date', start_date);
@@ -31,20 +31,20 @@ $filter_select_id = $options['filter_select_id'] ?? 'filters';
if (typeof end_date !== 'undefined') {
params.set('end_date', end_date);
}
// Add filters
var filters = $('#' + filter_select_id).val();
const filters = $('#' + filter_select_id).val();
if (filters) {
filters.forEach(function(filter) {
params.append('filters[]', filter);
});
}
// Add additional params
additional_params.forEach(function(param) {
var element = $('#' + param);
const element = $('#' + param);
if (element.length) {
var value = element.val();
const value = element.val();
if (Array.isArray(value) && value.length > 0) {
value.forEach(function(v) {
params.append(param + '[]', v);
@@ -54,31 +54,31 @@ $filter_select_id = $options['filter_select_id'] ?? 'filters';
}
}
});
// Update URL without page reload
var new_url = window.location.pathname;
var params_str = params.toString();
const new_url = window.location.pathname;
const params_str = params.toString();
if (params_str) {
new_url += '?' + params_str;
}
window.history.replaceState({}, '', new_url);
}
// Update URL when filter dropdown changes
$('#' + filter_select_id).on('hidden.bs.select', function(e) {
update_url();
});
// Update URL when stock location changes (if exists)
if ($('#stock_location').length) {
$("#stock_location").change(function() {
update_url();
});
}
// Update URL when daterangepicker changes
$("#daterangepicker").on('apply.daterangepicker', function(ev, picker) {
update_url();
});
});
</script>
</script>

View File

@@ -26,8 +26,8 @@ function safeRemoveItem(key) {
}
// Load saved column visibility from localStorage
var savedVisibility = JSON.parse(safeGetItem('columnVisibility')) || { cost: false, profit: false };
var visibleColumns = savedVisibility;
const savedVisibility = JSON.parse(safeGetItem('columnVisibility')) || { cost: false, profit: false };
let visibleColumns = savedVisibility;
// Function to save column visibility to localStorage
function saveColumnVisibility(visibility) {
@@ -56,13 +56,13 @@ $('#table').bootstrapTable('refreshOptions', {
});
// Initialize visibility settings from localStorage
var summaryVisibility = JSON.parse(safeGetItem('summaryVisibility')) || { cost: false, profit: false };
let summaryVisibility = JSON.parse(safeGetItem('summaryVisibility')) || { cost: false, profit: false };
// Function to apply visibility for cost and profit rows
function applySummaryVisibility() {
var rows = $('#report_summary .summary_row');
var costRow = rows.eq(rows.length - 2); // Second-to-last row
var profitRow = rows.eq(rows.length - 1); // Last row
const rows = $('#report_summary .summary_row');
const costRow = rows.eq(rows.length - 2); // Second-to-last row
const profitRow = rows.eq(rows.length - 1); // Last row
if (summaryVisibility.cost === false) {
costRow.hide(); // Hide the cost row
@@ -90,7 +90,7 @@ $('#toggleCostProfitButton').click(function () {
applySummaryVisibility();
// Initialize dialog (if editable)
var init_dialog = function () {
const init_dialog = function () {
<?php if (isset($editable)): ?>
table_support.submit_handler('<?php echo site_url("reports/get_detailed_{$editable}_row") ?>');
dialog_support.init("a.modal-dlg");

Some files were not shown because too many files have changed in this diff Show More