Bladeren bron

new site component and its CRUD and services

Dr-Swopt 4 dagen geleden
bovenliggende
commit
bb37e94e4a

+ 6 - 2
src/FFB/ffb-production.module.ts

@@ -1,7 +1,9 @@
-import { Module } from '@nestjs/common';
+import { Module, forwardRef } from '@nestjs/common';
 import { FFBProductionController } from './ffb-production.controller';
 import { FFBProductionService } from './services/ffb-production.service';
 import { MongoModule } from 'src/mongo/mongo.module';
+import { SiteModule } from '../site/site.module';
+
 import { FFBVectorService } from './services/ffb-vector.service';
 import { GeminiEmbeddingService } from './gemini-embedding.service';
 import { FFBLangChainService } from './services/ffb-langchain.service';
@@ -9,8 +11,10 @@ import { FFBGateway } from './ffb.gateway';
 
 @Module({
   imports: [
-    MongoModule
+    MongoModule,
+    forwardRef(() => SiteModule),
   ],
+
   controllers: [FFBProductionController],
   providers: [
     FFBProductionService,

+ 14 - 5
src/FFB/ffb-production.schema.ts

@@ -1,14 +1,23 @@
 export interface FFBProduction {
   _id?: string;
   productionDate: Date;
-  site: string;
-  phase: string;
-  block: string;
+  site: {
+    id: string;
+    name: string;
+  };
+  phase: {
+    id: string;
+    name: string;
+  };
+  block: {
+    id: string;
+    name: string;
+  };
   weight: number;
   weightUom: string;
   quantity: number;
   quantityUom: string;
-  remarks?: string;
+  remarks: string;
 }
 
 export interface ThoughtPayload {
@@ -25,4 +34,4 @@ export interface ThoughtPayload {
   input?: any;         // The raw data going IN
   output?: any;        // The data coming OUT (replacing 'result', 'results', 'pipeline')
   metadata?: Record<string, any>; // For extra stuff like resultsCount or contextLength
-}
+}

+ 5 - 0
src/FFB/mongo-ffb-production.repository.ts

@@ -52,7 +52,12 @@ export class FFBProductionRepository {
     return this.collection.deleteOne({ _id: new ObjectId(id) as any });
   }
 
+  async deleteMany(filter: Record<string, any>) {
+    return this.collection.deleteMany(filter);
+  }
+
   async update(id: string, update: Partial<FFBProduction>): Promise<void> {
+
     await this.collection.updateOne(
       { _id: new ObjectId(id) as any },
       { $set: update }

+ 53 - 2
src/FFB/services/ffb-production.service.ts

@@ -1,9 +1,12 @@
-import { Injectable } from '@nestjs/common';
+import { Injectable, BadRequestException, Inject, forwardRef } from '@nestjs/common';
 import { MongoCoreService } from '../../mongo/mongo-core.service';
 import { FFBProduction } from '../ffb-production.schema';
 import { FFBProductionRepository } from 'src/FFB/mongo-ffb-production.repository';
 import { FFBVectorService } from './ffb-vector.service';
 import { FFBLangChainService } from './ffb-langchain.service';
+import { SiteService } from '../../site/services/site.service';
+import { PhaseService } from '../../site/services/phase.service';
+import { BlockService } from '../../site/services/block.service';
 
 @Injectable()
 export class FFBProductionService {
@@ -11,10 +14,17 @@ export class FFBProductionService {
 
   constructor(
     private readonly mongoCore: MongoCoreService,
-    private readonly vectorService: FFBVectorService, // Inject vector service
+    private readonly vectorService: FFBVectorService,
     private readonly ffbLangChainService: FFBLangChainService,
+    @Inject(forwardRef(() => SiteService))
+    private readonly siteService: SiteService,
+    @Inject(forwardRef(() => PhaseService))
+    private readonly phaseService: PhaseService,
+    @Inject(forwardRef(() => BlockService))
+    private readonly blockService: BlockService,
   ) { }
 
+
   /** Lazily get or create the repository */
   private async getRepository(): Promise<FFBProductionRepository> {
     if (!this.repo) {
@@ -27,10 +37,44 @@ export class FFBProductionService {
 
   /** Create a new record with embedding */
   async create(record: FFBProduction) {
+    // 1. Validate Site
+    const site = await this.siteService.findById(record.site.id);
+    if (!site) {
+      throw new BadRequestException(`Site with ID ${record.site.id} does not exist.`);
+    }
+    if (site.name !== record.site.name) {
+      throw new BadRequestException(`Site name mismatch: expected ${site.name}, got ${record.site.name}.`);
+    }
+
+    // 2. Validate Phase
+    const phase = await this.phaseService.findById(record.phase.id);
+    if (!phase) {
+      throw new BadRequestException(`Phase with ID ${record.phase.id} does not exist.`);
+    }
+    if (phase.name !== record.phase.name) {
+      throw new BadRequestException(`Phase name mismatch: expected ${phase.name}, got ${record.phase.name}.`);
+    }
+    if (phase.siteId !== record.site.id) {
+      throw new BadRequestException(`Phase with ID ${record.phase.id} does not belong to Site ${record.site.id}.`);
+    }
+
+    // 3. Validate Block
+    const block = await this.blockService.findById(record.block.id);
+    if (!block) {
+      throw new BadRequestException(`Block with ID ${record.block.id} does not exist.`);
+    }
+    if (block.name !== record.block.name) {
+      throw new BadRequestException(`Block name mismatch: expected ${block.name}, got ${record.block.name}.`);
+    }
+    if (block.phaseId !== record.phase.id) {
+      throw new BadRequestException(`Block with ID ${record.block.id} does not belong to Phase ${record.phase.id}.`);
+    }
+
     // Use vector service to insert with embedding
     return this.vectorService.insertWithVector(record);
   }
 
+
   /** Find all records (no embedding required) */
   async findAll(filter: Record<string, any> = {}, options: { page?: number, limit?: number } = {}) {
     const repo = await this.getRepository();
@@ -49,6 +93,13 @@ export class FFBProductionService {
     return repo.delete(id);
   }
 
+  /** Delete many records by filter */
+  async deleteMany(filter: Record<string, any>) {
+    const repo = await this.getRepository();
+    return repo.deleteMany(filter);
+  }
+
+
   /** Search for top-k similar records using a text query */
   async search(query: string, k = 5) {
     return this.vectorService.vectorSearch(query, k);

+ 5 - 1
src/app.module.ts

@@ -10,8 +10,10 @@ import { ActivityModule } from './activity/activity.module';
 import { MongoModule } from './mongo/mongo.module';
 import { FFBProductionModule } from './FFB/ffb-production.module';
 import { FaceModule } from './face/face.module';
+import { SiteModule } from './site/site.module';
 
 @Module({
+
   imports: [
     MongooseModule.forRootAsync({
       useFactory: () => mongooseConfig,
@@ -22,8 +24,10 @@ import { FaceModule } from './face/face.module';
     PlantationTreeModule,
     ServiceModule,
     ActivityModule,
-    FaceModule
+    FaceModule,
+    SiteModule
   ],
+
   controllers: [AppController],
   providers: [AppService],
 })

+ 36 - 0
src/site/controllers/block.controller.ts

@@ -0,0 +1,36 @@
+import { Controller, Get, Post, Body, Param, Put, Delete, Query } from '@nestjs/common';
+import { BlockService } from '../services/block.service';
+import { Block } from '../schemas/site.schema';
+
+@Controller('blocks')
+export class BlockController {
+    constructor(private readonly blockService: BlockService) { }
+
+    @Post()
+    async create(@Body() block: Block) {
+        return this.blockService.create(block);
+    }
+
+    @Get()
+    async findAll(@Query('phaseId') phaseId?: string) {
+        const filter = phaseId ? { phaseId } : {};
+        return this.blockService.findAll(filter);
+    }
+
+    @Get(':id')
+    async findById(@Param('id') id: string) {
+        return this.blockService.findById(id);
+    }
+
+    @Put(':id')
+    async update(@Param('id') id: string, @Body() update: Partial<Block>) {
+        await this.blockService.update(id, update);
+        return { message: 'Block updated successfully' };
+    }
+
+    @Delete(':id')
+    async delete(@Param('id') id: string) {
+        await this.blockService.delete(id);
+        return { message: 'Block and related FFB data deleted successfully' };
+    }
+}

+ 36 - 0
src/site/controllers/phase.controller.ts

@@ -0,0 +1,36 @@
+import { Controller, Get, Post, Body, Param, Put, Delete, Query } from '@nestjs/common';
+import { PhaseService } from '../services/phase.service';
+import { Phase } from '../schemas/site.schema';
+
+@Controller('phases')
+export class PhaseController {
+    constructor(private readonly phaseService: PhaseService) { }
+
+    @Post()
+    async create(@Body() phase: Phase) {
+        return this.phaseService.create(phase);
+    }
+
+    @Get()
+    async findAll(@Query('siteId') siteId?: string) {
+        const filter = siteId ? { siteId } : {};
+        return this.phaseService.findAll(filter);
+    }
+
+    @Get(':id')
+    async findById(@Param('id') id: string, @Query('populate') populate: string) {
+        return this.phaseService.findById(id, populate === 'true');
+    }
+
+    @Put(':id')
+    async update(@Param('id') id: string, @Body() update: Partial<Phase>) {
+        await this.phaseService.update(id, update);
+        return { message: 'Phase updated successfully' };
+    }
+
+    @Delete(':id')
+    async delete(@Param('id') id: string) {
+        await this.phaseService.delete(id);
+        return { message: 'Phase and all related blocks/FFB data deleted successfully' };
+    }
+}

+ 35 - 0
src/site/controllers/site.controller.ts

@@ -0,0 +1,35 @@
+import { Controller, Get, Post, Body, Param, Put, Delete, Query } from '@nestjs/common';
+import { SiteService } from '../services/site.service';
+import { Site } from '../schemas/site.schema';
+
+@Controller('sites')
+export class SiteController {
+    constructor(private readonly siteService: SiteService) { }
+
+    @Post()
+    async create(@Body() site: Site) {
+        return this.siteService.create(site);
+    }
+
+    @Get()
+    async findAll() {
+        return this.siteService.findAll();
+    }
+
+    @Get(':id')
+    async findById(@Param('id') id: string, @Query('populate') populate: string) {
+        return this.siteService.findById(id, populate === 'true');
+    }
+
+    @Put(':id')
+    async update(@Param('id') id: string, @Body() update: Partial<Site>) {
+        await this.siteService.update(id, update);
+        return { message: 'Site updated successfully' };
+    }
+
+    @Delete(':id')
+    async delete(@Param('id') id: string) {
+        await this.siteService.delete(id);
+        return { message: 'Site and all related data deleted successfully' };
+    }
+}

+ 50 - 0
src/site/repositories/block.repository.ts

@@ -0,0 +1,50 @@
+import { Db, ObjectId, WithId } from 'mongodb';
+import { Block } from '../schemas/site.schema';
+
+export class BlockRepository {
+    private readonly collectionName = 'Block';
+
+    constructor(private readonly db: Db) { }
+
+    private get collection() {
+        return this.db.collection<Block>(this.collectionName);
+    }
+
+    async init() {
+        const collections = await this.db.listCollections({ name: this.collectionName }).toArray();
+        if (collections.length === 0) {
+            await this.db.createCollection(this.collectionName);
+            console.log(`✅ Created collection: ${this.collectionName}`);
+        }
+    }
+
+    async create(block: Block): Promise<Block> {
+        const result = await this.collection.insertOne(block as any);
+        return { ...block, _id: result.insertedId.toString() };
+    }
+
+    async findAll(filter: Record<string, any> = {}): Promise<Block[]> {
+        const results = await this.collection.find(filter).toArray();
+        return results.map(r => ({ ...r, _id: r._id.toString() } as Block));
+    }
+
+    async findById(id: string): Promise<Block | null> {
+        const result = await this.collection.findOne({ _id: new ObjectId(id) as any });
+        return result ? ({ ...result, _id: result._id.toString() } as Block) : null;
+    }
+
+    async update(id: string, update: Partial<Block>): Promise<void> {
+        await this.collection.updateOne(
+            { _id: new ObjectId(id) as any },
+            { $set: update }
+        );
+    }
+
+    async delete(id: string): Promise<void> {
+        await this.collection.deleteOne({ _id: new ObjectId(id) as any });
+    }
+
+    async deleteMany(filter: Record<string, any>): Promise<void> {
+        await this.collection.deleteMany(filter);
+    }
+}

+ 50 - 0
src/site/repositories/phase.repository.ts

@@ -0,0 +1,50 @@
+import { Db, ObjectId, WithId } from 'mongodb';
+import { Phase } from '../schemas/site.schema';
+
+export class PhaseRepository {
+    private readonly collectionName = 'Phase';
+
+    constructor(private readonly db: Db) { }
+
+    private get collection() {
+        return this.db.collection<Phase>(this.collectionName);
+    }
+
+    async init() {
+        const collections = await this.db.listCollections({ name: this.collectionName }).toArray();
+        if (collections.length === 0) {
+            await this.db.createCollection(this.collectionName);
+            console.log(`✅ Created collection: ${this.collectionName}`);
+        }
+    }
+
+    async create(phase: Phase): Promise<Phase> {
+        const result = await this.collection.insertOne(phase as any);
+        return { ...phase, _id: result.insertedId.toString() };
+    }
+
+    async findAll(filter: Record<string, any> = {}): Promise<Phase[]> {
+        const results = await this.collection.find(filter).toArray();
+        return results.map(r => ({ ...r, _id: r._id.toString() } as Phase));
+    }
+
+    async findById(id: string): Promise<Phase | null> {
+        const result = await this.collection.findOne({ _id: new ObjectId(id) as any });
+        return result ? ({ ...result, _id: result._id.toString() } as Phase) : null;
+    }
+
+    async update(id: string, update: Partial<Phase>): Promise<void> {
+        await this.collection.updateOne(
+            { _id: new ObjectId(id) as any },
+            { $set: update }
+        );
+    }
+
+    async delete(id: string): Promise<void> {
+        await this.collection.deleteOne({ _id: new ObjectId(id) as any });
+    }
+
+    async deleteMany(filter: Record<string, any>): Promise<void> {
+        await this.collection.deleteMany(filter);
+    }
+}

+ 58 - 0
src/site/repositories/site.repository.ts

@@ -0,0 +1,58 @@
+import { Db, ObjectId, WithId } from 'mongodb';
+import { Site } from '../schemas/site.schema';
+
+export class SiteRepository {
+    private readonly collectionName = 'Site';
+
+    constructor(private readonly db: Db) { }
+
+    private get collection() {
+        return this.db.collection<Site>(this.collectionName);
+    }
+
+    async init() {
+        const collections = await this.db.listCollections({ name: this.collectionName }).toArray();
+        if (collections.length === 0) {
+            await this.db.createCollection(this.collectionName);
+            console.log(`✅ Created collection: ${this.collectionName}`);
+        }
+    }
+
+    async create(site: Site): Promise<Site> {
+        const doc = {
+            ...site,
+            metadata: {
+                createdAt: new Date(),
+                updatedAt: new Date(),
+            },
+        };
+        const result = await this.collection.insertOne(doc as any);
+        return { ...doc, _id: result.insertedId.toString() };
+    }
+
+    async findAll(filter: Record<string, any> = {}): Promise<Site[]> {
+        const results = await this.collection.find(filter).toArray();
+        return results.map(r => ({ ...r, _id: r._id.toString() } as Site));
+    }
+
+    async findById(id: string): Promise<Site | null> {
+        const result = await this.collection.findOne({ _id: new ObjectId(id) as any });
+        return result ? ({ ...result, _id: result._id.toString() } as Site) : null;
+    }
+
+    async update(id: string, update: Partial<Site>): Promise<void> {
+        await this.collection.updateOne(
+            { _id: new ObjectId(id) as any },
+            {
+                $set: {
+                    ...update,
+                    'metadata.updatedAt': new Date()
+                }
+            }
+        );
+    }
+
+    async delete(id: string): Promise<void> {
+        await this.collection.deleteOne({ _id: new ObjectId(id) as any });
+    }
+}

+ 32 - 0
src/site/schemas/site.schema.ts

@@ -0,0 +1,32 @@
+export interface Block {
+    _id?: string;
+    phaseId: string; // Reference
+    name: string;
+    description?: string;
+    numOfTrees: number;
+    size: number;
+    sizeUom: 'sqft' | 'sqm' | 'acres';
+}
+
+export interface Phase {
+    _id?: string;
+    siteId: string; // Reference
+    name: string;
+    description?: string;
+    blocks?: Block[]; // Optional for populated view
+    status: string;
+}
+
+export interface Site {
+    _id?: string;
+    name: string;
+    address: string;
+    coordinates?: { lat: number; lng: number };
+    status: 'active' | 'inactive';
+    description: string;
+    phases?: Phase[]; // Optional for populated view
+    metadata: {
+        createdAt: Date;
+        updatedAt: Date;
+    };
+}

+ 77 - 0
src/site/services/block.service.ts

@@ -0,0 +1,77 @@
+import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
+import { MongoCoreService } from '../../mongo/mongo-core.service';
+import { BlockRepository } from '../repositories/block.repository';
+import { Block } from '../schemas/site.schema';
+import { PhaseRepository } from '../repositories/phase.repository';
+import { FFBProductionService } from '../../FFB/services/ffb-production.service';
+
+@Injectable()
+export class BlockService {
+    private repo: BlockRepository;
+    private phaseRepo: PhaseRepository;
+
+    constructor(
+        private readonly mongoCore: MongoCoreService,
+        private readonly ffbService: FFBProductionService,
+    ) { }
+
+    private async getRepos() {
+        if (!this.repo) {
+            const db = await this.mongoCore.getDb();
+            this.repo = new BlockRepository(db);
+            this.phaseRepo = new PhaseRepository(db);
+            await this.repo.init();
+            await this.phaseRepo.init();
+        }
+        return { repo: this.repo, phaseRepo: this.phaseRepo };
+    }
+
+    async create(block: Block): Promise<Block> {
+        const { repo, phaseRepo } = await this.getRepos();
+
+        // Parent Validation: Check if Phase exists
+        const phase = await phaseRepo.findById(block.phaseId);
+        if (!phase) {
+            throw new BadRequestException(`Phase with ID ${block.phaseId} does not exist.`);
+        }
+
+        return repo.create(block);
+    }
+
+    async findAll(filter: Record<string, any> = {}): Promise<Block[]> {
+        const { repo } = await this.getRepos();
+        return repo.findAll(filter);
+    }
+
+    async findById(id: string): Promise<Block | null> {
+        const { repo } = await this.getRepos();
+        return repo.findById(id);
+    }
+
+    async update(id: string, update: Partial<Block>): Promise<void> {
+        const { repo, phaseRepo } = await this.getRepos();
+
+        const block = await repo.findById(id);
+        if (!block) throw new NotFoundException('Block not found');
+
+        if (update.phaseId) {
+            const phase = await phaseRepo.findById(update.phaseId);
+            if (!phase) throw new BadRequestException(`Phase with ID ${update.phaseId} does not exist.`);
+        }
+
+        await repo.update(id, update);
+    }
+
+    async delete(id: string): Promise<void> {
+        const { repo } = await this.getRepos();
+        const block = await repo.findById(id);
+        if (!block) throw new NotFoundException('Block not found');
+
+        // Cascading Delete:
+        // 1. Delete all FFB Production records referencing this block
+        await this.ffbService.deleteMany({ 'block.id': id });
+
+        // 2. Delete the block itself
+        await repo.delete(id);
+    }
+}

+ 90 - 0
src/site/services/phase.service.ts

@@ -0,0 +1,90 @@
+import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
+import { MongoCoreService } from '../../mongo/mongo-core.service';
+import { PhaseRepository } from '../repositories/phase.repository';
+import { Phase } from '../schemas/site.schema';
+import { SiteRepository } from '../repositories/site.repository';
+import { BlockRepository } from '../repositories/block.repository';
+import { FFBProductionService } from '../../FFB/services/ffb-production.service';
+
+@Injectable()
+export class PhaseService {
+    private repo: PhaseRepository;
+    private siteRepo: SiteRepository;
+    private blockRepo: BlockRepository;
+
+    constructor(
+        private readonly mongoCore: MongoCoreService,
+        private readonly ffbService: FFBProductionService,
+    ) { }
+
+    private async getRepos() {
+        if (!this.repo) {
+            const db = await this.mongoCore.getDb();
+            this.repo = new PhaseRepository(db);
+            this.siteRepo = new SiteRepository(db);
+            this.blockRepo = new BlockRepository(db);
+            await this.repo.init();
+            await this.siteRepo.init();
+            await this.blockRepo.init();
+        }
+        return { repo: this.repo, siteRepo: this.siteRepo, blockRepo: this.blockRepo };
+    }
+
+    async create(phase: Phase): Promise<Phase> {
+        const { repo, siteRepo } = await this.getRepos();
+
+        // Parent Validation: Check if Site exists
+        const site = await siteRepo.findById(phase.siteId);
+        if (!site) {
+            throw new BadRequestException(`Site with ID ${phase.siteId} does not exist.`);
+        }
+
+        return repo.create(phase);
+    }
+
+    async findAll(filter: Record<string, any> = {}): Promise<Phase[]> {
+        const { repo } = await this.getRepos();
+        return repo.findAll(filter);
+    }
+
+    async findById(id: string, populate = false): Promise<Phase | null> {
+        const { repo, blockRepo } = await this.getRepos();
+        const phase = await repo.findById(id);
+        if (!phase) return null;
+
+        if (populate) {
+            phase.blocks = await blockRepo.findAll({ phaseId: id });
+        }
+        return phase;
+    }
+
+    async update(id: string, update: Partial<Phase>): Promise<void> {
+        const { repo, siteRepo } = await this.getRepos();
+
+        const phase = await repo.findById(id);
+        if (!phase) throw new NotFoundException('Phase not found');
+
+        if (update.siteId) {
+            const site = await siteRepo.findById(update.siteId);
+            if (!site) throw new BadRequestException(`Site with ID ${update.siteId} does not exist.`);
+        }
+
+        await repo.update(id, update);
+    }
+
+    async delete(id: string): Promise<void> {
+        const { repo, blockRepo } = await this.getRepos();
+        const phase = await repo.findById(id);
+        if (!phase) throw new NotFoundException('Phase not found');
+
+        // Cascading Delete:
+        // 1. Delete all blocks of this phase
+        await blockRepo.deleteMany({ phaseId: id });
+
+        // 2. Delete all FFB Production records referencing this phase
+        await this.ffbService.deleteMany({ 'phase.id': id });
+
+        // 3. Delete the phase itself
+        await repo.delete(id);
+    }
+}

+ 86 - 0
src/site/services/site.service.ts

@@ -0,0 +1,86 @@
+import { Injectable, NotFoundException } from '@nestjs/common';
+import { MongoCoreService } from '../../mongo/mongo-core.service';
+import { SiteRepository } from '../repositories/site.repository';
+import { Site } from '../schemas/site.schema';
+import { PhaseRepository } from '../repositories/phase.repository';
+import { BlockRepository } from '../repositories/block.repository';
+import { FFBProductionService } from '../../FFB/services/ffb-production.service';
+
+@Injectable()
+export class SiteService {
+    private repo: SiteRepository;
+    private phaseRepo: PhaseRepository;
+    private blockRepo: BlockRepository;
+
+    constructor(
+        private readonly mongoCore: MongoCoreService,
+        private readonly ffbService: FFBProductionService,
+    ) { }
+
+    private async getRepos() {
+        if (!this.repo) {
+            const db = await this.mongoCore.getDb();
+            this.repo = new SiteRepository(db);
+            this.phaseRepo = new PhaseRepository(db);
+            this.blockRepo = new BlockRepository(db);
+            await this.repo.init();
+            await this.phaseRepo.init();
+            await this.blockRepo.init();
+        }
+        return { repo: this.repo, phaseRepo: this.phaseRepo, blockRepo: this.blockRepo };
+    }
+
+    async create(site: Site): Promise<Site> {
+        const { repo } = await this.getRepos();
+        return repo.create(site);
+    }
+
+    async findAll(): Promise<Site[]> {
+        const { repo } = await this.getRepos();
+        return repo.findAll();
+    }
+
+    async findById(id: string, populate = false): Promise<Site | null> {
+        const { repo, phaseRepo, blockRepo } = await this.getRepos();
+        const site = await repo.findById(id);
+        if (!site) return null;
+
+        if (populate) {
+            const phases = await phaseRepo.findAll({ siteId: id });
+            for (const phase of phases) {
+                phase.blocks = await blockRepo.findAll({ phaseId: phase._id });
+            }
+            site.phases = phases;
+        }
+        return site;
+    }
+
+    async update(id: string, update: Partial<Site>): Promise<void> {
+        const { repo } = await this.getRepos();
+        const site = await repo.findById(id);
+        if (!site) throw new NotFoundException('Site not found');
+        await repo.update(id, update);
+    }
+
+    async delete(id: string): Promise<void> {
+        const { repo, phaseRepo, blockRepo } = await this.getRepos();
+        const site = await repo.findById(id);
+        if (!site) throw new NotFoundException('Site not found');
+
+        // Cascading Delete:
+        // 1. Find all phases
+        const phases = await phaseRepo.findAll({ siteId: id });
+        for (const phase of phases) {
+            // 2. Delete all blocks of each phase
+            await blockRepo.deleteMany({ phaseId: phase._id });
+        }
+        // 3. Delete all phases of the site
+        await phaseRepo.deleteMany({ siteId: id });
+
+        // 4. Delete all FFB Production records referencing this site
+        await this.ffbService.deleteMany({ 'site.id': id });
+
+        // 5. Delete the site itself
+        await repo.delete(id);
+    }
+}

+ 32 - 0
src/site/site.module.ts

@@ -0,0 +1,32 @@
+import { Module, forwardRef } from '@nestjs/common';
+import { SiteController } from './controllers/site.controller';
+import { PhaseController } from './controllers/phase.controller';
+import { BlockController } from './controllers/block.controller';
+import { SiteService } from './services/site.service';
+import { PhaseService } from './services/phase.service';
+import { BlockService } from './services/block.service';
+import { MongoModule } from '../mongo/mongo.module';
+import { FFBProductionModule } from '../FFB/ffb-production.module';
+
+@Module({
+    imports: [
+        MongoModule,
+        forwardRef(() => FFBProductionModule),
+    ],
+    controllers: [
+        SiteController,
+        PhaseController,
+        BlockController,
+    ],
+    providers: [
+        SiteService,
+        PhaseService,
+        BlockService,
+    ],
+    exports: [
+        SiteService,
+        PhaseService,
+        BlockService,
+    ],
+})
+export class SiteModule { }