merge: B2B into main
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,10 +51,10 @@
|
|||||||
|
|
||||||
<div class="product-price">
|
<div class="product-price">
|
||||||
@if (item.discount > 0) {
|
@if (item.discount > 0) {
|
||||||
<span class="original-price">{{ item.price | number:'1.2-2' }} {{ item.currency }}</span>
|
<span class="original-price">{{ item.price | currencyConvert:item.currency | number:'1.2-2' }} {{ displayCurrency() }}</span>
|
||||||
<span class="discounted-price">{{ getDiscountedPrice(item) | number:'1.2-2' }} {{ item.currency }}</span>
|
<span class="discounted-price">{{ getDiscountedPrice(item) | currencyConvert:item.currency | number:'1.2-2' }} {{ displayCurrency() }}</span>
|
||||||
} @else {
|
} @else {
|
||||||
<span class="current-price">{{ item.price | number:'1.2-2' }} {{ item.currency }}</span>
|
<span class="current-price">{{ item.price | currencyConvert:item.currency | number:'1.2-2' }} {{ displayCurrency() }}</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, inject } from '@angular/core';
|
||||||
import { DecimalPipe } from '@angular/common';
|
import { DecimalPipe } from '@angular/common';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { Product } from '../../core/products/models/product-domain.model';
|
import { Product } from '../../core/products/models/product-domain.model';
|
||||||
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
||||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||||
|
import { CurrencyConvertPipe } from '../../pipes/currency-convert.pipe';
|
||||||
|
import { LanguageService } from '../../services/language.service';
|
||||||
import { cleanDescription, getBadgeClass, getDiscountedPrice, getMainImage, onImageError } from '../../utils/item.utils';
|
import { cleanDescription, getBadgeClass, getDiscountedPrice, getMainImage, onImageError } from '../../utils/item.utils';
|
||||||
|
|
||||||
const STOCK_LABEL_KEYS: Record<string, string> = {
|
const STOCK_LABEL_KEYS: Record<string, string> = {
|
||||||
@@ -18,12 +20,15 @@ export type ProductCardAppearance = 'standard' | 'compact';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-product-card',
|
selector: 'app-product-card',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [DecimalPipe, RouterLink, LangRoutePipe, TranslatePipe],
|
imports: [DecimalPipe, RouterLink, LangRoutePipe, TranslatePipe, CurrencyConvertPipe],
|
||||||
templateUrl: './product-card.component.html',
|
templateUrl: './product-card.component.html',
|
||||||
styleUrls: ['./product-card.component.scss'],
|
styleUrls: ['./product-card.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class ProductCardComponent {
|
export class ProductCardComponent {
|
||||||
|
private readonly languageService = inject(LanguageService);
|
||||||
|
readonly displayCurrency = this.languageService.currentCurrency;
|
||||||
|
|
||||||
@Input({ required: true }) item!: Product;
|
@Input({ required: true }) item!: Product;
|
||||||
@Input() title = '';
|
@Input() title = '';
|
||||||
@Input() description = '';
|
@Input() description = '';
|
||||||
|
|||||||
@@ -11,4 +11,25 @@
|
|||||||
<span>{{ 'adminSettings.densityCompact' | translate }}</span>
|
<span>{{ 'adminSettings.densityCompact' | translate }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-card">
|
||||||
|
<h2>{{ 'adminSettings.currencyRates' | translate }}</h2>
|
||||||
|
<p class="settings-explain">{{ 'adminSettings.currencyRatesExplain' | translate }}</p>
|
||||||
|
<div class="rate-row" *ngFor="let currency of languageService.currencies">
|
||||||
|
<span class="rate-code">{{ currency.code }}</span>
|
||||||
|
<input
|
||||||
|
class="rate-input"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.0001"
|
||||||
|
[disabled]="currency.code === currencyRates.baseCurrency"
|
||||||
|
[ngModel]="rateDrafts()[currency.code]"
|
||||||
|
(ngModelChange)="onRateInput(currency.code, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="rate-actions">
|
||||||
|
<button type="button" class="save-button" (click)="saveRates()">{{ 'adminSettings.currencyRatesSave' | translate }}</button>
|
||||||
|
<span class="saved-message" *ngIf="showSavedMessage()">{{ 'adminSettings.currencyRatesSaved' | translate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -12,3 +12,25 @@
|
|||||||
.settings-explain { margin: 0; color: var(--text-secondary, #6b7280); font-size: var(--font-size-sm, 0.8125rem); }
|
.settings-explain { margin: 0; color: var(--text-secondary, #6b7280); font-size: var(--font-size-sm, 0.8125rem); }
|
||||||
|
|
||||||
.toggle-row { display: flex; align-items: center; gap: 8px; font-weight: var(--font-weight-normal, 400); }
|
.toggle-row { display: flex; align-items: center; gap: 8px; font-weight: var(--font-weight-normal, 400); }
|
||||||
|
|
||||||
|
.rate-row { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.rate-code { width: 48px; font-weight: var(--font-weight-medium, 500); }
|
||||||
|
.rate-input {
|
||||||
|
width: 120px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid var(--border-color, #d3dad9);
|
||||||
|
border-radius: var(--radius-sm, 4px);
|
||||||
|
background: var(--bg-primary, #fff);
|
||||||
|
}
|
||||||
|
.rate-input:disabled { opacity: 0.6; }
|
||||||
|
|
||||||
|
.rate-actions { display: flex; align-items: center; gap: 12px; margin-top: 4px; }
|
||||||
|
.save-button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm, 4px);
|
||||||
|
background: var(--color-primary, #16a34a);
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.saved-message { color: var(--color-success, #16a34a); font-size: var(--font-size-sm, 0.8125rem); }
|
||||||
|
|||||||
@@ -1,21 +1,47 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { AdminPreferencesService } from '../services/admin-preferences.service';
|
import { AdminPreferencesService } from '../services/admin-preferences.service';
|
||||||
import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||||
import { ToggleComponent } from '../../../../shared/ui/toggle/toggle.component';
|
import { ToggleComponent } from '../../../../shared/ui/toggle/toggle.component';
|
||||||
|
import { CurrencyRatesService } from '../../../../services/currency-rates.service';
|
||||||
|
import { LanguageService } from '../../../../services/language.service';
|
||||||
|
|
||||||
|
const SAVED_MESSAGE_DURATION_MS = 2000;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-admin-settings-page',
|
selector: 'app-admin-settings-page',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [FormsModule, TranslatePipe, ToggleComponent],
|
imports: [CommonModule, FormsModule, TranslatePipe, ToggleComponent],
|
||||||
templateUrl: './admin-settings-page.component.html',
|
templateUrl: './admin-settings-page.component.html',
|
||||||
styleUrls: ['./admin-settings-page.component.scss'],
|
styleUrls: ['./admin-settings-page.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class AdminSettingsPageComponent {
|
export class AdminSettingsPageComponent {
|
||||||
readonly preferences = inject(AdminPreferencesService);
|
readonly preferences = inject(AdminPreferencesService);
|
||||||
|
readonly currencyRates = inject(CurrencyRatesService);
|
||||||
|
readonly languageService = inject(LanguageService);
|
||||||
|
|
||||||
|
readonly rateDrafts = signal<Record<string, number>>({ ...this.currencyRates.rates() });
|
||||||
|
readonly showSavedMessage = signal(false);
|
||||||
|
|
||||||
onCompactToggle(compact: boolean): void {
|
onCompactToggle(compact: boolean): void {
|
||||||
this.preferences.setDensity(compact ? 'compact' : 'comfortable');
|
this.preferences.setDensity(compact ? 'compact' : 'comfortable');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onRateInput(code: string, value: string): void {
|
||||||
|
const parsed = Number(value);
|
||||||
|
this.rateDrafts.set({ ...this.rateDrafts(), [code]: parsed });
|
||||||
|
}
|
||||||
|
|
||||||
|
saveRates(): void {
|
||||||
|
for (const currency of this.languageService.currencies) {
|
||||||
|
const rate = this.rateDrafts()[currency.code];
|
||||||
|
if (Number.isFinite(rate) && rate > 0) {
|
||||||
|
this.currencyRates.setRate(currency.code, rate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.showSavedMessage.set(true);
|
||||||
|
setTimeout(() => this.showSavedMessage.set(false), SAVED_MESSAGE_DURATION_MS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<span>{{ 'cart.deliveryTime' | translate }}: {{ option.deliveryTime }}</span>
|
<span>{{ 'cart.deliveryTime' | translate }}: {{ option.deliveryTime }}</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<strong>{{ option.deliveryPrice | number:'1.2-2' }} {{ currency }}</strong>
|
<strong>{{ option.deliveryPrice | currencyConvert:currency | number:'1.2-2' }} {{ displayCurrency() }}</strong>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
import { DecimalPipe } from '@angular/common';
|
import { DecimalPipe } from '@angular/common';
|
||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, inject } from '@angular/core';
|
||||||
import { Product } from '../../../../../core/products/models/product-domain.model';
|
import { Product } from '../../../../../core/products/models/product-domain.model';
|
||||||
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
||||||
|
import { CurrencyConvertPipe } from '../../../../../pipes/currency-convert.pipe';
|
||||||
|
import { LanguageService } from '../../../../../services/language.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-product-delivery-information',
|
selector: 'app-product-delivery-information',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [DecimalPipe, TranslatePipe],
|
imports: [DecimalPipe, TranslatePipe, CurrencyConvertPipe],
|
||||||
templateUrl: './delivery-information.component.html',
|
templateUrl: './delivery-information.component.html',
|
||||||
styleUrls: ['./delivery-information.component.scss'],
|
styleUrls: ['./delivery-information.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class ProductDeliveryInformationComponent {
|
export class ProductDeliveryInformationComponent {
|
||||||
|
private readonly languageService = inject(LanguageService);
|
||||||
|
readonly displayCurrency = this.languageService.currentCurrency;
|
||||||
|
|
||||||
@Input({ required: true }) product!: Product;
|
@Input({ required: true }) product!: Product;
|
||||||
@Input() currency = '';
|
@Input() currency = '';
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,12 @@
|
|||||||
<div class="product-price">
|
<div class="product-price">
|
||||||
@if (product.discount > 0) {
|
@if (product.discount > 0) {
|
||||||
<div class="price-row">
|
<div class="price-row">
|
||||||
<span class="old-price">{{ price | number:'1.2-2' }} {{ currency }}</span>
|
<span class="old-price">{{ price | currencyConvert:currency | number:'1.2-2' }} {{ displayCurrency() }}</span>
|
||||||
<span class="discount-badge">-{{ product.discount }}%</span>
|
<span class="discount-badge">-{{ product.discount }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<strong>{{ discountedPrice | number:'1.2-2' }} {{ currency }}</strong>
|
<strong>{{ discountedPrice | currencyConvert:currency | number:'1.2-2' }} {{ displayCurrency() }}</strong>
|
||||||
} @else {
|
} @else {
|
||||||
<strong>{{ price | number:'1.2-2' }} {{ currency }}</strong>
|
<strong>{{ price | currencyConvert:currency | number:'1.2-2' }} {{ displayCurrency() }}</strong>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import { DecimalPipe } from '@angular/common';
|
import { DecimalPipe } from '@angular/common';
|
||||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, inject } from '@angular/core';
|
||||||
import { Product } from '../../../../../core/products/models/product-domain.model';
|
import { Product } from '../../../../../core/products/models/product-domain.model';
|
||||||
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
||||||
|
import { CurrencyConvertPipe } from '../../../../../pipes/currency-convert.pipe';
|
||||||
|
import { LanguageService } from '../../../../../services/language.service';
|
||||||
import { getBadgeClass } from '../../../../../utils/item.utils';
|
import { getBadgeClass } from '../../../../../utils/item.utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-product-information',
|
selector: 'app-product-information',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [DecimalPipe, TranslatePipe],
|
imports: [DecimalPipe, TranslatePipe, CurrencyConvertPipe],
|
||||||
templateUrl: './product-information.component.html',
|
templateUrl: './product-information.component.html',
|
||||||
styleUrls: ['./product-information.component.scss'],
|
styleUrls: ['./product-information.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class ProductInformationComponent {
|
export class ProductInformationComponent {
|
||||||
|
private readonly languageService = inject(LanguageService);
|
||||||
|
readonly displayCurrency = this.languageService.currentCurrency;
|
||||||
|
|
||||||
@Input({ required: true }) product!: Product;
|
@Input({ required: true }) product!: Product;
|
||||||
@Input() title = '';
|
@Input() title = '';
|
||||||
@Input() price = 0;
|
@Input() price = 0;
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
<h3 class="quick-view__title">{{ product.name }}</h3>
|
<h3 class="quick-view__title">{{ product.name }}</h3>
|
||||||
<p class="quick-view__price">
|
<p class="quick-view__price">
|
||||||
@if (hasDiscount) {
|
@if (hasDiscount) {
|
||||||
<span class="quick-view__price-original">{{ product.price | number:'1.2-2' }} {{ product.currency }}</span>
|
<span class="quick-view__price-original">{{ product.price | currencyConvert:product.currency | number:'1.2-2' }} {{ displayCurrency() }}</span>
|
||||||
<span class="quick-view__price-final">{{ discountedPrice | number:'1.2-2' }} {{ product.currency }}</span>
|
<span class="quick-view__price-final">{{ discountedPrice | currencyConvert:product.currency | number:'1.2-2' }} {{ displayCurrency() }}</span>
|
||||||
} @else {
|
} @else {
|
||||||
<span class="quick-view__price-final">{{ product.price | number:'1.2-2' }} {{ product.currency }}</span>
|
<span class="quick-view__price-final">{{ product.price | currencyConvert:product.currency | number:'1.2-2' }} {{ displayCurrency() }}</span>
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
@if (product.simpleDescription) {
|
@if (product.simpleDescription) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, inject } from '@angular/core';
|
||||||
import { DecimalPipe } from '@angular/common';
|
import { DecimalPipe } from '@angular/common';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { Product } from '../../../../../core/products/models/product-domain.model';
|
import { Product } from '../../../../../core/products/models/product-domain.model';
|
||||||
@@ -6,17 +6,22 @@ import { DialogComponent } from '../../../../../shared/ui/dialog/dialog.componen
|
|||||||
import { ButtonComponent } from '../../../../../shared/ui/button/button.component';
|
import { ButtonComponent } from '../../../../../shared/ui/button/button.component';
|
||||||
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
||||||
import { LangRoutePipe } from '../../../../../pipes/lang-route.pipe';
|
import { LangRoutePipe } from '../../../../../pipes/lang-route.pipe';
|
||||||
|
import { CurrencyConvertPipe } from '../../../../../pipes/currency-convert.pipe';
|
||||||
|
import { LanguageService } from '../../../../../services/language.service';
|
||||||
import { getDiscountedPrice, getMainImage } from '../../../../../utils/item.utils';
|
import { getDiscountedPrice, getMainImage } from '../../../../../utils/item.utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-quick-view-dialog',
|
selector: 'app-quick-view-dialog',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [DialogComponent, ButtonComponent, TranslatePipe, LangRoutePipe, DecimalPipe, RouterLink],
|
imports: [DialogComponent, ButtonComponent, TranslatePipe, LangRoutePipe, DecimalPipe, RouterLink, CurrencyConvertPipe],
|
||||||
templateUrl: './quick-view-dialog.component.html',
|
templateUrl: './quick-view-dialog.component.html',
|
||||||
styleUrl: './quick-view-dialog.component.scss',
|
styleUrl: './quick-view-dialog.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class QuickViewDialogComponent {
|
export class QuickViewDialogComponent {
|
||||||
|
private readonly languageService = inject(LanguageService);
|
||||||
|
readonly displayCurrency = this.languageService.currentCurrency;
|
||||||
|
|
||||||
@Input() product: Product | null = null;
|
@Input() product: Product | null = null;
|
||||||
@Input() loading = false;
|
@Input() loading = false;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Product } from '../../../../../core/products/models/product-domain.mode
|
|||||||
import { TranslateService } from '../../../../../i18n/translate.service';
|
import { TranslateService } from '../../../../../i18n/translate.service';
|
||||||
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
||||||
import { LanguageService } from '../../../../../services/language.service';
|
import { LanguageService } from '../../../../../services/language.service';
|
||||||
|
import { CurrencyRatesService } from '../../../../../services/currency-rates.service';
|
||||||
import { getTranslatedField } from '../../../../../utils/item.utils';
|
import { getTranslatedField } from '../../../../../utils/item.utils';
|
||||||
|
|
||||||
interface CompareRow {
|
interface CompareRow {
|
||||||
@@ -30,6 +31,7 @@ const STOCK_LABEL_KEYS: Record<string, string> = {
|
|||||||
export class CompareTableComponent {
|
export class CompareTableComponent {
|
||||||
private readonly i18n = inject(TranslateService);
|
private readonly i18n = inject(TranslateService);
|
||||||
private readonly languageService = inject(LanguageService);
|
private readonly languageService = inject(LanguageService);
|
||||||
|
private readonly currencyRates = inject(CurrencyRatesService);
|
||||||
|
|
||||||
@Input() products: Product[] = [];
|
@Input() products: Product[] = [];
|
||||||
@Input() hideIdentical = false;
|
@Input() hideIdentical = false;
|
||||||
@@ -41,8 +43,12 @@ export class CompareTableComponent {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const targetCurrency = this.languageService.currentCurrency();
|
||||||
|
|
||||||
const baseRows: CompareRow[] = [
|
const baseRows: CompareRow[] = [
|
||||||
this.toRow('price', this.i18n.t('ux.comparePrice'), products.map(product => `${product.price.toFixed(2)} ${product.currency}`)),
|
this.toRow('price', this.i18n.t('ux.comparePrice'), products.map(product =>
|
||||||
|
`${this.currencyRates.convert(product.price, product.currency, targetCurrency).toFixed(2)} ${targetCurrency}`
|
||||||
|
)),
|
||||||
this.toRow('rating', this.i18n.t('ux.compareRating'), products.map(product => `${(product.rating ?? 0).toFixed(1)}`)),
|
this.toRow('rating', this.i18n.t('ux.compareRating'), products.map(product => `${(product.rating ?? 0).toFixed(1)}`)),
|
||||||
this.toRow('stock', this.i18n.t('ux.compareStock'), products.map(product => this.stockLabel(product.remainings))),
|
this.toRow('stock', this.i18n.t('ux.compareStock'), products.map(product => this.stockLabel(product.remainings))),
|
||||||
this.toRow('discount', this.i18n.t('ux.compareDiscount'), products.map(product => `${product.discount ?? 0}%`)),
|
this.toRow('discount', this.i18n.t('ux.compareDiscount'), products.map(product => `${product.discount ?? 0}%`)),
|
||||||
|
|||||||
@@ -1947,6 +1947,10 @@ export const en: Translations = {
|
|||||||
density: 'Table density',
|
density: 'Table density',
|
||||||
densityExplain: 'Reduce row padding across backoffice list pages for a more compact view.',
|
densityExplain: 'Reduce row padding across backoffice list pages for a more compact view.',
|
||||||
densityCompact: 'Compact rows',
|
densityCompact: 'Compact rows',
|
||||||
|
currencyRates: 'Currency rates',
|
||||||
|
currencyRatesExplain: 'Rates relative to 1 RUB, used to convert storefront prices while the backend does not return prices per currency.',
|
||||||
|
currencyRatesSave: 'Save rates',
|
||||||
|
currencyRatesSaved: 'Rates saved',
|
||||||
},
|
},
|
||||||
adminAnalytics: {
|
adminAnalytics: {
|
||||||
topProductsEmptyTitle: 'No product sales in this period',
|
topProductsEmptyTitle: 'No product sales in this period',
|
||||||
|
|||||||
@@ -1942,6 +1942,10 @@ export const hy: Translations = {
|
|||||||
density: 'Աղյուսակի խտություն',
|
density: 'Աղյուսակի խտություն',
|
||||||
densityExplain: 'Փոքրացնել տողերի հեռավորությունը ադմինիստրատիվ վահանակի ցուցակներում՝ ավելի կոմպակտ տեսքի համար։',
|
densityExplain: 'Փոքրացնել տողերի հեռավորությունը ադմինիստրատիվ վահանակի ցուցակներում՝ ավելի կոմպակտ տեսքի համար։',
|
||||||
densityCompact: 'Կոմպակտ տողեր',
|
densityCompact: 'Կոմպակտ տողեր',
|
||||||
|
currencyRates: 'Արժույթների փոխարժեքներ',
|
||||||
|
currencyRatesExplain: 'Փոխարժեքներ՝ 1 RUB-ի նկատմամբ, օգտագործվում են կայքի գները փոխարկելու համար, քանի դեռ բեքենդը գներ չի վերադարձնում ըստ արժույթի։',
|
||||||
|
currencyRatesSave: 'Պահպանել փոխարժեքները',
|
||||||
|
currencyRatesSaved: 'Փոխարժեքները պահպանվեցին',
|
||||||
},
|
},
|
||||||
adminAnalytics: {
|
adminAnalytics: {
|
||||||
topProductsEmptyTitle: 'Այս ժամանակահատվածում ապրանքների վաճառք չկա',
|
topProductsEmptyTitle: 'Այս ժամանակահատվածում ապրանքների վաճառք չկա',
|
||||||
|
|||||||
@@ -1942,6 +1942,10 @@ export const ru: Translations = {
|
|||||||
density: 'Плотность таблиц',
|
density: 'Плотность таблиц',
|
||||||
densityExplain: 'Уменьшить отступы строк в списках панели управления для более компактного вида.',
|
densityExplain: 'Уменьшить отступы строк в списках панели управления для более компактного вида.',
|
||||||
densityCompact: 'Компактные строки',
|
densityCompact: 'Компактные строки',
|
||||||
|
currencyRates: 'Курсы валют',
|
||||||
|
currencyRatesExplain: 'Курсы относительно 1 RUB, используются для конвертации цен на сайте, пока бэкенд не возвращает цены в разных валютах.',
|
||||||
|
currencyRatesSave: 'Сохранить курсы',
|
||||||
|
currencyRatesSaved: 'Курсы сохранены',
|
||||||
},
|
},
|
||||||
adminAnalytics: {
|
adminAnalytics: {
|
||||||
topProductsEmptyTitle: 'Нет продаж товаров за этот период',
|
topProductsEmptyTitle: 'Нет продаж товаров за этот период',
|
||||||
|
|||||||
@@ -1955,6 +1955,10 @@ export interface Translations {
|
|||||||
density: string;
|
density: string;
|
||||||
densityExplain: string;
|
densityExplain: string;
|
||||||
densityCompact: string;
|
densityCompact: string;
|
||||||
|
currencyRates: string;
|
||||||
|
currencyRatesExplain: string;
|
||||||
|
currencyRatesSave: string;
|
||||||
|
currencyRatesSaved: string;
|
||||||
};
|
};
|
||||||
adminAnalytics: {
|
adminAnalytics: {
|
||||||
topProductsEmptyTitle: string;
|
topProductsEmptyTitle: string;
|
||||||
|
|||||||
@@ -70,11 +70,11 @@
|
|||||||
<div class="item-pricing">
|
<div class="item-pricing">
|
||||||
@if (item.discount > 0) {
|
@if (item.discount > 0) {
|
||||||
<div class="price-with-discount">
|
<div class="price-with-discount">
|
||||||
<span class="original-price">{{ item.price }} {{ item.currency }}</span>
|
<span class="original-price">{{ item.price | currencyConvert:item.currency | number:'1.2-2' }} {{ currentCurrency }}</span>
|
||||||
<span class="current-price">{{ getDiscountedPrice(item) | number:'1.2-2' }} {{ item.currency }}</span>
|
<span class="current-price">{{ getDiscountedPrice(item) | currencyConvert:item.currency | number:'1.2-2' }} {{ currentCurrency }}</span>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<span class="current-price">{{ item.price }} {{ item.currency }}</span>
|
<span class="current-price">{{ item.price | currencyConvert:item.currency | number:'1.2-2' }} {{ currentCurrency }}</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -118,13 +118,13 @@
|
|||||||
|
|
||||||
<div class="summary-row">
|
<div class="summary-row">
|
||||||
<span>{{ 'cart.items' | translate }} ({{ itemCount() }})</span>
|
<span>{{ 'cart.items' | translate }} ({{ itemCount() }})</span>
|
||||||
<span class="value">{{ totalPrice() | number:'1.2-2' }} {{ currentCurrency }}</span>
|
<span class="value">{{ convertTotal(totalPrice()) | number:'1.2-2' }} {{ currentCurrency }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (hasDeliveryPrice()) {
|
@if (hasDeliveryPrice()) {
|
||||||
<div class="summary-row delivery">
|
<div class="summary-row delivery">
|
||||||
<span>{{ 'cart.deliveryLabel' | translate }}</span>
|
<span>{{ 'cart.deliveryLabel' | translate }}</span>
|
||||||
<span class="value">{{ totalDeliveryPrice() | number:'1.2-2' }} {{ currentCurrency }}</span>
|
<span class="value">{{ convertTotal(totalDeliveryPrice()) | number:'1.2-2' }} {{ currentCurrency }}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
|
|
||||||
<div class="summary-row total">
|
<div class="summary-row total">
|
||||||
<span>{{ 'cart.toPay' | translate }}</span>
|
<span>{{ 'cart.toPay' | translate }}</span>
|
||||||
<span class="total-price">{{ totalWithDelivery() | number:'1.2-2' }} {{ currentCurrency }}</span>
|
<span class="total-price">{{ convertTotal(totalWithDelivery()) | number:'1.2-2' }} {{ currentCurrency }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="terms-agreement">
|
<div class="terms-agreement">
|
||||||
@@ -237,7 +237,7 @@
|
|||||||
<div class="payment-info">
|
<div class="payment-info">
|
||||||
<div class="payment-amount">
|
<div class="payment-amount">
|
||||||
<span class="label">{{ 'cart.amountToPay' | translate }}</span>
|
<span class="label">{{ 'cart.amountToPay' | translate }}</span>
|
||||||
<span class="amount">{{ totalWithDelivery() | number:'1.2-2' }} {{ currentCurrency }}</span>
|
<span class="amount">{{ convertTotal(totalWithDelivery()) | number:'1.2-2' }} {{ currentCurrency }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="waiting-indicator">
|
<div class="waiting-indicator">
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ import { TenantResolverService } from '../../core/config/tenant-resolver.service
|
|||||||
import { UserNotificationService } from '../../features/website/user-experience/services/user-notification.service';
|
import { UserNotificationService } from '../../features/website/user-experience/services/user-notification.service';
|
||||||
import { ConfirmDialogComponent } from '../../shared/ui/confirm-dialog/confirm-dialog.component';
|
import { ConfirmDialogComponent } from '../../shared/ui/confirm-dialog/confirm-dialog.component';
|
||||||
import { DialogComponent } from '../../shared/ui/dialog/dialog.component';
|
import { DialogComponent } from '../../shared/ui/dialog/dialog.component';
|
||||||
|
import { CurrencyConvertPipe } from '../../pipes/currency-convert.pipe';
|
||||||
|
import { CurrencyRatesService } from '../../services/currency-rates.service';
|
||||||
|
|
||||||
type PaymentMethod = 'qr' | 'card';
|
type PaymentMethod = 'qr' | 'card';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-cart',
|
selector: 'app-cart',
|
||||||
imports: [DecimalPipe, RouterLink, FormsModule, DeliverySelectorComponent, TelegramLoginComponent, LangRoutePipe, TranslatePipe, IconComponent, EmptyStateComponent, ButtonComponent, ConfirmDialogComponent, DialogComponent],
|
imports: [DecimalPipe, RouterLink, FormsModule, DeliverySelectorComponent, TelegramLoginComponent, LangRoutePipe, TranslatePipe, IconComponent, EmptyStateComponent, ButtonComponent, ConfirmDialogComponent, DialogComponent, CurrencyConvertPipe],
|
||||||
templateUrl: './cart.component.html',
|
templateUrl: './cart.component.html',
|
||||||
styleUrls: ['./cart.component.scss'],
|
styleUrls: ['./cart.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
@@ -80,6 +82,7 @@ export class CartComponent implements OnDestroy {
|
|||||||
|
|
||||||
private configService = inject(ConfigService);
|
private configService = inject(ConfigService);
|
||||||
private tenantResolver = inject(TenantResolverService);
|
private tenantResolver = inject(TenantResolverService);
|
||||||
|
private currencyRates = inject(CurrencyRatesService);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cartService: CartService,
|
private cartService: CartService,
|
||||||
@@ -183,6 +186,10 @@ export class CartComponent implements OnDestroy {
|
|||||||
itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); }
|
itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); }
|
||||||
itemDesc(item: Item): string { return getTranslatedField(item, 'simpleDescription', this.langService.currentLanguage()); }
|
itemDesc(item: Item): string { return getTranslatedField(item, 'simpleDescription', this.langService.currentLanguage()); }
|
||||||
get currentCurrency(): string { return this.langService.currentCurrency(); }
|
get currentCurrency(): string { return this.langService.currentCurrency(); }
|
||||||
|
/** Cart totals are summed in the base currency (RUB); convert to whatever the shopper has selected. */
|
||||||
|
convertTotal(amountInBaseCurrency: number): number {
|
||||||
|
return this.currencyRates.convert(amountInBaseCurrency, this.currencyRates.baseCurrency, this.currentCurrency);
|
||||||
|
}
|
||||||
get isCheckoutDisabled(): boolean { return !this.termsAccepted || !this.isAuthenticated() || !this.allRequiredDeliveriesSelected(); }
|
get isCheckoutDisabled(): boolean { return !this.termsAccepted || !this.isAuthenticated() || !this.allRequiredDeliveriesSelected(); }
|
||||||
|
|
||||||
selectDelivery(itemID: number, selectedDelivery: DeliveryOption | null): void {
|
selectDelivery(itemID: number, selectedDelivery: DeliveryOption | null): void {
|
||||||
@@ -255,7 +262,7 @@ export class CartComponent implements OnDestroy {
|
|||||||
createPayment(paymentMethod: PaymentMethod): void {
|
createPayment(paymentMethod: PaymentMethod): void {
|
||||||
const orderId = this.generateOrderId();
|
const orderId = this.generateOrderId();
|
||||||
const paymentPayload = {
|
const paymentPayload = {
|
||||||
amount: Number(this.totalWithDelivery()),
|
amount: Number(this.convertTotal(this.totalWithDelivery())),
|
||||||
currency: this.langService.currentCurrency(),
|
currency: this.langService.currentCurrency(),
|
||||||
siteuserID: this.getPaymentUserId(),
|
siteuserID: this.getPaymentUserId(),
|
||||||
siteorderID: orderId,
|
siteorderID: orderId,
|
||||||
|
|||||||
34
src/app/pipes/currency-convert.pipe.ts
Normal file
34
src/app/pipes/currency-convert.pipe.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Pipe, PipeTransform, inject } from '@angular/core';
|
||||||
|
import { LanguageService } from '../services/language.service';
|
||||||
|
import { CurrencyRatesService } from '../services/currency-rates.service';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'currencyConvert',
|
||||||
|
pure: false
|
||||||
|
})
|
||||||
|
export class CurrencyConvertPipe implements PipeTransform {
|
||||||
|
private readonly langService = inject(LanguageService);
|
||||||
|
private readonly ratesService = inject(CurrencyRatesService);
|
||||||
|
|
||||||
|
private lastAmount: number | null = null;
|
||||||
|
private lastFromCurrency = '';
|
||||||
|
private lastTargetCurrency = '';
|
||||||
|
private lastResult = 0;
|
||||||
|
|
||||||
|
transform(amount: number | null | undefined, fromCurrency: string | null | undefined): number {
|
||||||
|
const value = amount ?? 0;
|
||||||
|
const from = fromCurrency || this.ratesService.baseCurrency;
|
||||||
|
const to = this.langService.currentCurrency();
|
||||||
|
|
||||||
|
if (value === this.lastAmount && from === this.lastFromCurrency && to === this.lastTargetCurrency) {
|
||||||
|
return this.lastResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastAmount = value;
|
||||||
|
this.lastFromCurrency = from;
|
||||||
|
this.lastTargetCurrency = to;
|
||||||
|
this.lastResult = this.ratesService.convert(value, from, to);
|
||||||
|
|
||||||
|
return this.lastResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/app/services/currency-rates.service.ts
Normal file
53
src/app/services/currency-rates.service.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Injectable, Signal, inject, signal } from '@angular/core';
|
||||||
|
import { LocalStorageService } from '../core/storage/local-storage.service';
|
||||||
|
|
||||||
|
const RATES_KEY = 'currencyRates.v1';
|
||||||
|
|
||||||
|
/** Fallback rates relative to RUB (1 RUB = rate[code] units of code), used until admin overrides them. */
|
||||||
|
const DEFAULT_RATES: Record<string, number> = {
|
||||||
|
RUB: 1,
|
||||||
|
USD: 0.011,
|
||||||
|
EUR: 0.01,
|
||||||
|
AMD: 4.3,
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CurrencyRatesService {
|
||||||
|
private readonly localStorage = inject(LocalStorageService);
|
||||||
|
|
||||||
|
private readonly ratesSignal = signal<Record<string, number>>(this.readStoredRates());
|
||||||
|
|
||||||
|
readonly rates: Signal<Record<string, number>> = this.ratesSignal.asReadonly();
|
||||||
|
|
||||||
|
/** Base currency all rates are relative to. */
|
||||||
|
readonly baseCurrency = 'RUB';
|
||||||
|
|
||||||
|
getRate(code: string): number {
|
||||||
|
return this.ratesSignal()[code] ?? DEFAULT_RATES[code] ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRate(code: string, rate: number): void {
|
||||||
|
if (!Number.isFinite(rate) || rate <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const next = { ...this.ratesSignal(), [code]: rate };
|
||||||
|
this.ratesSignal.set(next);
|
||||||
|
this.localStorage.setJSON(RATES_KEY, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converts an amount expressed in `fromCurrency` into `toCurrency` via the RUB base rate. */
|
||||||
|
convert(amount: number, fromCurrency: string, toCurrency: string): number {
|
||||||
|
if (fromCurrency === toCurrency) {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
const fromRate = this.getRate(fromCurrency);
|
||||||
|
const toRate = this.getRate(toCurrency);
|
||||||
|
const amountInBase = amount / fromRate;
|
||||||
|
return amountInBase * toRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readStoredRates(): Record<string, number> {
|
||||||
|
const stored = this.localStorage.getJSON<Record<string, number>>(RATES_KEY);
|
||||||
|
return { ...DEFAULT_RATES, ...stored };
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user