Jelajahi Sumber

UI upgrades

Dr-Swopt 2 bulan lalu
induk
melakukan
ffded1d958

+ 56 - 12
package-lock.json

@@ -8,12 +8,13 @@
       "name": "mobile-auth-web-app",
       "version": "0.0.0",
       "dependencies": {
-        "@angular/cdk": "^20.0.4",
+        "@angular/animations": "^20.0.5",
+        "@angular/cdk": "^20.0.5",
         "@angular/common": "^20.0.5",
         "@angular/compiler": "^20.0.5",
         "@angular/core": "^20.0.5",
         "@angular/forms": "^20.0.5",
-        "@angular/material": "^20.0.4",
+        "@angular/material": "^20.0.5",
         "@angular/platform-browser": "^20.0.5",
         "@angular/platform-browser-dynamic": "^20.0.5",
         "@angular/router": "^20.0.5",
@@ -336,6 +337,23 @@
         "yarn": ">= 1.13.0"
       }
     },
+    "node_modules/@angular/animations": {
+      "version": "20.0.5",
+      "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.0.5.tgz",
+      "integrity": "sha512-v8dzr2tnju7Sa7XUhMY6yTJpRV3isMqP3mnOjrul2kkEY870a1tZ7VI7xp0qTx36086/+nzXAvOvOItmRkUaaQ==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "@angular/common": "20.0.5",
+        "@angular/core": "20.0.5"
+      }
+    },
     "node_modules/@angular/build": {
       "version": "20.0.4",
       "resolved": "https://registry.npmjs.org/@angular/build/-/build-20.0.4.tgz",
@@ -497,10 +515,11 @@
       }
     },
     "node_modules/@angular/cdk": {
-      "version": "20.0.4",
-      "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.0.4.tgz",
-      "integrity": "sha512-NCUuw0qQXwawLsT14JHApNB9or3XGs7D1pWXlOIix/fKqzHVfi4un9xHmpjH2Q1uCiwonuak7fDof8B+IXhbug==",
+      "version": "20.0.5",
+      "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.0.5.tgz",
+      "integrity": "sha512-WhJ1I/ib/Za0qjWkSzMYV0gM8NOWrtOcZ2TYZ4aYFsjd8E13rGhxOez0DWt2sN3vfjAc1iWMmGGbNZrkp98adg==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "parse5": "^7.1.2",
         "tslib": "^2.3.0"
@@ -562,6 +581,7 @@
       "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.0.5.tgz",
       "integrity": "sha512-R7SQaOVYjVnrGHOq2RnuPn0pGofGVTDgy5EoHzF8ulb5MG/d7GFwCaMgfAbp3/Cw1CJzP2ZB54O8x9SMuqExyg==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "tslib": "^2.3.0"
       },
@@ -578,6 +598,7 @@
       "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.0.5.tgz",
       "integrity": "sha512-eHHnh+wIUC+8mfmlPnkzVfonQCA3LAbPWgYpvEQtBh0/R3cZBN6tmOxWQB8IuLu+cZ0eXS/a14mqHJp3c3u7Hg==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "tslib": "^2.3.0"
       },
@@ -591,6 +612,7 @@
       "integrity": "sha512-v0DSeUU7cid7jqfK9RTkyhbZGNIiOyxRYeaqZMsu4UiYGwABIanM7lOcX++OYapfWj/TEPky+5wtbV8ScqAxiw==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@babel/core": "7.27.4",
         "@jridgewell/sourcemap-codec": "^1.4.14",
@@ -697,6 +719,7 @@
       "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.0.5.tgz",
       "integrity": "sha512-r7YQXZvKPAMUXeo3psKTZxyYJrwidTwDPrzxMX3EGqZxv0eDnMPWCxH2y0O2X4BT0Nm1iAqx3zhGrSFc0vD60Q==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "tslib": "^2.3.0"
       },
@@ -722,6 +745,7 @@
       "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.0.5.tgz",
       "integrity": "sha512-zoS0SaNUZBPtDfmr/edd3cHa9Z+vvPs8UXKMo9/i4YezWCskkZmW5qIJwISYJt4DHnHWoznlGBB9BQX8HgmQRw==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "tslib": "^2.3.0"
       },
@@ -736,15 +760,15 @@
       }
     },
     "node_modules/@angular/material": {
-      "version": "20.0.4",
-      "resolved": "https://registry.npmjs.org/@angular/material/-/material-20.0.4.tgz",
-      "integrity": "sha512-ET+znnyOVjBezHsjy7U42/88JPl9Mhumvf01gMBN8mNcaoSpeM4cc2uKBg30/3YzykKIsjXtvUJj/PaTujmJAQ==",
+      "version": "20.0.5",
+      "resolved": "https://registry.npmjs.org/@angular/material/-/material-20.0.5.tgz",
+      "integrity": "sha512-bKWtqbGxLuK6cbpy9hZtH+BGUrqxMQhaR/esW6LmjdNALrbfJx3r2wlOTCSydGP8FJDbg6qmHDYxsl1zha2Gbg==",
       "license": "MIT",
       "dependencies": {
         "tslib": "^2.3.0"
       },
       "peerDependencies": {
-        "@angular/cdk": "20.0.4",
+        "@angular/cdk": "20.0.5",
         "@angular/common": "^20.0.0 || ^21.0.0",
         "@angular/core": "^20.0.0 || ^21.0.0",
         "@angular/forms": "^20.0.0 || ^21.0.0",
@@ -757,6 +781,7 @@
       "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.0.5.tgz",
       "integrity": "sha512-gE3C5/ZAXdAlBFvvX/crboIy5skbV5mtxRoEULwf7xF9WJLlYzY3w+PCRHV6/Z21UJ3ikCcbaaowBx378FYhQg==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "tslib": "^2.3.0"
       },
@@ -841,6 +866,7 @@
       "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@ampproject/remapping": "^2.2.0",
         "@babel/code-frame": "^7.27.1",
@@ -2721,6 +2747,7 @@
       "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.0.tgz",
       "integrity": "sha512-P6NnjoHyobZgTjynlZSn27d0SUj6j38inlNxFnKZr9qwU7/r6+0Sg2nWkGkIH/pMmXHsvGD8zVe6KUq1UncIjA==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "tslib": "^2.1.0"
       }
@@ -3383,6 +3410,7 @@
       "integrity": "sha512-5AOrZPf2/GxZ+SDRZ5WFplCA2TAQgK3OYrXCYmJL5NaTu4ECcoWFlfUZuw7Es++6Njv7iu/8vpYJhuzxUH76Vg==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@inquirer/checkbox": "^4.1.6",
         "@inquirer/confirm": "^5.1.10",
