Feature/warehouse
This commit is contained in:
@@ -4,10 +4,9 @@ class Helper {
|
||||
/**
|
||||
* Generate SQL Filter condition (space separated) for a given column.
|
||||
*
|
||||
* @param string|null $filterValue The filter value to match against.
|
||||
* @param string|null|array $filterValue The filter value to match against.
|
||||
* @param string $columnName The name of the column in the database table.
|
||||
* @return string The SQL condition generated based on the filter value and column name.
|
||||
* @noinspection PhpMissingParamTypeInspection
|
||||
*/
|
||||
public static function generateFilterCondition($filterValue, string $columnName, bool $exactMatch = false): string {
|
||||
$sql = "";
|
||||
@@ -19,6 +18,8 @@ class Helper {
|
||||
$sql = " AND `$columnName` >= " . $filterValue['from'];
|
||||
} elseif (isset($filterValue['to'])) {
|
||||
$sql = " AND `$columnName` <= " . $filterValue['to'];
|
||||
} else if (isset($filterValue['exact'])) {
|
||||
$sql = " AND `$columnName` = " . "'{$filterValue['exact']}'";
|
||||
}
|
||||
} else if ($filterValue === "0" || $filterValue === "1") {
|
||||
$sql .= " AND `$columnName` = " . $filterValue;
|
||||
@@ -37,4 +38,95 @@ class Helper {
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an array of data based on a set of predefined rules.
|
||||
*
|
||||
* @param array $data The data to validate. Keys represent field names, and values are the corresponding data.
|
||||
* @param array $checkArray An associative array defining validation rules for each field:
|
||||
* - key: The field name to validate.
|
||||
* - value: An associative array of validation rules for that field:
|
||||
* - required (bool, optional): Whether the field is required. Default: false.
|
||||
* - title (string, optional): The human-readable name of the field to use in error messages.
|
||||
* - required_length (int, optional): The minimum required length of the value. Default: 1.
|
||||
* - regex (string, optional): A regular expression pattern the value must match.
|
||||
*
|
||||
* @return array|true Returns `true` if validation passes for all fields. Otherwise, returns an associative array of errors
|
||||
* where keys are field names, and values are error messages.
|
||||
*/
|
||||
public static function validateArray(array $data, array $checkArray, bool $printErrors = true) {
|
||||
$errors = [];
|
||||
|
||||
foreach ($checkArray as $key => $rules) {
|
||||
$value = $data[$key] ?? null;
|
||||
$title = $rules['title'] ?? $key;
|
||||
|
||||
// Apply default values for missing rules
|
||||
|
||||
$rules = array_merge([
|
||||
'required' => false,
|
||||
'required_length' => 1,
|
||||
'regex' => false,
|
||||
], $rules);
|
||||
|
||||
// Required Check
|
||||
if ($rules['required'] && (is_null($value) || $value === '')) {
|
||||
$errors[$key] = "$title wird benötigt.";
|
||||
}
|
||||
|
||||
// Length Check (only if value exists)
|
||||
if (!is_null($value) && strlen($value) < $rules['required_length']) {
|
||||
$errors[$key] = "$title muss mindestens $rules[required_length] Zeichen lang sein.";
|
||||
}
|
||||
|
||||
// Regex Check (only if value exists and regex is provided)
|
||||
if (!is_null($value) && $rules['regex'] && !preg_match($rules['regex'], $value)) {
|
||||
$errors[$key] = "$title hat ein ungültiges Format.";
|
||||
}
|
||||
}
|
||||
|
||||
if ($printErrors) {
|
||||
if (!empty($errors)) {
|
||||
header('Content-Type: application/json');
|
||||
die(json_encode(
|
||||
[
|
||||
'success' => false,
|
||||
'errors' => $errors
|
||||
]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return empty($errors) ? true : $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays Vue component with the given header title.
|
||||
*
|
||||
* @param mfBaseController $controller The controller instance to generate $JSGlobals for.
|
||||
* @param string $pageName The name of the Vue component to render.
|
||||
* @param string $headerTitle The title to display in the header.
|
||||
* @param array $additionalGlobals Additional global variables to pass to the Vue component.
|
||||
*/
|
||||
public static function renderVue(mfBaseController $controller, string $pageName, string $headerTitle, array $additionalGlobals = []) {
|
||||
$JSGlobals = ["BASE_URL" => $controller::getUrl($pageName),
|
||||
"MF_URL" => $controller::getUrl(""),
|
||||
"DASHBOARD_URL" => $controller::getUrl("Dashboard"),
|
||||
"MF_APP_NAME" => MFAPPNAME_SLUG,
|
||||
"BASE_PATH" => $controller::getUrl(""),
|
||||
"PAGE_TITLE" => $headerTitle,
|
||||
"PATH" => [
|
||||
["text" => MFAPPNAME_SLUG, "href" => $controller::getUrl("Dashboard")],
|
||||
["text" => $headerTitle, "href" => $controller::getUrl($pageName)]
|
||||
],
|
||||
];
|
||||
|
||||
$JSGlobals = array_merge($JSGlobals, $additionalGlobals);
|
||||
|
||||
$controller->layout()->set("vueViewName", $pageName);
|
||||
$controller->layout()->set("JSGlobals", $JSGlobals);
|
||||
$controller->layout()->setTemplate("VueViews/Vue");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
178
lib/TTCrud/TTCrud.php
Normal file
178
lib/TTCrud/TTCrud.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class TTCrud
|
||||
* @property string $headerTitle
|
||||
* @property string $createText
|
||||
* @property array $columns
|
||||
* @property array $additionalActions
|
||||
* @property array $infoMessages
|
||||
* @property bool $onlyView
|
||||
*/
|
||||
class TTCrud extends mfBaseController {
|
||||
public User $user;
|
||||
private array $checkArray;
|
||||
public ?array $postData;
|
||||
/** @noinspection PhpMissingFieldTypeInspection */
|
||||
public $model;
|
||||
|
||||
protected function init() {
|
||||
$this->needlogin = true;
|
||||
$me = new User();
|
||||
$me->loadMe();
|
||||
$this->user = $me;
|
||||
$this->layout()->set('me', $me);
|
||||
|
||||
if (!$me->is(["Admin"])) {
|
||||
$this->redirect("Dashboard");
|
||||
}
|
||||
|
||||
$modelName = $this->mod . 'Model';
|
||||
$this->model = new $modelName();
|
||||
$this->postData = json_decode(file_get_contents('php://input'), true);
|
||||
$this->checkArray = $this->getCheckArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the checkArray for the CRUD component.
|
||||
* @return array
|
||||
*/
|
||||
protected function getCheckArray(): array {
|
||||
$checkArray = [];
|
||||
|
||||
foreach ($this->columns as $column) {
|
||||
$checkArray[$column['key']] = ['required' => $column['required'] ?? false,
|
||||
'required_length' => $column['required_length'] ?? 0,
|
||||
'title' => $column['text'] ?? $column['key'],
|
||||
'regex' => $column['regex'] ?? false];
|
||||
}
|
||||
|
||||
return $checkArray;
|
||||
}
|
||||
|
||||
protected function indexAction() {
|
||||
$this->layout()->set('additionalJS', ['js/pages/WarehouseHistory/WarehouseHistoryModal.js']);
|
||||
$customJsFile = defined('BASEDIR') ? BASEDIR . "/public/js/pages/{$this->mod}/{$this->mod}.js" : null;
|
||||
|
||||
if ($customJsFile && file_exists($customJsFile)) {
|
||||
$pageName = $this->mod;
|
||||
} else {
|
||||
$pageName = "DefaultCrudView";
|
||||
}
|
||||
|
||||
Helper::renderVue($this, $pageName, $this->headerTitle, ["CRUD_CONFIG" => $this->getCrudConfig(),
|
||||
"CREATE_URL" => $this::getUrl($this->mod . "/create"),
|
||||
"TABLE_URL" => $this::getUrl($this->mod . "/get"),
|
||||
"UPDATE_URL" => $this::getUrl($this->mod . "/update"),
|
||||
"DELETE_URL" => $this::getUrl($this->mod . "/delete"),]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configuration for the CRUD component for the Vue component.
|
||||
* @return array
|
||||
*/
|
||||
protected function getCrudConfig(): array {
|
||||
if (method_exists($this, 'prepareCrudConfig')) {
|
||||
$this->prepareCrudConfig();
|
||||
}
|
||||
|
||||
|
||||
$columns = array_map(function ($column) {
|
||||
$newColumn = $column;
|
||||
unset($newColumn['required'], $newColumn['required_length'], $newColumn['regex']);
|
||||
return $newColumn;
|
||||
}, $this->columns);
|
||||
|
||||
return ['key' => $this->mod,
|
||||
'tableHeader' => $this->headerTitle,
|
||||
'createText' => $this->createText,
|
||||
'columns' => $columns,
|
||||
'additionalActions' => $this->additionalActions];
|
||||
}
|
||||
|
||||
protected function getAction() {
|
||||
$filter = $this->postData['filters'] ?? [];
|
||||
$order = $this->postData['order'] ?? ['key' => null, 'order' => 'ASC'];
|
||||
$page = $this->postData['pagination']['page'] ?? 1;
|
||||
$perPage = $this->postData['pagination']['per_page'] ?? 10;
|
||||
|
||||
$rows = $this->model::getAll($filter, $perPage, ($page - 1) * $perPage, $order);
|
||||
$filteredAvailable = $this->model::count($filter);
|
||||
$totalRows = $this->model::count();
|
||||
|
||||
self::returnJson(["rows" => $rows,
|
||||
"pagination" => ["page" => $page,
|
||||
"total_pages" => ceil($filteredAvailable / $perPage),
|
||||
"per_page" => $perPage,
|
||||
"filtered_available" => intval($filteredAvailable),
|
||||
"total_rows" => intval($totalRows)]]);
|
||||
}
|
||||
|
||||
protected function createAction() {
|
||||
Helper::validateArray($this->postData, $this->checkArray);
|
||||
|
||||
if (method_exists($this, 'beforeCreate') && !$this->beforeCreate($this->postData)) {
|
||||
self::returnJson(['success' => false, 'message' => 'Ein Fehler ist aufgetreten.']);
|
||||
}
|
||||
|
||||
$id = $this->model::create($this->postData);
|
||||
|
||||
if (method_exists($this, 'afterCreate')) {
|
||||
$this->afterCreate($this->postData);
|
||||
}
|
||||
|
||||
self::returnJson(['success' => true,
|
||||
'message' => $this->infoMessages['create'],
|
||||
'id' => $id]);
|
||||
}
|
||||
|
||||
protected function updateAction() {
|
||||
Helper::validateArray($this->postData, array_merge($this->checkArray, ['id' => ['required' => true]]));
|
||||
|
||||
if (method_exists($this, 'beforeUpdate') && !$this->beforeUpdate($this->postData)) {
|
||||
self::returnJson(['success' => false, 'message' => 'Ein Fehler ist aufgetreten.']);
|
||||
}
|
||||
|
||||
$affectedRows = $this->model::update($this->postData);
|
||||
|
||||
if (method_exists($this, 'afterUpdate')) {
|
||||
$this->afterUpdate($this->postData);
|
||||
}
|
||||
|
||||
self::returnJson(['success' => $affectedRows > 0,
|
||||
'message' => $affectedRows > 0 ? $this->infoMessages['update'] : $this->infoMessages['noChanges']]);
|
||||
}
|
||||
|
||||
protected function deleteAction() {
|
||||
Helper::validateArray($this->postData, ['id' => ['required' => true]]);
|
||||
|
||||
if (method_exists($this, 'beforeDelete') && !$this->beforeDelete($this->postData)) {
|
||||
self::returnJson(['success' => false, 'message' => 'Ein Fehler ist aufgetreten.']);
|
||||
}
|
||||
|
||||
$affectedRows = $this->model::delete($this->postData['id']);
|
||||
|
||||
self::returnJson(['success' => $affectedRows > 0,
|
||||
'message' => $affectedRows > 0 ? $this->infoMessages['delete'] : $this->infoMessages['noChanges']]);
|
||||
}
|
||||
|
||||
protected function autocompleteAction() {
|
||||
$searchedID = $this->request->searchedID;
|
||||
|
||||
$textKey = property_exists($this->model, 'name') ? 'name' : 'title';
|
||||
|
||||
if (strlen($searchedID) > 0) {
|
||||
$filter = ['id' => $searchedID];
|
||||
} else {
|
||||
$filter = [$textKey => $this->request->q];
|
||||
}
|
||||
|
||||
$data = $this->model::getAll($filter, 10);
|
||||
|
||||
self::returnJson(array_map(function ($item) use ($textKey) {
|
||||
return ['value' => $item->id, 'text' => $item->$textKey];
|
||||
}, $data));
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
170
lib/TTCrudBaseModel/TTCrudBaseModel.php
Normal file
170
lib/TTCrudBaseModel/TTCrudBaseModel.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
class TTCrudBaseModel {
|
||||
public function __construct($data = []) {
|
||||
foreach ($data as $field => $value) {
|
||||
if (property_exists(get_called_class(), $field)) {
|
||||
$this->$field = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function create($data) {
|
||||
$FronkDB = FronkDB::singleton();
|
||||
$db = $FronkDB->link;
|
||||
$table = self::getTable();
|
||||
self::checkAllFields($data, ['id']);
|
||||
|
||||
$sqlColumns = [];
|
||||
$sqlValues = [];
|
||||
foreach ($data as $field => $value) {
|
||||
if (!property_exists(get_called_class(), $field)) {
|
||||
throw new Exception("Field $field does not exist in " . get_called_class());
|
||||
}
|
||||
|
||||
$sqlValues[] = $value === null ? 'NULL' : "'" . $db->real_escape_string($value) . "'";
|
||||
$sqlColumns[] = "`$field`";
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO `$table` (" . implode(", ", $sqlColumns) . ") VALUES (" . implode(", ", $sqlValues) . ")";
|
||||
$db->query($sql) or die($db->error);
|
||||
|
||||
return $db->insert_id;
|
||||
}
|
||||
|
||||
public static function getTable(): string {
|
||||
return str_replace('Model', '', get_called_class());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all required fields of the current class are present in a given data array.
|
||||
*
|
||||
* This method uses reflection to determine which fields are required based on their type declarations.
|
||||
* It then iterates over these required fields and throws an exception if any of them are missing
|
||||
* from the provided data array.
|
||||
*
|
||||
* @param array $data The data array to check.
|
||||
* @param array $skip An optional array of field names to skip during the check.
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception If any required field is missing in the `$data` array.
|
||||
*
|
||||
*/
|
||||
public static function checkAllFields(array $data, array $skip = []) {
|
||||
$requiredVars = array_filter(get_class_vars(get_called_class()), function ($value, $key) {
|
||||
$reflectionProperty = new ReflectionProperty(get_called_class(), $key);
|
||||
return !$reflectionProperty->hasType() || !$reflectionProperty->getType()->allowsNull();
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
|
||||
foreach ($requiredVars as $field => $value) {
|
||||
if (in_array($field, $skip)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($data[$field])) {
|
||||
throw new Exception("Required field $field is missing in data array");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function get($id): TTCrudBaseModel {
|
||||
$FronkDB = FronkDB::singleton();
|
||||
$db = $FronkDB->link;
|
||||
$id = $db->real_escape_string($id);
|
||||
$table = self::getTable();
|
||||
$sql = "SELECT * FROM `$table` WHERE `id` = $id";
|
||||
$result = $db->query($sql);
|
||||
// as TTCRudBaseModel is abstract, we need to get the class name of the child class
|
||||
$class = get_called_class();
|
||||
return new $class($result->fetch_assoc());
|
||||
}
|
||||
|
||||
public static function count($filter = []): int {
|
||||
$FronkDB = FronkDB::singleton();
|
||||
$db = $FronkDB->link;
|
||||
$table = self::getTable();
|
||||
$filter = self::getSQLFilter($filter);
|
||||
$sql = "SELECT COUNT(*) as count FROM `$table` $filter";
|
||||
$result = $db->query($sql);
|
||||
|
||||
return $result->fetch_assoc()['count'];
|
||||
}
|
||||
|
||||
public static function getSQLFilter($filter): string {
|
||||
if (empty($filter)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
$sql = "WHERE 1=1";
|
||||
foreach ($filter as $key => $value) {
|
||||
if (!property_exists(get_called_class(), $key)) {
|
||||
throw new Exception("Field $key does not exist in " . get_called_class());
|
||||
}
|
||||
$sql .= Helper::generateFilterCondition($value, $key, gettype($value) === "integer");
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
public static function getAll($filter = [], $limit = null, $offset = 0, $order = ["key" => null]): array {
|
||||
$FronkDB = FronkDB::singleton();
|
||||
$db = $FronkDB->link;
|
||||
$table = self::getTable();
|
||||
$filter = self::getSQLFilter($filter);
|
||||
$sql = "SELECT * FROM `$table` $filter";
|
||||
|
||||
$sql .= $order['key'] === null ? " ORDER BY `id` ASC" : " ORDER BY `" . $order['key'] . "` " . $order['order'];
|
||||
$sql .= $limit === null ? "" : " LIMIT " . $limit . " OFFSET " . $offset;
|
||||
try {
|
||||
$result = $db->query($sql);
|
||||
} catch (Exception $e) {
|
||||
echo $sql;
|
||||
die($e->getMessage());
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$class = get_called_class();
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$rows[] = new $class($row);
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public static function update($data) {
|
||||
$FronkDB = FronkDB::singleton();
|
||||
$db = $FronkDB->link;
|
||||
$table = self::getTable();
|
||||
|
||||
// Check if all fields are set
|
||||
self::checkAllFields($data);
|
||||
|
||||
$values = [];
|
||||
foreach ($data as $field => $value) {
|
||||
if (!property_exists(get_called_class(), $field)) {
|
||||
throw new Exception("Field $field does not exist in " . get_called_class());
|
||||
}
|
||||
if ($field === "id") {
|
||||
continue;
|
||||
}
|
||||
|
||||
$values[] = $value === null ? "`$field` = NULL" : "`$field` = '" . $db->real_escape_string($value) . "'";
|
||||
}
|
||||
|
||||
$sql = "UPDATE `$table` SET " . implode(", ", $values) . " WHERE `id` = " . $db->real_escape_string($data['id']);
|
||||
$db->query($sql);
|
||||
return $db->affected_rows;
|
||||
}
|
||||
|
||||
public static function delete($id) {
|
||||
$FronkDB = FronkDB::singleton();
|
||||
$db = $FronkDB->link;
|
||||
$table = self::getTable();
|
||||
$id = $db->real_escape_string($id);
|
||||
$sql = "DELETE FROM `$table` WHERE `id` = $id";
|
||||
$db->query($sql);
|
||||
return $db->affected_rows;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user