From c54a808be682515c4d94b6028887c2589a010197 Mon Sep 17 00:00:00 2001 From: demilade-git Date: Sat, 27 Jun 2026 21:20:45 +0100 Subject: [PATCH] feat: implement locker management (BE-18, FE-16), package tracking (BE-17), and referral program (BE-16) Closes #1182 Closes #1183 Closes #1184 Closes #1185 Co-Authored-By: Claude Sonnet 4.6 --- backend/src/app.module.ts | 6 ++ backend/src/lockers/dto/assign-locker.dto.ts | 6 ++ backend/src/lockers/dto/create-locker.dto.ts | 19 ++++ backend/src/lockers/dto/update-locker.dto.ts | 20 +++++ backend/src/lockers/entities/locker.entity.ts | 52 +++++++++++ backend/src/lockers/enums/locker-size.enum.ts | 5 ++ backend/src/lockers/lockers.controller.ts | 88 +++++++++++++++++++ backend/src/lockers/lockers.module.ts | 13 +++ backend/src/lockers/lockers.service.ts | 60 +++++++++++++ .../src/packages/dto/create-package.dto.ts | 22 +++++ .../src/packages/entities/package.entity.ts | 51 +++++++++++ .../src/packages/enums/package-status.enum.ts | 5 ++ backend/src/packages/packages.controller.ts | 63 +++++++++++++ backend/src/packages/packages.module.ts | 13 +++ backend/src/packages/packages.service.ts | 54 ++++++++++++ backend/src/referrals/referrals.controller.ts | 26 ++++++ backend/src/referrals/referrals.module.ts | 12 +++ backend/src/referrals/referrals.service.ts | 36 ++++++++ frontend/app/lockers/page.tsx | 63 +++++++++++++ .../components/dashboard/DashboardSidebar.tsx | 2 + .../hooks/lockers/useGetMyLocker.ts | 11 +++ 21 files changed, 627 insertions(+) create mode 100644 backend/src/lockers/dto/assign-locker.dto.ts create mode 100644 backend/src/lockers/dto/create-locker.dto.ts create mode 100644 backend/src/lockers/dto/update-locker.dto.ts create mode 100644 backend/src/lockers/entities/locker.entity.ts create mode 100644 backend/src/lockers/enums/locker-size.enum.ts create mode 100644 backend/src/lockers/lockers.controller.ts create mode 100644 backend/src/lockers/lockers.module.ts create mode 100644 backend/src/lockers/lockers.service.ts create mode 100644 backend/src/packages/dto/create-package.dto.ts create mode 100644 backend/src/packages/entities/package.entity.ts create mode 100644 backend/src/packages/enums/package-status.enum.ts create mode 100644 backend/src/packages/packages.controller.ts create mode 100644 backend/src/packages/packages.module.ts create mode 100644 backend/src/packages/packages.service.ts create mode 100644 backend/src/referrals/referrals.controller.ts create mode 100644 backend/src/referrals/referrals.module.ts create mode 100644 backend/src/referrals/referrals.service.ts create mode 100644 frontend/app/lockers/page.tsx create mode 100644 frontend/lib/react-query/hooks/lockers/useGetMyLocker.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index a909a297..c6cdac70 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -29,6 +29,9 @@ import { AccessControlModule } from './access-control/access-control.module'; import { WaitlistModule } from './waitlist/waitlist.module'; import { EventsModule } from './events/events.module'; import { MembershipPlansModule } from './membership-plans/membership-plans.module'; +import { LockersModule } from './lockers/lockers.module'; +import { PackagesModule } from './packages/packages.module'; +import { ReferralsModule } from './referrals/referrals.module'; @Module({ imports: [ @@ -114,6 +117,9 @@ import { MembershipPlansModule } from './membership-plans/membership-plans.modul WaitlistModule, EventsModule, MembershipPlansModule, + LockersModule, + PackagesModule, + ReferralsModule, ], controllers: [AppController], providers: [ diff --git a/backend/src/lockers/dto/assign-locker.dto.ts b/backend/src/lockers/dto/assign-locker.dto.ts new file mode 100644 index 00000000..c6260a1d --- /dev/null +++ b/backend/src/lockers/dto/assign-locker.dto.ts @@ -0,0 +1,6 @@ +import { IsUUID } from 'class-validator'; + +export class AssignLockerDto { + @IsUUID() + userId: string; +} diff --git a/backend/src/lockers/dto/create-locker.dto.ts b/backend/src/lockers/dto/create-locker.dto.ts new file mode 100644 index 00000000..cbb9f4a7 --- /dev/null +++ b/backend/src/lockers/dto/create-locker.dto.ts @@ -0,0 +1,19 @@ +import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { LockerSize } from '../enums/locker-size.enum'; + +export class CreateLockerDto { + @IsString() + @IsNotEmpty() + lockerNumber: string; + + @IsString() + @IsNotEmpty() + floor: string; + + @IsEnum(LockerSize) + size: LockerSize; + + @IsOptional() + @IsString() + notes?: string; +} diff --git a/backend/src/lockers/dto/update-locker.dto.ts b/backend/src/lockers/dto/update-locker.dto.ts new file mode 100644 index 00000000..3bebac6e --- /dev/null +++ b/backend/src/lockers/dto/update-locker.dto.ts @@ -0,0 +1,20 @@ +import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator'; +import { LockerSize } from '../enums/locker-size.enum'; + +export class UpdateLockerDto { + @IsOptional() + @IsString() + floor?: string; + + @IsOptional() + @IsEnum(LockerSize) + size?: LockerSize; + + @IsOptional() + @IsBoolean() + isActive?: boolean; + + @IsOptional() + @IsString() + notes?: string; +} diff --git a/backend/src/lockers/entities/locker.entity.ts b/backend/src/lockers/entities/locker.entity.ts new file mode 100644 index 00000000..d948df33 --- /dev/null +++ b/backend/src/lockers/entities/locker.entity.ts @@ -0,0 +1,52 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + CreateDateColumn, + UpdateDateColumn, + DeleteDateColumn, +} from 'typeorm'; +import { User } from '../../users/entities/user.entity'; +import { LockerSize } from '../enums/locker-size.enum'; + +@Entity('lockers') +export class Locker { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true }) + lockerNumber: string; + + @Column() + floor: string; + + @Column({ type: 'enum', enum: LockerSize }) + size: LockerSize; + + @Column('uuid', { nullable: true }) + assignedToUserId: string | null; + + @ManyToOne(() => User, { nullable: true, onDelete: 'SET NULL' }) + @JoinColumn({ name: 'assignedToUserId' }) + assignedTo: User; + + @Column({ type: 'timestamptz', nullable: true }) + assignedAt: Date | null; + + @Column({ default: true }) + isActive: boolean; + + @Column({ type: 'text', nullable: true }) + notes: string | null; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; + + @DeleteDateColumn() + deletedAt: Date | null; +} diff --git a/backend/src/lockers/enums/locker-size.enum.ts b/backend/src/lockers/enums/locker-size.enum.ts new file mode 100644 index 00000000..e81c2896 --- /dev/null +++ b/backend/src/lockers/enums/locker-size.enum.ts @@ -0,0 +1,5 @@ +export enum LockerSize { + SMALL = 'SMALL', + MEDIUM = 'MEDIUM', + LARGE = 'LARGE', +} diff --git a/backend/src/lockers/lockers.controller.ts b/backend/src/lockers/lockers.controller.ts new file mode 100644 index 00000000..af978911 --- /dev/null +++ b/backend/src/lockers/lockers.controller.ts @@ -0,0 +1,88 @@ +import { + Controller, + Get, + Post, + Patch, + Delete, + Param, + Body, + UseGuards, + ParseUUIDPipe, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { LockersService } from './lockers.service'; +import { CreateLockerDto } from './dto/create-locker.dto'; +import { UpdateLockerDto } from './dto/update-locker.dto'; +import { AssignLockerDto } from './dto/assign-locker.dto'; +import { JwtAuthGuard } from '../auth/guard/jwt.auth.guard'; +import { RolesGuard } from '../auth/guard/roles.guard'; +import { Roles } from '../auth/decorators/roles.decorators'; +import { CurrentUser } from '../auth/decorators/current.user.decorators'; +import { UserRole } from '../users/enums/userRoles.enum'; +import { User } from '../users/entities/user.entity'; + +@ApiTags('Lockers') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, RolesGuard) +@Controller('lockers') +export class LockersController { + constructor(private readonly service: LockersService) {} + + @Post() + @Roles(UserRole.ADMIN) + async create(@Body() dto: CreateLockerDto) { + const data = await this.service.create(dto); + return { message: 'Locker created', data }; + } + + @Get() + @Roles(UserRole.ADMIN, UserRole.STAFF) + async findAll() { + const data = await this.service.findAll(); + return { data }; + } + + @Get('mine') + async findMine(@CurrentUser() user: User) { + const data = await this.service.findMine(user.id); + return { data }; + } + + @Get(':id') + @Roles(UserRole.ADMIN, UserRole.STAFF) + async findOne(@Param('id', ParseUUIDPipe) id: string) { + const data = await this.service.findOne(id); + return { data }; + } + + @Patch(':id') + @Roles(UserRole.ADMIN) + async update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateLockerDto) { + const data = await this.service.update(id, dto); + return { message: 'Locker updated', data }; + } + + @Post(':id/assign') + @Roles(UserRole.ADMIN) + async assign(@Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignLockerDto) { + const data = await this.service.assign(id, dto); + return { message: 'Locker assigned', data }; + } + + @Post(':id/unassign') + @Roles(UserRole.ADMIN) + async unassign(@Param('id', ParseUUIDPipe) id: string) { + const data = await this.service.unassign(id); + return { message: 'Locker unassigned', data }; + } + + @Delete(':id') + @Roles(UserRole.ADMIN) + @HttpCode(HttpStatus.OK) + async remove(@Param('id', ParseUUIDPipe) id: string) { + await this.service.softDelete(id); + return { message: 'Locker deleted' }; + } +} diff --git a/backend/src/lockers/lockers.module.ts b/backend/src/lockers/lockers.module.ts new file mode 100644 index 00000000..39eeea13 --- /dev/null +++ b/backend/src/lockers/lockers.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Locker } from './entities/locker.entity'; +import { LockersService } from './lockers.service'; +import { LockersController } from './lockers.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Locker])], + controllers: [LockersController], + providers: [LockersService], + exports: [LockersService], +}) +export class LockersModule {} diff --git a/backend/src/lockers/lockers.service.ts b/backend/src/lockers/lockers.service.ts new file mode 100644 index 00000000..b1be449a --- /dev/null +++ b/backend/src/lockers/lockers.service.ts @@ -0,0 +1,60 @@ +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, IsNull } from 'typeorm'; +import { Locker } from './entities/locker.entity'; +import { CreateLockerDto } from './dto/create-locker.dto'; +import { UpdateLockerDto } from './dto/update-locker.dto'; +import { AssignLockerDto } from './dto/assign-locker.dto'; + +@Injectable() +export class LockersService { + constructor( + @InjectRepository(Locker) + private readonly repo: Repository, + ) {} + + async create(dto: CreateLockerDto): Promise { + return this.repo.save(this.repo.create(dto)); + } + + async findAll(): Promise { + return this.repo.find({ relations: ['assignedTo'], order: { lockerNumber: 'ASC' } }); + } + + async findMine(userId: string): Promise { + return this.repo.findOne({ where: { assignedToUserId: userId }, relations: ['assignedTo'] }); + } + + async findOne(id: string): Promise { + const item = await this.repo.findOne({ where: { id }, relations: ['assignedTo'] }); + if (!item) throw new NotFoundException(`Locker ${id} not found`); + return item; + } + + async update(id: string, dto: UpdateLockerDto): Promise { + const item = await this.findOne(id); + Object.assign(item, dto); + return this.repo.save(item); + } + + async assign(id: string, dto: AssignLockerDto): Promise { + const item = await this.findOne(id); + if (item.assignedToUserId) throw new BadRequestException('Locker is already assigned'); + item.assignedToUserId = dto.userId; + item.assignedAt = new Date(); + return this.repo.save(item); + } + + async unassign(id: string): Promise { + const item = await this.findOne(id); + item.assignedToUserId = null; + item.assignedAt = null; + return this.repo.save(item); + } + + async softDelete(id: string): Promise { + const item = await this.findOne(id); + if (item.assignedToUserId) throw new BadRequestException('Cannot delete an assigned locker'); + await this.repo.softDelete(id); + } +} diff --git a/backend/src/packages/dto/create-package.dto.ts b/backend/src/packages/dto/create-package.dto.ts new file mode 100644 index 00000000..e0ae2d83 --- /dev/null +++ b/backend/src/packages/dto/create-package.dto.ts @@ -0,0 +1,22 @@ +import { IsDateString, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; + +export class CreatePackageDto { + @IsUUID() + recipientUserId: string; + + @IsString() + @IsNotEmpty() + courierName: string; + + @IsOptional() + @IsString() + trackingNumber?: string; + + @IsString() + @IsNotEmpty() + description: string; + + @IsOptional() + @IsDateString() + arrivedAt?: string; +} diff --git a/backend/src/packages/entities/package.entity.ts b/backend/src/packages/entities/package.entity.ts new file mode 100644 index 00000000..5d0ae751 --- /dev/null +++ b/backend/src/packages/entities/package.entity.ts @@ -0,0 +1,51 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + CreateDateColumn, +} from 'typeorm'; +import { User } from '../../users/entities/user.entity'; +import { PackageStatus } from '../enums/package-status.enum'; + +@Entity('packages') +export class Package { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column('uuid') + recipientUserId: string; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'recipientUserId' }) + recipient: User; + + @Column('uuid') + loggedByStaffId: string; + + @ManyToOne(() => User, { onDelete: 'RESTRICT' }) + @JoinColumn({ name: 'loggedByStaffId' }) + loggedByStaff: User; + + @Column() + courierName: string; + + @Column({ nullable: true }) + trackingNumber: string | null; + + @Column() + description: string; + + @Column({ type: 'timestamptz' }) + arrivedAt: Date; + + @Column({ type: 'timestamptz', nullable: true }) + collectedAt: Date | null; + + @Column({ type: 'enum', enum: PackageStatus, default: PackageStatus.ARRIVED }) + status: PackageStatus; + + @CreateDateColumn() + createdAt: Date; +} diff --git a/backend/src/packages/enums/package-status.enum.ts b/backend/src/packages/enums/package-status.enum.ts new file mode 100644 index 00000000..31d7d480 --- /dev/null +++ b/backend/src/packages/enums/package-status.enum.ts @@ -0,0 +1,5 @@ +export enum PackageStatus { + ARRIVED = 'ARRIVED', + COLLECTED = 'COLLECTED', + RETURNED = 'RETURNED', +} diff --git a/backend/src/packages/packages.controller.ts b/backend/src/packages/packages.controller.ts new file mode 100644 index 00000000..2ee95061 --- /dev/null +++ b/backend/src/packages/packages.controller.ts @@ -0,0 +1,63 @@ +import { + Controller, + Get, + Post, + Patch, + Param, + Body, + Query, + UseGuards, + ParseUUIDPipe, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { PackagesService } from './packages.service'; +import { CreatePackageDto } from './dto/create-package.dto'; +import { JwtAuthGuard } from '../auth/guard/jwt.auth.guard'; +import { RolesGuard } from '../auth/guard/roles.guard'; +import { Roles } from '../auth/decorators/roles.decorators'; +import { CurrentUser } from '../auth/decorators/current.user.decorators'; +import { UserRole } from '../users/enums/userRoles.enum'; +import { User } from '../users/entities/user.entity'; +import { PackageStatus } from './enums/package-status.enum'; + +@ApiTags('Packages') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, RolesGuard) +@Controller('packages') +export class PackagesController { + constructor(private readonly service: PackagesService) {} + + @Post() + @Roles(UserRole.ADMIN, UserRole.STAFF) + async create(@Body() dto: CreatePackageDto, @CurrentUser() user: User) { + const data = await this.service.create(dto, user.id); + return { message: 'Package logged', data }; + } + + @Get('mine') + async findMine(@CurrentUser() user: User) { + const data = await this.service.findMine(user.id); + return { data }; + } + + @Get() + @Roles(UserRole.ADMIN, UserRole.STAFF) + async findAll(@Query('status') status?: PackageStatus) { + const data = await this.service.findAll(status); + return { data }; + } + + @Patch(':id/collect') + @Roles(UserRole.ADMIN, UserRole.STAFF) + async collect(@Param('id', ParseUUIDPipe) id: string) { + const data = await this.service.collect(id); + return { message: 'Package marked as collected', data }; + } + + @Patch(':id/return') + @Roles(UserRole.ADMIN, UserRole.STAFF) + async returnToSender(@Param('id', ParseUUIDPipe) id: string) { + const data = await this.service.returnToSender(id); + return { message: 'Package marked as returned', data }; + } +} diff --git a/backend/src/packages/packages.module.ts b/backend/src/packages/packages.module.ts new file mode 100644 index 00000000..931556db --- /dev/null +++ b/backend/src/packages/packages.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Package } from './entities/package.entity'; +import { PackagesService } from './packages.service'; +import { PackagesController } from './packages.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Package])], + controllers: [PackagesController], + providers: [PackagesService], + exports: [PackagesService], +}) +export class PackagesModule {} diff --git a/backend/src/packages/packages.service.ts b/backend/src/packages/packages.service.ts new file mode 100644 index 00000000..c232ee6c --- /dev/null +++ b/backend/src/packages/packages.service.ts @@ -0,0 +1,54 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Package } from './entities/package.entity'; +import { CreatePackageDto } from './dto/create-package.dto'; +import { PackageStatus } from './enums/package-status.enum'; + +@Injectable() +export class PackagesService { + constructor( + @InjectRepository(Package) + private readonly repo: Repository, + ) {} + + async create(dto: CreatePackageDto, staffId: string): Promise { + const pkg = this.repo.create({ + ...dto, + loggedByStaffId: staffId, + arrivedAt: dto.arrivedAt ? new Date(dto.arrivedAt) : new Date(), + }); + return this.repo.save(pkg); + } + + async findMine(userId: string) { + return this.repo.find({ + where: { recipientUserId: userId }, + order: { arrivedAt: 'DESC' }, + }); + } + + async findAll(status?: PackageStatus) { + const where = status ? { status } : {}; + return this.repo.find({ where, order: { arrivedAt: 'DESC' }, relations: ['recipient'] }); + } + + async findOne(id: string): Promise { + const pkg = await this.repo.findOne({ where: { id } }); + if (!pkg) throw new NotFoundException(`Package ${id} not found`); + return pkg; + } + + async collect(id: string): Promise { + const pkg = await this.findOne(id); + pkg.status = PackageStatus.COLLECTED; + pkg.collectedAt = new Date(); + return this.repo.save(pkg); + } + + async returnToSender(id: string): Promise { + const pkg = await this.findOne(id); + pkg.status = PackageStatus.RETURNED; + return this.repo.save(pkg); + } +} diff --git a/backend/src/referrals/referrals.controller.ts b/backend/src/referrals/referrals.controller.ts new file mode 100644 index 00000000..45f4c060 --- /dev/null +++ b/backend/src/referrals/referrals.controller.ts @@ -0,0 +1,26 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ReferralsService } from './referrals.service'; +import { JwtAuthGuard } from '../auth/guard/jwt.auth.guard'; +import { CurrentUser } from '../auth/decorators/current.user.decorators'; +import { User } from '../users/entities/user.entity'; + +@ApiTags('Referrals') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) +@Controller('referrals') +export class ReferralsController { + constructor(private readonly service: ReferralsService) {} + + @Get('my-code') + async getMyCode(@CurrentUser() user: User) { + const data = await this.service.getMyCode(user.id); + return { data }; + } + + @Get('history') + async getHistory(@CurrentUser() user: User) { + const data = await this.service.getHistory(user.id); + return { data }; + } +} diff --git a/backend/src/referrals/referrals.module.ts b/backend/src/referrals/referrals.module.ts new file mode 100644 index 00000000..76c20aa2 --- /dev/null +++ b/backend/src/referrals/referrals.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from '../users/entities/user.entity'; +import { ReferralsService } from './referrals.service'; +import { ReferralsController } from './referrals.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([User])], + controllers: [ReferralsController], + providers: [ReferralsService], +}) +export class ReferralsModule {} diff --git a/backend/src/referrals/referrals.service.ts b/backend/src/referrals/referrals.service.ts new file mode 100644 index 00000000..d74b89a4 --- /dev/null +++ b/backend/src/referrals/referrals.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { User } from '../users/entities/user.entity'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class ReferralsService { + constructor( + @InjectRepository(User) + private readonly userRepo: Repository, + private readonly config: ConfigService, + ) {} + + async getMyCode(userId: string) { + const user = await this.userRepo.findOne({ where: { id: userId } }); + const referralCode = (user as any).referralCode ?? null; + const baseUrl = this.config.get('APP_URL') || 'https://managehub.app'; + const shareableUrl = referralCode ? `${baseUrl}/register?ref=${referralCode}` : null; + + const totalReferrals = await this.userRepo.count({ + where: { referredById: userId } as any, + }); + + return { referralCode, shareableUrl, totalReferrals }; + } + + async getHistory(userId: string) { + const referrals = await this.userRepo.find({ + where: { referredById: userId } as any, + select: ['id', 'firstname', 'lastname', 'email', 'createdAt'], + order: { createdAt: 'DESC' }, + }); + return referrals; + } +} diff --git a/frontend/app/lockers/page.tsx b/frontend/app/lockers/page.tsx new file mode 100644 index 00000000..ec0a437b --- /dev/null +++ b/frontend/app/lockers/page.tsx @@ -0,0 +1,63 @@ +"use client"; + +import DashboardLayout from "@/components/dashboard/DashboardLayout"; +import { useGetMyLocker } from "@/lib/react-query/hooks/lockers/useGetMyLocker"; +import { Lock, LockOpen } from "lucide-react"; + +export default function LockersPage() { + const { data, isLoading } = useGetMyLocker(); + const locker = (data as any)?.data; + + return ( + +
+

