Browse Source

added harvest components

Dr-Swopt 1 month ago
parent
commit
8d54143582

+ 3 - 0
analyze.prompt

@@ -0,0 +1,3 @@
+You are a software expert. Help me to understand the code. Please refer to the extension.ts file below.
+
+{{include "src/main.ts"}}

+ 42 - 0
package-lock.json

@@ -19,6 +19,8 @@
         "@nestjs/websockets": "^11.1.3",
         "@simplewebauthn/server": "^13.1.1",
         "bcrypt": "^6.0.0",
+        "class-transformer": "^0.5.1",
+        "class-validator": "^0.14.2",
         "dotenv": "^17.2.3",
         "express-list-endpoints": "^7.1.1",
         "express-session": "^1.18.1",
@@ -3826,6 +3828,12 @@
         "@types/superagent": "^8.1.0"
       }
     },
+    "node_modules/@types/validator": {
+      "version": "13.15.3",
+      "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz",
+      "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==",
+      "license": "MIT"
+    },
     "node_modules/@types/webidl-conversions": {
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
@@ -5993,6 +6001,25 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/class-transformer": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
+      "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/class-validator": {
+      "version": "0.14.2",
+      "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz",
+      "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@types/validator": "^13.11.8",
+        "libphonenumber-js": "^1.11.1",
+        "validator": "^13.9.0"
+      }
+    },
     "node_modules/cli-cursor": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -9388,6 +9415,12 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/libphonenumber-js": {
+      "version": "1.12.24",
+      "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.24.tgz",
+      "integrity": "sha512-l5IlyL9AONj4voSd7q9xkuQOL4u8Ty44puTic7J88CmdXkxfGsRfoVLXHCxppwehgpb/Chdb80FFehHqjN3ItQ==",
+      "license": "MIT"
+    },
     "node_modules/lines-and-columns": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -12687,6 +12720,15 @@
         "node": ">=10.12.0"
       }
     },
