summaryrefslogtreecommitdiff
path: root/html/app
diff options
context:
space:
mode:
authorGeorge Halkiadakis <gchalkiadakis@sklavenitis.co.gr>2023-03-12 07:16:23 +0200
committerGeorge Halkiadakis <gchalkiadakis@sklavenitis.co.gr>2023-03-12 07:16:23 +0200
commit7076343338ae3439f3c86f01144818abe8c31978 (patch)
tree794abb1e6b8f821091fd095341885496b6dad51d /html/app
parent47cbb529f5723b246125ae083a193e11481b89ef (diff)
downloadclassroom-7076343338ae3439f3c86f01144818abe8c31978.tar.gz
classroom-7076343338ae3439f3c86f01144818abe8c31978.tar.bz2
classroom-7076343338ae3439f3c86f01144818abe8c31978.zip
add container helpers; constuct public directory-tree
Diffstat (limited to 'html/app')
-rw-r--r--html/app/config/assets.php43
-rw-r--r--html/app/controllers/Product.php52
-rw-r--r--html/app/controllers/Resolve.php91
-rw-r--r--html/app/controllers/admin/.gitkeep0
-rw-r--r--html/app/controllers/api/.gitkeep0
-rw-r--r--html/app/controllers/api/Common_api.php281
-rw-r--r--html/app/controllers/api/Doc_api.php34
-rw-r--r--html/app/controllers/cli/CliContoller.php85
-rw-r--r--html/app/models/Jorge.php169
-rw-r--r--html/app/models/_info.md84
-rw-r--r--html/app/models/cms/Page_model.php18
-rw-r--r--html/app/models/market/Market_repository.php235
-rw-r--r--html/app/models/market/ProductCategories_model.php199
-rw-r--r--html/app/models/market/Product_model.php49
-rw-r--r--html/app/models/todo.md132
-rw-r--r--html/app/routes/api.php64
-rw-r--r--html/app/routes/backend.php9
-rw-r--r--html/app/routes/frontend.php135
-rw-r--r--html/app/views/basic.php53
-rw-r--r--html/app/views/group.php18
-rw-r--r--html/app/views/item.php4
-rw-r--r--html/app/views/products_list.php47
22 files changed, 1802 insertions, 0 deletions
diff --git a/html/app/config/assets.php b/html/app/config/assets.php
new file mode 100644
index 0000000..9203c47
--- /dev/null
+++ b/html/app/config/assets.php
@@ -0,0 +1,43 @@
+<?php
+
+// PATHS USED HEAVILY //////////////////////////////////////////////////////////
+// -----------------------------------------------------------------------------
+// these paths are used in order to easily find and autoload useful elemets
+
+
+// JAVASCRIPT LIBRARY PATHS
+// -----------------------------------------------------------------------------
+define('JS_DIR', '/content/'); // <base> path to contruct link
+define('JS_LIBRARIES', array( // path after <base> to the actual file
+ 'jquery' => 'lib/jquery/dist/jquery.min.js',
+
+ 'require/files' => 'lib/require/require.files.js',
+ 'require/slim' => 'lib/require/require.slim.js',
+
+ 'plugins' => 'lib/plugins/plugins.min.js',
+ 'plugins/setup' => 'lib/plugins/plugins.setup.js'
+ )
+);
+
+
+// CSS ASSET PATHS
+// -----------------------------------------------------------------------------
+define('CSS_DIR', '/content/css/'); // <base> path to contruct link
+define('CSS_FILES', array( // path after <base> to the actual file
+ 'main' => 'main.min.css',
+ 'filter' => 'filter.css',
+ 'overides' => 'overides.css',
+ )
+);
+
+
+// FONTS
+// -----------------------------------------------------------------------------
+define('FONTS_DIR', '/content/fonts/');
+define('FONT_FILES', array(
+ 'iconfont' => 'iconfont.woff2',
+ 'CFAstyStdBold' => 'CFAstyStd-Bold.woff2',
+ 'CFAstyStdBook' => 'CFAstyStd-Book.woff2',
+ 'CFAstyStdMedium' => 'CFAstyStd-Medium.woff2'
+ )
+);
diff --git a/html/app/controllers/Product.php b/html/app/controllers/Product.php
new file mode 100644
index 0000000..0193ed9
--- /dev/null
+++ b/html/app/controllers/Product.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace app\controllers;
+
+use app\models\market\Product_model;
+use app\models\market\ProductCategories_model;
+
+class Product {
+
+ public static function fromUrl($url, $pathArr)
+ {
+ $product = proxy([\app\models\market\Product_model::class, 'fromUrl'],
+ [ $url ], CACHE_PRODUCT_TTL
+ );
+
+ // TODO:
+ // verify path, else redirect
+
+ if ($product === false) {
+ header("HTTP/1.0 404 Not Found");
+ render_view('error/404', ['message' => 'Of all the things I\'ve lost, I miss my mind the most.']);
+ return ;
+ }
+
+ // render page
+ load_template('products', [ 'data' => [
+ 'title' => $product['Title'],
+ 'seo' => [],
+ 'tree' => proxy(
+ [\app\models\market\ProductCategories_model::class, 'tree'],
+ [], CACHE_ROOT_TTL
+ ),
+ 'hierarchyPath' => explode('.', substr($product['Hierarchy'], 1, -1)),
+ 'sections' => [
+ [
+ 'view' => 'content/product_list/Product_List_Filters',
+ 'key' => 'filters',
+ 'data' => []
+ ],
+ [
+ 'view' => 'content/product/product_details',
+ 'key' => 'product',
+ 'data' => $product
+ ]
+ ]
+ ]
+ ]);
+
+ }
+
+
+}
diff --git a/html/app/controllers/Resolve.php b/html/app/controllers/Resolve.php
new file mode 100644
index 0000000..7b9222c
--- /dev/null
+++ b/html/app/controllers/Resolve.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace app\controllers;
+
+use \Benchmark;
+
+/** Resolve class
+ * will resolve ambiguous utl-request patterns
+ * ---
+ * Then it can route the request to some controller;
+ * Alternatively it can proccess the request via Models
+ * and initiate the View engine
+ */
+class Resolve {
+
+ public static function url($array)
+ {
+ $fullUrl = implode('/', $array);
+ $url = $array[count($array) - 1];
+
+
+ if (!PRODUCTION) Benchmark::add_spot('proxy-page');
+
+ // proxying check if page with this url
+ $page = proxy(
+ [\app\models\cms\Page_model::class, 'fromUrl'],
+ [ $fullUrl ], CACHE_ROOT_TTL,
+ PROXY_CACHE_ERRORS
+ );
+ // if page exist, construct page
+ if ($page !== false) {
+ // page exists, render page
+ load_template('base', [ 'page' => [
+ 'title' => $page['Title'],
+ 'seo' => [],
+ 'sections' => [
+ [
+ 'view' => 'content/static',
+ 'key' => 'page',
+ 'data' => $page
+ ]
+ ]
+ ]
+ ]);
+ return ;
+ }
+
+
+ if (!PRODUCTION) Benchmark::add_spot('proxy-category');
+
+ // proxying check if products category with this url
+ $category = proxy(
+ [\app\models\market\ProductCategories_model::class, 'categoryFromUrl'],
+ [ $fullUrl ], CACHE_CATEGORY_TTL
+ );
+ // if category exist, construct category
+ if ($category !== false) {
+ // render page
+ load_template('products', [ 'data' => [
+ 'title' => $category['Title'],
+ 'seo' => [],
+ 'tree' => proxy(
+ [\app\models\market\ProductCategories_model::class, 'tree'],
+ [], CACHE_ROOT_TTL
+ ),
+ 'hierarchyPath' => explode('.', substr($category['Hierarchy'], 1, -1)),
+ 'sections' => [
+ [
+ 'view' => 'content/product_list/Product_List_Filters',
+ 'key' => 'filters',
+ 'data' => $category
+ ],
+ [
+ 'view' => 'content/product_list/product_list',
+ 'key' => 'products',
+ 'data' => $category['products']
+ ]
+ ]
+ ]
+ ]);
+ return ;
+ }
+
+
+ // not page nor category?: reply 404
+ header("HTTP/1.0 404 Not Found");
+ render_view('error/404', ['message' => 'Say something I\'m givin\' up on you.']);
+
+ }
+
+} \ No newline at end of file
diff --git a/html/app/controllers/admin/.gitkeep b/html/app/controllers/admin/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/html/app/controllers/admin/.gitkeep
diff --git a/html/app/controllers/api/.gitkeep b/html/app/controllers/api/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/html/app/controllers/api/.gitkeep
diff --git a/html/app/controllers/api/Common_api.php b/html/app/controllers/api/Common_api.php
new file mode 100644
index 0000000..a8bd75f
--- /dev/null
+++ b/html/app/controllers/api/Common_api.php
@@ -0,0 +1,281 @@
+<?php
+
+namespace app\controllers\api;
+
+use \Registry;
+use app\models\market\Product_model;
+use app\models\market\ProductCategories_model;
+
+/** Common_api
+ * ---
+ * Controller for handling common API calls
+ * about main tables of the project;
+ *
+ * includes aglo for passing special filters and sorting
+ * (check parse_Where_OrderBy() method)
+ */
+class Common_api
+{
+
+ /** APITables
+ * Method returns allowed tables to accept API calls
+ * The return is an array of [label => data-source] pairs
+ * where 'label' is a friendly name of the datasource
+ * (and 'datasource' the actual table/data-source)
+ */
+ public static function APITables()
+ {
+ /** allowed tables for api call
+ * @return: (array) an of api-call arrays
+ * each api-call array has the folloing form:
+ * [ label => [
+ * 'table' => (string) actual table in database,
+ * (optional) 'filter' => (array) of allowed filtering fields,
+ * (optional) 'sort' => (array) of allowed sorting fields,
+ * ]
+ * ]
+ */
+ return [
+ 'page' => [
+ 'table' => 'pages',
+ 'filter' => ['Title']
+ ],
+ 'product' => [
+ 'table' => 'products',
+ 'filter' => [ 'ID', 'Title' ],
+ 'sort' => ['ID']
+ ],
+ 'category' => [
+ 'table' => 'product_categories',
+ 'filter' => ['Title', 'Hierarchy', 'Level']
+ ]
+ ];
+ }
+
+
+ /** select anything on any (allowed table)
+ * + supports filtering and sorting
+ * check self::parse_Where_OrderBy() for options;
+ * sets a limti of 1000 records
+ */
+ public static function table($table)
+ {
+ $sources = self::APITables();
+ if (array_key_exists($table, $sources)) {
+
+ $query = Registry::get('REQUEST')->QUERY;
+
+ // Get parametres? => parse filters
+ if ($query !== false) {
+
+ $parsed = self::parse_Where_OrderBy(
+ $query,
+ ($sources[$table]['filter'] ?? []),
+ ($sources[$table][ 'sort' ] ?? [])
+ );
+ $where = $parsed['filter'] ? (' WHERE ' . $parsed['filter']) : '';
+ $orderBy = $parsed['sort'] ? (' ORDER BY '. $parsed['sort']) : '' ;
+ $bindArguments = $parsed['bind'];
+
+ } else {
+ $where = '';
+ $orderBy = '';
+ $bindArguments = [];
+ }
+
+ $db = Registry::use('database');
+ $result = $db->runQuery('SELECT *
+ FROM '. $sources[$table]['table']
+ . $where
+ . $orderBy
+ .' LIMIT 1000',
+ $bindArguments
+ );
+
+ reply_json([
+ 'success' => true,
+ 'client' => Registry::get('REQUEST')->SIGNATURE,
+ 'result' => $result
+ ]);
+
+ } else {
+ reply_json(['success' => false]);
+ }
+ die();
+ }
+
+
+ /** get record of {table} by ID
+ * (if table has no ID then return false)
+ */
+ public static function record($table, $id)
+ {
+ $sources = self::APITables();
+ if (array_key_exists($table, $sources)) {
+
+ $db = Registry::use('database');
+ $result = $db->runQuery("SELECT *
+ FROM ". $sources[$table]['table'] ."
+ WHERE ID = :id",
+ [':id' => $id]
+ );
+ reply_json([
+ 'success' => true,
+ 'data' => $result[0]
+ ]);
+
+ } else {
+ reply_json(['status' => false]);
+
+ }
+ die();
+ }
+
+
+ /** category_by_url
+ * product categorie by full-friendly-URL
+ * @param $url (string): full friendly url
+ */
+ public static function category_by_url($url)
+ {
+ $category = proxy(
+ [\app\models\market\ProductCategories_model::class, 'categoryFromUrl'],
+ [ $url ], CACHE_CATEGORY_TTL
+ );
+ if ($category !== false) {
+ reply_json([
+ 'success' => true,
+ 'result' => $category
+ ]);
+
+ } else {
+ reply_json(['status' => false]);
+
+ }
+ die();
+ }
+
+ /** parse Where & OrderBy
+ * -------------------------------------------------------------------------
+ * parses SAFELY the query string to Where {CONDITIONS} and ORDER BY clauses
+ * according to the specified rules.
+ *
+ * The rules:
+ * ** filters: ?fieldname=[operator]:value &...
+ * where operators:[ like | startlike | endlike | eq | gt | gteq | lt |t leq ]
+ *
+ * ** Order by: ?_sort=fieldname[:[asc|desc]][,field[,]]
+ *
+ * for example: ?id=gt:4&active=1&_sort:reputation:desc,category
+ * parses to WHERE id > 4 AND active = 4 ORDER BY reputation desc, catetory asc
+ *
+ * -------------------------------------------------------------------------
+ * arguments:
+ * @param $query (string): string to be parsed
+ * @param $filters (array): the allowed fields to apply filters
+ * @param $sortings (array): the allowed fields to sort the result
+ * @return array('filter'=>(string) , 'sort'=>(string) , 'bind'=>(array))
+ */
+ private static function parse_Where_OrderBy($query, $filters=[], $sortings=[])
+ {
+ // break query to [key => value] pairs
+ parse_str($query, $queryArray);
+
+ $filterClause = []; // array to hold filter/WHERE clauses
+ $sortClause = []; // array to hold sort/ORDER-BY caluses
+ $bindings = []; // array to hold variable bindigs
+
+ // parse filers
+ // ---------------------------------------------------------------------
+ foreach($queryArray as $filter => $value) {
+
+ if (in_array($filter, $filters)) {
+
+ $parts = explode(':', $value ); // that is => [operator], value
+ if (count($parts) == 0) {
+ // forget it
+
+ } else if (count($parts) == 1) {
+ $filterClause[] = "{$filter} = :{$filter}";
+ $bindings[$filter] = $parts[0];
+
+ } else {
+
+ switch ($parts[0]) {
+ case 'like':
+ $filterClause[] = "{$filter} LIKE :{$filter}";
+ $bindings[':'.$filter] = '%'.$parts[1].'%';
+ break;
+
+ case 'startlike':
+ $filterClause[] = "{$filter} LIKE :{$filter}";
+ $bindings[':'.$filter] = $parts[1].'%';
+ break;
+
+ case 'endlike':
+ $filterClause[] = "{$filter} LIKE :{$filter}";
+ $bindings[':'.$filter] = '%'.$parts[1];
+ break;
+
+ case 'gt':
+ $filterClause[] = "{$filter} > :{$filter}";
+ $bindings[':'.$filter] = $parts[1];
+ break;
+
+ case 'gteq':
+ $filterClause[] = "{$filter} >= :{$filter}";
+ $bindings[':'.$filter] = $parts[1];
+ break;
+
+ case 'lt':
+ $filterClause[] = "{$filter} < :{$filter}";
+ $bindings[':'.$filter] = $parts[1];
+ break;
+
+ case 'lteq':
+ $filterClause[] = "{$filter} <= :{$filter}";
+ $bindings[':'.$filter] = $parts[1];
+ break;
+
+ case 'eq':
+ default:
+ $filterClause[] = "{$filter} = :{$filter}";
+ $bindings[':'.$filter] = $parts[1];
+ }
+ }
+ }
+ }
+ $whereSQL = ($filterClause == [])
+ ? false
+ : implode(' AND ', $filterClause);
+
+
+ // parse sort options
+ // ---------------------------------------------------------------------
+ if (isset($queryArray['_sort'])) {
+ $sortTerms = explode(',', $queryArray['_sort']);
+
+ foreach($sortTerms as $term) {
+
+ $parts = explode(':', $term);
+ if ($parts != [] && in_array($parts[0], $sortings) ) {
+ $sortClause[] = (count($parts)==1)
+ ? $parts[0]
+ : $parts[0] .' '. (($parts[1] == 'desc') ? 'desc' : 'asc');
+ }
+ }
+
+ }
+ $sortSQL = ($sortClause == [])
+ ? false
+ : implode(', ', $sortClause);
+
+ return ([
+ 'filter' => $whereSQL,
+ 'sort' => $sortSQL,
+ 'bind' => $bindings
+ ]);
+
+ }
+
+} \ No newline at end of file
diff --git a/html/app/controllers/api/Doc_api.php b/html/app/controllers/api/Doc_api.php
new file mode 100644
index 0000000..e893552
--- /dev/null
+++ b/html/app/controllers/api/Doc_api.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace app\controllers\api;
+
+use app\models\market\ProductCategories_model;
+use app\models\Jorge;
+
+class Doc_api
+{
+
+ /** */
+ public static function tree($store = 904)
+ {
+ $tree = proxy(
+ [ProductCategories_model::class,'tree'],
+ [], CACHE_ROOT_TTL,
+ PROXY_IGNORE_CACHE
+ );
+ reply_json([ 'success' => true, 'result' => $tree ]);
+ die();
+ }
+
+
+ public static function dbDoc()
+ {
+ $j = new Jorge();
+ reply_json([
+ 'success' => true,
+ 'result' => $j->databaseDocumentation()
+ ]);
+ die();
+
+ }
+} \ No newline at end of file
diff --git a/html/app/controllers/cli/CliContoller.php b/html/app/controllers/cli/CliContoller.php
new file mode 100644
index 0000000..310e2ae
--- /dev/null
+++ b/html/app/controllers/cli/CliContoller.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace app\controllers\cli;
+
+/** CliContoller
+ *
+ * Re-generate cashes of commonly used entities
+ * like: product categories and trendy products
+ * (proxy calls make use of PROXY_IGNORE_CACHE)
+ *
+ * CliController calls Model methods who create
+ * cashed data with the exact same arguments as
+ * when called from the web interface; this way
+ * the Model::method(arguments) triplete creates
+ * the same keys as via the web interface.
+ *
+ * NOTE: Only Cli interface is allowed
+ * [if (php_sapi_name() != 'cli') return false;]
+ *
+ * The Cli-interfaced script can run manualy or
+ * (most usual scenario) scheduled via a cron job
+ * in order to refresh cashes befor expiration.
+ *
+ * ---------------------------------------------
+ */
+class CliContoller
+{
+
+
+ /** (re-) cacheCategoriesTree
+ * regenerate cache for product_categories tree
+ * @param (void)
+ */
+ public static function cacheCategoriesTree()
+ {
+ // Only Cli interface is allowed
+ if (php_sapi_name() != 'cli') return false;
+
+ # PROXY call:
+ # MARKET \ ProductCategories_model::tree():
+ # + Refresh Cache
+ # --------------------------------------
+ echo textColor("Creating category_products tree Cache ... ", \NORMAL);
+ $reply = proxy(
+ [\app\models\market\ProductCategories_model::class, 'tree'],
+ [], \CACHE_ROOT_TTL,
+ \PROXY_IGNORE_CACHE
+ );
+ echo ($reply == false) ? textColor("Failed\n", \FAIL) : textColor("Done!\n", \SUCCESS);
+ }
+
+
+
+ public static function getProductCategories()
+ {
+ return \Registry::use('database')->runQuery(
+ "SELECT * FROM product_categories",
+ []
+ );
+ }
+
+
+
+ /** (re-) cacheCategoryByUrl( url )
+ * regenerate cache of a category_product
+ * by category's friendly url
+ * @param $url (string): the category's friendly url
+ */
+ public static function cacheCategoryByUrl(string $url)
+ {
+ // Only Cli interface is allowed
+ if (php_sapi_name() != 'cli') return false;
+
+ # PROXY call:
+ # MARKET \ ProductCategories_model::tree():
+ # + Refresh Cache
+ # --------------------------------------
+ $reply = proxy(
+ [\app\models\market\ProductCategories_model::class, 'categoryFromUrl'],
+ [ $url ], \CACHE_CATEGORY_TTL,
+ \PROXY_IGNORE_CACHE
+ );
+ echo ($reply == false) ? textColor("Failed\n", \FAIL) : textColor("Done!\n", \SUCCESS);
+ }
+} \ No newline at end of file
diff --git a/html/app/models/Jorge.php b/html/app/models/Jorge.php
new file mode 100644
index 0000000..011b8c0
--- /dev/null
+++ b/html/app/models/Jorge.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace app\models;
+
+use \Registry;
+
+/** Jorge
+ * a database reference and adminitration toolkit
+ * ---
+ * The name Jorge comes from Uberto Eco's book: 'il nome della rosa',
+ * (Jorge is the blind monk who supervises the monastery's library ;)
+ */
+class Jorge
+{
+
+ /** tables
+ * array to keep all database-tables' critical information.
+ * @key create (array) includes all 'CREATE TABLE' statements
+ * @key relate (array) includes relations between tables
+ * @key fields (array) includes all important fields (when no select*)
+ * @key filter (array) includes all fields used to filter results
+ */
+ private $tables = [
+ 'create' => [],
+ 'relate' => [],
+ 'fields' => []
+ ];
+
+ public function __construct()
+ {
+ $db = Registry::use('database');
+
+ // get all tables
+ $tableList = $db->runQuery("SHOW TABLES", []);
+
+ // get all CREATE TABLE statements
+ foreach($tableList as $tableArray) {
+ $table = array_shift($tableArray);
+ $create = $db->runQuery("SHOW CREATE TABLE {$table}", []);
+ $this->tables['create'][$table] = $create[0]['Create Table'];
+ }
+
+ // define relations
+ $this->tables['relate'] = [
+
+ 'products' => [
+ 'brands' => 'products.BrandID = brands.ID',
+ 'products_to_product_categories' => 'products.ID = products_to_product_categories.SimpleProductID',
+ 'products_to_images' => 'products.ID = products_to_images.SimpleProductID',
+ 'prices' => 'products.SKU = prices.SKU'
+ ],
+
+ 'products_to_product_categories' => [
+ 'product_categories' => 'product_categories.ID = products_to_product_categories.ProductCategoryID'
+ ],
+
+ 'products_to_images' => [
+ 'products' => 'products_to_images.SimpleProductID = products.ID',
+ 'assets' => 'products_to_images.ImageID = assets.ID'
+ ]
+
+ ];
+
+ $this->extractTableFields();
+
+ // return true;
+ }
+
+
+ /** Create...
+ * one or more database tables
+ * ---
+ * @param $datasource (string|void) : table name or none
+ * if none then all tables will be created
+ * @return (boolean)
+ *
+ * NOTE:
+ * for safety reasons this method is not executing SQL;
+ * it just echoes the SQL that needs to be executed in
+ * order to crate all database tables.
+ */
+ public function create($datasource = '')
+ {
+ // if no datasource passed then datasource is all tables
+ if ($datasource == '') {
+ $datasource = array_keys(self::$tables['create']);
+
+ } else {
+ // if datasource exist, make it an array
+ if (array_key_exists($datasource, self::$tables['create'])) {
+ $datasource = [ $datasource ];
+
+ } else {
+ // table does not exist
+ return fasle;
+ }
+ }
+
+ $sql = "";
+ foreach( $datasource as $table ) {
+ $sql .= "-- CREATE ". $table .";\n". self::$tables['create'][$table] . "\n\n";
+ }
+
+ return ['sql' => $sql];
+ }
+
+
+ /** Calculate fields of every table
+ * ---
+ * Parse every CREATE SQL statement and extract list of fields;
+ * Algorithm implements linear-parsing (faster than a recursive one)
+ */
+ private function extractTableFields()
+ {
+ // words used by SQL that can not be field names
+ $nonFields = ['PRIMARY', 'KEY', '(', ')']; // reduced list (used on CREATE)
+
+ foreach( $this->tables['create'] as $table => $sqlCreate ) {
+
+ $fields = []; // variable to hold the extracted fields
+
+ // get text between first open-parentesis and last close-parentesis
+ // this is waht lies between 'CREATE TABLE table(' and ') [ENGINE whatever]'
+ // (including the patenteses)
+ preg_match_all(
+ "/\((((?>[^()]+)|(?R))*)\)/",
+ str_replace("\n", '', $sqlCreate),
+ $match
+ );
+ // remove 1st+last parentesis
+ $mainDefinitions = substr($match[0][0], 1, -1);
+
+ // replace comma (,) on decimal definitions
+ // example: 'decimal(18, 2)' turns to 'decimal(18: 2)'
+ $sentences = preg_replace(
+ '/\\(([0-9]*)[ ,]([0-9 ]*)\\)/',
+ '($1:$2)',
+ $mainDefinitions,
+ -1
+ );
+
+ // devide the banth of sentences to field definitions
+ $defines = explode(',', $sentences);
+
+ // now each denine holds a full field definition
+ // like: `ID` int(11) NOT NULL AUTO_INCREMENT
+
+ foreach($defines as $def) {
+ // check the first term of each sentence
+ $terms = explode(' ', trim($def,));
+ $term = array_shift($terms);
+
+ if (!in_array($term, $nonFields)) {
+ $fields[ str_replace('`', '', $term) ] = implode(' ', $terms);
+ }
+ }
+
+ $this->tables['fields'][$table] = $fields;
+ }
+
+ return;
+ }
+
+ public function databaseDocumentation()
+ {
+ return $this->tables['fields'];
+ }
+
+} \ No newline at end of file
diff --git a/html/app/models/_info.md b/html/app/models/_info.md
new file mode 100644
index 0000000..b5b2e1c
--- /dev/null
+++ b/html/app/models/_info.md
@@ -0,0 +1,84 @@
+
+# info about the structrure of the models
+
+According to the former impementation (atcom) the project will need to handle 200+ tables.
+The former project includes 209 tables with 5 or more rows (and 325 tables totaly)
+
+
+
+## Partitioning the database schema
+
+Models shall organized in various *thematic entity-groups* in order to handle
+the numerous relationd between tables.
+
+* market ~10
+ - [Ok] - products = cms_eStore_Products
+ - [Ok] - cms_Relationship_SimpleProducts_ProductCategories
+ - [Ok] - product_categries = cms_eStore_ProductCategories
+ - [Ok] - cms_eStore_PriceLists
+ - [Ok] - cms_eStore_Prices,
+ - [Ok] - products_to_images (assets)
+ - [Ok] - assets (*cms)
+ - [Ok] -brands = cms_eStore_Brands
+
+* sales ~ 15
+ - orders = cms_eStore_Orders
+ - orderItems = cms_eStore_OrderItems
+ - payments
+ - refunds
+ - tracking
+ - orderEvents = cms_eStore_OrderEventLogs
+ - cms_eStore_OrderStatusDescriptions,
+
+* care
+ - [data] - customers = cms_YodaAddon_Customers
+ - [data] - addresses = cms_YodaAddon_Addresses
+ - [data] - userlists = cms_YodaAddon_UserLists
+
+* system ~ 5
+ - security
+ - operators
+ - sessions * cms_UserSessions
+ - tokens
+
+* cms ~20
+ - [?Ok] - about = cms_Pages
+ - stores = cms_YodaAddon_Stores
+ - cms_Relationship_Stores_Services
+ - cms_Relationship_Stores_Images
+ - cms_Relationship_Stores_Services
+ - news
+ - contests
+ - [Ok] - assets
+ - snippets, translations, whatever...
+
+
+Then there shall be connecting-models betweem various entities, like:
+
+* Marketing: combines [market + cms]
+
+* Buing: combines [market + sales + care]
+
+
+
+## uncategorized:
+
+* cms_YodaAddon_Companies
+
+
+
+
+NOTE:
+
+Also need to partition the "working-on/preccessing data"
+
+- CMS : content creation, design, etc
+
+- Orders : pick, select, track, manare returns, etc
+
+- Customers: navigate on store, fill basket, order, pay
+
+- Prices: ERP
+
+I suggest each group of proccessing is make by different application.
+These applications may *speak* to eachother via API
diff --git a/html/app/models/cms/Page_model.php b/html/app/models/cms/Page_model.php
new file mode 100644
index 0000000..fda5fb1
--- /dev/null
+++ b/html/app/models/cms/Page_model.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace app\models\cms;
+
+use \Registry;
+
+class Page_model
+{
+ public static function fromUrl($url)
+ {
+ return Registry::use('database')->query(
+ "SELECT * FROM pages
+ WHERE FullFriendlyUrl = :url AND IsActive = 1",
+ [ ':url' => $url ]
+ )->getFirst();
+ }
+
+} \ No newline at end of file
diff --git a/html/app/models/market/Market_repository.php b/html/app/models/market/Market_repository.php
new file mode 100644
index 0000000..1338426
--- /dev/null
+++ b/html/app/models/market/Market_repository.php
@@ -0,0 +1,235 @@
+<?php
+
+namespace app\models\market;
+
+use \Repository;
+
+/** Market_repository
+ * (thematic-entity repository)
+ * ---
+ * SQL entity templates for Market
+ *
+ * Every '_base' is a repository of common 'prepare' SQL-queries;
+ * They usually include holders for attaching/binding data safely;
+ * These queries are expensive in syntax but fast on run time and
+ * are used to collect multi-joined amounts ot data.
+ *
+ * Organize yous repositories in logical/thematic groups (partitioning).
+ * This way your repository is managable and uses very few sources.
+ *
+ * This class acts like a namespaced repository for geting your code readable
+ * Avoid pushing very simple queries into repository classes. A record like...
+ * 'CARS_by:Age' => "SELECT * FROM cars WHERE age = :Age"
+ * ... won't make your code sexier nor easier to read; it will just increase
+ * your label's entropy and make trickier to pick your next sensable label;
+ *
+ * repository classes include two methods (inherited by the abstract, so
+ * you do not need to implement something more than just construct
+ * the $repository array)
+ * ** pull: for pulling a query by a friendly name
+ * ** echo: optional method used by some devolopment tools
+ *
+ * TODO: development tool using echo is still in progress
+ */
+class Market_repository extends Repository
+{
+ //// use Repository; // repository trait includes
+ //// // a method to pull an entity of the gathering
+ //// // and method to (TODO:) ...
+
+ /** repository is an array of Prepare SWL queries;
+ * each query is attached to the array via a friendly label;
+ *
+ * NOTE: (PROPOSAL)
+ * ---
+ * about the label:
+ * 1. keep names short descriptive but not too long
+ * 2. main entity/entities shall be UPPERCASED
+ * 3. determinands/adjectives shall be Cappitalized
+ * 4. words are connected with underscore (_)
+ * 5. if SQL inludes bind-holderd, label shall be suffixed with '_by:'
+ * followed by holder names connected with undercores (case-sensitively)
+ *
+ * about the data-bind holders (if present):
+ * 1. all holders are prefixed with ':'
+ * 2. holder name shall begin with a letter and contain one word.
+ *
+ * example:
+ * 'Active_EMPLOYEES_by:Age_CityID' => "SELECT Em.*, c.*
+ * FROM employee Em LEFT JOIN city c ON c.id = Em.city_id
+ * WHERE c.id = :CityID AND Em.age = :Age"
+ *
+ * It is recommended to leave a descriptive comment befor each declaration
+ * and you may also include sql-comments inside the query.
+ */
+ public static $repository = [
+
+ // PRODUCTS by: storeID, ProductUrl
+ // picks all product properties
+ // + default image + price for certain store
+ 'Active_PRODUCT_by:storeID_ProductUrl' =>
+ "SELECT p.*,
+ pc.ID AS ProductCategory,
+ pc.Hierarchy,
+ pc.FullFriendlyUrl AS `Path`,
+ prices.ProductPrice_OriginalPrice AS Price,
+ prices.ProductPrice_DiscountedPrice AS DiscountPrice,
+ prices.UnitMeasurePrice_OriginalPrice AS PerUnitPrice,
+ prices.UnitMeasurePrice_DiscountedPrice AS PerUnitDiscountPrice,
+ b.Title BrandName
+ FROM products p
+ LEFT JOIN products_to_product_categories p2pc ON p.ID = p2pc.SimpleProductID
+ LEFT JOIN product_categories pc ON p2pc.ProductCategoryID = pc.ID
+ LEFT JOIN prices ON p.SKU = prices.SKU
+ LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID
+ LEFT JOIN assets ON assets.ID = p2i.ImageID
+ LEFT JOIN brands b ON b.ID = p.BrandID
+ WHERE pc.IsActive = 1 AND pc.IsCurrentlyActive = 1
+ AND p2i.Order = 1
+ AND p.IsActive = 1
+ AND p.Published = 1
+ AND prices.PriceListID = :storeID
+ AND p.FriendlyUrl = :ProductUrl"
+ ,
+
+ // PRODUCTS by: storeID, ProductID
+ // picks all product properties
+ // + default image + price for certain store
+ 'Active_PRODUCT_by:storeID_ProductID' =>
+ "SELECT p.*,
+ pc.ID AS ProductCategory,
+ pc.Hierarchy,
+ pc.FullFriendlyUrl AS `Path`,
+ prices.ProductPrice_OriginalPrice AS Price,
+ prices.ProductPrice_DiscountedPrice AS DiscountPrice,
+ prices.UnitMeasurePrice_OriginalPrice AS PerUnitPrice,
+ prices.UnitMeasurePrice_DiscountedPrice AS PerUnitDiscountPrice,
+ b.Title BrandName
+ FROM products p
+ LEFT JOIN products_to_product_categories p2pc ON p.ID = p2pc.SimpleProductID
+ LEFT JOIN product_categories pc ON p2pc.ProductCategoryID = pc.ID
+ LEFT JOIN prices ON p.SKU = prices.SKU
+ LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID
+ LEFT JOIN assets ON assets.ID = p2i.ImageID
+ LEFT JOIN brands b ON b.ID = p.BrandID
+ WHERE pc.IsActive = 1 AND pc.IsCurrentlyActive = 1
+ AND p2i.Order = 1
+ AND p.IsActive = 1
+ AND p.Published = 1
+ AND prices.PriceListID = :storeID
+ AND p.ID = :ProductID"
+ ,
+
+ // CATEGORY-PRODUCTS by: storeID, likeHierarchy
+ // traverses all products from category and subcategories
+ // picke price and default image
+ 'CATEGORY-PRODUCTS_by:storeID_likeHierarchy' =>
+ "SELECT p.*,
+ assets.Url AS ImageUrl,
+ pc.FullFriendlyUrl AS `Path`,
+ prices.ProductPrice_OriginalPrice AS Price,
+ prices.ProductPrice_DiscountedPrice AS DiscountPrice,
+ prices.UnitMeasurePrice_OriginalPrice AS PerUnitPrice,
+ prices.UnitMeasurePrice_DiscountedPrice AS PerUnitDiscountPrice
+ FROM products p
+ LEFT JOIN prices ON p.SKU = prices.SKU
+ LEFT JOIN products_to_product_categories p2pc ON p2pc.SimpleProductID = p.ID
+ LEFT JOIN product_categories pc ON pc.ID = p2pc.ProductCategoryID
+ LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID
+ LEFT JOIN assets ON assets.ID = p2i.ImageID
+ WHERE p2pc.ProductCategoryID IN (
+ SELECT `ID` FROM product_categories
+ WHERE Hierarchy LIKE :likeHierarchy
+ )
+ AND p2i.Order = 1
+ AND p.IsActive = 1
+ AND p.Published = 1
+ AND prices.PriceListID = :storeID"
+ ,
+
+ // Count Products
+ // of all last-lever Product categories
+ // by StoreID
+ // ---
+ // used on creating product_categories tree
+ // ProductCategories_model::tree()
+ 'Count_Last_Level_PRODUCTS_by:storeID' =>
+ "SELECT pc.ID, COUNT(p.ID) as `Counter`
+ FROM product_categories pc
+ LEFT JOIN products_to_product_categories p2pc ON pc.ID = p2pc.ProductCategoryID
+ LEFT JOIN products p ON p2pc.SimpleProductID = p.ID
+ LEFT JOIN prices ON p.SKU = prices.SKU
+ LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID
+ LEFT JOIN assets ON assets.ID = p2i.ImageID
+ WHERE pc.IsActive = 1 AND pc.IsCurrentlyActive = 1
+ AND p2i.Order = 1
+ AND pc.Level = 2
+ AND p.IsActive = 1
+ AND p.Published = 1
+ AND prices.PriceListID = :storeID
+ GROUP BY pc.ID"
+ ,
+
+ // PRODUCT by: ProductID
+ // Join images array (json string)
+ // Join prices for all stores (json string)
+ 'Active_PRODUCT_complete_by:ProductID' =>
+ "SELECT p.*, pc.ID AS ProductCategory,
+ pc.Hierarchy, pc.FullFriendlyUrl AS `Path`,
+ ( -- get all store prices as Json
+ SELECT JSON_ARRAYAGG(JSON_OBJECT(
+ 'Store', prices.PriceListID,
+ 'Price', prices.ProductPrice_OriginalPrice,
+ 'DiscountPrice', prices.ProductPrice_DiscountedPrice,
+ 'PerUnitPrice', prices.UnitMeasurePrice_OriginalPrice,
+ 'PerUnitDiscountPrice', prices.UnitMeasurePrice_DiscountedPrice
+ ))
+ FROM prices
+ WHERE prices.SKU = p.SKU
+ ) as pricesJson,
+ ( -- get all images as Json
+ SELECT JSON_ARRAYAGG(JSON_OBJECT(
+ 'Order', p2i.Order,
+ 'Url', assets.Url,
+ 'Title', assets.Title,
+ 'Description', assets.Description
+ ))
+ FROM products_to_images p2i
+ LEFT JOIN assets ON assets.ID = p2i.ImageID
+ WHERE p2i.SimpleProductID = p.ID
+ ) AS imagesJson,
+ b.Title BrandName
+ FROM products p
+ LEFT JOIN products_to_product_categories p2pc ON p.ID = p2pc.SimpleProductID
+ LEFT JOIN product_categories pc ON p2pc.ProductCategoryID = pc.ID
+ LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID
+ LEFT JOIN assets ON assets.ID = p2i.ImageID
+ LEFT JOIN brands b ON b.ID = p.BrandID
+ WHERE pc.IsActive = 1 AND pc.IsCurrentlyActive = 1
+ AND p2i.Order = 1
+ AND p.IsActive = 1
+ AND p.Published = 1
+ AND p.ID = :ProductID"
+ ,
+
+ // Product images by: ProductID
+ // (sorted by 'order' field)
+ 'PRODUCT_IMAGES_by:productID' =>
+ "SELECT a.Url, a.Title, a.Description
+ FROM products p
+ LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID
+ LEFT JOIN assets a ON a.ID = p2i.ImageID
+ WHERE p.IsActive = 1
+ AND p.Published = 1
+ AND p.ID = :productID
+ ORDER BY p2i.Order"
+
+ ];
+
+ /* no need to impement anything else;
+ * you can overide if needed the default methods:
+ * + public static function pull($entity){ }
+ * + public static function echo($content =false){ }
+ */
+
+}
diff --git a/html/app/models/market/ProductCategories_model.php b/html/app/models/market/ProductCategories_model.php
new file mode 100644
index 0000000..d8c274e
--- /dev/null
+++ b/html/app/models/market/ProductCategories_model.php
@@ -0,0 +1,199 @@
+<?php
+
+namespace app\models\market;
+
+use \Registry;
+use app\models\market\Market_repository;
+
+
+/** ProductCategories_model
+ * ---
+ * undertakes to collect any product_categories data
+ * from the database and prepare needed data structures.
+ *
+ */
+class ProductCategories_model
+{
+
+ /** Get (product_) category from (full-friendly_) URL
+ * --- (self explanatory)
+ * @param $url (string): Full-Friendly-URL
+ * @return $category (array); also includes all category products
+ */
+ public static function categoryFromUrl($url)
+ {
+ $db = Registry::use('database');
+ $category = $db->query( "SELECT * FROM product_categories
+ WHERE FullFriendlyUrl = :url AND IsActive = 1",
+ [ ':url' => $url ]
+ )->getFirst();
+
+ if ($category === false) { return false; } // no category? -> false
+
+ // GET PRODUCTS of category
+ $category['products'] = self::categoryProductsByHierarchy($category['Hierarchy']);
+ return $category;
+ }
+
+
+ /** Get (product_) category from ID
+ * --- (self explanatory)
+ * @param $id (int): Catgory ID (usualy from products)
+ * @return $category (array); also includes all category products
+ */
+ public static function categoryFromID($id)
+ {
+ return Registry::use('database')->query( "SELECT * FROM product_categories
+ WHERE `ID` = :url AND IsActive = 1",
+ [':id' => $id]
+ )->getFirst();
+
+ }
+
+
+ /** categoryProductsByHierarchy
+ * ---
+ * This method returns the products belonging to a certain
+ * category (and all it's the children/sub-categories);
+ * It uses category.Hierarchy in a WHERE LIKE condition to
+ * make it fast.
+ *
+ * The method is category.Level agnostic
+ *
+ * @param $category (array)
+ * @return $products (array)
+ */
+ public static function categoryProductsByHierarchy($hierarchy, $store = 904)
+ {
+ // GET PRODUCTS of category
+ // NOTE:
+ // category.Hierarchy is used
+
+ return Registry::use('database')->runQuery(
+ Market_repository::pull('CATEGORY-PRODUCTS_by:storeID_likeHierarchy'),
+ [
+ ':likeHierarchy' => $hierarchy.'%',
+ ':storeID' => 904
+ ]
+ );
+ }
+
+
+ # depricated:
+ # now Proxy has the responsibility to cache whatever
+ # ---
+ # /** GET tree of product_categories
+ # * ---
+ # * get from cache or cache it after creation
+ # */
+ # public static function tree($store = 904)
+ # {
+ # $cacheKey = get_called_class() . $store .'/tree';
+ #
+ # // IF category CACHED
+ # $cache = Registry::use('cache');
+ # $tree = $cache->get($cacheKey);
+ # if ($tree !== false) {
+ # return $tree;
+ # }
+ #
+ # // (not cached) Create and Cache it
+ # $tree = self::createCategoriesTree();
+ # $cache->set($cacheKey, $tree, CACHE_ROOT_TTL);
+ #
+ # return $tree;
+ # }
+
+
+ /** CREATE product_categories tree
+ * NOTE: includes active categories only;
+ *
+ * Algorith uses 'Hierarchy' for category path-positioning,
+ * implementing a linear conctruction of the requested tree;
+ * (faster and less source-consuming than a recursive algo)
+ */
+ public static function tree($store = 904)
+ {
+ $tree = []; // variable to hold results
+
+ $db = Registry::use('database');
+
+ // get raw categories data ---------------------------------------------
+
+ $raw = $db->runQuery( "SELECT `ID`, Title, `Level`, Hierarchy,
+ ParentID, `Order`, FullFriendlyUrl
+ FROM product_categories pc
+ WHERE IsActive = 1 AND IsCurrentlyActive = 1
+ ORDER BY pc.Level asc, pc.Order asc, pc.Hierarchy asc",
+ []
+ );
+
+ // count products per 3rd level category -------------------------------
+
+ $countProducts = $db->runQuery(
+ Market_repository::pull('Count_Last_Level_PRODUCTS_by:storeID'),
+ [ ':storeID' => $store ]
+ );
+ // map categoryID -> Counter
+ $mapCounter = [];
+ foreach($countProducts as $rec) $mapCounter[$rec['ID']] = $rec['Counter'];
+
+ // parse raw data to hierarchical tree (2 pass) ------------------------
+
+ // 1st pass: parse raw data, construct and fill the $tree array ........
+ foreach($raw as $rec) {
+
+ // create category path from Hierarchy (format: .10.10100. )
+ // so remove 1st and last dots (.) from Hierarchy string
+ // then explode via dot-character (.)
+ $categoryPath = explode('.', substr($rec['Hierarchy'], 1, -1));
+
+ switch ($rec['Level']) {
+ case 0:
+ $tree[$categoryPath[0]] = [
+ 'info' => $rec,
+ 'childs' => []
+ ]; break;
+
+ case 1:
+ $tree[$categoryPath[0]]['childs'][$categoryPath[1]] = [
+ 'info' => $rec,
+ 'childs' => []
+ ]; break;
+
+ case 2:
+ $tree[$categoryPath[0]]['childs'][$categoryPath[1]]['childs'][$categoryPath[2]] = [
+ 'info' => $rec,
+ 'count' => isset($mapCounter[$categoryPath[2]]) ? $mapCounter[$categoryPath[2]] : 0
+ ]; break;
+
+ default:
+ // nothing...
+ }
+
+ }
+
+ // 2nd pass: remove categories with no products ........................
+ foreach($tree as $id0 => $rec0) {
+ $count0 = 0;
+
+ foreach($rec0['childs'] as $id1 => $rec1) {
+ $count1 = 0;
+
+ foreach($rec1['childs'] as $id2 => $rec2) {
+ if ($rec2['count']>0) $count1++;
+ else unset($tree[$id0]['childs'][$id1]['childs'][$id2]);
+ }
+
+ if ($count1 > 0) $count0++;
+ else unset($tree[$id0]['childs'][$id1]);
+ }
+
+ if (!$count0) unset($tree[$id0]);
+ }
+
+ return $tree;
+ }
+
+
+} \ No newline at end of file
diff --git a/html/app/models/market/Product_model.php b/html/app/models/market/Product_model.php
new file mode 100644
index 0000000..aa554a1
--- /dev/null
+++ b/html/app/models/market/Product_model.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace app\models\market;
+
+use \Registry;
+use app\models\market\Market_repository as Market;
+
+class Product_model
+{
+ public static function fromUrl($url, $store = 904)
+ {
+ $product = Registry::use('database')->query(
+ Market::pull('Active_PRODUCT_by:storeID_ProductUrl'),
+ [
+ ':storeID' => $store,
+ ':ProductUrl' => $url
+ ]
+ )->getFirst();
+
+ // if no product, return false
+ if ($product === false) return false;
+
+ // inject product's images
+ $product['Images'] = self::getProductImages($product['ID']);
+
+ return $product;
+ }
+
+
+ public static function getProductImages($id)
+ {
+ ## return Registry::use('database')->runQuery(
+ ## "SELECT a.Url, a.Title, a.Description
+ ## FROM products p
+ ## LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID
+ ## LEFT JOIN assets a ON a.ID = p2i.ImageID
+ ## WHERE p.IsActive = 1 AND p.Published = 1
+ ## AND p.ID = :id
+ ## ORDER BY p2i.Order",
+ ## [ ':id' => $id ]
+ ## );
+
+ return Registry::use('database')->runQuery(
+ Market::pull('PRODUCT_IMAGES_by:productID'),
+ ['productID' => $id ]
+ );
+ }
+
+} \ No newline at end of file
diff --git a/html/app/models/todo.md b/html/app/models/todo.md
new file mode 100644
index 0000000..defd21e
--- /dev/null
+++ b/html/app/models/todo.md
@@ -0,0 +1,132 @@
+
+
+Privilege
+---
+
+Privilege::list_of_privileges( privilege_id )
+
+Privilege::privileges_tree()
+
+
+
+
+
+User
+---
+
+User::get_privileges
+
+
+User::has_privilege( privilege_id )
+
+
+User::reset_password()
+ -> create otp
+ -> send email
+
+
+User::set_privilege( privilege_id )
+
+
+User::track_action()
+ -> user_id
+ -> action : Contoller::method(args)
+ -> timestamp
+
+
+
+Lesson:
+---
+
+Lesson::create_lesson( POST )
+
+Lesson::get_lesson( lesson_id )
+
+
+
+Course
+---
+
+Course::create_course( POST )
+
+Course::courses_tree()
+
+
+
+Upgrades (??)
+---
+
+
+A1
+
+A2 : includes A1
+
+A3 : includes A1, A2
+
+A4 : includes A1, A2, A3
+
+
+A1 100
+A2 200
+A3 350
+A4 500
+
+B1 100
+B2 200
+B3 400
+B4 800
+
+
+:1 -> 0%
+:2 -> 15%
+:3 -> 20%
+
+
+
+
+A1 -> A2
+A1 -> A3
+A1 -> A4
+
+A2 -> A3
+A2 -> A4
+
+A3 -> A4
+
+A1 < A2 < A3 < A4
+
+
+
+B1 -> B2
+B1 -> B3
+B1 -> B4
+
+B2 -> B3
+B2 -> B4
+
+B3 -> B4
+
+
+A1+B1
+
+A2+B2
+
+A3+B3
+
+
+A1+B1 -> A2+B2
+A1+B1 -> A3+B3
+A1+B1 -> A4+B4
+
+A2+B2 -> A3+B3
+A2+B2 -> A4+B4
+
+A3+B3 -> A4+B4
+
+
+
+UPGRADE PRICE = upper_priv_price - own_priv_price
+
+buy 1 item : -0%
+buy 2 items : -10%
+buy 3 items : -15%
diff --git a/html/app/routes/api.php b/html/app/routes/api.php
new file mode 100644
index 0000000..659c25f
--- /dev/null
+++ b/html/app/routes/api.php
@@ -0,0 +1,64 @@
+<?php
+
+use app\controllers\api\Common_api;
+use app\controllers\api\Doc_api;
+
+// api: tree of product-categories
+Route::add('/api/tree',
+ function() { Doc_api::tree(); },
+ 'get'
+);
+
+// api: database documentation
+Route::add('/api/doc',
+ function() { Doc_api::dbDoc(); },
+ 'get'
+);
+
+Route::add('/api/url/([0-9a-zA-Z-_\/]*)',
+ function($url) { Common_api::category_by_url($url); }
+);
+
+// route: /api/table
+Route::add('/api/([0-9a-zA-Z-_]*)',
+ function($table) { Common_api::table($table); },
+ 'get'
+);
+
+// route: /api/table/{id}
+Route::add('/api/([0-9a-zA-Z-_]*)/([0-9]*)',
+ function($table, $id) { Common_api::record($table, $id); },
+ 'get'
+);
+
+
+# // test on the go
+# // find products with large differences on price between stores
+# // ---
+# Route::add('/api/price-diffs', function() {
+# $wtf = Registry::use('database')->runQuery("SELECT sku,
+# max(ProductPrice_OriginalPrice) as MX,
+# min(ProductPrice_OriginalPrice) as MN,
+# ((max(ProductPrice_OriginalPrice)-min(ProductPrice_OriginalPrice))/max(ProductPrice_OriginalPrice)) as Di
+# FROM prices
+# GROUP BY sku
+# ORDER BY Di desc
+# LIMIT 300",
+# []
+# );
+# reply_json([
+# 'success' => true,
+# 'result' => $wtf
+# ]); die();
+# });
+
+
+// route for testing...
+// ---
+Route::add('/test/([0-9a-zA-Z-_\/]*)', function($test) {
+ if (file_exists(TESTS_DIRECTORY . $test .'.php')) {
+ if (!Benchmark::exist('start')) Benchmark::add_spot('start');
+ require(TESTS_DIRECTORY . $test .'.php');
+ } else { echo "Test {$test} not exist";
+ } die();
+});
diff --git a/html/app/routes/backend.php b/html/app/routes/backend.php
new file mode 100644
index 0000000..9b90cf1
--- /dev/null
+++ b/html/app/routes/backend.php
@@ -0,0 +1,9 @@
+<?php
+
+Route::add('/admin/info', // generaly you need to hide phpinfo()
+ function() { // so, remove this route on production
+ // Authenticate::session(); // or require authentication
+ phpinfo(); },
+ 'get'
+);
+
diff --git a/html/app/routes/frontend.php b/html/app/routes/frontend.php
new file mode 100644
index 0000000..1eea46c
--- /dev/null
+++ b/html/app/routes/frontend.php
@@ -0,0 +1,135 @@
+<?php
+
+use app\controllers\Resolve;
+use app\controllers\Product;
+
+
+// Home/Welcome
+// ---
+Route::add('/', function() { // echo 'Welcome!'; });
+ header('Location: /eidi-artozacharoplasteioy/psomi-artoskeyasmata', true, 302);
+});
+
+
+// Error Pages
+// ---
+Route::notFound( function() {
+ header("HTTP/1.0 404 Not Found");
+ render_view('error/404', ['message' => 'nobody knows it (but you got a secret smile;)']);
+}); // 404
+
+
+// Resolve friendly urls
+// ---
+Route::add('/([0-9a-zA-Z-_]*)',
+ function($a) { Resolve::url([$a]); }
+);
+Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)',
+ function($a, $b) { Resolve::url([$a, $b]); }
+);
+Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)',
+ function($a, $b, $c) { Resolve::url([$a, $b, $c]); }
+);
+
+
+// 4-th level paths belong to products
+// ---
+Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)',
+ function($a, $b, $c, $d) { Product::fromUrl($d, [$a, $b, $c]); }
+);
+
+
+/** URL-format Proposal:
+ * -----------------------------------------------------------------------------
+ *
+ * Page (almost-static content):
+ * /about/{url}
+# Route::add('/about/([0-9a-zA-Z-_\/]*)', function($url) { Page::fromUrl($url); });
+ *
+ * Product:
+ * /{level-1}/{level-2}/{level-3}/{friendly-url}-{id}
+# Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)-([0-9]*)',
+# function($category, $subcategory, $group, $label, $id) {
+# Product::fromID([$$category, $subcategory, $group, $label], $id);
+# }
+# );
+ *
+ * Category
+ * /{category}/[ {subcategory}[ /{group}]]
+Route::add('/([0-9a-zA-Z-_]*)', function($a) { Category::url([$a]); };
+Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', function($a, $b) { Category::url([$a, $b]); });
+Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', function($a, $b, $c) { Category::url([$a, $b, $c]); });
+ * -----------------------------------------------------------------------------
+ */
+
+
+/* EXAMPLES (use it for brainsrtorming)
+ * -----------------------------------------------------------------------------
+ *
+ // Typical Use
+ // ---
+ Route::add('/art', function() { Art::blah(); });
+ Route::add('/art/db', function() { Art::fromDatabase(); });
+ Route::add('/art/([0-9]*)', function($id) { Art::getInfo($id); });
+
+
+ // Get-Post route example
+ // ---
+ Route::add('/contact-form', function() {
+ echo '<form method="post"><input type="text" name="test" /><input type="submit" value="send" /></form>';
+ }, 'get');
+
+ Route::add('/contact-form', function() {
+ echo 'Hey! The form has been sent:<br/>'; print_r($_POST);
+ }, 'post');
+
+
+ // Accept number as parameter
+ // ---
+ Route::add('/foo/([0-9]*)/bar', function($var1) {
+ // echo $var1.' is a number!';
+ echo "{$var1} is a number!";
+ });
+
+
+ // About pages
+ // ---
+ Route::add('/about/([0-9a-zA-Z-_]*)', function($page) {
+ InfoPage::render('about/'.page);
+ });
+
+
+ // handle a request like: /foo/123/bar/3254-lefki-zaxari-marata-1kg
+ // where first-numeric-part of last-uri-section (3254) is the product number
+ // ---
+ Route::add('/foo/([0-9]*)/bar/([0-9]*)-([0-9a-zA-Z-_]*)', function($num, $id, $name) {
+ echo $num .' is some nomber, id = '. $id .' and label = '. $name;
+ });
+
+
+ // handle a request like: /zaxares-glykantika/lefki-zaxari-year-2022-45678
+ // where last-numeric-part of last-uri-section (45678) is the product number
+ // ---
+ Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)-([0-9]*)',
+ function($category, $subcategory, $group, $label, $id) {
+ Shop::product([$$category, $subcategory, $group, $label], $id);
+ });
+
+
+ // last chance to resolve (some friendly url)
+ // ---
+ Route::add('/([0-9a-zA-Z-_]*)', function($a) { Resolve::uri([$a]); });
+ Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', function($a, $b) { Resolve::uri([$a, $b]); });
+ Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', function($a, $b, $c) { Resolve::uri([$a, $b, $c]); });
+
+
+ // Product page
+ // handle a request like: /pantopoleio/zaxares/lefki-zaxari/lefki-zaxari-year-2022-45678
+ // where last-numeric-part of last-uri-section (45678) is the product number
+ // ---
+ Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)-([0-9]*)',
+ function($category, $subcategory, $group, $label, $id) {
+ Shop::product([$$category, $subcategory, $group, $label], $id);
+ });
+* ------------------------------------------------------------------------------
+*/
diff --git a/html/app/views/basic.php b/html/app/views/basic.php
new file mode 100644
index 0000000..3d9abde
--- /dev/null
+++ b/html/app/views/basic.php
@@ -0,0 +1,53 @@
+<?php ob_start(); ?><!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>ΣΚΛΑΒΕΝΙΤΗΣ - <?=$page['title']?></title>
+
+ <!-- import assets -->
+ <?php link_asset(
+ 'font',
+ ['iconfont', 'CFAstyStdBold', 'CFAstyStdBook', 'CFAstyStdMedium'],
+ 'crossorogin'
+ ) ?>
+
+ <?php link_asset('css', ['main']); ?>
+
+ <!-- TODO: parse seo data -->
+
+
+</head>
+<body class="homepage" data-plugin-lazyload>
+
+ <div class="mainContainer">
+
+ <div class="mainLayer"></div>
+
+
+ <!-- header -->
+ < ?php cache_view('partials/header', []); ? >
+ <?php ob_flush(); ?>
+
+
+ <!-- main content -->
+ <main class="main">
+
+ <?php parse_sections( $page['sections'] ); ?>
+
+ </main>
+ <?php ob_flush(); ?>
+
+
+ <!-- footer -->
+ < ?php cache_view('partials/footer', [], true) ? >
+
+ </div>
+
+ <!-- scripts -->
+ <?php link_asset('js',
+ ['require.files', 'require.slim', 'plugins.min', 'plugins.setup' ]
+ ); ?>
+
+</body>
+</html><?php ob_flush(); ?>
diff --git a/html/app/views/group.php b/html/app/views/group.php
new file mode 100644
index 0000000..fddd3d2
--- /dev/null
+++ b/html/app/views/group.php
@@ -0,0 +1,18 @@
+<img src="https://cdn.sklavenitis.co.gr/uploads/products/1180106.jpg">
+<!--
+<img src="https://s1.sklavenitis.gr/images/1600x1600/40/files/ProductMedia/Products/1222798/1.jpg" style="max-height:240px;"> -->
+<img src="https://s1.sklavenitis.gr/images/ProductDetail/40/files/ProductMedia/Products/1222798/1.jpg">
+
+<section>
+ <div style="padding: 1em">
+
+ <?php foreach ($group as $item) : ?>
+
+ <div style="padding: 1em">
+ <?php render_view('item', ['item' => $item]) ?>
+ </div>
+
+ <?php endforeach; ?>
+
+ </div>
+</section>
diff --git a/html/app/views/item.php b/html/app/views/item.php
new file mode 100644
index 0000000..2354c81
--- /dev/null
+++ b/html/app/views/item.php
@@ -0,0 +1,4 @@
+<h3><?=$item['title']?></h3>
+<p>
+ <i><?=$item['info']?></i>
+</p> \ No newline at end of file
diff --git a/html/app/views/products_list.php b/html/app/views/products_list.php
new file mode 100644
index 0000000..1d9cf38
--- /dev/null
+++ b/html/app/views/products_list.php
@@ -0,0 +1,47 @@
+<style>
+ body {
+ max-width: 960px; width: 100%; margin: 0 auto;
+ font-family: Ubuntu, Calibri, Helevetica, sans-serif;
+ padding: 2em 1em;
+ }
+ td {
+ vertical-align: top;
+ padding: 2px; margin: .5em;
+ font-size: .82em; text-align: right; color: #666;
+ }
+
+ h3 { color: #924; text-align: center; }
+
+ .-edit {
+ text-align: left; font-size: 1em;
+ color: #357; background: #f6f6f6;
+ padding: .5em;
+ }
+ td span { font-size: .8em; color: #9249; }
+
+ [contenteditable='true']:focus { border-radius: 3px; }
+ [contenteditable='true']:focus {
+ outline: none;
+ color: #924;
+ border: 2px solid #9249; border-top: 4px solid #9249;
+ }
+
+</style>
+
+<h3>— products —</h3>
+
+<?php foreach( $products as $product) : ?>
+
+ <table>
+ <?php foreach ($product as $key => $val) : ?>
+
+ <tr>
+ <td><?= $key ?><br/><span>int(11) NOT NULL</span></td>
+ <td class='-edit' id='<?=$key .'_'. $product['ID']?>' contenteditable="true"><?=$val?></td>
+ </tr>
+
+ <?php endforeach ?>
+ </table>
+ <hr />
+
+<?php endforeach ?>