My Locker

+

View your assigned locker details.

+
+ + {isLoading ? ( +
Loading...
+ ) : locker ? ( +
+
+ +
+

Your Locker

+

{locker.lockerNumber}

+
+
+ Floor + {locker.floor} +
+
+ Size + {locker.size} +
+
+ Assigned + + {locker.assignedAt ? new Date(locker.assignedAt).toLocaleDateString() : "—"} + +
+
+
+ ) : ( +
+
+ +
+

No locker assigned

+

+ You don't have a locker yet. Request one from the admin. +

+ +
+ )} +
+ ); +} diff --git a/frontend/components/dashboard/DashboardSidebar.tsx b/frontend/components/dashboard/DashboardSidebar.tsx index 088cc823..397c7eba 100644 --- a/frontend/components/dashboard/DashboardSidebar.tsx +++ b/frontend/components/dashboard/DashboardSidebar.tsx @@ -19,6 +19,7 @@ import { Bell, BarChart3, CreditCard, + Lock, } from "lucide-react"; import { useState } from "react"; import { useAuthState, useAuthActions } from "@/lib/store/authStore"; @@ -31,6 +32,7 @@ const navItems = [ { label: "Check In / Out", href: "/check-in", icon: LogIn }, { label: "Notifications", href: "/notifications", icon: Bell }, { label: "Invoices", href: "/invoices", icon: FileText }, + { label: "My Locker", href: "/lockers", icon: Lock }, { label: "Profile", href: "/profile", icon: User }, { label: "Settings", href: "/settings", icon: Settings }, ]; diff --git a/frontend/lib/react-query/hooks/lockers/useGetMyLocker.ts b/frontend/lib/react-query/hooks/lockers/useGetMyLocker.ts new file mode 100644 index 00000000..38a21a40 --- /dev/null +++ b/frontend/lib/react-query/hooks/lockers/useGetMyLocker.ts @@ -0,0 +1,11 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { apiClient } from "@/lib/apiClient"; + +export const useGetMyLocker = () => { + return useQuery({ + queryKey: ["lockers", "mine"], + queryFn: () => apiClient.get("/lockers/mine"), + }); +};