Skip to content

Commit 9a42c8b

Browse files
committed
feat(workplace): add initial API integration for team schedule
1 parent 7c6f837 commit 9a42c8b

File tree

10 files changed

+1049
-263
lines changed

10 files changed

+1049
-263
lines changed

apps/workplace/src/app/book/desk-flow/auto-assigned-desk-modal.component.ts

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import { MatRippleModule } from '@angular/material/core';
1212
import { MatDialogRef } from '@angular/material/dialog';
1313
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
1414
import { Router } from '@angular/router';
15-
import { BookingAsset, BookingFormService } from '@placeos/bookings';
15+
import {
16+
BookingAsset,
17+
BookingFormService,
18+
findNearbyFeature,
19+
} from '@placeos/bookings';
1620
import {
1721
AsyncHandler,
1822
Desk,
@@ -226,6 +230,10 @@ export class AutoAssignedDeskModalComponent
226230
public readonly assigned_desk = signal<BookingAsset | Desk | null>(null);
227231
public readonly date = model<number | undefined>(undefined);
228232
public readonly duration = model<number | undefined>(undefined);
233+
/** When set, tries to find a desk nearby to this desk */
234+
public readonly nearby_desk_id = model<string | undefined>(undefined);
235+
/** When set, filters available desks to this level */
236+
public readonly level_id = model<string | undefined>(undefined);
229237

230238
public zoom = 1.5;
231239
public center = signal({ x: 0.5, y: 0.5 });
@@ -262,7 +270,7 @@ export class AutoAssignedDeskModalComponent
262270
});
263271

264272
// Get available resources (desks)
265-
const available_desks = await firstTruthyValueFrom(
273+
let available_desks = await firstTruthyValueFrom(
266274
this._state.available_resources,
267275
);
268276

@@ -272,26 +280,79 @@ export class AutoAssignedDeskModalComponent
272280
return;
273281
}
274282

