diff --git a/src/app/components/inline-items-list/inline-items-list.component.html b/src/app/components/inline-items-list/inline-items-list.component.html new file mode 100644 index 0000000..36b862d --- /dev/null +++ b/src/app/components/inline-items-list/inline-items-list.component.html @@ -0,0 +1,73 @@ +
+
+ {{ totalCount() }} {{ 'ITEMS_COUNT' | translate }} + +
+ + @if (items().length > 0) { +
+ @for (item of items(); track item.id) { +
+
+ @if (item.imgs.length) { + + } +
+ image +
+ @if (item.quantity === 0) { +
{{ 'OUT_OF_STOCK' | translate }}
+ } +
+ +
+ {{ item.name }} +
+ {{ item.price }} {{ item.currency }} + @if (item.discount > 0) { + -{{ item.discount }}% + } +
+
+ {{ 'QTY' | translate }}: {{ item.quantity }} + + {{ item.visible ? 'visibility' : 'visibility_off' }} + +
+
+ + +
+ } +
+ } + + @if (loading()) { +
+ + {{ 'LOADING_MORE' | translate }} +
+ } + + @if (!loading() && items().length === 0) { +
+ inventory_2 + {{ 'NO_ITEMS_FOUND' | translate }} +
+ } + + @if (!hasMore() && items().length > 0 && !loading()) { +
{{ 'NO_MORE_ITEMS' | translate }}
+ } + +
+
diff --git a/src/app/components/inline-items-list/inline-items-list.component.scss b/src/app/components/inline-items-list/inline-items-list.component.scss new file mode 100644 index 0000000..60549eb --- /dev/null +++ b/src/app/components/inline-items-list/inline-items-list.component.scss @@ -0,0 +1,207 @@ +.inline-items { + width: 100%; +} + +.inline-items-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 0; + margin-bottom: 0.75rem; + + .items-count { + font-size: 0.85rem; + color: #666; + font-weight: 500; + } +} + +.inline-items-grid { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.inline-item-card { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 0.75rem; + border: 1px solid #e0e0e0; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; + background: #fff; + position: relative; + + &:hover { + border-color: #1976d2; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + + .delete-btn { + opacity: 1; + } + } + + &.hidden-item { + opacity: 0.6; + background: #fafafa; + } + + .delete-btn { + opacity: 0; + transition: opacity 0.2s; + flex-shrink: 0; + } +} + +.inline-item-image { + width: 48px; + height: 48px; + flex-shrink: 0; + border-radius: 6px; + overflow: hidden; + background: #f5f5f5; + display: flex; + align-items: center; + justify-content: center; + position: relative; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .no-image { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + color: #ccc; + + mat-icon { + font-size: 24px; + width: 24px; + height: 24px; + } + } + + .out-of-stock { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.5); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.5rem; + font-weight: 600; + text-transform: uppercase; + } +} + +.inline-item-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.15rem; + + .item-name { + font-size: 0.9rem; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: #333; + } + + .item-details { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8rem; + + .price { + font-weight: 600; + color: #1976d2; + } + + .discount { + padding: 1px 5px; + border-radius: 3px; + background: #e53935; + color: #fff; + font-size: 0.7rem; + font-weight: 600; + } + } + + .item-meta { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.75rem; + color: #999; + + .qty { + color: #666; + } + + .visibility-icon { + font-size: 16px; + width: 16px; + height: 16px; + + &.visible { color: #4caf50; } + &.not-visible { color: #f44336; } + } + } +} + +.inline-loading { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + padding: 1rem; + + span { + color: #666; + font-size: 0.8rem; + } +} + +.inline-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2rem 1rem; + color: #999; + + mat-icon { + font-size: 36px; + width: 36px; + height: 36px; + margin-bottom: 0.5rem; + } + + span { + font-size: 0.85rem; + } +} + +.inline-end { + text-align: center; + padding: 0.75rem; + color: #bbb; + font-size: 0.8rem; +} + +.scroll-sentinel { + height: 2px; + width: 100%; +} diff --git a/src/app/components/inline-items-list/inline-items-list.component.ts b/src/app/components/inline-items-list/inline-items-list.component.ts new file mode 100644 index 0000000..fb01a81 --- /dev/null +++ b/src/app/components/inline-items-list/inline-items-list.component.ts @@ -0,0 +1,201 @@ +import { + Component, Input, OnChanges, SimpleChanges, AfterViewInit, OnDestroy, + signal, ViewChild, ElementRef, DestroyRef, inject +} from '@angular/core'; +import { Router } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatDialog, MatDialogModule } from '@angular/material/dialog'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { ApiService } from '../../services'; +import { ToastService } from '../../services/toast.service'; +import { Item } from '../../models'; +import { CreateDialogComponent } from '../create-dialog/create-dialog.component'; +import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'; +import { LanguageService } from '../../services/language.service'; +import { TranslatePipe } from '../../pipes/translate.pipe'; + +@Component({ + selector: 'app-inline-items-list', + standalone: true, + imports: [ + CommonModule, + MatButtonModule, + MatIconModule, + MatProgressSpinnerModule, + MatChipsModule, + MatDialogModule, + MatTooltipModule, + TranslatePipe + ], + templateUrl: './inline-items-list.component.html', + styleUrls: ['./inline-items-list.component.scss'] +}) +export class InlineItemsListComponent implements OnChanges, AfterViewInit, OnDestroy { + @Input({ required: true }) subcategoryId!: string; + @Input({ required: true }) projectId!: string; + + items = signal([]); + loading = signal(false); + hasMore = signal(false); + page = signal(1); + totalCount = signal(0); + + @ViewChild('scrollSentinel') scrollSentinel!: ElementRef; + private intersectionObserver?: IntersectionObserver; + private destroyRef = inject(DestroyRef); + + constructor( + private router: Router, + private apiService: ApiService, + private toast: ToastService, + private dialog: MatDialog, + public lang: LanguageService + ) {} + + ngOnChanges(changes: SimpleChanges) { + if (changes['subcategoryId'] && this.subcategoryId) { + this.page.set(1); + this.items.set([]); + this.loadItems(); + } + } + + ngAfterViewInit() { + this.setupObserver(); + } + + ngOnDestroy() { + this.intersectionObserver?.disconnect(); + } + + private setupObserver() { + this.intersectionObserver?.disconnect(); + this.intersectionObserver = new IntersectionObserver( + entries => { + if (entries[0].isIntersecting && this.hasMore() && !this.loading()) { + this.loadItems(true); + } + }, + { rootMargin: '100px', threshold: 0 } + ); + if (this.scrollSentinel?.nativeElement) { + this.intersectionObserver.observe(this.scrollSentinel.nativeElement); + } + } + + loadItems(append = false) { + if (this.loading()) return; + + this.loading.set(true); + const currentPage = append ? this.page() + 1 : 1; + + this.apiService.getItems(this.subcategoryId, currentPage, 20).subscribe({ + next: (response) => { + if (append) { + this.items.set([...this.items(), ...response.items]); + } else { + this.items.set(response.items); + } + this.page.set(currentPage); + this.hasMore.set(response.hasMore); + this.totalCount.set(response.total); + this.loading.set(false); + }, + error: () => { + this.toast.error(this.lang.t('FAILED_LOAD_ITEMS')); + this.loading.set(false); + } + }); + } + + openItem(itemId: string) { + this.router.navigate(['/project', this.projectId, 'item', itemId]); + } + + addItem() { + const dialogRef = this.dialog.open(CreateDialogComponent, { + width: '500px', + data: { + title: this.lang.t('CREATE_NEW_ITEM'), + fields: [ + { name: 'name', label: this.lang.t('ITEM_NAME'), type: 'text', required: true }, + { name: 'simpleDescription', label: this.lang.t('SIMPLE_DESCRIPTION'), type: 'text', required: false }, + { name: 'price', label: this.lang.t('PRICE'), type: 'number', required: true }, + { + name: 'currency', label: this.lang.t('CURRENCY'), type: 'select', required: true, value: 'USD', + options: [ + { value: 'USD', label: 'πŸ‡ΊπŸ‡Έ USD' }, + { value: 'EUR', label: 'πŸ‡ͺπŸ‡Ί EUR' }, + { value: 'RUB', label: 'πŸ‡·πŸ‡Ί RUB' }, + { value: 'GBP', label: 'πŸ‡¬πŸ‡§ GBP' }, + { value: 'UAH', label: 'πŸ‡ΊπŸ‡¦ UAH' } + ] + }, + { name: 'quantity', label: this.lang.t('QUANTITY'), type: 'number', required: true, value: 0 }, + { name: 'visible', label: this.lang.t('VISIBLE'), type: 'toggle', required: false, value: true } + ] + } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.apiService.createItem(this.subcategoryId, result).subscribe({ + next: () => { + this.toast.success(this.lang.t('ITEM_CREATED')); + this.page.set(1); + this.items.set([]); + this.loadItems(); + }, + error: () => { + this.toast.error(this.lang.t('FAILED_CREATE_ITEM')); + } + }); + } + }); + } + + deleteItem(item: Item, event: Event) { + event.stopPropagation(); + + const dialogRef = this.dialog.open(ConfirmDialogComponent, { + data: { + title: this.lang.t('DELETE_ITEM'), + message: `${this.lang.t('CONFIRM_DELETE')} "${item.name}"?`, + confirmText: this.lang.t('DELETE'), + cancelText: this.lang.t('CANCEL'), + dangerous: true + } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.apiService.deleteItem(item.id).subscribe({ + next: () => { + this.toast.success(this.lang.t('ITEM_DELETED')); + this.page.set(1); + this.items.set([]); + this.loadItems(); + }, + error: () => { + this.toast.error(this.lang.t('FAILED_DELETE_ITEM')); + } + }); + } + }); + } + + onImageError(event: Event) { + const img = event.target as HTMLImageElement; + img.style.display = 'none'; + const parent = img.parentElement; + if (parent) { + const placeholder = parent.querySelector('.no-image') as HTMLElement | null; + if (placeholder) placeholder.style.display = 'flex'; + } + } +} diff --git a/src/app/components/loading-skeleton/loading-skeleton.component.ts b/src/app/components/loading-skeleton/loading-skeleton.component.ts index 22e9546..3574f5a 100644 --- a/src/app/components/loading-skeleton/loading-skeleton.component.ts +++ b/src/app/components/loading-skeleton/loading-skeleton.component.ts @@ -12,7 +12,7 @@ import { CommonModule } from '@angular/common'; @for (item of [1,2,3,4,5]; track item) {
-
+
} @@ -159,8 +159,8 @@ import { CommonModule } from '@angular/common'; export class LoadingSkeletonComponent { @Input() type: 'tree' | 'card' | 'list' | 'form' = 'list'; - getRandomWidth(): string { - const widths = ['60%', '70%', '80%', '90%']; - return widths[Math.floor(Math.random() * widths.length)]; - } + /** Pre-computed widths so they don't change between CD cycles (NG0100). */ + readonly treeWidths = [1, 2, 3, 4, 5].map( + (_, i) => ['60%', '80%', '70%', '90%', '75%'][i] + ); } diff --git a/src/app/pages/category-editor/category-editor.component.html b/src/app/pages/category-editor/category-editor.component.html index aae8c2c..c74876b 100644 --- a/src/app/pages/category-editor/category-editor.component.html +++ b/src/app/pages/category-editor/category-editor.component.html @@ -87,22 +87,34 @@ @if (category()!.subcategories?.length) { - + @for (sub of category()!.subcategories; track sub.id) { - - {{ sub.name }} - {{ 'PRIORITY' | translate }}: {{ sub.priority }} - - + + + + folder + {{ sub.name }} + + + {{ 'PRIORITY' | translate }}: {{ sub.priority }} + + + + + + } - + } @else {

{{ 'NO_SUBCATEGORIES' | translate }}

} + } diff --git a/src/app/pages/category-editor/category-editor.component.scss b/src/app/pages/category-editor/category-editor.component.scss index d0b8ed0..1c9aaac 100644 --- a/src/app/pages/category-editor/category-editor.component.scss +++ b/src/app/pages/category-editor/category-editor.component.scss @@ -124,12 +124,29 @@ } } - mat-list-item { - cursor: pointer; - border-bottom: 1px solid #f0f0f0; + mat-accordion { + display: block; - &:hover { - background-color: #f5f5f5; + mat-expansion-panel { + margin-bottom: 0.25rem; + + .sub-icon { + margin-right: 0.5rem; + font-size: 20px; + width: 20px; + height: 20px; + color: #666; + } + } + + mat-panel-description { + align-items: center; + justify-content: flex-end; + gap: 0.5rem; + + button { + margin-right: -8px; + } } } diff --git a/src/app/pages/category-editor/category-editor.component.ts b/src/app/pages/category-editor/category-editor.component.ts index 0f45bc2..b439500 100644 --- a/src/app/pages/category-editor/category-editor.component.ts +++ b/src/app/pages/category-editor/category-editor.component.ts @@ -11,6 +11,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatListModule } from '@angular/material/list'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatExpansionModule } from '@angular/material/expansion'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { ApiService } from '../../services'; import { ToastService } from '../../services/toast.service'; @@ -18,6 +19,7 @@ import { Category } from '../../models'; import { LoadingSkeletonComponent } from '../../components/loading-skeleton/loading-skeleton.component'; import { CreateDialogComponent } from '../../components/create-dialog/create-dialog.component'; import { ConfirmDialogComponent } from '../../components/confirm-dialog/confirm-dialog.component'; +import { InlineItemsListComponent } from '../../components/inline-items-list/inline-items-list.component'; import { LanguageService } from '../../services/language.service'; import { TranslatePipe } from '../../pipes/translate.pipe'; @@ -36,7 +38,9 @@ import { TranslatePipe } from '../../pipes/translate.pipe'; MatListModule, MatDialogModule, MatTooltipModule, + MatExpansionModule, LoadingSkeletonComponent, + InlineItemsListComponent, TranslatePipe ], templateUrl: './category-editor.component.html', diff --git a/src/app/pages/item-editor/item-editor.component.html b/src/app/pages/item-editor/item-editor.component.html index 6c8d48e..cd2a91c 100644 --- a/src/app/pages/item-editor/item-editor.component.html +++ b/src/app/pages/item-editor/item-editor.component.html @@ -395,7 +395,7 @@ - + } diff --git a/src/app/pages/item-editor/item-editor.component.ts b/src/app/pages/item-editor/item-editor.component.ts index 3c4f34e..1c8b7a4 100644 --- a/src/app/pages/item-editor/item-editor.component.ts +++ b/src/app/pages/item-editor/item-editor.component.ts @@ -113,6 +113,11 @@ export class ItemEditorComponent implements OnInit { this.loading.set(true); this.apiService.getItem(this.itemId()).subscribe({ next: (item) => { + if (!item) { + this.toast.error(this.lang.t('ITEM_NOT_FOUND')); + this.loading.set(false); + return; + } this.item.set(item); // Initialise Russian translation buffers const ru = item.translations?.['ru']; diff --git a/src/app/pages/subcategory-editor/subcategory-editor.component.html b/src/app/pages/subcategory-editor/subcategory-editor.component.html index 4b194e9..9df872f 100644 --- a/src/app/pages/subcategory-editor/subcategory-editor.component.html +++ b/src/app/pages/subcategory-editor/subcategory-editor.component.html @@ -99,19 +99,21 @@
+

{{ 'VIEW_ITEMS' | translate }}

@if (subcategory()!.subcategories?.length) {

account_tree {{ 'SUBCATEGORIES' | translate }}

} @else { - + + }
+ } diff --git a/src/app/pages/subcategory-editor/subcategory-editor.component.scss b/src/app/pages/subcategory-editor/subcategory-editor.component.scss index f382122..9fc946d 100644 --- a/src/app/pages/subcategory-editor/subcategory-editor.component.scss +++ b/src/app/pages/subcategory-editor/subcategory-editor.component.scss @@ -110,10 +110,10 @@ padding-top: 1rem; border-top: 1px solid #e0e0e0; - button { - display: flex; - align-items: center; - gap: 0.5rem; + h3 { + margin: 0 0 0.75rem 0; + font-size: 1.125rem; + font-weight: 500; } .no-items-note { diff --git a/src/app/pages/subcategory-editor/subcategory-editor.component.ts b/src/app/pages/subcategory-editor/subcategory-editor.component.ts index 4dfa203..958c749 100644 --- a/src/app/pages/subcategory-editor/subcategory-editor.component.ts +++ b/src/app/pages/subcategory-editor/subcategory-editor.component.ts @@ -16,6 +16,7 @@ import { ToastService } from '../../services/toast.service'; import { Subcategory } from '../../models'; import { LoadingSkeletonComponent } from '../../components/loading-skeleton/loading-skeleton.component'; import { ConfirmDialogComponent } from '../../components/confirm-dialog/confirm-dialog.component'; +import { InlineItemsListComponent } from '../../components/inline-items-list/inline-items-list.component'; import { LanguageService } from '../../services/language.service'; import { TranslatePipe } from '../../pipes/translate.pipe'; @@ -34,6 +35,7 @@ import { TranslatePipe } from '../../pipes/translate.pipe'; MatDialogModule, MatTooltipModule, LoadingSkeletonComponent, + InlineItemsListComponent, TranslatePipe ], templateUrl: './subcategory-editor.component.html', diff --git a/src/app/services/mock-data.service.ts b/src/app/services/mock-data.service.ts index 3211f6c..1cf1415 100644 --- a/src/app/services/mock-data.service.ts +++ b/src/app/services/mock-data.service.ts @@ -12,14 +12,14 @@ export class MockDataService { name: 'dexar', displayName: 'Dexar Marketplace', active: true, - logoUrl: 'https://via.placeholder.com/150?text=Dexar' + logoUrl: 'https://placehold.co/150?text=Dexar' }, { id: 'novo', name: 'novo', displayName: 'Novo Shop', active: true, - logoUrl: 'https://via.placeholder.com/150?text=Novo' + logoUrl: 'https://placehold.co/150?text=Novo' } ]; @@ -29,7 +29,7 @@ export class MockDataService { name: 'Electronics', visible: true, priority: 1, - img: 'https://via.placeholder.com/400x300?text=Electronics', + img: 'https://placehold.co/400x300?text=Electronics', projectId: 'dexar', subcategories: [ { @@ -37,7 +37,7 @@ export class MockDataService { name: 'Smartphones', visible: true, priority: 1, - img: 'https://via.placeholder.com/400x300?text=Smartphones', + img: 'https://placehold.co/400x300?text=Smartphones', categoryId: 'cat1', itemCount: 15 }, @@ -46,7 +46,7 @@ export class MockDataService { name: 'Laptops', visible: true, priority: 2, - img: 'https://via.placeholder.com/400x300?text=Laptops', + img: 'https://placehold.co/400x300?text=Laptops', categoryId: 'cat1', itemCount: 12 } @@ -57,7 +57,7 @@ export class MockDataService { name: 'Clothing', visible: true, priority: 2, - img: 'https://via.placeholder.com/400x300?text=Clothing', + img: 'https://placehold.co/400x300?text=Clothing', projectId: 'dexar', subcategories: [ { @@ -65,7 +65,7 @@ export class MockDataService { name: 'Men', visible: true, priority: 1, - img: 'https://via.placeholder.com/400x300?text=Men', + img: 'https://placehold.co/400x300?text=Men', categoryId: 'cat2', itemCount: 25 } @@ -76,7 +76,7 @@ export class MockDataService { name: 'Home & Garden', visible: false, priority: 3, - img: 'https://via.placeholder.com/400x300?text=Home', + img: 'https://placehold.co/400x300?text=Home', projectId: 'novo', subcategories: [] } @@ -93,8 +93,8 @@ export class MockDataService { discount: 0, currency: 'USD', imgs: [ - 'https://via.placeholder.com/600x400?text=iPhone+Front', - 'https://via.placeholder.com/600x400?text=iPhone+Back' + 'https://placehold.co/600x400?text=iPhone+Front', + 'https://placehold.co/600x400?text=iPhone+Back' ], tags: ['new', 'featured', 'bestseller'], badges: ['new', 'featured'], @@ -124,7 +124,7 @@ export class MockDataService { price: 1199, discount: 10, currency: 'USD', - imgs: ['https://via.placeholder.com/600x400?text=Samsung+S24'], + imgs: ['https://placehold.co/600x400?text=Samsung+S24'], tags: ['new', 'android'], badges: ['new'], simpleDescription: 'Premium Samsung flagship with S Pen', @@ -144,7 +144,7 @@ export class MockDataService { price: 999, discount: 15, currency: 'USD', - imgs: ['https://via.placeholder.com/600x400?text=Pixel+8'], + imgs: ['https://placehold.co/600x400?text=Pixel+8'], tags: ['sale', 'android', 'ai'], badges: ['sale', 'hot'], simpleDescription: 'Best AI photography phone', @@ -163,7 +163,7 @@ export class MockDataService { price: 2499, discount: 0, currency: 'USD', - imgs: ['https://via.placeholder.com/600x400?text=MacBook'], + imgs: ['https://placehold.co/600x400?text=MacBook'], tags: ['featured', 'professional'], badges: ['exclusive'], simpleDescription: 'Powerful laptop for professionals', @@ -183,7 +183,7 @@ export class MockDataService { price: 1799, discount: 5, currency: 'USD', - imgs: ['https://via.placeholder.com/600x400?text=Dell+XPS'], + imgs: ['https://placehold.co/600x400?text=Dell+XPS'], tags: ['out-of-stock'], simpleDescription: 'Premium Windows laptop', description: [ @@ -213,7 +213,7 @@ export class MockDataService { price: ((i * 13) % 1000) + 100, discount: i % 3 === 0 ? (i * 5) % 30 + 5 : 0, currency: 'USD', - imgs: [`https://via.placeholder.com/600x400?text=Product+${i}`], + imgs: [`https://placehold.co/600x400?text=Product+${i}`], tags: ['test'], simpleDescription: `This is test product number ${i}`, description: [{ key: 'Size', value: 'Medium' }], @@ -399,14 +399,26 @@ export class MockDataService { } getItem(itemId: string): Observable { - const item = this.items.find(i => i.id === itemId)!; - return of(item).pipe(delay(200)); + let item = this.items.find(i => i.id === itemId); + if (!item) { + for (const generated of this.generatedItems.values()) { + item = generated.find(i => i.id === itemId); + if (item) break; + } + } + return of(item!).pipe(delay(200)); } updateItem(itemId: string, data: Partial): Observable { - const item = this.items.find(i => i.id === itemId)!; - Object.assign(item, data); - return of(item).pipe(delay(300)); + let item = this.items.find(i => i.id === itemId); + if (!item) { + for (const generated of this.generatedItems.values()) { + item = generated.find(i => i.id === itemId); + if (item) break; + } + } + if (item) Object.assign(item, data); + return of(item!).pipe(delay(300)); } createItem(subcategoryId: string, data: Partial): Observable { @@ -438,7 +450,6 @@ export class MockDataService { } deleteItem(itemId: string): Observable { - const item = this.items.find(i => i.id === itemId); const index = this.items.findIndex(i => i.id === itemId); if (index > -1) { const subcategoryId = this.items[index].subcategoryId; @@ -452,13 +463,28 @@ export class MockDataService { subcategory.hasItems = false; } } + } else { + // Also remove from generated items cache + for (const [key, generated] of this.generatedItems.entries()) { + const gi = generated.findIndex(i => i.id === itemId); + if (gi > -1) { + generated.splice(gi, 1); + break; + } + } } return of(void 0).pipe(delay(300)); } bulkUpdateItems(itemIds: string[], data: Partial): Observable { itemIds.forEach(id => { - const item = this.items.find(i => i.id === id); + let item = this.items.find(i => i.id === id); + if (!item) { + for (const generated of this.generatedItems.values()) { + item = generated.find(i => i.id === id); + if (item) break; + } + } if (item) Object.assign(item, data); }); return of(void 0).pipe(delay(400)); @@ -466,7 +492,7 @@ export class MockDataService { uploadImage(file: File): Observable<{ url: string }> { // Simulate upload - const url = `https://via.placeholder.com/600x400?text=${encodeURIComponent(file.name)}`; + const url = `https://placehold.co/600x400?text=${encodeURIComponent(file.name)}`; return of({ url }).pipe(delay(1000)); } }