Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -114,6 +117,9 @@ import { MembershipPlansModule } from './membership-plans/membership-plans.modul
WaitlistModule,
EventsModule,
MembershipPlansModule,
LockersModule,
PackagesModule,
ReferralsModule,
],
controllers: [AppController],
providers: [
Expand Down
6 changes: 6 additions & 0 deletions backend/src/lockers/dto/assign-locker.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IsUUID } from 'class-validator';

export class AssignLockerDto {
@IsUUID()
userId: string;
}
19 changes: 19 additions & 0 deletions backend/src/lockers/dto/create-locker.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
20 changes: 20 additions & 0 deletions backend/src/lockers/dto/update-locker.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
52 changes: 52 additions & 0 deletions backend/src/lockers/entities/locker.entity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 5 additions & 0 deletions backend/src/lockers/enums/locker-size.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum LockerSize {
SMALL = 'SMALL',
MEDIUM = 'MEDIUM',
LARGE = 'LARGE',
}
88 changes: 88 additions & 0 deletions backend/src/lockers/lockers.controller.ts
Original file line number Diff line number Diff line change
@@ -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' };
}
}
13 changes: 13 additions & 0 deletions backend/src/lockers/lockers.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
60 changes: 60 additions & 0 deletions backend/src/lockers/lockers.service.ts
Original file line number Diff line number Diff line change
@@ -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<Locker>,
) {}

async create(dto: CreateLockerDto): Promise<Locker> {
return this.repo.save(this.repo.create(dto));
}

async findAll(): Promise<Locker[]> {
return this.repo.find({ relations: ['assignedTo'], order: { lockerNumber: 'ASC' } });
}

async findMine(userId: string): Promise<Locker | null> {
return this.repo.findOne({ where: { assignedToUserId: userId }, relations: ['assignedTo'] });
}

async findOne(id: string): Promise<Locker> {
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<Locker> {
const item = await this.findOne(id);
Object.assign(item, dto);
return this.repo.save(item);
}

async assign(id: string, dto: AssignLockerDto): Promise<Locker> {
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<Locker> {
const item = await this.findOne(id);
item.assignedToUserId = null;
item.assignedAt = null;
return this.repo.save(item);
}

async softDelete(id: string): Promise<void> {
const item = await this.findOne(id);
if (item.assignedToUserId) throw new BadRequestException('Cannot delete an assigned locker');
await this.repo.softDelete(id);
}
}
22 changes: 22 additions & 0 deletions backend/src/packages/dto/create-package.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
51 changes: 51 additions & 0 deletions backend/src/packages/entities/package.entity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 5 additions & 0 deletions backend/src/packages/enums/package-status.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum PackageStatus {
ARRIVED = 'ARRIVED',
COLLECTED = 'COLLECTED',
RETURNED = 'RETURNED',
}
Loading
Loading