[Feature]: Add person attributes support with unified attribute system

- Add person_id column to attribute_links table for person attributes
- Add person_attribute column to attribute_definitions to distinguish item vs person attributes
- Add visibility flags: SHOW_IN_CUSTOMERS, SHOW_IN_EMPLOYEES, SHOW_IN_SUPPLIERS
- Add person attribute methods to Attribute model (getAttributesByPerson, savePersonAttributeLink, etc.)
- Refactor Persons controller with shared attribute handling (getPersonAttributes, savePersonAttributes)
- Update Customers, Employees, Suppliers controllers with PSR-12 naming and attribute integration
- Create attributes/person.php view for rendering person attributes
- Add person attributes container to customer/employee/supplier forms

Relates to #3161
This commit is contained in:
Ollama
2026-04-20 06:36:24 +00:00
parent e602eddb47
commit 3fefd34864
13 changed files with 816 additions and 372 deletions

View File

@@ -246,7 +246,7 @@ class Attributes extends Secure_Controller
$data['definition_group'][''] = lang('Common.none_selected_text');
$data['definition_info'] = $info;
$show_all = Attribute::SHOW_IN_ITEMS | Attribute::SHOW_IN_RECEIVINGS | Attribute::SHOW_IN_SALES;
$show_all = Attribute::SHOW_IN_ITEMS | Attribute::SHOW_IN_RECEIVINGS | Attribute::SHOW_IN_SALES | Attribute::SHOW_IN_SEARCH | Attribute::SHOW_IN_CUSTOMERS | Attribute::SHOW_IN_EMPLOYEES | Attribute::SHOW_IN_SUPPLIERS;
$data['definition_flags'] = $this->get_attributes($show_all);
$selected_flags = $info->definition_flags === '' ? $show_all : $info->definition_flags;
$data['selected_definition_flags'] = $this->get_attributes($selected_flags);

View File

