Compare commits

...
7 changed files with 242 additions and 0 deletions
+49
View File
@@ -12523,6 +12523,40 @@ paths:
application/json: application/json:
schema: {} schema: {}
/superuser/departments/{id}/overview:
get:
tags:
- Departments
summary: Get superuser department overview
description: Returns the selected department metadata and operational overview metrics for a superuser without requiring scoped department access.
operationId: getSuperuserDepartmentOverview
parameters:
- name: id
in: path
required: true
schema: {type: integer}
- name: date
in: query
required: true
schema: {type: string}
- name: date_to
in: query
required: false
schema: {type: string}
responses:
'200':
description: Superuser department overview loaded successfully
content:
application/json:
schema:
$ref: '#/components/schemas/SuperuserDepartmentOverviewResponse'
'400':
$ref: '#/components/responses/BadRequest'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
/superuser/department/branding: /superuser/department/branding:
put: put:
tags: tags:
@@ -21550,6 +21584,21 @@ components:
data: data:
$ref: '#/components/schemas/DepartmentDailyReportOverviewPayload' $ref: '#/components/schemas/DepartmentDailyReportOverviewPayload'
SuperuserDepartmentOverviewPayload:
type: object
properties:
department:
$ref: '#/components/schemas/Department'
overview:
$ref: '#/components/schemas/DepartmentDailyReportOverviewPayload'
SuperuserDepartmentOverviewResponse:
type: object
properties:
success: { type: boolean, example: true }
data:
$ref: '#/components/schemas/SuperuserDepartmentOverviewPayload'
DepartmentDailyReportTransactionCountPayload: DepartmentDailyReportTransactionCountPayload:
type: object type: object
properties: properties:
@@ -812,6 +812,53 @@ class departmentDailyReportsRoute
] ]
); );
$this->get('/superuser/departments/{id}/overview', function () {
global $response;
$this->requirePermission('superuser_fetch_department');
$user = (new authentication())->get_user();
if (!$user) {
(new logs_o())->add('departments', 'global', 1, 0, 'SUPERUSER_DEPARTMENT_OVERVIEW', 'No user found, or invalid session');
$response->error('Invalid session', 400);
return;
}
$department_id_param = (string)($this->fromRoute('id') ?? '');
if (!ctype_digit($department_id_param) || (int)$department_id_param <= 0) {
$response->error('Parameter id must be a positive integer', 400);
return;
}
self::requireParameters([
'date',
]);
self::validateDateLocally();
$date_to = $this->getDate_to();
$department_id = (int)$department_id_param;
$department = (new departments_o())->select($department_id);
if (!$department->exists()) {
$response->error('Department not found', 404);
return;
}
(new logs_o())->add('departments', 'global', 1, $user->id, 'SUPERUSER_DEPARTMENT_OVERVIEW', 'Successfully loaded superuser department overview');
$response->success([
'department' => $department->asArray(['slack_webhook' => false]),
'overview' => $this->buildDailyReportOverview(
[$department_id],
(string)self::getParameter('date'),
$date_to
),
]);
},
[
'superuser_fetch_department' => 'Get the superuser department overview'
]
);
$this->get('/departments/daily-reports/overview', function () { $this->get('/departments/daily-reports/overview', function () {
global $response; global $response;
$this->requirePermission('list_department_daily_reports'); $this->requirePermission('list_department_daily_reports');
@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
usesApiSuite();
it('loads a single department overview for superusers without department scoped access', function (): void {
api_test_covers('GET /superuser/departments/{id}/overview', 'happy');
$department = api_fixtures()->createDepartment([
'name' => 'Overview Department ' . uniqid('', false),
'description' => 'Department overview fixture',
'economic_department_id' => 42,
'visible' => 1,
]);
$departmentRow = api_fixtures()->fetchRowById('departments', (int)$department['id']);
$session = api_fixtures()->createUserSession([
'superuser_fetch_department',
]);
$response = api_client()->get(
'/superuser/departments/' . $department['id'] . '/overview?date=2026-07-06&date_to=2026-07-06',
$session['headers']
);
$response
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$payload = $response->data();
expect($payload)->toBeArray();
expect($payload['department'])
->toBeArray()
->toHaveKey('id', (int)$department['id'])
->toHaveKey('name', $departmentRow['name'])
->toHaveKey('description', 'Department overview fixture')
->toHaveKey('economic_department_id', 42);
expect($payload['overview'])
->toBeArray()
->toHaveKey('department_ids', [(int)$department['id']])
->toHaveKey('date', '2026-07-06')
->toHaveKey('date_to', '2026-07-06');
expect($payload['overview']['metrics'])
->toBeArray()
->toHaveKeys([
'bookings',
'complaints',
'night_washes',
'revenue',
'washes',
'products_sold',
'transactions',
'water_usage',
'overtime',
]);
expect($payload['overview']['metrics']['revenue']['state'])->toBe('ready');
expect($payload['overview']['metrics']['revenue']['value'])->toBe(0);
expect($payload['overview']['products'])->toBeArray();
});
it('rejects superuser department overview requests without permission or valid input', function (): void {
api_test_covers('GET /superuser/departments/{id}/overview', 'auth');
api_test_covers('GET /superuser/departments/{id}/overview', 'failure');
$department = api_fixtures()->createDepartment();
$unauthorizedSession = api_fixtures()->createUserSession([]);
api_client()->get(
'/superuser/departments/' . $department['id'] . '/overview?date=2026-07-06',
$unauthorizedSession['headers']
)
->assertStatus(403)
->assertEnvelope()
->assertSuccess(false)
->assertMissingPermissions(['superuser_fetch_department']);
$session = api_fixtures()->createUserSession(['superuser_fetch_department']);
api_client()->get('/superuser/departments/bad/overview?date=2026-07-06', $session['headers'])
->assertStatus(400)
->assertEnvelope()
->assertSuccess(false)
->assertMessage('Parameter id must be a positive integer');
api_client()->get('/superuser/departments/' . $department['id'] . '/overview', $session['headers'])
->assertStatus(400)
->assertEnvelope()
->assertSuccess(false)
->assertMessage('Missing required parameters: date');
api_client()->get('/superuser/departments/99999999/overview?date=2026-07-06', $session['headers'])
->assertStatus(404)
->assertEnvelope()
->assertSuccess(false)
->assertMessage('Department not found');
});
@@ -19,6 +19,7 @@ return [
'GET /branding', 'GET /branding',
'POST /branding', 'POST /branding',
'PUT /branding', 'PUT /branding',
'GET /superuser/departments/{id}/overview',
'PUT /superuser/department/branding', 'PUT /superuser/department/branding',
'POST /bird/voice/calls/webhook/inbound', 'POST /bird/voice/calls/webhook/inbound',
], ],
@@ -111,6 +111,46 @@ CREATE TABLE IF NOT EXISTS `department_variables` (
KEY `idx_department_variables_department_id` (`department_id`), KEY `idx_department_variables_department_id` (`department_id`),
KEY `idx_department_variables_variable` (`variable`) KEY `idx_department_variables_variable` (`variable`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
SQL,
'department_daily_reports' => <<<'SQL'
CREATE TABLE IF NOT EXISTS `department_daily_reports` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`department_id` INT NOT NULL,
`water_usage` INT NOT NULL DEFAULT 0,
`water_usage_morning` INT NOT NULL DEFAULT 0,
`notes` TEXT NULL,
`filled_by` INT NOT NULL DEFAULT 0,
`created_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_department_daily_reports_department_id` (`department_id`),
KEY `idx_department_daily_reports_created_at` (`created_at`),
KEY `idx_department_daily_reports_department_created_at` (`department_id`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
SQL,
'department_time_bookings_opening_hours' => <<<'SQL'
CREATE TABLE IF NOT EXISTS `department_time_bookings_opening_hours` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`department` INT NOT NULL,
`monday_start` TIME NULL,
`monday_end` TIME NULL,
`tuesday_start` TIME NULL,
`tuesday_end` TIME NULL,
`wednesday_start` TIME NULL,
`wednesday_end` TIME NULL,
`thursday_start` TIME NULL,
`thursday_end` TIME NULL,
`friday_start` TIME NULL,
`friday_end` TIME NULL,
`saturday_start` TIME NULL,
`saturday_end` TIME NULL,
`sunday_start` TIME NULL,
`sunday_end` TIME NULL,
`created_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_department_time_bookings_opening_hours_department` (`department`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
SQL, SQL,
'department_gates' => <<<'SQL' 'department_gates' => <<<'SQL'
CREATE TABLE IF NOT EXISTS `department_gates` ( CREATE TABLE IF NOT EXISTS `department_gates` (
@@ -31,8 +31,11 @@ it('documents the daily report overview endpoint and reusable schemas in openapi
$content = department_daily_reports_openapi_content_or_skip(); $content = department_daily_reports_openapi_content_or_skip();
expect($content)->toContain('/departments/daily-reports/overview:'); expect($content)->toContain('/departments/daily-reports/overview:');
expect($content)->toContain('/superuser/departments/{id}/overview:');
expect($content)->toContain('operationId: getDailyReportOverview'); expect($content)->toContain('operationId: getDailyReportOverview');
expect($content)->toContain('operationId: getSuperuserDepartmentOverview');
expect($content)->toContain('DepartmentDailyReportOverviewResponse:'); expect($content)->toContain('DepartmentDailyReportOverviewResponse:');
expect($content)->toContain('SuperuserDepartmentOverviewResponse:');
expect($content)->toContain('DepartmentDailyReportMetric:'); expect($content)->toContain('DepartmentDailyReportMetric:');
expect($content)->toContain('DepartmentDailyReportProductTile:'); expect($content)->toContain('DepartmentDailyReportProductTile:');
expect($content)->toContain('- name: department_ids'); expect($content)->toContain('- name: department_ids');
@@ -342,6 +342,8 @@ 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')); $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/overview');
expect($routeContent)->toContain('/superuser/departments/{id}/overview');
expect($routeContent)->toContain('superuser_fetch_department');
expect($routeContent)->toContain('/departments/daily-reports/complaints'); expect($routeContent)->toContain('/departments/daily-reports/complaints');
expect($routeContent)->toContain('outsideHoursStatisticsService'); expect($routeContent)->toContain('outsideHoursStatisticsService');
expect($routeContent)->toContain('dailyReportComplaintsRepository'); expect($routeContent)->toContain('dailyReportComplaintsRepository');