mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-25 08:44:42 -04:00
Compare commits
2 Commits
feature/pa
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12146275f4 | ||
|
|
e45af91e2e |
@@ -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
|
||||
14
.github/workflows/phpunit.yml
vendored
14
.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
|
||||
@@ -107,15 +111,7 @@ jobs:
|
||||
env:
|
||||
CI_ENVIRONMENT: testing
|
||||
MYSQL_HOST_NAME: 127.0.0.1
|
||||
run: composer test -- --log-junit test-results/junit.xml
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-results-php-${{ matrix.php-version }}
|
||||
path: test-results/
|
||||
retention-days: 30
|
||||
run: composer test
|
||||
|
||||
- name: Stop MariaDB
|
||||
if: always()
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,8 +169,3 @@ const MAX_PRECISION = 1e14;
|
||||
const DEFAULT_PRECISION = 2;
|
||||
const DEFAULT_LANGUAGE = 'english';
|
||||
const DEFAULT_LANGUAGE_CODE = 'en';
|
||||
|
||||
/**
|
||||
* Admin modules - list of modules required for admin privileges
|
||||
*/
|
||||
const ADMIN_MODULES = ['customers', 'employees', 'giftcards', 'items', 'item_kits', 'messages', 'receivings', 'reports', 'sales', 'config', 'suppliers'];
|
||||
|
||||
@@ -12,18 +12,10 @@ use CodeIgniter\Filters\InvalidChars;
|
||||
use CodeIgniter\Filters\PageCache;
|
||||
use CodeIgniter\Filters\PerformanceMetrics;
|
||||
use CodeIgniter\Filters\SecureHeaders;
|
||||
use App\Filters\ApiAuth;
|
||||
|
||||
class Filters extends BaseFilters
|
||||
{
|
||||
/**
|
||||
* Configures aliases for Filter classes to
|
||||
* make reading things nicer and simpler.
|
||||
*
|
||||
* @var array<string, class-string|list<class-string>>
|
||||
*
|
||||
* [filter_name => classname]
|
||||
* or [filter_name => [classname1, classname2, ...]]
|
||||
*/
|
||||
public array $aliases = [
|
||||
'csrf' => CSRF::class,
|
||||
'toolbar' => DebugToolbar::class,
|
||||
@@ -34,6 +26,7 @@ class Filters extends BaseFilters
|
||||
'forcehttps' => ForceHTTPS::class,
|
||||
'pagecache' => PageCache::class,
|
||||
'performance' => PerformanceMetrics::class,
|
||||
'apiauth' => ApiAuth::class,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -70,7 +63,7 @@ class Filters extends BaseFilters
|
||||
public array $globals = [
|
||||
'before' => [
|
||||
'honeypot',
|
||||
'csrf' => ['except' => 'login'],
|
||||
'csrf' => ['except' => ['login', 'api/*']],
|
||||
'invalidchars',
|
||||
],
|
||||
'after' => [
|
||||
|
||||
@@ -11,10 +11,6 @@ $routes->get('/', 'Login::index');
|
||||
$routes->get('login', 'Login::index');
|
||||
$routes->post('login', 'Login::index');
|
||||
|
||||
// Payment provider webhook routes (no authentication required)
|
||||
$routes->post('payments/webhook/(:segment)', 'Payments\Webhook::handle/$1');
|
||||
$routes->get('payments/status/(:segment)/(:segment)', 'Payments\Webhook::status/$1/$2');
|
||||
|
||||
$routes->add('no_access/index/(:segment)', 'No_access::index/$1');
|
||||
$routes->add('no_access/index/(:segment)/(:segment)', 'No_access::index/$1/$2');
|
||||
|
||||
@@ -43,3 +39,50 @@ $routes->add('reports/specific_customers', 'Reports::specific_customer_input');
|
||||
$routes->add('reports/specific_employees', 'Reports::specific_employee_input');
|
||||
$routes->add('reports/specific_discounts', 'Reports::specific_discount_input');
|
||||
$routes->add('reports/specific_suppliers', 'Reports::specific_supplier_input');
|
||||
|
||||
$routes->group('office/api-keys', ['filter' => 'session'], static function(RouteCollection $routes): void {
|
||||
$routes->get('/', 'ApiKeys::index');
|
||||
$routes->post('generate', 'ApiKeys::generate');
|
||||
$routes->post('revoke/(:num)', 'ApiKeys::revoke/$1');
|
||||
$routes->post('regenerate/(:num)', 'ApiKeys::regenerate/$1');
|
||||
});
|
||||
|
||||
$routes->group('api/v1', ['filter' => 'apiauth'], static function(RouteCollection $routes): void {
|
||||
$routes->get('customers', 'Api\Customers::index');
|
||||
$routes->get('customers/(:num)', 'Api\Customers::show/$1');
|
||||
$routes->post('customers', 'Api\Customers::create');
|
||||
$routes->put('customers/(:num)', 'Api\Customers::update/$1');
|
||||
$routes->delete('customers/(:num)', 'Api\Customers::delete/$1');
|
||||
$routes->post('customers/batch-delete', 'Api\Customers::batchDelete');
|
||||
$routes->get('customers/suggest', 'Api\Customers::suggest');
|
||||
|
||||
$routes->get('suppliers', 'Api\Suppliers::index');
|
||||
$routes->get('suppliers/(:num)', 'Api\Suppliers::show/$1');
|
||||
$routes->post('suppliers', 'Api\Suppliers::create');
|
||||
$routes->put('suppliers/(:num)', 'Api\Suppliers::update/$1');
|
||||
$routes->delete('suppliers/(:num)', 'Api\Suppliers::delete/$1');
|
||||
$routes->post('suppliers/batch-delete', 'Api\Suppliers::batchDelete');
|
||||
$routes->get('suppliers/suggest', 'Api\Suppliers::suggest');
|
||||
|
||||
$routes->get('items', 'Api\Items::index');
|
||||
$routes->get('items/(:num)', 'Api\Items::show/$1');
|
||||
$routes->post('items', 'Api\Items::create');
|
||||
$routes->put('items/(:num)', 'Api\Items::update/$1');
|
||||
$routes->delete('items/(:num)', 'Api\Items::delete/$1');
|
||||
$routes->post('items/batch-delete', 'Api\Items::batchDelete');
|
||||
$routes->get('items/suggest', 'Api\Items::suggest');
|
||||
$routes->get('items/(:num)/quantities', 'Api\Items::quantities/$1');
|
||||
|
||||
$routes->get('inventory', 'Api\Inventory::index');
|
||||
$routes->post('inventory', 'Api\Inventory::create');
|
||||
$routes->post('inventory/bulk', 'Api\Inventory::create');
|
||||
|
||||
$routes->get('sales', 'Api\Sales::index');
|
||||
$routes->get('sales/(:num)', 'Api\Sales::show/$1');
|
||||
$routes->get('sales/(:num)/items', 'Api\Sales::items/$1');
|
||||
$routes->get('sales/(:num)/payments', 'Api\Sales::payments/$1');
|
||||
|
||||
$routes->get('receivings', 'Api\Receivings::index');
|
||||
$routes->get('receivings/(:num)', 'Api\Receivings::show/$1');
|
||||
$routes->get('receivings/(:num)/items', 'Api\Receivings::items/$1');
|
||||
});
|
||||
|
||||
129
app/Controllers/Api/BaseController.php
Normal file
129
app/Controllers/Api/BaseController.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Api;
|
||||
|
||||
use App\Models\Employee;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use CodeIgniter\RESTful\ResourceController;
|
||||
|
||||
class BaseController extends ResourceController
|
||||
{
|
||||
protected Employee $employee;
|
||||
protected int $employeeId = 0;
|
||||
protected $format = 'json';
|
||||
|
||||
public function initController(\CodeIgniter\HTTP\RequestInterface $request, \CodeIgniter\HTTP\ResponseInterface $response, \Psr\Log\LoggerInterface $logger): void
|
||||
{
|
||||
parent::initController($request, $response, $logger);
|
||||
|
||||
$this->employee = model(Employee::class);
|
||||
$this->employeeId = $request->employeeId ?? 0;
|
||||
}
|
||||
|
||||
protected function hasPermission(string $moduleId): bool
|
||||
{
|
||||
return $this->employee->has_grant($moduleId, $this->employeeId);
|
||||
}
|
||||
|
||||
protected function respondSuccess(array $data = [], int $code = 200, string $message = 'Success'): ResponseInterface
|
||||
{
|
||||
$response = ['success' => true];
|
||||
|
||||
if ($message) {
|
||||
$response['message'] = $message;
|
||||
}
|
||||
|
||||
$response = array_merge($response, $data);
|
||||
|
||||
return $this->respond($response, $code);
|
||||
}
|
||||
|
||||
protected function respondCreated(array $data = [], string $message = 'Resource created'): ResponseInterface
|
||||
{
|
||||
return $this->respondSuccess($data, 201, $message);
|
||||
}
|
||||
|
||||
protected function respondError(string $message, int $code = 400): ResponseInterface
|
||||
{
|
||||
return $this->respond([
|
||||
'success' => false,
|
||||
'message' => $message
|
||||
], $code);
|
||||
}
|
||||
|
||||
protected function respondNotFound(string $message = 'Resource not found'): ResponseInterface
|
||||
{
|
||||
return $this->respondError($message, 404);
|
||||
}
|
||||
|
||||
protected function respondUnauthorized(string $message = 'Unauthorized'): ResponseInterface
|
||||
{
|
||||
return $this->respondError($message, 403);
|
||||
}
|
||||
|
||||
protected function respondValidationError(array $errors): ResponseInterface
|
||||
{
|
||||
return $this->respond([
|
||||
'success' => false,
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $errors
|
||||
], 422);
|
||||
}
|
||||
|
||||
protected function getPagination(): array
|
||||
{
|
||||
$offset = (int) ($this->request->getGet('offset') ?? 0);
|
||||
$limit = (int) ($this->request->getGet('limit') ?? 25);
|
||||
$limit = min(max($limit, 1), 100);
|
||||
$offset = max($offset, 0);
|
||||
|
||||
return ['offset' => $offset, 'limit' => $limit];
|
||||
}
|
||||
|
||||
protected function getSort(array $allowedFields, string $default = 'id', string $defaultOrder = 'asc'): array
|
||||
{
|
||||
$sort = $this->request->getGet('sort') ?? $default;
|
||||
$order = strtolower($this->request->getGet('order') ?? $defaultOrder);
|
||||
|
||||
if (!in_array($sort, $allowedFields)) {
|
||||
$sort = $default;
|
||||
}
|
||||
|
||||
if (!in_array($order, ['asc', 'desc'])) {
|
||||
$order = $defaultOrder;
|
||||
}
|
||||
|
||||
return ['sort' => $sort, 'order' => $order];
|
||||
}
|
||||
|
||||
protected function toCamelCase(array $data): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($data as $key => $value) {
|
||||
$camelKey = lcfirst(str_replace('_', '', ucwords($key, '_')));
|
||||
$result[$camelKey] = $value;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function toSnakeCase(array $data): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($data as $key => $value) {
|
||||
$snakeKey = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $key));
|
||||
$result[$snakeKey] = $value;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function transformItem(object|array $item, array $additional = []): array
|
||||
{
|
||||
$item = is_object($item) ? (array) $item : $item;
|
||||
return $this->toCamelCase(array_merge($item, $additional));
|
||||
}
|
||||
|
||||
protected function transformCollection(array $items): array
|
||||
{
|
||||
return array_map([$this, 'transformItem'], $items);
|
||||
}
|
||||
}
|
||||
251
app/Controllers/Api/Customers.php
Normal file
251
app/Controllers/Api/Customers.php
Normal file
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Api;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\Person;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
class Customers extends BaseController
|
||||
{
|
||||
protected Customer $customerModel;
|
||||
protected Person $personModel;
|
||||
|
||||
protected array $allowedSortFields = ['person_id', 'last_name', 'first_name', 'email', 'company_name'];
|
||||
|
||||
public function initController(\CodeIgniter\HTTP\RequestInterface $request, \CodeIgniter\HTTP\ResponseInterface $response, \Psr\Log\LoggerInterface $logger): void
|
||||
{
|
||||
parent::initController($request, $response, $logger);
|
||||
$this->customerModel = model(Customer::class);
|
||||
$this->personModel = model(Person::class);
|
||||
}
|
||||
|
||||
public function index(): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('customers')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$search = $this->request->getGet('search');
|
||||
$pagination = $this->getPagination();
|
||||
$sort = $this->getSort($this->allowedSortFields, 'last_name');
|
||||
|
||||
$builder = $this->customerModel->builder();
|
||||
$builder->select('customers.*, people.*');
|
||||
$builder->join('people', 'people.person_id = customers.person_id');
|
||||
$builder->where('customers.deleted', 0);
|
||||
|
||||
if ($search) {
|
||||
$builder->groupStart();
|
||||
$builder->like('people.first_name', $search);
|
||||
$builder->orLike('people.last_name', $search);
|
||||
$builder->orLike('people.email', $search);
|
||||
$builder->orLike('customers.account_number', $search);
|
||||
$builder->orLike('customers.company_name', $search);
|
||||
$builder->groupEnd();
|
||||
}
|
||||
|
||||
$total = $builder->countAllResults(false);
|
||||
|
||||
$dbSort = $this->mapSortField($sort['sort']);
|
||||
$builder->orderBy($dbSort, $sort['order']);
|
||||
$builder->limit($pagination['limit'], $pagination['offset']);
|
||||
|
||||
$customers = $builder->get()->getResultArray();
|
||||
|
||||
return $this->respondSuccess([
|
||||
'total' => $total,
|
||||
'offset' => $pagination['offset'],
|
||||
'limit' => $pagination['limit'],
|
||||
'rows' => $this->transformCollection($customers)
|
||||
]);
|
||||
}
|
||||
|
||||
public function show($id = null): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('customers')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$customer = $this->customerModel->get_info($id);
|
||||
|
||||
if (empty($customer) || $customer->deleted) {
|
||||
return $this->respondNotFound('Customer not found');
|
||||
}
|
||||
|
||||
$person = (array) $this->personModel->get_info($id);
|
||||
$customer = (array) $customer;
|
||||
$data = array_merge($person, $customer);
|
||||
|
||||
return $this->respondSuccess($this->transformItem($data));
|
||||
}
|
||||
|
||||
public function create(): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('customers')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$data = $this->request->getJSON(true);
|
||||
if (empty($data)) {
|
||||
$data = $this->request->getPost();
|
||||
}
|
||||
|
||||
$data = $this->toSnakeCase($data);
|
||||
|
||||
$rules = [
|
||||
'first_name' => 'required|max_length[255]',
|
||||
'last_name' => 'required|max_length[255]',
|
||||
];
|
||||
|
||||
$snakeData = [];
|
||||
foreach ($data as $key => $value) {
|
||||
$snakeKey = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $key));
|
||||
$snakeData[$snakeKey] = $value;
|
||||
}
|
||||
|
||||
$personData = array_intersect_key($snakeData, array_flip([
|
||||
'first_name', 'last_name', 'gender', 'phone_number', 'email',
|
||||
'address_1', 'address_2', 'city', 'state', 'zip', 'country', 'comments'
|
||||
]));
|
||||
|
||||
$customerData = array_intersect_key($snakeData, array_flip([
|
||||
'account_number', 'taxable', 'tax_id', 'sales_tax_code_id',
|
||||
'discount', 'discount_type', 'company_name', 'package_id', 'consent'
|
||||
]));
|
||||
$customerData['employee_id'] = $this->employeeId;
|
||||
|
||||
$personId = false;
|
||||
$success = $this->personModel->save_value($personData);
|
||||
|
||||
if ($success && isset($personData['person_id'])) {
|
||||
$personId = $personData['person_id'];
|
||||
$customerData['person_id'] = $personId;
|
||||
$success = $this->customerModel->save_value($customerData);
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
return $this->respondCreated(['id' => $personId], 'Customer created successfully');
|
||||
}
|
||||
|
||||
return $this->respondError('Failed to create customer');
|
||||
}
|
||||
|
||||
public function update($id = null): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('customers')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$customer = $this->customerModel->get_info($id);
|
||||
|
||||
if (empty($customer) || $customer->deleted) {
|
||||
return $this->respondNotFound('Customer not found');
|
||||
}
|
||||
|
||||
$data = $this->request->getJSON(true);
|
||||
if (empty($data)) {
|
||||
$data = $this->request->getRawInput();
|
||||
}
|
||||
|
||||
$snakeData = [];
|
||||
foreach ($data as $key => $value) {
|
||||
$snakeKey = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $key));
|
||||
$snakeData[$snakeKey] = $value;
|
||||
}
|
||||
|
||||
$personData = array_intersect_key($snakeData, array_flip([
|
||||
'first_name', 'last_name', 'gender', 'phone_number', 'email',
|
||||
'address_1', 'address_2', 'city', 'state', 'zip', 'country', 'comments'
|
||||
]));
|
||||
|
||||
$customerData = array_intersect_key($snakeData, array_flip([
|
||||
'account_number', 'taxable', 'tax_id', 'sales_tax_code_id',
|
||||
'discount', 'discount_type', 'company_name', 'package_id', 'consent'
|
||||
]));
|
||||
|
||||
if (!empty($personData)) {
|
||||
$this->personModel->save_value($personData, $id);
|
||||
}
|
||||
|
||||
if (!empty($customerData)) {
|
||||
$this->customerModel->save_value($customerData, $id);
|
||||
}
|
||||
|
||||
return $this->respondSuccess([], 200, 'Customer updated successfully');
|
||||
}
|
||||
|
||||
public function delete($id = null): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('customers')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$customer = $this->customerModel->get_info($id);
|
||||
|
||||
if (empty($customer) || $customer->deleted) {
|
||||
return $this->respondNotFound('Customer not found');
|
||||
}
|
||||
|
||||
$success = $this->customerModel->delete($id);
|
||||
|
||||
if ($success) {
|
||||
return $this->respondSuccess([], 200, 'Customer deleted successfully');
|
||||
}
|
||||
|
||||
return $this->respondError('Failed to delete customer');
|
||||
}
|
||||
|
||||
public function batchDelete(): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('customers')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$data = $this->request->getJSON(true);
|
||||
$ids = $data['ids'] ?? [];
|
||||
|
||||
if (empty($ids)) {
|
||||
return $this->respondError('No customer IDs provided');
|
||||
}
|
||||
|
||||
$success = $this->customerModel->delete_list($ids);
|
||||
|
||||
if ($success) {
|
||||
return $this->respondSuccess([], 200, 'Customers deleted successfully');
|
||||
}
|
||||
|
||||
return $this->respondError('Failed to delete customers');
|
||||
}
|
||||
|
||||
public function suggest(): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('customers')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$term = $this->request->getGet('term');
|
||||
$limit = (int) ($this->request->getGet('limit') ?? 25);
|
||||
|
||||
if (empty($term)) {
|
||||
return $this->respondSuccess(['suggestions' => []]);
|
||||
}
|
||||
|
||||
$suggestions = $this->customerModel->get_search_suggestions($term, $limit);
|
||||
|
||||
return $this->respondSuccess(['suggestions' => $suggestions]);
|
||||
}
|
||||
|
||||
private function mapSortField(string $field): string
|
||||
{
|
||||
$map = [
|
||||
'personId' => 'people.person_id',
|
||||
'lastName' => 'people.last_name',
|
||||
'firstName' => 'people.first_name',
|
||||
'email' => 'people.email',
|
||||
'companyName' => 'customers.company_name'
|
||||
];
|
||||
|
||||
return $map[$field] ?? 'people.last_name';
|
||||
}
|
||||
}
|
||||
212
app/Controllers/Api/Inventory.php
Normal file
212
app/Controllers/Api/Inventory.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Api;
|
||||
|
||||
use App\Models\Inventory as InventoryModel;
|
||||
use App\Models\Item;
|
||||
use App\Models\Item_quantity;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
class Inventory extends BaseController
|
||||
{
|
||||
protected InventoryModel $inventory;
|
||||
protected Item $item;
|
||||
protected Item_quantity $itemQuantity;
|
||||
|
||||
protected array $allowedSortFields = ['trans_id', 'trans_date', 'trans_items'];
|
||||
|
||||
public function initController(\CodeIgniter\HTTP\RequestInterface $request, \CodeIgniter\HTTP\ResponseInterface $response, \Psr\Log\LoggerInterface $logger): void
|
||||
{
|
||||
parent::initController($request, $response, $logger);
|
||||
$this->inventory = model(InventoryModel::class);
|
||||
$this->item = model(Item::class);
|
||||
$this->itemQuantity = model(Item_quantity::class);
|
||||
}
|
||||
|
||||
public function index(): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('items')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$itemId = $this->request->getGet('itemId');
|
||||
$locationId = $this->request->getGet('locationId');
|
||||
$pagination = $this->getPagination();
|
||||
$sort = $this->getSort($this->allowedSortFields, 'trans_date');
|
||||
|
||||
$builder = $this->inventory->builder();
|
||||
|
||||
if ($itemId) {
|
||||
$builder->where('trans_items', $itemId);
|
||||
}
|
||||
|
||||
if ($locationId) {
|
||||
$builder->where('trans_location', $locationId);
|
||||
}
|
||||
|
||||
$total = $builder->countAllResults(false);
|
||||
|
||||
$builder->orderBy($sort['sort'], $sort['order']);
|
||||
$builder->limit($pagination['limit'], $pagination['offset']);
|
||||
|
||||
$transactions = $builder->get()->getResultArray();
|
||||
|
||||
return $this->respondSuccess([
|
||||
'total' => $total,
|
||||
'offset' => $pagination['offset'],
|
||||
'limit' => $pagination['limit'],
|
||||
'rows' => $this->transformCollection($transactions)
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('items')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$data = $this->request->getJSON(true);
|
||||
|
||||
if (isset($data['adjustments']) && is_array($data['adjustments'])) {
|
||||
return $this->bulkAdjust($data['adjustments']);
|
||||
}
|
||||
|
||||
return $this->singleAdjust($data);
|
||||
}
|
||||
|
||||
private function singleAdjust(array $data): ResponseInterface
|
||||
{
|
||||
if (empty($data['itemId'])) {
|
||||
return $this->respondError('itemId is required');
|
||||
}
|
||||
|
||||
if (!isset($data['quantity'])) {
|
||||
return $this->respondError('quantity is required');
|
||||
}
|
||||
|
||||
$mode = $data['mode'] ?? 'adjust';
|
||||
|
||||
if (!in_array($mode, ['adjust', 'set'])) {
|
||||
return $this->respondError('mode must be "adjust" or "set"');
|
||||
}
|
||||
|
||||
$item = $this->item->find($data['itemId']);
|
||||
if (!$item || $item->deleted) {
|
||||
return $this->respondNotFound('Item not found');
|
||||
}
|
||||
|
||||
$locationId = $data['locationId'] ?? 1;
|
||||
$comment = $data['comment'] ?? 'API inventory adjustment';
|
||||
$quantity = (float) $data['quantity'];
|
||||
|
||||
if ($mode === 'set') {
|
||||
$currentQty = $this->itemQuantity->get_item_quantity($data['itemId'], $locationId);
|
||||
$currentQty = $currentQty ? (float) $currentQty->quantity : 0;
|
||||
$adjustment = $quantity - $currentQty;
|
||||
|
||||
if ($adjustment == 0) {
|
||||
return $this->respondSuccess([
|
||||
'itemId' => (int) $data['itemId'],
|
||||
'locationId' => (int) $locationId,
|
||||
'newQuantity' => $quantity,
|
||||
'mode' => $mode
|
||||
], 200, 'Quantity already at requested level');
|
||||
}
|
||||
} else {
|
||||
$adjustment = $quantity;
|
||||
}
|
||||
|
||||
$invData = [
|
||||
'trans_date' => date('Y-m-d H:i:s'),
|
||||
'trans_items' => $data['itemId'],
|
||||
'trans_user' => $this->employeeId,
|
||||
'trans_location' => $locationId,
|
||||
'trans_comment' => $comment,
|
||||
'trans_inventory' => $adjustment
|
||||
];
|
||||
|
||||
$this->inventory->insert($invData);
|
||||
$this->itemQuantity->change_quantity($data['itemId'], $locationId, $adjustment);
|
||||
|
||||
$newQty = $this->itemQuantity->get_item_quantity($data['itemId'], $locationId);
|
||||
|
||||
return $this->respondSuccess([
|
||||
'itemId' => (int) $data['itemId'],
|
||||
'locationId' => (int) $locationId,
|
||||
'adjustment' => $adjustment,
|
||||
'newQuantity' => $newQty ? (float) $newQty->quantity : 0,
|
||||
'mode' => $mode
|
||||
], 200, 'Inventory adjusted successfully');
|
||||
}
|
||||
|
||||
private function bulkAdjust(array $adjustments): ResponseInterface
|
||||
{
|
||||
$results = [];
|
||||
$processed = 0;
|
||||
$errors = [];
|
||||
|
||||
$this->inventory->db->transStart();
|
||||
|
||||
foreach ($adjustments as $adjustment) {
|
||||
$itemId = $adjustment['itemId'] ?? $adjustment['item_id'] ?? null;
|
||||
|
||||
if (!$itemId) {
|
||||
$errors[] = ['itemId' => null, 'success' => false, 'error' => 'itemId is required'];
|
||||
continue;
|
||||
}
|
||||
|
||||
$item = $this->item->find($itemId);
|
||||
if (!$item || $item->deleted) {
|
||||
$errors[] = ['itemId' => $itemId, 'success' => false, 'error' => 'Item not found'];
|
||||
continue;
|
||||
}
|
||||
|
||||
$mode = $adjustment['mode'] ?? 'adjust';
|
||||
$locationId = $adjustment['locationId'] ?? $adjustment['location_id'] ?? 1;
|
||||
$quantity = (float) ($adjustment['quantity'] ?? 0);
|
||||
$comment = $adjustment['comment'] ?? 'Bulk API inventory adjustment';
|
||||
|
||||
if ($mode === 'set') {
|
||||
$currentQty = $this->itemQuantity->get_item_quantity($itemId, $locationId);
|
||||
$currentQty = $currentQty ? (float) $currentQty->quantity : 0;
|
||||
$adjustmentQty = $quantity - $currentQty;
|
||||
} else {
|
||||
$adjustmentQty = $quantity;
|
||||
}
|
||||
|
||||
$invData = [
|
||||
'trans_date' => date('Y-m-d H:i:s'),
|
||||
'trans_items' => $itemId,
|
||||
'trans_user' => $this->employeeId,
|
||||
'trans_location' => $locationId,
|
||||
'trans_comment' => $comment,
|
||||
'trans_inventory' => $adjustmentQty
|
||||
];
|
||||
|
||||
$this->inventory->insert($invData);
|
||||
$this->itemQuantity->change_quantity($itemId, $locationId, $adjustmentQty);
|
||||
|
||||
$results[] = ['itemId' => $itemId, 'success' => true];
|
||||
$processed++;
|
||||
}
|
||||
|
||||
$this->inventory->db->transComplete();
|
||||
|
||||
$response = [
|
||||
'processed' => $processed,
|
||||
'total' => count($adjustments),
|
||||
'results' => $results
|
||||
];
|
||||
|
||||
if (!empty($errors)) {
|
||||
$response['errors'] = $errors;
|
||||
$response['success'] = false;
|
||||
$response['message'] = 'Some adjustments failed';
|
||||
} else {
|
||||
$response['success'] = true;
|
||||
$response['message'] = 'All adjustments processed successfully';
|
||||
}
|
||||
|
||||
return $this->respondSuccess($response);
|
||||
}
|
||||
}
|
||||
237
app/Controllers/Api/Items.php
Normal file
237
app/Controllers/Api/Items.php
Normal file
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Api;
|
||||
|
||||
use App\Models\Item;
|
||||
use App\Models\Item_quantity;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
class Items extends BaseController
|
||||
{
|
||||
protected Item $itemModel;
|
||||
protected Item_quantity $itemQuantityModel;
|
||||
|
||||
protected array $allowedSortFields = ['item_id', 'name', 'category', 'cost_price', 'unit_price'];
|
||||
|
||||
public function initController(\CodeIgniter\HTTP\RequestInterface $request, \CodeIgniter\HTTP\ResponseInterface $response, \Psr\Log\LoggerInterface $logger): void
|
||||
{
|
||||
parent::initController($request, $response, $logger);
|
||||
$this->itemModel = model(Item::class);
|
||||
$this->itemQuantityModel = model(Item_quantity::class);
|
||||
}
|
||||
|
||||
public function index(): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('items')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$search = $this->request->getGet('search');
|
||||
$pagination = $this->getPagination();
|
||||
$sort = $this->getSort($this->allowedSortFields, 'name');
|
||||
$stockLocation = $this->request->getGet('stockLocation');
|
||||
|
||||
$builder = $this->itemModel->builder();
|
||||
$builder->where('deleted', 0);
|
||||
|
||||
if ($search) {
|
||||
$builder->groupStart();
|
||||
$builder->like('name', $search);
|
||||
$builder->orLike('item_number', $search);
|
||||
$builder->orLike('category', $search);
|
||||
$builder->orLike('description', $search);
|
||||
$builder->groupEnd();
|
||||
}
|
||||
|
||||
$total = $builder->countAllResults(false);
|
||||
|
||||
$dbSort = $this->mapSortField($sort['sort']);
|
||||
$builder->orderBy($dbSort, $sort['order']);
|
||||
$builder->limit($pagination['limit'], $pagination['offset']);
|
||||
|
||||
$items = $builder->get()->getResultArray();
|
||||
|
||||
return $this->respondSuccess([
|
||||
'total' => $total,
|
||||
'offset' => $pagination['offset'],
|
||||
'limit' => $pagination['limit'],
|
||||
'rows' => $this->transformCollection($items)
|
||||
]);
|
||||
}
|
||||
|
||||
public function show($id = null): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('items')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$item = $this->itemModel->find($id);
|
||||
|
||||
if (!$item || $item->deleted) {
|
||||
return $this->respondNotFound('Item not found');
|
||||
}
|
||||
|
||||
return $this->respondSuccess($this->transformItem($item));
|
||||
}
|
||||
|
||||
public function create(): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('items')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$data = $this->request->getJSON(true);
|
||||
if (empty($data)) {
|
||||
$data = $this->request->getPost();
|
||||
}
|
||||
|
||||
$snakeData = $this->toSnakeCase($data);
|
||||
|
||||
if (!empty($snakeData['item_number'])) {
|
||||
if ($this->itemModel->item_number_exists($snakeData['item_number'])) {
|
||||
return $this->respondError('Item number already exists', 409);
|
||||
}
|
||||
}
|
||||
|
||||
$itemId = $this->itemModel->save_value($snakeData);
|
||||
|
||||
if ($itemId) {
|
||||
return $this->respondCreated(['id' => $itemId], 'Item created successfully');
|
||||
}
|
||||
|
||||
return $this->respondError('Failed to create item');
|
||||
}
|
||||
|
||||
public function update($id = null): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('items')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$item = $this->itemModel->find($id);
|
||||
|
||||
if (!$item || $item->deleted) {
|
||||
return $this->respondNotFound('Item not found');
|
||||
}
|
||||
|
||||
$data = $this->request->getJSON(true);
|
||||
if (empty($data)) {
|
||||
$data = $this->request->getRawInput();
|
||||
}
|
||||
|
||||
$snakeData = $this->toSnakeCase($data);
|
||||
$snakeData['item_id'] = $id;
|
||||
|
||||
$success = $this->itemModel->save_value($snakeData);
|
||||
|
||||
if ($success) {
|
||||
return $this->respondSuccess([], 200, 'Item updated successfully');
|
||||
}
|
||||
|
||||
return $this->respondError('Failed to update item');
|
||||
}
|
||||
|
||||
public function delete($id = null): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('items')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$item = $this->itemModel->find($id);
|
||||
|
||||
if (!$item || $item->deleted) {
|
||||
return $this->respondNotFound('Item not found');
|
||||
}
|
||||
|
||||
$success = $this->itemModel->delete($id);
|
||||
|
||||
if ($success) {
|
||||
return $this->respondSuccess([], 200, 'Item deleted successfully');
|
||||
}
|
||||
|
||||
return $this->respondError('Failed to delete item');
|
||||
}
|
||||
|
||||
public function batchDelete(): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('items')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$data = $this->request->getJSON(true);
|
||||
$ids = $data['ids'] ?? [];
|
||||
|
||||
if (empty($ids)) {
|
||||
return $this->respondError('No item IDs provided');
|
||||
}
|
||||
|
||||
$success = $this->itemModel->delete_list($ids);
|
||||
|
||||
if ($success) {
|
||||
return $this->respondSuccess([], 200, 'Items deleted successfully');
|
||||
}
|
||||
|
||||
return $this->respondError('Failed to delete items');
|
||||
}
|
||||
|
||||
public function quantities($id = null): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('items')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$item = $this->itemModel->find($id);
|
||||
|
||||
if (!$item || $item->deleted) {
|
||||
return $this->respondNotFound('Item not found');
|
||||
}
|
||||
|
||||
$locations = model('App\Models\Stock_location')->get_all();
|
||||
$quantities = [];
|
||||
|
||||
foreach ($locations as $location) {
|
||||
$qty = $this->itemQuantityModel->get_item_quantity($id, $location->location_id);
|
||||
$quantities[] = [
|
||||
'locationId' => (int) $location->location_id,
|
||||
'locationName' => $location->location_name,
|
||||
'quantity' => $qty ? (float) $qty->quantity : 0
|
||||
];
|
||||
}
|
||||
|
||||
return $this->respondSuccess([
|
||||
'itemId' => (int) $id,
|
||||
'quantities' => $quantities
|
||||
]);
|
||||
}
|
||||
|
||||
public function suggest(): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('items')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$term = $this->request->getGet('term');
|
||||
$limit = (int) ($this->request->getGet('limit') ?? 25);
|
||||
|
||||
if (empty($term)) {
|
||||
return $this->respondSuccess(['suggestions' => []]);
|
||||
}
|
||||
|
||||
$suggestions = $this->itemModel->get_search_suggestions($term, $limit);
|
||||
|
||||
return $this->respondSuccess(['suggestions' => $suggestions]);
|
||||
}
|
||||
|
||||
private function mapSortField(string $field): string
|
||||
{
|
||||
$map = [
|
||||
'itemId' => 'item_id',
|
||||
'name' => 'name',
|
||||
'category' => 'category',
|
||||
'costPrice' => 'cost_price',
|
||||
'unitPrice' => 'unit_price'
|
||||
];
|
||||
|
||||
return $map[$field] ?? 'name';
|
||||
}
|
||||
}
|
||||
239
app/Controllers/Api/Suppliers.php
Normal file
239
app/Controllers/Api/Suppliers.php
Normal file
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Api;
|
||||
|
||||
use App\Models\Supplier;
|
||||
use App\Models\Person;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
class Suppliers extends BaseController
|
||||
{
|
||||
protected Supplier $supplierModel;
|
||||
protected Person $personModel;
|
||||
|
||||
protected array $allowedSortFields = ['person_id', 'last_name', 'company_name'];
|
||||
|
||||
public function initController(\CodeIgniter\HTTP\RequestInterface $request, \CodeIgniter\HTTP\ResponseInterface $response, \Psr\Log\LoggerInterface $logger): void
|
||||
{
|
||||
parent::initController($request, $response, $logger);
|
||||
$this->supplierModel = model(Supplier::class);
|
||||
$this->personModel = model(Person::class);
|
||||
}
|
||||
|
||||
public function index(): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('suppliers')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$search = $this->request->getGet('search');
|
||||
$pagination = $this->getPagination();
|
||||
$sort = $this->getSort($this->allowedSortFields, 'companyName');
|
||||
|
||||
$builder = $this->supplierModel->builder();
|
||||
$builder->select('suppliers.*, people.*');
|
||||
$builder->join('people', 'people.person_id = suppliers.person_id');
|
||||
$builder->where('suppliers.deleted', 0);
|
||||
|
||||
if ($search) {
|
||||
$builder->groupStart();
|
||||
$builder->like('people.first_name', $search);
|
||||
$builder->orLike('people.last_name', $search);
|
||||
$builder->orLike('people.email', $search);
|
||||
$builder->orLike('suppliers.account_number', $search);
|
||||
$builder->orLike('suppliers.company_name', $search);
|
||||
$builder->groupEnd();
|
||||
}
|
||||
|
||||
$total = $builder->countAllResults(false);
|
||||
|
||||
$dbSort = $this->mapSortField($sort['sort']);
|
||||
$builder->orderBy($dbSort, $sort['order']);
|
||||
$builder->limit($pagination['limit'], $pagination['offset']);
|
||||
|
||||
$suppliers = $builder->get()->getResultArray();
|
||||
|
||||
return $this->respondSuccess([
|
||||
'total' => $total,
|
||||
'offset' => $pagination['offset'],
|
||||
'limit' => $pagination['limit'],
|
||||
'rows' => $this->transformCollection($suppliers)
|
||||
]);
|
||||
}
|
||||
|
||||
public function show($id = null): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('suppliers')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$supplier = $this->supplierModel->get_info($id);
|
||||
|
||||
if (empty($supplier) || $supplier->deleted) {
|
||||
return $this->respondNotFound('Supplier not found');
|
||||
}
|
||||
|
||||
$person = (array) $this->personModel->get_info($id);
|
||||
$supplier = (array) $supplier;
|
||||
$data = array_merge($person, $supplier);
|
||||
|
||||
return $this->respondSuccess($this->transformItem($data));
|
||||
}
|
||||
|
||||
public function create(): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('suppliers')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$data = $this->request->getJSON(true);
|
||||
if (empty($data)) {
|
||||
$data = $this->request->getPost();
|
||||
}
|
||||
|
||||
$snakeData = [];
|
||||
foreach ($data as $key => $value) {
|
||||
$snakeKey = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $key));
|
||||
$snakeData[$snakeKey] = $value;
|
||||
}
|
||||
|
||||
$personData = array_intersect_key($snakeData, array_flip([
|
||||
'first_name', 'last_name', 'gender', 'phone_number', 'email',
|
||||
'address_1', 'address_2', 'city', 'state', 'zip', 'country', 'comments'
|
||||
]));
|
||||
|
||||
$supplierData = array_intersect_key($snakeData, array_flip([
|
||||
'company_name', 'account_number', 'tax_id', 'agency_name', 'category'
|
||||
]));
|
||||
|
||||
$personId = false;
|
||||
$success = $this->personModel->save_value($personData);
|
||||
|
||||
if ($success && isset($personData['person_id'])) {
|
||||
$personId = $personData['person_id'];
|
||||
$supplierData['person_id'] = $personId;
|
||||
$success = $this->supplierModel->save_value($supplierData);
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
return $this->respondCreated(['id' => $personId], 'Supplier created successfully');
|
||||
}
|
||||
|
||||
return $this->respondError('Failed to create supplier');
|
||||
}
|
||||
|
||||
public function update($id = null): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('suppliers')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$supplier = $this->supplierModel->get_info($id);
|
||||
|
||||
if (empty($supplier) || $supplier->deleted) {
|
||||
return $this->respondNotFound('Supplier not found');
|
||||
}
|
||||
|
||||
$data = $this->request->getJSON(true);
|
||||
if (empty($data)) {
|
||||
$data = $this->request->getRawInput();
|
||||
}
|
||||
|
||||
$snakeData = [];
|
||||
foreach ($data as $key => $value) {
|
||||
$snakeKey = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $key));
|
||||
$snakeData[$snakeKey] = $value;
|
||||
}
|
||||
|
||||
$personData = array_intersect_key($snakeData, array_flip([
|
||||
'first_name', 'last_name', 'gender', 'phone_number', 'email',
|
||||
'address_1', 'address_2', 'city', 'state', 'zip', 'country', 'comments'
|
||||
]));
|
||||
|
||||
$supplierData = array_intersect_key($snakeData, array_flip([
|
||||
'company_name', 'account_number', 'tax_id', 'agency_name', 'category'
|
||||
]));
|
||||
|
||||
if (!empty($personData)) {
|
||||
$this->personModel->save_value($personData, $id);
|
||||
}
|
||||
|
||||
if (!empty($supplierData)) {
|
||||
$this->supplierModel->save_value($supplierData, $id);
|
||||
}
|
||||
|
||||
return $this->respondSuccess([], 200, 'Supplier updated successfully');
|
||||
}
|
||||
|
||||
public function delete($id = null): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('suppliers')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$supplier = $this->supplierModel->get_info($id);
|
||||
|
||||
if (empty($supplier) || $supplier->deleted) {
|
||||
return $this->respondNotFound('Supplier not found');
|
||||
}
|
||||
|
||||
$success = $this->supplierModel->delete($id);
|
||||
|
||||
if ($success) {
|
||||
return $this->respondSuccess([], 200, 'Supplier deleted successfully');
|
||||
}
|
||||
|
||||
return $this->respondError('Failed to delete supplier');
|
||||
}
|
||||
|
||||
public function batchDelete(): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('suppliers')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$data = $this->request->getJSON(true);
|
||||
$ids = $data['ids'] ?? [];
|
||||
|
||||
if (empty($ids)) {
|
||||
return $this->respondError('No supplier IDs provided');
|
||||
}
|
||||
|
||||
$success = $this->supplierModel->delete_list($ids);
|
||||
|
||||
if ($success) {
|
||||
return $this->respondSuccess([], 200, 'Suppliers deleted successfully');
|
||||
}
|
||||
|
||||
return $this->respondError('Failed to delete suppliers');
|
||||
}
|
||||
|
||||
public function suggest(): ResponseInterface
|
||||
{
|
||||
if (!$this->hasPermission('suppliers')) {
|
||||
return $this->respondUnauthorized();
|
||||
}
|
||||
|
||||
$term = $this->request->getGet('term');
|
||||
$limit = (int) ($this->request->getGet('limit') ?? 25);
|
||||
|
||||
if (empty($term)) {
|
||||
return $this->respondSuccess(['suggestions' => []]);
|
||||
}
|
||||
|
||||
$suggestions = $this->supplierModel->get_search_suggestions($term, $limit);
|
||||
|
||||
return $this->respondSuccess(['suggestions' => $suggestions]);
|
||||
}
|
||||
|
||||
private function mapSortField(string $field): string
|
||||
{
|
||||
$map = [
|
||||
'personId' => 'people.person_id',
|
||||
'lastName' => 'people.last_name',
|
||||
'companyName' => 'suppliers.company_name'
|
||||
];
|
||||
|
||||
return $map[$field] ?? 'suppliers.company_name';
|
||||
}
|
||||
}
|
||||
84
app/Controllers/ApiKeys.php
Normal file
84
app/Controllers/ApiKeys.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\ApiKey;
|
||||
use App\Models\Employee;
|
||||
|
||||
class ApiKeys extends Secure_Controller
|
||||
{
|
||||
protected ApiKey $apiKeyModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('api_keys');
|
||||
$this->apiKeyModel = model(ApiKey::class);
|
||||
}
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
$employeeId = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
$keys = $this->apiKeyModel->getKeysForEmployee($employeeId);
|
||||
|
||||
echo view('api_keys/manage', [
|
||||
'keys' => $keys,
|
||||
'employee_info' => $this->employee->get_logged_in_employee_info()
|
||||
]);
|
||||
}
|
||||
|
||||
public function generate(): void
|
||||
{
|
||||
$employeeId = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
$name = $this->request->getPost('name');
|
||||
$expiresAt = $this->request->getPost('expires_at') ?: null;
|
||||
|
||||
$apiKey = $this->apiKeyModel->generateKey($employeeId, $name, $expiresAt);
|
||||
|
||||
if ($apiKey) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => lang('Api_keys.key_generated'),
|
||||
'apiKey' => $apiKey,
|
||||
'keyPrefix' => substr($apiKey, 0, 12) . '...'
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => lang('Api_keys.key_generation_failed')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function revoke(int $apiKeyId): void
|
||||
{
|
||||
$employeeId = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
|
||||
$success = $this->apiKeyModel->revokeKey($apiKeyId, $employeeId);
|
||||
|
||||
echo json_encode([
|
||||
'success' => $success,
|
||||
'message' => $success ? lang('Api_keys.key_revoked') : lang('Api_keys.key_revoke_failed')
|
||||
]);
|
||||
}
|
||||
|
||||
public function regenerate(int $apiKeyId): void
|
||||
{
|
||||
$employeeId = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
|
||||
$newKey = $this->apiKeyModel->regenerateKey($apiKeyId, $employeeId);
|
||||
|
||||
if ($newKey) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => lang('Api_keys.key_regenerated'),
|
||||
'apiKey' => $newKey,
|
||||
'keyPrefix' => substr($newKey, 0, 12) . '...'
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => lang('Api_keys.key_regeneration_failed')
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -462,9 +461,8 @@ class Config extends Secure_Controller
|
||||
public function postSaveLocale(): ResponseInterface
|
||||
{
|
||||
$exploded = explode(":", $this->request->getPost('language'));
|
||||
$currency_symbol = $this->request->getPost('currency_symbol');
|
||||
$batch_save_data = [
|
||||
'currency_symbol' => htmlspecialchars($currency_symbol ?? ''),
|
||||
'currency_symbol' => $this->request->getPost('currency_symbol'),
|
||||
'currency_code' => $this->request->getPost('currency_code'),
|
||||
'language_code' => $exploded[0],
|
||||
'language' => $exploded[1],
|
||||
@@ -977,26 +975,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ class Employees extends Persons
|
||||
$person_info = $this->employee->get_info($employee_id);
|
||||
$current_user = $this->employee->get_logged_in_employee_info();
|
||||
|
||||
if ($employee_id != NEW_ENTRY && !$this->employee->canModifyEmployee($person_info->person_id, $current_user->person_id)) {
|
||||
if ($employee_id != NEW_ENTRY && !$this->employee->can_modify_employee($person_info->person_id, $current_user->person_id)) {
|
||||
header('Location: ' . base_url('no_access/employees/employees'));
|
||||
exit();
|
||||
}
|
||||
@@ -120,7 +120,7 @@ class Employees extends Persons
|
||||
|
||||
if ($employee_id != NEW_ENTRY) {
|
||||
$target_employee = $this->employee->get_info($employee_id);
|
||||
if (!$this->employee->canModifyEmployee($target_employee->person_id, $current_user->person_id)) {
|
||||
if (!$this->employee->can_modify_employee($target_employee->person_id, $current_user->person_id)) {
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Employees.error_updating_admin'),
|
||||
@@ -153,14 +153,14 @@ class Employees extends Persons
|
||||
];
|
||||
|
||||
$grants_array = [];
|
||||
$isAdmin = $this->employee->isAdmin($current_user->person_id);
|
||||
$is_admin = $this->employee->is_admin($current_user->person_id);
|
||||
|
||||
foreach ($this->module->get_all_permissions()->getResult() as $permission) {
|
||||
$grants = [];
|
||||
$grant = $this->request->getPost('grant_' . $permission->permission_id) != null ? $this->request->getPost('grant_' . $permission->permission_id, FILTER_SANITIZE_FULL_SPECIAL_CHARS) : '';
|
||||
|
||||
if ($grant == $permission->permission_id) {
|
||||
if (!$isAdmin && !$this->employee->has_grant($permission->permission_id, $current_user->person_id)) {
|
||||
if (!$is_admin && !$this->employee->has_grant($permission->permission_id, $current_user->person_id)) {
|
||||
continue;
|
||||
}
|
||||
$grants['permission_id'] = $permission->permission_id;
|
||||
@@ -226,9 +226,9 @@ class Employees extends Persons
|
||||
$employees_to_delete = $this->request->getPost('ids', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$current_user = $this->employee->get_logged_in_employee_info();
|
||||
|
||||
if (!$this->employee->isAdmin($current_user->person_id)) {
|
||||
if (!$this->employee->is_admin($current_user->person_id)) {
|
||||
foreach ($employees_to_delete as $emp_id) {
|
||||
if ($this->employee->isAdmin((int)$emp_id)) {
|
||||
if ($this->employee->is_admin((int)$emp_id)) {
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Employees.error_deleting_admin')]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,21 +36,12 @@ 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 $employee_id = -1): string // TODO: Replace -1 with a constant
|
||||
{
|
||||
$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'));
|
||||
}
|
||||
|
||||
$person_info = $this->employee->get_info($employeeId);
|
||||
$person_info = $this->employee->get_info($employee_id);
|
||||
foreach (get_object_vars($person_info) as $property => $value) {
|
||||
$person_info->$property = $value;
|
||||
}
|
||||
@@ -64,20 +55,9 @@ class Home extends Secure_Controller
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function postSave(int $employeeId = NEW_ENTRY): ResponseInterface
|
||||
public function postSave(int $employee_id = -1): ResponseInterface // TODO: Replace -1 with a constant
|
||||
{
|
||||
$currentUser = $this->employee->get_logged_in_employee_info();
|
||||
|
||||
$employeeId = $employeeId === NEW_ENTRY ? $currentUser->person_id : $employeeId;
|
||||
|
||||
if (!$this->employee->isAdmin($currentUser->person_id) && $employeeId !== $currentUser->person_id) {
|
||||
return $this->response->setStatusCode(403)->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Employees.unauthorized_modify')
|
||||
]);
|
||||
}
|
||||
|
||||
if (!empty($this->request->getPost('current_password')) && $employeeId != NEW_ENTRY) {
|
||||
if (!empty($this->request->getPost('current_password')) && $employee_id != -1) {
|
||||
if ($this->employee->check_password($this->request->getPost('username', FILTER_SANITIZE_FULL_SPECIAL_CHARS), $this->request->getPost('current_password'))) {
|
||||
// Validate password length BEFORE hashing
|
||||
$new_password = $this->request->getPost('password');
|
||||
@@ -86,7 +66,7 @@ class Home extends Secure_Controller
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Employees.password_minlength'),
|
||||
'id' => NEW_ENTRY
|
||||
'id' => -1
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -96,32 +76,32 @@ class Home extends Secure_Controller
|
||||
'hash_version' => 2
|
||||
];
|
||||
|
||||
if ($this->employee->change_password($employee_data, $employeeId)) {
|
||||
if ($this->employee->change_password($employee_data, $employee_id)) {
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => lang('Employees.successful_change_password'),
|
||||
'id' => $employeeId
|
||||
'id' => $employee_id
|
||||
]);
|
||||
} else {
|
||||
} else { // Failure // TODO: Replace -1 with constant
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Employees.unsuccessful_change_password'),
|
||||
'id' => NEW_ENTRY
|
||||
'id' => -1
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
} else { // TODO: Replace -1 with constant
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Employees.current_password_invalid'),
|
||||
'id' => NEW_ENTRY
|
||||
'id' => -1
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
} else { // TODO: Replace -1 with constant
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Employees.current_password_invalid'),
|
||||
'id' => NEW_ENTRY
|
||||
'id' => -1
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -104,7 +96,7 @@ class Items extends Secure_Controller
|
||||
**/
|
||||
public function getSearch(): ResponseInterface
|
||||
{
|
||||
$search = $this->request->getGet('search', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$search = $this->request->getGet('search');
|
||||
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
||||
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
||||
$sort = $this->sanitizeSortColumn(item_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'item_id');
|
||||
@@ -156,7 +148,6 @@ class Items extends Secure_Controller
|
||||
{
|
||||
helper('file');
|
||||
|
||||
$pic_filename = rawurldecode($pic_filename);
|
||||
$file_extension = pathinfo($pic_filename, PATHINFO_EXTENSION);
|
||||
$images = glob("./uploads/item_pics/$pic_filename");
|
||||
$base_path = './uploads/item_pics/' . pathinfo($pic_filename, PATHINFO_FILENAME);
|
||||
@@ -386,7 +377,7 @@ class Items extends Secure_Controller
|
||||
} else {
|
||||
$images = glob("./uploads/item_pics/$item_info->pic_filename");
|
||||
}
|
||||
$data['image_path'] = sizeof($images) > 0 ? base_url(implode('/', array_map('rawurlencode', explode('/', ltrim($images[0], './'))))) : '';
|
||||
$data['image_path'] = sizeof($images) > 0 ? base_url($images[0]) : '';
|
||||
} else {
|
||||
$data['image_path'] = '';
|
||||
}
|
||||
@@ -626,7 +617,7 @@ class Items extends Secure_Controller
|
||||
// Save item data
|
||||
$item_data = [
|
||||
'name' => $this->request->getPost('name'),
|
||||
'description' => $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
'description' => $this->request->getPost('description'),
|
||||
'category' => $this->request->getPost('category'),
|
||||
'item_type' => $item_type,
|
||||
'stock_type' => $this->request->getPost('stock_type') === null ? HAS_STOCK : intval($this->request->getPost('stock_type')),
|
||||
@@ -777,13 +768,10 @@ class Items extends Secure_Controller
|
||||
|
||||
$filename = $file->getClientName();
|
||||
$info = pathinfo($filename);
|
||||
|
||||
// Sanitize filename to remove problematic characters like spaces
|
||||
$sanitized_name = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $info['filename']);
|
||||
|
||||
$file_info = [
|
||||
'orig_name' => $filename,
|
||||
'raw_name' => $sanitized_name,
|
||||
'raw_name' => $info['filename'],
|
||||
'file_ext' => $file->guessExtension()
|
||||
];
|
||||
|
||||
@@ -884,12 +872,12 @@ class Items extends Secure_Controller
|
||||
$items_to_update = $this->request->getPost('item_ids');
|
||||
$item_data = [];
|
||||
|
||||
foreach (Item::ALLOWED_BULK_EDIT_FIELDS as $field) {
|
||||
$value = $this->request->getPost($field);
|
||||
if ($field === 'supplier_id' && $value !== '') {
|
||||
$item_data[$field] = $value;
|
||||
} elseif ($value !== null && $value !== '') {
|
||||
$item_data[$field] = $value;
|
||||
foreach ($_POST as $key => $value) {
|
||||
// This field is nullable, so treat it differently
|
||||
if ($key === 'supplier_id' && $value !== '') {
|
||||
$item_data[$key] = $value;
|
||||
} elseif ($value !== '' && !(in_array($key, ['item_ids', 'tax_names', 'tax_percents']))) {
|
||||
$item_data[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Payments;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\Payments\PaymentProviderRegistry;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
class Webhook extends BaseController
|
||||
{
|
||||
public function handle(string $providerId): ResponseInterface
|
||||
{
|
||||
$provider = PaymentProviderRegistry::getInstance()->getProvider($providerId);
|
||||
|
||||
if ($provider === null) {
|
||||
log_message('error', "Webhook received for unknown provider: {$providerId}");
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'success' => false,
|
||||
'error' => 'Provider not found'
|
||||
]);
|
||||
}
|
||||
|
||||
$rawInput = $this->request->getBody();
|
||||
$data = json_decode($rawInput, true) ?? [];
|
||||
|
||||
if (empty($rawInput)) {
|
||||
$data = $this->request->getPost();
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $provider->processCallback($data);
|
||||
|
||||
if ($result['success'] ?? false) {
|
||||
log_message('info', "Webhook processed successfully for provider: {$providerId}", $result);
|
||||
return $this->response->setStatusCode(200)->setJSON($result);
|
||||
}
|
||||
|
||||
log_message('warning', "Webhook processing failed for provider: {$providerId}", $result);
|
||||
return $this->response->setStatusCode(400)->setJSON($result);
|
||||
} catch (\Exception $e) {
|
||||
log_message('error', "Webhook exception for provider {$providerId}: " . $e->getMessage());
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'success' => false,
|
||||
'error' => 'Internal server error'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function status(string $providerId, string $transactionId): ResponseInterface
|
||||
{
|
||||
$provider = PaymentProviderRegistry::getInstance()->getProvider($providerId);
|
||||
|
||||
if ($provider === null) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'success' => false,
|
||||
'error' => 'Provider not found'
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $provider->getPaymentStatus($transactionId);
|
||||
return $this->response->setStatusCode(200)->setJSON($result);
|
||||
} catch (\Exception $e) {
|
||||
log_message('error', "Status check exception for provider {$providerId}: " . $e->getMessage());
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'success' => false,
|
||||
'error' => 'Internal server error'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -20,7 +20,6 @@ use App\Models\Stock_location;
|
||||
use App\Models\Tokens\Token_invoice_count;
|
||||
use App\Models\Tokens\Token_customer;
|
||||
use App\Models\Tokens\Token_invoice_sequence;
|
||||
use CodeIgniter\Events\Events;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\Services;
|
||||
use Config\OSPOS;
|
||||
@@ -76,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();
|
||||
|
||||
@@ -93,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);
|
||||
}
|
||||
@@ -156,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)
|
||||
];
|
||||
@@ -472,13 +457,6 @@ class Sales extends Secure_Controller
|
||||
}
|
||||
}
|
||||
|
||||
Events::trigger('payment_initiated', [
|
||||
'payment_type' => $payment_type,
|
||||
'amount' => $amount_tendered ?? 0,
|
||||
'sale_id' => $this->sale_lib->get_sale_id(),
|
||||
'customer_id' => $this->sale_lib->get_customer(),
|
||||
]);
|
||||
|
||||
return $this->_reload($data);
|
||||
}
|
||||
|
||||
@@ -794,18 +772,8 @@ class Sales extends Secure_Controller
|
||||
$data['error_message'] = lang('Sales.transaction_failed');
|
||||
} else {
|
||||
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
|
||||
|
||||
Events::trigger('sale_completed', [
|
||||
'sale_id' => $data['sale_id_num'],
|
||||
'customer_id' => $customer_id,
|
||||
'employee_id' => $employee_id,
|
||||
'total' => $data['total'],
|
||||
'payments' => $data['payments'],
|
||||
'sale_type' => $sale_type,
|
||||
]);
|
||||
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/' . $invoice_view, $data);
|
||||
$this->sale_lib->clear_all();
|
||||
}
|
||||
}
|
||||
} elseif ($this->sale_lib->is_work_order_mode()) {
|
||||
@@ -838,8 +806,9 @@ class Sales extends Secure_Controller
|
||||
|
||||
$data['barcode'] = null;
|
||||
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/work_order', $data);
|
||||
$this->sale_lib->clear_mode();
|
||||
$this->sale_lib->clear_all();
|
||||
}
|
||||
} elseif ($this->sale_lib->is_quote_mode()) {
|
||||
$data['sales_quote'] = lang('Sales.quote');
|
||||
@@ -865,8 +834,9 @@ class Sales extends Secure_Controller
|
||||
$data['cart'] = $this->sale_lib->sort_and_filter_cart($data['cart']);
|
||||
$data['barcode'] = null;
|
||||
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/quote', $data);
|
||||
$this->sale_lib->clear_mode();
|
||||
$this->sale_lib->clear_all();
|
||||
}
|
||||
} else {
|
||||
// Save the data to the sales table
|
||||
@@ -887,18 +857,8 @@ class Sales extends Secure_Controller
|
||||
$data['error_message'] = lang('Sales.transaction_failed');
|
||||
} else {
|
||||
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
|
||||
|
||||
Events::trigger('sale_completed', [
|
||||
'sale_id' => $data['sale_id_num'],
|
||||
'customer_id' => $customer_id,
|
||||
'employee_id' => $employee_id,
|
||||
'total' => $data['total'],
|
||||
'payments' => $data['payments'],
|
||||
'sale_type' => $sale_type,
|
||||
]);
|
||||
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/receipt', $data);
|
||||
$this->sale_lib->clear_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
96
app/Database/Migrations/20250310000000_ApiKeys.php
Normal file
96
app/Database/Migrations/20250310000000_ApiKeys.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class ApiKeys extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
'api_key_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true
|
||||
],
|
||||
'employee_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 10
|
||||
],
|
||||
'key_hash' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 64
|
||||
],
|
||||
'key_prefix' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 12
|
||||
],
|
||||
'name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true
|
||||
],
|
||||
'last_used' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true
|
||||
],
|
||||
'created' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true
|
||||
],
|
||||
'expires_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true
|
||||
],
|
||||
'disabled' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 0
|
||||
]
|
||||
]);
|
||||
|
||||
$this->forge->addKey('api_key_id', true);
|
||||
$this->forge->addKey('employee_id');
|
||||
$this->forge->addKey('key_hash');
|
||||
|
||||
$this->forge->createTable('api_keys', true);
|
||||
|
||||
$this->db->query(
|
||||
'ALTER TABLE ' . $this->db->prefixTable('api_keys') .
|
||||
' ADD CONSTRAINT ' . $this->db->prefixTable('api_keys') . '_employee_id_foreign' .
|
||||
' FOREIGN KEY (employee_id) REFERENCES ' . $this->db->prefixTable('employees') .
|
||||
' (person_id) ON DELETE CASCADE ON UPDATE CASCADE'
|
||||
);
|
||||
|
||||
$this->db->query(
|
||||
'INSERT INTO ' . $this->db->prefixTable('permissions') . ' (permission_id, module_id)' .
|
||||
" VALUES ('api_keys', 'office') ON DUPLICATE KEY UPDATE permission_id = 'api_keys'"
|
||||
);
|
||||
|
||||
$this->db->query(
|
||||
'INSERT INTO ' . $this->db->prefixTable('modules') . ' (module_id, name_lang_key, desc_lang_key, sort)' .
|
||||
" VALUES ('api_keys', 'module_api_keys', 'module_desc_api_keys', 25)" .
|
||||
" ON DUPLICATE KEY UPDATE module_id = 'module_id'"
|
||||
);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->db->query(
|
||||
'ALTER TABLE ' . $this->db->prefixTable('api_keys') .
|
||||
' DROP FOREIGN KEY ' . $this->db->prefixTable('api_keys') . '_employee_id_foreign'
|
||||
);
|
||||
|
||||
$this->db->query(
|
||||
'DELETE FROM ' . $this->db->prefixTable('permissions') . " WHERE permission_id = 'api_keys'"
|
||||
);
|
||||
|
||||
$this->db->query(
|
||||
'DELETE FROM ' . $this->db->prefixTable('modules') . " WHERE module_id = 'api_keys'"
|
||||
);
|
||||
|
||||
$this->forge->dropTable('api_keys', true);
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
/**
|
||||
* Migration to sanitize existing image filenames by replacing spaces with underscores
|
||||
* This fixes issue #4372 where thumbnails failed to load for images with spaces in filenames
|
||||
*/
|
||||
class FixImageFilenameSpaces extends Migration
|
||||
{
|
||||
/**
|
||||
* Perform a migration.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $db->table('ospos_items');
|
||||
|
||||
// Get all items with pic_filename containing spaces
|
||||
$query = $builder->like('pic_filename', ' ', 'both')->get();
|
||||
$items = $query->getResult();
|
||||
|
||||
foreach ($items as $item) {
|
||||
$old_filename = $item->pic_filename;
|
||||
$ext = pathinfo($old_filename, PATHINFO_EXTENSION);
|
||||
$base_name = pathinfo($old_filename, PATHINFO_FILENAME);
|
||||
|
||||
// Sanitize the filename by replacing spaces and special characters
|
||||
$sanitized_name = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $base_name);
|
||||
$new_filename = $sanitized_name . '.' . $ext;
|
||||
|
||||
// Rename the file on the filesystem
|
||||
$old_path = FCPATH . 'uploads/item_pics/' . $old_filename;
|
||||
$new_path = FCPATH . 'uploads/item_pics/' . $new_filename;
|
||||
|
||||
if (file_exists($old_path)) {
|
||||
// Rename the original file
|
||||
if (rename($old_path, $new_path)) {
|
||||
// Check if thumbnail exists and rename it too
|
||||
$old_thumb = FCPATH . 'uploads/item_pics/' . $base_name . '_thumb.' . $ext;
|
||||
$new_thumb = FCPATH . 'uploads/item_pics/' . $sanitized_name . '_thumb.' . $ext;
|
||||
if (file_exists($old_thumb)) {
|
||||
rename($old_thumb, $new_thumb);
|
||||
}
|
||||
|
||||
// Update database record
|
||||
$builder->where('item_id', $item->item_id)
|
||||
->update(['pic_filename' => $new_filename]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert a migration.
|
||||
* Note: This migration does not support rollback as the original filenames are lost
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// This migration cannot be safely reversed as the original filenames are lost
|
||||
// after sanitization.
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class Migration_PaymentTransactions extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$forge = \Config\Services::forge();
|
||||
|
||||
$forge->addField([
|
||||
'id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true
|
||||
],
|
||||
'provider_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 100,
|
||||
'null' => false
|
||||
],
|
||||
'sale_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'null' => true
|
||||
],
|
||||
'transaction_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => false
|
||||
],
|
||||
'amount' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '15,2',
|
||||
'null' => false
|
||||
],
|
||||
'currency' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 3,
|
||||
'default' => 'USD',
|
||||
'null' => false
|
||||
],
|
||||
'status' => [
|
||||
'type' => 'ENUM',
|
||||
'constraint' => ['pending', 'authorized', 'completed', 'failed', 'refunded', 'cancelled'],
|
||||
'default' => 'pending',
|
||||
'null' => false
|
||||
],
|
||||
'metadata' => [
|
||||
'type' => 'JSON',
|
||||
'null' => true
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'TIMESTAMP',
|
||||
'null' => true
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'TIMESTAMP',
|
||||
'null' => true
|
||||
]
|
||||
]);
|
||||
|
||||
$forge->addKey('id', true);
|
||||
$forge->addKey('provider_id');
|
||||
$forge->addKey('sale_id');
|
||||
$forge->addKey('transaction_id');
|
||||
$forge->addKey('status');
|
||||
|
||||
$forge->createTable('payment_transactions', true);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$forge = \Config\Services::forge();
|
||||
$forge->dropTable('payment_transactions', true);
|
||||
}
|
||||
}
|
||||
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`);
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Libraries\Payments\PaymentProviderRegistry;
|
||||
use CodeIgniter\Events\Events;
|
||||
use Config\Services;
|
||||
|
||||
class PaymentEvents
|
||||
{
|
||||
public static function initialize(): void
|
||||
{
|
||||
Events::on('payment_initiated', [static::class, 'onPaymentInitiated']);
|
||||
Events::on('payment_completed', [static::class, 'onPaymentCompleted']);
|
||||
Events::on('payment_failed', [static::class, 'onPaymentFailed']);
|
||||
Events::on('sale_completed', [static::class, 'onSaleCompleted']);
|
||||
}
|
||||
|
||||
public static function onPaymentInitiated(array $data): void
|
||||
{
|
||||
log_message('debug', sprintf(
|
||||
'Payment initiated: type=%s, amount=%s, sale_id=%s',
|
||||
$data['payment_type'] ?? 'unknown',
|
||||
$data['amount'] ?? 0,
|
||||
$data['sale_id'] ?? 'pending'
|
||||
));
|
||||
}
|
||||
|
||||
public static function onPaymentCompleted(array $data): void
|
||||
{
|
||||
log_message('debug', sprintf(
|
||||
'Payment completed: type=%s, amount=%s, sale_id=%s',
|
||||
$data['payment_type'] ?? 'unknown',
|
||||
$data['amount'] ?? 0,
|
||||
$data['sale_id'] ?? 'pending'
|
||||
));
|
||||
}
|
||||
|
||||
public static function onPaymentFailed(array $data): void
|
||||
{
|
||||
log_message('warning', sprintf(
|
||||
'Payment failed: type=%s, amount=%s, error=%s',
|
||||
$data['payment_type'] ?? 'unknown',
|
||||
$data['amount'] ?? 0,
|
||||
$data['error'] ?? 'unknown error'
|
||||
));
|
||||
}
|
||||
|
||||
public static function onSaleCompleted(array $data): void
|
||||
{
|
||||
log_message('info', sprintf(
|
||||
'Sale completed: sale_id=%s, total=%s, payments=%s',
|
||||
$data['sale_id'] ?? 'unknown',
|
||||
$data['total'] ?? 0,
|
||||
json_encode($data['payments'] ?? [])
|
||||
));
|
||||
}
|
||||
}
|
||||
48
app/Filters/ApiAuth.php
Normal file
48
app/Filters/ApiAuth.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filters;
|
||||
|
||||
use App\Models\ApiKey;
|
||||
use CodeIgniter\Filters\FilterInterface;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\Services;
|
||||
|
||||
class ApiAuth implements FilterInterface
|
||||
{
|
||||
public function before(RequestInterface $request, $arguments = null): mixed
|
||||
{
|
||||
$apiKey = $request->getHeaderLine('X-API-Key');
|
||||
|
||||
if (empty($apiKey)) {
|
||||
return $this->unauthorized('API key required');
|
||||
}
|
||||
|
||||
$apiKeyModel = model(ApiKey::class);
|
||||
$employeeId = $apiKeyModel->validateKey($apiKey);
|
||||
|
||||
if (!$employeeId) {
|
||||
return $this->unauthorized('Invalid or expired API key');
|
||||
}
|
||||
|
||||
$request->employeeId = $employeeId;
|
||||
Services::set('apiEmployeeId', $employeeId);
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null): mixed
|
||||
{
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function unauthorized(string $message): ResponseInterface
|
||||
{
|
||||
return Services::response()
|
||||
->setStatusCode(401)
|
||||
->setJSON([
|
||||
'success' => false,
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Employee;
|
||||
use CodeIgniter\Events\Events;
|
||||
use Config\OSPOS;
|
||||
|
||||
/**
|
||||
@@ -277,12 +276,6 @@ function get_payment_options(): array
|
||||
$payments[lang('Sales.upi')] = lang('Sales.upi');
|
||||
}
|
||||
|
||||
// Allow payment provider plugins to add additional payment options
|
||||
$eventPayments = Events::trigger('payment_options', $payments);
|
||||
if (is_array($eventPayments)) {
|
||||
return $eventPayments;
|
||||
}
|
||||
|
||||
return $payments;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Libraries\Payments\PaymentProviderRegistry;
|
||||
use CodeIgniter\Events\Events;
|
||||
|
||||
if (!function_exists('register_payment_provider')) {
|
||||
function register_payment_provider(App\Libraries\Payments\PaymentProviderInterface $provider): void
|
||||
{
|
||||
PaymentProviderRegistry::getInstance()->register($provider);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_payment_providers')) {
|
||||
function get_payment_providers(): array
|
||||
{
|
||||
return PaymentProviderRegistry::getInstance()->getProviders();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_enabled_payment_providers')) {
|
||||
function get_enabled_payment_providers(): array
|
||||
{
|
||||
return PaymentProviderRegistry::getInstance()->getEnabledProviders();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_enabled_payment_types')) {
|
||||
function get_enabled_payment_types(): array
|
||||
{
|
||||
return PaymentProviderRegistry::getInstance()->getEnabledPaymentTypes();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_payment_provider')) {
|
||||
function get_payment_provider(string $providerId): ?App\Libraries\Payments\PaymentProviderInterface
|
||||
{
|
||||
return PaymentProviderRegistry::getInstance()->getProvider($providerId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_payment_provider_for_type')) {
|
||||
function get_payment_provider_for_type(string $paymentTypeKey): ?App\Libraries\Payments\PaymentProviderInterface
|
||||
{
|
||||
return PaymentProviderRegistry::getInstance()->getProviderForPaymentType($paymentTypeKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('payment_provider_content')) {
|
||||
function payment_provider_content(string $section, array $data = []): string
|
||||
{
|
||||
$results = Events::trigger("payment_view:{$section}", $data);
|
||||
$output = '';
|
||||
if (is_array($results)) {
|
||||
foreach ($results as $result) {
|
||||
if (is_string($result)) {
|
||||
$output .= $result;
|
||||
}
|
||||
}
|
||||
} elseif (is_string($results)) {
|
||||
$output = $results;
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ function transform_headers(array $headers, bool $readonly = false, bool $editabl
|
||||
'field' => key($element),
|
||||
'title' => current($element),
|
||||
'switchable' => $element['switchable'] ?? !preg_match('(^$| )', current($element)),
|
||||
'escape' => !preg_match("/(edit|email|messages|item_pic)/", key($element)) && !(isset($element['escape']) && !$element['escape']),
|
||||
'escape' => !preg_match("/(edit|email|messages|item_pic|customer_name|note)/", key($element)) && !(isset($element['escape']) && !$element['escape']),
|
||||
'sortable' => $element['sortable'] ?? current($element) != '',
|
||||
'checkbox' => $element['checkbox'] ?? false,
|
||||
'class' => isset($element['checkbox']) || preg_match('(^$| )', current($element)) ? 'print_hide' : '',
|
||||
@@ -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];
|
||||
@@ -470,8 +470,7 @@ function get_item_data_row(object $item): array
|
||||
: glob("./uploads/item_pics/$item->pic_filename");
|
||||
|
||||
if (sizeof($images) > 0) {
|
||||
$image_path = ltrim($images[0], './');
|
||||
$image .= '<a class="rollover" href="' . base_url(implode('/', array_map('rawurlencode', explode('/', $image_path)))) . '"><img alt="Image thumbnail" src="' . site_url('items/PicThumb/' . rawurlencode(pathinfo($images[0], PATHINFO_BASENAME))) . '"></a>';
|
||||
$image .= '<a class="rollover" href="' . base_url($images[0]) . '"><img alt="Image thumbnail" src="' . site_url('items/PicThumb/' . pathinfo($images[0], PATHINFO_BASENAME)) . '"></a>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,7 +478,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 +633,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 +650,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 +924,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" => "المجموعة",
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"su" => "أحد",
|
||||
"mo" => "اثنين",
|
||||
"tu" => "ثلاثاء",
|
||||
"we" => "أربعاء",
|
||||
"th" => "خميس",
|
||||
"fr" => "جمعة",
|
||||
"sa" => "سبت",
|
||||
"sun" => "الأحد",
|
||||
"mon" => "الاثنين",
|
||||
"tue" => "الثلاثاء",
|
||||
"wed" => "الأربعاء",
|
||||
"thu" => "الخميس",
|
||||
"fri" => "الجمعة",
|
||||
"sat" => "السبت",
|
||||
"sunday" => "الأحد",
|
||||
"monday" => "الاثنين",
|
||||
"tuesday" => "الثلاثاء",
|
||||
"wednesday" => "الأربعاء",
|
||||
"thursday" => "الخميس",
|
||||
"friday" => "الجمعة",
|
||||
"saturday" => "السبت",
|
||||
"jan" => "يناير",
|
||||
"feb" => "فبراير",
|
||||
"mar" => "مارس",
|
||||
"apr" => "أبريل",
|
||||
"may" => "مايو",
|
||||
"jun" => "يونيو",
|
||||
"jul" => "يوليو",
|
||||
"aug" => "أغسطس",
|
||||
"sep" => "سبتمبر",
|
||||
"oct" => "أكتوبر",
|
||||
"nov" => "نوفمبر",
|
||||
"dec" => "ديسمبر",
|
||||
"january" => "يناير",
|
||||
"february" => "فبراير",
|
||||
"march" => "مارس",
|
||||
"april" => "أبريل",
|
||||
"mayl" => "مايو",
|
||||
"june" => "يونيو",
|
||||
"july" => "يوليو",
|
||||
"august" => "أغسطس",
|
||||
"september" => "سبتمبر",
|
||||
"october" => "أكتوبر",
|
||||
"november" => "نوفمبر",
|
||||
"december" => "ديسمبر",
|
||||
];
|
||||
@@ -14,8 +14,6 @@ return [
|
||||
"current_password_invalid" => "كلمة المرور الحالية غير صحيحة.",
|
||||
"employee" => "موظف",
|
||||
"error_adding_updating" => "خطاء فى إضافة/تعديل موظف.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "لايمكن حذف المستخدم admin الخاص بنسخة العرض.",
|
||||
"error_updating_demo_admin" => "لايمكن تغيير بيانات المستخدم admin الخاص بنسخة العرض.",
|
||||
"language" => "اللغة",
|
||||
|
||||
@@ -26,7 +26,6 @@ return [
|
||||
"cost_price_required" => "سعر التكلفة مطلوب.",
|
||||
"count" => "تحديث المخزون",
|
||||
"csv_import_failed" => "فشل الإستيراد من اكسل",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "الملف الذى رفعته إما فارغ أو أنه مختلف البنية.",
|
||||
"csv_import_partially_failed" => "يوجد خطأ بنسبة {0} في استيراد الاصناف في السطر: {1}. لم يتم استيرادهم.",
|
||||
"csv_import_success" => "تم استيراد الأصناف بنجاح.",
|
||||
|
||||
@@ -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" => "المجموعة",
|
||||
|
||||
@@ -14,8 +14,6 @@ return [
|
||||
"current_password_invalid" => "كلمة المرور الحالية غير صحيحة.",
|
||||
"employee" => "موظف",
|
||||
"error_adding_updating" => "خطاء فى إضافة/تعديل موظف.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "لايمكن حذف المستخدم admin الخاص بنسخة العرض.",
|
||||
"error_updating_demo_admin" => "لايمكن تغيير بيانات المستخدم admin الخاص بنسخة العرض.",
|
||||
"language" => "اللغة",
|
||||
|
||||
@@ -26,7 +26,6 @@ return [
|
||||
"cost_price_required" => "سعر التكلفة مطلوب.",
|
||||
"count" => "تحديث المخزون",
|
||||
"csv_import_failed" => "فشل الإستيراد من اكسل",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "الملف الذى رفعته إما فارغ أو أنه مختلف البنية.",
|
||||
"csv_import_partially_failed" => "يوجد خطأ بنسبة {0} في استيراد الاصناف في السطر: {1}. لم يتم استيرادهم.",
|
||||
"csv_import_success" => "تم استيراد الأصناف بنجاح.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -14,8 +14,6 @@ return [
|
||||
"current_password_invalid" => "Hazirki Şifrə düzgün deyil.",
|
||||
"employee" => "Əməkdaş",
|
||||
"error_adding_updating" => "Əməkdaş əlavə etməsk və ya yeniləməsi baş vermədi.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Demo administrator istifadəçisini silə bilməzsiniz.",
|
||||
"error_updating_demo_admin" => "Demo administrator istifadəçisini dəyişə bilməzsiniz.",
|
||||
"language" => "Dil",
|
||||
|
||||
@@ -26,7 +26,6 @@ return [
|
||||
"cost_price_required" => "Topdan satiış - doldurulması vacib sahə.",
|
||||
"count" => "inventorun yenilənməsi",
|
||||
"csv_import_failed" => "səhv csv import",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "Yüklənmiş faylda məlumat yoxdur və ya düzgün formatlanmır.",
|
||||
"csv_import_partially_failed" => "Xətlərdə {0} element idxalı uğursuzluq (lar) var: {1}. Heç bir sıra idxal edilmədi.",
|
||||
"csv_import_success" => "Malların İdxalı Uğurla Həyata Keçdi.",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "",
|
||||
"confirm_restore" => "",
|
||||
"definition_cannot_be_deleted" => "",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "",
|
||||
"definition_flags" => "",
|
||||
"definition_group" => "",
|
||||
|
||||
@@ -14,8 +14,6 @@ return [
|
||||
"current_password_invalid" => "Текущата парола е невалидна.",
|
||||
"employee" => "Служител",
|
||||
"error_adding_updating" => "Добавянето или актуализирането на служителите е неуспешно.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Не може да изтриете Пробният Администратор.",
|
||||
"error_updating_demo_admin" => "Не може да промените Пробният Администратор.",
|
||||
"language" => "Език",
|
||||
|
||||
@@ -26,7 +26,6 @@ return [
|
||||
"cost_price_required" => "Wholesale Price is a required field.",
|
||||
"count" => "Update Inventory",
|
||||
"csv_import_failed" => "CSV import failed",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "The uploaded file has no data or is formatted incorrectly.",
|
||||
"csv_import_partially_failed" => "Item import successful with some failures:",
|
||||
"csv_import_success" => "Item import successful.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -14,8 +14,6 @@ return [
|
||||
"current_password_invalid" => "Trenutna lozinka je nevažeća.",
|
||||
"employee" => "Zaposlenik",
|
||||
"error_adding_updating" => "Dodavanje ili ažuriranje zaposlenika nije uspjelo.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Ne možete izbrisati demo korisnika administratora.",
|
||||
"error_updating_demo_admin" => "Ne možete promijeniti korisnika demo administratora.",
|
||||
"language" => "Jezik",
|
||||
|
||||
@@ -26,7 +26,6 @@ return [
|
||||
"cost_price_required" => "Fakturna cijena je obavezno polje.",
|
||||
"count" => "Ažuriraj zalihu",
|
||||
"csv_import_failed" => "Uvoz CSV-a nije uspio",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "Učitana CSV datoteka nema podatke ili je pogrešno formatirana.",
|
||||
"csv_import_partially_failed" => "Bilo je {0} grešaka pri uvozu stavke na liniji: {1}. Nijedan red nije uvezen.",
|
||||
"csv_import_success" => "Uvoz CSV stavke je uspješan.",
|
||||
|
||||
@@ -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" => "گروپ",
|
||||
|
||||
@@ -14,8 +14,6 @@ return [
|
||||
'current_password_invalid' => "وشەی نهێنی ئێستا نادروستە.",
|
||||
'employee' => "فەرمانبەر",
|
||||
'error_adding_updating' => "زیادکردن یان نوێکردنەوەی کارمەند سەرکەوتوو نەبوو.",
|
||||
'error_deleting_admin' => "",
|
||||
'error_updating_admin' => "",
|
||||
'error_deleting_demo_admin' => "ناتوانیت بەکارهێنەری ئەدمینی تاقیکردنەوەیی بسڕیتەوە.",
|
||||
'error_updating_demo_admin' => "ناتوانیت بەکارهێنەری ئەدمین تاقیکردنەوەیی بگۆڕیت.",
|
||||
'language' => "زمان",
|
||||
|
||||
@@ -26,7 +26,6 @@ return [
|
||||
'cost_price_required' => "نرخی جوملە خانەیەکی پێویستە.",
|
||||
'count' => "جەرد نوێ بکەوە",
|
||||
'csv_import_failed' => "هاوردەکردنی CSV سەرکەوتوو نەبوو",
|
||||
'csv_import_invalid_location' => "",
|
||||
'csv_import_nodata_wrongformat' => "پەڕگەی CSV بارکراو هیچ داتایەکی نییە یان بە هەڵە فۆرمات کراوە.",
|
||||
'csv_import_partially_failed' => "{0} شکستی هاوردەکردنی بابەتی لەسەر هێڵەکان هەبوو: {1}. هیچ ڕیزێک هاوردە نەکرا.",
|
||||
'csv_import_success' => "بابەتی هاوردەکردنی CSV سەرکەوتوو بوو.",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "",
|
||||
"confirm_restore" => "",
|
||||
"definition_cannot_be_deleted" => "",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "",
|
||||
"definition_flags" => "",
|
||||
"definition_group" => "",
|
||||
|
||||
@@ -14,8 +14,6 @@ return [
|
||||
"current_password_invalid" => "",
|
||||
"employee" => "",
|
||||
"error_adding_updating" => "",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "",
|
||||
"error_updating_demo_admin" => "",
|
||||
"language" => "",
|
||||
|
||||
@@ -26,7 +26,6 @@ return [
|
||||
"cost_price_required" => "Musíte zadat nákupní cenu.",
|
||||
"count" => "Upravit množství",
|
||||
"csv_import_failed" => "Import z CSVu se nepovedl",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "Nahraný soubor neobsahuje žádná data nebo má špatný formát.",
|
||||
"csv_import_partially_failed" => "Při importu položek došlo k několika chybám:",
|
||||
"csv_import_success" => "Import položek proběhl bez chyby.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -14,8 +14,6 @@ return [
|
||||
"current_password_invalid" => "Current Password is invalid.",
|
||||
"employee" => "Employee",
|
||||
"error_adding_updating" => "Employee add or update failed.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "You can not delete the demo admin user.",
|
||||
"error_updating_demo_admin" => "You can not change the demo admin user.",
|
||||
"language" => "Language",
|
||||
|
||||
@@ -26,7 +26,6 @@ return [
|
||||
"cost_price_required" => "",
|
||||
"count" => "",
|
||||
"csv_import_failed" => "",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "",
|
||||
"csv_import_partially_failed" => "",
|
||||
"csv_import_success" => "",
|
||||
|
||||
@@ -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" => "",
|
||||
|
||||
@@ -14,8 +14,6 @@ return [
|
||||
"current_password_invalid" => "",
|
||||
"employee" => "Mitarbeiter",
|
||||
"error_adding_updating" => "Fehler beim Hinzufügen/Ändern",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Sie können den Admin nicht löschen",
|
||||
"error_updating_demo_admin" => "Sie können den Admin nicht ändern",
|
||||
"language" => "",
|
||||
|
||||
@@ -26,7 +26,6 @@ return [
|
||||
"cost_price_required" => "Einstandspreis ist erforderlich",
|
||||
"count" => "Ändere Bestand",
|
||||
"csv_import_failed" => "CSV Import fehlerhaft",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "Your uploaded file has no data or wrong format",
|
||||
"csv_import_partially_failed" => "Most Items imported. But some were not, here is the list",
|
||||
"csv_import_success" => "Import of Items successful",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"su" => "So",
|
||||
"mo" => "Mo",
|
||||
"tu" => "Di",
|
||||
"we" => "Mi",
|
||||
"th" => "Do",
|
||||
"fr" => "Fr",
|
||||
"sa" => "Sa",
|
||||
"sun" => "Son",
|
||||
"mon" => "Mon",
|
||||
"tue" => "Die",
|
||||
"wed" => "Mit",
|
||||
"thu" => "Don",
|
||||
"fri" => "Fre",
|
||||
"sat" => "Sam",
|
||||
"sunday" => "Sonntag",
|
||||
"monday" => "Montag",
|
||||
"tuesday" => "Dienstag",
|
||||
"wednesday" => "Mittwoch",
|
||||
"thursday" => "Donnerstag",
|
||||
"friday" => "Freitag",
|
||||
"saturday" => "Samstag",
|
||||
"jan" => "Jan",
|
||||
"feb" => "Feb",
|
||||
"mar" => "Mär",
|
||||
"apr" => "Apr",
|
||||
"may" => "Mai",
|
||||
"jun" => "Jun",
|
||||
"jul" => "Jul",
|
||||
"aug" => "Aug",
|
||||
"sep" => "Sep",
|
||||
"oct" => "Okt",
|
||||
"nov" => "Nov",
|
||||
"dec" => "Dez",
|
||||
"january" => "Januar",
|
||||
"february" => "Februar",
|
||||
"march" => "März",
|
||||
"april" => "April",
|
||||
"mayl" => "Mai",
|
||||
"june" => "Juni",
|
||||
"july" => "Juli",
|
||||
"august" => "August",
|
||||
"september" => "September",
|
||||
"october" => "Oktober",
|
||||
"november" => "November",
|
||||
"december" => "Dezember",
|
||||
];
|
||||
@@ -3,18 +3,18 @@
|
||||
return [
|
||||
"address_1" => "Adresse 1",
|
||||
"address_2" => "Adresse 2",
|
||||
"admin" => "Administrator",
|
||||
"admin" => "",
|
||||
"city" => "Stadt",
|
||||
"clerk" => "Angestellter",
|
||||
"clerk" => "",
|
||||
"close" => "Schließen",
|
||||
"color" => "Theme-Farben",
|
||||
"color" => "",
|
||||
"comments" => "Kommentare",
|
||||
"common" => "Allgemein",
|
||||
"confirm_search" => "Sie haben eine oder mehrere Zeilen gewählt. Nach der Verarbeitung werden diese nicht mehr ausgewählt sein. Wollen Sie die Suche dennoch verarbeiten?",
|
||||
"copyrights" => "© 2010 - {0}",
|
||||
"correct_errors" => "Bitte korrigieren Sie vor dem Speichern die angezeigten Fehler",
|
||||
"country" => "Land",
|
||||
"dashboard" => "Dashboard",
|
||||
"dashboard" => "",
|
||||
"date" => "Datum",
|
||||
"delete" => "Löschen",
|
||||
"det" => "Details",
|
||||
@@ -26,15 +26,15 @@ return [
|
||||
"export_csv_no" => "Nein",
|
||||
"export_csv_yes" => "Ja",
|
||||
"fields_required_message" => "Die Felder in rot sind erforderlich",
|
||||
"fields_required_message_unique" => "Die rot markierten Felder sind erforderlich und müssen eindeutig sein",
|
||||
"fields_required_message_unique" => "",
|
||||
"first_name" => "Vorname",
|
||||
"first_name_required" => "Vorname ist erforderlich.",
|
||||
"first_page" => "Erste",
|
||||
"gender" => "Geschlecht",
|
||||
"gender_female" => "W",
|
||||
"gender_male" => "M",
|
||||
"gender_undefined" => "Undefiniert",
|
||||
"icon" => "Symbol",
|
||||
"gender_undefined" => "",
|
||||
"icon" => "",
|
||||
"id" => "ID",
|
||||
"import" => "Import",
|
||||
"import_change_file" => "Ändern",
|
||||
@@ -48,21 +48,21 @@ return [
|
||||
"last_page" => "Letzte",
|
||||
"learn_about_project" => "für neueste Nachrichten zum Projekt.",
|
||||
"list_of" => "Liste von",
|
||||
"logo" => "Logo",
|
||||
"logo_mark" => "Marke",
|
||||
"logo" => "",
|
||||
"logo_mark" => "",
|
||||
"logout" => "Ausloggen",
|
||||
"manager" => "Manager",
|
||||
"manager" => "",
|
||||
"migration_needed" => "Eine Datenbankmigration auf {0} wird nach der Anmeldung gestartet.",
|
||||
"new" => "Neu",
|
||||
"no" => "Nein",
|
||||
"no" => "",
|
||||
"no_persons_to_display" => "Keine Personen zum Anzeigen.",
|
||||
"none_selected_text" => "[auswählen]",
|
||||
"or" => "Oder",
|
||||
"people" => "Personen",
|
||||
"people" => "",
|
||||
"phone_number" => "Telefon",
|
||||
"phone_number_required" => "Telefon ist erforderlich",
|
||||
"please_visit_my" => "Bitte beuschen Sie",
|
||||
"position" => "Position",
|
||||
"position" => "",
|
||||
"powered_by" => "Unterstützt von",
|
||||
"price" => "Preis",
|
||||
"print" => "Drucken",
|
||||
@@ -73,8 +73,8 @@ return [
|
||||
"search" => "Suche",
|
||||
"search_options" => "Suchkriterien",
|
||||
"searched_for" => "Gescuht nach",
|
||||
"software_short" => "OSPOS",
|
||||
"software_title" => "Open Source Point of Sale",
|
||||
"software_short" => "",
|
||||
"software_title" => "",
|
||||
"state" => "BL/Kanton",
|
||||
"submit" => "Senden",
|
||||
"total_spent" => "Gesamtausgaben",
|
||||
@@ -83,7 +83,7 @@ return [
|
||||
"website" => "Website",
|
||||
"welcome" => "Willkommen",
|
||||
"welcome_message" => "Willkommen bei OSPOS, zum Beginnen auf ein Modul klicken.",
|
||||
"yes" => "Ja",
|
||||
"yes" => "",
|
||||
"you_are_using_ospos" => "Sie verwenden Open Source Point Of Sale Version",
|
||||
"zip" => "PLZ",
|
||||
];
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"administrator" => "Administrator",
|
||||
"administrator" => "",
|
||||
"basic_information" => "Mitarbeiter-Information",
|
||||
"cannot_be_deleted" => "Konnte gewählten Mitarbeiter nicht löschen, einer oder mehrere weisen Verkäufe aus.",
|
||||
"change_employee" => "Mitarbeiter ändern",
|
||||
"change_employee" => "",
|
||||
"change_password" => "Passwort Ändern",
|
||||
"clerk" => "Angestellter",
|
||||
"commission" => "Provision",
|
||||
"clerk" => "",
|
||||
"commission" => "",
|
||||
"confirm_delete" => "Wollen Sie diesen Mitarbeiter wirklich löschen?",
|
||||
"confirm_restore" => "Möchten Sie die ausgewählten Mitarbeiter wiederherstellen?",
|
||||
"current_password" => "Aktuelles Passwort",
|
||||
"current_password_invalid" => "Aktuelles Passwort ist ungültig.",
|
||||
"employee" => "Mitarbeiter",
|
||||
"error_adding_updating" => "Fehler beim Hinzufügen/Ändern.",
|
||||
"error_deleting_admin" => "Sie können keinen Administrator löschen.",
|
||||
"error_updating_admin" => "Sie können keinen Administrator ändern.",
|
||||
"error_deleting_demo_admin" => "Sie können den Demo-Administrator nicht löschen.",
|
||||
"error_updating_demo_admin" => "Sie können den Demo-Administrator nicht verändern.",
|
||||
"language" => "Sprache",
|
||||
"login_info" => "Mitarbeiter Login",
|
||||
"manager" => "Manager",
|
||||
"manager" => "",
|
||||
"new" => "Neuer Mitarbeiter",
|
||||
"none_selected" => "Sie haben keine Mitarbeiter zum Löschen gewählt.",
|
||||
"one_or_multiple" => "Mitarbeiter",
|
||||
|
||||
@@ -26,7 +26,6 @@ return [
|
||||
"cost_price_required" => "Der Großhandelspreis ist ein Pflichtfeld.",
|
||||
"count" => "Ändere Bestand",
|
||||
"csv_import_failed" => "CSV Import fehlgeschlagen",
|
||||
"csv_import_invalid_location" => "Ungültige Lagerorte gefunden: {0}. Nur gültige Lagerorte sind erlaubt.",
|
||||
"csv_import_nodata_wrongformat" => "Die hochgeladene Datei enthält keine Daten oder ist falsch formatiert.",
|
||||
"csv_import_partially_failed" => "{0} Artikel-Import Fehler in Zeile: {1}. Keine Reihen wurden importiert.",
|
||||
"csv_import_success" => "Artikelimport erfolgreich.",
|
||||
|
||||
@@ -146,5 +146,4 @@ return [
|
||||
"used" => "Punkte eingelöst",
|
||||
"work_orders" => "Arbeitsaufträge",
|
||||
"zero_and_less" => "Null und weniger",
|
||||
"toggle_cost_and_profit" => "Kosten & Gewinn umschalten",
|
||||
];
|
||||
|
||||
@@ -222,8 +222,4 @@ return [
|
||||
"work_order_number_duplicate" => "Arbeitsauftragsnummer muss eindeutig sein.",
|
||||
"work_order_sent" => "Arbeitsauftrag gesendet an",
|
||||
"work_order_unsent" => "Der Arbeitsauftrag konnte nicht gesendet werden an",
|
||||
"sale_not_found" => "Verkauf nicht gefunden",
|
||||
"ubl_invoice" => "UBL-Rechnung",
|
||||
"download_ubl" => "UBL-Rechnung herunterladen",
|
||||
"ubl_generation_failed" => "UBL-Rechnung konnte nicht erstellt werden",
|
||||
];
|
||||
|
||||
@@ -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" => "Ομάδα",
|
||||
|
||||
@@ -14,8 +14,6 @@ return [
|
||||
"current_password_invalid" => "",
|
||||
"employee" => "",
|
||||
"error_adding_updating" => "",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "",
|
||||
"error_updating_demo_admin" => "",
|
||||
"language" => "",
|
||||
|
||||
@@ -26,7 +26,6 @@ return [
|
||||
"cost_price_required" => "",
|
||||
"count" => "",
|
||||
"csv_import_failed" => "",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "",
|
||||
"csv_import_partially_failed" => "",
|
||||
"csv_import_success" => "",
|
||||
|
||||
@@ -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",
|
||||
|
||||
29
app/Language/en/Api_keys.php
Normal file
29
app/Language/en/Api_keys.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'module_api_keys' => 'API Keys',
|
||||
'module_desc_api_keys' => 'Manage API access keys for integrations.',
|
||||
'api_keys' => 'API Keys',
|
||||
'api_key' => 'API Key',
|
||||
'generate_key' => 'Generate API Key',
|
||||
'regenerate_key' => 'Regenerate',
|
||||
'revoke_key' => 'Revoke',
|
||||
'key_name' => 'Key Name',
|
||||
'key_prefix' => 'Key Prefix',
|
||||
'last_used' => 'Last Used',
|
||||
'created' => 'Created',
|
||||
'expires' => 'Expires',
|
||||
'never' => 'Never',
|
||||
'disabled' => 'Disabled',
|
||||
'key_generated' => 'API key generated successfully',
|
||||
'key_generation_failed' => 'Failed to generate API key',
|
||||
'key_revoked' => 'API key revoked successfully',
|
||||
'key_revoke_failed' => 'Failed to revoke API key',
|
||||
'key_regenerated' => 'API key regenerated successfully',
|
||||
'key_regeneration_failed' => 'Failed to regenerate API key',
|
||||
'copy_warning' => 'Copy this key now! It will not be shown again.',
|
||||
'no_keys' => 'No API keys have been generated yet.',
|
||||
'confirm_revoke' => 'Are you sure you want to revoke this API key? This action cannot be undone.',
|
||||
'confirm_regenerate' => 'Are you sure you want to regenerate this API key? The old key will immediately stop working.',
|
||||
'key_description' => 'API keys allow external applications to access your OSPOS data. Keep your keys secure and never share them publicly.',
|
||||
];
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"su" => "Do",
|
||||
"mo" => "Lu",
|
||||
"tu" => "Ma",
|
||||
"we" => "Mi",
|
||||
"th" => "Ju",
|
||||
"fr" => "Vi",
|
||||
"sa" => "Sá",
|
||||
"sun" => "Dom",
|
||||
"mon" => "Lun",
|
||||
"tue" => "Mar",
|
||||
"wed" => "Mié",
|
||||
"thu" => "Jue",
|
||||
"fri" => "Vie",
|
||||
"sat" => "Sáb",
|
||||
"sunday" => "Domingo",
|
||||
"monday" => "Lunes",
|
||||
"tuesday" => "Martes",
|
||||
"wednesday" => "Miércoles",
|
||||
"thursday" => "Jueves",
|
||||
"friday" => "Viernes",
|
||||
"saturday" => "Sábado",
|
||||
"jan" => "Ene",
|
||||
"feb" => "Feb",
|
||||
"mar" => "Mar",
|
||||
"apr" => "Abr",
|
||||
"may" => "May",
|
||||
"jun" => "Jun",
|
||||
"jul" => "Jul",
|
||||
"aug" => "Ago",
|
||||
"sep" => "Sep",
|
||||
"oct" => "Oct",
|
||||
"nov" => "Nov",
|
||||
"dec" => "Dic",
|
||||
"january" => "Enero",
|
||||
"february" => "Febrero",
|
||||
"march" => "Marzo",
|
||||
"april" => "Abril",
|
||||
"mayl" => "Mayo",
|
||||
"june" => "Junio",
|
||||
"july" => "Julio",
|
||||
"august" => "Agosto",
|
||||
"september" => "Septiembre",
|
||||
"october" => "Octubre",
|
||||
"november" => "Noviembre",
|
||||
"december" => "Diciembre",
|
||||
];
|
||||
@@ -14,8 +14,6 @@ return [
|
||||
"current_password_invalid" => "Contraseña Actual Inválida.",
|
||||
"employee" => "Empleado",
|
||||
"error_adding_updating" => "Error al agregar/actualizar empleado.",
|
||||
"error_deleting_admin" => "No puedes eliminar un usuario administrador.",
|
||||
"error_updating_admin" => "No puedes modificar un usuario administrador.",
|
||||
"error_deleting_demo_admin" => "No puedes borrar el usuario admin del demo.",
|
||||
"error_updating_demo_admin" => "No puedes cambiar el usuario admin del demo.",
|
||||
"language" => "Idioma",
|
||||
|
||||
@@ -26,7 +26,6 @@ return [
|
||||
"cost_price_required" => "Precio al Por Mayor es un campo requerido.",
|
||||
"count" => "Actualizar Inventario",
|
||||
"csv_import_failed" => "Falló la importación de Hoja de Cálculo",
|
||||
"csv_import_invalid_location" => "Ubicación(es) de stock inválida(s) encontrada(s): {0}. Solo ubicaciones de stock válidas son permitidas.",
|
||||
"csv_import_nodata_wrongformat" => "El archivo subido no tiene datos o el formato es incorrecto.",
|
||||
"csv_import_partially_failed" => "Hubo {0} falla(s) en la importación de producto(s) en la(s) línea(s): {1}. Ninguna fila ha sido importada.",
|
||||
"csv_import_success" => "Se importaron los articulos exitosamente.",
|
||||
|
||||
@@ -146,5 +146,4 @@ return [
|
||||
"used" => "Puntos usados",
|
||||
"work_orders" => "Ordenes",
|
||||
"zero_and_less" => "Cero y negativos",
|
||||
"toggle_cost_and_profit" => "Alternar Costo y Ganancia",
|
||||
];
|
||||
|
||||
@@ -222,9 +222,5 @@ return [
|
||||
"work_order_number_duplicate" => "El numero de orden de trabajo debe ser unico.",
|
||||
"work_order_sent" => "Orden de trabajo enviada a",
|
||||
"work_order_unsent" => "Orden de trabajo fallida al enviar a",
|
||||
"sale_not_found" => "Venta no encontrada",
|
||||
"ubl_invoice" => "Factura UBL",
|
||||
"download_ubl" => "Descargar Factura UBL",
|
||||
"ubl_generation_failed" => "Error al generar la factura UBL",
|
||||
"selected_customer" => "Cliente seleccionado",
|
||||
];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"su" => "Do",
|
||||
"mo" => "Lu",
|
||||
"tu" => "Ma",
|
||||
"we" => "Mi",
|
||||
"th" => "Ju",
|
||||
"fr" => "Vi",
|
||||
"sa" => "Sá",
|
||||
"sun" => "Dom",
|
||||
"mon" => "Lun",
|
||||
"tue" => "Mar",
|
||||
"wed" => "Mié",
|
||||
"thu" => "Jue",
|
||||
"fri" => "Vie",
|
||||
"sat" => "Sáb",
|
||||
"sunday" => "Domingo",
|
||||
"monday" => "Lunes",
|
||||
"tuesday" => "Martes",
|
||||
"wednesday" => "Miércoles",
|
||||
"thursday" => "Jueves",
|
||||
"friday" => "Viernes",
|
||||
"saturday" => "Sábado",
|
||||
"jan" => "Ene",
|
||||
"feb" => "Feb",
|
||||
"mar" => "Mar",
|
||||
"apr" => "Abr",
|
||||
"may" => "May",
|
||||
"jun" => "Jun",
|
||||
"jul" => "Jul",
|
||||
"aug" => "Ago",
|
||||
"sep" => "Sep",
|
||||
"oct" => "Oct",
|
||||
"nov" => "Nov",
|
||||
"dec" => "Dic",
|
||||
"january" => "Enero",
|
||||
"february" => "Febrero",
|
||||
"march" => "Marzo",
|
||||
"april" => "Abril",
|
||||
"mayl" => "Mayo",
|
||||
"june" => "Junio",
|
||||
"july" => "Julio",
|
||||
"august" => "Agosto",
|
||||
"september" => "Septiembre",
|
||||
"october" => "Octubre",
|
||||
"november" => "Noviembre",
|
||||
"december" => "Diciembre",
|
||||
];
|
||||
@@ -14,8 +14,6 @@ return [
|
||||
"current_password_invalid" => "La contraseña actual es inválida.",
|
||||
"employee" => "Empleado",
|
||||
"error_adding_updating" => "Agregar ó Actualizar empleado ha fallado.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "No puede borrar el usuario demo de administrador.",
|
||||
"error_updating_demo_admin" => "No puede cambiar el usuario demo de administrador.",
|
||||
"language" => "Idioma",
|
||||
|
||||
@@ -26,7 +26,6 @@ return [
|
||||
"cost_price_required" => "El precio de mayoreo es requerido.",
|
||||
"count" => "Actualizar inventario",
|
||||
"csv_import_failed" => "",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "",
|
||||
"csv_import_partially_failed" => "",
|
||||
"csv_import_success" => "",
|
||||
|
||||
@@ -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" => "گروه",
|
||||
|
||||
@@ -14,8 +14,6 @@ return [
|
||||
"current_password_invalid" => "گذرواژه فعلی نامعتبر است.",
|
||||
"employee" => "کارمند",
|
||||
"error_adding_updating" => "افزودن یا به روزرسانی کارکنان انجام نشد.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "شما نمی توانید کاربر مدیر نسخه ی نمایشی را حذف کنید.",
|
||||
"error_updating_demo_admin" => "شما نمی توانید کاربر مدیر نسخه ی نمایشی را تغییر دهید.",
|
||||
"language" => "زبان",
|
||||
|
||||
@@ -26,7 +26,6 @@ return [
|
||||
"cost_price_required" => "قیمت عمده فروشی یک زمینه ضروری است.",
|
||||
"count" => "به روزرسانی موجودی",
|
||||
"csv_import_failed" => "واردات سیاسوی انجام نشد",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "پرونده سیاسوی آپلود شده داده ای ندارد یا به طور نادرست قالب بندی شده است.",
|
||||
"csv_import_partially_failed" => "در خط (ها){0} شکست واردات کالا وجود دارد:{1}. هیچ سطر وارد نشده است.",
|
||||
"csv_import_success" => "وارد کردن سیاسوی مورد موفقیت آمیز است.",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user