Browse Source

prepare plantation API

Dr-Swopt 3 months ago
parent
commit
545128d324

+ 0 - 2
src/auth/auth.controller.spec.ts

@@ -46,8 +46,6 @@ describe('AuthController', () => {
 });
 
 
-
-
 /**
  * Factory for mock registration options
  */

+ 62 - 0
src/auth/auth.controller.ts

@@ -6,6 +6,7 @@ import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard';
 import { serverConfig } from 'src/config';
 import { UsersService } from 'src/users/user.service';
 import { WebauthnService } from 'src/auth/webauthn.service';
+import { LoginPayload, User } from 'src/interface/interface';
 
 @Controller('auth')
 export class AuthController {
@@ -221,6 +222,67 @@ export class AuthController {
         return verification;
     }
 
+    @Post('register')
+    async register(@Body() body: User) {
+        this.logger.log(`[REGISTER] Incoming registration request for: ${body.email}`);
+
+        if (!body.email || !body.password || !body.name) {
+            this.logger.warn(`[REGISTER] Missing required fields`);
+            throw new BadRequestException('Missing required fields');
+        }
+
+        let user;
+        try {
+            user = await this.usersService.createUser(body);
+            this.logger.log(`[REGISTER] ✅ User created successfully: ${user.email}`);
+        } catch (err) {
+            this.logger.error(`[REGISTER] ❌ Failed to create user: ${err.message}`);
+            throw new ConflictException(err);
+        }
+
+        // Generate JWT after successful registration
+        const payload = { sub: user.id, name: user.name, email: user.email };
+        const token = this.jwtService.sign(payload);
+
+        this.logger.debug(`[REGISTER] Returning success response for: ${body.email}`);
+        return {
+            verified: true,
+            access_token: token,
+            name: user.name,
+        };
+    }
+
+    @Post('login')
+    async login(@Body() body: LoginPayload) {
+        this.logger.log(`[LOGIN] Incoming login request for: ${body.email}`);
+
+        if (!body.email || !body.password) {
+            this.logger.warn(`[LOGIN] Missing email or password`);
+            throw new BadRequestException('Missing email or password');
+        }
+
+        let user;
+        try {
+            user = await this.usersService.validateUser(body);
+            this.logger.log(`[LOGIN] ✅ User validated: ${user.email}`);
+        } catch (err) {
+            this.logger.error(`[LOGIN] ❌ Invalid credentials or error: ${err.message}`);
+            throw new BadRequestException(err);
+        }
+
+        // Generate JWT for login as well
+        const payload = { sub: user.id, name: user.name, email: user.email };
+        const token = this.jwtService.sign(payload);
+
+        this.logger.debug(`[LOGIN] Returning success response for: ${body.email}`);
+        return {
+            verified: true,
+            access_token: token,
+            name: user.name,
+        };
+    }
+
+
     @Get(`server`)
     @UseGuards(JwtAuthGuard)
     getServerAPIurl(): string {

+ 31 - 2
src/plantation/plantation-node-data.interface.ts

@@ -1,5 +1,34 @@
 // plantation-node-data.interface.ts
 export interface PlantationNodeData {
+  id: string;                // domain identifier (redundant with TreeNode.id, but useful for external sync)
+  name: string;              // plantation section name (e.g., "Zone A", "Block 3")
+  location?: string;         // optional, could be GPS coordinate, area name, etc.
+  plantedDate?: Date;        // when this section was established
+  type?: 'ROOT' | 'ZONE' | 'BLOCK' | 'TREE';  // optional hierarchy classification
+
+  status?: 'ACTIVE' | 'INACTIVE' | 'MAINTENANCE'; // operational state
+  notes?: string;            // freeform description
+
+  // Domain relationships
+  workers?: Worker[];        // workers assigned to this node
+  metadata?: Record<string, any>;  // extensibility field
+}
+
+export interface Worker {
+  id: string;
   name: string;
-  // other fields, e.g., type, status, assignedTo
-}
+  DOB: Date;
+  age: Number;
+  nationality: string;
+  personCode: string;
+  assignedTasks?: Task[];
+  role?: string; // e.g., "Supervisor", "Harvester", "Planter"
+}
+
+export interface Task {
+  id: string;
+  description: string;
+  date: Date;
+  status: 'PENDING' | 'WIP' | 'DONE';
+  priority?: 'LOW' | 'MEDIUM' | 'HIGH';
+}

+ 113 - 8
src/plantation/plantation-tree.controller.ts

@@ -1,34 +1,139 @@
 // plantation-tree.controller.ts
-import { Controller, Get, Post, Delete, Body, Param } from '@nestjs/common';
+import {
+  Controller,
+  Get,
+  Post,
+  Put,
+  Delete,
+  Body,
+  Param,
+  Query,
+  NotFoundException,
+} from '@nestjs/common';
 import { PlantationTreeService } from './plantation-tree.service';
-import { PlantationNodeData } from './plantation-node-data.interface';
+import { PlantationNodeData, Worker, Task } from './plantation-node-data.interface';
 import { TreeNode } from 'src/services/tree.service';
 
 @Controller('plantation-tree')
 export class PlantationTreeController {
-  constructor(private readonly treeService: PlantationTreeService) {}
+  constructor(private readonly treeService: PlantationTreeService) { }
 
+  /** --- READ: Get entire tree --- */
   @Get()
   getTree() {
     return this.treeService.getTree();
   }
 
+  /** --- CREATE: add a new node under parent (single or multiple nodes) --- */
   @Post('add/:parentId')
   addNode(
     @Param('parentId') parentId: string,
-    @Body() node: TreeNode<PlantationNodeData>,
+    @Body() nodeOrNodes: TreeNode<PlantationNodeData> | TreeNode<PlantationNodeData>[] // single or array
   ) {
-    return this.treeService.addNode(parentId, node);
+    const parentNode = this.treeService.findNode(parentId);
+    if (!parentNode) throw new NotFoundException(`Parent node ${parentId} not found`);
+
+    const nodesArray = Array.isArray(nodeOrNodes) ? nodeOrNodes : [nodeOrNodes];
+    const results: TreeNode<PlantationNodeData>[] = [];
+
+    for (const node of nodesArray) {
+      results.push(this.treeService.addNode(parentId, node));
+    }
+
+    return results;
+  }
+
+  /** --- READ: find a node by ID --- */
+  @Get('find/:nodeId')
+  findNode(@Param('nodeId') nodeId: string) {
+    const node = this.treeService.findNode(nodeId);
+    if (!node) throw new NotFoundException(`Node ${nodeId} not found`);
+    return node;
   }
 
+  /** --- READ: find a node by name --- */
+  @Get('search')
+  searchNode(@Query('name') name: string) {
+    const node = this.treeService.findNodeByName(name);
+    if (!node) throw new NotFoundException(`Node with name "${name}" not found`);
+    return node;
+  }
+
+  /** --- UPDATE: update node data by ID --- */
+  @Put('update/:nodeId')
+  updateNode(
+    @Param('nodeId') nodeId: string,
+    @Body() newData: Partial<PlantationNodeData>,
+  ) {
+    const node = this.treeService.findNode(nodeId);
+    if (!node) throw new NotFoundException(`Node ${nodeId} not found`);
+    return this.treeService.updateNode(nodeId, newData);
+  }
+
+  /** --- UPDATE: update metadata --- */
+  @Put('metadata/:nodeId')
+  updateMetadata(@Param('nodeId') nodeId: string, @Body() metadata: Record<string, any>) {
+    const node = this.treeService.findNode(nodeId);
+    if (!node) throw new NotFoundException(`Node ${nodeId} not found`);
+    return this.treeService.updateMetadata(nodeId, metadata);
+  }
+
+  /** --- DELETE: remove a node by ID --- */
   @Delete('remove/:nodeId')
   removeNode(@Param('nodeId') nodeId: string) {
+    const node = this.treeService.findNode(nodeId);
+    if (!node) throw new NotFoundException(`Node ${nodeId} not found`);
     this.treeService.removeNode(nodeId);
     return { success: true };
   }
 
-  @Get('find/:nodeId')
-  findNode(@Param('nodeId') nodeId: string) {
-    return this.treeService.findNode(nodeId);
+  /** --- WORKERS: Assign worker(s) to node (delegates to service) --- */
+  @Post('add/:nodeId/workers') // plural
+  assignWorkers(
+    @Param('nodeId') nodeId: string,
+    @Body() workers: Worker | Worker[] // accept single or array
+  ) {
+    const node = this.treeService.findNode(nodeId);
+    if (!node) throw new NotFoundException(`Node ${nodeId} not found`);
+
+    const workerArray = Array.isArray(workers) ? workers : [workers];
+    const results: TreeNode<PlantationNodeData>[] = [];
+
+    for (const worker of workerArray) {
+      results.push(this.treeService.assignWorker(nodeId, worker));
+    }
+
+    return results;
+  }
+
+  /** --- TASKS: Assign task(s) to a worker by workerId --- */
+  @Post('worker/:workerId/tasks')
+  assignTasksByWorker(
+    @Param('workerId') workerId: string,
+    @Body() tasks: Task | Task[]
+  ) {
+    const taskArray = Array.isArray(tasks) ? tasks : [tasks];
+    const results: Worker[] = [];
+
+    for (const task of taskArray) {
+      results.push(this.treeService.assignTaskToWorkerById(workerId, task));
+    }
+
+    return results;
+  }
+
+
+  /** --- READ: Get all workers under a node (recursive) --- */
+  @Get(':nodeId/workers')
+  getWorkersUnderNode(@Param('nodeId') nodeId: string) {
+    const node = this.treeService.findNode(nodeId);
+    if (!node) throw new NotFoundException(`Node ${nodeId} not found`);
+    return this.treeService.getWorkersUnderNode(nodeId);
+  }
+
+  /** --- READ: Get a worker by unique ID --- */
+  @Get('worker/:workerId')
+  getWorker(@Param('workerId') workerId: string) {
+    return this.treeService.getWorkerById(workerId);
   }
 }

+ 124 - 7
src/plantation/plantation-tree.service.ts

@@ -1,20 +1,137 @@
-// plantation-tree.service.ts
-import { Injectable } from '@nestjs/common';
-import { PlantationNodeData } from './plantation-node-data.interface';
+import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
+import { PlantationNodeData, Worker, Task } from './plantation-node-data.interface';
 import { TreeNode, TreeService } from 'src/services/tree.service';
 
 @Injectable()
 export class PlantationTreeService extends TreeService<PlantationNodeData> {
+    private workerIndex: Map<string, Worker> = new Map();
+    // plantation-tree.service.ts (constructor only)
     constructor() {
-        super({ id: 'root', data: { name: 'Plantation' } });
+        super({
+            id: 'root',
+            data: {
+                id: 'root',
+                name: 'Plantation Root',
+                type: 'ROOT',     // <-- ensure ROOT here
+                status: 'ACTIVE',
+                workers: [],
+            },
+        });
     }
 
-    // Example of a domain-specific helper
-    findZoneByName(name: string): TreeNode<PlantationNodeData> | undefined {
+    /** 🔍 Find a node by name (case-insensitive) */
+    findNodeByName(name: string): TreeNode<PlantationNodeData> | undefined {
         let found: TreeNode<PlantationNodeData> | undefined;
         this.traverseTree(node => {
-            if (node.data.name === name) found = node;
+            if (node.data.name.toLowerCase() === name.toLowerCase()) found = node;
         });
         return found;
     }
+
+    /** 👷 Assign worker to ZONE or BLOCK (not TREE), propagates upward */
+    assignWorker(nodeId: string, worker: Worker): TreeNode<PlantationNodeData> {
+        // 1️⃣ Check uniqueness globally
+        if (this.workerIndex.has(worker.id)) {
+            throw new BadRequestException(`Worker ID ${worker.id} already exists`);
+        }
+
+        const node = this.root.first(n => n.model.id === nodeId);
+        if (!node) throw new NotFoundException(`Node ${nodeId} not found`);
+
+        if (!node.model.data.workers) node.model.data.workers = [];
+        node.model.data.workers.push(worker);
+
+        // 2️⃣ Propagate upward to ancestors
+        let current = node.parent;
+        while (current) {
+            if (!current.model.data.workers) current.model.data.workers = [];
+            const parentHasWorker = current.model.data.workers.some(w => w.id === worker.id);
+            if (!parentHasWorker) current.model.data.workers.push(worker);
+            current = current.parent;
+        }
+
+        // 3️⃣ Update global index
+        this.workerIndex.set(worker.id, worker);
+
+        return node.model;
+    }
+
+    /** 🌳 Assign tree to an existing worker (must exist in ancestor chain) */
+    assignTreeToWorker(treeId: string, workerId: string): TreeNode<PlantationNodeData> {
+        const treeNode = this.root.first(n => n.model.id === treeId);
+        if (!treeNode) throw new NotFoundException(`Tree node ${treeId} not found`);
+
+        if (treeNode.model.data.type !== 'TREE') {
+            throw new BadRequestException(`Only TREE nodes can be assigned to workers`);
+        }
+
+        // Search upward for worker existence
+        let current = treeNode.parent;
+        let foundWorker: Worker | null = null;
+        while (current) {
+            const worker = current.model.data.workers?.find(w => w.id === workerId);
+            if (worker) {
+                foundWorker = worker;
+                break;
+            }
+            current = current.parent;
+        }
+
+        if (!foundWorker) {
+            throw new NotFoundException(`Worker ${workerId} not found in ancestor hierarchy`);
+        }
+
+        // Assign minimal worker data to tree node
+        treeNode.model.data.workers = [
+            {
+                id: foundWorker.id,
+                name: foundWorker.name,
+                personCode: foundWorker.personCode,
+                role: foundWorker.role,
+                DOB: foundWorker.DOB,
+                age: foundWorker.age,
+                nationality: foundWorker.nationality,
+            },
+        ];
+
+        return treeNode.model;
+    }
+
+    /** 📋 Assign a task to a worker under a node */
+    assignTaskToWorkerById(workerId: string, task: Task): Worker {
+        const worker = this.workerIndex.get(workerId);
+        if (!worker) throw new NotFoundException(`Worker ${workerId} not found`);
+
+        if (!worker.assignedTasks) worker.assignedTasks = [];
+        worker.assignedTasks.push(task);
+
+        return worker;
+    }
+
+    /** 🌲 Recursively collect all workers under a node (aggregated view) */
+    getWorkersUnderNode(nodeId: string): Worker[] {
+        const start = this.root.first(n => n.model.id === nodeId);
+        if (!start) throw new NotFoundException(`Node ${nodeId} not found`);
+
+        const workers: Worker[] = [];
+        start.walk(n => {
+            if (n.model.data.workers) workers.push(...n.model.data.workers);
+            return true;
+        });
+        return workers;
+    }
+
+    /** ✅ Update node metadata safely */
+    updateMetadata(nodeId: string, metadata: Record<string, any>) {
+        const node = this.root.first(n => n.model.id === nodeId);
+        if (!node) throw new NotFoundException(`Node ${nodeId} not found`);
+        node.model.data.metadata = { ...node.model.data.metadata, ...metadata };
+        return node.model;
+    }
+
+    getWorkerById(workerId: string): Worker {
+        const worker = this.workerIndex.get(workerId);
+        if (!worker) throw new NotFoundException(`Worker ${workerId} not found`);
+        return worker;
+    }
 }

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

@@ -30,6 +30,15 @@ export class TreeService<T> {
     return treeNode.model;
   }
 
+  updateNode(nodeId: string, newData: Partial<T>): TreeNode<T> {
+    const node = this.root.first(n => n.model.id === nodeId);
+    if (!node) throw new Error('Node not found');
+
+    // Merge existing data with newData
+    node.model.data = { ...node.model.data, ...newData };
+    return node.model;
+  }
+
   removeNode(nodeId: string) {
     const node = this.root.first(n => n.model.id === nodeId);
     if (!node) throw new Error('Node not found');