@isaiahiroko/tilmart-ngx-apps-admin 0.2.2 → 0.2.4
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/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-bulk-upload.module-BGTbsYUP.mjs +370 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-bulk-upload.module-BGTbsYUP.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-catalog.module-BwjfFXDU.mjs +562 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-catalog.module-BwjfFXDU.mjs.map +1 -0
- package/fesm2022/{isaiahiroko-tilmart-ngx-apps-admin-orders.module-Cfeb0QjH.mjs → isaiahiroko-tilmart-ngx-apps-admin-orders.module-vldWwcAr.mjs} +26 -3
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-orders.module-vldWwcAr.mjs.map +1 -0
- package/fesm2022/{isaiahiroko-tilmart-ngx-apps-admin-settings.module-Cz6xPNwB.mjs → isaiahiroko-tilmart-ngx-apps-admin-settings.module-CFVgAQ2f.mjs} +48 -4
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-settings.module-CFVgAQ2f.mjs.map +1 -0
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin.mjs +14 -6
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin.mjs.map +1 -1
- package/package.json +1 -1
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-catalog.module-DoxDNaNn.mjs +0 -538
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-catalog.module-DoxDNaNn.mjs.map +0 -1
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-orders.module-Cfeb0QjH.mjs.map +0 -1
- package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-settings.module-Cz6xPNwB.mjs.map +0 -1
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, ChangeDetectorRef, Component, NgModule } from '@angular/core';
|
|
3
|
+
import * as i1$1 from '@angular/router';
|
|
4
|
+
import { RouterModule } from '@angular/router';
|
|
5
|
+
import { BaseDirective, notify, SharedModule } from '@thescaffold/ngx-core';
|
|
6
|
+
import { ElementsModule } from '@thescaffold/ngx-ui';
|
|
7
|
+
import { downloadBlob } from '@isaiahiroko/tilmart-ngx-ui';
|
|
8
|
+
import { takeUntil } from 'rxjs';
|
|
9
|
+
import * as i1 from '@angular/forms';
|
|
10
|
+
|
|
11
|
+
const MAX_FILE_BYTES = 15 * 1024 * 1024;
|
|
12
|
+
function toBase64(buffer) {
|
|
13
|
+
const bytes = new Uint8Array(buffer);
|
|
14
|
+
let binary = '';
|
|
15
|
+
const chunk = 0x8000;
|
|
16
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
17
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
18
|
+
}
|
|
19
|
+
return btoa(binary);
|
|
20
|
+
}
|
|
21
|
+
// Excel bulk product upload. Two steps on purpose: "Check file" runs the
|
|
22
|
+
// importer in dry-run mode (nothing is written) and lists every row's
|
|
23
|
+
// errors/warnings; "Import" then commits the same file. Re-uploading a file
|
|
24
|
+
// whose SKUs already exist updates those products rather than duplicating.
|
|
25
|
+
class BulkUploadComponent extends BaseDirective {
|
|
26
|
+
constructor() {
|
|
27
|
+
super(...arguments);
|
|
28
|
+
this.cd = inject(ChangeDetectorRef);
|
|
29
|
+
this.busy = false;
|
|
30
|
+
this.phase = null;
|
|
31
|
+
this.fileName = '';
|
|
32
|
+
this.content = '';
|
|
33
|
+
this.report = null;
|
|
34
|
+
this.onlyProblems = false;
|
|
35
|
+
// The file that produced a clean dry run — changing the file resets it.
|
|
36
|
+
this.checkedClean = false;
|
|
37
|
+
}
|
|
38
|
+
get canImport() {
|
|
39
|
+
return !!this.report?.dryRun && this.checkedClean && this.report.totals.created + this.report.totals.updated > 0;
|
|
40
|
+
}
|
|
41
|
+
get visibleResults() {
|
|
42
|
+
const rows = this.report?.results ?? [];
|
|
43
|
+
return this.onlyProblems ? rows.filter((r) => r.errors.length || r.warnings.length) : rows;
|
|
44
|
+
}
|
|
45
|
+
onFile(event) {
|
|
46
|
+
const file = event.target.files?.[0];
|
|
47
|
+
this.report = null;
|
|
48
|
+
this.checkedClean = false;
|
|
49
|
+
this.content = '';
|
|
50
|
+
this.fileName = '';
|
|
51
|
+
if (!file)
|
|
52
|
+
return;
|
|
53
|
+
if (file.size > MAX_FILE_BYTES) {
|
|
54
|
+
notify('That file is over 15 MB — split it into smaller files.', undefined, 'error');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
file
|
|
58
|
+
.arrayBuffer()
|
|
59
|
+
.then((buffer) => {
|
|
60
|
+
this.content = toBase64(buffer);
|
|
61
|
+
this.fileName = `${file.name} (${Math.max(1, Math.round(file.size / 1024))} KB)`;
|
|
62
|
+
this.cd.detectChanges();
|
|
63
|
+
})
|
|
64
|
+
.catch(() => notify('Could not read that file.', undefined, 'error'));
|
|
65
|
+
}
|
|
66
|
+
downloadTemplate() {
|
|
67
|
+
this.busy = true;
|
|
68
|
+
this.apiService.requestService
|
|
69
|
+
.raw('get', 'apps/commerce/catalog-import/template', {}, { responseType: 'blob' })
|
|
70
|
+
.pipe(takeUntil(this.$))
|
|
71
|
+
.subscribe({
|
|
72
|
+
next: (blob) => {
|
|
73
|
+
this.busy = false;
|
|
74
|
+
downloadBlob(blob, 'TILmart Product Upload Template.xlsx');
|
|
75
|
+
},
|
|
76
|
+
error: () => {
|
|
77
|
+
this.busy = false;
|
|
78
|
+
notify('Could not build the template.', undefined, 'error');
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
run(dryRun) {
|
|
83
|
+
if (!this.content)
|
|
84
|
+
return;
|
|
85
|
+
this.busy = true;
|
|
86
|
+
this.phase = dryRun ? 'check' : 'import';
|
|
87
|
+
this.apiService
|
|
88
|
+
.post('apps/commerce/catalog-import', { contentBase64: this.content, dryRun })
|
|
89
|
+
.pipe(takeUntil(this.$))
|
|
90
|
+
.subscribe({
|
|
91
|
+
next: (res) => {
|
|
92
|
+
this.busy = false;
|
|
93
|
+
this.phase = null;
|
|
94
|
+
this.report = res?.data ?? null;
|
|
95
|
+
this.checkedClean = dryRun && !!this.report;
|
|
96
|
+
if (this.report && !dryRun) {
|
|
97
|
+
notify(res?.message || 'Import finished.', undefined, this.report.totals.failed ? 'warning' : 'success');
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
error: (e) => {
|
|
101
|
+
this.busy = false;
|
|
102
|
+
this.phase = null;
|
|
103
|
+
notify(e.message || 'The import failed.', undefined, 'error');
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: BulkUploadComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
108
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: BulkUploadComponent, isStandalone: false, selector: "x-bulk-upload", usesInheritance: true, ngImport: i0, template: `
|
|
109
|
+
<div class="x-page-head">
|
|
110
|
+
<div class="x-page-title">Bulk upload</div>
|
|
111
|
+
<div class="x-page-subtitle">
|
|
112
|
+
Add or update many products from an Excel sheet — one row per product.
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="x-bu-card">
|
|
117
|
+
<ol class="x-bu-steps">
|
|
118
|
+
<li>
|
|
119
|
+
<button type="button" class="x-bu-btn x-bu-btn-ghost" [disabled]="busy" (click)="downloadTemplate()">
|
|
120
|
+
Download the template
|
|
121
|
+
</button>
|
|
122
|
+
<span class="x-bu-hint">
|
|
123
|
+
Includes the column guide and the live category / brand lists. Your own sheet works too if it has
|
|
124
|
+
<strong>Product name</strong>, <strong>Category</strong> and <strong>Price</strong> columns (a tab per brand is fine).
|
|
125
|
+
</span>
|
|
126
|
+
</li>
|
|
127
|
+
<li>
|
|
128
|
+
<label class="x-bu-file">
|
|
129
|
+
<input
|
|
130
|
+
type="file"
|
|
131
|
+
accept=".xlsx,.xlsm,.xls,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
132
|
+
[disabled]="busy"
|
|
133
|
+
(change)="onFile($event)"
|
|
134
|
+
data-testid="bulk-file"
|
|
135
|
+
/>
|
|
136
|
+
</label>
|
|
137
|
+
@if (fileName) {
|
|
138
|
+
<span class="x-bu-hint">{{ fileName }}</span>
|
|
139
|
+
}
|
|
140
|
+
</li>
|
|
141
|
+
<li>
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
class="x-bu-btn"
|
|
145
|
+
[disabled]="!content || busy"
|
|
146
|
+
(click)="run(true)"
|
|
147
|
+
data-testid="bulk-check"
|
|
148
|
+
>
|
|
149
|
+
{{ busy && phase === 'check' ? 'Checking…' : 'Check file' }}
|
|
150
|
+
</button>
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
class="x-bu-btn x-bu-btn-primary"
|
|
154
|
+
[disabled]="!canImport || busy"
|
|
155
|
+
(click)="run(false)"
|
|
156
|
+
data-testid="bulk-import"
|
|
157
|
+
>
|
|
158
|
+
{{ busy && phase === 'import' ? 'Importing…' : 'Import products' }}
|
|
159
|
+
</button>
|
|
160
|
+
@if (content && !canImport && report && report.dryRun) {
|
|
161
|
+
<span class="x-bu-hint">Fix the errors below (or upload a corrected file) to enable import.</span>
|
|
162
|
+
}
|
|
163
|
+
</li>
|
|
164
|
+
</ol>
|
|
165
|
+
<div class="x-bu-hint x-bu-images">
|
|
166
|
+
Product photos: JPG / PNG / WebP, at least <strong>1200 × 1200 px</strong>, square (or 4:3) on a plain
|
|
167
|
+
light background, under 8 MB. Put a public link in <em>Image Link</em> and extra views in
|
|
168
|
+
<em>Gallery Images</em> — the site resizes them for phones automatically.
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
@if (report) {
|
|
173
|
+
<div class="x-bu-summary" data-testid="bulk-summary">
|
|
174
|
+
<div class="x-bu-stat"><b>{{ report.totals.created }}</b> {{ report.dryRun ? 'to create' : 'created' }}</div>
|
|
175
|
+
<div class="x-bu-stat"><b>{{ report.totals.updated }}</b> {{ report.dryRun ? 'to update' : 'updated' }}</div>
|
|
176
|
+
<div class="x-bu-stat" [class.x-bu-bad]="report.totals.failed"><b>{{ report.totals.failed }}</b> with errors</div>
|
|
177
|
+
<div class="x-bu-stat"><b>{{ report.totals.skipped }}</b> skipped</div>
|
|
178
|
+
@if (!report.dryRun) {
|
|
179
|
+
<div class="x-bu-stat"><b>{{ report.totals.published }}</b> published</div>
|
|
180
|
+
<div class="x-bu-stat"><b>{{ report.totals.imagesUploaded }}</b> images</div>
|
|
181
|
+
}
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
@if (report.dryRun) {
|
|
185
|
+
<p class="x-bu-hint">This was a check only — nothing has been saved yet.</p>
|
|
186
|
+
} @else {
|
|
187
|
+
<p class="x-bu-hint">
|
|
188
|
+
Done. Products marked <em>draft</em> need the fixes listed below before they can go live —
|
|
189
|
+
see <a routerLink="/admin/catalog">Catalogue</a> and <a routerLink="/admin/data-quality">Data quality</a>.
|
|
190
|
+
</p>
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
<div class="x-bu-filter">
|
|
194
|
+
<label><input type="checkbox" [(ngModel)]="onlyProblems" /> Only rows with errors or warnings</label>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<table class="x-bu-table">
|
|
198
|
+
<thead>
|
|
199
|
+
<tr>
|
|
200
|
+
<th>Sheet</th><th>Row</th><th>Product</th><th>SKU</th><th>Result</th><th>Notes</th>
|
|
201
|
+
</tr>
|
|
202
|
+
</thead>
|
|
203
|
+
<tbody>
|
|
204
|
+
@for (r of visibleResults; track r.sheet + r.row) {
|
|
205
|
+
<tr>
|
|
206
|
+
<td>{{ r.sheet }}</td>
|
|
207
|
+
<td>{{ r.row }}</td>
|
|
208
|
+
<td>{{ r.name || '—' }}</td>
|
|
209
|
+
<td class="x-bu-mono">{{ r.sku || '—' }}</td>
|
|
210
|
+
<td>
|
|
211
|
+
<span class="x-bu-pill" [attr.data-action]="r.action">{{ r.action }}</span>
|
|
212
|
+
@if (r.status) { <span class="x-bu-status">{{ r.status }}</span> }
|
|
213
|
+
</td>
|
|
214
|
+
<td>
|
|
215
|
+
@for (e of r.errors; track e) { <div class="x-bu-err">{{ e }}</div> }
|
|
216
|
+
@for (w of r.warnings; track w) { <div class="x-bu-warn">{{ w }}</div> }
|
|
217
|
+
</td>
|
|
218
|
+
</tr>
|
|
219
|
+
} @empty {
|
|
220
|
+
<tr><td colspan="6" class="x-bu-hint">Nothing to show.</td></tr>
|
|
221
|
+
}
|
|
222
|
+
</tbody>
|
|
223
|
+
</table>
|
|
224
|
+
}
|
|
225
|
+
`, isInline: true, styles: [".x-page-head{margin-bottom:20px}.x-page-title{font-size:24px;font-weight:800;color:var(--color-text)}.x-page-subtitle{font-size:13.5px;color:var(--color-text-muted);margin-top:2px}.x-bu-card{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:18px 20px;margin-bottom:20px}.x-bu-steps{margin:0 0 12px;padding-left:20px;display:grid;gap:14px}.x-bu-steps li{line-height:1.6}.x-bu-hint{font-size:12.5px;color:var(--color-text-muted);margin-left:8px}.x-bu-images{margin:0;padding-top:12px;border-top:1px solid var(--color-border)}.x-bu-btn{font:inherit;font-weight:600;font-size:13px;padding:7px 14px;border-radius:var(--radius-sm);border:1px solid var(--color-border-strong, var(--color-border));background:var(--color-surface-2);color:var(--color-text);cursor:pointer;margin-right:8px}.x-bu-btn:disabled{opacity:.5;cursor:not-allowed}.x-bu-btn-primary{background:var(--color-brand);color:var(--color-brand-fg);border-color:var(--color-brand)}.x-bu-btn-ghost{background:transparent}.x-bu-summary{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:8px}.x-bu-stat{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);padding:10px 16px;font-size:13px;color:var(--color-text-muted)}.x-bu-stat b{display:block;font-size:22px;color:var(--color-text);font-family:var(--font-mono)}.x-bu-bad{border-color:var(--color-danger, #c0392b)}.x-bu-filter{margin:12px 0;font-size:13px}.x-bu-table{width:100%;border-collapse:collapse;font-size:13px}.x-bu-table th,.x-bu-table td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--color-border);vertical-align:top}.x-bu-mono{font-family:var(--font-mono);font-size:12px}.x-bu-pill{display:inline-block;padding:1px 8px;border-radius:999px;font-size:11.5px;font-weight:700;background:var(--color-surface-2);border:1px solid var(--color-border)}.x-bu-pill[data-action=failed]{border-color:var(--color-danger, #c0392b);color:var(--color-danger, #c0392b)}.x-bu-pill[data-action=created],.x-bu-pill[data-action=updated]{border-color:var(--color-success);color:var(--color-success)}.x-bu-status{margin-left:6px;font-size:11.5px;color:var(--color-text-muted)}.x-bu-err{color:var(--color-danger, #c0392b)}.x-bu-warn{color:var(--color-text-muted)}\n"], dependencies: [{ kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }] }); }
|
|
226
|
+
}
|
|
227
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: BulkUploadComponent, decorators: [{
|
|
228
|
+
type: Component,
|
|
229
|
+
args: [{ standalone: false, selector: 'x-bulk-upload', template: `
|
|
230
|
+
<div class="x-page-head">
|
|
231
|
+
<div class="x-page-title">Bulk upload</div>
|
|
232
|
+
<div class="x-page-subtitle">
|
|
233
|
+
Add or update many products from an Excel sheet — one row per product.
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div class="x-bu-card">
|
|
238
|
+
<ol class="x-bu-steps">
|
|
239
|
+
<li>
|
|
240
|
+
<button type="button" class="x-bu-btn x-bu-btn-ghost" [disabled]="busy" (click)="downloadTemplate()">
|
|
241
|
+
Download the template
|
|
242
|
+
</button>
|
|
243
|
+
<span class="x-bu-hint">
|
|
244
|
+
Includes the column guide and the live category / brand lists. Your own sheet works too if it has
|
|
245
|
+
<strong>Product name</strong>, <strong>Category</strong> and <strong>Price</strong> columns (a tab per brand is fine).
|
|
246
|
+
</span>
|
|
247
|
+
</li>
|
|
248
|
+
<li>
|
|
249
|
+
<label class="x-bu-file">
|
|
250
|
+
<input
|
|
251
|
+
type="file"
|
|
252
|
+
accept=".xlsx,.xlsm,.xls,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
253
|
+
[disabled]="busy"
|
|
254
|
+
(change)="onFile($event)"
|
|
255
|
+
data-testid="bulk-file"
|
|
256
|
+
/>
|
|
257
|
+
</label>
|
|
258
|
+
@if (fileName) {
|
|
259
|
+
<span class="x-bu-hint">{{ fileName }}</span>
|
|
260
|
+
}
|
|
261
|
+
</li>
|
|
262
|
+
<li>
|
|
263
|
+
<button
|
|
264
|
+
type="button"
|
|
265
|
+
class="x-bu-btn"
|
|
266
|
+
[disabled]="!content || busy"
|
|
267
|
+
(click)="run(true)"
|
|
268
|
+
data-testid="bulk-check"
|
|
269
|
+
>
|
|
270
|
+
{{ busy && phase === 'check' ? 'Checking…' : 'Check file' }}
|
|
271
|
+
</button>
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
|
+
class="x-bu-btn x-bu-btn-primary"
|
|
275
|
+
[disabled]="!canImport || busy"
|
|
276
|
+
(click)="run(false)"
|
|
277
|
+
data-testid="bulk-import"
|
|
278
|
+
>
|
|
279
|
+
{{ busy && phase === 'import' ? 'Importing…' : 'Import products' }}
|
|
280
|
+
</button>
|
|
281
|
+
@if (content && !canImport && report && report.dryRun) {
|
|
282
|
+
<span class="x-bu-hint">Fix the errors below (or upload a corrected file) to enable import.</span>
|
|
283
|
+
}
|
|
284
|
+
</li>
|
|
285
|
+
</ol>
|
|
286
|
+
<div class="x-bu-hint x-bu-images">
|
|
287
|
+
Product photos: JPG / PNG / WebP, at least <strong>1200 × 1200 px</strong>, square (or 4:3) on a plain
|
|
288
|
+
light background, under 8 MB. Put a public link in <em>Image Link</em> and extra views in
|
|
289
|
+
<em>Gallery Images</em> — the site resizes them for phones automatically.
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
@if (report) {
|
|
294
|
+
<div class="x-bu-summary" data-testid="bulk-summary">
|
|
295
|
+
<div class="x-bu-stat"><b>{{ report.totals.created }}</b> {{ report.dryRun ? 'to create' : 'created' }}</div>
|
|
296
|
+
<div class="x-bu-stat"><b>{{ report.totals.updated }}</b> {{ report.dryRun ? 'to update' : 'updated' }}</div>
|
|
297
|
+
<div class="x-bu-stat" [class.x-bu-bad]="report.totals.failed"><b>{{ report.totals.failed }}</b> with errors</div>
|
|
298
|
+
<div class="x-bu-stat"><b>{{ report.totals.skipped }}</b> skipped</div>
|
|
299
|
+
@if (!report.dryRun) {
|
|
300
|
+
<div class="x-bu-stat"><b>{{ report.totals.published }}</b> published</div>
|
|
301
|
+
<div class="x-bu-stat"><b>{{ report.totals.imagesUploaded }}</b> images</div>
|
|
302
|
+
}
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
@if (report.dryRun) {
|
|
306
|
+
<p class="x-bu-hint">This was a check only — nothing has been saved yet.</p>
|
|
307
|
+
} @else {
|
|
308
|
+
<p class="x-bu-hint">
|
|
309
|
+
Done. Products marked <em>draft</em> need the fixes listed below before they can go live —
|
|
310
|
+
see <a routerLink="/admin/catalog">Catalogue</a> and <a routerLink="/admin/data-quality">Data quality</a>.
|
|
311
|
+
</p>
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
<div class="x-bu-filter">
|
|
315
|
+
<label><input type="checkbox" [(ngModel)]="onlyProblems" /> Only rows with errors or warnings</label>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<table class="x-bu-table">
|
|
319
|
+
<thead>
|
|
320
|
+
<tr>
|
|
321
|
+
<th>Sheet</th><th>Row</th><th>Product</th><th>SKU</th><th>Result</th><th>Notes</th>
|
|
322
|
+
</tr>
|
|
323
|
+
</thead>
|
|
324
|
+
<tbody>
|
|
325
|
+
@for (r of visibleResults; track r.sheet + r.row) {
|
|
326
|
+
<tr>
|
|
327
|
+
<td>{{ r.sheet }}</td>
|
|
328
|
+
<td>{{ r.row }}</td>
|
|
329
|
+
<td>{{ r.name || '—' }}</td>
|
|
330
|
+
<td class="x-bu-mono">{{ r.sku || '—' }}</td>
|
|
331
|
+
<td>
|
|
332
|
+
<span class="x-bu-pill" [attr.data-action]="r.action">{{ r.action }}</span>
|
|
333
|
+
@if (r.status) { <span class="x-bu-status">{{ r.status }}</span> }
|
|
334
|
+
</td>
|
|
335
|
+
<td>
|
|
336
|
+
@for (e of r.errors; track e) { <div class="x-bu-err">{{ e }}</div> }
|
|
337
|
+
@for (w of r.warnings; track w) { <div class="x-bu-warn">{{ w }}</div> }
|
|
338
|
+
</td>
|
|
339
|
+
</tr>
|
|
340
|
+
} @empty {
|
|
341
|
+
<tr><td colspan="6" class="x-bu-hint">Nothing to show.</td></tr>
|
|
342
|
+
}
|
|
343
|
+
</tbody>
|
|
344
|
+
</table>
|
|
345
|
+
}
|
|
346
|
+
`, styles: [".x-page-head{margin-bottom:20px}.x-page-title{font-size:24px;font-weight:800;color:var(--color-text)}.x-page-subtitle{font-size:13.5px;color:var(--color-text-muted);margin-top:2px}.x-bu-card{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:18px 20px;margin-bottom:20px}.x-bu-steps{margin:0 0 12px;padding-left:20px;display:grid;gap:14px}.x-bu-steps li{line-height:1.6}.x-bu-hint{font-size:12.5px;color:var(--color-text-muted);margin-left:8px}.x-bu-images{margin:0;padding-top:12px;border-top:1px solid var(--color-border)}.x-bu-btn{font:inherit;font-weight:600;font-size:13px;padding:7px 14px;border-radius:var(--radius-sm);border:1px solid var(--color-border-strong, var(--color-border));background:var(--color-surface-2);color:var(--color-text);cursor:pointer;margin-right:8px}.x-bu-btn:disabled{opacity:.5;cursor:not-allowed}.x-bu-btn-primary{background:var(--color-brand);color:var(--color-brand-fg);border-color:var(--color-brand)}.x-bu-btn-ghost{background:transparent}.x-bu-summary{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:8px}.x-bu-stat{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);padding:10px 16px;font-size:13px;color:var(--color-text-muted)}.x-bu-stat b{display:block;font-size:22px;color:var(--color-text);font-family:var(--font-mono)}.x-bu-bad{border-color:var(--color-danger, #c0392b)}.x-bu-filter{margin:12px 0;font-size:13px}.x-bu-table{width:100%;border-collapse:collapse;font-size:13px}.x-bu-table th,.x-bu-table td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--color-border);vertical-align:top}.x-bu-mono{font-family:var(--font-mono);font-size:12px}.x-bu-pill{display:inline-block;padding:1px 8px;border-radius:999px;font-size:11.5px;font-weight:700;background:var(--color-surface-2);border:1px solid var(--color-border)}.x-bu-pill[data-action=failed]{border-color:var(--color-danger, #c0392b);color:var(--color-danger, #c0392b)}.x-bu-pill[data-action=created],.x-bu-pill[data-action=updated]{border-color:var(--color-success);color:var(--color-success)}.x-bu-status{margin-left:6px;font-size:11.5px;color:var(--color-text-muted)}.x-bu-err{color:var(--color-danger, #c0392b)}.x-bu-warn{color:var(--color-text-muted)}\n"] }]
|
|
347
|
+
}] });
|
|
348
|
+
|
|
349
|
+
class BulkUploadModule {
|
|
350
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: BulkUploadModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
351
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: BulkUploadModule, declarations: [BulkUploadComponent], imports: [SharedModule,
|
|
352
|
+
ElementsModule, i1$1.RouterModule] }); }
|
|
353
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: BulkUploadModule, imports: [SharedModule,
|
|
354
|
+
ElementsModule,
|
|
355
|
+
RouterModule.forChild([{ path: '', component: BulkUploadComponent }])] }); }
|
|
356
|
+
}
|
|
357
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: BulkUploadModule, decorators: [{
|
|
358
|
+
type: NgModule,
|
|
359
|
+
args: [{
|
|
360
|
+
declarations: [BulkUploadComponent],
|
|
361
|
+
imports: [
|
|
362
|
+
SharedModule,
|
|
363
|
+
ElementsModule,
|
|
364
|
+
RouterModule.forChild([{ path: '', component: BulkUploadComponent }]),
|
|
365
|
+
],
|
|
366
|
+
}]
|
|
367
|
+
}] });
|
|
368
|
+
|
|
369
|
+
export { BulkUploadModule };
|
|
370
|
+
//# sourceMappingURL=isaiahiroko-tilmart-ngx-apps-admin-bulk-upload.module-BGTbsYUP.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isaiahiroko-tilmart-ngx-apps-admin-bulk-upload.module-BGTbsYUP.mjs","sources":["../../../projects/admin/src/lib/pages/bulk-upload/bulk-upload.component.ts","../../../projects/admin/src/lib/pages/bulk-upload/bulk-upload.module.ts"],"sourcesContent":["import { ChangeDetectorRef, Component, inject } from '@angular/core';\nimport { BaseDirective, notify } from '@thescaffold/ngx-core';\nimport { downloadBlob } from '@isaiahiroko/tilmart-ngx-ui';\nimport { takeUntil } from 'rxjs';\n\ninterface ImportRowResult {\n sheet: string;\n row: number;\n name?: string;\n sku?: string;\n action: 'created' | 'updated' | 'skipped' | 'failed';\n status?: string;\n errors: string[];\n warnings: string[];\n}\n\ninterface ImportReport {\n dryRun: boolean;\n sheets: { name: string; rows: number; skipped?: string }[];\n totals: {\n created: number;\n updated: number;\n skipped: number;\n failed: number;\n published: number;\n imagesUploaded: number;\n };\n results: ImportRowResult[];\n}\n\nconst MAX_FILE_BYTES = 15 * 1024 * 1024;\n\nfunction toBase64(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let binary = '';\n const chunk = 0x8000;\n for (let i = 0; i < bytes.length; i += chunk) {\n binary += String.fromCharCode(...bytes.subarray(i, i + chunk));\n }\n return btoa(binary);\n}\n\n// Excel bulk product upload. Two steps on purpose: \"Check file\" runs the\n// importer in dry-run mode (nothing is written) and lists every row's\n// errors/warnings; \"Import\" then commits the same file. Re-uploading a file\n// whose SKUs already exist updates those products rather than duplicating.\n@Component({\n standalone: false,\n selector: 'x-bulk-upload',\n template: `\n <div class=\"x-page-head\">\n <div class=\"x-page-title\">Bulk upload</div>\n <div class=\"x-page-subtitle\">\n Add or update many products from an Excel sheet — one row per product.\n </div>\n </div>\n\n <div class=\"x-bu-card\">\n <ol class=\"x-bu-steps\">\n <li>\n <button type=\"button\" class=\"x-bu-btn x-bu-btn-ghost\" [disabled]=\"busy\" (click)=\"downloadTemplate()\">\n Download the template\n </button>\n <span class=\"x-bu-hint\">\n Includes the column guide and the live category / brand lists. Your own sheet works too if it has\n <strong>Product name</strong>, <strong>Category</strong> and <strong>Price</strong> columns (a tab per brand is fine).\n </span>\n </li>\n <li>\n <label class=\"x-bu-file\">\n <input\n type=\"file\"\n accept=\".xlsx,.xlsm,.xls,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n [disabled]=\"busy\"\n (change)=\"onFile($event)\"\n data-testid=\"bulk-file\"\n />\n </label>\n @if (fileName) {\n <span class=\"x-bu-hint\">{{ fileName }}</span>\n }\n </li>\n <li>\n <button\n type=\"button\"\n class=\"x-bu-btn\"\n [disabled]=\"!content || busy\"\n (click)=\"run(true)\"\n data-testid=\"bulk-check\"\n >\n {{ busy && phase === 'check' ? 'Checking…' : 'Check file' }}\n </button>\n <button\n type=\"button\"\n class=\"x-bu-btn x-bu-btn-primary\"\n [disabled]=\"!canImport || busy\"\n (click)=\"run(false)\"\n data-testid=\"bulk-import\"\n >\n {{ busy && phase === 'import' ? 'Importing…' : 'Import products' }}\n </button>\n @if (content && !canImport && report && report.dryRun) {\n <span class=\"x-bu-hint\">Fix the errors below (or upload a corrected file) to enable import.</span>\n }\n </li>\n </ol>\n <div class=\"x-bu-hint x-bu-images\">\n Product photos: JPG / PNG / WebP, at least <strong>1200 × 1200 px</strong>, square (or 4:3) on a plain\n light background, under 8 MB. Put a public link in <em>Image Link</em> and extra views in\n <em>Gallery Images</em> — the site resizes them for phones automatically.\n </div>\n </div>\n\n @if (report) {\n <div class=\"x-bu-summary\" data-testid=\"bulk-summary\">\n <div class=\"x-bu-stat\"><b>{{ report.totals.created }}</b> {{ report.dryRun ? 'to create' : 'created' }}</div>\n <div class=\"x-bu-stat\"><b>{{ report.totals.updated }}</b> {{ report.dryRun ? 'to update' : 'updated' }}</div>\n <div class=\"x-bu-stat\" [class.x-bu-bad]=\"report.totals.failed\"><b>{{ report.totals.failed }}</b> with errors</div>\n <div class=\"x-bu-stat\"><b>{{ report.totals.skipped }}</b> skipped</div>\n @if (!report.dryRun) {\n <div class=\"x-bu-stat\"><b>{{ report.totals.published }}</b> published</div>\n <div class=\"x-bu-stat\"><b>{{ report.totals.imagesUploaded }}</b> images</div>\n }\n </div>\n\n @if (report.dryRun) {\n <p class=\"x-bu-hint\">This was a check only — nothing has been saved yet.</p>\n } @else {\n <p class=\"x-bu-hint\">\n Done. Products marked <em>draft</em> need the fixes listed below before they can go live —\n see <a routerLink=\"/admin/catalog\">Catalogue</a> and <a routerLink=\"/admin/data-quality\">Data quality</a>.\n </p>\n }\n\n <div class=\"x-bu-filter\">\n <label><input type=\"checkbox\" [(ngModel)]=\"onlyProblems\" /> Only rows with errors or warnings</label>\n </div>\n\n <table class=\"x-bu-table\">\n <thead>\n <tr>\n <th>Sheet</th><th>Row</th><th>Product</th><th>SKU</th><th>Result</th><th>Notes</th>\n </tr>\n </thead>\n <tbody>\n @for (r of visibleResults; track r.sheet + r.row) {\n <tr>\n <td>{{ r.sheet }}</td>\n <td>{{ r.row }}</td>\n <td>{{ r.name || '—' }}</td>\n <td class=\"x-bu-mono\">{{ r.sku || '—' }}</td>\n <td>\n <span class=\"x-bu-pill\" [attr.data-action]=\"r.action\">{{ r.action }}</span>\n @if (r.status) { <span class=\"x-bu-status\">{{ r.status }}</span> }\n </td>\n <td>\n @for (e of r.errors; track e) { <div class=\"x-bu-err\">{{ e }}</div> }\n @for (w of r.warnings; track w) { <div class=\"x-bu-warn\">{{ w }}</div> }\n </td>\n </tr>\n } @empty {\n <tr><td colspan=\"6\" class=\"x-bu-hint\">Nothing to show.</td></tr>\n }\n </tbody>\n </table>\n }\n `,\n styles: [\n `\n .x-page-head { margin-bottom: 20px; }\n .x-page-title { font-size: 24px; font-weight: 800; color: var(--color-text); }\n .x-page-subtitle { font-size: 13.5px; color: var(--color-text-muted); margin-top: 2px; }\n .x-bu-card {\n background: var(--color-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-lg);\n padding: 18px 20px;\n margin-bottom: 20px;\n }\n .x-bu-steps { margin: 0 0 12px; padding-left: 20px; display: grid; gap: 14px; }\n .x-bu-steps li { line-height: 1.6; }\n .x-bu-hint { font-size: 12.5px; color: var(--color-text-muted); margin-left: 8px; }\n .x-bu-images { margin: 0; padding-top: 12px; border-top: 1px solid var(--color-border); }\n .x-bu-btn {\n font: inherit; font-weight: 600; font-size: 13px;\n padding: 7px 14px; border-radius: var(--radius-sm);\n border: 1px solid var(--color-border-strong, var(--color-border));\n background: var(--color-surface-2); color: var(--color-text); cursor: pointer;\n margin-right: 8px;\n }\n .x-bu-btn:disabled { opacity: 0.5; cursor: not-allowed; }\n .x-bu-btn-primary { background: var(--color-brand); color: var(--color-brand-fg); border-color: var(--color-brand); }\n .x-bu-btn-ghost { background: transparent; }\n .x-bu-summary { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 8px; }\n .x-bu-stat {\n background: var(--color-surface); border: 1px solid var(--color-border);\n border-radius: var(--radius-md); padding: 10px 16px; font-size: 13px; color: var(--color-text-muted);\n }\n .x-bu-stat b { display: block; font-size: 22px; color: var(--color-text); font-family: var(--font-mono); }\n .x-bu-bad { border-color: var(--color-danger, #c0392b); }\n .x-bu-filter { margin: 12px 0; font-size: 13px; }\n .x-bu-table { width: 100%; border-collapse: collapse; font-size: 13px; }\n .x-bu-table th, .x-bu-table td {\n text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--color-border); vertical-align: top;\n }\n .x-bu-mono { font-family: var(--font-mono); font-size: 12px; }\n .x-bu-pill {\n display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: 11.5px; font-weight: 700;\n background: var(--color-surface-2); border: 1px solid var(--color-border);\n }\n .x-bu-pill[data-action='failed'] { border-color: var(--color-danger, #c0392b); color: var(--color-danger, #c0392b); }\n .x-bu-pill[data-action='created'], .x-bu-pill[data-action='updated'] { border-color: var(--color-success); color: var(--color-success); }\n .x-bu-status { margin-left: 6px; font-size: 11.5px; color: var(--color-text-muted); }\n .x-bu-err { color: var(--color-danger, #c0392b); }\n .x-bu-warn { color: var(--color-text-muted); }\n `,\n ],\n})\nexport class BulkUploadComponent extends BaseDirective {\n private readonly cd = inject(ChangeDetectorRef);\n busy = false;\n phase: 'check' | 'import' | null = null;\n fileName = '';\n content = '';\n report: ImportReport | null = null;\n onlyProblems = false;\n // The file that produced a clean dry run — changing the file resets it.\n private checkedClean = false;\n\n get canImport(): boolean {\n return !!this.report?.dryRun && this.checkedClean && this.report.totals.created + this.report.totals.updated > 0;\n }\n\n get visibleResults(): ImportRowResult[] {\n const rows = this.report?.results ?? [];\n return this.onlyProblems ? rows.filter((r) => r.errors.length || r.warnings.length) : rows;\n }\n\n onFile(event: Event): void {\n const file = (event.target as HTMLInputElement).files?.[0];\n this.report = null;\n this.checkedClean = false;\n this.content = '';\n this.fileName = '';\n if (!file) return;\n if (file.size > MAX_FILE_BYTES) {\n notify('That file is over 15 MB — split it into smaller files.', undefined, 'error');\n return;\n }\n file\n .arrayBuffer()\n .then((buffer) => {\n this.content = toBase64(buffer);\n this.fileName = `${file.name} (${Math.max(1, Math.round(file.size / 1024))} KB)`;\n this.cd.detectChanges();\n })\n .catch(() => notify('Could not read that file.', undefined, 'error'));\n }\n\n downloadTemplate(): void {\n this.busy = true;\n this.apiService.requestService\n .raw<Blob>('get', 'apps/commerce/catalog-import/template', {}, { responseType: 'blob' })\n .pipe(takeUntil(this.$))\n .subscribe({\n next: (blob) => {\n this.busy = false;\n downloadBlob(blob, 'TILmart Product Upload Template.xlsx');\n },\n error: () => {\n this.busy = false;\n notify('Could not build the template.', undefined, 'error');\n },\n });\n }\n\n run(dryRun: boolean): void {\n if (!this.content) return;\n this.busy = true;\n this.phase = dryRun ? 'check' : 'import';\n this.apiService\n .post('apps/commerce/catalog-import', { contentBase64: this.content, dryRun })\n .pipe(takeUntil(this.$))\n .subscribe({\n next: (res: any) => {\n this.busy = false;\n this.phase = null;\n this.report = res?.data ?? null;\n this.checkedClean = dryRun && !!this.report;\n if (this.report && !dryRun) {\n notify(res?.message || 'Import finished.', undefined, this.report.totals.failed ? 'warning' : 'success');\n }\n },\n error: (e: Error) => {\n this.busy = false;\n this.phase = null;\n notify(e.message || 'The import failed.', undefined, 'error');\n },\n });\n }\n}\n","import { NgModule } from '@angular/core';\nimport { RouterModule } from '@angular/router';\nimport { SharedModule } from '@thescaffold/ngx-core';\nimport { ElementsModule } from '@thescaffold/ngx-ui';\nimport { BulkUploadComponent } from './bulk-upload.component';\n\n@NgModule({\n declarations: [BulkUploadComponent],\n imports: [\n SharedModule,\n ElementsModule,\n RouterModule.forChild([{ path: '', component: BulkUploadComponent }]),\n ],\n})\nexport class BulkUploadModule {}\n"],"names":["i2","i1"],"mappings":"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"}
|