@@ -5819,6 +5847,7 @@
       "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.1.tgz",
       "integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "undici-types": "~7.8.0"
       }
@@ -6209,6 +6238,7 @@
       "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "fast-deep-equal": "^3.1.3",
         "fast-uri": "^3.0.1",
@@ -6713,6 +6743,7 @@
         }
       ],
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "caniuse-lite": "^1.0.30001718",
         "electron-to-chromium": "^1.5.160",
@@ -9737,7 +9768,8 @@
       "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.8.0.tgz",
       "integrity": "sha512-Q9dqmpUAfptwyueW3+HqBOkSuYd9I/clZSSfN97wXE/Nr2ROFNCwIBEC1F6kb3QXS9Fcz0LjFYSDQT+BiwjuhA==",
       "dev": true,
-      "license": "MIT"
+      "license": "MIT",
+      "peer": true
     },
     "node_modules/jest-worker": {
       "version": "27.5.1",
@@ -9776,6 +9808,7 @@
       "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "bin": {
         "jiti": "bin/jiti.js"
       }
@@ -9883,6 +9916,7 @@
       "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@colors/colors": "1.5.0",
         "body-parser": "^1.19.0",
@@ -10251,6 +10285,7 @@
       "integrity": "sha512-X9RyH9fvemArzfdP8Pi3irr7lor2Ok4rOttDXBhlwDg+wKQsXOXgHWduAJE1EsF7JJx0w0bcO6BC6tCKKYnXKA==",
       "dev": true,
       "license": "Apache-2.0",
+      "peer": true,
       "dependencies": {
         "copy-anything": "^2.0.1",
         "parse-node-version": "^1.0.1",
@@ -12121,6 +12156,7 @@
         }
       ],
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "nanoid": "^3.3.8",
         "picocolors": "^1.1.1",
@@ -12963,6 +12999,7 @@
       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
       "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
       "license": "Apache-2.0",
+      "peer": true,
       "dependencies": {
         "tslib": "^2.1.0"
       }
@@ -14192,6 +14229,7 @@
       "integrity": "sha512-Mm6+uad0ZuDtcV8/4uOZQDQ8RuiC5Pu+iZRedJtF7yA/27sPL7d++In/AJKpWZlU3SYMPPkVfwetn6sgZ66pUA==",
       "dev": true,
       "license": "BSD-2-Clause",
+      "peer": true,
       "dependencies": {
         "@jridgewell/source-map": "^0.3.3",
         "acorn": "^8.8.2",
@@ -14354,7 +14392,8 @@
       "version": "2.8.1",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
       "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
-      "license": "0BSD"
+      "license": "0BSD",
+      "peer": true
     },
     "node_modules/tuf-js": {
       "version": "3.0.1",
@@ -14411,6 +14450,7 @@
       "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
       "dev": true,
       "license": "Apache-2.0",
+      "peer": true,
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
@@ -14646,6 +14686,7 @@
       "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "esbuild": "^0.25.0",
         "fdir": "^6.4.4",
@@ -14763,6 +14804,7 @@
       "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@types/eslint-scope": "^3.7.7",
         "@types/estree": "^1.0.6",
@@ -14841,6 +14883,7 @@
       "integrity": "sha512-ml/0HIj9NLpVKOMq+SuBPLHcmbG+TGIjXRHsYfZwocUBIqEvws8NnS/V9AFQ5FKP+tgn5adwVwRrTEpGL33QFQ==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@types/bonjour": "^3.5.13",
         "@types/connect-history-api-fallback": "^1.5.4",
@@ -15459,7 +15502,8 @@
       "version": "0.15.1",
       "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
       "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
-      "license": "MIT"
+      "license": "MIT",
+      "peer": true
     }
   }
 }

+ 4 - 3
package.json

@@ -12,12 +12,13 @@
   },
   "private": true,
   "dependencies": {
-    "@angular/cdk": "^20.0.4",
+    "@angular/animations": "^20.0.5",
+    "@angular/cdk": "^20.0.5",
     "@angular/common": "^20.0.5",
     "@angular/compiler": "^20.0.5",
     "@angular/core": "^20.0.5",
     "@angular/forms": "^20.0.5",
-    "@angular/material": "^20.0.4",
+    "@angular/material": "^20.0.5",
     "@angular/platform-browser": "^20.0.5",
     "@angular/platform-browser-dynamic": "^20.0.5",
     "@angular/router": "^20.0.5",
@@ -47,4 +48,4 @@
     "karma-jasmine-html-reporter": "~2.1.0",
     "typescript": "~5.8.3"
   }
-}
+}

+ 4 - 0
src/app/app.routes.ts

@@ -4,10 +4,14 @@ import { DashboardComponent } from './dashboard/dashboard.component';
 import { authGuard } from './services/auth.guard';
 import { WebauthnRegisterComponent } from './webauthn-register/webauthn-register.component';
 import { WebauthnLoginComponent } from './webauthn-login/webauthn-login.component';
+import { LoginComponent } from './login/login.component';
+import { RegisterComponent } from './register/register.component';
 
 export const routes: Routes = [
   { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
   { path: 'webauthn-register', component: WebauthnRegisterComponent },
   { path: 'webauthn-login', component: WebauthnLoginComponent },
   { path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] },
+  { path: 'login', component: LoginComponent },
+  { path: 'register', component: RegisterComponent },
 ];

+ 14 - 0
src/app/components/node form/node-form-dialog.component.css

@@ -0,0 +1,14 @@
+.form-container {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  width: 100%;
+  min-width: 320px;
+}
+
+.buttons {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 8px;
+  gap: 8px;
+}

+ 57 - 0
src/app/components/node form/node-form-dialog.component.html

