@isaiahiroko/tilmart-ngx-apps-buyer 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-account.module-DkScEdS0.mjs +1753 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-account.module-DkScEdS0.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-brand.module-CgHrtffy.mjs +102 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-brand.module-CgHrtffy.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-calculator.module-BjV4sLrl.mjs +267 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-calculator.module-BjV4sLrl.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-cart-sync.service-FVpoOMXK.mjs +85 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-cart-sync.service-FVpoOMXK.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-cart.module-bj9TAppU.mjs +206 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-cart.module-bj9TAppU.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-category.module-rO8B-yQ-.mjs +102 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-category.module-rO8B-yQ-.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-checkout.module-ByHqfQJJ.mjs +423 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-checkout.module-ByHqfQJJ.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-compare-tray.service-DITbKdyD.mjs +82 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-compare-tray.service-DITbKdyD.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-compare.module-B1hRah67.mjs +266 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-compare.module-B1hRah67.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-compatibility.module-pA7nau85.mjs +210 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-compatibility.module-pA7nau85.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-expert-review-DoJsoGgp.mjs +27 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-expert-review-DoJsoGgp.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-help.module-CTL4vRKb.mjs +220 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-help.module-CTL4vRKb.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-home.module-B4iLd9-f.mjs +363 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-home.module-B4iLd9-f.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-index-C-Wy1WQB.mjs +342 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-index-C-Wy1WQB.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-invite-respond.module-CmjB-eHA.mjs +325 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-invite-respond.module-CmjB-eHA.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-invoice.module-CXjjs2Y4.mjs +81 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-invoice.module-CXjjs2Y4.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-kit-builder.module-CwAR9vRY.mjs +474 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-kit-builder.module-CwAR9vRY.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-kits.module-DEyCahqn.mjs +147 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-kits.module-DEyCahqn.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-learn.module-Dx2Ls_ie.mjs +199 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-learn.module-Dx2Ls_ie.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-media-DIgo1ZHk.mjs +20 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-media-DIgo1ZHk.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-nigeria-xVatCeYL.mjs +12 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-nigeria-xVatCeYL.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-order-confirmation.module-DQOe7rR9.mjs +112 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-order-confirmation.module-DQOe7rR9.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-payment-return.module-2h6yGyW6.mjs +176 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-payment-return.module-2h6yGyW6.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-pending-payment-ChGs1h50.mjs +35 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-pending-payment-ChGs1h50.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-product.module-Y5dhSf1T.mjs +738 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-product.module-Y5dhSf1T.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-runtime.module-DBpKBd7C.mjs +88 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-runtime.module-DBpKBd7C.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-savings.module-DqkHKzbd.mjs +87 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-savings.module-DqkHKzbd.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-search-result-card-DK7QQcu1.mjs +26 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-search-result-card-DK7QQcu1.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-search.module-wrKCi7aQ.mjs +71 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-search.module-wrKCi7aQ.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-seo.service-BAXpRG4P.mjs +91 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-seo.service-BAXpRG4P.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-taxonomy-DdxSUDs5.mjs +60 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-taxonomy-DdxSUDs5.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-tool-session-DcRvQngw.mjs +54 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-tool-session-DcRvQngw.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-track.module-DR4Sv8He.mjs +116 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-track.module-DR4Sv8He.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-use-case.module-DDikDUAV.mjs +102 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer-use-case.module-DDikDUAV.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer.mjs +564 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-buyer.mjs.map +1 -0
- package/package.json +35 -0
- package/types/isaiahiroko-tilmart-ngx-apps-buyer.d.ts +70 -0
|
@@ -0,0 +1,1753 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, Injectable, ViewEncapsulation, Component, NgModule } from '@angular/core';
|
|
3
|
+
import * as i1 from '@angular/router';
|
|
4
|
+
import { ActivatedRoute, RouterModule } from '@angular/router';
|
|
5
|
+
import { ApiService, ControlService, BaseDirective, using, notify, SharedModule } from '@thescaffold/ngx-core';
|
|
6
|
+
import { ReplaySubject, firstValueFrom, map, catchError, of, takeUntil, switchMap, forkJoin } from 'rxjs';
|
|
7
|
+
import { N as NIGERIAN_STATES } from './isaiahiroko-tilmart-ngx-apps-buyer-nigeria-xVatCeYL.mjs';
|
|
8
|
+
import * as i1$1 from '@angular/forms';
|
|
9
|
+
import * as i2 from '@angular/common';
|
|
10
|
+
import { CartStoreService } from './isaiahiroko-tilmart-ngx-apps-buyer.mjs';
|
|
11
|
+
import { ThemeService } from '@thescaffold/ngx-common';
|
|
12
|
+
|
|
13
|
+
// P5.3 — resolves the signed-in visitor's commerce `Customer` once and
|
|
14
|
+
// shares it across the account sub-pages (each of orders / addresses /
|
|
15
|
+
// payments / wishlists needs the `customerId` to filter its own list).
|
|
16
|
+
// A guest has no `Customer` here — the shell shows a sign-in prompt
|
|
17
|
+
// instead of the sub-pages.
|
|
18
|
+
class AccountStoreService {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.apiService = inject(ApiService);
|
|
21
|
+
this.controlService = inject(ControlService);
|
|
22
|
+
this.customer$ = new ReplaySubject(1);
|
|
23
|
+
this.loaded = false;
|
|
24
|
+
}
|
|
25
|
+
customer() {
|
|
26
|
+
if (!this.loaded) {
|
|
27
|
+
this.loaded = true;
|
|
28
|
+
this.load();
|
|
29
|
+
}
|
|
30
|
+
return this.customer$.asObservable();
|
|
31
|
+
}
|
|
32
|
+
async refreshCustomer() {
|
|
33
|
+
const c = await this.fetch();
|
|
34
|
+
this.customer$.next(c);
|
|
35
|
+
return c;
|
|
36
|
+
}
|
|
37
|
+
load() {
|
|
38
|
+
this.fetch().then((c) => this.customer$.next(c));
|
|
39
|
+
}
|
|
40
|
+
async fetch() {
|
|
41
|
+
if (!this.controlService.isLoggedIn())
|
|
42
|
+
return null;
|
|
43
|
+
const userId = this.controlService.getContext()?.userId;
|
|
44
|
+
if (!userId)
|
|
45
|
+
return null;
|
|
46
|
+
return firstValueFrom(this.apiService
|
|
47
|
+
.on('apps/commerce/customer')
|
|
48
|
+
.getMany({ userId, perPage: 1 })
|
|
49
|
+
.pipe(map((res) => {
|
|
50
|
+
const row = (res?.data ?? [])[0];
|
|
51
|
+
return row ? row : null;
|
|
52
|
+
}), catchError(() => of(null))));
|
|
53
|
+
}
|
|
54
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountStoreService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
55
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountStoreService }); }
|
|
56
|
+
}
|
|
57
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountStoreService, decorators: [{
|
|
58
|
+
type: Injectable
|
|
59
|
+
}] });
|
|
60
|
+
|
|
61
|
+
const NAV = [
|
|
62
|
+
{ path: 'dashboard', label: 'Overview', icon: 'fa-gauge' },
|
|
63
|
+
{ path: 'orders', label: 'Orders', icon: 'fa-box' },
|
|
64
|
+
{ path: 'systems', label: 'My Systems', icon: 'fa-solar-panel' },
|
|
65
|
+
{ path: 'returns', label: 'Returns', icon: 'fa-rotate-left' },
|
|
66
|
+
{ path: 'addresses', label: 'Addresses', icon: 'fa-location-dot' },
|
|
67
|
+
{ path: 'payments', label: 'Payments', icon: 'fa-credit-card' },
|
|
68
|
+
{ path: 'wishlists', label: 'Wishlists', icon: 'fa-heart' },
|
|
69
|
+
{ path: 'settings', label: 'Settings', icon: 'fa-gear' },
|
|
70
|
+
{ path: 'privacy', label: 'Privacy & data', icon: 'fa-shield-halved' },
|
|
71
|
+
];
|
|
72
|
+
// P5.3, ACC-02…ACC-10 — the storefront account area. A shell with a nav
|
|
73
|
+
// rail + `<router-outlet>`; the signed-in `Customer` is resolved once
|
|
74
|
+
// here (via `AccountStoreService`) and read by every sub-page. Guest-
|
|
75
|
+
// first like the rest of the storefront: a signed-out visitor sees a
|
|
76
|
+
// sign-in prompt, not a redirect.
|
|
77
|
+
class AccountComponent extends BaseDirective {
|
|
78
|
+
constructor() {
|
|
79
|
+
super(...arguments);
|
|
80
|
+
this.controlService = inject(ControlService);
|
|
81
|
+
this.store = inject(AccountStoreService);
|
|
82
|
+
this.nav = NAV;
|
|
83
|
+
this.isLoggedIn = false;
|
|
84
|
+
this.loading = true;
|
|
85
|
+
this.customerName = '';
|
|
86
|
+
this.init = () => {
|
|
87
|
+
this.isLoggedIn = this.controlService.isLoggedIn();
|
|
88
|
+
if (!this.isLoggedIn) {
|
|
89
|
+
this.loading = false;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.store
|
|
93
|
+
.customer()
|
|
94
|
+
.pipe(takeUntil(this.$))
|
|
95
|
+
.subscribe(using((customer) => {
|
|
96
|
+
this.loading = false;
|
|
97
|
+
this.customerName =
|
|
98
|
+
customer?.name || customer?.email || customer?.phone || 'Your account';
|
|
99
|
+
}));
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
signIn() {
|
|
103
|
+
this.router.navigate(['/start']);
|
|
104
|
+
}
|
|
105
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
106
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: AccountComponent, isStandalone: false, selector: "x-account", usesInheritance: true, ngImport: i0, template: "<div class=\"x-acct\">\n @if (loading) {\n <div class=\"x-acct-note\">Loading your account\u2026</div>\n } @else if (!isLoggedIn) {\n <div class=\"x-acct-signin\">\n <h1>Your account</h1>\n <p>Sign in to see your orders, saved systems, addresses and settings.</p>\n <button type=\"button\" class=\"x-acct-btn\" (click)=\"signIn()\">Sign in</button>\n <p class=\"x-acct-muted\">\n No account yet? You can still\n <a routerLink=\"/track\">track an order</a> with your order number.\n </p>\n </div>\n } @else {\n <header class=\"x-acct-head\">\n <h1>{{ customerName }}</h1>\n </header>\n <div class=\"x-acct-body\">\n <nav class=\"x-acct-nav\">\n @for (item of nav; track item.path) {\n <a\n [routerLink]=\"item.path\"\n routerLinkActive=\"x-acct-nav-on\"\n class=\"x-acct-nav-link\"\n >\n <i class=\"fa-solid\" [class]=\"item.icon\"></i>\n <span>{{ item.label }}</span>\n </a>\n }\n </nav>\n <main class=\"x-acct-main\">\n <router-outlet></router-outlet>\n </main>\n </div>\n }\n</div>\n", styles: [".x-acct{max-width:1080px;margin:0 auto;padding:24px 20px 64px}.x-acct-note,.x-acct-muted{font-size:13.5px;color:var(--color-text-muted)}.x-acct-signin{max-width:420px;margin:48px auto;text-align:center}.x-acct-signin h1{font-size:26px;font-weight:800;color:var(--color-text);margin-bottom:10px}.x-acct-signin p{font-size:14.5px;color:var(--color-text-muted);margin-bottom:18px}.x-acct-signin a{color:var(--color-accent-hover);font-weight:700}.x-acct-head h1{font-size:28px;font-weight:800;color:var(--color-text);margin:4px 0 24px}.x-acct-body{display:grid;grid-template-columns:208px 1fr;gap:32px}.x-acct-nav{display:flex;flex-direction:column;gap:2px}.x-acct-nav-link{display:flex;align-items:center;gap:12px;padding:11px 14px;border-radius:var(--radius-lg);font-size:14px;font-weight:700;color:var(--color-text-muted);text-decoration:none;cursor:pointer}.x-acct-nav-link i{width:16px;text-align:center}.x-acct-nav-link:hover{background:var(--color-surface-2);color:var(--color-text)}.x-acct-nav-on{background:color-mix(in srgb,var(--color-accent) 12%,var(--color-surface));color:var(--color-accent-hover)}.x-acct-main{min-width:0}.x-acct-h2{font-size:19px;font-weight:800;color:var(--color-text);margin:0 0 4px}.x-acct-sub{font-size:13.5px;color:var(--color-text-muted);margin:0 0 20px}.x-acct-card{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);padding:20px;margin-bottom:16px}.x-acct-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:14px 0;border-bottom:1px solid var(--color-border-muted)}.x-acct-row:last-child{border-bottom:0}.x-acct-empty{font-size:13.5px;color:var(--color-text-muted);padding:8px 0}.x-acct-btn{display:inline-block;background:var(--color-accent);color:var(--color-accent-fg);border:0;border-radius:var(--radius-lg);padding:11px 18px;font-size:14px;font-weight:800;cursor:pointer;text-decoration:none}.x-acct-btn:hover:not(:disabled){background:var(--color-accent-hover)}.x-acct-btn:disabled{opacity:.55;cursor:default}.x-acct-btn-ghost{background:transparent;color:var(--color-accent-hover);border:1px solid var(--color-border);padding:9px 14px;border-radius:var(--radius-lg);font-size:13px;font-weight:700;cursor:pointer}.x-acct-btn-ghost:hover{border-color:var(--color-accent)}.x-acct-pill{display:inline-block;font-size:11.5px;font-weight:800;padding:3px 9px;border-radius:var(--radius-sm);background:var(--color-surface-2);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.3px}.x-acct-pill-ok{background:color-mix(in srgb,var(--color-accent) 15%,var(--color-surface));color:var(--color-accent-hover)}.x-acct-pill-warn{background:color-mix(in srgb,var(--color-warn) 20%,var(--color-surface));color:color-mix(in srgb,var(--color-warn) 72%,black)}.x-acct-pill-bad{background:color-mix(in srgb,var(--color-danger) 15%,var(--color-surface));color:var(--color-danger)}.x-acct-field{margin-bottom:16px}.x-acct-field label{display:block;font-size:11.5px;font-weight:800;text-transform:uppercase;letter-spacing:.4px;color:var(--color-text-muted);margin-bottom:7px}.x-acct-field input,.x-acct-field select{width:100%;box-sizing:border-box;padding:12px 14px;border-radius:var(--radius-lg);border:1px solid var(--color-border);font:inherit;font-size:14.5px;background:var(--color-surface);color:var(--color-text);outline:none}.x-acct-err{font-size:12.5px;color:var(--color-danger);margin-bottom:12px}.x-acct-ok{font-size:12.5px;color:var(--color-accent-hover);margin-bottom:12px}.x-acct-checkline{display:flex;align-items:center;gap:10px;padding:9px 0;font-size:13.5px;color:var(--color-text)}.x-acct-checkline input{width:auto}@media(max-width:720px){.x-acct-body{grid-template-columns:1fr;gap:16px}.x-acct-nav{flex-direction:row;flex-wrap:wrap}}\n"], dependencies: [{ kind: "directive", type: i1.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "directive", type: i1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i1.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
107
|
+
}
|
|
108
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountComponent, decorators: [{
|
|
109
|
+
type: Component,
|
|
110
|
+
args: [{ standalone: false, selector: 'x-account', encapsulation: ViewEncapsulation.None, template: "<div class=\"x-acct\">\n @if (loading) {\n <div class=\"x-acct-note\">Loading your account\u2026</div>\n } @else if (!isLoggedIn) {\n <div class=\"x-acct-signin\">\n <h1>Your account</h1>\n <p>Sign in to see your orders, saved systems, addresses and settings.</p>\n <button type=\"button\" class=\"x-acct-btn\" (click)=\"signIn()\">Sign in</button>\n <p class=\"x-acct-muted\">\n No account yet? You can still\n <a routerLink=\"/track\">track an order</a> with your order number.\n </p>\n </div>\n } @else {\n <header class=\"x-acct-head\">\n <h1>{{ customerName }}</h1>\n </header>\n <div class=\"x-acct-body\">\n <nav class=\"x-acct-nav\">\n @for (item of nav; track item.path) {\n <a\n [routerLink]=\"item.path\"\n routerLinkActive=\"x-acct-nav-on\"\n class=\"x-acct-nav-link\"\n >\n <i class=\"fa-solid\" [class]=\"item.icon\"></i>\n <span>{{ item.label }}</span>\n </a>\n }\n </nav>\n <main class=\"x-acct-main\">\n <router-outlet></router-outlet>\n </main>\n </div>\n }\n</div>\n", styles: [".x-acct{max-width:1080px;margin:0 auto;padding:24px 20px 64px}.x-acct-note,.x-acct-muted{font-size:13.5px;color:var(--color-text-muted)}.x-acct-signin{max-width:420px;margin:48px auto;text-align:center}.x-acct-signin h1{font-size:26px;font-weight:800;color:var(--color-text);margin-bottom:10px}.x-acct-signin p{font-size:14.5px;color:var(--color-text-muted);margin-bottom:18px}.x-acct-signin a{color:var(--color-accent-hover);font-weight:700}.x-acct-head h1{font-size:28px;font-weight:800;color:var(--color-text);margin:4px 0 24px}.x-acct-body{display:grid;grid-template-columns:208px 1fr;gap:32px}.x-acct-nav{display:flex;flex-direction:column;gap:2px}.x-acct-nav-link{display:flex;align-items:center;gap:12px;padding:11px 14px;border-radius:var(--radius-lg);font-size:14px;font-weight:700;color:var(--color-text-muted);text-decoration:none;cursor:pointer}.x-acct-nav-link i{width:16px;text-align:center}.x-acct-nav-link:hover{background:var(--color-surface-2);color:var(--color-text)}.x-acct-nav-on{background:color-mix(in srgb,var(--color-accent) 12%,var(--color-surface));color:var(--color-accent-hover)}.x-acct-main{min-width:0}.x-acct-h2{font-size:19px;font-weight:800;color:var(--color-text);margin:0 0 4px}.x-acct-sub{font-size:13.5px;color:var(--color-text-muted);margin:0 0 20px}.x-acct-card{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);padding:20px;margin-bottom:16px}.x-acct-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:14px 0;border-bottom:1px solid var(--color-border-muted)}.x-acct-row:last-child{border-bottom:0}.x-acct-empty{font-size:13.5px;color:var(--color-text-muted);padding:8px 0}.x-acct-btn{display:inline-block;background:var(--color-accent);color:var(--color-accent-fg);border:0;border-radius:var(--radius-lg);padding:11px 18px;font-size:14px;font-weight:800;cursor:pointer;text-decoration:none}.x-acct-btn:hover:not(:disabled){background:var(--color-accent-hover)}.x-acct-btn:disabled{opacity:.55;cursor:default}.x-acct-btn-ghost{background:transparent;color:var(--color-accent-hover);border:1px solid var(--color-border);padding:9px 14px;border-radius:var(--radius-lg);font-size:13px;font-weight:700;cursor:pointer}.x-acct-btn-ghost:hover{border-color:var(--color-accent)}.x-acct-pill{display:inline-block;font-size:11.5px;font-weight:800;padding:3px 9px;border-radius:var(--radius-sm);background:var(--color-surface-2);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.3px}.x-acct-pill-ok{background:color-mix(in srgb,var(--color-accent) 15%,var(--color-surface));color:var(--color-accent-hover)}.x-acct-pill-warn{background:color-mix(in srgb,var(--color-warn) 20%,var(--color-surface));color:color-mix(in srgb,var(--color-warn) 72%,black)}.x-acct-pill-bad{background:color-mix(in srgb,var(--color-danger) 15%,var(--color-surface));color:var(--color-danger)}.x-acct-field{margin-bottom:16px}.x-acct-field label{display:block;font-size:11.5px;font-weight:800;text-transform:uppercase;letter-spacing:.4px;color:var(--color-text-muted);margin-bottom:7px}.x-acct-field input,.x-acct-field select{width:100%;box-sizing:border-box;padding:12px 14px;border-radius:var(--radius-lg);border:1px solid var(--color-border);font:inherit;font-size:14.5px;background:var(--color-surface);color:var(--color-text);outline:none}.x-acct-err{font-size:12.5px;color:var(--color-danger);margin-bottom:12px}.x-acct-ok{font-size:12.5px;color:var(--color-accent-hover);margin-bottom:12px}.x-acct-checkline{display:flex;align-items:center;gap:10px;padding:9px 0;font-size:13.5px;color:var(--color-text)}.x-acct-checkline input{width:auto}@media(max-width:720px){.x-acct-body{grid-template-columns:1fr;gap:16px}.x-acct-nav{flex-direction:row;flex-wrap:wrap}}\n"] }]
|
|
111
|
+
}] });
|
|
112
|
+
|
|
113
|
+
function blankForm() {
|
|
114
|
+
return {
|
|
115
|
+
label: '',
|
|
116
|
+
state: '',
|
|
117
|
+
lga: '',
|
|
118
|
+
city: '',
|
|
119
|
+
street: '',
|
|
120
|
+
area: '',
|
|
121
|
+
landmark: '',
|
|
122
|
+
buildingInfo: '',
|
|
123
|
+
notes: '',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// P5.3, ACC-04 — the address book. Labelled addresses with one default,
|
|
127
|
+
// editable and deletable, shared with checkout + (R2) installation.
|
|
128
|
+
class AccountAddressesComponent extends BaseDirective {
|
|
129
|
+
constructor() {
|
|
130
|
+
super(...arguments);
|
|
131
|
+
this.store = inject(AccountStoreService);
|
|
132
|
+
this.states = NIGERIAN_STATES;
|
|
133
|
+
this.loading = true;
|
|
134
|
+
this.customerId = '';
|
|
135
|
+
this.addresses = [];
|
|
136
|
+
this.editing = false;
|
|
137
|
+
this.saving = false;
|
|
138
|
+
this.form = blankForm();
|
|
139
|
+
this.formError = '';
|
|
140
|
+
this.init = () => {
|
|
141
|
+
this.store
|
|
142
|
+
.customer()
|
|
143
|
+
.pipe(takeUntil(this.$))
|
|
144
|
+
.subscribe(using((customer) => {
|
|
145
|
+
if (!customer) {
|
|
146
|
+
this.loading = false;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.customerId = customer.id;
|
|
150
|
+
void this.load();
|
|
151
|
+
}));
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async load() {
|
|
155
|
+
const res = await firstValueFrom(this.apiService
|
|
156
|
+
.on('apps/commerce/address')
|
|
157
|
+
.getMany({ customerId: this.customerId, perPage: 100 })
|
|
158
|
+
.pipe(catchError(() => of({ data: [] }))));
|
|
159
|
+
this.addresses = (res?.data ?? []).map((a) => ({
|
|
160
|
+
id: a.id,
|
|
161
|
+
label: a.label,
|
|
162
|
+
state: a.state,
|
|
163
|
+
lga: a.lga,
|
|
164
|
+
city: a.city,
|
|
165
|
+
street: a.street,
|
|
166
|
+
area: a.area,
|
|
167
|
+
landmark: a.landmark,
|
|
168
|
+
buildingInfo: a.buildingInfo,
|
|
169
|
+
notes: a.notes,
|
|
170
|
+
isDefault: !!a.isDefault,
|
|
171
|
+
}));
|
|
172
|
+
this.loading = false;
|
|
173
|
+
}
|
|
174
|
+
startAdd() {
|
|
175
|
+
this.form = blankForm();
|
|
176
|
+
this.formError = '';
|
|
177
|
+
this.editing = true;
|
|
178
|
+
}
|
|
179
|
+
startEdit(a) {
|
|
180
|
+
this.form = { ...a };
|
|
181
|
+
this.formError = '';
|
|
182
|
+
this.editing = true;
|
|
183
|
+
}
|
|
184
|
+
cancel() {
|
|
185
|
+
this.editing = false;
|
|
186
|
+
}
|
|
187
|
+
async save() {
|
|
188
|
+
if (this.saving)
|
|
189
|
+
return;
|
|
190
|
+
if (!this.form.label.trim() || !this.form.state || !this.form.street.trim()) {
|
|
191
|
+
this.formError = 'Label, state and street are required.';
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this.saving = true;
|
|
195
|
+
this.formError = '';
|
|
196
|
+
const payload = {
|
|
197
|
+
customerId: this.customerId,
|
|
198
|
+
label: this.form.label.trim(),
|
|
199
|
+
state: this.form.state,
|
|
200
|
+
lga: this.form.lga || undefined,
|
|
201
|
+
city: this.form.city || undefined,
|
|
202
|
+
street: this.form.street.trim(),
|
|
203
|
+
area: this.form.area || this.form.city || this.form.state,
|
|
204
|
+
landmark: this.form.landmark || undefined,
|
|
205
|
+
buildingInfo: this.form.buildingInfo || undefined,
|
|
206
|
+
notes: this.form.notes || undefined,
|
|
207
|
+
};
|
|
208
|
+
try {
|
|
209
|
+
if (this.form.id) {
|
|
210
|
+
await firstValueFrom(this.apiService.on('apps/commerce/address').update(this.form.id, payload));
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
payload.isDefault = this.addresses.length === 0;
|
|
214
|
+
await firstValueFrom(this.apiService.on('apps/commerce/address').create(payload));
|
|
215
|
+
}
|
|
216
|
+
this.editing = false;
|
|
217
|
+
await this.load();
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
this.formError = err?.error?.message ?? 'Could not save this address.';
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
this.saving = false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async remove(a) {
|
|
227
|
+
if (!confirm(`Delete "${a.label}"?`))
|
|
228
|
+
return;
|
|
229
|
+
try {
|
|
230
|
+
await firstValueFrom(this.apiService.on('apps/commerce/address').remove(a.id));
|
|
231
|
+
await this.load();
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
notify('Could not delete that address.', undefined, 'error');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async makeDefault(a) {
|
|
238
|
+
if (a.isDefault)
|
|
239
|
+
return;
|
|
240
|
+
const current = this.addresses.find((x) => x.isDefault);
|
|
241
|
+
try {
|
|
242
|
+
await firstValueFrom(this.apiService.on('apps/commerce/address').update(a.id, { isDefault: true }));
|
|
243
|
+
if (current) {
|
|
244
|
+
await firstValueFrom(this.apiService
|
|
245
|
+
.on('apps/commerce/address')
|
|
246
|
+
.update(current.id, { isDefault: false }));
|
|
247
|
+
}
|
|
248
|
+
await this.load();
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
notify('Could not update the default address.', undefined, 'error');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountAddressesComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
255
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: AccountAddressesComponent, isStandalone: false, selector: "x-account-addresses", usesInheritance: true, ngImport: i0, template: "<h2 class=\"x-acct-h2\">Addresses</h2>\n<p class=\"x-acct-sub\">Used for delivery and, later, installation visits.</p>\n\n@if (loading) {\n <div class=\"x-acct-empty\">Loading\u2026</div>\n} @else {\n @if (editing) {\n <div class=\"x-acct-card\">\n <strong>{{ form.id ? 'Edit address' : 'New address' }}</strong>\n <div style=\"margin-top: 14px\">\n <div class=\"x-acct-field\">\n <label>Label</label>\n <input [(ngModel)]=\"form.label\" placeholder=\"Home, Office, Site\u2026\" />\n </div>\n <div class=\"x-acct-field\">\n <label>State</label>\n <select aria-label=\"State\" [(ngModel)]=\"form.state\">\n <option value=\"\">Select a state</option>\n @for (s of states; track s) {\n <option [value]=\"s\">{{ s }}</option>\n }\n </select>\n </div>\n <div class=\"x-acct-field\">\n <label>LGA / area</label>\n <input [(ngModel)]=\"form.lga\" />\n </div>\n <div class=\"x-acct-field\">\n <label>Street</label>\n <input [(ngModel)]=\"form.street\" />\n </div>\n <div class=\"x-acct-field\">\n <label>Nearest landmark</label>\n <input [(ngModel)]=\"form.landmark\" placeholder=\"e.g. opposite Zenith Bank\" />\n </div>\n <div class=\"x-acct-field\">\n <label>Building / flat info</label>\n <input [(ngModel)]=\"form.buildingInfo\" />\n </div>\n <div class=\"x-acct-field\">\n <label>Delivery notes</label>\n <input [(ngModel)]=\"form.notes\" />\n </div>\n @if (formError) {\n <div class=\"x-acct-err\">{{ formError }}</div>\n }\n <button type=\"button\" class=\"x-acct-btn\" [disabled]=\"saving\" (click)=\"save()\">\n {{ saving ? 'Saving\u2026' : 'Save address' }}\n </button>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"cancel()\" style=\"margin-left: 8px\">\n Cancel\n </button>\n </div>\n </div>\n } @else {\n <button type=\"button\" class=\"x-acct-btn\" (click)=\"startAdd()\">Add an address</button>\n }\n\n @for (a of addresses; track a.id) {\n <div class=\"x-acct-card\">\n <div class=\"x-acct-row\" style=\"border-bottom: 0; padding-top: 0\">\n <strong>\n {{ a.label }}\n @if (a.isDefault) {\n <span class=\"x-acct-pill x-acct-pill-ok\" style=\"margin-left: 6px\">Default</span>\n }\n </strong>\n <span>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"startEdit(a)\">Edit</button>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"remove(a)\" style=\"margin-left: 6px\">\n Delete\n </button>\n </span>\n </div>\n <p class=\"x-acct-sub\" style=\"margin: 0\">\n {{ a.street }}@if (a.landmark) {, near {{ a.landmark }}}<br />\n {{ a.lga ? a.lga + ', ' : '' }}{{ a.state }}\n </p>\n @if (!a.isDefault) {\n <button\n type=\"button\"\n class=\"x-acct-btn-ghost\"\n style=\"margin-top: 10px\"\n (click)=\"makeDefault(a)\"\n >\n Make default\n </button>\n }\n </div>\n } @empty {\n @if (!editing) {\n <div class=\"x-acct-empty\">No saved addresses yet.</div>\n }\n }\n}\n", dependencies: [{ kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] }); }
|
|
256
|
+
}
|
|
257
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountAddressesComponent, decorators: [{
|
|
258
|
+
type: Component,
|
|
259
|
+
args: [{ standalone: false, selector: 'x-account-addresses', template: "<h2 class=\"x-acct-h2\">Addresses</h2>\n<p class=\"x-acct-sub\">Used for delivery and, later, installation visits.</p>\n\n@if (loading) {\n <div class=\"x-acct-empty\">Loading\u2026</div>\n} @else {\n @if (editing) {\n <div class=\"x-acct-card\">\n <strong>{{ form.id ? 'Edit address' : 'New address' }}</strong>\n <div style=\"margin-top: 14px\">\n <div class=\"x-acct-field\">\n <label>Label</label>\n <input [(ngModel)]=\"form.label\" placeholder=\"Home, Office, Site\u2026\" />\n </div>\n <div class=\"x-acct-field\">\n <label>State</label>\n <select aria-label=\"State\" [(ngModel)]=\"form.state\">\n <option value=\"\">Select a state</option>\n @for (s of states; track s) {\n <option [value]=\"s\">{{ s }}</option>\n }\n </select>\n </div>\n <div class=\"x-acct-field\">\n <label>LGA / area</label>\n <input [(ngModel)]=\"form.lga\" />\n </div>\n <div class=\"x-acct-field\">\n <label>Street</label>\n <input [(ngModel)]=\"form.street\" />\n </div>\n <div class=\"x-acct-field\">\n <label>Nearest landmark</label>\n <input [(ngModel)]=\"form.landmark\" placeholder=\"e.g. opposite Zenith Bank\" />\n </div>\n <div class=\"x-acct-field\">\n <label>Building / flat info</label>\n <input [(ngModel)]=\"form.buildingInfo\" />\n </div>\n <div class=\"x-acct-field\">\n <label>Delivery notes</label>\n <input [(ngModel)]=\"form.notes\" />\n </div>\n @if (formError) {\n <div class=\"x-acct-err\">{{ formError }}</div>\n }\n <button type=\"button\" class=\"x-acct-btn\" [disabled]=\"saving\" (click)=\"save()\">\n {{ saving ? 'Saving\u2026' : 'Save address' }}\n </button>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"cancel()\" style=\"margin-left: 8px\">\n Cancel\n </button>\n </div>\n </div>\n } @else {\n <button type=\"button\" class=\"x-acct-btn\" (click)=\"startAdd()\">Add an address</button>\n }\n\n @for (a of addresses; track a.id) {\n <div class=\"x-acct-card\">\n <div class=\"x-acct-row\" style=\"border-bottom: 0; padding-top: 0\">\n <strong>\n {{ a.label }}\n @if (a.isDefault) {\n <span class=\"x-acct-pill x-acct-pill-ok\" style=\"margin-left: 6px\">Default</span>\n }\n </strong>\n <span>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"startEdit(a)\">Edit</button>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"remove(a)\" style=\"margin-left: 6px\">\n Delete\n </button>\n </span>\n </div>\n <p class=\"x-acct-sub\" style=\"margin: 0\">\n {{ a.street }}@if (a.landmark) {, near {{ a.landmark }}}<br />\n {{ a.lga ? a.lga + ', ' : '' }}{{ a.state }}\n </p>\n @if (!a.isDefault) {\n <button\n type=\"button\"\n class=\"x-acct-btn-ghost\"\n style=\"margin-top: 10px\"\n (click)=\"makeDefault(a)\"\n >\n Make default\n </button>\n }\n </div>\n } @empty {\n @if (!editing) {\n <div class=\"x-acct-empty\">No saved addresses yet.</div>\n }\n }\n}\n" }]
|
|
260
|
+
}] });
|
|
261
|
+
|
|
262
|
+
// PRD Appendix B — the 17 order states, buyer-friendly labels. Shared by
|
|
263
|
+
// the account dashboard / orders / order-detail pages (the guest `/track`
|
|
264
|
+
// page keeps its own copy — it's a separate module tree).
|
|
265
|
+
const ORDER_STATUS_LABELS = {
|
|
266
|
+
pending_payment: 'Awaiting payment',
|
|
267
|
+
payment_received: 'Payment received',
|
|
268
|
+
on_hold: 'On hold',
|
|
269
|
+
confirmed: 'Confirmed',
|
|
270
|
+
processing: 'Being prepared',
|
|
271
|
+
partially_fulfilled: 'Partly on the way',
|
|
272
|
+
ready_to_ship: 'Ready to ship',
|
|
273
|
+
ready_for_pickup: 'Ready for pickup',
|
|
274
|
+
dispatched: 'Dispatched',
|
|
275
|
+
out_for_delivery: 'Out for delivery',
|
|
276
|
+
failed_delivery: 'Delivery attempt failed',
|
|
277
|
+
delivered: 'Delivered',
|
|
278
|
+
completed: 'Completed',
|
|
279
|
+
cancelled: 'Cancelled',
|
|
280
|
+
return_in_flight: 'Return in progress',
|
|
281
|
+
returned: 'Returned',
|
|
282
|
+
refunded: 'Refunded',
|
|
283
|
+
};
|
|
284
|
+
function orderStatusLabel(status) {
|
|
285
|
+
return ORDER_STATUS_LABELS[status] ?? status;
|
|
286
|
+
}
|
|
287
|
+
function orderStatusTone(status) {
|
|
288
|
+
if (status === 'delivered' || status === 'completed')
|
|
289
|
+
return 'ok';
|
|
290
|
+
if (status === 'cancelled' || status === 'refunded' || status === 'failed_delivery')
|
|
291
|
+
return 'bad';
|
|
292
|
+
if (status === 'pending_payment' || status === 'on_hold' || status === 'return_in_flight')
|
|
293
|
+
return 'warn';
|
|
294
|
+
return '';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const OPEN_STATUSES = new Set([
|
|
298
|
+
'pending_payment',
|
|
299
|
+
'payment_received',
|
|
300
|
+
'on_hold',
|
|
301
|
+
'confirmed',
|
|
302
|
+
'processing',
|
|
303
|
+
'partially_fulfilled',
|
|
304
|
+
'ready_to_ship',
|
|
305
|
+
'ready_for_pickup',
|
|
306
|
+
'dispatched',
|
|
307
|
+
'out_for_delivery',
|
|
308
|
+
'failed_delivery',
|
|
309
|
+
]);
|
|
310
|
+
// P5.3, ACC-02 — the account overview. Open-order count, the three most
|
|
311
|
+
// recent orders, warranties inside 60 days of expiry, and quick links.
|
|
312
|
+
// Bookings / wallet balance are R2 / PAY-11 (not built) — deliberately
|
|
313
|
+
// left off rather than stubbed.
|
|
314
|
+
class AccountDashboardComponent extends BaseDirective {
|
|
315
|
+
constructor() {
|
|
316
|
+
super(...arguments);
|
|
317
|
+
this.store = inject(AccountStoreService);
|
|
318
|
+
this.loading = true;
|
|
319
|
+
this.customerId = '';
|
|
320
|
+
this.openCount = 0;
|
|
321
|
+
this.recentOrders = [];
|
|
322
|
+
this.expiringWarranties = [];
|
|
323
|
+
this.wishlistCount = 0;
|
|
324
|
+
this.referralCode = '';
|
|
325
|
+
this.referralConverted = 0;
|
|
326
|
+
this.referralPending = 0;
|
|
327
|
+
this.init = () => {
|
|
328
|
+
this.store
|
|
329
|
+
.customer()
|
|
330
|
+
.pipe(takeUntil(this.$), switchMap((customer) => {
|
|
331
|
+
if (!customer)
|
|
332
|
+
return of(null);
|
|
333
|
+
this.customerId = customer.id;
|
|
334
|
+
return of(customer);
|
|
335
|
+
}))
|
|
336
|
+
.subscribe(using((customer) => {
|
|
337
|
+
if (!customer) {
|
|
338
|
+
this.loading = false;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
void this.hydrate(customer.id);
|
|
342
|
+
}));
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
async hydrate(customerId) {
|
|
346
|
+
const [orders, warranties, wishlists, refCode, refs] = await Promise.all([
|
|
347
|
+
firstValueFrom(this.apiService
|
|
348
|
+
.on('apps/commerce/order')
|
|
349
|
+
.getMany({ customerId, perPage: 100 })
|
|
350
|
+
.pipe(catchError(() => of({ data: [] })))),
|
|
351
|
+
firstValueFrom(this.apiService
|
|
352
|
+
.on('apps/commerce/warranty')
|
|
353
|
+
.getMany({ customerId, perPage: 100 })
|
|
354
|
+
.pipe(catchError(() => of({ data: [] })))),
|
|
355
|
+
firstValueFrom(this.apiService
|
|
356
|
+
.on('apps/commerce/wishlist')
|
|
357
|
+
.getMany({ customerId, perPage: 100 })
|
|
358
|
+
.pipe(catchError(() => of({ data: [] })))),
|
|
359
|
+
firstValueFrom(this.apiService
|
|
360
|
+
.get('apps/commerce/referral/my-code')
|
|
361
|
+
.pipe(catchError(() => of(undefined)))),
|
|
362
|
+
firstValueFrom(this.apiService
|
|
363
|
+
.get('apps/commerce/referral/mine')
|
|
364
|
+
.pipe(catchError(() => of({ data: [] })))),
|
|
365
|
+
]);
|
|
366
|
+
this.referralCode = refCode?.data?.code ?? '';
|
|
367
|
+
const refRows = refs?.data ?? [];
|
|
368
|
+
this.referralConverted = refRows.filter((r) => r.status === 'converted' || r.status === 'rewarded').length;
|
|
369
|
+
this.referralPending = refRows.filter((r) => r.status === 'pending' && r.refereeCustomerId).length;
|
|
370
|
+
const orderRows = orders?.data ?? [];
|
|
371
|
+
this.openCount = orderRows.filter((o) => OPEN_STATUSES.has(o.status)).length;
|
|
372
|
+
this.recentOrders = orderRows
|
|
373
|
+
.slice()
|
|
374
|
+
.sort((a, b) => this.ts(b.placedAt ?? b.createdAt) - this.ts(a.placedAt ?? a.createdAt))
|
|
375
|
+
.slice(0, 3)
|
|
376
|
+
.map((o) => ({
|
|
377
|
+
id: o.id,
|
|
378
|
+
orderNumber: o.orderNumber,
|
|
379
|
+
status: o.status,
|
|
380
|
+
statusLabel: orderStatusLabel(o.status),
|
|
381
|
+
placedAt: o.placedAt ?? o.createdAt,
|
|
382
|
+
grandTotal: Number(o.grandTotal ?? 0),
|
|
383
|
+
currency: o.currency ?? 'NGN',
|
|
384
|
+
}));
|
|
385
|
+
const now = Date.now();
|
|
386
|
+
this.expiringWarranties = (warranties?.data ?? [])
|
|
387
|
+
.filter((w) => w.expiresAt)
|
|
388
|
+
.map((w) => ({
|
|
389
|
+
id: w.id,
|
|
390
|
+
label: w.serialNumberId ? 'Registered unit' : 'Warranty',
|
|
391
|
+
expiresAt: w.expiresAt,
|
|
392
|
+
daysLeft: Math.round((this.ts(w.expiresAt) - now) / 86_400_000),
|
|
393
|
+
}))
|
|
394
|
+
.filter((w) => w.daysLeft >= 0 && w.daysLeft <= 60)
|
|
395
|
+
.sort((a, b) => a.daysLeft - b.daysLeft);
|
|
396
|
+
this.wishlistCount = (wishlists?.data ?? []).length;
|
|
397
|
+
this.loading = false;
|
|
398
|
+
}
|
|
399
|
+
ts(v) {
|
|
400
|
+
const t = v ? Date.parse(v) : NaN;
|
|
401
|
+
return Number.isNaN(t) ? 0 : t;
|
|
402
|
+
}
|
|
403
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountDashboardComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
404
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: AccountDashboardComponent, isStandalone: false, selector: "x-account-dashboard", usesInheritance: true, ngImport: i0, template: "<h2 class=\"x-acct-h2\">Overview</h2>\n<p class=\"x-acct-sub\">A snapshot of your orders and cover.</p>\n\n@if (loading) {\n <div class=\"x-acct-empty\">Loading\u2026</div>\n} @else {\n <div class=\"x-acct-card\">\n <div class=\"x-acct-row\">\n <span>Open orders</span>\n <strong>{{ openCount }}</strong>\n </div>\n <div class=\"x-acct-row\">\n <span>Saved wishlists</span>\n <a routerLink=\"../wishlists\">{{ wishlistCount }}</a>\n </div>\n </div>\n\n <div class=\"x-acct-card\">\n <div class=\"x-acct-row\" style=\"border-bottom: 0; padding-bottom: 4px\">\n <strong>Recent orders</strong>\n <a routerLink=\"../orders\" class=\"x-acct-btn-ghost\">All orders</a>\n </div>\n @for (order of recentOrders; track order.id) {\n <a class=\"x-acct-row\" [routerLink]=\"['../orders', order.id]\">\n <span>\n <strong>{{ order.orderNumber }}</strong>\n <br />\n <span class=\"x-acct-pill\">{{ order.statusLabel }}</span>\n </span>\n <span>{{ order.currency }} {{ order.grandTotal | number: '1.2-2' }}</span>\n </a>\n } @empty {\n <div class=\"x-acct-empty\">No orders yet.</div>\n }\n </div>\n\n @if (referralCode) {\n <div class=\"x-acct-card\">\n <div class=\"x-acct-row\" style=\"border-bottom: 0; padding-bottom: 4px\">\n <strong>Refer a friend</strong>\n </div>\n <p class=\"x-acct-sub\" style=\"margin: 0 0 8px\">\n Share your code \u2014 you both benefit when they complete their first order.\n </p>\n <div class=\"x-acct-row\">\n <span>Your code</span>\n <strong style=\"letter-spacing: 1px\">{{ referralCode }}</strong>\n </div>\n @if (referralConverted || referralPending) {\n <div class=\"x-acct-row\">\n <span>Referrals</span>\n <span>{{ referralConverted }} completed \u00B7 {{ referralPending }} pending</span>\n </div>\n }\n </div>\n }\n\n @if (expiringWarranties.length) {\n <div class=\"x-acct-card\">\n <div class=\"x-acct-row\" style=\"border-bottom: 0; padding-bottom: 4px\">\n <strong>Warranties expiring soon</strong>\n <a routerLink=\"../systems\" class=\"x-acct-btn-ghost\">My Systems</a>\n </div>\n @for (w of expiringWarranties; track w.id) {\n <div class=\"x-acct-row\">\n <span>{{ w.label }}</span>\n <span class=\"x-acct-pill-warn x-acct-pill\">{{ w.daysLeft }} days left</span>\n </div>\n }\n </div>\n }\n}\n", dependencies: [{ kind: "directive", type: i1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: i2.DecimalPipe, name: "number" }] }); }
|
|
405
|
+
}
|
|
406
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountDashboardComponent, decorators: [{
|
|
407
|
+
type: Component,
|
|
408
|
+
args: [{ standalone: false, selector: 'x-account-dashboard', template: "<h2 class=\"x-acct-h2\">Overview</h2>\n<p class=\"x-acct-sub\">A snapshot of your orders and cover.</p>\n\n@if (loading) {\n <div class=\"x-acct-empty\">Loading\u2026</div>\n} @else {\n <div class=\"x-acct-card\">\n <div class=\"x-acct-row\">\n <span>Open orders</span>\n <strong>{{ openCount }}</strong>\n </div>\n <div class=\"x-acct-row\">\n <span>Saved wishlists</span>\n <a routerLink=\"../wishlists\">{{ wishlistCount }}</a>\n </div>\n </div>\n\n <div class=\"x-acct-card\">\n <div class=\"x-acct-row\" style=\"border-bottom: 0; padding-bottom: 4px\">\n <strong>Recent orders</strong>\n <a routerLink=\"../orders\" class=\"x-acct-btn-ghost\">All orders</a>\n </div>\n @for (order of recentOrders; track order.id) {\n <a class=\"x-acct-row\" [routerLink]=\"['../orders', order.id]\">\n <span>\n <strong>{{ order.orderNumber }}</strong>\n <br />\n <span class=\"x-acct-pill\">{{ order.statusLabel }}</span>\n </span>\n <span>{{ order.currency }} {{ order.grandTotal | number: '1.2-2' }}</span>\n </a>\n } @empty {\n <div class=\"x-acct-empty\">No orders yet.</div>\n }\n </div>\n\n @if (referralCode) {\n <div class=\"x-acct-card\">\n <div class=\"x-acct-row\" style=\"border-bottom: 0; padding-bottom: 4px\">\n <strong>Refer a friend</strong>\n </div>\n <p class=\"x-acct-sub\" style=\"margin: 0 0 8px\">\n Share your code \u2014 you both benefit when they complete their first order.\n </p>\n <div class=\"x-acct-row\">\n <span>Your code</span>\n <strong style=\"letter-spacing: 1px\">{{ referralCode }}</strong>\n </div>\n @if (referralConverted || referralPending) {\n <div class=\"x-acct-row\">\n <span>Referrals</span>\n <span>{{ referralConverted }} completed \u00B7 {{ referralPending }} pending</span>\n </div>\n }\n </div>\n }\n\n @if (expiringWarranties.length) {\n <div class=\"x-acct-card\">\n <div class=\"x-acct-row\" style=\"border-bottom: 0; padding-bottom: 4px\">\n <strong>Warranties expiring soon</strong>\n <a routerLink=\"../systems\" class=\"x-acct-btn-ghost\">My Systems</a>\n </div>\n @for (w of expiringWarranties; track w.id) {\n <div class=\"x-acct-row\">\n <span>{{ w.label }}</span>\n <span class=\"x-acct-pill-warn x-acct-pill\">{{ w.daysLeft }} days left</span>\n </div>\n }\n </div>\n }\n}\n" }]
|
|
409
|
+
}] });
|
|
410
|
+
|
|
411
|
+
// P5.3, ACC-03 — one order in full: lines, totals, the transition
|
|
412
|
+
// timeline, payments, and the context actions (reorder, invoice, track,
|
|
413
|
+
// return). Return / review targets are P5.9 / P5.11 — linked when they
|
|
414
|
+
// exist, absent otherwise rather than dead-ended.
|
|
415
|
+
class AccountOrderDetailComponent extends BaseDirective {
|
|
416
|
+
constructor() {
|
|
417
|
+
super(...arguments);
|
|
418
|
+
this.route = inject(ActivatedRoute);
|
|
419
|
+
this.cart = inject(CartStoreService);
|
|
420
|
+
this.loading = true;
|
|
421
|
+
this.found = false;
|
|
422
|
+
this.orderId = '';
|
|
423
|
+
this.orderNumber = '';
|
|
424
|
+
this.status = '';
|
|
425
|
+
this.statusLabel = '';
|
|
426
|
+
this.tone = '';
|
|
427
|
+
this.currency = 'NGN';
|
|
428
|
+
this.recipientName = '';
|
|
429
|
+
this.recipientPhone = '';
|
|
430
|
+
this.lines = [];
|
|
431
|
+
this.subtotal = 0;
|
|
432
|
+
this.deliveryTotal = 0;
|
|
433
|
+
this.taxTotal = 0;
|
|
434
|
+
this.discountTotal = 0;
|
|
435
|
+
this.grandTotal = 0;
|
|
436
|
+
this.amountPaid = 0;
|
|
437
|
+
this.codAmountDue = 0;
|
|
438
|
+
this.timeline = [];
|
|
439
|
+
this.payments = [];
|
|
440
|
+
this.init = () => {
|
|
441
|
+
this.orderId = this.route.snapshot.paramMap.get('id') ?? '';
|
|
442
|
+
if (!this.orderId) {
|
|
443
|
+
this.loading = false;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
forkJoin({
|
|
447
|
+
order: this.apiService
|
|
448
|
+
.on('apps/commerce/order')
|
|
449
|
+
.getOne(this.orderId)
|
|
450
|
+
.pipe(catchError(() => of(undefined))),
|
|
451
|
+
lines: this.apiService
|
|
452
|
+
.on('apps/commerce/order-line')
|
|
453
|
+
.getMany({ orderId: this.orderId, perPage: 200 })
|
|
454
|
+
.pipe(catchError(() => of(undefined))),
|
|
455
|
+
history: this.apiService
|
|
456
|
+
.on('apps/commerce/order-status-history')
|
|
457
|
+
.getMany({ orderId: this.orderId, perPage: 200 })
|
|
458
|
+
.pipe(catchError(() => of(undefined))),
|
|
459
|
+
payments: this.apiService
|
|
460
|
+
.on('apps/commerce/payment')
|
|
461
|
+
.getMany({ orderId: this.orderId, perPage: 50 })
|
|
462
|
+
.pipe(catchError(() => of(undefined))),
|
|
463
|
+
})
|
|
464
|
+
.pipe(takeUntil(this.$))
|
|
465
|
+
.subscribe(using((res) => {
|
|
466
|
+
this.loading = false;
|
|
467
|
+
const order = res?.order?.data;
|
|
468
|
+
if (!order)
|
|
469
|
+
return;
|
|
470
|
+
this.found = true;
|
|
471
|
+
this.orderNumber = order.orderNumber;
|
|
472
|
+
this.status = order.status;
|
|
473
|
+
this.statusLabel = orderStatusLabel(order.status);
|
|
474
|
+
this.tone = orderStatusTone(order.status);
|
|
475
|
+
this.placedAt = order.placedAt ?? order.createdAt;
|
|
476
|
+
this.currency = order.currency ?? 'NGN';
|
|
477
|
+
this.recipientName = order.recipientName ?? '';
|
|
478
|
+
this.recipientPhone = order.recipientPhone ?? '';
|
|
479
|
+
this.subtotal = Number(order.subtotal ?? 0);
|
|
480
|
+
this.deliveryTotal = Number(order.deliveryTotal ?? 0);
|
|
481
|
+
this.taxTotal = Number(order.taxTotal ?? 0);
|
|
482
|
+
this.discountTotal = Number(order.discountTotal ?? 0);
|
|
483
|
+
this.grandTotal = Number(order.grandTotal ?? 0);
|
|
484
|
+
this.amountPaid = Number(order.amountPaid ?? 0);
|
|
485
|
+
this.codAmountDue = Number(order.codAmountDue ?? 0);
|
|
486
|
+
this.lines = (res?.lines?.data ?? []).map((l) => ({
|
|
487
|
+
variantId: l.variantId,
|
|
488
|
+
name: l.nameSnapshot || 'Item',
|
|
489
|
+
quantity: Number(l.quantity ?? 1),
|
|
490
|
+
unitPrice: Number(l.unitPrice ?? 0),
|
|
491
|
+
lineTotal: Number(l.unitPrice ?? 0) * Number(l.quantity ?? 1),
|
|
492
|
+
fulfilmentStatus: l.fulfilmentStatus,
|
|
493
|
+
}));
|
|
494
|
+
this.timeline = (res?.history?.data ?? [])
|
|
495
|
+
.slice()
|
|
496
|
+
.sort((a, b) => this.ts(a.at ?? a.createdAt) - this.ts(b.at ?? b.createdAt))
|
|
497
|
+
.map((h) => ({
|
|
498
|
+
toStatus: h.toStatus,
|
|
499
|
+
label: orderStatusLabel(h.toStatus),
|
|
500
|
+
at: h.at ?? h.createdAt,
|
|
501
|
+
reason: h.reason,
|
|
502
|
+
}));
|
|
503
|
+
this.payments = (res?.payments?.data ?? []).map((p) => ({
|
|
504
|
+
provider: p.provider,
|
|
505
|
+
method: p.method,
|
|
506
|
+
status: p.status,
|
|
507
|
+
amount: Number(p.amount ?? 0),
|
|
508
|
+
}));
|
|
509
|
+
}));
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
reorder() {
|
|
513
|
+
if (!this.lines.length)
|
|
514
|
+
return;
|
|
515
|
+
for (const line of this.lines) {
|
|
516
|
+
this.cart.addLine({
|
|
517
|
+
variantId: line.variantId,
|
|
518
|
+
productId: '',
|
|
519
|
+
name: line.name,
|
|
520
|
+
quantity: line.quantity,
|
|
521
|
+
unitPrice: line.unitPrice,
|
|
522
|
+
currency: this.currency,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
notify('Added to your cart', undefined, 'success');
|
|
526
|
+
this.router.navigate(['/cart']);
|
|
527
|
+
}
|
|
528
|
+
ts(v) {
|
|
529
|
+
const t = v ? Date.parse(v) : NaN;
|
|
530
|
+
return Number.isNaN(t) ? 0 : t;
|
|
531
|
+
}
|
|
532
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountOrderDetailComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
533
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: AccountOrderDetailComponent, isStandalone: false, selector: "x-account-order-detail", usesInheritance: true, ngImport: i0, template: "<a routerLink=\"../\" class=\"x-acct-btn-ghost\">\u2039 All orders</a>\n\n@if (loading) {\n <div class=\"x-acct-empty\" style=\"margin-top: 16px\">Loading\u2026</div>\n} @else if (!found) {\n <div class=\"x-acct-card\" style=\"margin-top: 16px\">\n <div class=\"x-acct-empty\">We couldn't find that order.</div>\n </div>\n} @else {\n <header style=\"margin: 16px 0 8px\">\n <h2 class=\"x-acct-h2\">{{ orderNumber }}</h2>\n <p class=\"x-acct-sub\">\n @if (placedAt) {\n Placed {{ placedAt | date: 'medium' }} \u00B7\n }\n <span\n class=\"x-acct-pill\"\n [class.x-acct-pill-ok]=\"tone === 'ok'\"\n [class.x-acct-pill-warn]=\"tone === 'warn'\"\n [class.x-acct-pill-bad]=\"tone === 'bad'\"\n >{{ statusLabel }}</span\n >\n </p>\n </header>\n\n <div class=\"x-acct-card\" style=\"display: flex; flex-wrap: wrap; gap: 10px\">\n <button type=\"button\" class=\"x-acct-btn\" (click)=\"reorder()\">Buy these again</button>\n <a\n class=\"x-acct-btn-ghost\"\n [routerLink]=\"['/order/invoice']\"\n [queryParams]=\"{ orderId: orderId }\"\n >VAT invoice</a\n >\n <a\n class=\"x-acct-btn-ghost\"\n [routerLink]=\"['/track']\"\n [queryParams]=\"{ number: orderNumber }\"\n >Track delivery</a\n >\n <a class=\"x-acct-btn-ghost\" [routerLink]=\"['return']\">Request a return</a>\n <a class=\"x-acct-btn-ghost\" routerLink=\"/help\">Contact support</a>\n </div>\n\n <div class=\"x-acct-card\">\n <strong>Items</strong>\n @for (line of lines; track line.variantId) {\n <div class=\"x-acct-row\">\n <span>\n {{ line.name }}\n <span class=\"x-acct-sub\" style=\"display: block; margin-top: 2px\">\n Qty {{ line.quantity }} \u00B7 {{ currency }} {{ line.unitPrice | number: '1.2-2' }} each\n @if (line.fulfilmentStatus) {\n \u00B7 {{ line.fulfilmentStatus }}\n }\n </span>\n </span>\n <strong>{{ currency }} {{ line.lineTotal | number: '1.2-2' }}</strong>\n </div>\n }\n <div class=\"x-acct-row\"><span>Subtotal</span><span>{{ currency }} {{ subtotal | number: '1.2-2' }}</span></div>\n @if (discountTotal > 0) {\n <div class=\"x-acct-row\"><span>Discount</span><span>\u2212 {{ currency }} {{ discountTotal | number: '1.2-2' }}</span></div>\n }\n <div class=\"x-acct-row\"><span>Delivery</span><span>{{ currency }} {{ deliveryTotal | number: '1.2-2' }}</span></div>\n <div class=\"x-acct-row\"><span>VAT (incl.)</span><span>{{ currency }} {{ taxTotal | number: '1.2-2' }}</span></div>\n <div class=\"x-acct-row\"><strong>Total</strong><strong>{{ currency }} {{ grandTotal | number: '1.2-2' }}</strong></div>\n <div class=\"x-acct-row\"><span>Paid</span><span>{{ currency }} {{ amountPaid | number: '1.2-2' }}</span></div>\n @if (codAmountDue > 0) {\n <div class=\"x-acct-row\">\n <span>Due on delivery</span>\n <strong>{{ currency }} {{ codAmountDue | number: '1.2-2' }}</strong>\n </div>\n }\n </div>\n\n @if (recipientName || recipientPhone) {\n <div class=\"x-acct-card\">\n <strong>Delivery to</strong>\n <div class=\"x-acct-row\">\n <span>{{ recipientName }}</span>\n <span>{{ recipientPhone }}</span>\n </div>\n </div>\n }\n\n @if (payments.length) {\n <div class=\"x-acct-card\">\n <strong>Payments</strong>\n @for (p of payments; track $index) {\n <div class=\"x-acct-row\">\n <span>{{ p.method || p.provider || 'Payment' }}</span>\n <span class=\"x-acct-pill\">{{ p.status }}</span>\n <span>{{ currency }} {{ p.amount | number: '1.2-2' }}</span>\n </div>\n }\n </div>\n }\n\n @if (timeline.length) {\n <div class=\"x-acct-card\">\n <strong>Progress</strong>\n @for (step of timeline; track $index) {\n <div class=\"x-acct-row\">\n <span>\n {{ step.label }}\n @if (step.reason) {\n <span class=\"x-acct-sub\" style=\"display: block; margin-top: 2px\">{{ step.reason }}</span>\n }\n </span>\n <span class=\"x-acct-sub\">{{ step.at | date: 'short' }}</span>\n </div>\n }\n </div>\n }\n}\n", dependencies: [{ kind: "directive", type: i1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: i2.DecimalPipe, name: "number" }, { kind: "pipe", type: i2.DatePipe, name: "date" }] }); }
|
|
534
|
+
}
|
|
535
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountOrderDetailComponent, decorators: [{
|
|
536
|
+
type: Component,
|
|
537
|
+
args: [{ standalone: false, selector: 'x-account-order-detail', template: "<a routerLink=\"../\" class=\"x-acct-btn-ghost\">\u2039 All orders</a>\n\n@if (loading) {\n <div class=\"x-acct-empty\" style=\"margin-top: 16px\">Loading\u2026</div>\n} @else if (!found) {\n <div class=\"x-acct-card\" style=\"margin-top: 16px\">\n <div class=\"x-acct-empty\">We couldn't find that order.</div>\n </div>\n} @else {\n <header style=\"margin: 16px 0 8px\">\n <h2 class=\"x-acct-h2\">{{ orderNumber }}</h2>\n <p class=\"x-acct-sub\">\n @if (placedAt) {\n Placed {{ placedAt | date: 'medium' }} \u00B7\n }\n <span\n class=\"x-acct-pill\"\n [class.x-acct-pill-ok]=\"tone === 'ok'\"\n [class.x-acct-pill-warn]=\"tone === 'warn'\"\n [class.x-acct-pill-bad]=\"tone === 'bad'\"\n >{{ statusLabel }}</span\n >\n </p>\n </header>\n\n <div class=\"x-acct-card\" style=\"display: flex; flex-wrap: wrap; gap: 10px\">\n <button type=\"button\" class=\"x-acct-btn\" (click)=\"reorder()\">Buy these again</button>\n <a\n class=\"x-acct-btn-ghost\"\n [routerLink]=\"['/order/invoice']\"\n [queryParams]=\"{ orderId: orderId }\"\n >VAT invoice</a\n >\n <a\n class=\"x-acct-btn-ghost\"\n [routerLink]=\"['/track']\"\n [queryParams]=\"{ number: orderNumber }\"\n >Track delivery</a\n >\n <a class=\"x-acct-btn-ghost\" [routerLink]=\"['return']\">Request a return</a>\n <a class=\"x-acct-btn-ghost\" routerLink=\"/help\">Contact support</a>\n </div>\n\n <div class=\"x-acct-card\">\n <strong>Items</strong>\n @for (line of lines; track line.variantId) {\n <div class=\"x-acct-row\">\n <span>\n {{ line.name }}\n <span class=\"x-acct-sub\" style=\"display: block; margin-top: 2px\">\n Qty {{ line.quantity }} \u00B7 {{ currency }} {{ line.unitPrice | number: '1.2-2' }} each\n @if (line.fulfilmentStatus) {\n \u00B7 {{ line.fulfilmentStatus }}\n }\n </span>\n </span>\n <strong>{{ currency }} {{ line.lineTotal | number: '1.2-2' }}</strong>\n </div>\n }\n <div class=\"x-acct-row\"><span>Subtotal</span><span>{{ currency }} {{ subtotal | number: '1.2-2' }}</span></div>\n @if (discountTotal > 0) {\n <div class=\"x-acct-row\"><span>Discount</span><span>\u2212 {{ currency }} {{ discountTotal | number: '1.2-2' }}</span></div>\n }\n <div class=\"x-acct-row\"><span>Delivery</span><span>{{ currency }} {{ deliveryTotal | number: '1.2-2' }}</span></div>\n <div class=\"x-acct-row\"><span>VAT (incl.)</span><span>{{ currency }} {{ taxTotal | number: '1.2-2' }}</span></div>\n <div class=\"x-acct-row\"><strong>Total</strong><strong>{{ currency }} {{ grandTotal | number: '1.2-2' }}</strong></div>\n <div class=\"x-acct-row\"><span>Paid</span><span>{{ currency }} {{ amountPaid | number: '1.2-2' }}</span></div>\n @if (codAmountDue > 0) {\n <div class=\"x-acct-row\">\n <span>Due on delivery</span>\n <strong>{{ currency }} {{ codAmountDue | number: '1.2-2' }}</strong>\n </div>\n }\n </div>\n\n @if (recipientName || recipientPhone) {\n <div class=\"x-acct-card\">\n <strong>Delivery to</strong>\n <div class=\"x-acct-row\">\n <span>{{ recipientName }}</span>\n <span>{{ recipientPhone }}</span>\n </div>\n </div>\n }\n\n @if (payments.length) {\n <div class=\"x-acct-card\">\n <strong>Payments</strong>\n @for (p of payments; track $index) {\n <div class=\"x-acct-row\">\n <span>{{ p.method || p.provider || 'Payment' }}</span>\n <span class=\"x-acct-pill\">{{ p.status }}</span>\n <span>{{ currency }} {{ p.amount | number: '1.2-2' }}</span>\n </div>\n }\n </div>\n }\n\n @if (timeline.length) {\n <div class=\"x-acct-card\">\n <strong>Progress</strong>\n @for (step of timeline; track $index) {\n <div class=\"x-acct-row\">\n <span>\n {{ step.label }}\n @if (step.reason) {\n <span class=\"x-acct-sub\" style=\"display: block; margin-top: 2px\">{{ step.reason }}</span>\n }\n </span>\n <span class=\"x-acct-sub\">{{ step.at | date: 'short' }}</span>\n </div>\n }\n </div>\n }\n}\n" }]
|
|
538
|
+
}] });
|
|
539
|
+
|
|
540
|
+
// P5.3, ACC-03 — the buyer's order history. Every Order for their
|
|
541
|
+
// Customer, newest first, each linking to the detail page (reorder /
|
|
542
|
+
// invoice / track / return live there).
|
|
543
|
+
class AccountOrdersComponent extends BaseDirective {
|
|
544
|
+
constructor() {
|
|
545
|
+
super(...arguments);
|
|
546
|
+
this.store = inject(AccountStoreService);
|
|
547
|
+
this.loading = true;
|
|
548
|
+
this.orders = [];
|
|
549
|
+
this.init = () => {
|
|
550
|
+
this.store
|
|
551
|
+
.customer()
|
|
552
|
+
.pipe(takeUntil(this.$))
|
|
553
|
+
.subscribe(using((customer) => {
|
|
554
|
+
if (!customer) {
|
|
555
|
+
this.loading = false;
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
void this.load(customer.id);
|
|
559
|
+
}));
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
async load(customerId) {
|
|
563
|
+
const res = await firstValueFrom(this.apiService
|
|
564
|
+
.on('apps/commerce/order')
|
|
565
|
+
.getMany({ customerId, perPage: 200 })
|
|
566
|
+
.pipe(catchError(() => of({ data: [] }))));
|
|
567
|
+
this.orders = (res?.data ?? [])
|
|
568
|
+
.slice()
|
|
569
|
+
.sort((a, b) => this.ts(b.placedAt ?? b.createdAt) - this.ts(a.placedAt ?? a.createdAt))
|
|
570
|
+
.map((o) => ({
|
|
571
|
+
id: o.id,
|
|
572
|
+
orderNumber: o.orderNumber,
|
|
573
|
+
status: o.status,
|
|
574
|
+
statusLabel: orderStatusLabel(o.status),
|
|
575
|
+
tone: orderStatusTone(o.status),
|
|
576
|
+
placedAt: o.placedAt ?? o.createdAt,
|
|
577
|
+
lineCount: Number(o.lineCount ?? 0),
|
|
578
|
+
grandTotal: Number(o.grandTotal ?? 0),
|
|
579
|
+
codAmountDue: Number(o.codAmountDue ?? 0),
|
|
580
|
+
currency: o.currency ?? 'NGN',
|
|
581
|
+
}));
|
|
582
|
+
this.loading = false;
|
|
583
|
+
}
|
|
584
|
+
ts(v) {
|
|
585
|
+
const t = v ? Date.parse(v) : NaN;
|
|
586
|
+
return Number.isNaN(t) ? 0 : t;
|
|
587
|
+
}
|
|
588
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountOrdersComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
589
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: AccountOrdersComponent, isStandalone: false, selector: "x-account-orders", usesInheritance: true, ngImport: i0, template: "<h2 class=\"x-acct-h2\">Orders</h2>\n<p class=\"x-acct-sub\">Track a delivery, download an invoice or start a return.</p>\n\n@if (loading) {\n <div class=\"x-acct-empty\">Loading your orders\u2026</div>\n} @else if (!orders.length) {\n <div class=\"x-acct-card\">\n <div class=\"x-acct-empty\">\n No orders yet. When you place one it'll show up here.\n </div>\n </div>\n} @else {\n <div class=\"x-acct-card\">\n @for (order of orders; track order.id) {\n <a class=\"x-acct-row\" [routerLink]=\"[order.id]\">\n <span>\n <strong>{{ order.orderNumber }}</strong>\n @if (order.placedAt) {\n <span class=\"x-acct-sub\" style=\"margin: 2px 0 0; display: block\">\n {{ order.placedAt | date: 'mediumDate' }}\n </span>\n }\n </span>\n <span\n class=\"x-acct-pill\"\n [class.x-acct-pill-ok]=\"order.tone === 'ok'\"\n [class.x-acct-pill-warn]=\"order.tone === 'warn'\"\n [class.x-acct-pill-bad]=\"order.tone === 'bad'\"\n >{{ order.statusLabel }}</span\n >\n <span style=\"text-align: right\">\n <strong>{{ order.currency }} {{ order.grandTotal | number: '1.2-2' }}</strong>\n @if (order.codAmountDue > 0) {\n <span class=\"x-acct-sub\" style=\"margin: 2px 0 0; display: block\">\n {{ order.currency }} {{ order.codAmountDue | number: '1.2-2' }} on delivery\n </span>\n }\n </span>\n </a>\n }\n </div>\n}\n", dependencies: [{ kind: "directive", type: i1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: i2.DecimalPipe, name: "number" }, { kind: "pipe", type: i2.DatePipe, name: "date" }] }); }
|
|
590
|
+
}
|
|
591
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountOrdersComponent, decorators: [{
|
|
592
|
+
type: Component,
|
|
593
|
+
args: [{ standalone: false, selector: 'x-account-orders', template: "<h2 class=\"x-acct-h2\">Orders</h2>\n<p class=\"x-acct-sub\">Track a delivery, download an invoice or start a return.</p>\n\n@if (loading) {\n <div class=\"x-acct-empty\">Loading your orders\u2026</div>\n} @else if (!orders.length) {\n <div class=\"x-acct-card\">\n <div class=\"x-acct-empty\">\n No orders yet. When you place one it'll show up here.\n </div>\n </div>\n} @else {\n <div class=\"x-acct-card\">\n @for (order of orders; track order.id) {\n <a class=\"x-acct-row\" [routerLink]=\"[order.id]\">\n <span>\n <strong>{{ order.orderNumber }}</strong>\n @if (order.placedAt) {\n <span class=\"x-acct-sub\" style=\"margin: 2px 0 0; display: block\">\n {{ order.placedAt | date: 'mediumDate' }}\n </span>\n }\n </span>\n <span\n class=\"x-acct-pill\"\n [class.x-acct-pill-ok]=\"order.tone === 'ok'\"\n [class.x-acct-pill-warn]=\"order.tone === 'warn'\"\n [class.x-acct-pill-bad]=\"order.tone === 'bad'\"\n >{{ order.statusLabel }}</span\n >\n <span style=\"text-align: right\">\n <strong>{{ order.currency }} {{ order.grandTotal | number: '1.2-2' }}</strong>\n @if (order.codAmountDue > 0) {\n <span class=\"x-acct-sub\" style=\"margin: 2px 0 0; display: block\">\n {{ order.currency }} {{ order.codAmountDue | number: '1.2-2' }} on delivery\n </span>\n }\n </span>\n </a>\n }\n </div>\n}\n" }]
|
|
594
|
+
}] });
|
|
595
|
+
|
|
596
|
+
// P5.3, ACC-05 (the R1 slice) — a read-only payment history across the
|
|
597
|
+
// buyer's orders. Saved / tokenised cards are P1 and not built.
|
|
598
|
+
class AccountPaymentsComponent extends BaseDirective {
|
|
599
|
+
constructor() {
|
|
600
|
+
super(...arguments);
|
|
601
|
+
this.store = inject(AccountStoreService);
|
|
602
|
+
this.loading = true;
|
|
603
|
+
this.payments = [];
|
|
604
|
+
this.init = () => {
|
|
605
|
+
this.store
|
|
606
|
+
.customer()
|
|
607
|
+
.pipe(takeUntil(this.$))
|
|
608
|
+
.subscribe(using((customer) => {
|
|
609
|
+
if (!customer) {
|
|
610
|
+
this.loading = false;
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
void this.load(customer.id);
|
|
614
|
+
}));
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
async load(customerId) {
|
|
618
|
+
const ordersRes = await firstValueFrom(this.apiService
|
|
619
|
+
.on('apps/commerce/order')
|
|
620
|
+
.getMany({ customerId, perPage: 200 })
|
|
621
|
+
.pipe(catchError(() => of({ data: [] }))));
|
|
622
|
+
const orders = ordersRes?.data ?? [];
|
|
623
|
+
const numberById = new Map(orders.map((o) => [o.id, o.orderNumber]));
|
|
624
|
+
const perOrder = await Promise.all(orders.map((o) => firstValueFrom(this.apiService
|
|
625
|
+
.on('apps/commerce/payment')
|
|
626
|
+
.getMany({ orderId: o.id, perPage: 50 })
|
|
627
|
+
.pipe(catchError(() => of({ data: [] }))))));
|
|
628
|
+
this.payments = perOrder
|
|
629
|
+
.flatMap((res) => res?.data ?? [])
|
|
630
|
+
.map((p) => ({
|
|
631
|
+
id: p.id,
|
|
632
|
+
orderId: p.orderId,
|
|
633
|
+
orderNumber: numberById.get(p.orderId) ?? '',
|
|
634
|
+
provider: p.provider,
|
|
635
|
+
method: p.method,
|
|
636
|
+
status: p.status,
|
|
637
|
+
amount: Number(p.amount ?? 0),
|
|
638
|
+
currency: p.currency ?? 'NGN',
|
|
639
|
+
paidAt: p.paidAt,
|
|
640
|
+
createdAt: p.createdAt,
|
|
641
|
+
}))
|
|
642
|
+
.sort((a, b) => this.ts(b.paidAt ?? b.createdAt) - this.ts(a.paidAt ?? a.createdAt));
|
|
643
|
+
this.loading = false;
|
|
644
|
+
}
|
|
645
|
+
ts(v) {
|
|
646
|
+
const t = v ? Date.parse(v) : NaN;
|
|
647
|
+
return Number.isNaN(t) ? 0 : t;
|
|
648
|
+
}
|
|
649
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountPaymentsComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
650
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: AccountPaymentsComponent, isStandalone: false, selector: "x-account-payments", usesInheritance: true, ngImport: i0, template: "<h2 class=\"x-acct-h2\">Payments</h2>\n<p class=\"x-acct-sub\">Every payment across your orders.</p>\n\n@if (loading) {\n <div class=\"x-acct-empty\">Loading\u2026</div>\n} @else if (!payments.length) {\n <div class=\"x-acct-card\"><div class=\"x-acct-empty\">No payments yet.</div></div>\n} @else {\n <div class=\"x-acct-card\">\n @for (p of payments; track p.id) {\n <div class=\"x-acct-row\">\n <span>\n @if (p.orderId) {\n <a [routerLink]=\"['../orders', p.orderId]\"><strong>{{ p.orderNumber }}</strong></a>\n } @else {\n <strong>{{ p.orderNumber || 'Payment' }}</strong>\n }\n <span class=\"x-acct-sub\" style=\"display: block; margin-top: 2px\">\n {{ p.method || p.provider }} \u00B7 {{ (p.paidAt || p.createdAt) | date: 'mediumDate' }}\n </span>\n </span>\n <span class=\"x-acct-pill\" [class.x-acct-pill-ok]=\"p.status === 'success'\">\n {{ p.status }}\n </span>\n <strong>{{ p.currency }} {{ p.amount | number: '1.2-2' }}</strong>\n </div>\n }\n </div>\n}\n", dependencies: [{ kind: "directive", type: i1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: i2.DecimalPipe, name: "number" }, { kind: "pipe", type: i2.DatePipe, name: "date" }] }); }
|
|
651
|
+
}
|
|
652
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountPaymentsComponent, decorators: [{
|
|
653
|
+
type: Component,
|
|
654
|
+
args: [{ standalone: false, selector: 'x-account-payments', template: "<h2 class=\"x-acct-h2\">Payments</h2>\n<p class=\"x-acct-sub\">Every payment across your orders.</p>\n\n@if (loading) {\n <div class=\"x-acct-empty\">Loading\u2026</div>\n} @else if (!payments.length) {\n <div class=\"x-acct-card\"><div class=\"x-acct-empty\">No payments yet.</div></div>\n} @else {\n <div class=\"x-acct-card\">\n @for (p of payments; track p.id) {\n <div class=\"x-acct-row\">\n <span>\n @if (p.orderId) {\n <a [routerLink]=\"['../orders', p.orderId]\"><strong>{{ p.orderNumber }}</strong></a>\n } @else {\n <strong>{{ p.orderNumber || 'Payment' }}</strong>\n }\n <span class=\"x-acct-sub\" style=\"display: block; margin-top: 2px\">\n {{ p.method || p.provider }} \u00B7 {{ (p.paidAt || p.createdAt) | date: 'mediumDate' }}\n </span>\n </span>\n <span class=\"x-acct-pill\" [class.x-acct-pill-ok]=\"p.status === 'success'\">\n {{ p.status }}\n </span>\n <strong>{{ p.currency }} {{ p.amount | number: '1.2-2' }}</strong>\n </div>\n }\n </div>\n}\n" }]
|
|
655
|
+
}] });
|
|
656
|
+
|
|
657
|
+
// P7.7, SEC-04 / SEC-05 (NDPR) — the customer's data controls: export a
|
|
658
|
+
// copy of everything held about them, and lodge an account-deletion
|
|
659
|
+
// request. Cookie / tracking consent is the site-wide consent bar; this
|
|
660
|
+
// page is the data-subject access + erasure surface.
|
|
661
|
+
class AccountPrivacyComponent extends BaseDirective {
|
|
662
|
+
constructor() {
|
|
663
|
+
super(...arguments);
|
|
664
|
+
this.api = inject(ApiService);
|
|
665
|
+
this.store = inject(AccountStoreService);
|
|
666
|
+
this.exporting = false;
|
|
667
|
+
this.requesting = false;
|
|
668
|
+
this.deletionLodged = false;
|
|
669
|
+
this.deletionReason = '';
|
|
670
|
+
this.init = () => {
|
|
671
|
+
this.store
|
|
672
|
+
.customer()
|
|
673
|
+
.subscribe((c) => (this.customerId = c?.id));
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
async downloadData() {
|
|
677
|
+
if (this.exporting)
|
|
678
|
+
return;
|
|
679
|
+
this.exporting = true;
|
|
680
|
+
try {
|
|
681
|
+
const res = await firstValueFrom(this.api.post('apps/commerce/data-request/export', {
|
|
682
|
+
customerId: this.customerId,
|
|
683
|
+
}));
|
|
684
|
+
const bundle = res?.data ?? res;
|
|
685
|
+
const blob = new Blob([JSON.stringify(bundle, null, 2)], {
|
|
686
|
+
type: 'application/json',
|
|
687
|
+
});
|
|
688
|
+
const url = URL.createObjectURL(blob);
|
|
689
|
+
const a = document.createElement('a');
|
|
690
|
+
a.href = url;
|
|
691
|
+
a.download = `tilmart-data-${new Date().toISOString().slice(0, 10)}.json`;
|
|
692
|
+
a.click();
|
|
693
|
+
URL.revokeObjectURL(url);
|
|
694
|
+
}
|
|
695
|
+
catch {
|
|
696
|
+
notify('We couldn’t prepare your data right now. Please try again.', undefined, 'error');
|
|
697
|
+
}
|
|
698
|
+
finally {
|
|
699
|
+
this.exporting = false;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async requestDeletion() {
|
|
703
|
+
if (this.requesting)
|
|
704
|
+
return;
|
|
705
|
+
this.requesting = true;
|
|
706
|
+
try {
|
|
707
|
+
await firstValueFrom(this.api.post('apps/commerce/data-request', {
|
|
708
|
+
type: 'deletion',
|
|
709
|
+
customerId: this.customerId,
|
|
710
|
+
note: this.deletionReason || undefined,
|
|
711
|
+
}));
|
|
712
|
+
this.deletionLodged = true;
|
|
713
|
+
notify('Your deletion request has been lodged.', undefined, 'success');
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
notify('We couldn’t lodge your request. Please try again.', undefined, 'error');
|
|
717
|
+
}
|
|
718
|
+
finally {
|
|
719
|
+
this.requesting = false;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountPrivacyComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
723
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: AccountPrivacyComponent, isStandalone: false, selector: "x-account-privacy", usesInheritance: true, ngImport: i0, template: "<div class=\"x-pv\">\n <h1 class=\"x-pv-title\">Privacy & your data</h1>\n <p class=\"x-pv-intro\">\n Under the Nigeria Data Protection Act you can get a copy of the personal\n data we hold about you, and ask us to delete your account.\n </p>\n\n <section class=\"x-pv-card\">\n <h2>Download my data</h2>\n <p>\n A machine-readable copy of your profile, addresses, orders, returns,\n warranties, reviews, questions, support messages, notification\n preferences and saved tools.\n </p>\n <button\n type=\"button\"\n class=\"x-pv-btn\"\n [disabled]=\"exporting || !customerId\"\n (click)=\"downloadData()\"\n >\n {{ exporting ? 'Preparing\u2026' : 'Download my data (JSON)' }}\n </button>\n </section>\n\n <section class=\"x-pv-card\">\n <h2>Delete my account</h2>\n @if (deletionLodged) {\n <p class=\"x-pv-ok\">\n Your request is lodged. Our team will complete it within 30 days and\n email you when it\u2019s done.\n </p>\n } @else {\n <p>\n We\u2019ll remove your personal details. Some records are kept where the\n law requires \u2014 see below.\n </p>\n <textarea\n class=\"x-pv-reason\"\n rows=\"2\"\n placeholder=\"Anything you'd like us to know (optional)\"\n [(ngModel)]=\"deletionReason\"\n ></textarea>\n <button\n type=\"button\"\n class=\"x-pv-btn x-pv-btn-danger\"\n [disabled]=\"requesting || !customerId\"\n (click)=\"requestDeletion()\"\n >\n {{ requesting ? 'Sending\u2026' : 'Request account deletion' }}\n </button>\n }\n </section>\n\n <section class=\"x-pv-card x-pv-retain\">\n <h2>What we keep, and why</h2>\n <ul>\n <li><strong>Order & payment records</strong> \u2014 financial and tax law.</li>\n <li><strong>Warranty registrations & claim outcomes</strong> \u2014 our ongoing warranty obligation to the product owner.</li>\n <li><strong>Audit-log entries</strong> \u2014 security and accountability.</li>\n </ul>\n <p class=\"x-pv-fine\">\n Kept records are stripped of contact details where possible and are not\n used for marketing.\n </p>\n </section>\n</div>\n\n<style>\n .x-pv { max-width: 640px; }\n .x-pv-title { font-size: 22px; font-weight: 800; color: var(--color-text); margin: 0 0 6px; }\n .x-pv-intro { font-size: 14px; color: var(--color-text-muted); line-height: 1.6; margin: 0 0 20px; }\n .x-pv-card { border: 1px solid var(--color-border); border-radius: 12px; padding: 16px 18px; margin-bottom: 14px; }\n .x-pv-card h2 { font-size: 15px; font-weight: 700; color: var(--color-text); margin: 0 0 6px; }\n .x-pv-card p { font-size: 13.5px; color: var(--color-text-muted); line-height: 1.6; margin: 0 0 12px; }\n .x-pv-btn { font: inherit; font-weight: 700; font-size: 13.5px; padding: 10px 18px; border: 0; border-radius: 8px; background: var(--color-brand); color: #fff; cursor: pointer; }\n .x-pv-btn:disabled { opacity: 0.5; cursor: not-allowed; }\n .x-pv-btn-danger { background: var(--color-danger, #c0392b); }\n .x-pv-reason { width: 100%; box-sizing: border-box; font: inherit; padding: 8px 10px; border: 1px solid var(--color-border); border-radius: 6px; background: var(--color-surface, #fff); color: var(--color-text); margin-bottom: 12px; }\n .x-pv-ok { color: var(--color-text); }\n .x-pv-retain ul { margin: 0 0 8px; padding-left: 18px; font-size: 13.5px; color: var(--color-text-muted); line-height: 1.7; }\n .x-pv-fine { font-size: 12.5px; }\n</style>\n", styles: [".x-pv{max-width:640px}.x-pv-title{font-size:22px;font-weight:800;color:var(--color-text);margin:0 0 6px}.x-pv-intro{font-size:14px;color:var(--color-text-muted);line-height:1.6;margin:0 0 20px}.x-pv-card{border:1px solid var(--color-border);border-radius:12px;padding:16px 18px;margin-bottom:14px}.x-pv-card h2{font-size:15px;font-weight:700;color:var(--color-text);margin:0 0 6px}.x-pv-card p{font-size:13.5px;color:var(--color-text-muted);line-height:1.6;margin:0 0 12px}.x-pv-btn{font:inherit;font-weight:700;font-size:13.5px;padding:10px 18px;border:0;border-radius:8px;background:var(--color-brand);color:#fff;cursor:pointer}.x-pv-btn:disabled{opacity:.5;cursor:not-allowed}.x-pv-btn-danger{background:var(--color-danger, #c0392b)}.x-pv-reason{width:100%;box-sizing:border-box;font:inherit;padding:8px 10px;border:1px solid var(--color-border);border-radius:6px;background:var(--color-surface, #fff);color:var(--color-text);margin-bottom:12px}.x-pv-ok{color:var(--color-text)}.x-pv-retain ul{margin:0 0 8px;padding-left:18px;font-size:13.5px;color:var(--color-text-muted);line-height:1.7}.x-pv-fine{font-size:12.5px}\n"], dependencies: [{ kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] }); }
|
|
724
|
+
}
|
|
725
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountPrivacyComponent, decorators: [{
|
|
726
|
+
type: Component,
|
|
727
|
+
args: [{ standalone: false, selector: 'x-account-privacy', template: "<div class=\"x-pv\">\n <h1 class=\"x-pv-title\">Privacy & your data</h1>\n <p class=\"x-pv-intro\">\n Under the Nigeria Data Protection Act you can get a copy of the personal\n data we hold about you, and ask us to delete your account.\n </p>\n\n <section class=\"x-pv-card\">\n <h2>Download my data</h2>\n <p>\n A machine-readable copy of your profile, addresses, orders, returns,\n warranties, reviews, questions, support messages, notification\n preferences and saved tools.\n </p>\n <button\n type=\"button\"\n class=\"x-pv-btn\"\n [disabled]=\"exporting || !customerId\"\n (click)=\"downloadData()\"\n >\n {{ exporting ? 'Preparing\u2026' : 'Download my data (JSON)' }}\n </button>\n </section>\n\n <section class=\"x-pv-card\">\n <h2>Delete my account</h2>\n @if (deletionLodged) {\n <p class=\"x-pv-ok\">\n Your request is lodged. Our team will complete it within 30 days and\n email you when it\u2019s done.\n </p>\n } @else {\n <p>\n We\u2019ll remove your personal details. Some records are kept where the\n law requires \u2014 see below.\n </p>\n <textarea\n class=\"x-pv-reason\"\n rows=\"2\"\n placeholder=\"Anything you'd like us to know (optional)\"\n [(ngModel)]=\"deletionReason\"\n ></textarea>\n <button\n type=\"button\"\n class=\"x-pv-btn x-pv-btn-danger\"\n [disabled]=\"requesting || !customerId\"\n (click)=\"requestDeletion()\"\n >\n {{ requesting ? 'Sending\u2026' : 'Request account deletion' }}\n </button>\n }\n </section>\n\n <section class=\"x-pv-card x-pv-retain\">\n <h2>What we keep, and why</h2>\n <ul>\n <li><strong>Order & payment records</strong> \u2014 financial and tax law.</li>\n <li><strong>Warranty registrations & claim outcomes</strong> \u2014 our ongoing warranty obligation to the product owner.</li>\n <li><strong>Audit-log entries</strong> \u2014 security and accountability.</li>\n </ul>\n <p class=\"x-pv-fine\">\n Kept records are stripped of contact details where possible and are not\n used for marketing.\n </p>\n </section>\n</div>\n\n<style>\n .x-pv { max-width: 640px; }\n .x-pv-title { font-size: 22px; font-weight: 800; color: var(--color-text); margin: 0 0 6px; }\n .x-pv-intro { font-size: 14px; color: var(--color-text-muted); line-height: 1.6; margin: 0 0 20px; }\n .x-pv-card { border: 1px solid var(--color-border); border-radius: 12px; padding: 16px 18px; margin-bottom: 14px; }\n .x-pv-card h2 { font-size: 15px; font-weight: 700; color: var(--color-text); margin: 0 0 6px; }\n .x-pv-card p { font-size: 13.5px; color: var(--color-text-muted); line-height: 1.6; margin: 0 0 12px; }\n .x-pv-btn { font: inherit; font-weight: 700; font-size: 13.5px; padding: 10px 18px; border: 0; border-radius: 8px; background: var(--color-brand); color: #fff; cursor: pointer; }\n .x-pv-btn:disabled { opacity: 0.5; cursor: not-allowed; }\n .x-pv-btn-danger { background: var(--color-danger, #c0392b); }\n .x-pv-reason { width: 100%; box-sizing: border-box; font: inherit; padding: 8px 10px; border: 1px solid var(--color-border); border-radius: 6px; background: var(--color-surface, #fff); color: var(--color-text); margin-bottom: 12px; }\n .x-pv-ok { color: var(--color-text); }\n .x-pv-retain ul { margin: 0 0 8px; padding-left: 18px; font-size: 13.5px; color: var(--color-text-muted); line-height: 1.7; }\n .x-pv-fine { font-size: 12.5px; }\n</style>\n" }]
|
|
728
|
+
}] });
|
|
729
|
+
|
|
730
|
+
const STATUS_LABELS$1 = {
|
|
731
|
+
requested: 'Requested',
|
|
732
|
+
approved: 'Approved',
|
|
733
|
+
rejected: 'Rejected',
|
|
734
|
+
awaiting_return: 'Awaiting your return',
|
|
735
|
+
in_transit: 'On its way back',
|
|
736
|
+
received: 'Received',
|
|
737
|
+
inspecting: 'Being inspected',
|
|
738
|
+
resolved: 'Resolved',
|
|
739
|
+
cancelled: 'Cancelled',
|
|
740
|
+
};
|
|
741
|
+
// P5.9, RET-03 — one RMA in full: status, instructions, per-line
|
|
742
|
+
// inspection outcome, the resolution and any refund amount.
|
|
743
|
+
class AccountReturnDetailComponent extends BaseDirective {
|
|
744
|
+
constructor() {
|
|
745
|
+
super(...arguments);
|
|
746
|
+
this.route = inject(ActivatedRoute);
|
|
747
|
+
this.loading = true;
|
|
748
|
+
this.found = false;
|
|
749
|
+
this.rmaNumber = '';
|
|
750
|
+
this.status = '';
|
|
751
|
+
this.statusLabel = '';
|
|
752
|
+
this.reasonCode = '';
|
|
753
|
+
this.orderId = '';
|
|
754
|
+
this.lines = [];
|
|
755
|
+
this.init = () => {
|
|
756
|
+
const id = this.route.snapshot.paramMap.get('id') ?? '';
|
|
757
|
+
if (!id) {
|
|
758
|
+
this.loading = false;
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
forkJoin({
|
|
762
|
+
request: this.apiService
|
|
763
|
+
.on('apps/commerce/return-request')
|
|
764
|
+
.getOne(id)
|
|
765
|
+
.pipe(catchError(() => of(undefined))),
|
|
766
|
+
lines: this.apiService
|
|
767
|
+
.on('apps/commerce/return-line')
|
|
768
|
+
.getMany({ returnRequestId: id, perPage: 100 })
|
|
769
|
+
.pipe(catchError(() => of({ data: [] }))),
|
|
770
|
+
})
|
|
771
|
+
.pipe(takeUntil(this.$))
|
|
772
|
+
.subscribe(using((res) => {
|
|
773
|
+
this.loading = false;
|
|
774
|
+
const r = res?.request?.data;
|
|
775
|
+
if (!r)
|
|
776
|
+
return;
|
|
777
|
+
this.found = true;
|
|
778
|
+
this.rmaNumber = r.rmaNumber;
|
|
779
|
+
this.status = r.status;
|
|
780
|
+
this.statusLabel = STATUS_LABELS$1[r.status] ?? r.status;
|
|
781
|
+
this.reasonCode = r.reasonCode;
|
|
782
|
+
this.method = r.method;
|
|
783
|
+
this.instructions = r.instructions;
|
|
784
|
+
this.whoPaysReturn = r.whoPaysReturn;
|
|
785
|
+
this.resolution = r.resolution;
|
|
786
|
+
this.refundAmount = r.refundAmount != null ? Number(r.refundAmount) : undefined;
|
|
787
|
+
this.createdAt = r.createdAt;
|
|
788
|
+
this.resolvedAt = r.resolvedAt;
|
|
789
|
+
this.orderId = r.orderId;
|
|
790
|
+
const rawLines = res?.lines?.data ?? [];
|
|
791
|
+
this.lines = rawLines.map((l) => ({
|
|
792
|
+
name: 'Item',
|
|
793
|
+
quantity: Number(l.quantity ?? 1),
|
|
794
|
+
inspectionOutcome: l.inspectionOutcome,
|
|
795
|
+
acceptedQuantity: l.acceptedQuantity,
|
|
796
|
+
disposition: l.disposition,
|
|
797
|
+
}));
|
|
798
|
+
void this.nameLines(r.orderId, rawLines);
|
|
799
|
+
}));
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
async nameLines(orderId, rawLines) {
|
|
803
|
+
const orderLines = await firstValueFrom(this.apiService
|
|
804
|
+
.on('apps/commerce/order-line')
|
|
805
|
+
.getMany({ orderId, perPage: 200 })
|
|
806
|
+
.pipe(catchError(() => of({ data: [] }))));
|
|
807
|
+
const nameById = new Map((orderLines?.data ?? []).map((o) => [o.id, o.nameSnapshot || 'Item']));
|
|
808
|
+
this.lines = this.lines.map((vm, i) => ({
|
|
809
|
+
...vm,
|
|
810
|
+
name: nameById.get(rawLines[i]?.orderLineId) ?? 'Item',
|
|
811
|
+
}));
|
|
812
|
+
}
|
|
813
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountReturnDetailComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
814
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: AccountReturnDetailComponent, isStandalone: false, selector: "x-account-return-detail", usesInheritance: true, ngImport: i0, template: "<a routerLink=\"../\" class=\"x-acct-btn-ghost\">\u2039 All returns</a>\n\n@if (loading) {\n <div class=\"x-acct-empty\" style=\"margin-top: 16px\">Loading\u2026</div>\n} @else if (!found) {\n <div class=\"x-acct-card\" style=\"margin-top: 16px\">\n <div class=\"x-acct-empty\">We couldn't find that return.</div>\n </div>\n} @else {\n <header style=\"margin: 16px 0 8px\">\n <h2 class=\"x-acct-h2\">{{ rmaNumber }}</h2>\n <p class=\"x-acct-sub\">\n <span class=\"x-acct-pill\">{{ statusLabel }}</span>\n \u00B7 requested {{ createdAt | date: 'mediumDate' }}\n @if (orderId) {\n \u00B7 <a [routerLink]=\"['/account/orders', orderId]\">view order</a>\n }\n </p>\n </header>\n\n @if (instructions) {\n <div class=\"x-acct-card\">\n <strong>What to do</strong>\n <p class=\"x-acct-sub\" style=\"margin: 6px 0 0\">{{ instructions }}</p>\n </div>\n }\n\n <div class=\"x-acct-card\">\n <strong>Items</strong>\n @for (line of lines; track $index) {\n <div class=\"x-acct-row\">\n <span>\n {{ line.name }}\n <span class=\"x-acct-sub\" style=\"display: block; margin-top: 2px\">\n Qty {{ line.quantity }}\n @if (line.inspectionOutcome !== 'pending') {\n \u00B7 inspected: {{ line.inspectionOutcome }}\n @if (line.acceptedQuantity != null) {\n ({{ line.acceptedQuantity }} accepted)\n }\n }\n </span>\n </span>\n </div>\n }\n </div>\n\n @if (resolution) {\n <div class=\"x-acct-card\">\n <strong>Outcome</strong>\n <div class=\"x-acct-row\">\n <span>{{ resolution }}</span>\n @if (refundAmount != null) {\n <strong>Refund: {{ refundAmount | number: '1.2-2' }}</strong>\n }\n </div>\n @if (resolvedAt) {\n <p class=\"x-acct-sub\" style=\"margin: 0\">Resolved {{ resolvedAt | date: 'medium' }}</p>\n }\n </div>\n }\n}\n", dependencies: [{ kind: "directive", type: i1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: i2.DecimalPipe, name: "number" }, { kind: "pipe", type: i2.DatePipe, name: "date" }] }); }
|
|
815
|
+
}
|
|
816
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountReturnDetailComponent, decorators: [{
|
|
817
|
+
type: Component,
|
|
818
|
+
args: [{ standalone: false, selector: 'x-account-return-detail', template: "<a routerLink=\"../\" class=\"x-acct-btn-ghost\">\u2039 All returns</a>\n\n@if (loading) {\n <div class=\"x-acct-empty\" style=\"margin-top: 16px\">Loading\u2026</div>\n} @else if (!found) {\n <div class=\"x-acct-card\" style=\"margin-top: 16px\">\n <div class=\"x-acct-empty\">We couldn't find that return.</div>\n </div>\n} @else {\n <header style=\"margin: 16px 0 8px\">\n <h2 class=\"x-acct-h2\">{{ rmaNumber }}</h2>\n <p class=\"x-acct-sub\">\n <span class=\"x-acct-pill\">{{ statusLabel }}</span>\n \u00B7 requested {{ createdAt | date: 'mediumDate' }}\n @if (orderId) {\n \u00B7 <a [routerLink]=\"['/account/orders', orderId]\">view order</a>\n }\n </p>\n </header>\n\n @if (instructions) {\n <div class=\"x-acct-card\">\n <strong>What to do</strong>\n <p class=\"x-acct-sub\" style=\"margin: 6px 0 0\">{{ instructions }}</p>\n </div>\n }\n\n <div class=\"x-acct-card\">\n <strong>Items</strong>\n @for (line of lines; track $index) {\n <div class=\"x-acct-row\">\n <span>\n {{ line.name }}\n <span class=\"x-acct-sub\" style=\"display: block; margin-top: 2px\">\n Qty {{ line.quantity }}\n @if (line.inspectionOutcome !== 'pending') {\n \u00B7 inspected: {{ line.inspectionOutcome }}\n @if (line.acceptedQuantity != null) {\n ({{ line.acceptedQuantity }} accepted)\n }\n }\n </span>\n </span>\n </div>\n }\n </div>\n\n @if (resolution) {\n <div class=\"x-acct-card\">\n <strong>Outcome</strong>\n <div class=\"x-acct-row\">\n <span>{{ resolution }}</span>\n @if (refundAmount != null) {\n <strong>Refund: {{ refundAmount | number: '1.2-2' }}</strong>\n }\n </div>\n @if (resolvedAt) {\n <p class=\"x-acct-sub\" style=\"margin: 0\">Resolved {{ resolvedAt | date: 'medium' }}</p>\n }\n </div>\n }\n}\n" }]
|
|
819
|
+
}] });
|
|
820
|
+
|
|
821
|
+
const REASONS = [
|
|
822
|
+
{ code: 'damaged_in_transit', label: 'Damaged in transit' },
|
|
823
|
+
{ code: 'wrong_item', label: 'Wrong item sent' },
|
|
824
|
+
{ code: 'not_as_described', label: 'Not as described' },
|
|
825
|
+
{ code: 'faulty_on_arrival', label: 'Faulty on arrival' },
|
|
826
|
+
{ code: 'changed_mind', label: 'Changed my mind' },
|
|
827
|
+
{ code: 'missing_parts', label: 'Missing parts' },
|
|
828
|
+
];
|
|
829
|
+
const METHODS = [
|
|
830
|
+
{ code: 'pickup', label: 'Pickup from my address' },
|
|
831
|
+
{ code: 'drop_off', label: 'Drop off at a collection point' },
|
|
832
|
+
{ code: 'station', label: 'Drop at a pickup station' },
|
|
833
|
+
];
|
|
834
|
+
// P5.9, RET-02 — the self-serve return request. Pick items + a reason;
|
|
835
|
+
// the eligibility check (window, who pays) runs before the RMA is minted.
|
|
836
|
+
class AccountReturnRequestComponent extends BaseDirective {
|
|
837
|
+
constructor() {
|
|
838
|
+
super(...arguments);
|
|
839
|
+
this.route = inject(ActivatedRoute);
|
|
840
|
+
this.store = inject(AccountStoreService);
|
|
841
|
+
this.reasons = REASONS;
|
|
842
|
+
this.methods = METHODS;
|
|
843
|
+
this.loading = true;
|
|
844
|
+
this.orderId = '';
|
|
845
|
+
this.orderNumber = '';
|
|
846
|
+
this.customerId = '';
|
|
847
|
+
this.lines = [];
|
|
848
|
+
this.reasonCode = '';
|
|
849
|
+
this.method = 'pickup';
|
|
850
|
+
this.notes = '';
|
|
851
|
+
this.checking = false;
|
|
852
|
+
this.eligibility = null;
|
|
853
|
+
this.submitting = false;
|
|
854
|
+
this.error = '';
|
|
855
|
+
this.createdRma = '';
|
|
856
|
+
this.init = () => {
|
|
857
|
+
this.orderId = this.route.snapshot.paramMap.get('id') ?? '';
|
|
858
|
+
this.store.customer().subscribe((c) => (this.customerId = c?.id ?? ''));
|
|
859
|
+
void this.load();
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
async load() {
|
|
863
|
+
const [order, lines] = await Promise.all([
|
|
864
|
+
firstValueFrom(this.apiService
|
|
865
|
+
.on('apps/commerce/order')
|
|
866
|
+
.getOne(this.orderId)
|
|
867
|
+
.pipe(catchError(() => of(undefined)))),
|
|
868
|
+
firstValueFrom(this.apiService
|
|
869
|
+
.on('apps/commerce/order-line')
|
|
870
|
+
.getMany({ orderId: this.orderId, perPage: 200 })
|
|
871
|
+
.pipe(catchError(() => of({ data: [] })))),
|
|
872
|
+
]);
|
|
873
|
+
this.orderNumber = order?.data?.orderNumber ?? '';
|
|
874
|
+
this.lines = (lines?.data ?? []).map((l) => ({
|
|
875
|
+
orderLineId: l.id,
|
|
876
|
+
name: l.nameSnapshot || 'Item',
|
|
877
|
+
orderedQty: Number(l.quantity ?? 1),
|
|
878
|
+
selected: false,
|
|
879
|
+
quantity: Number(l.quantity ?? 1),
|
|
880
|
+
}));
|
|
881
|
+
this.loading = false;
|
|
882
|
+
}
|
|
883
|
+
get selectedLines() {
|
|
884
|
+
return this.lines.filter((l) => l.selected);
|
|
885
|
+
}
|
|
886
|
+
get canSubmit() {
|
|
887
|
+
return (!!this.reasonCode &&
|
|
888
|
+
this.selectedLines.length > 0 &&
|
|
889
|
+
this.eligibility?.eligible === true);
|
|
890
|
+
}
|
|
891
|
+
async checkEligibility() {
|
|
892
|
+
if (!this.reasonCode) {
|
|
893
|
+
this.eligibility = null;
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
this.checking = true;
|
|
897
|
+
try {
|
|
898
|
+
const res = await firstValueFrom(this.apiService.post('apps/commerce/return-request/eligibility', {
|
|
899
|
+
orderId: this.orderId,
|
|
900
|
+
reasonCode: this.reasonCode,
|
|
901
|
+
}));
|
|
902
|
+
this.eligibility = res?.data ?? null;
|
|
903
|
+
}
|
|
904
|
+
catch {
|
|
905
|
+
this.eligibility = null;
|
|
906
|
+
}
|
|
907
|
+
finally {
|
|
908
|
+
this.checking = false;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
async submit() {
|
|
912
|
+
if (this.submitting || !this.canSubmit)
|
|
913
|
+
return;
|
|
914
|
+
this.submitting = true;
|
|
915
|
+
this.error = '';
|
|
916
|
+
try {
|
|
917
|
+
const res = await firstValueFrom(this.apiService.post('apps/commerce/return-request/request', {
|
|
918
|
+
orderId: this.orderId,
|
|
919
|
+
customerId: this.customerId || undefined,
|
|
920
|
+
reasonCode: this.reasonCode,
|
|
921
|
+
method: this.method,
|
|
922
|
+
notes: this.notes || undefined,
|
|
923
|
+
lines: this.selectedLines.map((l) => ({
|
|
924
|
+
orderLineId: l.orderLineId,
|
|
925
|
+
quantity: Math.min(l.quantity, l.orderedQty),
|
|
926
|
+
})),
|
|
927
|
+
}));
|
|
928
|
+
if (res?.status === 'error') {
|
|
929
|
+
this.error = res?.message ?? 'Could not create your return.';
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
this.createdRma = res?.data?.rmaNumber ?? '';
|
|
933
|
+
notify('Return requested', undefined, 'success');
|
|
934
|
+
}
|
|
935
|
+
catch (err) {
|
|
936
|
+
this.error = err?.error?.message ?? 'Could not create your return.';
|
|
937
|
+
}
|
|
938
|
+
finally {
|
|
939
|
+
this.submitting = false;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountReturnRequestComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
943
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: AccountReturnRequestComponent, isStandalone: false, selector: "x-account-return-request", usesInheritance: true, ngImport: i0, template: "<a [routerLink]=\"['/account/orders', orderId]\" class=\"x-acct-btn-ghost\">\u2039 Back to order</a>\n\n@if (loading) {\n <div class=\"x-acct-empty\" style=\"margin-top: 16px\">Loading\u2026</div>\n} @else if (createdRma) {\n <div class=\"x-acct-card\" style=\"margin-top: 16px\">\n <h2 class=\"x-acct-h2\">Return requested</h2>\n <p class=\"x-acct-sub\">\n Your RMA number is <strong>{{ createdRma }}</strong>. We've emailed the\n instructions \u2014 you can also see them on the returns page.\n </p>\n <a class=\"x-acct-btn\" routerLink=\"/account/returns\">View my returns</a>\n </div>\n} @else {\n <h2 class=\"x-acct-h2\" style=\"margin-top: 16px\">Return an item</h2>\n <p class=\"x-acct-sub\">Order {{ orderNumber }}</p>\n\n <div class=\"x-acct-card\">\n <strong>Which items?</strong>\n @for (line of lines; track line.orderLineId) {\n <div class=\"x-acct-row\">\n <label class=\"x-acct-checkline\" style=\"flex: 1\">\n <input type=\"checkbox\" [(ngModel)]=\"line.selected\" />\n {{ line.name }}\n </label>\n @if (line.selected && line.orderedQty > 1) {\n <input\n type=\"number\"\n min=\"1\"\n [max]=\"line.orderedQty\"\n [(ngModel)]=\"line.quantity\"\n style=\"width: 64px\"\n />\n }\n <span class=\"x-acct-sub\">of {{ line.orderedQty }}</span>\n </div>\n }\n </div>\n\n <div class=\"x-acct-card\">\n <div class=\"x-acct-field\">\n <label>Reason</label>\n <select aria-label=\"Reason\" [ngModel]=\"reasonCode\" (ngModelChange)=\"reasonCode = $event; checkEligibility()\">\n <option value=\"\">Choose a reason</option>\n @for (r of reasons; track r.code) {\n <option [value]=\"r.code\">{{ r.label }}</option>\n }\n </select>\n </div>\n\n @if (checking) {\n <div class=\"x-acct-empty\">Checking eligibility\u2026</div>\n } @else if (eligibility) {\n @if (eligibility.eligible) {\n <div class=\"x-acct-ok\">\n Eligible \u2014 {{ eligibility.windowDays }}-day window.\n {{ eligibility.whoPaysReturn === 'customer' ? 'You cover return delivery for this reason.' : 'Return delivery is on us.' }}\n </div>\n } @else {\n <div class=\"x-acct-err\">\n @switch (eligibility.reasons[0]) {\n @case ('window-closed') { The return window for this order has closed. }\n @case ('change-of-mind-not-allowed') { Change-of-mind returns aren't accepted for these items. }\n @case ('returns-not-offered') { Returns aren't available for this order. }\n @default { This return isn't eligible. }\n }\n </div>\n }\n }\n\n <div class=\"x-acct-field\">\n <label>How will you return it?</label>\n <select aria-label=\"How will you return it?\" [(ngModel)]=\"method\">\n @for (m of methods; track m.code) {\n <option [value]=\"m.code\">{{ m.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"x-acct-field\">\n <label>Anything else we should know? (optional)</label>\n <input [(ngModel)]=\"notes\" />\n </div>\n\n @if (error) {\n <div class=\"x-acct-err\">{{ error }}</div>\n }\n <button\n type=\"button\"\n class=\"x-acct-btn\"\n [disabled]=\"!canSubmit || submitting\"\n (click)=\"submit()\"\n >\n {{ submitting ? 'Submitting\u2026' : 'Request return' }}\n </button>\n </div>\n}\n", dependencies: [{ kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }] }); }
|
|
944
|
+
}
|
|
945
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountReturnRequestComponent, decorators: [{
|
|
946
|
+
type: Component,
|
|
947
|
+
args: [{ standalone: false, selector: 'x-account-return-request', template: "<a [routerLink]=\"['/account/orders', orderId]\" class=\"x-acct-btn-ghost\">\u2039 Back to order</a>\n\n@if (loading) {\n <div class=\"x-acct-empty\" style=\"margin-top: 16px\">Loading\u2026</div>\n} @else if (createdRma) {\n <div class=\"x-acct-card\" style=\"margin-top: 16px\">\n <h2 class=\"x-acct-h2\">Return requested</h2>\n <p class=\"x-acct-sub\">\n Your RMA number is <strong>{{ createdRma }}</strong>. We've emailed the\n instructions \u2014 you can also see them on the returns page.\n </p>\n <a class=\"x-acct-btn\" routerLink=\"/account/returns\">View my returns</a>\n </div>\n} @else {\n <h2 class=\"x-acct-h2\" style=\"margin-top: 16px\">Return an item</h2>\n <p class=\"x-acct-sub\">Order {{ orderNumber }}</p>\n\n <div class=\"x-acct-card\">\n <strong>Which items?</strong>\n @for (line of lines; track line.orderLineId) {\n <div class=\"x-acct-row\">\n <label class=\"x-acct-checkline\" style=\"flex: 1\">\n <input type=\"checkbox\" [(ngModel)]=\"line.selected\" />\n {{ line.name }}\n </label>\n @if (line.selected && line.orderedQty > 1) {\n <input\n type=\"number\"\n min=\"1\"\n [max]=\"line.orderedQty\"\n [(ngModel)]=\"line.quantity\"\n style=\"width: 64px\"\n />\n }\n <span class=\"x-acct-sub\">of {{ line.orderedQty }}</span>\n </div>\n }\n </div>\n\n <div class=\"x-acct-card\">\n <div class=\"x-acct-field\">\n <label>Reason</label>\n <select aria-label=\"Reason\" [ngModel]=\"reasonCode\" (ngModelChange)=\"reasonCode = $event; checkEligibility()\">\n <option value=\"\">Choose a reason</option>\n @for (r of reasons; track r.code) {\n <option [value]=\"r.code\">{{ r.label }}</option>\n }\n </select>\n </div>\n\n @if (checking) {\n <div class=\"x-acct-empty\">Checking eligibility\u2026</div>\n } @else if (eligibility) {\n @if (eligibility.eligible) {\n <div class=\"x-acct-ok\">\n Eligible \u2014 {{ eligibility.windowDays }}-day window.\n {{ eligibility.whoPaysReturn === 'customer' ? 'You cover return delivery for this reason.' : 'Return delivery is on us.' }}\n </div>\n } @else {\n <div class=\"x-acct-err\">\n @switch (eligibility.reasons[0]) {\n @case ('window-closed') { The return window for this order has closed. }\n @case ('change-of-mind-not-allowed') { Change-of-mind returns aren't accepted for these items. }\n @case ('returns-not-offered') { Returns aren't available for this order. }\n @default { This return isn't eligible. }\n }\n </div>\n }\n }\n\n <div class=\"x-acct-field\">\n <label>How will you return it?</label>\n <select aria-label=\"How will you return it?\" [(ngModel)]=\"method\">\n @for (m of methods; track m.code) {\n <option [value]=\"m.code\">{{ m.label }}</option>\n }\n </select>\n </div>\n\n <div class=\"x-acct-field\">\n <label>Anything else we should know? (optional)</label>\n <input [(ngModel)]=\"notes\" />\n </div>\n\n @if (error) {\n <div class=\"x-acct-err\">{{ error }}</div>\n }\n <button\n type=\"button\"\n class=\"x-acct-btn\"\n [disabled]=\"!canSubmit || submitting\"\n (click)=\"submit()\"\n >\n {{ submitting ? 'Submitting\u2026' : 'Request return' }}\n </button>\n </div>\n}\n" }]
|
|
948
|
+
}] });
|
|
949
|
+
|
|
950
|
+
const STATUS_LABELS = {
|
|
951
|
+
requested: 'Requested',
|
|
952
|
+
approved: 'Approved',
|
|
953
|
+
rejected: 'Rejected',
|
|
954
|
+
awaiting_return: 'Awaiting your return',
|
|
955
|
+
in_transit: 'On its way back',
|
|
956
|
+
received: 'Received',
|
|
957
|
+
inspecting: 'Being inspected',
|
|
958
|
+
resolved: 'Resolved',
|
|
959
|
+
cancelled: 'Cancelled',
|
|
960
|
+
};
|
|
961
|
+
const REASON_LABELS = {
|
|
962
|
+
damaged_in_transit: 'Damaged in transit',
|
|
963
|
+
wrong_item: 'Wrong item',
|
|
964
|
+
not_as_described: 'Not as described',
|
|
965
|
+
faulty_on_arrival: 'Faulty on arrival',
|
|
966
|
+
changed_mind: 'Changed mind',
|
|
967
|
+
missing_parts: 'Missing parts',
|
|
968
|
+
};
|
|
969
|
+
// P5.9, RET-02/03 — the buyer's returns list. Each RMA links to its
|
|
970
|
+
// detail (status timeline + resolution).
|
|
971
|
+
class AccountReturnsComponent extends BaseDirective {
|
|
972
|
+
constructor() {
|
|
973
|
+
super(...arguments);
|
|
974
|
+
this.store = inject(AccountStoreService);
|
|
975
|
+
this.loading = true;
|
|
976
|
+
this.returns = [];
|
|
977
|
+
this.init = () => {
|
|
978
|
+
this.store
|
|
979
|
+
.customer()
|
|
980
|
+
.pipe(takeUntil(this.$))
|
|
981
|
+
.subscribe(using((customer) => {
|
|
982
|
+
if (!customer) {
|
|
983
|
+
this.loading = false;
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
void this.load(customer.id);
|
|
987
|
+
}));
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
async load(customerId) {
|
|
991
|
+
const res = await firstValueFrom(this.apiService
|
|
992
|
+
.on('apps/commerce/return-request')
|
|
993
|
+
.getMany({ customerId, perPage: 100 })
|
|
994
|
+
.pipe(catchError(() => of({ data: [] }))));
|
|
995
|
+
this.returns = (res?.data ?? [])
|
|
996
|
+
.slice()
|
|
997
|
+
.sort((a, b) => this.ts(b.createdAt) - this.ts(a.createdAt))
|
|
998
|
+
.map((r) => ({
|
|
999
|
+
id: r.id,
|
|
1000
|
+
rmaNumber: r.rmaNumber,
|
|
1001
|
+
reasonCode: REASON_LABELS[r.reasonCode] ?? r.reasonCode,
|
|
1002
|
+
status: r.status,
|
|
1003
|
+
statusLabel: STATUS_LABELS[r.status] ?? r.status,
|
|
1004
|
+
tone: r.status === 'resolved'
|
|
1005
|
+
? 'ok'
|
|
1006
|
+
: r.status === 'rejected' || r.status === 'cancelled'
|
|
1007
|
+
? 'bad'
|
|
1008
|
+
: 'warn',
|
|
1009
|
+
resolution: r.resolution,
|
|
1010
|
+
createdAt: r.createdAt,
|
|
1011
|
+
}));
|
|
1012
|
+
this.loading = false;
|
|
1013
|
+
}
|
|
1014
|
+
ts(v) {
|
|
1015
|
+
const t = v ? Date.parse(v) : NaN;
|
|
1016
|
+
return Number.isNaN(t) ? 0 : t;
|
|
1017
|
+
}
|
|
1018
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountReturnsComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1019
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: AccountReturnsComponent, isStandalone: false, selector: "x-account-returns", usesInheritance: true, ngImport: i0, template: "<h2 class=\"x-acct-h2\">Returns</h2>\n<p class=\"x-acct-sub\">Start a return from any order \u2014 it'll show up here.</p>\n\n@if (loading) {\n <div class=\"x-acct-empty\">Loading\u2026</div>\n} @else if (!returns.length) {\n <div class=\"x-acct-card\">\n <div class=\"x-acct-empty\">\n No returns yet. Open an order and choose \"Request a return\".\n </div>\n </div>\n} @else {\n <div class=\"x-acct-card\">\n @for (r of returns; track r.id) {\n <a class=\"x-acct-row\" [routerLink]=\"[r.id]\">\n <span>\n <strong>{{ r.rmaNumber }}</strong>\n <span class=\"x-acct-sub\" style=\"display: block; margin-top: 2px\">\n {{ r.reasonCode }} \u00B7 {{ r.createdAt | date: 'mediumDate' }}\n </span>\n </span>\n <span\n class=\"x-acct-pill\"\n [class.x-acct-pill-ok]=\"r.tone === 'ok'\"\n [class.x-acct-pill-warn]=\"r.tone === 'warn'\"\n [class.x-acct-pill-bad]=\"r.tone === 'bad'\"\n >{{ r.statusLabel }}</span\n >\n @if (r.resolution) {\n <span class=\"x-acct-sub\">{{ r.resolution }}</span>\n }\n </a>\n }\n </div>\n}\n", dependencies: [{ kind: "directive", type: i1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: i2.DatePipe, name: "date" }] }); }
|
|
1020
|
+
}
|
|
1021
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountReturnsComponent, decorators: [{
|
|
1022
|
+
type: Component,
|
|
1023
|
+
args: [{ standalone: false, selector: 'x-account-returns', template: "<h2 class=\"x-acct-h2\">Returns</h2>\n<p class=\"x-acct-sub\">Start a return from any order \u2014 it'll show up here.</p>\n\n@if (loading) {\n <div class=\"x-acct-empty\">Loading\u2026</div>\n} @else if (!returns.length) {\n <div class=\"x-acct-card\">\n <div class=\"x-acct-empty\">\n No returns yet. Open an order and choose \"Request a return\".\n </div>\n </div>\n} @else {\n <div class=\"x-acct-card\">\n @for (r of returns; track r.id) {\n <a class=\"x-acct-row\" [routerLink]=\"[r.id]\">\n <span>\n <strong>{{ r.rmaNumber }}</strong>\n <span class=\"x-acct-sub\" style=\"display: block; margin-top: 2px\">\n {{ r.reasonCode }} \u00B7 {{ r.createdAt | date: 'mediumDate' }}\n </span>\n </span>\n <span\n class=\"x-acct-pill\"\n [class.x-acct-pill-ok]=\"r.tone === 'ok'\"\n [class.x-acct-pill-warn]=\"r.tone === 'warn'\"\n [class.x-acct-pill-bad]=\"r.tone === 'bad'\"\n >{{ r.statusLabel }}</span\n >\n @if (r.resolution) {\n <span class=\"x-acct-sub\">{{ r.resolution }}</span>\n }\n </a>\n }\n </div>\n}\n" }]
|
|
1024
|
+
}] });
|
|
1025
|
+
|
|
1026
|
+
class AccountApiService {
|
|
1027
|
+
constructor() {
|
|
1028
|
+
this.apiService = inject(ApiService);
|
|
1029
|
+
}
|
|
1030
|
+
async getProfile() {
|
|
1031
|
+
const res = await firstValueFrom(this.apiService.get('apps/identity/attributes'));
|
|
1032
|
+
return res?.data?.attributes?.user ?? {};
|
|
1033
|
+
}
|
|
1034
|
+
async updateProfile(payload) {
|
|
1035
|
+
await firstValueFrom(this.apiService.patch('apps/identity/attributes/user', payload));
|
|
1036
|
+
}
|
|
1037
|
+
async changePassword(payload) {
|
|
1038
|
+
await firstValueFrom(this.apiService.post('apps/identity/reset-secret/change', payload));
|
|
1039
|
+
}
|
|
1040
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1041
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountApiService, providedIn: 'root' }); }
|
|
1042
|
+
}
|
|
1043
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountApiService, decorators: [{
|
|
1044
|
+
type: Injectable,
|
|
1045
|
+
args: [{
|
|
1046
|
+
providedIn: 'root',
|
|
1047
|
+
}]
|
|
1048
|
+
}] });
|
|
1049
|
+
|
|
1050
|
+
const CATEGORIES = [
|
|
1051
|
+
{ key: 'transactional', label: 'Order & payment updates', locked: true },
|
|
1052
|
+
{ key: 'delivery', label: 'Delivery & dispatch' },
|
|
1053
|
+
{ key: 'marketing', label: 'Offers & promotions', consent: true },
|
|
1054
|
+
{ key: 'price_alert', label: 'Price drop alerts' },
|
|
1055
|
+
{ key: 'back_in_stock', label: 'Back-in-stock alerts' },
|
|
1056
|
+
];
|
|
1057
|
+
const CHANNELS = [
|
|
1058
|
+
{ key: 'email', label: 'Email' },
|
|
1059
|
+
{ key: 'sms', label: 'SMS' },
|
|
1060
|
+
{ key: 'whatsapp', label: 'WhatsApp' },
|
|
1061
|
+
];
|
|
1062
|
+
// P5.3, ACC-01 / ACC-09 / ACC-10 — profile, password, theme and
|
|
1063
|
+
// notification preferences in one place. Theme rides the scaffold
|
|
1064
|
+
// PreferenceService (P5.2); notifications call `notification-preference/
|
|
1065
|
+
// mine` (P5.2's NotificationPreferenceWorkflow).
|
|
1066
|
+
class AccountSettingsComponent extends BaseDirective {
|
|
1067
|
+
constructor() {
|
|
1068
|
+
super(...arguments);
|
|
1069
|
+
this.accountApi = inject(AccountApiService);
|
|
1070
|
+
this.themeService = inject(ThemeService);
|
|
1071
|
+
this.controlService = inject(ControlService);
|
|
1072
|
+
this.categories = CATEGORIES;
|
|
1073
|
+
this.channels = CHANNELS;
|
|
1074
|
+
this.loadingProfile = true;
|
|
1075
|
+
this.savingProfile = false;
|
|
1076
|
+
this.profileError = '';
|
|
1077
|
+
this.fullName = '';
|
|
1078
|
+
this.phoneNumber = '';
|
|
1079
|
+
this.currentSecret = '';
|
|
1080
|
+
this.newSecret = '';
|
|
1081
|
+
this.confirmNewSecret = '';
|
|
1082
|
+
this.changingPassword = false;
|
|
1083
|
+
this.passwordError = '';
|
|
1084
|
+
this.passwordDone = false;
|
|
1085
|
+
this.theme = 'system';
|
|
1086
|
+
this.loadingPrefs = true;
|
|
1087
|
+
this.savingPrefs = false;
|
|
1088
|
+
this.prefsError = '';
|
|
1089
|
+
this.prefCategories = {};
|
|
1090
|
+
this.marketingConsent = false;
|
|
1091
|
+
this.quietHoursEnabled = false;
|
|
1092
|
+
this.quietHoursStart = '21:00';
|
|
1093
|
+
this.quietHoursEnd = '07:00';
|
|
1094
|
+
this.unsubscribedAll = false;
|
|
1095
|
+
this.init = () => {
|
|
1096
|
+
void this.loadProfile();
|
|
1097
|
+
void this.loadTheme();
|
|
1098
|
+
void this.loadPrefs();
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
get canChangePassword() {
|
|
1102
|
+
return (!!this.currentSecret &&
|
|
1103
|
+
!!this.newSecret &&
|
|
1104
|
+
this.newSecret === this.confirmNewSecret);
|
|
1105
|
+
}
|
|
1106
|
+
async loadProfile() {
|
|
1107
|
+
try {
|
|
1108
|
+
const p = await this.accountApi.getProfile();
|
|
1109
|
+
this.fullName = [p.first_name, p.last_name].filter(Boolean).join(' ');
|
|
1110
|
+
this.phoneNumber = p.phone_number ?? '';
|
|
1111
|
+
}
|
|
1112
|
+
catch {
|
|
1113
|
+
this.profileError = 'Could not load your profile.';
|
|
1114
|
+
}
|
|
1115
|
+
finally {
|
|
1116
|
+
this.loadingProfile = false;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
async loadTheme() {
|
|
1120
|
+
try {
|
|
1121
|
+
const v = await firstValueFrom(this.themeService.get());
|
|
1122
|
+
if (v === 'light' || v === 'dark' || v === 'system')
|
|
1123
|
+
this.theme = v;
|
|
1124
|
+
}
|
|
1125
|
+
catch {
|
|
1126
|
+
// keep default
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
async loadPrefs() {
|
|
1130
|
+
try {
|
|
1131
|
+
const res = await firstValueFrom(this.apiService.get('apps/commerce/notification-preference/mine'));
|
|
1132
|
+
const pref = res?.data;
|
|
1133
|
+
if (pref) {
|
|
1134
|
+
this.prefCategories = pref.categories ?? {};
|
|
1135
|
+
this.marketingConsent = !!pref.marketingConsent;
|
|
1136
|
+
this.quietHoursEnabled = !!pref.quietHoursEnabled;
|
|
1137
|
+
this.quietHoursStart = pref.quietHoursStart ?? '21:00';
|
|
1138
|
+
this.quietHoursEnd = pref.quietHoursEnd ?? '07:00';
|
|
1139
|
+
this.unsubscribedAll = !!pref.unsubscribedAll;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
catch {
|
|
1143
|
+
this.prefsError = 'Could not load your notification preferences.';
|
|
1144
|
+
}
|
|
1145
|
+
finally {
|
|
1146
|
+
this.loadingPrefs = false;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
async saveProfile() {
|
|
1150
|
+
if (this.savingProfile)
|
|
1151
|
+
return;
|
|
1152
|
+
this.savingProfile = true;
|
|
1153
|
+
this.profileError = '';
|
|
1154
|
+
try {
|
|
1155
|
+
await this.accountApi.updateProfile({ phone_number: this.phoneNumber });
|
|
1156
|
+
notify('Profile saved', undefined, 'success');
|
|
1157
|
+
}
|
|
1158
|
+
catch (err) {
|
|
1159
|
+
this.profileError = err?.error?.message ?? 'Could not save your profile.';
|
|
1160
|
+
}
|
|
1161
|
+
finally {
|
|
1162
|
+
this.savingProfile = false;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
async changePassword() {
|
|
1166
|
+
if (this.changingPassword || !this.canChangePassword)
|
|
1167
|
+
return;
|
|
1168
|
+
this.changingPassword = true;
|
|
1169
|
+
this.passwordError = '';
|
|
1170
|
+
try {
|
|
1171
|
+
await this.accountApi.changePassword({
|
|
1172
|
+
currentSecret: this.currentSecret,
|
|
1173
|
+
newSecret: this.newSecret,
|
|
1174
|
+
confirmNewSecret: this.confirmNewSecret,
|
|
1175
|
+
});
|
|
1176
|
+
this.passwordDone = true;
|
|
1177
|
+
setTimeout(() => {
|
|
1178
|
+
this.controlService.logout();
|
|
1179
|
+
this.router.navigate(['/start']);
|
|
1180
|
+
}, 1500);
|
|
1181
|
+
}
|
|
1182
|
+
catch (err) {
|
|
1183
|
+
this.passwordError =
|
|
1184
|
+
err?.error?.message ?? 'Could not change your password.';
|
|
1185
|
+
}
|
|
1186
|
+
finally {
|
|
1187
|
+
this.changingPassword = false;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
setTheme(choice) {
|
|
1191
|
+
this.theme = choice;
|
|
1192
|
+
this.themeService.set({ value: choice });
|
|
1193
|
+
}
|
|
1194
|
+
channelOn(category, channel) {
|
|
1195
|
+
return (this.prefCategories[category] ?? []).includes(channel);
|
|
1196
|
+
}
|
|
1197
|
+
toggleChannel(category, channel) {
|
|
1198
|
+
const set = new Set(this.prefCategories[category] ?? []);
|
|
1199
|
+
set.has(channel) ? set.delete(channel) : set.add(channel);
|
|
1200
|
+
this.prefCategories = { ...this.prefCategories, [category]: Array.from(set) };
|
|
1201
|
+
}
|
|
1202
|
+
async savePrefs() {
|
|
1203
|
+
if (this.savingPrefs)
|
|
1204
|
+
return;
|
|
1205
|
+
this.savingPrefs = true;
|
|
1206
|
+
this.prefsError = '';
|
|
1207
|
+
try {
|
|
1208
|
+
await firstValueFrom(this.apiService.post('apps/commerce/notification-preference/mine', {
|
|
1209
|
+
categories: this.prefCategories,
|
|
1210
|
+
marketingConsent: this.marketingConsent,
|
|
1211
|
+
marketingConsentSource: 'account',
|
|
1212
|
+
quietHoursEnabled: this.quietHoursEnabled,
|
|
1213
|
+
quietHoursStart: this.quietHoursStart,
|
|
1214
|
+
quietHoursEnd: this.quietHoursEnd,
|
|
1215
|
+
unsubscribedAll: this.unsubscribedAll,
|
|
1216
|
+
}));
|
|
1217
|
+
notify('Preferences saved', undefined, 'success');
|
|
1218
|
+
}
|
|
1219
|
+
catch (err) {
|
|
1220
|
+
this.prefsError = err?.error?.message ?? 'Could not save your preferences.';
|
|
1221
|
+
}
|
|
1222
|
+
finally {
|
|
1223
|
+
this.savingPrefs = false;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountSettingsComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1227
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: AccountSettingsComponent, isStandalone: false, selector: "x-account-settings", usesInheritance: true, ngImport: i0, template: "<h2 class=\"x-acct-h2\">Settings</h2>\n<p class=\"x-acct-sub\">Profile, security, appearance and how we reach you.</p>\n\n<div class=\"x-acct-card\">\n <strong>Profile</strong>\n @if (loadingProfile) {\n <div class=\"x-acct-empty\">Loading\u2026</div>\n } @else {\n <div style=\"margin-top: 14px\">\n <div class=\"x-acct-field\">\n <label>Name</label>\n <input [value]=\"fullName\" disabled />\n </div>\n <div class=\"x-acct-field\">\n <label>Phone number</label>\n <input type=\"tel\" [(ngModel)]=\"phoneNumber\" placeholder=\"+234\u2026\" />\n </div>\n @if (profileError) {\n <div class=\"x-acct-err\">{{ profileError }}</div>\n }\n <button type=\"button\" class=\"x-acct-btn\" [disabled]=\"savingProfile\" (click)=\"saveProfile()\">\n {{ savingProfile ? 'Saving\u2026' : 'Save profile' }}\n </button>\n </div>\n }\n</div>\n\n<div class=\"x-acct-card\">\n <strong>Change password</strong>\n <div style=\"margin-top: 14px\">\n <div class=\"x-acct-field\">\n <label>Current password</label>\n <input type=\"password\" [(ngModel)]=\"currentSecret\" />\n </div>\n <div class=\"x-acct-field\">\n <label>New password</label>\n <input type=\"password\" [(ngModel)]=\"newSecret\" />\n </div>\n <div class=\"x-acct-field\">\n <label>Confirm new password</label>\n <input type=\"password\" [(ngModel)]=\"confirmNewSecret\" />\n </div>\n @if (passwordError) {\n <div class=\"x-acct-err\">{{ passwordError }}</div>\n }\n @if (passwordDone) {\n <div class=\"x-acct-ok\">Password changed. Signing you out\u2026</div>\n }\n <button\n type=\"button\"\n class=\"x-acct-btn\"\n [disabled]=\"!canChangePassword || changingPassword\"\n (click)=\"changePassword()\"\n >\n {{ changingPassword ? 'Saving\u2026' : 'Change password' }}\n </button>\n </div>\n</div>\n\n<div class=\"x-acct-card\">\n <strong>Appearance</strong>\n <div class=\"x-acct-field\" style=\"margin-top: 14px; max-width: 240px\">\n <label>Theme</label>\n <select aria-label=\"Theme\" [ngModel]=\"theme\" (ngModelChange)=\"setTheme($event)\">\n <option value=\"system\">Match my device</option>\n <option value=\"light\">Light</option>\n <option value=\"dark\">Dark</option>\n </select>\n </div>\n</div>\n\n<div class=\"x-acct-card\">\n <strong>Notifications</strong>\n @if (loadingPrefs) {\n <div class=\"x-acct-empty\">Loading\u2026</div>\n } @else {\n <p class=\"x-acct-sub\" style=\"margin: 8px 0 16px\">\n Order and payment updates are always sent. Everything else is up to you.\n </p>\n\n @for (cat of categories; track cat.key) {\n <div style=\"padding: 10px 0; border-bottom: 1px solid var(--color-border-muted)\">\n <div style=\"font-weight: 700; font-size: 13.5px; margin-bottom: 8px\">\n {{ cat.label }}\n @if (cat.locked) {\n <span class=\"x-acct-pill\" style=\"margin-left: 6px\">Always on</span>\n }\n </div>\n @if (cat.consent && !marketingConsent) {\n <label class=\"x-acct-checkline\">\n <input type=\"checkbox\" [(ngModel)]=\"marketingConsent\" />\n Opt in to offers and promotions\n </label>\n } @else {\n <div style=\"display: flex; gap: 16px; flex-wrap: wrap\">\n @for (ch of channels; track ch.key) {\n <label class=\"x-acct-checkline\">\n <input\n type=\"checkbox\"\n [disabled]=\"cat.locked || unsubscribedAll || (cat.consent && !marketingConsent)\"\n [checked]=\"channelOn(cat.key, ch.key)\"\n (change)=\"toggleChannel(cat.key, ch.key)\"\n />\n {{ ch.label }}\n </label>\n }\n </div>\n @if (cat.consent && marketingConsent) {\n <label class=\"x-acct-checkline\" style=\"margin-top: 4px\">\n <input type=\"checkbox\" [(ngModel)]=\"marketingConsent\" />\n Keep me opted in to offers\n </label>\n }\n }\n </div>\n }\n\n <div style=\"padding: 14px 0\">\n <label class=\"x-acct-checkline\">\n <input type=\"checkbox\" [(ngModel)]=\"quietHoursEnabled\" />\n Quiet hours \u2014 hold non-urgent messages overnight\n </label>\n @if (quietHoursEnabled) {\n <div style=\"display: flex; gap: 12px; margin-top: 8px\">\n <div class=\"x-acct-field\" style=\"margin: 0\">\n <label>From</label>\n <input type=\"time\" [(ngModel)]=\"quietHoursStart\" />\n </div>\n <div class=\"x-acct-field\" style=\"margin: 0\">\n <label>To</label>\n <input type=\"time\" [(ngModel)]=\"quietHoursEnd\" />\n </div>\n </div>\n }\n </div>\n\n <label class=\"x-acct-checkline\">\n <input type=\"checkbox\" [(ngModel)]=\"unsubscribedAll\" />\n Unsubscribe from everything except order & payment updates\n </label>\n\n @if (prefsError) {\n <div class=\"x-acct-err\" style=\"margin-top: 12px\">{{ prefsError }}</div>\n }\n <button\n type=\"button\"\n class=\"x-acct-btn\"\n style=\"margin-top: 14px\"\n [disabled]=\"savingPrefs\"\n (click)=\"savePrefs()\"\n >\n {{ savingPrefs ? 'Saving\u2026' : 'Save preferences' }}\n </button>\n }\n</div>\n", dependencies: [{ kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] }); }
|
|
1228
|
+
}
|
|
1229
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountSettingsComponent, decorators: [{
|
|
1230
|
+
type: Component,
|
|
1231
|
+
args: [{ standalone: false, selector: 'x-account-settings', template: "<h2 class=\"x-acct-h2\">Settings</h2>\n<p class=\"x-acct-sub\">Profile, security, appearance and how we reach you.</p>\n\n<div class=\"x-acct-card\">\n <strong>Profile</strong>\n @if (loadingProfile) {\n <div class=\"x-acct-empty\">Loading\u2026</div>\n } @else {\n <div style=\"margin-top: 14px\">\n <div class=\"x-acct-field\">\n <label>Name</label>\n <input [value]=\"fullName\" disabled />\n </div>\n <div class=\"x-acct-field\">\n <label>Phone number</label>\n <input type=\"tel\" [(ngModel)]=\"phoneNumber\" placeholder=\"+234\u2026\" />\n </div>\n @if (profileError) {\n <div class=\"x-acct-err\">{{ profileError }}</div>\n }\n <button type=\"button\" class=\"x-acct-btn\" [disabled]=\"savingProfile\" (click)=\"saveProfile()\">\n {{ savingProfile ? 'Saving\u2026' : 'Save profile' }}\n </button>\n </div>\n }\n</div>\n\n<div class=\"x-acct-card\">\n <strong>Change password</strong>\n <div style=\"margin-top: 14px\">\n <div class=\"x-acct-field\">\n <label>Current password</label>\n <input type=\"password\" [(ngModel)]=\"currentSecret\" />\n </div>\n <div class=\"x-acct-field\">\n <label>New password</label>\n <input type=\"password\" [(ngModel)]=\"newSecret\" />\n </div>\n <div class=\"x-acct-field\">\n <label>Confirm new password</label>\n <input type=\"password\" [(ngModel)]=\"confirmNewSecret\" />\n </div>\n @if (passwordError) {\n <div class=\"x-acct-err\">{{ passwordError }}</div>\n }\n @if (passwordDone) {\n <div class=\"x-acct-ok\">Password changed. Signing you out\u2026</div>\n }\n <button\n type=\"button\"\n class=\"x-acct-btn\"\n [disabled]=\"!canChangePassword || changingPassword\"\n (click)=\"changePassword()\"\n >\n {{ changingPassword ? 'Saving\u2026' : 'Change password' }}\n </button>\n </div>\n</div>\n\n<div class=\"x-acct-card\">\n <strong>Appearance</strong>\n <div class=\"x-acct-field\" style=\"margin-top: 14px; max-width: 240px\">\n <label>Theme</label>\n <select aria-label=\"Theme\" [ngModel]=\"theme\" (ngModelChange)=\"setTheme($event)\">\n <option value=\"system\">Match my device</option>\n <option value=\"light\">Light</option>\n <option value=\"dark\">Dark</option>\n </select>\n </div>\n</div>\n\n<div class=\"x-acct-card\">\n <strong>Notifications</strong>\n @if (loadingPrefs) {\n <div class=\"x-acct-empty\">Loading\u2026</div>\n } @else {\n <p class=\"x-acct-sub\" style=\"margin: 8px 0 16px\">\n Order and payment updates are always sent. Everything else is up to you.\n </p>\n\n @for (cat of categories; track cat.key) {\n <div style=\"padding: 10px 0; border-bottom: 1px solid var(--color-border-muted)\">\n <div style=\"font-weight: 700; font-size: 13.5px; margin-bottom: 8px\">\n {{ cat.label }}\n @if (cat.locked) {\n <span class=\"x-acct-pill\" style=\"margin-left: 6px\">Always on</span>\n }\n </div>\n @if (cat.consent && !marketingConsent) {\n <label class=\"x-acct-checkline\">\n <input type=\"checkbox\" [(ngModel)]=\"marketingConsent\" />\n Opt in to offers and promotions\n </label>\n } @else {\n <div style=\"display: flex; gap: 16px; flex-wrap: wrap\">\n @for (ch of channels; track ch.key) {\n <label class=\"x-acct-checkline\">\n <input\n type=\"checkbox\"\n [disabled]=\"cat.locked || unsubscribedAll || (cat.consent && !marketingConsent)\"\n [checked]=\"channelOn(cat.key, ch.key)\"\n (change)=\"toggleChannel(cat.key, ch.key)\"\n />\n {{ ch.label }}\n </label>\n }\n </div>\n @if (cat.consent && marketingConsent) {\n <label class=\"x-acct-checkline\" style=\"margin-top: 4px\">\n <input type=\"checkbox\" [(ngModel)]=\"marketingConsent\" />\n Keep me opted in to offers\n </label>\n }\n }\n </div>\n }\n\n <div style=\"padding: 14px 0\">\n <label class=\"x-acct-checkline\">\n <input type=\"checkbox\" [(ngModel)]=\"quietHoursEnabled\" />\n Quiet hours \u2014 hold non-urgent messages overnight\n </label>\n @if (quietHoursEnabled) {\n <div style=\"display: flex; gap: 12px; margin-top: 8px\">\n <div class=\"x-acct-field\" style=\"margin: 0\">\n <label>From</label>\n <input type=\"time\" [(ngModel)]=\"quietHoursStart\" />\n </div>\n <div class=\"x-acct-field\" style=\"margin: 0\">\n <label>To</label>\n <input type=\"time\" [(ngModel)]=\"quietHoursEnd\" />\n </div>\n </div>\n }\n </div>\n\n <label class=\"x-acct-checkline\">\n <input type=\"checkbox\" [(ngModel)]=\"unsubscribedAll\" />\n Unsubscribe from everything except order & payment updates\n </label>\n\n @if (prefsError) {\n <div class=\"x-acct-err\" style=\"margin-top: 12px\">{{ prefsError }}</div>\n }\n <button\n type=\"button\"\n class=\"x-acct-btn\"\n style=\"margin-top: 14px\"\n [disabled]=\"savingPrefs\"\n (click)=\"savePrefs()\"\n >\n {{ savingPrefs ? 'Saving\u2026' : 'Save preferences' }}\n </button>\n }\n</div>\n" }]
|
|
1232
|
+
}] });
|
|
1233
|
+
|
|
1234
|
+
// P5.6, ACC-06 — one registered system: product, serial, dates,
|
|
1235
|
+
// certificate download (P4.18), any open safety notice for the variant,
|
|
1236
|
+
// and the claim history. "Start a claim" links to the claim form.
|
|
1237
|
+
class AccountSystemDetailComponent extends BaseDirective {
|
|
1238
|
+
constructor() {
|
|
1239
|
+
super(...arguments);
|
|
1240
|
+
this.route = inject(ActivatedRoute);
|
|
1241
|
+
this.loading = true;
|
|
1242
|
+
this.found = false;
|
|
1243
|
+
this.warrantyId = '';
|
|
1244
|
+
this.productName = 'Registered unit';
|
|
1245
|
+
this.variantId = '';
|
|
1246
|
+
this.isVerified = false;
|
|
1247
|
+
this.monthsLeft = null;
|
|
1248
|
+
this.status = '';
|
|
1249
|
+
this.claims = [];
|
|
1250
|
+
this.notices = [];
|
|
1251
|
+
this.downloadingCert = false;
|
|
1252
|
+
this.init = () => {
|
|
1253
|
+
this.warrantyId = this.route.snapshot.paramMap.get('id') ?? '';
|
|
1254
|
+
if (!this.warrantyId) {
|
|
1255
|
+
this.loading = false;
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
this.apiService
|
|
1259
|
+
.on('apps/commerce/warranty')
|
|
1260
|
+
.getOne(this.warrantyId)
|
|
1261
|
+
.pipe(takeUntil(this.$), catchError(() => of(undefined)))
|
|
1262
|
+
.subscribe(using((res) => {
|
|
1263
|
+
const w = res?.data;
|
|
1264
|
+
if (!w) {
|
|
1265
|
+
this.loading = false;
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
this.found = true;
|
|
1269
|
+
this.variantId = w.variantId;
|
|
1270
|
+
this.isVerified = !!w.isVerified;
|
|
1271
|
+
this.startsAt = w.startsAt;
|
|
1272
|
+
this.expiresAt = w.expiresAt;
|
|
1273
|
+
this.status = w.status;
|
|
1274
|
+
this.orderLineId = w.orderLineId;
|
|
1275
|
+
this.monthsLeft = w.expiresAt
|
|
1276
|
+
? Math.max(0, Math.round((Date.parse(w.expiresAt) - Date.now()) / (86_400_000 * 30.44)))
|
|
1277
|
+
: null;
|
|
1278
|
+
void this.hydrate(w);
|
|
1279
|
+
}));
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
async hydrate(w) {
|
|
1283
|
+
const [variant, claims, notices] = await Promise.all([
|
|
1284
|
+
firstValueFrom(this.apiService
|
|
1285
|
+
.on('apps/commerce/product-variant')
|
|
1286
|
+
.getOne(w.variantId)
|
|
1287
|
+
.pipe(catchError(() => of(undefined)))),
|
|
1288
|
+
firstValueFrom(this.apiService
|
|
1289
|
+
.on('apps/commerce/warranty-claim')
|
|
1290
|
+
.getMany({ warrantyId: w.id, perPage: 50 })
|
|
1291
|
+
.pipe(catchError(() => of({ data: [] })))),
|
|
1292
|
+
firstValueFrom(this.apiService
|
|
1293
|
+
.on('apps/commerce/safety-notice')
|
|
1294
|
+
.getMany({ variantId: w.variantId, status: 'published', perPage: 20 })
|
|
1295
|
+
.pipe(catchError(() => of({ data: [] })))),
|
|
1296
|
+
]);
|
|
1297
|
+
const v = variant;
|
|
1298
|
+
if (v?.data?.productId) {
|
|
1299
|
+
const p = await firstValueFrom(this.apiService
|
|
1300
|
+
.on('apps/commerce/product')
|
|
1301
|
+
.getOne(v.data.productId)
|
|
1302
|
+
.pipe(catchError(() => of(undefined))));
|
|
1303
|
+
this.productName = p?.data?.name ?? v?.data?.sku ?? this.productName;
|
|
1304
|
+
this.productSlug = p?.data?.slug;
|
|
1305
|
+
}
|
|
1306
|
+
this.claims = (claims?.data ?? []).map((c) => ({
|
|
1307
|
+
id: c.id,
|
|
1308
|
+
status: c.status,
|
|
1309
|
+
faultDescription: c.faultDescription,
|
|
1310
|
+
triageOutcome: c.triageOutcome,
|
|
1311
|
+
createdAt: c.createdAt,
|
|
1312
|
+
}));
|
|
1313
|
+
this.notices = (notices?.data ?? []).map((n) => ({
|
|
1314
|
+
id: n.id,
|
|
1315
|
+
severity: n.severity,
|
|
1316
|
+
title: n.title,
|
|
1317
|
+
message: n.message,
|
|
1318
|
+
actionRequired: n.actionRequired,
|
|
1319
|
+
}));
|
|
1320
|
+
this.loading = false;
|
|
1321
|
+
}
|
|
1322
|
+
// The doc endpoint needs the session token (global AuthMiddleware), so a
|
|
1323
|
+
// plain <a href> won't do — fetch the print HTML through ApiService and
|
|
1324
|
+
// hand it to a new window (RET-06 "certificates are downloadable").
|
|
1325
|
+
async downloadCertificate() {
|
|
1326
|
+
if (this.downloadingCert)
|
|
1327
|
+
return;
|
|
1328
|
+
this.downloadingCert = true;
|
|
1329
|
+
try {
|
|
1330
|
+
const res = await firstValueFrom(this.apiService.get(`apps/commerce/document/warranty/${this.warrantyId}/certificate`, { format: 'html' }));
|
|
1331
|
+
const html = typeof res === 'string' ? res : (res?.data ?? res?.html);
|
|
1332
|
+
const win = window.open('', '_blank');
|
|
1333
|
+
if (win && typeof html === 'string') {
|
|
1334
|
+
win.document.open();
|
|
1335
|
+
win.document.write(html);
|
|
1336
|
+
win.document.close();
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
finally {
|
|
1340
|
+
this.downloadingCert = false;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountSystemDetailComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1344
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: AccountSystemDetailComponent, isStandalone: false, selector: "x-account-system-detail", usesInheritance: true, ngImport: i0, template: "<a routerLink=\"../\" class=\"x-acct-btn-ghost\">\u2039 My Systems</a>\n\n@if (loading) {\n <div class=\"x-acct-empty\" style=\"margin-top: 16px\">Loading\u2026</div>\n} @else if (!found) {\n <div class=\"x-acct-card\" style=\"margin-top: 16px\">\n <div class=\"x-acct-empty\">We couldn't find that record.</div>\n </div>\n} @else {\n <header style=\"margin: 16px 0 8px\">\n <h2 class=\"x-acct-h2\">\n @if (productSlug) {\n <a [routerLink]=\"['/p', productSlug]\">{{ productName }}</a>\n } @else {\n {{ productName }}\n }\n </h2>\n <p class=\"x-acct-sub\">\n @if (isVerified) {\n <span class=\"x-acct-pill x-acct-pill-ok\">Verified</span>\n } @else {\n <span class=\"x-acct-pill\">Self-added</span>\n }\n @if (status === 'claimed') {\n <span class=\"x-acct-pill x-acct-pill-warn\" style=\"margin-left: 6px\">Claim in progress</span>\n }\n </p>\n </header>\n\n @for (n of notices; track n.id) {\n <div\n class=\"x-acct-card\"\n [class.x-acct-pill-bad]=\"n.severity === 'recall'\"\n style=\"border-color: var(--color-warn)\"\n >\n <strong>\u26A0 {{ n.title }}</strong>\n <p class=\"x-acct-sub\" style=\"margin: 6px 0 0\">{{ n.message }}</p>\n @if (n.actionRequired) {\n <p style=\"margin: 8px 0 0; font-weight: 700; font-size: 13px\">\n What to do: {{ n.actionRequired }}\n </p>\n }\n </div>\n }\n\n <div class=\"x-acct-card\">\n <div class=\"x-acct-row\"><span>Serial</span><span>{{ serial || '\u2014' }}</span></div>\n <div class=\"x-acct-row\"><span>Registered</span><span>{{ startsAt | date: 'mediumDate' }}</span></div>\n <div class=\"x-acct-row\">\n <span>Warranty until</span>\n <span>\n {{ expiresAt ? (expiresAt | date: 'mediumDate') : '\u2014' }}\n @if (monthsLeft !== null) {\n ({{ monthsLeft > 0 ? monthsLeft + ' months left' : 'expired' }})\n }\n </span>\n </div>\n </div>\n\n <div class=\"x-acct-card\" style=\"display: flex; gap: 10px; flex-wrap: wrap\">\n @if (isVerified) {\n <button\n type=\"button\"\n class=\"x-acct-btn-ghost\"\n [disabled]=\"downloadingCert\"\n (click)=\"downloadCertificate()\"\n >\n {{ downloadingCert ? 'Opening\u2026' : 'View / print certificate' }}\n </button>\n }\n <a class=\"x-acct-btn\" routerLink=\"claim\">Start a claim</a>\n </div>\n\n @if (claims.length) {\n <div class=\"x-acct-card\">\n <strong>Claims</strong>\n @for (c of claims; track c.id) {\n <div class=\"x-acct-row\">\n <span>\n {{ c.faultDescription || 'Claim' }}\n <span class=\"x-acct-sub\" style=\"display: block; margin-top: 2px\">\n {{ c.createdAt | date: 'mediumDate' }}\n @if (c.triageOutcome) {\n \u00B7 {{ c.triageOutcome }}\n }\n </span>\n </span>\n <span class=\"x-acct-pill\">{{ c.status }}</span>\n </div>\n }\n </div>\n }\n}\n", dependencies: [{ kind: "directive", type: i1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: i2.DatePipe, name: "date" }] }); }
|
|
1345
|
+
}
|
|
1346
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountSystemDetailComponent, decorators: [{
|
|
1347
|
+
type: Component,
|
|
1348
|
+
args: [{ standalone: false, selector: 'x-account-system-detail', template: "<a routerLink=\"../\" class=\"x-acct-btn-ghost\">\u2039 My Systems</a>\n\n@if (loading) {\n <div class=\"x-acct-empty\" style=\"margin-top: 16px\">Loading\u2026</div>\n} @else if (!found) {\n <div class=\"x-acct-card\" style=\"margin-top: 16px\">\n <div class=\"x-acct-empty\">We couldn't find that record.</div>\n </div>\n} @else {\n <header style=\"margin: 16px 0 8px\">\n <h2 class=\"x-acct-h2\">\n @if (productSlug) {\n <a [routerLink]=\"['/p', productSlug]\">{{ productName }}</a>\n } @else {\n {{ productName }}\n }\n </h2>\n <p class=\"x-acct-sub\">\n @if (isVerified) {\n <span class=\"x-acct-pill x-acct-pill-ok\">Verified</span>\n } @else {\n <span class=\"x-acct-pill\">Self-added</span>\n }\n @if (status === 'claimed') {\n <span class=\"x-acct-pill x-acct-pill-warn\" style=\"margin-left: 6px\">Claim in progress</span>\n }\n </p>\n </header>\n\n @for (n of notices; track n.id) {\n <div\n class=\"x-acct-card\"\n [class.x-acct-pill-bad]=\"n.severity === 'recall'\"\n style=\"border-color: var(--color-warn)\"\n >\n <strong>\u26A0 {{ n.title }}</strong>\n <p class=\"x-acct-sub\" style=\"margin: 6px 0 0\">{{ n.message }}</p>\n @if (n.actionRequired) {\n <p style=\"margin: 8px 0 0; font-weight: 700; font-size: 13px\">\n What to do: {{ n.actionRequired }}\n </p>\n }\n </div>\n }\n\n <div class=\"x-acct-card\">\n <div class=\"x-acct-row\"><span>Serial</span><span>{{ serial || '\u2014' }}</span></div>\n <div class=\"x-acct-row\"><span>Registered</span><span>{{ startsAt | date: 'mediumDate' }}</span></div>\n <div class=\"x-acct-row\">\n <span>Warranty until</span>\n <span>\n {{ expiresAt ? (expiresAt | date: 'mediumDate') : '\u2014' }}\n @if (monthsLeft !== null) {\n ({{ monthsLeft > 0 ? monthsLeft + ' months left' : 'expired' }})\n }\n </span>\n </div>\n </div>\n\n <div class=\"x-acct-card\" style=\"display: flex; gap: 10px; flex-wrap: wrap\">\n @if (isVerified) {\n <button\n type=\"button\"\n class=\"x-acct-btn-ghost\"\n [disabled]=\"downloadingCert\"\n (click)=\"downloadCertificate()\"\n >\n {{ downloadingCert ? 'Opening\u2026' : 'View / print certificate' }}\n </button>\n }\n <a class=\"x-acct-btn\" routerLink=\"claim\">Start a claim</a>\n </div>\n\n @if (claims.length) {\n <div class=\"x-acct-card\">\n <strong>Claims</strong>\n @for (c of claims; track c.id) {\n <div class=\"x-acct-row\">\n <span>\n {{ c.faultDescription || 'Claim' }}\n <span class=\"x-acct-sub\" style=\"display: block; margin-top: 2px\">\n {{ c.createdAt | date: 'mediumDate' }}\n @if (c.triageOutcome) {\n \u00B7 {{ c.triageOutcome }}\n }\n </span>\n </span>\n <span class=\"x-acct-pill\">{{ c.status }}</span>\n </div>\n }\n </div>\n }\n}\n" }]
|
|
1349
|
+
}] });
|
|
1350
|
+
|
|
1351
|
+
// P5.6, ACC-06 / RET-06 — "My Systems": every product the customer owns,
|
|
1352
|
+
// its serial, its warranty standing. Auto-populated by delivery (P4.15);
|
|
1353
|
+
// a unit bought elsewhere is added here as unverified.
|
|
1354
|
+
class AccountSystemsComponent extends BaseDirective {
|
|
1355
|
+
constructor() {
|
|
1356
|
+
super(...arguments);
|
|
1357
|
+
this.store = inject(AccountStoreService);
|
|
1358
|
+
this.loading = true;
|
|
1359
|
+
this.customerId = '';
|
|
1360
|
+
this.clientId = '';
|
|
1361
|
+
this.workspaceId = '';
|
|
1362
|
+
this.systems = [];
|
|
1363
|
+
this.adding = false;
|
|
1364
|
+
this.savingExternal = false;
|
|
1365
|
+
this.externalError = '';
|
|
1366
|
+
this.ext = { variantSku: '', serial: '', startsAt: '', months: 12 };
|
|
1367
|
+
this.init = () => {
|
|
1368
|
+
this.store
|
|
1369
|
+
.customer()
|
|
1370
|
+
.pipe(takeUntil(this.$))
|
|
1371
|
+
.subscribe(using((customer) => {
|
|
1372
|
+
if (!customer) {
|
|
1373
|
+
this.loading = false;
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
this.customerId = customer.id;
|
|
1377
|
+
void this.load();
|
|
1378
|
+
}));
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
async load() {
|
|
1382
|
+
const res = await firstValueFrom(this.apiService
|
|
1383
|
+
.on('apps/commerce/warranty')
|
|
1384
|
+
.getMany({ customerId: this.customerId, perPage: 200 })
|
|
1385
|
+
.pipe(catchError(() => of({ data: [] }))));
|
|
1386
|
+
const rows = res?.data ?? [];
|
|
1387
|
+
this.systems = await Promise.all(rows.map(async (w) => {
|
|
1388
|
+
const { name, slug } = await this.resolveProduct(w.variantId);
|
|
1389
|
+
const monthsLeft = w.expiresAt
|
|
1390
|
+
? Math.max(0, Math.round((Date.parse(w.expiresAt) - Date.now()) / (86_400_000 * 30.44)))
|
|
1391
|
+
: null;
|
|
1392
|
+
return {
|
|
1393
|
+
id: w.id,
|
|
1394
|
+
name,
|
|
1395
|
+
slug,
|
|
1396
|
+
serial: undefined,
|
|
1397
|
+
isVerified: !!w.isVerified,
|
|
1398
|
+
expiresAt: w.expiresAt,
|
|
1399
|
+
monthsLeft,
|
|
1400
|
+
status: w.status,
|
|
1401
|
+
};
|
|
1402
|
+
}));
|
|
1403
|
+
this.loading = false;
|
|
1404
|
+
}
|
|
1405
|
+
async resolveProduct(variantId) {
|
|
1406
|
+
try {
|
|
1407
|
+
const v = await firstValueFrom(this.apiService.on('apps/commerce/product-variant').getOne(variantId));
|
|
1408
|
+
const productId = v?.data?.productId;
|
|
1409
|
+
if (productId) {
|
|
1410
|
+
const p = await firstValueFrom(this.apiService.on('apps/commerce/product').getOne(productId));
|
|
1411
|
+
return { name: p?.data?.name ?? v?.data?.sku ?? 'Registered unit', slug: p?.data?.slug };
|
|
1412
|
+
}
|
|
1413
|
+
return { name: v?.data?.sku ?? 'Registered unit' };
|
|
1414
|
+
}
|
|
1415
|
+
catch {
|
|
1416
|
+
return { name: 'Registered unit' };
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
async addExternal() {
|
|
1420
|
+
if (this.savingExternal)
|
|
1421
|
+
return;
|
|
1422
|
+
const sku = this.ext.variantSku.trim();
|
|
1423
|
+
if (!sku) {
|
|
1424
|
+
this.externalError = 'Enter the product SKU or model shown on the label.';
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
this.savingExternal = true;
|
|
1428
|
+
this.externalError = '';
|
|
1429
|
+
try {
|
|
1430
|
+
// Resolve the SKU to a variant id (the label carries the SKU/model).
|
|
1431
|
+
const found = await firstValueFrom(this.apiService
|
|
1432
|
+
.on('apps/commerce/product-variant')
|
|
1433
|
+
.getMany({ sku, perPage: 1 })
|
|
1434
|
+
.pipe(catchError(() => of({ data: [] }))));
|
|
1435
|
+
const variantId = (found?.data ?? [])[0]?.id;
|
|
1436
|
+
if (!variantId) {
|
|
1437
|
+
this.externalError = "We couldn't match that SKU to a product we stock.";
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
const res = await firstValueFrom(this.apiService.post('apps/commerce/warranty/external', {
|
|
1441
|
+
customerId: this.customerId,
|
|
1442
|
+
variantId,
|
|
1443
|
+
serial: this.ext.serial || undefined,
|
|
1444
|
+
startsAt: this.ext.startsAt || undefined,
|
|
1445
|
+
months: this.ext.months || undefined,
|
|
1446
|
+
}));
|
|
1447
|
+
if (res?.status === 'error') {
|
|
1448
|
+
this.externalError = res?.message ?? 'Could not add that unit.';
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
this.adding = false;
|
|
1452
|
+
this.ext = { variantSku: '', serial: '', startsAt: '', months: 12 };
|
|
1453
|
+
notify('Added to My Systems', undefined, 'success');
|
|
1454
|
+
await this.load();
|
|
1455
|
+
}
|
|
1456
|
+
catch (err) {
|
|
1457
|
+
this.externalError = err?.error?.message ?? 'Could not add that unit.';
|
|
1458
|
+
}
|
|
1459
|
+
finally {
|
|
1460
|
+
this.savingExternal = false;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountSystemsComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1464
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: AccountSystemsComponent, isStandalone: false, selector: "x-account-systems", usesInheritance: true, ngImport: i0, template: "<h2 class=\"x-acct-h2\">My Systems</h2>\n<p class=\"x-acct-sub\">\n Every product you've bought from us, with its serial and warranty. Units bought\n elsewhere can be added for context.\n</p>\n\n@if (loading) {\n <div class=\"x-acct-empty\">Loading\u2026</div>\n} @else {\n @if (adding) {\n <div class=\"x-acct-card\">\n <strong>Add a unit bought elsewhere</strong>\n <p class=\"x-acct-sub\" style=\"margin: 6px 0 14px\">\n Unverified \u2014 it won't have a Tilmart certificate, but the compatibility\n checker and support can use it.\n </p>\n <div class=\"x-acct-field\">\n <label>Product SKU / model (on the label)</label>\n <input [(ngModel)]=\"ext.variantSku\" />\n </div>\n <div class=\"x-acct-field\">\n <label>Serial number (optional)</label>\n <input [(ngModel)]=\"ext.serial\" />\n </div>\n <div class=\"x-acct-field\">\n <label>Purchase date (optional)</label>\n <input type=\"date\" [(ngModel)]=\"ext.startsAt\" />\n </div>\n <div class=\"x-acct-field\" style=\"max-width: 160px\">\n <label>Warranty months</label>\n <input type=\"number\" [(ngModel)]=\"ext.months\" />\n </div>\n @if (externalError) {\n <div class=\"x-acct-err\">{{ externalError }}</div>\n }\n <button type=\"button\" class=\"x-acct-btn\" [disabled]=\"savingExternal\" (click)=\"addExternal()\">\n {{ savingExternal ? 'Adding\u2026' : 'Add unit' }}\n </button>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"adding = false\" style=\"margin-left: 8px\">\n Cancel\n </button>\n </div>\n } @else {\n <button type=\"button\" class=\"x-acct-btn\" (click)=\"adding = true\">Add external unit</button>\n }\n\n @for (sys of systems; track sys.id) {\n <a class=\"x-acct-card\" [routerLink]=\"[sys.id]\" style=\"display: block; text-decoration: none\">\n <div class=\"x-acct-row\" style=\"border-bottom: 0; padding-top: 0\">\n <strong style=\"color: var(--color-text)\">{{ sys.name }}</strong>\n @if (sys.isVerified) {\n <span class=\"x-acct-pill x-acct-pill-ok\">Verified</span>\n } @else {\n <span class=\"x-acct-pill\">Self-added</span>\n }\n </div>\n <p class=\"x-acct-sub\" style=\"margin: 0\">\n @if (sys.monthsLeft !== null) {\n @if (sys.monthsLeft > 0) {\n {{ sys.monthsLeft }} months of warranty left\n } @else {\n Warranty expired\n }\n } @else {\n Warranty length not set\n }\n @if (sys.status === 'claimed') {\n \u00B7 claim in progress\n }\n </p>\n </a>\n } @empty {\n @if (!adding) {\n <div class=\"x-acct-empty\">\n Nothing here yet \u2014 a warranty is registered automatically when a\n serialised item is delivered.\n </div>\n }\n }\n}\n", dependencies: [{ kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }] }); }
|
|
1465
|
+
}
|
|
1466
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountSystemsComponent, decorators: [{
|
|
1467
|
+
type: Component,
|
|
1468
|
+
args: [{ standalone: false, selector: 'x-account-systems', template: "<h2 class=\"x-acct-h2\">My Systems</h2>\n<p class=\"x-acct-sub\">\n Every product you've bought from us, with its serial and warranty. Units bought\n elsewhere can be added for context.\n</p>\n\n@if (loading) {\n <div class=\"x-acct-empty\">Loading\u2026</div>\n} @else {\n @if (adding) {\n <div class=\"x-acct-card\">\n <strong>Add a unit bought elsewhere</strong>\n <p class=\"x-acct-sub\" style=\"margin: 6px 0 14px\">\n Unverified \u2014 it won't have a Tilmart certificate, but the compatibility\n checker and support can use it.\n </p>\n <div class=\"x-acct-field\">\n <label>Product SKU / model (on the label)</label>\n <input [(ngModel)]=\"ext.variantSku\" />\n </div>\n <div class=\"x-acct-field\">\n <label>Serial number (optional)</label>\n <input [(ngModel)]=\"ext.serial\" />\n </div>\n <div class=\"x-acct-field\">\n <label>Purchase date (optional)</label>\n <input type=\"date\" [(ngModel)]=\"ext.startsAt\" />\n </div>\n <div class=\"x-acct-field\" style=\"max-width: 160px\">\n <label>Warranty months</label>\n <input type=\"number\" [(ngModel)]=\"ext.months\" />\n </div>\n @if (externalError) {\n <div class=\"x-acct-err\">{{ externalError }}</div>\n }\n <button type=\"button\" class=\"x-acct-btn\" [disabled]=\"savingExternal\" (click)=\"addExternal()\">\n {{ savingExternal ? 'Adding\u2026' : 'Add unit' }}\n </button>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"adding = false\" style=\"margin-left: 8px\">\n Cancel\n </button>\n </div>\n } @else {\n <button type=\"button\" class=\"x-acct-btn\" (click)=\"adding = true\">Add external unit</button>\n }\n\n @for (sys of systems; track sys.id) {\n <a class=\"x-acct-card\" [routerLink]=\"[sys.id]\" style=\"display: block; text-decoration: none\">\n <div class=\"x-acct-row\" style=\"border-bottom: 0; padding-top: 0\">\n <strong style=\"color: var(--color-text)\">{{ sys.name }}</strong>\n @if (sys.isVerified) {\n <span class=\"x-acct-pill x-acct-pill-ok\">Verified</span>\n } @else {\n <span class=\"x-acct-pill\">Self-added</span>\n }\n </div>\n <p class=\"x-acct-sub\" style=\"margin: 0\">\n @if (sys.monthsLeft !== null) {\n @if (sys.monthsLeft > 0) {\n {{ sys.monthsLeft }} months of warranty left\n } @else {\n Warranty expired\n }\n } @else {\n Warranty length not set\n }\n @if (sys.status === 'claimed') {\n \u00B7 claim in progress\n }\n </p>\n </a>\n } @empty {\n @if (!adding) {\n <div class=\"x-acct-empty\">\n Nothing here yet \u2014 a warranty is registered automatically when a\n serialised item is delivered.\n </div>\n }\n }\n}\n" }]
|
|
1469
|
+
}] });
|
|
1470
|
+
|
|
1471
|
+
const SYMPTOM_OPTIONS = [
|
|
1472
|
+
'Not turning on',
|
|
1473
|
+
'Shuts down under load',
|
|
1474
|
+
'Overheating',
|
|
1475
|
+
'Error code / warning light',
|
|
1476
|
+
'Unusual noise',
|
|
1477
|
+
'Not charging',
|
|
1478
|
+
'Reduced runtime / output',
|
|
1479
|
+
'Physical damage',
|
|
1480
|
+
];
|
|
1481
|
+
// P5.6, RET-07 — the warranty claim form. Fault description + a symptoms
|
|
1482
|
+
// checklist + whether a Tilmart installer fitted it. Photo/video upload
|
|
1483
|
+
// is a follow-up (needs the blobs pipeline) — noted, not blocking.
|
|
1484
|
+
class AccountWarrantyClaimComponent extends BaseDirective {
|
|
1485
|
+
constructor() {
|
|
1486
|
+
super(...arguments);
|
|
1487
|
+
this.route = inject(ActivatedRoute);
|
|
1488
|
+
this.store = inject(AccountStoreService);
|
|
1489
|
+
this.symptomOptions = SYMPTOM_OPTIONS;
|
|
1490
|
+
this.warrantyId = '';
|
|
1491
|
+
this.customerId = '';
|
|
1492
|
+
this.faultDescription = '';
|
|
1493
|
+
this.symptoms = {};
|
|
1494
|
+
this.installedByTilmart = false;
|
|
1495
|
+
this.submitting = false;
|
|
1496
|
+
this.error = '';
|
|
1497
|
+
this.done = false;
|
|
1498
|
+
this.init = () => {
|
|
1499
|
+
this.warrantyId = this.route.snapshot.paramMap.get('id') ?? '';
|
|
1500
|
+
this.store.customer().subscribe((c) => (this.customerId = c?.id ?? ''));
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
get canSubmit() {
|
|
1504
|
+
return !!this.faultDescription.trim();
|
|
1505
|
+
}
|
|
1506
|
+
async submit() {
|
|
1507
|
+
if (this.submitting || !this.canSubmit)
|
|
1508
|
+
return;
|
|
1509
|
+
this.submitting = true;
|
|
1510
|
+
this.error = '';
|
|
1511
|
+
try {
|
|
1512
|
+
const res = await firstValueFrom(this.apiService.post(`apps/commerce/warranty/${this.warrantyId}/claim`, {
|
|
1513
|
+
customerId: this.customerId || undefined,
|
|
1514
|
+
faultDescription: this.faultDescription.trim(),
|
|
1515
|
+
symptoms: Object.keys(this.symptoms).filter((k) => this.symptoms[k]),
|
|
1516
|
+
installedByTilmart: this.installedByTilmart,
|
|
1517
|
+
}));
|
|
1518
|
+
if (res?.status === 'error') {
|
|
1519
|
+
this.error = res?.message ?? 'Could not submit your claim.';
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
this.done = true;
|
|
1523
|
+
notify('Claim submitted', undefined, 'success');
|
|
1524
|
+
}
|
|
1525
|
+
catch (err) {
|
|
1526
|
+
this.error = err?.error?.message ?? 'Could not submit your claim.';
|
|
1527
|
+
}
|
|
1528
|
+
finally {
|
|
1529
|
+
this.submitting = false;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountWarrantyClaimComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1533
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: AccountWarrantyClaimComponent, isStandalone: false, selector: "x-account-warranty-claim", usesInheritance: true, ngImport: i0, template: "<a routerLink=\"../\" class=\"x-acct-btn-ghost\">\u2039 Back to system</a>\n\n@if (done) {\n <div class=\"x-acct-card\" style=\"margin-top: 16px\">\n <h2 class=\"x-acct-h2\">Claim submitted</h2>\n <p class=\"x-acct-sub\">\n We'll review the details and get back to you with next steps. You can see\n the status on the system's page.\n </p>\n <a class=\"x-acct-btn\" routerLink=\"../\">Back to system</a>\n </div>\n} @else {\n <h2 class=\"x-acct-h2\" style=\"margin-top: 16px\">Start a warranty claim</h2>\n <p class=\"x-acct-sub\">Tell us what's wrong and we'll take it from there.</p>\n\n <div class=\"x-acct-card\">\n <div class=\"x-acct-field\">\n <label>What's the problem?</label>\n <input\n [(ngModel)]=\"faultDescription\"\n placeholder=\"Describe the fault in your own words\"\n />\n </div>\n\n <div style=\"margin: 8px 0 16px\">\n <div style=\"font-weight: 700; font-size: 13px; margin-bottom: 8px\">Symptoms</div>\n @for (s of symptomOptions; track s) {\n <label class=\"x-acct-checkline\">\n <input type=\"checkbox\" [(ngModel)]=\"symptoms[s]\" />\n {{ s }}\n </label>\n }\n </div>\n\n <label class=\"x-acct-checkline\">\n <input type=\"checkbox\" [(ngModel)]=\"installedByTilmart\" />\n A Tilmart installer fitted this unit\n </label>\n\n @if (error) {\n <div class=\"x-acct-err\" style=\"margin-top: 12px\">{{ error }}</div>\n }\n <button\n type=\"button\"\n class=\"x-acct-btn\"\n style=\"margin-top: 14px\"\n [disabled]=\"!canSubmit || submitting\"\n (click)=\"submit()\"\n >\n {{ submitting ? 'Submitting\u2026' : 'Submit claim' }}\n </button>\n <p class=\"x-acct-sub\" style=\"margin-top: 12px\">\n Photos and video help us diagnose faster \u2014 you'll be able to add them once\n the claim is open.\n </p>\n </div>\n}\n", dependencies: [{ kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }] }); }
|
|
1534
|
+
}
|
|
1535
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountWarrantyClaimComponent, decorators: [{
|
|
1536
|
+
type: Component,
|
|
1537
|
+
args: [{ standalone: false, selector: 'x-account-warranty-claim', template: "<a routerLink=\"../\" class=\"x-acct-btn-ghost\">\u2039 Back to system</a>\n\n@if (done) {\n <div class=\"x-acct-card\" style=\"margin-top: 16px\">\n <h2 class=\"x-acct-h2\">Claim submitted</h2>\n <p class=\"x-acct-sub\">\n We'll review the details and get back to you with next steps. You can see\n the status on the system's page.\n </p>\n <a class=\"x-acct-btn\" routerLink=\"../\">Back to system</a>\n </div>\n} @else {\n <h2 class=\"x-acct-h2\" style=\"margin-top: 16px\">Start a warranty claim</h2>\n <p class=\"x-acct-sub\">Tell us what's wrong and we'll take it from there.</p>\n\n <div class=\"x-acct-card\">\n <div class=\"x-acct-field\">\n <label>What's the problem?</label>\n <input\n [(ngModel)]=\"faultDescription\"\n placeholder=\"Describe the fault in your own words\"\n />\n </div>\n\n <div style=\"margin: 8px 0 16px\">\n <div style=\"font-weight: 700; font-size: 13px; margin-bottom: 8px\">Symptoms</div>\n @for (s of symptomOptions; track s) {\n <label class=\"x-acct-checkline\">\n <input type=\"checkbox\" [(ngModel)]=\"symptoms[s]\" />\n {{ s }}\n </label>\n }\n </div>\n\n <label class=\"x-acct-checkline\">\n <input type=\"checkbox\" [(ngModel)]=\"installedByTilmart\" />\n A Tilmart installer fitted this unit\n </label>\n\n @if (error) {\n <div class=\"x-acct-err\" style=\"margin-top: 12px\">{{ error }}</div>\n }\n <button\n type=\"button\"\n class=\"x-acct-btn\"\n style=\"margin-top: 14px\"\n [disabled]=\"!canSubmit || submitting\"\n (click)=\"submit()\"\n >\n {{ submitting ? 'Submitting\u2026' : 'Submit claim' }}\n </button>\n <p class=\"x-acct-sub\" style=\"margin-top: 12px\">\n Photos and video help us diagnose faster \u2014 you'll be able to add them once\n the claim is open.\n </p>\n </div>\n}\n" }]
|
|
1538
|
+
}] });
|
|
1539
|
+
|
|
1540
|
+
// P5.3, CART-06 — saved lists. View / rename / delete a list, toggle
|
|
1541
|
+
// public sharing, remove an item. Adding items happens on the product
|
|
1542
|
+
// pages (a wishlist button there, P2 PDP work).
|
|
1543
|
+
class AccountWishlistsComponent extends BaseDirective {
|
|
1544
|
+
constructor() {
|
|
1545
|
+
super(...arguments);
|
|
1546
|
+
this.store = inject(AccountStoreService);
|
|
1547
|
+
this.loading = true;
|
|
1548
|
+
this.customerId = '';
|
|
1549
|
+
this.wishlists = [];
|
|
1550
|
+
this.init = () => {
|
|
1551
|
+
this.store
|
|
1552
|
+
.customer()
|
|
1553
|
+
.pipe(takeUntil(this.$))
|
|
1554
|
+
.subscribe(using((customer) => {
|
|
1555
|
+
if (!customer) {
|
|
1556
|
+
this.loading = false;
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
this.customerId = customer.id;
|
|
1560
|
+
void this.load();
|
|
1561
|
+
}));
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
async load() {
|
|
1565
|
+
const listsRes = await firstValueFrom(this.apiService
|
|
1566
|
+
.on('apps/commerce/wishlist')
|
|
1567
|
+
.getMany({ customerId: this.customerId, perPage: 50 })
|
|
1568
|
+
.pipe(catchError(() => of({ data: [] }))));
|
|
1569
|
+
const lists = listsRes?.data ?? [];
|
|
1570
|
+
const withItems = await Promise.all(lists.map(async (list) => {
|
|
1571
|
+
const itemsRes = await firstValueFrom(this.apiService
|
|
1572
|
+
.on('apps/commerce/wishlist-item')
|
|
1573
|
+
.getMany({ wishlistId: list.id, perPage: 200 })
|
|
1574
|
+
.pipe(catchError(() => of({ data: [] }))));
|
|
1575
|
+
const items = await Promise.all((itemsRes?.data ?? []).map((it) => this.resolveItem(it)));
|
|
1576
|
+
return {
|
|
1577
|
+
id: list.id,
|
|
1578
|
+
name: list.name,
|
|
1579
|
+
isPublic: !!list.isPublic,
|
|
1580
|
+
shareToken: list.shareToken,
|
|
1581
|
+
items,
|
|
1582
|
+
};
|
|
1583
|
+
}));
|
|
1584
|
+
this.wishlists = withItems;
|
|
1585
|
+
this.loading = false;
|
|
1586
|
+
}
|
|
1587
|
+
async resolveItem(it) {
|
|
1588
|
+
let name = 'Saved item';
|
|
1589
|
+
let slug;
|
|
1590
|
+
try {
|
|
1591
|
+
const v = await firstValueFrom(this.apiService.on('apps/commerce/product-variant').getOne(it.variantId));
|
|
1592
|
+
const productId = v?.data?.productId;
|
|
1593
|
+
if (productId) {
|
|
1594
|
+
const p = await firstValueFrom(this.apiService.on('apps/commerce/product').getOne(productId));
|
|
1595
|
+
name = p?.data?.name ?? v?.data?.sku ?? name;
|
|
1596
|
+
slug = p?.data?.slug;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
catch {
|
|
1600
|
+
// leave the fallback name
|
|
1601
|
+
}
|
|
1602
|
+
return { id: it.id, variantId: it.variantId, name, slug };
|
|
1603
|
+
}
|
|
1604
|
+
startRename(list) {
|
|
1605
|
+
list.renaming = true;
|
|
1606
|
+
list.draftName = list.name;
|
|
1607
|
+
}
|
|
1608
|
+
async saveName(list) {
|
|
1609
|
+
const name = (list.draftName ?? '').trim();
|
|
1610
|
+
if (!name)
|
|
1611
|
+
return;
|
|
1612
|
+
try {
|
|
1613
|
+
await firstValueFrom(this.apiService.on('apps/commerce/wishlist').update(list.id, { name }));
|
|
1614
|
+
list.name = name;
|
|
1615
|
+
list.renaming = false;
|
|
1616
|
+
}
|
|
1617
|
+
catch {
|
|
1618
|
+
notify('Could not rename that list.', undefined, 'error');
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
async togglePublic(list) {
|
|
1622
|
+
try {
|
|
1623
|
+
await firstValueFrom(this.apiService
|
|
1624
|
+
.on('apps/commerce/wishlist')
|
|
1625
|
+
.update(list.id, { isPublic: !list.isPublic }));
|
|
1626
|
+
await this.load();
|
|
1627
|
+
}
|
|
1628
|
+
catch {
|
|
1629
|
+
notify('Could not update sharing.', undefined, 'error');
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
async removeList(list) {
|
|
1633
|
+
if (!confirm(`Delete "${list.name}"?`))
|
|
1634
|
+
return;
|
|
1635
|
+
try {
|
|
1636
|
+
await firstValueFrom(this.apiService.on('apps/commerce/wishlist').remove(list.id));
|
|
1637
|
+
await this.load();
|
|
1638
|
+
}
|
|
1639
|
+
catch {
|
|
1640
|
+
notify('Could not delete that list.', undefined, 'error');
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
async removeItem(list, item) {
|
|
1644
|
+
try {
|
|
1645
|
+
await firstValueFrom(this.apiService.on('apps/commerce/wishlist-item').remove(item.id));
|
|
1646
|
+
list.items = list.items.filter((x) => x.id !== item.id);
|
|
1647
|
+
}
|
|
1648
|
+
catch {
|
|
1649
|
+
notify('Could not remove that item.', undefined, 'error');
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountWishlistsComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1653
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: AccountWishlistsComponent, isStandalone: false, selector: "x-account-wishlists", usesInheritance: true, ngImport: i0, template: "<h2 class=\"x-acct-h2\">Wishlists</h2>\n<p class=\"x-acct-sub\">Add items from any product page. Share a list with an installer or a colleague.</p>\n\n@if (loading) {\n <div class=\"x-acct-empty\">Loading\u2026</div>\n} @else if (!wishlists.length) {\n <div class=\"x-acct-card\">\n <div class=\"x-acct-empty\">\n No saved lists yet \u2014 tap the heart on a product to start one.\n </div>\n </div>\n} @else {\n @for (list of wishlists; track list.id) {\n <div class=\"x-acct-card\">\n <div class=\"x-acct-row\" style=\"border-bottom: 0; padding-top: 0\">\n @if (list.renaming) {\n <input class=\"x-acct-field\" style=\"margin: 0; max-width: 240px\" [(ngModel)]=\"list.draftName\" />\n <span>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"saveName(list)\">Save</button>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"list.renaming = false\" style=\"margin-left: 6px\">\n Cancel\n </button>\n </span>\n } @else {\n <strong>\n {{ list.name }}\n @if (list.isPublic) {\n <span class=\"x-acct-pill x-acct-pill-ok\" style=\"margin-left: 6px\">Shared</span>\n }\n </strong>\n <span>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"startRename(list)\">Rename</button>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"togglePublic(list)\" style=\"margin-left: 6px\">\n {{ list.isPublic ? 'Make private' : 'Share' }}\n </button>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"removeList(list)\" style=\"margin-left: 6px\">\n Delete\n </button>\n </span>\n }\n </div>\n\n @for (item of list.items; track item.id) {\n <div class=\"x-acct-row\">\n @if (item.slug) {\n <a [routerLink]=\"['/p', item.slug]\">{{ item.name }}</a>\n } @else {\n <span>{{ item.name }}</span>\n }\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"removeItem(list, item)\">Remove</button>\n </div>\n } @empty {\n <div class=\"x-acct-empty\">This list is empty.</div>\n }\n </div>\n }\n}\n", dependencies: [{ kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }] }); }
|
|
1654
|
+
}
|
|
1655
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountWishlistsComponent, decorators: [{
|
|
1656
|
+
type: Component,
|
|
1657
|
+
args: [{ standalone: false, selector: 'x-account-wishlists', template: "<h2 class=\"x-acct-h2\">Wishlists</h2>\n<p class=\"x-acct-sub\">Add items from any product page. Share a list with an installer or a colleague.</p>\n\n@if (loading) {\n <div class=\"x-acct-empty\">Loading\u2026</div>\n} @else if (!wishlists.length) {\n <div class=\"x-acct-card\">\n <div class=\"x-acct-empty\">\n No saved lists yet \u2014 tap the heart on a product to start one.\n </div>\n </div>\n} @else {\n @for (list of wishlists; track list.id) {\n <div class=\"x-acct-card\">\n <div class=\"x-acct-row\" style=\"border-bottom: 0; padding-top: 0\">\n @if (list.renaming) {\n <input class=\"x-acct-field\" style=\"margin: 0; max-width: 240px\" [(ngModel)]=\"list.draftName\" />\n <span>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"saveName(list)\">Save</button>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"list.renaming = false\" style=\"margin-left: 6px\">\n Cancel\n </button>\n </span>\n } @else {\n <strong>\n {{ list.name }}\n @if (list.isPublic) {\n <span class=\"x-acct-pill x-acct-pill-ok\" style=\"margin-left: 6px\">Shared</span>\n }\n </strong>\n <span>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"startRename(list)\">Rename</button>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"togglePublic(list)\" style=\"margin-left: 6px\">\n {{ list.isPublic ? 'Make private' : 'Share' }}\n </button>\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"removeList(list)\" style=\"margin-left: 6px\">\n Delete\n </button>\n </span>\n }\n </div>\n\n @for (item of list.items; track item.id) {\n <div class=\"x-acct-row\">\n @if (item.slug) {\n <a [routerLink]=\"['/p', item.slug]\">{{ item.name }}</a>\n } @else {\n <span>{{ item.name }}</span>\n }\n <button type=\"button\" class=\"x-acct-btn-ghost\" (click)=\"removeItem(list, item)\">Remove</button>\n </div>\n } @empty {\n <div class=\"x-acct-empty\">This list is empty.</div>\n }\n </div>\n }\n}\n" }]
|
|
1658
|
+
}] });
|
|
1659
|
+
|
|
1660
|
+
class AccountModule {
|
|
1661
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
1662
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: AccountModule, declarations: [AccountComponent,
|
|
1663
|
+
AccountDashboardComponent,
|
|
1664
|
+
AccountOrdersComponent,
|
|
1665
|
+
AccountOrderDetailComponent,
|
|
1666
|
+
AccountAddressesComponent,
|
|
1667
|
+
AccountPaymentsComponent,
|
|
1668
|
+
AccountWishlistsComponent,
|
|
1669
|
+
AccountSettingsComponent,
|
|
1670
|
+
AccountSystemsComponent,
|
|
1671
|
+
AccountSystemDetailComponent,
|
|
1672
|
+
AccountWarrantyClaimComponent,
|
|
1673
|
+
AccountReturnsComponent,
|
|
1674
|
+
AccountReturnDetailComponent,
|
|
1675
|
+
AccountReturnRequestComponent,
|
|
1676
|
+
AccountPrivacyComponent], imports: [SharedModule, i1.RouterModule] }); }
|
|
1677
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountModule, providers: [AccountStoreService], imports: [SharedModule,
|
|
1678
|
+
RouterModule.forChild([
|
|
1679
|
+
{
|
|
1680
|
+
path: '',
|
|
1681
|
+
component: AccountComponent,
|
|
1682
|
+
children: [
|
|
1683
|
+
{ path: '', pathMatch: 'full', redirectTo: 'dashboard' },
|
|
1684
|
+
{ path: 'dashboard', component: AccountDashboardComponent },
|
|
1685
|
+
{ path: 'orders', component: AccountOrdersComponent },
|
|
1686
|
+
{ path: 'orders/:id', component: AccountOrderDetailComponent },
|
|
1687
|
+
{ path: 'orders/:id/return', component: AccountReturnRequestComponent },
|
|
1688
|
+
{ path: 'returns', component: AccountReturnsComponent },
|
|
1689
|
+
{ path: 'returns/:id', component: AccountReturnDetailComponent },
|
|
1690
|
+
{ path: 'systems', component: AccountSystemsComponent },
|
|
1691
|
+
{ path: 'systems/:id', component: AccountSystemDetailComponent },
|
|
1692
|
+
{ path: 'systems/:id/claim', component: AccountWarrantyClaimComponent },
|
|
1693
|
+
{ path: 'addresses', component: AccountAddressesComponent },
|
|
1694
|
+
{ path: 'payments', component: AccountPaymentsComponent },
|
|
1695
|
+
{ path: 'wishlists', component: AccountWishlistsComponent },
|
|
1696
|
+
{ path: 'settings', component: AccountSettingsComponent },
|
|
1697
|
+
{ path: 'privacy', component: AccountPrivacyComponent },
|
|
1698
|
+
],
|
|
1699
|
+
},
|
|
1700
|
+
])] }); }
|
|
1701
|
+
}
|
|
1702
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: AccountModule, decorators: [{
|
|
1703
|
+
type: NgModule,
|
|
1704
|
+
args: [{
|
|
1705
|
+
declarations: [
|
|
1706
|
+
AccountComponent,
|
|
1707
|
+
AccountDashboardComponent,
|
|
1708
|
+
AccountOrdersComponent,
|
|
1709
|
+
AccountOrderDetailComponent,
|
|
1710
|
+
AccountAddressesComponent,
|
|
1711
|
+
AccountPaymentsComponent,
|
|
1712
|
+
AccountWishlistsComponent,
|
|
1713
|
+
AccountSettingsComponent,
|
|
1714
|
+
AccountSystemsComponent,
|
|
1715
|
+
AccountSystemDetailComponent,
|
|
1716
|
+
AccountWarrantyClaimComponent,
|
|
1717
|
+
AccountReturnsComponent,
|
|
1718
|
+
AccountReturnDetailComponent,
|
|
1719
|
+
AccountReturnRequestComponent,
|
|
1720
|
+
AccountPrivacyComponent,
|
|
1721
|
+
],
|
|
1722
|
+
providers: [AccountStoreService],
|
|
1723
|
+
imports: [
|
|
1724
|
+
SharedModule,
|
|
1725
|
+
RouterModule.forChild([
|
|
1726
|
+
{
|
|
1727
|
+
path: '',
|
|
1728
|
+
component: AccountComponent,
|
|
1729
|
+
children: [
|
|
1730
|
+
{ path: '', pathMatch: 'full', redirectTo: 'dashboard' },
|
|
1731
|
+
{ path: 'dashboard', component: AccountDashboardComponent },
|
|
1732
|
+
{ path: 'orders', component: AccountOrdersComponent },
|
|
1733
|
+
{ path: 'orders/:id', component: AccountOrderDetailComponent },
|
|
1734
|
+
{ path: 'orders/:id/return', component: AccountReturnRequestComponent },
|
|
1735
|
+
{ path: 'returns', component: AccountReturnsComponent },
|
|
1736
|
+
{ path: 'returns/:id', component: AccountReturnDetailComponent },
|
|
1737
|
+
{ path: 'systems', component: AccountSystemsComponent },
|
|
1738
|
+
{ path: 'systems/:id', component: AccountSystemDetailComponent },
|
|
1739
|
+
{ path: 'systems/:id/claim', component: AccountWarrantyClaimComponent },
|
|
1740
|
+
{ path: 'addresses', component: AccountAddressesComponent },
|
|
1741
|
+
{ path: 'payments', component: AccountPaymentsComponent },
|
|
1742
|
+
{ path: 'wishlists', component: AccountWishlistsComponent },
|
|
1743
|
+
{ path: 'settings', component: AccountSettingsComponent },
|
|
1744
|
+
{ path: 'privacy', component: AccountPrivacyComponent },
|
|
1745
|
+
],
|
|
1746
|
+
},
|
|
1747
|
+
]),
|
|
1748
|
+
],
|
|
1749
|
+
}]
|
|
1750
|
+
}] });
|
|
1751
|
+
|
|
1752
|
+
export { AccountModule };
|
|
1753
|
+
//# sourceMappingURL=isaiahiroko-tilmart-ngx-apps-buyer-account.module-DkScEdS0.mjs.map
|