275-
// Group desks by level and find level with most available desks
276-
const desks_by_level = available_desks.reduce(
277-
(acc, desk) => {
278-
const level_id = desk.zone?.id || 'unknown';
279-
if (!acc[level_id]) {
280-
acc[level_id] = [];
283+
let assigned_desk: BookingAsset;
284+
const nearby_desk_id = this.nearby_desk_id();
285+
const level_id = this.level_id();
286+
287+
// If nearby_desk_id is provided, try to find a desk nearby
288+
if (nearby_desk_id) {
289+
// Filter to same level if level_id is provided
290+
if (level_id) {
291+
const level_desks = available_desks.filter(
292+
(desk) => desk.zone?.id === level_id,
293+
);
294+
if (level_desks.length > 0) {
295+
available_desks = level_desks;
281296
}
282-
acc[level_id].push(desk);
283-
return acc;
284-
},
285-
{} as Record<string, typeof available_desks>,
286-
);
297+
}
287298

288-
// Find the level with the most available desks
289-
const level_with_most_desks = Object.entries(desks_by_level).sort(
290-
([, a], [, b]) => b.length - a.length,
291-
)[0];
299+
// Try to find the level by looking at nearby desk in all resources
300+
const all_resources = await firstTruthyValueFrom(
301+
this._state.resources,
302+
);
303+
const nearby_resource = all_resources.find(
304+
(r) =>
305+
r.id === nearby_desk_id || r.map_id === nearby_desk_id,
306+
);
307+
308+
if (nearby_resource?.zone?.id) {
309+
const level = this._org.levelWithID([
310+
nearby_resource.zone.id,
311+
]);
312+
if (level?.map_id) {
313+
// Use findNearbyFeature to get the closest desk
314+
const desk_ids = available_desks.map(
315+
(d) => d.map_id || d.id,
316+
);
317+
const closest_id = await findNearbyFeature(
318+
level.map_id,
319+
nearby_desk_id,
320+
desk_ids,
321+
);
322+
if (closest_id) {
323+
assigned_desk = available_desks.find(
324+
(d) =>
325+
d.id === closest_id ||
326+
d.map_id === closest_id,
327+
);
328+
}
329+
}
330+
}
331+
}
292332

293-
// Pick the first desk from the level with most availability
294-
const assigned_desk = level_with_most_desks[1][0];
333+
// Fallback to original logic if no nearby desk found
334+
if (!assigned_desk) {
335+
// Group desks by level and find level with most available desks
336+
const desks_by_level = available_desks.reduce(
337+
(acc, desk) => {
338+
const zone_id = desk.zone?.id || 'unknown';
339+
if (!acc[zone_id]) {
340+
acc[zone_id] = [];
341+
}
342+
acc[zone_id].push(desk);
343+
return acc;
344+
},
345+
{} as Record<string, typeof available_desks>,
346+
);
347+
348+
// Find the level with the most available desks
349+
const level_with_most_desks = Object.entries(
350+
desks_by_level,
351+
).sort(([, a], [, b]) => b.length - a.length)[0];
352+
353+
// Pick the first desk from the level with most availability
354+
assigned_desk = level_with_most_desks[1][0];
355+
}
295356

296357
form.patchValue({
297358
asset_id: assigned_desk.id,

apps/workplace/src/app/team-schedule/common.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { User } from '@placeos/common';
1+
import { StaffUser, User } from '@placeos/common';
22

33
export type LocationStatus =
44
| 'unspecified'
@@ -11,6 +11,10 @@ export type LocationStatus =
1111
export interface DeskBooking {
1212
building_name: string;
1313
desk_code: string;
14+
desk_id?: string;
15+
level_id?: string;
16+
date?: number;
17+
duration?: number;
1418
}
1519

1620
export interface DayStatus {
@@ -20,7 +24,7 @@ export interface DayStatus {
2024
}
2125

2226
export interface TeamMember {
23-
user: User;
27+
user: StaffUser | User;
2428
is_favorite: boolean;
2529
is_my_team: boolean;
2630
department?: string;

apps/workplace/src/app/team-schedule/team-quick-actions.component.ts

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Component, inject } from '@angular/core';
22
import { MatDialog } from '@angular/material/dialog';
33
import { Router } from '@angular/router';
4-
import { settingSignal } from '@placeos/common';
4+
import { currentUser, settingSignal, User } from '@placeos/common';
55
import { IconComponent } from '@placeos/components';
6+
import { BookingFormService } from '@placeos/bookings';
7+
import { TeamScheduleService } from './team-schedule.service';
68

79
@Component({
810
selector: 'team-quick-actions',
@@ -37,18 +39,33 @@ import { IconComponent } from '@placeos/components';
3739
btn
3840
matRipple
3941
class="white w-full flex-1 space-x-2"
40-
(click)="autoAssignDesk()"
42+
(click)="bookForGroup()"
4143
>
4244
<icon class="text-2xl">bolt</icon>
43-
<div>Book for team</div>
45+
@if (select_mode()) {
46+
<div>
47+
Book selected
48+
@if (selected_count() > 0) {
49+
({{ selected_count() }})
50+
}
51+
</div>
52+
} @else {
53+
<div>Book for team</div>
54+
}
4455
</button>
4556
<button
4657
btn
4758
matRipple
4859
class="inverse white w-full flex-1 space-x-2"
60+
(click)="toggleSelectMode()"
4961
>
50-
<icon class="text-2xl">map</icon>
51-
<div>Select Colleagues</div>
62+
@if (select_mode()) {
63+
<icon class="text-2xl">close</icon>
64+
<div>Clear</div>
65+
} @else {
66+
<icon class="text-2xl">group_add</icon>
67+
<div>Select Colleagues</div>
68+
}
5269
</button>
5370
</div>
5471
</div>
@@ -69,8 +86,53 @@ import { IconComponent } from '@placeos/components';
6986
export class TeamQuickActionsComponent {
7087
private _dialog = inject(MatDialog);
7188
private _router = inject(Router);
89+
private _booking_form = inject(BookingFormService);
90+
private _service = inject(TeamScheduleService);
7291

7392
public readonly features = settingSignal<string[]>('features', []);
7493

75-
public readonly autoAssignDesk = () => {};
94+
// Expose data
95+
public readonly select_mode = this._service.select_mode;
96+
public readonly selected_count = this._service.selected_count;
97+
98+
public toggleSelectMode() {
99+
if (this._service.select_mode()) {
100+
this._service.clearSelection();
101+
} else {
102+
this._service.toggleSelectMode();
103+
}
104+
}
105+
106+
public bookForGroup() {
107+
let members: User[];
108+
109+
if (this._service.select_mode() && this._service.selected_count() > 0) {
110+
// Use selected members
111+
members = this._service
112+
.selected_members()
113+
.map((m) => m.user as User);
114+
} else {
115+
// Use team members
116+
members = this._service.getTeamMembers().map((m) => m.user as User);
117+
}
118+
119+
// Add current user if not already included
120+
const current = currentUser();
121+
if (!members.find((m) => m.email === current.email)) {
122+
members = [current, ...members];
123+
}
124+
125+
// Clear selection mode
126+
this._service.clearSelection();
127+
128+
// Set up group booking
129+
this._booking_form.setOptions({
130+
type: 'desk',
131+
group: true,
132+
members,
133+
});
134+
135+
// Navigate to desk booking flow
136+
this._router.navigate(['/book', 'desks']);
137+
}
76138
}

0 commit comments

Comments
 (0)