mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-25 00:44:03 -04:00
Compare commits
26 Commits
feature/re
...
feature/pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ea3ced674 | ||
|
|
896ed87797 | ||
|
|
eb264ad76d | ||
|
|
10a64e7af9 | ||
|
|
6e99f05d63 | ||
|
|
c430c7afb5 | ||
|
|
519347f4f5 | ||
|
|
62d84411b2 | ||
|
|
6bd4bb545d | ||
|
|
66f7d70749 | ||
|
|
bd8b4fa6c1 | ||
|
|
a9669ddf19 | ||
|
|
9a2b308647 | ||
|
|
1f55d96580 | ||
|
|
b2fadea44a | ||
|
|
0fdb3ba37b | ||
|
|
d7b2264ac1 | ||
|
|
a229bf6031 | ||
|
|
977fa5647b | ||
|
|
52b0a83190 | ||
|
|
f25a0f5b09 | ||
|
|
f0f288797a | ||
|
|
63083a0946 | ||
|
|
3a33098776 | ||
|
|
ca6a1b35af | ||
|
|
418580a52d |
10
.github/workflows/phpunit.yml
vendored
10
.github/workflows/phpunit.yml
vendored
@@ -111,7 +111,15 @@ jobs:
|
||||
env:
|
||||
CI_ENVIRONMENT: testing
|
||||
MYSQL_HOST_NAME: 127.0.0.1
|
||||
run: composer test
|
||||
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
|
||||
|
||||
- name: Stop MariaDB
|
||||
if: always()
|
||||
|
||||
@@ -205,6 +205,7 @@ class Autoload extends AutoloadConfig
|
||||
'cookie',
|
||||
'tabular',
|
||||
'locale',
|
||||
'security'
|
||||
'security',
|
||||
'plugin'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -169,3 +169,8 @@ 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'];
|
||||
|
||||
@@ -8,23 +8,7 @@ use CodeIgniter\HotReloader\HotReloader;
|
||||
use App\Events\Db_log;
|
||||
use App\Events\Load_config;
|
||||
use App\Events\Method;
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
* Application Events
|
||||
* --------------------------------------------------------------------
|
||||
* Events allow you to tap into the execution of the program without
|
||||
* modifying or extending core files. This file provides a central
|
||||
* location to define your events, though they can always be added
|
||||
* at run-time, also, if needed.
|
||||
*
|
||||
* You create code that can execute by subscribing to events with
|
||||
* the 'on()' method. This accepts any form of callable, including
|
||||
* Closures, that will be executed when the event is triggered.
|
||||
*
|
||||
* Example:
|
||||
* Events::on('create', [$myInstance, 'myMethod']);
|
||||
*/
|
||||
use App\Libraries\Plugins\PluginManager;
|
||||
|
||||
Events::on('pre_system', static function (): void {
|
||||
if (ENVIRONMENT !== 'testing') {
|
||||
@@ -39,22 +23,19 @@ Events::on('pre_system', static function (): void {
|
||||
ob_start(static fn ($buffer) => $buffer);
|
||||
}
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
* Debug Toolbar Listeners.
|
||||
* --------------------------------------------------------------------
|
||||
* If you delete, they will no longer be collected.
|
||||
*/
|
||||
if (CI_DEBUG && ! is_cli()) {
|
||||
Events::on('DBQuery', 'CodeIgniter\Debug\Toolbar\Collectors\Database::collect');
|
||||
service('toolbar')->respond();
|
||||
// Hot Reload route - for framework use on the hot reloader.
|
||||
if (ENVIRONMENT === 'development') {
|
||||
service('routes')->get('__hot-reload', static function (): void {
|
||||
(new HotReloader())->run();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$pluginManager = new PluginManager();
|
||||
$pluginManager->discoverPlugins();
|
||||
$pluginManager->registerPluginEvents();
|
||||
});
|
||||
|
||||
$config = new Load_config();
|
||||
@@ -64,4 +45,4 @@ $db_log = new Db_log();
|
||||
Events::on('DBQuery', [$db_log, 'db_log_queries']);
|
||||
|
||||
$method = new Method();
|
||||
Events::on('pre_controller', [$method, 'validate_method']);
|
||||
Events::on('pre_controller', [$method, 'validate_method']);
|
||||
@@ -12,10 +12,18 @@ 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,
|
||||
@@ -26,7 +34,6 @@ class Filters extends BaseFilters
|
||||
'forcehttps' => ForceHTTPS::class,
|
||||
'pagecache' => PageCache::class,
|
||||
'performance' => PerformanceMetrics::class,
|
||||
'apiauth' => ApiAuth::class,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -63,7 +70,7 @@ class Filters extends BaseFilters
|
||||
public array $globals = [
|
||||
'before' => [
|
||||
'honeypot',
|
||||
'csrf' => ['except' => ['login', 'api/*']],
|
||||
'csrf' => ['except' => 'login'],
|
||||
'invalidchars',
|
||||
],
|
||||
'after' => [
|
||||
|
||||
@@ -39,50 +39,3 @@ $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');
|
||||
});
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
<?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';
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
<?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';
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
<?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';
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
<?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')
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -461,8 +461,9 @@ 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' => $this->request->getPost('currency_symbol'),
|
||||
'currency_symbol' => htmlspecialchars($currency_symbol ?? ''),
|
||||
'currency_code' => $this->request->getPost('currency_code'),
|
||||
'language_code' => $exploded[0],
|
||||
'language' => $exploded[1],
|
||||
|
||||
@@ -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->can_modify_employee($person_info->person_id, $current_user->person_id)) {
|
||||
if ($employee_id != NEW_ENTRY && !$this->employee->canModifyEmployee($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->can_modify_employee($target_employee->person_id, $current_user->person_id)) {
|
||||
if (!$this->employee->canModifyEmployee($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 = [];
|
||||
$is_admin = $this->employee->is_admin($current_user->person_id);
|
||||
$isAdmin = $this->employee->isAdmin($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 (!$is_admin && !$this->employee->has_grant($permission->permission_id, $current_user->person_id)) {
|
||||
if (!$isAdmin && !$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->is_admin($current_user->person_id)) {
|
||||
if (!$this->employee->isAdmin($current_user->person_id)) {
|
||||
foreach ($employees_to_delete as $emp_id) {
|
||||
if ($this->employee->is_admin((int)$emp_id)) {
|
||||
if ($this->employee->isAdmin((int)$emp_id)) {
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Employees.error_deleting_admin')]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,19 @@ class Home extends Secure_Controller
|
||||
* @return string
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function getChangePassword(int $employee_id = -1): string // TODO: Replace -1 with a constant
|
||||
public function getChangePassword(int $employeeId = NEW_ENTRY): string
|
||||
{
|
||||
$person_info = $this->employee->get_info($employee_id);
|
||||
$loggedInEmployee = $this->employee->get_logged_in_employee_info();
|
||||
$currentPersonId = $loggedInEmployee->person_id;
|
||||
|
||||
$employeeId = $employeeId === NEW_ENTRY ? $currentPersonId : $employeeId;
|
||||
|
||||
if (!$this->employee->can_modify_employee($employeeId, $currentPersonId)) {
|
||||
header('Location: ' . base_url('no_access/home/home'));
|
||||
exit();
|
||||
}
|
||||
|
||||
$person_info = $this->employee->get_info($employeeId);
|
||||
foreach (get_object_vars($person_info) as $property => $value) {
|
||||
$person_info->$property = $value;
|
||||
}
|
||||
@@ -55,9 +65,20 @@ class Home extends Secure_Controller
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function postSave(int $employee_id = -1): ResponseInterface // TODO: Replace -1 with a constant
|
||||
public function postSave(int $employeeId = NEW_ENTRY): ResponseInterface
|
||||
{
|
||||
if (!empty($this->request->getPost('current_password')) && $employee_id != -1) {
|
||||
$currentUser = $this->employee->get_logged_in_employee_info();
|
||||
|
||||
$employeeId = $employeeId === NEW_ENTRY ? $currentUser->person_id : $employeeId;
|
||||
|
||||
if (!$this->employee->can_modify_employee($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 ($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');
|
||||
@@ -66,7 +87,7 @@ class Home extends Secure_Controller
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Employees.password_minlength'),
|
||||
'id' => -1
|
||||
'id' => NEW_ENTRY
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -76,32 +97,32 @@ class Home extends Secure_Controller
|
||||
'hash_version' => 2
|
||||
];
|
||||
|
||||
if ($this->employee->change_password($employee_data, $employee_id)) {
|
||||
if ($this->employee->change_password($employee_data, $employeeId)) {
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => lang('Employees.successful_change_password'),
|
||||
'id' => $employee_id
|
||||
'id' => $employeeId
|
||||
]);
|
||||
} else { // Failure // TODO: Replace -1 with constant
|
||||
} else {
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Employees.unsuccessful_change_password'),
|
||||
'id' => -1
|
||||
'id' => NEW_ENTRY
|
||||
]);
|
||||
}
|
||||
} else { // TODO: Replace -1 with constant
|
||||
} else {
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Employees.current_password_invalid'),
|
||||
'id' => -1
|
||||
'id' => NEW_ENTRY
|
||||
]);
|
||||
}
|
||||
} else { // TODO: Replace -1 with constant
|
||||
} else {
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Employees.current_password_invalid'),
|
||||
'id' => -1
|
||||
'id' => NEW_ENTRY
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ class Items extends Secure_Controller
|
||||
**/
|
||||
public function getSearch(): ResponseInterface
|
||||
{
|
||||
$search = $this->request->getGet('search');
|
||||
$search = $this->request->getGet('search', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$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');
|
||||
@@ -148,6 +148,7 @@ 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);
|
||||
@@ -377,7 +378,7 @@ class Items extends Secure_Controller
|
||||
} else {
|
||||
$images = glob("./uploads/item_pics/$item_info->pic_filename");
|
||||
}
|
||||
$data['image_path'] = sizeof($images) > 0 ? base_url($images[0]) : '';
|
||||
$data['image_path'] = sizeof($images) > 0 ? base_url(implode('/', array_map('rawurlencode', explode('/', ltrim($images[0], './'))))) : '';
|
||||
} else {
|
||||
$data['image_path'] = '';
|
||||
}
|
||||
@@ -617,7 +618,7 @@ class Items extends Secure_Controller
|
||||
// Save item data
|
||||
$item_data = [
|
||||
'name' => $this->request->getPost('name'),
|
||||
'description' => $this->request->getPost('description'),
|
||||
'description' => $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
'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')),
|
||||
@@ -768,10 +769,13 @@ 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' => $info['filename'],
|
||||
'raw_name' => $sanitized_name,
|
||||
'file_ext' => $file->guessExtension()
|
||||
];
|
||||
|
||||
@@ -872,12 +876,12 @@ class Items extends Secure_Controller
|
||||
$items_to_update = $this->request->getPost('item_ids');
|
||||
$item_data = [];
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
99
app/Controllers/Plugins/Manage.php
Normal file
99
app/Controllers/Plugins/Manage.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Plugins;
|
||||
|
||||
use App\Controllers\Secure_Controller;
|
||||
use App\Libraries\Plugins\PluginManager;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
class Manage extends Secure_Controller
|
||||
{
|
||||
private PluginManager $pluginManager;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('plugins');
|
||||
$this->pluginManager = new PluginManager();
|
||||
$this->pluginManager->discoverPlugins();
|
||||
}
|
||||
|
||||
public function getIndex(): string
|
||||
{
|
||||
$plugins = $this->pluginManager->getAllPlugins();
|
||||
$enabledPlugins = $this->pluginManager->getEnabledPlugins();
|
||||
|
||||
$pluginData = [];
|
||||
foreach ($plugins as $pluginId => $plugin) {
|
||||
$pluginData[$pluginId] = [
|
||||
'id' => $plugin->getPluginId(),
|
||||
'name' => $plugin->getPluginName(),
|
||||
'description' => $plugin->getPluginDescription(),
|
||||
'version' => $plugin->getVersion(),
|
||||
'enabled' => isset($enabledPlugins[$pluginId]),
|
||||
'has_config' => $plugin->getConfigView() !== null,
|
||||
];
|
||||
}
|
||||
|
||||
echo view('plugins/manage', ['plugins' => $pluginData]);
|
||||
return '';
|
||||
}
|
||||
|
||||
public function postEnable(string $pluginId): ResponseInterface
|
||||
{
|
||||
if ($this->pluginManager->enablePlugin($pluginId)) {
|
||||
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.plugin_enabled')]);
|
||||
}
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_enable_failed')]);
|
||||
}
|
||||
|
||||
public function postDisable(string $pluginId): ResponseInterface
|
||||
{
|
||||
if ($this->pluginManager->disablePlugin($pluginId)) {
|
||||
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.plugin_disabled')]);
|
||||
}
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_disable_failed')]);
|
||||
}
|
||||
|
||||
public function postUninstall(string $pluginId): ResponseInterface
|
||||
{
|
||||
if ($this->pluginManager->uninstallPlugin($pluginId)) {
|
||||
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.plugin_uninstalled')]);
|
||||
}
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_uninstall_failed')]);
|
||||
}
|
||||
|
||||
public function getConfig(string $pluginId): ResponseInterface
|
||||
{
|
||||
$plugin = $this->pluginManager->getPlugin($pluginId);
|
||||
|
||||
if (!$plugin) {
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_not_found')]);
|
||||
}
|
||||
|
||||
$configView = $plugin->getConfigView();
|
||||
if (!$configView) {
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_no_config')]);
|
||||
}
|
||||
|
||||
$settings = $plugin->getSettings();
|
||||
echo view($configView, ['settings' => $settings, 'plugin' => $plugin]);
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
public function postSaveConfig(string $pluginId): ResponseInterface
|
||||
{
|
||||
$plugin = $this->pluginManager->getPlugin($pluginId);
|
||||
|
||||
if (!$plugin) {
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_not_found')]);
|
||||
}
|
||||
|
||||
$settings = $this->request->getPost();
|
||||
unset($settings['_method'], $settings['csrf_token_name']);
|
||||
|
||||
if ($plugin->saveSettings($settings)) {
|
||||
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.settings_saved')]);
|
||||
}
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.settings_save_failed')]);
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?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.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class PluginConfigTableCreate extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
log_message('info', 'Migrating plugin_config table started');
|
||||
|
||||
execute_script(APPPATH . 'Database/Migrations/sqlscripts/3.4.1_PluginConfigTableCreate.sql');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('plugin_config', true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS `ospos_plugin_config` (
|
||||
`key` varchar(100) NOT NULL,
|
||||
`value` text NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (`key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -1,48 +0,0 @@
|
||||
<?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
|
||||
]);
|
||||
}
|
||||
}
|
||||
24
app/Helpers/plugin_helper.php
Normal file
24
app/Helpers/plugin_helper.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use CodeIgniter\Events\Events;
|
||||
|
||||
if (!function_exists('plugin_content')) {
|
||||
function plugin_content(string $section, array $data = []): string
|
||||
{
|
||||
$results = Events::trigger("view:{$section}", $data);
|
||||
|
||||
if (is_array($results)) {
|
||||
return implode('', array_filter($results, fn($r) => is_string($r)));
|
||||
}
|
||||
|
||||
return is_string($results) ? $results : '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('plugin_content_exists')) {
|
||||
function plugin_content_exists(string $section): bool
|
||||
{
|
||||
$observers = Events::listRegistered("view:{$section}");
|
||||
return !empty($observers);
|
||||
}
|
||||
}
|
||||
@@ -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|customer_name|note)/", key($element)) && !(isset($element['escape']) && !$element['escape']),
|
||||
'escape' => !preg_match("/(edit|email|messages|item_pic)/", 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' : '',
|
||||
@@ -470,7 +470,8 @@ function get_item_data_row(object $item): array
|
||||
: glob("./uploads/item_pics/$item->pic_filename");
|
||||
|
||||
if (sizeof($images) > 0) {
|
||||
$image .= '<a class="rollover" href="' . base_url($images[0]) . '"><img alt="Image thumbnail" src="' . site_url('items/PicThumb/' . pathinfo($images[0], PATHINFO_BASENAME)) . '"></a>';
|
||||
$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>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ 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,6 +26,7 @@ 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" => "تم استيراد الأصناف بنجاح.",
|
||||
|
||||
@@ -14,6 +14,8 @@ 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,6 +26,7 @@ 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" => "تم استيراد الأصناف بنجاح.",
|
||||
|
||||
@@ -14,6 +14,8 @@ 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,6 +26,7 @@ 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.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "Текущата парола е невалидна.",
|
||||
"employee" => "Служител",
|
||||
"error_adding_updating" => "Добавянето или актуализирането на служителите е неуспешно.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Не може да изтриете Пробният Администратор.",
|
||||
"error_updating_demo_admin" => "Не може да промените Пробният Администратор.",
|
||||
"language" => "Език",
|
||||
|
||||
@@ -26,6 +26,7 @@ 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.",
|
||||
|
||||
@@ -14,6 +14,8 @@ 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,6 +26,7 @@ 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.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
'current_password_invalid' => "وشەی نهێنی ئێستا نادروستە.",
|
||||
'employee' => "فەرمانبەر",
|
||||
'error_adding_updating' => "زیادکردن یان نوێکردنەوەی کارمەند سەرکەوتوو نەبوو.",
|
||||
'error_deleting_admin' => "",
|
||||
'error_updating_admin' => "",
|
||||
'error_deleting_demo_admin' => "ناتوانیت بەکارهێنەری ئەدمینی تاقیکردنەوەیی بسڕیتەوە.",
|
||||
'error_updating_demo_admin' => "ناتوانیت بەکارهێنەری ئەدمین تاقیکردنەوەیی بگۆڕیت.",
|
||||
'language' => "زمان",
|
||||
|
||||
@@ -26,6 +26,7 @@ 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 سەرکەوتوو بوو.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "",
|
||||
"employee" => "",
|
||||
"error_adding_updating" => "",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "",
|
||||
"error_updating_demo_admin" => "",
|
||||
"language" => "",
|
||||
|
||||
@@ -26,6 +26,7 @@ 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.",
|
||||
|
||||
@@ -14,6 +14,8 @@ 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,6 +26,7 @@ return [
|
||||
"cost_price_required" => "",
|
||||
"count" => "",
|
||||
"csv_import_failed" => "",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "",
|
||||
"csv_import_partially_failed" => "",
|
||||
"csv_import_success" => "",
|
||||
|
||||
@@ -14,6 +14,8 @@ 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,6 +26,7 @@ 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",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "Aktuelles Passwort ist ungültig.",
|
||||
"employee" => "Mitarbeiter",
|
||||
"error_adding_updating" => "Fehler beim Hinzufügen/Ändern.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"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",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "Der Großhandelspreis ist ein Pflichtfeld.",
|
||||
"count" => "Ändere Bestand",
|
||||
"csv_import_failed" => "CSV Import fehlgeschlagen",
|
||||
"csv_import_invalid_location" => "",
|
||||
"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.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "",
|
||||
"employee" => "",
|
||||
"error_adding_updating" => "",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "",
|
||||
"error_updating_demo_admin" => "",
|
||||
"language" => "",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "",
|
||||
"count" => "",
|
||||
"csv_import_failed" => "",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "",
|
||||
"csv_import_partially_failed" => "",
|
||||
"csv_import_success" => "",
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<?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.',
|
||||
];
|
||||
27
app/Language/en/Plugins.php
Normal file
27
app/Language/en/Plugins.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
return [
|
||||
// Plugin Management
|
||||
"plugins" => "Plugins",
|
||||
"plugin_management" => "Plugin Management",
|
||||
"plugin_name" => "Plugin Name",
|
||||
"plugin_description" => "Description",
|
||||
"plugin_version" => "Version",
|
||||
"plugin_status" => "Status",
|
||||
"plugin_enabled" => "Plugin enabled successfully",
|
||||
"plugin_enable_failed" => "Failed to enable plugin",
|
||||
"plugin_disabled" => "Plugin disabled successfully",
|
||||
"plugin_disable_failed" => "Failed to disable plugin",
|
||||
"plugin_uninstalled" => "Plugin uninstalled successfully",
|
||||
"plugin_uninstall_failed" => "Failed to uninstall plugin",
|
||||
"plugin_not_found" => "Plugin not found",
|
||||
"plugin_no_config" => "This plugin has no configuration options",
|
||||
"settings_saved" => "Plugin settings saved successfully",
|
||||
"settings_save_failed" => "Failed to save plugin settings",
|
||||
"enable" => "Enable",
|
||||
"disable" => "Disable",
|
||||
"configure" => "Configure",
|
||||
"uninstall" => "Uninstall",
|
||||
"no_plugins_found" => "No plugins found",
|
||||
"active" => "Active",
|
||||
"inactive" => "Inactive",
|
||||
];
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "Contraseña Actual Inválida.",
|
||||
"employee" => "Empleado",
|
||||
"error_adding_updating" => "Error al agregar/actualizar empleado.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"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,6 +26,7 @@ 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" => "",
|
||||
"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.",
|
||||
|
||||
@@ -14,6 +14,8 @@ 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,6 +26,7 @@ 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" => "",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "گذرواژه فعلی نامعتبر است.",
|
||||
"employee" => "کارمند",
|
||||
"error_adding_updating" => "افزودن یا به روزرسانی کارکنان انجام نشد.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "شما نمی توانید کاربر مدیر نسخه ی نمایشی را حذف کنید.",
|
||||
"error_updating_demo_admin" => "شما نمی توانید کاربر مدیر نسخه ی نمایشی را تغییر دهید.",
|
||||
"language" => "زبان",
|
||||
|
||||
@@ -26,6 +26,7 @@ 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" => "وارد کردن سیاسوی مورد موفقیت آمیز است.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "Le mot de passe actuel est invalide.",
|
||||
"employee" => "Employé",
|
||||
"error_adding_updating" => "Erreur d'ajout/édition d'employé.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Vous ne pouvez pas supprimer l'utilisateur de démonstration admin.",
|
||||
"error_updating_demo_admin" => "Vous ne pouvez pas modifier l'utilisateur de démonstration admin.",
|
||||
"language" => "Langue",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "Le prix de gros est requis.",
|
||||
"count" => "Mise à jour de l'inventaire",
|
||||
"csv_import_failed" => "Échec d'import CSV",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "Le CSV envoyé ne contient aucune donnée, ou elles sont dans un format erroné.",
|
||||
"csv_import_partially_failed" => "Il y a eu {0} importation(s) d'articles échoué(s) au(x) ligne(s) : {1}. Aucune ligne n'a été importée.",
|
||||
"csv_import_success" => "Importation des articles réussie.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "הסיסמה הנוכחית אינה חוקית.",
|
||||
"employee" => "עובד",
|
||||
"error_adding_updating" => "הוספה או עדכון של עובד נכשלה.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "לא ניתן למחוק את משתמש המנהל ההדגמה.",
|
||||
"error_updating_demo_admin" => "לא ניתן לשנות את משתמש המנהל ההדגמה.",
|
||||
"language" => "שפה",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "מחיר סיטונאי הינו שדה חובה.",
|
||||
"count" => "עדכן מלאי",
|
||||
"csv_import_failed" => "ייבוא אקסל נכשל",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "בקובץ שהועלה אין נתונים או פורמט שגוי.",
|
||||
"csv_import_partially_failed" => "ייבוא פריט הצליח עם מספר שגיאות:",
|
||||
"csv_import_success" => "ייבוא הפריט הצליח.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "",
|
||||
"employee" => "Radnik",
|
||||
"error_adding_updating" => "Greška kod dodavanja/ažuriranja radnika",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Ne možete obrisati demo admin korisnika",
|
||||
"error_updating_demo_admin" => "Ne možete promijeniti demo admin korisnika",
|
||||
"language" => "",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "Nabavna cijena je potrebna",
|
||||
"count" => "Ažuriraj inveturu",
|
||||
"csv_import_failed" => "Greška kod uvoza iz CSV-a",
|
||||
"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",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "",
|
||||
"employee" => "Munkavállaló",
|
||||
"error_adding_updating" => "Hiba a munkavállaló módosításánál/hozzáadásánál",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Nem tudja törölni a demo admin felhasználót",
|
||||
"error_updating_demo_admin" => "Nem tudja módosítani a demo admin felhasználót",
|
||||
"language" => "",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "Bekerülési ár kötelező mező",
|
||||
"count" => "Raktárkészlet módosítása",
|
||||
"csv_import_failed" => "CSV import sikertelen",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "A feltöltött fájlban nincs adat, vagy rossz formátum.",
|
||||
"csv_import_partially_failed" => "Most Items imported. But some were not, here is the list",
|
||||
"csv_import_success" => "Import of Items successful",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "",
|
||||
"employee" => "",
|
||||
"error_adding_updating" => "",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "",
|
||||
"error_updating_demo_admin" => "",
|
||||
"language" => "",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "",
|
||||
"count" => "",
|
||||
"csv_import_failed" => "",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "",
|
||||
"csv_import_partially_failed" => "",
|
||||
"csv_import_success" => "",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "Kata kunci sekarang salah.",
|
||||
"employee" => "Karyawan",
|
||||
"error_adding_updating" => "Kesalahan menambah / memperbarui karyawan.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Anda tidak dapat menghapus Demo admin user.",
|
||||
"error_updating_demo_admin" => "Anda tidak dapat mengubah Demo admin user.",
|
||||
"language" => "Bahasa",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "Harga beli harus diisi.",
|
||||
"count" => "Mutasi Inventori",
|
||||
"csv_import_failed" => "Gagal impor CSV",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "Berkas CSV terunggah tidak berisi data atau formatnya salah.",
|
||||
"csv_import_partially_failed" => "Terdapat {0} item gagal impor pada baris: {1}. Tidak ada baris yang diimpor.",
|
||||
"csv_import_success" => "Impor item CSV berhasil.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "Password corrente non valida.",
|
||||
"employee" => "Impiegato",
|
||||
"error_adding_updating" => "Aggiunta o aggiornamento di impiegati fallito.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Non puoi eliminare l'utente admin demo.",
|
||||
"error_updating_demo_admin" => "Non puoi cambiare l'utente admin demo.",
|
||||
"language" => "Lingua",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "Prezzo all'ingrosso è un campo obbligatorio.",
|
||||
"count" => "Aggiorna Inventario",
|
||||
"csv_import_failed" => "Importazione CSV fallita",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "L'upload del file non ha dati o non è formattato correttamente.",
|
||||
"csv_import_partially_failed" => "Si sono verificati {0} errori di importazione degli elementi nelle righe: {1}. Nessuna riga è stata importata.",
|
||||
"csv_import_success" => "Importazione CSV dell'articolo riuscita.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "ពាក្យសម្ងាត់បច្ចុប្បន្ន មិនត្រឹមត្រូវ។",
|
||||
"employee" => "បុគ្គលិក",
|
||||
"error_adding_updating" => "បន្ថែម ឬកែប្រែបុគ្គលិកមិនបានសំរេច។",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "អ្នកមិនអាចលុប គណនីសាកល្បង បានទេ។",
|
||||
"error_updating_demo_admin" => "អ្នកមិនអាចកែប្រែ គណនីសាកល្បងបានទេ។",
|
||||
"language" => "ភាសា",
|
||||
|
||||
@@ -26,6 +26,7 @@ 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 បញ្ចូលបានសំរេច។",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "Password ປັດຈຸບັນບໍ່ຖືກຕ້ອງ.",
|
||||
"employee" => "ພະນັກງານ",
|
||||
"error_adding_updating" => "ເພີ່ມ ຫຼື ແກ້ໄຂ ພະນັກງານ ບໍ່ສຳເລັດ.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "ທ່ານບໍ່ສາມາດລຶບບັນຊີທົດລອງຜູ້ດູແລລະບົບໄດ້.",
|
||||
"error_updating_demo_admin" => "ທ່ານບໍ່ສາມາດປ່ຽນແປງບັນຊີທົດລອງຜູ້ດູແລລະບົບໄດ້.",
|
||||
"language" => "ພາສາ",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "ກະລຸນາກຳນົດລາຄາຕົ້ນທຶນ.",
|
||||
"count" => "ອັບເດດປະລິມານສິນຄ້າໃນສາງ",
|
||||
"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.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "",
|
||||
"employee" => "",
|
||||
"error_adding_updating" => "",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "",
|
||||
"error_updating_demo_admin" => "",
|
||||
"language" => "",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "",
|
||||
"count" => "",
|
||||
"csv_import_failed" => "",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "",
|
||||
"csv_import_partially_failed" => "",
|
||||
"csv_import_success" => "",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "",
|
||||
"employee" => "",
|
||||
"error_adding_updating" => "",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "",
|
||||
"error_updating_demo_admin" => "",
|
||||
"language" => "",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "",
|
||||
"count" => "",
|
||||
"csv_import_failed" => "",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "",
|
||||
"csv_import_partially_failed" => "",
|
||||
"csv_import_success" => "",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "Huidig paswoord is ongeldig.",
|
||||
"employee" => "Werknemer",
|
||||
"error_adding_updating" => "Fout bij het toevoegen/aanpassen medewerker.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Je kan de demo gebruilker niet verwijderen.",
|
||||
"error_updating_demo_admin" => "Jij kan de demo gebruiker niet veranderen.",
|
||||
"language" => "Taal",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "Groothandelsprijs is een verplicht veld.",
|
||||
"count" => "Update Stock",
|
||||
"csv_import_failed" => "CSV import mislukt",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "Het geüploade CSV-bestand bevat geen gegevens of is onjuist geformatteerd.",
|
||||
"csv_import_partially_failed" => "Er waren {0} artikel import fout(en) op regel(s): {1}. Er werden geen rijen geïmporteerd.",
|
||||
"csv_import_success" => "Artikel CSV import geslaagd.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "Huidige wachtwoord is ongeldig.",
|
||||
"employee" => "Werknemer",
|
||||
"error_adding_updating" => "Werknemer toevoegen of bijwerken mislukt.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Kan de demo admin gebruiker niet verwijderen.",
|
||||
"error_updating_demo_admin" => "Kan de demo admin gebruiker niet wijzigen.",
|
||||
"language" => "Taal",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "Inkoopprijs is een vereist veld.",
|
||||
"count" => "Voorraad bijwerken",
|
||||
"csv_import_failed" => "CSV importeren mislukt",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "Het geüploade CSV-bestand bevat geen gegevens or heeft de verkeerde indeling.",
|
||||
"csv_import_partially_failed" => "Er zijn {0} artikel import fout(en) in lijn(en): {1}. Geen rijen geïmporteerd.",
|
||||
"csv_import_success" => "Artikel CSV geïmporteerd.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "",
|
||||
"employee" => "",
|
||||
"error_adding_updating" => "",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "",
|
||||
"error_updating_demo_admin" => "",
|
||||
"language" => "",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "",
|
||||
"count" => "",
|
||||
"csv_import_failed" => "",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "",
|
||||
"csv_import_partially_failed" => "",
|
||||
"csv_import_success" => "",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "Senha atual inválida.",
|
||||
"employee" => "Funcionário",
|
||||
"error_adding_updating" => "Erro ao adicionar/atualizar funcionário.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Você não pode excluir o usuário administrador de demonstração.",
|
||||
"error_updating_demo_admin" => "Você não pode alterar o usuário de demonstração de administração.",
|
||||
"language" => "Linguagem",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "Preço de custo é um campo obrigatório.",
|
||||
"count" => "Acrescentar ao Inventário",
|
||||
"csv_import_failed" => "Importação do CSV falhou",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "Seu arquivo enviado não contém dados ou formato errado.",
|
||||
"csv_import_partially_failed" => "Houve {0} falha na importação de itens na(s) linha(s): {1}. Nenhuma linha foi importada.",
|
||||
"csv_import_success" => "Importação de Itens com sucesso.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "",
|
||||
"employee" => "",
|
||||
"error_adding_updating" => "",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "",
|
||||
"error_updating_demo_admin" => "",
|
||||
"language" => "",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "",
|
||||
"count" => "",
|
||||
"csv_import_failed" => "",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "",
|
||||
"csv_import_partially_failed" => "",
|
||||
"csv_import_success" => "",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "Текущий пароль введен неверно.",
|
||||
"employee" => "Сотрудник",
|
||||
"error_adding_updating" => "Ошибка при добавлении/обновлении сотрудника.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Вы не можете удалить демо-администратора.",
|
||||
"error_updating_demo_admin" => "Вы не можете изменить демо-администратора.",
|
||||
"language" => "Язык",
|
||||
|
||||
@@ -26,6 +26,7 @@ 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.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "Nuvarande lösenord är fel.",
|
||||
"employee" => "Anställd",
|
||||
"error_adding_updating" => "Anställd lägg till eller uppdatering misslyckades.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Du kan inte radera demo admin-användaren.",
|
||||
"error_updating_demo_admin" => "Du kan inte ändra demo admin-användaren.",
|
||||
"language" => "Språk",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "Grossistpris är ett obligatoriskt fält.",
|
||||
"count" => "Uppdatera Inventory",
|
||||
"csv_import_failed" => "CSV-import misslyckades",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "Den uppladdade filen har ingen data eller är formaterad felaktigt.",
|
||||
"csv_import_partially_failed" => "Det fanns{0} importfel (er) på rad (er):{1}. Inga rader importerades.",
|
||||
"csv_import_success" => "Artikelimporten lyckades.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "Nenosiri la sasa si sahihi.",
|
||||
"employee" => "Mfanyakazi",
|
||||
"error_adding_updating" => "Kuongeza au kusasisha mfanyakazi kumeshindikana.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Huwezi kufuta mtumiaji wa admin wa majaribio.",
|
||||
"error_updating_demo_admin" => "Huwezi kubadilisha mtumiaji wa admin wa majaribio.",
|
||||
"language" => "Lugha",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "Bei ya Jumla ni kiashiria kinachohitajika.",
|
||||
"count" => "Sasisha Hisa",
|
||||
"csv_import_failed" => "Uingizaji wa CSV umeshindikana",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "Faili ya CSV iliyopakiwa haina data au imepangwa vibaya.",
|
||||
"csv_import_partially_failed" => "Kumekuwa na makosa {0} ya uingizaji wa bidhaa kwenye mstari: {1}. Hakuna safu zilizoingizwa.",
|
||||
"csv_import_success" => "Uingizaji wa Bidhaa kutoka CSV umefanikiwa.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "Nenosiri la sasa si sahihi.",
|
||||
"employee" => "Mfanyakazi",
|
||||
"error_adding_updating" => "Kuongeza au kusasisha mfanyakazi kumeshindikana.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "Huwezi kufuta mtumiaji wa admin wa majaribio.",
|
||||
"error_updating_demo_admin" => "Huwezi kubadilisha mtumiaji wa admin wa majaribio.",
|
||||
"language" => "Lugha",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "Bei ya Jumla ni kiashiria kinachohitajika.",
|
||||
"count" => "Sasisha Hisa",
|
||||
"csv_import_failed" => "Uingizaji wa CSV umeshindikana",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "Faili ya CSV iliyopakiwa haina data au imepangwa vibaya.",
|
||||
"csv_import_partially_failed" => "Kumekuwa na makosa {0} ya uingizaji wa bidhaa kwenye mstari: {1}. Hakuna safu zilizoingizwa.",
|
||||
"csv_import_success" => "Uingizaji wa Bidhaa kutoka CSV umefanikiwa.",
|
||||
|
||||
@@ -14,6 +14,8 @@ 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,6 +26,7 @@ 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 CSV file has no data or is formatted incorrectly.",
|
||||
"csv_import_partially_failed" => "There were {0} item import failure(s) on line(s): {1}. No rows were imported.",
|
||||
"csv_import_success" => "Item CSV import successful.",
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
"current_password_invalid" => "รหัสผ่านปัจจุบันไม่ถูกต้อง",
|
||||
"employee" => "พนักงาน",
|
||||
"error_adding_updating" => "การเพิ่มหรือปรับปรุงข้อมูลพนักงานผิดพลาด",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "คุณไม่สามารถลบผู้ใช้งานสำหรับการเดโม้ได้",
|
||||
"error_updating_demo_admin" => "คุณไม่สามารถทำการเปลี่ยนข้อมูลผู้ใช้งานเดโม้ได้",
|
||||
"language" => "ภาษา",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "ต้องกรอกราคาขายส่ง",
|
||||
"count" => "แก้ไขจำนวนสินค้าคงคลัง",
|
||||
"csv_import_failed" => "นำเข้าข้อมูล CSV ล้มเหลว",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "Your uploaded file has no data or wrong format",
|
||||
"csv_import_partially_failed" => "มีรายการ {0} รายการที่นำเข้าล้มเหลว : {1} รายการที่ยังไม่ได้นำเข้า",
|
||||
"csv_import_success" => "Import of Items successful",
|
||||
|
||||
@@ -14,6 +14,8 @@ 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 change the demo admin user.",
|
||||
"error_updating_demo_admin" => "You can not delete the demo admin user.",
|
||||
"language" => "Language",
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
"cost_price_required" => "Purchase 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" => "Customer import successful with some failures:",
|
||||
"csv_import_success" => "Item import successful.",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user