diff --git a/services/nginx/app/openapi.yaml b/services/nginx/app/openapi.yaml index dd1674c1..7ff2474a 100644 --- a/services/nginx/app/openapi.yaml +++ b/services/nginx/app/openapi.yaml @@ -12523,6 +12523,40 @@ paths: application/json: 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: put: tags: @@ -21551,6 +21585,21 @@ components: data: $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: type: object properties: diff --git a/services/nginx/app/routes/departmentDailyReportsRoute.php b/services/nginx/app/routes/departmentDailyReportsRoute.php index a54e74ec..53042661 100644 --- a/services/nginx/app/routes/departmentDailyReportsRoute.php +++ b/services/nginx/app/routes/departmentDailyReportsRoute.php @@ -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 () { global $response; $this->requirePermission('list_department_daily_reports'); diff --git a/services/nginx/app/tests/Api/SuperuserDepartmentOverviewApiTest.php b/services/nginx/app/tests/Api/SuperuserDepartmentOverviewApiTest.php new file mode 100644 index 00000000..1c892f93 --- /dev/null +++ b/services/nginx/app/tests/Api/SuperuserDepartmentOverviewApiTest.php @@ -0,0 +1,100 @@ +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'); +}); diff --git a/services/nginx/app/tests/Api/api_coverage_manifest.php b/services/nginx/app/tests/Api/api_coverage_manifest.php index 12b3ff8b..6de06af1 100644 --- a/services/nginx/app/tests/Api/api_coverage_manifest.php +++ b/services/nginx/app/tests/Api/api_coverage_manifest.php @@ -19,6 +19,7 @@ return [ 'GET /branding', 'POST /branding', 'PUT /branding', + 'GET /superuser/departments/{id}/overview', 'PUT /superuser/department/branding', 'POST /bird/voice/calls/webhook/inbound', ], diff --git a/services/nginx/app/tests/Support/Api/ApiSchemaBootstrap.php b/services/nginx/app/tests/Support/Api/ApiSchemaBootstrap.php index 00f8338e..c602d32a 100644 --- a/services/nginx/app/tests/Support/Api/ApiSchemaBootstrap.php +++ b/services/nginx/app/tests/Support/Api/ApiSchemaBootstrap.php @@ -112,6 +112,46 @@ CREATE TABLE IF NOT EXISTS `department_variables` ( KEY `idx_department_variables_department_id` (`department_id`), KEY `idx_department_variables_variable` (`variable`) ) 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, 'department_gates' => <<<'SQL' CREATE TABLE IF NOT EXISTS `department_gates` ( diff --git a/services/nginx/app/tests/Unit/DailyReports/DepartmentDailyReportsOverviewOpenApiSpecTest.php b/services/nginx/app/tests/Unit/DailyReports/DepartmentDailyReportsOverviewOpenApiSpecTest.php index 40855994..3cd74066 100644 --- a/services/nginx/app/tests/Unit/DailyReports/DepartmentDailyReportsOverviewOpenApiSpecTest.php +++ b/services/nginx/app/tests/Unit/DailyReports/DepartmentDailyReportsOverviewOpenApiSpecTest.php @@ -31,8 +31,11 @@ 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('/superuser/departments/{id}/overview:'); expect($content)->toContain('operationId: getDailyReportOverview'); + expect($content)->toContain('operationId: getSuperuserDepartmentOverview'); expect($content)->toContain('DepartmentDailyReportOverviewResponse:'); + expect($content)->toContain('SuperuserDepartmentOverviewResponse:'); expect($content)->toContain('DepartmentDailyReportMetric:'); expect($content)->toContain('DepartmentDailyReportProductTile:'); expect($content)->toContain('- name: department_ids'); diff --git a/services/nginx/app/tests/Unit/DailyReports/DepartmentDailyReportsOverviewRouteTest.php b/services/nginx/app/tests/Unit/DailyReports/DepartmentDailyReportsOverviewRouteTest.php index 49af59ec..cb984dbf 100644 --- a/services/nginx/app/tests/Unit/DailyReports/DepartmentDailyReportsOverviewRouteTest.php +++ b/services/nginx/app/tests/Unit/DailyReports/DepartmentDailyReportsOverviewRouteTest.php @@ -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')); 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('outsideHoursStatisticsService'); expect($routeContent)->toContain('dailyReportComplaintsRepository');