Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
ca7abb4
fix(bookings): combine booked with clashes for availability
MrYuion Jun 24, 2026
663571c
fix(workplace): tweak level selection when on map view
MrYuion Jun 24, 2026
2d7d7b4
fix(workplace): fix z-indexing of layout items on mobile
MrYuion Jun 24, 2026
2d38e6a
chore(workplace): tweak mobile styles for parking request form.
MrYuion Jun 25, 2026
ca32cf7
fix(concierge): fix bookings modals not have the model passed to the …
MrYuion Jun 25, 2026
3dd0bc8
Merge branch 'develop' into custom/HIO687AS
MrYuion Jun 25, 2026
90736ee
chore(workplace): tweak styles for parking request form on mobile
MrYuion Jun 25, 2026
8765e1f
fix(concierge): allow searching parking bookings by plate number
MrYuion Jun 25, 2026
63659cc
fix(concierge): add default sort by bay number on parking bookings
MrYuion Jun 25, 2026
7a64dc6
fix(concierge): add ability to sort parking bookings by plate number
MrYuion Jun 25, 2026
f622b79
feat(signage-manager): add list and folder views to media list
MrYuion Jun 25, 2026
4f8992a
feat(signage-manager): add ability to add new playlists from media view
MrYuion Jun 25, 2026
badf3d6
fix(signage-manager): fix display status on schedule view
MrYuion Jun 25, 2026
dc6f00d
chore(concierge): fix tests
MrYuion Jun 25, 2026
7ae6771
fix(signage-mananger): add loading states for media items and playlis…
MrYuion Jun 25, 2026
f1aeab7
feat(signage-manager): paginate media, playlist and display requests
MrYuion Jun 25, 2026
cb552f2
feat(signage-manager): update media folders to use tags endpoint
MrYuion Jun 25, 2026
e9cf1d7
chore(signage-manager): tweaks
MrYuion Jun 25, 2026
58cb816
feat(signage-manager): improve media topbar flow on mobile
MrYuion Jun 26, 2026
f01e83b
chore(signage-manager): move tests to separate folder
MrYuion Jun 26, 2026
a8f3732
fix(signage-manager): fix race condition for authorised check
MrYuion Jun 26, 2026
99d42e5
fix(events): enforce room booking rules in booking panel
MrYuion Jun 26, 2026
2d33140
fix(workplace): prevent deleting early-ended bookings from home page
MrYuion Jun 26, 2026
30df8c3
fix(bookings): tweak readiness gate for form state
MrYuion Jun 27, 2026
4672498
fix(signage): fix takeover playlists not triggering after 30 second s…
MrYuion Jun 27, 2026
78324d4
fix(signage-manager): add line through disabled playlists on schedule…
MrYuion Jun 27, 2026
4052f92
feat(concierge): add user groups column to parking bookings
MrYuion Jun 29, 2026
ec141a1
fix(workplace): fix initialising event form state on meeting form
MrYuion Jun 29, 2026
3bea669
Merge branch 'develop' into custom/HIO687AS
MrYuion Jun 29, 2026
cb3d0f3
feat(mobile): add MS Intune support to mobile apps
MrYuion Jun 29, 2026
5858f06
refactor(concierge): clean up styles for day view
MrYuion Jun 29, 2026
c584e62
refactor(concierge): improve table overflow for parking
MrYuion Jun 29, 2026
35716fc
fix(concierge): fix locker infinite loop
MrYuion Jun 29, 2026
8658bbc
chore(concierge): increase table min-size
MrYuion Jun 29, 2026
f50b9e1
fix(concierge): fix sorting of parking bay numbers
MrYuion Jun 29, 2026
6004fbc
chore: tweak sort indicators on tables
MrYuion Jun 29, 2026
4f8f6b6
chore: tweak sort indicators on tables
MrYuion Jun 29, 2026
1064042
refactor(concierge): update parking report graphs and metrics to be m…
MrYuion Jun 29, 2026
ad1c207
chore(concierge): reorder parking report tables
MrYuion Jun 29, 2026
52fbfad
fix(workplace): tweak desk select mobile filter overlay
MrYuion Jun 29, 2026
f0914f5
feat(workplace): add ability to change room capacity option on select…
MrYuion Jun 29, 2026
fdc0fae
chore(workplace): minor style tweaks to parking request form
MrYuion Jun 29, 2026
48a7ac3
fix(concierge): tweak handling of bay number displays
MrYuion Jun 29, 2026
5bc2148
feat(concierge): add checked in time column to parking report bookings
MrYuion Jun 29, 2026
5336fe8
feat(workplace): add any capacity option to meeting flow form
MrYuion Jun 29, 2026
b0bbf8c
chore(booking-panel): minor tweaks to checkin flow
MrYuion Jun 30, 2026
00d0831
fix(booking): add guard for form cross-contamination
MrYuion Jun 30, 2026
08114c1
Merge branch 'develop' into custom/HIO687AS
MrYuion Jun 30, 2026
576c63e
Merge branch 'custom/HIO687AS-prod' into custom/HIO687AS
w-le Jun 30, 2026
620a75b
fix(bookings): restrict booking extension data
MrYuion Jun 30, 2026
442f0c2
fix(bookings): restrict booking extension data
MrYuion Jun 30, 2026
7b921a4
fix(visitor-kiosk): force host to null on registration form
MrYuion Jun 30, 2026
c1e68ad
chore: minor fix to setting up mocks
MrYuion Jun 30, 2026
517dbea
Merge branch 'develop' into custom/HIO687AS
MrYuion Jun 30, 2026
e600b59
fix(bookings): generalize reserved-resource booking restriction
MrYuion Jul 1, 2026
05855e4
fix(bookings): generalize reserved-resource booking restriction
MrYuion Jul 1, 2026
3be1ca0
feat(concierge): split parking user and vehicle visibility
MrYuion Jul 1, 2026
0c52006
fix(visitor-kiosk): prevent next button "submitting" the details form
MrYuion Jul 1, 2026
e42073d
fix(visitor-kiosk): keep registration host blank
MrYuion Jul 1, 2026
ce33820
fix(concierge): update parking report columns
MrYuion Jul 1, 2026
35d5f1c
Merge branch 'develop' into custom/HIO687AS
MrYuion Jul 1, 2026
7e6d4fa
feat(workplace): add bookings for others schedule filter
MrYuion Jul 1, 2026
ac6f75a
fix(form-fields): hide empty user in search field
MrYuion Jul 1, 2026
3ccc37e
Merge branch 'develop' into custom/HIO687AS
MrYuion Jul 1, 2026
e0d240d
fix(events): keep submitted meeting success details
MrYuion Jul 1, 2026
b0cb0b6
fix(events): keep submitted meeting success details
MrYuion Jul 1, 2026
920100b
fix(events): preserve selected meeting booking time
MrYuion Jul 1, 2026
6486e73
fix(events): preserve selected meeting booking time
MrYuion Jul 1, 2026
9d0b56c
fix(forms): wait for user before form initialisation
MrYuion Jul 1, 2026
d1dade8
Merge branch 'develop' into custom/HIO687AS
MrYuion Jul 1, 2026
749023f
Merge branch 'custom/HIO687AS-prod' into custom/HIO687AS
w-le Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,9 @@ export class ParkingSpecialRequestModalComponent {
const urls: string[] = (
this._data.booking?.extension_data?.attachments || []
).filter((url) => !!url);
const names: string[] =
this._data.booking?.extension_data?.p2_document_names || [];
return urls.map((url, index) => ({
return urls.map((url) => ({
url,
name: names[index] || this._fileNameFromUrl(url),
name: this._fileNameFromUrl(url),
}));
})();

Expand Down
22 changes: 19 additions & 3 deletions apps/concierge/src/app/parking/parking.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { ParkingTopbarComponent } from './parking-topbar.component';
| translate
}}
</a>
@if (!hide_users_and_vehicles) {
@if (!hide_users) {
<a
mat-tab-link
[routerLink]="[
Expand All @@ -54,6 +54,8 @@ import { ParkingTopbarComponent } from './parking-topbar.component';
| translate
}}
</a>
}
@if (!hide_vehicles) {
<a
mat-tab-link
[routerLink]="[
Expand Down Expand Up @@ -163,6 +165,20 @@ export class ParkingComponent extends AsyncHandler implements OnInit {
return !!this._settings.get('app.parking.hide_users_and_vehicles');
}

public get hide_users() {
return (
this.hide_users_and_vehicles ||
!!this._settings.get('app.parking.hide_users')
);
}

public get hide_vehicles() {
return (
this.hide_users_and_vehicles ||
!!this._settings.get('app.parking.hide_vehicles')
);
}

public get is_admin() {
const groups = currentUser().groups || [];
const admin_group = this._settings.get('app.admin_group') || 'admin';
Expand Down Expand Up @@ -203,8 +219,8 @@ export class ParkingComponent extends AsyncHandler implements OnInit {
this.section.set(section as any);
if (
section === 'manage' &&
this.hide_users_and_vehicles &&
['fleet', 'users'].includes(current_view)
((current_view === 'users' && this.hide_users) ||
(current_view === 'fleet' && this.hide_vehicles))
) {
this.view.set('spaces');
void this._router.navigate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { Component, computed, inject, input } from '@angular/core';
import { MatRippleModule } from '@angular/material/core';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ParkingSpacePipe } from '@placeos/assets';
import { downloadFile, i18n, jsonToCsv } from '@placeos/common';
import {
IconComponent,
Expand All @@ -19,9 +20,9 @@ import { ParkingReportService } from './parking-report.service';

const TABLE_METRIC_GUIDE: ReportMetricGuideItem[] = [
{
label: 'Parking space',
label: 'Bay number',
description:
'Uses asset name, extension asset name, description, then asset ID as fallback.',
'Resolves the booking asset ID to the parking space identifier.',
},
{
label: 'Duration',
Expand Down Expand Up @@ -55,11 +56,7 @@ const TABLE_METRIC_GUIDE: ReportMetricGuideItem[] = [
class="border-base-200 bg-base-100 m-4 overflow-hidden rounded-sm border"
>
<div class="border-base-200 flex items-center border-b px-4 py-2">
<h3 class="flex-1 text-xl font-bold">
{{
'APP.CONCIERGE.REPORTS_PARKING_UTIL_HEADER' | translate
}}
</h3>
<h3 class="flex-1 text-xl font-bold">Parking Bookings</h3>
@if (!print()) {
<button
icon
Expand All @@ -84,8 +81,9 @@ const TABLE_METRIC_GUIDE: ReportMetricGuideItem[] = [
[data]="parking_bookings()"
[columns]="[
{
key: 'parking_name',
name: 'RESOURCE.PARKING_SPACE' | translate,
key: 'asset_id',
name: 'APP.CONCIERGE.PARKING_BAY_NUMBER' | translate,
content: bay_template,
},
{
key: 'date',
Expand All @@ -101,6 +99,10 @@ const TABLE_METRIC_GUIDE: ReportMetricGuideItem[] = [
key: 'host',
name: 'APP.CONCIERGE.PARKING_RESERVED_FOR' | translate,
},
{
key: 'plate_number',
name: 'EXPLORE.PARKING_PLATE_NUMBER' | translate,
},
{
key: 'checked_in',
name: 'COMMON.CHECKED_IN' | translate,
Expand All @@ -121,6 +123,17 @@ const TABLE_METRIC_GUIDE: ReportMetricGuideItem[] = [
'APP.CONCIERGE.REPORTS_DAILY_EMPTY' | translate
"
></simple-table>
<ng-template #bay_template let-id="data">
<div class="p-4">
@if (id) {
{{ (id | parkingSpace | async)?.identifier || id }}
} @else {
<span class="opacity-30">
{{ 'COMMON.EMPTY' | translate }}
</span>
}
</div>
</ng-template>
<ng-template #date_template let-row="row">
<div class="p-4">
{{ row.date | date: 'mediumDate' }}
Expand Down Expand Up @@ -154,6 +167,7 @@ const TABLE_METRIC_GUIDE: ReportMetricGuideItem[] = [
SimpleTableComponent,
DurationPipe,
TranslatePipe,
ParkingSpacePipe,
IconComponent,
MatRippleModule,
MatTooltipModule,
Expand All @@ -171,15 +185,12 @@ export class ParkingReportListComponent {
const list = [];
for (const booking of this._bookings()) {
list.push({
parking_name:
booking.asset_name ||
booking.extension_data?.asset_name ||
booking.description ||
booking.asset_id,
asset_id: booking.asset_id,
date: booking.date,
duration: booking.duration,
all_day: booking.all_day,
host: booking.user_name || booking.user_email,
plate_number: booking.extension_data?.plate_number || '',
checked_in: i18n(
booking.checked_in ? 'COMMON.TRUE' : 'COMMON.FALSE',
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1554,7 +1554,15 @@ import { UploadButtonComponent } from './upload-button.component';
"
></settings-toggle>
<settings-toggle
label="Hide users and vehicle tabs"
label="Hide user tab"
[formField]="form.parking.hide_users"
></settings-toggle>
<settings-toggle
label="Hide vehicle tab"
[formField]="form.parking.hide_vehicles"
></settings-toggle>
<settings-toggle
label="Hide user and vehicle tabs"
[formField]="
form.parking.hide_users_and_vehicles
"
Expand Down Expand Up @@ -1927,6 +1935,8 @@ export class ConciergeSettingsFormModalComponent implements OnInit {
show_waitlist: true,
hide_bay_number: false,
hide_level_selector_on_booking_list: false,
hide_users: false,
hide_vehicles: false,
hide_users_and_vehicles: false,
hide_assign_space: false,
allow_deleting: false,
Expand Down
2 changes: 2 additions & 0 deletions apps/concierge/src/environments/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ const app = {
disable_styles: false,
hide_bay_number: false,
hide_level_selector_on_booking_list: false,
hide_users: false,
hide_vehicles: false,
hide_users_and_vehicles: false,
hide_availability_counter: false,
hide_assign_space: false,
Expand Down
40 changes: 39 additions & 1 deletion apps/concierge/src/tests/parking/parking.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ describe('ParkingComponent', () => {
beforeEach(() => {
settings_map = {
'app.parking.show_requests': true,
'app.parking.hide_users': false,
'app.parking.hide_vehicles': false,
'app.parking.hide_users_and_vehicles': false,
'app.feature_groups': { 'parking-requests': ['parking-team'] },
'app.admin_group': 'admin',
Expand Down Expand Up @@ -99,7 +101,43 @@ describe('ParkingComponent', () => {
);
});

it('should redirect hidden user and vehicle management tabs to spaces', () => {
it('should redirect hidden user management tab to spaces', () => {
const router = spectator.inject(Router);
jest.spyOn(router, 'navigate').mockResolvedValue(true);
settings_map['app.parking.hide_users'] = true;
Object.defineProperty(router, 'url', {
value: '/book/parking/manage/users',
configurable: true,
});

(spectator.component as any)._updatePath();

expect(spectator.component.view()).toBe('spaces');
expect(router.navigate).toHaveBeenCalledWith(
['/book', 'parking', 'manage', 'spaces'],
{ replaceUrl: true },
);
});

it('should redirect hidden vehicle management tab to spaces', () => {
const router = spectator.inject(Router);
jest.spyOn(router, 'navigate').mockResolvedValue(true);
settings_map['app.parking.hide_vehicles'] = true;
Object.defineProperty(router, 'url', {
value: '/book/parking/manage/fleet',
configurable: true,
});

(spectator.component as any)._updatePath();

expect(spectator.component.view()).toBe('spaces');
expect(router.navigate).toHaveBeenCalledWith(
['/book', 'parking', 'manage', 'spaces'],
{ replaceUrl: true },
);
});

it('should keep the combined user and vehicle tab setting working', () => {
const router = spectator.inject(Router);
jest.spyOn(router, 'navigate').mockResolvedValue(true);
settings_map['app.parking.hide_users_and_vehicles'] = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ import { CheckinStateService } from './checkin-state.service';
</mat-form-field>
</div>
}
<button next btn matRipple (click)="updateGuest()">
<button next type="button" btn matRipple (click)="updateGuest()">
{{ 'APP.VISITOR_KIOSK.CONTINUE' | translate }}
</button>
<a
Expand Down
15 changes: 14 additions & 1 deletion apps/visitor-kiosk/src/app/visitor-registration.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { startOfMinute } from 'date-fns';

import {
AsyncHandler,
EMPTY_USER,
getInvalidSignalFields,
i18n,
isEmptyUser,
Expand Down Expand Up @@ -304,7 +305,10 @@ export class VisitorRegistrationComponent
});

public setHost(user: User | null) {
this._booking_form.model.update((m) => ({ ...m, user }));
this._booking_form.model.update((m) => ({
...m,
user: user || EMPTY_USER,
}));
}

public setDuration(duration: number) {
Expand All @@ -324,6 +328,8 @@ export class VisitorRegistrationComponent
...m,
booking_type: 'visitor',
title: 'Visit',
// Always ask for the host; null is sanitized back to currentUser().
user: EMPTY_USER,
}));
setTimeout(() => {
if (this.allow_self_registration()) return;
Expand All @@ -343,6 +349,13 @@ export class VisitorRegistrationComponent
}),
);
}
if (!this.host()) {
return notifyError(
i18n('FORM.INVALID_FIELDS', {
field_list: i18n('APP.VISITOR_KIOSK.HOST'),
}),
);
}
this.loading.set(true);
try {
const value = this._booking_form.model();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest';
import { CheckinCovidComponent } from '../../app/checkin/checkin-covid.component';
import { CheckinStateService } from '../../app/checkin/checkin-state.service';

jest.mock('@placeos/common');
jest.mock('@placeos/common', () => ({
...jest.requireActual('@placeos/common'),
notifyError: jest.fn(),
}));

import { FormsModule } from '@angular/forms';
import * as common_mod from '@placeos/common';
Expand All @@ -26,14 +29,16 @@ describe('CheckinCovidComponent', () => {
imports: [MatRadioModule, FormsModule],
});

beforeEach(() => (spectator = createComponent()));
beforeEach(() => {
(common_mod.notifyError as jest.Mock).mockClear();
spectator = createComponent();
});

it('should create component', () => {
expect(spectator.component).toBeTruthy();
});

it('should allow confirming questions', () => {
(common_mod.notifyError as any) = jest.fn();
spectator.component.confirm();
expect(common_mod.notifyError).toHaveBeenCalledWith(
'Please select yes or no for each question',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Router } from '@angular/router';
import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest';
import { BookingFormService } from '@placeos/bookings';
import {
EMPTY_USER,
isEmptyUser,
OrganisationService,
Region,
SettingsService,
Expand Down Expand Up @@ -85,6 +87,14 @@ describe('VisitorRegistrationComponent', () => {
expect(spectator.component.host()).toEqual(host);
});

it('should keep cleared host blank in the backing model', () => {
spectator.component.setHost(null);

expect(spectator.component.form_value().user).toEqual(EMPTY_USER);
expect(isEmptyUser(spectator.component.form_value().user)).toBe(true);
expect(spectator.component.host()).toBeNull();
});

it('should keep the host field bindable when the user value is cleared', fakeAsync(() => {
const console_error = jest
.spyOn(console, 'error')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,17 @@ export class NewDeskFlowFormComponent implements OnInit {
public level = '';
public levels = [];

/** Block the form when the user has a reserved desk and isn't allowed to book another */
/**
* Blanket-block the form only when the user has a reserved desk and booking
* with a reserved desk is disallowed entirely. When it is allowed, a
* `prevent_self_booking_if_assigned_resource` restriction blocks only their
* own bookings (enforced at submit), so the form stays open for booking on
* behalf of others.
*/
public readonly show_reserved_desk_overlay = computed(
() =>
this._state.has_assigned_desk() &&
!this._state.canBookWithReservedDesk(),
!this._state.allowsBookingWithReservedResource('desk'),
);

public get form() {
Expand Down
Loading
Loading