mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-25 08:44:42 -04:00
Compare commits
12 Commits
feature/tr
...
feature/pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ea3ced674 | ||
|
|
896ed87797 | ||
|
|
eb264ad76d | ||
|
|
10a64e7af9 | ||
|
|
6e99f05d63 | ||
|
|
c430c7afb5 | ||
|
|
519347f4f5 | ||
|
|
62d84411b2 | ||
|
|
6bd4bb545d | ||
|
|
66f7d70749 | ||
|
|
bd8b4fa6c1 | ||
|
|
a9669ddf19 |
@@ -1,56 +1,23 @@
|
||||
# Version control
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Sensitive config (user may mount their own)
|
||||
node_modules
|
||||
tmp
|
||||
app/Config/Email.php
|
||||
|
||||
# Build artifacts
|
||||
node_modules/
|
||||
dist/
|
||||
tmp/
|
||||
*.patch
|
||||
patches/
|
||||
|
||||
# IDE and editor files
|
||||
.idea/
|
||||
.vscode/
|
||||
git-svn-diff.py
|
||||
*.bash
|
||||
.swp
|
||||
*.swp
|
||||
.buildpath
|
||||
.project
|
||||
.settings/
|
||||
|
||||
# Development tools and configs
|
||||
tests/
|
||||
phpunit.xml
|
||||
.php-cs-fixer.*
|
||||
phpstan.neon
|
||||
*.bash
|
||||
git-svn-diff.py
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
!LICENSE
|
||||
branding/
|
||||
|
||||
# Build configs (not needed at runtime)
|
||||
composer.json
|
||||
composer.lock
|
||||
package.json
|
||||
package-lock.json
|
||||
gulpfile.js
|
||||
.env.example
|
||||
.dockerignore
|
||||
|
||||
# Temporary and backup files
|
||||
.settings/*
|
||||
.git
|
||||
dist/
|
||||
node_modules/
|
||||
*.swp
|
||||
*.rej
|
||||
*.orig
|
||||
*~
|
||||
*.~
|
||||
*.log
|
||||
|
||||
# CI
|
||||
.github/
|
||||
.github/workflows/
|
||||
build/
|
||||
app/writable/session/*
|
||||
!app/writable/session/index.html
|
||||
|
||||
29
.env.example
29
.env.example
@@ -4,35 +4,6 @@
|
||||
|
||||
CI_ENVIRONMENT = production
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# SECURITY: ALLOWED HOSTNAMES
|
||||
#--------------------------------------------------------------------
|
||||
# IMPORTANT: Whitelist of allowed hostnames to prevent Host Header
|
||||
# Injection attacks (GHSA-jchf-7hr6-h4f3).
|
||||
#
|
||||
# If not configured, the application will default to 'localhost',
|
||||
# which may break functionality in production.
|
||||
#
|
||||
# Configure this with all domains/subdomains that host your application:
|
||||
# - Primary domain
|
||||
# - WWW subdomain (if used)
|
||||
# - Any alternative domains
|
||||
#
|
||||
# Examples:
|
||||
# Single domain:
|
||||
# app.allowedHostnames.0 = 'example.com'
|
||||
#
|
||||
# Multiple domains:
|
||||
# app.allowedHostnames.0 = 'example.com'
|
||||
# app.allowedHostnames.1 = 'www.example.com'
|
||||
# app.allowedHostnames.2 = 'demo.opensourcepos.org'
|
||||
#
|
||||
# For localhost development:
|
||||
# app.allowedHostnames.0 = 'localhost'
|
||||
#
|
||||
# Note: Do not include the protocol (http/https) or port number.
|
||||
#app.allowedHostnames.0 = ''
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# DATABASE
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
61
.github/workflows/README.md
vendored
61
.github/workflows/README.md
vendored
@@ -1,61 +0,0 @@
|
||||
# GitHub Actions
|
||||
|
||||
This document describes the CI/CD workflows for OSPOS.
|
||||
|
||||
## Build and Release Workflow (`.github/workflows/build-release.yml`)
|
||||
|
||||
### Build Process
|
||||
- Setup PHP 8.2 with required extensions
|
||||
- Setup Node.js 20
|
||||
- Install composer dependencies
|
||||
- Install npm dependencies
|
||||
- Build frontend assets with Gulp
|
||||
|
||||
### Docker Images
|
||||
- Build and push `opensourcepos` Docker image for multiple architectures (linux/amd64, linux/arm64)
|
||||
- On master: tagged with version and `latest`
|
||||
- On other branches: tagged with version only
|
||||
- Pushed to Docker Hub
|
||||
|
||||
### Releases
|
||||
- Create distribution archives (tar.gz, zip)
|
||||
- Create/update GitHub "unstable" release on master branch only
|
||||
|
||||
## Required Secrets
|
||||
|
||||
To use this workflow, you need to add the following secrets to your repository:
|
||||
|
||||
1. **DOCKER_USERNAME** - Docker Hub username for pushing images
|
||||
2. **DOCKER_PASSWORD** - Docker Hub password/token for pushing images
|
||||
|
||||
### How to add secrets
|
||||
|
||||
1. Go to your repository on GitHub
|
||||
2. Click **Settings** → **Secrets and variables** → **Actions**
|
||||
3. Click **New repository secret**
|
||||
4. Add `DOCKER_USERNAME` and `DOCKER_PASSWORD`
|
||||
|
||||
The `GITHUB_TOKEN` is automatically provided by GitHub Actions.
|
||||
|
||||
## Workflow Triggers
|
||||
|
||||
- **Push to master** - Runs build, Docker push (with `latest` tag), and release
|
||||
- **Push to other branches** - Runs build and Docker push (version tag only)
|
||||
- **Push tags** - Runs build and Docker push (version tag only)
|
||||
- **Pull requests** - Runs build only (PHPUnit tests run in parallel via phpunit.yml)
|
||||
|
||||
## Existing Workflows
|
||||
|
||||
This repository also has these workflows:
|
||||
- `.github/workflows/main.yml` - PHP linting with PHP-CS-Fixer
|
||||
- `.github/workflows/phpunit.yml` - PHPUnit tests (runs on all PHP versions 8.1-8.4)
|
||||
- `.github/workflows/php-linter.yml` - PHP linting
|
||||
|
||||
## Testing
|
||||
|
||||
PHPUnit tests are run separately via `.github/workflows/phpunit.yml` on every push and pull request, testing against PHP 8.1, 8.2, 8.3, and 8.4.
|
||||
|
||||
To test the build workflow:
|
||||
1. Add the required secrets
|
||||
2. Push to master or create a PR
|
||||
3. Monitor the Actions tab in GitHub
|
||||
218
.github/workflows/build-release.yml
vendored
218
.github/workflows/build-release.yml
vendored
@@ -1,218 +0,0 @@
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- '*'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
version-tag: ${{ steps.version.outputs.version-tag }}
|
||||
short-sha: ${{ steps.version.outputs.short-sha }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.2'
|
||||
extensions: intl, mbstring, mysqli, gd, bcmath, zip
|
||||
coverage: none
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Get composer cache directory
|
||||
run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache composer dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.COMPOSER_CACHE_FILES_DIR }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-composer-
|
||||
|
||||
- name: Get npm cache directory
|
||||
run: echo "NPM_CACHE_DIR=$(npm config get cache)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache npm dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.NPM_CACHE_DIR }}
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Install composer dependencies
|
||||
run: composer install --no-dev --optimize-autoloader
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install gulp globally
|
||||
run: npm install -g gulp-cli
|
||||
|
||||
- name: Get version info
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(grep "application_version" app/Config/App.php | sed "s/.*= '\(.*\)';/\1/g")
|
||||
BRANCH=$(echo "${GITHUB_REF#refs/heads/}" | sed 's/feature\///')
|
||||
TAG=$(echo "${GITHUB_TAG:-$BRANCH}" | tr '/' '-')
|
||||
SHORT_SHA=$(git rev-parse --short=6 HEAD)
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "version-tag=$VERSION-$BRANCH-$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
echo "short-sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
GITHUB_TAG: ${{ github.ref_name }}
|
||||
|
||||
- name: Create .env file
|
||||
run: |
|
||||
cp .env.example .env
|
||||
sed -i 's/production/development/g' .env
|
||||
|
||||
- name: Update commit hash
|
||||
run: |
|
||||
SHORT_SHA="${{ steps.version.outputs.short-sha }}"
|
||||
sed -i "s/commit_sha1 = 'dev'/commit_sha1 = '$SHORT_SHA'/g" app/Config/OSPOS.php
|
||||
|
||||
- name: Build frontend assets
|
||||
run: npm run build
|
||||
|
||||
- name: Create distribution archives
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gulp compress
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
SHORT_SHA="${{ steps.version.outputs.short-sha }}"
|
||||
mv dist/opensourcepos.tar "dist/opensourcepos.$VERSION.$SHORT_SHA.tar"
|
||||
mv dist/opensourcepos.zip "dist/opensourcepos.$VERSION.$SHORT_SHA.zip"
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist-${{ steps.version.outputs.short-sha }}
|
||||
path: dist/
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload build context for Docker
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-context-${{ steps.version.outputs.short-sha }}
|
||||
path: |
|
||||
.
|
||||
!.git
|
||||
!node_modules
|
||||
retention-days: 1
|
||||
|
||||
docker:
|
||||
name: Build Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- name: Download build context
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-context-${{ needs.build.outputs.short-sha }}
|
||||
path: .
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Determine Docker tags
|
||||
id: tags
|
||||
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
|
||||
else
|
||||
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
target: ospos
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.tags.outputs.tags }}
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist-${{ needs.build.outputs.short-sha }}
|
||||
path: dist/
|
||||
|
||||
- name: Get version info
|
||||
id: version
|
||||
run: |
|
||||
VERSION="${{ needs.build.outputs.version }}"
|
||||
SHORT_SHA=$(git rev-parse --short=6 HEAD)
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "short-sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create/Update unstable release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: unstable
|
||||
name: Unstable OpenSourcePOS
|
||||
body: |
|
||||
This is a build of the latest master which might contain bugs. Use at your own risk.
|
||||
|
||||
Check the releases section for the latest official release.
|
||||
files: |
|
||||
dist/opensourcepos.${{ steps.version.outputs.version }}.${{ steps.version.outputs.short-sha }}.zip
|
||||
prerelease: true
|
||||
draft: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '21 12 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
4
.github/workflows/phpunit.yml
vendored
4
.github/workflows/phpunit.yml
vendored
@@ -69,6 +69,9 @@ jobs:
|
||||
- name: Install npm dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build database.sql
|
||||
run: npm run gulp build-database
|
||||
|
||||
- name: Start MariaDB
|
||||
run: |
|
||||
docker run -d --name mysql \
|
||||
@@ -76,6 +79,7 @@ jobs:
|
||||
-e MYSQL_DATABASE=ospos \
|
||||
-e MYSQL_USER=admin \
|
||||
-e MYSQL_PASSWORD=pointofsale \
|
||||
-v $PWD/app/Database/database.sql:/docker-entrypoint-initdb.d/database.sql \
|
||||
-p 3306:3306 \
|
||||
mariadb:10.5
|
||||
# Wait for MariaDB to be ready
|
||||
|
||||
72
.github/workflows/update-issue-templates.yml
vendored
72
.github/workflows/update-issue-templates.yml
vendored
@@ -1,72 +0,0 @@
|
||||
name: Update Issue Templates
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
jobs:
|
||||
update-templates:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch releases and update templates
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Fetch releases from GitHub API
|
||||
RELEASES=$(gh api repos/${{ github.repository }}/releases --jq '.[].tag_name' | head -n 10)
|
||||
|
||||
# Create temporary file with options
|
||||
OPTIONS_FILE=$(mktemp)
|
||||
echo " - development (unreleased)" >> "$OPTIONS_FILE"
|
||||
while IFS= read -r release; do
|
||||
echo " - opensourcepos $release" >> "$OPTIONS_FILE"
|
||||
done <<< "$RELEASES"
|
||||
|
||||
update_template() {
|
||||
local template="$1"
|
||||
local template_path=".github/ISSUE_TEMPLATE/$template"
|
||||
|
||||
# Find the line numbers for the OpensourcePOS Version dropdown
|
||||
start_line=$(grep -n "label: OpensourcePOS Version" "$template_path" | cut -d: -f1)
|
||||
|
||||
if [ -z "$start_line" ]; then
|
||||
echo "Could not find OpensourcePOS Version in $template"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Find the options section and default line
|
||||
options_start=$((start_line + 3))
|
||||
default_line=$(grep -n "default:" "$template_path" | awk -F: -v opts="$options_start" '$1 > opts {print $1; exit}')
|
||||
|
||||
# Create new template file
|
||||
head -n $((options_start - 1)) "$template_path" > "${template_path}.new"
|
||||
cat "$OPTIONS_FILE" >> "${template_path}.new"
|
||||
tail -n +$default_line "$template_path" >> "${template_path}.new"
|
||||
mv "${template_path}.new" "$template_path"
|
||||
|
||||
echo "Updated $template"
|
||||
}
|
||||
|
||||
update_template "bug report.yml"
|
||||
update_template "feature_request.yml"
|
||||
|
||||
- name: Commit and push changes
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add .github/ISSUE_TEMPLATE/*.yml
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "Update issue templates with latest releases [skip ci]"
|
||||
git push
|
||||
fi
|
||||
54
.travis.yml
Normal file
54
.travis.yml
Normal file
@@ -0,0 +1,54 @@
|
||||
sudo: required
|
||||
|
||||
branches:
|
||||
except:
|
||||
- unstable
|
||||
- weblate
|
||||
services:
|
||||
- docker
|
||||
|
||||
dist: jammy
|
||||
language: node_js
|
||||
node_js:
|
||||
- 20
|
||||
script:
|
||||
- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
- docker run --rm -u $(id -u) -v $(pwd):/app opensourcepos/composer:ci4 composer install
|
||||
- version=$(grep application_version app/Config/App.php | sed "s/.*=\s'\(.*\)';/\1/g")
|
||||
- cp .env.example .env && sed -i 's/production/development/g' .env
|
||||
- sed -i "s/commit_sha1 = 'dev'/commit_sha1 = '$rev'/g" app/Config/OSPOS.php
|
||||
- echo "$version-$branch-$rev"
|
||||
- npm version "$version-$branch-$rev" --force || true
|
||||
- sed -i 's/opensourcepos.tar.gz/opensourcepos.$version.tgz/g' package.json
|
||||
- npm ci && npm install -g gulp && npm run build
|
||||
- docker build . --target ospos -t ospos
|
||||
- docker build . --target ospos_test -t ospos_test
|
||||
- docker run --rm ospos_test /app/vendor/bin/phpunit --testdox
|
||||
- docker build app/Database/ -t "jekkos/opensourcepos:sql-$TAG"
|
||||
env:
|
||||
global:
|
||||
- BRANCH=$(echo ${TRAVIS_BRANCH} | sed s/feature\\///)
|
||||
- TAG=$(echo "${TRAVIS_TAG:-$BRANCH}" | tr '/' '-')
|
||||
- date=`date +%Y%m%d%H%M%S` && branch=${TRAVIS_BRANCH} && rev=`git rev-parse --short=6 HEAD`
|
||||
after_success:
|
||||
- docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" && docker tag "ospos:latest"
|
||||
"jekkos/opensourcepos:$TAG" && docker push "jekkos/opensourcepos:$TAG" && docker push "jekkos/opensourcepos:sql-$TAG"
|
||||
- gulp compress
|
||||
- mv dist/opensourcepos.tar.gz "dist/opensourcepos.$version.$rev.tgz"
|
||||
- mv dist/opensourcepos.zip "dist/opensourcepos.$version.$rev.zip"
|
||||
deploy:
|
||||
- provider: releases
|
||||
edge: true
|
||||
file: dist/opensourcepos.$version.$rev.zip
|
||||
name: "Unstable OpensourcePos"
|
||||
overwrite: true
|
||||
release_notes: "This is a build of the latest master which might contain bugs. Use at your own risk. Check releases section for the latest official release"
|
||||
prerelease: true
|
||||
tag_name: unstable
|
||||
user: jekkos
|
||||
|
||||
api_key:
|
||||
secure: "KOukL8IFf/uL/BjMyCSKjf2vylydjcWqgEx0eMqFCg3nZ4ybMaOwPORRthIfyT72/FvGX/aoxxEn0uR/AEtb+hYQXHmNS+kZdX72JCe8LpGuZ7FJ5X+Eo9mhJcsmS+smd1sC95DySSc/GolKPo+0WtJYONY/xGCLxm+9Ay4HREg="
|
||||
|
||||
on:
|
||||
branch: master
|
||||
40
AGENTS.md
40
AGENTS.md
@@ -1,40 +0,0 @@
|
||||
# Agent Instructions
|
||||
|
||||
This document provides guidance for AI agents working on the Open Source Point of Sale (OSPOS) codebase.
|
||||
|
||||
## Code Style
|
||||
|
||||
- Follow PHP CodeIgniter 4 coding standards
|
||||
- Run PHP-CS-Fixer before committing: `vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.no-header.php`
|
||||
- Write PHP 8.1+ compatible code with proper type declarations
|
||||
- Use PSR-12 naming conventions: `camelCase` for variables and functions, `PascalCase` for classes, `UPPER_CASE` for constants
|
||||
|
||||
## Development
|
||||
|
||||
- Create a new git worktree for each issue, based on the latest state of `origin/master`
|
||||
- Commit fixes to the worktree and push to the remote
|
||||
|
||||
## Testing
|
||||
|
||||
- Run PHPUnit tests: `composer test`
|
||||
- Tests must pass before submitting changes
|
||||
|
||||
## Build
|
||||
|
||||
- Install dependencies: `composer install && npm install`
|
||||
- Build assets: `npm run build` or `gulp`
|
||||
|
||||
## Conventions
|
||||
|
||||
- Controllers go in `app/Controllers/`
|
||||
- Models go in `app/Models/`
|
||||
- Views go in `app/Views/`
|
||||
- Database migrations in `app/Database/Migrations/`
|
||||
- Use CodeIgniter 4 framework patterns and helpers
|
||||
- Sanitize user input; escape output using `esc()` helper
|
||||
|
||||
## Security
|
||||
|
||||
- Never commit secrets, credentials, or `.env` files
|
||||
- Use parameterized queries to prevent SQL injection
|
||||
- Validate and sanitize all user input
|
||||
32
Dockerfile
32
Dockerfile
@@ -1,22 +1,28 @@
|
||||
FROM php:8.2-apache AS ospos
|
||||
LABEL maintainer="jekkos"
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libicu-dev \
|
||||
libgd-dev \
|
||||
&& docker-php-ext-install mysqli bcmath intl gd \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& a2enmod rewrite
|
||||
|
||||
RUN apt update && apt-get install -y libicu-dev libgd-dev
|
||||
RUN a2enmod rewrite
|
||||
RUN docker-php-ext-install mysqli bcmath intl gd
|
||||
RUN echo "date.timezone = \"\${PHP_TIMEZONE}\"" > /usr/local/etc/php/conf.d/timezone.ini
|
||||
|
||||
WORKDIR /app
|
||||
COPY --chown=www-data:www-data . /app
|
||||
RUN chmod 770 /app/writable/uploads /app/writable/logs /app/writable/cache \
|
||||
&& ln -s /app/*[^public] /var/www \
|
||||
&& rm -rf /var/www/html \
|
||||
&& ln -nsf /app/public /var/www/html
|
||||
COPY . /app
|
||||
RUN ln -s /app/*[^public] /var/www && rm -rf /var/www/html && ln -nsf /app/public /var/www/html
|
||||
RUN chmod -R 770 /app/writable/uploads /app/writable/logs /app/writable/cache && chown -R www-data:www-data /app
|
||||
|
||||
FROM ospos AS ospos_test
|
||||
|
||||
COPY --from=composer /usr/bin/composer /usr/bin/composer
|
||||
|
||||
RUN apt-get install -y libzip-dev wget git
|
||||
RUN wget https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh -O /bin/wait-for-it.sh && chmod +x /bin/wait-for-it.sh
|
||||
RUN docker-php-ext-install zip
|
||||
RUN composer install -d/app
|
||||
#RUN sed -i 's/backupGlobals="true"/backupGlobals="false"/g' /app/tests/phpunit.xml
|
||||
WORKDIR /app/tests
|
||||
|
||||
CMD ["/app/vendor/phpunit/phpunit/phpunit", "/app/test/helpers"]
|
||||
|
||||
FROM ospos AS ospos_dev
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM php:8.4-cli
|
||||
RUN apt-get update && apt-get install -y libicu-dev && docker-php-ext-install intl
|
||||
WORKDIR /app
|
||||
43
INSTALL.md
43
INSTALL.md
@@ -6,53 +6,22 @@
|
||||
- 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)>).
|
||||
- For Windows based installations please read [the wiki](https://github.com/opensourcepos/opensourcepos/wiki). There are closed issues about this subject, as this topic has been covered a lot.
|
||||
|
||||
## Security Configuration
|
||||
|
||||
### Allowed Hostnames (Required for Production)
|
||||
|
||||
OpenSourcePOS validates the Host header against a whitelist to prevent Host Header Injection attacks (GHSA-jchf-7hr6-h4f3). **You must configure this for production deployments.**
|
||||
|
||||
Add the following to your `.env` file:
|
||||
|
||||
```
|
||||
app.allowedHostnames.0 = 'yourdomain.com'
|
||||
app.allowedHostnames.1 = 'www.yourdomain.com'
|
||||
```
|
||||
|
||||
**For local development**, use:
|
||||
```
|
||||
app.allowedHostnames.0 = 'localhost'
|
||||
```
|
||||
|
||||
If `allowedHostnames` is not configured:
|
||||
1. A security warning will be logged
|
||||
2. The application will fall back to 'localhost' as the hostname
|
||||
3. This means URLs generated by the application (links, redirects, etc.) will point to 'localhost'
|
||||
|
||||
### HTTPS Behind Proxy
|
||||
|
||||
If your installation is behind a proxy with SSL offloading, set:
|
||||
```
|
||||
FORCE_HTTPS = true
|
||||
```
|
||||
|
||||
## Local install
|
||||
|
||||
First of all, if you're seeing the message `system folder missing` after launching your browser, that most likely means you have cloned the repository and have not built the project. To build the project from a source commit point instead of from an official release check out [Building OSPOS](BUILD.md). Otherwise, continue with the following steps.
|
||||
First of all, if you're seeing the message `system folder missing` after launching your browser, or cannot find `database.sql`, that most likely means you have cloned the repository and have not built the project. To build the project from a source commit point instead of from an official release check out [Building OSPOS](BUILD.md). Otherwise, continue with the following steps.
|
||||
|
||||
1. Download the a [pre-release for a specific branch](https://github.com/opensourcepos/opensourcepos/releases) or the latest stable [from GitHub here](https://github.com/opensourcepos/opensourcepos/releases). A repository clone will not work unless know how to build the project.
|
||||
2. Create/locate a new MySQL database to install Open Source Point of Sale into.
|
||||
3. Unzip and upload Open Source Point of Sale files to the web-server.
|
||||
4. If `.env` does not exist, copy `.env.example` to `.env`.
|
||||
5. Open `.env` and modify credentials to connect to your database if needed.
|
||||
6. The database schema will be automatically created when you first access the application. Migrations run automatically on fresh installs.
|
||||
3. Execute the file `app/Database/database.sql` to create the tables needed.
|
||||
4. Unzip and upload Open Source Point of Sale files to the web-server.
|
||||
5. Open `.env` file and modify credentials to connect to your database if needed. (First copy .env.example to .env and update)
|
||||
7. Go to your install `public` dir via the browser.
|
||||
8. Log in using
|
||||
- Username: admin
|
||||
- Password: pointofsale
|
||||
9. If everything works, then set the `CI_ENVIRONMENT` variable to `production` in the .env file
|
||||
10. Enjoy!
|
||||
11. Oops, an issue? Please make sure you read the FAQ, wiki page, and you checked open and closed issues on GitHub. PHP `display_errors` is disabled by default. Create an` app/Config/.env` file from the `.env.example` to enable it in a development environment.
|
||||
9. Enjoy!
|
||||
10. Oops, an issue? Please make sure you read the FAQ, wiki page, and you checked open and closed issues on GitHub. PHP `display_errors` is disabled by default. Create an` app/Config/.env` file from the `.env.example` to enable it in a development environment.
|
||||
|
||||
## Local install using Docker
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/opensourcepos/opensourcepos/actions/workflows/build-release.yml" target="_blank"><img src="https://github.com/opensourcepos/opensourcepos/actions/workflows/build-release.yml/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://app.travis-ci.com/opensourcepos/opensourcepos" target="_blank"><img src="https://api.travis-ci.com/opensourcepos/opensourcepos.svg?branch=master" alt="Build Status"></a>
|
||||
<a href="https://app.gitter.im/#/room/#opensourcepos_Lobby:gitter.im?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge" target="_blank"><img src="https://badges.gitter.im/jekkos/opensourcepos.svg" alt="Join the chat at https://app.gitter.im"></a>
|
||||
<a href="https://badge.fury.io/gh/opensourcepos%2Fopensourcepos" target="_blank"><img src="https://badge.fury.io/gh/opensourcepos%2Fopensourcepos.svg" alt="Project Version"></a>
|
||||
<a href="https://translate.opensourcepos.org/engage/opensourcepos/?utm_source=widget" target="_blank"><img src="https://translate.opensourcepos.org/widgets/opensourcepos/-/svg-badge.svg" alt="Translation Status"></a>
|
||||
@@ -137,7 +137,7 @@ Any person or company found breaching the license agreement might find a bunch o
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
| <div align="center">DigitalOcean</div> | <div align="center">JetBrains</div> | <div align="center">GitHub</div> |
|
||||
| <div align="center">DigitalOcean</div> | <div align="center">JetBrains</div> | <div align="center">Travis CI</div> |
|
||||
| --- | --- | --- |
|
||||
| <div align="center"><a href="https://www.digitalocean.com?utm_medium=opensource&utm_source=opensourcepos" target="_blank"><img src="https://github.com/user-attachments/assets/fbbf7433-ed35-407d-8946-fd03d236d350" alt="DigitalOcean Logo" height="50"></a></div> | <div align="center"><a href="https://www.jetbrains.com/idea/" target="_blank"><img src="https://github.com/opensourcepos/opensourcepos/assets/12870258/187f9bbe-4484-475c-9b58-5e5d5f931f09" alt="IntelliJ IDEA Logo" height="50"></a></div> | <div align="center"><a href="https://github.com/features/actions" target="_blank"><img src="https://github.githubassets.com/images/modules/site/icons/eyebrow-panel/actions-icon.svg" alt="GitHub Actions Logo" height="50"></a></div> |
|
||||
| Many thanks to [DigitalOcean](https://www.digitalocean.com) for providing the project with hosting credits. | Many thanks to [JetBrains](https://www.jetbrains.com/) for providing a free license of [IntelliJ IDEA](https://www.jetbrains.com/idea/) to kindly support the development of OSPOS. | Many thanks to [GitHub](https://github.com) for providing free continuous integration via GitHub Actions for open-source projects. |
|
||||
| <div align="center"><a href="https://www.digitalocean.com?utm_medium=opensource&utm_source=opensourcepos" target="_blank"><img src="https://github.com/user-attachments/assets/fbbf7433-ed35-407d-8946-fd03d236d350" alt="DigitalOcean Logo" height="50"></a></div> | <div align="center"><a href="https://www.jetbrains.com/idea/" target="_blank"><img src="https://github.com/opensourcepos/opensourcepos/assets/12870258/187f9bbe-4484-475c-9b58-5e5d5f931f09" alt="IntelliJ IDEA Logo" height="50"></a></div> | <div align="center"><a href="https://www.travis-ci.com/" target="_blank"><img src="https://github.com/opensourcepos/opensourcepos/assets/12870258/71cc2b44-83af-4510-a543-6358285f43c6" alt="Travis CI Logo" height="50"></a></div> |
|
||||
| Many thanks to [DigitalOcean](https://www.digitalocean.com) for providing the project with hosting credits. | Many thanks to [JetBrains](https://www.jetbrains.com/) for providing a free license of [IntelliJ IDEA](https://www.jetbrains.com/idea/) to kindly support the development of OSPOS. | Many thanks to [Travis CI](https://www.travis-ci.com/) for providing a free continuous integration service for open source projects. |
|
||||
|
||||
37
SECURITY.md
37
SECURITY.md
@@ -1,9 +1,9 @@
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
|
||||
- [Security Policy](#security-policy)
|
||||
- [Supported Versions](#supported-versions)
|
||||
- [Security Advisories](#security-advisories)
|
||||
- [Reporting a Vulnerability](#reporting-a-vulnerability)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
@@ -12,35 +12,14 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We release patches for security vulnerabilities.
|
||||
We release patches for security vulnerabilities. Which versions are eligible to receive such patches depend on the CVSS v3.0 Rating:
|
||||
|
||||
| Version | Supported |
|
||||
| --------- | ------------------ |
|
||||
| >= 3.4.2 | :white_check_mark: |
|
||||
| < 3.4.2 | :x: |
|
||||
|
||||
## Security Advisories
|
||||
|
||||
The following security vulnerabilities have been published:
|
||||
|
||||
### High Severity
|
||||
|
||||
| CVE | Vulnerability | CVSS | Published | Fixed In | Credit |
|
||||
|-----|--------------|------|-----------|----------|--------|
|
||||
| [CVE-2025-68434](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-wjm4-hfwg-5w5r) | CSRF leading to Admin Creation | 8.8 | 2025-12-17 | 3.4.2 | @Nixon-H, @jekkos |
|
||||
| [CVE-2025-68147](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-xgr7-7pvw-fpmh) | Stored XSS in Return Policy | 8.1 | 2025-12-17 | 3.4.2 | @Nixon-H, @jekkos |
|
||||
| [CVE-2025-66924](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-gv8j-f6gq-g59m) | Stored XSS in Item Kits | 7.2 | 2026-03-04 | 3.4.2 | @hungnqdz, @omkaryepre |
|
||||
|
||||
### Medium Severity
|
||||
|
||||
| CVE | Vulnerability | CVSS | Published | Fixed In | Credit |
|
||||
|-----|--------------|------|-----------|----------|--------|
|
||||
| [CVE-2025-68658](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-32r8-8r9r-9chw) | Stored XSS in Company Name | 4.3 | 2026-01-13 | 3.4.2 | @hungnqdz |
|
||||
|
||||
For a complete list including draft advisories, see our [GitHub Security Advisories page](https://github.com/opensourcepos/opensourcepos/security/advisories).
|
||||
| CVSS v3.0 | Supported Versions |
|
||||
| --------- | -------------------------------------------------- |
|
||||
| 7.3 | 3.3.5 |
|
||||
| 9.8 | 3.3.6 |
|
||||
| 6.8 | 3.4.2 |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report (suspected) security vulnerabilities to **[jeroen@steganos.dev](mailto:jeroen@steganos.dev)**.
|
||||
|
||||
You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days.
|
||||
Please report (suspected) security vulnerabilities to **[jeroen@steganos.dev](mailto:jeroen@steganos.dev)**. You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days.
|
||||
|
||||
@@ -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, the application will fall back to 'localhost'.
|
||||
*
|
||||
* Configure via .env file:
|
||||
* app.allowedHostnames.0 = 'example.com'
|
||||
* app.allowedHostnames.1 = 'www.example.com'
|
||||
*
|
||||
* For local development:
|
||||
* app.allowedHostnames.0 = '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>
|
||||
*/
|
||||
@@ -292,44 +284,8 @@ class App extends BaseConfig
|
||||
{
|
||||
parent::__construct();
|
||||
$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 . '/';
|
||||
$this->baseURL .= '://' . ((isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : 'localhost') . '/';
|
||||
$this->baseURL .= str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return string A validated hostname
|
||||
*/
|
||||
private function getValidHost(): string
|
||||
{
|
||||
$httpHost = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
|
||||
if (empty($this->allowedHostnames)) {
|
||||
log_message('warning',
|
||||
'Security: allowedHostnames is not configured. ' .
|
||||
'Host header injection protection is disabled. ' .
|
||||
'Please set app.allowedHostnames in your .env file. ' .
|
||||
'Received Host: ' . $httpHost
|
||||
);
|
||||
return 'localhost';
|
||||
}
|
||||
|
||||
if (in_array($httpHost, $this->allowedHostnames, true)) {
|
||||
return $httpHost;
|
||||
}
|
||||
|
||||
log_message('warning',
|
||||
'Security: Rejected HTTP_HOST "' . $httpHost . '" - not in allowedHostnames whitelist. ' .
|
||||
'Using fallback: ' . $this->allowedHostnames[0]
|
||||
);
|
||||
|
||||
return $this->allowedHostnames[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +205,7 @@ class Autoload extends AutoloadConfig
|
||||
'cookie',
|
||||
'tabular',
|
||||
'locale',
|
||||
'security'
|
||||
'security',
|
||||
'plugin'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -8,23 +8,7 @@ use CodeIgniter\HotReloader\HotReloader;
|
||||
use App\Events\Db_log;
|
||||
use App\Events\Load_config;
|
||||
use App\Events\Method;
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
* Application Events
|
||||
* --------------------------------------------------------------------
|
||||
* Events allow you to tap into the execution of the program without
|
||||
* modifying or extending core files. This file provides a central
|
||||
* location to define your events, though they can always be added
|
||||
* at run-time, also, if needed.
|
||||
*
|
||||
* You create code that can execute by subscribing to events with
|
||||
* the 'on()' method. This accepts any form of callable, including
|
||||
* Closures, that will be executed when the event is triggered.
|
||||
*
|
||||
* Example:
|
||||
* Events::on('create', [$myInstance, 'myMethod']);
|
||||
*/
|
||||
use App\Libraries\Plugins\PluginManager;
|
||||
|
||||
Events::on('pre_system', static function (): void {
|
||||
if (ENVIRONMENT !== 'testing') {
|
||||
@@ -39,22 +23,19 @@ Events::on('pre_system', static function (): void {
|
||||
ob_start(static fn ($buffer) => $buffer);
|
||||
}
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
* Debug Toolbar Listeners.
|
||||
* --------------------------------------------------------------------
|
||||
* If you delete, they will no longer be collected.
|
||||
*/
|
||||
if (CI_DEBUG && ! is_cli()) {
|
||||
Events::on('DBQuery', 'CodeIgniter\Debug\Toolbar\Collectors\Database::collect');
|
||||
service('toolbar')->respond();
|
||||
// Hot Reload route - for framework use on the hot reloader.
|
||||
if (ENVIRONMENT === 'development') {
|
||||
service('routes')->get('__hot-reload', static function (): void {
|
||||
(new HotReloader())->run();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$pluginManager = new PluginManager();
|
||||
$pluginManager->discoverPlugins();
|
||||
$pluginManager->registerPluginEvents();
|
||||
});
|
||||
|
||||
$config = new Load_config();
|
||||
@@ -64,4 +45,4 @@ $db_log = new Db_log();
|
||||
Events::on('DBQuery', [$db_log, 'db_log_queries']);
|
||||
|
||||
$method = new Method();
|
||||
Events::on('pre_controller', [$method, 'validate_method']);
|
||||
Events::on('pre_controller', [$method, 'validate_method']);
|
||||
@@ -106,24 +106,12 @@ class Attributes extends Secure_Controller
|
||||
$definition_flags |= $flag;
|
||||
}
|
||||
|
||||
// Validate definition_group (definition_fk) foreign key
|
||||
$definition_group_input = $this->request->getPost('definition_group');
|
||||
$definition_fk = $this->validateDefinitionGroup($definition_group_input);
|
||||
|
||||
if ($definition_fk === false) {
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Attributes.definition_invalid_group'),
|
||||
'id' => NEW_ENTRY
|
||||
]);
|
||||
}
|
||||
|
||||
// Save definition data
|
||||
$definition_data = [
|
||||
'definition_name' => $this->request->getPost('definition_name'),
|
||||
'definition_unit' => $this->request->getPost('definition_unit') != '' ? $this->request->getPost('definition_unit') : null,
|
||||
'definition_flags' => $definition_flags,
|
||||
'definition_fk' => $definition_fk
|
||||
'definition_fk' => $this->request->getPost('definition_group') != '' ? $this->request->getPost('definition_group') : null
|
||||
];
|
||||
|
||||
if ($this->request->getPost('definition_type') != null) {
|
||||
@@ -162,32 +150,6 @@ class Attributes extends Secure_Controller
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a definition_group foreign key.
|
||||
* Returns the validated integer ID, null if empty, or false if invalid.
|
||||
*
|
||||
* @param mixed $definition_group_input
|
||||
* @return int|null|false
|
||||
*/
|
||||
private function validateDefinitionGroup(mixed $definition_group_input): int|null|false
|
||||
{
|
||||
if ($definition_group_input === '' || $definition_group_input === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$definition_group_id = (int) $definition_group_input;
|
||||
|
||||
// Must be a positive integer, exist in attribute_definitions, and be of type GROUP
|
||||
if ($definition_group_id <= 0
|
||||
|| !$this->attribute->exists($definition_group_id)
|
||||
|| $this->attribute->getAttributeInfo($definition_group_id)->definition_type !== GROUP
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $definition_group_id;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param int $definition_id
|
||||
|
||||
@@ -36,9 +36,6 @@ class Cashups extends Secure_Controller
|
||||
// filters that will be loaded in the multiselect dropdown
|
||||
$data['filters'] = ['is_deleted' => lang('Cashups.is_deleted')];
|
||||
|
||||
// Restore filters from URL
|
||||
$data = array_merge($data, restoreTableFilters($this->request));
|
||||
|
||||
return view('cashups/manage', $data);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ use App\Models\Appconfig;
|
||||
use App\Models\Attribute;
|
||||
use App\Models\Customer_rewards;
|
||||
use App\Models\Dinner_table;
|
||||
use App\Models\Item;
|
||||
use App\Models\Module;
|
||||
use App\Models\Enums\Rounding_mode;
|
||||
use App\Models\Stock_location;
|
||||
@@ -386,9 +385,9 @@ class Config extends Secure_Controller
|
||||
'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'),
|
||||
'suggestions_first_column' => $this->validateSuggestionsColumn($this->request->getPost('suggestions_first_column'), 'first'),
|
||||
'suggestions_second_column' => $this->validateSuggestionsColumn($this->request->getPost('suggestions_second_column'), 'other'),
|
||||
'suggestions_third_column' => $this->validateSuggestionsColumn($this->request->getPost('suggestions_third_column'), 'other'),
|
||||
'suggestions_first_column' => $this->request->getPost('suggestions_first_column'),
|
||||
'suggestions_second_column' => $this->request->getPost('suggestions_second_column'),
|
||||
'suggestions_third_column' => $this->request->getPost('suggestions_third_column'),
|
||||
'giftcard_number' => $this->request->getPost('giftcard_number'),
|
||||
'derive_sale_quantity' => $this->request->getPost('derive_sale_quantity') != null,
|
||||
'multi_pack_enabled' => $this->request->getPost('multi_pack_enabled') != null,
|
||||
@@ -977,26 +976,4 @@ class Config extends Secure_Controller
|
||||
|
||||
return $this->response->setJSON(['success' => $success]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates suggestions column configuration to prevent SQL injection.
|
||||
*
|
||||
* @param mixed $column The column value from POST
|
||||
* @param string $fieldType Either 'first' or 'other' to determine default fallback
|
||||
* @return string Validated column name
|
||||
*/
|
||||
private function validateSuggestionsColumn(mixed $column, string $fieldType): string
|
||||
{
|
||||
if (!is_string($column)) {
|
||||
return $fieldType === 'first' ? 'name' : '';
|
||||
}
|
||||
|
||||
$allowed = $fieldType === 'first'
|
||||
? Item::ALLOWED_SUGGESTIONS_COLUMNS
|
||||
: Item::ALLOWED_SUGGESTIONS_COLUMNS_WITH_EMPTY;
|
||||
|
||||
$fallback = $fieldType === 'first' ? 'name' : '';
|
||||
|
||||
return in_array($column, $allowed, true) ? $column : $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,6 @@ class Expenses extends Secure_Controller
|
||||
'is_deleted' => lang('Expenses.is_deleted')
|
||||
];
|
||||
|
||||
// Restore filters from URL
|
||||
$data = array_merge($data, restoreTableFilters($this->request));
|
||||
|
||||
return view('expenses/manage', $data);
|
||||
}
|
||||
|
||||
@@ -93,23 +90,16 @@ class Expenses extends Secure_Controller
|
||||
{
|
||||
$data = []; // TODO: Duplicated code
|
||||
|
||||
$data['expenses_info'] = $this->expense->get_info($expense_id);
|
||||
$expense_id = $data['expenses_info']->expense_id;
|
||||
|
||||
$current_employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
$can_assign_employee = $this->employee->has_grant('employees', $current_employee_id);
|
||||
|
||||
$data['employees'] = [];
|
||||
if ($can_assign_employee) {
|
||||
foreach ($this->employee->get_all()->getResult() as $employee) {
|
||||
$data['employees'][$employee->person_id] = $employee->first_name . ' ' . $employee->last_name;
|
||||
foreach ($this->employee->get_all()->getResult() as $employee) {
|
||||
foreach (get_object_vars($employee) as $property => $value) {
|
||||
$employee->$property = $value;
|
||||
}
|
||||
} else {
|
||||
$stored_employee_id = $expense_id == NEW_ENTRY ? $current_employee_id : $data['expenses_info']->employee_id;
|
||||
$stored_employee = $this->employee->get_info($stored_employee_id);
|
||||
$data['employees'][$stored_employee_id] = $stored_employee->first_name . ' ' . $stored_employee->last_name;
|
||||
|
||||
$data['employees'][$employee->person_id] = $employee->first_name . ' ' . $employee->last_name;
|
||||
}
|
||||
$data['can_assign_employee'] = $can_assign_employee;
|
||||
|
||||
$data['expenses_info'] = $this->expense->get_info($expense_id);
|
||||
|
||||
$expense_categories = [];
|
||||
foreach ($this->expense_category->get_all(0, 0, true)->getResultArray() as $row) {
|
||||
@@ -117,9 +107,11 @@ class Expenses extends Secure_Controller
|
||||
}
|
||||
$data['expense_categories'] = $expense_categories;
|
||||
|
||||
$expense_id = $data['expenses_info']->expense_id;
|
||||
|
||||
if ($expense_id == NEW_ENTRY) {
|
||||
$data['expenses_info']->date = date('Y-m-d H:i:s');
|
||||
$data['expenses_info']->employee_id = $current_employee_id;
|
||||
$data['expenses_info']->employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
}
|
||||
|
||||
$data['payments'] = [];
|
||||
@@ -160,20 +152,6 @@ class Expenses extends Secure_Controller
|
||||
|
||||
$date_formatter = date_create_from_format($config['dateformat'] . ' ' . $config['timeformat'], $newdate);
|
||||
|
||||
$current_employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
$submitted_employee_id = $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT);
|
||||
|
||||
if (!$this->employee->has_grant('employees', $current_employee_id)) {
|
||||
if ($expense_id == NEW_ENTRY) {
|
||||
$employee_id = $current_employee_id;
|
||||
} else {
|
||||
$existing_expense = $this->expense->get_info($expense_id);
|
||||
$employee_id = $existing_expense->employee_id;
|
||||
}
|
||||
} else {
|
||||
$employee_id = $submitted_employee_id;
|
||||
}
|
||||
|
||||
$expense_data = [
|
||||
'date' => $date_formatter->format('Y-m-d H:i:s'),
|
||||
'supplier_id' => $this->request->getPost('supplier_id') == '' ? null : $this->request->getPost('supplier_id', FILTER_SANITIZE_NUMBER_INT),
|
||||
@@ -183,7 +161,7 @@ class Expenses extends Secure_Controller
|
||||
'payment_type' => $this->request->getPost('payment_type', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
'expense_category_id' => $this->request->getPost('expense_category_id', FILTER_SANITIZE_NUMBER_INT),
|
||||
'description' => $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
'employee_id' => $employee_id,
|
||||
'employee_id' => $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT),
|
||||
'deleted' => $this->request->getPost('deleted') != null
|
||||
];
|
||||
|
||||
|
||||
@@ -36,18 +36,19 @@ class Home extends Secure_Controller
|
||||
/**
|
||||
* Load "change employee password" form
|
||||
*
|
||||
* @return ResponseInterface|string
|
||||
* @return string
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function getChangePassword(int $employeeId = NEW_ENTRY)
|
||||
public function getChangePassword(int $employeeId = NEW_ENTRY): string
|
||||
{
|
||||
$loggedInEmployee = $this->employee->get_logged_in_employee_info();
|
||||
$currentPersonId = $loggedInEmployee->person_id;
|
||||
|
||||
$employeeId = $employeeId === NEW_ENTRY ? $currentPersonId : $employeeId;
|
||||
|
||||
if (!$this->employee->isAdmin($currentPersonId) && $employeeId !== $currentPersonId) {
|
||||
return $this->response->setStatusCode(403)->setBody(lang('Employees.unauthorized_modify'));
|
||||
if (!$this->employee->can_modify_employee($employeeId, $currentPersonId)) {
|
||||
header('Location: ' . base_url('no_access/home/home'));
|
||||
exit();
|
||||
}
|
||||
|
||||
$person_info = $this->employee->get_info($employeeId);
|
||||
@@ -70,7 +71,7 @@ class Home extends Secure_Controller
|
||||
|
||||
$employeeId = $employeeId === NEW_ENTRY ? $currentUser->person_id : $employeeId;
|
||||
|
||||
if (!$this->employee->isAdmin($currentUser->person_id) && $employeeId !== $currentUser->person_id) {
|
||||
if (!$this->employee->can_modify_employee($employeeId, $currentUser->person_id)) {
|
||||
return $this->response->setStatusCode(403)->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Employees.unauthorized_modify')
|
||||
|
||||
@@ -73,12 +73,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
|
||||
? $stockLocation
|
||||
: $this->item_lib->get_item_location();
|
||||
$data['stock_location'] = $this->item_lib->get_item_location();
|
||||
$data['stock_locations'] = $this->stock_location->get_allowed_locations();
|
||||
|
||||
// Filters that will be loaded in the multiselect dropdown
|
||||
@@ -92,9 +87,6 @@ class Items extends Secure_Controller
|
||||
'temporary' => lang('Items.temp')
|
||||
];
|
||||
|
||||
// Restore filters from URL
|
||||
$data = array_merge($data, restoreTableFilters($this->request));
|
||||
|
||||
return view('items/manage', $data);
|
||||
}
|
||||
|
||||
|
||||
99
app/Controllers/Plugins/Manage.php
Normal file
99
app/Controllers/Plugins/Manage.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Plugins;
|
||||
|
||||
use App\Controllers\Secure_Controller;
|
||||
use App\Libraries\Plugins\PluginManager;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
class Manage extends Secure_Controller
|
||||
{
|
||||
private PluginManager $pluginManager;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('plugins');
|
||||
$this->pluginManager = new PluginManager();
|
||||
$this->pluginManager->discoverPlugins();
|
||||
}
|
||||
|
||||
public function getIndex(): string
|
||||
{
|
||||
$plugins = $this->pluginManager->getAllPlugins();
|
||||
$enabledPlugins = $this->pluginManager->getEnabledPlugins();
|
||||
|
||||
$pluginData = [];
|
||||
foreach ($plugins as $pluginId => $plugin) {
|
||||
$pluginData[$pluginId] = [
|
||||
'id' => $plugin->getPluginId(),
|
||||
'name' => $plugin->getPluginName(),
|
||||
'description' => $plugin->getPluginDescription(),
|
||||
'version' => $plugin->getVersion(),
|
||||
'enabled' => isset($enabledPlugins[$pluginId]),
|
||||
'has_config' => $plugin->getConfigView() !== null,
|
||||
];
|
||||
}
|
||||
|
||||
echo view('plugins/manage', ['plugins' => $pluginData]);
|
||||
return '';
|
||||
}
|
||||
|
||||
public function postEnable(string $pluginId): ResponseInterface
|
||||
{
|
||||
if ($this->pluginManager->enablePlugin($pluginId)) {
|
||||
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.plugin_enabled')]);
|
||||
}
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_enable_failed')]);
|
||||
}
|
||||
|
||||
public function postDisable(string $pluginId): ResponseInterface
|
||||
{
|
||||
if ($this->pluginManager->disablePlugin($pluginId)) {
|
||||
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.plugin_disabled')]);
|
||||
}
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_disable_failed')]);
|
||||
}
|
||||
|
||||
public function postUninstall(string $pluginId): ResponseInterface
|
||||
{
|
||||
if ($this->pluginManager->uninstallPlugin($pluginId)) {
|
||||
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.plugin_uninstalled')]);
|
||||
}
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_uninstall_failed')]);
|
||||
}
|
||||
|
||||
public function getConfig(string $pluginId): ResponseInterface
|
||||
{
|
||||
$plugin = $this->pluginManager->getPlugin($pluginId);
|
||||
|
||||
if (!$plugin) {
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_not_found')]);
|
||||
}
|
||||
|
||||
$configView = $plugin->getConfigView();
|
||||
if (!$configView) {
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_no_config')]);
|
||||
}
|
||||
|
||||
$settings = $plugin->getSettings();
|
||||
echo view($configView, ['settings' => $settings, 'plugin' => $plugin]);
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
public function postSaveConfig(string $pluginId): ResponseInterface
|
||||
{
|
||||
$plugin = $this->pluginManager->getPlugin($pluginId);
|
||||
|
||||
if (!$plugin) {
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_not_found')]);
|
||||
}
|
||||
|
||||
$settings = $this->request->getPost();
|
||||
unset($settings['_method'], $settings['csrf_token_name']);
|
||||
|
||||
if ($plugin->saveSettings($settings)) {
|
||||
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.settings_saved')]);
|
||||
}
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.settings_save_failed')]);
|
||||
}
|
||||
}
|
||||
@@ -241,26 +241,15 @@ class Receivings extends Secure_Controller
|
||||
$data['suppliers'][$supplier->person_id] = $supplier->first_name . ' ' . $supplier->last_name;
|
||||
}
|
||||
|
||||
$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);
|
||||
|
||||
$data['employees'] = [];
|
||||
if ($can_assign_employee) {
|
||||
foreach ($this->employee->get_all()->getResult() as $employee) {
|
||||
$data['employees'][$employee->person_id] = $employee->first_name . ' ' . $employee->last_name;
|
||||
}
|
||||
} else {
|
||||
$stored_employee_id = $receiving_info['employee_id'];
|
||||
$stored_employee = $this->employee->get_info($stored_employee_id);
|
||||
$data['employees'][$stored_employee_id] = $stored_employee->first_name . ' ' . $stored_employee->last_name;
|
||||
foreach ($this->employee->get_all()->getResult() as $employee) {
|
||||
$data['employees'][$employee->person_id] = $employee->first_name . ' ' . $employee->last_name;
|
||||
}
|
||||
|
||||
$receiving_info = $this->receiving->get_info($receiving_id)->getRowArray();
|
||||
$data['selected_supplier_name'] = !empty($receiving_info['supplier_id']) ? $receiving_info['company_name'] : '';
|
||||
$data['selected_supplier_id'] = $receiving_info['supplier_id'];
|
||||
$data['receiving_info'] = $receiving_info;
|
||||
$data['can_assign_employee'] = $can_assign_employee;
|
||||
|
||||
return view('receivings/form', $data);
|
||||
}
|
||||
@@ -502,20 +491,10 @@ class Receivings extends Secure_Controller
|
||||
$date_formatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $newdate);
|
||||
$receiving_time = $date_formatter->format('Y-m-d H:i:s');
|
||||
|
||||
$current_employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
$submitted_employee_id = $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT);
|
||||
|
||||
if (!$this->employee->has_grant('employees', $current_employee_id)) {
|
||||
$existing_receiving = $this->receiving->get_info($receiving_id)->getRowArray();
|
||||
$employee_id = $existing_receiving['employee_id'];
|
||||
} else {
|
||||
$employee_id = $submitted_employee_id;
|
||||
}
|
||||
|
||||
$receiving_data = [
|
||||
'receiving_time' => $receiving_time,
|
||||
'supplier_id' => $this->request->getPost('supplier_id') ? $this->request->getPost('supplier_id', FILTER_SANITIZE_NUMBER_INT) : null,
|
||||
'employee_id' => $employee_id,
|
||||
'employee_id' => $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT),
|
||||
'comment' => $this->request->getPost('comment', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
'reference' => $this->request->getPost('reference') != '' ? $this->request->getPost('reference', FILTER_SANITIZE_FULL_SPECIAL_CHARS) : null
|
||||
];
|
||||
|
||||
@@ -1776,7 +1776,7 @@ class Reports extends Secure_Controller
|
||||
{
|
||||
$this->clearCache();
|
||||
|
||||
$definition_names = $this->attribute->get_definitions_by_flags(attribute::SHOW_IN_SALES, true);
|
||||
$definition_names = $this->attribute->get_definitions_by_flags(attribute::SHOW_IN_SALES);
|
||||
|
||||
$inputs = [
|
||||
'start_date' => $start_date,
|
||||
@@ -1789,12 +1789,7 @@ class Reports extends Secure_Controller
|
||||
$this->detailed_sales->create($inputs);
|
||||
|
||||
$columns = $this->detailed_sales->getDataColumns();
|
||||
// Extract just names for column headers
|
||||
$definitionHeaders = [];
|
||||
foreach ($definition_names as $definition_id => $definitionInfo) {
|
||||
$definitionHeaders[$definition_id] = $definitionInfo['name'];
|
||||
}
|
||||
$columns['details'] = array_merge($columns['details'], $definitionHeaders);
|
||||
$columns['details'] = array_merge($columns['details'], $definition_names);
|
||||
|
||||
$headers = $columns;
|
||||
|
||||
@@ -1935,19 +1930,14 @@ class Reports extends Secure_Controller
|
||||
{
|
||||
$this->clearCache();
|
||||
|
||||
$definition_names = $this->attribute->get_definitions_by_flags(attribute::SHOW_IN_RECEIVINGS, true);
|
||||
$definition_names = $this->attribute->get_definitions_by_flags(attribute::SHOW_IN_RECEIVINGS);
|
||||
|
||||
$inputs = ['start_date' => $start_date, 'end_date' => $end_date, 'receiving_type' => $receiving_type, 'location_id' => $location_id, 'definition_ids' => array_keys($definition_names)];
|
||||
|
||||
$this->detailed_receivings->create($inputs);
|
||||
|
||||
$columns = $this->detailed_receivings->getDataColumns();
|
||||
// Extract just names for column headers
|
||||
$definitionHeaders = [];
|
||||
foreach ($definition_names as $definition_id => $definitionInfo) {
|
||||
$definitionHeaders[$definition_id] = $definitionInfo['name'];
|
||||
}
|
||||
$columns['details'] = array_merge($columns['details'], $definitionHeaders);
|
||||
$columns['details'] = array_merge($columns['details'], $definition_names);
|
||||
|
||||
$headers = $columns;
|
||||
$report_data = $this->detailed_receivings->getData($inputs);
|
||||
|
||||
@@ -75,15 +75,15 @@ class Sales extends Secure_Controller
|
||||
/**
|
||||
* Load the sale edit modal. Used in app/Views/sales/register.php.
|
||||
*
|
||||
* @return ResponseInterface|string
|
||||
* @return string
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function getManage(): ResponseInterface|string
|
||||
public function getManage(): string
|
||||
{
|
||||
$personId = $this->session->get('person_id');
|
||||
$person_id = $this->session->get('person_id');
|
||||
|
||||
if (!$this->employee->has_grant('reports_sales', $personId)) {
|
||||
return redirect()->to('no_access/sales/reports_sales');
|
||||
if (!$this->employee->has_grant('reports_sales', $person_id)) {
|
||||
redirect('no_access/sales/reports_sales');
|
||||
} else {
|
||||
$data['table_headers'] = get_sales_manage_table_headers();
|
||||
|
||||
@@ -92,31 +92,18 @@ class Sales extends Secure_Controller
|
||||
'only_due' => lang('Sales.due_filter'),
|
||||
'only_check' => lang('Sales.check_filter'),
|
||||
'only_creditcard' => lang('Sales.credit_filter'),
|
||||
'only_debit' => lang('Sales.debit'),
|
||||
'only_invoices' => lang('Sales.invoice_filter'),
|
||||
'selected_customer' => lang('Sales.selected_customer')
|
||||
];
|
||||
|
||||
if ($this->sale_lib->get_customer() != -1) {
|
||||
$selectedFilters = ['selected_customer'];
|
||||
$selected_filters = ['selected_customer'];
|
||||
$data['customer_selected'] = true;
|
||||
} else {
|
||||
$data['customer_selected'] = false;
|
||||
$selectedFilters = [];
|
||||
$selected_filters = [];
|
||||
}
|
||||
|
||||
// Restore filters from URL query string
|
||||
$filters = restoreTableFilters($this->request);
|
||||
if (!empty($filters['selected_filters'])) {
|
||||
$selectedFilters = array_merge($selectedFilters, $filters['selected_filters']);
|
||||
}
|
||||
if (isset($filters['start_date'])) {
|
||||
$data['start_date'] = $filters['start_date'];
|
||||
}
|
||||
if (isset($filters['end_date'])) {
|
||||
$data['end_date'] = $filters['end_date'];
|
||||
}
|
||||
$data['selected_filters'] = $selectedFilters;
|
||||
$data['selected_filters'] = $selected_filters;
|
||||
|
||||
return view('sales/manage', $data);
|
||||
}
|
||||
@@ -155,7 +142,6 @@ class Sales extends Secure_Controller
|
||||
'only_check' => false,
|
||||
'selected_customer' => false,
|
||||
'only_creditcard' => false,
|
||||
'only_debit' => false,
|
||||
'only_invoices' => $this->config['invoice_enable'] && $this->request->getGet('only_invoices', FILTER_SANITIZE_NUMBER_INT),
|
||||
'is_valid_receipt' => $this->sale->is_valid_receipt($search)
|
||||
];
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class Migration_Initial_Schema extends Migration
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a migration step.
|
||||
* Only runs on fresh installs - skips if database already has tables.
|
||||
*
|
||||
* For testing: CI4's DatabaseTestTrait with $refresh=true handles table
|
||||
* cleanup/creation automatically. This migration only loads initial schema
|
||||
* on fresh databases where no application tables exist.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Check if core application tables exist (existing install)
|
||||
// Note: migrations table may exist even on fresh DB due to migration tracking
|
||||
$tables = $this->db->listTables();
|
||||
|
||||
// Check for a core application table, not just migrations table
|
||||
foreach ($tables as $table) {
|
||||
// Strip prefix if present for comparison
|
||||
$tableName = str_replace($this->db->getPrefix(), '', $table);
|
||||
if (in_array($tableName, ['app_config', 'items', 'employees', 'people'])) {
|
||||
// Database already populated - skip initial schema
|
||||
// This is an existing installation upgrading from older version
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fresh install - load initial schema
|
||||
helper('migration');
|
||||
execute_script(APPPATH . 'Database/Migrations/sqlscripts/initial_schema.sql');
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert a migration step.
|
||||
* Cannot revert initial schema - would lose all data.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Cannot safely revert initial schema
|
||||
// Would require dropping all tables which would lose all data
|
||||
$this->db->query('SET FOREIGN_KEY_CHECKS = 0');
|
||||
|
||||
foreach ($this->db->listTables() as $table) {
|
||||
$this->db->query('DROP TABLE IF EXISTS `' . $table . '`');
|
||||
}
|
||||
|
||||
$this->db->query('SET FOREIGN_KEY_CHECKS = 1');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class PluginConfigTableCreate extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
log_message('info', 'Migrating plugin_config table started');
|
||||
|
||||
execute_script(APPPATH . 'Database/Migrations/sqlscripts/3.4.1_PluginConfigTableCreate.sql');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('plugin_config', true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS `ospos_plugin_config` (
|
||||
`key` varchar(100) NOT NULL,
|
||||
`value` text NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (`key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
145
app/Database/constraints.sql
Normal file
145
app/Database/constraints.sql
Normal file
@@ -0,0 +1,145 @@
|
||||
--
|
||||
-- Constraints for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_customers`
|
||||
--
|
||||
ALTER TABLE `ospos_customers`
|
||||
ADD CONSTRAINT `ospos_customers_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_employees`
|
||||
--
|
||||
ALTER TABLE `ospos_employees`
|
||||
ADD CONSTRAINT `ospos_employees_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_inventory`
|
||||
--
|
||||
ALTER TABLE `ospos_inventory`
|
||||
ADD CONSTRAINT `ospos_inventory_ibfk_1` FOREIGN KEY (`trans_items`) REFERENCES `ospos_items` (`item_id`),
|
||||
ADD CONSTRAINT `ospos_inventory_ibfk_2` FOREIGN KEY (`trans_user`) REFERENCES `ospos_employees` (`person_id`),
|
||||
ADD CONSTRAINT `ospos_inventory_ibfk_3` FOREIGN KEY (`trans_location`) REFERENCES `ospos_stock_locations` (`location_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_items`
|
||||
--
|
||||
ALTER TABLE `ospos_items`
|
||||
ADD CONSTRAINT `ospos_items_ibfk_1` FOREIGN KEY (`supplier_id`) REFERENCES `ospos_suppliers` (`person_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_items_taxes`
|
||||
--
|
||||
ALTER TABLE `ospos_items_taxes`
|
||||
ADD CONSTRAINT `ospos_items_taxes_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`) ON DELETE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_item_kit_items`
|
||||
--
|
||||
ALTER TABLE `ospos_item_kit_items`
|
||||
ADD CONSTRAINT `ospos_item_kit_items_ibfk_1` FOREIGN KEY (`item_kit_id`) REFERENCES `ospos_item_kits` (`item_kit_id`) ON DELETE CASCADE,
|
||||
ADD CONSTRAINT `ospos_item_kit_items_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`) ON DELETE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_permissions`
|
||||
--
|
||||
ALTER TABLE `ospos_permissions`
|
||||
ADD CONSTRAINT `ospos_permissions_ibfk_1` FOREIGN KEY (`module_id`) REFERENCES `ospos_modules` (`module_id`) ON DELETE CASCADE,
|
||||
ADD CONSTRAINT `ospos_permissions_ibfk_2` FOREIGN KEY (`location_id`) REFERENCES `ospos_stock_locations` (`location_id`) ON DELETE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_grants`
|
||||
--
|
||||
ALTER TABLE `ospos_grants`
|
||||
ADD CONSTRAINT `ospos_grants_ibfk_1` foreign key (`permission_id`) references `ospos_permissions` (`permission_id`) ON DELETE CASCADE,
|
||||
ADD CONSTRAINT `ospos_grants_ibfk_2` foreign key (`person_id`) references `ospos_employees` (`person_id`) ON DELETE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_receivings`
|
||||
--
|
||||
ALTER TABLE `ospos_receivings`
|
||||
ADD CONSTRAINT `ospos_receivings_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `ospos_employees` (`person_id`),
|
||||
ADD CONSTRAINT `ospos_receivings_ibfk_2` FOREIGN KEY (`supplier_id`) REFERENCES `ospos_suppliers` (`person_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_receivings_items`
|
||||
--
|
||||
ALTER TABLE `ospos_receivings_items`
|
||||
ADD CONSTRAINT `ospos_receivings_items_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`),
|
||||
ADD CONSTRAINT `ospos_receivings_items_ibfk_2` FOREIGN KEY (`receiving_id`) REFERENCES `ospos_receivings` (`receiving_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_sales`
|
||||
--
|
||||
ALTER TABLE `ospos_sales`
|
||||
ADD CONSTRAINT `ospos_sales_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `ospos_employees` (`person_id`),
|
||||
ADD CONSTRAINT `ospos_sales_ibfk_2` FOREIGN KEY (`customer_id`) REFERENCES `ospos_customers` (`person_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_sales_items`
|
||||
--
|
||||
ALTER TABLE `ospos_sales_items`
|
||||
ADD CONSTRAINT `ospos_sales_items_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`),
|
||||
ADD CONSTRAINT `ospos_sales_items_ibfk_2` FOREIGN KEY (`sale_id`) REFERENCES `ospos_sales` (`sale_id`),
|
||||
ADD CONSTRAINT `ospos_sales_items_ibfk_3` FOREIGN KEY (`item_location`) REFERENCES `ospos_stock_locations` (`location_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_sales_items_taxes`
|
||||
--
|
||||
ALTER TABLE `ospos_sales_items_taxes`
|
||||
ADD CONSTRAINT `ospos_sales_items_taxes_ibfk_1` FOREIGN KEY (`sale_id`,`item_id`,`line`) REFERENCES `ospos_sales_items` (`sale_id`,`item_id`,`line`),
|
||||
ADD CONSTRAINT `ospos_sales_items_taxes_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_sales_payments`
|
||||
--
|
||||
ALTER TABLE `ospos_sales_payments`
|
||||
ADD CONSTRAINT `ospos_sales_payments_ibfk_1` FOREIGN KEY (`sale_id`) REFERENCES `ospos_sales` (`sale_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_sales_suspended`
|
||||
--
|
||||
ALTER TABLE `ospos_sales_suspended`
|
||||
ADD CONSTRAINT `ospos_sales_suspended_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `ospos_employees` (`person_id`),
|
||||
ADD CONSTRAINT `ospos_sales_suspended_ibfk_2` FOREIGN KEY (`customer_id`) REFERENCES `ospos_customers` (`person_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_sales_suspended_items`
|
||||
--
|
||||
ALTER TABLE `ospos_sales_suspended_items`
|
||||
ADD CONSTRAINT `ospos_sales_suspended_items_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`),
|
||||
ADD CONSTRAINT `ospos_sales_suspended_items_ibfk_2` FOREIGN KEY (`sale_id`) REFERENCES `ospos_sales_suspended` (`sale_id`),
|
||||
ADD CONSTRAINT `ospos_sales_suspended_items_ibfk_3` FOREIGN KEY (`item_location`) REFERENCES `ospos_stock_locations` (`location_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_sales_suspended_items_taxes`
|
||||
--
|
||||
ALTER TABLE `ospos_sales_suspended_items_taxes`
|
||||
ADD CONSTRAINT `ospos_sales_suspended_items_taxes_ibfk_1` FOREIGN KEY (`sale_id`,`item_id`,`line`) REFERENCES `ospos_sales_suspended_items` (`sale_id`,`item_id`,`line`),
|
||||
ADD CONSTRAINT `ospos_sales_suspended_items_taxes_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_sales_suspended_payments`
|
||||
--
|
||||
ALTER TABLE `ospos_sales_suspended_payments`
|
||||
ADD CONSTRAINT `ospos_sales_suspended_payments_ibfk_1` FOREIGN KEY (`sale_id`) REFERENCES `ospos_sales_suspended` (`sale_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_item_quantities`
|
||||
--
|
||||
ALTER TABLE `ospos_item_quantities`
|
||||
ADD CONSTRAINT `ospos_item_quantities_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`),
|
||||
ADD CONSTRAINT `ospos_item_quantities_ibfk_2` FOREIGN KEY (`location_id`) REFERENCES `ospos_stock_locations` (`location_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_suppliers`
|
||||
--
|
||||
ALTER TABLE `ospos_suppliers`
|
||||
ADD CONSTRAINT `ospos_suppliers_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_giftcards`
|
||||
--
|
||||
ALTER TABLE `ospos_giftcards`
|
||||
ADD CONSTRAINT `ospos_giftcards_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`);
|
||||
@@ -730,148 +730,3 @@ CREATE TABLE `ospos_suppliers` (
|
||||
--
|
||||
-- Dumping data for table `ospos_suppliers`
|
||||
--
|
||||
--
|
||||
-- Constraints for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_customers`
|
||||
--
|
||||
ALTER TABLE `ospos_customers`
|
||||
ADD CONSTRAINT `ospos_customers_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_employees`
|
||||
--
|
||||
ALTER TABLE `ospos_employees`
|
||||
ADD CONSTRAINT `ospos_employees_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_inventory`
|
||||
--
|
||||
ALTER TABLE `ospos_inventory`
|
||||
ADD CONSTRAINT `ospos_inventory_ibfk_1` FOREIGN KEY (`trans_items`) REFERENCES `ospos_items` (`item_id`),
|
||||
ADD CONSTRAINT `ospos_inventory_ibfk_2` FOREIGN KEY (`trans_user`) REFERENCES `ospos_employees` (`person_id`),
|
||||
ADD CONSTRAINT `ospos_inventory_ibfk_3` FOREIGN KEY (`trans_location`) REFERENCES `ospos_stock_locations` (`location_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_items`
|
||||
--
|
||||
ALTER TABLE `ospos_items`
|
||||
ADD CONSTRAINT `ospos_items_ibfk_1` FOREIGN KEY (`supplier_id`) REFERENCES `ospos_suppliers` (`person_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_items_taxes`
|
||||
--
|
||||
ALTER TABLE `ospos_items_taxes`
|
||||
ADD CONSTRAINT `ospos_items_taxes_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`) ON DELETE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_item_kit_items`
|
||||
--
|
||||
ALTER TABLE `ospos_item_kit_items`
|
||||
ADD CONSTRAINT `ospos_item_kit_items_ibfk_1` FOREIGN KEY (`item_kit_id`) REFERENCES `ospos_item_kits` (`item_kit_id`) ON DELETE CASCADE,
|
||||
ADD CONSTRAINT `ospos_item_kit_items_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`) ON DELETE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_permissions`
|
||||
--
|
||||
ALTER TABLE `ospos_permissions`
|
||||
ADD CONSTRAINT `ospos_permissions_ibfk_1` FOREIGN KEY (`module_id`) REFERENCES `ospos_modules` (`module_id`) ON DELETE CASCADE,
|
||||
ADD CONSTRAINT `ospos_permissions_ibfk_2` FOREIGN KEY (`location_id`) REFERENCES `ospos_stock_locations` (`location_id`) ON DELETE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_grants`
|
||||
--
|
||||
ALTER TABLE `ospos_grants`
|
||||
ADD CONSTRAINT `ospos_grants_ibfk_1` foreign key (`permission_id`) references `ospos_permissions` (`permission_id`) ON DELETE CASCADE,
|
||||
ADD CONSTRAINT `ospos_grants_ibfk_2` foreign key (`person_id`) references `ospos_employees` (`person_id`) ON DELETE CASCADE;
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_receivings`
|
||||
--
|
||||
ALTER TABLE `ospos_receivings`
|
||||
ADD CONSTRAINT `ospos_receivings_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `ospos_employees` (`person_id`),
|
||||
ADD CONSTRAINT `ospos_receivings_ibfk_2` FOREIGN KEY (`supplier_id`) REFERENCES `ospos_suppliers` (`person_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_receivings_items`
|
||||
--
|
||||
ALTER TABLE `ospos_receivings_items`
|
||||
ADD CONSTRAINT `ospos_receivings_items_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`),
|
||||
ADD CONSTRAINT `ospos_receivings_items_ibfk_2` FOREIGN KEY (`receiving_id`) REFERENCES `ospos_receivings` (`receiving_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_sales`
|
||||
--
|
||||
ALTER TABLE `ospos_sales`
|
||||
ADD CONSTRAINT `ospos_sales_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `ospos_employees` (`person_id`),
|
||||
ADD CONSTRAINT `ospos_sales_ibfk_2` FOREIGN KEY (`customer_id`) REFERENCES `ospos_customers` (`person_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_sales_items`
|
||||
--
|
||||
ALTER TABLE `ospos_sales_items`
|
||||
ADD CONSTRAINT `ospos_sales_items_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`),
|
||||
ADD CONSTRAINT `ospos_sales_items_ibfk_2` FOREIGN KEY (`sale_id`) REFERENCES `ospos_sales` (`sale_id`),
|
||||
ADD CONSTRAINT `ospos_sales_items_ibfk_3` FOREIGN KEY (`item_location`) REFERENCES `ospos_stock_locations` (`location_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_sales_items_taxes`
|
||||
--
|
||||
ALTER TABLE `ospos_sales_items_taxes`
|
||||
ADD CONSTRAINT `ospos_sales_items_taxes_ibfk_1` FOREIGN KEY (`sale_id`,`item_id`,`line`) REFERENCES `ospos_sales_items` (`sale_id`,`item_id`,`line`),
|
||||
ADD CONSTRAINT `ospos_sales_items_taxes_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_sales_payments`
|
||||
--
|
||||
ALTER TABLE `ospos_sales_payments`
|
||||
ADD CONSTRAINT `ospos_sales_payments_ibfk_1` FOREIGN KEY (`sale_id`) REFERENCES `ospos_sales` (`sale_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_sales_suspended`
|
||||
--
|
||||
ALTER TABLE `ospos_sales_suspended`
|
||||
ADD CONSTRAINT `ospos_sales_suspended_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `ospos_employees` (`person_id`),
|
||||
ADD CONSTRAINT `ospos_sales_suspended_ibfk_2` FOREIGN KEY (`customer_id`) REFERENCES `ospos_customers` (`person_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_sales_suspended_items`
|
||||
--
|
||||
ALTER TABLE `ospos_sales_suspended_items`
|
||||
ADD CONSTRAINT `ospos_sales_suspended_items_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`),
|
||||
ADD CONSTRAINT `ospos_sales_suspended_items_ibfk_2` FOREIGN KEY (`sale_id`) REFERENCES `ospos_sales_suspended` (`sale_id`),
|
||||
ADD CONSTRAINT `ospos_sales_suspended_items_ibfk_3` FOREIGN KEY (`item_location`) REFERENCES `ospos_stock_locations` (`location_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_sales_suspended_items_taxes`
|
||||
--
|
||||
ALTER TABLE `ospos_sales_suspended_items_taxes`
|
||||
ADD CONSTRAINT `ospos_sales_suspended_items_taxes_ibfk_1` FOREIGN KEY (`sale_id`,`item_id`,`line`) REFERENCES `ospos_sales_suspended_items` (`sale_id`,`item_id`,`line`),
|
||||
ADD CONSTRAINT `ospos_sales_suspended_items_taxes_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_sales_suspended_payments`
|
||||
--
|
||||
ALTER TABLE `ospos_sales_suspended_payments`
|
||||
ADD CONSTRAINT `ospos_sales_suspended_payments_ibfk_1` FOREIGN KEY (`sale_id`) REFERENCES `ospos_sales_suspended` (`sale_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_item_quantities`
|
||||
--
|
||||
ALTER TABLE `ospos_item_quantities`
|
||||
ADD CONSTRAINT `ospos_item_quantities_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`),
|
||||
ADD CONSTRAINT `ospos_item_quantities_ibfk_2` FOREIGN KEY (`location_id`) REFERENCES `ospos_stock_locations` (`location_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_suppliers`
|
||||
--
|
||||
ALTER TABLE `ospos_suppliers`
|
||||
ADD CONSTRAINT `ospos_suppliers_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`);
|
||||
|
||||
--
|
||||
-- Constraints for table `ospos_giftcards`
|
||||
--
|
||||
ALTER TABLE `ospos_giftcards`
|
||||
ADD CONSTRAINT `ospos_giftcards_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`);
|
||||
24
app/Helpers/plugin_helper.php
Normal file
24
app/Helpers/plugin_helper.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use CodeIgniter\Events\Events;
|
||||
|
||||
if (!function_exists('plugin_content')) {
|
||||
function plugin_content(string $section, array $data = []): string
|
||||
{
|
||||
$results = Events::trigger("view:{$section}", $data);
|
||||
|
||||
if (is_array($results)) {
|
||||
return implode('', array_filter($results, fn($r) => is_string($r)));
|
||||
}
|
||||
|
||||
return is_string($results) ? $results : '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('plugin_content_exists')) {
|
||||
function plugin_content_exists(string $section): bool
|
||||
{
|
||||
$observers = Events::listRegistered("view:{$section}");
|
||||
return !empty($observers);
|
||||
}
|
||||
}
|
||||
@@ -408,7 +408,7 @@ function get_items_manage_table_headers(): string
|
||||
{
|
||||
$attribute = model(Attribute::class);
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$definitionsWithTypes = $attribute->get_definitions_by_flags($attribute::SHOW_IN_ITEMS, true);
|
||||
$definition_names = $attribute->get_definitions_by_flags($attribute::SHOW_IN_ITEMS); // TODO: this should be made into a constant in constants.php
|
||||
|
||||
$headers = item_headers();
|
||||
|
||||
@@ -420,8 +420,8 @@ function get_items_manage_table_headers(): string
|
||||
|
||||
$headers[] = ['item_pic' => lang('Items.image'), 'sortable' => false];
|
||||
|
||||
foreach ($definitionsWithTypes as $definition_id => $definitionInfo) {
|
||||
$headers[] = [$definition_id => $definitionInfo['name'], 'sortable' => false];
|
||||
foreach ($definition_names as $definition_id => $definition_name) {
|
||||
$headers[] = [$definition_id => $definition_name, 'sortable' => false];
|
||||
}
|
||||
|
||||
$headers[] = ['inventory' => '', 'escape' => false];
|
||||
@@ -479,7 +479,7 @@ function get_item_data_row(object $item): array
|
||||
$item->name .= NAME_SEPARATOR . $item->pack_name;
|
||||
}
|
||||
|
||||
$definition_names = $attribute->get_definitions_by_flags($attribute::SHOW_IN_ITEMS, true);
|
||||
$definition_names = $attribute->get_definitions_by_flags($attribute::SHOW_IN_ITEMS);
|
||||
|
||||
$columns = [
|
||||
'items.item_id' => $item->item_id,
|
||||
@@ -634,7 +634,7 @@ function parse_attribute_values(array $columns, array $row): array
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $definition_names Array of definition_id => ['name' => name, 'type' => type] or definition_id => name
|
||||
* @param array $definition_names
|
||||
* @param array $row
|
||||
* @return array
|
||||
*/
|
||||
@@ -651,16 +651,10 @@ function expand_attribute_values(array $definition_names, array $row): array
|
||||
}
|
||||
|
||||
$attribute_values = [];
|
||||
foreach ($definition_names as $definition_id => $definitionInfo) {
|
||||
foreach ($definition_names as $definition_id => $definition_name) {
|
||||
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);
|
||||
} else {
|
||||
$attribute_values["$definition_id"] = $raw_value;
|
||||
}
|
||||
$attribute_value = $indexed_values[$definition_id];
|
||||
$attribute_values["$definition_id"] = $attribute_value;
|
||||
} else {
|
||||
$attribute_values["$definition_id"] = "";
|
||||
}
|
||||
@@ -931,24 +925,3 @@ function get_controller(): string
|
||||
$controller_name_parts = explode('\\', $controller_name);
|
||||
return end($controller_name_parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores filter values from URL query string.
|
||||
*
|
||||
* @param CodeIgniter\HTTP\IncomingRequest $request The request object
|
||||
* @return array Array with 'start_date', 'end_date', and 'selected_filters' keys
|
||||
*/
|
||||
function restoreTableFilters($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) {
|
||||
return $value !== null && $value !== [];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "هل أنت متأكد من أنك تريد حذف الميزات المحددة ؟",
|
||||
"confirm_restore" => "هل أنت متأكد من أنك تريد استعادة السمة (السمات) المحددة؟",
|
||||
"definition_cannot_be_deleted" => "لا يمكن حذف السمات المحددة",
|
||||
"definition_invalid_group" => "المجموعة المحددة غير موجودة أو غير صالحة.",
|
||||
"definition_error_adding_updating" => "لا يمكن إضافة السمة {0} أو تحديثها. يرجى التحقق من سجل الخطأ.",
|
||||
"definition_flags" => "رؤية الميزات",
|
||||
"definition_group" => "المجموعة",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "هل أنت متأكد من أنك تريد حذف الميزات المحددة ؟",
|
||||
"confirm_restore" => "هل أنت متأكد من أنك تريد استعادة السمة (السمات) المحددة؟",
|
||||
"definition_cannot_be_deleted" => "لا يمكن حذف السمات المحددة",
|
||||
"definition_invalid_group" => "المجموعة المحددة غير موجودة أو غير صالحة.",
|
||||
"definition_error_adding_updating" => "لا يمكن إضافة السمة {0} أو تحديثها. يرجى التحقق من سجل الخطأ.",
|
||||
"definition_flags" => "رؤية الميزات",
|
||||
"definition_group" => "المجموعة",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Seçilmiş Atributları silmək istədiyinizdən əminsinizmi?",
|
||||
"confirm_restore" => "Seçilmiş atributları bərpa etmək istədiyinizə əminsinizmi?",
|
||||
"definition_cannot_be_deleted" => "Seçilmiş xüsusiyyətləri silmək olmadı",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "{0} -in atributları əlavə oluna və yenilənə bilmədi. Lütfən XƏTA loq faylını yoxlayın.",
|
||||
"definition_flags" => "Atribut görünüşü",
|
||||
"definition_group" => "Qrup",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "",
|
||||
"confirm_restore" => "",
|
||||
"definition_cannot_be_deleted" => "",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "",
|
||||
"definition_flags" => "",
|
||||
"definition_group" => "",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Da li ste sigurni da želite da izbrišete izabrani atribut?",
|
||||
"confirm_restore" => "Da li ste sigurni da želite vratiti izabrane atribute?",
|
||||
"definition_cannot_be_deleted" => "Nije moguće izbrisati izabrane atribut",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "Atribut {0} nije moguće dodati ili ažurirati. Molimo provjerite dnevnik grešaka.",
|
||||
"definition_flags" => "Vidljivost atributa",
|
||||
"definition_group" => "Grupa",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "ئایا دڵنیای کە دەتەوێت تایبەتمەندییە هەڵبژێردراوەکە(کان) بسڕیتەوە؟",
|
||||
"confirm_restore" => "ئایا دڵنیای کە دەتەوێت تایبەتمەندییە هەڵبژێردراوەکە(کان) بگەڕێنیتەوە؟",
|
||||
"definition_cannot_be_deleted" => "نەتوانرا تایبەتمەندی هەڵبژێردراو بسڕدرێتەوە",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "تایبەتمەندی {0} نەتوانرا زیاد بکرێت یان نوێ بکرێتەوە. تکایە لیستی هەڵەکان بپشکنە.",
|
||||
"definition_flags" => "توانای بینراویی تایبەتمەندی",
|
||||
"definition_group" => "گروپ",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "",
|
||||
"confirm_restore" => "",
|
||||
"definition_cannot_be_deleted" => "",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "",
|
||||
"definition_flags" => "",
|
||||
"definition_group" => "",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Er du sikker på, at du vil slette de valgte egenskaber?",
|
||||
"confirm_restore" => "Er du sikker på, at du vil gendanne de valgte egenskaber?",
|
||||
"definition_cannot_be_deleted" => "De valgte egenskaber kunne ikke slettes",
|
||||
"definition_invalid_group" => "Den valgte gruppe findes ikke eller er ugyldig.",
|
||||
"definition_error_adding_updating" => "Egenskab {0} Kunne ikke tilføjes eller opdateres. Tjek venligst fejlprotokollen.",
|
||||
"definition_flags" => "Egenskabens Synlighed",
|
||||
"definition_group" => "Gruppe",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "",
|
||||
"confirm_restore" => "",
|
||||
"definition_cannot_be_deleted" => "",
|
||||
"definition_invalid_group" => "Die ausgewählte Gruppe existiert nicht oder ist ungültig.",
|
||||
"definition_error_adding_updating" => "",
|
||||
"definition_flags" => "",
|
||||
"definition_group" => "",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Sind Sie sicher, dass Sie die ausgewählten Attribute löschen möchten?",
|
||||
"confirm_restore" => "Sind Sie sicher, dass Sie die ausgewählten Attribute wiederherstellen möchten?",
|
||||
"definition_cannot_be_deleted" => "Ausgewählte Attribute konnten nicht gelöscht werden",
|
||||
"definition_invalid_group" => "Die ausgewählte Gruppe existiert nicht oder ist ungültig.",
|
||||
"definition_error_adding_updating" => "Das Attribut {0} konnte nicht hinzugefügt oder aktualisiert werden. Bitte überprüfen Sie den Error-Log.",
|
||||
"definition_flags" => "Attribut Sichtbarkeit",
|
||||
"definition_group" => "Gruppe",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Είστε βέβαιοι ότι θέλετε να διαγράψετε τα επιλεγμένα χαρακτηριστικά;",
|
||||
"confirm_restore" => "Είστε βέβαιοι ότι θέλετε να επαναφέρετε τα επιλεγμένα χαρακτηριστικά;",
|
||||
"definition_cannot_be_deleted" => "Δεν ήταν δυνατή η διαγραφή των επιλεγμένων χαρακτηριστικών",
|
||||
"definition_invalid_group" => "Η επιλεγμένη ομάδα δεν υπάρχει ή δεν είναι έγκυρη.",
|
||||
"definition_error_adding_updating" => "Το χαρακτηριστικό {0} δεν ήταν δυνατό να προστεθεί ή να ενημερωθεί. Ελέγξτε το αρχείο καταγραφής σφαλμάτων.",
|
||||
"definition_flags" => "Ορατότητα χαρακτηριστικών",
|
||||
"definition_group" => "Ομάδα",
|
||||
|
||||
@@ -6,7 +6,6 @@ return [
|
||||
"confirm_restore" => "Are you sure you want to restore the selected attribute(s)?",
|
||||
"definition_cannot_be_deleted" => "Could not delete selected attribute(s)",
|
||||
"definition_error_adding_updating" => "Attribute {0} could not be added or updated. Please check the error log.",
|
||||
"definition_invalid_group" => "The selected group does not exist or is invalid.",
|
||||
"definition_flags" => "Attribute Visibility",
|
||||
"definition_group" => "Group",
|
||||
"definition_id" => "Id",
|
||||
|
||||
@@ -6,7 +6,6 @@ return [
|
||||
"confirm_restore" => "Are you sure you want to restore the selected attribute(s)?",
|
||||
"definition_cannot_be_deleted" => "Could not delete selected attribute(s)",
|
||||
"definition_error_adding_updating" => "Attribute {0} could not be added or updated. Please check the error log.",
|
||||
"definition_invalid_group" => "The selected group does not exist or is invalid.",
|
||||
"definition_flags" => "Attribute Visibility",
|
||||
"definition_group" => "Group",
|
||||
"definition_id" => "Id",
|
||||
|
||||
27
app/Language/en/Plugins.php
Normal file
27
app/Language/en/Plugins.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
return [
|
||||
// Plugin Management
|
||||
"plugins" => "Plugins",
|
||||
"plugin_management" => "Plugin Management",
|
||||
"plugin_name" => "Plugin Name",
|
||||
"plugin_description" => "Description",
|
||||
"plugin_version" => "Version",
|
||||
"plugin_status" => "Status",
|
||||
"plugin_enabled" => "Plugin enabled successfully",
|
||||
"plugin_enable_failed" => "Failed to enable plugin",
|
||||
"plugin_disabled" => "Plugin disabled successfully",
|
||||
"plugin_disable_failed" => "Failed to disable plugin",
|
||||
"plugin_uninstalled" => "Plugin uninstalled successfully",
|
||||
"plugin_uninstall_failed" => "Failed to uninstall plugin",
|
||||
"plugin_not_found" => "Plugin not found",
|
||||
"plugin_no_config" => "This plugin has no configuration options",
|
||||
"settings_saved" => "Plugin settings saved successfully",
|
||||
"settings_save_failed" => "Failed to save plugin settings",
|
||||
"enable" => "Enable",
|
||||
"disable" => "Disable",
|
||||
"configure" => "Configure",
|
||||
"uninstall" => "Uninstall",
|
||||
"no_plugins_found" => "No plugins found",
|
||||
"active" => "Active",
|
||||
"inactive" => "Inactive",
|
||||
];
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "¿Está seguro de que desea borrar los atributos seleccionados?",
|
||||
"confirm_restore" => "¿Está seguro de que desea restaurar los atributos seleccionados?",
|
||||
"definition_cannot_be_deleted" => "No se han podido borrar los atributos seleccionados",
|
||||
"definition_invalid_group" => "El grupo seleccionado no existe o no es válido.",
|
||||
"definition_error_adding_updating" => "El atributo {0} no pudo ser agregado o actulizado. Por favor compruebe el registro de errores.",
|
||||
"definition_flags" => "Visibilidad del atributo",
|
||||
"definition_group" => "Grupo",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "¿Está seguro de eliminar el/los atributo(s) seleccionado(s)?",
|
||||
"confirm_restore" => "¿Está seguro que quiere restaurar los atributos seleccionados?",
|
||||
"definition_cannot_be_deleted" => "No ha sido posible eliminar el/los atributo(s) seleccionado(s)",
|
||||
"definition_invalid_group" => "El grupo seleccionado no existe o no es válido.",
|
||||
"definition_error_adding_updating" => "El atributo {0} no pudo ser agregado o actualizado. Favor de revisar el registro de errorres.",
|
||||
"definition_flags" => "Visibilidad del atributo",
|
||||
"definition_group" => "Grupo",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "آیا مطمئن هستید که می خواهید ویژگی (های) انتخاب شده را حذف کنید؟",
|
||||
"confirm_restore" => "آیا مطمئن هستید که می خواهید ویژگی (های) انتخاب شده را بازیابی کنید؟",
|
||||
"definition_cannot_be_deleted" => "نمی توان ویژگی (های) انتخابی را حذف کرد",
|
||||
"definition_invalid_group" => "گروه انتخاب شده وجود ندارد یا نامعتبر است.",
|
||||
"definition_error_adding_updating" => "ویژگی{0} اضافه نشد یا به روز نمی شود. لطفا گزارش خطا را بررسی کنید.",
|
||||
"definition_flags" => "قابلیت مشاهده ویژگی",
|
||||
"definition_group" => "گروه",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Êtes-vous certain de vouloir supprimer le(s) attribut(s) sélectionné(s) ?",
|
||||
"confirm_restore" => "Êtes-vous certain de vouloir restaurer le(s) attribut(s) sélectionné(s) ?",
|
||||
"definition_cannot_be_deleted" => "Le(s) attribut(s) sélectionné(s) n'ont pas pu être supprimé(s)",
|
||||
"definition_invalid_group" => "Le groupe sélectionné n'existe pas ou est invalide.",
|
||||
"definition_error_adding_updating" => "L'attribut {0} n'a pas pu être ajouté ou mis à jour. Veuillez vérifier le journal d'erreurs.",
|
||||
"definition_flags" => "Visibilité de l'attribut",
|
||||
"definition_group" => "Groupe",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "האם אתה בטוח שברצונך למחוק את המאפיינים שנבחרו?",
|
||||
"confirm_restore" => "האם אתה בטוח שברצונך לשחזר את המאפיינים שנבחרו?",
|
||||
"definition_cannot_be_deleted" => "לא ניתן למחוק מאפיינים נבחר(ים)",
|
||||
"definition_invalid_group" => "הקבוצה שנבחרה לא קיימת או אינה תקינה.",
|
||||
"definition_error_adding_updating" => "לא ניתן להוסיף או לעדכן את הערך {0}. בדוק את יומן השגיאות.",
|
||||
"definition_flags" => "מאפיין גלוי",
|
||||
"definition_group" => "קבוצה",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "",
|
||||
"confirm_restore" => "",
|
||||
"definition_cannot_be_deleted" => "",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "",
|
||||
"definition_flags" => "",
|
||||
"definition_group" => "",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Biztosan törli szeretné a kijelölt tulajdonságokat?",
|
||||
"confirm_restore" => "Biztosan visszaállítja a kijelölt tulajdonságokat?",
|
||||
"definition_cannot_be_deleted" => "Nem sikerült törölni a kijelölt tulajdonságokat",
|
||||
"definition_invalid_group" => "A kiválasztott csoport nem létezik vagy érvénytelen.",
|
||||
"definition_error_adding_updating" => "{0} attribútum nem adható hozzá és nem frissíthető. Kérjük, ellenőrizze a hibanaplót.",
|
||||
"definition_flags" => "Tulajdonság láthatósága",
|
||||
"definition_group" => "Csoport",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Are you sure you want to delete the selected attribute(s)?",
|
||||
"confirm_restore" => "Are you sure you want to restore the selected attribute(s)?",
|
||||
"definition_cannot_be_deleted" => "Could not delete selected attribute(s)",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "Attribute {0} could not be added or updated. Please check the error log.",
|
||||
"definition_flags" => "Attribute Visibility",
|
||||
"definition_group" => "Group",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Apakah Anda yakin ingin menghapus atribut tersebut?",
|
||||
"confirm_restore" => "Apakah Anda yakin ingin mengembalikan atribut tersebut?",
|
||||
"definition_cannot_be_deleted" => "Tidak bisa menghapus atribut terpilih",
|
||||
"definition_invalid_group" => "Grup yang dipilih tidak ada atau tidak valid.",
|
||||
"definition_error_adding_updating" => "Atribut {0} tidak dapat ditambah atau diperbaharui. Silahkan periksa log kesalahan.",
|
||||
"definition_flags" => "Visibilitas Atribut",
|
||||
"definition_group" => "Grup",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Sei sicuro di voler eliminare gli attributi selezionati?",
|
||||
"confirm_restore" => "Sei sicuro di voler ripristinare l'attributo selezionato?",
|
||||
"definition_cannot_be_deleted" => "Non riesco a cancellare l'attributo selezionato",
|
||||
"definition_invalid_group" => "Il gruppo selezionato non esiste o non è valido.",
|
||||
"definition_error_adding_updating" => "Impossibile aggiungere o aggiornare l'attributo {0}. Si prega di controllare il registro degli errori.",
|
||||
"definition_flags" => "Visibilità attributo",
|
||||
"definition_group" => "Gruppo",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "តើអ្នកពិតជាចង់លុប ព៌តមានបន្ថែម ដែលបានជ្រើសរើស?",
|
||||
"confirm_restore" => "តើអ្នកពិតជាដាក់ឡើងវិញនៅ ព៌តមានបន្ថែម ដែលបានជ្រើសរើស?",
|
||||
"definition_cannot_be_deleted" => "មិនអាចលុបព៌តមានបន្ថែមដែលបានជ្រើសរើស",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "ព៌តមានបន្ថែម {0} មិនអាចថែម រឺកែប្រែបានឡើយ។ សូមចូលទៅឆែករបាយការណ៍កំហុស។",
|
||||
"definition_flags" => "ដាក់បង្ហាញព៌តមានបន្ថែម",
|
||||
"definition_group" => "ក្រុម",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "ແນ່ໃຈຫຼືບໍທີ່ຈະລືບລາຍການທີ່ເລືອກ",
|
||||
"confirm_restore" => "ແນ່ໃຈຫຼືບໍທີ່ຈະຄືນຄ່າແອັດທິບິ້ວດັ່ງກ່າວ?",
|
||||
"definition_cannot_be_deleted" => "ບໍສາມາດລືບລາຍການທີ່ເລືອກໄດ້",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "ລາຍການ {0} ບໍສາມາດເພີ່ມ ຫຼື ແກ້ໄຂ. ກະລຸນາກວດສອບຢູ່ log ຂໍ້ຜິດຜາດ",
|
||||
"definition_flags" => "ຄູນສົມບັດການເບິ່ງເຫັນ",
|
||||
"definition_group" => "ກຸ່ມ",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Are you sure you want to delete the selected attribute(s)?",
|
||||
"confirm_restore" => "",
|
||||
"definition_cannot_be_deleted" => "Could not delete selected attribute(s)",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "",
|
||||
"definition_flags" => "Attribute Visibility",
|
||||
"definition_group" => "Group",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Are you sure you want to delete the selected attribute(s)?",
|
||||
"confirm_restore" => "Are you sure you want to restore the selected attribute(s)?",
|
||||
"definition_cannot_be_deleted" => "Could not delete selected attribute(s)",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "Attribute {0} could not be added or updated. Please check the error log.",
|
||||
"definition_flags" => "Attribute Visibility",
|
||||
"definition_group" => "Group",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Bent u zeker dat u de geselecteerde attributen wil verwijderen?",
|
||||
"confirm_restore" => "Bent u zeker dat u de geselecteerde attributen wil herstellen?",
|
||||
"definition_cannot_be_deleted" => "De geselecteerde attributen konden niet verwijderd worden",
|
||||
"definition_invalid_group" => "De geselecteerde groep bestaat niet of is ongeldig.",
|
||||
"definition_error_adding_updating" => "Attribuut {0} kon niet toegevoegd of gewijzigd worden. Kijk de error logs na.",
|
||||
"definition_flags" => "Zichtbaarheid",
|
||||
"definition_group" => "Groep",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Weet u zeker dat u de geselecteerde kenmerken wilt verwijderen?",
|
||||
"confirm_restore" => "Weet u zeker dat u de geselecteerde kenmerken wilt herstellen?",
|
||||
"definition_cannot_be_deleted" => "Kan geselecteerde kenmerk(en) niet verwijderen",
|
||||
"definition_invalid_group" => "De geselecteerde groep bestaat niet of is ongeldig.",
|
||||
"definition_error_adding_updating" => "Kenmerk {0} kan niet worden toegevoegd of bijgewerkt. Bekijk het foutenlogboek.",
|
||||
"definition_flags" => "Kenmerk zichtbaarheid",
|
||||
"definition_group" => "Groep",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Czy jesteś pewny, że chcesz usunąć wybrane atrybuty?",
|
||||
"confirm_restore" => "Czy jesteś pewien, że chcesz przywrócić zaznaczone atrybuty?",
|
||||
"definition_cannot_be_deleted" => "Nie można usunąć wybranych atrybutów",
|
||||
"definition_invalid_group" => "Wybrana grupa nie istnieje lub jest nieprawidłowa.",
|
||||
"definition_error_adding_updating" => "Atrybut 51 nie może zostać dodany lub zaktualizowany. Sprawdź dziennik błędów.",
|
||||
"definition_flags" => "Widoczność atrybutu",
|
||||
"definition_group" => "Grupa",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Tem certeza de que deseja excluir os atributos selecionados?",
|
||||
"confirm_restore" => "Tem certeza de que deseja restaurar o(s) atributo(s) selecionado(s)?",
|
||||
"definition_cannot_be_deleted" => "Não foi possível excluir atributo selecionado (s)",
|
||||
"definition_invalid_group" => "O grupo selecionado não existe ou é inválido.",
|
||||
"definition_error_adding_updating" => "Atributo {0} não pode ser adicionado ou atualizado. Por favor verifique o log de erros.",
|
||||
"definition_flags" => "Visibilidade de atributo",
|
||||
"definition_group" => "Grupo",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Sigur doriti stergerea atributului/atributelor selectat(e)?",
|
||||
"confirm_restore" => "",
|
||||
"definition_cannot_be_deleted" => "Nu se poate sterge atributul/atributele selectat(e)",
|
||||
"definition_invalid_group" => "Grupul selectat nu există sau este invalid.",
|
||||
"definition_error_adding_updating" => "",
|
||||
"definition_flags" => "Vizibilitate atribut",
|
||||
"definition_group" => "Grup",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Вы уверены, что хотите удалить выбранные атрибут(ы)?",
|
||||
"confirm_restore" => "Вы уверены, что хотите восстановить выбранные атрибут(ы)?",
|
||||
"definition_cannot_be_deleted" => "Не удалось удалить выбранные атрибут(ы)",
|
||||
"definition_invalid_group" => "Выбранная группа не существует или недействительна.",
|
||||
"definition_error_adding_updating" => "Атрибут {0} не может быть добавлен или обновлен. Пожалуйста, проверьте журнал ошибок.",
|
||||
"definition_flags" => "Видимость атрибута",
|
||||
"definition_group" => "Группа",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Är du säker på att du vill ta bort de valda attributen?",
|
||||
"confirm_restore" => "Är du säker på att du vill återställa de valda attributen?",
|
||||
"definition_cannot_be_deleted" => "Det gick inte att ta bort valda attribut",
|
||||
"definition_invalid_group" => "Den valda gruppen finns inte eller är ogiltig.",
|
||||
"definition_error_adding_updating" => "Attribut{0} kunde inte läggas till eller uppdateras. Kontrollera felloggen.",
|
||||
"definition_flags" => "Attribut synlighet",
|
||||
"definition_group" => "Grupp",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Una uhakika unataka kufuta sifa iliyochaguliwa/zilizochaguliwa?",
|
||||
"confirm_restore" => "Una uhakika unataka kurejesha sifa iliyochaguliwa/zilizochaguliwa?",
|
||||
"definition_cannot_be_deleted" => "Haiwezekani kufuta sifa iliyochaguliwa/zilizochaguliwa",
|
||||
"definition_invalid_group" => "Kikundi ulichochagua hakipo au hakitoshi.",
|
||||
"definition_error_adding_updating" => "Sifa {0} haiwezekani kuongezwa au kusasishwa. Tafadhali angalia logi ya makosa.",
|
||||
"definition_flags" => "Uonekano wa Sifa",
|
||||
"definition_group" => "Kundi",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Una uhakika unataka kufuta sifa iliyochaguliwa/zilizochaguliwa?",
|
||||
"confirm_restore" => "Una uhakika unataka kurejesha sifa iliyochaguliwa/zilizochaguliwa?",
|
||||
"definition_cannot_be_deleted" => "Haiwezekani kufuta sifa iliyochaguliwa/zilizochaguliwa",
|
||||
"definition_invalid_group" => "Kikundi ulichochagua hakipo au hakitoshi.",
|
||||
"definition_error_adding_updating" => "Sifa {0} haiwezekani kuongezwa au kusasishwa. Tafadhali angalia logi ya makosa.",
|
||||
"definition_flags" => "Uonekano wa Sifa",
|
||||
"definition_group" => "Kundi",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "தேர்ந்தெடுக்கப்பட்ட பண்புக்கூறு (களை) நீக்க விரும்புகிறீர்களா?",
|
||||
"confirm_restore" => "தேர்ந்தெடுக்கப்பட்ட பண்புக்கூறுகளை (களை) மீட்டெடுக்க விரும்புகிறீர்களா?",
|
||||
"definition_cannot_be_deleted" => "Could not delete selected attribute(s)",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "Attribute {0} could not be added or updated. Please check the error log.",
|
||||
"definition_flags" => "Attribute Visibility",
|
||||
"definition_group" => "Group",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "ต้องการลบคุณลักษณะที่เลือกหรือไม่ ?",
|
||||
"confirm_restore" => "ต้องการคืนค่าคุณลักษณะที่เลือกหรือไม่ ?",
|
||||
"definition_cannot_be_deleted" => "ไม่สามารถลบคุณลักษณะที่เลือก",
|
||||
"definition_invalid_group" => "กลุ่มที่เลือกไม่มีอยู่หรือไม่ถูกต้อง",
|
||||
"definition_error_adding_updating" => "ไม่สามารถเพิ่มหรือแก้ไขคุณลักษณะ {0}, โปรดตรวจสอบความผิดพลาดในบันทึก",
|
||||
"definition_flags" => "การมองเห็นคุณลักษณะ",
|
||||
"definition_group" => "กลุ่ม",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Are you sure you want to restore the selected attribute(s)?",
|
||||
"confirm_restore" => "Are you sure you want to delete the selected attribute(s)?",
|
||||
"definition_cannot_be_deleted" => "Could not delete selected attribute(s)",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "Attribute {0} could not be added or updated. Please check the error log.",
|
||||
"definition_flags" => "Attribute Visibility",
|
||||
"definition_group" => "Group",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Seçili niteliği ya da nitelikleri silmek istediğinize emin misiniz?",
|
||||
"confirm_restore" => "Seçili nitelik ya da nitelikleri kurtarmak istediğinize emin misiniz?",
|
||||
"definition_cannot_be_deleted" => "Seçili nitelik ya da nitelikler silinemedi",
|
||||
"definition_invalid_group" => "Seçilen grup mevcut değil veya geçersiz.",
|
||||
"definition_error_adding_updating" => "Nitelik {0} eklenemedi ya da güncellenemedi. Lütfen hata kaydını gözden geçirin.",
|
||||
"definition_flags" => "Nitelik Görünebilirliği",
|
||||
"definition_group" => "Küme",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Ви впевнені, що хочете видалити вибрані атрибут(и)?",
|
||||
"confirm_restore" => "Ви впевнені, що хочете відновити вибрані атрибут(и)?",
|
||||
"definition_cannot_be_deleted" => "Не вдалося видалити вибрані атрибут(и)",
|
||||
"definition_invalid_group" => "Вибрана група не існує або недійсна.",
|
||||
"definition_error_adding_updating" => "Атрибут {0} не може бути доданий або оновлений. Будь ласка, перевірте журнал помилок.",
|
||||
"definition_flags" => "Видимість атрибуту",
|
||||
"definition_group" => "Група",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "کیا آپ منتخب شدہ کو حذف کرنا چاہتے ہیں ؟",
|
||||
"confirm_restore" => "کیا آپ منتخب شدہ کو بحال کرنا چاہتے ہیں ؟",
|
||||
"definition_cannot_be_deleted" => "منتخب شدہ کو حذف نہیں کیا جا سکتا",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "Attribute {0} could not be added or updated. Please check the error log.",
|
||||
"definition_flags" => "Attribute Visibility",
|
||||
"definition_group" => "Group",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "Bạn có chắc chắn muốn xóa (các) thuộc tính đã chọn không?",
|
||||
"confirm_restore" => "Bạn có chắc chắn muốn khôi phục (các) thuộc tính đã chọn không?",
|
||||
"definition_cannot_be_deleted" => "Không thể xóa (các) thuộc tính được chọn",
|
||||
"definition_invalid_group" => "Nhóm đã chọn không tồn tại hoặc không hợp lệ.",
|
||||
"definition_error_adding_updating" => "Thuộc tính {0} không thể thêm hoặc cập nhật. Vui lòng kiểm tra nhật ký lỗi.",
|
||||
"definition_flags" => "Hiển thị thuộc tính",
|
||||
"definition_group" => "Nhóm",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "确定要删除所选属性吗",
|
||||
"confirm_restore" => "您确定要还原所选属性吗?",
|
||||
"definition_cannot_be_deleted" => "不能删除该特征属性",
|
||||
"definition_invalid_group" => "所选组不存在或无效。",
|
||||
"definition_error_adding_updating" => "无法添加或更新属性{0}。 请检查错误日志。",
|
||||
"definition_flags" => "属性可见性",
|
||||
"definition_group" => "组",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "您確定要刪除此屬性?",
|
||||
"confirm_restore" => "您確定要還原所選屬性嗎?",
|
||||
"definition_cannot_be_deleted" => "無法刪除所選屬性",
|
||||
"definition_invalid_group" => "所選擇的群組不存在或無效。",
|
||||
"definition_error_adding_updating" => "無法添加或更新屬性 {0}。 請檢查錯誤日誌。",
|
||||
"definition_flags" => "屬性可見性",
|
||||
"definition_group" => "群組",
|
||||
|
||||
70
app/Libraries/Plugins/BasePlugin.php
Normal file
70
app/Libraries/Plugins/BasePlugin.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\Plugins;
|
||||
|
||||
use App\Models\PluginConfig;
|
||||
|
||||
abstract class BasePlugin implements PluginInterface
|
||||
{
|
||||
protected PluginConfig $configModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->configModel = new PluginConfig();
|
||||
}
|
||||
|
||||
public function install(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function uninstall(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
$enabled = $this->configModel->getValue("{$this->getPluginId()}_enabled");
|
||||
return $enabled === '1' || $enabled === 'true';
|
||||
}
|
||||
|
||||
protected function getSetting(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$value = $this->configModel->getValue("{$this->getPluginId()}_{$key}");
|
||||
return $value ?? $default;
|
||||
}
|
||||
|
||||
protected function setSetting(string $key, mixed $value): bool
|
||||
{
|
||||
$stringValue = is_array($value) || is_object($value)
|
||||
? json_encode($value)
|
||||
: (string)$value;
|
||||
|
||||
return $this->configModel->setValue("{$this->getPluginId()}_{$key}", $stringValue);
|
||||
}
|
||||
|
||||
public function getSettings(): array
|
||||
{
|
||||
return $this->configModel->getPluginSettings($this->getPluginId());
|
||||
}
|
||||
|
||||
public function saveSettings(array $settings): bool
|
||||
{
|
||||
$prefixedSettings = [];
|
||||
foreach ($settings as $key => $value) {
|
||||
if (is_array($value) || is_object($value)) {
|
||||
$prefixedSettings["{$this->getPluginId()}_{$key}"] = json_encode($value);
|
||||
} else {
|
||||
$prefixedSettings["{$this->getPluginId()}_{$key}"] = (string)$value;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->configModel->batchSave($prefixedSettings);
|
||||
}
|
||||
|
||||
protected function log(string $level, string $message): void
|
||||
{
|
||||
log_message($level, "[Plugin:{$this->getPluginName()}] {$message}");
|
||||
}
|
||||
}
|
||||
56
app/Libraries/Plugins/PluginInterface.php
Normal file
56
app/Libraries/Plugins/PluginInterface.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\Plugins;
|
||||
|
||||
interface PluginInterface
|
||||
{
|
||||
public function getPluginId(): string;
|
||||
|
||||
public function getPluginName(): string;
|
||||
|
||||
public function getPluginDescription(): string;
|
||||
|
||||
public function getVersion(): string;
|
||||
|
||||
/**
|
||||
* Register event listeners for this plugin.
|
||||
*
|
||||
* Use Events::on() to register callbacks for OSPOS events.
|
||||
* This method is called when the plugin is loaded and enabled.
|
||||
*
|
||||
* Example:
|
||||
* Events::on('item_sale', [$this, 'onItemSale']);
|
||||
* Events::on('item_change', [$this, 'onItemChange']);
|
||||
*/
|
||||
public function registerEvents(): void;
|
||||
|
||||
/**
|
||||
* Install the plugin.
|
||||
*
|
||||
* Called when the plugin is first enabled. Use this to create database tables,
|
||||
* set default configuration values, and run any setup required.
|
||||
*/
|
||||
public function install(): bool;
|
||||
|
||||
/**
|
||||
* Uninstall the plugin.
|
||||
*
|
||||
* Called when the plugin is being removed. Use this to remove database tables,
|
||||
* clean up configuration, etc.
|
||||
*/
|
||||
public function uninstall(): bool;
|
||||
|
||||
public function isEnabled(): bool;
|
||||
|
||||
/**
|
||||
* Get the path to the plugin's configuration view file.
|
||||
* Returns null if the plugin has no configuration UI.
|
||||
*
|
||||
* Example: 'Plugins/mailchimp/config'
|
||||
*/
|
||||
public function getConfigView(): ?string;
|
||||
|
||||
public function getSettings(): array;
|
||||
|
||||
public function saveSettings(array $settings): bool;
|
||||
}
|
||||
174
app/Libraries/Plugins/PluginManager.php
Normal file
174
app/Libraries/Plugins/PluginManager.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\Plugins;
|
||||
|
||||
use App\Models\PluginConfig;
|
||||
use CodeIgniter\Events\Events;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
class PluginManager
|
||||
{
|
||||
private array $plugins = [];
|
||||
private array $enabledPlugins = [];
|
||||
private PluginConfig $configModel;
|
||||
private string $pluginsPath;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->configModel = new PluginConfig();
|
||||
$this->pluginsPath = APPPATH . 'Plugins';
|
||||
}
|
||||
|
||||
public function discoverPlugins(): void
|
||||
{
|
||||
if (!is_dir($this->pluginsPath)) {
|
||||
log_message('debug', 'Plugins directory does not exist: ' . $this->pluginsPath);
|
||||
return;
|
||||
}
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($this->pluginsPath, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isDir() || $file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$className = $this->getClassNameFromFile($file->getPathname());
|
||||
|
||||
if (!$className) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!class_exists($className)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_subclass_of($className, PluginInterface::class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$plugin = new $className();
|
||||
|
||||
$this->plugins[$plugin->getPluginId()] = $plugin;
|
||||
log_message('debug', "Discovered plugin: {$plugin->getPluginName()}");
|
||||
}
|
||||
}
|
||||
|
||||
private function getClassNameFromFile(string $pathname): ?string
|
||||
{
|
||||
$relativePath = str_replace($this->pluginsPath . DIRECTORY_SEPARATOR, '', $pathname);
|
||||
$relativePath = str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath);
|
||||
$className = 'App\\Plugins\\' . str_replace('.php', '', $relativePath);
|
||||
|
||||
return $className;
|
||||
}
|
||||
|
||||
public function registerPluginEvents(): void
|
||||
{
|
||||
foreach ($this->plugins as $pluginId => $plugin) {
|
||||
if ($this->isPluginEnabled($pluginId)) {
|
||||
$this->enabledPlugins[$pluginId] = $plugin;
|
||||
$plugin->registerEvents();
|
||||
log_message('debug', "Registered events for plugin: {$plugin->getPluginName()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getAllPlugins(): array
|
||||
{
|
||||
return $this->plugins;
|
||||
}
|
||||
|
||||
public function getEnabledPlugins(): array
|
||||
{
|
||||
return $this->enabledPlugins;
|
||||
}
|
||||
|
||||
public function getPlugin(string $pluginId): ?PluginInterface
|
||||
{
|
||||
return $this->plugins[$pluginId] ?? null;
|
||||
}
|
||||
|
||||
public function isPluginEnabled(string $pluginId): bool
|
||||
{
|
||||
$enabled = $this->configModel->getValue($this->getEnabledKey($pluginId));
|
||||
return $enabled === '1' || $enabled === 'true';
|
||||
}
|
||||
|
||||
public function enablePlugin(string $pluginId): bool
|
||||
{
|
||||
$plugin = $this->getPlugin($pluginId);
|
||||
if (!$plugin) {
|
||||
log_message('error', "Plugin not found: {$pluginId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->configModel->exists($this->getInstalledKey($pluginId))) {
|
||||
if (!$plugin->install()) {
|
||||
log_message('error', "Failed to install plugin: {$pluginId}");
|
||||
return false;
|
||||
}
|
||||
$this->configModel->setValue($this->getInstalledKey($pluginId), '1');
|
||||
}
|
||||
|
||||
$this->configModel->setValue($this->getEnabledKey($pluginId), '1');
|
||||
log_message('info', "Plugin enabled: {$pluginId}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function disablePlugin(string $pluginId): bool
|
||||
{
|
||||
if (!$this->getPlugin($pluginId)) {
|
||||
log_message('error', "Plugin not found: {$pluginId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->configModel->setValue($this->getEnabledKey($pluginId), '0');
|
||||
log_message('info', "Plugin disabled: {$pluginId}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function uninstallPlugin(string $pluginId): bool
|
||||
{
|
||||
$plugin = $this->getPlugin($pluginId);
|
||||
if (!$plugin) {
|
||||
log_message('error', "Plugin not found: {$pluginId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$plugin->uninstall()) {
|
||||
log_message('error', "Failed to uninstall plugin: {$pluginId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->configModel->deleteAllStartingWith($pluginId . '_');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getSetting(string $pluginId, string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->configModel->getValue("{$pluginId}_{$key}") ?? $default;
|
||||
}
|
||||
|
||||
public function setSetting(string $pluginId, string $key, mixed $value): bool
|
||||
{
|
||||
return $this->configModel->setValue("{$pluginId}_{$key}", $value);
|
||||
}
|
||||
|
||||
private function getEnabledKey(string $pluginId): string
|
||||
{
|
||||
return "{$pluginId}_enabled";
|
||||
}
|
||||
|
||||
private function getInstalledKey(string $pluginId): string
|
||||
{
|
||||
return "{$pluginId}_installed";
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries;
|
||||
namespace app\Libraries;
|
||||
|
||||
use App\Models\Tokens\Token;
|
||||
use Config\OSPOS;
|
||||
@@ -14,137 +14,40 @@ use DateTime;
|
||||
*/
|
||||
class Token_lib
|
||||
{
|
||||
private array $strftimeToIntlPatternMap = [
|
||||
'%a' => 'EEE',
|
||||
'%A' => 'EEEE',
|
||||
'%b' => 'MMM',
|
||||
'%B' => 'MMMM',
|
||||
'%d' => 'dd',
|
||||
'%D' => 'MM/dd/yy',
|
||||
'%e' => 'd',
|
||||
'%F' => 'yyyy-MM-dd',
|
||||
'%h' => 'MMM',
|
||||
'%j' => 'D',
|
||||
'%m' => 'MM',
|
||||
'%U' => 'w',
|
||||
'%V' => 'ww',
|
||||
'%W' => 'ww',
|
||||
'%y' => 'yy',
|
||||
'%Y' => 'yyyy',
|
||||
'%H' => 'HH',
|
||||
'%I' => 'hh',
|
||||
'%l' => 'h',
|
||||
'%M' => 'mm',
|
||||
'%p' => 'a',
|
||||
'%P' => 'a',
|
||||
'%r' => 'hh:mm:ss a',
|
||||
'%R' => 'HH:mm',
|
||||
'%S' => 'ss',
|
||||
'%T' => 'HH:mm:ss',
|
||||
'%X' => 'HH:mm:ss',
|
||||
'%z' => 'ZZZZZ',
|
||||
'%Z' => 'z',
|
||||
'%g' => 'yy',
|
||||
'%G' => 'yyyy',
|
||||
'%u' => 'e',
|
||||
'%w' => 'c',
|
||||
];
|
||||
|
||||
private array $validStrftimeFormats = [
|
||||
'a', 'A', 'b', 'B', 'c', 'd', 'D', 'e', 'F', 'g', 'G',
|
||||
'h', 'H', 'I', 'j', 'm', 'M', 'n', 'p', 'P', 'r', 'R',
|
||||
'S', 't', 'T', 'u', 'U', 'V', 'w', 'W', 'x', 'X', 'y', 'Y', 'z', 'Z'
|
||||
];
|
||||
|
||||
/**
|
||||
* Expands all the tokens found in a given text string and returns the results.
|
||||
*/
|
||||
public function render(string $tokened_text, array $tokens = [], $save = true): string
|
||||
{
|
||||
if (str_contains($tokened_text, '%')) {
|
||||
$tokened_text = $this->applyDateFormats($tokened_text);
|
||||
// Apply the transformation for the "%" tokens if any are used
|
||||
if (strpos($tokened_text, '%') !== false) {
|
||||
$tokened_text = strftime($tokened_text); // TODO: these need to be converted to IntlDateFormatter::format()
|
||||
}
|
||||
|
||||
// Call scan to build an array of all of the tokens used in the text to be transformed
|
||||
$token_tree = $this->scan($tokened_text);
|
||||
|
||||
if (empty($token_tree)) {
|
||||
return $tokened_text;
|
||||
if (strpos($tokened_text, '%') !== false) {
|
||||
return strftime($tokened_text);
|
||||
} else {
|
||||
return $tokened_text;
|
||||
}
|
||||
}
|
||||
|
||||
$token_values = [];
|
||||
$tokens_to_replace = [];
|
||||
$this->generate($token_tree, $tokens, $tokens_to_replace, $token_values, $save);
|
||||
$this->generate($token_tree, $tokens_to_replace, $token_values, $tokens, $save);
|
||||
|
||||
return str_replace($tokens_to_replace, $token_values, $tokened_text);
|
||||
}
|
||||
|
||||
private function applyDateFormats(string $text): string
|
||||
{
|
||||
$formatter = new IntlDateFormatter(
|
||||
null,
|
||||
IntlDateFormatter::FULL,
|
||||
IntlDateFormatter::FULL,
|
||||
null,
|
||||
null,
|
||||
''
|
||||
);
|
||||
|
||||
$dateTime = new DateTime();
|
||||
|
||||
return preg_replace_callback(
|
||||
'/%([a-zA-Z%])/',
|
||||
function ($match) use ($formatter, $dateTime) {
|
||||
$formatChar = $match[1];
|
||||
|
||||
if ($formatChar === '%') {
|
||||
return '%';
|
||||
}
|
||||
|
||||
if ($formatChar === 'n') {
|
||||
return "\n";
|
||||
}
|
||||
|
||||
if ($formatChar === 't') {
|
||||
return "\t";
|
||||
}
|
||||
|
||||
if ($formatChar === 'C') {
|
||||
return str_pad((string) intdiv((int) $dateTime->format('Y'), 100), 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
if ($formatChar === 'c') {
|
||||
$formatter->setPattern('yyyy-MM-dd HH:mm:ss');
|
||||
$result = $formatter->format($dateTime);
|
||||
return $result !== false ? $result : $match[0];
|
||||
}
|
||||
|
||||
if ($formatChar === 'x') {
|
||||
$formatter->setPattern('yyyy-MM-dd');
|
||||
$result = $formatter->format($dateTime);
|
||||
return $result !== false ? $result : $match[0];
|
||||
}
|
||||
|
||||
if (!in_array($formatChar, $this->validStrftimeFormats, true)) {
|
||||
return $match[0];
|
||||
}
|
||||
|
||||
$intlPattern = $this->strftimeToIntlPatternMap[$match[0]] ?? null;
|
||||
|
||||
if ($intlPattern === null) {
|
||||
return $match[0];
|
||||
}
|
||||
|
||||
$formatter->setPattern($intlPattern);
|
||||
$result = $formatter->format($dateTime);
|
||||
|
||||
return $result !== false ? $result : $match[0];
|
||||
},
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses out the all the tokens enclosed in braces {} and subparses on the colon : character where supplied
|
||||
*/
|
||||
public function scan(string $text): array
|
||||
{
|
||||
// Matches tokens with the following pattern: [$token:$length]
|
||||
preg_match_all('/
|
||||
\{ # [ - pattern start
|
||||
([^\s\{\}:]+) # match $token not containing whitespace : { or }
|
||||
@@ -166,6 +69,12 @@ class Token_lib
|
||||
return $token_tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $quantity
|
||||
* @param string|null $price
|
||||
* @param string|null $item_id_or_number_or_item_kit_or_receipt
|
||||
* @return void
|
||||
*/
|
||||
public function parse_barcode(?string &$quantity, ?string &$price, ?string &$item_id_or_number_or_item_kit_or_receipt): void
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
@@ -181,11 +90,17 @@ class Token_lib
|
||||
$price = (isset($parsed_results['P'])) ? (double) $parsed_results['P'] : null;
|
||||
}
|
||||
} else {
|
||||
$quantity = 1;
|
||||
$quantity = 1; // TODO: Quantity is handled using bcmath functions so that it is precision safe. This should be '1'
|
||||
}
|
||||
}
|
||||
|
||||
public function parse(string $string, string $pattern, array $tokens = []): array
|
||||
/**
|
||||
* @param string $string
|
||||
* @param string $pattern
|
||||
* @param array $tokens
|
||||
* @return array
|
||||
*/
|
||||
public function parse(string $string, string $pattern, array $tokens = []): array // TODO: $string is a poor name for this parameter.
|
||||
{
|
||||
$token_tree = $this->scan($pattern);
|
||||
|
||||
@@ -214,10 +129,19 @@ class Token_lib
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function generate(array $used_tokens, array $tokens, array &$tokens_to_replace, array &$token_values, bool $save = true): void
|
||||
/**
|
||||
* @param array $used_tokens
|
||||
* @param array $tokens_to_replace
|
||||
* @param array $token_values
|
||||
* @param array $tokens
|
||||
* @param bool $save
|
||||
* @return array
|
||||
*/
|
||||
public function generate(array $used_tokens, array &$tokens_to_replace, array &$token_values, array $tokens, bool $save = true): array // TODO: $tokens
|
||||
{
|
||||
foreach ($used_tokens as $token_code => $token_info) {
|
||||
$token_value = $this->resolve_token($token_code, $tokens, $save);
|
||||
// Generate value here based on the key value
|
||||
$token_value = $this->resolve_token($token_code, [], $save);
|
||||
|
||||
foreach ($token_info as $length => $token_spec) {
|
||||
$tokens_to_replace[] = $token_spec;
|
||||
@@ -228,8 +152,16 @@ class Token_lib
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $token_values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $token_code
|
||||
* @param array $tokens
|
||||
* @param bool $save
|
||||
* @return string
|
||||
*/
|
||||
private function resolve_token($token_code, array $tokens = [], bool $save = true): string
|
||||
{
|
||||
foreach (array_merge($tokens, Token::get_tokens()) as $token) {
|
||||
@@ -240,4 +172,4 @@ class Token_lib
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,10 +262,9 @@ class Attribute extends Model
|
||||
|
||||
/**
|
||||
* @param int $definition_flags
|
||||
* @param bool $include_types If true, returns array with definition_id => ['name' => name, 'type' => type]
|
||||
* @return array
|
||||
*/
|
||||
public function get_definitions_by_flags(int $definition_flags, bool $include_types = false): array
|
||||
public function get_definitions_by_flags(int $definition_flags): array
|
||||
{
|
||||
$builder = $this->db->table('attribute_definitions');
|
||||
$builder->where(new RawSql("definition_flags & $definition_flags")); // TODO: we need to heed CI warnings to escape properly
|
||||
@@ -275,17 +274,6 @@ class Attribute extends Model
|
||||
|
||||
$results = $builder->get()->getResultArray();
|
||||
|
||||
if ($include_types) {
|
||||
$definitions = [];
|
||||
foreach ($results as $result) {
|
||||
$definitions[$result['definition_id']] = [
|
||||
'name' => $result['definition_name'],
|
||||
'type' => $result['definition_type']
|
||||
];
|
||||
}
|
||||
return $definitions;
|
||||
}
|
||||
|
||||
return $this->to_array($results, 'definition_id', 'definition_name');
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,6 @@ use stdClass;
|
||||
*/
|
||||
class Item extends Model
|
||||
{
|
||||
|
||||
public const ALLOWED_SUGGESTIONS_COLUMNS = ['name', 'item_number', 'description', 'cost_price', 'unit_price'];
|
||||
public const ALLOWED_SUGGESTIONS_COLUMNS_WITH_EMPTY = ['', 'name', 'item_number', 'description', 'cost_price', 'unit_price'];
|
||||
|
||||
public const ALLOWED_BULK_EDIT_FIELDS = [
|
||||
'name',
|
||||
'category',
|
||||
@@ -31,6 +27,7 @@ class Item extends Model
|
||||
'allow_alt_description',
|
||||
'is_serialized'
|
||||
];
|
||||
|
||||
protected $table = 'items';
|
||||
protected $primaryKey = 'item_id';
|
||||
protected $useAutoIncrement = true;
|
||||
@@ -547,17 +544,13 @@ 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']
|
||||
: 'name';
|
||||
$seed .= ',' . $suggestionsFirstColumn;
|
||||
$seed .= ',' . $config['suggestions_first_column'];
|
||||
|
||||
if ($config['suggestions_second_column'] !== '' && $this->suggestionColumnIsAllowed($config['suggestions_second_column'])) {
|
||||
if ($config['suggestions_second_column'] !== '') {
|
||||
$seed .= ',' . $config['suggestions_second_column'];
|
||||
}
|
||||
|
||||
if ($config['suggestions_third_column'] !== '' && $this->suggestionColumnIsAllowed($config['suggestions_third_column'])) {
|
||||
if ($config['suggestions_third_column'] !== '') {
|
||||
$seed .= ',' . $config['suggestions_third_column'];
|
||||
}
|
||||
|
||||
@@ -573,15 +566,9 @@ class Item extends Model
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
$label = '';
|
||||
$label1 = $this->suggestionColumnIsAllowed($config['suggestions_first_column'])
|
||||
? $config['suggestions_first_column']
|
||||
: 'name';
|
||||
$label2 = $this->suggestionColumnIsAllowed($config['suggestions_second_column'])
|
||||
? $config['suggestions_second_column']
|
||||
: '';
|
||||
$label3 = $this->suggestionColumnIsAllowed($config['suggestions_third_column'])
|
||||
? $config['suggestions_third_column']
|
||||
: '';
|
||||
$label1 = $config['suggestions_first_column'];
|
||||
$label2 = $config['suggestions_second_column'];
|
||||
$label3 = $config['suggestions_third_column'];
|
||||
|
||||
$this->format_result_numbers($result_row);
|
||||
|
||||
@@ -605,17 +592,6 @@ class Item extends Model
|
||||
return $label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a column name is in the allowed suggestions columns.
|
||||
*
|
||||
* @param string $columnName
|
||||
* @return bool
|
||||
*/
|
||||
private function suggestionColumnIsAllowed(string $columnName): bool
|
||||
{
|
||||
return in_array($columnName, self::ALLOWED_SUGGESTIONS_COLUMNS, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts decimal money values to their correct locale format.
|
||||
*
|
||||
|
||||
107
app/Models/PluginConfig.php
Normal file
107
app/Models/PluginConfig.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PluginConfig extends Model
|
||||
{
|
||||
protected $table = 'plugin_config';
|
||||
protected $primaryKey = 'key';
|
||||
protected $useAutoIncrement = false;
|
||||
protected $useSoftDeletes = false;
|
||||
protected $allowedFields = [
|
||||
'key',
|
||||
'value'
|
||||
];
|
||||
|
||||
public function exists(string $key): bool
|
||||
{
|
||||
$builder = $this->db->table('plugin_config');
|
||||
$builder->where('key', $key);
|
||||
|
||||
return ($builder->get()->getNumRows() === 1);
|
||||
}
|
||||
|
||||
public function getValue(string $key): ?string
|
||||
{
|
||||
$builder = $this->db->table('plugin_config');
|
||||
$query = $builder->getWhere(['key' => $key], 1);
|
||||
|
||||
if ($query->getNumRows() === 1) {
|
||||
return $query->getRow()->value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function setValue(string $key, string $value): bool
|
||||
{
|
||||
$builder = $this->db->table('plugin_config');
|
||||
|
||||
if ($this->exists($key)) {
|
||||
return $builder->update(['value' => $value], ['key' => $key]);
|
||||
}
|
||||
|
||||
return $builder->insert(['key' => $key, 'value' => $value]);
|
||||
}
|
||||
|
||||
public function getPluginSettings(string $pluginId): array
|
||||
{
|
||||
$builder = $this->db->table('plugin_config');
|
||||
$builder->like('key', $pluginId . '_', 'after');
|
||||
$query = $builder->get();
|
||||
|
||||
$settings = [];
|
||||
$prefix = $pluginId . '_';
|
||||
foreach ($query->getResult() as $row) {
|
||||
$key = str_starts_with($row->key, $prefix)
|
||||
? substr($row->key, strlen($prefix))
|
||||
: $row->key;
|
||||
$settings[$key] = $row->value;
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public function deleteKey(string $key): bool
|
||||
{
|
||||
$builder = $this->db->table('plugin_config');
|
||||
return $builder->delete(['key' => $key]);
|
||||
}
|
||||
|
||||
public function deleteAllStartingWith(string $prefix): bool
|
||||
{
|
||||
$builder = $this->db->table('plugin_config');
|
||||
$builder->like('key', $prefix, 'after');
|
||||
return $builder->delete();
|
||||
}
|
||||
|
||||
public function batchSave(array $data): bool
|
||||
{
|
||||
$success = true;
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$success &= $this->setValue($key, $value);
|
||||
}
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
return $success && $this->db->transStatus();
|
||||
}
|
||||
|
||||
public function getAll(): array
|
||||
{
|
||||
$builder = $this->db->table('plugin_config');
|
||||
$query = $builder->get();
|
||||
|
||||
$configs = [];
|
||||
foreach ($query->getResult() as $row) {
|
||||
$configs[$row->key] = $row->value;
|
||||
}
|
||||
|
||||
return $configs;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,10 @@ class Summary_taxes extends Summary_report
|
||||
$this->config = config(OSPOS::class)->settings;
|
||||
}
|
||||
|
||||
protected function _get_data_columns(): array
|
||||
/**
|
||||
* @return array[]
|
||||
*/
|
||||
protected function _get_data_columns(): array // TODO: hungarian notation
|
||||
{
|
||||
return [
|
||||
['tax_name' => lang('Reports.tax_name'), 'sortable' => false],
|
||||
@@ -26,7 +29,12 @@ class Summary_taxes extends Summary_report
|
||||
];
|
||||
}
|
||||
|
||||
protected function _where(array $inputs, &$builder): void
|
||||
/**
|
||||
* @param array $inputs
|
||||
* @param $builder
|
||||
* @return void
|
||||
*/
|
||||
protected function _where(array $inputs, &$builder): void // TODO: hungarian notation
|
||||
{
|
||||
$builder->where('sales.sale_status', COMPLETED);
|
||||
|
||||
@@ -37,90 +45,51 @@ class Summary_taxes extends Summary_report
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $inputs
|
||||
* @return array
|
||||
*/
|
||||
public function getData(array $inputs): array
|
||||
{
|
||||
$decimals = totals_decimals();
|
||||
$db_prefix = $this->db->getPrefix();
|
||||
|
||||
$sale_amount = '(CASE WHEN ' . $db_prefix . 'sales_items.discount_type = ' . PERCENT
|
||||
. " THEN " . $db_prefix . "sales_items.quantity_purchased * " . $db_prefix . "sales_items.item_unit_price - ROUND(" . $db_prefix . "sales_items.quantity_purchased * " . $db_prefix . "sales_items.item_unit_price * " . $db_prefix . "sales_items.discount / 100, $decimals)"
|
||||
. ' ELSE ' . $db_prefix . 'sales_items.quantity_purchased * (' . $db_prefix . "sales_items.item_unit_price - " . $db_prefix . "sales_items.discount) END)";
|
||||
|
||||
$sale_tax = "IFNULL(" . $db_prefix . "sales_items_taxes.item_tax_amount, 0)";
|
||||
|
||||
if ($this->config['tax_included']) {
|
||||
$sale_subtotal = "ROUND($sale_amount - $sale_tax, $decimals)";
|
||||
$sale_total = '(CASE WHEN ' . $db_prefix . 'sales_items.discount_type = ' . PERCENT
|
||||
. " THEN " . $db_prefix . "sales_items.quantity_purchased * " . $db_prefix . "sales_items.item_unit_price - ROUND(" . $db_prefix . "sales_items.quantity_purchased * " . $db_prefix . "sales_items.item_unit_price * " . $db_prefix . "sales_items.discount / 100, $decimals)"
|
||||
. ' ELSE ' . $db_prefix . 'sales_items.quantity_purchased * (' . $db_prefix . 'sales_items.item_unit_price - ' . $db_prefix . 'sales_items.discount) END)';
|
||||
|
||||
$sale_subtotal = '(CASE WHEN ' . $db_prefix . 'sales_items.discount_type = ' . PERCENT
|
||||
. " THEN " . $db_prefix . "sales_items.quantity_purchased * " . $db_prefix . "sales_items.item_unit_price - ROUND(" . $db_prefix . "sales_items.quantity_purchased * " . $db_prefix . "sales_items.item_unit_price * " . $db_prefix . "sales_items.discount / 100, $decimals) "
|
||||
. 'ELSE ' . $db_prefix . 'sales_items.quantity_purchased * ' . $db_prefix . 'sales_items.item_unit_price - ' . $db_prefix . 'sales_items.discount END * (100 / (100 + ' . $db_prefix . 'sales_items_taxes.percent)))';
|
||||
} else {
|
||||
$sale_subtotal = "ROUND($sale_amount, $decimals)";
|
||||
$sale_total = '(CASE WHEN ' . $db_prefix . 'sales_items.discount_type = ' . PERCENT
|
||||
. " THEN " . $db_prefix . "sales_items.quantity_purchased * " . $db_prefix . "sales_items.item_unit_price - ROUND(" . $db_prefix . "sales_items.quantity_purchased * " . $db_prefix . "sales_items.item_unit_price * " . $db_prefix . "sales_items.discount / 100, $decimals)"
|
||||
. ' ELSE ' . $db_prefix . 'sales_items.quantity_purchased * ' . $db_prefix . 'sales_items.item_unit_price - ' . $db_prefix . 'sales_items.discount END * (1 + (' . $db_prefix . 'sales_items_taxes.percent / 100)))';
|
||||
|
||||
$sale_subtotal = '(CASE WHEN ' . $db_prefix . 'sales_items.discount_type = ' . PERCENT
|
||||
. " THEN " . $db_prefix . "sales_items.quantity_purchased * " . $db_prefix . "sales_items.item_unit_price - ROUND(" . $db_prefix . "sales_items.quantity_purchased * " . $db_prefix . "sales_items.item_unit_price * " . $db_prefix . "sales_items.discount / 100, $decimals)"
|
||||
. ' ELSE ' . $db_prefix . 'sales_items.quantity_purchased * (' . $db_prefix . 'sales_items.item_unit_price - ' . $db_prefix . 'sales_items.discount) END)';
|
||||
}
|
||||
$sale_tax_rounded = "ROUND($sale_tax, $decimals)";
|
||||
$sale_total = "($sale_subtotal + $sale_tax_rounded)";
|
||||
|
||||
$subquery_builder = $this->db->table('sales_items');
|
||||
$subquery_builder->select(
|
||||
"name AS name, "
|
||||
. "CONCAT(IFNULL(ROUND(percent, $decimals), 0), '%') AS percent, "
|
||||
. "sales.sale_id AS sale_id, "
|
||||
. "$sale_subtotal AS subtotal, "
|
||||
. "$sale_tax_rounded AS tax, "
|
||||
. "$sale_total AS total"
|
||||
);
|
||||
$subquery_builder->select("name AS name, CONCAT(IFNULL(ROUND(percent, $decimals), 0), '%') AS percent, sales.sale_id AS sale_id, $sale_subtotal AS subtotal, IFNULL($db_prefix" . "sales_items_taxes.item_tax_amount, 0) AS tax, IFNULL($sale_total, $sale_subtotal) AS total");
|
||||
|
||||
$subquery_builder->join('sales', 'sales_items.sale_id = sales.sale_id', 'inner');
|
||||
$subquery_builder->join(
|
||||
'sales_items_taxes',
|
||||
'sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.item_id = sales_items_taxes.item_id AND sales_items.line = sales_items_taxes.line',
|
||||
'left outer'
|
||||
);
|
||||
$subquery_builder->join('sales_items_taxes', 'sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.item_id = sales_items_taxes.item_id AND sales_items.line = sales_items_taxes.line', 'left outer');
|
||||
|
||||
$subquery_builder->where('sale_status', COMPLETED);
|
||||
|
||||
if (empty($this->config['date_or_time_format'])) {
|
||||
$subquery_builder->where(
|
||||
'DATE(' . $db_prefix . 'sales.sale_time) BETWEEN ' . $this->db->escape($inputs['start_date'])
|
||||
. ' AND ' . $this->db->escape($inputs['end_date'])
|
||||
);
|
||||
$subquery_builder->where('DATE(' . $db_prefix . 'sales.sale_time) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']));
|
||||
} else {
|
||||
$subquery_builder->where(
|
||||
'sales.sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date']))
|
||||
. ' AND ' . $this->db->escape(rawurldecode($inputs['end_date']))
|
||||
);
|
||||
$subquery_builder->where('sales.sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));
|
||||
}
|
||||
|
||||
$builder = $this->db->newQuery()->fromSubquery($subquery_builder, 'temp_taxes');
|
||||
$builder->select(
|
||||
"name, percent, COUNT(DISTINCT sale_id) AS count, "
|
||||
. "ROUND(SUM(subtotal), $decimals) AS subtotal, "
|
||||
. "ROUND(SUM(tax), $decimals) AS tax, "
|
||||
. "ROUND(SUM(total), $decimals) AS total"
|
||||
);
|
||||
$builder->select("name, percent, COUNT(DISTINCT sale_id) AS count, ROUND(SUM(subtotal), $decimals) AS subtotal, ROUND(SUM(tax), $decimals) AS tax, ROUND(SUM(total), $decimals) total");
|
||||
$builder->groupBy('percent, name');
|
||||
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
public function getSummaryData(array $inputs): array
|
||||
{
|
||||
$decimals = totals_decimals();
|
||||
$data = $this->getData($inputs);
|
||||
|
||||
$subtotal = 0;
|
||||
$tax = 0;
|
||||
$total = 0;
|
||||
$count = 0;
|
||||
|
||||
foreach ($data as $row) {
|
||||
$subtotal += (float) $row['subtotal'];
|
||||
$tax += (float) $row['tax'];
|
||||
$total += (float) $row['total'];
|
||||
$count += (int) $row['count'];
|
||||
}
|
||||
|
||||
return [
|
||||
'subtotal' => round($subtotal, $decimals),
|
||||
'tax' => round($tax, $decimals),
|
||||
'total' => round($total, $decimals),
|
||||
'count' => $count
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,10 +273,6 @@ class Sale extends Model
|
||||
$builder->like('payment_type', lang('Sales.credit'));
|
||||
}
|
||||
|
||||
if ($filters['only_debit']) {
|
||||
$builder->like('payment_type', lang('Sales.debit'));
|
||||
}
|
||||
|
||||
$builder->groupBy('payment_type');
|
||||
|
||||
$payments = $builder->get()->getResultArray();
|
||||
@@ -1498,10 +1494,6 @@ class Sale extends Model
|
||||
$builder->like('payments.payment_type', lang('Sales.credit'));
|
||||
}
|
||||
|
||||
if ($filters['only_debit']) {
|
||||
$builder->like('payments.payment_type', lang('Sales.debit'));
|
||||
}
|
||||
|
||||
if ($filters['only_due']) {
|
||||
$builder->like('payments.payment_type', lang('Sales.due'));
|
||||
}
|
||||
|
||||
193
app/Plugins/MailchimpPlugin.php
Normal file
193
app/Plugins/MailchimpPlugin.php
Normal file
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
namespace App\Plugins;
|
||||
|
||||
use App\Libraries\Plugins\BasePlugin;
|
||||
use App\Libraries\Mailchimp_lib;
|
||||
use CodeIgniter\Events\Events;
|
||||
|
||||
/**
|
||||
* Plugin that integrates OSPOS with Mailchimp for customer newsletter subscriptions.
|
||||
*/
|
||||
class MailchimpPlugin extends BasePlugin
|
||||
{
|
||||
private ?Mailchimp_lib $mailchimpLib = null;
|
||||
|
||||
public function getPluginId(): string
|
||||
{
|
||||
return 'mailchimp';
|
||||
}
|
||||
|
||||
public function getPluginName(): string
|
||||
{
|
||||
return 'Mailchimp';
|
||||
}
|
||||
|
||||
public function getPluginDescription(): string
|
||||
{
|
||||
return $this->lang('mailchimp_description');
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
public function registerEvents(): void
|
||||
{
|
||||
Events::on('customer_saved', [$this, 'onCustomerSaved']);
|
||||
Events::on('customer_deleted', [$this, 'onCustomerDeleted']);
|
||||
|
||||
log_message('debug', 'Mailchimp plugin events registered');
|
||||
}
|
||||
|
||||
public function install(): bool
|
||||
{
|
||||
log_message('info', 'Installing Mailchimp plugin');
|
||||
|
||||
$this->setSetting('api_key', '');
|
||||
$this->setSetting('list_id', '');
|
||||
$this->setSetting('sync_on_save', '1');
|
||||
$this->setSetting('enabled', '0');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function uninstall(): bool
|
||||
{
|
||||
log_message('info', 'Uninstalling Mailchimp plugin');
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getConfigView(): ?string
|
||||
{
|
||||
return 'Plugins/mailchimp/config';
|
||||
}
|
||||
|
||||
public function getSettings(): array
|
||||
{
|
||||
return [
|
||||
'api_key' => $this->getSetting('api_key', ''),
|
||||
'list_id' => $this->getSetting('list_id', ''),
|
||||
'sync_on_save' => $this->getSetting('sync_on_save', '1'),
|
||||
'enabled' => $this->getSetting('enabled', '0'),
|
||||
];
|
||||
}
|
||||
|
||||
public function saveSettings(array $settings): bool
|
||||
{
|
||||
if (isset($settings['api_key'])) {
|
||||
$this->setSetting('api_key', $settings['api_key']);
|
||||
}
|
||||
|
||||
if (isset($settings['list_id'])) {
|
||||
$this->setSetting('list_id', $settings['list_id']);
|
||||
}
|
||||
|
||||
if (isset($settings['sync_on_save'])) {
|
||||
$this->setSetting('sync_on_save', $settings['sync_on_save'] ? '1' : '0');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function onCustomerSaved(array $customerData): void
|
||||
{
|
||||
if (!$this->shouldSyncOnSave()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log_message('debug', "Customer saved event received for ID: {$customerData['person_id']}");
|
||||
|
||||
try {
|
||||
$this->subscribeCustomer($customerData);
|
||||
} catch (\Exception $e) {
|
||||
log_message('error', "Failed to sync customer to Mailchimp: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
public function onCustomerDeleted(int $customerId): void
|
||||
{
|
||||
log_message('debug', "Customer deleted event received for ID: {$customerId}");
|
||||
}
|
||||
|
||||
private function subscribeCustomer(array $customerData): bool
|
||||
{
|
||||
$apiKey = $this->getSetting('api_key');
|
||||
$listId = $this->getSetting('list_id');
|
||||
|
||||
if (empty($apiKey) || empty($listId)) {
|
||||
log_message('warning', 'Mailchimp API key or List ID not configured');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($customerData['email'])) {
|
||||
log_message('debug', 'Customer has no email, skipping Mailchimp sync');
|
||||
return false;
|
||||
}
|
||||
|
||||
$mailchimp = $this->getMailchimpLib(['api_key' => $apiKey]);
|
||||
|
||||
$result = $mailchimp->addOrUpdateMember(
|
||||
$listId,
|
||||
$customerData['email'],
|
||||
$customerData['first_name'] ?? '',
|
||||
$customerData['last_name'] ?? '',
|
||||
'subscribed'
|
||||
);
|
||||
|
||||
if ($result) {
|
||||
log_message('info', "Successfully subscribed customer ID {$customerData['person_id']} to Mailchimp");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function shouldSyncOnSave(): bool
|
||||
{
|
||||
return $this->getSetting('sync_on_save', '1') === '1';
|
||||
}
|
||||
|
||||
private function getMailchimpLib(array $params = []): Mailchimp_lib
|
||||
{
|
||||
if ($this->mailchimpLib === null) {
|
||||
$this->mailchimpLib = new Mailchimp_lib($params);
|
||||
}
|
||||
return $this->mailchimpLib;
|
||||
}
|
||||
|
||||
public function testConnection(): array
|
||||
{
|
||||
$apiKey = $this->getSetting('api_key');
|
||||
|
||||
if (empty($apiKey)) {
|
||||
return ['success' => false, 'message' => $this->lang('mailchimp_api_key_required')];
|
||||
}
|
||||
|
||||
$mailchimp = $this->getMailchimpLib(['api_key' => $apiKey]);
|
||||
$result = $mailchimp->getLists();
|
||||
|
||||
if ($result && isset($result['lists'])) {
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $this->lang('mailchimp_key_successfully'),
|
||||
'lists' => $result['lists']
|
||||
];
|
||||
}
|
||||
|
||||
return ['success' => false, 'message' => $this->lang('mailchimp_key_unsuccessfully')];
|
||||
}
|
||||
|
||||
protected function lang(string $key, array $data = []): string
|
||||
{
|
||||
$language = \Config\Services::language();
|
||||
$language->addLanguagePath(APPPATH . 'Plugins/MailchimpPlugin/Language/');
|
||||
return $language->getLine($key, $data);
|
||||
}
|
||||
|
||||
protected function getPluginDir(): string
|
||||
{
|
||||
return 'MailchimpPlugin';
|
||||
}
|
||||
}
|
||||
13
app/Plugins/MailchimpPlugin/Language/en/MailchimpPlugin.php
Normal file
13
app/Plugins/MailchimpPlugin/Language/en/MailchimpPlugin.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'mailchimp' => 'Mailchimp',
|
||||
'mailchimp_description' => 'Integrate with Mailchimp to sync customers to mailing lists when they are created or updated.',
|
||||
'mailchimp_api_key' => 'Mailchimp API Key',
|
||||
'mailchimp_api_key_required' => 'API key not configured',
|
||||
'mailchimp_configuration' => 'Mailchimp Configuration',
|
||||
'mailchimp_key_successfully' => 'API Key is valid.',
|
||||
'mailchimp_key_unsuccessfully' => 'API Key is invalid.',
|
||||
'mailchimp_lists' => 'Mailchimp List(s)',
|
||||
'mailchimp_tooltip' => 'Click the icon for an API Key.',
|
||||
];
|
||||
528
app/Plugins/README.md
Normal file
528
app/Plugins/README.md
Normal file
@@ -0,0 +1,528 @@
|
||||
# OSPOS Plugin System
|
||||
|
||||
## Overview
|
||||
|
||||
The OSPOS Plugin System allows third-party integrations to extend the application's functionality without modifying core code. Plugins can listen to events, add configuration settings, and integrate with external services.
|
||||
|
||||
## Installation
|
||||
|
||||
### Self-Contained Plugin Packages
|
||||
|
||||
Plugins are self-contained packages that can be installed by simply dropping the plugin folder into `app/Plugins/`:
|
||||
|
||||
```
|
||||
app/Plugins/
|
||||
├── MailchimpPlugin/ # Plugin directory (self-contained)
|
||||
│ ├── MailchimpPlugin.php # Main plugin class (required - must match directory name)
|
||||
│ ├── Language/ # Plugin-specific translations (self-contained)
|
||||
│ │ ├── en/
|
||||
│ │ │ └── MailchimpPlugin.php
|
||||
│ │ └── es-ES/
|
||||
│ │ └── MailchimpPlugin.php
|
||||
│ └── Views/ # Plugin-specific views
|
||||
│ └── config.php
|
||||
```
|
||||
|
||||
### Installation Steps
|
||||
|
||||
1. **Download the plugin** - Copy the plugin folder/file to `app/Plugins/`
|
||||
2. **Auto-discovery** - The plugin will be automatically discovered on next page load
|
||||
3. **Enable** - Enable it from the admin interface (Plugins menu)
|
||||
4. **Configure** - Configure plugin settings if needed
|
||||
|
||||
### Plugin Discovery
|
||||
|
||||
The PluginManager recursively scans `app/Plugins/` directory:
|
||||
|
||||
- **Single-file plugins**: `app/Plugins/MyPlugin.php` with namespace `App\Plugins\MyPlugin`
|
||||
- **Directory plugins**: `app/Plugins/MyPlugin/MyPlugin.php` with namespace `App\Plugins\MyPlugin\MyPlugin`
|
||||
|
||||
Both formats are supported, but directory plugins allow for self-contained packages with their own components.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Plugin Interface
|
||||
|
||||
All plugins must implement `App\Libraries\Plugins\PluginInterface`:
|
||||
|
||||
```php
|
||||
interface PluginInterface
|
||||
{
|
||||
public function getPluginId(): string; // Unique identifier
|
||||
public function getPluginName(): string; // Display name
|
||||
public function getPluginDescription(): string;
|
||||
public function getVersion(): string;
|
||||
public function registerEvents(): void; // Register event listeners
|
||||
public function install(): bool; // First-time setup
|
||||
public function uninstall(): bool; // Cleanup
|
||||
public function isEnabled(): bool;
|
||||
public function getConfigView(): ?string; // Configuration view path
|
||||
public function getSettings(): array;
|
||||
public function saveSettings(array $settings): bool;
|
||||
}
|
||||
```
|
||||
|
||||
### Base Plugin Class
|
||||
|
||||
Extend `App\Libraries\Plugins\BasePlugin` for common functionality:
|
||||
|
||||
```php
|
||||
class MyPlugin extends BasePlugin
|
||||
{
|
||||
public function getPluginId(): string { return 'my_plugin'; }
|
||||
public function getPluginName(): string { return 'My Plugin'; }
|
||||
// ... implement other methods
|
||||
}
|
||||
```
|
||||
|
||||
### Plugin Manager
|
||||
|
||||
The `PluginManager` class handles:
|
||||
- Plugin discovery from `app/Plugins/` directory (recursive scan)
|
||||
- Loading and registering enabled plugins
|
||||
- Managing plugin settings
|
||||
|
||||
**Important:** The PluginManager only calls `registerEvents()` for enabled plugins. Disabled plugins never have their event callbacks registered with `Events::on()`. This means **you do not need to check `$this->isEnabled()` in your callback methods** - if the callback is registered, the plugin is enabled.
|
||||
|
||||
## Available Events
|
||||
|
||||
OSPOS fires these events that plugins can listen to:
|
||||
|
||||
| Event | Arguments | Description |
|
||||
|-------|-----------|-------------|
|
||||
| `item_sale` | `array $saleData` | Fired when a sale is completed |
|
||||
| `item_return` | `array $returnData` | Fired when a return is processed |
|
||||
| `item_change` | `int $itemId` | Fired when an item is created/updated/deleted |
|
||||
| `item_inventory` | `array $inventoryData` | Fired on inventory changes |
|
||||
| `items_csv_import` | `array $importData` | Fired after items CSV import |
|
||||
| `customers_csv_import` | `array $importData` | Fired after customers CSV import |
|
||||
|
||||
## View Hooks (Injecting Plugin Content into Views)
|
||||
|
||||
Plugins can inject UI elements into core views using the event-based view hook system. This allows plugins to add buttons, tabs, or other content without modifying core view files.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Core views define hook points** using the `plugin_content()` helper
|
||||
2. **Plugins register listeners** for these view hooks in `registerEvents()`
|
||||
3. **Content is rendered** only when the plugin is enabled
|
||||
|
||||
### Step 1: Adding Hook Points in Core Views
|
||||
|
||||
In your core view files, use the `plugin_content()` helper to define injection points:
|
||||
|
||||
```php
|
||||
// In app/Views/sales/receipt.php
|
||||
<div class="receipt-actions">
|
||||
<!-- Existing buttons -->
|
||||
<?= plugin_content('receipt_actions', ['sale' => $sale]) ?>
|
||||
</div>
|
||||
|
||||
// In app/Views/customers/form.php
|
||||
<ul class="nav nav-tabs">
|
||||
<!-- Existing tabs -->
|
||||
<?= plugin_content('customer_tabs', ['customer' => $customer]) ?>
|
||||
</ul>
|
||||
```
|
||||
|
||||
### Step 2: Plugin Registers View Hook
|
||||
|
||||
In your plugin class, register a listener that returns HTML content:
|
||||
|
||||
```php
|
||||
class MailchimpPlugin extends BasePlugin
|
||||
{
|
||||
public function registerEvents(): void
|
||||
{
|
||||
Events::on('customer_saved', [$this, 'onCustomerSaved']);
|
||||
|
||||
// View hooks - inject content into core views
|
||||
Events::on('view:customer_tabs', [$this, 'injectCustomerTab']);
|
||||
}
|
||||
|
||||
public function injectCustomerTab(array $data): string
|
||||
{
|
||||
return view('Plugins/MailchimpPlugin/Views/customer_tab', $data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Plugin View Files
|
||||
|
||||
The plugin's view files are self-contained within the plugin directory:
|
||||
|
||||
```php
|
||||
// app/Plugins/MailchimpPlugin/Views/customer_tab.php
|
||||
<li>
|
||||
<a href="#mailchimp_panel" data-toggle="tab">
|
||||
<span class="glyphicon glyphicon-envelope"> </span>
|
||||
Mailchimp
|
||||
</a>
|
||||
</li>
|
||||
```
|
||||
|
||||
### Helper Functions
|
||||
|
||||
The `plugin_helper.php` provides two functions:
|
||||
|
||||
```php
|
||||
// Render plugin content for a hook point
|
||||
plugin_content(string $section, array $data = []): string
|
||||
|
||||
// Check if any plugin has registered for a hook (for conditional rendering)
|
||||
plugin_content_exists(string $section): bool
|
||||
```
|
||||
|
||||
### Standard Hook Points
|
||||
|
||||
Core views should define these standard hook points:
|
||||
|
||||
| Hook Name | Location | Usage |
|
||||
|-----------|----------|-------|
|
||||
| `view:receipt_actions` | Receipt view action buttons | Add receipt-related buttons |
|
||||
| `view:customer_tabs` | Customer form tabs | Add customer-related tabs |
|
||||
| `view:item_form_buttons` | Item form action buttons | Add item-related buttons |
|
||||
| `view:sales_complete` | Sale complete screen | Post-sale integration UI |
|
||||
| `view:reports_menu` | Reports menu | Add custom report links |
|
||||
|
||||
### Benefits
|
||||
|
||||
- **Self-Contained**: Plugin UI stays in plugin directory
|
||||
- **Conditional**: Only renders when plugin is enabled
|
||||
- **Data Access**: Pass context (sale, customer, etc.) to plugin views
|
||||
- **Multiple Plugins**: Multiple plugins can hook the same location
|
||||
- **Clean Separation**: Core views remain unmodified
|
||||
|
||||
## Creating a Plugin
|
||||
|
||||
### Simple Plugin (Single File)
|
||||
|
||||
For plugins that only need to listen to events without complex UI or database tables:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// app/Plugins/MyPlugin.php
|
||||
|
||||
namespace App\Plugins;
|
||||
|
||||
use App\Libraries\Plugins\BasePlugin;
|
||||
use CodeIgniter\Events\Events;
|
||||
|
||||
class MyPlugin extends BasePlugin
|
||||
{
|
||||
public function getPluginId(): string
|
||||
{
|
||||
return 'my_plugin';
|
||||
}
|
||||
|
||||
public function getPluginName(): string
|
||||
{
|
||||
return 'My Integration Plugin';
|
||||
}
|
||||
|
||||
public function getPluginDescription(): string
|
||||
{
|
||||
return 'Integrates OSPOS with external service';
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
public function registerEvents(): void
|
||||
{
|
||||
Events::on('item_sale', [$this, 'onItemSale']);
|
||||
Events::on('item_change', [$this, 'onItemChange']);
|
||||
}
|
||||
|
||||
public function onItemSale(array $saleData): void
|
||||
{
|
||||
log_message('info', "Processing sale: {$saleData['sale_id_num']}");
|
||||
}
|
||||
|
||||
public function onItemChange(int $itemId): void
|
||||
{
|
||||
log_message('info', "Item changed: {$itemId}");
|
||||
}
|
||||
|
||||
public function install(): bool
|
||||
{
|
||||
$this->setSetting('api_key', '');
|
||||
$this->setSetting('enabled', '0');
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getConfigView(): ?string
|
||||
{
|
||||
return 'Plugins/my_plugin/config';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complex Plugin (Self-Contained Directory)
|
||||
|
||||
For plugins that need database tables, controllers, models, and views:
|
||||
|
||||
```
|
||||
app/Plugins/
|
||||
└── MailchimpPlugin/ # Plugin directory
|
||||
├── MailchimpPlugin.php # Main class - namespace: App\Plugins\MailchimpPlugin\MailchimpPlugin
|
||||
├── Models/ # Plugin models
|
||||
│ └── MailchimpData.php
|
||||
├── Controllers/ # Plugin controllers
|
||||
│ └── Dashboard.php
|
||||
├── Views/ # Plugin views
|
||||
│ ├── config.php
|
||||
│ └── dashboard.php
|
||||
├── Language/ # Plugin translations (self-contained)
|
||||
│ ├── en/
|
||||
│ │ └── MailchimpPlugin.php
|
||||
│ └── es-ES/
|
||||
│ └── MailchimpPlugin.php
|
||||
└── Libraries/ # Plugin libraries
|
||||
└── ApiClient.php
|
||||
```
|
||||
|
||||
**Main Plugin Class:**
|
||||
|
||||
```php
|
||||
<?php
|
||||
// app/Plugins/MailchimpPlugin/MailchimpPlugin.php
|
||||
|
||||
namespace App\Plugins\MailchimpPlugin;
|
||||
|
||||
use App\Libraries\Plugins\BasePlugin;
|
||||
use App\Plugins\MailchimpPlugin\Models\MailchimpData;
|
||||
use CodeIgniter\Events\Events;
|
||||
|
||||
class MailchimpPlugin extends BasePlugin
|
||||
{
|
||||
private ?MailchimpData $dataModel = null;
|
||||
|
||||
public function getPluginId(): string
|
||||
{
|
||||
return 'mailchimp';
|
||||
}
|
||||
|
||||
public function getPluginName(): string
|
||||
{
|
||||
return 'Mailchimp';
|
||||
}
|
||||
|
||||
public function getPluginDescription(): string
|
||||
{
|
||||
return 'Integrate with Mailchimp to sync customers to mailing lists.';
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
public function registerEvents(): void
|
||||
{
|
||||
Events::on('customer_saved', [$this, 'onCustomerSaved']);
|
||||
Events::on('customer_deleted', [$this, 'onCustomerDeleted']);
|
||||
}
|
||||
|
||||
private function getDataModel(): MailchimpData
|
||||
{
|
||||
if ($this->dataModel === null) {
|
||||
$this->dataModel = new MailchimpData();
|
||||
}
|
||||
return $this->dataModel;
|
||||
}
|
||||
|
||||
public function onCustomerSaved(array $customerData): void
|
||||
{
|
||||
if (!$this->shouldSyncOnSave()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->getDataModel()->syncCustomer($customerData);
|
||||
}
|
||||
|
||||
public function install(): bool
|
||||
{
|
||||
$this->setSetting('api_key', '');
|
||||
$this->setSetting('list_id', '');
|
||||
$this->setSetting('sync_on_save', '1');
|
||||
return true;
|
||||
}
|
||||
|
||||
public function uninstall(): bool
|
||||
{
|
||||
$this->getDataModel()->dropTable();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getConfigView(): ?string
|
||||
{
|
||||
return 'Plugins/MailchimpPlugin/Views/config';
|
||||
}
|
||||
|
||||
protected function lang(string $key, array $data = []): string
|
||||
{
|
||||
$language = \Config\Services::language();
|
||||
$language->addLanguagePath(APPPATH . 'Plugins/MailchimpPlugin/Language/');
|
||||
return $language->getLine($key, $data);
|
||||
}
|
||||
|
||||
protected function getPluginDir(): string
|
||||
{
|
||||
return 'MailchimpPlugin';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Internationalization (Language Files)
|
||||
|
||||
Plugins can include their own language files, making them completely self-contained. This allows plugins to provide translations without modifying core language files.
|
||||
|
||||
### Plugin Language Directory Structure
|
||||
|
||||
```
|
||||
app/Plugins/
|
||||
└── MailchimpPlugin/
|
||||
├── MailchimpPlugin.php
|
||||
├── Language/
|
||||
│ ├── en/
|
||||
│ │ └── MailchimpPlugin.php # English translations
|
||||
│ ├── es-ES/
|
||||
│ │ └── MailchimpPlugin.php # Spanish translations
|
||||
│ └── de-DE/
|
||||
│ └── MailchimpPlugin.php # German translations
|
||||
└── Views/
|
||||
└── config.php
|
||||
```
|
||||
|
||||
### Language File Format
|
||||
|
||||
Each language file returns an array of translation strings:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// app/Plugins/MailchimpPlugin/Language/en/MailchimpPlugin.php
|
||||
|
||||
return [
|
||||
'mailchimp' => 'Mailchimp',
|
||||
'mailchimp_description' => 'Integrate with Mailchimp to sync customers to mailing lists.',
|
||||
'mailchimp_api_key' => 'Mailchimp API Key',
|
||||
'mailchimp_configuration' => 'Mailchimp Configuration',
|
||||
'mailchimp_key_successfully' => 'API Key is valid.',
|
||||
'mailchimp_key_unsuccessfully' => 'API Key is invalid.',
|
||||
];
|
||||
```
|
||||
|
||||
### Loading Language Strings in Plugins
|
||||
|
||||
The `BasePlugin` class can provide a helper method to load plugin-specific language strings:
|
||||
|
||||
```php
|
||||
protected function lang(string $key, array $data = []): string
|
||||
{
|
||||
$language = \Config\Services::language();
|
||||
$language->addLanguagePath(APPPATH . 'Plugins/' . $this->getPluginDir() . '/Language/');
|
||||
return $language->getLine($key, $data);
|
||||
}
|
||||
|
||||
protected function getPluginDir(): string
|
||||
{
|
||||
return 'MailchimpPlugin';
|
||||
}
|
||||
```
|
||||
|
||||
### Benefits of Self-Contained Language Files
|
||||
|
||||
1. **Plugin Independence**: No need to modify core language files
|
||||
2. **Easy Distribution**: Plugin zip includes all translations
|
||||
3. **Fallback Support**: Missing translations fall back to English
|
||||
4. **User Contributions**: Users can add translations to `Language/{locale}/` in the plugin directory
|
||||
|
||||
## Plugin Settings
|
||||
|
||||
Store plugin-specific settings using:
|
||||
|
||||
```php
|
||||
// Get setting
|
||||
$value = $this->getSetting('setting_key', 'default_value');
|
||||
|
||||
// Set setting
|
||||
$this->setSetting('setting_key', 'value');
|
||||
|
||||
// Get all plugin settings
|
||||
$settings = $this->getSettings();
|
||||
|
||||
// Save multiple settings
|
||||
$this->saveSettings(['key1' => 'value1', 'key2' => 'value2']);
|
||||
```
|
||||
|
||||
Settings are prefixed with the plugin ID (e.g., `mailchimp_api_key`) and stored in `ospos_plugin_config` table.
|
||||
|
||||
## Namespace Reference
|
||||
|
||||
| File Location | Namespace |
|
||||
|--------------|-----------|
|
||||
| `app/Plugins/MyPlugin.php` | `App\Plugins\MyPlugin` |
|
||||
| `app/Plugins/MailchimpPlugin/MailchimpPlugin.php` | `App\Plugins\MailchimpPlugin\MailchimpPlugin` |
|
||||
| `app/Plugins/MailchimpPlugin/Models/MailchimpData.php` | `App\Plugins\MailchimpPlugin\Models\MailchimpData` |
|
||||
| `app/Plugins/MailchimpPlugin/Controllers/Dashboard.php` | `App\Plugins\MailchimpPlugin\Controllers\Dashboard` |
|
||||
| `app/Plugins/MailchimpPlugin/Libraries/ApiClient.php` | `App\Plugins\MailchimpPlugin\Libraries\ApiClient` |
|
||||
| `app/Plugins/MailchimpPlugin/Language/en/MailchimpPlugin.php` | *(Language file - returns array, no namespace)* |
|
||||
|
||||
## Database
|
||||
|
||||
Plugin settings are stored in the `ospos_plugin_config` table:
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `ospos_plugin_config` (
|
||||
`key` varchar(100) NOT NULL,
|
||||
`value` text NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (`key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
For custom tables, plugins can create them during `install()` and drop them during `uninstall()`.
|
||||
|
||||
## Event Flow
|
||||
|
||||
1. Application triggers event: `Events::trigger('item_sale', $data)`
|
||||
2. PluginManager recursively scans `app/Plugins/` directory
|
||||
3. Each enabled plugin registers its listeners via `registerEvents()`
|
||||
4. Events::on() callbacks are invoked automatically
|
||||
|
||||
## Testing
|
||||
|
||||
Enable plugin logging to debug:
|
||||
|
||||
```php
|
||||
log_message('debug', 'Debug message');
|
||||
log_message('info', 'Info message');
|
||||
log_message('error', 'Error message');
|
||||
```
|
||||
|
||||
Check logs in `writable/logs/`.
|
||||
|
||||
## Distributing Plugins
|
||||
|
||||
Plugin developers can package their plugins as zip files:
|
||||
|
||||
```
|
||||
MailchimpPlugin-1.0.0.zip
|
||||
└── MailchimpPlugin/
|
||||
├── MailchimpPlugin.php
|
||||
├── Models/
|
||||
├── Controllers/
|
||||
├── Views/
|
||||
├── Language/
|
||||
│ ├── en/
|
||||
│ │ └── MailchimpPlugin.php
|
||||
│ └── es-ES/
|
||||
│ └── MailchimpPlugin.php
|
||||
└── README.md # Plugin documentation
|
||||
```
|
||||
|
||||
Users extract the zip to `app/Plugins/` and the plugin is ready to use.
|
||||
@@ -23,7 +23,7 @@
|
||||
'name' => 'definition_name',
|
||||
'id' => 'definition_name',
|
||||
'class' => 'form-control input-sm',
|
||||
'value' => esc($definition_info->definition_name)
|
||||
'value' => $definition_info->definition_name
|
||||
]) ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,7 +69,7 @@
|
||||
<div class="input-group">
|
||||
<?= form_input([
|
||||
'name' => 'definition_unit',
|
||||
'value' => esc($definition_info->definition_unit),
|
||||
'value' => $definition_info->definition_unit,
|
||||
'class' => 'form-control input-sm',
|
||||
'id' => 'definition_unit'
|
||||
]) ?>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<?php foreach ($definition_values as $definition_id => $definition_value) { ?>
|
||||
|
||||
<div class="form-group form-group-sm">
|
||||
<?= form_label(esc($definition_value['definition_name']), esc($definition_value['definition_name']), ['class' => 'control-label col-xs-3']) ?>
|
||||
<?= form_label($definition_value['definition_name'], $definition_value['definition_name'], ['class' => 'control-label col-xs-3']) ?>
|
||||
<div class="col-xs-8">
|
||||
<div class="input-group">
|
||||
<?php
|
||||
@@ -55,7 +55,7 @@
|
||||
$value = (empty($attribute_value) || empty($attribute_value->attribute_value)) ? $definition_value['selected_value'] : $attribute_value->attribute_value;
|
||||
echo form_input([
|
||||
'name' => "attribute_links[$definition_id]",
|
||||
'value' => esc($value),
|
||||
'value' => $value,
|
||||
'class' => 'form-control valid_chars',
|
||||
'data-definition-id' => $definition_id
|
||||
]);
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
* @var string $controller_name
|
||||
* @var string $table_headers
|
||||
* @var array $filters
|
||||
* @var array $selected_filters
|
||||
* @var array $config
|
||||
* @var string|null $start_date
|
||||
* @var string|null $end_date
|
||||
*/
|
||||
?>
|
||||
|
||||
@@ -14,18 +11,19 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
// When any filter is clicked and the dropdown window is closed
|
||||
$('#filters').on('hidden.bs.select', function(e) {
|
||||
table_support.refresh();
|
||||
});
|
||||
|
||||
// Load the preset datarange picker
|
||||
<?= view('partial/daterangepicker') ?>
|
||||
|
||||
<?= view('partial/bootstrap_tables_locale') ?>
|
||||
$("#daterangepicker").on('apply.daterangepicker', function(ev, picker) {
|
||||
table_support.refresh();
|
||||
});
|
||||
|
||||
// Override dates from server if provided
|
||||
<?php if (isset($start_date) && $start_date): ?>
|
||||
start_date = "<?= esc($start_date) ?>";
|
||||
<?php endif; ?>
|
||||
<?php if (isset($end_date) && $end_date): ?>
|
||||
end_date = "<?= esc($end_date) ?>";
|
||||
<?php endif; ?>
|
||||
<?= view('partial/bootstrap_tables_locale') ?>
|
||||
|
||||
table_support.init({
|
||||
resource: '<?= esc($controller_name) ?>',
|
||||
@@ -42,7 +40,6 @@
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?= view('partial/table_filter_persistence') ?>
|
||||
|
||||
<?= view('partial/print_receipt', ['print_after_sale' => false, 'selected_printer' => 'takings_printer']) ?>
|
||||
|
||||
@@ -61,7 +58,7 @@
|
||||
<span class="glyphicon glyphicon-trash"> </span><?= lang('Common.delete') ?>
|
||||
</button>
|
||||
<?= form_input(['name' => 'daterangepicker', 'class' => 'form-control input-sm', 'id' => 'daterangepicker']) ?>
|
||||
<?= form_multiselect('filters[]', $filters, $selected_filters ?? [], [
|
||||
<?= form_multiselect('filters[]', $filters, [''], [
|
||||
'id' => 'filters',
|
||||
'data-none-selected-text' => lang('Common.none_selected_text'),
|
||||
'class' => 'selectpicker show-menu-arrow',
|
||||
|
||||
@@ -126,12 +126,7 @@
|
||||
<div class="form-group form-group-sm">
|
||||
<?= form_label(lang('Expenses.employee'), 'employee', ['class' => 'control-label col-xs-3']) ?>
|
||||
<div class="col-xs-6">
|
||||
<?php if ($can_assign_employee): ?>
|
||||
<?= form_dropdown('employee_id', $employees, $expenses_info->employee_id, 'id="employee_id" class="form-control"') ?>
|
||||
<?php else: ?>
|
||||
<?= form_hidden('employee_id', $expenses_info->employee_id) ?>
|
||||
<?= form_input(['name' => 'employee_name', 'value' => esc($employees[$expenses_info->employee_id] ?? ''), 'class' => 'form-control', 'readonly' => 'readonly']) ?>
|
||||
<?php endif; ?>
|
||||
<?= form_dropdown('employee_id', $employees, $expenses_info->employee_id, 'id="employee_id" class="form-control"') ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
* @var string $controller_name
|
||||
* @var string $table_headers
|
||||
* @var array $filters
|
||||
* @var array $selected_filters
|
||||
* @var array $config
|
||||
* @var string|null $start_date
|
||||
* @var string|null $end_date
|
||||
*/
|
||||
?>
|
||||
|
||||
@@ -14,18 +11,19 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
// When any filter is clicked and the dropdown window is closed
|
||||
$('#filters').on('hidden.bs.select', function(e) {
|
||||
table_support.refresh();
|
||||
});
|
||||
|
||||
// Load the preset datarange picker
|
||||
<?= view('partial/daterangepicker') ?>
|
||||
|
||||
<?= view('partial/bootstrap_tables_locale') ?>
|
||||
$("#daterangepicker").on('apply.daterangepicker', function(ev, picker) {
|
||||
table_support.refresh();
|
||||
});
|
||||
|
||||
// Override dates from server if provided
|
||||
<?php if (isset($start_date) && $start_date): ?>
|
||||
start_date = "<?= esc($start_date) ?>";
|
||||
<?php endif; ?>
|
||||
<?php if (isset($end_date) && $end_date): ?>
|
||||
end_date = "<?= esc($end_date) ?>";
|
||||
<?php endif; ?>
|
||||
<?= view('partial/bootstrap_tables_locale') ?>
|
||||
|
||||
table_support.init({
|
||||
resource: '<?= esc($controller_name) ?>',
|
||||
@@ -47,10 +45,8 @@
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
</script
|
||||
<?= view('partial/table_filter_persistence') ?>>
|
||||
</script>
|
||||
|
||||
<?= view('partial/print_receipt', ['print_after_sale' => false, 'selected_printer' => 'takings_printer']) ?>
|
||||
|
||||
@@ -69,7 +65,7 @@
|
||||
<span class="glyphicon glyphicon-trash"> </span><?= lang('Common.delete') ?>
|
||||
</button>
|
||||
<?= form_input(['name' => 'daterangepicker', 'class' => 'form-control input-sm', 'id' => 'daterangepicker']) ?>
|
||||
<?= form_multiselect('filters[]', esc($filters), $selected_filters ?? [], [
|
||||
<?= form_multiselect('filters[]', esc($filters), [''], [
|
||||
'id' => 'filters',
|
||||
'data-none-selected-text' => lang('Common.none_selected_text'),
|
||||
'class' => 'selectpicker show-menu-arrow',
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
* @var array $stock_locations
|
||||
* @var int $stock_location
|
||||
* @var array $config
|
||||
* @var string|null $start_date
|
||||
* @var string|null $end_date
|
||||
* @var array $selected_filters
|
||||
*/
|
||||
|
||||
use App\Models\Employee;
|
||||
@@ -25,20 +22,24 @@ use App\Models\Employee;
|
||||
);
|
||||
});
|
||||
|
||||
// When any filter is clicked and the dropdown window is closed
|
||||
$('#filters').on('hidden.bs.select', function(e) {
|
||||
table_support.refresh();
|
||||
});
|
||||
|
||||
// Load the preset daterange picker
|
||||
<?= view('partial/daterangepicker') ?>
|
||||
// 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)) ?>";
|
||||
$("#daterangepicker").on('apply.daterangepicker', function(ev, picker) {
|
||||
table_support.refresh();
|
||||
});
|
||||
|
||||
// Override dates from server if provided
|
||||
<?php if (isset($start_date) && $start_date): ?>
|
||||
start_date = "<?= esc($start_date) ?>";
|
||||
<?php endif; ?>
|
||||
<?php if (isset($end_date) && $end_date): ?>
|
||||
end_date = "<?= esc($end_date) ?>";
|
||||
<?php endif; ?>
|
||||
$("#stock_location").change(function() {
|
||||
table_support.refresh();
|
||||
});
|
||||
|
||||
<?php
|
||||
echo view('partial/bootstrap_tables_locale');
|
||||
@@ -74,8 +75,6 @@ use App\Models\Employee;
|
||||
});
|
||||
</script>
|
||||
|
||||
<?= view('partial/table_filter_persistence', ['additional_params' => ['stock_location']]) ?>
|
||||
|
||||
<div id="title_bar" class="btn-toolbar print_hide">
|
||||
<button class="btn btn-info btn-sm pull-right modal-dlg" data-btn-submit="<?= lang('Common.submit') ?>" data-href="<?= "$controller_name/csvImport" ?>" title="<?= lang('Items.import_items_csv') ?>">
|
||||
<span class="glyphicon glyphicon-import"> </span><?= lang('Common.import_csv') ?>
|
||||
@@ -98,7 +97,7 @@ use App\Models\Employee;
|
||||
<span class="glyphicon glyphicon-barcode"> </span><?= lang('Items.generate_barcodes') ?>
|
||||
</button>
|
||||
<?= form_input(['name' => 'daterangepicker', 'class' => 'form-control input-sm', 'id' => 'daterangepicker']) ?>
|
||||
<?= form_multiselect('filters[]', $filters, $selected_filters ?? [], [
|
||||
<?= form_multiselect('filters[]', $filters, [''], [
|
||||
'id' => 'filters',
|
||||
'class' => 'selectpicker show-menu-arrow',
|
||||
'data-none-selected-text' => lang('Common.none_selected_text'),
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
<?php
|
||||
if ($gcaptcha_enabled) {
|
||||
echo '<script src="https://www.google.com/recaptcha/api.js"></script>';
|
||||
echo '<div class="g-recaptcha mb-3" style="text-align: center;" data-sitekey="' . esc($config['gcaptcha_site_key']) . '"></div>';
|
||||
echo '<div class="g-recaptcha mb-3" style="text-align: center;" data-sitekey="' . $config['gcaptcha_site_key'] . '"></div>';
|
||||
}
|
||||
?>
|
||||
<div class="d-grid">
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Table Filter Persistence
|
||||
*
|
||||
* This partial 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')
|
||||
*/
|
||||
$options = $options ?? [];
|
||||
$additional_params = $options['additional_params'] ?? [];
|
||||
$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) ?>';
|
||||
|
||||
function update_url() {
|
||||
var params = new URLSearchParams();
|
||||
|
||||
// Add dates
|
||||
if (typeof start_date !== 'undefined') {
|
||||
params.set('start_date', start_date);
|
||||
}
|
||||
if (typeof end_date !== 'undefined') {
|
||||
params.set('end_date', end_date);
|
||||
}
|
||||
|
||||
// Add filters
|
||||
var 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);
|
||||
if (element.length) {
|
||||
var value = element.val();
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
value.forEach(function(v) {
|
||||
params.append(param + '[]', v);
|
||||
});
|
||||
} else if (value) {
|
||||
params.set(param, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update URL without page reload
|
||||
var new_url = window.location.pathname;
|
||||
var 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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user