@@ -0,0 +1,57 @@
+<h2 mat-dialog-title>{{ data?.id ? 'Edit Node' : 'Add Node' }}</h2>
+
+<form [formGroup]="form" (ngSubmit)="onSubmit()" class="form-container">
+  <mat-form-field appearance="outline">
+    <mat-label>Name</mat-label>
+    <input matInput formControlName="name" required />
+  </mat-form-field>
+
+  <mat-form-field appearance="outline">
+    <mat-label>Location</mat-label>
+    <input matInput formControlName="location" />
+  </mat-form-field>
+
+  <mat-form-field appearance="outline">
+    <mat-label>Type</mat-label>
+    <mat-select formControlName="type">
+      <mat-option value="ROOT">Root</mat-option>
+      <mat-option value="SITE">Site</mat-option>
+      <mat-option value="ZONE">Zone</mat-option>
+      <mat-option value="BLOCK">Block</mat-option>
+      <mat-option value="TREE">Tree</mat-option>
+    </mat-select>
+  </mat-form-field>
+
+  <mat-form-field appearance="outline">
+    <mat-label>Status</mat-label>
+    <mat-select formControlName="status">
+      <mat-option value="ACTIVE">Active</mat-option>
+      <mat-option value="INACTIVE">Inactive</mat-option>
+      <mat-option value="MAINTENANCE">Maintenance</mat-option>
+    </mat-select>
+  </mat-form-field>
+
+  <mat-form-field appearance="outline">
+    <mat-label>Planted Date</mat-label>
+    <input
+      matInput
+      [matDatepicker]="picker"
+      formControlName="plantedDate"
+      placeholder="Select date"
+    />
+    <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
+    <mat-datepicker #picker></mat-datepicker>
+  </mat-form-field>
+
+  <mat-form-field appearance="outline">
+    <mat-label>Notes</mat-label>
+    <textarea matInput rows="3" formControlName="notes"></textarea>
+  </mat-form-field>
+
+  <div class="buttons">
+    <button mat-button type="button" (click)="onCancel()">Cancel</button>
+    <button mat-flat-button color="primary" type="submit" [disabled]="form.invalid">
+      Save
+    </button>
+  </div>
+</form>

+ 66 - 0
src/app/components/node form/node-form-dialog.component.ts

@@ -0,0 +1,66 @@
+import { Component, Inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
+import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatSelectModule } from '@angular/material/select';
+import { MatButtonModule } from '@angular/material/button';
+import { MatDatepickerModule } from '@angular/material/datepicker';
+import { MAT_DATE_LOCALE, DateAdapter, MatNativeDateModule, NativeDateAdapter } from '@angular/material/core';
+
+export interface PlantationNodeFormData {
+  id?: string;
+  name: string;
+  location?: string;
+  plantedDate?: Date;
+  type?: 'ROOT' | 'SITE' | 'ZONE' | 'BLOCK' | 'TREE';
+  status?: 'ACTIVE' | 'INACTIVE' | 'MAINTENANCE';
+  notes?: string;
+}
+
+@Component({
+  selector: 'app-node-form-dialog',
+  standalone: true,
+  imports: [
+    MatDialogModule,
+    MatFormFieldModule,
+    MatInputModule,
+    MatDatepickerModule,
+    MatNativeDateModule,
+    MatButtonModule,
+    ReactiveFormsModule,
+    MatSelectModule
+  ],
+  providers: [],
+  templateUrl: './node-form-dialog.component.html',
+  styleUrls: ['./node-form-dialog.component.css']
+})
+export class NodeFormDialogComponent {
+  form: FormGroup;
+
+  constructor(
+    private fb: FormBuilder,
+    private dialogRef: MatDialogRef<NodeFormDialogComponent>,
+    @Inject(MAT_DIALOG_DATA) public data: PlantationNodeFormData
+  ) {
+    this.form = this.fb.group({
+      name: [data?.name || '', Validators.required],
+      location: [data?.location || ''],
+      plantedDate: [data?.plantedDate || null],
+      type: [data?.type || 'TREE'],
+      status: [data?.status || 'ACTIVE'],
+      notes: [data?.notes || ''],
+    });
+  }
+
+  onSubmit() {
+    if (this.form.valid) {
+      this.dialogRef.close(this.form.value);
+    }
+  }
+
+  onCancel() {
+    this.dialogRef.close(null);
+  }
+}

+ 44 - 0
src/app/components/worker-dialog/worker-form-dialog.component.html

@@ -0,0 +1,44 @@
+<h2 mat-dialog-title>Add Worker</h2>
+
+<div mat-dialog-content>
+  <p *ngIf="data?.nodeName" class="mb-2 text-sm text-gray-600">
+    Assigning to: <strong>{{ data.nodeName }}</strong>
+  </p>
+
+  <mat-form-field appearance="outline" class="w-full mb-3">
+    <mat-label>Name</mat-label>
+    <input matInput [(ngModel)]="form.name" required />
+  </mat-form-field>
+
+  <mat-form-field appearance="outline" class="w-full mb-3">
+    <mat-label>Date of Birth</mat-label>
+    <input matInput [matDatepicker]="picker" [(ngModel)]="form.DOB" required />
+    <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
+    <mat-datepicker #picker></mat-datepicker>
+  </mat-form-field>
+
+  <mat-form-field appearance="outline" class="w-full mb-3">
+    <mat-label>Nationality</mat-label>
+    <input matInput [(ngModel)]="form.nationality" placeholder="e.g. MY, ID, PH" />
+  </mat-form-field>
+
+  <mat-form-field appearance="outline" class="w-full mb-3">
+    <mat-label>Worker Code</mat-label>
+    <input matInput [(ngModel)]="form.personCode" />
+  </mat-form-field>
+
+  <mat-form-field appearance="outline" class="w-full mb-3">
+    <mat-label>Role</mat-label>
+    <mat-select [(ngModel)]="form.role" required>
+      <mat-option value="SUPERVISOR">Supervisor</mat-option>
+      <mat-option value="WORKER">Worker</mat-option>
+      <mat-option value="INSPECTOR">Inspector</mat-option>
+      <mat-option value="ADMIN">Admin</mat-option>
+    </mat-select>
+  </mat-form-field>
+</div>
+
+<div mat-dialog-actions align="end">
+  <button mat-stroked-button color="warn" (click)="onCancel()">Cancel</button>
+  <button mat-flat-button color="primary" (click)="onSave()">Save</button>
+</div>

+ 58 - 0
src/app/components/worker-dialog/worker-form-dialog.component.ts

@@ -0,0 +1,58 @@
+import { Component, Inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatButtonModule } from '@angular/material/button';
+import { MatSelectModule } from '@angular/material/select';
+import { MatDatepickerModule } from '@angular/material/datepicker';
+import { MatNativeDateModule } from '@angular/material/core';
+
+export interface WorkerFormData {
+  id?: string;
+  name: string;
+  DOB: Date;
+  nationality: string;
+  personCode: string;
+  role: string;
+}
+
+@Component({
+  selector: 'app-worker-form-dialog',
+  standalone: true,
+  imports: [
+    CommonModule,
+    FormsModule,
+    MatDialogModule,
+    MatFormFieldModule,
+    MatInputModule,
+    MatButtonModule,
+    MatSelectModule,
+    MatDatepickerModule,
+    MatNativeDateModule,
+  ],
+  templateUrl: './worker-form-dialog.component.html',
+})
+export class WorkerFormDialogComponent {
+  form: WorkerFormData = {
+    name: '',
+    DOB: new Date(),
+    nationality: '',
+    personCode: '',
+    role: '',
+  };
+
+  constructor(
+    public dialogRef: MatDialogRef<WorkerFormDialogComponent>,
+    @Inject(MAT_DIALOG_DATA) public data: { nodeId: string; nodeName: string }
+  ) {}
+
+  onCancel(): void {
+    this.dialogRef.close();
+  }
+
+  onSave(): void {
+    this.dialogRef.close(this.form);
+  }
+}

