{"sha":"73c0e18199d9159e6ee996278760ea769569c4c7","node_id":"C_kwDOLgQlp9oAKDczYzBlMTgxOTlkOTE1OWU2ZWU5OTYyNzg3NjBlYTc2OTU2OWM0Yzc","commit":{"author":{"name":"Sigurd Nes","email":"sigurdne@users.noreply.github.com","date":"2026-07-01T11:01:18Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2026-07-01T11:01:18Z"},"message":"Merge pull request #1219 from PorticoEstate/master\n\nMerge from master","tree":{"sha":"69f9ed1becf4bfccc6ba9fe4333e899da96bbdca","url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/git/trees/69f9ed1becf4bfccc6ba9fe4333e899da96bbdca"},"url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/git/commits/73c0e18199d9159e6ee996278760ea769569c4c7","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJqRPN+CRC1aQ7uu5UhlAAAt0IQAIBwSNFsYVFa5eC9FIg3Oktt\n6URtGo9HQh0UTzipegq1y5uYiL1YyZk+e0+gebgNlo+gu12SBvCMha5xxje7Ue1G\ni/lCmfxwPadT7FX9Ii2IFnkwYcWox4Xc7v+dzHSLeqQDncyxEtT5SqyJxV0RVRO2\nWRenKO06c30KbDB+ZNkxI94VYblRCZpg5nlgtP8C3MWuy8zT1VuBS1toVouIXt67\nalDHF0bxvdR0MH9Sn1OMLY6yKoId0ydRivRKu7qwQ4LLM2b7zbILU4j8F+hawbWK\nfZMJHR+tvaYikcywkdoLaZIIhruS2SgDYKrj9Q+R+J5XVXbAMhuujcxpSIHWkAr2\np83+vmAecXP5WsYW7fGM9SjWB08YvH+bRYRO7qVXUbVMd8rkqGBlLuOnc51nF80W\nP56XZ1hkd3njd2D6x3UUP8FYRDwSOSjizVVyDQS0l5YLEDe5TRL75V40Pem9z3ZG\nAbj+SCqQi2s81ix/7YK7cepin0drujrS+FWk8L/56vwx3X7jQQNmoJQdNV760shA\ndtuapgRykFOSPiq/lBUjmmYvOvHtglPCJRAWhb7d8F2i3UCXtKmryO+7FkUyPPpa\nIOgSEt4lXbA/GLWAYY/bySAqvjgc0+ZEcbcgqQZkDbNNRue6wkSU7u9gha/F2gCr\nmqA+9k0jI3B5mN+XGCHM\n=mnzR\n-----END PGP SIGNATURE-----\n","payload":"tree 69f9ed1becf4bfccc6ba9fe4333e899da96bbdca\nparent d98546f665f3dd87ce7db875125cb20769945355\nparent 07b1b08c1ef322693d398e0c84d3bd82999a8a20\nauthor Sigurd Nes <sigurdne@users.noreply.github.com> 1782903678 +0200\ncommitter GitHub <noreply@github.com> 1782903678 +0200\n\nMerge pull request #1219 from PorticoEstate/master\n\nMerge from master","verified_at":"2026-07-01T11:01:18Z"}},"url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/commits/73c0e18199d9159e6ee996278760ea769569c4c7","html_url":"https://github.com/PorticoEstate/PorticoEstate-v2/commit/73c0e18199d9159e6ee996278760ea769569c4c7","comments_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/commits/73c0e18199d9159e6ee996278760ea769569c4c7/comments","author":{"login":"sigurdne","id":12719970,"node_id":"MDQ6VXNlcjEyNzE5OTcw","avatar_url":"https://avatars.githubusercontent.com/u/12719970?v=4","gravatar_id":"","url":"https://api.github.com/users/sigurdne","html_url":"https://github.com/sigurdne","followers_url":"https://api.github.com/users/sigurdne/followers","following_url":"https://api.github.com/users/sigurdne/following{/other_user}","gists_url":"https://api.github.com/users/sigurdne/gists{/gist_id}","starred_url":"https://api.github.com/users/sigurdne/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sigurdne/subscriptions","organizations_url":"https://api.github.com/users/sigurdne/orgs","repos_url":"https://api.github.com/users/sigurdne/repos","events_url":"https://api.github.com/users/sigurdne/events{/privacy}","received_events_url":"https://api.github.com/users/sigurdne/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"d98546f665f3dd87ce7db875125cb20769945355","url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/commits/d98546f665f3dd87ce7db875125cb20769945355","html_url":"https://github.com/PorticoEstate/PorticoEstate-v2/commit/d98546f665f3dd87ce7db875125cb20769945355"},{"sha":"07b1b08c1ef322693d398e0c84d3bd82999a8a20","url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/commits/07b1b08c1ef322693d398e0c84d3bd82999a8a20","html_url":"https://github.com/PorticoEstate/PorticoEstate-v2/commit/07b1b08c1ef322693d398e0c84d3bd82999a8a20"}],"stats":{"total":77,"additions":55,"deletions":22},"files":[{"sha":"08b8bc929ca2df9e695bbec469d2ed2d1587d007","filename":"src/modules/booking/inc/class.boapplication.inc.php","status":"modified","additions":7,"deletions":5,"changes":12,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/73c0e18199d9159e6ee996278760ea769569c4c7/src%2Fmodules%2Fbooking%2Finc%2Fclass.boapplication.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/73c0e18199d9159e6ee996278760ea769569c4c7/src%2Fmodules%2Fbooking%2Finc%2Fclass.boapplication.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Fbooking%2Finc%2Fclass.boapplication.inc.php?ref=73c0e18199d9159e6ee996278760ea769569c4c7","patch":"@@ -145,16 +145,18 @@ function get_export_text2($application, $config)\n \t}\n \n \n-\tfunction send_notification($application, $created = false, $assocciated = false)\n+\tfunction send_notification($application, $created = false, $assocciated = false, $is_comment_reply = false)\n \t{\n-\t\t// Skip email notifications for PENDING status\n-\t\tif ($application['status'] == 'PENDING' && !$created) {\n+\t\t// Skip email notifications for PENDING status — but NOT for an explicit\n+\t\t// case-officer comment reply, which must always reach the applicant\n+\t\t// (the skip is only meant to suppress passive status-only PENDING events).\n+\t\tif ($application['status'] == 'PENDING' && !$created && !$is_comment_reply) {\n \t\t\treturn true;\n \t\t}\n-\t\t\n+\n \t\t// Use modern EmailService for email notifications\n \t\t$emailService = new EmailService();\n-\t\t$success = $emailService->sendApplicationNotification($application, $created, $assocciated);\n+\t\t$success = $emailService->sendApplicationNotification($application, $created, $assocciated, $is_comment_reply);\n \t\t\n \t\t// Handle additional notifications to case officers (BCC functionality)\n \t\tif ($created) {"},{"sha":"e8a3677372335f5848c68afbfd2e016c4a5ef5c2","filename":"src/modules/booking/inc/class.uiapplication.inc.php","status":"modified","additions":5,"deletions":2,"changes":7,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/73c0e18199d9159e6ee996278760ea769569c4c7/src%2Fmodules%2Fbooking%2Finc%2Fclass.uiapplication.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/73c0e18199d9159e6ee996278760ea769569c4c7/src%2Fmodules%2Fbooking%2Finc%2Fclass.uiapplication.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Fbooking%2Finc%2Fclass.uiapplication.inc.php?ref=73c0e18199d9159e6ee996278760ea769569c4c7","patch":"@@ -3980,6 +3980,7 @@ public function show()\n \n \t\t\t$update = false;\n \t\t\t$notify = false;\n+\t\t\t$is_comment_reply = false; // true only for an explicit case-officer comment reply\n \t\t\t$recurring_summary = null; // Track when recurring allocations are created\n \n \t\t\t$return_after_action = false;\n@@ -4374,6 +4375,7 @@ public function show()\n \t\t\t\t$this->add_comment($application, $application['comment']);\n \t\t\t\t$update = true;\n \t\t\t\t$notify = true;\n+\t\t\t\t$is_comment_reply = true; // explicit reply to applicant — must email regardless of PENDING status\n \t\t\t\t$return_after_action = true;\n \t\t\t}\n \n@@ -4395,8 +4397,9 @@ public function show()\n \t\t\t\tif ($this->combine_applications && !empty($related_info['application_ids']) && count($related_info['application_ids']) > 1) {\n \t\t\t\t\t$recipient = $this->sendGroupNotification($related_info['application_ids'], $_application);\n \t\t\t\t} else {\n-\t\t\t\t\t// Single application notification\n-\t\t\t\t\t$recipient = $this->bo->send_notification($_application);\n+\t\t\t\t\t// Single application notification — pass the comment-reply flag so a\n+\t\t\t\t\t// case-officer reply bypasses the PENDING email-skip and renders the comment body.\n+\t\t\t\t\t$recipient = $this->bo->send_notification($_application, false, false, $is_comment_reply);\n \t\t\t\t}\n \n \t\t\t\tif ($recipient)"},{"sha":"d7bd24b7e73bb572488f07b81babe1d53783e25d","filename":"src/modules/booking/inc/class.uibooking.inc.php","status":"modified","additions":3,"deletions":2,"changes":5,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/73c0e18199d9159e6ee996278760ea769569c4c7/src%2Fmodules%2Fbooking%2Finc%2Fclass.uibooking.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/73c0e18199d9159e6ee996278760ea769569c4c7/src%2Fmodules%2Fbooking%2Finc%2Fclass.uibooking.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Fbooking%2Finc%2Fclass.uibooking.inc.php?ref=73c0e18199d9159e6ee996278760ea769569c4c7","patch":"@@ -1065,8 +1065,6 @@ public function delete()\n \t\tphpgwapi_jquery::load_widget('autocomplete');\n \t\tself::add_javascript('booking', 'base', 'booking.js');\n \n-\t\t$booking['from_'] = pretty_timestamp($booking['from_']);\n-\t\t$booking['to_'] = pretty_timestamp($booking['to_']);\n \n \t\t$booking['resources_json'] = json_encode(array_map('intval', (array)$booking['resources']));\n \t\t$booking['cancel_link'] = self::link(array(\n@@ -1112,6 +1110,9 @@ public function delete()\n \t\t\t}\n \t\t}\n \n+\t\t$booking['from_'] = pretty_timestamp($booking['from_']);\n+\t\t$booking['to_'] = pretty_timestamp($booking['to_']);\n+\n \t\tif ($step < 2)\n \t\t{\n \t\t\tself::render_template_xsl('booking_delete', array("},{"sha":"fd6443369d95dc7f9e9e8210026c956644302b97","filename":"src/modules/booking/services/ApplicationService.php","status":"modified","additions":17,"deletions":4,"changes":21,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/73c0e18199d9159e6ee996278760ea769569c4c7/src%2Fmodules%2Fbooking%2Fservices%2FApplicationService.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/73c0e18199d9159e6ee996278760ea769569c4c7/src%2Fmodules%2Fbooking%2Fservices%2FApplicationService.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Fbooking%2Fservices%2FApplicationService.php?ref=73c0e18199d9159e6ee996278760ea769569c4c7","patch":"@@ -186,8 +186,10 @@ public function addComment(int $appId, int $accountId, string $comment): void\n \t\t$authorName = $this->repo->fetchAccountName($accountId) ?? 'Unknown';\n \t\t$commentId = $this->repo->addComment($appId, $authorName, $comment, 'comment');\n \n-\t\t// Send email notification (non-fatal on failure)\n-\t\t$this->sendNotificationSafe($appId);\n+ \t\t// Send email notification (non-fatal on failure). Pass the officer's reply text\n+\t\t// so the applicant email contains the actual message — without it the email is\n+\t\t// rebuilt from the DB row alone and the reply body is empty.\n+\t\t$this->sendNotificationSafe($appId, $comment);\n \n \t\t// Create in-app notification for the applicant (non-fatal)\n \t\ttry {\n@@ -459,16 +461,27 @@ private function buildEmailApplicationData(int $appId, array $row): array\n \n \t/**\n \t * Send notification email, suppressing exceptions (logs on failure).\n+\t *\n+\t * @param string|null $commentText When set (case-officer reply path), the text is\n+\t *                                 threaded into the email body so the applicant sees\n+\t *                                 the actual message. Left null for status-change\n+\t *                                 notifications (accept/reject), which are unchanged.\n \t */\n-\tprivate function sendNotificationSafe(int $appId): void\n+\tprivate function sendNotificationSafe(int $appId, ?string $commentText = null): void\n \t{\n \t\ttry {\n \t\t\t$row = $this->repo->getById($appId);\n \t\t\tif (!$row) return;\n \n \t\t\t$emailApp = $this->buildEmailApplicationData($appId, $row);\n+\t\t\t// A non-null comment text means this is an explicit case-officer reply: render\n+\t\t\t// the comment email (regardless of status) and carry the message text.\n+\t\t\t$isCommentReply = $commentText !== null;\n+\t\t\tif ($isCommentReply) {\n+\t\t\t\t$emailApp['comment'] = $commentText;\n+\t\t\t}\n \t\t\t$emailService = new EmailService();\n-\t\t\t$emailService->sendApplicationNotification($emailApp);\n+\t\t\t$emailService->sendApplicationNotification($emailApp, false, false, $isCommentReply);\n \t\t} catch (\\Throwable $e) {\n \t\t\terror_log(\"Failed to send notification for application {$appId}: \" . $e->getMessage());\n \t\t}"},{"sha":"d80c9252e2cd338f88720d998c862162904dbb07","filename":"src/modules/booking/services/EmailService.php","status":"modified","additions":20,"deletions":6,"changes":26,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/73c0e18199d9159e6ee996278760ea769569c4c7/src%2Fmodules%2Fbooking%2Fservices%2FEmailService.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/73c0e18199d9159e6ee996278760ea769569c4c7/src%2Fmodules%2Fbooking%2Fservices%2FEmailService.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Fbooking%2Fservices%2FEmailService.php?ref=73c0e18199d9159e6ee996278760ea769569c4c7","patch":"@@ -305,8 +305,11 @@ public function sendApplicationGroupNotification(array $applications, bool $crea\n      * @param array $application Application data\n      * @param bool $created Whether this is a new application (true) or status update (false)\n      * @param bool $assocciated Whether this is an associated booking (unused in legacy code)\n+     * @param bool $isCommentReply When true this is an explicit case-officer reply to the\n+     *                             applicant: render the comment email regardless of status,\n+     *                             and skip acceptance-only extras (attachments, building notify).\n      */\n-    public function sendApplicationNotification(array $application, bool $created = false, bool $assocciated = false): bool\n+    public function sendApplicationNotification(array $application, bool $created = false, bool $assocciated = false, bool $isCommentReply = false): bool\n     {\n         // Skip if SMTP is not configured\n         if (!(isset($this->serverSettings['smtp_server']) && $this->serverSettings['smtp_server'])) {\n@@ -345,11 +348,11 @@ public function sendApplicationNotification(array $application, bool $created =\n             // preserves the secret query parameter.\n             $link = $external_site_address . '/bookingfrontend/client/user/applications/' . $application['id'] . '?secret=' . $application['secret'];\n \n-            $body = $this->buildEmailBody($application, $config, $created, $resourcename, $link, $e_lock_instructions);\n+            $body = $this->buildEmailBody($application, $config, $created, $resourcename, $link, $e_lock_instructions, $isCommentReply);\n \n-            // Get attachments for accepted applications\n+            // Get attachments for accepted applications (never for a plain comment reply)\n             $attachments = [];\n-            if ($application['status'] == 'ACCEPTED') {\n+            if (!$isCommentReply && $application['status'] == 'ACCEPTED') {\n                 $attachments = $this->getRelatedFiles($application);\n             }\n \n@@ -379,7 +382,9 @@ public function sendApplicationNotification(array $application, bool $created =\n             );\n \n             // Send notification to building contacts for accepted applications\n-            if ($application['status'] == 'ACCEPTED' &&\n+            // (never for a plain comment reply — that is just a message to the applicant)\n+            if (!$isCommentReply &&\n+                $application['status'] == 'ACCEPTED' &&\n                 isset($config['application_notify_on_accepted']) &&\n                 $config['application_notify_on_accepted'] == 1) {\n \n@@ -771,7 +776,7 @@ protected function buildEmailBodyForGroup(array $applications, array $config, bo\n     /**\n      * Build email body based on application status (single application)\n      */\n-    protected function buildEmailBody(array $application, array $config, bool $created, string $resourcename, string $link, array $e_lock_instructions): string\n+    protected function buildEmailBody(array $application, array $config, bool $created, string $resourcename, string $link, array $e_lock_instructions, bool $isCommentReply = false): string\n     {\n         $twig = $this->getEmailTwigHelper();\n \n@@ -788,6 +793,15 @@ protected function buildEmailBody(array $application, array $config, bool $creat\n             'signature' => $config['application_mail_signature'],\n         ];\n \n+        // An explicit case-officer reply is always a comment email, regardless of the\n+        // application's current status — so the applicant sees the message itself rather\n+        // than a status-update body.\n+        if ($isCommentReply) {\n+            return $twig->render('@views/emails/application_comment.twig', array_merge($baseData, [\n+                'comment_added_mail' => $config['application_comment_added_mail'],\n+            ]));\n+        }\n+\n         if ($created) {\n             $dates = !empty($application['dates']) ? $this->formatDates($application['dates']) : [];\n "},{"sha":"a709d65829e6da06f9d89ad989e769f6df9ec8a5","filename":"src/modules/phpgwapi/templates/bootstrap/navbar.inc.php","status":"modified","additions":3,"deletions":3,"changes":6,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/73c0e18199d9159e6ee996278760ea769569c4c7/src%2Fmodules%2Fphpgwapi%2Ftemplates%2Fbootstrap%2Fnavbar.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/73c0e18199d9159e6ee996278760ea769569c4c7/src%2Fmodules%2Fphpgwapi%2Ftemplates%2Fbootstrap%2Fnavbar.inc.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Fphpgwapi%2Ftemplates%2Fbootstrap%2Fnavbar.inc.php?ref=73c0e18199d9159e6ee996278760ea769569c4c7","patch":"@@ -85,9 +85,9 @@ function parse_navbar($force = False)\n \t$template_selector = <<<HTML\n \n \t   <select id = \"template_selector\" class=\"btn btn-link btn-sm nav-item dropdown no-arrow nav-link text-white dropdown-toggle\" style=\"height:2rem\">\n-\t\t<option class=\"nav-link text-white\" value=\"portico\"{$selecte_portico}>Portico</option>\n-\t\t<option class=\"nav-link text-white\" value=\"bootstrap\"{$selecte_bootstrap}>Bootstrap</option>\n-\t\t<option class=\"nav-link text-white\" value=\"digdir\"{$selecte_digdir}>Digdir</option>\n+\t\t<option value=\"portico\"{$selecte_portico} style=\"color:#212529;background-color:#ffffff;\">Portico</option>\n+\t\t<option value=\"bootstrap\"{$selecte_bootstrap} style=\"color:#212529;background-color:#ffffff;\">Bootstrap</option>\n+\t\t<option value=\"digdir\"{$selecte_digdir} style=\"color:#212529;background-color:#ffffff;\">Digdir</option>\n \t   </select>\n HTML;\n "}]}