Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 4 additions & 7 deletions apps/concierge/src/app/desks/desks-manage.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,13 +388,10 @@ export class DesksManageComponent extends AsyncHandler {
try {
const list = csvToJson(data) || [];
this._state.addDesks(
list.map(
(_) =>
new Desk({
..._,
id: _.id || `desk-${randomInt(999_999)}`,
}),
),
list.map((_) => ({
..._,
id: _.id || `desk-${randomInt(999_999)}`,
})),
);
} catch (e) {
console.error(e);
Expand Down
32 changes: 18 additions & 14 deletions apps/concierge/src/app/desks/desks-state.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import {
deleteDeskAsset,
deskFromAsset,
findDeskByLegacyId,
queryDeskAssets,
queryDeskAssetsForZones,
queryDeskAssetsInZone,
Expand Down Expand Up @@ -509,14 +510,18 @@ export class DesksStateService extends AsyncHandler {
downloadFile('desks.csv', jsonToCsv(rows));
}

public async addDesks(list: Desk[]) {
/**
* Save new or imported desks. Pass imported CSV rows as plain objects, not
* `Desk` instances, so only the row's own columns update an existing desk.
*/
public async addDesks(list: Partial<Desk>[]) {
const selected_zones = this._getSelectedZones();
const all_zones = this._filters().zones?.includes('All');
const fallback_zone =
!all_zones && selected_zones.length ? selected_zones[0] : '';
// Group the desks by their level so each level's metadata is only
// written with its own desks.
const groups = new Map<string, Desk[]>();
const groups = new Map<string, Partial<Desk>[]>();
for (const desk of list) {
// imported rows carry the level as `zone_id`
const zone = desk.zone?.id || desk['zone_id'] || fallback_zone;
Expand Down Expand Up @@ -544,7 +549,8 @@ export class DesksStateService extends AsyncHandler {
continue;
}
const desk_list = await this._storedDeskList(zone);
for (const desk of desks) {
for (const row of desks) {
const desk = row instanceof Desk ? row : new Desk(row);
const idx = desk_list.findIndex((_) => _.id === desk.id);
if (idx >= 0) desk_list[idx] = desk;
else desk_list.push(desk);
Expand All @@ -559,20 +565,18 @@ export class DesksStateService extends AsyncHandler {
}

/**
* Desks without an asset type (imported rows, metadata era exports) update
* the desk asset with the same id or map id rather than creating a duplicate.
* Migrated desk assets keep the original desk id as their map id.
* Imported rows update the desk asset with the same id or map id rather
* than creating a duplicate. Migrated desk assets keep the original desk
* id as their map id. Columns missing from the row keep the asset's values.
*/
private _matchDeskAsset(desk: Desk, existing: Desk[]): Desk {
if (desk['asset_type_id']) return desk;
const map_id = desk.map_id || desk.id;
const match = existing.find(
(asset) => asset.id === desk.id || asset.map_id === map_id,
);
if (!match) return desk;
private _matchDeskAsset(row: Partial<Desk>, existing: Desk[]): Desk {
const match =
findDeskByLegacyId(existing, row.id) ||
findDeskByLegacyId(existing, row.map_id);
if (!match) return new Desk(row);
return new Desk({
...match,
...desk,
...row,
id: match.id,
map_id: match.map_id,
asset_type_id: match['asset_type_id'],
Expand Down
11 changes: 4 additions & 7 deletions apps/concierge/src/app/desks/desks-topbar.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,13 +298,10 @@ export class DesksTopbarComponent extends AsyncHandler implements OnInit {
try {
const list = csvToJson(data) || [];
this._desks.addDesks(
list.map(
(_) =>
new Desk({
..._,
id: _.id || `desk-${randomInt(999_999)}`,
}),
),
list.map((_) => ({
..._,
id: _.id || `desk-${randomInt(999_999)}`,
})),
);
} catch (e) {
console.error(e);
Expand Down
12 changes: 4 additions & 8 deletions apps/concierge/src/app/desks/desks.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
AsyncHandler,
BuildingLevel,
csvToJson,
Desk,
downloadFile,
jsonToCsv,
loadTextFileFromInputEvent,
Expand Down Expand Up @@ -533,13 +532,10 @@ export class DesksComponent extends AsyncHandler implements OnInit, OnDestroy {
try {
const list = csvToJson(data) || [];
this._state.addDesks(
list.map(
(_) =>
new Desk({
..._,
id: _.id || `desk-${randomInt(999_999)}`,
}),
),
list.map((_) => ({
..._,
id: _.id || `desk-${randomInt(999_999)}`,
})),
);
} catch (e) {
console.error(e);
Expand Down
13 changes: 7 additions & 6 deletions apps/concierge/src/tests/desks/desks-state.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ describe('DesksStateService', () => {
zone_id: 'level-2',
identifier: 'Old Name',
map_id: 'desk-6.06.1',
bookable: true,
images: ['desk.png'],
other_data: { floorsense_id: 'fs-1' },
},
]
Expand All @@ -300,12 +302,8 @@ describe('DesksStateService', () => {

// a metadata era CSV row, identified by the original desk id
await spectator.service.addDesks([
new Desk({
id: 'desk-6.06.1',
name: 'New Name',
zone_id: 'level-2',
} as any),
new Desk({ id: 'desk-new', name: 'Brand New' }),
{ id: 'desk-6.06.1', name: 'New Name', zone_id: 'level-2' } as any,
{ id: 'desk-new', name: 'Brand New' },
]);

expect(ts_client_mod.updateAsset).toHaveBeenCalledTimes(1);
Expand All @@ -315,6 +313,9 @@ describe('DesksStateService', () => {
zone_id: 'level-2',
identifier: 'New Name',
map_id: 'desk-6.06.1',
// columns missing from the row keep the asset's values
bookable: true,
images: ['desk.png'],
other_data: expect.objectContaining({ floorsense_id: 'fs-1' }),
}),
);
Expand Down
22 changes: 11 additions & 11 deletions apps/workplace/src/app/book/code-flow.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import {
viewChild,
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { queryDeskAssetsInZone } from '@placeos/assets';
import {
deskFromAsset,
findDeskByLegacyId,
queryDeskAssetsInZone,
} from '@placeos/assets';
import {
BookingFormService,
queryBookings,
Expand Down Expand Up @@ -345,19 +349,15 @@ export class BookCodeFlowComponent
/**
* Codes printed before desks moved to assets hold the original desk id,
* which desk assets keep as their map id. Bookings reference the asset id.
* The booked desk can be in any building, so search the whole org.
*/
private async _deskAssetId(code: string): Promise<string> {
if (!this._settings.get('app.desks.use_assets')) return code;
const building_id = this._org.building?.id;
if (!building_id) return code;
const assets = await queryDeskAssetsInZone(building_id).catch(() => []);
const asset = assets.find(
(item) =>
item.id === code ||
item.map_id === code ||
item.other_data?.['map_id'] === code,
);
return asset?.id || code;
const org_id = this._org.organisation?.id;
if (!org_id) return code;
const assets = await queryDeskAssetsInZone(org_id).catch(() => []);
const desks = assets.map((asset) => deskFromAsset(asset));
return findDeskByLegacyId(desks, code)?.id || code;
}

private async _checkinEvent(space_id: string, email?: string) {
Expand Down
50 changes: 22 additions & 28 deletions apps/workplace/src/app/book/desk-flow-new/desk-flow.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import {
} from '@angular/core';
import { MatRippleModule } from '@angular/material/core';
import { ActivatedRoute, Router } from '@angular/router';
import { deskFromAsset, queryDeskAssetsInZone } from '@placeos/assets';
import {
deskFromAsset,
findDeskByLegacyId,
queryDeskAssetsInZone,
} from '@placeos/assets';
import {
BookingAsset,
BookingFlowView,
Expand Down Expand Up @@ -42,14 +46,6 @@ import {
import { DeskFlowSelectComponent } from './desk-flow-select.component';
import { NewDeskFlowSuccessComponent } from './desk-flow-success.component';

/** Desk links use the desk asset id, or the original desk id kept as the map id */
function deskMatchesLink(
desk: { id: string; map_id?: string },
asset_id: string,
) {
return desk.id === asset_id || desk.map_id === asset_id;
}

@Component({
selector: 'desk-flow-new',
template: `
Expand Down Expand Up @@ -335,9 +331,7 @@ export class DeskFlowNewComponent extends AsyncHandler implements OnInit {
const deadline = Date.now() + wait_ms;
do {
const resources = await this._booking_form.listResources();
const resource = resources.find((item) =>
deskMatchesLink(item, asset_id),
);
const resource = findDeskByLegacyId(resources, asset_id);
if (resource) return resource;
if (Date.now() >= deadline) break;
await new Promise((resolve) => setTimeout(resolve, 50));
Expand Down Expand Up @@ -373,25 +367,25 @@ export class DeskFlowNewComponent extends AsyncHandler implements OnInit {
return null;
}

/**
* Desk assets list the org in their `zones`, so one query covers every
* building. The desk's building may be in a region that is not loaded yet.
*/
private async _findDeskAssetFromBuildings(
asset_id: string,
): Promise<BookingAsset | null> {
for (const building of this._org.buildings || []) {
const levels = this._org.levelsForBuilding(building);
const assets = await queryDeskAssetsInZone(building.id).catch(
() => [],
);
const desk = assets
.map((asset) =>
deskFromAsset(
asset,
levels.find((level) => level.id === asset.zone_id),
),
)
.find((item) => deskMatchesLink(item, asset_id));
if (desk) return desk as unknown as BookingAsset;
}
return null;
const assets = await queryDeskAssetsInZone(
this._org.organisation?.id,
).catch(() => []);
const desks = assets.map((asset) => deskFromAsset(asset));
const asset =
assets[desks.indexOf(findDeskByLegacyId(desks, asset_id))];
if (!asset) return null;
await this._org.loadBuildingsForZones([asset.zones || []]);
const level = (this._org.levels || []).find(
({ id }) => id === asset.zone_id,
);
return deskFromAsset(asset, level) as unknown as BookingAsset;
}

private async _initNearbyDeskBooking(space_id: string, event_date: number) {
Expand Down
8 changes: 4 additions & 4 deletions apps/workplace/src/tests/book/code-flow.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ describe('BookCodeFlowComponent', () => {
);
vi.spyOn(
spectator.inject(OrganisationService),
'building',
'organisation',
'get',
).mockReturnValue({ id: 'bld-1' } as any);
).mockReturnValue({ id: 'org-1' } as any);
const page = (data: any[]) =>
Promise.resolve({ data, total: data.length, next: () => null });
vi.mocked(ts_client.queryAssetCategories).mockReturnValue(
Expand All @@ -79,11 +79,11 @@ describe('BookCodeFlowComponent', () => {
vi.mocked(ts_client.queryAssetTypes).mockReturnValue(
page([{ id: 'type-1', name: '_DESKS_' }]) as any,
);
// desk assets are found by building
// the booked desk can be in any building, so search the org
vi.mocked(ts_client.queryAssets).mockImplementation(
(query: any) =>
page(
query.zones === 'bld-1'
query.zones === 'org-1'
? [
{
id: 'asset-1',
Expand Down
17 changes: 16 additions & 1 deletion apps/workplace/src/tests/book/desk-flow-new.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,15 @@ describe('DeskFlowNewComponent', () => {
},
]),
levelsForRegion: vi.fn(() => []),
loadBuildingsForZones: vi.fn(async () => []),
organisation: { id: 'org-1' },
levels: [
{
id: 'level-2',
parent_id: 'building-2',
tags: [],
},
],
find: vi.fn((id: string) =>
['building-1', 'building-2'].includes(id)
? { id }
Expand Down Expand Up @@ -324,12 +333,14 @@ describe('DeskFlowNewComponent', () => {
vi.mocked(queryAssets).mockImplementation(
(query: any) =>
page(
query.zones === 'building-2'
// desk assets list the org in their zones
query.zones === 'org-1'
? [
{
id: 'asset-456',
asset_type_id: 'type-1',
zone_id: 'level-2',
zones: ['org-1', 'building-2', 'level-2'],
identifier: 'Desk 456',
map_id: 'desk-456',
},
Expand All @@ -351,6 +362,10 @@ describe('DeskFlowNewComponent', () => {
expect(model().resources).toEqual([
expect.objectContaining({ id: 'asset-456', name: 'Desk 456' }),
]);
// the desk's building may be in a region that is not loaded yet
expect(
spectator.inject(OrganisationService).loadBuildingsForZones,
).toHaveBeenCalledWith([['org-1', 'building-2', 'level-2']]);
});

it('should select a nearby desk by resource id when no map id is available', async () => {
Expand Down
15 changes: 12 additions & 3 deletions libs/assets/src/lib/assets.fn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,24 @@ export function saveAssetCategory(category: Partial<PlaceAssetCategory>) {
const _GROUPS_CACHE = new Map<string, AssetGroup[]>();
const REMOVE_QUERY_KEYS = ['period_start', 'period_end', 'type', 'rejected'];

/** Query assets, following pagination until every page is loaded */
export async function queryAllAssetPages(query: any = {}) {
/** Upper bound on pages loaded by `queryAllAssetPages` (100 × 500 assets) */
const MAX_ASSET_PAGES = 100;

/**
* Query assets, following pagination until every page is loaded.
* `zones` is a comma separated list, assets must be in every zone listed.
*/
export async function queryAllAssetPages(
query: Parameters<typeof queryAssetsAPI>[0] & { zones?: string } = {},
) {
let response = await queryAssetsAPI({
...query,
limit: query.limit || 500,
});
let total = response.total;
const data: PlaceAsset[] = [...response.data];
while (typeof response.next === 'function') {
for (let page = 1; page < MAX_ASSET_PAGES; page++) {
if (typeof response.next !== 'function') break;
const next = response.next();
if (!next) break;
response = await next;
Expand Down
Loading
Loading