From ef127e481fb337f78f1a6df4d74adfb0e1948438 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Tue, 13 Jan 2026 11:17:25 +0200 Subject: [PATCH 1/3] feat: Add year selection for vacation balances - Add year selection component and logic - Update balance API to accept year parameter - Refactor balance fetching and updating to support selected year - Add watcher for year changes to refetch balances - Update UI to display selected year in title and alert --- client/src/clients/api/vacations.ts | 2 +- .../dashboard/SetUserVacationBalance.vue | 47 ++++++++++++++++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/client/src/clients/api/vacations.ts b/client/src/clients/api/vacations.ts index a329a36e6..409424444 100644 --- a/client/src/clients/api/vacations.ts +++ b/client/src/clients/api/vacations.ts @@ -58,7 +58,7 @@ export class VacationsApi extends ApiClientBase { return userBalance } - async updateUsersBalance(data: { user_ids: number[], balance: Api.BalanceVacation }) { + async updateUsersBalance(data: { user_ids: number[], balance: Api.BalanceVacation, year?: number }) { ApiClientBase.assertUser() return await this.unwrap(() => this.$http.put(this.getUrl(`/balance/users`), data), { transform: (d) => d.results diff --git a/client/src/components/dashboard/SetUserVacationBalance.vue b/client/src/components/dashboard/SetUserVacationBalance.vue index a9baac56f..39b115737 100644 --- a/client/src/components/dashboard/SetUserVacationBalance.vue +++ b/client/src/components/dashboard/SetUserVacationBalance.vue @@ -34,7 +34,7 @@ + + + + + - The form below allows you to set vacation balances for users. You can select multiple users and then update - the - balance as the base balance for all selected users. + The form below allows you to set vacation balances for users. You can select the year and multiple users, + then + update + the balance as the base balance for all selected users for the specified year. @@ -121,6 +128,11 @@ export default { const balances: Ref<{ [userId: number]: Api.BalanceVacation }> = ref({}) const isLoading = ref(false) + // Year selection - dynamically generate last 5 years + const currentYear = new Date().getFullYear() + const availableYears = Array.from({ length: 5 }, (_, i) => currentYear - i) + const selectedYear = ref(currentYear) + // Computed Properties const selectedSomeUsers = computed(() => selectedUsers.value.length > 0) const selectedAllUsers = computed(() => selectedUsers.value.length === officeUsers.value.length) @@ -128,6 +140,7 @@ export default { const balance_result = await $api.vacations.updateUsersBalance( { user_ids: selectedUsers.value.map(user => user.id), + year: selectedYear.value, balance: { total_days: { annual: +totalBalanceFields.value.find((i) => i.key === 'annual')!.value, @@ -149,6 +162,8 @@ export default { } ) + // Clear old balances and update with new ones for the selected year + balances.value = {} for (let i = 0; i < balance_result!.length; i++) { balances.value[balance_result![i].user!.id] = balance_result![i] } @@ -169,9 +184,9 @@ export default { ] - // Fetch and Cache User Balance + // Fetch and Cache User Balance for selected year const fetchUserBalance = async (userId: number) => { - const result = await $api.vacations.getUserBalance(userId) + const result = await $api.vacations.getUserBalance(userId, { year: selectedYear.value }) Reflect.set(balances.value, userId, result) } @@ -290,6 +305,24 @@ export default { } }, { deep: true }) + // Watch for year changes to refetch balances + watch(selectedYear, async () => { + // Clear cached balances when year changes + balances.value = {} + + // Refetch balances for all selected users with new year + for (const user of selectedUsers.value) { + await fetchUserBalance(user.id) + } + + // Update form values + if (selectedUsers.value.length == 1) { + updateBalanceFormValues({ user: selectedUsers.value[0] }) + } else { + updateBalanceFormValues({ reset: true }) + } + }) + const updateBalanceFormValues = (options?: { reset?: boolean, user?: Api.User }) => { if (options && options.reset) { for (let i = 0; i < totalBalanceFields.value.length; i++) { @@ -341,6 +374,8 @@ export default { Reflect, isLoading, submitBalance, + availableYears, + selectedYear, fetchUserBalance, getUserById, From 3950d4a55d1142e3eaa0d129cf569ad7a39d94b3 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Tue, 13 Jan 2026 11:19:25 +0200 Subject: [PATCH 2/3] feat: Add year parameter for vacation balance - Add IntegerField for year in serializer - Allow filtering vacation balance by year - Update docstring with year parameter --- server/cshr/serializers/vacations.py | 2 ++ server/cshr/views/vacations.py | 25 ++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/server/cshr/serializers/vacations.py b/server/cshr/serializers/vacations.py index 25c00f71e..f6cddf98b 100644 --- a/server/cshr/serializers/vacations.py +++ b/server/cshr/serializers/vacations.py @@ -9,6 +9,7 @@ FloatField, ReadOnlyField, CharField, + IntegerField, ) from cshr.models.vacations import ( UserVacationBalance, @@ -145,6 +146,7 @@ class BalanceObjectSerializer(Serializer): class UpdateVacationBalanceSerializer(Serializer): user_ids = ListField() balance = BalanceObjectSerializer() + year = IntegerField(required=False) class UserVacationBalanceSerializer(ModelSerializer): diff --git a/server/cshr/views/vacations.py b/server/cshr/views/vacations.py index 5eac9b8b3..28f51371c 100644 --- a/server/cshr/views/vacations.py +++ b/server/cshr/views/vacations.py @@ -275,12 +275,35 @@ def get(self, request: Request, user_id: str) -> Response: Use this endpoint to get user balance ### Parameters - `user_id`: The id of the user + - `year`: Optional year parameter (defaults to current year) """ user = get_user_by_id(user_id) if user is None: return CustomResponse.not_found(message="User not found", status_code=404) + year = request.query_params.get("year") + if year: + try: + year = int(year) + except ValueError: + return CustomResponse.bad_request(message="Invalid year parameter") + + # Get or create balance for specific year + user_balance, _ = UserVacationBalance.objects.get_or_create( + user=user, + year=year, + defaults={ + "total_days": VacationBalanceModel.objects.create(), + "remaining_days": VacationBalanceModel.objects.create(compensation=0, paternity=0, maternity=0), + "transferred_days": None, + }, + ) + return CustomResponse.success( + message="Success found balance.", + data=self.serializer_class(user_balance).data, + ) + balance_calculator = VacationBalanceCalculator( applying_user=user, vacation=None ) @@ -305,7 +328,7 @@ def put(self, request: Request): user_ids = serializer.validated_data.get("user_ids", []) balance = serializer.validated_data.get("balance", {}) - current_year = datetime.now().year + current_year = serializer.validated_data.get("year", datetime.now().year) users = self.get_users_by_ids(user_ids) if not isinstance(users, list): # If not a list, it's a response (error case) From 51ca744208c3f4cffe8fcc685f51f380bc20789f Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Tue, 13 Jan 2026 11:49:40 +0200 Subject: [PATCH 3/3] feat: Improve vacation balance calculation logic - Filter vacations affecting current year balance - Add balance source description to report - Indicate if vacation affects year balance --- .../components/dashboard/AuditUserBalance.vue | 8 +++--- server/cshr/services/balance.py | 26 +++++++++++++++++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/client/src/components/dashboard/AuditUserBalance.vue b/client/src/components/dashboard/AuditUserBalance.vue index bb1b41c1e..2ddbe04c6 100644 --- a/client/src/components/dashboard/AuditUserBalance.vue +++ b/client/src/components/dashboard/AuditUserBalance.vue @@ -45,7 +45,7 @@

Audit Results for {{ reportData.user_full_name - }}

+ }}
Period: {{ reportData.year }} | Reason: {{ reportData.reason }}
@@ -124,9 +124,9 @@ {{ vac.recalc }} - - {{ vac.is_old_balance ? 'Transferred' : 'Main' }} + + {{ vac.balance_source || (vac.is_old_balance ? 'Transferred' : 'Main') }} diff --git a/server/cshr/services/balance.py b/server/cshr/services/balance.py index 77cc36ec0..b32a4a6f9 100644 --- a/server/cshr/services/balance.py +++ b/server/cshr/services/balance.py @@ -1,5 +1,6 @@ from datetime import datetime from typing import Any, Dict, List, Optional +from django.db.models import Q from cshr.models.users import User, USER_TYPE from cshr.models.vacations import UserVacationBalance, Vacation from cshr.models.requests import STATUS_CHOICES @@ -31,11 +32,16 @@ def audit_user_balance(user: User, year: int, reason: str) -> Dict[str, Any]: total_quota = getattr(balance_obj.total_days, reason, 0) current_db_remaining = getattr(balance_obj.remaining_days, reason, 0) + # Query vacations that affect this year's balance: + # 1. Vacations from this year that are NOT from old balance (consumed from current year's balance) + # 2. Vacations from the next year that ARE from old balance (consumed from this year's transferred balance) vacations = Vacation.objects.filter( applying_user=user, - from_date__year=year, status__in=[STATUS_CHOICES.APPROVED, STATUS_CHOICES.CANCEL_REJECTED], reason=reason, + ).filter( + Q(from_date__year=year, is_old_balance=False) | # Regular vacations from this year + Q(from_date__year=year + 1, is_old_balance=True) # Next year vacations using this year's balance ).order_by("from_date") reports = [] @@ -52,9 +58,23 @@ def audit_user_balance(user: User, year: int, reason: str) -> Dict[str, Any]: ): days = 1.0 - if not vac.is_old_balance: + # Count days that affect this year's remaining balance: + # - Current year vacations (not from old balance) → deducted from this year's remaining + # - Next year vacations (from old balance) → deducted from this year's remaining (via transferred) + is_current_year_main = vac.from_date.year == year and not vac.is_old_balance + is_next_year_transferred = vac.from_date.year == year + 1 and vac.is_old_balance + + if is_current_year_main or is_next_year_transferred: total_recalc_current += days + # Determine the balance source description + if is_next_year_transferred: + balance_source = f"Transferred (from {year})" + elif vac.is_old_balance: + balance_source = f"Transferred (from {year - 1})" + else: + balance_source = "Main" + reports.append( { "id": vac.id, @@ -63,6 +83,8 @@ def audit_user_balance(user: User, year: int, reason: str) -> Dict[str, Any]: "stored": vac.actual_days, "recalc": days, "is_old_balance": vac.is_old_balance, + "balance_source": balance_source, + "affects_year_balance": is_current_year_main or is_next_year_transferred, } )