database connection

This commit is contained in:
Evert Prants 2023-01-18 23:20:06 +02:00
parent 452bcd5c01
commit ccfe07a0b8
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
18 changed files with 309 additions and 142 deletions

View File

@ -23,7 +23,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ChevronDownIcon } from '@heroicons/vue/24/outline'; import { ChevronDownIcon } from '@heroicons/vue/24/outline';
import { onMounted, ref } from 'vue'; import { onBeforeUnmount, onMounted, ref } from 'vue';
import { onBeforeRouteLeave } from 'vue-router'; import { onBeforeRouteLeave } from 'vue-router';
const open = ref(false); const open = ref(false);
@ -37,15 +37,19 @@ const toggle = (to?: boolean) => {
open.value = to ?? !open.value; open.value = to ?? !open.value;
}; };
const event = (e: MouseEvent) => {
if (wrapper.value.contains(e.target as HTMLElement)) {
return;
}
open.value = false;
};
onMounted(() => { onMounted(() => {
const event = (e: MouseEvent) => {
if (wrapper.value.contains(e.target as HTMLElement)) {
return;
}
open.value = false;
};
window.addEventListener('click', event); window.addEventListener('click', event);
return () => window.removeEventListener('click', event); });
onBeforeUnmount(() => {
window.removeEventListener('click', event);
}); });
onBeforeRouteLeave(() => { onBeforeRouteLeave(() => {

View File

@ -38,14 +38,14 @@
<PlannerSidebars> <PlannerSidebars>
<PlannerLayerPanel <PlannerLayerPanel
:layers="serializedLayers" :layers="localFloorDocument.layers"
@layer-name="commitLayerName" @layer-name="commitLayerName"
@object-name="commitObjectName" @object-name="commitObjectName"
@select-object="clickedOnObject" @select-object="clickedOnObject"
@select-layer="clickedOnLayer" @select-layer="clickedOnLayer"
/> />
<PlannerPropertyPanel <PlannerPropertyPanel
:layers="serializedLayers" :layers="localFloorDocument.layers"
@update="updateObjectProperty" @update="updateObjectProperty"
/> />
</PlannerSidebars> </PlannerSidebars>
@ -62,16 +62,17 @@ import {
ScissorsIcon, ScissorsIcon,
XMarkIcon, XMarkIcon,
} from '@heroicons/vue/24/outline'; } from '@heroicons/vue/24/outline';
import { useSessionStorage } from '@vueuse/core'; import { useDebounceFn } from '@vueuse/shared';
import { onMounted, ref, shallowRef } from 'vue'; import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
import { HousePlanner } from '../../modules/house-planner'; import { HousePlanner } from '../../modules/house-planner';
import { import {
Layer,
RepositionEvent, RepositionEvent,
ToolEvent, ToolEvent,
Vec2, Vec2,
} from '../../modules/house-planner/interfaces'; } from '../../modules/house-planner/interfaces';
import type { HousePlannerCanvasTools } from '../../modules/house-planner/tools';
import deepUnref from '../../utils/deep-unref'; import deepUnref from '../../utils/deep-unref';
import { FloorDocument } from './interfaces/floor-document.interface';
import { ToolbarTool } from './interfaces/toolbar.interfaces'; import { ToolbarTool } from './interfaces/toolbar.interfaces';
import PlannerLayerPanel from './PlannerLayerPanel.vue'; import PlannerLayerPanel from './PlannerLayerPanel.vue';
import PlannerPropertyPanel from './PlannerPropertyPanel.vue'; import PlannerPropertyPanel from './PlannerPropertyPanel.vue';
@ -86,27 +87,20 @@ const subTool = ref<string | undefined>(undefined);
const canvasDim = ref<Vec2>([1920, 1080]); const canvasDim = ref<Vec2>([1920, 1080]);
const canvasPos = ref<Vec2>([0, 0]); const canvasPos = ref<Vec2>([0, 0]);
const canvasZoom = ref<number>(1); const canvasZoom = ref<number>(1);
const serializedLayers = useSessionStorage<Layer[]>(
'roomData', const props = defineProps<{
[ floorDocument: FloorDocument;
{ }>();
id: 1,
name: 'Rooms', const emit = defineEmits<{
color: '#00ddff', (e: 'update', document: FloorDocument): void;
contents: [], (e: 'edited'): void;
visible: true, }>();
active: false,
}, const localFloorDocument = ref(deepUnref(props.floorDocument));
{ const debouncedUpdate = useDebounceFn(
id: 0, () => emit('update', localFloorDocument.value),
name: 'Base', 5000
color: '#00ddff',
contents: [],
visible: true,
active: true,
},
],
{ writeDefaults: false }
); );
const toolbar: ToolbarTool[] = [ const toolbar: ToolbarTool[] = [
@ -148,7 +142,7 @@ const toolbar: ToolbarTool[] = [
subTool: 'cut', subTool: 'cut',
children: [ children: [
{ {
title: 'Cut Segment', title: 'Remove Segment',
icon: XMarkIcon, icon: XMarkIcon,
tool: 'cut', tool: 'cut',
subTool: 'remove-segment', subTool: 'remove-segment',
@ -161,7 +155,10 @@ const selectTool = (newTool?: string, newSubTool?: string) => {
if (newTool === tool.value && !newSubTool) { if (newTool === tool.value && !newSubTool) {
newTool = undefined; newTool = undefined;
} }
module.value.manager?.tools?.setTool(newTool, newSubTool); (module.value.manager?.tools as HousePlannerCanvasTools).setTool(
newTool,
newSubTool
);
}; };
const commitObjectName = (layerId: number, objectId: number, name: string) => { const commitObjectName = (layerId: number, objectId: number, name: string) => {
@ -175,13 +172,16 @@ const commitLayerName = (layerId: number, name: string) => {
const clickedOnObject = (layerId: number, objectId: number, add?: boolean) => { const clickedOnObject = (layerId: number, objectId: number, add?: boolean) => {
const object = module.value.manager?.getLayerObjectById(layerId, objectId); const object = module.value.manager?.getLayerObjectById(layerId, objectId);
if (!object) return; if (!object) return;
module.value.manager?.tools?.selectObject(object, add); (module.value.manager?.tools as HousePlannerCanvasTools).selectObject(
object,
add
);
}; };
const clickedOnLayer = (layerId: number) => { const clickedOnLayer = (layerId: number) => {
const layer = module.value.manager?.getLayerById(layerId); const layer = module.value.manager?.getLayerById(layerId);
if (!layer) return; if (!layer) return;
module.value.manager?.tools?.selectLayer(layer); (module.value.manager?.tools as HousePlannerCanvasTools).selectLayer(layer);
}; };
const updateObjectProperty = ( const updateObjectProperty = (
@ -195,41 +195,44 @@ const updateObjectProperty = (
}); });
}; };
const events: Record<string, (e: CustomEvent) => void> = {
'hpc:update': (e: CustomEvent) => {
localFloorDocument.value.layers = deepUnref(module.value.manager!.layers);
emit('edited');
debouncedUpdate();
},
'hpc:selectionchange': (e: CustomEvent) => {
localFloorDocument.value.layers = deepUnref(module.value.manager!.layers);
emit('edited');
debouncedUpdate();
},
'hpc:tool': (e: CustomEvent<ToolEvent>) => {
tool.value = e.detail.primary;
subTool.value = e.detail.secondary as string;
},
'hpc:position': (e: CustomEvent<RepositionEvent>) => {
canvasPos.value = e.detail.position;
canvasZoom.value = e.detail.zoom;
},
};
onMounted(() => { onMounted(() => {
const cleanUp = module.value.initialize( module.value.initialize(
canvas.value, canvas.value,
deepUnref(serializedLayers.value), deepUnref(localFloorDocument.value.layers),
[1920, 1080], [localFloorDocument.value.width, localFloorDocument.value.height],
canvasPos.value, canvasPos.value,
canvasZoom.value canvasZoom.value
); );
const events: Record<string, (e: CustomEvent) => void> = {
'hpc:update': (e: CustomEvent) => {
serializedLayers.value = deepUnref(module.value.manager!.layers);
},
'hpc:selectionchange': (e: CustomEvent) => {
serializedLayers.value = deepUnref(module.value.manager!.layers);
},
'hpc:tool': (e: CustomEvent<ToolEvent>) => {
tool.value = e.detail.primary;
subTool.value = e.detail.secondary as string;
},
'hpc:position': (e: CustomEvent<RepositionEvent>) => {
canvasPos.value = e.detail.position;
canvasZoom.value = e.detail.zoom;
},
};
Object.keys(events).forEach((event) => Object.keys(events).forEach((event) =>
canvas.value.addEventListener(event, events[event]) canvas.value.addEventListener(event, events[event])
); );
});
return () => { onBeforeUnmount(() => {
Object.keys(events).forEach((event) => Object.keys(events).forEach((event) =>
canvas.value.removeEventListener(event, events[event]) canvas.value.removeEventListener(event, events[event])
); );
cleanUp(); module.value?.cleanUp();
};
}); });
</script> </script>

View File

@ -50,7 +50,7 @@ const lineProps: ObjectProperty[] = [
{ key: 'color', title: 'Color', type: 'color', groupable: true }, { key: 'color', title: 'Color', type: 'color', groupable: true },
{ {
key: 'lineCap', key: 'lineCap',
title: 'Line Cap', title: 'Line Cap Style',
type: 'select', type: 'select',
groupable: true, groupable: true,
options: [ options: [
@ -62,7 +62,7 @@ const lineProps: ObjectProperty[] = [
}, },
{ {
key: 'lineJoin', key: 'lineJoin',
title: 'Line Join', title: 'Line Join Style',
type: 'select', type: 'select',
groupable: true, groupable: true,
options: [ options: [

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="absolute right-0 top-0 bottom-0 z-10 my-4 overflow-hidden"> <div class="z-8 absolute right-0 top-0 bottom-0 my-4 overflow-hidden">
<div class="flex h-full flex-col items-end space-y-2"> <div class="flex h-full flex-col items-end space-y-2">
<slot /> <slot />
</div> </div>

View File

@ -1,6 +1,6 @@
<template> <template>
<div <div
class="absolute bottom-0 z-10 w-screen max-w-md rounded-lg bg-white lg:left-1/2 lg:ml-0 lg:-translate-x-1/2" class="z-8 absolute bottom-0 w-screen max-w-md rounded-lg bg-white lg:left-1/2 lg:ml-0 lg:-translate-x-1/2"
> >
<div <div
class="flex flex-1 flex-row items-center justify-center space-x-2 py-4 px-4 shadow-lg ring-1 ring-black ring-opacity-5" class="flex flex-1 flex-row items-center justify-center space-x-2 py-4 px-4 shadow-lg ring-1 ring-black ring-opacity-5"

View File

@ -0,0 +1,26 @@
import { FloorDocument } from '../interfaces/floor-document.interface';
export const defaultRoomData: FloorDocument = {
id: 0,
width: 1920,
height: 1080,
name: 'Floor 0',
layers: [
{
id: 1,
name: 'Rooms',
color: '#00ddff',
contents: [],
visible: true,
active: false,
},
{
id: 0,
name: 'Base',
color: '#00ddff',
contents: [],
visible: true,
active: true,
},
],
};

View File

@ -0,0 +1,9 @@
import { Layer } from '../../../modules/house-planner/interfaces';
export interface FloorDocument {
id: number;
name: string;
width: number;
height: number;
layers: Layer[];
}

View File

@ -0,0 +1,11 @@
import { RoomListItem } from './room.interfaces';
export interface FloorListItem {
id: number;
displayName: string;
number: number;
plan: string;
createdAt: string;
updatedAt: string;
rooms: RoomListItem[];
}

View File

@ -0,0 +1,4 @@
export interface RoomListItem {
id: number;
displayName: string;
}

View File

@ -1,8 +1,8 @@
import { clamp } from '@vueuse/core'; import { clamp } from '@vueuse/core';
import type { Ref } from 'vue';
import { HousePlannerCanvasGrid } from './grid'; import { HousePlannerCanvasGrid } from './grid';
import { import {
BezierSegment, BezierSegment,
ICanvasToolkit,
Layer, Layer,
LayerObject, LayerObject,
Line, Line,
@ -12,14 +12,12 @@ import {
} from './interfaces'; } from './interfaces';
import { HousePlannerCanvasTools } from './tools'; import { HousePlannerCanvasTools } from './tools';
import { import {
rad2deg,
vec2Add, vec2Add,
vec2AngleFromOrigin, vec2AngleFromOrigin,
vec2Clamp,
vec2Distance, vec2Distance,
vec2DivideScalar, vec2DivideScalar,
vec2Equals,
vec2MultiplyScalar, vec2MultiplyScalar,
vec2PointFromAngle,
vec2Snap, vec2Snap,
vec2Sub, vec2Sub,
} from './utils'; } from './utils';
@ -27,7 +25,7 @@ import {
export class HousePlannerCanvas { export class HousePlannerCanvas {
public ctx!: CanvasRenderingContext2D; public ctx!: CanvasRenderingContext2D;
public layers: Layer[] = []; public layers: Layer[] = [];
public tools?; public tools?: ICanvasToolkit;
public grid = new HousePlannerCanvasGrid(this, 8); public grid = new HousePlannerCanvasGrid(this, 8);
public mousePosition: Vec2 = [0, 0]; public mousePosition: Vec2 = [0, 0];
public mouseClickPosition: Vec2 = [0, 0]; public mouseClickPosition: Vec2 = [0, 0];
@ -290,23 +288,23 @@ export class HousePlannerCanvas {
} }
private drawRoomText(line: Line) { private drawRoomText(line: Line) {
const points = line.segments
.reduce<Vec2[]>((list, segment) => {
if (segment.start) return [...list, segment.start, segment.end];
return [...list, segment.end];
}, [])
.filter(
(vec, index, arry) =>
arry.findIndex((point) => vec2Equals(point, vec)) === index
);
const centerPoint = vec2DivideScalar( const centerPoint = vec2DivideScalar(
line.segments.reduce<Vec2 | null>((prev, curr) => { points.reduce<Vec2 | null>(
if (!prev) { (prev, current) => (prev ? vec2Add(prev, current) : current),
if (curr.start) { null
return vec2Add(curr.start, curr.end); ) as Vec2,
} points.length
return curr.end;
}
let preadd = vec2Add(prev, curr.end);
if (curr.start) {
preadd = vec2Add(curr.start, preadd);
}
return preadd;
}, null) as Vec2,
line.segments.length + 1
); );
this.ctx.font = '16px Arial'; this.ctx.font = '16px Arial';
this.ctx.fillStyle = line.color; this.ctx.fillStyle = line.color;
const { width } = this.ctx.measureText(line.name); const { width } = this.ctx.measureText(line.name);

View File

@ -1,6 +1,7 @@
import { Ref } from 'vue'; import { Ref } from 'vue';
import { HousePlannerCanvas } from './canvas'; import { HousePlannerCanvas } from './canvas';
import { Layer, Vec2 } from './interfaces'; import { Layer, Vec2 } from './interfaces';
import { HousePlannerCanvasTools } from './tools';
export class HousePlanner { export class HousePlanner {
public canvas!: HTMLCanvasElement; public canvas!: HTMLCanvasElement;
@ -25,15 +26,15 @@ export class HousePlanner {
this.manager.layers = initialData; this.manager.layers = initialData;
if (editable && this.manager.tools) { if (editable && this.manager.tools) {
this.manager.tools.selectLayer( const stdToolkit = this.manager.tools as HousePlannerCanvasTools;
stdToolkit.selectLayer(
initialData[initialData.findIndex((layer) => layer.active)] initialData[initialData.findIndex((layer) => layer.active)]
); );
this.manager.tools.setInitialSelection(); stdToolkit.setInitialSelection();
this.manager.tools.setTool('move'); stdToolkit.setTool('move');
} }
this.manager.draw(); this.manager.draw();
return () => this.cleanUp();
} }
cleanUp() { cleanUp() {

View File

@ -1,3 +1,4 @@
import { HousePlannerCanvasHistory } from './history';
import { LayerObjectType } from './types'; import { LayerObjectType } from './types';
export type Vec2 = [number, number]; export type Vec2 = [number, number];
@ -13,6 +14,7 @@ export interface BezierSegment extends LineSegment {
export interface LayerObject { export interface LayerObject {
id: number; id: number;
databaseId?: number;
name: string; name: string;
visible: boolean; visible: boolean;
selected: boolean; selected: boolean;
@ -66,6 +68,16 @@ export interface ICanvasToolMouseEvents {
mouseUp(moved: boolean): void; mouseUp(moved: boolean): void;
} }
export interface ICanvasToolkit extends ICanvasToolMouseEvents {
history: HousePlannerCanvasHistory;
drawHighlights(): void;
drawControls(): void;
cleanUp(): void;
onKeyDown(e: KeyboardEvent): void;
onKeyUp(e: KeyboardEvent): void;
getMousedObject(): LayerObject | null;
}
export interface ICanvasToolBase<U = undefined> extends ICanvasToolMouseEvents { export interface ICanvasToolBase<U = undefined> extends ICanvasToolMouseEvents {
name: string; name: string;
subTool: U | undefined; subTool: U | undefined;

View File

@ -3,7 +3,7 @@ import { HousePlannerCanvasHistory } from './history';
import { import {
History, History,
ICanvasToolBase, ICanvasToolBase,
ICanvasToolMouseEvents, ICanvasToolkit,
Layer, Layer,
LayerObject, LayerObject,
Line, Line,
@ -14,11 +14,12 @@ import { CutTool } from './tools/cut';
import { LineTool } from './tools/line'; import { LineTool } from './tools/line';
import { MoveTool } from './tools/move'; import { MoveTool } from './tools/move';
export class HousePlannerCanvasTools implements ICanvasToolMouseEvents { export class HousePlannerCanvasTools implements ICanvasToolkit {
public selectedLayer?: Layer; public selectedLayer?: Layer;
public selectedObjects: LayerObject[] = []; public selectedObjects: LayerObject[] = [];
public gridSnap = true; public gridSnap = true;
public gridSnapScale = 8; public gridSnapScale = 8;
public autoClose = false;
public tool?: ICanvasToolBase<unknown>; public tool?: ICanvasToolBase<unknown>;
public tools: Record<string, ICanvasToolBase<unknown>> = { public tools: Record<string, ICanvasToolBase<unknown>> = {
['move']: new MoveTool(this), ['move']: new MoveTool(this),

View File

@ -1,10 +1,4 @@
import { import { LayerObject, Line, LineSegment, Vec2 } from '../interfaces';
BezierSegment,
LayerObject,
Line,
LineSegment,
Vec2,
} from '../interfaces';
import { CanvasToolBase } from './tool-base'; import { CanvasToolBase } from './tool-base';
export type CutToolType = 'cut' | 'remove-segment'; export type CutToolType = 'cut' | 'remove-segment';
@ -46,34 +40,7 @@ export class CutTool extends CanvasToolBase<CutToolType> {
this.renderer.draw(); this.renderer.draw();
} }
drawBezierControls(bezier: BezierSegment, previousEnd: Vec2) { drawLineControls(line: LineSegment) {
const [cp1x, cp1y] = bezier.startControl;
const [cp2x, cp2y] = bezier.endControl;
const [endx, endy] = bezier.end;
const [prevx, prevy] = previousEnd;
this.ctx.fillStyle = '#00ddffaa';
this.ctx.strokeStyle = '#00ddffaa';
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.ctx.arc(cp1x, cp1y, this.manager.selectError / 2, 0, 2 * Math.PI);
this.ctx.fill();
this.ctx.beginPath();
this.ctx.arc(cp2x, cp2y, this.manager.selectError / 2, 0, 2 * Math.PI);
this.ctx.fill();
this.ctx.beginPath();
this.ctx.moveTo(cp1x, cp1y);
this.ctx.lineTo(prevx, prevy);
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.moveTo(cp2x, cp2y);
this.ctx.lineTo(endx, endy);
this.ctx.stroke();
}
drawLineControls(line: LineSegment, previousEnd: Vec2) {
const [endx, endy] = line.end; const [endx, endy] = line.end;
this.ctx.fillStyle = '#00ddffaa'; this.ctx.fillStyle = '#00ddffaa';
@ -99,15 +66,8 @@ export class CutTool extends CanvasToolBase<CutToolType> {
for (const object of this.manager.selectedObjects) { for (const object of this.manager.selectedObjects) {
const line = object as Line; const line = object as Line;
if (line.segments && line.render) { if (line.segments && line.render) {
let lastSegment = null;
for (const segment of line.segments) { for (const segment of line.segments) {
const bezier = segment as BezierSegment; this.drawLineControls(segment);
const previousPoint = lastSegment ? lastSegment.end : segment.start!;
if (bezier.startControl && bezier.endControl) {
this.drawBezierControls(bezier, previousPoint);
}
this.drawLineControls(segment, previousPoint);
lastSegment = segment;
} }
} }
} }

View File

@ -6,7 +6,6 @@ import { CanvasToolBase } from './tool-base';
export type LineToolType = 'line' | 'curve' | 'room'; export type LineToolType = 'line' | 'curve' | 'room';
export class LineTool extends CanvasToolBase<LineToolType> { export class LineTool extends CanvasToolBase<LineToolType> {
public name = 'line'; public name = 'line';
public autoClose = false;
private drawingLine: Line | null = null; private drawingLine: Line | null = null;
public subTool: LineToolType = 'line'; public subTool: LineToolType = 'line';
@ -115,7 +114,7 @@ export class LineTool extends CanvasToolBase<LineToolType> {
) || ) ||
this.drawingLine.type === 'curve' this.drawingLine.type === 'curve'
) { ) {
if (this.drawingLine.type !== 'curve' && this.autoClose) { if (this.drawingLine.type !== 'curve' && this.manager.autoClose) {
this.drawingLine.segments.splice( this.drawingLine.segments.splice(
this.drawingLine.segments.length - 1, this.drawingLine.segments.length - 1,
1 1
@ -135,7 +134,10 @@ export class LineTool extends CanvasToolBase<LineToolType> {
value: [], value: [],
}, },
]); ]);
this.drawingLine.closed = this.autoClose; this.drawingLine.closed = this.manager.autoClose;
if (!this.manager.autoClose) {
this.drawingLine.lineCap = 'square';
}
this.canvas.dispatchEvent( this.canvas.dispatchEvent(
new CustomEvent('hpc:newobject', { new CustomEvent('hpc:newobject', {
detail: this.drawingLine, detail: this.drawingLine,

View File

@ -3,7 +3,7 @@ import Dashboard from '../views/Dashboard.vue';
import Login from '../views/Login.vue'; import Login from '../views/Login.vue';
import { createRouter, createWebHashHistory } from 'vue-router'; import { createRouter, createWebHashHistory } from 'vue-router';
import { useUserStore } from '../store/user.store'; import { useUserStore } from '../store/user.store';
import HousePlanner from '../components/house-planner/HousePlanner.vue'; import HousePlanner from '../views/HousePlanner.vue';
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ {

View File

@ -2,6 +2,8 @@ import { defineStore } from 'pinia';
import { useAccessToken } from '../composables/useAccessToken'; import { useAccessToken } from '../composables/useAccessToken';
import { BACKEND_URL } from '../constants'; import { BACKEND_URL } from '../constants';
import { BuildingListItem } from '../interfaces/building.interfaces'; import { BuildingListItem } from '../interfaces/building.interfaces';
import { FloorListItem } from '../interfaces/floor.interfaces';
import { RoomListItem } from '../interfaces/room.interfaces';
import jfetch from '../utils/jfetch'; import jfetch from '../utils/jfetch';
const { authHeader } = useAccessToken(); const { authHeader } = useAccessToken();
@ -9,6 +11,7 @@ export const useBuildingStore = defineStore('building', {
state: () => { state: () => {
return { return {
buildings: [] as BuildingListItem[], buildings: [] as BuildingListItem[],
floors: [] as FloorListItem[],
}; };
}, },
actions: { actions: {
@ -18,5 +21,34 @@ export const useBuildingStore = defineStore('building', {
}); });
this.buildings = buildings; this.buildings = buildings;
}, },
async getFloors(building: number) {
const { data: floors } = await jfetch(
`${BACKEND_URL}/buildings/${building}/floors`,
{
headers: authHeader.value,
}
);
this.floors = floors;
},
async saveFloor(
building: number,
number: number,
floor: Partial<FloorListItem>
) {
await jfetch(`${BACKEND_URL}/buildings/${building}/floors/${number}`, {
method: 'PATCH',
headers: {
...authHeader.value,
'Content-Type': 'application/json',
},
body: JSON.stringify(floor),
});
},
async upsertFloorRooms(
building: number,
floorNo: number,
rooms: RoomListItem[],
removedRooms: number[]
) {},
}, },
}); });

104
src/views/HousePlanner.vue Normal file
View File

@ -0,0 +1,104 @@
<template>
<div class="relative h-full">
<div
class="absolute top-0 left-0 z-10 rounded-br-md border-b-2 border-r-2 border-gray-200 bg-white px-2 py-2 shadow-lg"
>
<div class="flex flex-row items-center space-x-4 px-4">
<div class="flex flex-row items-center space-x-4">
<label for="building">Building:</label>
<select
id="building"
class="rounded-sm border-gray-300 py-1 focus:ring-2 focus:ring-blue-200"
v-model="selectedBuildingId"
@change="buildingSelected()"
>
<option v-for="building of buildings" :value="building.id">
{{ building.displayName }}
</option>
</select>
</div>
<div
class="flex flex-row items-center space-x-4"
v-if="selectedBuildingId"
>
<label for="floor">Floor:</label>
<select
id="floor"
class="rounded-sm border-gray-300 py-1 focus:ring-2 focus:ring-blue-200"
v-model="selectedFloorId"
>
<option v-for="floor of floors" :value="floor.id">
{{ floor.displayName }} ({{ floor.number }})
</option>
</select>
</div>
<div v-if="selectedFloorId">{{ status }}</div>
</div>
</div>
<HousePlanner
v-if="selectedFloorId"
:floor-document="floorPlan"
@update="($newValue) => updateDocument($newValue)"
@edited="status = 'Modified'"
/>
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { computed, onMounted, ref } from 'vue';
import { defaultRoomData } from '../components/house-planner/helpers/default-room';
import HousePlanner from '../components/house-planner/HousePlanner.vue';
import { FloorDocument } from '../components/house-planner/interfaces/floor-document.interface';
import { useBuildingStore } from '../store/building.store';
const building = useBuildingStore();
const { buildings, floors } = storeToRefs(building);
const selectedBuildingId = ref<number>();
const selectedFloorId = ref<number>();
const status = ref('No changes');
const currentFloor = computed(
() =>
selectedFloorId.value &&
floors.value.find((floor) => floor.id === selectedFloorId.value)
);
const floorPlan = computed(
() =>
currentFloor.value &&
Object.assign({
...defaultRoomData,
id: selectedFloorId.value,
...JSON.parse(currentFloor.value.plan),
})
);
const buildingSelected = async () => {
if (selectedBuildingId.value == null) return;
await building.getFloors(selectedBuildingId.value);
};
const updateDocument = async (data: FloorDocument) => {
if (
!selectedBuildingId.value ||
!selectedFloorId.value ||
!currentFloor.value
)
return;
status.value = 'Saving...';
await building.saveFloor(
selectedBuildingId.value,
currentFloor.value.number,
{
plan: JSON.stringify(data),
}
);
status.value = 'Saved!';
};
onMounted(() => {
floors.value = [];
building.getBuildings();
});
</script>