diff --git a/Admin/Install/Navigation.install.json b/Admin/Install/Navigation.install.json
index 66d3f22..c466b0f 100644
--- a/Admin/Install/Navigation.install.json
+++ b/Admin/Install/Navigation.install.json
@@ -5,131 +5,101 @@
"type": 2,
"subtype": 1,
"name": "Analysis",
- "uri": "{/prefix}sales/analysis/overview/dashboard?{?}",
+ "uri": "{/base}/sales/analysis?{?}",
"target": "self",
"icon": null,
"order": 15,
- "from": "Analysis",
- "permission": { "permission": 2, "type": null, "element": null },
+ "from": "SalesAnalysis",
+ "permission": { "permission": 2, "category": null, "element": null },
"parent": 1001601001,
"children": [
{
- "id": 1005401002,
- "pid": "44cbe06dd734865b28c110fbbf91d78ccf8124ae",
+ "id": 1001602001,
+ "pid": "/sales/analysis",
"type": 3,
"subtype": 1,
- "name": "Overview",
- "uri": "{/prefix}sales/analysis/overview/dashboard?{?}",
+ "name": "Dashboard",
+ "uri": "{/base}/sales/analysis?{?}",
"target": "self",
- "icon": "fa fa-bar-chart",
+ "icon": null,
"order": 1,
- "from": "Analysis",
- "permission": { "permission": 2, "type": null, "element": null },
+ "from": "SalesAnalysis",
+ "permission": { "permission": 2, "category": null, "element": null },
"parent": 1005401001,
"children": []
},
{
- "id": 1005401003,
- "pid": "44cbe06dd734865b28c110fbbf91d78ccf8124ae",
+ "id": 1001603001,
+ "pid": "/sales/analysis",
"type": 3,
"subtype": 1,
- "name": "Products",
- "uri": "{/prefix}sales/analysis/product/dashboard?{?}",
+ "name": "Item",
+ "uri": "{/base}/sales/analysis/item",
"target": "self",
- "icon": "fa fa-tag",
- "order": 1,
- "from": "Analysis",
- "permission": { "permission": 2, "type": null, "element": null },
+ "icon": null,
+ "order": 5,
+ "from": "SalesAnalysis",
+ "permission": { "permission": 2, "category": null, "element": null },
"parent": 1005401001,
"children": []
},
{
- "id": 1005401004,
- "pid": "44cbe06dd734865b28c110fbbf91d78ccf8124ae",
+ "id": 1005404001,
+ "pid": "/sales/analysis",
"type": 3,
"subtype": 1,
- "name": "Customers",
- "uri": "{/prefix}sales/analysis/customer/dashboard?{?}",
+ "name": "Client",
+ "uri": "{/base}/sales/analysis/client?{?}",
"target": "self",
- "icon": "fa fa-users",
- "order": 1,
- "from": "Analysis",
- "permission": { "permission": 2, "type": null, "element": null },
+ "icon": null,
+ "order": 10,
+ "from": "SalesAnalysis",
+ "permission": { "permission": 2, "category": null, "element": null },
"parent": 1005401001,
"children": []
},
{
- "id": 1005401005,
- "pid": "44cbe06dd734865b28c110fbbf91d78ccf8124ae",
+ "id": 1005405001,
+ "pid": "/sales/analysis",
"type": 3,
"subtype": 1,
- "name": "Regions",
- "uri": "{/prefix}sales/analysis/region/dashboard?{?}",
+ "name": "Bill",
+ "uri": "{/base}/sales/analysis/bill",
"target": "self",
- "icon": "fa fa-globe",
- "order": 1,
- "from": "Analysis",
- "permission": { "permission": 2, "type": null, "element": null },
+ "icon": null,
+ "order": 15,
+ "from": "SalesAnalysis",
+ "permission": { "permission": 2, "category": null, "element": null },
"parent": 1005401001,
"children": []
},
{
- "id": 1005401006,
- "pid": "44cbe06dd734865b28c110fbbf91d78ccf8124ae",
+ "id": 1005406001,
+ "pid": "/sales/analysis",
"type": 3,
"subtype": 1,
- "name": "Marketing",
- "uri": "{/prefix}sales/analysis/marketing/dashboard?{?}",
+ "name": "Region",
+ "uri": "{/base}/sales/analysis/region?{?}",
"target": "self",
- "icon": "fa fa-paint-brush",
- "order": 1,
- "from": "Analysis",
- "permission": { "permission": 2, "type": null, "element": null },
+ "icon": null,
+ "order": 20,
+ "from": "SalesAnalysis",
+ "permission": { "permission": 2, "category": null, "element": null },
"parent": 1005401001,
"children": []
},
{
- "id": 1005401007,
- "pid": "44cbe06dd734865b28c110fbbf91d78ccf8124ae",
+ "id": 1005407001,
+ "pid": "/sales/analysis",
"type": 3,
"subtype": 1,
- "name": "Employees",
- "uri": "{/prefix}sales/analysis/employees/dashboard?{?}",
+ "name": "SalesRep",
+ "uri": "{/base}/sales/analysis/rep?{?}",
"target": "self",
- "icon": "fa fa-building-o",
- "order": 1,
- "from": "Analysis",
- "permission": { "permission": 2, "type": null, "element": null },
- "parent": 1005401001,
- "children": []
- },
- {
- "id": 1005401008,
- "pid": "44cbe06dd734865b28c110fbbf91d78ccf8124ae",
- "type": 3,
- "subtype": 1,
- "name": "Invoices",
- "uri": "{/prefix}sales/analysis/invoices/dashboard?{?}",
- "target": "self",
- "icon": "fa fa-envelope",
- "order": 1,
- "from": "Analysis",
- "permission": { "permission": 2, "type": null, "element": null },
- "parent": 1005401001,
- "children": []
- },
- {
- "id": 1005401009,
- "pid": "44cbe06dd734865b28c110fbbf91d78ccf8124ae",
- "type": 3,
- "subtype": 1,
- "name": "Database",
- "uri": "{/prefix}sales/analysis/invoices/dashboard?{?}",
- "target": "self",
- "icon": "fa fa-database",
- "order": 1,
- "from": "Analysis",
- "permission": { "permission": 2, "type": null, "element": null },
+ "icon": null,
+ "order": 25,
+ "from": "SalesAnalysis",
+ "permission": { "permission": 2, "category": null, "element": null },
"parent": 1005401001,
"children": []
}
diff --git a/Admin/Install/Navigation.php b/Admin/Install/Navigation.php
index c64d7a2..373e5d3 100644
--- a/Admin/Install/Navigation.php
+++ b/Admin/Install/Navigation.php
@@ -8,20 +8,20 @@
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
- * @link https://orange-management.org
+ * @link https://jingga.app
*/
declare(strict_types=1);
namespace Modules\SalesAnalysis\Admin\Install;
-use phpOMS\DataStorage\Database\DatabasePool;
+use phpOMS\Application\ApplicationAbstract;
/**
* Navigation class.
*
* @package Modules\SalesAnalysis\Admin\Install
* @license OMS License 1.0
- * @link https://orange-management.org
+ * @link https://jingga.app
* @since 1.0.0
*/
class Navigation
@@ -29,15 +29,15 @@ class Navigation
/**
* Install navigation providing
*
- * @param string $path Module path
- * @param DatabasePool $dbPool Database pool for database interaction
+ * @param ApplicationAbstract $app Application
+ * @param string $path Module path
*
* @return void
*
* @since 1.0.0
*/
- public static function install(string $path, DatabasePool $dbPool) : void
+ public static function install(ApplicationAbstract $app, string $path) : void
{
- \Modules\Navigation\Admin\Installer::installExternal($dbPool, ['path' => __DIR__ . '/Navigation.install.json']);
+ \Modules\Navigation\Admin\Installer::installExternal($app, ['path' => __DIR__ . '/Navigation.install.json']);
}
}
diff --git a/Admin/Installer.php b/Admin/Installer.php
index 1020eae..a4cd5fe 100644
--- a/Admin/Installer.php
+++ b/Admin/Installer.php
@@ -8,7 +8,7 @@
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
- * @link https://orange-management.org
+ * @link https://jingga.app
*/
declare(strict_types=1);
@@ -21,9 +21,16 @@ use phpOMS\Module\InstallerAbstract;
*
* @package Modules\SalesAnalysis\Admin
* @license OMS License 1.0
- * @link https://orange-management.org
+ * @link https://jingga.app
* @since 1.0.0
*/
final class Installer extends InstallerAbstract
{
+ /**
+ * Path of the file
+ *
+ * @var string
+ * @since 1.0.0
+ */
+ public const PATH = __DIR__;
}
diff --git a/Admin/Routes/Web/Backend.php b/Admin/Routes/Web/Backend.php
index b714d75..409c886 100644
--- a/Admin/Routes/Web/Backend.php
+++ b/Admin/Routes/Web/Backend.php
@@ -1,31 +1,75 @@
[
+ '^.*/sales/analysis(\?.*|$)$' => [
[
- 'dest' => '\Modules\SalesAnalysis\Controller\BackendController:viewBackendDashboard',
- 'verb' => RouteVerb::GET,
+ 'dest' => '\Modules\SalesAnalysis\Controller\BackendController:viewDashboard',
+ 'verb' => RouteVerb::GET,
'permission' => [
- 'module' => BackendController::MODULE_NAME,
- 'type' => PermissionType::READ,
- 'state' => PermissionState::DASHBOARD,
+ 'module' => BackendController::NAME,
+ 'type' => PermissionType::CREATE,
+ 'state' => PermissionCategory::DASHBOARD,
],
],
],
- '^.*/sales/analysis/overview/dashboard.*$' => [
+ '^.*/sales/analysis/bill(\?.*|$)$' => [
[
- 'dest' => '\Modules\SalesAnalysis\Controller\BackendController:viewBackendOverviewDashboard',
- 'verb' => RouteVerb::GET,
+ 'dest' => '\Modules\SalesAnalysis\Controller\BackendController:viewBillAnalysis',
+ 'verb' => RouteVerb::GET,
'permission' => [
- 'module' => BackendController::MODULE_NAME,
- 'type' => PermissionType::READ,
- 'state' => PermissionState::DASHBOARD,
+ 'module' => BackendController::NAME,
+ 'type' => PermissionType::READ,
+ 'state' => PermissionCategory::DASHBOARD,
+ ],
+ ],
+ ],
+ '^.*/sales/analysis/rep(\?.*|$)$' => [
+ [
+ 'dest' => '\Modules\SalesAnalysis\Controller\BackendController:viewSalesRepAnalysis',
+ 'verb' => RouteVerb::GET,
+ 'permission' => [
+ 'module' => BackendController::NAME,
+ 'type' => PermissionType::READ,
+ 'state' => PermissionCategory::DASHBOARD,
+ ],
+ ],
+ ],
+ '^.*/sales/analysis/region(\?.*|$)$' => [
+ [
+ 'dest' => '\Modules\SalesAnalysis\Controller\BackendController:viewRegionAnalysis',
+ 'verb' => RouteVerb::GET,
+ 'permission' => [
+ 'module' => BackendController::NAME,
+ 'type' => PermissionType::READ,
+ 'state' => PermissionCategory::DASHBOARD,
+ ],
+ ],
+ ],
+ '^.*/sales/analysis/client(\?.*|$)$' => [
+ [
+ 'dest' => '\Modules\SalesAnalysis\Controller\BackendController:viewClientAnalysis',
+ 'verb' => RouteVerb::GET,
+ 'permission' => [
+ 'module' => BackendController::NAME,
+ 'type' => PermissionType::READ,
+ 'state' => PermissionCategory::DASHBOARD,
+ ],
+ ],
+ ],
+ '^.*/sales/analysis/item(\?.*|$)$' => [
+ [
+ 'dest' => '\Modules\SalesAnalysis\Controller\BackendController:viewItemSalesAnalysis',
+ 'verb' => RouteVerb::GET,
+ 'permission' => [
+ 'module' => BackendController::NAME,
+ 'type' => PermissionType::READ,
+ 'state' => PermissionCategory::DASHBOARD,
],
],
],
diff --git a/Admin/Status.php b/Admin/Status.php
index c13b96e..86b1012 100644
--- a/Admin/Status.php
+++ b/Admin/Status.php
@@ -8,7 +8,7 @@
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
- * @link https://orange-management.org
+ * @link https://jingga.app
*/
declare(strict_types=1);
@@ -21,9 +21,16 @@ use phpOMS\Module\StatusAbstract;
*
* @package Modules\SalesAnalysis\Admin
* @license OMS License 1.0
- * @link https://orange-management.org
+ * @link https://jingga.app
* @since 1.0.0
*/
final class Status extends StatusAbstract
{
+ /**
+ * Path of the file
+ *
+ * @var string
+ * @since 1.0.0
+ */
+ public const PATH = __DIR__;
}
diff --git a/Admin/Uninstaller.php b/Admin/Uninstaller.php
index bb54adb..be8c394 100644
--- a/Admin/Uninstaller.php
+++ b/Admin/Uninstaller.php
@@ -8,7 +8,7 @@
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
- * @link https://orange-management.org
+ * @link https://jingga.app
*/
declare(strict_types=1);
@@ -21,9 +21,16 @@ use phpOMS\Module\UninstallerAbstract;
*
* @package Modules\SalesAnalysis\Admin
* @license OMS License 1.0
- * @link https://orange-management.org
+ * @link https://jingga.app
* @since 1.0.0
*/
final class Uninstaller extends UninstallerAbstract
{
+ /**
+ * Path of the file
+ *
+ * @var string
+ * @since 1.0.0
+ */
+ public const PATH = __DIR__;
}
diff --git a/Admin/Updater.php b/Admin/Updater.php
index 546dac9..230887a 100644
--- a/Admin/Updater.php
+++ b/Admin/Updater.php
@@ -8,7 +8,7 @@
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
- * @link https://orange-management.org
+ * @link https://jingga.app
*/
declare(strict_types=1);
@@ -21,9 +21,16 @@ use phpOMS\Module\UpdaterAbstract;
*
* @package Modules\SalesAnalysis\Admin
* @license OMS License 1.0
- * @link https://orange-management.org
+ * @link https://jingga.app
* @since 1.0.0
*/
final class Updater extends UpdaterAbstract
{
+ /**
+ * Path of the file
+ *
+ * @var string
+ * @since 1.0.0
+ */
+ public const PATH = __DIR__;
}
diff --git a/Controller/BackendController.php b/Controller/BackendController.php
index 79477bf..7693eeb 100644
--- a/Controller/BackendController.php
+++ b/Controller/BackendController.php
@@ -8,15 +8,27 @@
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
- * @link https://orange-management.org
+ * @link https://jingga.app
*/
declare(strict_types=1);
namespace Modules\SalesAnalysis\Controller;
+use Modules\Organization\Models\UnitMapper;
+use Modules\SalesAnalysis\Models\ClientMapper;
+use Modules\SalesAnalysis\Models\GeneralMapper;
+use Modules\SalesAnalysis\Models\ItemMapper;
+use Modules\SalesAnalysis\Models\RegionMapper;
+use phpOMS\Asset\AssetType;
use phpOMS\Contract\RenderableInterface;
+use phpOMS\DataStorage\Database\Query\Builder;
+use phpOMS\Localization\ISO3166CharEnum;
+use phpOMS\Localization\ISO3166NameEnum;
+use phpOMS\Localization\ISO3166TwoEnum;
+use phpOMS\Localization\RegionEnum;
use phpOMS\Message\RequestAbstract;
use phpOMS\Message\ResponseAbstract;
+use phpOMS\Stdlib\Base\SmartDateTime;
use phpOMS\Views\View;
/**
@@ -24,28 +36,108 @@ use phpOMS\Views\View;
*
* @package Modules\SalesAnalysis
* @license OMS License 1.0
- * @link https://orange-management.org
+ * @link https://jingga.app
* @since 1.0.0
*/
final class BackendController extends Controller
{
/**
- * Routing end-point for application behaviour.
+ * Method which shows the sales dashboard
*
* @param RequestAbstract $request Request
* @param ResponseAbstract $response Response
- * @param mixed $data Generic data
+ * @param array $data Generic data
*
- * @return RenderableInterface
+ * @return RenderableInterface Response can be rendered
*
* @since 1.0.0
- * @codeCoverageIgnore
*/
- public function viewBackendDashboard(RequestAbstract $request, ResponseAbstract $response, $data = null) : RenderableInterface
+ public function viewDashboard(RequestAbstract $request, ResponseAbstract $response, array $data = []) : RenderableInterface
{
+ $head = $response->data['Content']->head;
+ $nonce = $this->app->appSettings->getOption('script-nonce');
+
+ $head->addAsset(AssetType::CSS, 'Resources/chartjs/chart.css');
+ $head->addAsset(AssetType::JSLATE, 'Resources/chartjs/chart.js', ['nonce' => $nonce]);
+ $head->addAsset(AssetType::JSLATE, 'Modules/SalesAnalysis/Controller/Controller.js', ['nonce' => $nonce, 'type' => 'module']);
+
$view = new View($this->app->l11nManager, $request, $response);
- $view->setTemplate('/Modules/SalesAnalysis/Theme/Backend/analysis-dashboard');
- $view->addData('nav', $this->app->moduleManager->get('Navigation')->createNavigationSplash(1005401001, $request, $response));
+ $view->setTemplate('/Modules/SalesAnalysis/Theme/Backend/analysis-overview-dashboard');
+ $view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1005401001, $request, $response);
+
+ // @todo: limit bill type (invoice/credit note) (customers only)
+ // @todo: limit bill status
+
+ $businessStart = 1;
+ $startOfYear = SmartDateTime::createFromDateTime(SmartDateTime::startOfYear($businessStart));
+ $startCurrent = $request->getDataDateTime('startcurrent') ?? clone $startOfYear;
+ $endCurrent = $request->getDataDateTime('endcurrent') ?? SmartDateTime::endOfMonth();
+ $endCurrentIndex = SmartDateTime::calculateMonthIndex((int) $endCurrent->format('m'), $businessStart);
+ $startComparison = $request->getDataDateTime('startcomparison') ?? SmartDateTime::createFromDateTime($startCurrent)->createModify(-1);
+ $endComparison = $request->getDataDateTime('endcomparison') ?? SmartDateTime::createFromDateTime(SmartDateTime::endOfYear($businessStart))->smartModify(-1);
+
+ $view->data['startCurrent'] = $startCurrent;
+ $view->data['endCurrent'] = $endCurrent;
+ $view->data['startComparison'] = $startComparison;
+ $view->data['endComparison'] = $endComparison;
+
+ [
+ $view->data['mtdA'],
+ $view->data['mtdPY'],
+ $view->data['ytdA'],
+ $view->data['ytdPY'],
+ $view->data['monthlySales']
+ ] = GeneralMapper::monthlySalesProfit(
+ $startCurrent,
+ $endCurrent,
+ $startComparison,
+ $endComparison,
+ $businessStart
+ );
+
+ $historyStart = $startOfYear->createModify(-9);
+ $view->data['annualSales'] = GeneralMapper::annualSalesProfit($historyStart, $endCurrent, $businessStart);
+
+ [
+ $view->data['mtdAItemAttribute'],
+ $view->data['mtdPYItemAttribute'],
+ $view->data['ytdAItemAttribute'],
+ $view->data['ytdPYItemAttribute']
+ ] = ItemMapper::mtdYtdItemAttribute(
+ $startCurrent,
+ $endCurrent,
+ $startComparison,
+ $endComparison,
+ $businessStart,
+ $request->header->l11n->language
+ );
+
+ [
+ $view->data['mtdAClientAttribute'],
+ $view->data['mtdPYClientAttribute'],
+ $view->data['ytdAClientAttribute'],
+ $view->data['ytdPYClientAttribute']
+ ] = ClientMapper::mtdYtdClientAttribute(
+ $startCurrent,
+ $endCurrent,
+ $startComparison,
+ $endComparison,
+ $businessStart,
+ $request->header->l11n->language
+ );
+
+ [
+ $view->data['mtdAClientCountry'],
+ $view->data['mtdPYClientCountry'],
+ $view->data['ytdAClientCountry'],
+ $view->data['ytdPYClientCountry']
+ ] = RegionMapper::mtdYtdCountry(
+ $startCurrent,
+ $endCurrent,
+ $startComparison,
+ $endComparison,
+ $businessStart
+ );
return $view;
}
@@ -55,18 +147,528 @@ final class BackendController extends Controller
*
* @param RequestAbstract $request Request
* @param ResponseAbstract $response Response
- * @param mixed $data Generic data
+ * @param array $data Generic data
*
* @return RenderableInterface
*
* @since 1.0.0
* @codeCoverageIgnore
*/
- public function viewBackendOverviewDashboard(RequestAbstract $request, ResponseAbstract $response, $data = null) : RenderableInterface
+ public function viewRegionAnalysis(RequestAbstract $request, ResponseAbstract $response, array $data = []) : RenderableInterface
{
+ $head = $response->data['Content']->head;
+ $nonce = $this->app->appSettings->getOption('script-nonce');
+
+ $head->addAsset(AssetType::CSS, 'Resources/chartjs/chart.css');
+ $head->addAsset(AssetType::JSLATE, 'Resources/chartjs/chart.js', ['nonce' => $nonce]);
+ $head->addAsset(AssetType::JSLATE, 'Resources/chartjs/plugins/chartjs-chart-geo.js', ['nonce' => $nonce]);
+ $head->addAsset(AssetType::JSLATE, 'Modules/SalesAnalysis/Controller/Controller.js', ['nonce' => $nonce, 'type' => 'module']);
+
$view = new View($this->app->l11nManager, $request, $response);
- $view->setTemplate('/Modules/SalesAnalysis/Theme/Backend/analysis-overview-dashboard');
- $view->addData('nav', $this->app->moduleManager->get('Navigation')->createNavigationMid(1005401001, $request, $response));
+ $view->setTemplate('/Modules/SalesAnalysis/Theme/Backend/analysis-region');
+ $view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1005401001, $request, $response);
+
+ $businessStart = 1;
+ $startOfYear = SmartDateTime::createFromDateTime(SmartDateTime::startOfYear($businessStart));
+ $startCurrent = $request->getDataDateTime('startcurrent') ?? clone $startOfYear;
+ $endCurrent = $request->getDataDateTime('endcurrent') ?? SmartDateTime::endOfMonth();
+ $endCurrentIndex = SmartDateTime::calculateMonthIndex((int) $endCurrent->format('m'), $businessStart);
+ $startComparison = $request->getDataDateTime('startcomparison') ?? SmartDateTime::createFromDateTime($startCurrent)->createModify(-1);
+ $endComparison = $request->getDataDateTime('endcomparison') ?? SmartDateTime::createFromDateTime(SmartDateTime::endOfYear($businessStart))->smartModify(-1);
+ $historyStart = $startOfYear->createModify(-9);
+
+ $view->data['startCurrent'] = $startCurrent;
+ $view->data['endCurrent'] = $endCurrent;
+ $view->data['startComparison'] = $startComparison;
+ $view->data['endComparison'] = $endComparison;
+ $view->data['historyStart'] = $historyStart;
+
+ $domestic = UnitMapper::get()
+ ->with('mainAddress')
+ ->where('id', $this->app->unitId)
+ ->execute();
+
+ $view->data['domestic'] = $domestic->mainAddress->country === ISO3166TwoEnum::_XXX ? 'US' : $domestic->mainAddress->country;
+
+ [
+ $mtdCurrent,
+ $ytdCurrent,
+ $monthlyCurrent
+ ] = RegionMapper::monthlySalesProfit(
+ $startCurrent,
+ $endCurrent,
+ $businessStart
+ );
+
+ $view->data['monthlyDomesticExportCurrent'] = RegionMapper::countryIntervalToRegion(
+ $monthlyCurrent,
+ [$view->data['domestic']],
+ ['net_sales', 'net_profit']
+ );
+
+ [
+ $mtdPY,
+ $ytdPY,
+ $monthlyPY
+ ] = RegionMapper::monthlySalesProfit(
+ $startComparison,
+ $endComparison,
+ $businessStart
+ );
+
+ $view->data['monthlyDomesticExportPY'] = RegionMapper::countryIntervalToRegion(
+ $monthlyPY,
+ [$view->data['domestic']],
+ ['net_sales', 'net_profit']
+ );
+
+ [
+ $view->data['mtdPYClientCountry'],
+ $view->data['mtdAClientCountry'],
+ $view->data['ytdPYClientCountry'],
+ $view->data['ytdAClientCountry'],
+ ] = RegionMapper::mtdYtdCountry(
+ $startCurrent,
+ $endCurrent,
+ $startComparison,
+ $endComparison,
+ $businessStart
+ );
+
+ $annualCountrySales = RegionMapper::annualSalesProfitCountry(clone $historyStart, $endCurrent);
+
+ $view->data['ytdADomesticExport'] = RegionMapper::countryToRegion(
+ $view->data['ytdAClientCountry'],
+ [$view->data['domestic']],
+ ['net_sales', 'net_profit']
+ );
+
+ $view->data['annualDomesticExport'] = RegionMapper::countryIntervalToRegion(
+ $annualCountrySales,
+ [$view->data['domestic']],
+ ['net_sales', 'net_profit']
+ );
+
+ [
+ $view->data['mtdPYClientCountryCount'],
+ $view->data['mtdAClientCountryCount'],
+ $view->data['ytdPYClientCountryCount'],
+ $view->data['ytdAClientCountryCount'],
+ ] = RegionMapper::mtdYtdClientCountry(
+ $startCurrent,
+ $endCurrent,
+ $startComparison,
+ $endComparison,
+ $businessStart
+ );
+
+ $annualCountryCount = RegionMapper::annualCustomerCountry(clone $historyStart, $endCurrent);
+
+ $view->data['ytdADomesticExportCount'] = RegionMapper::countryToRegion(
+ $view->data['ytdAClientCountryCount'],
+ [$view->data['domestic']],
+ ['client_count']
+ );
+
+ $view->data['annualDomesticExportCount'] = RegionMapper::countryIntervalToRegion(
+ $annualCountryCount,
+ [$view->data['domestic']],
+ ['client_count']
+ );
+
+ ///
+
+ $view->data['ytdAContinent'] = RegionMapper::countryToRegion(
+ $view->data['ytdAClientCountry'],
+ ISO3166NameEnum::getSubregions('continents'),
+ ['net_sales', 'net_profit']
+ );
+
+ $view->data['annualContinent'] = RegionMapper::countryIntervalToRegion(
+ $annualCountrySales,
+ ISO3166NameEnum::getSubregions('continents'),
+ ['net_sales', 'net_profit']
+ );
+
+ $view->data['ytdAContinentCount'] = RegionMapper::countryToRegion(
+ $view->data['ytdAClientCountryCount'],
+ ISO3166NameEnum::getSubregions('continents'),
+ ['client_count']
+ );
+
+ $view->data['annualContinentCount'] = RegionMapper::countryIntervalToRegion(
+ $annualCountryCount,
+ ISO3166NameEnum::getSubregions('continents'),
+ ['client_count']
+ );
+
+ ///
+
+ $view->data['ytdARegions'] = RegionMapper::countryToRegion(
+ $view->data['ytdAClientCountry'],
+ RegionEnum::getConstants(),
+ ['net_sales', 'net_profit']
+ );
+
+ $view->data['ytdPYRegions'] = RegionMapper::countryToRegion(
+ $view->data['ytdPYClientCountry'],
+ RegionEnum::getConstants(),
+ ['net_sales', 'net_profit']
+ );
+
+ $view->data['mtdARegions'] = RegionMapper::countryToRegion(
+ $view->data['mtdAClientCountry'],
+ RegionEnum::getConstants(),
+ ['net_sales', 'net_profit']
+ );
+
+ $view->data['mtdPYRegions'] = RegionMapper::countryToRegion(
+ $view->data['mtdPYClientCountry'],
+ RegionEnum::getConstants(),
+ ['net_sales', 'net_profit']
+ );
+
+ return $view;
+ }
+
+ /**
+ * Routing end-point for application behaviour.
+ *
+ * @param RequestAbstract $request Request
+ * @param ResponseAbstract $response Response
+ * @param array $data Generic data
+ *
+ * @return RenderableInterface
+ *
+ * @since 1.0.0
+ * @codeCoverageIgnore
+ */
+ public function viewBillAnalysis(RequestAbstract $request, ResponseAbstract $response, array $data = []) : RenderableInterface
+ {
+ $head = $response->data['Content']->head;
+ $nonce = $this->app->appSettings->getOption('script-nonce');
+
+ $head->addAsset(AssetType::CSS, 'Resources/chartjs/chart.css');
+ $head->addAsset(AssetType::JSLATE, 'Resources/chartjs/chart.js', ['nonce' => $nonce]);
+ $head->addAsset(AssetType::JSLATE, 'Modules/SalesAnalysis/Controller/Controller.js', ['nonce' => $nonce, 'type' => 'module']);
+
+ $view = new View($this->app->l11nManager, $request, $response);
+ $view->setTemplate('/Modules/SalesAnalysis/Theme/Backend/analysis-bill');
+ $view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1005401001, $request, $response);
+
+ return $view;
+ }
+
+ /**
+ * Routing end-point for application behaviour.
+ *
+ * @param RequestAbstract $request Request
+ * @param ResponseAbstract $response Response
+ * @param array $data Generic data
+ *
+ * @return RenderableInterface
+ *
+ * @since 1.0.0
+ * @codeCoverageIgnore
+ */
+ public function viewSalesRepAnalysis(RequestAbstract $request, ResponseAbstract $response, array $data = []) : RenderableInterface
+ {
+ $head = $response->data['Content']->head;
+ $nonce = $this->app->appSettings->getOption('script-nonce');
+
+ $head->addAsset(AssetType::CSS, 'Resources/chartjs/chart.css');
+ $head->addAsset(AssetType::JSLATE, 'Resources/chartjs/chart.js', ['nonce' => $nonce]);
+ $head->addAsset(AssetType::JSLATE, 'Modules/SalesAnalysis/Controller/Controller.js', ['nonce' => $nonce, 'type' => 'module']);
+
+ $view = new View($this->app->l11nManager, $request, $response);
+ $view->setTemplate('/Modules/SalesAnalysis/Theme/Backend/analysis-rep');
+ $view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1005401001, $request, $response);
+
+ /////
+ $currentCustomerRegion = [
+ 'Europe' => (int) (\mt_rand(200, 400) / 4),
+ 'America' => (int) (\mt_rand(200, 400) / 4),
+ 'Asia' => (int) (\mt_rand(200, 400) / 4),
+ 'Africa' => (int) (\mt_rand(200, 400) / 4),
+ 'CIS' => (int) (\mt_rand(200, 400) / 4),
+ 'Other' => (int) (\mt_rand(200, 400) / 4),
+ ];
+
+ $view->data['currentCustomerRegion'] = $currentCustomerRegion;
+
+ $annualCustomerRegion = [];
+ for ($i = 1; $i < 11; ++$i) {
+ $annualCustomerRegion[] = [
+ 'year' => 2020 - 10 + $i,
+ 'Europe' => $a = (int) (\mt_rand(200, 400) / 4),
+ 'America' => $b = (int) (\mt_rand(200, 400) / 4),
+ 'Asia' => $c = (int) (\mt_rand(200, 400) / 4),
+ 'Africa' => $d = (int) (\mt_rand(200, 400) / 4),
+ 'CIS' => $e = (int) (\mt_rand(200, 400) / 4),
+ 'Other' => $f = (int) (\mt_rand(200, 400) / 4),
+ 'Total' => $a + $b + $c + $d + $e + $f,
+ ];
+ }
+
+ $view->data['annualCustomerRegion'] = $annualCustomerRegion;
+
+ /////
+ $currentCustomersRep = [];
+ for ($i = 1; $i < 13; ++$i) {
+ $currentCustomersRep['Rep ' . $i] = [
+ 'customers' => (int) (\mt_rand(200, 400) / 12),
+ ];
+ }
+
+ \uasort($currentCustomersRep, function($a, $b) {
+ return $b['customers'] <=> $a['customers'];
+ });
+
+ $view->data['currentCustomersRep'] = $currentCustomersRep;
+
+ $annualCustomersRep = [];
+ for ($i = 1; $i < 13; ++$i) {
+ $annualCustomersRep['Rep ' . $i] = [];
+
+ for ($j = 1; $j < 11; ++$j) {
+ $annualCustomersRep['Rep ' . $i][] = [
+ 'customers' => (int) (\mt_rand(200, 400) / 12),
+ 'year' => 2020 - 10 + $j,
+ ];
+ }
+ }
+
+ $view->data['annualCustomersRep'] = $annualCustomersRep;
+
+ return $view;
+ }
+
+ /**
+ * Routing end-point for application behaviour.
+ *
+ * @param RequestAbstract $request Request
+ * @param ResponseAbstract $response Response
+ * @param array $data Generic data
+ *
+ * @return RenderableInterface
+ *
+ * @since 1.0.0
+ * @codeCoverageIgnore
+ */
+ public function viewClientAnalysis(RequestAbstract $request, ResponseAbstract $response, array $data = []) : RenderableInterface
+ {
+ $head = $response->data['Content']->head;
+ $nonce = $this->app->appSettings->getOption('script-nonce');
+
+ $head->addAsset(AssetType::CSS, 'Resources/chartjs/chart.css');
+ $head->addAsset(AssetType::JSLATE, 'Resources/chartjs/chart.js', ['nonce' => $nonce]);
+ $head->addAsset(AssetType::JSLATE, 'Resources/chartjs/plugins/chartjs-chart-geo.js', ['nonce' => $nonce]);
+ $head->addAsset(AssetType::JSLATE, 'Modules/SalesAnalysis/Controller/Controller.js', ['nonce' => $nonce, 'type' => 'module']);
+
+ $view = new View($this->app->l11nManager, $request, $response);
+ $view->setTemplate('/Modules/SalesAnalysis/Theme/Backend/analysis-client');
+ $view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1005401001, $request, $response);
+
+ $monthlySalesCosts = [];
+ for ($i = 1; $i < 13; ++$i) {
+ $monthlySalesCosts[] = [
+ 'net_sales' => $sales = \mt_rand(1200000000, 2000000000),
+ 'net_costs' => (int) ($sales * \mt_rand(25, 55) / 100),
+ 'year' => 2020,
+ 'month' => $i,
+ ];
+ }
+
+ $view->data['monthlySalesCosts'] = $monthlySalesCosts;
+
+ /////
+ $monthlySalesCustomer = [];
+ for ($i = 1; $i < 13; ++$i) {
+ $monthlySalesCustomer[] = [
+ 'net_sales' => $sales = \mt_rand(1200000000, 2000000000),
+ 'customers' => \mt_rand(200, 400),
+ 'year' => 2020,
+ 'month' => $i,
+ ];
+ }
+
+ $view->data['monthlySalesCustomer'] = $monthlySalesCustomer;
+
+ $annualSalesCustomer = [];
+ for ($i = 1; $i < 11; ++$i) {
+ $annualSalesCustomer[] = [
+ 'net_sales' => $sales = \mt_rand(1200000000, 2000000000) * 12,
+ 'customers' => \mt_rand(200, 400) * 6,
+ 'year' => 2020 - 10 + $i,
+ ];
+ }
+
+ $view->data['annualSalesCustomer'] = $annualSalesCustomer;
+
+ /////
+ $monthlyCustomerRetention = [];
+ for ($i = 1; $i < 10; ++$i) {
+ $monthlyCustomerRetention[] = [
+ 'customers' => \mt_rand(200, 400),
+ 'year' => \date('y') - 9 + $i,
+ ];
+ }
+
+ $view->data['monthlyCustomerRetention'] = $monthlyCustomerRetention;
+
+ /////
+ $currentCustomerRegion = [
+ 'Europe' => (int) (\mt_rand(200, 400) / 4),
+ 'America' => (int) (\mt_rand(200, 400) / 4),
+ 'Asia' => (int) (\mt_rand(200, 400) / 4),
+ 'Africa' => (int) (\mt_rand(200, 400) / 4),
+ 'CIS' => (int) (\mt_rand(200, 400) / 4),
+ 'Other' => (int) (\mt_rand(200, 400) / 4),
+ ];
+
+ $view->data['currentCustomerRegion'] = $currentCustomerRegion;
+
+ $annualCustomerRegion = [];
+ for ($i = 1; $i < 11; ++$i) {
+ $annualCustomerRegion[] = [
+ 'year' => 2020 - 10 + $i,
+ 'Europe' => $a = (int) (\mt_rand(200, 400) / 4),
+ 'America' => $b = (int) (\mt_rand(200, 400) / 4),
+ 'Asia' => $c = (int) (\mt_rand(200, 400) / 4),
+ 'Africa' => $d = (int) (\mt_rand(200, 400) / 4),
+ 'CIS' => $e = (int) (\mt_rand(200, 400) / 4),
+ 'Other' => $f = (int) (\mt_rand(200, 400) / 4),
+ 'Total' => $a + $b + $c + $d + $e + $f,
+ ];
+ }
+
+ $view->data['annualCustomerRegion'] = $annualCustomerRegion;
+
+ /////
+ $currentCustomersRep = [];
+ for ($i = 1; $i < 13; ++$i) {
+ $currentCustomersRep['Rep ' . $i] = [
+ 'customers' => (int) (\mt_rand(200, 400) / 12),
+ ];
+ }
+
+ \uasort($currentCustomersRep, function($a, $b) {
+ return $b['customers'] <=> $a['customers'];
+ });
+
+ $view->data['currentCustomersRep'] = $currentCustomersRep;
+
+ $annualCustomersRep = [];
+ for ($i = 1; $i < 13; ++$i) {
+ $annualCustomersRep['Rep ' . $i] = [];
+
+ for ($j = 1; $j < 11; ++$j) {
+ $annualCustomersRep['Rep ' . $i][] = [
+ 'customers' => (int) (\mt_rand(200, 400) / 12),
+ 'year' => 2020 - 10 + $j,
+ ];
+ }
+ }
+
+ $view->data['annualCustomersRep'] = $annualCustomersRep;
+
+ /////
+ $currentCustomersCountry = [];
+ for ($i = 1; $i < 51; ++$i) {
+ $country = (string) ISO3166NameEnum::getRandom();
+ $currentCustomersCountry[\substr($country, 0, 20)] = [
+ 'customers' => (int) (\mt_rand(200, 400) / 12),
+ ];
+ }
+
+ \uasort($currentCustomersCountry, function($a, $b) {
+ return $b['customers'] <=> $a['customers'];
+ });
+
+ $view->data['currentCustomersCountry'] = $currentCustomersCountry;
+
+ $annualCustomersCountry = [];
+ for ($i = 1; $i < 51; ++$i) {
+ $countryCode = ISO3166CharEnum::getRandom();
+ $countryName = (string) ISO3166NameEnum::getByName('_' . $countryCode);
+ $annualCustomersCountry[\substr($countryName, 0, 20)] = [];
+
+ for ($j = 1; $j < 11; ++$j) {
+ $annualCustomersCountry[\substr($countryName, 0, 20)][] = [
+ 'customers' => (int) (\mt_rand(200, 400) / 12),
+ 'year' => 2020 - 10 + $j,
+ 'name' => $countryName,
+ 'code' => $countryCode,
+ ];
+ }
+ }
+
+ $view->data['annualCustomersCountry'] = $annualCustomersCountry;
+
+ /////
+ $customerGroups = [];
+ for ($i = 1; $i < 7; ++$i) {
+ $customerGroups['Group ' . $i] = [
+ 'customers' => (int) (\mt_rand(200, 400) / 12),
+ ];
+ }
+
+ $view->data['customerGroups'] = $customerGroups;
+
+ return $view;
+ }
+
+ /**
+ * Routing end-point for application behaviour.
+ *
+ * @param RequestAbstract $request Request
+ * @param ResponseAbstract $response Response
+ * @param array $data Generic data
+ *
+ * @return RenderableInterface
+ *
+ * @since 1.0.0
+ * @codeCoverageIgnore
+ */
+ public function viewItemSalesAnalysis(RequestAbstract $request, ResponseAbstract $response, array $data = []) : RenderableInterface
+ {
+ $head = $response->data['Content']->head;
+ $nonce = $this->app->appSettings->getOption('script-nonce');
+
+ $head->addAsset(AssetType::CSS, 'Resources/chartjs/chart.css');
+ $head->addAsset(AssetType::JSLATE, 'Resources/chartjs/chart.js', ['nonce' => $nonce]);
+ $head->addAsset(AssetType::JSLATE, 'Modules/SalesAnalysis/Controller/Controller.js', ['nonce' => $nonce, 'type' => 'module']);
+
+ $view = new View($this->app->l11nManager, $request, $response);
+ $view->setTemplate('/Modules/SalesAnalysis/Theme/Backend/analysis-item');
+ $view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1005401001, $request, $response);
+
+ $businessStart = 1;
+ $startOfYear = SmartDateTime::createFromDateTime(SmartDateTime::startOfYear($businessStart));
+ $startCurrent = $request->getDataDateTime('startcurrent') ?? clone $startOfYear;
+ $endCurrent = $request->getDataDateTime('endcurrent') ?? SmartDateTime::endOfMonth();
+ $endCurrentIndex = SmartDateTime::calculateMonthIndex((int) $endCurrent->format('m'), $businessStart);
+ $startComparison = $request->getDataDateTime('startcomparison') ?? SmartDateTime::createFromDateTime($startCurrent)->createModify(-1);
+ $endComparison = $request->getDataDateTime('endcomparison') ?? SmartDateTime::createFromDateTime(SmartDateTime::endOfYear($businessStart))->smartModify(-1);
+
+ $view->data['startCurrent'] = $startCurrent;
+ $view->data['endCurrent'] = $endCurrent;
+ $view->data['startComparison'] = $startComparison;
+ $view->data['endComparison'] = $endComparison;
+
+ [
+ $view->data['mtdAItemAttribute'],
+ $view->data['mtdPYItemAttribute'],
+ $view->data['ytdAItemAttribute'],
+ $view->data['ytdPYItemAttribute']
+ ] = ItemMapper::mtdYtdItemAttribute(
+ $startCurrent,
+ $endCurrent,
+ $startComparison,
+ $endComparison,
+ $businessStart,
+ $request->header->l11n->language
+ );
return $view;
}
diff --git a/Controller/Controller.js b/Controller/Controller.js
new file mode 100644
index 0000000..8eccbaf
--- /dev/null
+++ b/Controller/Controller.js
@@ -0,0 +1,73 @@
+import { jsOMS } from '../../../jsOMS/Utils/oLib.js';
+import { Autoloader } from '../../../jsOMS/Autoloader.js';
+
+Autoloader.defineNamespace('omsApp.Modules');
+
+omsApp.Modules.SalesAnalysis = class {
+ /**
+ * @constructor
+ *
+ * @since 1.0.0
+ */
+ constructor (app)
+ {
+ this.app = app;
+ };
+
+ bind (id)
+ {
+ const charts = typeof id === 'undefined' ? document.getElementsByTagName('canvas') : [document.getElementById(id)];
+ let length = charts.length;
+
+ for (let i = 0; i < length; ++i) {
+ if (charts[i].getAttribute('data-chart') === null
+ && charts[i].getAttribute('data-chart') !== 'undefined'
+ ) {
+ continue;
+ }
+
+ this.bindChart(charts[i]);
+ }
+ };
+
+ bindChart (chart)
+ {
+ if (typeof chart === 'undefined' || !chart) {
+ jsOMS.Log.Logger.instance.error('Invalid chart: ' + chart, 'ClientManagement');
+
+ return;
+ }
+
+ const self = this;
+ const data = JSON.parse(chart.getAttribute('data-chart'));
+
+ if (data.type === 'choropleth') {
+ const parts = data.mapurl.split('/');
+ const fileName = parts[parts.length - 1];
+ const mapName = fileName.replace('.topo.json', '');
+
+ fetch(data.mapurl).then((r) => r.json()).then((d) => {
+ const countries = ChartGeo.topojson.feature(d, d.objects[mapName]).features;
+
+ data.data.labels = countries.map((c) => c.properties.name);
+
+ const vals = {};
+ const length = data.data.datasets[0].data.length;
+ for (let i = 0; i < length; ++i) {
+ vals[data.data.datasets[0].data[i].id] = data.data.datasets[0].data[i].value;
+ }
+
+ data.data.datasets[0].data = countries.map((c) => (
+ {feature: c, value: (vals.hasOwnProperty(c.id) ? vals[c.id] : null)}
+ ));
+
+ const myChart = new Chart(chart.getContext('2d'), data);
+ });
+ } else {
+ const myChart = new Chart(chart.getContext('2d'), data);
+ }
+
+ };
+};
+
+window.omsApp.moduleManager.get('SalesAnalysis').bind();
diff --git a/Controller/Controller.php b/Controller/Controller.php
index 369cfa1..c18f36b 100644
--- a/Controller/Controller.php
+++ b/Controller/Controller.php
@@ -8,24 +8,23 @@
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
- * @link https://orange-management.org
+ * @link https://jingga.app
*/
declare(strict_types=1);
namespace Modules\SalesAnalysis\Controller;
use phpOMS\Module\ModuleAbstract;
-use phpOMS\Module\WebInterface;
/**
* Sales class.
*
* @package Modules\SalesAnalysis
* @license OMS License 1.0
- * @link https://orange-management.org
+ * @link https://jingga.app
* @since 1.0.0
*/
-class Controller extends ModuleAbstract implements WebInterface
+class Controller extends ModuleAbstract
{
/**
* Module path.
@@ -33,7 +32,7 @@ class Controller extends ModuleAbstract implements WebInterface
* @var string
* @since 1.0.0
*/
- public const MODULE_PATH = __DIR__ . '/../';
+ public const PATH = __DIR__ . '/../';
/**
* Module version.
@@ -65,7 +64,7 @@ class Controller extends ModuleAbstract implements WebInterface
* @var string[]
* @since 1.0.0
*/
- protected static array $providing = [];
+ public static array $providing = [];
/**
* Dependencies.
@@ -73,5 +72,5 @@ class Controller extends ModuleAbstract implements WebInterface
* @var string[]
* @since 1.0.0
*/
- protected static array $dependencies = [];
+ public static array $dependencies = [];
}
diff --git a/Models/ClientMapper.php b/Models/ClientMapper.php
new file mode 100644
index 0000000..23abc66
--- /dev/null
+++ b/Models/ClientMapper.php
@@ -0,0 +1,157 @@
+format('m'), $businessStart);
+
+ // @todo: this query doesn't return clients that have not segment etc. defined.
+ $query = new Builder(self::$db);
+ $query->raw(
+ 'SELECT
+ clientmgmt_attr_type_name,
+ clientmgmt_attr_type_l11n_title,
+ clientmgmt_attr_value_id,
+ clientmgmt_attr_value_l11n_title,
+ YEAR(billing_bill_performance_date) as salesyear,
+ MONTH(billing_bill_performance_date) as salesmonth,
+ SUM(billing_bill_netsales) as netsales,
+ SUM(billing_bill_netprofit) as netprofit
+ FROM billing_bill
+ LEFT JOIN clientmgmt_client
+ ON clientmgmt_client_id = billing_bill_client
+ LEFT JOIN clientmgmt_client_attr
+ ON clientmgmt_client_id = clientmgmt_client_attr_client
+ LEFT JOIN clientmgmt_attr_type
+ ON clientmgmt_client_attr_type = clientmgmt_attr_type_id
+ LEFT JOIN clientmgmt_attr_type_l11n
+ ON clientmgmt_attr_type_id = clientmgmt_attr_type_l11n_type AND clientmgmt_attr_type_l11n_lang = \'' . $language . '\'
+ LEFT JOIN clientmgmt_attr_value
+ ON clientmgmt_client_attr_value = clientmgmt_attr_value_id
+ LEFT JOIN clientmgmt_attr_value_l11n
+ ON clientmgmt_attr_value_id = clientmgmt_attr_value_l11n_value AND clientmgmt_attr_value_l11n_lang = \'' . $language . '\'
+ WHERE
+ billing_bill_performance_date >= \'' . $startComparison->format('Y-m-d') . '\'
+ AND billing_bill_performance_date <= \'' . $endCurrent->format('Y-m-d') . '\'
+ AND clientmgmt_attr_type_name IN (\'segment\', \'section\', \'client_group\', \'client_type\')
+ GROUP BY
+ clientmgmt_attr_type_name,
+ clientmgmt_attr_type_l11n_title,
+ clientmgmt_attr_value_id,
+ clientmgmt_attr_value_l11n_title,
+ YEAR(billing_bill_performance_date),
+ MONTH(billing_bill_performance_date)
+ ORDER BY
+ YEAR(billing_bill_performance_date) ASC,
+ MONTH(billing_bill_performance_date) ASC'
+ );
+
+ $results = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
+
+ $oldIndex = 1;
+ $period = 1;
+
+ $mtdAClientAttribute = [];
+ $mtdPYClientAttribute = [];
+
+ $ytdAClientAttribute = [];
+ $ytdPYClientAttribute = [];
+
+ foreach ($results as $result) {
+ $monthIndex = SmartDateTime::calculateMonthIndex((int) $result['salesmonth'], $businessStart);
+ if ($monthIndex < $oldIndex) {
+ $oldIndex = $monthIndex;
+
+ ++$period;
+ }
+
+ if ($period > 2) {
+ break;
+ }
+
+ $oldIndex = $monthIndex;
+
+ // indexed according to the fiscal year
+ $temp = [
+ 'net_sales' => (int) $result['netsales'],
+ 'net_profit' => (int) $result['netprofit'],
+ ];
+
+ if (($temp['net_sales'] === 0 && $temp['net_profit'] === 0)) {
+ continue;
+ }
+
+ if ($monthIndex === $endCurrentIndex) {
+ if ($period === 1) {
+ $mtdPYClientAttribute[$result['clientmgmt_attr_type_name']] = $temp;
+ } else {
+ $mtdAClientAttribute[$result['clientmgmt_attr_type_name']] = $temp;
+ }
+ }
+
+ if ($monthIndex <= $endCurrentIndex) {
+ if (!isset($ytdPYClientAttribute[$result['clientmgmt_attr_type_name']])) {
+ $ytdPYClientAttribute[$result['clientmgmt_attr_type_name']] = [
+ 'net_sales' => 0,
+ 'net_profit' => 0,
+ 'value_l11n' => $result['clientmgmt_attr_value_l11n_title'],
+ ];
+
+ $ytdAClientAttribute[$result['clientmgmt_attr_type_name']] = [
+ 'net_sales' => 0,
+ 'net_profit' => 0,
+ 'value_l11n' => $result['clientmgmt_attr_value_l11n_title'],
+ ];
+ }
+
+ if ($period === 1) {
+ $ytdPYClientAttribute[$result['clientmgmt_attr_type_name']]['net_sales'] += $temp['net_sales'];
+ $ytdPYClientAttribute[$result['clientmgmt_attr_type_name']]['net_profit'] += $temp['net_profit'];
+ } else {
+ $ytdAClientAttribute[$result['clientmgmt_attr_type_name']]['net_sales'] += $temp['net_sales'];
+ $ytdAClientAttribute[$result['clientmgmt_attr_type_name']]['net_profit'] += $temp['net_profit'];
+ }
+ }
+ }
+
+ return [
+ $mtdAClientAttribute,
+ $mtdPYClientAttribute,
+ $ytdAClientAttribute,
+ $ytdPYClientAttribute,
+ ];
+ }
+}
\ No newline at end of file
diff --git a/Models/GeneralMapper.php b/Models/GeneralMapper.php
new file mode 100644
index 0000000..afdd720
--- /dev/null
+++ b/Models/GeneralMapper.php
@@ -0,0 +1,193 @@
+format('m'), $businessStart);
+
+ $query = new Builder(self::$db);
+ $query->raw(
+ 'SELECT
+ YEAR(billing_bill_performance_date) as salesyear,
+ MONTH(billing_bill_performance_date) as salesmonth,
+ SUM(billing_bill_netsales) as netsales,
+ SUM(billing_bill_netprofit) as netprofit
+ FROM billing_bill
+ WHERE
+ billing_bill_performance_date >= \'' . $startComparison->format('Y-m-d') . '\'
+ AND billing_bill_performance_date <= \'' . $endCurrent->format('Y-m-d') . '\'
+ GROUP BY
+ YEAR(billing_bill_performance_date),
+ MONTH(billing_bill_performance_date)
+ ORDER BY
+ YEAR(billing_bill_performance_date) ASC,
+ MONTH(billing_bill_performance_date) ASC'
+ );
+
+ $results = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
+
+ $oldIndex = 1;
+ $period = 1;
+
+ $monthlySales = [];
+ for ($i = 1; $i < 3; ++$i) {
+ $monthlySales[$i] = \array_fill(0, 12, [
+ 'net_sales' => null,
+ 'net_profit' => null,
+ ]);
+ }
+
+ $mtdA = ['net_sales' => 0, 'net_profit' => 0];
+ $mtdPY = ['net_sales' => 0, 'net_profit' => 0];
+
+ $ytdA = ['net_sales' => 0, 'net_profit' => 0];
+ $ytdPY = ['net_sales' => 0, 'net_profit' => 0];
+
+ foreach ($results as $result) {
+ $monthIndex = SmartDateTime::calculateMonthIndex((int) $result['salesmonth'], $businessStart);
+ if ($monthIndex < $oldIndex) {
+ $oldIndex = $monthIndex;
+
+ ++$period;
+ }
+
+ if ($period > 2) {
+ break;
+ }
+
+ $oldIndex = $monthIndex;
+
+ // indexed according to the fiscal year
+ $monthlySales[$period][$monthIndex - 1] = [
+ 'net_sales' => (int) $result['netsales'],
+ 'net_profit' => (int) $result['netprofit'],
+ ];
+
+ if ($monthIndex === $endCurrentIndex) {
+ if ($period === 1) {
+ $mtdPY = $monthlySales[$period][$monthIndex - 1];
+ } else {
+ $mtdA = $monthlySales[$period][$monthIndex - 1];
+ }
+ }
+
+ if ($monthIndex <= $endCurrentIndex) {
+ if ($period === 1) {
+ $ytdPY['net_sales'] += $monthlySales[$period][$monthIndex - 1]['net_sales'];
+ $ytdPY['net_profit'] += $monthlySales[$period][$monthIndex - 1]['net_profit'];
+ } else {
+ $ytdA['net_sales'] += $monthlySales[$period][$monthIndex - 1]['net_sales'];
+ $ytdA['net_profit'] += $monthlySales[$period][$monthIndex - 1]['net_profit'];
+ }
+ }
+ }
+
+ return [
+ $mtdA, $mtdPY,
+ $ytdA, $ytdPY,
+ $monthlySales
+ ];
+ }
+
+ public static function annualSalesProfit(
+ SmartDateTime $historyStart,
+ \DateTime $endCurrent,
+ int $businessStart = 1
+ ) : array {
+
+ $query = new Builder(self::$db);
+ $query->raw(
+ 'SELECT
+ YEAR(billing_bill_performance_date) as salesyear,
+ MONTH(billing_bill_performance_date) as salesmonth,
+ SUM(billing_bill_netsales) as netsales,
+ SUM(billing_bill_netprofit) as netprofit
+ FROM billing_bill
+ WHERE
+ billing_bill_performance_date >= \'' . $historyStart->format('Y-m-d') . '\'
+ AND billing_bill_performance_date <= \'' . $endCurrent->format('Y-m-d') . '\'
+ GROUP BY
+ YEAR(billing_bill_performance_date),
+ MONTH(billing_bill_performance_date)
+ ORDER BY
+ YEAR(billing_bill_performance_date) ASC,
+ MONTH(billing_bill_performance_date) ASC'
+ );
+
+ $results = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
+
+ $annualSales = [];
+ for ($i = 1; $i < 11; ++$i) {
+ $annualSales[$i] = [
+ 'net_sales' => null,
+ 'net_profit' => null,
+ 'year' => $historyStart->format('Y'),
+ ];
+
+ $historyStart->smartModify(1);
+ }
+
+ $historyStart->smartModify(-10);
+
+ $oldIndex = 1;
+ // @todo: this calculation doesn't consider the start of the fiscal year
+ $period = ((((int) $results[0]['salesyear']) - ((int) $historyStart->format('Y'))) * 12
+ - ((int) $results[0]['salesmonth']) + ((int) $historyStart->format('m'))) / 12 + 1;
+
+ foreach ($results as $result) {
+ $monthIndex = SmartDateTime::calculateMonthIndex((int) $result['salesmonth'], $businessStart);
+ if ($monthIndex < $oldIndex) {
+ $oldIndex = $monthIndex;
+
+ ++$period;
+ }
+
+ if ($period > 10) {
+ break;
+ }
+
+ $oldIndex = $monthIndex;
+
+ // indexed according to the fiscal year
+ $annualSales[$period]['net_sales'] ??= 0;
+ $annualSales[$period]['net_profit'] ??= 0;
+
+ $annualSales[$period]['net_sales'] += (int) $result['netsales'];
+ $annualSales[$period]['net_profit'] += (int) $result['netprofit'];
+ }
+
+ return $annualSales;
+ }
+}
\ No newline at end of file
diff --git a/Models/ItemMapper.php b/Models/ItemMapper.php
new file mode 100644
index 0000000..3435c61
--- /dev/null
+++ b/Models/ItemMapper.php
@@ -0,0 +1,159 @@
+format('m'), $businessStart);
+
+ // @todo: this query doesn't return clients that have not segment etc. defined.
+ $query = new Builder(self::$db);
+ $query->raw(
+ 'SELECT
+ itemmgmt_attr_type_name,
+ itemmgmt_attr_type_l11n_title,
+ itemmgmt_attr_value_id,
+ itemmgmt_attr_value_l11n_title,
+ YEAR(billing_bill_performance_date) as salesyear,
+ MONTH(billing_bill_performance_date) as salesmonth,
+ SUM(billing_bill_element_total_netsalesprice) as netsales,
+ SUM(billing_bill_element_total_netprofit) as netprofit
+ FROM billing_bill
+ LEFT JOIN billing_bill_element
+ ON billing_bill_id = billing_bill_element_bill
+ LEFT JOIN itemmgmt_item
+ ON itemmgmt_item_id = billing_bill_element_item
+ LEFT JOIN itemmgmt_item_attr
+ ON itemmgmt_item_id = itemmgmt_item_attr_item
+ LEFT JOIN itemmgmt_attr_type
+ ON itemmgmt_item_attr_type = itemmgmt_attr_type_id
+ LEFT JOIN itemmgmt_attr_type_l11n
+ ON itemmgmt_attr_type_id = itemmgmt_attr_type_l11n_type AND itemmgmt_attr_type_l11n_lang = \'' . $language . '\'
+ LEFT JOIN itemmgmt_attr_value
+ ON itemmgmt_item_attr_value = itemmgmt_attr_value_id
+ LEFT JOIN itemmgmt_attr_value_l11n
+ ON itemmgmt_attr_value_id = itemmgmt_attr_value_l11n_value AND itemmgmt_attr_value_l11n_lang = \'' . $language . '\'
+ WHERE
+ billing_bill_performance_date >= \'' . $startComparison->format('Y-m-d') . '\'
+ AND billing_bill_performance_date <= \'' . $endCurrent->format('Y-m-d') . '\'
+ AND itemmgmt_attr_type_name IN (\'segment\', \'section\', \'product_group\', \'product_type\')
+ GROUP BY
+ itemmgmt_attr_type_name,
+ itemmgmt_attr_type_l11n_title,
+ itemmgmt_attr_value_id,
+ itemmgmt_attr_value_l11n_title,
+ YEAR(billing_bill_performance_date),
+ MONTH(billing_bill_performance_date)
+ ORDER BY
+ YEAR(billing_bill_performance_date) ASC,
+ MONTH(billing_bill_performance_date) ASC'
+ );
+
+ $results = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
+
+ $oldIndex = 1;
+ $period = 1;
+
+ $mtdAItemAttribute = [];
+ $mtdPYItemAttribute = [];
+
+ $ytdAItemAttribute = [];
+ $ytdPYItemAttribute = [];
+
+ foreach ($results as $result) {
+ $monthIndex = SmartDateTime::calculateMonthIndex((int) $result['salesmonth'], $businessStart);
+ if ($monthIndex < $oldIndex) {
+ $oldIndex = $monthIndex;
+
+ ++$period;
+ }
+
+ if ($period > 2) {
+ break;
+ }
+
+ $oldIndex = $monthIndex;
+
+ // indexed according to the fiscal year
+ $temp = [
+ 'net_sales' => (int) $result['netsales'],
+ 'net_profit' => (int) $result['netprofit'],
+ ];
+
+ if (($temp['net_sales'] === 0 && $temp['net_profit'] === 0)) {
+ continue;
+ }
+
+ if ($monthIndex === $endCurrentIndex) {
+ if ($period === 1) {
+ $mtdPYItemAttribute[$result['itemmgmt_attr_type_name']] = $temp;
+ } else {
+ $mtdAItemAttribute[$result['itemmgmt_attr_type_name']] = $temp;
+ }
+ }
+
+ if ($monthIndex <= $endCurrentIndex) {
+ if (!isset($ytdPYItemAttribute[$result['itemmgmt_attr_type_name']])) {
+ $ytdPYItemAttribute[$result['itemmgmt_attr_type_name']] = [
+ 'net_sales' => 0,
+ 'net_profit' => 0,
+ 'value_l11n' => $result['itemmgmt_attr_value_l11n_title'],
+ ];
+
+ $ytdAItemAttribute[$result['itemmgmt_attr_type_name']] = [
+ 'net_sales' => 0,
+ 'net_profit' => 0,
+ 'value_l11n' => $result['itemmgmt_attr_value_l11n_title'],
+ ];
+ }
+
+ if ($period === 1) {
+ $ytdPYItemAttribute[$result['itemmgmt_attr_type_name']]['net_sales'] += $temp['net_sales'];
+ $ytdPYItemAttribute[$result['itemmgmt_attr_type_name']]['net_profit'] += $temp['net_profit'];
+ } else {
+ $ytdAItemAttribute[$result['itemmgmt_attr_type_name']]['net_sales'] += $temp['net_sales'];
+ $ytdAItemAttribute[$result['itemmgmt_attr_type_name']]['net_profit'] += $temp['net_profit'];
+ }
+ }
+ }
+
+ return [
+ $mtdAItemAttribute,
+ $mtdPYItemAttribute,
+ $ytdAItemAttribute,
+ $ytdPYItemAttribute,
+ ];
+ }
+}
\ No newline at end of file
diff --git a/Models/PermissionState.php b/Models/PermissionCategory.php
similarity index 77%
rename from Models/PermissionState.php
rename to Models/PermissionCategory.php
index 3c0272d..14fff13 100644
--- a/Models/PermissionState.php
+++ b/Models/PermissionCategory.php
@@ -8,7 +8,7 @@
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
- * @link https://orange-management.org
+ * @link https://jingga.app
*/
declare(strict_types=1);
@@ -21,10 +21,10 @@ use phpOMS\Stdlib\Base\Enum;
*
* @package Modules\SalesAnalysis\Models
* @license OMS License 1.0
- * @link https://orange-management.org
+ * @link https://jingga.app
* @since 1.0.0
*/
-abstract class PermissionState extends Enum
+abstract class PermissionCategory extends Enum
{
public const DASHBOARD = 1;
}
diff --git a/Models/RegionMapper.php b/Models/RegionMapper.php
new file mode 100644
index 0000000..b454837
--- /dev/null
+++ b/Models/RegionMapper.php
@@ -0,0 +1,554 @@
+format('m'), $businessStart);
+
+ $query = new Builder(self::$db);
+ $query->raw(
+ 'SELECT
+ address_country,
+ YEAR(billing_bill_performance_date) as salesyear,
+ MONTH(billing_bill_performance_date) as salesmonth,
+ SUM(billing_bill_netsales) as netsales,
+ SUM(billing_bill_netprofit) as netprofit
+ FROM billing_bill
+ LEFT JOIN clientmgmt_client
+ ON clientmgmt_client_id = billing_bill_client
+ LEFT JOIN address
+ ON clientmgmt_client_address = address_id
+ WHERE
+ billing_bill_performance_date >= \'' . $start->format('Y-m-d') . '\'
+ AND billing_bill_performance_date <= \'' . $end->format('Y-m-d') . '\'
+ GROUP BY
+ address_country,
+ YEAR(billing_bill_performance_date),
+ MONTH(billing_bill_performance_date)
+ ORDER BY
+ YEAR(billing_bill_performance_date) ASC,
+ MONTH(billing_bill_performance_date) ASC,
+ address_country'
+ );
+
+ $results = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
+
+ $monthlySales = [];
+
+ $mtd = [];
+ $ytd = [];
+
+ foreach ($results as $result) {
+ $monthIndex = SmartDateTime::calculateMonthIndex((int) $result['salesmonth'], $businessStart);
+
+ if (!isset($monthlySales[$result['address_country']])) {
+ $monthlySales[$result['address_country']] = [];
+
+ $mtdA[$result['address_country']] = ['net_sales' => 0, 'net_profit' => 0];
+ $mtdPY[$result['address_country']] = ['net_sales' => 0, 'net_profit' => 0];
+
+ $ytdA[$result['address_country']] = ['net_sales' => 0, 'net_profit' => 0];
+ $ytdPY[$result['address_country']] = ['net_sales' => 0, 'net_profit' => 0];
+
+ for ($i = 1; $i < 3; ++$i) {
+ $monthlySales[$result['address_country']][$i] = \array_fill(1, 12, [
+ 'net_sales' => null,
+ 'net_profit' => null,
+ ]);
+ }
+ }
+
+ // indexed according to the fiscal year
+ $monthlySales[$result['address_country']][$monthIndex] = [
+ 'net_sales' => (int) $result['netsales'],
+ 'net_profit' => (int) $result['netprofit'],
+ ];
+
+ if ($monthIndex === $endCurrentIndex) {
+ $mtd[$result['address_country']] = $monthlySales[$result['address_country']][$monthIndex];
+ }
+
+ if ($monthIndex <= $endCurrentIndex) {
+ $ytd[$result['address_country']]['net_sales'] += $monthlySales[$result['address_country']][$monthIndex]['net_sales'];
+ $ytd[$result['address_country']]['net_profit'] += $monthlySales[$result['address_country']][$monthIndex]['net_profit'];
+ }
+ }
+
+ return [$mtd, $ytd, $monthlySales];
+ }
+
+ public static function annualCustomerCountry(
+ SmartDateTime $historyStart,
+ \DateTime $endCurrent,
+ int $businessStart = 1
+ ) : array {
+ $query = new Builder(self::$db);
+ $query->raw(
+ 'SELECT
+ address_country,
+ YEAR(billing_bill_performance_date) as salesyear,
+ MONTH(billing_bill_performance_date) as salesmonth,
+ COUNT(billing_bill_netsales) as client_count
+ FROM billing_bill
+ LEFT JOIN clientmgmt_client
+ ON clientmgmt_client_id = billing_bill_client
+ LEFT JOIN address
+ ON clientmgmt_client_address = address_id
+ WHERE
+ billing_bill_performance_date >= \'' . $historyStart->format('Y-m-d') . '\'
+ AND billing_bill_performance_date <= \'' . $endCurrent->format('Y-m-d') . '\'
+ GROUP BY
+ address_country,
+ YEAR(billing_bill_performance_date),
+ MONTH(billing_bill_performance_date)
+ ORDER BY
+ YEAR(billing_bill_performance_date) ASC,
+ MONTH(billing_bill_performance_date) ASC,
+ address_country'
+ );
+
+ $results = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
+
+ $annualCustomer = [];
+
+ $oldIndex = 1;
+ // @todo: this calculation doesn't consider the start of the fiscal year
+ $period = ((((int) $results[0]['salesyear']) - ((int) $historyStart->format('Y'))) * 12
+ - ((int) $results[0]['salesmonth']) + ((int) $historyStart->format('m'))) / 12 + 1;
+
+ foreach ($results as $result) {
+ $monthIndex = SmartDateTime::calculateMonthIndex((int) $result['salesmonth'], $businessStart);
+ if ($monthIndex < $oldIndex) {
+ $oldIndex = $monthIndex;
+
+ ++$period;
+ }
+
+ if ($period > 10) {
+ break;
+ }
+
+ $oldIndex = $monthIndex;
+
+ if (!isset($annualCustomer[$result['address_country']])) {
+ for ($i = 1; $i < 11; ++$i) {
+ $annualCustomer[$result['address_country']][$i] = [
+ 'client_count' => 0
+ ];
+
+ $historyStart->smartModify(1);
+ }
+
+ $historyStart->smartModify(-10);
+ }
+
+ // indexed according to the fiscal year
+ $annualCustomer[$result['address_country']][$period]['client_count'] += (int) $result['client_count'];
+ }
+
+ return $annualCustomer;
+ }
+
+ public static function mtdYtdClientCountry(
+ \DateTime $startCurrent,
+ \DateTime $endCurrent,
+ \DateTime $startComparison,
+ \DateTime $endComparison,
+ int $businessStart = 1,
+ ) : array {
+ // @todo: this cannot be correct since the same customer may buy something in two month (distinct is required over an actual period)
+ $endCurrentIndex = SmartDateTime::calculateMonthIndex((int) $endCurrent->format('m'), $businessStart);
+
+ $query = new Builder(self::$db);
+ $query->raw(
+ 'SELECT
+ address_country,
+ YEAR(billing_bill_performance_date) as salesyear,
+ MONTH(billing_bill_performance_date) as salesmonth,
+ COUNT(billing_bill_netsales) as client_count
+ FROM billing_bill
+ LEFT JOIN clientmgmt_client
+ ON clientmgmt_client_id = billing_bill_client
+ LEFT JOIN address
+ ON clientmgmt_client_address = address_id
+ WHERE
+ billing_bill_performance_date >= \'' . $startComparison->format('Y-m-d') . '\'
+ AND billing_bill_performance_date <= \'' . $endCurrent->format('Y-m-d') . '\'
+ GROUP BY
+ address_country,
+ YEAR(billing_bill_performance_date),
+ MONTH(billing_bill_performance_date)
+ ORDER BY
+ YEAR(billing_bill_performance_date) ASC,
+ MONTH(billing_bill_performance_date) ASC,
+ address_country ASC'
+ );
+
+ $results = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
+
+ $oldIndex = 1;
+ $period = 1;
+
+ $mtdAClientCountry = [];
+ $mtdPYClientCountry = [];
+
+ $ytdAClientCountry = [];
+ $ytdPYClientCountry = [];
+
+ foreach ($results as $result) {
+ $monthIndex = SmartDateTime::calculateMonthIndex((int) $result['salesmonth'], $businessStart);
+ if ($monthIndex < $oldIndex) {
+ $oldIndex = $monthIndex;
+
+ ++$period;
+ }
+
+ if ($period > 2) {
+ break;
+ }
+
+ $oldIndex = $monthIndex;
+
+ // indexed according to the fiscal year
+ $temp = [
+ 'client_count' => (int) $result['client_count'],
+ ];
+
+ if ($temp['client_count'] === 0) {
+ continue;
+ }
+
+ if ($monthIndex === $endCurrentIndex) {
+ if ($period === 1) {
+ $mtdPYClientCountry[$result['address_country']] = $temp;
+ } else {
+ $mtdAClientCountry[$result['address_country']] = $temp;
+ }
+ }
+
+ if ($monthIndex <= $endCurrentIndex) {
+ if (!isset($ytdPYClientCountry[$result['address_country']])) {
+ $ytdPYClientCountry[$result['address_country']] = [
+ 'client_count' => 0,
+ ];
+
+ $ytdAClientCountry[$result['address_country']] = [
+ 'client_count' => 0,
+ ];
+ }
+
+ if ($period === 1) {
+ $ytdPYClientCountry[$result['address_country']]['client_count'] += $temp['client_count'];
+ } else {
+ $ytdAClientCountry[$result['address_country']]['client_count'] += $temp['client_count'];
+ }
+ }
+ }
+
+ return [
+ $mtdPYClientCountry,
+ $mtdAClientCountry,
+ $ytdPYClientCountry,
+ $ytdAClientCountry,
+ ];
+ }
+
+ public static function annualSalesProfitCountry(
+ SmartDateTime $historyStart,
+ \DateTime $endCurrent,
+ int $businessStart = 1
+ ) : array {
+ $query = new Builder(self::$db);
+ $query->raw(
+ 'SELECT
+ address_country,
+ YEAR(billing_bill_performance_date) as salesyear,
+ MONTH(billing_bill_performance_date) as salesmonth,
+ SUM(billing_bill_netsales) as netsales,
+ SUM(billing_bill_netprofit) as netprofit
+ FROM billing_bill
+ LEFT JOIN clientmgmt_client
+ ON clientmgmt_client_id = billing_bill_client
+ LEFT JOIN address
+ ON clientmgmt_client_address = address_id
+ WHERE
+ billing_bill_performance_date >= \'' . $historyStart->format('Y-m-d') . '\'
+ AND billing_bill_performance_date <= \'' . $endCurrent->format('Y-m-d') . '\'
+ GROUP BY
+ address_country,
+ YEAR(billing_bill_performance_date),
+ MONTH(billing_bill_performance_date)
+ ORDER BY
+ YEAR(billing_bill_performance_date) ASC,
+ MONTH(billing_bill_performance_date) ASC,
+ address_country'
+ );
+
+ $results = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
+
+ $annualSales = [];
+
+ $oldIndex = 1;
+ // @todo: this calculation doesn't consider the start of the fiscal year
+ $period = ((((int) $results[0]['salesyear']) - ((int) $historyStart->format('Y'))) * 12
+ - ((int) $results[0]['salesmonth']) + ((int) $historyStart->format('m'))) / 12 + 1;
+
+ foreach ($results as $result) {
+ $monthIndex = SmartDateTime::calculateMonthIndex((int) $result['salesmonth'], $businessStart);
+ if ($monthIndex < $oldIndex) {
+ $oldIndex = $monthIndex;
+
+ ++$period;
+ }
+
+ if ($period > 10) {
+ break;
+ }
+
+ $oldIndex = $monthIndex;
+
+ if (!isset($annualSales[$result['address_country']])) {
+ for ($i = 1; $i < 11; ++$i) {
+ $annualSales[$result['address_country']][$i] = [
+ 'net_sales' => 0,
+ 'net_profit' => 0,
+ 'year' => $historyStart->format('Y'),
+ ];
+
+ $historyStart->smartModify(1);
+ }
+
+ $historyStart->smartModify(-10);
+ }
+
+ // indexed according to the fiscal year
+ $annualSales[$result['address_country']][$period]['net_sales'] += (int) $result['netsales'];
+ $annualSales[$result['address_country']][$period]['net_profit'] += (int) $result['netprofit'];
+ }
+
+ return $annualSales;
+ }
+
+ public static function mtdYtdCountry(
+ \DateTime $startCurrent,
+ \DateTime $endCurrent,
+ \DateTime $startComparison,
+ \DateTime $endComparison,
+ int $businessStart = 1,
+ ) : array {
+ $endCurrentIndex = SmartDateTime::calculateMonthIndex((int) $endCurrent->format('m'), $businessStart);
+
+ $query = new Builder(self::$db);
+ $query->raw(
+ 'SELECT
+ address_country,
+ YEAR(billing_bill_performance_date) as salesyear,
+ MONTH(billing_bill_performance_date) as salesmonth,
+ SUM(billing_bill_netsales) as netsales,
+ SUM(billing_bill_netprofit) as netprofit
+ FROM billing_bill
+ LEFT JOIN clientmgmt_client
+ ON clientmgmt_client_id = billing_bill_client
+ LEFT JOIN address
+ ON clientmgmt_client_address = address_id
+ WHERE
+ billing_bill_performance_date >= \'' . $startComparison->format('Y-m-d') . '\'
+ AND billing_bill_performance_date <= \'' . $endCurrent->format('Y-m-d') . '\'
+ GROUP BY
+ address_country,
+ YEAR(billing_bill_performance_date),
+ MONTH(billing_bill_performance_date)
+ ORDER BY
+ YEAR(billing_bill_performance_date) ASC,
+ MONTH(billing_bill_performance_date) ASC,
+ address_country ASC'
+ );
+
+ $results = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
+
+ $oldIndex = 1;
+ $period = 1;
+
+ $mtdAClientCountry = [];
+ $mtdPYClientCountry = [];
+
+ $ytdAClientCountry = [];
+ $ytdPYClientCountry = [];
+
+ foreach ($results as $result) {
+ $monthIndex = SmartDateTime::calculateMonthIndex((int) $result['salesmonth'], $businessStart);
+ if ($monthIndex < $oldIndex) {
+ $oldIndex = $monthIndex;
+
+ ++$period;
+ }
+
+ if ($period > 2) {
+ break;
+ }
+
+ $oldIndex = $monthIndex;
+
+ // indexed according to the fiscal year
+ $temp = [
+ 'net_sales' => (int) $result['netsales'],
+ 'net_profit' => (int) $result['netprofit'],
+ ];
+
+ if (($temp['net_sales'] === 0 && $temp['net_profit'] === 0)) {
+ continue;
+ }
+
+ if ($monthIndex === $endCurrentIndex) {
+ if ($period === 1) {
+ $mtdPYClientCountry[$result['address_country']] = $temp;
+ } else {
+ $mtdAClientCountry[$result['address_country']] = $temp;
+ }
+ }
+
+ if ($monthIndex <= $endCurrentIndex) {
+ if (!isset($ytdPYClientCountry[$result['address_country']])) {
+ $ytdPYClientCountry[$result['address_country']] = [
+ 'net_sales' => 0,
+ 'net_profit' => 0,
+ ];
+
+ $ytdAClientCountry[$result['address_country']] = [
+ 'net_sales' => 0,
+ 'net_profit' => 0,
+ ];
+ }
+
+ if ($period === 1) {
+ $ytdPYClientCountry[$result['address_country']]['net_sales'] += $temp['net_sales'];
+ $ytdPYClientCountry[$result['address_country']]['net_profit'] += $temp['net_profit'];
+ } else {
+ $ytdAClientCountry[$result['address_country']]['net_sales'] += $temp['net_sales'];
+ $ytdAClientCountry[$result['address_country']]['net_profit'] += $temp['net_profit'];
+ }
+ }
+ }
+
+ return [
+ $mtdPYClientCountry,
+ $mtdAClientCountry,
+ $ytdPYClientCountry,
+ $ytdAClientCountry,
+ ];
+ }
+
+ public static function countryToRegion(array $countries, array $region, array $columns) : array
+ {
+ $tempStruct = [];
+ foreach ($columns as $column) {
+ $tempStruct[$column] = 0;
+ }
+
+ $regions = ['Other' => $tempStruct];
+
+ foreach ($region as $r) {
+ $definitions[$r] = ($temp = ISO3166TwoEnum::getRegion($r)) === [] ? [$r] : $temp;
+ $regions[$r] = $tempStruct;
+ }
+
+ foreach ($countries as $country => $data) {
+ $found = false;
+ foreach ($definitions as $r => $c) {
+ if (\in_array($country, $c)) {
+ foreach ($columns as $column) {
+ $regions[$r][$column] += $data[$column];
+ }
+
+ $found = true;
+ }
+ }
+
+ if (!$found) {
+ foreach ($columns as $column) {
+ $regions['Other'][$column] += $data[$column];
+ }
+ }
+ }
+
+ return $regions;
+ }
+
+ public static function countryIntervalToRegion(array $countries, array $region, array $columns) : array
+ {
+ $count = \count(\reset($countries));
+
+ $tempStruct = [];
+ foreach ($columns as $column) {
+ $tempStruct[$column] = 0;
+ }
+
+ $regions = [
+ 'Other' => \array_fill(1, $count, $tempStruct),
+ ];
+
+ foreach ($region as $r) {
+ $definitions[$r] = ($temp = ISO3166TwoEnum::getRegion($r)) === [] ? [$r] : $temp;
+ $regions[$r] = \array_fill(1, $count, $tempStruct);
+ }
+
+ foreach ($countries as $country => $data) {
+ $found = false;
+ foreach ($definitions as $r => $c) {
+ if (\in_array($country, $c)) {
+ foreach ($data as $idx => $value) {
+ foreach ($columns as $column) {
+ $regions[$r][$idx][$column] += $value[$column];
+ }
+ }
+
+ $found = true;
+ }
+ }
+
+ if (!$found) {
+ foreach ($data as $idx => $value) {
+ foreach ($columns as $column) {
+ $regions['Other'][$idx][$column] += $value[$column];
+ }
+ }
+ }
+ }
+
+ return $regions;
+ }
+}
diff --git a/Theme/Backend/Lang/Navigation.en.lang.php b/Theme/Backend/Lang/Navigation.en.lang.php
index fdfe5d1..e0f0255 100644
--- a/Theme/Backend/Lang/Navigation.en.lang.php
+++ b/Theme/Backend/Lang/Navigation.en.lang.php
@@ -8,13 +8,13 @@
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
- * @link https://orange-management.org
+ * @link https://jingga.app
*/
declare(strict_types=1);
return ['Navigation' => [
'Analysis' => 'Analysis',
- 'Customers' => 'Customers',
+ 'Dashboard' => 'Dashboard',
'Database' => 'Database',
'Invoices' => 'Invoices',
'Marketing' => 'Marketing',
diff --git a/Theme/Backend/Lang/api.en.lang.php b/Theme/Backend/Lang/api.en.lang.php
deleted file mode 100644
index f58af37..0000000
--- a/Theme/Backend/Lang/api.en.lang.php
+++ /dev/null
@@ -1,16 +0,0 @@
- [
+ 'ItemAttribute' => 'Item Attribute',
+ 'ClientAttribute' => 'Client Attribute',
+ 'SalesRegion' => 'Sales Region',
+ 'Customers' => 'Customers',
+ 'Months' => 'Months',
+ 'Total' => 'Total',
+ 'Year' => 'Year',
+ 'Month' => 'Month',
+ 'A' => 'A',
+ 'PY' => 'PY',
+ 'B' => 'B',
+ 'All' => 'All',
+ 'New' => 'New',
+ 'Lost' => 'Lost',
+ 'Product' => 'Product',
+ 'Continent' => 'Continent',
+ 'SalesPY' => 'Sales PY',
+ 'SalesB' => 'Sales B',
+ 'SalesA' => 'Sales A',
+ 'ProfitPY' => 'Profit PY',
+ 'ProfitB' => 'Profit B',
+ 'ProfitA' => 'Profit A',
+ 'DiffPY' => 'Δ PY',
+ 'DiffB' => 'Δ B',
+ 'Data' => 'Data',
+ 'Actual' => 'Actual',
+ 'Budget' => 'Budget',
+ 'Segment' => 'Segment',
+ 'Section' => 'Section',
+ 'Group' => 'Group',
+ 'Region' => 'Region',
+ 'Filter' => 'Filter',
+ 'Country' => 'Country',
+ 'Category' => 'Category',
+ 'General' => 'General',
+ 'MTD' => 'MTD',
+ 'Other' => 'Other',
+ 'YTD' => 'YTD',
+ 'GrossProfit' => 'Gross Profit',
+ 'SalesProfit' => 'Sales / Profit',
+ 'monthly' => 'monthly',
+ 'annually' => 'annually',
+ 'Profit' => 'Profit',
+ 'Start' => 'Start',
+ 'End' => 'End',
+ 'Sales' => 'Sales',
+ 'Client' => 'Client',
+ 'Clients' => 'Clients',
+ 'Item' => 'Item',
+ 'World' => 'World',
'Africa' => 'Africa',
'America' => 'America',
+ 'DomesticExport' => 'Domestic & Export',
+ 'Oceania' => 'Oceania',
'Analysis' => 'Analysis',
'Asia' => 'Asia',
'Change' => 'Change',
@@ -47,6 +99,8 @@ return ['SalesAnalysis' => [
'Marketing' => 'Marketing',
'Misc' => 'Misc',
'Month' => 'Month',
+ 'Current' => 'Current',
+ 'Analyze' => 'Analyze',
'Overview' => 'Overview',
'Products' => 'Products',
'Regions' => 'Regions',
diff --git a/Theme/Backend/analysis-bill.tpl.php b/Theme/Backend/analysis-bill.tpl.php
new file mode 100755
index 0000000..5901c1e
--- /dev/null
+++ b/Theme/Backend/analysis-bill.tpl.php
@@ -0,0 +1,23 @@
+data['nav']->render();
+?>
+
\ No newline at end of file
diff --git a/Theme/Backend/analysis-client.tpl.php b/Theme/Backend/analysis-client.tpl.php
new file mode 100755
index 0000000..7b1fd60
--- /dev/null
+++ b/Theme/Backend/analysis-client.tpl.php
@@ -0,0 +1,52 @@
+data['nav']->render();
+?>
+
+