239 lines
7.0 KiB
TypeScript
239 lines
7.0 KiB
TypeScript
import { Component, OnInit, signal, computed } from '@angular/core';
|
|
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
|
|
import { CommonModule } from '@angular/common';
|
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
|
import { MatTreeModule } from '@angular/material/tree';
|
|
import { MatIconModule } from '@angular/material/icon';
|
|
import { MatButtonModule } from '@angular/material/button';
|
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
|
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
|
import { ApiService } from '../../services';
|
|
import { Category, Subcategory } from '../../models';
|
|
import { CreateDialogComponent } from '../../components/create-dialog/create-dialog.component';
|
|
import { ConfirmDialogComponent } from '../../components/confirm-dialog/confirm-dialog.component';
|
|
|
|
interface CategoryNode {
|
|
id: string;
|
|
name: string;
|
|
type: 'category' | 'subcategory';
|
|
visible: boolean;
|
|
expanded?: boolean;
|
|
children?: CategoryNode[];
|
|
categoryId?: string;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'app-project-view',
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
RouterOutlet,
|
|
MatSidenavModule,
|
|
MatTreeModule,
|
|
MatIconModule,
|
|
MatButtonModule,
|
|
MatSlideToggleModule,
|
|
MatToolbarModule,
|
|
MatProgressSpinnerModule,
|
|
MatDialogModule,
|
|
MatSnackBarModule
|
|
],
|
|
templateUrl: './project-view.component.html',
|
|
styleUrls: ['./project-view.component.scss']
|
|
})
|
|
export class ProjectViewComponent implements OnInit {
|
|
projectId = signal<string>('');
|
|
project = signal<any>(null);
|
|
categories = signal<Category[]>([]);
|
|
loading = signal(true);
|
|
treeData = signal<CategoryNode[]>([]);
|
|
|
|
constructor(
|
|
private route: ActivatedRoute,
|
|
private router: Router,
|
|
private apiService: ApiService,
|
|
private dialog: MatDialog,
|
|
private snackBar: MatSnackBar
|
|
) {}
|
|
|
|
ngOnInit() {
|
|
this.route.params.subscribe(params => {
|
|
this.projectId.set(params['projectId']);
|
|
this.loadProject();
|
|
this.loadCategories();
|
|
});
|
|
}
|
|
|
|
hasActiveRoute(): boolean {
|
|
return this.route.children.length > 0;
|
|
}
|
|
|
|
loadProject() {
|
|
// Load project details
|
|
this.apiService.getProjects().subscribe({
|
|
next: (projects) => {
|
|
const project = projects.find(p => p.id === this.projectId());
|
|
this.project.set(project);
|
|
},
|
|
error: (err) => {
|
|
console.error('Failed to load project', err);
|
|
}
|
|
});
|
|
}
|
|
|
|
loadCategories() {
|
|
this.loading.set(true);
|
|
this.apiService.getCategories(this.projectId()).subscribe({
|
|
next: (categories) => {
|
|
this.categories.set(categories);
|
|
this.buildTree();
|
|
this.loading.set(false);
|
|
},
|
|
error: (err) => {
|
|
console.error('Failed to load categories', err);
|
|
this.loading.set(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
buildTree() {
|
|
const tree: CategoryNode[] = this.categories().map(cat => ({
|
|
id: cat.id,
|
|
name: cat.name,
|
|
type: 'category' as const,
|
|
visible: cat.visible,
|
|
expanded: false,
|
|
children: (cat.subcategories || []).map(sub => ({
|
|
id: sub.id,
|
|
name: sub.name,
|
|
type: 'subcategory' as const,
|
|
visible: sub.visible,
|
|
categoryId: cat.id
|
|
}))
|
|
}));
|
|
this.treeData.set(tree);
|
|
}
|
|
|
|
toggleNode(node: CategoryNode) {
|
|
node.expanded = !node.expanded;
|
|
}
|
|
|
|
editNode(node: CategoryNode, event: Event) {
|
|
event.stopPropagation();
|
|
if (node.type === 'category') {
|
|
this.router.navigate(['category', node.id], { relativeTo: this.route });
|
|
} else {
|
|
this.router.navigate(['subcategory', node.id], { relativeTo: this.route });
|
|
}
|
|
}
|
|
|
|
toggleVisibility(node: CategoryNode, event: any) {
|
|
event.stopPropagation();
|
|
node.visible = !node.visible;
|
|
|
|
if (node.type === 'category') {
|
|
this.apiService.updateCategory(node.id, { visible: node.visible }).subscribe();
|
|
} else {
|
|
this.apiService.updateSubcategory(node.id, { visible: node.visible }).subscribe();
|
|
}
|
|
}
|
|
|
|
viewItems(node: CategoryNode, event: Event) {
|
|
event.stopPropagation();
|
|
if (node.type === 'subcategory') {
|
|
console.log('Navigating to items for subcategory:', node.id);
|
|
this.router.navigate(['/project', this.projectId(), 'items', node.id]);
|
|
}
|
|
}
|
|
|
|
goBack() {
|
|
this.router.navigate(['/']);
|
|
}
|
|
|
|
addCategory() {
|
|
const dialogRef = this.dialog.open(CreateDialogComponent, {
|
|
data: {
|
|
title: 'Create New Category',
|
|
type: 'category',
|
|
fields: [
|
|
{ name: 'name', label: 'Name', type: 'text', required: true },
|
|
{ name: 'priority', label: 'Priority', type: 'number', value: 99 },
|
|
{ name: 'visible', label: 'Visible', type: 'toggle', value: true }
|
|
]
|
|
}
|
|
});
|
|
|
|
dialogRef.afterClosed().subscribe(result => {
|
|
if (result) {
|
|
this.apiService.createCategory(this.projectId(), result).subscribe({
|
|
next: () => {
|
|
this.snackBar.open('Category created!', 'Close', { duration: 2000 });
|
|
this.loadCategories();
|
|
},
|
|
error: (err) => {
|
|
this.snackBar.open('Failed to create category', 'Close', { duration: 3000 });
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
deleteCategory(node: CategoryNode, event: Event) {
|
|
event.stopPropagation();
|
|
|
|
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
|
data: {
|
|
title: 'Delete Category',
|
|
message: `Are you sure you want to delete "${node.name}"? This will also delete all subcategories and items.`,
|
|
confirmText: 'Delete',
|
|
warning: true
|
|
}
|
|
});
|
|
|
|
dialogRef.afterClosed().subscribe(confirmed => {
|
|
if (confirmed) {
|
|
this.apiService.deleteCategory(node.id).subscribe({
|
|
next: () => {
|
|
this.snackBar.open('Category deleted', 'Close', { duration: 2000 });
|
|
this.loadCategories();
|
|
},
|
|
error: (err) => {
|
|
this.snackBar.open('Failed to delete category', 'Close', { duration: 3000 });
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
deleteSubcategory(node: CategoryNode, event: Event) {
|
|
event.stopPropagation();
|
|
|
|
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
|
data: {
|
|
title: 'Delete Subcategory',
|
|
message: `Are you sure you want to delete "${node.name}"? This will also delete all items.`,
|
|
confirmText: 'Delete',
|
|
cancelText: 'Cancel',
|
|
dangerous: true
|
|
}
|
|
});
|
|
|
|
dialogRef.afterClosed().subscribe(confirmed => {
|
|
if (confirmed) {
|
|
this.apiService.deleteSubcategory(node.id).subscribe({
|
|
next: () => {
|
|
this.snackBar.open('Subcategory deleted', 'Close', { duration: 2000 });
|
|
this.loadCategories();
|
|
},
|
|
error: (err) => {
|
|
this.snackBar.open('Failed to delete subcategory', 'Close', { duration: 3000 });
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|