+ 1 - 1
src/app/config.ts

@@ -1,3 +1,3 @@
 export const webConfig = {
-    exposedUrl: `https://192.168.100.100:4200`,
+    exposedUrl: `https://localhost:3000`,
 }

+ 6 - 1
src/app/dashboard/dashboard.component.html

@@ -15,4 +15,9 @@
   <mat-tab label="Payment">
     <app-payment></app-payment>
   </mat-tab>
-</mat-tab-group>
+
+  <mat-tab label="Plantation">
+    <!-- Integrate PlantationTreeComponent here -->
+    <app-plantation-tree></app-plantation-tree>
+  </mat-tab>
+</mat-tab-group>

+ 4 - 2
src/app/dashboard/dashboard.component.ts

@@ -7,6 +7,7 @@ import { MatButtonModule } from '@angular/material/button';
 import { AttendanceComponent } from '../attendance/attendance.component';
 import { DashboardHomeComponent } from './dashboard.home.component';
 import { PaymentComponent } from '../payment/payment.component';
+import { PlantationTreeComponent } from "../plantation/plantation-tree.component";
 
 @Component({
   selector: 'app-dashboard',
@@ -18,8 +19,9 @@ import { PaymentComponent } from '../payment/payment.component';
     MatButtonModule,
     DashboardHomeComponent,
     PaymentComponent,
-    AttendanceComponent
-  ],
+    AttendanceComponent,
+    PlantationTreeComponent
+],
   templateUrl: './dashboard.component.html',
   styleUrls: ['./dashboard.component.css']
 })

+ 12 - 0
src/app/login/login.component.css

@@ -0,0 +1,12 @@
+.login-card {
+  max-width: 400px;
+  margin: 5rem auto;
+  padding: 2rem;
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+}
+
+.full-width {
+  width: 100%;
+}

+ 69 - 0
src/app/login/login.component.html

@@ -0,0 +1,69 @@
+<mat-card class="login-card">
+  <h2>Login with Email</h2>
+
+  <form (ngSubmit)="login()" #loginForm="ngForm">
+    <mat-form-field appearance="outline" class="full-width">
+      <mat-label>Email</mat-label>
+      <input
+        matInput
+        type="email"
+        [(ngModel)]="email"
+        name="email"
+        required
+        email
+        #emailInput="ngModel"
+      />
+      <mat-error *ngIf="emailInput.invalid && emailInput.touched">
+        Valid email is required
+      </mat-error>
+    </mat-form-field>
+
+    <mat-form-field appearance="outline" class="full-width">
+      <mat-label>Password</mat-label>
+      <input
+        matInput
+        type="password"
+        [(ngModel)]="password"
+        name="password"
+        required
+        minlength="6"
+        #passwordInput="ngModel"
+      />
+      <mat-error *ngIf="passwordInput.invalid && passwordInput.touched">
+        Password is required
+      </mat-error>
+    </mat-form-field>
+
+    <button
+      mat-raised-button
+      color="primary"
+      type="submit"
+      class="full-width"
+      [disabled]="loading || loginForm.invalid"
+    >
+      Login
+    </button>
+  </form>
+
+  <mat-divider></mat-divider>
+
+  <button
+    mat-stroked-button
+    color="accent"
+    class="full-width"
+    (click)="goToWebauthnLogin()"
+  >
+    Login with Passkey
+  </button>
+
+  <button
+    mat-button
+    type="button"
+    class="full-width"
+    (click)="goToRegister()"
+  >
+    Register
+  </button>
+
+  <mat-hint *ngIf="message">{{ message }}</mat-hint>
+</mat-card>

+ 80 - 0
src/app/login/login.component.ts

@@ -0,0 +1,80 @@
+import { Component, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { Router, RouterModule } from '@angular/router';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatButtonModule } from '@angular/material/button';
+import { MatCardModule } from '@angular/material/card';
+import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
+import { MatDividerModule } from '@angular/material/divider';
+import { AuthService } from '../services/auth.service';
+import { AuthResponse } from '../interfaces/interface';
+
+@Component({
+  selector: 'app-standard-login',
+  standalone: true,
+  imports: [
+    CommonModule,
+    FormsModule,
+    RouterModule,
+    MatFormFieldModule,
+    MatInputModule,
+    MatButtonModule,
+    MatCardModule,
+    MatSnackBarModule,
+    MatDividerModule
+  ],
+  templateUrl: './login.component.html',
+  styleUrls: ['./login.component.css']
+})
+export class LoginComponent {
+  private snackbar = inject(MatSnackBar);
+
+  email = '';
+  password = '';
+  loading = false;
+  message = '';
+
+  constructor(private router: Router, private authService: AuthService) {}
+
+  async login() {
+    if (!this.email.trim() || !this.password.trim()) {
+      this.snackbar.open('Please enter your email and password', 'Dismiss', { duration: 3000 });
+      return;
+    }
+
+    this.loading = true;
+    try {
+      const res: AuthResponse | undefined = await this.authService.login({
+        email: this.email,
+        password: this.password,
+      }).toPromise();
+
+      if (!res || !res.access_token) {
+        throw new Error('Invalid response from server');
+      }
+
+      // ✅ Store token and username properly
+      this.authService.storeToken(res.access_token);
+      this.authService.setUserName(res.name);
+
+      this.snackbar.open('Login successful!', 'OK', { duration: 3000 });
+      this.router.navigate(['/dashboard']);
+    } catch (err: any) {
+      console.error('[LoginComponent] Login failed:', err);
+      this.snackbar.open('Login failed', 'Dismiss', { duration: 5000 });
+      this.message = 'Login failed: ' + (err?.message || 'Unknown error');
+    } finally {
+      this.loading = false;
+    }
+  }
+
+  goToWebauthnLogin() {
+    this.router.navigate(['/webauthn-login']);
+  }
+
+  goToRegister() {
+    this.router.navigate(['/standard-register']);
+  }
+}

+ 84 - 0
src/app/plantation/plantation-tree.component.css