+    "node_modules/validator": {
+      "version": "13.15.15",
+      "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
+      "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
     "node_modules/vary": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

+ 2 - 0
package.json

@@ -30,6 +30,8 @@
     "@nestjs/websockets": "^11.1.3",
     "@simplewebauthn/server": "^13.1.1",
     "bcrypt": "^6.0.0",
+    "class-transformer": "^0.5.1",
+    "class-validator": "^0.14.2",
     "dotenv": "^17.2.3",
     "express-list-endpoints": "^7.1.1",
     "express-session": "^1.18.1",

+ 5 - 1
src/app.module.ts

@@ -7,6 +7,8 @@ import { PlantationTreeModule } from './plantation/plantation-tree.module';
 import { AppService } from './app.service';
 import { MongooseModule } from '@nestjs/mongoose';
 import { mongooseConfig } from './config/mongoose.config';
+import { ServiceModule } from './services/service.module';
+import { HarvestModule } from './harvest/harvest.module';
 
 @Module({
   imports: [
@@ -16,7 +18,9 @@ import { mongooseConfig } from './config/mongoose.config';
     AuthModule,
     AttendanceModule,
     PaymentModule,
-    PlantationTreeModule
+    PlantationTreeModule,
+    ServiceModule,
+    HarvestModule
   ],
   controllers: [AppController],
   providers: [AppService],

+ 49 - 0
src/harvest/harvest.controller.ts

@@ -0,0 +1,49 @@
+// src/harvest/harvest.controller.ts
+import { Controller, Get, Post, Body, Param, Put, Delete, Query } from '@nestjs/common';
+import { HarvestService } from './harvest.service';
+import { CreateHarvestDto, UpdateHarvestDto } from './harvest.dto';
+
+@Controller('harvest')
+export class HarvestController {
+  constructor(private readonly harvestService: HarvestService) {}
+
+  @Post()
+  async create(@Body() dto: CreateHarvestDto) {
+    return this.harvestService.create(dto);
+  }
+
+  @Get()
+  async findAll(@Query() query: Record<string, any>) {
+    return this.harvestService.findAll(query);
+  }
+
+  @Get(':id')
+  async findOne(@Param('id') id: string) {
+    return this.harvestService.findById(id);
+  }
+
+  @Put(':id')
+  async update(@Param('id') id: string, @Body() dto: UpdateHarvestDto) {
+    return this.harvestService.update(id, dto);
+  }
+
+  @Delete(':id')
+  async remove(@Param('id') id: string) {
+    return this.harvestService.delete(id);
+  }
+
+  @Get('stats/resources')
+  async totalResources() {
+    return this.harvestService.getTotalResourceHours();
+  }
+
+  @Get('stats/outputs')
+  async totalOutputs() {
+    return this.harvestService.getTotalOutputWeight();
+  }
+
+  @Get('date-range')
+  async byDateRange(@Query('start') start: string, @Query('end') end: string) {
+    return this.harvestService.findByDateRange(new Date(start), new Date(end));
+  }
+}

+ 173 - 0
src/harvest/harvest.dto.ts

@@ -0,0 +1,173 @@
+// src/harvest/harvest.dto.ts
+import {
+  IsString,
+  IsNotEmpty,
+  IsArray,
+  ValidateNested,
+  IsNumber,
+  IsOptional,
+  IsDateString,
+} from 'class-validator';
+import { Type } from 'class-transformer';
+
+/* ------------------------------
+ * Shared Sub-schemas
+ * ------------------------------ */
+
+class QuantityValueDto {
+  @IsNumber()
+  quantity: number;
+
+  @IsString()
+  uom: string;
+}
+
+class ResourceDto {
+  @IsString()
+  type: string;
+
+  @IsString()
+  name: string;
+
+  @ValidateNested()
+  @Type(() => QuantityValueDto)
+  value: QuantityValueDto;
+
+  @IsOptional()
+  @IsString()
+  id?: string;
+}
+
+class OutputDto {
+  @IsString()
+  type: string;
+
+  @IsString()
+  name: string;
+
+  @ValidateNested()
+  @Type(() => QuantityValueDto)
+  value: QuantityValueDto;
+
+  @IsOptional()
+  @IsString()
+  id?: string;
+
+  @ValidateNested()
+  @Type(() => WeightValueDto)
+  weightValue: WeightValueDto;
+}
+
+class WeightValueDto {
+  @IsNumber()
+  weight: number;
+
+  @IsString()
+  uom: string;
+}
+
+class TargetDto {
+  @IsString()
+  type: string;
+
+  @IsString()
+  name: string;
+
+  @ValidateNested()
+  @Type(() => QuantityValueDto)
+  value: QuantityValueDto;
+
+  @IsOptional()
+  @IsString()
+  id?: string;
+}
+
+/* ------------------------------
+ * Main Harvest DTO
+ * ------------------------------ */
+
+export class CreateHarvestDto {
+  @IsString()
+  @IsNotEmpty()
+  name: string;
+
+  @IsString()
+  @IsNotEmpty()
+  type: string;
+
+  @IsArray()
+  @ValidateNested({ each: true })
+  @Type(() => ResourceDto)
+  resources: ResourceDto[];
+
+  @ValidateNested()
+  @Type(() => DurationDto)
+  duration: DurationDto;
+
+  @IsArray()
+  @ValidateNested({ each: true })
+  @Type(() => OutputDto)
+  outputs: OutputDto[];
+
+  @IsArray()
+  @ValidateNested({ each: true })
+  @Type(() => TargetDto)
+  targets: TargetDto[];
+
+  @IsDateString()
+  dateStart: string;
+
+  @IsDateString()
+  dateEnd: string;
+}
+
+class DurationDto {
+  @ValidateNested()
+  @Type(() => QuantityValueDto)
+  value: QuantityValueDto;
+}
+
+/* ------------------------------
+ * Update DTO (partial)
+ * ------------------------------ */
+
+export class UpdateHarvestDto {
+  @IsOptional()
+  @IsString()
+  name?: string;
+
+  @IsOptional()
+  @IsString()
+  type?: string;
+
+  @IsOptional()
+  @IsArray()
+  @ValidateNested({ each: true })
+  @Type(() => ResourceDto)
+  resources?: ResourceDto[];
+
+  @IsOptional()
+  @ValidateNested()
+  @Type(() => DurationDto)
+  duration?: DurationDto;
+
+  @IsOptional()
+  @IsArray()
+  @ValidateNested({ each: true })
+  @Type(() => OutputDto)
+  outputs?: OutputDto[];
+
+  @IsOptional()
+  @IsArray()
+  @ValidateNested({ each: true })
+  @Type(() => TargetDto)
+  targets?: TargetDto[];
+
+  @IsOptional()
+  @IsDateString()
+  dateStart?: string;
+
+  @IsOptional()
+  @IsDateString()
+  dateEnd?: string;
+}

+ 13 - 0
src/harvest/harvest.module.ts

@@ -0,0 +1,13 @@
+// src/harvest/harvest.module.ts
+import { Module } from '@nestjs/common';
+import { HarvestController } from './harvest.controller';
+import { HarvestService } from './harvest.service';
+import { ServiceModule } from 'src/services/service.module';
+
+@Module({
+  imports: [ServiceModule],
+  controllers: [HarvestController],
+  providers: [HarvestService],
+  exports: [HarvestService],
+})
+export class HarvestModule {}

+ 48 - 0
src/harvest/harvest.schema.ts

@@ -0,0 +1,48 @@
+// src/harvest/harvest.schema.ts
+import { ObjectId } from 'mongodb';
+
+export interface QuantityValue {
+  quantity: number;
+  uom: string;
+}
+
+export interface Resource {
+  type: string;
+  name: string;
+  value: QuantityValue;
+  id: ObjectId;
+}
+
+export interface Output {
+  type: string;
+  name: string;
+  value: QuantityValue;
+  id: ObjectId;
+  weightValue: {
+    weight: number;
+    uom: string;
+  };
+}
+
+export interface Target {
+  type: string;
+  name: string;
+  value: QuantityValue;
+  id: ObjectId;
+}
+
+export interface Duration {
+  value: QuantityValue;
+}
+
+export interface HarvestActivity {
+  _id?: ObjectId;
+  name: string;
+  type: string;
+  resources: Resource[];
+  duration: Duration;
+  outputs: Output[];
+  targets: Target[];
+  dateStart: Date;
+  dateEnd: Date;
+}

+ 101 - 0
src/harvest/harvest.service.ts

@@ -0,0 +1,101 @@
+// src/harvest/harvest.service.ts
+import { Injectable } from '@nestjs/common';
+import { HarvestActivity } from './harvest.schema';
+import { CreateHarvestDto, UpdateHarvestDto } from './harvest.dto';
+import { ObjectId } from 'mongodb';
+import { MongoService } from 'src/services/mongo.service';
+
+@Injectable()
+export class HarvestService {
+  private readonly collectionName = 'activities';
+
+  constructor(private readonly mongo: MongoService) {}
+
+  /* ------------------------------
+   * Utility: Convert DTO to Schema
+   * ------------------------------ */
+  private dtoToHarvestActivity(dto: CreateHarvestDto): HarvestActivity {
+    return {
+      name: dto.name,
+      type: dto.type,
+      resources: dto.resources.map((r) => ({
+        ...r,
+        id: r.id ? new ObjectId(r.id) : new ObjectId(),
+      })),
+      duration: dto.duration,
+      outputs: dto.outputs.map((o) => ({
+        ...o,
+        id: o.id ? new ObjectId(o.id) : new ObjectId(),
+      })),
+      targets: dto.targets.map((t) => ({
+        ...t,
+        id: t.id ? new ObjectId(t.id) : new ObjectId(),
+      })),
+      dateStart: new Date(dto.dateStart),
+      dateEnd: new Date(dto.dateEnd),
+    };
+  }
+
+  /* ------------------------------
+   * CRUD OPERATIONS
+   * ------------------------------ */
+  async create(dto: CreateHarvestDto) {
+    const activity = this.dtoToHarvestActivity(dto);
+    return this.mongo.createActivity(activity);
+  }
+
+  async findAll(filter: Record<string, any> = {}) {
+    return this.mongo.getAllActivities(filter);
+  }
+
+  async findById(id: string) {
+    return this.mongo.getActivityById(id);
+  }
+
+  async update(id: string, dto: UpdateHarvestDto) {
+    // For partial updates, we only convert provided IDs
+    const update: any = { ...dto };
+    if (dto.resources) {
+      update.resources = dto.resources.map((r) => ({
+        ...r,
+        id: r.id ? new ObjectId(r.id) : new ObjectId(),
+      }));
+    }
+    if (dto.outputs) {
+      update.outputs = dto.outputs.map((o) => ({
+        ...o,
+        id: o.id ? new ObjectId(o.id) : new ObjectId(),
+      }));
+    }
+    if (dto.targets) {
+      update.targets = dto.targets.map((t) => ({
+        ...t,
+        id: t.id ? new ObjectId(t.id) : new ObjectId(),
+      }));
+    }
+    if (dto.dateStart) update.dateStart = new Date(dto.dateStart);
+    if (dto.dateEnd) update.dateEnd = new Date(dto.dateEnd);
+
+    return this.mongo.updateActivity(id, update);
+  }
+
+  async delete(id: string) {
+    return this.mongo.deleteActivity(id);
+  }
+
+  /* ------------------------------
+   * Aggregations / Queries
+   * ------------------------------ */
+
+  async getTotalResourceHours() {
+    return this.mongo.getTotalResourceHours();
+  }
+
+  async getTotalOutputWeight() {
+    return this.mongo.getTotalOutputWeight();
+  }
+
+  async findByDateRange(start: Date, end: Date) {
+    return this.mongo.getActivitiesByDateRange(start, end);
+  }
+}

+ 13 - 2
src/main.ts

@@ -5,12 +5,15 @@ import { join } from 'path';
 import * as fs from 'fs';
 import { NestExpressApplication } from '@nestjs/platform-express';
 import session from 'express-session';
+import { ValidationPipe } from '@nestjs/common';
 
 async function bootstrap() {
   const certsDir = join(__dirname, '..', 'certs');
   const httpsOptions = {
-    key: fs.readFileSync(join(certsDir, 'local-key.pem')),
-    cert: fs.readFileSync(join(certsDir, 'local-cert.pem')),
+    key: fs.readFileSync(join(certsDir, 'localhost+4-key.pem')),
+    cert: fs.readFileSync(join(certsDir, 'localhost+4.pem')),
+    // key: fs.readFileSync(join(certsDir, 'local-key.pem')),
+    // cert: fs.readFileSync(join(certsDir, 'local-cert.pem')),
   };
 
   // const app = await NestFactory.create<NestExpressApplication>(AppModule);
@@ -18,6 +21,14 @@ async function bootstrap() {
     httpsOptions, // <-- Let Nest bind HTTPS
   });
 
+  app.useGlobalPipes(
+    new ValidationPipe({
+      whitelist: true, // strips extra fields
+      forbidNonWhitelisted: true, // rejects unknown fields
+      transform: true, // converts types (e.g. string -> number/date)
+    }),
+  );
+
   app.enableCors();
   app.setGlobalPrefix('api');
 

+ 1 - 0
src/plantation/plantation-node-data.interface.ts

@@ -14,6 +14,7 @@ export interface PlantationNodeData {
   metadata?: Record<string, any>;  // extensibility field
 }
 
+
 export interface Worker {
   id: string;
   name: string;

+ 0 - 36
src/plantation/plantation-tree-mongo.service.ts

@@ -1,36 +0,0 @@
-import { Injectable } from "@nestjs/common";
-import { InjectModel } from "@nestjs/mongoose";
-import { Model } from "mongoose";
-import { PlantationNode, PlantationNodeDocument } from "src/common/schemas/plantation.node.schema";
-import { Task } from "./plantation-node-data.interface";
-
-@Injectable()
-export class PlantationTreeMongoService {
-  constructor(
-    @InjectModel(PlantationNode.name) private nodeModel: Model<PlantationNodeDocument>,
-  ) {}
-
-  async createNode(nodeData: Partial<PlantationNode>): Promise<PlantationNode> {
-    const node = new this.nodeModel(nodeData);
-    return node.save();
-  }
-
-  async findNodeById(id: string) {
-    return this.nodeModel.findOne({ id }).populate('childrenNode').exec();
-  }
-
-  async addWorker(nodeId: string, worker: Worker) {
-    return this.nodeModel.findOneAndUpdate(
-      { id: nodeId },
-      { $addToSet: { workers: worker } },
-      { new: true }
-    );
-  }
-
-  async assignTaskToWorker(workerId: string, task: Task) {
-    return this.nodeModel.updateMany(
-      { 'workers.id': workerId },
-      { $push: { 'workers.$.assignedTasks': task } },
-    );
-  }
-}

+ 2 - 0
src/plantation/plantation-tree.module.ts

@@ -2,8 +2,10 @@
 import { Module } from '@nestjs/common';
 import { PlantationTreeService } from './plantation-tree.service';
 import { PlantationTreeController } from './plantation-tree.controller';
+import { ServiceModule } from 'src/services/service.module';
 
 @Module({
+  imports: [ServiceModule],
   providers: [PlantationTreeService],
   controllers: [PlantationTreeController],
   exports: [PlantationTreeService], // export if other modules need to use it

+ 104 - 0
src/services/mongo.service.ts

@@ -0,0 +1,104 @@
+import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common';
+import { MongoClient, Db, Collection, ObjectId } from 'mongodb';
+
+@Injectable()
+export class MongoService implements OnModuleInit, OnModuleDestroy {
+  private client: MongoClient;
+  private db: Db;
+  private readonly logger = new Logger(MongoService.name);
+
+  async onModuleInit() {
+    const uri = process.env.MONGO_URI;
+    if (!uri) throw new Error('MONGO_URI not set in environment variables');
+
+    this.client = new MongoClient(uri);
+    await this.client.connect();
+
+    this.db = this.client.db('swopt-ai-test-a');
+    this.logger.log('Connected to MongoDB');
+  }
+
+  async onModuleDestroy() {
+    await this.client.close();
+    this.logger.log('MongoDB connection closed');
+  }
+
+  // Get collection reference
+  private collection(name: string): Collection {
+    return this.db.collection(name);
+  }
+
+  /* ------------------------------
+   * CRUD OPERATIONS (activities)
+   * ------------------------------ */
+
+  async createActivity(data: any) {
+    const result = await this.collection('activities').insertOne(data);
+    return { insertedId: result.insertedId };
+  }
+
+  async getAllActivities(filter: Record<string, any> = {}) {
+    return this.collection('activities').find(filter).toArray();
+  }
+
+  async getActivityById(id: string) {
+    return this.collection('activities').findOne({ _id: new ObjectId(id) });
+  }
+
+  async updateActivity(id: string, update: Record<string, any>) {
+    const result = await this.collection('activities').updateOne(
+      { _id: new ObjectId(id) },
+      { $set: update },
+    );
+    return { modifiedCount: result.modifiedCount };
+  }
+
+  async deleteActivity(id: string) {
+    const result = await this.collection('activities').deleteOne({ _id: new ObjectId(id) });
+    return { deletedCount: result.deletedCount };
+  }
+
+  /* ------------------------------
+   * AGGREGATION EXAMPLES
+   * ------------------------------ */
+
+  // Example 1: Get total hours worked per resource type
+  async getTotalResourceHours() {
+    return this.collection('activities')
+      .aggregate([
+        { $unwind: '$resources' },
+        {
+          $group: {
+            _id: '$resources.type',
+            totalHours: { $sum: { $toDecimal: '$resources.value.quantity' } },
+          },
+        },
+      ])
+      .toArray();
+  }
+
+  // Example 2: Get total outputs by weight (e.g., total FFB harvested)
+  async getTotalOutputWeight() {
+    return this.collection('activities')
+      .aggregate([
+        { $unwind: '$outputs' },
+        {
+          $group: {
+            _id: '$outputs.type',
+            totalWeightKg: { $sum: { $toDecimal: '$outputs.weightValue.weight' } },
+          },
+        },
+      ])
+      .toArray();
+  }
+
+  // Example 3: Activities within date range
+  async getActivitiesByDateRange(start: Date, end: Date) {
+    return this.collection('activities')
+      .find({
+        dateStart: { $gte: start },
+        dateEnd: { $lte: end },
+      })
+      .toArray();
+  }
+}

+ 9 - 0
src/services/service.module.ts

@@ -0,0 +1,9 @@
+import { Module } from '@nestjs/common';
+import { TreeService } from './tree.service';
+import { MongoService } from './mongo.service';
+
+@Module({
+  providers: [TreeService, MongoService],
+  exports: [TreeService, MongoService],
+})
+export class ServiceModule {}

+ 6 - 0
test.prompt

@@ -0,0 +1,6 @@
+---
+files:
+  - ./README.md
+---
+You are a helpful assistant.
+Read the attached file and give me a **three-sentence summary** of its purpose.