{"sha":"07b1b08c1ef322693d398e0c84d3bd82999a8a20","node_id":"C_kwDOLgQlp9oAKDA3YjFiMDhjMWVmMzIyNjkzZDM5OGUwYzg0ZDNiZDgyOTk5YThhMjA","commit":{"author":{"name":"Sigurd Nes","email":"sigurdne@users.noreply.github.com","date":"2026-07-01T10:47:55Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2026-07-01T10:47:55Z"},"message":"Merge pull request #1218 from PorticoEstate/testing\n\nmerge from testing\nTweak email-variants delete booking","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/07b1b08c1ef322693d398e0c84d3bd82999a8a20","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJqRPBbCRC1aQ7uu5UhlAAAKmQQABkrd4KB7SHt5AQx61i5Mnx0\nVryHmV93m6SfY/TTHYEc1FuPUnHPJP23wgStlLm5VjceTbkQ32qTQT2Qb1xmPONC\ncTRZCC7lkdWQnOv9ZTMd1dApl/qcTCFGvdMs+RzoEtt8F1NK5aPZY8pTgNgoIs4u\nDAic4PZ9bcuvy2uaH4VNSNcpQ48L2zDb3DvakZ1kh+gTJmpUXxZ4l7vmBpHKgIk2\nI/GcVPOTdIUzc4dQ9pNBdYIqto9r0eZ81WrnSz5dktCT1KEOshfpLiAyahyNYbE7\nEp74VF941aBCjR+gzYn+XiKawRqGQ24MYmFoF2o/pY5ynHWdu8lYGSxCqxwCxKYS\nNQytlmntIt3tiyi4VWZ3832pHRC9J4sfqsq9ccTO1ImVo0XN7LgmXNmgedi6fOjT\nzGdzFKE5dezMm/sWEeNylgZ7SXQyxZ8XsqowgypFEe8ZxzqWQtIRyFJxgm7UsjgF\n8ZwfCxgQHP4vUfJ92WAC/nEZKZeKIG9CzyoxQT48ifrP3puKG1OQbsrkbkpB+gDo\nHFmqoyK3GKZBEhfwlOvbJkplCRyDAkceIsXM5Y60uLVnhQsLTuH0XXONBni5ObMq\nGD6rLA0YxqvooAlX4YayD2kkjz0Y5LlDuWx3hznC0JjiwFO4vrehDUZQpMPMDkWB\nzpLKtJ20uZjEorNgQGnS\n=RLo3\n-----END PGP SIGNATURE-----\n","payload":"tree 69f9ed1becf4bfccc6ba9fe4333e899da96bbdca\nparent e8542099599295611d649a60473220cb7d976193\nparent 21925c2e01c1a258820e13ea25438a067b5f6a0f\nauthor Sigurd Nes <sigurdne@users.noreply.github.com> 1782902875 +0200\ncommitter GitHub <noreply@github.com> 1782902875 +0200\n\nMerge pull request #1218 from PorticoEstate/testing\n\nmerge from testing\nTweak email-variants delete booking","verified_at":"2026-07-01T10:47:55Z"}},"url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/commits/07b1b08c1ef322693d398e0c84d3bd82999a8a20","html_url":"https://github.com/PorticoEstate/PorticoEstate-v2/commit/07b1b08c1ef322693d398e0c84d3bd82999a8a20","comments_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/commits/07b1b08c1ef322693d398e0c84d3bd82999a8a20/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":"e8542099599295611d649a60473220cb7d976193","url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/commits/e8542099599295611d649a60473220cb7d976193","html_url":"https://github.com/PorticoEstate/PorticoEstate-v2/commit/e8542099599295611d649a60473220cb7d976193"},{"sha":"21925c2e01c1a258820e13ea25438a067b5f6a0f","url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/commits/21925c2e01c1a258820e13ea25438a067b5f6a0f","html_url":"https://github.com/PorticoEstate/PorticoEstate-v2/commit/21925c2e01c1a258820e13ea25438a067b5f6a0f"}],"stats":{"total":81,"additions":58,"deletions":23},"files":[{"sha":"c2a2382379c0746f2f3a83a76a36d3c72841469a","filename":"Dockerfile","status":"modified","additions":3,"deletions":1,"changes":4,"blob_url":"https://github.com/PorticoEstate/PorticoEstate-v2/blob/07b1b08c1ef322693d398e0c84d3bd82999a8a20/Dockerfile","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/07b1b08c1ef322693d398e0c84d3bd82999a8a20/Dockerfile","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/Dockerfile?ref=07b1b08c1ef322693d398e0c84d3bd82999a8a20","patch":"@@ -230,7 +230,9 @@ WORKDIR /var/www/html\n COPY composer.json composer.lock* ./\n \n # Install all dependencies during build time\n-RUN XDEBUG_MODE=off composer install --no-dev --optimize-autoloader\n+# --prefer-source git-clones packages (git is installed above) instead of downloading\n+# GitHub dist zips, which intermittently fail with HTTP 400 from codeload when unauthenticated.\n+RUN XDEBUG_MODE=off composer install --no-dev --optimize-autoloader --prefer-source\n \n # Install Node.js 22 LTS and npm via NodeSource\n RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \\"},{"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/07b1b08c1ef322693d398e0c84d3bd82999a8a20/src%2Fmodules%2Fbooking%2Finc%2Fclass.boapplication.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/07b1b08c1ef322693d398e0c84d3bd82999a8a20/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=07b1b08c1ef322693d398e0c84d3bd82999a8a20","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/07b1b08c1ef322693d398e0c84d3bd82999a8a20/src%2Fmodules%2Fbooking%2Finc%2Fclass.uiapplication.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/07b1b08c1ef322693d398e0c84d3bd82999a8a20/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=07b1b08c1ef322693d398e0c84d3bd82999a8a20","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/07b1b08c1ef322693d398e0c84d3bd82999a8a20/src%2Fmodules%2Fbooking%2Finc%2Fclass.uibooking.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/07b1b08c1ef322693d398e0c84d3bd82999a8a20/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=07b1b08c1ef322693d398e0c84d3bd82999a8a20","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/07b1b08c1ef322693d398e0c84d3bd82999a8a20/src%2Fmodules%2Fbooking%2Fservices%2FApplicationService.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/07b1b08c1ef322693d398e0c84d3bd82999a8a20/src%2Fmodules%2Fbooking%2Fservices%2FApplicationService.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Fbooking%2Fservices%2FApplicationService.php?ref=07b1b08c1ef322693d398e0c84d3bd82999a8a20","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/07b1b08c1ef322693d398e0c84d3bd82999a8a20/src%2Fmodules%2Fbooking%2Fservices%2FEmailService.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/07b1b08c1ef322693d398e0c84d3bd82999a8a20/src%2Fmodules%2Fbooking%2Fservices%2FEmailService.php","contents_url":"https://api.github.com/repos/PorticoEstate/PorticoEstate-v2/contents/src%2Fmodules%2Fbooking%2Fservices%2FEmailService.php?ref=07b1b08c1ef322693d398e0c84d3bd82999a8a20","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/07b1b08c1ef322693d398e0c84d3bd82999a8a20/src%2Fmodules%2Fphpgwapi%2Ftemplates%2Fbootstrap%2Fnavbar.inc.php","raw_url":"https://github.com/PorticoEstate/PorticoEstate-v2/raw/07b1b08c1ef322693d398e0c84d3bd82999a8a20/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=07b1b08c1ef322693d398e0c84d3bd82999a8a20","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 "}]}