@@ -0,0 +1,84 @@
+.node-row {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.type-icon {
+  font-size: 20px;
+}
+
+.node-name {
+  font-weight: 500;
+}
+
+.node-type {
+  font-size: 12px;
+  color: #888;
+}
+
+.status-badge {
+  padding: 2px 6px;
+  border-radius: 4px;
+  font-size: 11px;
+  text-transform: uppercase;
+}
+
+.status-active { background-color: #d4edda; color: #155724; }
+.status-inactive { background-color: #f8d7da; color: #721c24; }
+.status-maintenance { background-color: #fff3cd; color: #856404; }
+
+.actions {
+  margin-left: auto;
+  display: flex;
+  gap: 4px;
+}
+
+.meta-text {
+  margin-left: 10px;
+  font-size: 12px;
+  color: #6c757d;
+}
+
+/* Icons */
+.icon-root::before { content: "🌿"; }
+.icon-site::before { content: "🏗️"; }
+.icon-zone::before { content: "🗺️"; }
+.icon-block::before { content: "📦"; }
+.icon-tree::before { content: "🌳"; }
+
+.node-row {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.actions {
+  margin-left: auto;
+  display: flex;
+  gap: 4px;
+}
+
+.node-details {
+  margin-left: 40px;
+  background: #fafafa;
+  border-left: 3px solid #ddd;
+  padding: 6px 10px;
+  border-radius: 4px;
+  font-size: 13px;
+  color: #333;
+}
+
+.node-details p {
+  margin: 2px 0;
+}
+
+.workers-section ul {
+  margin: 4px 0 0 15px;
+  padding: 0;
+  list-style-type: none;
+}
+
+.workers-section li {
+  padding: 2px 0;
+}

+ 106 - 0
src/app/plantation/plantation-tree.component.html

@@ -0,0 +1,106 @@
+<!-- plantation-tree.component.html -->
+<div *ngIf="loading" class="spinner">
+  <mat-spinner diameter="50"></mat-spinner>
+</div>
+
+<mat-tree *ngIf="!loading" [dataSource]="dataSource" [treeControl]="treeControl">
+  <!-- EXPANDABLE NODE -->
+  <mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild">
+    <div class="mat-tree-node node-row" matTreeNodePadding>
+      <!-- Toggle for children -->
+      <button mat-icon-button matTreeNodeToggle>
+        <mat-icon>
+          {{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
+        </mat-icon>
+      </button>
+
+      <mat-icon class="type-icon" [ngClass]="getNodeIconClass(node.data.type)"></mat-icon>
+
+      <span class="node-name">{{ node.data.name }}</span>
+      <span class="node-type">({{ node.data.type }})</span>
+
+      <span class="status-badge" [ngClass]="getStatusClass(node.data.status)">
+        {{ node.data.status }}
+      </span>
+
+      <span class="actions">
+        <button mat-icon-button matTooltip="View Details" (click)="toggleDetails(node)">
+          <mat-icon>info</mat-icon>
+        </button>
+
+        <button *ngIf="canAddChild(node.data.type)" mat-icon-button color="primary" (click)="addNode(node)"
+          matTooltip="Add child node">
+          <mat-icon>add</mat-icon>
+        </button>
+
+        <button mat-icon-button color="accent" matTooltip="Add Worker" *ngIf="canAddWorker(node.data.type)"
+          (click)="addWorker(node)">
+          <mat-icon>person_add</mat-icon>
+        </button>
+
+        <button mat-icon-button color="accent" (click)="updateNode(node)" matTooltip="Edit node">
+          <mat-icon>edit</mat-icon>
+        </button>
+
+        <button *ngIf="!isRootNode(node)" mat-icon-button color="warn" (click)="removeNode(node)"
+          matTooltip="Delete node">
+          <mat-icon>delete</mat-icon>
+        </button>
+      </span>
+    </div>
+
+    <!-- 🔽 Node details panel -->
+    <div class="node-details" *ngIf="isDetailsExpanded(node)">
+      <div class="detail-section">
+        <p><strong>Location:</strong> {{ node.data.location || '—' }}</p>
+        <p><strong>Planted Date:</strong> {{ node.data.plantedDate | date }}</p>
+        <p><strong>Notes:</strong> {{ node.data.notes || '—' }}</p>
+      </div>
+
+      <div *ngIf="node.data.workers?.length" class="workers-section">
+        <strong>Workers:</strong>
+        <ul>
+          <li *ngFor="let w of node.data.workers">
+            👷‍♂️ {{ w.name }} ({{ w.role || 'Worker' }}) – {{ w.personCode }}
+          </li>
+        </ul>
+      </div>
+    </div>
+
+    <!-- 🔽 Child nodes -->
+    <div [style.display]="treeControl.isExpanded(node) ? 'block' : 'none'">
+      <ng-container matTreeNodeOutlet></ng-container>
+    </div>
+  </mat-nested-tree-node>
+
+
+  <!-- LEAF NODE -->
+  <mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
+    <mat-icon class="type-icon" [ngClass]="getNodeIconClass(node.data.type)"></mat-icon>
+
+    <span class="node-name">{{ node.data.name }}</span>
+    <span class="node-type">({{ node.data.type }})</span>
+
+    <span class="status-badge" [ngClass]="getStatusClass(node.data.status)">
+      {{ node.data.status }}
+    </span>
+
+    <span class="actions">
+      <!-- ✅ Leaf nodes may still be addable if not TREE -->
+      <button *ngIf="canAddChild(node.data.type)" mat-icon-button color="primary" (click)="addNode(node)"
+        matTooltip="Add child node">
+        <mat-icon>add</mat-icon>
+      </button>
+
+      <button mat-icon-button color="accent" (click)="updateNode(node)" matTooltip="Edit node">
+        <mat-icon>edit</mat-icon>
+      </button>
+
+      <!-- ✅ Hide Delete if root -->
+      <button *ngIf="!isRootNode(node)" mat-icon-button color="warn" (click)="removeNode(node)"
+        matTooltip="Delete node">
+        <mat-icon>delete</mat-icon>
+      </button>
+    </span>
+  </mat-tree-node>
+</mat-tree>

+ 196 - 0
src/app/plantation/plantation-tree.component.ts

@@ -0,0 +1,196 @@
+// plantation-tree.component.ts
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { NestedTreeControl } from '@angular/cdk/tree';
+import { MatTreeModule } from '@angular/material/tree';
+import { MatIconModule } from '@angular/material/icon';
+import { MatButtonModule } from '@angular/material/button';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { HttpClient, HttpClientModule } from '@angular/common/http';
+import { catchError, of } from 'rxjs';
+import { webConfig } from '../config';
+import { MatDialog } from '@angular/material/dialog';
+import { NodeFormDialogComponent, PlantationNodeFormData } from '../components/node form/node-form-dialog.component';
+import { WorkerFormData, WorkerFormDialogComponent } from '../components/worker-dialog/worker-form-dialog.component';
+
+interface PlantationNodeData {
+    id: string;
+    name: string;
+    location?: string;
+    plantedDate?: string;
+    type?: 'ROOT' | 'SITE' | 'ZONE' | 'BLOCK' | 'TREE';
+    status?: 'ACTIVE' | 'INACTIVE' | 'MAINTENANCE';
+    notes?: string;
+}
+
+interface TreeNode<T = PlantationNodeData> {
+    id: string;
+    data: T;
+    children?: TreeNode<T>[];
+}
+
+@Component({
+    selector: 'app-plantation-tree',
+    standalone: true,
+    imports: [
+        CommonModule,
+        HttpClientModule,
+        MatTreeModule,
+        MatIconModule,
+        MatButtonModule,
+        MatProgressSpinnerModule,
+        MatTooltipModule,
+    ],
+    templateUrl: './plantation-tree.component.html',
+    styleUrls: ['./plantation-tree.component.css'],
+})
+export class PlantationTreeComponent implements OnInit {
+    treeControl = new NestedTreeControl<TreeNode>(node => node.children);
+    dataSource: TreeNode[] = [];
+    apiUrl = webConfig.exposedUrl + '/api/plantation-tree';
+    loading = false;
+    /** track which nodes have their detail panel open */
+    expandedDetails = new Set<string>();
+
+    constructor(private http: HttpClient, private dialog: MatDialog) { }
+
+    ngOnInit() {
+        this.loadTree();
+    }
+
+    hasChild = (_: number, node: TreeNode) => !!node.children && node.children.length > 0;
+
+    loadTree() {
+        this.loading = true;
+        this.http
+            .get<TreeNode>(this.apiUrl)
+            .pipe(
+                catchError(err => {
+                    console.error(err);
+                    this.loading = false;
+                    return of(null);
+                })
+            )
+            .subscribe(tree => {
+                this.loading = false;
+                if (tree) {
+                    this.dataSource = [tree];
+                    this.treeControl.dataNodes = this.dataSource;
+                    this.treeControl.expandAll();
+                }
+            });
+    }
+
+    addNode(parent: TreeNode) {
+        const dialogRef = this.dialog.open(NodeFormDialogComponent, {
+            width: '400px',
+            data: { type: 'TREE', status: 'ACTIVE' },
+        });
+
+        dialogRef.afterClosed().subscribe((formData: PlantationNodeFormData | null) => {
+            if (!formData) return;
+
+            const newNode = {
+                id: Date.now().toString(),
+                data: { ...formData, id: Date.now().toString() },
+            };
+
+            this.http.post(`${this.apiUrl}/add/${parent.id}`, newNode).subscribe({
+                next: () => this.loadTree(),
+                error: err => console.error('Add node failed:', err),
+            });
+        });
+    }
+
+    updateNode(node: TreeNode) {
+        const dialogRef = this.dialog.open(NodeFormDialogComponent, {
+            width: '400px',
+            data: { ...node.data },
+        });
+
+        dialogRef.afterClosed().subscribe((updated: PlantationNodeFormData | null) => {
+            if (!updated) return;
+
+            this.http.put(`${this.apiUrl}/update/${node.id}`, updated).subscribe({
+                next: () => this.loadTree(),
+                error: err => console.error('Update failed:', err),
+            });
+        });
+    }
+
+    removeNode(node: TreeNode) {
+        if (this.isRootNode(node)) {
+            alert("Root node can't be deleted.");
+            return;
+        }
+
+        if (!confirm(`Delete node "${node.data.name}"?`)) return;
+        this.http.delete(`${this.apiUrl}/remove/${node.id}`).subscribe({
+            next: () => this.loadTree(),
+            error: err => console.error('Remove failed:', err),
+        });
+    }
+
+    addWorker(node: TreeNode) {
+        const dialogRef = this.dialog.open(WorkerFormDialogComponent, {
+            width: '400px',
+            data: { nodeId: node.id, nodeName: node.data.name },
+        });
+
+        dialogRef.afterClosed().subscribe((workerData: WorkerFormData | null) => {
+            if (!workerData) return;
+
+            // Assuming backend endpoint: POST /api/plantation-tree/:nodeId/workers/add
+            this.http.post(`${this.apiUrl}/add/${node.id}/workers/`, workerData).subscribe({
+                next: () => this.loadTree(),
+                error: err => console.error('Add worker failed:', err),
+            });
+        });
+    }
+
+    getNodeIconClass(type?: string): string {
+        switch (type) {
+            case 'ROOT': return 'icon-root';
+            case 'SITE': return 'icon-site';
+            case 'ZONE': return 'icon-zone';
+            case 'BLOCK': return 'icon-block';
+            case 'TREE': return 'icon-tree';
+            default: return 'icon-default';
+        }
+    }
+
+    getStatusClass(status?: string): string {
+        switch (status) {
+            case 'ACTIVE': return 'status-active';
+            case 'INACTIVE': return 'status-inactive';
+            case 'MAINTENANCE': return 'status-maintenance';
+            default: return 'status-unknown';
+        }
+    }
+
+    canAddChild(type?: string): boolean {
+        return type !== 'TREE';
+    }
+
+    canAddWorker(type?: string): boolean {
+        // Workers are allowed for BLOCK or TREE levels
+        return type === 'BLOCK' || type === 'ZONE';
+    }
+
+    isRootNode(node: TreeNode): boolean {
+        return node.data.type === 'ROOT';
+    }
+
+    toggleDetails(node: TreeNode) {
+        if (this.expandedDetails.has(node.id)) {
+            this.expandedDetails.delete(node.id);
+        } else {
+            this.expandedDetails.add(node.id);
+        }
+    }
+
+    isDetailsExpanded(node: TreeNode): boolean {
+        return this.expandedDetails.has(node.id);
+    }
+}

+ 12 - 0
src/app/register/register.component.css

@@ -0,0 +1,12 @@
+.login-card {
+  max-width: 400px;
+  margin: 5rem auto;
+  padding: 2rem;
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+}
+
+.full-width {
+  width: 100%;
+}

+ 98 - 0
src/app/register/register.component.html

@@ -0,0 +1,98 @@
+<mat-card class="login-card">
+  <h2>Register with Email</h2>
+
+  <form (ngSubmit)="register()" #registerForm="ngForm">
+    <mat-form-field appearance="outline" class="full-width">
+      <mat-label>Username</mat-label>
+      <input
+        matInput
+        [(ngModel)]="username"
+        name="username"
+        required
+        #usernameInput="ngModel"
+      />
+      <mat-error *ngIf="usernameInput.invalid && usernameInput.touched">
+        Username is required
+      </mat-error>
+    </mat-form-field>
+
+    <mat-form-field appearance="outline" class="full-width">
+      <mat-label>Email</mat-label>
+      <input
+        matInput
+        type="email"
+        [(ngModel)]="email"
+        name="email"
+        required
+        email
+        #emailInput="ngModel"
+      />
+      <mat-error *ngIf="emailInput.invalid && emailInput.touched">
+        Valid email is required
+      </mat-error>
+    </mat-form-field>
+
+    <mat-form-field appearance="outline" class="full-width">
+      <mat-label>Password</mat-label>
+      <input
+        matInput
+        type="password"
+        [(ngModel)]="password"
+        name="password"
+        required
+        minlength="6"
+        #passwordInput="ngModel"
+      />
+      <mat-error *ngIf="passwordInput.invalid && passwordInput.touched">
+        Password must be at least 6 characters
+      </mat-error>
+    </mat-form-field>
+
+    <mat-form-field appearance="outline" class="full-width">
+      <mat-label>Confirm Password</mat-label>
+      <input
+        matInput
+        type="password"
+        [(ngModel)]="confirmPassword"
+        name="confirmPassword"
+        required
+        #confirmPasswordInput="ngModel"
+      />
+      <mat-error *ngIf="confirmPasswordInput.invalid && confirmPasswordInput.touched">
+        Confirm your password
+      </mat-error>
+    </mat-form-field>
+
+    <button
+      mat-raised-button
+      color="primary"
+      type="submit"
+      class="full-width"
+      [disabled]="loading || registerForm.invalid"
+    >
+      Register
+    </button>
+  </form>
+
+  <mat-divider></mat-divider>
+
+  <button
+    mat-stroked-button
+    color="accent"
+    class="full-width"
+    (click)="goToWebauthnRegister()"
+  >
+    Register with Passkey
+  </button>
+
+  <button
+    mat-button
+    type="button"
+    class="full-width"
+    (click)="goBack()"
+  >
+    Back to Login
+  </button>
+
+  <mat-hint *ngIf="message">{{ message }}</mat-hint>
+</mat-card>

+ 87 - 0
src/app/register/register.component.ts

@@ -0,0 +1,87 @@
+import { Component, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { Router, RouterModule } from '@angular/router';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatButtonModule } from '@angular/material/button';
+import { MatCardModule } from '@angular/material/card';
+import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
+import { MatDividerModule } from '@angular/material/divider';
+import { AuthService } from '../services/auth.service';
+import { AuthResponse } from '../interfaces/interface';
+
+@Component({
+  selector: 'app-standard-register',
+  standalone: true,
+  imports: [
+    CommonModule,
+    FormsModule,
+    RouterModule,
+    MatFormFieldModule,
+    MatInputModule,
+    MatButtonModule,
+    MatCardModule,
+    MatSnackBarModule,
+    MatDividerModule
+  ],
+  templateUrl: './register.component.html',
+  styleUrls: ['./register.component.css']
+})
+export class RegisterComponent {
+  private snackbar = inject(MatSnackBar);
+
+  username = '';
+  email = '';
+  password = '';
+  confirmPassword = '';
+  loading = false;
+  message = '';
+
+  constructor(private router: Router, private authService: AuthService) {}
+
+  async register() {
+    if (this.password !== this.confirmPassword) {
+      this.snackbar.open('Passwords do not match', 'Dismiss', { duration: 3000 });
+      return;
+    }
+
+    this.loading = true;
+    this.message = '';
+
+    try {
+      const response: AuthResponse | undefined = await this.authService
+        .register({ name: this.username, email: this.email, password: this.password })
+        .toPromise();
+
+      if (response && response.access_token && response.name) {
+        // ✅ Store token and proceed
+        this.authService.storeToken(response.access_token);
+        this.authService.setUserName(response.name);
+
+        this.snackbar.open('Registration successful!', 'OK', { duration: 3000 });
+        this.router.navigate(['/dashboard']);
+      } else {
+        throw new Error('Invalid response from server');
+      }
+    } catch (err: any) {
+      console.error('Registration failed:', err);
+      const message =
+        err?.error?.message ||
+        err?.message ||
+        'Registration failed. Please try again.';
+      this.snackbar.open(message, 'Dismiss', { duration: 5000 });
+      this.message = message;
+    } finally {
+      this.loading = false;
+    }
+  }
+
+  goToWebauthnRegister() {
+    this.router.navigate(['/webauthn-register']);
+  }
+
+  goBack() {
+    this.router.navigate(['/webauthn-login']);
+  }
+}

+ 1 - 0
src/app/services/auth.interceptor.spec.ts

@@ -1,5 +1,6 @@
 import { TestBed } from '@angular/core/testing';
 import { HttpInterceptorFn } from '@angular/common/http';
+import { authInterceptor } from './auth.interceptor';
 
 
 describe('authInterceptor', () => {

+ 32 - 2
src/app/services/auth.service.ts

@@ -18,11 +18,41 @@ export class AuthService {
   // -- API Calls --
 
   register(payload: RegisterPayload): Observable<AuthResponse> {
-    return this.http.post<AuthResponse>(`${this.baseUrl}/auth/register`, payload);
+    return new Observable<AuthResponse>((observer) => {
+      this.http.post<AuthResponse>(`${this.baseUrl}/auth/register`, payload)
+        .subscribe({
+          next: (res) => {
+            if (res && res.access_token) {
+              this.storeToken(res.access_token);
+              this.setUserName(res.name);
+              observer.next(res);
+            } else {
+              observer.error(new Error('Invalid response from server'));
+            }
+            observer.complete();
+          },
+          error: (err) => observer.error(err)
+        });
+    });
   }
 
   login(payload: LoginPayload): Observable<AuthResponse> {
-    return this.http.post<AuthResponse>(`${this.baseUrl}/auth/login`, payload);
+    return new Observable<AuthResponse>((observer) => {
+      this.http.post<AuthResponse>(`${this.baseUrl}/auth/login`, payload)
+        .subscribe({
+          next: (res) => {
+            if (res && res.access_token) {
+              this.storeToken(res.access_token);
+              this.setUserName(res.name);
+              observer.next(res);
+            } else {
+              observer.error(new Error('Invalid response from server'));
+            }
+            observer.complete();
+          },
+          error: (err) => observer.error(err)
+        });
+    });
   }
 
   async webauthnRegister(username: string, email: string): Promise<AuthResponse> {

+ 1 - 1
src/app/socket.config.ts

@@ -3,6 +3,6 @@
 import { SocketIoConfig } from 'ngx-socket-io';
 
 export const socketConfig: SocketIoConfig = {
-    url: 'http://localhost:3000/ws', // change this to your backend URL
+    url: 'https://localhost:3000/ws', // change this to your backend URL
     options: {}
 };

+ 21 - 1
src/app/webauthn-login/webauthn-login.component.html

@@ -11,13 +11,33 @@
     Login with Passkey
   </button>
 
+  <mat-divider class="divider"></mat-divider>
+
+  <button
+    mat-stroked-button
+    color="primary"
+    class="full-width"
+    (click)="goToStandardLogin()"
+  >
+    Login with Email & Password
+  </button>
+
   <button
     mat-button
     type="button"
     class="full-width"
     (click)="goToRegistration()"
   >
-    Register Account
+    Register with Passkey
+  </button>
+
+  <button
+    mat-stroked-button
+    color="accent"
+    class="full-width"
+    (click)="goToStandardRegister()"
+  >
+    Register with Email & Password
   </button>
 
   <mat-hint *ngIf="message">{{ message }}</mat-hint>

+ 8 - 1
src/app/webauthn-login/webauthn-login.component.ts

@@ -56,8 +56,15 @@ export class WebauthnLoginComponent implements OnInit {
     }
   }
 
-
   goToRegistration() {
     this.router.navigate(['/webauthn-register']);
   }
+
+  goToStandardLogin() {
+    this.router.navigate(['/login']);
+  }
+
+  goToStandardRegister() {
+    this.router.navigate(['/register']);
+  }
 }

+ 24 - 6
src/app/webauthn-register/webauthn-register.component.html

@@ -1,6 +1,7 @@
 <mat-card class="login-card">
-  <h2>Register with Passkey</h2>
+  <h2>Register</h2>
 
+  <!-- WebAuthn (Passkey) Registration -->
   <form (ngSubmit)="register()" #registerForm="ngForm">
     <mat-form-field appearance="outline" class="full-width">
       <mat-label>Username</mat-label>
@@ -41,11 +42,28 @@
     >
       Register with Passkey
     </button>
+  </form>
 
-    <button mat-button type="button" class="full-width" (click)="goBack()">
-      Back to Login
-    </button>
+  <mat-divider class="divider"></mat-divider>
 
-    <mat-hint *ngIf="message">{{ message }}</mat-hint>
-  </form>
+  <!-- Alternate Registration Option -->
+  <button
+    mat-stroked-button
+    color="accent"
+    class="full-width"
+    (click)="goToStandardRegister()"
+  >
+    Register with Email & Password
+  </button>
+
+  <button
+    mat-button
+    type="button"
+    class="full-width"
+    (click)="goBack()"
+  >
+    Back to Login
+  </button>
+
+  <mat-hint *ngIf="message">{{ message }}</mat-hint>
 </mat-card>

+ 13 - 10
src/app/webauthn-register/webauthn-register.component.ts

@@ -8,6 +8,7 @@ import { MatButtonModule } from '@angular/material/button';
 import { MatCardModule } from '@angular/material/card';
 import { AuthService } from '../services/auth.service';
 import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
+import { MatDividerModule } from '@angular/material/divider';
 
 @Component({
   selector: 'app-webauthn-register',
@@ -20,7 +21,8 @@ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
     MatInputModule,
     MatButtonModule,
     MatCardModule,
-    MatSnackBarModule
+    MatSnackBarModule,
+    MatDividerModule
   ],
   templateUrl: './webauthn-register.component.html',
   styleUrls: ['./webauthn-register.component.css']
@@ -29,23 +31,20 @@ export class WebauthnRegisterComponent {
   private snackbar = inject(MatSnackBar);
   username = '';
   email = '';
-  message = ''
+  message = '';
   loading = false;
 
-  constructor(private router: Router, private authService: AuthService) { }
+  constructor(private router: Router, private authService: AuthService) {}
 
   async register() {
     this.loading = true;
-    // ✅ Show snackbar before sending
     try {
       await this.authService.webauthnRegister(this.username, this.email);
-      // ✅ Navigate to dashboard
+      this.snackbar.open('Registration successful!', 'OK', { duration: 3000 });
       this.router.navigate(['/dashboard']);
-    } catch (err) {
-      this.snackbar.open('Registration Failed', 'Dismiss', {
-        duration: undefined, // stays until explicitly closed
-      });
-      this.message = 'Registration failed: ' + (err as any).message;
+    } catch (err: any) {
+      this.snackbar.open('Registration Failed', 'Dismiss', { duration: undefined });
+      this.message = 'Registration failed: ' + (err?.message || 'Unknown error');
     } finally {
       this.loading = false;
     }
@@ -54,4 +53,8 @@ export class WebauthnRegisterComponent {
   goBack() {
     this.router.navigate(['/webauthn-login']);
   }
+
+  goToStandardRegister() {
+    this.router.navigate(['/register']);
+  }
 }

+ 5 - 4
src/main.ts

@@ -4,10 +4,9 @@ import { provideRouter } from '@angular/router';
 import { routes } from './app/app.routes';
 import { provideHttpClient, withInterceptors } from '@angular/common/http';
 import { authInterceptor } from './app/services/auth.interceptor';
-import { SocketIoModule } from 'ngx-socket-io';
-import { socketConfig } from './app/socket.config';
-import { importProvidersFrom } from '@angular/core';
+import { provideAnimations } from '@angular/platform-browser/animations';
 import { Capacitor } from '@capacitor/core';
+import { provideNativeDateAdapter } from '@angular/material/core';
 
 const isWeb = Capacitor.getPlatform() === 'web';
 
@@ -15,6 +14,8 @@ bootstrapApplication(AppComponent, {
   providers: [
     provideRouter(routes),
     provideHttpClient(withInterceptors([authInterceptor])),
-    ...(isWeb ? [importProvidersFrom(SocketIoModule.forRoot(socketConfig))] : []),
+    provideAnimations(),
+    provideNativeDateAdapter(), // ✅ registers MAT_DATE_FORMATS + adapter
+    // ...(isWeb ? [importProvidersFrom(SocketIoModule.forRoot(socketConfig))] : []),
   ],
 });