diff --git a/src/app/components/create-dialog/create-dialog.component.ts b/src/app/components/create-dialog/create-dialog.component.ts index 4123536..7b5bdce 100644 --- a/src/app/components/create-dialog/create-dialog.component.ts +++ b/src/app/components/create-dialog/create-dialog.component.ts @@ -16,6 +16,7 @@ export interface CreateDialogData { type: 'text' | 'number' | 'toggle'; required?: boolean; value?: any; + hint?: string; }[]; } @@ -50,6 +51,9 @@ export interface CreateDialogData { [type]="field.type" [(ngModel)]="formData[field.name]" [required]="!!field.required"> + @if (field.hint) { + {{ field.hint }} + } @if (field.required && !formData[field.name]) { {{ field.label }} is required } diff --git a/src/app/models/category.model.ts b/src/app/models/category.model.ts index 1dfebd9..aef5c5a 100644 --- a/src/app/models/category.model.ts +++ b/src/app/models/category.model.ts @@ -14,7 +14,10 @@ export interface Subcategory { visible: boolean; priority: number; img?: string; + /** Root-level category this subcategory belongs to */ categoryId: string; + /** Direct parent ID — could be a category ID or a parent subcategory ID */ + parentId?: string; itemCount?: number; subcategories?: Subcategory[]; hasItems?: boolean; diff --git a/src/app/pages/category-editor/category-editor.component.ts b/src/app/pages/category-editor/category-editor.component.ts index 7f17c12..5ed9029 100644 --- a/src/app/pages/category-editor/category-editor.component.ts +++ b/src/app/pages/category-editor/category-editor.component.ts @@ -143,7 +143,7 @@ export class CategoryEditorComponent implements OnInit { dialogRef.afterClosed().subscribe(result => { if (result) { - this.apiService.createSubcategory(this.categoryId(), result).subscribe({ + this.apiService.createSubcategory(this.categoryId(), 'category', result).subscribe({ next: () => { this.snackBar.open('Subcategory created!', 'Close', { duration: 2000 }); this.loadCategory(); diff --git a/src/app/pages/item-editor/item-editor.component.scss b/src/app/pages/item-editor/item-editor.component.scss index ae67471..c5c83cb 100644 --- a/src/app/pages/item-editor/item-editor.component.scss +++ b/src/app/pages/item-editor/item-editor.component.scss @@ -61,7 +61,6 @@ gap: 1.5rem; background-color: #fff; border-radius: 0 0 8px 8px; - gap: 1.5rem; .full-width { width: 100%; diff --git a/src/app/pages/item-editor/item-editor.component.ts b/src/app/pages/item-editor/item-editor.component.ts index f1cbf1a..64b230e 100644 --- a/src/app/pages/item-editor/item-editor.component.ts +++ b/src/app/pages/item-editor/item-editor.component.ts @@ -125,17 +125,21 @@ export class ItemEditorComponent implements OnInit { pathSegments.unshift(subcategory.id); // Add to beginning - // Check if this subcategory has a parent subcategory or belongs to a category - if (subcategory.categoryId && subcategory.categoryId.startsWith('cat')) { - // This is directly under a category, add category and stop + // parentId = direct parent (another subcategory or the root category) + // categoryId = always the root category + const isDirectCategoryChild = + !subcategory.parentId || subcategory.parentId === subcategory.categoryId; + + if (isDirectCategoryChild) { + // This subcategory sits directly under a root category const category = await this.apiService.getCategory(subcategory.categoryId).toPromise(); if (category) { pathSegments.unshift(category.id); } break; } else { - // This is under another subcategory, continue traversing - currentSubcategoryId = subcategory.categoryId; + // Still inside a parent subcategory — keep traversing up + currentSubcategoryId = subcategory.parentId!; } } catch (err) { console.error('Error building path:', err); diff --git a/src/app/pages/items-list/items-list.component.html b/src/app/pages/items-list/items-list.component.html index fa43bbc..dd0dc18 100644 --- a/src/app/pages/items-list/items-list.component.html +++ b/src/app/pages/items-list/items-list.component.html @@ -3,7 +3,7 @@ - Items List + {{ subcategoryName() || 'Items' }} - Project: {{ projectId() }} + {{ project()?.displayName || projectId() }} @@ -87,7 +87,7 @@ -
+
@for (subNode of nodes; track subNode.id) {
diff --git a/src/app/pages/project-view/project-view.component.scss b/src/app/pages/project-view/project-view.component.scss index f4732ac..698ce02 100644 --- a/src/app/pages/project-view/project-view.component.scss +++ b/src/app/pages/project-view/project-view.component.scss @@ -15,7 +15,7 @@ mat-toolbar { } .categories-sidebar { - width: 380px; + width: 420px; border-right: 1px solid #e0e0e0; background-color: #fff; @@ -31,6 +31,8 @@ mat-toolbar { font-size: 1.25rem; font-weight: 500; } + + // icon centering handled globally via styles.scss } .loading-container { @@ -68,6 +70,10 @@ mat-toolbar { background-color: #bbdefb; border-left: 4px solid #1976d2; padding-left: calc(0.5rem - 4px); + + .node-actions { + opacity: 1; + } &:hover { background-color: #90caf9; @@ -85,13 +91,12 @@ mat-toolbar { } &.subcategory-node { - padding-left: 3rem; font-size: 0.95rem; background-color: #fff; - + &.selected { background-color: #bbdefb; - padding-left: calc(3rem - 4px); + border-left: 4px solid #1976d2; } } @@ -110,8 +115,8 @@ mat-toolbar { align-items: center; gap: 0.25rem; flex-shrink: 0; - opacity: 0.7; - transition: opacity 0.2s; + opacity: 0; + transition: opacity 0.15s; mat-slide-toggle { transform: scale(0.75); @@ -119,15 +124,16 @@ mat-toolbar { } button { - // width: 32px; - // height: 32px; - // line-height: 32px; - + --mdc-icon-button-state-layer-size: 30px; + --mdc-icon-button-icon-size: 18px; + width: 30px; + height: 30px; + padding: 0; + mat-icon { font-size: 18px; width: 18px; height: 18px; - line-height: 18px; } &:hover { @@ -143,6 +149,10 @@ mat-toolbar { .subcategories { background-color: #fafafa; + padding-left: 1rem; + border-left: 2px solid #e3e8ef; + margin-left: 1.25rem; + overflow: hidden; } } diff --git a/src/app/pages/project-view/project-view.component.ts b/src/app/pages/project-view/project-view.component.ts index b178728..8800601 100644 --- a/src/app/pages/project-view/project-view.component.ts +++ b/src/app/pages/project-view/project-view.component.ts @@ -253,8 +253,8 @@ export class ProjectViewComponent implements OnInit { return; } - const parentId = parentNode.type === 'category' ? parentNode.id : parentNode.id; - this.apiService.createSubcategory(parentId, result).subscribe({ + const parentType = parentNode.type === 'category' ? 'category' : 'subcategory'; + this.apiService.createSubcategory(parentNode.id, parentType, result).subscribe({ next: () => { this.snackBar.open('Subcategory created!', 'Close', { duration: 2000 }); this.loadCategories(); diff --git a/src/app/pages/subcategory-editor/subcategory-editor.component.html b/src/app/pages/subcategory-editor/subcategory-editor.component.html index bb42346..0818cb6 100644 --- a/src/app/pages/subcategory-editor/subcategory-editor.component.html +++ b/src/app/pages/subcategory-editor/subcategory-editor.component.html @@ -15,11 +15,7 @@ @if (saving()) { Saving... } - -
@@ -109,10 +105,17 @@
- + @if (subcategory()!.subcategories?.length) { +

+ account_tree + This subcategory has child subcategories — items can only be added to leaf nodes. +

+ } @else { + + }
} diff --git a/src/app/pages/subcategory-editor/subcategory-editor.component.scss b/src/app/pages/subcategory-editor/subcategory-editor.component.scss index aa2a05a..d92317b 100644 --- a/src/app/pages/subcategory-editor/subcategory-editor.component.scss +++ b/src/app/pages/subcategory-editor/subcategory-editor.component.scss @@ -115,4 +115,19 @@ align-items: center; gap: 0.5rem; } + + .no-items-note { + display: flex; + align-items: center; + gap: 0.5rem; + color: #888; + font-size: 0.875rem; + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + color: #bbb; + } + } } diff --git a/src/app/pages/subcategory-editor/subcategory-editor.component.ts b/src/app/pages/subcategory-editor/subcategory-editor.component.ts index 9b59273..e6e7b83 100644 --- a/src/app/pages/subcategory-editor/subcategory-editor.component.ts +++ b/src/app/pages/subcategory-editor/subcategory-editor.component.ts @@ -142,7 +142,12 @@ export class SubcategoryEditorComponent implements OnInit { this.apiService.deleteSubcategory(sub.id).subscribe({ next: () => { this.snackBar.open('Subcategory deleted successfully', 'Close', { duration: 3000 }); - this.router.navigate(['/project', this.projectId(), 'category', sub.categoryId]); + // Navigate to the direct parent (subcategory) if parentId exists, otherwise the root category + if (sub.parentId && sub.parentId !== sub.categoryId) { + this.router.navigate(['/project', this.projectId(), 'subcategory', sub.parentId]); + } else { + this.router.navigate(['/project', this.projectId(), 'category', sub.categoryId]); + } }, error: (err: any) => { console.error('Error deleting subcategory:', err); diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index 4bb7adf..1ccbf96 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -103,9 +103,12 @@ export class ApiService { ); } - createSubcategory(categoryId: string, data: Partial): Observable { - if (environment.useMockData) return this.mockService.createSubcategory(categoryId, data); - return this.http.post(`${this.API_BASE}/categories/${categoryId}/subcategories`, data).pipe( + createSubcategory(parentId: string, parentType: 'category' | 'subcategory', data: Partial): Observable { + if (environment.useMockData) return this.mockService.createSubcategory(parentId, data); + const endpoint = parentType === 'category' + ? `${this.API_BASE}/categories/${parentId}/subcategories` + : `${this.API_BASE}/subcategories/${parentId}/subcategories`; + return this.http.post(endpoint, data).pipe( catchError(this.handleError) ); } diff --git a/src/app/services/mock-data.service.ts b/src/app/services/mock-data.service.ts index 3f5c31e..77c4744 100644 --- a/src/app/services/mock-data.service.ts +++ b/src/app/services/mock-data.service.ts @@ -273,7 +273,8 @@ export class MockDataService { visible: data.visible ?? true, priority: data.priority || 99, img: data.img, - categoryId: parentId, + categoryId: parentId, // will be root category ID after backend resolves; mock keeps direct parent for simplicity + parentId: parentId, itemCount: 0 }; diff --git a/src/styles.scss b/src/styles.scss index 9d7f90b..e0a6531 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -22,15 +22,41 @@ button[mat-raised-button], button[mat-flat-button] { letter-spacing: 0.5px; } +// Icon buttons — always center the icon in its circle button[mat-icon-button] { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + + .mat-icon { + display: flex !important; + align-items: center !important; + justify-content: center !important; + margin: 0 !important; + padding: 0 !important; + line-height: 1 !important; + } + &:hover { background-color: rgba(0, 0, 0, 0.04); } } button[mat-mini-fab] { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; box-shadow: 0 3px 8px rgba(0, 0, 0, 0.25) !important; - + + .mat-icon { + display: flex !important; + align-items: center !important; + justify-content: center !important; + margin: 0 !important; + padding: 0 !important; + line-height: 1 !important; + } + &:hover { box-shadow: 0 5px 12px rgba(0, 0, 0, 0.35) !important; } @@ -55,8 +81,8 @@ mat-card { // Scrollbar styling ::-webkit-scrollbar { - width: 10px; - height: 10px; + width: 6px; + height: 6px; } ::-webkit-scrollbar-track { @@ -64,11 +90,11 @@ mat-card { } ::-webkit-scrollbar-thumb { - background: #888; - border-radius: 5px; - + background: #aaa; + border-radius: 6px; + &:hover { - background: #555; + background: #666; } } @@ -85,6 +111,11 @@ mat-card { --mat-snack-bar-button-color: white !important; } +// Common editor layout helpers +.editor-page { + padding: 1.5rem 2rem; +} + .toast-warning { --mdc-snackbar-container-color: #ff9800 !important; --mdc-snackbar-supporting-text-color: white !important;