@@ -3,7 +3,7 @@
namespace App\Controllers;
use App\Libraries\Mailchimp_lib;
use App\Models\Attribute;
use App\Models\Customer;
use App\Models\Customer_rewards;
use App\Models\Tax_code;
@@ -15,34 +15,31 @@ use stdClass;
class Customers extends Persons
{
private string $_list_id;
private Mailchimp_lib $mailchimp_lib;
private Customer_rewards $customer_rewards;
private string $listId;
private Mailchimp_lib $mailchimpLib;
private Customer_rewards $customerRewards;
private Customer $customer;
private Tax_code $tax_code;
private array $config;
private Tax_code $taxCode;
private array $appConfig;
public function __construct()
{
parent::__construct('customers');
$this->mailchimp_lib = new Mailchimp_lib();
$this->customer_rewards = model(Customer_rewards::class);
$this->mailchimpLib = new Mailchimp_lib();
$this->customerRewards = model(Customer_rewards::class);
$this->customer = model(Customer::class);
$this->tax_code = model(Tax_code::class);
$this->config = config(OSPOS::class)->settings;
$this->taxCode = model(Tax_code::class);
$this->appConfig = config(OSPOS::class)->settings;
$encrypter = Services::encrypter();
if (!empty($this->config['mailchimp_list_id'])) {
$this->_list_id = $encrypter->decrypt($this->config['mailchimp_list_id']);
if (!empty($this->appConfig['mailchimp_list_id'])) {
$this->listId = $encrypter->decrypt($this->appConfig['mailchimp_list_id']);
} else {
$this->_list_id = '';
$this->listId = '';
}
}
/**
* @return string
*/
public function getIndex(): string
{
$data['table_headers'] = get_customer_manage_table_headers();
@@ -50,19 +47,13 @@ class Customers extends Persons
return view('people/manage', $data);
}
/**
* Gets one row for a customer manage table. This is called using AJAX to update one row.
* @return ResponseInterface
*/
public function getRow(int $row_id): ResponseInterface
public function getRow(int $rowId): ResponseInterface
{
$person = $this->customer->get_info($row_id);
$person = $this->customer->get_info($rowId);
// Retrieve the total amount the customer spent so far together with min, max and average values
$stats = $this->customer->get_stats($person->person_id); // TODO: This and the next 11 lines are duplicated in search(). Extract a method.
$stats = $this->customer->get_stats($person->person_id);
if (empty($stats)) {
// Create object with empty properties.
$stats = new stdClass();
$stats->total = 0;
$stats->min = 0;
@@ -72,17 +63,11 @@ class Customers extends Persons
$stats->quantity = 0;
}
$data_row = get_customer_data_row($person, $stats);
$dataRow = get_customer_data_row($person, $stats);
return $this->response->setJSON($data_row);
return $this->response->setJSON($dataRow);
}
/**
* Returns customer table data rows. This will be called with AJAX.
*
* @return void
*/
public function getSearch(): ResponseInterface
{
$search = $this->request->getGet('search');
@@ -92,15 +77,13 @@ class Customers extends Persons
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$customers = $this->customer->search($search, $limit, $offset, $sort, $order);
$total_rows = $this->customer->get_found_rows($search);
$totalRows = $this->customer->get_found_rows($search);
$data_rows = [];
$dataRows = [];
foreach ($customers->getResult() as $person) {
// Retrieve the total amount the customer spent so far together with min, max and average values
$stats = $this->customer->get_stats($person->person_id); // TODO: duplicated... see above
$stats = $this->customer->get_stats($person->person_id);
if (empty($stats)) {
// Create object with empty properties.
$stats = new stdClass();
$stats->total = 0;
$stats->min = 0;
@@ -110,16 +93,12 @@ class Customers extends Persons
$stats->quantity = 0;
}
$data_rows[] = get_customer_data_row($person, $stats);
$dataRows[] = get_customer_data_row($person, $stats);
}
return $this->response->setJSON(['total' => $total_rows, 'rows' => $data_rows]);
return $this->response->setJSON(['total' => $totalRows, 'rows' => $dataRows]);
}
/**
* Gives search suggestions based on what is being searched for
* @return ResponseInterface
*/
public function getSuggest(): ResponseInterface
{
$search = $this->request->getGet('term');
@@ -128,10 +107,7 @@ class Customers extends Persons
return $this->response->setJSON($suggestions);
}
/**
* @return ResponseInterface
*/
public function suggest_search(): ResponseInterface
public function suggestSearch(): ResponseInterface
{
$search = $this->request->getGet('term');
$suggestions = $this->customer->get_search_suggestions($search, 25, false);
@@ -139,16 +115,11 @@ class Customers extends Persons
return $this->response->setJSON($suggestions);
}
/**
* Loads the customer edit form
* @return string
*/
public function getView(int $customer_id = NEW_ENTRY): string
public function getView(int $customerId = NEW_ENTRY): string
{
// Set default values
if ($customer_id == null) $customer_id = NEW_ENTRY;
if ($customerId == null) $customerId = NEW_ENTRY;
$info = $this->customer->get_info($customer_id);
$info = $this->customer->get_info($customerId);
foreach (get_object_vars($info) as $property => $value) {
$info->$property = $value;
}
@@ -159,28 +130,27 @@ class Customers extends Persons
$data['person_info']->employee_id = $this->employee->get_logged_in_employee_info()->person_id;
}
$employee_info = $this->employee->get_info($info->employee_id);
$data['employee'] = $employee_info->first_name . ' ' . $employee_info->last_name;
$employeeInfo = $this->employee->get_info($info->employee_id);
$data['employee'] = $employeeInfo->first_name . ' ' . $employeeInfo->last_name;
$tax_code_info = $this->tax_code->get_info($info->sales_tax_code_id);
$taxCodeInfo = $this->taxCode->get_info($info->sales_tax_code_id);
if ($tax_code_info->tax_code != null) {
$data['sales_tax_code_label'] = $tax_code_info->tax_code . ' ' . $tax_code_info->tax_code_name;
if ($taxCodeInfo->tax_code != null) {
$data['sales_tax_code_label'] = $taxCodeInfo->tax_code . ' ' . $taxCodeInfo->tax_code_name;
} else {
$data['sales_tax_code_label'] = '';
}
$packages = ['' => lang('Items.none')];
foreach ($this->customer_rewards->get_all()->getResultArray() as $row) {
foreach ($this->customerRewards->get_all()->getResultArray() as $row) {
$packages[$row['package_id']] = $row['package_name'];
}
$data['packages'] = $packages;
$data['selected_package'] = $info->package_id;
$data['use_destination_based_tax'] = $this->config['use_destination_based_tax'];
$data['use_destination_based_tax'] = $this->appConfig['use_destination_based_tax'];
// Retrieve the total amount the customer spent so far together with min, max and average values
$stats = $this->customer->get_stats($customer_id);
$stats = $this->customer->get_stats($customerId);
if (!empty($stats)) {
foreach (get_object_vars($stats) as $property => $value) {
$info->$property = $value;
@@ -188,14 +158,11 @@ class Customers extends Persons
$data['stats'] = $stats;
}
// Retrieve the info from Mailchimp only if there is an email address assigned
if (!empty($info->email)) {
// Collect Mailchimp customer info
if (($mailchimp_info = $this->mailchimp_lib->getMemberInfo($this->_list_id, $info->email)) !== false) {
$data['mailchimp_info'] = $mailchimp_info;
if (($mailchimpInfo = $this->mailchimpLib->getMemberInfo($this->listId, $info->email)) !== false) {
$data['mailchimp_info'] = $mailchimpInfo;
// Collect customer Mailchimp emails activities (stats)
if (($activities = $this->mailchimp_lib->getMemberActivity($this->_list_id, $info->email)) !== false) {
if (($activities = $this->mailchimpLib->getMemberActivity($this->listId, $info->email)) !== false) {
if (array_key_exists('activity', $activities)) {
$open = 0;
$unopen = 0;
@@ -235,22 +202,25 @@ class Customers extends Persons
}
/**
* Inserts/updates a customer
* @return ResponseInterface
* Gets person attributes for a customer (AJAX)
*/
public function postSave(int $customer_id = NEW_ENTRY): ResponseInterface
public function getAttributes(int $customerId = NEW_ENTRY): string
{
$first_name = $this->request->getPost('first_name');
$last_name = $this->request->getPost('last_name');
return $this->getPersonAttributes($customerId, Attribute::SHOW_IN_CUSTOMERS);
}
public function postSave(int $customerId = NEW_ENTRY): ResponseInterface
{
$firstName = $this->request->getPost('first_name');
$lastName = $this->request->getPost('last_name');
$email = strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL));
// Format first and last name properly
$first_name = $this->nameize($first_name);
$last_name = $this->nameize($last_name);
$firstName = $this->nameize($firstName);
$lastName = $this->nameize($lastName);
$person_data = [
'first_name' => $first_name,
'last_name' => $last_name,
$personData = [
'first_name' => $firstName,
'last_name' => $lastName,
'gender' => $this->request->getPost('gender', FILTER_SANITIZE_NUMBER_INT),
'email' => $email,
'phone_number' => $this->request->getPost('phone_number'),
@@ -263,9 +233,9 @@ class Customers extends Persons
'comments' => $this->request->getPost('comments')
];
$date_formatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $this->request->getPost('date'));
$dateFormatter = date_create_from_format($this->appConfig['dateformat'] . ' ' . $this->appConfig['timeformat'], $this->request->getPost('date'));
$customer_data = [
$customerData = [
'consent' => $this->request->getPost('consent') != null,
'account_number' => $this->request->getPost('account_number') == '' ? null : $this->request->getPost('account_number'),
'tax_id' => $this->request->getPost('tax_id'),
@@ -274,68 +244,57 @@ class Customers extends Persons
'discount_type' => $this->request->getPost('discount_type') == null ? PERCENT : $this->request->getPost('discount_type', FILTER_SANITIZE_NUMBER_INT),
'package_id' => $this->request->getPost('package_id') == '' ? null : $this->request->getPost('package_id'),
'taxable' => $this->request->getPost('taxable') != null,
'date' => $date_formatter->format('Y-m-d H:i:s'),
'date' => $dateFormatter->format('Y-m-d H:i:s'),
'employee_id' => $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT),
'sales_tax_code_id' => $this->request->getPost('sales_tax_code_id') == '' ? null : $this->request->getPost('sales_tax_code_id', FILTER_SANITIZE_NUMBER_INT)
];
if ($this->customer->save_customer($person_data, $customer_data, $customer_id)) {
// Save customer to Mailchimp selected list // TODO: addOrUpdateMember should be refactored. Potentially pass an array or object instead of 6 parameters.
$mailchimp_status = $this->request->getPost('mailchimp_status');
$this->mailchimp_lib->addOrUpdateMember(
$this->_list_id,
if ($this->customer->save_customer($personData, $customerData, $customerId)) {
$personId = $customerId == NEW_ENTRY ? $customerData['person_id'] : $customerId;
$this->savePersonAttributes($personId, Attribute::SHOW_IN_CUSTOMERS);
$mailchimpStatus = $this->request->getPost('mailchimp_status');
$this->mailchimpLib->addOrUpdateMember(
$this->listId,
$email,
$first_name,
$last_name,
$mailchimp_status == null ? "" : $mailchimp_status,
$firstName,
$lastName,
$mailchimpStatus == null ? "" : $mailchimpStatus,
['vip' => $this->request->getPost('mailchimp_vip') != null]
);
// New customer
if ($customer_id == NEW_ENTRY) {
if ($customerId == NEW_ENTRY) {
return $this->response->setJSON([
'success' => true,
'message' => lang('Customers.successful_adding') . ' ' . $first_name . ' ' . $last_name,
'id' => $customer_data['person_id']
'message' => lang('Customers.successful_adding') . ' ' . $firstName . ' ' . $lastName,
'id' => $customerData['person_id']
]);
} else { // Existing customer
} else {
return $this->response->setJSON([
'success' => true,
'message' => lang('Customers.successful_updating') . ' ' . $first_name . ' ' . $last_name,
'id' => $customer_id
'message' => lang('Customers.successful_updating') . ' ' . $firstName . ' ' . $lastName,
'id' => $customerId
]);
}
} else { // Failure
} else {
return $this->response->setJSON([
'success' => false,
'message' => lang('Customers.error_adding_updating') . ' ' . $first_name . ' ' . $last_name,
'message' => lang('Customers.error_adding_updating') . ' ' . $firstName . ' ' . $lastName,
'id' => NEW_ENTRY
]);
}
}
/**
* Verifies if an email address already exists. Used in app/Views/customers/form.php
*
* @return ResponseInterface
* @noinspection PhpUnused
*/
public function postCheckEmail(): ResponseInterface
{
$email = strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL));
$person_id = $this->request->getPost('person_id', FILTER_SANITIZE_NUMBER_INT);
$personId = $this->request->getPost('person_id', FILTER_SANITIZE_NUMBER_INT);
$exists = $this->customer->check_email_exists($email, $person_id);
$exists = $this->customer->check_email_exists($email, $personId);
return $this->response->setJSON(!$exists ? 'true' : 'false');
}
/**
* Verifies if an account number already exists. Used in app/Views/customers/form.php
*
* @return ResponseInterface
* @noinspection PhpUnused
*/
public function postCheckAccountNumber(): ResponseInterface
{
$exists = $this->customer->check_account_number_exists($this->request->getPost('account_number'), $this->request->getPost('person_id', FILTER_SANITIZE_NUMBER_INT));
@@ -343,27 +302,22 @@ class Customers extends Persons
return $this->response->setJSON(!$exists ? 'true' : 'false');
}
/**
* This deletes customers from the customers table
* @return ResponseInterface
*/
public function postDelete(): ResponseInterface
{
$customers_to_delete = $this->request->getPost('ids');
$customers_info = $this->customer->get_multiple_info($customers_to_delete);
$customersToDelete = $this->request->getPost('ids');
$customersInfo = $this->customer->get_multiple_info($customersToDelete);
$count = 0;
foreach ($customers_info->getResult() as $info) {
foreach ($customersInfo->getResult() as $info) {
if ($this->customer->delete($info->person_id)) {
// remove customer from Mailchimp selected list
$this->mailchimp_lib->removeMember($this->_list_id, $info->email);
$this->mailchimpLib->removeMember($this->listId, $info->email);
$count++;
}
}
if ($count == count($customers_to_delete)) {
if ($count == count($customersToDelete)) {
return $this->response->setJSON([
'success' => true,
'message' => lang('Customers.successful_deleted') . ' ' . $count . ' ' . lang('Customers.one_or_multiple')
@@ -373,12 +327,6 @@ class Customers extends Persons
}
}
/**
* Customers import from csv spreadsheet
*
* @return DownloadResponse The template for Customer CSV imports is returned and download forced.
* @noinspection PhpUnused
*/
public function getCsv(): DownloadResponse
{
$name = 'importCustomers.csv';
@@ -386,30 +334,17 @@ class Customers extends Persons
return $this->response->download($name, $data);
}
/**
* Displays the customer CSV import modal. Used in app/Views/people/manage.php
*
* @return string
* @noinspection PhpUnused
*/
public function getCsvImport(): string
{
return view('customers/form_csv_import');
}
/**
* Imports a CSV file containing customers. Used in app/Views/customers/form_csv_import.php
*
* @return ResponseInterface
* @noinspection PhpUnused
*/
public function postImportCsvFile(): ResponseInterface
{
if ($_FILES['file_path']['error'] != UPLOAD_ERR_OK) {
return $this->response->setJSON(['success' => false, 'message' => lang('Customers.csv_import_failed')]);
} else {
if (($handle = fopen($_FILES['file_path']['tmp_name'], 'r')) !== false) {
// Skip the first row as it's the table description
fgetcsv($handle);
$i = 1;
@@ -420,7 +355,7 @@ class Customers extends Persons
if (sizeof($data) >= 16 && $consent) {
$email = strtolower($data[4]);
$person_data = [
$personData = [
'first_name' => $data[0],
'last_name' => $data[1],
'gender' => $data[2],
@@ -435,7 +370,7 @@ class Customers extends Persons
'comments' => $data[12]
];
$customer_data = [
$customerData = [
'consent' => $consent,
'company_name' => $data[13],
'discount' => $data[15],
@@ -444,14 +379,13 @@ class Customers extends Persons
'date' => date('Y-m-d H:i:s'),
'employee_id' => $this->employee->get_logged_in_employee_info()->person_id
];
$account_number = $data[14];
$accountNumber = $data[14];
// Don't duplicate people with same email
$invalidated = $this->customer->check_email_exists($email);
if ($account_number != '') {
$customer_data['account_number'] = $account_number;
$invalidated &= $this->customer->check_account_number_exists($account_number);
if ($accountNumber != '') {
$customerData['account_number'] = $accountNumber;
$invalidated &= $this->customer->check_account_number_exists($accountNumber);
}
} else {
$invalidated = true;
@@ -460,9 +394,8 @@ class Customers extends Persons
if ($invalidated) {
$failCodes[] = $i;
log_message('error', "Row $i was not imported: Either email or account number already exist or data was invalid.");
} elseif ($this->customer->save_customer($person_data, $customer_data)) {
// Save customer to Mailchimp selected list
$this->mailchimp_lib->addOrUpdateMember($this->_list_id, $person_data['email'], $person_data['first_name'], '', $person_data['last_name']);
} elseif ($this->customer->save_customer($personData, $customerData)) {
$this->mailchimpLib->addOrUpdateMember($this->listId, $personData['email'], $personData['first_name'], '', $personData['last_name']);
} else {
$failCodes[] = $i;
}
@@ -482,4 +415,4 @@ class Customers extends Persons
}
}
}
}
}

View File

@@ -2,18 +2,15 @@
namespace App\Controllers;
use App\Models\Attribute;
use App\Models\Module;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;
/**
*
*
* @property module module
*
*/
class Employees extends Persons
{
protected Module $module;
public function __construct()
{
parent::__construct('employees');
@@ -21,35 +18,25 @@ class Employees extends Persons
$this->module = model('Module');
}
/**
* Returns employee table data rows. This will be called with AJAX.
*
* @return void
*/
public function getSearch(): ResponseInterface
{
$search = $this->request->getGet('search');
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
$sort = $this->sanitizeSortColumn(person_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'people.person_id');
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$sort = $this->sanitizeSortColumn(person_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'people.person_id');
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$employees = $this->employee->search($search, $limit, $offset, $sort, $order);
$total_rows = $this->employee->get_found_rows($search);
$totalRows = $this->employee->get_found_rows($search);
$data_rows = [];
$dataRows = [];
foreach ($employees->getResult() as $person) {
$data_rows[] = get_person_data_row($person);
$dataRows[] = get_person_data_row($person);
}
return $this->response->setJSON(['total' => $total_rows, 'rows' => $data_rows]);
return $this->response->setJSON(['total' => $totalRows, 'rows' => $dataRows]);
}
/**
* AJAX called function gives search suggestions based on what is being searched for.
*
* @return ResponseInterface
*/
public function getSuggest(): ResponseInterface
{
$search = $this->request->getGet('term');
@@ -58,10 +45,7 @@ class Employees extends Persons
return $this->response->setJSON($suggestions);
}
/**
* @return ResponseInterface
*/
public function suggest_search(): ResponseInterface
public function suggestSearch(): ResponseInterface
{
$search = $this->request->getPost('term');
$suggestions = $this->employee->get_search_suggestions($search);
@@ -69,39 +53,35 @@ class Employees extends Persons
return $this->response->setJSON($suggestions);
}
/**
* Loads the employee edit form
* @return string
*/
public function getView(int $employee_id = NEW_ENTRY): string
public function getView(int $employeeId = NEW_ENTRY): string
{
$person_info = $this->employee->get_info($employee_id);
$current_user = $this->employee->get_logged_in_employee_info();
$personInfo = $this->employee->get_info($employeeId);
$currentUser = $this->employee->get_logged_in_employee_info();
if ($employee_id != NEW_ENTRY && !$this->employee->canModifyEmployee($person_info->person_id, $current_user->person_id)) {
if ($employeeId != NEW_ENTRY && !$this->employee->canModifyEmployee($personInfo->person_id, $currentUser->person_id)) {
header('Location: ' . base_url('no_access/employees/employees'));
exit();
}
foreach (get_object_vars($person_info) as $property => $value) {
$person_info->$property = $value;
foreach (get_object_vars($personInfo) as $property => $value) {
$personInfo->$property = $value;
}
$data['person_info'] = $person_info;
$data['employee_id'] = $employee_id;
$data['person_info'] = $personInfo;
$data['employee_id'] = $employeeId;
$modules = [];
foreach ($this->module->get_all_modules()->getResult() as $module) {
$module->grant = $this->employee->has_grant($module->module_id, $person_info->person_id);
$module->menu_group = $this->employee->get_menu_group($module->module_id, $person_info->person_id);
$module->grant = $this->employee->has_grant($module->module_id, $personInfo->person_id);
$module->menu_group = $this->employee->get_menu_group($module->module_id, $personInfo->person_id);
$modules[] = $module;
}
$data['all_modules'] = $modules;
$permissions = [];
foreach ($this->module->get_all_subpermissions()->getResult() as $permission) { // TODO: subpermissions does not follow naming standards.
foreach ($this->module->get_all_subpermissions()->getResult() as $permission) {
$permission->permission_id = str_replace(' ', '_', $permission->permission_id);
$permission->grant = $this->employee->has_grant($permission->permission_id, $person_info->person_id);
$permission->grant = $this->employee->has_grant($permission->permission_id, $personInfo->person_id);
$permissions[] = $permission;
}
@@ -110,17 +90,18 @@ class Employees extends Persons
return view('employees/form', $data);
}
/**
* Inserts/updates an employee
* @return ResponseInterface
*/
public function postSave(int $employee_id = NEW_ENTRY): ResponseInterface
public function getAttributes(int $employeeId = NEW_ENTRY): string
{
$current_user = $this->employee->get_logged_in_employee_info();
return $this->getPersonAttributes($employeeId, Attribute::SHOW_IN_EMPLOYEES);
}
if ($employee_id != NEW_ENTRY) {
$target_employee = $this->employee->get_info($employee_id);
if (!$this->employee->canModifyEmployee($target_employee->person_id, $current_user->person_id)) {
public function postSave(int $employeeId = NEW_ENTRY): ResponseInterface
{
$currentUser = $this->employee->get_logged_in_employee_info();
if ($employeeId != NEW_ENTRY) {
$targetEmployee = $this->employee->get_info($employeeId);
if (!$this->employee->canModifyEmployee($targetEmployee->person_id, $currentUser->person_id)) {
return $this->response->setJSON([
'success' => false,
'message' => lang('Employees.error_updating_admin'),
@@ -129,17 +110,16 @@ class Employees extends Persons
}
}
$first_name = $this->request->getPost('first_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS); // TODO: duplicated code
$last_name = $this->request->getPost('last_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$firstName = $this->request->getPost('first_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$lastName = $this->request->getPost('last_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$email = strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL));
// format first and last name properly
$first_name = $this->nameize($first_name);
$last_name = $this->nameize($last_name);
$firstName = $this->nameize($firstName);
$lastName = $this->nameize($lastName);
$person_data = [
'first_name' => $first_name,
'last_name' => $last_name,
$personData = [
'first_name' => $firstName,
'last_name' => $lastName,
'gender' => $this->request->getPost('gender', FILTER_SANITIZE_NUMBER_INT),
'email' => $email,
'phone_number' => $this->request->getPost('phone_number', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
@@ -152,108 +132,98 @@ class Employees extends Persons
'comments' => $this->request->getPost('comments', FILTER_SANITIZE_FULL_SPECIAL_CHARS)
];
$grants_array = [];
$isAdmin = $this->employee->isAdmin($current_user->person_id);
$grantsArray = [];
$isAdmin = $this->employee->isAdmin($currentUser->person_id);
foreach ($this->module->get_all_permissions()->getResult() as $permission) {
$grants = [];
$grant = $this->request->getPost('grant_' . $permission->permission_id) != null ? $this->request->getPost('grant_' . $permission->permission_id, FILTER_SANITIZE_FULL_SPECIAL_CHARS) : '';
if ($grant == $permission->permission_id) {
if (!$isAdmin && !$this->employee->has_grant($permission->permission_id, $current_user->person_id)) {
if (!$isAdmin && !$this->employee->has_grant($permission->permission_id, $currentUser->person_id)) {
continue;
}
$grants['permission_id'] = $permission->permission_id;
$grants['menu_group'] = $this->request->getPost('menu_group_' . $permission->permission_id) != null ? $this->request->getPost('menu_group_' . $permission->permission_id, FILTER_SANITIZE_FULL_SPECIAL_CHARS) : '--';
$grants_array[] = $grants;
$grantsArray[] = $grants;
}
}
// Password has been changed OR first time password set
if (!empty($this->request->getPost('password')) && ENVIRONMENT != 'testing') {
$exploded = explode(":", $this->request->getPost('language', FILTER_SANITIZE_FULL_SPECIAL_CHARS));
$employee_data = [
$employeeData = [
'username' => $this->request->getPost('username', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
'password' => password_hash($this->request->getPost('password'), PASSWORD_DEFAULT),
'hash_version' => 2,
'language_code' => $exploded[0],
'language' => $exploded[1]
];
} else { // Password not changed
} else {
$exploded = explode(":", $this->request->getPost('language', FILTER_SANITIZE_FULL_SPECIAL_CHARS));
$employee_data = [
$employeeData = [
'username' => $this->request->getPost('username', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
'language_code' => $exploded[0],
'language' => $exploded[1]
];
}
if ($this->employee->save_employee($person_data, $employee_data, $grants_array, $employee_id)) {
// New employee
if ($employee_id == NEW_ENTRY) {
if ($this->employee->save_employee($personData, $employeeData, $grantsArray, $employeeId)) {
$personId = $employeeId == NEW_ENTRY ? $employeeData['person_id'] : $employeeId;
$this->savePersonAttributes($personId, Attribute::SHOW_IN_EMPLOYEES);
if ($employeeId == NEW_ENTRY) {
return $this->response->setJSON([
'success' => true,
'message' => lang('Employees.successful_adding') . ' ' . $first_name . ' ' . $last_name,
'id' => $employee_data['person_id']
'message' => lang('Employees.successful_adding') . ' ' . $firstName . ' ' . $lastName,
'id' => $employeeData['person_id']
]);
} else { // Existing employee
$logged_in_employee_id = session()->get('person_id');
if ($employee_id == $logged_in_employee_id) {
session()->set('language_code', $employee_data['language_code']);
session()->set('language', $employee_data['language']);
} else {
$loggedInEmployeeId = session()->get('person_id');
if ($employeeId == $loggedInEmployeeId) {
session()->set('language_code', $employeeData['language_code']);
session()->set('language', $employeeData['language']);
}
return $this->response->setJSON([
'success' => true,
'message' => lang('Employees.successful_updating') . ' ' . $first_name . ' ' . $last_name,
'id' => $employee_id
'message' => lang('Employees.successful_updating') . ' ' . $firstName . ' ' . $lastName,
'id' => $employeeId
]);
}
} else { // Failure
} else {
return $this->response->setJSON([
'success' => false,
'message' => lang('Employees.error_adding_updating') . ' ' . $first_name . ' ' . $last_name,
'message' => lang('Employees.error_adding_updating') . ' ' . $firstName . ' ' . $lastName,
'id' => NEW_ENTRY
]);
}
}
/**
* This deletes employees from the employees table
* @return ResponseInterface
*/
public function postDelete(): ResponseInterface
{
$employees_to_delete = $this->request->getPost('ids', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$current_user = $this->employee->get_logged_in_employee_info();
$employeesToDelete = $this->request->getPost('ids', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$currentUser = $this->employee->get_logged_in_employee_info();
if (!$this->employee->isAdmin($current_user->person_id)) {
foreach ($employees_to_delete as $emp_id) {
if ($this->employee->isAdmin((int)$emp_id)) {
if (!$this->employee->isAdmin($currentUser->person_id)) {
foreach ($employeesToDelete as $empId) {
if ($this->employee->isAdmin((int)$empId)) {
return $this->response->setJSON(['success' => false, 'message' => lang('Employees.error_deleting_admin')]);
}
}
}
if ($this->employee->delete_list($employees_to_delete)) { // TODO: this is passing a string, but delete_list expects an array
if ($this->employee->delete_list($employeesToDelete)) {
return $this->response->setJSON([
'success' => true,
'message' => lang('Employees.successful_deleted') . ' ' . count($employees_to_delete) . ' ' . lang('Employees.one_or_multiple')
'message' => lang('Employees.successful_deleted') . ' ' . count($employeesToDelete) . ' ' . lang('Employees.one_or_multiple')
]);
} else {
return $this->response->setJSON(['success' => false, 'message' => lang('Employees.cannot_be_deleted')]);
}
}
/**
* Checks an employee username against the database. Used in app\Views\employees\form.php
*
* @param $employee_id
* @return ResponseInterface
* @noinspection PhpUnused
*/
public function getCheckUsername($employee_id): ResponseInterface
public function getCheckUsername($employeeId): ResponseInterface
{
$exists = $this->employee->username_exists($employee_id, $this->request->getGet('username'));
$exists = $this->employee->username_exists($employeeId, $this->request->getGet('username'));
return $this->response->setJSON(!$exists ? 'true' : 'false');
}
}
}

View File

@@ -2,28 +2,28 @@
namespace App\Controllers;
use App\Models\Attribute;
use App\Models\Person;
use CodeIgniter\HTTP\ResponseInterface;
use Config\OSPOS;
use Config\Services;
use function Tamtamchik\NameCase\str_name_case;
abstract class Persons extends Secure_Controller
{
protected Person $person;
protected Attribute $attribute;
protected array $appConfig;
/**
* @param string|null $module_id
*/
public function __construct(?string $module_id = null)
public function __construct(?string $moduleId = null)
{
parent::__construct($module_id);
parent::__construct($moduleId);
$this->person = model(Person::class);
$this->attribute = model(Attribute::class);
$this->appConfig = config(OSPOS::class)->settings;
}
/**
* @return string
*/
public function getIndex(): string
{
$data['table_headers'] = get_people_manage_table_headers();
@@ -31,10 +31,6 @@ abstract class Persons extends Secure_Controller
return view('people/manage', $data);
}
/**
* Gives search suggestions based on what is being searched for
* @return ResponseInterface
*/
public function getSuggest(): ResponseInterface
{
$search = $this->request->getGet('term');
@@ -43,34 +39,88 @@ abstract class Persons extends Secure_Controller
return $this->response->setJSON($suggestions);
}
/**
* Gets one row for a person manage table. This is called using AJAX to update one row.
* @return ResponseInterface
*/
public function getRow(int $row_id): ResponseInterface
public function getRow(int $rowId): ResponseInterface
{
$data_row = get_person_data_row($this->person->get_info($row_id));
$dataRow = get_person_data_row($this->person->get_info($rowId));
return $this->response->setJSON($data_row);
return $this->response->setJSON($dataRow);
}
protected function getPersonAttributes(int $personId, int $definitionFlags): string
{
$data['person_id'] = $personId;
$data['config'] = $this->appConfig;
$definitionIds = json_decode($this->request->getGet('definition_ids') ?? '', true);
$data['definition_values'] = $this->attribute->getAttributesByPerson($personId) + $this->attribute->get_values_by_definitions($definitionIds);
$data['definition_names'] = $this->attribute->getDefinitionsByType(true, $definitionFlags);
foreach ($data['definition_values'] as $definitionId => $definitionValue) {
$attributeValue = $this->attribute->getPersonAttributeValue($personId, $definitionId);
$attributeId = (empty($attributeValue) || empty($attributeValue->attribute_id)) ? null : $attributeValue->attribute_id;
$values = &$data['definition_values'][$definitionId];
$values['attribute_id'] = $attributeId;
$values['attribute_value'] = $attributeValue;
$values['selected_value'] = '';
if ($definitionValue['definition_type'] === DROPDOWN) {
$values['values'] = $this->attribute->get_definition_values($definitionId);
$linkValue = $this->getPersonLinkValue($personId, $definitionId);
$values['selected_value'] = (empty($linkValue)) ? '' : $linkValue->attribute_id;
}
if (!empty($definitionIds[$definitionId])) {
$values['selected_value'] = $definitionIds[$definitionId];
}
unset($data['definition_names'][$definitionId]);
}
return view('attributes/person', $data);
}
private function getPersonLinkValue(int $personId, int $definitionId): ?object
{
$builder = $this->db->table('attribute_links');
$builder->where('person_id', $personId);
$builder->where('item_id', null);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
$builder->where('definition_id', $definitionId);
return $builder->get()->getRowObject();
}
protected function savePersonAttributes(int $personId, int $definitionFlags): void
{
$attributeLinks = $this->request->getPost('attribute_links') ?? [];
$attributeIds = $this->request->getPost('attribute_ids') ?? [];
$this->attribute->deletePersonAttributeLinks($personId);
foreach ($attributeLinks as $definitionId => $attributeId) {
$definitionInfo = $this->attribute->getAttributeInfo((int)$definitionId);
$definitionType = $definitionInfo->definition_type;
if ($definitionType !== DROPDOWN) {
$attributeId = $this->attribute->savePersonAttributeValue(
$attributeId,
(int)$definitionId,
$personId,
$attributeIds[$definitionId] ?? false,
$definitionType
);
}
$this->attribute->savePersonAttributeLink($personId, (int)$definitionId, (int)$attributeId);
}
}
/**
* Capitalize segments of a name, and put the rest into lower case.
* You can pass the characters you want to use as delimiters as exceptions.
* The function supports UTF-8 strings
*
* Example:
* i.e. <?php echo nameize("john o'grady-smith"); ?>
*
* returns John O'Grady-Smith
*/
protected function nameize(string $input): string
{
$adjusted_name = str_name_case($input);
$adjustedName = str_name_case($input);
// TODO: Use preg_replace to match HTML entities and convert them to lowercase. This is a workaround for https://github.com/tamtamchik/namecase/issues/20
return preg_replace_callback('/&[a-zA-Z0-9#]+;/', function ($matches) {
return strtolower($matches[0]);
}, $adjusted_name);
}, $adjustedName);
}
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Controllers;
use App\Models\Attribute;
use App\Models\Supplier;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;
@@ -17,9 +18,6 @@ class Suppliers extends Persons
$this->supplier = model(Supplier::class);
}
/**
* @return string
*/
public function getIndex(): string
{
$data['table_headers'] = get_suppliers_manage_table_headers();
@@ -27,23 +25,14 @@ class Suppliers extends Persons
return view('people/manage', $data);
}
/**
* Gets one row for a supplier manage table. This is called using AJAX to update one row.
* @param $row_id
* @return ResponseInterface
*/
public function getRow($row_id): ResponseInterface
public function getRow($rowId): ResponseInterface
{
$data_row = get_supplier_data_row($this->supplier->get_info($row_id));
$data_row['category'] = $this->supplier->get_category_name($data_row['category']);
$dataRow = get_supplier_data_row($this->supplier->get_info($rowId));
$dataRow['category'] = $this->supplier->get_category_name($dataRow['category']);
return $this->response->setJSON($data_row);
return $this->response->setJSON($dataRow);
}
/**
* Returns Supplier table data rows. This will be called with AJAX.
* @return void
**/
public function getSearch(): ResponseInterface
{
$search = $this->request->getGet('search');
@@ -53,23 +42,19 @@ class Suppliers extends Persons
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$suppliers = $this->supplier->search($search, $limit, $offset, $sort, $order);
$total_rows = $this->supplier->get_found_rows($search);
$totalRows = $this->supplier->get_found_rows($search);
$data_rows = [];
$dataRows = [];
foreach ($suppliers->getResult() as $supplier) {
$row = get_supplier_data_row($supplier);
$row['category'] = $this->supplier->get_category_name($row['category']);
$data_rows[] = $row;
$dataRows[] = $row;
}
return $this->response->setJSON(['total' => $total_rows, 'rows' => $data_rows]);
return $this->response->setJSON(['total' => $totalRows, 'rows' => $dataRows]);
}
/**
* Gives search suggestions based on what is being searched for
* @return ResponseInterface
**/
public function getSuggest(): ResponseInterface
{
$search = $this->request->getGet('term');
@@ -78,10 +63,7 @@ class Suppliers extends Persons
return $this->response->setJSON($suggestions);
}
/**
* @return ResponseInterface
*/
public function suggest_search(): ResponseInterface
public function suggestSearch(): ResponseInterface
{
$search = $this->request->getPost('term');
$suggestions = $this->supplier->get_search_suggestions($search, false);
@@ -89,15 +71,9 @@ class Suppliers extends Persons
return $this->response->setJSON($suggestions);
}
/**
* Loads the supplier edit form
*
* @param int $supplier_id
* @return string
*/
public function getView(int $supplier_id = NEW_ENTRY): string
public function getView(int $supplierId = NEW_ENTRY): string
{
$info = $this->supplier->get_info($supplier_id);
$info = $this->supplier->get_info($supplierId);
foreach (get_object_vars($info) as $property => $value) {
$info->$property = $value;
}
@@ -107,25 +83,23 @@ class Suppliers extends Persons
return view("suppliers/form", $data);
}
/**
* Inserts/updates a supplier
*
* @param int $supplier_id
* @return ResponseInterface
*/
public function postSave(int $supplier_id = NEW_ENTRY): ResponseInterface
public function getAttributes(int $supplierId = NEW_ENTRY): string
{
$first_name = $this->request->getPost('first_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS); // TODO: Duplicate code
$last_name = $this->request->getPost('last_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
return $this->getPersonAttributes($supplierId, Attribute::SHOW_IN_SUPPLIERS);
}
public function postSave(int $supplierId = NEW_ENTRY): ResponseInterface
{
$firstName = $this->request->getPost('first_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$lastName = $this->request->getPost('last_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$email = strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL));
// Format first and last name properly
$first_name = $this->nameize($first_name);
$last_name = $this->nameize($last_name);
$firstName = $this->nameize($firstName);
$lastName = $this->nameize($lastName);
$person_data = [
'first_name' => $first_name,
'last_name' => $last_name,
$personData = [
'first_name' => $firstName,
'last_name' => $lastName,
'gender' => $this->request->getPost('gender'),
'email' => $email,
'phone_number' => $this->request->getPost('phone_number', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
@@ -138,7 +112,7 @@ class Suppliers extends Persons
'comments' => $this->request->getPost('comments', FILTER_SANITIZE_FULL_SPECIAL_CHARS)
];
$supplier_data = [
$supplierData = [
'company_name' => $this->request->getPost('company_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
'agency_name' => $this->request->getPost('agency_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
'category' => $this->request->getPost('category', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
@@ -146,47 +120,43 @@ class Suppliers extends Persons
'tax_id' => $this->request->getPost('tax_id', FILTER_SANITIZE_NUMBER_INT)
];
if ($this->supplier->save_supplier($person_data, $supplier_data, $supplier_id)) {
// New supplier
if ($supplier_id == NEW_ENTRY) {
return $this->response->setJSON([
'success' => true,
'message' => lang('Suppliers.successful_adding') . ' ' . $supplier_data['company_name'],
'id' => $supplier_data['person_id']
]);
} else { // Existing supplier
if ($this->supplier->save_supplier($personData, $supplierData, $supplierId)) {
$personId = $supplierId == NEW_ENTRY ? $supplierData['person_id'] : $supplierId;
$this->savePersonAttributes($personId, Attribute::SHOW_IN_SUPPLIERS);
if ($supplierId == NEW_ENTRY) {
return $this->response->setJSON([
'success' => true,
'message' => lang('Suppliers.successful_updating') . ' ' . $supplier_data['company_name'],
'id' => $supplier_id
'message' => lang('Suppliers.successful_adding') . ' ' . $supplierData['company_name'],
'id' => $supplierData['person_id']
]);
} else {
return $this->response->setJSON([
'success' => true,
'message' => lang('Suppliers.successful_updating') . ' ' . $supplierData['company_name'],
'id' => $supplierId
]);
}
} else { // Failure
} else {
return $this->response->setJSON([
'success' => false,
'message' => lang('Suppliers.error_adding_updating') . ' ' . $supplier_data['company_name'],
'message' => lang('Suppliers.error_adding_updating') . ' ' . $supplierData['company_name'],
'id' => NEW_ENTRY
]);
}
}
/**
* This deletes suppliers from the suppliers table
*
* @return ResponseInterface
*/
public function postDelete(): ResponseInterface
{
$suppliers_to_delete = $this->request->getPost('ids', FILTER_SANITIZE_NUMBER_INT);
$suppliersToDelete = $this->request->getPost('ids', FILTER_SANITIZE_NUMBER_INT);
if ($this->supplier->delete_list($suppliers_to_delete)) {
if ($this->supplier->delete_list($suppliersToDelete)) {
return $this->response->setJSON([
'success' => true,
'message' => lang('Suppliers.successful_deleted') . ' ' . count($suppliers_to_delete) . ' ' . lang('Suppliers.one_or_multiple')
'message' => lang('Suppliers.successful_deleted') . ' ' . count($suppliersToDelete) . ' ' . lang('Suppliers.one_or_multiple')
]);
} else {
return $this->response->setJSON(['success' => false, 'message' => lang('Suppliers.cannot_be_deleted')]);
}
}
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use Config\Database;
class AddPersonToAttributeLinks extends Migration
{
public function up(): void
{
helper('migration');
// First, modify the generated unique column to include person_id
// Drop the existing unique constraint
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP INDEX `attribute_links_uq3`');
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP COLUMN `generated_unique_column`');
// Add person_id column
$this->db->query('ALTER TABLE `ospos_attribute_links` ADD COLUMN `person_id` INT(10) NULL AFTER `receiving_id`');
// Add index for person_id
$this->db->query('ALTER TABLE `ospos_attribute_links` ADD KEY `person_id` (`person_id`)');
// Add foreign key constraint for person_id
$this->db->query('ALTER TABLE `ospos_attribute_links` ADD CONSTRAINT `ospos_attribute_links_ibfk_6` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`) ON DELETE CASCADE');
// Recreate the generated unique column with person_id support
// This ensures uniqueness for both item attributes and person attributes
$this->db->query("ALTER TABLE `ospos_attribute_links`
ADD COLUMN `generated_unique_column` VARCHAR(255) GENERATED ALWAYS AS (
CASE
WHEN `sale_id` IS NULL AND `receiving_id` IS NULL AND `item_id` IS NOT NULL THEN CONCAT('item-', `definition_id`, '-', `item_id`)
WHEN `sale_id` IS NULL AND `receiving_id` IS NULL AND `item_id` IS NULL AND `person_id` IS NOT NULL THEN CONCAT('person-', `definition_id`, '-', `person_id`)
ELSE NULL
END
) STORED");
// Re-add unique constraint
$this->db->query('ALTER TABLE `ospos_attribute_links` ADD UNIQUE INDEX `attribute_links_uq3` (`generated_unique_column`)');
}
public function down(): void
{
// Drop person_id related constraints and column
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP INDEX `attribute_links_uq3`');
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP COLUMN `generated_unique_column`');
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP FOREIGN KEY `ospos_attribute_links_ibfk_6`');
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP COLUMN `person_id`');
// Restore original generated column
$this->db->query("ALTER TABLE `ospos_attribute_links`
ADD COLUMN `generated_unique_column` VARCHAR(255) GENERATED ALWAYS AS (
CASE
WHEN `sale_id` IS NULL AND `receiving_id` IS NULL AND `item_id` IS NOT NULL THEN CONCAT(`definition_id`, '-', `item_id`)
ELSE NULL
END
) STORED");
$this->db->query('ALTER TABLE `ospos_attribute_links` ADD UNIQUE INDEX `attribute_links_uq3` (`generated_unique_column`)');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPersonAttributeFlag extends Migration
{
public function up(): void
{
$this->db->query('ALTER TABLE `ospos_attribute_definitions` ADD COLUMN `person_attribute` TINYINT(1) DEFAULT 0 AFTER `definition_flags`');
}
public function down(): void
{
$this->db->query('ALTER TABLE `ospos_attribute_definitions` DROP COLUMN `person_attribute`');
}
}

View File

@@ -23,6 +23,10 @@ return [
"new" => "New Attribute",
"no_attributes_to_display" => "No Attributes to display",
"receipt_visibility" => "Receipt",
"show_in_customers" => "Show in customers",
"show_in_customers_visibility" => "Customers",
"show_in_employees" => "Show in employees",
"show_in_employees_visibility" => "Employees",
"show_in_items" => "Show in items",
"show_in_items_visibility" => "Items",
"show_in_receipt" => "Show in receipt",
@@ -30,5 +34,7 @@ return [
"show_in_receivings_visibility" => "Receivings",
"show_in_sales" => "Show in sales",
"show_in_sales_visibility" => "Sales",
"show_in_suppliers" => "Show in suppliers",
"show_in_suppliers_visibility" => "Suppliers",
"update" => "Update Attribute",
];

View File

@@ -27,12 +27,14 @@ class Attribute extends Model
'definition_type',
'definition_unit',
'definition_flags',
'person_attribute',
'deleted',
'attribute_id',
'definition_id',
'item_id',
'sale_id',
'receiving_id',
'person_id',
'attribute_value',
'attribute_date',
'attribute_decimal'
@@ -41,7 +43,12 @@ class Attribute extends Model
public const SHOW_IN_ITEMS = 1; // TODO: These need to be moved to constants.php
public const SHOW_IN_SALES = 2;
public const SHOW_IN_RECEIVINGS = 4;
public function deleteDropdownAttributeValue(string $attribute_value, int $definition_id): bool
public const SHOW_IN_SEARCH = 8;
public const SHOW_IN_CUSTOMERS = 16;
public const SHOW_IN_EMPLOYEES = 32;
public const SHOW_IN_SUPPLIERS = 64;
public function deleteDropdownAttributeValue(string $attributeValue, int $definitionId): bool
{
$attribute_id = $this->getAttributeIdByValue($attribute_value);
$this->deleteAttributeLinksByDefinitionIdAndAttributeId($definition_id, $attribute_id);
@@ -269,7 +276,7 @@ class Attribute extends Model
public function get_definitions_by_flags(int $definition_flags, bool $include_types = false): array
{
$builder = $this->db->table('attribute_definitions');
$builder->where(new RawSql("definition_flags & $definition_flags")); // TODO: we need to heed CI warnings to escape properly
$builder->where(new RawSql("definition_flags & $definition_flags"));
$builder->where('deleted', 0);
$builder->where('definition_type <>', GROUP);
$builder->orderBy('definition_id');
@@ -291,11 +298,30 @@ class Attribute extends Model
}
/**
* Returns an array of attribute definition names and IDs
* Gets attribute definitions filtered by type (person or item)
*
* @param boolean $groups If false does not return GROUP type attributes in the array
* @return array Array containing definition IDs, attribute names and -1 index with the local language '[SELECT]' line.
* @param bool $isPersonAttribute True for person attributes, false for item attributes
* @param int $definitionFlags Optional visibility flags to further filter
* @return array
*/
public function getDefinitionsByType(bool $isPersonAttribute, int $definitionFlags = 0): array
{
$builder = $this->db->table('attribute_definitions');
$builder->where('person_attribute', $isPersonAttribute ? 1 : 0);
$builder->where('deleted', 0);
$builder->where('definition_type <>', GROUP);
if ($definitionFlags > 0) {
$builder->where(new RawSql("definition_flags & $definitionFlags"));
}
$builder->orderBy('definition_name', 'ASC');
$results = $builder->get()->getResultArray();
return $this->to_array($results, 'definition_id', 'definition_name');
}
public function get_definition_names(bool $groups = true): array
{
$builder = $this->db->table('attribute_definitions');
@@ -1227,4 +1253,227 @@ class Attribute extends Model
$itemsBuilder->update();
}
}
/**
* Gets all attributes connected to a person given the person_id
*
* @param int $personId Person to retrieve attributes for.
* @return array Attributes for the person.
*/
public function getAttributesByPerson(int $personId): array
{
$builder = $this->db->table('attribute_definitions');
$builder->join('attribute_links', 'attribute_links.definition_id = attribute_definitions.definition_id');
$builder->where('person_id', $personId);
$builder->where('item_id', null);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
$builder->where('deleted', 0);
$builder->orderBy('definition_name', 'ASC');
$results = $builder->get()->getResultArray();
return $this->to_array($results, 'definition_id');
}
/**
* Returns whether an attribute_link row exists given a person_id and optionally a definition_id
*
* @param int $personId ID of the person to check for an associated attribute.
* @param int|bool $definitionId Attribute definition ID to check.
* @return bool Returns true if at least one attribute_link exists or false if no attributes exist for that person and attribute.
*/
public function personAttributeLinkExists(int $personId, int|bool $definitionId = false): bool
{
$builder = $this->db->table('attribute_links');
$builder->where('person_id', $personId);
$builder->where('item_id', null);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
if ($definitionId) {
$builder->where('definition_id', $definitionId);
} else {
$builder->where('definition_id IS NOT NULL');
$builder->where('attribute_id', null);
}
$results = $builder->countAllResults();
return $results > 0;
}
/**
* Inserts or updates an attribute link for a person
*
* @param int $personId
* @param int $definitionId
* @param int $attributeId
* @return bool True if the attribute link was saved successfully, false otherwise.
*/
public function savePersonAttributeLink(int $personId, int $definitionId, int $attributeId): bool
{
$this->db->transStart();
$builder = $this->db->table('attribute_links');
if ($this->personAttributeLinkExists($personId, $definitionId)) {
$builder->set(['attribute_id' => $attributeId]);
$builder->where('definition_id', $definitionId);
$builder->where('person_id', $personId);
$builder->where('item_id', null);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
$builder->update();
} else {
$data = [
'attribute_id' => $attributeId,
'person_id' => $personId,
'definition_id' => $definitionId
];
$builder->insert($data);
}
$this->db->transComplete();
return $this->db->transStatus();
}
/**
* Deletes attribute links for a person
*
* @param int $personId
* @param int|bool $definitionId
* @return bool
*/
public function deletePersonAttributeLinks(int $personId, int|bool $definitionId = false): bool
{
$deleteData = ['person_id' => $personId];
$builder = $this->db->table('attribute_links');
$builder->where('item_id', null);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
if (!empty($definitionId)) {
$deleteData['definition_id'] = $definitionId;
}
return $builder->delete($deleteData);
}
/**
* Gets the attribute value for a person and definition
*
* @param int $personId
* @param int $definitionId
* @return object|null
*/
public function getPersonAttributeValue(int $personId, int $definitionId): ?object
{
$builder = $this->db->table('attribute_values');
$builder->join('attribute_links', 'attribute_links.attribute_id = attribute_values.attribute_id');
$builder->where('person_id', $personId);
$builder->where('item_id', null);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
$builder->where('definition_id', $definitionId);
$query = $builder->get();
if ($query->getNumRows() == 1) {
return $query->getRow();
}
return $this->getEmptyObject('attribute_values');
}
/**
* Saves an attribute value for a person
*
* @param string $attributeValue
* @param int $definitionId
* @param int $personId
* @param int|bool $attributeId
* @param string $definitionType
* @return int
*/
public function savePersonAttributeValue(string $attributeValue, int $definitionId, int $personId, int|bool $attributeId = false, string $definitionType = DROPDOWN): int
{
$config = config(OSPOS::class)->settings;
$this->db->transStart();
switch ($definitionType) {
case DATE:
$dataType = 'date';
$attributeDateValue = DateTime::createFromFormat($config['dateformat'], $attributeValue);
$attributeValue = $attributeDateValue ? $attributeDateValue->format('Y-m-d') : $attributeValue;
break;
case DECIMAL:
$dataType = 'decimal';
break;
default:
$dataType = 'value';
break;
}
// New Attribute
if (empty($attributeId) || empty($personId) || $attributeId == -1) {
$attributeId = $this->attributeValueExists($attributeValue, $definitionType);
if (!$attributeId) {
$builder = $this->db->table('attribute_values');
$builder->set(["attribute_$dataType" => $attributeValue]);
$builder->insert();
$attributeId = $this->db->insertID();
}
$data = [
'attribute_id' => empty($attributeId) ? null : $attributeId,
'person_id' => $personId,
'definition_id' => $definitionId
];
$builder = $this->db->table('attribute_links');
$builder->set($data);
$builder->insert();
}
// Existing Attribute
else {
$builder = $this->db->table('attribute_values');
$builder->set(["attribute_$dataType" => $attributeValue]);
$builder->where('attribute_id', $attributeId);
$builder->update();
}
$this->db->transComplete();
return $attributeId;
}
/**
* Gets link values for a person given the person_id and visibility flags
*
* @param int $personId
* @param int $definitionFlags
* @return ResultInterface
*/
public function getPersonLinkValues(int $personId, int $definitionFlags): ResultInterface
{
$format = $this->db->escape(dateformat_mysql());
$builder = $this->db->table('attribute_links');
$builder->select("GROUP_CONCAT(attribute_value SEPARATOR ', ') AS attribute_values");
$builder->select("GROUP_CONCAT(DATE_FORMAT(attribute_date, $format) SEPARATOR ', ') AS attribute_dtvalues");
$builder->join('attribute_values', 'attribute_values.attribute_id = attribute_links.attribute_id');
$builder->join('attribute_definitions', 'attribute_definitions.definition_id = attribute_links.definition_id');
$builder->where('definition_type <>', GROUP);
$builder->where('deleted', ACTIVE);
$builder->where('person_id', $personId);
$builder->where('item_id', null);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
$builder->where(new RawSql("definition_flags & $definitionFlags"));
return $builder->get();
}
}

View File

@@ -0,0 +1,169 @@
<?php
/**
* @var array $definition_names
* @var array $definition_values
* @var int $person_id
* @var array $config
*/
use App\Models\Attribute;
?>
<div class="form-group form-group-sm">
<?= form_label(lang('Attributes.definition_name'), 'definition_name_label', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-8">
<?= form_dropdown([
'name' => 'definition_name',
'options' => $definition_names,
'selected' => -1,
'class' => 'form-control',
'id' => 'definition_name'
]) ?>
</div>
</div>
<?php foreach ($definition_values as $definitionId => $definitionValue) { ?>
<div class="form-group form-group-sm">
<?= form_label(esc($definitionValue['definition_name']), esc($definitionValue['definition_name']), ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-8">
<div class="input-group">
<?php
echo form_hidden("attribute_ids[$definitionId]", strval($definitionValue['attribute_id']));
$attributeValue = $definitionValue['attribute_value'];
switch ($definitionValue['definition_type']) {
case DATE:
$value = (empty($attributeValue) || empty($attributeValue->attribute_date)) ? NOW : strtotime($attributeValue->attribute_date);
echo form_input([
'name' => "attribute_links[$definitionId]",
'value' => to_date($value),
'class' => 'form-control input-sm datetime',
'data-definition-id' => $definitionId,
'readonly' => 'true'
]);
break;
case DROPDOWN:
$selectedValue = $definitionValue['selected_value'];
echo form_dropdown([
'name' => "attribute_links[$definitionId]",
'options' => $definitionValue['values'],
'selected' => $selectedValue,
'class' => 'form-control',
'data-definition-id' => $definitionId
]);
break;
case TEXT:
$value = (empty($attributeValue) || empty($attributeValue->attribute_value)) ? $definitionValue['selected_value'] : $attributeValue->attribute_value;
echo form_input([
'name' => "attribute_links[$definitionId]",
'value' => esc($value),
'class' => 'form-control valid_chars',
'data-definition-id' => $definitionId
]);
break;
case DECIMAL:
$value = (empty($attributeValue) || empty($attributeValue->attribute_decimal)) ? $definitionValue['selected_value'] : $attributeValue->attribute_decimal;
echo form_input([
'name' => "attribute_links[$definitionId]",
'value' => to_decimals((float)$value),
'class' => 'form-control valid_chars',
'data-definition-id' => $definitionId
]);
break;
case CHECKBOX:
$value = (empty($attributeValue) || empty($attributeValue->attribute_value)) ? $definitionValue['selected_value'] : $attributeValue->attribute_value;
// Sends 0 if the box is unchecked instead of not sending anything.
echo form_input([
'type' => 'hidden',
'name' => "attribute_links[$definitionId]",
'id' => "attribute_links[$definitionId]",
'value' => 0,
'data-definition-id' => $definitionId
]);
echo form_checkbox([
'name' => "attribute_links[$definitionId]",
'id' => "attribute_links[$definitionId]",
'value' => 1,
'checked' => $value == 1,
'class' => 'checkbox-inline',
'data-definition-id' => $definitionId
]);
break;
}
?>
<span class="input-group-addon input-sm btn btn-default remove_attribute_btn">
<span class="glyphicon glyphicon-trash"></span>
</span>
</div>
</div>
</div>
<?php } ?>
<script type="text/javascript">
(function() {
<?= view('partial/datepicker_locale', ['format' => dateformat_bootstrap($config['dateformat'])]) ?>
var enableDelete = function() {
$('.remove_attribute_btn').click(function() {
$(this).parents('.form-group').remove();
});
};
enableDelete();
$("input[name*='attribute_links']").change(function() {
var definitionId = $(this).data('definition-id');
$("input[name='attribute_ids[" + definitionId + "]']").val('');
}).autocomplete({
source: function(request, response) {
$.get('<?= 'attributes/suggestAttribute/' ?>' + this.element.data('definition-id') + '?term=' + request.term, function(data) {
return response(data);
}, 'json');
},
appendTo: '.modal-content',
select: function(event, ui) {
event.preventDefault();
$(this).val(ui.item.label);
},
delay: 10
});
var getDefinitionValues = function() {
var result = {};
$("[name*='attribute_links']").each(function() {
var definitionId = $(this).data('definition-id');
var element = $(this);
// For checkboxes, use the visible checkbox, not the hidden input
if (element.attr('type') === 'hidden' && element.siblings('input[type="checkbox"]').length > 0) {
// Skip hidden inputs that have a corresponding checkbox
return;
}
// For checkboxes, get the checked state
if (element.attr('type') === 'checkbox') {
result[definitionId] = element.prop('checked') ? '1' : '0';
} else {
result[definitionId] = element.val();
}
});
return result;
};
var refresh = function() {
var definitionId = $("#definition_name option:selected").val();
var attributeValues = getDefinitionValues();
attributeValues[definitionId] = '';
$('#person_attributes').load(window.location.href, {
'definition_ids': JSON.stringify(attributeValues)
}, enableDelete);
};
$('#definition_name').change(function() {
refresh();
});
})();
</script>

View File

@@ -44,6 +44,12 @@
<?= view('people/form_basic_info') ?>
<div id="person_attributes">
<script type="text/javascript">
$('#person_attributes').load('<?= "customers/attributes/$person_info->person_id" ?>');
</script>
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('Customers.discount_type'), 'discount_type', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-8">

View File

@@ -29,6 +29,12 @@
<div class="tab-pane fade in active" id="employee_basic_info">
<fieldset>
<?= view('people/form_basic_info') ?>
<div id="person_attributes">
<script type="text/javascript">
$('#person_attributes').load('<?= "employees/attributes/$person_info->person_id" ?>');
</script>
</div>
</fieldset>
</div>

View File

@@ -45,6 +45,12 @@
<?= view('people/form_basic_info') ?>
<div id="person_attributes">
<script type="text/javascript">
$('#person_attributes').load('<?= "suppliers/attributes/$person_info->person_id" ?>');
</script>
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('Suppliers.account_number'), 'account_number', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-8">