diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 3e171cf..9bc9b58 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -6,6 +6,7 @@ import { ScheduleModule } from '@nestjs/schedule'; import { ThrottlerModule } from '@nestjs/throttler'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { LocationsModule } from './locations/locations.module'; @Module({ imports: [ @@ -33,6 +34,7 @@ import { AppService } from './app.service'; }), inject: [ConfigService], }), + LocationsModule, ], controllers: [AppController], providers: [AppService], diff --git a/backend/src/locations/dto/create-location.dto.ts b/backend/src/locations/dto/create-location.dto.ts new file mode 100644 index 0000000..f7f0ad9 --- /dev/null +++ b/backend/src/locations/dto/create-location.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { LocationType } from '../location.entity'; + +export class CreateLocationDto { + @ApiProperty({ example: 'Lagos HQ' }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty({ enum: LocationType }) + @IsEnum(LocationType) + type: LocationType; + + @ApiProperty({ required: false, example: '1 Nelson Mandela St, Lagos' }) + @IsOptional() + @IsString() + address?: string; + + @ApiProperty({ required: false, example: 'Main office for the company' }) + @IsOptional() + @IsString() + description?: string; +} diff --git a/backend/src/locations/dto/update-location.dto.ts b/backend/src/locations/dto/update-location.dto.ts new file mode 100644 index 0000000..27875b2 --- /dev/null +++ b/backend/src/locations/dto/update-location.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateLocationDto } from './create-location.dto'; + +export class UpdateLocationDto extends PartialType(CreateLocationDto) {} diff --git a/backend/src/locations/location.entity.ts b/backend/src/locations/location.entity.ts new file mode 100644 index 0000000..c6fd2e5 --- /dev/null +++ b/backend/src/locations/location.entity.ts @@ -0,0 +1,38 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +export enum LocationType { + OFFICE = 'office', + WAREHOUSE = 'warehouse', + BRANCH = 'branch', + REMOTE = 'remote', +} + +@Entity('locations') +export class Location { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true }) + name: string; + + @Column({ type: 'enum', enum: LocationType }) + type: LocationType; + + @Column({ nullable: true }) + address?: string; + + @Column({ nullable: true, type: 'text' }) + description?: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/backend/src/locations/locations.controller.ts b/backend/src/locations/locations.controller.ts new file mode 100644 index 0000000..066cf93 --- /dev/null +++ b/backend/src/locations/locations.controller.ts @@ -0,0 +1,56 @@ +import { + Controller, + Get, + Post, + Patch, + Delete, + Param, + Body, + UseGuards, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { LocationsService } from './locations.service'; +import { CreateLocationDto } from './dto/create-location.dto'; +import { UpdateLocationDto } from './dto/update-location.dto'; + +@ApiTags('Locations') +@ApiBearerAuth('JWT-auth') +@UseGuards(JwtAuthGuard) +@Controller('locations') +export class LocationsController { + constructor(private readonly locationsService: LocationsService) {} + + @Get() + @ApiOperation({ summary: 'List all tracked locations' }) + findAll() { + return this.locationsService.findAll(); + } + + @Get(':id') + @ApiOperation({ summary: 'Get a location by ID' }) + findOne(@Param('id') id: string) { + return this.locationsService.findOne(id); + } + + @Post() + @ApiOperation({ summary: 'Create a new location' }) + create(@Body() dto: CreateLocationDto) { + return this.locationsService.create(dto); + } + + @Patch(':id') + @ApiOperation({ summary: 'Update a location' }) + update(@Param('id') id: string, @Body() dto: UpdateLocationDto) { + return this.locationsService.update(id, dto); + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ summary: 'Delete a location' }) + remove(@Param('id') id: string) { + return this.locationsService.remove(id); + } +} diff --git a/backend/src/locations/locations.module.ts b/backend/src/locations/locations.module.ts new file mode 100644 index 0000000..e9f07f0 --- /dev/null +++ b/backend/src/locations/locations.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Location } from './location.entity'; +import { LocationsService } from './locations.service'; +import { LocationsController } from './locations.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Location])], + providers: [LocationsService], + controllers: [LocationsController], + exports: [LocationsService], +}) +export class LocationsModule {} diff --git a/backend/src/locations/locations.service.ts b/backend/src/locations/locations.service.ts new file mode 100644 index 0000000..4b42ac9 --- /dev/null +++ b/backend/src/locations/locations.service.ts @@ -0,0 +1,57 @@ +import { + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Location } from './location.entity'; +import { CreateLocationDto } from './dto/create-location.dto'; +import { UpdateLocationDto } from './dto/update-location.dto'; + +@Injectable() +export class LocationsService { + constructor( + @InjectRepository(Location) + private readonly repo: Repository, + ) {} + + findAll(): Promise { + return this.repo.find({ order: { name: 'ASC' } }); + } + + async findOne(id: string): Promise { + const location = await this.repo.findOne({ where: { id } }); + if (!location) { + throw new NotFoundException('Location not found'); + } + return location; + } + + async create(dto: CreateLocationDto): Promise { + await this.ensureNameUnique(dto.name); + return this.repo.save(this.repo.create(dto)); + } + + async update(id: string, dto: UpdateLocationDto): Promise { + const location = await this.findOne(id); + if (dto.name && dto.name !== location.name) { + await this.ensureNameUnique(dto.name); + } + + Object.assign(location, dto); + return this.repo.save(location); + } + + async remove(id: string): Promise { + const location = await this.findOne(id); + await this.repo.remove(location); + } + + private async ensureNameUnique(name: string): Promise { + const existing = await this.repo.findOne({ where: { name } }); + if (existing) { + throw new ConflictException('A location with this name already exists'); + } + } +}