import { Component, OnInit, OnDestroy, signal, computed, ChangeDetectionStrategy, inject } from '@angular/core'; import { DecimalPipe } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { ApiService, CartService, TelegramService, LanguageService, SeoService } from '../../services'; import { AuthService } from '../../services/auth.service'; import { Item, ItemDetail, DescriptionField } from '../../models'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { Subscription } from 'rxjs'; import { environment } from '../../../environments/environment'; import { SecurityContext } from '@angular/core'; import { getDiscountedPrice, getAllImages, getStockStatus, getBadgeClass, getTranslatedField } from '../../utils/item.utils'; import { LangRoutePipe } from '../../pipes/lang-route.pipe'; import { TranslatePipe } from '../../i18n/translate.pipe'; import { TranslateService } from '../../i18n/translate.service'; @Component({ selector: 'app-item-detail', imports: [DecimalPipe, RouterLink, FormsModule, LangRoutePipe, TranslatePipe], templateUrl: './item-detail.component.html', styleUrls: ['./item-detail.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class ItemDetailComponent implements OnInit, OnDestroy { item = signal(null); selectedPhotoIndex = signal(0); loading = signal(true); error = signal(null); isnovo = environment.theme === 'novo'; // Variant selection selectedColour = signal(null); selectedSize = signal(null); availableColours = computed(() => { const details = this.item()?.itemDetails; if (!details?.length) return [] as string[]; const unique = [...new Set(details.map(d => d.colour || d.color).filter((c): c is string => !!c))]; return unique; }); availableSizes = computed(() => { const details = this.item()?.itemDetails; if (!details?.length) return [] as string[]; // If a colour is selected, only show sizes available for that colour const colour = this.selectedColour(); const filtered = colour ? details.filter(d => (d.colour || d.color) === colour) : details; const unique = [...new Set(filtered.map(d => d.size).filter((s): s is string => !!s && s.toLowerCase() !== 'default'))]; return unique; }); selectedDetail = computed(() => { const details = this.item()?.itemDetails; if (!details?.length) return null; const colour = this.selectedColour(); const size = this.selectedSize(); return details.find(d => (!colour || (d.colour || d.color) === colour) && (!size || d.size === size) ) ?? null; }); effectivePrice = computed(() => { const detail = this.selectedDetail(); return detail?.price ?? this.item()?.price ?? 0; }); effectiveCurrency = computed(() => { const detail = this.selectedDetail(); return detail?.currency ?? this.item()?.currency ?? ''; }); effectiveRemaining = computed(() => { const detail = this.selectedDetail(); return detail?.remaining ?? this.item()?.quantity ?? null; }); newReview = { rating: 0, comment: '', anonymous: false }; reviewSubmitStatus = signal<'idle' | 'loading' | 'success' | 'error'>('idle'); private routeSubscription?: Subscription; private reviewResetTimeout?: ReturnType; private reviewErrorTimeout?: ReturnType; private reloadTimeout?: ReturnType; private seoService = inject(SeoService); private i18n = inject(TranslateService); private authService = inject(AuthService); constructor( private route: ActivatedRoute, private apiService: ApiService, private cartService: CartService, private telegramService: TelegramService, private sanitizer: DomSanitizer, private languageService: LanguageService ) {} ngOnInit(): void { this.routeSubscription = this.route.params.subscribe(params => { const id = parseInt(params['id'], 10); this.loadItem(id); }); } ngOnDestroy(): void { this.routeSubscription?.unsubscribe(); if (this.reviewResetTimeout) clearTimeout(this.reviewResetTimeout); if (this.reviewErrorTimeout) clearTimeout(this.reviewErrorTimeout); if (this.reloadTimeout) clearTimeout(this.reloadTimeout); this.seoService.resetToDefaults(); } loadItem(itemID: number): void { this.loading.set(true); this.apiService.getItem(itemID).subscribe({ next: (item) => { this.item.set(item); this.initVariantSelection(item); this.seoService.setItemMeta(item); this.loading.set(false); }, error: (err) => { this.error.set('Failed to load item'); this.loading.set(false); console.error('Error loading item:', err); } }); } private initVariantSelection(item: Item): void { // Auto-select the first available colour and size from itemDetails const details = item.itemDetails; if (details?.length) { const firstColour = details[0].colour || details[0].color || null; this.selectedColour.set(firstColour); const firstSize = details[0].size || null; this.selectedSize.set(firstSize); } else { this.selectedColour.set(item.colour ?? null); this.selectedSize.set(item.size ?? null); } } selectColour(colour: string): void { this.selectedColour.set(colour); // If current size is not available for the new colour, reset to first available const sizes = this.availableSizes(); if (sizes.length && this.selectedSize() && !sizes.includes(this.selectedSize()!)) { this.selectedSize.set(sizes[0]); } } selectSize(size: string): void { this.selectedSize.set(size); } selectPhoto(index: number): void { this.selectedPhotoIndex.set(index); } addToCart(): void { const currentItem = this.item(); if (currentItem) { this.cartService.addItem(currentItem.itemID, 1, { colour: this.selectedColour() ?? undefined, size: this.selectedSize() ?? undefined, price: this.effectivePrice(), currency: this.effectiveCurrency() }); } } getDiscountedPrice(): number { const currentItem = this.item(); if (!currentItem) return 0; const price = this.effectivePrice(); const discount = currentItem.discount || 0; return discount > 0 ? price * (1 - discount / 100) : price; } // BackOffice integration helpers getItemName(): string { const currentItem = this.item(); if (!currentItem) return ''; const lang = this.languageService.currentLanguage(); return getTranslatedField(currentItem, 'name', lang); } getSimpleDescription(): string { const currentItem = this.item(); if (!currentItem) return ''; return currentItem.simpleDescription || currentItem.description || ''; } hasDescriptionFields(): boolean { const currentItem = this.item(); return !!(currentItem?.descriptionFields && currentItem.descriptionFields.length > 0); } getTranslatedDescriptionFields(): DescriptionField[] { const currentItem = this.item(); if (!currentItem) return []; const lang = this.languageService.currentLanguage(); const translation = currentItem.translations?.[lang]; if (translation?.description && translation.description.length > 0) { return translation.description; } return currentItem.descriptionFields || []; } getStockClass(): string { const currentItem = this.item(); if (!currentItem) return 'high'; return getStockStatus(currentItem); } getStockLabel(): string { const status = this.getStockClass(); switch (status) { case 'high': return 'В наличии'; case 'medium': return 'Заканчивается'; case 'low': return 'Последние штуки'; case 'out': return 'Нет в наличии'; default: return 'В наличии'; } } readonly getBadgeClass = getBadgeClass; getSafeHtml(html: string): SafeHtml { return this.sanitizer.sanitize(SecurityContext.HTML, html) || ''; } getRatingStars(rating: number): string { const fullStars = Math.floor(rating); const hasHalfStar = rating % 1 >= 0.5; let stars = '★'.repeat(fullStars); if (hasHalfStar) stars += '☆'; return stars; } formatDate(timestamp: string): string { const date = new Date(timestamp); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffDays === 0) return this.i18n.t('itemDetail.today'); if (diffDays === 1) return this.i18n.t('itemDetail.yesterday'); if (diffDays < 7) return `${diffDays} ${this.i18n.t('itemDetail.daysAgo')}`; if (diffDays < 30) return `${Math.floor(diffDays / 7)} ${this.i18n.t('itemDetail.weeksAgo')}`; return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' }); } setRating(rating: number): void { this.newReview.rating = rating; } getUserDisplayName(): string | null { if (!this.telegramService.isTelegramApp()) { return this.i18n.t('itemDetail.defaultUser'); } return this.telegramService.getDisplayName(); } submitReview(): void { if (!this.newReview.rating || !this.newReview.comment.trim()) { return; } const currentItem = this.item(); if (!currentItem) return; this.reviewSubmitStatus.set('loading'); const reviewData = { itemID: currentItem.itemID, rating: this.newReview.rating, comment: this.newReview.comment.trim(), sessionID: this.authService.session()?.sessionId || '', timestamp: new Date().toISOString() }; this.apiService.submitReview(reviewData).subscribe({ next: (response) => { this.reviewSubmitStatus.set('success'); this.newReview = { rating: 0, comment: '', anonymous: false }; // Сброс состояния через 3 секунды this.reviewResetTimeout = setTimeout(() => { this.reviewSubmitStatus.set('idle'); }, 3000); // Перезагрузить данные товара после отправки отзыва this.reloadTimeout = setTimeout(() => { this.loadItem(currentItem.itemID); }, 500); }, error: (err) => { console.error('Error submitting review:', err); this.reviewSubmitStatus.set('error'); // Сброс состояния об ошибке через 5 секунд this.reviewErrorTimeout = setTimeout(() => { this.reviewSubmitStatus.set('idle'); }, 5000); } }); } }