Skip to content

Commit 37fcfb1

Browse files
committed
feat(concierge): update emergency contacts to use the asset endpoints
1 parent 9af15b9 commit 37fcfb1

File tree

9 files changed

+827
-232
lines changed

9 files changed

+827
-232
lines changed

apps/concierge/src/app/asset-manager/asset-manager-state.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,9 @@ export class AssetManagerStateService extends AsyncHandler {
269269
? item.category_id
270270
: '',
271271
}));
272-
const categories = unique(mapped_products.map((i) => i.category_id));
272+
const categories = unique(
273+
mapped_products.map((i) => i.category_id),
274+
);
273275
for (const group of categories) {
274276
map[group] = mapped_products.filter(
275277
(i) => i.category_id === group,

apps/concierge/src/app/staff/emergency-contact-modal.component.ts

Lines changed: 51 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,17 @@ import { MatFormFieldModule } from '@angular/material/form-field';
1616
import { MatInputModule } from '@angular/material/input';
1717
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
1818
import { MatSelectModule } from '@angular/material/select';
19-
import {
20-
i18n,
21-
nextValueFrom,
22-
notifyError,
23-
notifySuccess,
24-
OrganisationService,
25-
randomString,
26-
} from '@placeos/common';
19+
import { OrganisationService } from '@placeos/common';
2720
import {
2821
CustomTooltipComponent,
2922
IconComponent,
3023
TranslatePipe,
3124
} from '@placeos/components';
3225
import { UserSearchFieldComponent } from '@placeos/form-fields';
33-
import { showMetadata, updateMetadata } from '@placeos/ts-client';
34-
import { BehaviorSubject, combineLatest } from 'rxjs';
35-
import { filter, map, shareReplay, switchMap } from 'rxjs/operators';
36-
import { EmergencyContact } from './emergency-contacts.component';
26+
import {
27+
EmergencyContact,
28+
EmergencyContactsService,
29+
} from './emergency-contacts.service';
3730

3831
@Component({
3932
selector: 'emergency-contact-modal',
@@ -106,7 +99,29 @@ import { EmergencyContact } from './emergency-contacts.component';
10699
</div>
107100
</div>
108101
<div class="flex flex-col">
109-
<label for="email">{{
102+
<label for="zone">{{
103+
'RESOURCE.LEVEL' | translate
104+
}}</label>
105+
<mat-form-field appearance="outline">
106+
<mat-select
107+
formControlName="zone"
108+
[placeholder]="
109+
'COMMON.LEVEL_SELECT' | translate
110+
"
111+
>
112+
<mat-option value="">{{
113+
'COMMON.LEVEL_ANY' | translate
114+
}}</mat-option>
115+
@for (level of levels | async; track level.id) {
116+
<mat-option [value]="level.id">
117+
{{ level.display_name || level.name }}
118+
</mat-option>
119+
}
120+
</mat-select>
121+
</mat-form-field>
122+
</div>
123+
<div class="flex flex-col">
124+
<label for="roles">{{
110125
'APP.CONCIERGE.CONTACTS_ROLES' | translate
111126
}}</label>
112127
<div class="flex items-center space-x-4">
@@ -122,10 +137,7 @@ import { EmergencyContact } from './emergency-contacts.component';
122137
| translate
123138
"
124139
>
125-
@for (
126-
role of (data | async)?.roles || [];
127-
track $index
128-
) {
140+
@for (role of roles | async; track $index) {
129141
@if (role) {
130142
<mat-option [value]="role">
131143
{{ role }}
@@ -166,7 +178,7 @@ import { EmergencyContact } from './emergency-contacts.component';
166178
<footer
167179
class="flex items-center justify-end border-t border-base-200 px-4 py-2"
168180
>
169-
<button btn matRipple class="w-32" (click)="save()">
181+
<button btn matRipple class="w-48" (click)="save()">
170182
{{ 'COMMON.SAVE' | translate }}
171183
</button>
172184
</footer>
@@ -210,23 +222,16 @@ export class EmergencyContactModalComponent {
210222
private _dialog_ref =
211223
inject<MatDialogRef<EmergencyContactModalComponent>>(MatDialogRef);
212224
private _org = inject(OrganisationService);
213-
214-
private _changes = new BehaviorSubject(0);
225+
private _contacts_service = inject(EmergencyContactsService);
215226

216227
public loading = signal(false);
217228
public role_name: string;
218229
public readonly contact?: EmergencyContact = this._data;
219-
public readonly data = combineLatest([
220-
this._org.active_building,
221-
this._changes,
222-
]).pipe(
223-
filter(([bld]) => !!bld),
224-
switchMap(([bld]) => showMetadata(bld.id, 'emergency_contacts')),
225-
map(({ details }) => (details as any) || { roles: [], contacts: [] }),
226-
shareReplay(1),
227-
);
230+
public readonly roles = this._contacts_service.roles$;
228231
public readonly form = new FormGroup({
229-
id: new FormControl(this._data?.id || `contact-${randomString(8)}`),
232+
id: new FormControl(
233+
this._data?.id || this._contacts_service.generateContactId(),
234+
),
230235
name: new FormControl(this._data?.name || ''),
231236
email: new FormControl(this._data?.email || ''),
232237
phone: new FormControl(this._data?.phone || ''),
@@ -238,23 +243,12 @@ export class EmergencyContactModalComponent {
238243

239244
private readonly _tooltip = viewChild(CustomTooltipComponent);
240245

241-
public async addRole() {
246+
public async addRole(): Promise<void> {
242247
if (!this.role_name) return;
243248
this._tooltip().close();
244249
this.loading.set(true);
245250
this._dialog_ref.disableClose = true;
246-
const data: any = await nextValueFrom(this.data);
247-
await updateMetadata(this._org.building.id, {
248-
name: 'emergency_contacts',
249-
description: 'Emergency Contacts',
250-
details: {
251-
roles: [...(data.roles || []), this.role_name].filter(
252-
(_) => !!_,
253-
),
254-
contacts: data.contacts,
255-
},
256-
}).toPromise();
257-
this._changes.next(0);
251+
await this._contacts_service.addRole(this.role_name);
258252
this.form.patchValue({
259253
roles: [...(this.form.value.roles || []), this.role_name],
260254
});
@@ -263,40 +257,30 @@ export class EmergencyContactModalComponent {
263257
this._dialog_ref.disableClose = false;
264258
}
265259

266-
public setUser(user: any) {
260+
public setUser(user: any): void {
267261
this.form.patchValue({
268262
name: user?.name,
269263
email: user?.email,
270264
phone: user?.phone,
271265
});
272266
}
273267

274-
public async save() {
268+
public async save(): Promise<void> {
275269
this.loading.set(true);
276270
this._dialog_ref.disableClose = true;
277-
const data: any = await nextValueFrom(this.data);
278-
const contacts = data?.contacts || [];
279-
const new_contacts = [
280-
...contacts.filter((_) => _.id !== this.contact?.id),
281-
this.form.value,
282-
].sort((a, b) => a.name.localeCompare(b.name));
283-
await updateMetadata(this._org.building.id, {
284-
name: 'emergency_contacts',
285-
description: 'Emergency Contacts',
286-
details: { roles: data.roles || [], contacts: new_contacts },
287-
})
288-
.toPromise()
289-
.catch((e) => {
290-
this._dialog_ref.disableClose = false;
291-
this.loading.set(false);
292-
notifyError(
293-
i18n('APP.CONCIERGE.CONTACTS_SAVE_ERROR', { error: e }),
294-
);
295-
throw e;
296-
});
271+
const contact: EmergencyContact = {
272+
id: this.form.value.id,
273+
name: this.form.value.name,
274+
email: this.form.value.email,
275+
phone: this.form.value.phone,
276+
zone: this.form.value.zone,
277+
roles: this.form.value.roles || [],
278+
};
279+
const success = await this._contacts_service.saveContact(contact);
297280
this._dialog_ref.disableClose = false;
298-
notifySuccess(i18n('APP.CONCIERGE.CONTACTS_SAVE_SUCCESS'));
299281
this.loading.set(false);
300-
this._dialog_ref.close();
282+
if (success) {
283+
this._dialog_ref.close();
284+
}
301285
}
302286
}

apps/concierge/src/app/staff/emergency-contacts.component.ts

Lines changed: 46 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,32 @@
11
import { Clipboard } from '@angular/cdk/clipboard';
22
import { CommonModule } from '@angular/common';
3-
import { Component, inject } from '@angular/core';
3+
import { Component, inject, OnInit } from '@angular/core';
44
import { FormsModule } from '@angular/forms';
55
import { MatRippleModule } from '@angular/material/core';
66
import { MatDialog } from '@angular/material/dialog';
77
import { MatFormFieldModule } from '@angular/material/form-field';
88
import { MatInputModule } from '@angular/material/input';
99
import { MatSelectModule } from '@angular/material/select';
1010
import { MatTooltipModule } from '@angular/material/tooltip';
11-
import {
12-
nextValueFrom,
13-
notifySuccess,
14-
OrganisationService,
15-
} from '@placeos/common';
11+
import { notifySuccess, OrganisationService } from '@placeos/common';
1612
import {
1713
IconComponent,
1814
openConfirmModal,
1915
SimpleTableComponent,
2016
TranslatePipe,
2117
} from '@placeos/components';
22-
import { showMetadata, updateMetadata } from '@placeos/ts-client';
2318
import { BehaviorSubject, combineLatest } from 'rxjs';
24-
import { filter, map, shareReplay, switchMap } from 'rxjs/operators';
19+
import { map } from 'rxjs/operators';
2520
import { ApplicationSidebarComponent } from '../ui/app-sidebar.component';
2621
import { ApplicationTopbarComponent } from '../ui/app-topbar.component';
2722
import { EmergencyContactModalComponent } from './emergency-contact-modal.component';
23+
import {
24+
EmergencyContact,
25+
EmergencyContactsService,
26+
} from './emergency-contacts.service';
2827
import { RoleManagementModalComponent } from './role-management-modal.component';
2928

30-
export interface EmergencyContact {
31-
id: string;
32-
email: string;
33-
name: string;
34-
phone: string;
35-
roles: string[];
36-
zone: string;
37-
}
38-
39-
export interface EmergencyContactData {
40-
contacts: EmergencyContact[];
41-
roles: string[];
42-
}
29+
export { EmergencyContact } from './emergency-contacts.service';
4330

4431
@Component({
4532
selector: '[app-emergency-contacts]',
@@ -245,26 +232,17 @@ export interface EmergencyContactData {
245232
TranslatePipe,
246233
],
247234
})
248-
export class EmergencyContactsComponent {
235+
export class EmergencyContactsComponent implements OnInit {
249236
private _org = inject(OrganisationService);
250237
private _dialog = inject(MatDialog);
251238
private _clipboard = inject(Clipboard);
252-
253-
private _change = new BehaviorSubject<number>(0);
239+
private _contacts_service = inject(EmergencyContactsService);
254240

255241
public search = '';
256242
public readonly role_filter = new BehaviorSubject<string>('');
257-
public readonly data = combineLatest([
258-
this._org.active_building,
259-
this._change,
260-
]).pipe(
261-
filter(([bld]) => !!bld),
262-
switchMap(([bld]) => showMetadata(bld.id, 'emergency_contacts')),
263-
map(({ details }) => (details as any) || { roles: [], contacts: [] }),
264-
shareReplay(1),
265-
);
266-
public readonly roles = this.data.pipe(map((_) => _?.roles || []));
267-
public readonly contacts = this.data.pipe(map((_) => _?.contacts || []));
243+
public readonly data$ = this._contacts_service.data$;
244+
public readonly roles = this._contacts_service.roles$;
245+
public readonly contacts = this._contacts_service.contacts$;
268246
public readonly filtered_contacts = combineLatest([
269247
this.contacts,
270248
this.role_filter,
@@ -274,24 +252,51 @@ export class EmergencyContactsComponent {
274252
),
275253
);
276254

255+
public ngOnInit(): void {
256+
// Check if migration from metadata is needed
257+
this.checkMigration();
258+
}
259+
260+
private async checkMigration(): Promise<void> {
261+
const needs_migration = await this._contacts_service.needsMigration();
262+
if (needs_migration) {
263+
const result = await openConfirmModal(
264+
{
265+
title: 'Migrate Emergency Contacts',
266+
content:
267+
'Emergency contacts data from the old system was found. Would you like to migrate it to the new system?',
268+
icon: { content: 'sync' },
269+
},
270+
this._dialog,
271+
);
272+
if (result.reason === 'done') {
273+
result.loading('Migrating contacts...');
274+
await this._contacts_service.migrateFromMetadata();
275+
result.close();
276+
} else {
277+
result.close();
278+
}
279+
}
280+
}
281+
277282
public readonly copyToClipboard = (id: string) => {
278283
const success = this._clipboard.copy(id);
279284
if (success) notifySuccess("User's email copied to clipboard.");
280285
};
281286

282-
public manageRoles() {
287+
public manageRoles(): void {
283288
const ref = this._dialog.open(RoleManagementModalComponent, {});
284-
ref.afterClosed().subscribe(() => this._change.next(Date.now()));
289+
ref.afterClosed().subscribe(() => this._contacts_service.refresh());
285290
}
286291

287-
public editContact(contact?: EmergencyContact) {
292+
public editContact(contact?: EmergencyContact): void {
288293
const ref = this._dialog.open(EmergencyContactModalComponent, {
289294
data: contact,
290295
});
291-
ref.afterClosed().subscribe(() => this._change.next(Date.now()));
296+
ref.afterClosed().subscribe(() => this._contacts_service.refresh());
292297
}
293298

294-
public async removeContact(contact: EmergencyContact) {
299+
public async removeContact(contact: EmergencyContact): Promise<void> {
295300
const result = await openConfirmModal(
296301
{
297302
title: 'Remove Emergency Contact',
@@ -302,17 +307,7 @@ export class EmergencyContactsComponent {
302307
);
303308
if (result.reason !== 'done') return;
304309
result.loading('Removing contact...');
305-
const data: any = await nextValueFrom(this.data);
306-
const new_contacts = (data?.contacts || []).filter(
307-
(_) => _.id !== contact.id,
308-
);
309-
await updateMetadata(this._org.building.id, {
310-
name: 'emergency_contacts',
311-
description: 'Emergency Contacts',
312-
details: { roles: data.roles, contacts: new_contacts },
313-
}).toPromise();
310+
await this._contacts_service.deleteContact(contact.id);
314311
result.close();
315-
this._change.next(Date.now());
316-
notifySuccess('Successfully removed emergency contact.');
317312
}
318313
}

0 commit comments

Comments
 (0)