@open-mercato/core 0.6.6-develop.6286.1.d985c5c374 → 0.6.6-develop.6296.1.5e8bdfee17
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/catalog/components/products/ProductComplianceSection.js +235 -62
- package/dist/modules/catalog/components/products/ProductComplianceSection.js.map +2 -2
- package/dist/modules/configs/components/CachePanel.js +2 -1
- package/dist/modules/configs/components/CachePanel.js.map +2 -2
- package/dist/modules/configs/components/SystemStatusPanel.js +10 -12
- package/dist/modules/configs/components/SystemStatusPanel.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-scheduled.js +13 -14
- package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
- package/dist/modules/directory/components/TenantSelect.js +29 -16
- package/dist/modules/directory/components/TenantSelect.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js +47 -1
- package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/hiddenWidgetsStorage.js +35 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/hiddenWidgetsStorage.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +18 -19
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/catalog/components/products/ProductComplianceSection.tsx +138 -27
- package/src/modules/configs/components/CachePanel.tsx +2 -3
- package/src/modules/configs/components/SystemStatusPanel.tsx +12 -19
- package/src/modules/data_sync/workers/sync-scheduled.ts +13 -14
- package/src/modules/directory/components/TenantSelect.tsx +33 -16
- package/src/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.ts +54 -1
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/hiddenWidgetsStorage.ts +36 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +22 -23
|
@@ -29,9 +29,26 @@ type ProductComplianceSectionProps = {
|
|
|
29
29
|
embedded?: boolean;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
function
|
|
32
|
+
function fieldErrorId(inputId: string) {
|
|
33
|
+
return `${inputId}-error`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function describedByError(
|
|
37
|
+
inputId: string,
|
|
38
|
+
message?: string,
|
|
39
|
+
): { "aria-invalid"?: true; "aria-describedby"?: string } {
|
|
40
|
+
return message
|
|
41
|
+
? { "aria-invalid": true, "aria-describedby": fieldErrorId(inputId) }
|
|
42
|
+
: {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function FieldError({ id, message }: { id: string; message?: string }) {
|
|
33
46
|
if (!message) return null;
|
|
34
|
-
return
|
|
47
|
+
return (
|
|
48
|
+
<p id={id} className="text-xs text-destructive">
|
|
49
|
+
{message}
|
|
50
|
+
</p>
|
|
51
|
+
);
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
export function ProductComplianceSection({
|
|
@@ -85,8 +102,12 @@ export function ProductComplianceSection({
|
|
|
85
102
|
onChange={(event) =>
|
|
86
103
|
setValue("countryOfOriginCode", event.target.value.toUpperCase())
|
|
87
104
|
}
|
|
105
|
+
{...describedByError("catalog-product-compliance-country", errors.countryOfOriginCode)}
|
|
106
|
+
/>
|
|
107
|
+
<FieldError
|
|
108
|
+
id={fieldErrorId("catalog-product-compliance-country")}
|
|
109
|
+
message={errors.countryOfOriginCode}
|
|
88
110
|
/>
|
|
89
|
-
<FieldError message={errors.countryOfOriginCode} />
|
|
90
111
|
</div>
|
|
91
112
|
<div className="space-y-2">
|
|
92
113
|
<Label htmlFor="catalog-product-compliance-tax-classification">
|
|
@@ -96,8 +117,12 @@ export function ProductComplianceSection({
|
|
|
96
117
|
id="catalog-product-compliance-tax-classification"
|
|
97
118
|
value={values.taxClassificationCode ?? ""}
|
|
98
119
|
onChange={(event) => setValue("taxClassificationCode", event.target.value)}
|
|
120
|
+
{...describedByError("catalog-product-compliance-tax-classification", errors.taxClassificationCode)}
|
|
121
|
+
/>
|
|
122
|
+
<FieldError
|
|
123
|
+
id={fieldErrorId("catalog-product-compliance-tax-classification")}
|
|
124
|
+
message={errors.taxClassificationCode}
|
|
99
125
|
/>
|
|
100
|
-
<FieldError message={errors.taxClassificationCode} />
|
|
101
126
|
</div>
|
|
102
127
|
<div className="space-y-2">
|
|
103
128
|
<Label htmlFor="catalog-product-compliance-pkwiu">
|
|
@@ -107,8 +132,12 @@ export function ProductComplianceSection({
|
|
|
107
132
|
id="catalog-product-compliance-pkwiu"
|
|
108
133
|
value={values.pkwiuCode ?? ""}
|
|
109
134
|
onChange={(event) => setValue("pkwiuCode", event.target.value)}
|
|
135
|
+
{...describedByError("catalog-product-compliance-pkwiu", errors.pkwiuCode)}
|
|
136
|
+
/>
|
|
137
|
+
<FieldError
|
|
138
|
+
id={fieldErrorId("catalog-product-compliance-pkwiu")}
|
|
139
|
+
message={errors.pkwiuCode}
|
|
110
140
|
/>
|
|
111
|
-
<FieldError message={errors.pkwiuCode} />
|
|
112
141
|
</div>
|
|
113
142
|
<div className="space-y-2">
|
|
114
143
|
<Label htmlFor="catalog-product-compliance-cn">
|
|
@@ -118,8 +147,12 @@ export function ProductComplianceSection({
|
|
|
118
147
|
id="catalog-product-compliance-cn"
|
|
119
148
|
value={values.cnCode ?? ""}
|
|
120
149
|
onChange={(event) => setValue("cnCode", event.target.value)}
|
|
150
|
+
{...describedByError("catalog-product-compliance-cn", errors.cnCode)}
|
|
151
|
+
/>
|
|
152
|
+
<FieldError
|
|
153
|
+
id={fieldErrorId("catalog-product-compliance-cn")}
|
|
154
|
+
message={errors.cnCode}
|
|
121
155
|
/>
|
|
122
|
-
<FieldError message={errors.cnCode} />
|
|
123
156
|
</div>
|
|
124
157
|
<div className="space-y-2 md:col-span-2">
|
|
125
158
|
<Label htmlFor="catalog-product-compliance-hs">
|
|
@@ -129,13 +162,24 @@ export function ProductComplianceSection({
|
|
|
129
162
|
id="catalog-product-compliance-hs"
|
|
130
163
|
value={values.hsCode ?? ""}
|
|
131
164
|
onChange={(event) => setValue("hsCode", event.target.value)}
|
|
165
|
+
{...describedByError("catalog-product-compliance-hs", errors.hsCode)}
|
|
166
|
+
/>
|
|
167
|
+
<FieldError
|
|
168
|
+
id={fieldErrorId("catalog-product-compliance-hs")}
|
|
169
|
+
message={errors.hsCode}
|
|
132
170
|
/>
|
|
133
|
-
<FieldError message={errors.hsCode} />
|
|
134
171
|
</div>
|
|
135
172
|
</div>
|
|
136
173
|
<div className="space-y-2">
|
|
137
|
-
<Label
|
|
138
|
-
|
|
174
|
+
<Label id="catalog-product-compliance-gtu-label">
|
|
175
|
+
{t("catalog.products.compliance.fields.gtuCodes", "GTU codes (JPK_V7)")}
|
|
176
|
+
</Label>
|
|
177
|
+
<div
|
|
178
|
+
role="group"
|
|
179
|
+
aria-labelledby="catalog-product-compliance-gtu-label"
|
|
180
|
+
className="grid gap-2 sm:grid-cols-3 md:grid-cols-4"
|
|
181
|
+
{...describedByError("catalog-product-compliance-gtu", errors.gtuCodes)}
|
|
182
|
+
>
|
|
139
183
|
{CATALOG_GTU_CODES.map((code) => (
|
|
140
184
|
<label
|
|
141
185
|
key={code}
|
|
@@ -151,7 +195,10 @@ export function ProductComplianceSection({
|
|
|
151
195
|
</label>
|
|
152
196
|
))}
|
|
153
197
|
</div>
|
|
154
|
-
<FieldError
|
|
198
|
+
<FieldError
|
|
199
|
+
id={fieldErrorId("catalog-product-compliance-gtu")}
|
|
200
|
+
message={errors.gtuCodes}
|
|
201
|
+
/>
|
|
155
202
|
</div>
|
|
156
203
|
</div>
|
|
157
204
|
|
|
@@ -179,8 +226,12 @@ export function ProductComplianceSection({
|
|
|
179
226
|
max={120}
|
|
180
227
|
value={values.ageMin ?? ""}
|
|
181
228
|
onChange={(event) => setValue("ageMin", event.target.value)}
|
|
229
|
+
{...describedByError("catalog-product-compliance-age-min", errors.ageMin)}
|
|
230
|
+
/>
|
|
231
|
+
<FieldError
|
|
232
|
+
id={fieldErrorId("catalog-product-compliance-age-min")}
|
|
233
|
+
message={errors.ageMin}
|
|
182
234
|
/>
|
|
183
|
-
<FieldError message={errors.ageMin} />
|
|
184
235
|
</div>
|
|
185
236
|
<div className="space-y-2">
|
|
186
237
|
<Label htmlFor="catalog-product-compliance-excise-category">
|
|
@@ -192,7 +243,10 @@ export function ProductComplianceSection({
|
|
|
192
243
|
setValue("exciseCategory", value === NONE_OPTION ? null : value)
|
|
193
244
|
}
|
|
194
245
|
>
|
|
195
|
-
<SelectTrigger
|
|
246
|
+
<SelectTrigger
|
|
247
|
+
id="catalog-product-compliance-excise-category"
|
|
248
|
+
{...describedByError("catalog-product-compliance-excise-category", errors.exciseCategory)}
|
|
249
|
+
>
|
|
196
250
|
<SelectValue />
|
|
197
251
|
</SelectTrigger>
|
|
198
252
|
<SelectContent>
|
|
@@ -206,7 +260,10 @@ export function ProductComplianceSection({
|
|
|
206
260
|
))}
|
|
207
261
|
</SelectContent>
|
|
208
262
|
</Select>
|
|
209
|
-
<FieldError
|
|
263
|
+
<FieldError
|
|
264
|
+
id={fieldErrorId("catalog-product-compliance-excise-category")}
|
|
265
|
+
message={errors.exciseCategory}
|
|
266
|
+
/>
|
|
210
267
|
</div>
|
|
211
268
|
</div>
|
|
212
269
|
<div className="grid gap-2 sm:grid-cols-2">
|
|
@@ -253,8 +310,12 @@ export function ProductComplianceSection({
|
|
|
253
310
|
id="catalog-product-compliance-hazmat-class"
|
|
254
311
|
value={values.hazmatClass ?? ""}
|
|
255
312
|
onChange={(event) => setValue("hazmatClass", event.target.value)}
|
|
313
|
+
{...describedByError("catalog-product-compliance-hazmat-class", errors.hazmatClass)}
|
|
314
|
+
/>
|
|
315
|
+
<FieldError
|
|
316
|
+
id={fieldErrorId("catalog-product-compliance-hazmat-class")}
|
|
317
|
+
message={errors.hazmatClass}
|
|
256
318
|
/>
|
|
257
|
-
<FieldError message={errors.hazmatClass} />
|
|
258
319
|
</div>
|
|
259
320
|
<div className="space-y-2">
|
|
260
321
|
<Label htmlFor="catalog-product-compliance-un-number">
|
|
@@ -265,8 +326,12 @@ export function ProductComplianceSection({
|
|
|
265
326
|
placeholder={t("catalog.products.compliance.placeholders.unNumber", "UN1234")}
|
|
266
327
|
value={values.unNumber ?? ""}
|
|
267
328
|
onChange={(event) => setValue("unNumber", event.target.value)}
|
|
329
|
+
{...describedByError("catalog-product-compliance-un-number", errors.unNumber)}
|
|
330
|
+
/>
|
|
331
|
+
<FieldError
|
|
332
|
+
id={fieldErrorId("catalog-product-compliance-un-number")}
|
|
333
|
+
message={errors.unNumber}
|
|
268
334
|
/>
|
|
269
|
-
<FieldError message={errors.unNumber} />
|
|
270
335
|
</div>
|
|
271
336
|
<div className="space-y-2">
|
|
272
337
|
<Label htmlFor="catalog-product-compliance-packing-group">
|
|
@@ -278,7 +343,10 @@ export function ProductComplianceSection({
|
|
|
278
343
|
setValue("hazmatPackingGroup", value === NONE_OPTION ? null : value)
|
|
279
344
|
}
|
|
280
345
|
>
|
|
281
|
-
<SelectTrigger
|
|
346
|
+
<SelectTrigger
|
|
347
|
+
id="catalog-product-compliance-packing-group"
|
|
348
|
+
{...describedByError("catalog-product-compliance-packing-group", errors.hazmatPackingGroup)}
|
|
349
|
+
>
|
|
282
350
|
<SelectValue />
|
|
283
351
|
</SelectTrigger>
|
|
284
352
|
<SelectContent>
|
|
@@ -292,7 +360,10 @@ export function ProductComplianceSection({
|
|
|
292
360
|
))}
|
|
293
361
|
</SelectContent>
|
|
294
362
|
</Select>
|
|
295
|
-
<FieldError
|
|
363
|
+
<FieldError
|
|
364
|
+
id={fieldErrorId("catalog-product-compliance-packing-group")}
|
|
365
|
+
message={errors.hazmatPackingGroup}
|
|
366
|
+
/>
|
|
296
367
|
</div>
|
|
297
368
|
</div>
|
|
298
369
|
</div>
|
|
@@ -319,8 +390,12 @@ export function ProductComplianceSection({
|
|
|
319
390
|
type="date"
|
|
320
391
|
value={values.launchAt ?? ""}
|
|
321
392
|
onChange={(event) => setValue("launchAt", event.target.value)}
|
|
393
|
+
{...describedByError("catalog-product-compliance-launch-at", errors.launchAt)}
|
|
394
|
+
/>
|
|
395
|
+
<FieldError
|
|
396
|
+
id={fieldErrorId("catalog-product-compliance-launch-at")}
|
|
397
|
+
message={errors.launchAt}
|
|
322
398
|
/>
|
|
323
|
-
<FieldError message={errors.launchAt} />
|
|
324
399
|
</div>
|
|
325
400
|
<div className="space-y-2">
|
|
326
401
|
<Label htmlFor="catalog-product-compliance-eol-at">
|
|
@@ -331,8 +406,12 @@ export function ProductComplianceSection({
|
|
|
331
406
|
type="date"
|
|
332
407
|
value={values.endOfLifeAt ?? ""}
|
|
333
408
|
onChange={(event) => setValue("endOfLifeAt", event.target.value)}
|
|
409
|
+
{...describedByError("catalog-product-compliance-eol-at", errors.endOfLifeAt)}
|
|
410
|
+
/>
|
|
411
|
+
<FieldError
|
|
412
|
+
id={fieldErrorId("catalog-product-compliance-eol-at")}
|
|
413
|
+
message={errors.endOfLifeAt}
|
|
334
414
|
/>
|
|
335
|
-
<FieldError message={errors.endOfLifeAt} />
|
|
336
415
|
</div>
|
|
337
416
|
<div className="space-y-2">
|
|
338
417
|
<Label htmlFor="catalog-product-compliance-available-from">
|
|
@@ -343,8 +422,12 @@ export function ProductComplianceSection({
|
|
|
343
422
|
type="date"
|
|
344
423
|
value={values.availableFrom ?? ""}
|
|
345
424
|
onChange={(event) => setValue("availableFrom", event.target.value)}
|
|
425
|
+
{...describedByError("catalog-product-compliance-available-from", errors.availableFrom)}
|
|
426
|
+
/>
|
|
427
|
+
<FieldError
|
|
428
|
+
id={fieldErrorId("catalog-product-compliance-available-from")}
|
|
429
|
+
message={errors.availableFrom}
|
|
346
430
|
/>
|
|
347
|
-
<FieldError message={errors.availableFrom} />
|
|
348
431
|
</div>
|
|
349
432
|
<div className="space-y-2">
|
|
350
433
|
<Label htmlFor="catalog-product-compliance-available-until">
|
|
@@ -355,8 +438,12 @@ export function ProductComplianceSection({
|
|
|
355
438
|
type="date"
|
|
356
439
|
value={values.availableUntil ?? ""}
|
|
357
440
|
onChange={(event) => setValue("availableUntil", event.target.value)}
|
|
441
|
+
{...describedByError("catalog-product-compliance-available-until", errors.availableUntil)}
|
|
442
|
+
/>
|
|
443
|
+
<FieldError
|
|
444
|
+
id={fieldErrorId("catalog-product-compliance-available-until")}
|
|
445
|
+
message={errors.availableUntil}
|
|
358
446
|
/>
|
|
359
|
-
<FieldError message={errors.availableUntil} />
|
|
360
447
|
</div>
|
|
361
448
|
</div>
|
|
362
449
|
<label
|
|
@@ -395,8 +482,12 @@ export function ProductComplianceSection({
|
|
|
395
482
|
min={1}
|
|
396
483
|
value={values.minOrderQty ?? ""}
|
|
397
484
|
onChange={(event) => setValue("minOrderQty", event.target.value)}
|
|
485
|
+
{...describedByError("catalog-product-compliance-min-qty", errors.minOrderQty)}
|
|
486
|
+
/>
|
|
487
|
+
<FieldError
|
|
488
|
+
id={fieldErrorId("catalog-product-compliance-min-qty")}
|
|
489
|
+
message={errors.minOrderQty}
|
|
398
490
|
/>
|
|
399
|
-
<FieldError message={errors.minOrderQty} />
|
|
400
491
|
</div>
|
|
401
492
|
<div className="space-y-2">
|
|
402
493
|
<Label htmlFor="catalog-product-compliance-max-qty">
|
|
@@ -408,8 +499,12 @@ export function ProductComplianceSection({
|
|
|
408
499
|
min={1}
|
|
409
500
|
value={values.maxOrderQty ?? ""}
|
|
410
501
|
onChange={(event) => setValue("maxOrderQty", event.target.value)}
|
|
502
|
+
{...describedByError("catalog-product-compliance-max-qty", errors.maxOrderQty)}
|
|
503
|
+
/>
|
|
504
|
+
<FieldError
|
|
505
|
+
id={fieldErrorId("catalog-product-compliance-max-qty")}
|
|
506
|
+
message={errors.maxOrderQty}
|
|
411
507
|
/>
|
|
412
|
-
<FieldError message={errors.maxOrderQty} />
|
|
413
508
|
</div>
|
|
414
509
|
<div className="space-y-2">
|
|
415
510
|
<Label htmlFor="catalog-product-compliance-qty-increment">
|
|
@@ -421,8 +516,12 @@ export function ProductComplianceSection({
|
|
|
421
516
|
min={1}
|
|
422
517
|
value={values.orderQtyIncrement ?? ""}
|
|
423
518
|
onChange={(event) => setValue("orderQtyIncrement", event.target.value)}
|
|
519
|
+
{...describedByError("catalog-product-compliance-qty-increment", errors.orderQtyIncrement)}
|
|
520
|
+
/>
|
|
521
|
+
<FieldError
|
|
522
|
+
id={fieldErrorId("catalog-product-compliance-qty-increment")}
|
|
523
|
+
message={errors.orderQtyIncrement}
|
|
424
524
|
/>
|
|
425
|
-
<FieldError message={errors.orderQtyIncrement} />
|
|
426
525
|
</div>
|
|
427
526
|
</div>
|
|
428
527
|
<label
|
|
@@ -460,8 +559,12 @@ export function ProductComplianceSection({
|
|
|
460
559
|
maxLength={255}
|
|
461
560
|
value={values.seoTitle ?? ""}
|
|
462
561
|
onChange={(event) => setValue("seoTitle", event.target.value)}
|
|
562
|
+
{...describedByError("catalog-product-compliance-seo-title", errors.seoTitle)}
|
|
563
|
+
/>
|
|
564
|
+
<FieldError
|
|
565
|
+
id={fieldErrorId("catalog-product-compliance-seo-title")}
|
|
566
|
+
message={errors.seoTitle}
|
|
463
567
|
/>
|
|
464
|
-
<FieldError message={errors.seoTitle} />
|
|
465
568
|
</div>
|
|
466
569
|
<div className="space-y-2">
|
|
467
570
|
<Label htmlFor="catalog-product-compliance-seo-description">
|
|
@@ -473,8 +576,12 @@ export function ProductComplianceSection({
|
|
|
473
576
|
maxLength={1000}
|
|
474
577
|
value={values.seoDescription ?? ""}
|
|
475
578
|
onChange={(event) => setValue("seoDescription", event.target.value)}
|
|
579
|
+
{...describedByError("catalog-product-compliance-seo-description", errors.seoDescription)}
|
|
580
|
+
/>
|
|
581
|
+
<FieldError
|
|
582
|
+
id={fieldErrorId("catalog-product-compliance-seo-description")}
|
|
583
|
+
message={errors.seoDescription}
|
|
476
584
|
/>
|
|
477
|
-
<FieldError message={errors.seoDescription} />
|
|
478
585
|
</div>
|
|
479
586
|
<div className="space-y-2">
|
|
480
587
|
<Label htmlFor="catalog-product-compliance-canonical-url">
|
|
@@ -487,8 +594,12 @@ export function ProductComplianceSection({
|
|
|
487
594
|
placeholder="https://"
|
|
488
595
|
value={values.canonicalUrl ?? ""}
|
|
489
596
|
onChange={(event) => setValue("canonicalUrl", event.target.value)}
|
|
597
|
+
{...describedByError("catalog-product-compliance-canonical-url", errors.canonicalUrl)}
|
|
598
|
+
/>
|
|
599
|
+
<FieldError
|
|
600
|
+
id={fieldErrorId("catalog-product-compliance-canonical-url")}
|
|
601
|
+
message={errors.canonicalUrl}
|
|
490
602
|
/>
|
|
491
|
-
<FieldError message={errors.canonicalUrl} />
|
|
492
603
|
</div>
|
|
493
604
|
</div>
|
|
494
605
|
</div>
|
|
@@ -4,6 +4,7 @@ import * as React from 'react'
|
|
|
4
4
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
5
5
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
6
6
|
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
7
|
+
import { ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
7
8
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
8
9
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
9
10
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -248,9 +249,7 @@ export function CachePanel() {
|
|
|
248
249
|
{t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}
|
|
249
250
|
</p>
|
|
250
251
|
</header>
|
|
251
|
-
<
|
|
252
|
-
{state.error}
|
|
253
|
-
</div>
|
|
252
|
+
<ErrorMessage label={state.error} />
|
|
254
253
|
<div className="flex flex-wrap gap-2">
|
|
255
254
|
<Button variant="outline" type="button" onClick={handleRefresh}>
|
|
256
255
|
{t('configs.cache.retry', 'Retry')}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
5
5
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
6
|
+
import { StatusBadge, type StatusMap } from '@open-mercato/ui/primitives/status-badge'
|
|
7
|
+
import { ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
6
8
|
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
7
9
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
10
|
import type {
|
|
@@ -23,12 +25,12 @@ const STATUS_LABEL_KEYS: Record<SystemStatusState, string> = {
|
|
|
23
25
|
unknown: 'configs.systemStatus.state.unknown',
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
const
|
|
27
|
-
enabled: '
|
|
28
|
-
disabled: '
|
|
29
|
-
set: '
|
|
30
|
-
unset: '
|
|
31
|
-
unknown: '
|
|
28
|
+
const STATUS_BADGE_VARIANTS: StatusMap<SystemStatusState> = {
|
|
29
|
+
enabled: 'success',
|
|
30
|
+
disabled: 'neutral',
|
|
31
|
+
set: 'info',
|
|
32
|
+
unset: 'neutral',
|
|
33
|
+
unknown: 'warning',
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
const RUNTIME_MODE_LABEL_KEYS: Record<SystemStatusRuntimeMode, string> = {
|
|
@@ -84,15 +86,6 @@ function renderEnvAssignment(
|
|
|
84
86
|
)
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
function StatusBadge({ state }: { state: SystemStatusState }) {
|
|
88
|
-
const t = useT()
|
|
89
|
-
return (
|
|
90
|
-
<span className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-medium ${STATUS_BADGE_CLASSES[state]}`}>
|
|
91
|
-
{t(STATUS_LABEL_KEYS[state])}
|
|
92
|
-
</span>
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
89
|
type FetchState = {
|
|
97
90
|
loading: boolean
|
|
98
91
|
error: string | null
|
|
@@ -163,9 +156,7 @@ export function SystemStatusPanel() {
|
|
|
163
156
|
)}
|
|
164
157
|
</p>
|
|
165
158
|
</header>
|
|
166
|
-
<
|
|
167
|
-
{state.error}
|
|
168
|
-
</div>
|
|
159
|
+
<ErrorMessage label={state.error} />
|
|
169
160
|
<Button type="button" variant="outline" onClick={() => loadSnapshot().catch(() => {})}>
|
|
170
161
|
{t('configs.systemStatus.retry', 'Retry')}
|
|
171
162
|
</Button>
|
|
@@ -215,7 +206,9 @@ export function SystemStatusPanel() {
|
|
|
215
206
|
<h4 className="text-sm font-semibold">{t(item.labelKey)}</h4>
|
|
216
207
|
<p className="text-xs text-muted-foreground">{t(item.descriptionKey)}</p>
|
|
217
208
|
</div>
|
|
218
|
-
<StatusBadge
|
|
209
|
+
<StatusBadge variant={STATUS_BADGE_VARIANTS[item.state]}>
|
|
210
|
+
{t(STATUS_LABEL_KEYS[item.state])}
|
|
211
|
+
</StatusBadge>
|
|
219
212
|
</div>
|
|
220
213
|
<dl className="grid gap-3 text-sm sm:grid-cols-2">
|
|
221
214
|
<div className="space-y-1">
|
|
@@ -2,9 +2,10 @@ import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
|
|
|
2
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
3
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
4
|
import type { IntegrationStateService } from '../../integrations/lib/state-service'
|
|
5
|
+
import type { ProgressService } from '../../progress/lib/progressService'
|
|
5
6
|
import type { SyncRunService } from '../lib/sync-run-service'
|
|
6
7
|
import { SyncSchedule } from '../data/entities'
|
|
7
|
-
import {
|
|
8
|
+
import { startDataSyncRun } from '../lib/start-run'
|
|
8
9
|
|
|
9
10
|
type ScheduledSyncPayload = {
|
|
10
11
|
scheduleId: string
|
|
@@ -27,6 +28,7 @@ type HandlerContext = JobContext & {
|
|
|
27
28
|
export default async function handle(job: QueuedJob<ScheduledSyncPayload>, ctx: HandlerContext): Promise<void> {
|
|
28
29
|
const em = ctx.resolve<EntityManager>('em')
|
|
29
30
|
const syncRunService = ctx.resolve<SyncRunService>('dataSyncRunService')
|
|
31
|
+
const progressService = ctx.resolve<ProgressService>('progressService')
|
|
30
32
|
const integrationStateService = ctx.resolve<IntegrationStateService>('integrationStateService')
|
|
31
33
|
|
|
32
34
|
const schedule = await findOneWithDecryption(
|
|
@@ -70,22 +72,19 @@ export default async function handle(job: QueuedJob<ScheduledSyncPayload>, ctx:
|
|
|
70
72
|
job.payload.scope,
|
|
71
73
|
)
|
|
72
74
|
|
|
73
|
-
const run = await syncRunService.createRun({
|
|
74
|
-
integrationId: schedule.integrationId,
|
|
75
|
-
entityType: schedule.entityType,
|
|
76
|
-
direction: schedule.direction,
|
|
77
|
-
cursor,
|
|
78
|
-
triggeredBy: 'scheduler',
|
|
79
|
-
}, job.payload.scope)
|
|
80
|
-
|
|
81
75
|
schedule.lastRunAt = new Date()
|
|
82
76
|
await em.flush()
|
|
83
77
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
runId: run.id,
|
|
88
|
-
batchSize: 100,
|
|
78
|
+
await startDataSyncRun({
|
|
79
|
+
syncRunService,
|
|
80
|
+
progressService,
|
|
89
81
|
scope: job.payload.scope,
|
|
82
|
+
input: {
|
|
83
|
+
integrationId: schedule.integrationId,
|
|
84
|
+
entityType: schedule.entityType,
|
|
85
|
+
direction: schedule.direction,
|
|
86
|
+
cursor,
|
|
87
|
+
triggeredBy: 'scheduler',
|
|
88
|
+
},
|
|
90
89
|
})
|
|
91
90
|
}
|
|
@@ -57,21 +57,11 @@ function filterTenantsByStatus(list: TenantRecord[], status: 'all' | 'active' |
|
|
|
57
57
|
return list
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
search.set('sortDir', 'asc')
|
|
66
|
-
if (status === 'active') search.set('isActive', 'true')
|
|
67
|
-
if (status === 'inactive') search.set('isActive', 'false')
|
|
68
|
-
const json = await readApiResultOrThrow<{ items?: unknown[] }>(
|
|
69
|
-
`/api/directory/tenants?${search.toString()}`,
|
|
70
|
-
undefined,
|
|
71
|
-
{ errorMessage, allowNullResult: true },
|
|
72
|
-
)
|
|
73
|
-
const items = Array.isArray(json?.items) ? json.items : []
|
|
74
|
-
const normalized = items
|
|
60
|
+
const DIRECTORY_TENANTS_PAGE_SIZE = 100
|
|
61
|
+
const DIRECTORY_TENANTS_MAX_PAGES = 200
|
|
62
|
+
|
|
63
|
+
function normalizeTenantItems(items: unknown[]): TenantRecord[] {
|
|
64
|
+
return items
|
|
75
65
|
.map((item: unknown): TenantRecord | null => {
|
|
76
66
|
if (!item || typeof item !== 'object') return null
|
|
77
67
|
const entry = item as Record<string, unknown>
|
|
@@ -84,7 +74,34 @@ async function fetchDirectoryTenants(status: 'all' | 'active' | 'inactive', erro
|
|
|
84
74
|
return { id, name, isActive }
|
|
85
75
|
})
|
|
86
76
|
.filter((tenant: TenantRecord | null): tenant is TenantRecord => tenant !== null)
|
|
87
|
-
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function fetchDirectoryTenants(status: 'all' | 'active' | 'inactive', errorMessage: string): Promise<TenantRecord[]> {
|
|
80
|
+
const collected: TenantRecord[] = []
|
|
81
|
+
let page = 1
|
|
82
|
+
let totalPages = 1
|
|
83
|
+
do {
|
|
84
|
+
const search = new URLSearchParams()
|
|
85
|
+
search.set('page', String(page))
|
|
86
|
+
search.set('pageSize', String(DIRECTORY_TENANTS_PAGE_SIZE))
|
|
87
|
+
search.set('sortField', 'name')
|
|
88
|
+
search.set('sortDir', 'asc')
|
|
89
|
+
if (status === 'active') search.set('isActive', 'true')
|
|
90
|
+
if (status === 'inactive') search.set('isActive', 'false')
|
|
91
|
+
const json = await readApiResultOrThrow<{ items?: unknown[]; totalPages?: unknown }>(
|
|
92
|
+
`/api/directory/tenants?${search.toString()}`,
|
|
93
|
+
undefined,
|
|
94
|
+
{ errorMessage, allowNullResult: true },
|
|
95
|
+
)
|
|
96
|
+
const items = Array.isArray(json?.items) ? json.items : []
|
|
97
|
+
collected.push(...normalizeTenantItems(items))
|
|
98
|
+
totalPages = typeof json?.totalPages === 'number' && Number.isFinite(json.totalPages)
|
|
99
|
+
? Math.max(1, Math.floor(json.totalPages))
|
|
100
|
+
: 1
|
|
101
|
+
if (items.length < DIRECTORY_TENANTS_PAGE_SIZE) break
|
|
102
|
+
page += 1
|
|
103
|
+
} while (page <= totalPages && page <= DIRECTORY_TENANTS_MAX_PAGES)
|
|
104
|
+
return mergeTenantLists(filterTenantsByStatus(collected, status))
|
|
88
105
|
}
|
|
89
106
|
|
|
90
107
|
async function fetchTenantsFromOrganizationSwitcher(status: 'all' | 'active' | 'inactive', errorMessage: string): Promise<TenantRecord[]> {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
+
import { raw } from '@mikro-orm/core'
|
|
2
3
|
import { Resend } from 'resend'
|
|
3
4
|
import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
4
5
|
import { resolveDefaultEmailFromAddress } from '@open-mercato/shared/lib/email/config'
|
|
@@ -111,6 +112,57 @@ export async function POST(req: Request) {
|
|
|
111
112
|
headers['References'] = references.join(' ')
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
// Atomically claim the send right before calling the external email provider so
|
|
116
|
+
// two concurrent requests cannot both deliver the same reply. The claim only
|
|
117
|
+
// succeeds when neither replySentAt nor replySendClaimedAt is already set in the
|
|
118
|
+
// metadata; a losing request gets 0 rows back and is rejected with 409.
|
|
119
|
+
const sendClaimedAt = new Date().toISOString()
|
|
120
|
+
const claimed = await ctx.em.nativeUpdate(
|
|
121
|
+
InboxProposalAction,
|
|
122
|
+
{
|
|
123
|
+
id: action.id,
|
|
124
|
+
proposalId: proposal.id,
|
|
125
|
+
actionType: 'draft_reply',
|
|
126
|
+
organizationId: ctx.organizationId,
|
|
127
|
+
tenantId: ctx.tenantId,
|
|
128
|
+
deletedAt: null,
|
|
129
|
+
status: 'executed',
|
|
130
|
+
[raw(`coalesce("metadata"->>'replySentAt', '')`)]: '',
|
|
131
|
+
[raw(`coalesce("metadata"->>'replySendClaimedAt', '')`)]: '',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
metadata: raw(
|
|
135
|
+
`coalesce("metadata", '{}'::jsonb) || jsonb_build_object('replySendClaimedAt', ?::text)`,
|
|
136
|
+
[sendClaimedAt],
|
|
137
|
+
),
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if (claimed === 0) {
|
|
142
|
+
return NextResponse.json(
|
|
143
|
+
{ error: 'Reply send is already in progress or has been sent for this action' },
|
|
144
|
+
{ status: 409 },
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const releaseSendClaim = async () => {
|
|
149
|
+
try {
|
|
150
|
+
await ctx.em.nativeUpdate(
|
|
151
|
+
InboxProposalAction,
|
|
152
|
+
{
|
|
153
|
+
id: action.id,
|
|
154
|
+
organizationId: ctx.organizationId,
|
|
155
|
+
tenantId: ctx.tenantId,
|
|
156
|
+
[raw(`coalesce("metadata"->>'replySentAt', '')`)]: '',
|
|
157
|
+
[raw(`("metadata"->>'replySendClaimedAt')`)]: sendClaimedAt,
|
|
158
|
+
},
|
|
159
|
+
{ metadata: raw(`"metadata" - 'replySendClaimedAt'`) },
|
|
160
|
+
)
|
|
161
|
+
} catch (releaseError) {
|
|
162
|
+
console.error('[inbox_ops:reply:send] Failed to release send claim:', releaseError)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
114
166
|
const resend = new Resend(apiKey)
|
|
115
167
|
const { data: sendData, error: sendError } = await resend.emails.send({
|
|
116
168
|
to: toAddress,
|
|
@@ -121,6 +173,7 @@ export async function POST(req: Request) {
|
|
|
121
173
|
})
|
|
122
174
|
|
|
123
175
|
if (sendError) {
|
|
176
|
+
await releaseSendClaim()
|
|
124
177
|
const errorMessage = sendError.message || 'Unknown error'
|
|
125
178
|
return NextResponse.json({ error: `Failed to send email: ${errorMessage}` }, { status: 502 })
|
|
126
179
|
}
|
|
@@ -182,7 +235,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
182
235
|
{ status: 200, description: 'Reply sent successfully' },
|
|
183
236
|
{ status: 400, description: 'Missing required payload fields' },
|
|
184
237
|
{ status: 404, description: 'Reply action not found' },
|
|
185
|
-
{ status: 409, description: 'Action in invalid state for sending' },
|
|
238
|
+
{ status: 409, description: 'Action in invalid state for sending, or a send is already in progress / completed for this reply' },
|
|
186
239
|
{ status: 502, description: 'Email delivery failed' },
|
|
187
240
|
{ status: 503, description: 'Email service not configured or disabled' },
|
|
188
241
|
],
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readVersionedPreference,
|
|
3
|
+
writeVersionedPreference,
|
|
4
|
+
clearVersionedPreference,
|
|
5
|
+
} from '@open-mercato/shared/lib/browser/versionedPreference'
|
|
6
|
+
|
|
7
|
+
const LEGACY_HIDDEN_WIDGETS_KEY = 'om:portal:dashboard:hidden'
|
|
8
|
+
const HIDDEN_WIDGETS_KEY_PREFIX = 'om:portal:dashboard:hidden:v1:'
|
|
9
|
+
const HIDDEN_WIDGETS_VERSION = 1
|
|
10
|
+
|
|
11
|
+
function buildHiddenWidgetsKey(orgSlug: string, userId: string): string {
|
|
12
|
+
return `${HIDDEN_WIDGETS_KEY_PREFIX}${orgSlug}:${userId}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isStringArray(value: unknown): value is string[] {
|
|
16
|
+
return Array.isArray(value) && value.every((id) => typeof id === 'string')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function loadHiddenWidgets(orgSlug: string, userId: string): Set<string> {
|
|
20
|
+
const hidden = readVersionedPreference<string[]>(
|
|
21
|
+
buildHiddenWidgetsKey(orgSlug, userId),
|
|
22
|
+
HIDDEN_WIDGETS_VERSION,
|
|
23
|
+
isStringArray,
|
|
24
|
+
[],
|
|
25
|
+
)
|
|
26
|
+
return new Set(hidden)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function saveHiddenWidgets(orgSlug: string, userId: string, hidden: Set<string>): void {
|
|
30
|
+
writeVersionedPreference(buildHiddenWidgetsKey(orgSlug, userId), HIDDEN_WIDGETS_VERSION, Array.from(hidden))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Drops the pre-scoping global key so stale preferences stop leaking across org/user contexts. */
|
|
34
|
+
export function clearLegacyHiddenWidgetsKey(): void {
|
|
35
|
+
clearVersionedPreference(LEGACY_HIDDEN_WIDGETS_KEY)
|
|
36
|
+
}
|