From d5850041a75da8cb9448ebcd2d49319f44fb6717 Mon Sep 17 00:00:00 2001 From: jubnl Date: Thu, 18 Jun 2026 13:59:10 +0200 Subject: [PATCH] fix(costs): rework the cost panel UX wise and apply prettier on the shared package --- client/src/api/client.ts | 1 + .../src/components/Budget/CostsPanel.test.tsx | 138 +++++++ client/src/components/Budget/CostsPanel.tsx | 356 ++++++++++++------ server/src/nest/budget/budget.controller.ts | 25 ++ server/src/nest/budget/budget.service.ts | 4 + server/src/services/budgetService.ts | 26 +- server/tests/e2e/budget.e2e.test.ts | 15 + .../tests/unit/nest/budget.controller.test.ts | 31 ++ .../tests/unit/services/budgetService.test.ts | 58 ++- shared/.prettierrc | 1 + shared/src/admin/admin.schema.spec.ts | 25 +- shared/src/admin/admin.schema.ts | 16 +- .../src/assignment/assignment.schema.spec.ts | 27 +- shared/src/assignment/assignment.schema.ts | 12 +- shared/src/atlas/atlas.schema.spec.ts | 28 +- shared/src/atlas/atlas.schema.ts | 190 ++++++++-- shared/src/auth/auth.schema.spec.ts | 51 +-- shared/src/backup/backup.schema.spec.ts | 9 +- shared/src/backup/backup.schema.ts | 4 +- shared/src/budget/budget.schema.spec.ts | 29 +- shared/src/budget/budget.schema.ts | 40 +- shared/src/category/category.schema.spec.ts | 18 +- shared/src/collab/collab.schema.spec.ts | 47 +-- shared/src/collab/collab.schema.ts | 16 +- shared/src/common/primitives.schema.spec.ts | 11 +- shared/src/day/day.schema.spec.ts | 31 +- shared/src/file/file.schema.spec.ts | 25 +- shared/src/i18n/ar/admin.ts | 105 ++---- shared/src/i18n/ar/backup.ts | 12 +- shared/src/i18n/ar/budget.ts | 154 ++++---- shared/src/i18n/ar/categories.ts | 3 +- shared/src/i18n/ar/dashboard.ts | 12 +- shared/src/i18n/ar/day.ts | 3 +- shared/src/i18n/ar/dayplan.ts | 12 +- shared/src/i18n/ar/externalNotifications.ts | 3 +- shared/src/i18n/ar/files.ts | 9 +- shared/src/i18n/ar/journey.ts | 39 +- shared/src/i18n/ar/login.ts | 18 +- shared/src/i18n/ar/memories.ts | 12 +- shared/src/i18n/ar/notif.ts | 3 +- shared/src/i18n/ar/notifications.ts | 6 +- shared/src/i18n/ar/oauth.ts | 84 ++--- shared/src/i18n/ar/packing.ts | 3 +- shared/src/i18n/ar/perm.ts | 15 +- shared/src/i18n/ar/photos.ts | 3 +- shared/src/i18n/ar/places.ts | 15 +- shared/src/i18n/ar/planner.ts | 3 +- shared/src/i18n/ar/reservations.ts | 42 +-- shared/src/i18n/ar/settings.ts | 97 ++--- shared/src/i18n/ar/system_notice.ts | 15 +- shared/src/i18n/ar/trip.ts | 2 +- shared/src/i18n/ar/trips.ts | 3 +- shared/src/i18n/ar/vacay.ts | 9 +- shared/src/i18n/br/admin.ts | 150 +++----- shared/src/i18n/br/atlas.ts | 3 +- shared/src/i18n/br/backup.ts | 18 +- shared/src/i18n/br/budget.ts | 157 ++++---- shared/src/i18n/br/categories.ts | 3 +- shared/src/i18n/br/collab.ts | 6 +- shared/src/i18n/br/dashboard.ts | 12 +- shared/src/i18n/br/day.ts | 6 +- shared/src/i18n/br/dayplan.ts | 18 +- shared/src/i18n/br/externalNotifications.ts | 3 +- shared/src/i18n/br/files.ts | 12 +- shared/src/i18n/br/journey.ts | 42 +-- shared/src/i18n/br/login.ts | 33 +- shared/src/i18n/br/memories.ts | 24 +- shared/src/i18n/br/notif.ts | 6 +- shared/src/i18n/br/notifications.ts | 3 +- shared/src/i18n/br/oauth.ts | 90 ++--- shared/src/i18n/br/packing.ts | 6 +- shared/src/i18n/br/perm.ts | 39 +- shared/src/i18n/br/places.ts | 21 +- shared/src/i18n/br/planner.ts | 9 +- shared/src/i18n/br/reservations.ts | 54 +-- shared/src/i18n/br/settings.ts | 85 ++--- shared/src/i18n/br/shared.ts | 3 +- shared/src/i18n/br/system_notice.ts | 42 +-- shared/src/i18n/br/trip.ts | 2 +- shared/src/i18n/br/vacay.ts | 39 +- shared/src/i18n/cs/admin.ts | 141 +++---- shared/src/i18n/cs/atlas.ts | 3 +- shared/src/i18n/cs/backup.ts | 12 +- shared/src/i18n/cs/budget.ts | 157 ++++---- shared/src/i18n/cs/categories.ts | 3 +- shared/src/i18n/cs/collab.ts | 3 +- shared/src/i18n/cs/dashboard.ts | 12 +- shared/src/i18n/cs/day.ts | 6 +- shared/src/i18n/cs/dayplan.ts | 18 +- shared/src/i18n/cs/externalNotifications.ts | 3 +- shared/src/i18n/cs/files.ts | 9 +- shared/src/i18n/cs/journey.ts | 39 +- shared/src/i18n/cs/login.ts | 33 +- shared/src/i18n/cs/memories.ts | 18 +- shared/src/i18n/cs/notif.ts | 6 +- shared/src/i18n/cs/notifications.ts | 3 +- shared/src/i18n/cs/oauth.ts | 93 ++--- shared/src/i18n/cs/packing.ts | 6 +- shared/src/i18n/cs/perm.ts | 24 +- shared/src/i18n/cs/places.ts | 21 +- shared/src/i18n/cs/planner.ts | 6 +- shared/src/i18n/cs/register.ts | 3 +- shared/src/i18n/cs/reservations.ts | 48 +-- shared/src/i18n/cs/settings.ts | 97 ++--- shared/src/i18n/cs/system_notice.ts | 27 +- shared/src/i18n/cs/trip.ts | 2 +- shared/src/i18n/cs/trips.ts | 3 +- shared/src/i18n/cs/vacay.ts | 18 +- shared/src/i18n/de/admin.ts | 135 +++---- shared/src/i18n/de/backup.ts | 12 +- shared/src/i18n/de/budget.ts | 160 ++++---- shared/src/i18n/de/categories.ts | 3 +- shared/src/i18n/de/collab.ts | 6 +- shared/src/i18n/de/common.ts | 3 +- shared/src/i18n/de/dashboard.ts | 12 +- shared/src/i18n/de/day.ts | 3 +- shared/src/i18n/de/dayplan.ts | 15 +- shared/src/i18n/de/externalNotifications.ts | 6 +- shared/src/i18n/de/files.ts | 12 +- shared/src/i18n/de/journey.ts | 48 +-- shared/src/i18n/de/login.ts | 36 +- shared/src/i18n/de/memories.ts | 21 +- shared/src/i18n/de/notif.ts | 12 +- shared/src/i18n/de/notifications.ts | 9 +- shared/src/i18n/de/oauth.ts | 87 ++--- shared/src/i18n/de/packing.ts | 6 +- shared/src/i18n/de/perm.ts | 36 +- shared/src/i18n/de/places.ts | 21 +- shared/src/i18n/de/planner.ts | 6 +- shared/src/i18n/de/register.ts | 3 +- shared/src/i18n/de/reservations.ts | 51 +-- shared/src/i18n/de/settings.ts | 94 ++--- shared/src/i18n/de/system_notice.ts | 42 +-- shared/src/i18n/de/trip.ts | 2 +- shared/src/i18n/de/vacay.ts | 36 +- shared/src/i18n/en/admin.ts | 120 ++---- shared/src/i18n/en/backup.ts | 15 +- shared/src/i18n/en/budget.ts | 157 ++++---- shared/src/i18n/en/categories.ts | 3 +- shared/src/i18n/en/collab.ts | 3 +- shared/src/i18n/en/dashboard.ts | 12 +- shared/src/i18n/en/day.ts | 3 +- shared/src/i18n/en/dayplan.ts | 15 +- shared/src/i18n/en/externalNotifications.ts | 3 +- shared/src/i18n/en/files.ts | 9 +- shared/src/i18n/en/journey.ts | 39 +- shared/src/i18n/en/login.ts | 27 +- shared/src/i18n/en/memories.ts | 15 +- shared/src/i18n/en/notif.ts | 3 +- shared/src/i18n/en/notifications.ts | 6 +- shared/src/i18n/en/oauth.ts | 96 ++--- shared/src/i18n/en/packing.ts | 6 +- shared/src/i18n/en/perm.ts | 18 +- shared/src/i18n/en/places.ts | 18 +- shared/src/i18n/en/planner.ts | 6 +- shared/src/i18n/en/reservations.ts | 45 +-- shared/src/i18n/en/settings.ts | 67 ++-- shared/src/i18n/en/system_notice.ts | 30 +- shared/src/i18n/en/trip.ts | 2 +- shared/src/i18n/en/trips.ts | 3 +- shared/src/i18n/en/vacay.ts | 30 +- shared/src/i18n/es/admin.ts | 141 +++---- shared/src/i18n/es/atlas.ts | 6 +- shared/src/i18n/es/backup.ts | 15 +- shared/src/i18n/es/budget.ts | 160 ++++---- shared/src/i18n/es/categories.ts | 3 +- shared/src/i18n/es/collab.ts | 6 +- shared/src/i18n/es/common.ts | 3 +- shared/src/i18n/es/dashboard.ts | 15 +- shared/src/i18n/es/day.ts | 3 +- shared/src/i18n/es/dayplan.ts | 15 +- shared/src/i18n/es/externalNotifications.ts | 6 +- shared/src/i18n/es/files.ts | 12 +- shared/src/i18n/es/journey.ts | 42 +-- shared/src/i18n/es/login.ts | 39 +- shared/src/i18n/es/memories.ts | 21 +- shared/src/i18n/es/notif.ts | 6 +- shared/src/i18n/es/notifications.ts | 6 +- shared/src/i18n/es/oauth.ts | 93 ++--- shared/src/i18n/es/packing.ts | 6 +- shared/src/i18n/es/perm.ts | 36 +- shared/src/i18n/es/places.ts | 21 +- shared/src/i18n/es/planner.ts | 3 +- shared/src/i18n/es/reservations.ts | 45 +-- shared/src/i18n/es/settings.ts | 79 ++-- shared/src/i18n/es/system_notice.ts | 42 +-- shared/src/i18n/es/trip.ts | 2 +- shared/src/i18n/es/vacay.ts | 36 +- .../src/i18n/externalNotifications/index.ts | 14 +- shared/src/i18n/fr/admin.ts | 156 +++----- shared/src/i18n/fr/atlas.ts | 6 +- shared/src/i18n/fr/backup.ts | 15 +- shared/src/i18n/fr/budget.ts | 160 ++++---- shared/src/i18n/fr/categories.ts | 3 +- shared/src/i18n/fr/collab.ts | 9 +- shared/src/i18n/fr/dashboard.ts | 18 +- shared/src/i18n/fr/day.ts | 6 +- shared/src/i18n/fr/dayplan.ts | 25 +- shared/src/i18n/fr/externalNotifications.ts | 6 +- shared/src/i18n/fr/files.ts | 9 +- shared/src/i18n/fr/journey.ts | 42 +-- shared/src/i18n/fr/login.ts | 42 +-- shared/src/i18n/fr/memories.ts | 21 +- shared/src/i18n/fr/notif.ts | 15 +- shared/src/i18n/fr/notifications.ts | 9 +- shared/src/i18n/fr/oauth.ts | 87 ++--- shared/src/i18n/fr/packing.ts | 9 +- shared/src/i18n/fr/perm.ts | 42 +-- shared/src/i18n/fr/places.ts | 24 +- shared/src/i18n/fr/planner.ts | 9 +- shared/src/i18n/fr/register.ts | 6 +- shared/src/i18n/fr/reservations.ts | 36 +- shared/src/i18n/fr/settings.ts | 91 ++--- shared/src/i18n/fr/system_notice.ts | 39 +- shared/src/i18n/fr/todo.ts | 3 +- shared/src/i18n/fr/trip.ts | 2 +- shared/src/i18n/fr/vacay.ts | 42 +-- shared/src/i18n/gr/admin.ts | 132 +++---- shared/src/i18n/gr/atlas.ts | 9 +- shared/src/i18n/gr/backup.ts | 15 +- shared/src/i18n/gr/budget.ts | 157 ++++---- shared/src/i18n/gr/categories.ts | 3 +- shared/src/i18n/gr/collab.ts | 6 +- shared/src/i18n/gr/common.ts | 3 +- shared/src/i18n/gr/dashboard.ts | 18 +- shared/src/i18n/gr/day.ts | 3 +- shared/src/i18n/gr/dayplan.ts | 21 +- shared/src/i18n/gr/externalNotifications.ts | 6 +- shared/src/i18n/gr/files.ts | 15 +- shared/src/i18n/gr/journey.ts | 42 +-- shared/src/i18n/gr/login.ts | 45 +-- shared/src/i18n/gr/memories.ts | 24 +- shared/src/i18n/gr/notif.ts | 15 +- shared/src/i18n/gr/notifications.ts | 18 +- shared/src/i18n/gr/oauth.ts | 81 ++-- shared/src/i18n/gr/packing.ts | 6 +- shared/src/i18n/gr/perm.ts | 51 +-- shared/src/i18n/gr/places.ts | 27 +- shared/src/i18n/gr/planner.ts | 6 +- shared/src/i18n/gr/register.ts | 6 +- shared/src/i18n/gr/reservations.ts | 48 +-- shared/src/i18n/gr/settings.ts | 79 ++-- shared/src/i18n/gr/shared.ts | 3 +- shared/src/i18n/gr/system_notice.ts | 42 +-- shared/src/i18n/gr/trip.ts | 5 +- shared/src/i18n/gr/vacay.ts | 42 +-- shared/src/i18n/hu/admin.ts | 132 +++---- shared/src/i18n/hu/atlas.ts | 12 +- shared/src/i18n/hu/backup.ts | 12 +- shared/src/i18n/hu/budget.ts | 160 ++++---- shared/src/i18n/hu/categories.ts | 3 +- shared/src/i18n/hu/collab.ts | 9 +- shared/src/i18n/hu/common.ts | 3 +- shared/src/i18n/hu/dashboard.ts | 12 +- shared/src/i18n/hu/day.ts | 6 +- shared/src/i18n/hu/dayplan.ts | 18 +- shared/src/i18n/hu/externalNotifications.ts | 6 +- shared/src/i18n/hu/files.ts | 9 +- shared/src/i18n/hu/journey.ts | 42 +-- shared/src/i18n/hu/login.ts | 48 +-- shared/src/i18n/hu/memories.ts | 15 +- shared/src/i18n/hu/notif.ts | 15 +- shared/src/i18n/hu/notifications.ts | 3 +- shared/src/i18n/hu/oauth.ts | 87 ++--- shared/src/i18n/hu/packing.ts | 6 +- shared/src/i18n/hu/perm.ts | 42 +-- shared/src/i18n/hu/places.ts | 24 +- shared/src/i18n/hu/planner.ts | 6 +- shared/src/i18n/hu/register.ts | 6 +- shared/src/i18n/hu/reservations.ts | 45 +-- shared/src/i18n/hu/settings.ts | 88 ++--- shared/src/i18n/hu/system_notice.ts | 24 +- shared/src/i18n/hu/trip.ts | 2 +- shared/src/i18n/hu/vacay.ts | 42 +-- shared/src/i18n/id/admin.ts | 129 +++---- shared/src/i18n/id/atlas.ts | 3 +- shared/src/i18n/id/backup.ts | 15 +- shared/src/i18n/id/budget.ts | 157 ++++---- shared/src/i18n/id/categories.ts | 3 +- shared/src/i18n/id/collab.ts | 3 +- shared/src/i18n/id/dashboard.ts | 12 +- shared/src/i18n/id/day.ts | 6 +- shared/src/i18n/id/dayplan.ts | 18 +- shared/src/i18n/id/externalNotifications.ts | 6 +- shared/src/i18n/id/files.ts | 9 +- shared/src/i18n/id/journey.ts | 45 +-- shared/src/i18n/id/login.ts | 39 +- shared/src/i18n/id/memories.ts | 12 +- shared/src/i18n/id/notif.ts | 6 +- shared/src/i18n/id/notifications.ts | 6 +- shared/src/i18n/id/oauth.ts | 96 ++--- shared/src/i18n/id/packing.ts | 6 +- shared/src/i18n/id/perm.ts | 45 +-- shared/src/i18n/id/places.ts | 24 +- shared/src/i18n/id/planner.ts | 3 +- shared/src/i18n/id/reservations.ts | 51 +-- shared/src/i18n/id/settings.ts | 85 ++--- shared/src/i18n/id/system_notice.ts | 33 +- shared/src/i18n/id/trip.ts | 2 +- shared/src/i18n/id/trips.ts | 3 +- shared/src/i18n/id/vacay.ts | 39 +- shared/src/i18n/it/admin.ts | 146 +++---- shared/src/i18n/it/atlas.ts | 9 +- shared/src/i18n/it/backup.ts | 9 +- shared/src/i18n/it/budget.ts | 160 ++++---- shared/src/i18n/it/categories.ts | 3 +- shared/src/i18n/it/collab.ts | 6 +- shared/src/i18n/it/dashboard.ts | 18 +- shared/src/i18n/it/day.ts | 6 +- shared/src/i18n/it/dayplan.ts | 18 +- shared/src/i18n/it/externalNotifications.ts | 6 +- shared/src/i18n/it/files.ts | 3 +- shared/src/i18n/it/journey.ts | 42 +-- shared/src/i18n/it/login.ts | 30 +- shared/src/i18n/it/memories.ts | 18 +- shared/src/i18n/it/notif.ts | 6 +- shared/src/i18n/it/notifications.ts | 3 +- shared/src/i18n/it/oauth.ts | 93 ++--- shared/src/i18n/it/packing.ts | 6 +- shared/src/i18n/it/perm.ts | 36 +- shared/src/i18n/it/places.ts | 21 +- shared/src/i18n/it/planner.ts | 9 +- shared/src/i18n/it/register.ts | 3 +- shared/src/i18n/it/reservations.ts | 57 +-- shared/src/i18n/it/settings.ts | 84 ++--- shared/src/i18n/it/system_notice.ts | 36 +- shared/src/i18n/it/trip.ts | 2 +- shared/src/i18n/it/vacay.ts | 30 +- shared/src/i18n/ja/admin.ts | 102 ++--- shared/src/i18n/ja/backup.ts | 9 +- shared/src/i18n/ja/budget.ts | 154 ++++---- shared/src/i18n/ja/categories.ts | 3 +- shared/src/i18n/ja/collab.ts | 3 +- shared/src/i18n/ja/common.ts | 3 +- shared/src/i18n/ja/dashboard.ts | 9 +- shared/src/i18n/ja/day.ts | 3 +- shared/src/i18n/ja/dayplan.ts | 3 +- shared/src/i18n/ja/externalNotifications.ts | 3 +- shared/src/i18n/ja/files.ts | 9 +- shared/src/i18n/ja/journey.ts | 24 +- shared/src/i18n/ja/login.ts | 30 +- shared/src/i18n/ja/memories.ts | 18 +- shared/src/i18n/ja/notif.ts | 12 +- shared/src/i18n/ja/notifications.ts | 3 +- shared/src/i18n/ja/oauth.ts | 48 +-- shared/src/i18n/ja/packing.ts | 6 +- shared/src/i18n/ja/places.ts | 27 +- shared/src/i18n/ja/planner.ts | 3 +- shared/src/i18n/ja/reservations.ts | 51 +-- shared/src/i18n/ja/settings.ts | 97 ++--- shared/src/i18n/ja/system_notice.ts | 15 +- shared/src/i18n/ja/trip.ts | 2 +- shared/src/i18n/ja/trips.ts | 3 +- shared/src/i18n/ja/vacay.ts | 12 +- shared/src/i18n/ko/admin.ts | 132 +++---- shared/src/i18n/ko/backup.ts | 3 +- shared/src/i18n/ko/budget.ts | 154 ++++---- shared/src/i18n/ko/categories.ts | 3 +- shared/src/i18n/ko/common.ts | 3 +- shared/src/i18n/ko/dashboard.ts | 9 +- shared/src/i18n/ko/day.ts | 3 +- shared/src/i18n/ko/dayplan.ts | 9 +- shared/src/i18n/ko/externalNotifications.ts | 3 +- shared/src/i18n/ko/files.ts | 9 +- shared/src/i18n/ko/journey.ts | 36 +- shared/src/i18n/ko/login.ts | 27 +- shared/src/i18n/ko/memories.ts | 18 +- shared/src/i18n/ko/notif.ts | 9 +- shared/src/i18n/ko/notifications.ts | 12 +- shared/src/i18n/ko/oauth.ts | 60 +-- shared/src/i18n/ko/packing.ts | 6 +- shared/src/i18n/ko/perm.ts | 27 +- shared/src/i18n/ko/places.ts | 21 +- shared/src/i18n/ko/planner.ts | 3 +- shared/src/i18n/ko/reservations.ts | 42 +-- shared/src/i18n/ko/settings.ts | 91 ++--- shared/src/i18n/ko/share.ts | 3 +- shared/src/i18n/ko/system_notice.ts | 18 +- shared/src/i18n/ko/trip.ts | 2 +- shared/src/i18n/ko/trips.ts | 3 +- shared/src/i18n/ko/vacay.ts | 18 +- shared/src/i18n/languages.ts | 7 +- shared/src/i18n/nl/admin.ts | 131 +++---- shared/src/i18n/nl/atlas.ts | 3 +- shared/src/i18n/nl/backup.ts | 15 +- shared/src/i18n/nl/budget.ts | 160 ++++---- shared/src/i18n/nl/categories.ts | 3 +- shared/src/i18n/nl/dashboard.ts | 12 +- shared/src/i18n/nl/day.ts | 6 +- shared/src/i18n/nl/dayplan.ts | 18 +- shared/src/i18n/nl/externalNotifications.ts | 3 +- shared/src/i18n/nl/files.ts | 9 +- shared/src/i18n/nl/journey.ts | 39 +- shared/src/i18n/nl/login.ts | 36 +- shared/src/i18n/nl/memories.ts | 21 +- shared/src/i18n/nl/notif.ts | 12 +- shared/src/i18n/nl/notifications.ts | 3 +- shared/src/i18n/nl/oauth.ts | 90 ++--- shared/src/i18n/nl/packing.ts | 9 +- shared/src/i18n/nl/perm.ts | 33 +- shared/src/i18n/nl/places.ts | 24 +- shared/src/i18n/nl/planner.ts | 6 +- shared/src/i18n/nl/register.ts | 3 +- shared/src/i18n/nl/reservations.ts | 54 +-- shared/src/i18n/nl/settings.ts | 85 ++--- shared/src/i18n/nl/system_notice.ts | 33 +- shared/src/i18n/nl/trip.ts | 5 +- shared/src/i18n/nl/vacay.ts | 36 +- shared/src/i18n/pl/admin.ts | 123 +++--- shared/src/i18n/pl/atlas.ts | 3 +- shared/src/i18n/pl/backup.ts | 21 +- shared/src/i18n/pl/budget.ts | 154 ++++---- shared/src/i18n/pl/categories.ts | 3 +- shared/src/i18n/pl/collab.ts | 6 +- shared/src/i18n/pl/dashboard.ts | 9 +- shared/src/i18n/pl/day.ts | 3 +- shared/src/i18n/pl/dayplan.ts | 15 +- shared/src/i18n/pl/externalNotifications.ts | 6 +- shared/src/i18n/pl/files.ts | 6 +- shared/src/i18n/pl/journey.ts | 39 +- shared/src/i18n/pl/login.ts | 39 +- shared/src/i18n/pl/memories.ts | 15 +- shared/src/i18n/pl/notif.ts | 9 +- shared/src/i18n/pl/oauth.ts | 93 ++--- shared/src/i18n/pl/packing.ts | 6 +- shared/src/i18n/pl/places.ts | 21 +- shared/src/i18n/pl/planner.ts | 9 +- shared/src/i18n/pl/register.ts | 3 +- shared/src/i18n/pl/reservations.ts | 54 +-- shared/src/i18n/pl/settings.ts | 91 ++--- shared/src/i18n/pl/system_notice.ts | 30 +- shared/src/i18n/pl/trip.ts | 2 +- shared/src/i18n/pl/vacay.ts | 36 +- shared/src/i18n/ru/admin.ts | 150 +++----- shared/src/i18n/ru/atlas.ts | 3 +- shared/src/i18n/ru/backup.ts | 15 +- shared/src/i18n/ru/budget.ts | 160 ++++---- shared/src/i18n/ru/categories.ts | 3 +- shared/src/i18n/ru/collab.ts | 6 +- shared/src/i18n/ru/dashboard.ts | 12 +- shared/src/i18n/ru/day.ts | 3 +- shared/src/i18n/ru/dayplan.ts | 18 +- shared/src/i18n/ru/externalNotifications.ts | 3 +- shared/src/i18n/ru/files.ts | 12 +- shared/src/i18n/ru/journey.ts | 42 +-- shared/src/i18n/ru/login.ts | 30 +- shared/src/i18n/ru/memories.ts | 15 +- shared/src/i18n/ru/notif.ts | 3 +- shared/src/i18n/ru/notifications.ts | 3 +- shared/src/i18n/ru/oauth.ts | 93 ++--- shared/src/i18n/ru/packing.ts | 9 +- shared/src/i18n/ru/perm.ts | 36 +- shared/src/i18n/ru/places.ts | 21 +- shared/src/i18n/ru/planner.ts | 6 +- shared/src/i18n/ru/reservations.ts | 57 +-- shared/src/i18n/ru/settings.ts | 82 ++-- shared/src/i18n/ru/system_notice.ts | 27 +- shared/src/i18n/ru/trip.ts | 2 +- shared/src/i18n/ru/trips.ts | 3 +- shared/src/i18n/ru/vacay.ts | 42 +-- shared/src/i18n/tr/admin.ts | 129 +++---- shared/src/i18n/tr/atlas.ts | 6 +- shared/src/i18n/tr/backup.ts | 9 +- shared/src/i18n/tr/budget.ts | 157 ++++---- shared/src/i18n/tr/categories.ts | 3 +- shared/src/i18n/tr/collab.ts | 3 +- shared/src/i18n/tr/common.ts | 3 +- shared/src/i18n/tr/dashboard.ts | 9 +- shared/src/i18n/tr/day.ts | 3 +- shared/src/i18n/tr/dayplan.ts | 15 +- shared/src/i18n/tr/externalNotifications.ts | 3 +- shared/src/i18n/tr/files.ts | 9 +- shared/src/i18n/tr/journey.ts | 45 +-- shared/src/i18n/tr/login.ts | 42 +-- shared/src/i18n/tr/members.ts | 3 +- shared/src/i18n/tr/memories.ts | 18 +- shared/src/i18n/tr/notif.ts | 15 +- shared/src/i18n/tr/notifications.ts | 9 +- shared/src/i18n/tr/oauth.ts | 84 ++--- shared/src/i18n/tr/packing.ts | 6 +- shared/src/i18n/tr/perm.ts | 42 +-- shared/src/i18n/tr/places.ts | 12 +- shared/src/i18n/tr/planner.ts | 6 +- shared/src/i18n/tr/register.ts | 3 +- shared/src/i18n/tr/reservations.ts | 42 +-- shared/src/i18n/tr/settings.ts | 98 ++--- shared/src/i18n/tr/system_notice.ts | 45 +-- shared/src/i18n/tr/trip.ts | 2 +- shared/src/i18n/tr/trips.ts | 3 +- shared/src/i18n/tr/vacay.ts | 36 +- shared/src/i18n/uk/admin.ts | 162 +++----- shared/src/i18n/uk/atlas.ts | 3 +- shared/src/i18n/uk/backup.ts | 9 +- shared/src/i18n/uk/budget.ts | 157 ++++---- shared/src/i18n/uk/categories.ts | 3 +- shared/src/i18n/uk/collab.ts | 3 +- shared/src/i18n/uk/dashboard.ts | 12 +- shared/src/i18n/uk/day.ts | 3 +- shared/src/i18n/uk/dayplan.ts | 15 +- shared/src/i18n/uk/externalNotifications.ts | 3 +- shared/src/i18n/uk/files.ts | 9 +- shared/src/i18n/uk/journey.ts | 39 +- shared/src/i18n/uk/login.ts | 36 +- shared/src/i18n/uk/memories.ts | 18 +- shared/src/i18n/uk/notif.ts | 6 +- shared/src/i18n/uk/notifications.ts | 3 +- shared/src/i18n/uk/oauth.ts | 96 ++--- shared/src/i18n/uk/packing.ts | 9 +- shared/src/i18n/uk/perm.ts | 36 +- shared/src/i18n/uk/places.ts | 21 +- shared/src/i18n/uk/planner.ts | 6 +- shared/src/i18n/uk/reservations.ts | 57 +-- shared/src/i18n/uk/settings.ts | 82 ++-- shared/src/i18n/uk/system_notice.ts | 27 +- shared/src/i18n/uk/trip.ts | 2 +- shared/src/i18n/uk/trips.ts | 3 +- shared/src/i18n/uk/vacay.ts | 42 +-- shared/src/i18n/zh-TW/admin.ts | 78 ++-- shared/src/i18n/zh-TW/backup.ts | 12 +- shared/src/i18n/zh-TW/budget.ts | 157 ++++---- shared/src/i18n/zh-TW/dashboard.ts | 6 +- shared/src/i18n/zh-TW/dayplan.ts | 3 +- shared/src/i18n/zh-TW/files.ts | 3 +- shared/src/i18n/zh-TW/journey.ts | 12 +- shared/src/i18n/zh-TW/login.ts | 15 +- shared/src/i18n/zh-TW/memories.ts | 15 +- shared/src/i18n/zh-TW/notif.ts | 3 +- shared/src/i18n/zh-TW/notifications.ts | 3 +- shared/src/i18n/zh-TW/oauth.ts | 30 +- shared/src/i18n/zh-TW/packing.ts | 3 +- shared/src/i18n/zh-TW/places.ts | 18 +- shared/src/i18n/zh-TW/planner.ts | 3 +- shared/src/i18n/zh-TW/reservations.ts | 21 +- shared/src/i18n/zh-TW/settings.ts | 85 ++--- shared/src/i18n/zh-TW/share.ts | 3 +- shared/src/i18n/zh-TW/system_notice.ts | 3 +- shared/src/i18n/zh-TW/trip.ts | 2 +- shared/src/i18n/zh-TW/trips.ts | 3 +- shared/src/i18n/zh/admin.ts | 78 ++-- shared/src/i18n/zh/backup.ts | 12 +- shared/src/i18n/zh/budget.ts | 157 ++++---- shared/src/i18n/zh/dashboard.ts | 6 +- shared/src/i18n/zh/dayplan.ts | 3 +- shared/src/i18n/zh/files.ts | 3 +- shared/src/i18n/zh/journey.ts | 12 +- shared/src/i18n/zh/login.ts | 12 +- shared/src/i18n/zh/memories.ts | 15 +- shared/src/i18n/zh/notif.ts | 3 +- shared/src/i18n/zh/notifications.ts | 3 +- shared/src/i18n/zh/oauth.ts | 30 +- shared/src/i18n/zh/packing.ts | 3 +- shared/src/i18n/zh/places.ts | 18 +- shared/src/i18n/zh/planner.ts | 3 +- shared/src/i18n/zh/reservations.ts | 21 +- shared/src/i18n/zh/settings.ts | 88 ++--- shared/src/i18n/zh/share.ts | 3 +- shared/src/i18n/zh/system_notice.ts | 6 +- shared/src/i18n/zh/trip.ts | 2 +- shared/src/i18n/zh/trips.ts | 3 +- shared/src/journey/journey.schema.spec.ts | 49 +-- shared/src/journey/journey.schema.ts | 16 +- shared/src/maps/maps.schema.spec.ts | 29 +- shared/src/maps/maps.schema.ts | 12 +- .../notification/notification.schema.spec.ts | 18 +- .../src/notification/notification.schema.ts | 13 +- shared/src/oauth/oauth.schema.spec.ts | 14 +- shared/src/oauth/oauth.schema.ts | 4 +- shared/src/oidc/oidc.schema.spec.ts | 13 +- shared/src/packing/packing.schema.spec.ts | 20 +- shared/src/packing/packing.schema.ts | 36 +- shared/src/place/place.schema.spec.ts | 22 +- shared/src/place/place.schema.ts | 12 +- .../reservation/reservation.schema.spec.ts | 13 +- shared/src/reservation/reservation.schema.ts | 28 +- shared/src/sanitize/sanitize.spec.ts | 38 +- shared/src/settings/settings.schema.spec.ts | 28 +- shared/src/share/share.schema.spec.ts | 9 +- shared/src/tag/tag.schema.spec.ts | 10 +- shared/src/todo/todo.schema.spec.ts | 31 +- shared/src/todo/todo.schema.ts | 4 +- shared/src/trip/trip.schema.spec.ts | 19 +- shared/src/vacay/vacay.schema.spec.ts | 29 +- shared/src/vacay/vacay.schema.ts | 20 +- shared/src/weather/weather.schema.spec.ts | 14 +- 584 files changed, 6915 insertions(+), 10724 deletions(-) create mode 100644 client/src/components/Budget/CostsPanel.test.tsx diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 01a15e1b..dab6ba43 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -595,6 +595,7 @@ export const budgetApi = { perPersonSummary: (tripId: number | string) => apiClient.get(`/trips/${tripId}/budget/summary/per-person`).then(r => r.data), settlement: (tripId: number | string, base?: string) => apiClient.get(`/trips/${tripId}/budget/settlement`, base ? { params: { base } } : undefined).then(r => r.data), createSettlement: (tripId: number | string, data: { from_user_id: number; to_user_id: number; amount: number }) => apiClient.post(`/trips/${tripId}/budget/settlements`, data).then(r => r.data), + updateSettlement: (tripId: number | string, settlementId: number, data: { from_user_id: number; to_user_id: number; amount: number }) => apiClient.put(`/trips/${tripId}/budget/settlements/${settlementId}`, data).then(r => r.data), deleteSettlement: (tripId: number | string, settlementId: number) => apiClient.delete(`/trips/${tripId}/budget/settlements/${settlementId}`).then(r => r.data), reorderItems: (tripId: number | string, orderedIds: number[]) => apiClient.put(`/trips/${tripId}/budget/reorder/items`, { orderedIds }).then(r => r.data), reorderCategories: (tripId: number | string, orderedCategories: string[]) => apiClient.put(`/trips/${tripId}/budget/reorder/categories`, { orderedCategories } satisfies BudgetReorderCategoriesRequest).then(r => r.data), diff --git a/client/src/components/Budget/CostsPanel.test.tsx b/client/src/components/Budget/CostsPanel.test.tsx new file mode 100644 index 00000000..1d3fe503 --- /dev/null +++ b/client/src/components/Budget/CostsPanel.test.tsx @@ -0,0 +1,138 @@ +// FE-COMP-COSTS: settlements surfaced inline in the Costs ledger (issue #1241) +import { render, screen, waitFor } from '../../../tests/helpers/render' +import { http, HttpResponse } from 'msw' +import { server } from '../../../tests/helpers/msw/server' +import { useAuthStore } from '../../store/authStore' +import { useTripStore } from '../../store/tripStore' +import { resetAllStores, seedStore } from '../../../tests/helpers/store' +import { buildUser, buildTrip, buildBudgetItem } from '../../../tests/helpers/factories' +import CostsPanel from './CostsPanel' + +const tripMembers = [ + { id: 1, username: 'alice', avatar_url: null }, + { id: 2, username: 'bob', avatar_url: null }, +] + +beforeEach(() => { + resetAllStores() + seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true }) + seedStore(useTripStore, { trip: buildTrip({ id: 1, currency: 'EUR' }) }) +}) + +describe('CostsPanel — settlements in the ledger', () => { + it('renders a settle-up payment as a ledger row with an undo action', async () => { + const item = { ...buildBudgetItem({ trip_id: 1, category: 'food', name: 'Dinner' }), total_price: 90, expense_date: '2025-06-15' } + server.use( + http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] })), + http.get('/api/trips/1/budget/settlement', () => + HttpResponse.json({ + balances: [], + flows: [], + settlements: [ + { id: 7, trip_id: 1, from_user_id: 2, to_user_id: 1, amount: 30, created_at: '2025-06-16 10:00:00', from_username: 'bob', to_username: 'alice' }, + ], + }) + ), + ) + render() + + // The expense and the settlement (payment) both appear in the unified ledger. + await screen.findByText('Dinner') + await screen.findByText('Payment') + // The payment row exposes an inline undo (no need to open a separate History modal). + expect(screen.getByTitle('Undo')).toBeInTheDocument() + }) + + it('records a manual payment via the Add payment button', async () => { + let posted: Record | null = null + server.use( + http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [] })), + http.get('/api/trips/1/budget/settlement', () => HttpResponse.json({ balances: [], flows: [], settlements: [] })), + http.post('/api/trips/1/budget/settlements', async ({ request }) => { + posted = await request.json() as Record + return HttpResponse.json({ settlement: { id: 1, ...posted } }) + }), + ) + const { default: userEvent } = await import('@testing-library/user-event') + const user = userEvent.setup() + render() + + await user.click(await screen.findByRole('button', { name: 'Add payment' })) + await user.type(await screen.findByPlaceholderText('0.00'), '25') + // The footer submit is the second "Add payment" control once the modal is open. + const addButtons = screen.getAllByRole('button', { name: 'Add payment' }) + const submit = addButtons[addButtons.length - 1] + await user.click(submit) + await waitFor(() => expect(posted).toMatchObject({ amount: 25 })) + }) + + it('hides payment rows while a text search is active', async () => { + const item = { ...buildBudgetItem({ trip_id: 1, category: 'food', name: 'Dinner' }), total_price: 90, expense_date: '2025-06-15' } + server.use( + http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] })), + http.get('/api/trips/1/budget/settlement', () => + HttpResponse.json({ + balances: [], + flows: [], + settlements: [ + { id: 7, trip_id: 1, from_user_id: 2, to_user_id: 1, amount: 30, created_at: '2025-06-16 10:00:00', from_username: 'bob', to_username: 'alice' }, + ], + }) + ), + ) + const { default: userEvent } = await import('@testing-library/user-event') + const user = userEvent.setup() + render() + + await screen.findByText('Payment') + await user.type(screen.getByPlaceholderText('Search expenses…'), 'Dinner') + // Payment rows have no name, so a search hides them while the matching expense stays. + expect(screen.queryByText('Payment')).not.toBeInTheDocument() + expect(screen.getByText('Dinner')).toBeInTheDocument() + }) + + it('auto-splits the total across participants and rebalances a pinned amount on save', async () => { + let posted: Record | null = null + server.use( + http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [] })), + http.get('/api/trips/1/budget/settlement', () => HttpResponse.json({ balances: [], flows: [], settlements: [] })), + http.post('/api/trips/1/budget', async ({ request }) => { + posted = await request.json() as Record + return HttpResponse.json({ item: { ...buildBudgetItem({ trip_id: 1, name: 'Dinner' }), id: 5 } }) + }), + ) + const { default: userEvent } = await import('@testing-library/user-event') + const user = userEvent.setup() + render() + + await user.click(await screen.findByRole('button', { name: 'Add expense' })) + await user.type(await screen.findByPlaceholderText('e.g. Dinner, souvenirs, gas…'), 'Dinner') + const nums = () => screen.getAllByRole('spinbutton') as HTMLInputElement[] + await user.type(nums()[0], '100') // total → auto equal-split across the 2 participants + await waitFor(() => expect(nums()[1].value).toBe('50')) + expect(nums()[2].value).toBe('50') + // Pin the first participant to 30 → the other non-pinned field rebalances to 70. + await user.clear(nums()[1]); await user.type(nums()[1], '30') + await waitFor(() => expect(nums()[2].value).toBe('70')) + + const addBtns = screen.getAllByRole('button', { name: 'Add expense' }) + await user.click(addBtns[addBtns.length - 1]) // footer submit + await waitFor(() => expect(posted).toBeTruthy()) + expect(posted!.total_price).toBe(100) + expect(posted!.payers).toEqual(expect.arrayContaining([ + expect.objectContaining({ user_id: 1, amount: 30 }), + expect.objectContaining({ user_id: 2, amount: 70 }), + ])) + }) + + it('marks an expense with no payer as Unfinished', async () => { + const item = { ...buildBudgetItem({ trip_id: 1, category: 'food', name: 'Hotel' }), total_price: 90, payers: [], members: [{ user_id: 1, username: 'alice', paid: 0 }] } + server.use( + http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] })), + http.get('/api/trips/1/budget/settlement', () => HttpResponse.json({ balances: [], flows: [], settlements: [] })), + ) + render() + await screen.findByText('Hotel') + expect(screen.getByText('Unfinished')).toBeInTheDocument() + }) +}) diff --git a/client/src/components/Budget/CostsPanel.tsx b/client/src/components/Budget/CostsPanel.tsx index bc3f778c..9541275a 100644 --- a/client/src/components/Budget/CostsPanel.tsx +++ b/client/src/components/Budget/CostsPanel.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useMemo, useCallback } from 'react' import { useSearchParams } from 'react-router-dom' -import { ArrowDown, ArrowUp, BarChart3, Plus, Search, ArrowRight, Check, RotateCcw, History, Pencil, Trash2 } from 'lucide-react' +import { ArrowDown, ArrowUp, BarChart3, Plus, Search, ArrowRight, ArrowLeftRight, Check, RotateCcw, Pencil, Trash2 } from 'lucide-react' import { useTripStore } from '../../store/tripStore' import { useAuthStore } from '../../store/authStore' import { useSettingsStore } from '../../store/settingsStore' @@ -39,6 +39,12 @@ interface SettlementData { settlements: Settlement[] } +// One row in the unified Costs ledger — either an expense or a settle-up payment, +// carrying the date used to group it by day. +type LedgerEntry = + | { kind: 'expense'; date: string; e: BudgetItem } + | { kind: 'payment'; date: string; s: Settlement } + const round2 = (n: number) => Math.round(n * 100) / 100 const FIELD_H = 40 // shared height for the amount / currency / day row in the modal @@ -62,9 +68,10 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps const [settlement, setSettlement] = useState(null) const [filter, setFilter] = useState<'all' | 'mine' | 'owed'>('all') const [search, setSearch] = useState('') - const [histOpen, setHistOpen] = useState(false) const [modalOpen, setModalOpen] = useState(false) const [editing, setEditing] = useState(null) + const [editingSettlement, setEditingSettlement] = useState(null) + const [addingPayment, setAddingPayment] = useState(false) const people = tripMembers const personById = useCallback((id: number) => people.find(p => p.id === id), [people]) @@ -122,21 +129,37 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps return list }, [budgetItems, filter, search, me]) + // Settlements ("payments") shown inline in the ledger. They have no name, so a + // text search hides them; they're excluded from the "owed" expense filter and, + // under "mine", only show transfers I'm part of. + const filteredSettlements = useMemo(() => { + if (search.trim()) return [] + if (filter === 'owed') return [] + let list = settlement?.settlements || [] + if (filter === 'mine') list = list.filter(s => s.from_user_id === me || s.to_user_id === me) + return list + }, [settlement, filter, search, me]) + const dayGroups = useMemo(() => { - const groups: { day: string; items: BudgetItem[] }[] = [] - const labelOf = (e: BudgetItem) => { - if (!e.expense_date) return t('costs.noDate') - try { return new Date(e.expense_date + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' }) } catch { return e.expense_date } + const entries: LedgerEntry[] = [ + ...filtered.map(e => ({ kind: 'expense' as const, date: e.expense_date || '', e })), + ...filteredSettlements.map(s => ({ kind: 'payment' as const, date: (s.created_at || '').slice(0, 10), s })), + ] + const labelOf = (date: string) => { + if (!date) return t('costs.noDate') + try { return new Date(date + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' }) } catch { return date } } - const sorted = filtered.slice().sort((a, b) => (b.expense_date || '').localeCompare(a.expense_date || '')) - for (const e of sorted) { - const day = labelOf(e) + // Newest day first; within a day, expenses before payments (insertion order). + const sorted = entries.slice().sort((a, b) => (b.date || '').localeCompare(a.date || '')) + const groups: { day: string; entries: LedgerEntry[] }[] = [] + for (const en of sorted) { + const day = labelOf(en.date) let g = groups.find(x => x.day === day) - if (!g) { g = { day, items: [] }; groups.push(g) } - g.items.push(e) + if (!g) { g = { day, entries: [] }; groups.push(g) } + g.entries.push(en) } return groups - }, [filtered, locale, t]) + }, [filtered, filteredSettlements, locale, t]) // ── settle actions ────────────────────────────────────────────────────── const settleFlow = async (fromId: number, toId: number, amount: number) => { @@ -280,14 +303,16 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps {search ? t('costs.noMatch') : t('costs.emptyText')} ) : dayGroups.map(g => { - const dtot = g.items.reduce((a, e) => a + baseTotal(e), 0) + const dtot = g.entries.reduce((a, en) => en.kind === 'expense' ? a + baseTotal(en.e) : a, 0) return (
{g.day}{t('costs.spent', { amount: fmt(dtot) })}
- {g.items.map(e => )} + {g.entries.map(en => en.kind === 'expense' + ? + : )}
) @@ -300,11 +325,13 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
{t('costs.settleUp')} · {(settlement?.flows || []).length}
- + {canEdit && ( + + )}
@@ -330,9 +357,11 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps onSaved={() => { setModalOpen(false); loadBudgetItems(tripId); loadSettlement() }} /> )} - setHistOpen(false)} title={t('costs.settleHistory')} size="md"> - - + {(editingSettlement || addingPayment) && ( + { setEditingSettlement(null); setAddingPayment(false) }} + onSaved={() => { setEditingSettlement(null); setAddingPayment(false); loadSettlement() }} /> + )}