- Swap `send_message` with `send_webhook_message` to fix departmental Slack notification issues. - Adjust `COUNT_ONLY` in `renderDanishPeriodSummary` to exclude targets for raw count-focused alerts.
380 lines
18 KiB
PHP
380 lines
18 KiB
PHP
<?php
|
|
|
|
namespace routes;
|
|
|
|
use classes\authentication;
|
|
use classes\response;
|
|
use classes\email as Email;
|
|
use classes\gatewayapi as GatewayAPI;
|
|
use classes\slack as Slack;
|
|
use goals\classes\goals_criteria;
|
|
use goals\services\goals_progress_alert_renderer;
|
|
use goals\helpers\goals_criteria_progress_alert_destination as Dest;
|
|
use objects\departments_o;
|
|
use objects\department_goals_o;
|
|
use traits\route_t;
|
|
|
|
class departmentGoalsRoute
|
|
{
|
|
use route_t;
|
|
|
|
public function run(): void
|
|
{
|
|
// List or get single department goal(s)
|
|
$this->get('/goals/department', function () {
|
|
global /** @var response $response */ $response;
|
|
$this->requirePermission('goals_department_list');
|
|
|
|
// Current user and access helpers
|
|
$user = (new authentication())->get_user();
|
|
if (!$user) {
|
|
$response->error('Unauthorized', 401);
|
|
}
|
|
$isSuperuser = method_exists($user, 'hasPermission') && $user->hasPermission('superuser');
|
|
$userDepartments = method_exists($user, 'getGroup') ? (array)$user->getGroup()->getDepartments() : [];
|
|
|
|
$hasAllDepartments = function (array $goalDepartments) use ($userDepartments): bool {
|
|
// Ensure ints
|
|
$goalDepartments = array_map('intval', $goalDepartments);
|
|
$userDepartments = array_map('intval', $userDepartments);
|
|
return count(array_diff($goalDepartments, $userDepartments)) === 0;
|
|
};
|
|
|
|
$goals = new department_goals_o();
|
|
|
|
// Single by id
|
|
if (self::isParametersSet(['id'])) {
|
|
$id = self::getParameter('id');
|
|
if (!is_numeric($id)) {
|
|
$response->error('id must be numeric', 400);
|
|
}
|
|
$goal = (new department_goals_o())->select((int)$id);
|
|
if (!$goal->exists()) {
|
|
$response->error('Goal not found', 404);
|
|
}
|
|
// Access control: must have all departments unless superuser
|
|
if (!$isSuperuser && !$hasAllDepartments((array)$goal->departments->value())) {
|
|
$response->error('You are not allowed to access this goal', 403);
|
|
}
|
|
$response->success($goal->asArray());
|
|
}
|
|
|
|
// List with pagination if set (page, limit, search, filters, order supported by db_object_t)
|
|
$list = $goals->listObjectsWithPaginationIfSet(
|
|
function ($row) {
|
|
return (new department_goals_o())->select((int)$row['id'])->asArray();
|
|
}
|
|
);
|
|
// Filter unauthorized results unless superuser
|
|
if (!$isSuperuser && is_array($list)) {
|
|
$list = array_values(array_filter($list, function ($item) use ($hasAllDepartments) {
|
|
$deps = isset($item['departments']) && is_array($item['departments']) ? $item['departments'] : [];
|
|
return $hasAllDepartments($deps);
|
|
}));
|
|
}
|
|
$response->success($list);
|
|
}, [
|
|
'goals_department_list' => 'List department goals or retrieve a single goal when id is provided'
|
|
]);
|
|
|
|
// Create a new department goal
|
|
$this->post('/goals/department', function () {
|
|
global /** @var response $response */ $response;
|
|
$this->requirePermission('goals_department_create');
|
|
|
|
// Authenticated user (creator)
|
|
$user = (new authentication())->get_user();
|
|
if (!$user) {
|
|
$response->error('Unauthorized', 401);
|
|
}
|
|
$isSuperuser = method_exists($user, 'hasPermission') && $user->hasPermission('superuser');
|
|
$userDepartments = method_exists($user, 'getGroup') ? (array)$user->getGroup()->getDepartments() : [];
|
|
|
|
// Validate input
|
|
$this->requireParameters(['departments', 'criteria']);
|
|
$departmentsInput = self::getParameter('departments');
|
|
$criteriaInput = self::getParameter('criteria');
|
|
$this->requireType($departmentsInput, 'array');
|
|
$this->requireTypeIn($criteriaInput, ['array', 'object']);
|
|
|
|
// Build departments objects array
|
|
$departments = [];
|
|
foreach ($departmentsInput as $deptId) {
|
|
if (!is_numeric($deptId)) {
|
|
$response->error('All department ids must be numeric', 400);
|
|
}
|
|
$dept = (new departments_o())->select((int)$deptId);
|
|
if (!$dept->exists()) {
|
|
$response->error('Department not found: ' . $deptId, 404);
|
|
}
|
|
$departments[] = $dept;
|
|
}
|
|
|
|
// Access control: creator must have all departments unless superuser
|
|
if (!$isSuperuser) {
|
|
$goalDepartments = array_map(fn($d) => (int)$d->id, $departments);
|
|
$missing = array_diff($goalDepartments, array_map('intval', $userDepartments));
|
|
if (count($missing) > 0) {
|
|
$response->error('You are not allowed to create goals for one or more selected departments', 403);
|
|
}
|
|
}
|
|
|
|
// Parse criteria (preserve unicode characters like æ, ø, å)
|
|
$criteria = goals_criteria::fromJson(json_encode($criteriaInput, JSON_UNESCAPED_UNICODE));
|
|
// Create the goal
|
|
$goal = (new department_goals_o())->add($user, $departments, $criteria);
|
|
$response->success($goal->asArray(), 201);
|
|
}, [
|
|
'goals_department_create' => 'Create a new department goal'
|
|
]);
|
|
|
|
// Update an existing department goal
|
|
$this->put('/goals/department', function () {
|
|
global /** @var response $response */ $response;
|
|
$this->requirePermission('goals_department_update');
|
|
|
|
$this->requireParameters(['id']);
|
|
$id = self::getParameter('id');
|
|
if (!is_numeric($id)) {
|
|
$response->error('id must be numeric', 400);
|
|
}
|
|
$goal = (new department_goals_o())->select((int)$id);
|
|
if (!$goal->exists()) {
|
|
$response->error('Goal not found', 404);
|
|
}
|
|
|
|
// Current user and access helpers
|
|
$user = (new authentication())->get_user();
|
|
if (!$user) {
|
|
$response->error('Unauthorized', 401);
|
|
}
|
|
$isSuperuser = method_exists($user, 'hasPermission') && $user->hasPermission('superuser');
|
|
$isCreator = ((int)$goal->created_by->value()) === (int)$user->id;
|
|
$userDepartments = method_exists($user, 'getGroup') ? (array)$user->getGroup()->getDepartments() : [];
|
|
$hasAllDepartments = function (array $goalDepartments, array $userDepartments) {
|
|
$goalDepartments = array_map('intval', $goalDepartments);
|
|
$userDepartments = array_map('intval', $userDepartments);
|
|
return count(array_diff($goalDepartments, $userDepartments)) === 0;
|
|
};
|
|
|
|
// If not superuser or creator, require access to existing goal departments
|
|
if (!$isSuperuser && !$isCreator && !$hasAllDepartments((array)$goal->departments->value(), $userDepartments)) {
|
|
$response->error('You are not allowed to update this goal', 403);
|
|
}
|
|
|
|
$dataToUpdate = [];
|
|
|
|
// Optional: departments
|
|
if ($response->isRequestParameterSet('departments')) {
|
|
$departmentsInput = self::getParameter('departments');
|
|
$this->requireType($departmentsInput, 'array');
|
|
foreach ($departmentsInput as $deptId) {
|
|
if (!is_numeric($deptId)) {
|
|
$response->error('All department ids must be numeric', 400);
|
|
}
|
|
}
|
|
// If not superuser or creator, ensure new departments are within user departments
|
|
if (!$isSuperuser && !$isCreator) {
|
|
$missing = array_diff(array_map('intval', $departmentsInput), array_map('intval', $userDepartments));
|
|
if (count($missing) > 0) {
|
|
$response->error('You are not allowed to assign one or more selected departments to this goal', 403);
|
|
}
|
|
}
|
|
// store as ids array in column
|
|
$dataToUpdate['departments'] = array_map('intval', $departmentsInput);
|
|
}
|
|
|
|
// Optional: criteria
|
|
if ($response->isRequestParameterSet('criteria')) {
|
|
$criteriaInput = self::getParameter('criteria');
|
|
$this->requireTypeIn($criteriaInput, ['array', 'object']);
|
|
// Preserve unicode characters when (re)encoding criteria
|
|
$criteria = goals_criteria::fromJson(json_encode($criteriaInput, JSON_UNESCAPED_UNICODE));
|
|
$dataToUpdate['criteria'] = $criteria->toArray();
|
|
}
|
|
|
|
if (empty($dataToUpdate)) {
|
|
$response->error('No updatable fields provided. Allowed: departments, criteria', 400);
|
|
}
|
|
|
|
$goal->update($dataToUpdate);
|
|
|
|
$response->success($goal->asArray());
|
|
}, [
|
|
'goals_department_update' => 'Update an existing department goal'
|
|
]);
|
|
|
|
// Delete a department goal (soft delete if supported)
|
|
$this->delete('/goals/department', function () {
|
|
global /** @var response $response */ $response;
|
|
$this->requirePermission('goals_department_delete');
|
|
|
|
$this->requireParameters(['id']);
|
|
$id = self::getParameter('id');
|
|
if (!is_numeric($id)) {
|
|
$response->error('id must be numeric', 400);
|
|
}
|
|
$goal = (new department_goals_o())->select((int)$id);
|
|
if (!$goal->exists()) {
|
|
$response->error('Goal not found', 404);
|
|
}
|
|
|
|
// Current user and access helpers
|
|
$user = (new authentication())->get_user();
|
|
if (!$user) {
|
|
$response->error('Unauthorized', 401);
|
|
}
|
|
$isSuperuser = method_exists($user, 'hasPermission') && $user->hasPermission('superuser');
|
|
$isCreator = ((int)$goal->created_by->value()) === (int)$user->id;
|
|
$userDepartments = method_exists($user, 'getGroup') ? (array)$user->getGroup()->getDepartments() : [];
|
|
$goalDepartments = (array)$goal->departments->value();
|
|
$hasAll = count(array_diff(array_map('intval', $goalDepartments), array_map('intval', $userDepartments))) === 0;
|
|
|
|
if (!$isSuperuser && !$isCreator && !$hasAll) {
|
|
$response->error('You are not allowed to delete this goal', 403);
|
|
}
|
|
$goal->delete();
|
|
$response->success(['message' => 'Deleted']);
|
|
}, [
|
|
'goals_department_delete' => 'Delete a department goal (soft delete when supported)'
|
|
]);
|
|
|
|
// Send a test progress alert for a department goal
|
|
$this->post('/goals/department/progress-alert/test', function () {
|
|
global /** @var response $response */ $response;
|
|
$this->requirePermission('goals_department_progress_alert_test');
|
|
|
|
// Required: id of the goal
|
|
$this->requireParameters(['id']);
|
|
$id = self::getParameter('id');
|
|
if (!is_numeric($id)) {
|
|
$response->error('id must be numeric', 400);
|
|
}
|
|
|
|
$goal = (new department_goals_o())->select((int)$id);
|
|
if (!$goal->exists()) {
|
|
$response->error('Goal not found', 404);
|
|
}
|
|
|
|
// Authorization: reuse list/get policy — must be superuser or have all goal departments
|
|
$user = (new authentication())->get_user();
|
|
if (!$user) {
|
|
$response->error('Unauthorized', 401);
|
|
}
|
|
$isSuperuser = method_exists($user, 'hasPermission') && $user->hasPermission('superuser');
|
|
$userDepartments = method_exists($user, 'getGroup') ? (array)$user->getGroup()->getDepartments() : [];
|
|
$goalDepartments = (array)$goal->departments->value();
|
|
$hasAll = count(array_diff(array_map('intval', $goalDepartments), array_map('intval', $userDepartments))) === 0;
|
|
if (!$isSuperuser && !$hasAll) {
|
|
$response->error('You are not allowed to send progress alerts for this goal', 403);
|
|
}
|
|
|
|
// Rebuild criteria from stored array
|
|
$criteriaArray = (array)$goal->criteria->value();
|
|
$criteria = goals_criteria::fromJson(json_encode($criteriaArray, JSON_UNESCAPED_UNICODE));
|
|
|
|
// Attach departments to criteria (so renderer can include them)
|
|
$deptObjs = [];
|
|
foreach ($goalDepartments as $deptId) {
|
|
if (is_numeric($deptId)) {
|
|
$dept = (new departments_o())->select((int)$deptId);
|
|
if ($dept->exists()) {
|
|
$deptObjs[] = $dept;
|
|
}
|
|
}
|
|
}
|
|
if (isset($criteria->departments)) {
|
|
$criteria->departments->set($deptObjs);
|
|
}
|
|
|
|
// Allow overriding the destination for testing
|
|
$destination = $criteria->progress_alert_destination ?? Dest::NONE;
|
|
if ($response->isRequestParameterSet('overrideDestination')) {
|
|
$override = (string)self::getParameter('overrideDestination');
|
|
$try = \goals\helpers\goals_criteria_progress_alert_destination::tryFrom($override);
|
|
if ($try !== null) {
|
|
$destination = $try;
|
|
}
|
|
}
|
|
|
|
// Render message
|
|
$message = goals_progress_alert_renderer::render($criteria);
|
|
|
|
// Dispatch according to destination
|
|
$providerResponse = null;
|
|
$target = null;
|
|
try {
|
|
switch ($destination) {
|
|
case Dest::EMAIL:
|
|
$this->requireParameters(['email_to']);
|
|
$to = (string)self::getParameter('email_to');
|
|
$subject = $response->isRequestParameterSet('subject') ? (string)self::getParameter('subject') : 'Goals progress alert';
|
|
(new Email())->sendEmail($to, 'Goals Tester', $subject, $message, '');
|
|
$providerResponse = 'Email queued/sent';
|
|
$target = $to;
|
|
break;
|
|
case Dest::SMS:
|
|
$this->requireParameters(['sms_to']);
|
|
$smsParam = self::getParameter('sms_to');
|
|
$numbers = [];
|
|
if (is_string($smsParam)) {
|
|
$numbers = array_values(array_filter(array_map('trim', preg_split('/[,;]/', $smsParam))));
|
|
} elseif (is_array($smsParam)) {
|
|
$numbers = array_map('strval', $smsParam);
|
|
} else {
|
|
$response->error('sms_to must be string or array', 400);
|
|
}
|
|
(new GatewayAPI())->send($numbers, $message);
|
|
$providerResponse = 'SMS sent';
|
|
$target = $numbers;
|
|
break;
|
|
case Dest::SLACK:
|
|
// Prefer explicit webhook; else allow department_id; else fallback to default webhook
|
|
if ($response->isRequestParameterSet('slack_webhook')) {
|
|
$webhook = (string)self::getParameter('slack_webhook');
|
|
$providerResponse = (new Slack())->send_webhook_message($message, $webhook);
|
|
$target = $webhook;
|
|
} elseif ($response->isRequestParameterSet('department_id')) {
|
|
$deptId = self::getParameter('department_id');
|
|
if (!is_numeric($deptId)) {
|
|
$response->error('department_id must be numeric', 400);
|
|
}
|
|
(new Slack())->send_department_booking_notification((int)$deptId, $message);
|
|
$providerResponse = 'Slack message sent to department webhook';
|
|
$target = (int)$deptId;
|
|
} elseif (isset($criteria->departments) && is_array($criteria->departments->list()) && count($criteria->departments->list()) > 0) {
|
|
// Loop through departments and send notifications
|
|
/** @var departments_o $dept */
|
|
foreach ( $criteria->departments->list() as $dept) {
|
|
$slackWebhook = $dept->slack_webhook->value();
|
|
if (empty($slackWebhook)) {
|
|
continue;
|
|
}
|
|
//(new Slack())->send_message((string)goals_progress_alert_renderer::render($criteria, $dept), (string)$dept->name->value());
|
|
(new Slack())->send_webhook_message((string)goals_progress_alert_renderer::render($criteria, $dept), (string)$dept->slack_webhook->value());
|
|
}
|
|
} else {
|
|
(new Slack())->send_message($message);
|
|
$providerResponse = 'Slack message sent to default webhook';
|
|
}
|
|
break;
|
|
default:
|
|
$response->error('Unsupported or NONE destination for progress alert', 400);
|
|
}
|
|
} catch (\Exception $e) {
|
|
$response->error('Failed to send progress alert: ' . $e->getMessage(), 500);
|
|
}
|
|
|
|
$response->success([
|
|
'id' => (int)$goal->id,
|
|
'destination' => (string)$destination->name,
|
|
'target' => $target,
|
|
'message_preview' => $message,
|
|
'provider_response' => $providerResponse,
|
|
]);
|
|
}, [
|
|
'goals_department_progress_alert_test' => 'Send a test progress alert for a department goal (actual delivery)'
|
|
]);
|
|
}
|
|
}
|