Merge pull request #303 from copenhagentruckwash/codex/daily-report-product-targets-api
[codex] Add daily report product target API
This commit is contained in:
@@ -12757,6 +12757,33 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DepartmentDailyReportOverviewResponse'
|
||||
|
||||
/departments/daily-reports/product-targets:
|
||||
put:
|
||||
tags:
|
||||
- Departments
|
||||
summary: Set daily report product target
|
||||
description: Requires set_department_daily_report_product_targets and department_access_:department_id. Send a null target_percentage to clear the target.
|
||||
operationId: setDailyReportProductTarget
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DepartmentDailyReportProductTargetRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Daily report product target updated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DepartmentDailyReportProductTargetResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
|
||||
/departments/daily-reports/get:
|
||||
get:
|
||||
tags:
|
||||
@@ -21566,6 +21593,36 @@ components:
|
||||
state: { type: string }
|
||||
value: { type: integer }
|
||||
out_of: { type: integer }
|
||||
target_percentage: { type: number, format: float, nullable: true }
|
||||
target_department_id: { type: integer, nullable: true }
|
||||
|
||||
DepartmentDailyReportProductTargetRequest:
|
||||
type: object
|
||||
required:
|
||||
- department_id
|
||||
- product_id
|
||||
- target_percentage
|
||||
properties:
|
||||
department_id: { type: integer }
|
||||
product_id: { type: integer }
|
||||
target_percentage:
|
||||
type: number
|
||||
format: float
|
||||
nullable: true
|
||||
|
||||
DepartmentDailyReportProductTarget:
|
||||
type: object
|
||||
properties:
|
||||
department_id: { type: integer }
|
||||
product_id: { type: integer }
|
||||
target_percentage: { type: number, format: float, nullable: true }
|
||||
|
||||
DepartmentDailyReportProductTargetResponse:
|
||||
type: object
|
||||
properties:
|
||||
success: { type: boolean, example: true }
|
||||
data:
|
||||
$ref: '#/components/schemas/DepartmentDailyReportProductTarget'
|
||||
|
||||
DepartmentDailyReportOverviewPayload:
|
||||
type: object
|
||||
|
||||
@@ -12768,6 +12768,33 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DepartmentDailyReportOverviewResponse'
|
||||
|
||||
/departments/daily-reports/product-targets:
|
||||
put:
|
||||
tags:
|
||||
- Departments
|
||||
summary: Set daily report product target
|
||||
description: Requires set_department_daily_report_product_targets and department_access_:department_id. Send a null target_percentage to clear the target.
|
||||
operationId: setDailyReportProductTarget
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DepartmentDailyReportProductTargetRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Daily report product target updated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DepartmentDailyReportProductTargetResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
|
||||
/departments/daily-reports/get:
|
||||
get:
|
||||
tags:
|
||||
@@ -21577,6 +21604,36 @@ components:
|
||||
state: { type: string }
|
||||
value: { type: integer }
|
||||
out_of: { type: integer }
|
||||
target_percentage: { type: number, format: float, nullable: true }
|
||||
target_department_id: { type: integer, nullable: true }
|
||||
|
||||
DepartmentDailyReportProductTargetRequest:
|
||||
type: object
|
||||
required:
|
||||
- department_id
|
||||
- product_id
|
||||
- target_percentage
|
||||
properties:
|
||||
department_id: { type: integer }
|
||||
product_id: { type: integer }
|
||||
target_percentage:
|
||||
type: number
|
||||
format: float
|
||||
nullable: true
|
||||
|
||||
DepartmentDailyReportProductTarget:
|
||||
type: object
|
||||
properties:
|
||||
department_id: { type: integer }
|
||||
product_id: { type: integer }
|
||||
target_percentage: { type: number, format: float, nullable: true }
|
||||
|
||||
DepartmentDailyReportProductTargetResponse:
|
||||
type: object
|
||||
properties:
|
||||
success: { type: boolean, example: true }
|
||||
data:
|
||||
$ref: '#/components/schemas/DepartmentDailyReportProductTarget'
|
||||
|
||||
DepartmentDailyReportOverviewPayload:
|
||||
type: object
|
||||
|
||||
@@ -13,6 +13,7 @@ use DateTimeZone;
|
||||
use Exception;
|
||||
use objects\department_daily_report_complaints_o;
|
||||
use objects\department_daily_reports_o;
|
||||
use objects\department_variables_o;
|
||||
use objects\departments_o;
|
||||
use objects\logs_o;
|
||||
use objects\users_o;
|
||||
@@ -22,6 +23,8 @@ class departmentDailyReportsRoute
|
||||
{
|
||||
use route_t;
|
||||
|
||||
private const SET_PRODUCT_TARGET_PERMISSION = 'set_department_daily_report_product_targets';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->get('/departments/daily-reports', function () {
|
||||
@@ -850,7 +853,8 @@ class departmentDailyReportsRoute
|
||||
'overview' => $this->buildDailyReportOverview(
|
||||
[$department_id],
|
||||
(string)self::getParameter('date'),
|
||||
$date_to
|
||||
$date_to,
|
||||
self::hasPermission(self::SET_PRODUCT_TARGET_PERMISSION)
|
||||
),
|
||||
]);
|
||||
},
|
||||
@@ -895,7 +899,8 @@ class departmentDailyReportsRoute
|
||||
$this->buildDailyReportOverview(
|
||||
$department_ids,
|
||||
(string)self::getParameter('date'),
|
||||
$date_to
|
||||
$date_to,
|
||||
self::hasPermission(self::SET_PRODUCT_TARGET_PERMISSION)
|
||||
)
|
||||
);
|
||||
},
|
||||
@@ -906,6 +911,73 @@ class departmentDailyReportsRoute
|
||||
]
|
||||
);
|
||||
|
||||
$this->put('/departments/daily-reports/product-targets', function () {
|
||||
global $response;
|
||||
$this->requirePermission(self::SET_PRODUCT_TARGET_PERMISSION);
|
||||
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
(new logs_o())->add('departments', 'global', 1, 0, 'SET_DEPARTMENT_DAILY_REPORT_PRODUCT_TARGET', 'No user found, or invalid session');
|
||||
$response->error('Invalid session', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
self::requireParameters([
|
||||
'department_id',
|
||||
'product_id',
|
||||
'target_percentage',
|
||||
]);
|
||||
|
||||
$department_id = (int)self::getParameter('department_id');
|
||||
if ($department_id <= 0) {
|
||||
$response->error('Parameter department_id must be a positive integer', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$department = (new departments_o())->select($department_id);
|
||||
if (!$department->exists()) {
|
||||
$response->error('Department not found', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
self::requireDepartmentAccess($department_id);
|
||||
|
||||
$product_id = (int)self::getParameter('product_id');
|
||||
if (!$this->isDailyReportProductId($product_id)) {
|
||||
$response->error('Invalid daily report product_id', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$parsed_target = $this->parseDailyReportProductTargetPercentage(self::getParameter('target_percentage'));
|
||||
if (!$parsed_target['valid']) {
|
||||
$response->error($parsed_target['message'], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$target_percentage = $parsed_target['value'];
|
||||
$department_variables = (new department_variables_o())->selectDepartment($department_id);
|
||||
$target_key = $this->dailyReportProductTargetVariableKey($product_id);
|
||||
|
||||
if ($target_percentage === null) {
|
||||
$this->clearDailyReportProductTarget($department_variables, $target_key);
|
||||
} else {
|
||||
$department_variables->set($target_key, number_format($target_percentage, 1, '.', ''));
|
||||
}
|
||||
|
||||
(new logs_o())->add('departments', 'global', 1, $user->id, 'SET_DEPARTMENT_DAILY_REPORT_PRODUCT_TARGET', 'Successfully updated department daily report product target');
|
||||
|
||||
$response->success([
|
||||
'department_id' => $department_id,
|
||||
'product_id' => $product_id,
|
||||
'target_percentage' => $target_percentage,
|
||||
]);
|
||||
},
|
||||
[
|
||||
self::SET_PRODUCT_TARGET_PERMISSION => 'Set department daily report product target percentages',
|
||||
'department_access_:department_id' => 'Access the department'
|
||||
]
|
||||
);
|
||||
|
||||
$this->get('/departments/daily-reports/product-count', function () {
|
||||
// Require the user to be logged in
|
||||
global $response;
|
||||
@@ -1369,7 +1441,7 @@ class departmentDailyReportsRoute
|
||||
* }
|
||||
* @throws Exception
|
||||
*/
|
||||
private function buildDailyReportOverview(array $department_ids, string $date, string $date_to): array
|
||||
private function buildDailyReportOverview(array $department_ids, string $date, string $date_to, bool $include_product_targets = false): array
|
||||
{
|
||||
$repository = $this->dailyReportRepository();
|
||||
$transaction_summary = $repository->getTransactionSummaryForDepartments($date, $department_ids, $date_to);
|
||||
@@ -1389,6 +1461,11 @@ class departmentDailyReportsRoute
|
||||
|
||||
$overtime_metric = $this->buildOvertimeMetric($department_ids, $date, $date_to);
|
||||
|
||||
$product_target_lookup = [];
|
||||
if ($include_product_targets && count($department_ids) === 1) {
|
||||
$product_target_lookup = $this->getDailyReportProductTargetsForDepartment((int)$department_ids[0], $product_definitions);
|
||||
}
|
||||
|
||||
return $this->assembleDailyReportOverview(
|
||||
$department_ids,
|
||||
$date,
|
||||
@@ -1399,7 +1476,8 @@ class departmentDailyReportsRoute
|
||||
$product_summary_lookup,
|
||||
$complaints_metric,
|
||||
$night_wash_metric,
|
||||
$overtime_metric
|
||||
$overtime_metric,
|
||||
$product_target_lookup
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1412,6 +1490,7 @@ class departmentDailyReportsRoute
|
||||
* @param array<string,mixed> $complaints_metric
|
||||
* @param array<string,mixed> $night_wash_metric
|
||||
* @param array<string,mixed> $overtime_metric
|
||||
* @param array<int,float> $product_target_lookup
|
||||
* @return array{
|
||||
* department_ids:array<int>,
|
||||
* date:string,
|
||||
@@ -1430,7 +1509,8 @@ class departmentDailyReportsRoute
|
||||
array $product_summary_lookup,
|
||||
array $complaints_metric,
|
||||
array $night_wash_metric,
|
||||
array $overtime_metric
|
||||
array $overtime_metric,
|
||||
array $product_target_lookup = []
|
||||
): array {
|
||||
$products = [];
|
||||
foreach ($product_definitions as $definition) {
|
||||
@@ -1448,6 +1528,12 @@ class departmentDailyReportsRoute
|
||||
'state' => 'ready',
|
||||
'value' => (int)($product_summary['quantity'] ?? 0),
|
||||
'out_of' => (int)($product_summary['out_of'] ?? 0),
|
||||
'target_percentage' => array_key_exists($product_id, $product_target_lookup)
|
||||
? (float)$product_target_lookup[$product_id]
|
||||
: null,
|
||||
'target_department_id' => array_key_exists($product_id, $product_target_lookup)
|
||||
? (int)$department_ids[0]
|
||||
: null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1506,6 +1592,87 @@ class departmentDailyReportsRoute
|
||||
return array_values($normalized);
|
||||
}
|
||||
|
||||
private function isDailyReportProductId(int $product_id): bool
|
||||
{
|
||||
return in_array(
|
||||
$product_id,
|
||||
array_map(static fn(array $definition): int => (int)$definition['product_id'], $this->getDailyReportProductDefinitions()),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{valid:bool,value:?float,message:string}
|
||||
*/
|
||||
private function parseDailyReportProductTargetPercentage(mixed $target_percentage): array
|
||||
{
|
||||
if ($target_percentage === null) {
|
||||
return ['valid' => true, 'value' => null, 'message' => ''];
|
||||
}
|
||||
|
||||
if (is_string($target_percentage)) {
|
||||
$target_percentage = trim($target_percentage);
|
||||
if ($target_percentage === '') {
|
||||
return ['valid' => true, 'value' => null, 'message' => ''];
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_int($target_percentage) && !is_float($target_percentage) && !(is_string($target_percentage) && is_numeric($target_percentage))) {
|
||||
return ['valid' => false, 'value' => null, 'message' => 'Parameter target_percentage must be numeric, null, or empty'];
|
||||
}
|
||||
|
||||
$target_percentage = round((float)$target_percentage, 1);
|
||||
if ($target_percentage < 0.0 || $target_percentage > 100.0) {
|
||||
return ['valid' => false, 'value' => null, 'message' => 'Parameter target_percentage must be between 0 and 100'];
|
||||
}
|
||||
|
||||
return ['valid' => true, 'value' => $target_percentage, 'message' => ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array{product_id:int,slug:string,title:string}> $product_definitions
|
||||
* @return array<int,float>
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getDailyReportProductTargetsForDepartment(int $department_id, array $product_definitions): array
|
||||
{
|
||||
$department_variables = (new department_variables_o())->selectDepartment($department_id);
|
||||
$targets = [];
|
||||
|
||||
foreach ($product_definitions as $definition) {
|
||||
$product_id = (int)$definition['product_id'];
|
||||
$stored_target = $department_variables->getVariable($this->dailyReportProductTargetVariableKey($product_id));
|
||||
|
||||
if ($stored_target === null || $stored_target === '' || !is_numeric($stored_target)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targets[$product_id] = round((float)$stored_target, 1);
|
||||
}
|
||||
|
||||
return $targets;
|
||||
}
|
||||
|
||||
protected function clearDailyReportProductTarget(department_variables_o $department_variables, string $target_key): void
|
||||
{
|
||||
$existing_targets = $department_variables->getFieldsWhere([
|
||||
'department_id' => $department_variables->department_id,
|
||||
'variable' => $target_key,
|
||||
], ['id']);
|
||||
|
||||
if (!$existing_targets) {
|
||||
return;
|
||||
}
|
||||
|
||||
department_variables_o::delete_object('department_variables', (int)$existing_targets[0]['id']);
|
||||
$department_variables->objectChanged();
|
||||
}
|
||||
|
||||
private function dailyReportProductTargetVariableKey(int $product_id): string
|
||||
{
|
||||
return 'daily_report_product_target_percentage_' . $product_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,array{product_id:int,slug:string,title:string}>
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
usesApiSuite();
|
||||
|
||||
function daily_report_product_from_overview(array $overview, int $productId): array
|
||||
{
|
||||
foreach (($overview['products'] ?? []) as $product) {
|
||||
if ((int)($product['product_id'] ?? 0) === $productId) {
|
||||
return $product;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
it('stores clears and permission-gates department daily report product targets', function (): void {
|
||||
api_test_covers('PUT /departments/daily-reports/product-targets', 'happy');
|
||||
api_test_covers('GET /departments/daily-reports/overview', 'happy');
|
||||
|
||||
$department = api_fixtures()->createDepartment([
|
||||
'name' => 'Daily Report Product Target ' . uniqid('', false),
|
||||
]);
|
||||
$departmentId = (int)$department['id'];
|
||||
$editorPermissions = [
|
||||
'list_department_daily_reports',
|
||||
'list_bookings',
|
||||
'set_department_daily_report_product_targets',
|
||||
'department_access_' . $departmentId,
|
||||
];
|
||||
$editorSession = api_fixtures()->createUserSession($editorPermissions);
|
||||
$viewerSession = api_fixtures()->createUserSession([
|
||||
'list_department_daily_reports',
|
||||
'list_bookings',
|
||||
'department_access_' . $departmentId,
|
||||
]);
|
||||
|
||||
try {
|
||||
$saveResponse = api_client()->put('/departments/daily-reports/product-targets', [
|
||||
'department_id' => $departmentId,
|
||||
'product_id' => 24,
|
||||
'target_percentage' => 47.55,
|
||||
], $editorSession['headers']);
|
||||
|
||||
$saveResponse
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
expect($saveResponse->data())->toMatchArray([
|
||||
'department_id' => $departmentId,
|
||||
'product_id' => 24,
|
||||
'target_percentage' => 47.6,
|
||||
]);
|
||||
|
||||
$overviewResponse = api_client()->get(
|
||||
'/departments/daily-reports/overview?date=2026-07-06&department_ids=' . $departmentId,
|
||||
$editorSession['headers']
|
||||
);
|
||||
|
||||
$overviewResponse
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$editorProduct = daily_report_product_from_overview($overviewResponse->data(), 24);
|
||||
expect($editorProduct['target_percentage'])->toBe(47.6);
|
||||
expect($editorProduct['target_department_id'])->toBe($departmentId);
|
||||
|
||||
$viewerOverviewResponse = api_client()->get(
|
||||
'/departments/daily-reports/overview?date=2026-07-06&department_ids=' . $departmentId,
|
||||
$viewerSession['headers']
|
||||
);
|
||||
|
||||
$viewerOverviewResponse
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$viewerProduct = daily_report_product_from_overview($viewerOverviewResponse->data(), 24);
|
||||
expect($viewerProduct['target_percentage'])->toBeNull();
|
||||
expect($viewerProduct['target_department_id'])->toBeNull();
|
||||
|
||||
$clearResponse = api_client()->put('/departments/daily-reports/product-targets', [
|
||||
'department_id' => $departmentId,
|
||||
'product_id' => 24,
|
||||
'target_percentage' => null,
|
||||
], $editorSession['headers']);
|
||||
|
||||
$clearResponse
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
expect($clearResponse->data())->toMatchArray([
|
||||
'department_id' => $departmentId,
|
||||
'product_id' => 24,
|
||||
'target_percentage' => null,
|
||||
]);
|
||||
|
||||
$clearedOverviewResponse = api_client()->get(
|
||||
'/departments/daily-reports/overview?date=2026-07-06&department_ids=' . $departmentId,
|
||||
$editorSession['headers']
|
||||
);
|
||||
|
||||
$clearedProduct = daily_report_product_from_overview($clearedOverviewResponse->data(), 24);
|
||||
expect($clearedProduct['target_percentage'])->toBeNull();
|
||||
expect($clearedProduct['target_department_id'])->toBeNull();
|
||||
} finally {
|
||||
api_client()->put('/departments/daily-reports/product-targets', [
|
||||
'department_id' => $departmentId,
|
||||
'product_id' => 24,
|
||||
'target_percentage' => null,
|
||||
], $editorSession['headers']);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects product target updates without permission access or valid input', function (): void {
|
||||
api_test_covers('PUT /departments/daily-reports/product-targets', 'auth');
|
||||
api_test_covers('PUT /departments/daily-reports/product-targets', 'failure');
|
||||
|
||||
$department = api_fixtures()->createDepartment();
|
||||
$departmentId = (int)$department['id'];
|
||||
|
||||
$missingPermissionSession = api_fixtures()->createUserSession([
|
||||
'department_access_' . $departmentId,
|
||||
]);
|
||||
|
||||
api_client()->put('/departments/daily-reports/product-targets', [
|
||||
'department_id' => $departmentId,
|
||||
'product_id' => 24,
|
||||
'target_percentage' => 50,
|
||||
], $missingPermissionSession['headers'])
|
||||
->assertStatus(403)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMissingPermissions(['set_department_daily_report_product_targets']);
|
||||
|
||||
$missingDepartmentAccessSession = api_fixtures()->createUserSession([
|
||||
'set_department_daily_report_product_targets',
|
||||
]);
|
||||
|
||||
api_client()->put('/departments/daily-reports/product-targets', [
|
||||
'department_id' => $departmentId,
|
||||
'product_id' => 24,
|
||||
'target_percentage' => 50,
|
||||
], $missingDepartmentAccessSession['headers'])
|
||||
->assertStatus(403)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMissingPermissions(['department_access_' . $departmentId]);
|
||||
|
||||
$editorSession = api_fixtures()->createUserSession([
|
||||
'set_department_daily_report_product_targets',
|
||||
'department_access_' . $departmentId,
|
||||
]);
|
||||
|
||||
api_client()->put('/departments/daily-reports/product-targets', [
|
||||
'department_id' => $departmentId,
|
||||
'product_id' => 999999,
|
||||
'target_percentage' => 50,
|
||||
], $editorSession['headers'])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage('Invalid daily report product_id');
|
||||
|
||||
api_client()->put('/departments/daily-reports/product-targets', [
|
||||
'department_id' => $departmentId,
|
||||
'product_id' => 24,
|
||||
'target_percentage' => 101,
|
||||
], $editorSession['headers'])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage('Parameter target_percentage must be between 0 and 100');
|
||||
});
|
||||
+5
@@ -31,10 +31,15 @@ it('documents the daily report overview endpoint and reusable schemas in openapi
|
||||
$content = department_daily_reports_openapi_content_or_skip();
|
||||
|
||||
expect($content)->toContain('/departments/daily-reports/overview:');
|
||||
expect($content)->toContain('/departments/daily-reports/product-targets:');
|
||||
expect($content)->toContain('/superuser/departments/{id}/overview:');
|
||||
expect($content)->toContain('operationId: getDailyReportOverview');
|
||||
expect($content)->toContain('operationId: setDailyReportProductTarget');
|
||||
expect($content)->toContain('operationId: getSuperuserDepartmentOverview');
|
||||
expect($content)->toContain('DepartmentDailyReportOverviewResponse:');
|
||||
expect($content)->toContain('DepartmentDailyReportProductTargetRequest:');
|
||||
expect($content)->toContain('set_department_daily_report_product_targets');
|
||||
expect($content)->toContain('target_percentage');
|
||||
expect($content)->toContain('SuperuserDepartmentOverviewResponse:');
|
||||
expect($content)->toContain('DepartmentDailyReportMetric:');
|
||||
expect($content)->toContain('DepartmentDailyReportProductTile:');
|
||||
|
||||
+42
@@ -123,6 +123,7 @@ final class DepartmentDailyReportsOverviewRouteDouble extends departmentDailyRep
|
||||
public object $complaints_repository;
|
||||
public array $opening_hours = [];
|
||||
public array $departments = [];
|
||||
public array $product_targets_by_department = [];
|
||||
public array $workfeed_departments = [];
|
||||
public array $workfeed_shifts = [];
|
||||
public department_outside_hours_statistics_service $outside_hours_service;
|
||||
@@ -161,6 +162,11 @@ final class DepartmentDailyReportsOverviewRouteDouble extends departmentDailyRep
|
||||
{
|
||||
return $this->outside_hours_service;
|
||||
}
|
||||
|
||||
protected function getDailyReportProductTargetsForDepartment(int $department_id, array $product_definitions): array
|
||||
{
|
||||
return $this->product_targets_by_department[$department_id] ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
function fake_daily_report_department(int $id, string $name, array $variables = []): object
|
||||
@@ -250,6 +256,8 @@ it('builds the overview payload from batched repository data with deterministic
|
||||
expect($overview['products'][0]['slug'])->toBe('spot-free-lastbil');
|
||||
expect($overview['products'][0]['title'])->toBe('Spot Free (Lastbil)');
|
||||
expect($overview['products'][0]['value'])->toBe(3);
|
||||
expect($overview['products'][0]['target_percentage'])->toBeNull();
|
||||
expect($overview['products'][0]['target_department_id'])->toBeNull();
|
||||
expect($overview['products'][1]['title'])->toBe('Fælg flex pr. enhed');
|
||||
expect($overview['products'][1]['value'])->toBe(2);
|
||||
expect(array_column($overview['products'], 'title'))->toBe([
|
||||
@@ -262,6 +270,38 @@ it('builds the overview payload from batched repository data with deterministic
|
||||
]);
|
||||
});
|
||||
|
||||
it('adds product targets to single department overview payloads when requested', function (): void {
|
||||
$repository = new FakeDailyReportRepository();
|
||||
$repository->product_overview = [
|
||||
24 => ['product_id' => 24, 'quantity' => 3, 'out_of' => 14],
|
||||
25 => ['product_id' => 25, 'quantity' => 2, 'out_of' => 14],
|
||||
];
|
||||
|
||||
$route = new DepartmentDailyReportsOverviewRouteDouble();
|
||||
$route->repository = $repository;
|
||||
$route->complaints_repository = new FakeDailyReportComplaintsRepository();
|
||||
$route->outside_hours_service = new FakeOutsideHoursStatisticsService();
|
||||
$route->product_targets_by_department = [
|
||||
7 => [
|
||||
24 => 75.5,
|
||||
25 => 0.0,
|
||||
],
|
||||
];
|
||||
|
||||
$overview = department_daily_reports_route_invoke_private($route, 'buildDailyReportOverview', [[7], '2026-03-23', '2026-03-23', true]);
|
||||
$products_by_id = [];
|
||||
foreach ($overview['products'] as $product) {
|
||||
$products_by_id[$product['product_id']] = $product;
|
||||
}
|
||||
|
||||
expect($products_by_id[24]['target_percentage'])->toBe(75.5);
|
||||
expect($products_by_id[24]['target_department_id'])->toBe(7);
|
||||
expect($products_by_id[25]['target_percentage'])->toBe(0.0);
|
||||
expect($products_by_id[25]['target_department_id'])->toBe(7);
|
||||
expect($products_by_id[27]['target_percentage'])->toBeNull();
|
||||
expect($products_by_id[27]['target_department_id'])->toBeNull();
|
||||
});
|
||||
|
||||
it('marks overtime unavailable when not every selected department can be mapped to workfeed', function (): void {
|
||||
$repository = new FakeDailyReportRepository();
|
||||
|
||||
@@ -342,8 +382,10 @@ it('wires the overview route to batched repository methods and overview path', f
|
||||
$objectContent = (string)file_get_contents(app_path('objects/department_daily_reports_o.php'));
|
||||
|
||||
expect($routeContent)->toContain('/departments/daily-reports/overview');
|
||||
expect($routeContent)->toContain('/departments/daily-reports/product-targets');
|
||||
expect($routeContent)->toContain('/superuser/departments/{id}/overview');
|
||||
expect($routeContent)->toContain('superuser_fetch_department');
|
||||
expect($routeContent)->toContain('set_department_daily_report_product_targets');
|
||||
expect($routeContent)->toContain('/departments/daily-reports/complaints');
|
||||
expect($routeContent)->toContain('outsideHoursStatisticsService');
|
||||
expect($routeContent)->toContain('dailyReportComplaintsRepository');
|
||||
|
||||
Reference in New Issue
Block a user