This commit is contained in:
Dennis Eichhorn 2023-10-09 22:06:39 +00:00
parent 32fa739301
commit ae44226b1f
25 changed files with 4278 additions and 311 deletions

View File

@ -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": []
}

View File

@ -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 ApplicationAbstract $app Application
* @param string $path Module path
* @param DatabasePool $dbPool Database pool for database interaction
*
* @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']);
}
}

View File

@ -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__;
}

View File

@ -1,31 +1,75 @@
<?php declare(strict_types=1);
use Modules\SalesAnalysis\Controller\BackendController;
use Modules\SalesAnalysis\Models\PermissionState;
use Modules\SalesAnalysis\Models\PermissionCategory;
use phpOMS\Account\PermissionType;
use phpOMS\Router\RouteVerb;
return [
'^.*/sales/analysis/dashboard.*$' => [
'^.*/sales/analysis(\?.*|$)$' => [
[
'dest' => '\Modules\SalesAnalysis\Controller\BackendController:viewBackendDashboard',
'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',
'dest' => '\Modules\SalesAnalysis\Controller\BackendController:viewBillAnalysis',
'verb' => RouteVerb::GET,
'permission' => [
'module' => BackendController::MODULE_NAME,
'module' => BackendController::NAME,
'type' => PermissionType::READ,
'state' => PermissionState::DASHBOARD,
'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,
],
],
],

View File

@ -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__;
}

View File

@ -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__;
}

View File

@ -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__;
}

View File

@ -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;
}

73
Controller/Controller.js Normal file
View File

@ -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();

View File

@ -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 = [];
}

157
Models/ClientMapper.php Normal file
View File

@ -0,0 +1,157 @@
<?php
/**
* Orange Management
*
* PHP Version 7.4
*
* @package Modules\SalesAnalysis\Models
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace Modules\SalesAnalysis\Models;
use phpOMS\DataStorage\Database\Mapper\DataMapperFactory;
use phpOMS\DataStorage\Database\Query\Builder;
use phpOMS\Stdlib\Base\SmartDateTime;
/**
* Permision state enum.
*
* @package Modules\SalesAnalysis\Models
* @license OMS License 1.0
* @link https://jingga.app
* @since 1.0.0
*/
class ClientMapper extends DataMapperFactory
{
public static function mtdYtdClientAttribute(
\DateTime $startCurrent,
\DateTime $endCurrent,
\DateTime $startComparison,
\DateTime $endComparison,
int $businessStart = 1,
string $language = 'en'
) {
$endCurrentIndex = SmartDateTime::calculateMonthIndex((int) $endCurrent->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,
];
}
}

193
Models/GeneralMapper.php Normal file
View File

@ -0,0 +1,193 @@
<?php
/**
* Orange Management
*
* PHP Version 7.4
*
* @package Modules\SalesAnalysis\Models
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace Modules\SalesAnalysis\Models;
use phpOMS\DataStorage\Database\Mapper\DataMapperFactory;
use phpOMS\DataStorage\Database\Query\Builder;
use phpOMS\Stdlib\Base\SmartDateTime;
/**
* Permision state enum.
*
* @package Modules\SalesAnalysis\Models
* @license OMS License 1.0
* @link https://jingga.app
* @since 1.0.0
*/
class GeneralMapper extends DataMapperFactory
{
public static function monthlySalesProfit(
\DateTime $startCurrent,
\DateTime $endCurrent,
\DateTime $startComparison,
\DateTime $endComparison,
int $businessStart = 1
) {
$endCurrentIndex = SmartDateTime::calculateMonthIndex((int) $endCurrent->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;
}
}

159
Models/ItemMapper.php Normal file
View File

@ -0,0 +1,159 @@
<?php
/**
* Orange Management
*
* PHP Version 7.4
*
* @package Modules\SalesAnalysis\Models
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace Modules\SalesAnalysis\Models;
use phpOMS\DataStorage\Database\Mapper\DataMapperFactory;
use phpOMS\DataStorage\Database\Query\Builder;
use phpOMS\Stdlib\Base\SmartDateTime;
/**
* Permision state enum.
*
* @package Modules\SalesAnalysis\Models
* @license OMS License 1.0
* @link https://jingga.app
* @since 1.0.0
*/
class ItemMapper extends DataMapperFactory
{
public static function mtdYtdItemAttribute(
\DateTime $startCurrent,
\DateTime $endCurrent,
\DateTime $startComparison,
\DateTime $endComparison,
int $businessStart = 1,
string $language = 'en'
) {
$endCurrentIndex = SmartDateTime::calculateMonthIndex((int) $endCurrent->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,
];
}
}

View File

@ -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;
}

554
Models/RegionMapper.php Normal file
View File

