Skip to content

Commit 2e22ede

Browse files
authored
Development debugging user balance (#546)
* feat: Display consumed from balance year - Add `formatConsumedFrom` utility function - Import and use function in vacation request template - Export function from utils module * feat: Add user balance debug and fix features - Add API endpoints for debugging and fixing user balances - Implement Vue component for auditing and correcting vacation balances - Update dashboard navigation to include "Danger Zone" with balance audit option - Add Django management commands for debugging and recalculating user balances * refactor: Improve user fetching and API handling - Update UsersAdminOfficeUsersApi to accept query params - Remove redundant pagination logic from frontend components - Refactor SupervisorsAPIView to fetch all active users - Update GetUsersInAdminOfficeAPIView to support 'all' and 'active_only' params - Add get_active_admin_office_users to filter users by office and active status * feat: Import VacationBalanceCalculator - Import VacationBalanceCalculator from utils * style: Adjust button size attribute - Remove `size="comfortable"` from v-btn * refactor: Improve vacation filtering and status handling - Add status filtering to vacation list endpoint - Normalize request type parameter for filtering - Update vacation list query logic for better filtering - Remove unused 'pending-requests' route and component * refactor: Improve null checks and type declarations - Add `ws` connection ref to Window interface - Initialize `result` to null for vacation fetching - Add null checks for API responses before accessing `results` and `count` * refactor: Update vacation retrieval and validation - Simplify queryset logic for vacation retrieval - Add default values for request_type and status parameters - Implement validation for user_id and request_type in GET method - Improve user existence check before processing requests * fix: Adjust query parameters for vacation list - Update vacation query to conditionally add status and type - Set default status to "all" in team pending requests * style: Adjust column widths in vacation request view - Conditionally set col widths based on tab - Hide user select when not on explore tab - Adjust status and type select widths for team tab * style: Add pointer cursor to buttons - Add pointer cursor for buttons * feat: Allow all users to view requests and filter by name - Display "Not Provided" if approval user is missing - Set selected user to logged-in user initially - Fetch users for all users to explore - Allow viewing requests for any user via targetUserId - Enable filtering user requests by full name in backend * refactor: Update user fetching logic for explore tab - Make "All Users History" tab accessible to all users - Use general user list endpoint - Fetch up to 1000 users - Assign results to users.value * feat: Add API endpoint for active users - Add UsersActiveApi client class - Update users API client to include active endpoint - Implement AllActiveUsersAPIView for active users - Add URL path for active users endpoint * refactor: Update unwrap for get list - Add transform for get list response * fix: respect useOldBalance switch when switching users in vacation request * feat: Add old_balance flag to vacation creation - Add `is_old_balance` to vacation serializer - Pass `is_old_balance` to Vacation model creation - Pass `is_old_balance` to VacationNotification
1 parent 265c914 commit 2e22ede

30 files changed

Lines changed: 1416 additions & 742 deletions

client/global.d.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
declare global {
22
export interface Window {
3-
// Define window props here
4-
// -->
3+
connections: {
4+
ws: import('vue').Ref<WebSocket | null>
5+
}
56
}
67
}
78

8-
export {} // Important
9+
export { } // Important

client/src/App.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,8 @@ export default defineComponent({
3535
.vue3-notifier-container .text-success {
3636
background-color: rgb(2, 29, 3) !important;
3737
}
38+
39+
button {
40+
cursor: pointer !important;
41+
}
3842
</style>

client/src/clients/api/base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ export abstract class ApiClientBase {
234234
description: errorDescription
235235
})
236236

237-
panic(err)
237+
panic(errorDescription)
238238
}
239239

240240
// Transform and return the response data

client/src/clients/api/users.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export class UsersApi extends ApiClientBase {
1111
readonly skills: UsersSkillsApi
1212
readonly team: UsersTeamApi
1313
readonly supervisors: UsersSupervisorsApi
14+
readonly active: UsersActiveApi
1415

1516
constructor(options: Api.ClientOptions) {
1617
super(options)
@@ -22,6 +23,7 @@ export class UsersApi extends ApiClientBase {
2223
this.skills = new UsersSkillsApi(options, this.path)
2324
this.team = new UsersTeamApi(options, this.path)
2425
this.supervisors = new UsersSupervisorsApi(options, this.path)
26+
this.active = new UsersActiveApi(options, this.path)
2527
}
2628

2729
list(query?: any) {
@@ -43,12 +45,12 @@ export class UsersApi extends ApiClientBase {
4345
class UsersAdminApi extends ApiClientBase {
4446
protected readonly path = '/admin'
4547

46-
readonly office_users: UsersAdminofficeUsersApi
48+
readonly office_users: UsersAdminOfficeUsersApi
4749

4850
constructor(options: Api.ClientOptions, prePath: string) {
4951
super(options, prePath)
5052

51-
this.office_users = new UsersAdminofficeUsersApi(options, prePath + this.path)
53+
this.office_users = new UsersAdminOfficeUsersApi(options, prePath + this.path)
5254
}
5355

5456
list(query?: Api.Inputs.List) {
@@ -73,18 +75,19 @@ class UsersAdminApi extends ApiClientBase {
7375
}
7476
}
7577

76-
class UsersAdminofficeUsersApi extends ApiClientBase {
78+
class UsersAdminOfficeUsersApi extends ApiClientBase {
7779
protected readonly path = '/office_users'
7880

79-
list(query?: Api.Inputs.List) {
81+
list(query?: Api.Inputs.List & { all?: boolean; active_only?: boolean }) {
8082
return this.unwrap(() => this.$http.get(this.getUrl('', query)))
8183
}
8284
}
8385

86+
8487
class UsersBirthdatesApi extends ApiClientBase {
8588
protected readonly path = '/birthdates'
8689

87-
list() {}
90+
list() { }
8891
}
8992

9093
class UsersSetActiveApi extends ApiClientBase {
@@ -120,9 +123,9 @@ class UsersSkillsApi extends ApiClientBase {
120123
class UsersSupervisorsApi extends ApiClientBase {
121124
protected readonly path = '/supervisors'
122125

123-
async list( query?: Api.Inputs.List) {
126+
async list() {
124127
ApiClientBase.assertUser()
125-
return this.unwrap(() => this.$http.get<Api.Returns.List<Api.User>>(this.getUrl("", query)))
128+
return this.unwrap(() => this.$http.get(this.getUrl()))
126129
}
127130
}
128131

@@ -138,7 +141,7 @@ class UsersTeamApi extends ApiClientBase {
138141
}
139142

140143
list(query?: any) {
141-
return this.unwrap(() => this.$http.get<Api.Returns.List<Api.User>>(this.getUrl('', query)))
144+
return this.unwrap(() => this.$http.get<Api.Returns.List<Api.User>>(this.getUrl('', query)))
142145
}
143146
}
144147

@@ -154,3 +157,13 @@ class UsersTeamSupervisorsApi extends ApiClientBase {
154157
)
155158
}
156159
}
160+
161+
class UsersActiveApi extends ApiClientBase {
162+
protected readonly path = '/active'
163+
164+
async list() {
165+
return this.unwrap(() => this.$http.get(this.getUrl()), {
166+
transform: (d: any) => d.results
167+
})
168+
}
169+
}

client/src/clients/api/vacations.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,22 @@ class VacationsAdmin extends ApiClientBase {
182182

183183
return result
184184
}
185+
186+
async debugBalance(userId: number, query: { year: number, reason: string }) {
187+
ApiClientBase.assertUser()
188+
return this.unwrap(
189+
() => this.$http.get<any>(this.getUrl(`/debug-balance/${userId}`, query)),
190+
{ transform: (d) => d.results }
191+
)
192+
}
193+
194+
async fixBalance(userId: number, data: { year: number, reason: string }) {
195+
ApiClientBase.assertUser()
196+
return this.unwrap(
197+
() => this.$http.post<any>(this.getUrl(`/fix-balance/${userId}`), data),
198+
{ transform: (d) => d.results }
199+
)
200+
}
185201
}
186202

187203
class VacationsUserApi extends ApiClientBase {
Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,38 @@
11
<template>
2-
<v-card variant="outlined" class="pa-2 card-outline">
3-
<v-list active-class="v-list-item__overlay">
4-
<v-list-item v-for="item in items" :key="item.id" @click="selectTab(item)"
5-
:class="{ 'active-item': item.active }">
6-
<v-list-item-content>
2+
<v-card variant="outlined" class="pa-0 card-outline border-opacity-25 shadow-sm">
3+
<v-list v-model:opened="openedGroups" density="comfortable" color="primary">
4+
<template v-for="item in items" :key="item.id">
5+
<!-- Expandable Item (Group) -->
6+
<v-list-group v-if="item.children" :value="item.id">
7+
<template v-slot:activator="{ props }">
8+
<v-list-item v-bind="props" class="py-2" base-color="white">
9+
<template v-slot:prepend>
10+
<v-icon :icon="item.icon" size="small" class="mr-2"></v-icon>
11+
</template>
12+
<v-list-item-title class="font-weight-bold text-uppercase text-caption">
13+
{{ item.name }}
14+
</v-list-item-title>
15+
</v-list-item>
16+
</template>
17+
18+
<v-list-item v-for="child in item.children" :key="child.id" @click="selectTab(child)"
19+
:class="{ 'active-item': child.active }" class="pl-10" min-height="40" rounded="0">
20+
<template v-slot:prepend>
21+
<v-icon :icon="child.icon" size="small" class="mr-2"></v-icon>
22+
</template>
23+
<v-list-item-title class="text-body-2">{{ child.name }}</v-list-item-title>
24+
</v-list-item>
25+
</v-list-group>
26+
27+
<!-- Standard Item -->
28+
<v-list-item v-else @click="selectTab(item)" :class="{ 'active-item': item.active }" class="py-2"
29+
min-height="48">
30+
<template v-slot:prepend>
31+
<v-icon :icon="item.icon" size="small" class="mr-2"></v-icon>
32+
</template>
733
<v-list-item-title class="font-weight-bold">{{ item.name }}</v-list-item-title>
8-
</v-list-item-content>
9-
</v-list-item>
34+
</v-list-item>
35+
</template>
1036
</v-list>
1137
</v-card>
1238
</template>
@@ -23,14 +49,20 @@ export default {
2349
setup(_, { emit }) {
2450
const activeTab = useRouteQuery<undefined | string>('tab', undefined);
2551
const $route = useRoute();
52+
const openedGroups = ref<number[]>([]);
2653
27-
const itemsRef = ref(items);
54+
const itemsRef = items;
2855
2956
const resetActiveTab = () => {
30-
itemsRef.value.forEach(item => item.active = false);
57+
itemsRef.value.forEach(item => {
58+
item.active = false
59+
if (item.children) {
60+
item.children.forEach(child => child.active = false)
61+
}
62+
});
3163
};
3264
33-
const selectTab = (item: { id: number; name: string; active: boolean }) => {
65+
const selectTab = (item: any) => {
3466
resetActiveTab();
3567
item.active = true;
3668
activeTab.value = `${item.id}`;
@@ -40,13 +72,27 @@ export default {
4072
onMounted(() => {
4173
const tabQuery = $route.query['tab'];
4274
if (tabQuery && !isNaN(+tabQuery)) {
43-
const item = itemsRef.value.find(i => i.id === +tabQuery);
44-
if (item) {
75+
let foundItem: any = itemsRef.value.find(i => i.id === +tabQuery);
76+
77+
if (!foundItem) {
78+
// Search in children
79+
for (const parent of itemsRef.value) {
80+
if (parent.children) {
81+
const child = parent.children.find(c => c.id === +tabQuery);
82+
if (child) {
83+
foundItem = child;
84+
// Auto-open parent group
85+
openedGroups.value = [parent.id];
86+
break;
87+
}
88+
}
89+
}
90+
}
91+
92+
if (foundItem) {
4593
resetActiveTab();
46-
item.active = true;
47-
emit('item-selected', item);
48-
} else {
49-
activeTab.value = undefined;
94+
foundItem.active = true;
95+
emit('item-selected', foundItem);
5096
}
5197
} else {
5298
resetActiveTab();
@@ -56,30 +102,28 @@ export default {
56102
57103
return {
58104
items: itemsRef,
59-
selectTab
105+
selectTab,
106+
openedGroups
60107
};
61108
}
62109
};
63110
</script>
64111

65-
<style>
66-
.v-list-item__overlay {
67-
background-color: #47a2ff !important;
68-
}
69-
70-
.v-list-item:hover>.v-list-item__overlay {
71-
background-color: #47a2ff !important;
112+
<style scoped>
113+
.active-item {
114+
background-color: rgb(var(--v-theme-primary)) !important;
115+
color: white !important;
72116
}
73117
74-
.v-list-item--variant-text .v-list-item__overlay {
75-
background-color: #47a2ff !important;
118+
.card-outline {
119+
border-color: rgba(255, 255, 255, 0.12) !important;
76120
}
77121
78-
.active-item {
79-
background-color: #143453 !important;
122+
.v-list-group__header.v-list-item--active {
123+
color: rgb(var(--v-theme-primary)) !important;
80124
}
81125
82-
.card-outline {
83-
border-color: #67696b !important;
126+
:deep(.v-list-item__spacer) {
127+
display: none !important;
84128
}
85129
</style>

client/src/components/SideDrawer.vue

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,24 @@
11
<template>
22
<v-card class="layout-container">
3-
<v-navigation-drawer
4-
v-if="$route.path != '/login'"
5-
v-model="drawer"
6-
theme="dark"
7-
:permanent="!isMobile"
8-
:temporary="isMobile"
9-
class="fixed-sidebar">
3+
<v-navigation-drawer v-if="$route.path != '/login'" v-model="drawer" theme="dark" :permanent="!isMobile"
4+
:temporary="isMobile" class="fixed-sidebar">
105
<router-link to="/">
116
<v-img :src="logo" max-width="110" class="ml-3 mt-5 mb-4"></v-img>
127
</router-link>
138
<v-divider></v-divider>
149
<v-list color="transparent">
1510
<template v-for="item in filteredItems" :key="item.title">
16-
<v-list-item
17-
:to="item.path"
18-
:prepend-icon="item.icon"
19-
color="white"
20-
:class="{ 'router-link-active': $route.path === item.path }"
21-
@click="isMobile && (drawer = false)">
11+
<v-list-item :to="item.path" :prepend-icon="item.icon" color="white"
12+
:class="{ 'router-link-active': $route.path === item.path }" @click="isMobile && (drawer = false)">
2213
{{ item.title }}
2314
</v-list-item>
2415
</template>
2516
</v-list>
2617
</v-navigation-drawer>
2718

2819
<div :class="$route.path != '/login' ? 'main-container' : ''">
29-
<CshrToolbar v-if="$route.path != '/login'" @logout="logout" @toggle-drawer="drawer = !drawer" :is-mobile="isMobile" />
20+
<CshrToolbar v-if="$route.path != '/login'" @logout="logout" @toggle-drawer="drawer = !drawer"
21+
:is-mobile="isMobile" />
3022
<div class="scrollable-content">
3123
<template v-if="isReadyRouter.state.value">
3224
<router-view />
@@ -94,11 +86,6 @@ export default {
9486
title: 'Requests',
9587
path: '/requests'
9688
},
97-
{
98-
icon: 'mdi-account-clock',
99-
title: 'Pending Requests',
100-
path: '/pending-requests'
101-
},
10289
{
10390
icon: 'mdi-bell',
10491
title: 'Notifications',

client/src/components/dashboard/AddUser.vue

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export default {
8383
isLoading.value = true
8484
clearForm();
8585
locations.value = await listOffices({ count: 10, key: 'location', items: [], page: 1 })
86-
teamLeads.value = await listTeamLeads({ count: 10, key: 'reporting_to', items: [], page: 1 })
86+
teamLeads.value = await listTeamLeads()
8787
} catch (error) {
8888
console.error(error)
8989
} finally {
@@ -106,8 +106,6 @@ export default {
106106
options.page += 1
107107
if (options.key === 'location') {
108108
locations.value = await listOffices(options)
109-
} else if (options.key === 'reporting_to') {
110-
teamLeads.value = await listTeamLeads(options)
111109
}
112110
}
113111

0 commit comments

Comments
 (0)