2026-02-19 01:23:25 +04:00
|
|
|
|
import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy } from '@angular/core';
|
|
|
|
|
|
import { DecimalPipe } from '@angular/common';
|
2026-01-18 18:57:06 +04:00
|
|
|
|
import { FormsModule } from '@angular/forms';
|
|
|
|
|
|
import { ActivatedRoute, RouterLink } from '@angular/router';
|
2026-02-20 10:44:03 +04:00
|
|
|
|
import { ApiService, CartService, TelegramService, LanguageService } from '../../services';
|
|
|
|
|
|
import { Item, DescriptionField } from '../../models';
|
2026-01-18 18:57:06 +04:00
|
|
|
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
|
|
|
|
|
import { Subscription } from 'rxjs';
|
|
|
|
|
|
import { environment } from '../../../environments/environment';
|
2026-02-20 10:44:03 +04:00
|
|
|
|
import { getAllImages, getStockStatus, getBadgeClass, getTranslatedField } from '../../utils/item.utils';
|
2026-01-18 18:57:06 +04:00
|
|
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
|
selector: 'app-item-detail',
|
2026-02-19 01:23:25 +04:00
|
|
|
|
imports: [DecimalPipe, RouterLink, FormsModule],
|
2026-01-18 18:57:06 +04:00
|
|
|
|
templateUrl: './item-detail.component.html',
|
|
|
|
|
|
styleUrls: ['./item-detail.component.scss'],
|
|
|
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush
|
|
|
|
|
|
})
|
|
|
|
|
|
export class ItemDetailComponent implements OnInit, OnDestroy {
|
|
|
|
|
|
item = signal<Item | null>(null);
|
|
|
|
|
|
selectedPhotoIndex = signal(0);
|
|
|
|
|
|
loading = signal(true);
|
|
|
|
|
|
error = signal<string | null>(null);
|
|
|
|
|
|
isnovo = environment.theme === 'novo';
|
|
|
|
|
|
|
|
|
|
|
|
newReview = {
|
|
|
|
|
|
rating: 0,
|
|
|
|
|
|
comment: '',
|
|
|
|
|
|
anonymous: false
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
reviewSubmitStatus = signal<'idle' | 'loading' | 'success' | 'error'>('idle');
|
|
|
|
|
|
|
|
|
|
|
|
private routeSubscription?: Subscription;
|
|
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
|
private route: ActivatedRoute,
|
|
|
|
|
|
private apiService: ApiService,
|
|
|
|
|
|
private cartService: CartService,
|
|
|
|
|
|
private telegramService: TelegramService,
|
2026-02-20 10:44:03 +04:00
|
|
|
|
private sanitizer: DomSanitizer,
|
|
|
|
|
|
private languageService: LanguageService
|
2026-01-18 18:57:06 +04:00
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
|
|
ngOnInit(): void {
|
|
|
|
|
|
this.routeSubscription = this.route.params.subscribe(params => {
|
|
|
|
|
|
const id = parseInt(params['id'], 10);
|
|
|
|
|
|
this.loadItem(id);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ngOnDestroy(): void {
|
|
|
|
|
|
this.routeSubscription?.unsubscribe();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
loadItem(itemID: number): void {
|
|
|
|
|
|
this.loading.set(true);
|
|
|
|
|
|
|
|
|
|
|
|
this.apiService.getItem(itemID).subscribe({
|
|
|
|
|
|
next: (item) => {
|
|
|
|
|
|
this.item.set(item);
|
|
|
|
|
|
this.loading.set(false);
|
|
|
|
|
|
},
|
|
|
|
|
|
error: (err) => {
|
|
|
|
|
|
this.error.set('Failed to load item');
|
|
|
|
|
|
this.loading.set(false);
|
|
|
|
|
|
console.error('Error loading item:', err);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
selectPhoto(index: number): void {
|
|
|
|
|
|
this.selectedPhotoIndex.set(index);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
addToCart(): void {
|
|
|
|
|
|
const currentItem = this.item();
|
|
|
|
|
|
if (currentItem) {
|
|
|
|
|
|
this.cartService.addItem(currentItem.itemID);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getDiscountedPrice(): number {
|
|
|
|
|
|
const currentItem = this.item();
|
|
|
|
|
|
if (!currentItem) return 0;
|
2026-02-20 10:44:03 +04:00
|
|
|
|
return currentItem.price * (1 - (currentItem.discount || 0) / 100);
|
2026-01-18 18:57:06 +04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-20 10:44:03 +04:00
|
|
|
|
// 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 '';
|
|
|
|
|
|
const lang = this.languageService.currentLanguage();
|
|
|
|
|
|
return getTranslatedField(currentItem, 'simpleDescription', lang);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2026-01-18 18:57:06 +04:00
|
|
|
|
getSafeHtml(html: string): SafeHtml {
|
|
|
|
|
|
return this.sanitizer.sanitize(1, html) || '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getRatingStars(rating: number): string {
|
|
|
|
|
|
const fullStars = Math.floor(rating);
|
|
|
|
|
|
const hasHalfStar = rating % 1 >= 0.5;
|
2026-02-19 01:23:25 +04:00
|
|
|
|
let stars = '?'.repeat(fullStars);
|
|
|
|
|
|
if (hasHalfStar) stars += '?';
|
2026-01-18 18:57:06 +04:00
|
|
|
|
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));
|
|
|
|
|
|
|
2026-02-19 01:23:25 +04:00
|
|
|
|
if (diffDays === 0) return '<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>';
|
|
|
|
|
|
if (diffDays === 1) return '<27><><EFBFBD><EFBFBD><EFBFBD>';
|
|
|
|
|
|
if (diffDays < 7) return `${diffDays} <20><>. <20><><EFBFBD><EFBFBD><EFBFBD>`;
|
|
|
|
|
|
if (diffDays < 30) return `${Math.floor(diffDays / 7)} <20><><EFBFBD>. <20><><EFBFBD><EFBFBD><EFBFBD>`;
|
2026-01-18 18:57:06 +04:00
|
|
|
|
|
|
|
|
|
|
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()) {
|
2026-02-20 10:44:03 +04:00
|
|
|
|
return 'Пользователь';
|
2026-01-18 18:57:06 +04:00
|
|
|
|
}
|
|
|
|
|
|
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(),
|
|
|
|
|
|
username: this.newReview.anonymous ? null : this.getUserDisplayName(),
|
|
|
|
|
|
userId: this.telegramService.getUserId(),
|
|
|
|
|
|
timestamp: new Date().toISOString()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
this.apiService.submitReview(reviewData).subscribe({
|
|
|
|
|
|
next: (response) => {
|
|
|
|
|
|
this.reviewSubmitStatus.set('success');
|
|
|
|
|
|
this.newReview = { rating: 0, comment: '', anonymous: false };
|
|
|
|
|
|
|
2026-02-19 01:23:25 +04:00
|
|
|
|
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> 3 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
2026-01-18 18:57:06 +04:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.reviewSubmitStatus.set('idle');
|
|
|
|
|
|
}, 3000);
|
|
|
|
|
|
|
2026-02-19 01:23:25 +04:00
|
|
|
|
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
2026-01-18 18:57:06 +04:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.loadItem(currentItem.itemID);
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
},
|
|
|
|
|
|
error: (err) => {
|
|
|
|
|
|
console.error('Error submitting review:', err);
|
|
|
|
|
|
this.reviewSubmitStatus.set('error');
|
|
|
|
|
|
|
2026-02-19 01:23:25 +04:00
|
|
|
|
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> 5 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
2026-01-18 18:57:06 +04:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.reviewSubmitStatus.set('idle');
|
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|