From d2fa1af8bc8630e53073a09578dd89d63f81725e Mon Sep 17 00:00:00 2001 From: Darko Gjorgjijoski Date: Tue, 15 Sep 2026 10:24:25 +0200 Subject: [PATCH 01/11] fix(billing): say and 1 more entry in the singular The composer's cap on a note's length always pluralised the summary line, so a note that trimmed exactly one entry read "and 1 more entries". Pluralise only when more than one entry was dropped. --- app/Application/InvoiceLineComposer.php | 3 ++- tests/Unit/BillingServiceTest.php | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/Application/InvoiceLineComposer.php b/app/Application/InvoiceLineComposer.php index c00b335..d6734c4 100644 --- a/app/Application/InvoiceLineComposer.php +++ b/app/Application/InvoiceLineComposer.php @@ -140,7 +140,8 @@ private function capped(array $heading, array $lines): string while ($kept !== []) { array_pop($kept); $dropped = count($lines) - count($kept); - $note = implode("\n", [...$heading, ...$kept, 'and '.$dropped.' more entries']); + $summary = 'and '.$dropped.' more '.($dropped === 1 ? 'entry' : 'entries'); + $note = implode("\n", [...$heading, ...$kept, $summary]); if (mb_strlen($note) <= self::MAX_LENGTH) { return $note; diff --git a/tests/Unit/BillingServiceTest.php b/tests/Unit/BillingServiceTest.php index 3977356..62a54c9 100644 --- a/tests/Unit/BillingServiceTest.php +++ b/tests/Unit/BillingServiceTest.php @@ -732,6 +732,24 @@ public function test_a_note_too_long_to_print_says_how_many_entries_it_left_out( self::assertSame('2026-09-01 '.str_repeat('x', 60), $lines[0]); } + public function test_a_note_that_drops_exactly_one_entry_says_so_in_the_singular(): void + { + $this->noteSettings('invoice_entry_dates', 'invoice_entry_descriptions'); + $entries = [ + $this->entry($this->landing, 7, 60, '2026-09-01', ['description' => str_repeat('x', 990)]), + $this->entry($this->landing, 7, 60, '2026-09-02', ['description' => str_repeat('x', 990)]), + ]; + + $payload = $this->billing->prepare(self::COMPANY, BillingSelection::fromEntryIds($this->ids($entries))); + $description = (string) $payload['items'][0]['description']; + $lines = explode("\n", $description); + + self::assertLessThanOrEqual(InvoiceLineComposer::MAX_LENGTH, mb_strlen($description)); + self::assertCount(2, $lines); + self::assertSame('and 1 more entry', end($lines)); + self::assertSame('2026-09-01 '.str_repeat('x', 990), $lines[0]); + } + /** * Turn on exactly these line note settings, and nothing else. * From 6893768da08224347192967b4f2442a61025fc04 Mon Sep 17 00:00:00 2001 From: Darko Gjorgjijoski Date: Tue, 15 Sep 2026 10:25:18 +0200 Subject: [PATCH 02/11] fix(ui): keep the quick-start launcher clear of settings forms and the AI bubble The floating launcher covered the bottom of long settings pages, such as the status editor, with no way to see past it. It now hides on /admin/settings/... routes, tracked through the host router's afterEach rather than a prop that was never wired through. It also lifts clear of the AI assistant module's own launcher button when one is on the page, checked once at mount and again on every company switch through the overlay's existing remount key. --- resources/js/components/QuickStartOverlay.vue | 53 +++++++++++++++++-- resources/js/registrations/time.ts | 5 +- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/resources/js/components/QuickStartOverlay.vue b/resources/js/components/QuickStartOverlay.vue index b3f15ac..a9f4041 100644 --- a/resources/js/components/QuickStartOverlay.vue +++ b/resources/js/components/QuickStartOverlay.vue @@ -1,6 +1,7 @@