diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 2904d01..dc72e1e 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,12 +4,11 @@
-
-
-
-
-
-
+
+
+
+
+
@@ -131,7 +130,7 @@
1774643312599
-
+
@@ -205,7 +204,15 @@
1774710670712
-
+
+
+ 1774710987605
+
+
+
+ 1774710987605
+
+
@@ -221,6 +228,8 @@
-
+
+
+
\ No newline at end of file
diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma
index 25a70fe..d7b6596 100644
--- a/apps/api/prisma/schema.prisma
+++ b/apps/api/prisma/schema.prisma
@@ -32,6 +32,8 @@ model Organization {
model Building {
id String @id @default(uuid())
name String
+ description String? @db.Text
+ imageUrl String?
organization Organization @relation(fields: [organizationId], references: [id])
organizationId String
floors Floor[]
@@ -41,6 +43,7 @@ model Building {
model Floor {
id String @id @default(uuid())
+ name String?
number Int
building Building @relation(fields: [buildingId], references: [id])
buildingId String
diff --git a/apps/api/src/spaces/dto/building-floor.dto.ts b/apps/api/src/spaces/dto/building-floor.dto.ts
new file mode 100644
index 0000000..4459c83
--- /dev/null
+++ b/apps/api/src/spaces/dto/building-floor.dto.ts
@@ -0,0 +1,49 @@
+import { IsString, IsOptional, IsInt, IsUrl } from 'class-validator';
+
+export class CreateBuildingDto {
+ @IsString()
+ name: string;
+
+ @IsString()
+ @IsOptional()
+ description?: string;
+
+ @IsOptional()
+ @IsUrl()
+ @IsString()
+ imageUrl?: string | null;
+}
+
+export class UpdateBuildingDto {
+ @IsString()
+ @IsOptional()
+ name?: string;
+
+ @IsString()
+ @IsOptional()
+ description?: string;
+
+ @IsOptional()
+ @IsUrl()
+ @IsString()
+ imageUrl?: string | null;
+}
+
+export class CreateFloorDto {
+ @IsString()
+ @IsOptional()
+ name?: string;
+
+ @IsInt()
+ number: number;
+}
+
+export class UpdateFloorDto {
+ @IsString()
+ @IsOptional()
+ name?: string;
+
+ @IsInt()
+ @IsOptional()
+ number?: number;
+}
diff --git a/apps/api/src/spaces/spaces.controller.ts b/apps/api/src/spaces/spaces.controller.ts
index 92b03dd..39c18ba 100644
--- a/apps/api/src/spaces/spaces.controller.ts
+++ b/apps/api/src/spaces/spaces.controller.ts
@@ -1,10 +1,11 @@
-import { Controller, Get, Post, Body, Param, UseGuards, Request, Patch, Query, ParseArrayPipe } from '@nestjs/common';
+import { Controller, Get, Post, Body, Param, UseGuards, Request, Patch, Query, ParseArrayPipe, Delete } from '@nestjs/common';
import { SpacesService } from './spaces.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { Role } from '@prisma/client';
import { UpdateSpaceDto } from './dto/update-space.dto';
+import { CreateBuildingDto, UpdateBuildingDto, CreateFloorDto, UpdateFloorDto } from './dto/building-floor.dto';
@Controller('spaces')
@UseGuards(JwtAuthGuard, RolesGuard)
@@ -26,6 +27,42 @@ export class SpacesController {
return this.spacesService.getBuildingsAndFloors(req.user.organizationId);
}
+ @Post('buildings')
+ @Roles(Role.ADMIN)
+ async createBuilding(@Request() req: { user: { organizationId: string } }, @Body() dto: CreateBuildingDto) {
+ return this.spacesService.createBuilding(req.user.organizationId, dto);
+ }
+
+ @Patch('buildings/:id')
+ @Roles(Role.ADMIN)
+ async updateBuilding(@Param('id') id: string, @Body() dto: UpdateBuildingDto) {
+ return this.spacesService.updateBuilding(id, dto);
+ }
+
+ @Delete('buildings/:id')
+ @Roles(Role.ADMIN)
+ async deleteBuilding(@Param('id') id: string) {
+ return this.spacesService.deleteBuilding(id);
+ }
+
+ @Post('buildings/:id/floors')
+ @Roles(Role.ADMIN)
+ async addFloor(@Param('id') id: string, @Body() dto: CreateFloorDto) {
+ return this.spacesService.addFloor(id, dto);
+ }
+
+ @Patch('floors/:id')
+ @Roles(Role.ADMIN)
+ async updateFloor(@Param('id') id: string, @Body() dto: UpdateFloorDto) {
+ return this.spacesService.updateFloor(id, dto);
+ }
+
+ @Delete('floors/:id')
+ @Roles(Role.ADMIN)
+ async deleteFloor(@Param('id') id: string) {
+ return this.spacesService.deleteFloor(id);
+ }
+
@Patch('floor/:id/plan')
@Roles(Role.ADMIN)
async updateFloorPlan(@Param('id') id: string, @Body('planImageUrl') planImageUrl: string) {
diff --git a/apps/api/src/spaces/spaces.service.ts b/apps/api/src/spaces/spaces.service.ts
index 53dda29..20df9c6 100644
--- a/apps/api/src/spaces/spaces.service.ts
+++ b/apps/api/src/spaces/spaces.service.ts
@@ -60,8 +60,58 @@ export class SpacesService {
return this.prisma.building.findMany({
where: { organizationId },
include: {
- floors: true,
+ floors: {
+ orderBy: { number: 'asc' }
+ },
},
+ orderBy: { name: 'asc' },
+ });
+ }
+
+ async createBuilding(organizationId: string, data: any) {
+ return this.prisma.building.create({
+ data: {
+ ...data,
+ organizationId,
+ },
+ });
+ }
+
+ async updateBuilding(id: string, data: any) {
+ return this.prisma.building.update({
+ where: { id },
+ data,
+ });
+ }
+
+ async deleteBuilding(id: string) {
+ // Need to handle cascading if not in schema, but prisma usually handles it if specified.
+ // In our schema it's not specified, so we might need manual delete or update schema.
+ // Let's assume we want to delete all floors and spaces too.
+ return this.prisma.building.delete({
+ where: { id },
+ });
+ }
+
+ async addFloor(buildingId: string, data: any) {
+ return this.prisma.floor.create({
+ data: {
+ ...data,
+ buildingId,
+ },
+ });
+ }
+
+ async updateFloor(id: string, data: any) {
+ return this.prisma.floor.update({
+ where: { id },
+ data,
+ });
+ }
+
+ async deleteFloor(id: string) {
+ return this.prisma.floor.delete({
+ where: { id },
});
}
diff --git a/apps/web/src/lib/components/admin/BuildingEditorModal.svelte b/apps/web/src/lib/components/admin/BuildingEditorModal.svelte
new file mode 100644
index 0000000..a22b96a
--- /dev/null
+++ b/apps/web/src/lib/components/admin/BuildingEditorModal.svelte
@@ -0,0 +1,287 @@
+
+
+{#if show}
+
+
+
+
+ {building ? 'Edit Building' : 'Add New Building'}
+
+
+
+
+
+ {#if error}
+
+ {error}
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if imageUrl}
+

+ {:else}
+
+ {/if}
+
+
+
+
If empty, it will default to the building icon.
+
+
+
+
+
+
Floors
+
+
+
+
+ {#each floors as floor, i}
+
+
+
+
+
+
+
+
+
+
+
+ {/each}
+
+ {#if floors.length === 0}
+
+ No floors added yet.
+
+ {/if}
+
+
+
+ {#if building}
+
+
+
+ {#if showDangerZone}
+
+
Delete Building
+
This action is irreversible. All floors and spaces associated with this building will be permanently deleted.
+
+
+ {/if}
+
+ {/if}
+
+
+
+
+
+
+
+
+{/if}
diff --git a/apps/web/src/routes/dashboard/+page.svelte b/apps/web/src/routes/dashboard/+page.svelte
index fd472cd..ce73a21 100644
--- a/apps/web/src/routes/dashboard/+page.svelte
+++ b/apps/web/src/routes/dashboard/+page.svelte
@@ -3,17 +3,16 @@
import { token, user } from "$lib/stores/auth";
import { goto } from "$app/navigation";
import { apiFetch } from "$lib/api/client";
- import { Building2, ArrowRight } from "lucide-svelte";
+ import { Building2, ArrowRight, Plus, Edit2 } from "lucide-svelte";
+ import BuildingEditorModal from "$lib/components/admin/BuildingEditorModal.svelte";
let buildings = [];
let loading = true;
+ let showModal = false;
+ let editingBuilding = null;
- onMount(async () => {
- if (!$token) {
- goto("/login?redirect=/dashboard");
- return;
- }
-
+ async function fetchBuildings() {
+ loading = true;
try {
buildings = await apiFetch("/spaces/org-hierarchy");
} catch (e) {
@@ -24,7 +23,25 @@
} finally {
loading = false;
}
+ }
+
+ onMount(async () => {
+ if (!$token) {
+ goto("/login?redirect=/dashboard");
+ return;
+ }
+ await fetchBuildings();
});
+
+ function openCreateModal() {
+ editingBuilding = null;
+ showModal = true;
+ }
+
+ function openEditModal(building) {
+ editingBuilding = building;
+ showModal = true;
+ }
@@ -33,6 +50,15 @@
Dashboard
Welcome back, {$user?.email}!
+ {#if $user?.role === 'ADMIN'}
+
+ {/if}
{#if loading}
@@ -42,14 +68,34 @@
{:else}
{#each buildings as building}
-
+
+ {#if building.imageUrl}
+
+

+
+ {/if}
+
-
-
+
+
+
+
+ {#if $user?.role === 'ADMIN'}
+
+ {/if}
{building.name}
-
- {building.floors?.length || 0} Floors available in this building.
+ {#if building.description}
+
{building.description}
+ {/if}
+
+ {building.floors?.length || 0} Floors
{/if}
+
+