fixing and styleing
This commit is contained in:
@@ -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) {
|
||||
<mat-hint>{{ field.hint }}</mat-hint>
|
||||
}
|
||||
@if (field.required && !formData[field.name]) {
|
||||
<mat-error>{{ field.label }} is required</mat-error>
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
gap: 1.5rem;
|
||||
background-color: #fff;
|
||||
border-radius: 0 0 8px 8px;
|
||||
gap: 1.5rem;
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<button mat-icon-button (click)="goBack()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<span class="toolbar-title">Items List</span>
|
||||
<span class="toolbar-title">{{ subcategoryName() || 'Items' }}</span>
|
||||
<span class="toolbar-spacer"></span>
|
||||
<button mat-mini-fab color="accent" (click)="addItem()">
|
||||
<mat-icon>add</mat-icon>
|
||||
@@ -133,4 +133,6 @@
|
||||
<p>No items found</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div #scrollSentinel class="scroll-sentinel"></div>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.toolbar-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.filters-bar {
|
||||
@@ -267,3 +271,8 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-sentinel {
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, signal, HostListener } from '@angular/core';
|
||||
import { Component, OnInit, AfterViewInit, OnDestroy, signal, ViewChild, ElementRef } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
@@ -39,14 +39,18 @@ import { ConfirmDialogComponent } from '../../components/confirm-dialog/confirm-
|
||||
templateUrl: './items-list.component.html',
|
||||
styleUrls: ['./items-list.component.scss']
|
||||
})
|
||||
export class ItemsListComponent implements OnInit {
|
||||
export class ItemsListComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
items = signal<Item[]>([]);
|
||||
loading = signal(false);
|
||||
hasMore = signal(true);
|
||||
hasMore = signal(false);
|
||||
page = signal(1);
|
||||
searchQuery = signal('');
|
||||
visibilityFilter = signal<boolean | undefined>(undefined);
|
||||
searchQuery = '';
|
||||
visibilityFilter: boolean | undefined = undefined;
|
||||
selectedItems = signal<Set<string>>(new Set());
|
||||
subcategoryName = signal<string>('');
|
||||
|
||||
@ViewChild('scrollSentinel') scrollSentinel!: ElementRef;
|
||||
private intersectionObserver?: IntersectionObserver;
|
||||
|
||||
subcategoryId = signal<string>('');
|
||||
projectId = signal<string>('');
|
||||
@@ -68,14 +72,17 @@ export class ItemsListComponent implements OnInit {
|
||||
|
||||
this.route.params.subscribe(params => {
|
||||
this.subcategoryId.set(params['subcategoryId']);
|
||||
this.page.set(1);
|
||||
this.items.set([]);
|
||||
this.selectedItems.set(new Set());
|
||||
this.subcategoryName.set('');
|
||||
this.loadSubcategoryName();
|
||||
this.loadItems();
|
||||
});
|
||||
}
|
||||
|
||||
loadItems(append = false) {
|
||||
if (this.loading() || (!append && this.items().length > 0)) {
|
||||
return;
|
||||
}
|
||||
if (this.loading()) return;
|
||||
|
||||
this.loading.set(true);
|
||||
const currentPage = append ? this.page() + 1 : 1;
|
||||
@@ -84,9 +91,9 @@ export class ItemsListComponent implements OnInit {
|
||||
this.subcategoryId(),
|
||||
currentPage,
|
||||
20,
|
||||
this.searchQuery() || undefined,
|
||||
this.searchQuery || undefined,
|
||||
{
|
||||
visible: this.visibilityFilter(),
|
||||
visible: this.visibilityFilter,
|
||||
tags: []
|
||||
}
|
||||
).subscribe({
|
||||
@@ -108,16 +115,31 @@ export class ItemsListComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('window:scroll', [])
|
||||
onScroll() {
|
||||
const scrollPosition = window.pageYOffset + window.innerHeight;
|
||||
const documentHeight = document.documentElement.scrollHeight;
|
||||
|
||||
if (scrollPosition >= documentHeight - 200 && this.hasMore() && !this.loading()) {
|
||||
this.loadItems(true);
|
||||
ngAfterViewInit() {
|
||||
this.intersectionObserver = new IntersectionObserver(
|
||||
entries => {
|
||||
if (entries[0].isIntersecting && this.hasMore() && !this.loading()) {
|
||||
this.loadItems(true);
|
||||
}
|
||||
},
|
||||
{ rootMargin: '200px', threshold: 0 }
|
||||
);
|
||||
if (this.scrollSentinel?.nativeElement) {
|
||||
this.intersectionObserver.observe(this.scrollSentinel.nativeElement);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.intersectionObserver?.disconnect();
|
||||
}
|
||||
|
||||
loadSubcategoryName() {
|
||||
this.apiService.getSubcategory(this.subcategoryId()).subscribe({
|
||||
next: (sub) => this.subcategoryName.set(sub.name),
|
||||
error: () => {}
|
||||
});
|
||||
}
|
||||
|
||||
onSearch() {
|
||||
this.page.set(1);
|
||||
this.items.set([]);
|
||||
@@ -205,6 +227,8 @@ export class ItemsListComponent implements OnInit {
|
||||
this.apiService.createItem(subcategoryId, result).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('Item created successfully', 'Close', { duration: 3000 });
|
||||
this.page.set(1);
|
||||
this.items.set([]);
|
||||
this.loadItems();
|
||||
},
|
||||
error: (err) => {
|
||||
@@ -234,6 +258,8 @@ export class ItemsListComponent implements OnInit {
|
||||
this.apiService.deleteItem(item.id).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('Item deleted successfully', 'Close', { duration: 3000 });
|
||||
this.page.set(1);
|
||||
this.items.set([]);
|
||||
this.loadItems();
|
||||
},
|
||||
error: (err) => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<button mat-icon-button (click)="goBack()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<span>Project: {{ projectId() }}</span>
|
||||
<span>{{ project()?.displayName || projectId() }}</span>
|
||||
</mat-toolbar>
|
||||
|
||||
<mat-sidenav-container class="sidenav-container">
|
||||
@@ -87,7 +87,7 @@
|
||||
</div>
|
||||
|
||||
<ng-template #subcategoryTree let-nodes="nodes" let-level="level">
|
||||
<div class="subcategories" [style.padding-left.rem]="level * 1.5">
|
||||
<div class="subcategories">
|
||||
@for (subNode of nodes; track subNode.id) {
|
||||
<div>
|
||||
<div class="node-content subcategory-node" [class.selected]="selectedNodeId() === subNode.id">
|
||||
|
||||
@@ -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 {
|
||||
@@ -69,6 +71,10 @@ mat-toolbar {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -15,11 +15,7 @@
|
||||
@if (saving()) {
|
||||
<span class="save-indicator">Saving...</span>
|
||||
}
|
||||
<button mat-raised-button color="accent" (click)="viewItems()">
|
||||
<mat-icon>list</mat-icon>
|
||||
View Items
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteSubcategory()">
|
||||
<button mat-icon-button color="warn" (click)="deleteSubcategory()" matTooltip="Delete Subcategory">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
@@ -109,10 +105,17 @@
|
||||
</div>
|
||||
|
||||
<div class="items-section">
|
||||
<button mat-raised-button color="primary" (click)="viewItems()">
|
||||
<mat-icon>list</mat-icon>
|
||||
View Items ({{ subcategory()!.itemCount || 0 }})
|
||||
</button>
|
||||
@if (subcategory()!.subcategories?.length) {
|
||||
<p class="no-items-note">
|
||||
<mat-icon>account_tree</mat-icon>
|
||||
This subcategory has child subcategories — items can only be added to leaf nodes.
|
||||
</p>
|
||||
} @else {
|
||||
<button mat-raised-button color="primary" (click)="viewItems()">
|
||||
<mat-icon>{{ subcategory()!.hasItems ? 'list' : 'add' }}</mat-icon>
|
||||
{{ subcategory()!.hasItems ? 'View Items (' + (subcategory()!.itemCount || 0) + ')' : 'Add Items' }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -103,9 +103,12 @@ export class ApiService {
|
||||
);
|
||||
}
|
||||
|
||||
createSubcategory(categoryId: string, data: Partial<Subcategory>): Observable<Subcategory> {
|
||||
if (environment.useMockData) return this.mockService.createSubcategory(categoryId, data);
|
||||
return this.http.post<Subcategory>(`${this.API_BASE}/categories/${categoryId}/subcategories`, data).pipe(
|
||||
createSubcategory(parentId: string, parentType: 'category' | 'subcategory', data: Partial<Subcategory>): Observable<Subcategory> {
|
||||
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<Subcategory>(endpoint, data).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user