Add building and floor management with admin editor modal
- Implement `BuildingEditorModal.svelte` for creating, editing, and deleting buildings and floors. - Add `CreateBuildingDto`, `UpdateBuildingDto`, `CreateFloorDto`, and `UpdateFloorDto` for validation. - Extend `SpacesService` and `SpacesController` with endpoints for building and floor CRUD operations. - Update Prisma schema to include building descriptions and image URLs, and floor names. - Enhance dashboard with building management functionality for admin users.main
parent
165cd76b6f
commit
1b3c3044ca
|
|
@ -4,12 +4,11 @@
|
|||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Add user management and registration features with invite-based onboarding - Implement `+page.svelte` for admin user management, allowing role updates, password resets, and invite link generation.">
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/src/reservations/reservations.controller.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/reservations/reservations.controller.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/src/reservations/reservations.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/reservations/reservations.service.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/web/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/package.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/+layout.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/+layout.svelte" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/package-lock.json" afterDir="false" />
|
||||
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Add "My Bookings" page and enable reservation updates and cancellations - Implement `+page.svelte` for managing user reservations with real-time updates. - Add API endpoints for updating and canceling reservations with enhanced validation and authorization. - Introduce `UpdateReservationDto` for refined reservation update handling. - Update `reservations.service.ts` to handle overlapping checks, time adjustments, and admin actions. - Extend layout to include a link to the "My Bookings" page. - Add `date-fns` library for date formatting.">
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/prisma/schema.prisma" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/prisma/schema.prisma" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/src/spaces/spaces.controller.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/spaces/spaces.controller.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/src/spaces/spaces.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/spaces/spaces.service.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/dashboard/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/dashboard/+page.svelte" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
|
|
@ -131,7 +130,7 @@
|
|||
<updated>1774643312599</updated>
|
||||
<workItem from="1774643313611" duration="74000" />
|
||||
<workItem from="1774643422240" duration="5278000" />
|
||||
<workItem from="1774706361502" duration="4540000" />
|
||||
<workItem from="1774706361502" duration="4842000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="Initialize Hot Desking app with web and API services - Set up SvelteKit for the web interface with Tailwind CSS. - Build the API using NestJS with Prisma ORM for database interaction. - Add environment variable management and docker-compose for PostgreSQL and PgAdmin instances. - Create shared data models for consistent typing across services. - Establish basic routing and layouts for web and API services.">
|
||||
<option name="closed" value="true" />
|
||||
|
|
@ -205,7 +204,15 @@
|
|||
<option name="project" value="LOCAL" />
|
||||
<updated>1774710670712</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="10" />
|
||||
<task id="LOCAL-00010" summary="Add "My Bookings" page and enable reservation updates and cancellations - Implement `+page.svelte` for managing user reservations with real-time updates. - Add API endpoints for updating and canceling reservations with enhanced validation and authorization. - Introduce `UpdateReservationDto` for refined reservation update handling. - Update `reservations.service.ts` to handle overlapping checks, time adjustments, and admin actions. - Extend layout to include a link to the "My Bookings" page. - Add `date-fns` library for date formatting.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1774710987605</created>
|
||||
<option name="number" value="00010" />
|
||||
<option name="presentableId" value="LOCAL-00010" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1774710987605</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="11" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
|
|
@ -221,6 +228,8 @@
|
|||
<MESSAGE value="Add support for space resizing and enhanced layout management - Update `SpacesService` to handle width and height properties for spaces. - Introduce `UpdateSpaceDto` for improved validation in batch update operations. - Extend `FloorplanEditor.svelte` and `FloorplanViewer.svelte` to support dynamic container scaling and space resizing. - Modify Prisma schema to include `width` and `height` fields for spaces. - Enhance floorplan rendering to maintain proportional layout during editing and viewing." />
|
||||
<MESSAGE value="Add admin reporting dashboard with utilization statistics and downloadable reports - Implement `+page.svelte` for admin reporting dashboard showing utilization and employee stats. - Add API endpoints for utilization stats, employee stats, and Excel report export (NestJS). - Create `ReportsService` and `ReportsModule` for backend logic. - Introduce `xlsx` package for generating Excel reports. - Update layout to include "Reports" link for admin users. - Extend Prisma querying for detailed building, space type, and employee role data." />
|
||||
<MESSAGE value="Add user management and registration features with invite-based onboarding - Implement `+page.svelte` for admin user management, allowing role updates, password resets, and invite link generation." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Add user management and registration features with invite-based onboarding - Implement `+page.svelte` for admin user management, allowing role updates, password resets, and invite link generation." />
|
||||
<MESSAGE value="Add user management and registration features with invite-based onboarding - Implement `+page.svelte` for admin user management, allowing role updates, password resets, and invite link generation. Update `UsersController` and `UsersService` to support user role updates, password changes, and invite token generation. - Create DTOs (`RegisterDto`, `UpdateUserDto`) for input validation in user management endpoints. - Extend `AuthService` and `AuthController` with invite-based registration logic. - Update admin layout to include "Users" link for easy access to user management." />
|
||||
<MESSAGE value="Add "My Bookings" page and enable reservation updates and cancellations - Implement `+page.svelte` for managing user reservations with real-time updates. - Add API endpoints for updating and canceling reservations with enhanced validation and authorization. - Introduce `UpdateReservationDto` for refined reservation update handling. - Update `reservations.service.ts` to handle overlapping checks, time adjustments, and admin actions. - Extend layout to include a link to the "My Bookings" page. - Add `date-fns` library for date formatting." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Add "My Bookings" page and enable reservation updates and cancellations - Implement `+page.svelte` for managing user reservations with real-time updates. - Add API endpoints for updating and canceling reservations with enhanced validation and authorization. - Introduce `UpdateReservationDto` for refined reservation update handling. - Update `reservations.service.ts` to handle overlapping checks, time adjustments, and admin actions. - Extend layout to include a link to the "My Bookings" page. - Add `date-fns` library for date formatting." />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,287 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { X, Plus, Trash2, Building2 } from 'lucide-svelte';
|
||||
import { apiFetch } from '$lib/api/client';
|
||||
|
||||
export let building: any = null; // if null, we are creating a new building
|
||||
export let show = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let name = building?.name || '';
|
||||
let description = building?.description || '';
|
||||
let imageUrl = building?.imageUrl || '';
|
||||
let floors = building?.floors ? JSON.parse(JSON.stringify(building.floors)) : [];
|
||||
let loading = false;
|
||||
let error = '';
|
||||
let showDangerZone = false;
|
||||
|
||||
$: {
|
||||
if (show && building) {
|
||||
name = building.name;
|
||||
description = building.description || '';
|
||||
imageUrl = building.imageUrl || '';
|
||||
floors = JSON.parse(JSON.stringify(building.floors || []));
|
||||
} else if (show && !building) {
|
||||
name = '';
|
||||
description = '';
|
||||
imageUrl = '';
|
||||
floors = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function saveBuilding() {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
let savedBuilding;
|
||||
if (building) {
|
||||
savedBuilding = await apiFetch(`/spaces/buildings/${building.id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
description: description || null,
|
||||
imageUrl: imageUrl || null
|
||||
})
|
||||
});
|
||||
|
||||
// Update floors
|
||||
for (const floor of floors) {
|
||||
if (floor.id) {
|
||||
await apiFetch(`/spaces/floors/${floor.id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ name: floor.name, number: floor.number })
|
||||
});
|
||||
} else {
|
||||
await apiFetch(`/spaces/buildings/${building.id}/floors`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: floor.name, number: floor.number })
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
savedBuilding = await apiFetch('/spaces/buildings', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
description: description || null,
|
||||
imageUrl: imageUrl || null
|
||||
})
|
||||
});
|
||||
|
||||
// Add floors if any were defined during creation
|
||||
for (const floor of floors) {
|
||||
await apiFetch(`/spaces/buildings/${savedBuilding.id}/floors`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: floor.name, number: floor.number })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dispatch('save');
|
||||
close();
|
||||
} catch (e: any) {
|
||||
error = e.message || 'Failed to save building';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteBuilding() {
|
||||
if (!confirm('Are you absolutely sure you want to delete this building and all its floors?')) return;
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
await apiFetch(`/spaces/buildings/${building.id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
dispatch('save');
|
||||
close();
|
||||
} catch (e: any) {
|
||||
error = e.message || 'Failed to delete building';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFloor(index: number) {
|
||||
const floor = floors[index];
|
||||
if (floor.id) {
|
||||
if (!confirm('Are you sure you want to delete this floor?')) return;
|
||||
try {
|
||||
await apiFetch(`/spaces/floors/${floor.id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
} catch (e: any) {
|
||||
error = e.message || 'Failed to delete floor';
|
||||
return;
|
||||
}
|
||||
}
|
||||
floors = floors.filter((_, i) => i !== index);
|
||||
}
|
||||
|
||||
function addFloor() {
|
||||
const maxNumber = floors.reduce((max, f) => Math.max(max, f.number), -1);
|
||||
floors = [...floors, { name: '', number: maxNumber + 1 }];
|
||||
}
|
||||
|
||||
function close() {
|
||||
show = false;
|
||||
showDangerZone = false;
|
||||
dispatch('close');
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<div class="flex justify-between items-center p-6 border-b sticky top-0 bg-white z-10">
|
||||
<h2 class="text-2xl font-bold text-gray-900">
|
||||
{building ? 'Edit Building' : 'Add New Building'}
|
||||
</h2>
|
||||
<button on:click={close} class="text-gray-400 hover:text-gray-600">
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-6">
|
||||
{#if error}
|
||||
<div class="bg-red-50 text-red-600 p-4 rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Building Name</label>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={name}
|
||||
placeholder="e.g. Headquarters"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
||||
<textarea
|
||||
bind:value={description}
|
||||
placeholder="Tell something about this building..."
|
||||
rows="3"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Header Image URL</label>
|
||||
<div class="flex gap-4 items-center">
|
||||
<div class="w-16 h-16 bg-gray-100 rounded-lg flex-shrink-0 flex items-center justify-center overflow-hidden">
|
||||
{#if imageUrl}
|
||||
<img src={imageUrl} alt="Preview" class="w-full h-full object-cover" />
|
||||
{:else}
|
||||
<Building2 class="text-gray-400" size={32} />
|
||||
{/if}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={imageUrl}
|
||||
placeholder="https://images.unsplash.com/..."
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1">If empty, it will default to the building icon.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Floors</h3>
|
||||
<button
|
||||
on:click={addFloor}
|
||||
class="flex items-center gap-1 text-sm text-indigo-600 hover:text-indigo-700 font-medium"
|
||||
>
|
||||
<Plus size={18} /> Add Floor
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
{#each floors as floor, i}
|
||||
<div class="flex gap-3 items-center bg-gray-50 p-3 rounded-lg border border-gray-100">
|
||||
<div class="w-16">
|
||||
<label class="block text-[10px] uppercase tracking-wider font-bold text-gray-400 mb-1">Num</label>
|
||||
<input
|
||||
type="number"
|
||||
bind:value={floor.number}
|
||||
class="w-full bg-white px-2 py-1 border border-gray-200 rounded text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<label class="block text-[10px] uppercase tracking-wider font-bold text-gray-400 mb-1">Floor Name</label>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={floor.name}
|
||||
placeholder="e.g. Ground Floor"
|
||||
class="w-full bg-white px-2 py-1 border border-gray-200 rounded text-sm"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
on:click={() => deleteFloor(i)}
|
||||
class="mt-5 text-gray-400 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if floors.length === 0}
|
||||
<p class="text-center py-4 text-gray-500 text-sm bg-gray-50 rounded-lg border border-dashed border-gray-200">
|
||||
No floors added yet.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if building}
|
||||
<div class="border-t pt-6">
|
||||
<button
|
||||
on:click={() => showDangerZone = !showDangerZone}
|
||||
class="text-sm text-red-600 hover:text-red-700 font-medium"
|
||||
>
|
||||
{showDangerZone ? 'Hide' : 'Show'} Danger Zone
|
||||
</button>
|
||||
|
||||
{#if showDangerZone}
|
||||
<div class="mt-4 p-4 border border-red-200 bg-red-50 rounded-lg">
|
||||
<h4 class="text-red-800 font-bold mb-1">Delete Building</h4>
|
||||
<p class="text-red-600 text-sm mb-4">This action is irreversible. All floors and spaces associated with this building will be permanently deleted.</p>
|
||||
<button
|
||||
on:click={deleteBuilding}
|
||||
disabled={loading}
|
||||
class="px-4 py-2 bg-red-600 text-white rounded-lg text-sm font-medium hover:bg-red-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Deleting...' : 'Permanently Delete Building'}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="p-6 border-t sticky bottom-0 bg-white flex justify-end gap-3">
|
||||
<button
|
||||
on:click={close}
|
||||
class="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
on:click={saveBuilding}
|
||||
disabled={loading || !name}
|
||||
class="px-6 py-2 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Saving...' : 'Save Building'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -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;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-8 max-w-6xl mx-auto">
|
||||
|
|
@ -33,6 +50,15 @@
|
|||
<h1 class="text-3xl font-bold text-gray-900">Dashboard</h1>
|
||||
<p class="text-gray-500 mt-1">Welcome back, {$user?.email}!</p>
|
||||
</div>
|
||||
{#if $user?.role === 'ADMIN'}
|
||||
<button
|
||||
on:click={openCreateModal}
|
||||
class="flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700 transition-colors"
|
||||
>
|
||||
<Plus size={20} />
|
||||
Add Building
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
|
|
@ -42,14 +68,34 @@
|
|||
{:else}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{#each buildings as building}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow group">
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow group relative">
|
||||
{#if building.imageUrl}
|
||||
<div class="h-32 w-full overflow-hidden">
|
||||
<img src={building.imageUrl} alt={building.name} class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="p-6">
|
||||
<div class="w-12 h-12 bg-indigo-50 rounded-lg flex items-center justify-center mb-4 group-hover:bg-indigo-100 transition-colors">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="w-12 h-12 bg-indigo-50 rounded-lg flex items-center justify-center group-hover:bg-indigo-100 transition-colors">
|
||||
<Building2 class="text-indigo-600" size={24} />
|
||||
</div>
|
||||
{#if $user?.role === 'ADMIN'}
|
||||
<button
|
||||
on:click={() => openEditModal(building)}
|
||||
class="p-2 text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-lg transition-colors"
|
||||
title="Edit Building"
|
||||
>
|
||||
<Edit2 size={18} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-2">{building.name}</h2>
|
||||
<p class="text-gray-500 text-sm mb-6">
|
||||
{building.floors?.length || 0} Floors available in this building.
|
||||
{#if building.description}
|
||||
<p class="text-gray-500 text-sm mb-4 line-clamp-2">{building.description}</p>
|
||||
{/if}
|
||||
<p class="text-gray-400 text-xs mb-6 uppercase tracking-wider font-semibold">
|
||||
{building.floors?.length || 0} Floors
|
||||
</p>
|
||||
|
||||
<a
|
||||
|
|
@ -73,3 +119,9 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<BuildingEditorModal
|
||||
bind:show={showModal}
|
||||
building={editingBuilding}
|
||||
on:save={fetchBuildings}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue