diff --git a/src/app/components/decision-list/decision-list.component.css b/src/app/components/decision-list/decision-list.component.css
index 59737e5..b0fd5e5 100644
--- a/src/app/components/decision-list/decision-list.component.css
+++ b/src/app/components/decision-list/decision-list.component.css
@@ -64,6 +64,16 @@
border-radius: 8px;
cursor: pointer;
font-size: 13px;
+ transition: opacity 0.15s;
+}
+
+.btn-danger:hover:not(:disabled) {
+ opacity: 0.82;
+}
+
+.btn-danger:disabled {
+ opacity: 0.55;
+ cursor: not-allowed;
}
.decision-card {
@@ -171,3 +181,129 @@
@keyframes spin {
to { transform: rotate(360deg); }
}
+
+/* ── Delete confirmation modal ───────────────────────────────────────────── */
+
+.modal-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 50;
+ background: rgba(2, 6, 23, 0.55);
+ display: grid;
+ place-items: center;
+ padding: 18px;
+ animation: backdropIn 0.18s ease;
+}
+
+@keyframes backdropIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+.delete-modal {
+ width: min(460px, 100%);
+ background: #fff;
+ border: 1px solid rgba(0, 0, 0, 0.12);
+ border-radius: 16px;
+ box-shadow: 0 28px 72px rgba(0, 0, 0, 0.24);
+ padding: 24px 24px 22px;
+ animation: modalIn 0.2s cubic-bezier(0.22, 1, 0.36, 1);
+}
+
+@keyframes modalIn {
+ from { opacity: 0; transform: translateY(10px) scale(0.97); }
+ to { opacity: 1; transform: translateY(0) scale(1); }
+}
+
+.delete-modal-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ border-radius: 10px;
+ background: #f3f4f6;
+ border: 1px solid #e5e7eb;
+ color: #374151;
+ margin-bottom: 14px;
+}
+
+.delete-modal-eyebrow {
+ margin: 0 0 6px;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ font-size: 11px;
+ color: #6b7280;
+ font-weight: 700;
+}
+
+.delete-modal-title {
+ margin: 0;
+ font-size: 20px;
+ line-height: 1.3;
+ font-weight: 700;
+ color: #0a0a0a;
+ word-break: break-word;
+}
+
+.delete-modal-copy {
+ margin: 10px 0 0;
+ font-size: 14px;
+ color: #4b5563;
+ line-height: 1.55;
+}
+
+.delete-modal-error {
+ margin: 14px 0 0;
+ background: #fdecec;
+ color: #8d1c1c;
+ border: 1px solid #f7c5c5;
+ border-radius: 10px;
+ padding: 9px 12px;
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.delete-modal-actions {
+ margin-top: 22px;
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+
+.modal-btn {
+ border-radius: 10px;
+ border: 1px solid transparent;
+ padding: 9px 16px;
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: opacity 0.15s, background 0.15s;
+ line-height: 1;
+}
+
+.modal-btn.cancel {
+ background: #fff;
+ color: #111;
+ border-color: #d1d5db;
+}
+
+.modal-btn.cancel:hover:not(:disabled) {
+ background: #f9fafb;
+}
+
+.modal-btn.confirm {
+ background: #111;
+ color: #fff;
+ border-color: #111;
+ min-width: 100px;
+}
+
+.modal-btn.confirm:hover:not(:disabled) {
+ background: #000;
+}
+
+.modal-btn:disabled {
+ opacity: 0.55;
+ cursor: not-allowed;
+}
diff --git a/src/app/components/decision-list/decision-list.component.html b/src/app/components/decision-list/decision-list.component.html
index 687fb9c..628d28e 100644
--- a/src/app/components/decision-list/decision-list.component.html
+++ b/src/app/components/decision-list/decision-list.component.html
@@ -34,8 +34,64 @@
{{ decision.title }}
Edit
-
+
+
+
+
+
+
+
+ Delete Decision
+ "{{ decision.title }}"
+
+ This will permanently remove this decision and all associated data.
+ This action cannot be undone.
+
+
+ {{ deleteDecisionError }}
+
+
+
+
+
+
+
diff --git a/src/app/components/decision-list/decision-list.component.spec.ts b/src/app/components/decision-list/decision-list.component.spec.ts
index f4e20f3..02b2ad3 100644
--- a/src/app/components/decision-list/decision-list.component.spec.ts
+++ b/src/app/components/decision-list/decision-list.component.spec.ts
@@ -66,8 +66,76 @@ describe('DecisionListComponent', () => {
it('should have correct edit link', () => {
const editButton = fixture.debugElement.query(By.css('.btn-secondary'));
expect(editButton).toBeTruthy();
- // Since it's a relative link [decision.id, 'edit'], we check if the attribute is present or just trust the binding
const link = editButton.nativeElement as HTMLAnchorElement;
expect(link.textContent).toContain('Edit');
});
+
+ // ── Delete modal flow ────────────────────────────────────────────────────
+
+ it('should not show delete modal by default', () => {
+ const backdrop = fixture.nativeElement.querySelector('.modal-backdrop');
+ expect(backdrop).toBeNull();
+ });
+
+ it('should open delete modal with decision title when requestDeleteDecision is called', () => {
+ component.requestDeleteDecision(mockDecisions[0]);
+ fixture.detectChanges();
+
+ const backdrop = fixture.nativeElement.querySelector('.modal-backdrop');
+ expect(backdrop).toBeTruthy();
+
+ const title = fixture.nativeElement.querySelector('.delete-modal-title');
+ expect(title.textContent).toContain('Test Decision');
+ });
+
+ it('should close modal and clear state when cancelDeleteDecision is called', () => {
+ component.requestDeleteDecision(mockDecisions[0]);
+ fixture.detectChanges();
+
+ component.cancelDeleteDecision();
+ fixture.detectChanges();
+
+ const backdrop = fixture.nativeElement.querySelector('.modal-backdrop');
+ expect(backdrop).toBeNull();
+ expect(component.pendingDeleteDecision).toBeNull();
+ expect(component.deleteDecisionError).toBe('');
+ });
+
+ it('should close modal when backdrop overlay is clicked', () => {
+ component.requestDeleteDecision(mockDecisions[0]);
+ fixture.detectChanges();
+
+ const backdrop = fixture.nativeElement.querySelector('.modal-backdrop') as HTMLElement;
+ backdrop.click();
+ fixture.detectChanges();
+
+ expect(component.pendingDeleteDecision).toBeNull();
+ });
+
+ it('should call deleteDecision service and close modal on confirmDeleteDecision', () => {
+ component.requestDeleteDecision(mockDecisions[0]);
+ fixture.detectChanges();
+
+ component.confirmDeleteDecision();
+ fixture.detectChanges();
+
+ expect(mockDecisionService.deleteDecision).toHaveBeenCalledWith('10', '1');
+ expect(component.pendingDeleteDecision).toBeNull();
+ expect(component.isDeletingDecision).toBeFalse();
+ });
+
+ it('should not open modal while a delete is already in progress', () => {
+ component.isDeletingDecision = true;
+ component.requestDeleteDecision(mockDecisions[0]);
+
+ expect(component.pendingDeleteDecision).toBeNull();
+ });
+
+ it('should not close modal while a delete is in progress', () => {
+ component.requestDeleteDecision(mockDecisions[0]);
+ component.isDeletingDecision = true;
+ component.cancelDeleteDecision();
+
+ expect(component.pendingDeleteDecision).toEqual(mockDecisions[0]);
+ });
});
diff --git a/src/app/components/decision-list/decision-list.component.ts b/src/app/components/decision-list/decision-list.component.ts
index 432a775..efd612f 100644
--- a/src/app/components/decision-list/decision-list.component.ts
+++ b/src/app/components/decision-list/decision-list.component.ts
@@ -15,6 +15,12 @@ import { Observable, Subject, takeUntil, startWith, switchMap, catchError, of }
export class DecisionListComponent implements OnInit, OnDestroy {
decisions$: Observable | undefined;
error: string | null = null;
+
+ // Delete confirmation state
+ pendingDeleteDecision: Decision | null = null;
+ isDeletingDecision = false;
+ deleteDecisionError = '';
+
private refresh$ = new Subject();
private destroy$ = new Subject();
private workspaceId: string | null = null;
@@ -30,7 +36,7 @@ export class DecisionListComponent implements OnInit, OnDestroy {
this.decisions$ = this.refresh$.pipe(
startWith(undefined),
switchMap(() => this.decisionService.getDecisions(this.workspaceId!).pipe(
- catchError(err => {
+ catchError(() => {
this.error = 'Failed to load decisions. Please try again.';
return of([]);
})
@@ -44,21 +50,39 @@ export class DecisionListComponent implements OnInit, OnDestroy {
this.destroy$.complete();
}
- deleteDecision(id: string): void {
- if (confirm('Are you sure you want to delete this decision?') && this.workspaceId) {
- this.error = null;
- this.decisionService.deleteDecision(this.workspaceId, id)
- .pipe(takeUntil(this.destroy$))
- .subscribe({
- next: () => {
- this.refresh$.next();
- },
- error: (err) => {
- console.error('Delete failed', err);
- this.error = 'Failed to delete decision. Please try again.';
- }
- });
- }
+ requestDeleteDecision(decision: Decision): void {
+ if (this.isDeletingDecision) return;
+ this.pendingDeleteDecision = decision;
+ this.deleteDecisionError = '';
+ }
+
+ cancelDeleteDecision(): void {
+ if (this.isDeletingDecision) return;
+ this.pendingDeleteDecision = null;
+ this.deleteDecisionError = '';
+ }
+
+ confirmDeleteDecision(): void {
+ const decision = this.pendingDeleteDecision;
+ if (!decision || !this.workspaceId || this.isDeletingDecision) return;
+
+ this.isDeletingDecision = true;
+ this.deleteDecisionError = '';
+
+ this.decisionService.deleteDecision(this.workspaceId, decision.id)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe({
+ next: () => {
+ this.pendingDeleteDecision = null;
+ this.isDeletingDecision = false;
+ this.error = null;
+ this.refresh$.next();
+ },
+ error: () => {
+ this.isDeletingDecision = false;
+ this.deleteDecisionError = 'Unable to delete decision. Please try again.';
+ }
+ });
}
isOverdue(decision: Decision): boolean {