Add real-time space status updates and reservation management
- Integrate WebSocket functionality using `socket.io` for floor-specific updates. - Implement `ReservationsModule` with CRUD for managing reservations (API). - Create `LoggerMiddleware` for consistent HTTP request logging. - Update `SpacesService` to include real-time reservation status checks. - Add `SpaceCard.svelte` component to reflect live space status (Web). - Enable environmental base API URL configuration for flexible deployments.main
parent
8dc99bc1a4
commit
3b09daeb04
|
|
@ -4,21 +4,15 @@
|
|||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Remove compiled API files from version control - Deleted compiled JavaScript files, type definitions, and source maps from the `dist` folder.">
|
||||
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
|
||||
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Implement authentication, organization management, and floor layout features - Add API authentication with JWT strategy using NestJS. - Configure Prisma schema for organizations, users, buildings, floors, and spaces. - Seed initial database with test data and relationships. - Build SvelteKit web pages for dashboard and floor layout with secured routing and organization hierarchy display. - Remove deprecated `AppService` from API.">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/dist/app.service.d.ts" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/package.json" afterDir="false" />
|
||||
<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/app.module.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/app.module.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/src/main.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/main.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/src/prisma/prisma.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/prisma/prisma.service.ts" 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/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$/apps/web/src/routes/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/+page.svelte" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/login/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/login/+page.svelte" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/web/src/lib/api/client.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/lib/api/client.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/dashboard/floor/[id]/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/dashboard/floor/[id]/+page.svelte" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/package-lock.json" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
|
|
@ -72,6 +66,11 @@
|
|||
"to.speed.mode.migration.done": "true",
|
||||
"ts.external.directory.path": "/home/micosil/WebstormProjects/desker/node_modules/typescript/lib",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
},
|
||||
"keyToStringList": {
|
||||
"DatabaseDriversLRU": [
|
||||
"postgresql"
|
||||
]
|
||||
}
|
||||
}]]></component>
|
||||
<component name="RunManager" selected="npm.web > dev">
|
||||
|
|
@ -130,7 +129,7 @@
|
|||
<option name="presentableId" value="Default" />
|
||||
<updated>1774643312599</updated>
|
||||
<workItem from="1774643313611" duration="74000" />
|
||||
<workItem from="1774643422240" duration="4582000" />
|
||||
<workItem from="1774643422240" duration="5278000" />
|
||||
</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" />
|
||||
|
|
@ -148,7 +147,15 @@
|
|||
<option name="project" value="LOCAL" />
|
||||
<updated>1774653909300</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="3" />
|
||||
<task id="LOCAL-00003" summary="Implement authentication, organization management, and floor layout features - Add API authentication with JWT strategy using NestJS. - Configure Prisma schema for organizations, users, buildings, floors, and spaces. - Seed initial database with test data and relationships. - Build SvelteKit web pages for dashboard and floor layout with secured routing and organization hierarchy display. - Remove deprecated `AppService` from API.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1774654565964</created>
|
||||
<option name="number" value="00003" />
|
||||
<option name="presentableId" value="LOCAL-00003" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1774654565964</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="4" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
|
|
@ -157,6 +164,7 @@
|
|||
<component name="VcsManagerConfiguration">
|
||||
<MESSAGE value="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." />
|
||||
<MESSAGE value="Remove compiled API files from version control - Deleted compiled JavaScript files, type definitions, and source maps from the `dist` folder." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Remove compiled API files from version control - Deleted compiled JavaScript files, type definitions, and source maps from the `dist` folder." />
|
||||
<MESSAGE value="Implement authentication, organization management, and floor layout features - Add API authentication with JWT strategy using NestJS. - Configure Prisma schema for organizations, users, buildings, floors, and spaces. - Seed initial database with test data and relationships. - Build SvelteKit web pages for dashboard and floor layout with secured routing and organization hierarchy display. - Remove deprecated `AppService` from API." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Implement authentication, organization management, and floor layout features - Add API authentication with JWT strategy using NestJS. - Configure Prisma schema for organizations, users, buildings, floors, and spaces. - Seed initial database with test data and relationships. - Build SvelteKit web pages for dashboard and floor layout with secured routing and organization hierarchy display. - Remove deprecated `AppService` from API." />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -26,6 +26,8 @@
|
|||
"@nestjs/jwt": "^11.0.2",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^10.4.22",
|
||||
"@nestjs/platform-socket.io": "^10.4.22",
|
||||
"@nestjs/websockets": "^10.4.22",
|
||||
"@prisma/client": "^6.4.0",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
|
|
@ -36,7 +38,8 @@
|
|||
"passport-jwt": "^4.0.1",
|
||||
"prisma": "^6.4.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2"
|
||||
"rxjs": "^7.8.2",
|
||||
"socket.io": "^4.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
|
|
|
|||
|
|
@ -50,24 +50,38 @@ model Floor {
|
|||
}
|
||||
|
||||
model Space {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
type SpaceType
|
||||
floor Floor @relation(fields: [floorId], references: [id])
|
||||
floorId String
|
||||
x Float? // Percentage-based for responsiveness (Stage 3)
|
||||
y Float?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
type SpaceType
|
||||
floor Floor @relation(fields: [floorId], references: [id])
|
||||
floorId String
|
||||
x Float? // Percentage-based for responsiveness (Stage 3)
|
||||
y Float?
|
||||
reservations Reservation[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
password String
|
||||
role Role @default(EMPLOYEE)
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
role Role @default(EMPLOYEE)
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
organizationId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
reservations Reservation[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Reservation {
|
||||
id String @id @default(uuid())
|
||||
startTime DateTime
|
||||
endTime DateTime
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
space Space @relation(fields: [spaceId], references: [id])
|
||||
spaceId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { SpacesModule } from './spaces/spaces.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { ReservationsModule } from './reservations/reservations.module';
|
||||
import { EventsModule } from './events/events.module';
|
||||
import { LoggerMiddleware } from './common/middleware/logger.middleware';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -12,8 +15,14 @@ import { AuthModule } from './auth/auth.module';
|
|||
PrismaModule,
|
||||
SpacesModule,
|
||||
AuthModule,
|
||||
ReservationsModule,
|
||||
EventsModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(LoggerMiddleware).forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
|||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
if (!isHttpException) {
|
||||
console.error(exception);
|
||||
}
|
||||
|
||||
const message = isHttpException
|
||||
? exception.getResponse()
|
||||
: 'Internal server error';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class LoggerMiddleware implements NestMiddleware {
|
||||
private logger = new Logger('HTTP');
|
||||
|
||||
use(request: Request, response: Response, next: NextFunction): void {
|
||||
const { method, originalUrl } = request;
|
||||
const userAgent = request.get('user-agent') || '';
|
||||
|
||||
response.on('finish', () => {
|
||||
const { statusCode } = response;
|
||||
const contentLength = response.get('content-length');
|
||||
|
||||
this.logger.log(
|
||||
`${method} ${originalUrl} ${statusCode} ${contentLength} - ${userAgent}`,
|
||||
);
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import {
|
||||
WebSocketGateway,
|
||||
WebSocketServer,
|
||||
SubscribeMessage,
|
||||
OnGatewayConnection,
|
||||
OnGatewayDisconnect,
|
||||
} from '@nestjs/websockets';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
|
||||
@WebSocketGateway({
|
||||
cors: {
|
||||
origin: '*',
|
||||
},
|
||||
})
|
||||
export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||
@WebSocketServer()
|
||||
server: Server;
|
||||
|
||||
handleConnection(client: Socket) {
|
||||
console.log(`Client connected: ${client.id}`);
|
||||
}
|
||||
|
||||
handleDisconnect(client: Socket) {
|
||||
console.log(`Client disconnected: ${client.id}`);
|
||||
}
|
||||
|
||||
@SubscribeMessage('join_floor')
|
||||
handleJoinFloor(client: Socket, floorId: string) {
|
||||
client.join(floorId);
|
||||
console.log(`Client ${client.id} joined floor: ${floorId}`);
|
||||
}
|
||||
|
||||
@SubscribeMessage('leave_floor')
|
||||
handleLeaveFloor(client: Socket, floorId: string) {
|
||||
client.leave(floorId);
|
||||
console.log(`Client ${client.id} left floor: ${floorId}`);
|
||||
}
|
||||
|
||||
emitSpaceStatusChanged(floorId: string, spaceId: string, isOccupied: boolean) {
|
||||
this.server.to(floorId).emit('space_status_changed', {
|
||||
spaceId,
|
||||
isOccupied,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { EventsGateway } from './events.gateway';
|
||||
|
||||
@Module({
|
||||
providers: [EventsGateway],
|
||||
exports: [EventsGateway],
|
||||
})
|
||||
export class EventsModule {}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { IsString, IsDateString, IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class CreateReservationDto {
|
||||
@IsDateString()
|
||||
@IsNotEmpty()
|
||||
startTime: string;
|
||||
|
||||
@IsDateString()
|
||||
@IsNotEmpty()
|
||||
endTime: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
spaceId: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Delete,
|
||||
Get,
|
||||
Body,
|
||||
Param,
|
||||
UseGuards,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import { ReservationsService } from './reservations.service';
|
||||
import { CreateReservationDto } from './dto/create-reservation.dto';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
|
||||
@Controller('reservations')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class ReservationsController {
|
||||
constructor(private readonly reservationsService: ReservationsService) {}
|
||||
|
||||
@Post()
|
||||
create(@Req() req, @Body() createReservationDto: CreateReservationDto) {
|
||||
return this.reservationsService.create(req.user.id, createReservationDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
cancel(@Param('id') id: string) {
|
||||
return this.reservationsService.cancel(id);
|
||||
}
|
||||
|
||||
@Get('my')
|
||||
findAllMy(@Req() req) {
|
||||
return this.reservationsService.findAllByUserId(req.user.id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ReservationsService } from './reservations.service';
|
||||
import { ReservationsController } from './reservations.controller';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { EventsModule } from '../events/events.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, EventsModule],
|
||||
controllers: [ReservationsController],
|
||||
providers: [ReservationsService],
|
||||
exports: [ReservationsService],
|
||||
})
|
||||
export class ReservationsModule {}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { EventsGateway } from '../events/events.gateway';
|
||||
import { CreateReservationDto } from './dto/create-reservation.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ReservationsService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private eventsGateway: EventsGateway,
|
||||
) {}
|
||||
|
||||
async create(userId: string, createReservationDto: CreateReservationDto) {
|
||||
const { spaceId, startTime, endTime } = createReservationDto;
|
||||
|
||||
const space = await this.prisma.space.findUnique({
|
||||
where: { id: spaceId },
|
||||
});
|
||||
|
||||
if (!space) {
|
||||
throw new NotFoundException(`Space with ID ${spaceId} not found`);
|
||||
}
|
||||
|
||||
const reservation = await this.prisma.reservation.create({
|
||||
data: {
|
||||
startTime: new Date(startTime),
|
||||
endTime: new Date(endTime),
|
||||
userId,
|
||||
spaceId,
|
||||
},
|
||||
});
|
||||
|
||||
// Notify clients on the floor
|
||||
this.eventsGateway.emitSpaceStatusChanged(space.floorId, spaceId, true);
|
||||
|
||||
return reservation;
|
||||
}
|
||||
|
||||
async cancel(reservationId: string) {
|
||||
const reservation = await this.prisma.reservation.findUnique({
|
||||
where: { id: reservationId },
|
||||
include: { space: true },
|
||||
});
|
||||
|
||||
if (!reservation) {
|
||||
throw new NotFoundException(`Reservation with ID ${reservationId} not found`);
|
||||
}
|
||||
|
||||
await this.prisma.reservation.delete({
|
||||
where: { id: reservationId },
|
||||
});
|
||||
|
||||
// Notify clients on the floor
|
||||
this.eventsGateway.emitSpaceStatusChanged(
|
||||
reservation.space.floorId,
|
||||
reservation.spaceId,
|
||||
false,
|
||||
);
|
||||
|
||||
return { message: 'Reservation cancelled' };
|
||||
}
|
||||
|
||||
async findAllByUserId(userId: string) {
|
||||
return this.prisma.reservation.findMany({
|
||||
where: { userId },
|
||||
include: { space: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,30 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class SpacesService {
|
||||
private readonly logger = new Logger(SpacesService.name);
|
||||
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async getFloorLayout(floorId: string) {
|
||||
return this.prisma.floor.findUnique({
|
||||
where: { id: floorId },
|
||||
include: {
|
||||
spaces: true,
|
||||
spaces: {
|
||||
include: {
|
||||
reservations: {
|
||||
where: {
|
||||
endTime: {
|
||||
gte: new Date(),
|
||||
},
|
||||
startTime: {
|
||||
lte: new Date(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
building: {
|
||||
include: {
|
||||
organization: true,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"import-meta-resolve": "^4.2.0"
|
||||
"import-meta-resolve": "^4.2.0",
|
||||
"socket.io-client": "^4.8.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000
|
|||
|
||||
export async function apiFetch<T>(
|
||||
path: string,
|
||||
options: RequestInit = {},
|
||||
options: RequestInit & { fetch?: typeof fetch } = {},
|
||||
): Promise<T> {
|
||||
const res = await fetch(`${API_BASE_URL}${path}`, {
|
||||
const fetchFn = options.fetch ?? fetch;
|
||||
const res = await fetchFn(`${API_BASE_URL}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers ?? {}),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
|
||||
export let spaceId: string;
|
||||
export let name: string;
|
||||
export let type: string;
|
||||
export let floorId: string;
|
||||
export let initialStatus: boolean = false; // true if occupied
|
||||
|
||||
let isOccupied = initialStatus;
|
||||
let socket: Socket;
|
||||
|
||||
onMount(() => {
|
||||
// Connect to WebSocket using the environment variable for the backend URL.
|
||||
const socketUrl = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000';
|
||||
socket = io(socketUrl);
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('Connected to WebSocket');
|
||||
socket.emit('join_floor', floorId);
|
||||
});
|
||||
|
||||
socket.on('space_status_changed', (data: { spaceId: string, isOccupied: boolean }) => {
|
||||
if (data.spaceId === spaceId) {
|
||||
isOccupied = data.isOccupied;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Disconnected from WebSocket');
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (socket) {
|
||||
socket.emit('leave_floor', floorId);
|
||||
socket.disconnect();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="p-4 border rounded shadow-md {isOccupied ? 'bg-red-100 border-red-500' : 'bg-green-100 border-green-500'}">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 class="font-bold text-lg">{name}</h3>
|
||||
<p class="text-sm text-gray-600">{type}</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-end">
|
||||
<span class="px-2 py-1 rounded text-xs font-semibold {isOccupied ? 'bg-red-500 text-white' : 'bg-green-500 text-white'}">
|
||||
{isOccupied ? 'Occupied' : 'Available'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import { onMount } from "svelte";
|
||||
import { token } from "$lib/stores/auth";
|
||||
import { goto } from "$app/navigation";
|
||||
import SpaceCard from '$lib/components/SpaceCard.svelte';
|
||||
|
||||
let floor: any = null;
|
||||
let loading = true;
|
||||
|
|
@ -10,18 +11,22 @@
|
|||
onMount(async () => {
|
||||
const floorId = $page.params.id;
|
||||
if (!$token) {
|
||||
goto("/login");
|
||||
goto(`/login?redirect=/dashboard/floor/${floorId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`http://localhost:3000/spaces/floor/${floorId}`, {
|
||||
const apiBase = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000';
|
||||
const res = await fetch(`${apiBase}/spaces/floor/${floorId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${$token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${$token}`,
|
||||
},
|
||||
});
|
||||
if (res.ok) {
|
||||
floor = await res.json();
|
||||
} else if (res.status === 401) {
|
||||
goto("/login");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
@ -42,10 +47,13 @@
|
|||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
{#each floor.spaces as space}
|
||||
<div class="border p-4 rounded bg-gray-50 flex flex-col items-center justify-center text-center">
|
||||
<span class="font-semibold">{space.name}</span>
|
||||
<span class="text-xs text-gray-500 uppercase">{space.type}</span>
|
||||
</div>
|
||||
<SpaceCard
|
||||
spaceId={space.id}
|
||||
name={space.name}
|
||||
type={space.type}
|
||||
floorId={floor.id}
|
||||
initialStatus={space.reservations && space.reservations.length > 0}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import { apiFetch } from '$lib/api/client';
|
||||
|
||||
export const load = async () => {
|
||||
return {};
|
||||
};
|
||||
|
||||
export const ssr = false;
|
||||
|
|
@ -20,6 +20,8 @@
|
|||
"@nestjs/jwt": "^11.0.2",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^10.4.22",
|
||||
"@nestjs/platform-socket.io": "^10.4.22",
|
||||
"@nestjs/websockets": "^10.4.22",
|
||||
"@prisma/client": "^6.4.0",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
|
|
@ -30,7 +32,8 @@
|
|||
"passport-jwt": "^4.0.1",
|
||||
"prisma": "^6.4.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2"
|
||||
"rxjs": "^7.8.2",
|
||||
"socket.io": "^4.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
|
|
@ -61,7 +64,8 @@
|
|||
"apps/web": {
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"import-meta-resolve": "^4.2.0"
|
||||
"import-meta-resolve": "^4.2.0",
|
||||
"socket.io-client": "^4.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.3.1",
|
||||
|
|
@ -2498,6 +2502,57 @@
|
|||
"@nestjs/core": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-socket.io": {
|
||||
"version": "10.4.22",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.22.tgz",
|
||||
"integrity": "sha512-xxGw3R0Ihr51/Omq23z3//bKmCXyVKaikxbH0/pkwqMsQrxkUv9NabNUZ22b4Jnlwwi02X+zlwo8GRa9u8oV9g==",
|
||||
"dependencies": {
|
||||
"socket.io": "4.8.1",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/nest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/websockets": "^10.0.0",
|
||||
"rxjs": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-socket.io/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-socket.io/node_modules/socket.io": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.6.0",
|
||||
"socket.io-adapter": "~2.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/schematics": {
|
||||
"version": "10.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz",
|
||||
|
|
@ -2547,6 +2602,28 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/websockets": {
|
||||
"version": "10.4.22",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.22.tgz",
|
||||
"integrity": "sha512-OLd4i0Faq7vgdtB5vVUrJ54hWEtcXy9poJ6n7kbbh/5ms+KffUl+wwGsbe7uSXLrkoyI8xXU6fZPkFArI+XiRg==",
|
||||
"dependencies": {
|
||||
"iterare": "1.2.1",
|
||||
"object-hash": "3.0.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/platform-socket.io": "^10.0.0",
|
||||
"reflect-metadata": "^0.1.12 || ^0.2.0",
|
||||
"rxjs": "^7.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nestjs/platform-socket.io": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
|
|
@ -3069,6 +3146,11 @@
|
|||
"@sinonjs/commons": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
|
|
@ -3309,6 +3391,14 @@
|
|||
"integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||
|
|
@ -3527,6 +3617,14 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
|
||||
"integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA=="
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||
|
|
@ -4642,6 +4740,14 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"node_modules/base64id": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||
"engines": {
|
||||
"node": "^4.5.0 || >= 5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.11",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.11.tgz",
|
||||
|
|
@ -5757,6 +5863,54 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.6.6",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.6.tgz",
|
||||
"integrity": "sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==",
|
||||
"dependencies": {
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/node": ">=10.0.0",
|
||||
"@types/ws": "^8.5.12",
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "2.0.0",
|
||||
"cookie": "~0.7.2",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.4.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.18.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.4",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz",
|
||||
"integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.4.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.18.3",
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.20.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
|
||||
|
|
@ -8860,7 +9014,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
|
|
@ -10198,6 +10351,58 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.8.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
|
||||
"integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.4.1",
|
||||
"engine.io": "~6.6.0",
|
||||
"socket.io-adapter": "~2.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz",
|
||||
"integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==",
|
||||
"dependencies": {
|
||||
"debug": "~4.4.1",
|
||||
"ws": "~8.18.3"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.8.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
|
||||
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.4.1",
|
||||
"engine.io-client": "~6.6.1",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.6",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
|
||||
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sorcery": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.1.tgz",
|
||||
|
|
@ -11803,6 +12008,34 @@
|
|||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
|
|
|||
Loading…
Reference in New Issue