@ -0,0 +1,554 @@
<?php
/**
* Orange Management
*
* PHP Version 7.4
*
* @package Modules\SalesAnalysis\Models
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace Modules\SalesAnalysis\Models;
use phpOMS\DataStorage\Database\Mapper\DataMapperFactory;
use phpOMS\DataStorage\Database\Query\Builder;
use phpOMS\Localization\ISO3166TwoEnum;
use phpOMS\Stdlib\Base\SmartDateTime;
/**
* Permision state enum.
*
* @package Modules\SalesAnalysis\Models
* @license OMS License 1.0
* @link https://jingga.app
* @since 1.0.0
*
* @todo: the periods are wrong if they are disjunct (e.g. A vs PPY)
* solution: functions need a clear start-end time and then called twice for A vs PY comparison
*/
class RegionMapper extends DataMapperFactory
{
public static function monthlySalesProfit(
\DateTime $start,
\DateTime $end,
int $businessStart = 1
) {
$endCurrentIndex = SmartDateTime::calculateMonthIndex((int) $end->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;
}
}

View File

@ -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',

View File

@ -1,16 +0,0 @@
<?php
/**
* Orange Management
*
* PHP Version 7.4
*
* @package Modules\SalesAnalysis
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://orange-management.org
*/
declare(strict_types=1);
$MODLANG[1] = [
];

View File

@ -8,13 +8,65 @@
* @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 ['SalesAnalysis' => [
'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',

View File

@ -0,0 +1,23 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package Modules\SalesAnalysis
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
/* @todo: single month/quarter/fiscal year/calendar year */
/* @todo: time range (<= 12 month = monthly view; else annual view/comparison) */
/**
* @var \phpOMS\Views\View $this
*/
echo $this->data['nav']->render();
?>
<img height="100%" src="Web/Backend/img/under_construction.svg">

View File

@ -0,0 +1,52 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package Modules\SalesAnalysis
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
use phpOMS\Localization\Money;
use phpOMS\Utils\RnG\Name;
/* @todo: single month/quarter/fiscal year/calendar year */
/* @todo: time range (<= 12 month = monthly view; else annual view/comparison) */
/**
* @var \phpOMS\Views\View $this
*/
echo $this->data['nav']->render();
?>
<div class="tabview tab-2">
<div class="box">
<ul class="tab-links">
<li><label for="c-tab-1"><?= $this->getHtml('All'); ?></label></li>
<li><label for="c-tab-2"><?= $this->getHtml('New'); ?></label></li>
<li><label for="c-tab-3"><?= $this->getHtml('Lost'); ?></label></li>
<!--<li><label for="c-tab-1"><?= $this->getHtml('Filter'); ?></label></li>-->
</ul>
</div>
<div class="tab-content">
<input type="radio" id="c-tab-1" name="tabular-2"<?= $this->request->uri->fragment === 'c-tab-1' ? ' checked' : ''; ?>>
<div class="tab">
<img height="100%" src="Web/Backend/img/under_construction.svg">
</div>
<input type="radio" id="c-tab-2" name="tabular-2"<?= $this->request->uri->fragment === 'c-tab-2' ? ' checked' : ''; ?>>
<div class="tab">
<img height="100%" src="Web/Backend/img/under_construction.svg">
</div>
<input type="radio" id="c-tab-3" name="tabular-2"<?= $this->request->uri->fragment === 'c-tab-3' ? ' checked' : ''; ?>>
<div class="tab">
<img height="100%" src="Web/Backend/img/under_construction.svg">
</div>
</div>
</div>

View File

@ -0,0 +1,216 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package Modules\SalesAnalysis
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
/**
* @var \phpOMS\Views\View $this
*/
echo $this->data['nav']->render();
?>
<div class="tabview tab-2">
<div class="box">
<ul class="tab-links">
<li><label for="c-tab-1"><?= $this->getHtml('Segment'); ?></label></li>
<li><label for="c-tab-2"><?= $this->getHtml('Section'); ?></label></li>
<li><label for="c-tab-3"><?= $this->getHtml('Group'); ?></label></li>
<li><label for="c-tab-4"><?= $this->getHtml('Type'); ?></label></li>
<!--<li><label for="c-tab-5"><?= $this->getHtml('Filter'); ?></label></li>-->
</ul>
</div>
<div class="tab-content">
<input type="radio" id="c-tab-1" name="tabular-2"<?= $this->request->uri->fragment === 'c-tab-1' ? ' checked' : ''; ?>>
<div class="tab">
<div class="row">
<div class="col-xs-12">
<section class="portlet">
<div class="portlet-head"><?= $this->getHtml('Segment'); ?></div>
<div class="slider">
<table class="default">
<thead>
<tr>
<td><?= $this->getHtml('Category'); ?>
<td><?= $this->getHtml('SalesPY'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('SalesA'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('DiffPY'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('SalesPY'); ?> (<?= $this->getHtml('MTD'); ?>)
<td><?= $this->getHtml('SalesA'); ?> (<?= $this->getHtml('MTD'); ?>)
<td><?= $this->getHtml('DiffPY'); ?> (<?= $this->getHtml('MTD'); ?>)
<tbody>
<?php
foreach ($this->data['ytdAItemAttribute'] as $type => $values) :
if ($type !== 'segment') {
continue;
}
?>
<tr>
<td><?= $this->printHtml($this->data['ytdPYItemAttribute'][$type]['value_l11n']); ?>
<td><?= $this->getCurrency((int) ($this->data['ytdPYItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency((int) ($this->data['ytdAItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency(
((int) ($this->data['ytdAItemAttribute'][$type]['net_sales'] ?? 0)) -
((int) ($this->data['ytdPYItemAttribute'][$type]['net_sales'] ?? 0))
); ?>
<td><?= $this->getCurrency((int) ($this->data['mtdPYItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency((int) ($this->data['mtdAItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency(
((int) ($this->data['mtdAItemAttribute'][$type]['net_sales'] ?? 0)) -
((int) ($this->data['mtdPYItemAttribute'][$type]['net_sales'] ?? 0))
); ?>
<?php endforeach; ?>
</table>
</div>
</section>
</div>
</div>
</div>
<input type="radio" id="c-tab-2" name="tabular-2"<?= $this->request->uri->fragment === 'c-tab-2' ? ' checked' : ''; ?>>
<div class="tab">
<div class="row">
<div class="col-xs-12">
<section class="portlet">
<div class="portlet-head"><?= $this->getHtml('Section'); ?></div>
<div class="slider">
<table class="default">
<thead>
<tr>
<td><?= $this->getHtml('Category'); ?>
<td><?= $this->getHtml('SalesPY'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('SalesA'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('DiffPY'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('SalesPY'); ?> (<?= $this->getHtml('MTD'); ?>)
<td><?= $this->getHtml('SalesA'); ?> (<?= $this->getHtml('MTD'); ?>)
<td><?= $this->getHtml('DiffPY'); ?> (<?= $this->getHtml('MTD'); ?>)
<tbody>
<?php
foreach ($this->data['ytdAItemAttribute'] as $type => $values) :
if ($type !== 'section') {
continue;
}
?>
<tr>
<td><?= $this->printHtml($this->data['ytdPYItemAttribute'][$type]['value_l11n']); ?>
<td><?= $this->getCurrency((int) ($this->data['ytdPYItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency((int) ($this->data['ytdAItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency(
((int) ($this->data['ytdAItemAttribute'][$type]['net_sales'] ?? 0)) -
((int) ($this->data['ytdPYItemAttribute'][$type]['net_sales'] ?? 0))
); ?>
<td><?= $this->getCurrency((int) ($this->data['mtdPYItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency((int) ($this->data['mtdAItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency(
((int) ($this->data['mtdAItemAttribute'][$type]['net_sales'] ?? 0)) -
((int) ($this->data['mtdPYItemAttribute'][$type]['net_sales'] ?? 0))
); ?>
<?php endforeach; ?>
</table>
</div>
</section>
</div>
</div>
</div>
<input type="radio" id="c-tab-3" name="tabular-2"<?= $this->request->uri->fragment === 'c-tab-3' ? ' checked' : ''; ?>>
<div class="tab">
<div class="row">
<div class="col-xs-12">
<section class="portlet">
<div class="portlet-head"><?= $this->getHtml('Group'); ?></div>
<div class="slider">
<table class="default">
<thead>
<tr>
<td><?= $this->getHtml('Category'); ?>
<td><?= $this->getHtml('SalesPY'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('SalesA'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('DiffPY'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('SalesPY'); ?> (<?= $this->getHtml('MTD'); ?>)
<td><?= $this->getHtml('SalesA'); ?> (<?= $this->getHtml('MTD'); ?>)
<td><?= $this->getHtml('DiffPY'); ?> (<?= $this->getHtml('MTD'); ?>)
<tbody>
<?php
foreach ($this->data['ytdAItemAttribute'] as $type => $values) :
if ($type !== 'product_group') {
continue;
}
?>
<tr>
<td><?= $this->printHtml($this->data['ytdPYItemAttribute'][$type]['value_l11n']); ?>
<td><?= $this->getCurrency((int) ($this->data['ytdPYItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency((int) ($this->data['ytdAItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency(
((int) ($this->data['ytdAItemAttribute'][$type]['net_sales'] ?? 0)) -
((int) ($this->data['ytdPYItemAttribute'][$type]['net_sales'] ?? 0))
); ?>
<td><?= $this->getCurrency((int) ($this->data['mtdPYItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency((int) ($this->data['mtdAItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency(
((int) ($this->data['mtdAItemAttribute'][$type]['net_sales'] ?? 0)) -
((int) ($this->data['mtdPYItemAttribute'][$type]['net_sales'] ?? 0))
); ?>
<?php endforeach; ?>
</table>
</div>
</section>
</div>
</div>
</div>
<input type="radio" id="c-tab-4" name="tabular-2"<?= $this->request->uri->fragment === 'c-tab-4' ? ' checked' : ''; ?>>
<div class="tab">
<div class="row">
<div class="col-xs-12">
<section class="portlet">
<div class="portlet-head"><?= $this->getHtml('Type'); ?></div>
<div class="slider">
<table class="default">
<thead>
<tr>
<td><?= $this->getHtml('Category'); ?>
<td><?= $this->getHtml('SalesPY'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('SalesA'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('DiffPY'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('SalesPY'); ?> (<?= $this->getHtml('MTD'); ?>)
<td><?= $this->getHtml('SalesA'); ?> (<?= $this->getHtml('MTD'); ?>)
<td><?= $this->getHtml('DiffPY'); ?> (<?= $this->getHtml('MTD'); ?>)
<tbody>
<?php
foreach ($this->data['ytdAItemAttribute'] as $type => $values) :
if ($type !== 'product_type') {
continue;
}
?>
<tr>
<td><?= $this->printHtml($this->data['ytdPYItemAttribute'][$type]['value_l11n']); ?>
<td><?= $this->getCurrency((int) ($this->data['ytdPYItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency((int) ($this->data['ytdAItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency(
((int) ($this->data['ytdAItemAttribute'][$type]['net_sales'] ?? 0)) -
((int) ($this->data['ytdPYItemAttribute'][$type]['net_sales'] ?? 0))
); ?>
<td><?= $this->getCurrency((int) ($this->data['mtdPYItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency((int) ($this->data['mtdAItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency(
((int) ($this->data['mtdAItemAttribute'][$type]['net_sales'] ?? 0)) -
((int) ($this->data['mtdPYItemAttribute'][$type]['net_sales'] ?? 0))
); ?>
<?php endforeach; ?>
</table>
</div>
</section>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,190 +1,530 @@
<?php
/**
* Orange Management
* Jingga
*
* PHP Version 7.4
* PHP Version 8.1
*
* @package Modules\SalesAnalysis
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @license OMS License 2.0
* @version 1.0.0
* @link https://orange-management.org
* @link https://jingga.app
*/
declare(strict_types=1);
use phpOMS\Localization\ISO3166NameEnum;
use phpOMS\Localization\Money;
use phpOMS\Uri\UriFactory;
/**
* @var \phpOMS\Views\View $this
*/
echo $this->data['nav']->render();
?>
<div class="row">
<div class="col-xs-12 col-lg-4">
<section class="portlet">
<form id="sales-dashboard-analysis" action="<?= UriFactory::build('{/backend}sales/analysis'); ?>" method="get">
<div class="portlet-body">
<div><?= $this->getHtml('Current'); ?></div>
<div class="form-group">
<div class="input-control">
<label for="iStartCurrent"><?= $this->getHtml('Start'); ?></label>
<input id="iStartCurrent" name="startcurrent" type="date" value="<?= $this->data['startCurrent']->format('Y-m-d'); ?>">
</div>
echo $this->getData('nav')->render(); ?>
<div class="input-control">
<label for="iEndCurrent"><?= $this->getHtml('End'); ?></label>
<input id="iEndCurrent" name="endcurrent" type="date" value="<?= $this->data['endCurrent']->format('Y-m-d'); ?>">
</div>
</div>
<div class="box w-100">
<div class="tabview tab-2">
<ul class="tab-links">
<li><label for="c-tab2-1"><?= $this->getHtml('Overview') ?></label>
<li><label for="c-tab2-2"><?= $this->getHtml('Month') ?></label>
<li><label for="c-tab2-3"><?= $this->getHtml('Year') ?></label>
<li><label for="c-tab2-4"><?= $this->getHtml('Top10') ?></label>
<li><label for="c-tab2-5"><?= $this->getHtml('Charts') ?></label>
</ul>
<div class="tab-content">
<input type="radio" id="c-tab2-1" name="tabular-2" checked>
<div class="tab">
<section class="box wf-100 floatLeft">
<table class="default">
<caption><?= $this->getHtml('Overview'); ?><i class="fa fa-download floatRight download btn"></i></caption>
<thead>
<tr>
<td><?= $this->getHtml('Type') ?>
<td><?= $this->getHtml('LastMonth') ?>
<td><?= $this->getHtml('CurrentMonth') ?>
<td><?= $this->getHtml('Change') ?>
<td><?= $this->getHtml('LastYear') ?>
<td><?= $this->getHtml('CurrentYear') ?>
<td><?= $this->getHtml('Change') ?>
<td><?= $this->getHtml('LastYearAcc') ?>
<td><?= $this->getHtml('CurrentYearAcc') ?>
<td><?= $this->getHtml('Change') ?>
<td><?= $this->getHtml('LastYear') ?>
<td><?= $this->getHtml('Forecast') ?>
<td><?= $this->getHtml('Change') ?>
<tbody>
<tr><th><?= $this->getHtml('Domestic') ?><td><td><td><td><td><td><td><td><td><td><td><td>
<tr><th><?= $this->getHtml('Export') ?><td><td><td><td><td><td><td><td><td><td><td><td>
<tr><th><?= $this->getHtml('Developed') ?><td><td><td><td><td><td><td><td><td><td><td><td>
<tr><th><?= $this->getHtml('Undeveloped') ?><td><td><td><td><td><td><td><td><td><td><td><td>
<tr><th><?= $this->getHtml('Europe') ?><td><td><td><td><td><td><td><td><td><td><td><td>
<tr><th><?= $this->getHtml('America') ?><td><td><td><td><td><td><td><td><td><td><td><td>
<tr><th><?= $this->getHtml('Asia') ?><td><td><td><td><td><td><td><td><td><td><td><td>
<tr><th><?= $this->getHtml('Africa') ?><td><td><td><td><td><td><td><td><td><td><td><td>
<tr><th><?= $this->getHtml('Total') ?><td><td><td><td><td><td><td><td><td><td><td><td>
</table>
</section>
<div><?= $this->getHtml('Comparison'); ?></div>
<section class="box wf-100 floatLeft">
<table class="default">
<caption><?= $this->getHtml('Misc'); ?><i class="fa fa-download floatRight download btn"></i></caption>
<thead>
<tr>
<td><?= $this->getHtml('Type') ?>
<td><?= $this->getHtml('LastYear') ?>
<td><?= $this->getHtml('CurrentYear') ?>
<td><?= $this->getHtml('LastMonth') ?>
<td><?= $this->getHtml('CurrentMonth') ?>
<td><?= $this->getHtml('Yesterday') ?>
<td><?= $this->getHtml('Today') ?>
<tbody>
<tr><th><?= $this->getHtml('Customers') ?><td><td><td><td><td><td>
<tr><th><?= $this->getHtml('Invoices') ?><td><td><td><td><td><td>
</table>
<div class="form-group">
<div class="input-control">
<label for="iStartComparison"><?= $this->getHtml('Start'); ?></label>
<input id="iStartComparison" name="startcomparison" type="date" value="<?= $this->data['startComparison']->format('Y-m-d'); ?>">
</div>
<div class="input-control">
<label for="iEndComparison"><?= $this->getHtml('End'); ?></label>
<input id="iEndComparison" name="endcomparison" type="date" value="<?= $this->data['endComparison']->format('Y-m-d'); ?>">
</div>
</div>
</div>
<div class="portlet-foot">
<input id="iSubmitGeneral" name="submitGeneral" type="submit" value="<?= $this->getHtml('Analyze'); ?>">
</div>
</form>
</section>
</div>
<input type="radio" id="c-tab2-2" name="tabular-2">
<div class="tab">
<section class="box wf-100 floatLeft">
<table class="default">
<caption><?= $this->getHtml('Month'); ?><i class="fa fa-download floatRight download btn"></i></caption>
<thead>
<tr>
<td><?= $this->getHtml('Day') ?>
<td><?= $this->getHtml('Day') ?>
<td><?= $this->getHtml('LastMonth') ?>
<td><?= $this->getHtml('CurrentMonth') ?>
<td><?= $this->getHtml('Change') ?>
<td><?= $this->getHtml('ChangeAcc') ?>
<tbody>
<tr><td><td><td><td><td><td>
</table>
</section>
<div class="col-xs-12 col-lg-4">
<section class="portlet highlight-3">
<div class="portlet-head"><?= $this->getHtml('Actual'); ?></div>
<div class="portlet-body">
<div class="form-group">
<div><?= $this->getHtml('Sales'); ?> (<?= $this->getHtml('MTD'); ?>):</div>
<div>&nbsp;<?= \sprintf('%+.2f', $this->data['mtdPY']['net_sales'] == 0 ? 0 : $this->data['mtdA']['net_sales'] * 100 / $this->data['mtdPY']['net_sales'] - 100); ?> %</div>
</div>
<input type="radio" id="c-tab2-3" name="tabular-2">
<div class="tabview tab-3">
<section class="box wf-100 floatLeft">
<table class="default">
<caption><?= $this->getHtml('Year'); ?><i class="fa fa-download floatRight download btn"></i></caption>
<thead>
<tr>
<td><?= $this->getHtml('Year') ?>
<td><?= $this->getHtml('January') ?>
<td><?= $this->getHtml('February') ?>
<td><?= $this->getHtml('March') ?>
<td><?= $this->getHtml('April') ?>
<td><?= $this->getHtml('May') ?>
<td><?= $this->getHtml('June') ?>
<td><?= $this->getHtml('July') ?>
<td><?= $this->getHtml('August') ?>
<td><?= $this->getHtml('September') ?>
<td><?= $this->getHtml('October') ?>
<td><?= $this->getHtml('November') ?>
<td><?= $this->getHtml('December') ?>
<tbody>
<tr><th>2013<td><td><td><td><td><td><td><td><td><td><td><td>
<tr><th>2014<td><td><td><td><td><td><td><td><td><td><td><td>
<tr><th>2015<td><td><td><td><td><td><td><td><td><td><td><td>
<tr><th>CY 2016<td><td><td><td><td><td><td><td><td><td><td><td>
<tr><th>2017<td><td><td><td><td><td><td><td><td><td><td><td>
<tr><th>2018<td><td><td><td><td><td><td><td><td><td><td><td>
<tr><th>2019<td><td><td><td><td><td><td><td><td><td><td><td>
<tr><th>2020<td><td><td><td><td><td><td><td><td><td><td><td>
<tr><th>2021<td><td><td><td><td><td><td><td><td><td><td><td>
</table>
</section>
<div class="form-group">
<div><?= $this->getHtml('Sales'); ?> (<?= $this->getHtml('YTD'); ?>):</div>
<div>&nbsp;<?= \sprintf('%+.2f', $this->data['ytdPY']['net_sales'] == 0 ? 0 : $this->data['ytdA']['net_sales'] * 100 / $this->data['ytdPY']['net_sales'] - 100); ?> %</div>
</div>
<div class="form-group">
<div><?= $this->getHtml('GrossProfit'); ?> (<?= $this->getHtml('MTD'); ?>):</div>
<div>&nbsp;<?= \sprintf('%+.2f', $this->data['mtdPY']['net_profit'] == 0 ? 0 : $this->data['mtdA']['net_profit'] * 100 / $this->data['mtdPY']['net_profit'] - 100); ?> %</div>
</div>
<div class="form-group">
<div><?= $this->getHtml('GrossProfit'); ?> (<?= $this->getHtml('YTD'); ?>):</div>
<div>&nbsp;<?= \sprintf('%+.2f', $this->data['ytdPY']['net_profit'] == 0 ? 0 : $this->data['ytdA']['net_profit'] * 100 / $this->data['ytdPY']['net_profit'] - 100); ?> %</div>
</div>
<input type="radio" id="c-tab2-4" name="tabular-2">
<div class="tabview tab-4">
<section class="box w-33 floatLeft">
<header>
<h1><?= $this->getHtml('Customers') ?></h1>
</header>
<div class="inner">
</div>
</section>
<section class="box w-33 floatLeft">
<header>
<h1><?= $this->getHtml('Products') ?></h1>
</header>
<div class="inner">
</div>
</section>
<section class="box w-33 floatLeft">
<header>
<h1><?= $this->getHtml('Employees') ?></h1>
</header>
<div class="inner">
</div>
</section>
</div>
<input type="radio" id="c-tab2-5" name="tabular-2">
<div class="tabview tab-5">
<section class="box w-33 floatLeft">
<header>
<h1><?= $this->getHtml('Domestic/Export') ?></h1>
</header>
<div class="inner">
</div>
</section>
<section class="box w-33 floatLeft">
<header>
<h1><?= $this->getHtml('Developed/Undeveloped') ?></h1>
</header>
<div class="inner">
</div>
</section>
<section class="box w-33 floatLeft">
<header>
<h1><?= $this->getHtml('Continents') ?></h1>
</header>
<div class="inner">
</div>
</section>
<section class="box w-100 floatLeft">
<header>
<h1><?= $this->getHtml('Development') ?></h1>
</header>
<div class="inner">
</div>
</section>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-lg-6">
<section class="portlet">
<div class="portlet-head">
<?= $this->getHtml('SalesProfit'); ?> (<?= $this->getHtml('monthly'); ?>)
</div>
<?php $sales = $this->data['monthlySales']; ?>
<div class="portlet-body">
<canvas id="sales-profit-monthly" data-chart='{
"type": "bar",
"data": {
"labels": [
<?php
$temp = [];
for ($i = 1; $i < 13; ++$i) {
$temp[] = \sprintf('"%02d"', $i);
}
echo \implode(',', $temp);
?>
],
"datasets": [
{
"label": "<?= $this->getHtml('Profit'); ?> PY",
"type": "line",
"data": [
<?php
$temp = [];
for ($i = 0; $i < 12; ++$i) {
if (!isset($sales[1][$i]['net_sales']) || !isset($sales[1][$i]['net_profit'])) {
$temp[] = 'null';
continue;
}
$temp[] = $sales[1][$i]['net_sales'] == 0
? 0
: $sales[1][$i]['net_profit'] * 100 / $sales[1][$i]['net_sales'];
}
echo \implode(',', $temp);
?>
],
"yAxisID": "y1",
"fill": false,
"tension": 0.0,
"borderColor": "rgb(166, 193, 178)",
"backgroundColor": "rgb(166, 193, 178)"
},
{
"label": "<?= $this->getHtml('Profit'); ?> A",
"type": "line",
"data": [
<?php
$temp = [];
for ($i = 0; $i < 12; ++$i) {
if (!isset($sales[2][$i]['net_sales']) || !isset($sales[2][$i]['net_profit'])) {
$temp[] = 'null';
continue;
}
$temp[] = $sales[2][$i]['net_sales'] == 0
? 0
: $sales[2][$i]['net_profit'] * 100 / $sales[2][$i]['net_sales'];
}
echo \implode(',', $temp);
?>
],
"yAxisID": "y1",
"fill": false,
"tension": 0.0,
"borderColor": "rgb(46, 204, 113)",
"backgroundColor": "rgb(46, 204, 113)"
},
{
"label": "<?= $this->getHtml('Sales'); ?> PY",
"type": "bar",
"data": [
<?php
$temp = [];
for ($i = 0; $i < 12; ++$i) {
$temp[] = ($sales[1][$i]['net_sales'] ?? 0) / 10000;
}
echo \implode(',', $temp);
?>
],
"yAxisID": "y",
"fill": false,
"tension": 0.0,
"backgroundColor": "rgb(177, 195, 206)"
},
{
"label": "<?= $this->getHtml('Sales'); ?> A",
"type": "bar",
"data": [
<?php
$temp = [];
for ($i = 0; $i < 12; ++$i) {
$temp[] = ($sales[2][$i]['net_sales'] ?? 0) / 10000;
}
echo \implode(',', $temp);
?>
],
"yAxisID": "y",
"fill": false,
"tension": 0.0,
"backgroundColor": "rgb(54, 162, 235)"
}
]
},
"options": {
"responsive": true,
"scales": {
"x": {
"title": {
"display": true,
"text": "<?= $this->getHtml('Months'); ?>"
}
},
"y": {
"title": {
"display": true,
"text": "<?= $this->getHtml('Sales'); ?>"
},
"display": true,
"position": "left"
},
"y1": {
"title": {
"display": true,
"text": "<?= $this->getHtml('Profit'); ?> %"
},
"display": true,
"position": "right",
"scaleLabel": {
"display": true,
"labelString": "<?= $this->getHtml('Profit'); ?>"
},
"grid": {
"drawOnChartArea": false
}
}
}
}
}'></canvas>
<div class="more-container">
<input id="more-customer-sales" type="checkbox" name="more-container">
<label for="more-customer-sales">
<span><?= $this->getHtml('Data'); ?></span>
<i class="fa fa-chevron-right expand"></i>
</label>
<div class="slider">
<table class="default">
<thead>
<tr>
<td><?= $this->getHtml('Month'); ?>
<td><?= $this->getHtml('SalesPY'); ?>
<td><?= $this->getHtml('SalesA'); ?>
<td><?= $this->getHtml('ProfitPY'); ?>
<td><?= $this->getHtml('ProfitA'); ?>
<tbody>
<?php
$sum1 = 0;
$sum2 = 0;
$sum3 = 0;
$sum4 = 0;
for ($i = 0; $i < 12; ++$i) :
$sum1 += (int) ($sales[1][$i]['net_sales'] ?? 0);
$sum2 += (int) ($sales[2][$i]['net_sales'] ?? 0);
$sum3 += (int) ($sales[1][$i]['net_profit'] ?? 0);
$sum4 += (int) ($sales[2][$i]['net_profit'] ?? 0);
?>
<tr>
<td><?= \sprintf('%02d', $i + 1); ?>
<td><?= $this->getCurrency((int) ($sales[1][$i]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency((int) ($sales[2][$i]['net_sales'] ?? 0)); ?>
<td><?= \sprintf('%.2f', ($sales[1][$i]['net_sales'] ?? 0) == 0 ? 0 : $sales[1][$i]['net_profit'] * 100 / $sales[1][$i]['net_sales']); ?> %
<td><?= \sprintf('%.2f', ($sales[2][$i]['net_sales'] ?? 0) == 0 ? 0 : $sales[2][$i]['net_profit'] * 100 / $sales[2][$i]['net_sales']); ?> %
<?php endfor; ?>
<tr>
<td><?= $this->getHtml('Total'); ?>
<td><?= $this->getCurrency($sum1); ?>
<td><?= $this->getCurrency($sum2); ?>
<td><?= \sprintf('%.2f', $sum3 == 0 ? 0 : $sum1 / $sum3); ?> %
<td><?= \sprintf('%.2f', $sum3 == 0 ? 0 : $sum2 / $sum4); ?> %
</table>
</div>
</div>
</div>
</section>
</div>
<div class="col-xs-12 col-lg-6">
<section class="portlet">
<div class="portlet-head">
<?= $this->getHtml('SalesProfit'); ?> (<?= $this->getHtml('annually'); ?>)
</div>
<?php $sales = $this->data['annualSales']; ?>
<div class="portlet-body">
<canvas id="sales-profit-annually" data-chart='{
"type": "bar",
"data": {
"labels": [
<?php
$temp = [];
for ($i = 1; $i < 11; ++$i) {
$temp[] = $sales[$i]['year'];
}
echo \implode(',', $temp);
?>
],
"datasets": [
{
"label": "<?= $this->getHtml('Profit'); ?>",
"type": "line",
"data": [
<?php
$temp = [];
for ($i = 1; $i < 11; ++$i) {
if ($sales[$i]['net_sales'] === null || $sales[$i]['net_profit'] === null) {
$temp[] = 'null';
continue;
}
$temp[] = $sales[$i]['net_sales'] == 0
? 0
: $sales[$i]['net_profit'] * 100 / $sales[$i]['net_sales'];
}
echo \implode(',', $temp);
?>
],
"yAxisID": "y1",
"fill": false,
"tension": 0.0,
"borderColor": "rgb(46, 204, 113)",
"backgroundColor": "rgb(46, 204, 113)"
},
{
"label": "<?= $this->getHtml('Sales'); ?>",
"type": "bar",
"data": [
<?php
$temp = [];
for ($i = 1; $i < 11; ++$i) {
$temp[] = $sales[$i]['net_sales'] / 10000;
}
echo \implode(',', $temp);
?>
],
"yAxisID": "y",
"fill": false,
"tension": 0.0,
"backgroundColor": "rgb(54, 162, 235)"
}
]
},
"options": {
"responsive": true,
"scales": {
"x": {
"title": {
"display": true,
"text": "<?= $this->getHtml('Months'); ?>"
}
},
"y": {
"title": {
"display": true,
"text": "<?= $this->getHtml('Sales'); ?>"
},
"display": true,
"position": "left"
},
"y1": {
"title": {
"display": true,
"text": "<?= $this->getHtml('Profit'); ?> %"
},
"display": true,
"position": "right",
"scaleLabel": {
"display": true,
"labelString": "<?= $this->getHtml('Profit'); ?>"
},
"grid": {
"drawOnChartArea": false
}
}
}
}
}'></canvas>
<div class="more-container">
<input id="more-customer-sales-annual" type="checkbox" name="more-container">
<label for="more-customer-sales-annual">
<span><?= $this->getHtml('Data'); ?></span>
<i class="fa fa-chevron-right expand"></i>
</label>
<div class="slider">
<table class="default">
<thead>
<tr>
<td><?= $this->getHtml('Year'); ?>
<td><?= $this->getHtml('Sales'); ?>
<td><?= $this->getHtml('Profit'); ?>
<tbody>
<?php
foreach ($sales as $values) :
?>
<tr>
<td><?= (string) $values['year']; ?>
<td><?= $this->getCurrency(((int) $values['net_sales']) / 10000); ?>
<td><?= \sprintf('%.2f', $values['net_sales'] == 0 ? 0 : $values['net_profit'] * 100 / $values['net_sales']); ?> %
<?php endforeach; ?>
</table>
</div>
</div>
</div>
</section>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<section class="portlet">
<div class="portlet-head">
<?= $this->getHtml('ItemAttribute'); ?>
</div>
<div class="slider">
<table class="default">
<thead>
<tr>
<td><?= $this->getHtml('Category'); ?>
<td><?= $this->getHtml('SalesPY'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('SalesA'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('DiffPY'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('SalesPY'); ?> (<?= $this->getHtml('MTD'); ?>)
<td><?= $this->getHtml('SalesA'); ?> (<?= $this->getHtml('MTD'); ?>)
<td><?= $this->getHtml('DiffPY'); ?> (<?= $this->getHtml('MTD'); ?>)
<tbody>
<?php foreach ($this->data['ytdAItemAttribute'] as $type => $values) : ?>
<tr>
<td><?= $this->printHtml($this->data['ytdPYItemAttribute'][$type]['value_l11n']); ?>
<td><?= $this->getCurrency((int) ($this->data['ytdPYItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency((int) ($this->data['ytdAItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency(
((int) ($this->data['ytdAItemAttribute'][$type]['net_sales'] ?? 0)) -
((int) ($this->data['ytdPYItemAttribute'][$type]['net_sales'] ?? 0))
); ?>
<td><?= $this->getCurrency((int) ($this->data['mtdPYItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency((int) ($this->data['mtdAItemAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency(
((int) ($this->data['mtdAItemAttribute'][$type]['net_sales'] ?? 0)) -
((int) ($this->data['mtdPYItemAttribute'][$type]['net_sales'] ?? 0))
); ?>
<?php endforeach; ?>
</table>
</div>
</section>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<section class="portlet">
<div class="portlet-head">
<?= $this->getHtml('ClientAttribute'); ?>
</div>
<div class="slider">
<table class="default">
<thead>
<tr>
<td><?= $this->getHtml('Category'); ?>
<td><?= $this->getHtml('SalesPY'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('SalesA'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('DiffPY'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('SalesPY'); ?> (<?= $this->getHtml('MTD'); ?>)
<td><?= $this->getHtml('SalesA'); ?> (<?= $this->getHtml('MTD'); ?>)
<td><?= $this->getHtml('DiffPY'); ?> (<?= $this->getHtml('MTD'); ?>)
<tbody>
<?php foreach ($this->data['ytdAClientAttribute'] as $type => $values) : ?>
<tr>
<td><?= $this->printHtml($this->data['ytdPYClientAttribute'][$type]['value_l11n']); ?>
<td><?= $this->getCurrency((int) ($this->data['ytdPYClientAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency((int) ($this->data['ytdAClientAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency(
((int) ($this->data['ytdAClientAttribute'][$type]['net_sales'] ?? 0)) -
((int) ($this->data['ytdPYClientAttribute'][$type]['net_sales'] ?? 0))
); ?>
<td><?= $this->getCurrency((int) ($this->data['mtdPYClientAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency((int) ($this->data['mtdAClientAttribute'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency(
((int) ($this->data['mtdAClientAttribute'][$type]['net_sales'] ?? 0)) -
((int) ($this->data['mtdPYClientAttribute'][$type]['net_sales'] ?? 0))
); ?>
<?php endforeach; ?>
</table>
</div>
</section>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<section class="portlet">
<div class="portlet-head">
<?= $this->getHtml('Country'); ?>
</div>
<div class="slider">
<table class="default">
<thead>
<tr>
<td><?= $this->getHtml('Country'); ?>
<td><?= $this->getHtml('SalesPY'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('SalesA'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('DiffPY'); ?> (<?= $this->getHtml('YTD'); ?>)
<td><?= $this->getHtml('SalesPY'); ?> (<?= $this->getHtml('MTD'); ?>)
<td><?= $this->getHtml('SalesA'); ?> (<?= $this->getHtml('MTD'); ?>)
<td><?= $this->getHtml('DiffPY'); ?> (<?= $this->getHtml('MTD'); ?>)
<tbody>
<?php foreach ($this->data['ytdAClientCountry'] as $type => $values) : ?>
<tr>
<td><?= $this->printHtml(ISO3166NameEnum::getBy2Code($type)); ?>
<td><?= $this->getCurrency((int) ($this->data['ytdPYClientCountry'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency((int) ($this->data['ytdAClientCountry'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency(
((int) ($this->data['ytdAClientCountry'][$type]['net_sales'] ?? 0)) -
((int) ($this->data['ytdPYClientCountry'][$type]['net_sales'] ?? 0))
); ?>
<td><?= $this->getCurrency((int) ($this->data['mtdPYClientCountry'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency((int) ($this->data['mtdAClientCountry'][$type]['net_sales'] ?? 0)); ?>
<td><?= $this->getCurrency(
((int) ($this->data['mtdAClientCountry'][$type]['net_sales'] ?? 0)) -
((int) ($this->data['mtdPYClientCountry'][$type]['net_sales'] ?? 0))
); ?>
<?php endforeach; ?>
</table>
</div>
</section>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package Modules\SalesAnalysis
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
/* @todo: single month/quarter/fiscal year/calendar year */
/* @todo: time range (<= 12 month = monthly view; else annual view/comparison) */
/**
* @var \phpOMS\Views\View $this
*/
echo $this->data['nav']->render();
?>
<div class="tabview tab-2">
<div class="box">
<ul class="tab-links">
<li><label for="c-tab-1"><?= $this->getHtml('General'); ?></label></li>
</ul>
</div>
<div class="tab-content">
<input type="radio" id="c-tab-1" name="tabular-2"<?= $this->request->uri->fragment === 'c-tab-1' ? ' checked' : ''; ?>>
<div class="tab">
<img height="100%" src="Web/Backend/img/under_construction.svg">
</div>
</div>
</div>

View File

@ -11,13 +11,16 @@
"phpOMS-db": "1.0.0"
},
"creator": {
"name": "Orange Management",
"website": "www.spl1nes.com"
"name": "Jingga",
"website": "jingga.app"
},
"description": "Sales module.",
"directory": "SalesAnalysis",
"dependencies": {
"Admin": "1.0.0"
"Admin": "1.0.0",
"Sales": "1.0.0",
"ClientManagement": "1.0.0",
"Billing": "1.0.0"
},
"providing": {
"Navigation": "*"