diff --git a/src/app/components/dashboard/dashboard.css b/src/app/components/dashboard/dashboard.css index 84888f0..6f5c325 100644 --- a/src/app/components/dashboard/dashboard.css +++ b/src/app/components/dashboard/dashboard.css @@ -13,6 +13,17 @@ header { padding-bottom: 20px; } +nav { + display: flex; + align-items: center; + gap: 20px; +} + +.workspace-card-link { + text-decoration: none; + color: inherit; +} + .logout-btn { background-color: #f44336; color: white; diff --git a/src/app/components/dashboard/dashboard.html b/src/app/components/dashboard/dashboard.html index 70e8929..fafcfb9 100644 --- a/src/app/components/dashboard/dashboard.html +++ b/src/app/components/dashboard/dashboard.html @@ -11,13 +11,15 @@

Your Workspaces

-
-

{{ ws.name }}

-

{{ ws.description }}

- Created: {{ ws.createdDate | date }} -
+ +
+

{{ ws.name }}

+

{{ ws.description }}

+ Created: {{ ws.createdDate | date }} +
+
- +

No workspaces found. Create one to get started!

diff --git a/src/app/components/decision-form/decision-form.component.css b/src/app/components/decision-form/decision-form.component.css new file mode 100644 index 0000000..71e22d1 --- /dev/null +++ b/src/app/components/decision-form/decision-form.component.css @@ -0,0 +1,67 @@ +.form-container { + max-width: 600px; + margin: 20px auto; + padding: 20px; + border: 1px solid #ddd; + border-radius: 4px; + background-color: #fff; +} + +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} + +.form-control { + width: 100%; + padding: 8px; + border: 1px solid #ced4da; + border-radius: 4px; + box-sizing: border-box; + /* Ensures padding doesn't increase width */ +} + +.form-control.is-invalid { + border-color: #dc3545; +} + +.invalid-feedback { + color: #dc3545; + font-size: 12px; + margin-top: 5px; +} + +.form-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; +} + +.btn-primary { + background-color: #007bff; + color: white; + padding: 8px 16px; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.btn-primary:disabled { + background-color: #a0c4ff; + cursor: not-allowed; +} + +.btn-secondary { + background-color: #6c757d; + color: white; + padding: 8px 16px; + border: none; + border-radius: 4px; + cursor: pointer; +} \ No newline at end of file diff --git a/src/app/components/decision-form/decision-form.component.html b/src/app/components/decision-form/decision-form.component.html new file mode 100644 index 0000000..68930f8 --- /dev/null +++ b/src/app/components/decision-form/decision-form.component.html @@ -0,0 +1,36 @@ +
+

{{ isEditMode ? 'Edit Decision' : 'Create New Decision' }}

+ +
+
+ + +
+ Title is required. +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
\ No newline at end of file diff --git a/src/app/components/decision-form/decision-form.component.ts b/src/app/components/decision-form/decision-form.component.ts new file mode 100644 index 0000000..70adcba --- /dev/null +++ b/src/app/components/decision-form/decision-form.component.ts @@ -0,0 +1,88 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { DecisionService } from '../../services/decision.service'; +import { Decision } from '../../models/decision.model'; + +@Component({ + selector: 'app-decision-form', + standalone: true, + imports: [CommonModule, ReactiveFormsModule, RouterModule], + templateUrl: './decision-form.component.html', + styleUrls: ['./decision-form.component.css'] +}) +export class DecisionFormComponent implements OnInit { + decisionForm: FormGroup; + isEditMode = false; + decisionId: string | null = null; + isLoading = false; + + constructor( + private fb: FormBuilder, + private decisionService: DecisionService, + private route: ActivatedRoute, + private router: Router + ) { + this.decisionForm = this.fb.group({ + title: ['', Validators.required], + description: [''], + status: ['DRAFT', Validators.required] + }); + } + + ngOnInit(): void { + // Get workspaceId from parent + this.route.parent?.paramMap.subscribe(params => { + const workspaceId = params.get('id'); + if (workspaceId) { + this.decisionForm.patchValue({ workspaceId }); + } + }); + + // Get decisionId from current route + this.route.paramMap.subscribe(params => { + this.decisionId = params.get('decisionId'); + if (this.decisionId) { + this.isEditMode = true; + this.loadDecision(this.decisionId); + } + }); + } + + loadDecision(id: string): void { + this.isLoading = true; + this.decisionService.getDecision(id).subscribe(decision => { + this.isLoading = false; + if (decision) { + this.decisionForm.patchValue({ + title: decision.title, + description: decision.description, + status: decision.status + }); + } else { + this.router.navigate(['../'], { relativeTo: this.route }); + } + }); + } + + onSubmit(): void { + if (this.decisionForm.invalid) { + return; + } + + const formValue = this.decisionForm.value; + + if (this.isEditMode && this.decisionId) { + this.decisionService.updateDecision(this.decisionId, formValue).subscribe(() => { + this.router.navigate(['../../'], { relativeTo: this.route }); + }); + } else { + // Include workspaceId from parent route if available + const workspaceId = this.route.parent?.snapshot.paramMap.get('id'); + this.decisionService.createDecision({ ...formValue, workspaceId }).subscribe(() => { + this.router.navigate(['../'], { relativeTo: this.route }); + }); + } + } +} diff --git a/src/app/components/decision-form/decision-form.css b/src/app/components/decision-form/decision-form.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/decision-form/decision-form.html b/src/app/components/decision-form/decision-form.html new file mode 100644 index 0000000..7e9c4ac --- /dev/null +++ b/src/app/components/decision-form/decision-form.html @@ -0,0 +1 @@ +

decision-form works!

diff --git a/src/app/components/decision-form/decision-form.ts b/src/app/components/decision-form/decision-form.ts new file mode 100644 index 0000000..0a59e66 --- /dev/null +++ b/src/app/components/decision-form/decision-form.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-decision-form', + imports: [], + templateUrl: './decision-form.html', + styleUrl: './decision-form.css', +}) +export class DecisionForm { + +} diff --git a/src/app/components/decision-list/decision-list.component.css b/src/app/components/decision-list/decision-list.component.css new file mode 100644 index 0000000..781008a --- /dev/null +++ b/src/app/components/decision-list/decision-list.component.css @@ -0,0 +1,87 @@ +.decision-container { + padding: 20px; + max-width: 800px; + margin: 0 auto; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.btn-primary { + background-color: #007bff; + color: white; + padding: 10px 15px; + text-decoration: none; + border-radius: 4px; +} + +.btn-secondary { + background-color: #6c757d; + color: white; + padding: 5px 10px; + text-decoration: none; + border-radius: 4px; + margin-right: 5px; + cursor: pointer; + border: none; +} + +.btn-danger { + background-color: #dc3545; + color: white; + padding: 5px 10px; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.decision-card { + border: 1px solid #ddd; + padding: 15px; + margin-bottom: 10px; + border-radius: 4px; + background-color: #fff; +} + +.decision-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.decision-header h3 { + margin: 0; +} + +.status-badge { + padding: 3px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: bold; +} + +.status-badge.draft { + background-color: #e2e3e5; + color: #383d41; +} + +.status-badge.open { + background-color: #d4edda; + color: #155724; +} + +.status-badge.closed { + background-color: #d1ecf1; + color: #0c5460; +} + +.decision-meta { + font-size: 12px; + color: #666; + margin-bottom: 10px; +} \ No newline at end of file diff --git a/src/app/components/decision-list/decision-list.component.html b/src/app/components/decision-list/decision-list.component.html new file mode 100644 index 0000000..4196208 --- /dev/null +++ b/src/app/components/decision-list/decision-list.component.html @@ -0,0 +1,27 @@ +
+
+

Decisions

+ Create New Decision +
+ +
+
+ No decisions found. Create one to get started! +
+ +
+
+

{{ decision.title }}

+ {{ decision.status }} +
+

{{ decision.description || 'No description provided.' }}

+
+ Created: {{ decision.createdAt | date:'shortDate' }} +
+
+ Edit + +
+
+
+
\ No newline at end of file diff --git a/src/app/components/decision-list/decision-list.component.ts b/src/app/components/decision-list/decision-list.component.ts new file mode 100644 index 0000000..17b4681 --- /dev/null +++ b/src/app/components/decision-list/decision-list.component.ts @@ -0,0 +1,38 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, RouterModule } from '@angular/router'; +import { Decision } from '../../models/decision.model'; +import { DecisionService } from '../../services/decision.service'; +import { Observable } from 'rxjs'; + +@Component({ + selector: 'app-decision-list', + standalone: true, + imports: [CommonModule, RouterModule], + templateUrl: './decision-list.component.html', + styleUrls: ['./decision-list.component.css'] +}) +export class DecisionListComponent implements OnInit { + decisions$: Observable | undefined; + + constructor( + private decisionService: DecisionService, + private route: ActivatedRoute + ) { } + + ngOnInit(): void { + // Get workspaceId from the parent route (workspaces/:id) + this.route.parent?.paramMap.subscribe(params => { + const workspaceId = params.get('id'); + if (workspaceId) { + this.decisions$ = this.decisionService.getDecisions(workspaceId); + } + }); + } + + deleteDecision(id: string): void { + if (confirm('Are you sure you want to delete this decision?')) { + this.decisionService.deleteDecision(id); + } + } +} diff --git a/src/app/components/decision-list/decision-list.css b/src/app/components/decision-list/decision-list.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/decision-list/decision-list.html b/src/app/components/decision-list/decision-list.html new file mode 100644 index 0000000..0c2fbfa --- /dev/null +++ b/src/app/components/decision-list/decision-list.html @@ -0,0 +1 @@ +

decision-list works!

diff --git a/src/app/components/decision-list/decision-list.ts b/src/app/components/decision-list/decision-list.ts new file mode 100644 index 0000000..732933e --- /dev/null +++ b/src/app/components/decision-list/decision-list.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-decision-list', + imports: [], + templateUrl: './decision-list.html', + styleUrl: './decision-list.css', +}) +export class DecisionList { + +} diff --git a/src/app/components/workspace/create-workspace-placeholder.ts b/src/app/components/workspace/create-workspace-placeholder.ts new file mode 100644 index 0000000..e785b87 --- /dev/null +++ b/src/app/components/workspace/create-workspace-placeholder.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-create-workspace-placeholder', + standalone: true, + imports: [RouterLink], + template: ` +
+

Create Workspace

+

This feature is being implemented in Issue #4.

+ Back to Dashboard +
+ ` +}) +export class CreateWorkspacePlaceholder { } diff --git a/src/app/components/workspace/workspace-details/workspace-details.css b/src/app/components/workspace/workspace-details/workspace-details.css new file mode 100644 index 0000000..ec5419e --- /dev/null +++ b/src/app/components/workspace/workspace-details/workspace-details.css @@ -0,0 +1,46 @@ +.workspace-details-container { + padding: 20px; + max-width: 1000px; + margin: 0 auto; +} + +header { + margin-bottom: 20px; + border-bottom: 1px solid #eee; + padding-bottom: 20px; +} + +header h1 { + margin: 0 0 10px 0; +} + +header p { + color: #666; + margin: 0; +} + +.workspace-nav { + display: flex; + gap: 20px; + border-bottom: 1px solid #ddd; + margin-bottom: 20px; +} + +.workspace-nav a { + padding: 10px 15px; + text-decoration: none; + color: #555; + border-bottom: 2px solid transparent; +} + +.workspace-nav a.active { + color: #007bff; + border-bottom-color: #007bff; + font-weight: 500; +} + +.loading { + text-align: center; + padding: 40px; + color: #666; +} \ No newline at end of file diff --git a/src/app/components/workspace/workspace-details/workspace-details.html b/src/app/components/workspace/workspace-details/workspace-details.html new file mode 100644 index 0000000..6cf2abf --- /dev/null +++ b/src/app/components/workspace/workspace-details/workspace-details.html @@ -0,0 +1,19 @@ +
+
+

{{ workspace.name }}

+

{{ workspace.description }}

+
+ + + +
+ +
+
+ + +
Loading workspace...
+
\ No newline at end of file diff --git a/src/app/components/workspace/workspace-details/workspace-details.ts b/src/app/components/workspace/workspace-details/workspace-details.ts new file mode 100644 index 0000000..6863e45 --- /dev/null +++ b/src/app/components/workspace/workspace-details/workspace-details.ts @@ -0,0 +1,31 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, RouterModule, RouterLink, RouterOutlet } from '@angular/router'; +import { WorkspaceService } from '../../../services/workspace'; +import { Workspace } from '../../../models/workspace'; +import { Observable } from 'rxjs'; + +@Component({ + selector: 'app-workspace-details', + standalone: true, + imports: [CommonModule, RouterModule, RouterLink, RouterOutlet], + templateUrl: './workspace-details.html', + styleUrls: ['./workspace-details.css'] +}) +export class WorkspaceDetailsComponent implements OnInit { + workspace$: Observable | undefined; + + constructor( + private route: ActivatedRoute, + private workspaceService: WorkspaceService + ) { } + + ngOnInit(): void { + this.route.paramMap.subscribe(params => { + const id = params.get('id'); + if (id) { + this.workspace$ = this.workspaceService.getWorkspace(id); + } + }); + } +} diff --git a/src/app/models/decision.model.ts b/src/app/models/decision.model.ts new file mode 100644 index 0000000..515b0c1 --- /dev/null +++ b/src/app/models/decision.model.ts @@ -0,0 +1,11 @@ +export interface Decision { + id: string; + title: string; + description?: string; + status: 'DRAFT' | 'OPEN' | 'CLOSED'; + workspaceId: string; + userId: string; + createdAt: Date; + updatedAt: Date; + isDeleted: boolean; +} diff --git a/src/app/services/decision.service.ts b/src/app/services/decision.service.ts new file mode 100644 index 0000000..3e1aced --- /dev/null +++ b/src/app/services/decision.service.ts @@ -0,0 +1,94 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Decision } from '../models/decision.model'; + +@Injectable({ + providedIn: 'root' +}) +export class DecisionService { + private decisions: Decision[] = []; + private decisionsSubject = new BehaviorSubject([]); + + constructor() { + // Initialize with some mock data + this.decisions = [ + { + id: '1', + title: 'Choose Frontend Framework', + description: 'Decide between Angular and React', + status: 'CLOSED', + workspaceId: 'ws-1', + userId: 'user-1', + createdAt: new Date('2023-10-25'), + updatedAt: new Date('2023-10-26'), + isDeleted: false + }, + { + id: '2', + title: 'Database Selection', + description: 'Evaluate SQLite vs PostgreSQL for local dev', + status: 'OPEN', + workspaceId: 'ws-1', + userId: 'user-1', + createdAt: new Date(), + updatedAt: new Date(), + isDeleted: false + } + ]; + this.decisionsSubject.next(this.decisions); + } + + getDecisions(workspaceId: string): Observable { + return this.decisionsSubject.asObservable().pipe( + map(decisions => decisions.filter(d => d.workspaceId === workspaceId && !d.isDeleted)) + ); + } + + getDecision(id: string): Observable { + return this.decisionsSubject.asObservable().pipe( + map(decisions => decisions.find(d => d.id === id)) + ); + } + + createDecision(decision: Partial): Observable { + const newDecision: Decision = { + id: Math.random().toString(36).substring(2, 9), + title: decision.title!, + description: decision.description, + status: decision.status || 'DRAFT', + workspaceId: decision.workspaceId || 'ws-1', // Default to ws-1 for now + userId: 'user-1', // Mock user + createdAt: new Date(), + updatedAt: new Date(), + isDeleted: false + }; + + this.decisions.push(newDecision); + this.decisionsSubject.next(this.decisions); + return of(newDecision); + } + + updateDecision(id: string, updates: Partial): Observable { + const index = this.decisions.findIndex(d => d.id === id); + if (index !== -1) { + this.decisions[index] = { + ...this.decisions[index], + ...updates, + updatedAt: new Date() + }; + this.decisionsSubject.next(this.decisions); + return of(this.decisions[index]); + } + return of(undefined); + } + + deleteDecision(id: string): Observable { + const index = this.decisions.findIndex(d => d.id === id); + if (index !== -1) { + this.decisions[index].isDeleted = true; + this.decisionsSubject.next(this.decisions); + } + return of(void 0); + } +} diff --git a/src/app/services/workspace.ts b/src/app/services/workspace.ts index 2065013..3895d13 100644 --- a/src/app/services/workspace.ts +++ b/src/app/services/workspace.ts @@ -34,6 +34,10 @@ export class WorkspaceService { return of(this.mockWorkspaces); } + getWorkspace(id: string): Observable { + return of(this.mockWorkspaces.find(w => w.id === id)); + } + createWorkspace(name: string): Observable { const newWorkspace: Workspace = { id: Math.random().toString(36).substring(7),