@mohasinac/appkit 4.31.0 → 4.32.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/dist/_internal/server/features/products/list-public.d.ts +18 -0
- package/dist/_internal/server/features/products/list-public.js +27 -9
- package/dist/client.d.ts +9 -0
- package/dist/client.js +12 -0
- package/dist/features/admin/components/AdminAdsView.js +36 -34
- package/dist/features/admin/components/AdminCarouselEditorView.js +78 -10
- package/dist/features/admin/components/AdminOrderEditorView.js +55 -24
- package/dist/features/admin/components/AdminProductEditorView.js +78 -18
- package/dist/features/admin/components/AdminSectionsView.js +89 -32
- package/dist/features/admin/components/AdminSiteSettingsView.js +96 -44
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +58 -43
- package/dist/features/admin/components/AdminUserEditorView.js +65 -82
- package/dist/features/admin/constants/admin-page-tabs.d.ts +36 -0
- package/dist/features/admin/constants/admin-page-tabs.js +45 -0
- package/dist/features/admin/schemas/admin-ops-forms.d.ts +81 -0
- package/dist/features/admin/schemas/admin-ops-forms.js +123 -1
- package/dist/features/admin/schemas/admin-user-form.d.ts +78 -3
- package/dist/features/admin/schemas/admin-user-form.js +96 -7
- package/dist/features/admin/schemas/homepage-section-form.d.ts +9 -0
- package/dist/features/layout/PageTabs.d.ts +17 -0
- package/dist/features/layout/PageTabs.js +45 -0
- package/dist/features/products/components/GroupSettingsPanel.js +62 -16
- package/dist/features/search/action-index/tab-entries.d.ts +18 -0
- package/dist/features/search/action-index/tab-entries.js +128 -0
- package/dist/features/seller/components/SellerGoogleReviewsView.js +31 -19
- package/dist/features/seller/components/SellerOrdersView.js +57 -19
- package/dist/features/seller/components/SellerReviewsView.d.ts +2 -1
- package/dist/features/seller/components/SellerReviewsView.js +54 -19
- package/dist/features/seller/constants/store-tabs.d.ts +64 -0
- package/dist/features/seller/constants/store-tabs.js +60 -0
- package/dist/features/seller/schemas/order-forms.d.ts +33 -0
- package/dist/features/seller/schemas/order-forms.js +75 -0
- package/dist/features/seller/schemas/review-forms.d.ts +37 -0
- package/dist/features/seller/schemas/review-forms.js +82 -0
- package/dist/features/shell/index.d.ts +1 -1
- package/dist/features/shell/index.js +5 -1
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +44 -15
- package/dist/features/shipments/schemas/validation.d.ts +41 -0
- package/dist/features/shipments/schemas/validation.js +49 -0
- package/dist/features/store-extensions/schemas/google-config-form.d.ts +36 -0
- package/dist/features/store-extensions/schemas/google-config-form.js +24 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +67 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +53 -22
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/next/index.d.ts +8 -0
- package/dist/next/index.js +37 -0
- package/dist/react/hooks/useTabParam.d.ts +19 -0
- package/dist/react/hooks/useTabParam.js +65 -0
- package/dist/schemas/registry.d.ts +27 -3
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: W8's C2 folds sibling store pages that are one task into one tabbed
|
|
3
|
+
* page. Each absorbed page keeps a `?tab=` deep link so it stays findable
|
|
4
|
+
* from the command palette and from a bookmark, and the action index's
|
|
5
|
+
* `AIX_DEAD_DEEPLINK` check needs a union to validate those links
|
|
6
|
+
* against — a deep link naming a tab that does not exist opens the page
|
|
7
|
+
* on its default tab and reads as the search having missed.
|
|
8
|
+
*
|
|
9
|
+
* ## 🛑 Tab ids are NOT nav hrefs
|
|
10
|
+
*
|
|
11
|
+
* `navItemId(portal, href)` strips the query, so `/store/payouts` and
|
|
12
|
+
* `/store/payouts?tab=methods` produce the SAME nav id — which is at once the
|
|
13
|
+
* `navConfig` toggle key, the gate that makes `requiredPermission` run, and the
|
|
14
|
+
* action-index entry id. So the absorbed pages keep their own hrefs (as
|
|
15
|
+
* redirect shells) and these ids live only in `?tab=`.
|
|
16
|
+
*
|
|
17
|
+
* EXPORTS:
|
|
18
|
+
* PAYOUTS_TABS / isPayoutsTabId, SHIPPING_TABS / isShippingTabId,
|
|
19
|
+
* FULFILLMENT_TABS / isFulfillmentTabId, STOREFRONT_TABS / isStorefrontTabId
|
|
20
|
+
*
|
|
21
|
+
* @tag domain:seller
|
|
22
|
+
* @tag layer:constants
|
|
23
|
+
* @tag pattern:registry
|
|
24
|
+
* @tag access:isomorphic
|
|
25
|
+
* @tag consumers:store payouts/shipping/fulfillment/storefront pages,action index
|
|
26
|
+
* @tag sideEffects:none
|
|
27
|
+
*/
|
|
28
|
+
/** "Get paid": what I am owed, where it goes, on what schedule. */
|
|
29
|
+
export const PAYOUTS_TABS = [
|
|
30
|
+
{ id: "payouts", label: "Payouts", description: "What you have been paid, and what is still due." },
|
|
31
|
+
{ id: "methods", label: "Methods", description: "Where your money goes — bank or UPI." },
|
|
32
|
+
{ id: "settings", label: "Settings", description: "How often you get paid, and the minimum." },
|
|
33
|
+
];
|
|
34
|
+
export function isPayoutsTabId(value) {
|
|
35
|
+
return !!value && PAYOUTS_TABS.some((t) => t.id === value);
|
|
36
|
+
}
|
|
37
|
+
/** Shipping defaults, plus the reusable named presets built on top of them. */
|
|
38
|
+
export const SHIPPING_TABS = [
|
|
39
|
+
{ id: "rates", label: "Rates & zones", description: "What you charge to ship, and where you ship to." },
|
|
40
|
+
{ id: "configs", label: "Named configs", description: "Named shipping rules you can reuse per listing." },
|
|
41
|
+
];
|
|
42
|
+
export function isShippingTabId(value) {
|
|
43
|
+
return !!value && SHIPPING_TABS.some((t) => t.id === value);
|
|
44
|
+
}
|
|
45
|
+
/** The pack-and-dispatch loop: the queue, then the paperwork it needs. */
|
|
46
|
+
export const FULFILLMENT_TABS = [
|
|
47
|
+
{ id: "queue", label: "Queue", description: "Orders waiting to be packed or handed over." },
|
|
48
|
+
{ id: "print", label: "Print centre", description: "Print labels, invoices and barcodes in a batch." },
|
|
49
|
+
];
|
|
50
|
+
export function isFulfillmentTabId(value) {
|
|
51
|
+
return !!value && FULFILLMENT_TABS.some((t) => t.id === value);
|
|
52
|
+
}
|
|
53
|
+
/** The shop's identity — its look, and the address buyers reach it at. */
|
|
54
|
+
export const STOREFRONT_TABS = [
|
|
55
|
+
{ id: "profile", label: "Profile", description: "Your shop's name, logo, banner and description." },
|
|
56
|
+
{ id: "slug", label: "Web address", description: "The web address buyers reach your shop at." },
|
|
57
|
+
];
|
|
58
|
+
export function isStorefrontTabId(value) {
|
|
59
|
+
return !!value && STOREFRONT_TABS.some((t) => t.id === value);
|
|
60
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* 🛑 `""` is a real, meaningful value here — it is "keep the current status",
|
|
4
|
+
* which is what the panel's first option means and what its diff-against-the-
|
|
5
|
+
* loaded-order payload relies on. It is not an empty required field.
|
|
6
|
+
*/
|
|
7
|
+
export declare const sellerOrderUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
8
|
+
status: z.ZodEnum<["", "confirmed", "processing", "shipped", "delivered", "cancelled"]>;
|
|
9
|
+
trackingNumber: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
10
|
+
carrier: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
11
|
+
trackingUrl: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
status: "" | "cancelled" | "processing" | "delivered" | "confirmed" | "shipped";
|
|
14
|
+
trackingNumber?: string | undefined;
|
|
15
|
+
trackingUrl?: string | undefined;
|
|
16
|
+
carrier?: string | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
status: "" | "cancelled" | "processing" | "delivered" | "confirmed" | "shipped";
|
|
19
|
+
trackingNumber?: string | undefined;
|
|
20
|
+
trackingUrl?: string | undefined;
|
|
21
|
+
carrier?: string | undefined;
|
|
22
|
+
}>, {
|
|
23
|
+
status: "" | "cancelled" | "processing" | "delivered" | "confirmed" | "shipped";
|
|
24
|
+
trackingNumber?: string | undefined;
|
|
25
|
+
trackingUrl?: string | undefined;
|
|
26
|
+
carrier?: string | undefined;
|
|
27
|
+
}, {
|
|
28
|
+
status: "" | "cancelled" | "processing" | "delivered" | "confirmed" | "shipped";
|
|
29
|
+
trackingNumber?: string | undefined;
|
|
30
|
+
trackingUrl?: string | undefined;
|
|
31
|
+
carrier?: string | undefined;
|
|
32
|
+
}>;
|
|
33
|
+
export type SellerOrderUpdateValues = z.infer<typeof sellerOrderUpdateSchema>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: The seller's "Update order" panel wrote a status change, a tracking
|
|
3
|
+
* number, a carrier and a tracking URL with no schema at all — the one
|
|
4
|
+
* form in the seller dashboard that changes what a BUYER sees about a
|
|
5
|
+
* shipment they are waiting on.
|
|
6
|
+
*
|
|
7
|
+
* Two rules were missing, both invisible until a buyer complained. A
|
|
8
|
+
* tracking URL was a free string, so a typo'd or non-http value went
|
|
9
|
+
* straight into the tracking link a buyer clicks. And a status could move
|
|
10
|
+
* to `shipped` with no tracking number at all — the seller's own carrier
|
|
11
|
+
* fields sat right there, empty, and nothing said so.
|
|
12
|
+
*
|
|
13
|
+
* WHAT: The four fields the panel offers, annotated for
|
|
14
|
+
* `buildSectionsFromSchema`.
|
|
15
|
+
*
|
|
16
|
+
* EXPORTS:
|
|
17
|
+
* sellerOrderUpdateSchema, type SellerOrderUpdateValues
|
|
18
|
+
*
|
|
19
|
+
* @tag domain:seller,orders
|
|
20
|
+
* @tag layer:schema
|
|
21
|
+
* @tag pattern:none
|
|
22
|
+
* @tag access:isomorphic
|
|
23
|
+
* @tag consumers:SellerOrderDetailPanel
|
|
24
|
+
* @tag sideEffects:none
|
|
25
|
+
*/
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
28
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
29
|
+
/**
|
|
30
|
+
* 🛑 `""` is a real, meaningful value here — it is "keep the current status",
|
|
31
|
+
* which is what the panel's first option means and what its diff-against-the-
|
|
32
|
+
* loaded-order payload relies on. It is not an empty required field.
|
|
33
|
+
*/
|
|
34
|
+
export const sellerOrderUpdateSchema = z
|
|
35
|
+
.object({
|
|
36
|
+
status: annotate(z.enum(["", "confirmed", "processing", "shipped", "delivered", "cancelled"]), {
|
|
37
|
+
section: "status", sectionLabel: "Update order", sectionRequired: true,
|
|
38
|
+
quick: true, order: 1, row: "full", kind: "select", label: "New status",
|
|
39
|
+
}),
|
|
40
|
+
trackingNumber: annotate(z.string().trim().max(120).optional().or(z.literal("")), {
|
|
41
|
+
section: "shipping", sectionLabel: "Shipment", order: 1, row: "pair",
|
|
42
|
+
label: "Tracking number",
|
|
43
|
+
}),
|
|
44
|
+
carrier: annotate(z.string().trim().max(80).optional().or(z.literal("")), {
|
|
45
|
+
section: "shipping", order: 2, row: "pair",
|
|
46
|
+
}),
|
|
47
|
+
trackingUrl: annotate(z.string().trim().max(2000).optional().or(z.literal("")), {
|
|
48
|
+
section: "shipping", order: 3, row: "full", inputType: "url",
|
|
49
|
+
label: "Tracking URL (optional)",
|
|
50
|
+
}),
|
|
51
|
+
})
|
|
52
|
+
.superRefine((v, ctx) => {
|
|
53
|
+
const issue = (path, message) => ctx.addIssue({ code: z.ZodIssueCode.custom, path: [path], message });
|
|
54
|
+
// Marking an order shipped is what starts the buyer's "where is it"
|
|
55
|
+
// question, and the field that answers it is on this same panel.
|
|
56
|
+
if (v.status === "shipped" && !v.trackingNumber?.trim()) {
|
|
57
|
+
issue("trackingNumber", "A shipped order needs a tracking number.");
|
|
58
|
+
}
|
|
59
|
+
const url = v.trackingUrl?.trim();
|
|
60
|
+
if (url) {
|
|
61
|
+
let parsed = null;
|
|
62
|
+
try {
|
|
63
|
+
parsed = new URL(url);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
void normalizeError(err);
|
|
67
|
+
parsed = null;
|
|
68
|
+
}
|
|
69
|
+
if (!parsed)
|
|
70
|
+
issue("trackingUrl", "That is not a valid URL.");
|
|
71
|
+
else if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
72
|
+
issue("trackingUrl", "A tracking URL must start with http or https.");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Matches `replySchema` on `POST /api/store/reviews/[id]/reply`. */
|
|
3
|
+
export declare const reviewReplySchema: z.ZodObject<{
|
|
4
|
+
reply: z.ZodString;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
reply: string;
|
|
7
|
+
}, {
|
|
8
|
+
reply: string;
|
|
9
|
+
}>;
|
|
10
|
+
export type ReviewReplyValues = z.infer<typeof reviewReplySchema>;
|
|
11
|
+
/** The same reply, posted on every selected review. */
|
|
12
|
+
export declare const reviewBulkReplySchema: z.ZodObject<{
|
|
13
|
+
reply: z.ZodString;
|
|
14
|
+
}, "strip", z.ZodTypeAny, {
|
|
15
|
+
reply: string;
|
|
16
|
+
}, {
|
|
17
|
+
reply: string;
|
|
18
|
+
}>;
|
|
19
|
+
export type ReviewBulkReplyValues = z.infer<typeof reviewBulkReplySchema>;
|
|
20
|
+
/** Flags a review for admin investigation. */
|
|
21
|
+
export declare const reviewContestSchema: z.ZodObject<{
|
|
22
|
+
reason: z.ZodString;
|
|
23
|
+
}, "strip", z.ZodTypeAny, {
|
|
24
|
+
reason: string;
|
|
25
|
+
}, {
|
|
26
|
+
reason: string;
|
|
27
|
+
}>;
|
|
28
|
+
export type ReviewContestValues = z.infer<typeof reviewContestSchema>;
|
|
29
|
+
/** A private note to the buyer, never shown on the public review. */
|
|
30
|
+
export declare const reviewFeedbackSchema: z.ZodObject<{
|
|
31
|
+
feedback: z.ZodString;
|
|
32
|
+
}, "strip", z.ZodTypeAny, {
|
|
33
|
+
feedback: string;
|
|
34
|
+
}, {
|
|
35
|
+
feedback: string;
|
|
36
|
+
}>;
|
|
37
|
+
export type ReviewFeedbackValues = z.infer<typeof reviewFeedbackSchema>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: The seller's reviews page has FOUR write surfaces — reply, bulk reply,
|
|
3
|
+
* contest, send-feedback — and not one of them had a schema. Every guard
|
|
4
|
+
* was a `disabled={!text.trim()}` on the button, which is a UI hint, not a
|
|
5
|
+
* rule: it says nothing about the ceiling the route enforces, and it
|
|
6
|
+
* cannot say anything at all about content.
|
|
7
|
+
*
|
|
8
|
+
* The reply route caps at 1000 characters. The textarea carried
|
|
9
|
+
* `maxLength={1000}`, so the browser truncated silently rather than
|
|
10
|
+
* saying so — a seller writing a long reply lost the tail with no message.
|
|
11
|
+
* A schema puts the same number where it can be reported.
|
|
12
|
+
*
|
|
13
|
+
* The contest route trims and requires a non-empty `reason` and nothing
|
|
14
|
+
* more, so "x" filed a moderation request a human then has to interpret.
|
|
15
|
+
* A ten-character floor matches the admin offer-cancel reason, which is
|
|
16
|
+
* the same kind of record: the only durable account of why.
|
|
17
|
+
*
|
|
18
|
+
* WHAT: One schema per surface. Each is a single field, and that is the point —
|
|
19
|
+
* they are the same shape as `hardBanReasonSchema`, and they get the same
|
|
20
|
+
* error summary and section badge as every other form.
|
|
21
|
+
*
|
|
22
|
+
* EXPORTS:
|
|
23
|
+
* reviewReplySchema, reviewBulkReplySchema, reviewContestSchema,
|
|
24
|
+
* reviewFeedbackSchema, and their value types
|
|
25
|
+
*
|
|
26
|
+
* @tag domain:seller,reviews
|
|
27
|
+
* @tag layer:schema
|
|
28
|
+
* @tag pattern:none
|
|
29
|
+
* @tag access:isomorphic
|
|
30
|
+
* @tag consumers:SellerReviewsView
|
|
31
|
+
* @tag sideEffects:none
|
|
32
|
+
*/
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
35
|
+
/** Matches `replySchema` on `POST /api/store/reviews/[id]/reply`. */
|
|
36
|
+
export const reviewReplySchema = z.object({
|
|
37
|
+
reply: annotate(z
|
|
38
|
+
.string()
|
|
39
|
+
.trim()
|
|
40
|
+
.min(1, "Write a reply before posting it.")
|
|
41
|
+
.max(1000, "A reply is capped at 1000 characters."), {
|
|
42
|
+
section: "reply", sectionLabel: "Store reply", sectionRequired: true,
|
|
43
|
+
quick: true, order: 1, row: "full", kind: "textarea",
|
|
44
|
+
help: "Shown publicly beneath the buyer's review.",
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
/** The same reply, posted on every selected review. */
|
|
48
|
+
export const reviewBulkReplySchema = z.object({
|
|
49
|
+
reply: annotate(z
|
|
50
|
+
.string()
|
|
51
|
+
.trim()
|
|
52
|
+
.min(1, "Write a reply before sending it.")
|
|
53
|
+
.max(1000, "A reply is capped at 1000 characters."), {
|
|
54
|
+
section: "reply", sectionLabel: "Reply", sectionRequired: true,
|
|
55
|
+
quick: true, order: 1, row: "full", kind: "textarea",
|
|
56
|
+
help: "The same reply will be posted on all selected reviews.",
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
/** Flags a review for admin investigation. */
|
|
60
|
+
export const reviewContestSchema = z.object({
|
|
61
|
+
reason: annotate(z
|
|
62
|
+
.string()
|
|
63
|
+
.trim()
|
|
64
|
+
.min(10, "Say why this review should be investigated — at least 10 characters.")
|
|
65
|
+
.max(1000), {
|
|
66
|
+
section: "contest", sectionLabel: "Reason", sectionRequired: true,
|
|
67
|
+
quick: true, order: 1, row: "full", kind: "textarea",
|
|
68
|
+
help: "Fake, abusive, off-topic — be specific; a moderator reads this.",
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
/** A private note to the buyer, never shown on the public review. */
|
|
72
|
+
export const reviewFeedbackSchema = z.object({
|
|
73
|
+
feedback: annotate(z
|
|
74
|
+
.string()
|
|
75
|
+
.trim()
|
|
76
|
+
.min(1, "Write your message before sending it.")
|
|
77
|
+
.max(1000, "Keep the message under 1000 characters."), {
|
|
78
|
+
section: "feedback", sectionLabel: "Feedback", sectionRequired: true,
|
|
79
|
+
quick: true, order: 1, row: "full", kind: "textarea",
|
|
80
|
+
help: "Sent to the buyer's notification inbox. Does not appear on the review.",
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
@@ -11,6 +11,6 @@ export { deriveQuickFields, deriveQuickFieldNames, allDefinitionFields, applyDer
|
|
|
11
11
|
export type { FieldUiMeta, FieldRow, FieldKind, CalculatedTier, } from "./field-ui-meta";
|
|
12
12
|
export { ui, annotate, ROW_COLUMNS, inferFieldUiMeta, resolveFieldUiMeta, unwrapSchema, schemaEnumOptions, schemaIsOptional, objectShape, } from "./field-ui-meta";
|
|
13
13
|
export type { ResolvedField, FieldRenderer, BuildSectionsOptions, } from "./build-sections";
|
|
14
|
-
export { buildSectionsFromSchema, resolveFields, groupFieldsBySection, packRows, } from "./build-sections";
|
|
14
|
+
export { buildSectionsFromSchema, resolveFields, groupFieldsBySection, packRows, visibleValues, } from "./build-sections";
|
|
15
15
|
export type { TitleDescriptionGroupProps, ImageFieldGroupProps, SeoFieldGroupProps, StatusFieldGroupProps, StatusFieldGroupOption, } from "./field-groups";
|
|
16
16
|
export { TitleDescriptionGroup, ImageFieldGroup, SeoFieldGroup, StatusFieldGroup, } from "./field-groups";
|
|
@@ -4,5 +4,9 @@ export { QuickFormDrawer } from "./QuickFormDrawer";
|
|
|
4
4
|
export { SectionForm, useSectionFormNav, orderSections, buildFieldToSectionIndex, sectionAnchorId, } from "./SectionForm";
|
|
5
5
|
export { deriveQuickFields, deriveQuickFieldNames, allDefinitionFields, applyDerive, assertNoDeriveCollision, humaniseFieldName, } from "./entity-form";
|
|
6
6
|
export { ui, annotate, ROW_COLUMNS, inferFieldUiMeta, resolveFieldUiMeta, unwrapSchema, schemaEnumOptions, schemaIsOptional, objectShape, } from "./field-ui-meta";
|
|
7
|
-
export { buildSectionsFromSchema, resolveFields, groupFieldsBySection, packRows,
|
|
7
|
+
export { buildSectionsFromSchema, resolveFields, groupFieldsBySection, packRows,
|
|
8
|
+
// The payload builder. A `when` predicate hides a CONTROL, not a VALUE, so a
|
|
9
|
+
// draft can still carry a field the user can no longer see — build the
|
|
10
|
+
// submitted object from this, never from the raw draft.
|
|
11
|
+
visibleValues, } from "./build-sections";
|
|
8
12
|
export { TitleDescriptionGroup, ImageFieldGroup, SeoFieldGroup, StatusFieldGroup, } from "./field-groups";
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
5
|
-
import { Alert, Badge, Button, Div,
|
|
5
|
+
import { Alert, Badge, Button, Div, Heading, Modal, Pagination, Skeleton, Stack, Table, Thead, Tbody, Tr, Th, Td, Text, Textarea, } from "../../../ui";
|
|
6
6
|
import { apiClient } from "../../../http";
|
|
7
|
-
import { createShipmentItemSchema } from "../schemas/validation";
|
|
7
|
+
import { createShipmentItemSchema, shipmentItemFormSchema, } from "../schemas/validation";
|
|
8
|
+
import { SectionForm, useSectionFormNav, buildSectionsFromSchema, visibleValues, } from "../../shell";
|
|
9
|
+
import { useFormShellState, FormShellContext, FormErrorSummary } from "../../../ui/forms";
|
|
10
|
+
import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
|
|
8
11
|
import { ValidationError } from "../../../errors/validation-error";
|
|
9
12
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
10
13
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
@@ -34,10 +37,19 @@ export function AdminShipmentLotItemsView({ shipmentId, lotId }) {
|
|
|
34
37
|
const [pageSize, setPageSize] = React.useState(50);
|
|
35
38
|
const { items, meta, isLoading, refetch } = useShipmentItems(shipmentId, lotId, { page, pageSize });
|
|
36
39
|
const [showAddItem, setShowAddItem] = React.useState(false);
|
|
37
|
-
const [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
const [itemDraft, setItemDraft] = React.useState({
|
|
41
|
+
title: "",
|
|
42
|
+
quantity: 1,
|
|
43
|
+
isForSelfUse: false,
|
|
44
|
+
price: 0,
|
|
45
|
+
});
|
|
46
|
+
const itemSections = React.useMemo(() => buildSectionsFromSchema(shipmentItemFormSchema), []);
|
|
47
|
+
const itemNav = useSectionFormNav(itemSections, itemDraft, { scope: "admin:shipment-item" });
|
|
48
|
+
const itemForm = useFormShellState(shipmentItemFormSchema, {
|
|
49
|
+
sections: itemNav.sectionMeta,
|
|
50
|
+
onGoToSection: itemNav.goToSection,
|
|
51
|
+
fieldToSectionIndex: itemNav.fieldToSectionIndex,
|
|
52
|
+
});
|
|
41
53
|
const [showBulkImport, setShowBulkImport] = React.useState(false);
|
|
42
54
|
const [bulkText, setBulkText] = React.useState("");
|
|
43
55
|
const parsedBulkRows = React.useMemo(() => parseBulkText(bulkText), [bulkText]);
|
|
@@ -52,11 +64,19 @@ export function AdminShipmentLotItemsView({ shipmentId, lotId }) {
|
|
|
52
64
|
* named no field. `Number(priceRupees)` also yields NaN on a typo,
|
|
53
65
|
* which the schema rejects and the old payload sent.
|
|
54
66
|
*/
|
|
67
|
+
/*
|
|
68
|
+
* Built from `visibleValues`, so a price typed before "for self use" was
|
|
69
|
+
* ticked is dropped rather than submitted — the control is hidden by a
|
|
70
|
+
* `when` predicate, and a hidden control's value must not travel.
|
|
71
|
+
*/
|
|
72
|
+
const draft = visibleValues(shipmentItemFormSchema, itemDraft);
|
|
55
73
|
const parsed = createShipmentItemSchema.safeParse({
|
|
56
|
-
title,
|
|
57
|
-
quantity: Number(quantity),
|
|
58
|
-
isForSelfUse,
|
|
59
|
-
price: isForSelfUse
|
|
74
|
+
title: draft.title,
|
|
75
|
+
quantity: Number(draft.quantity),
|
|
76
|
+
isForSelfUse: draft.isForSelfUse,
|
|
77
|
+
price: draft.isForSelfUse
|
|
78
|
+
? undefined
|
|
79
|
+
: Math.round(Number(draft.price ?? 0) * 100) / 100,
|
|
60
80
|
});
|
|
61
81
|
if (!parsed.success) {
|
|
62
82
|
// ValidationError carries the issue list; a bare Error flattens it to
|
|
@@ -66,10 +86,7 @@ export function AdminShipmentLotItemsView({ shipmentId, lotId }) {
|
|
|
66
86
|
return apiClient.post(ADMIN_ENDPOINTS.SHIPMENT_LOT_ITEMS(shipmentId, lotId), parsed.data);
|
|
67
87
|
},
|
|
68
88
|
onSuccess: () => {
|
|
69
|
-
|
|
70
|
-
setQuantity("1");
|
|
71
|
-
setPriceRupees("0");
|
|
72
|
-
setIsForSelfUse(false);
|
|
89
|
+
setItemDraft({ title: "", quantity: 1, isForSelfUse: false, price: 0 });
|
|
73
90
|
setShowAddItem(false);
|
|
74
91
|
refetch();
|
|
75
92
|
},
|
|
@@ -105,7 +122,19 @@ export function AdminShipmentLotItemsView({ shipmentId, lotId }) {
|
|
|
105
122
|
}),
|
|
106
123
|
onSuccess: () => refetch(),
|
|
107
124
|
});
|
|
108
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { layout: "flex", align: "center", justify: "between", children: [_jsxs(Heading, { level: 3, children: ["Lot Items (", meta?.total ?? items.length, "/500)"] }), _jsxs(Div, { className: "flex gap-[var(--appkit-space-2)]", children: [_jsx(Button, { size: "sm", variant: "secondary", onClick: () => setShowAddItem((v) => !v), children: showAddItem ? "Cancel" : "+ Add Item" }), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => setShowBulkImport(true), children: "Bulk Import" })] })] }), _jsx(Alert, { variant: "info", children: "Only individually notable \"main\" items need a row here \u2014 everything else in the lot can be captured as a single remainder estimate on the lot itself. Linking an item to a product does not sync future edits either way." }), showAddItem && (_jsxs(
|
|
125
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { layout: "flex", align: "center", justify: "between", children: [_jsxs(Heading, { level: 3, children: ["Lot Items (", meta?.total ?? items.length, "/500)"] }), _jsxs(Div, { className: "flex gap-[var(--appkit-space-2)]", children: [_jsx(Button, { size: "sm", variant: "secondary", onClick: () => setShowAddItem((v) => !v), children: showAddItem ? "Cancel" : "+ Add Item" }), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => setShowBulkImport(true), children: "Bulk Import" })] })] }), _jsx(Alert, { variant: "info", children: "Only individually notable \"main\" items need a row here \u2014 everything else in the lot can be captured as a single remainder estimate on the lot itself. Linking an item to a product does not sync future edits either way." }), showAddItem && (_jsxs(FormShellContext.Provider, { value: itemForm.shellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: itemSections, values: itemDraft, onChange: (partial) => setItemDraft((d) => ({ ...d, ...partial })), onSubmit: () => {
|
|
126
|
+
/*
|
|
127
|
+
* The guard lives here: SectionForm surfaces errors and scrolls to
|
|
128
|
+
* them, then calls onSubmit unconditionally.
|
|
129
|
+
*/
|
|
130
|
+
itemForm.clearErrors();
|
|
131
|
+
const parsed = shipmentItemFormSchema.safeParse(visibleValues(shipmentItemFormSchema, itemDraft));
|
|
132
|
+
if (!parsed.success) {
|
|
133
|
+
applyZodIssues(parsed.error.issues, itemForm.setFieldError);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
addItemMutation.mutate();
|
|
137
|
+
}, onValidationChange: () => itemForm.validate(itemDraft), schema: shipmentItemFormSchema, openIds: itemNav.openIds, onOpenChange: itemNav.setOpenIds, submitLabel: "Save Item", cancelLabel: "Cancel", onCancel: () => setShowAddItem(false), isLoading: addItemMutation.isPending })] })), isLoading ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" })] })) : items.length === 0 ? (_jsx(Text, { variant: "secondary", children: "No items tracked yet." })) : (_jsxs(Table, { children: [_jsx(Thead, { children: _jsxs(Tr, { children: [_jsx(Th, { children: "Title" }), _jsx(Th, { children: "Qty" }), _jsx(Th, { children: "Price" }), _jsx(Th, { children: "Status" }), _jsx(Th, {})] }) }), _jsx(Tbody, { children: items.map((item) => (_jsxs(Tr, { children: [_jsx(Td, { children: item.title }), _jsx(Td, { children: item.quantity }), _jsx(Td, { children: item.isForSelfUse ? "—" : formatCurrency(item.price ?? 0) }), _jsx(Td, { children: item.isForSelfUse ? (_jsx(Badge, { variant: "secondary", children: "Self use" })) : item.linkedProductId ? (_jsx(Badge, { variant: "success", children: "Linked" })) : (_jsx(Badge, { variant: "default", children: "Unlinked" })) }), _jsxs(Td, { className: "flex gap-[var(--appkit-space-2)]", children: [!item.isForSelfUse &&
|
|
109
138
|
(item.linkedProductId ? (_jsx(Button, { size: "sm", variant: "ghost", isLoading: unlinkItemMutation.isPending, onClick: () => unlinkItemMutation.mutate(item.id), children: "Unlink" })) : (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setLinkTarget({ itemId: item.id, title: item.title }), children: "Create pre-order link" }))), _jsx(Button, { size: "sm", variant: "ghost", action: ACTIONS.SHIPMENT["delete"], isLoading: deleteItemMutation.isPending, onClick: () => deleteItemMutation.mutate(item.id), children: "Delete" })] })] }, item.id))) })] })), meta && meta.totalPages > 1 && (_jsx(Pagination, { currentPage: page, totalPages: meta.totalPages, onPageChange: setPage, pageSize: pageSize, onPageSizeChange: (size) => {
|
|
110
139
|
setPageSize(size);
|
|
111
140
|
setPage(1);
|
|
@@ -262,6 +262,47 @@ export declare const createShipmentItemSchema: z.ZodEffects<z.ZodObject<{
|
|
|
262
262
|
categorySlugs?: string[] | undefined;
|
|
263
263
|
condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | "graded" | undefined;
|
|
264
264
|
}>;
|
|
265
|
+
/**
|
|
266
|
+
* The four fields the add-item form actually offers, annotated for
|
|
267
|
+
* `buildSectionsFromSchema`.
|
|
268
|
+
*
|
|
269
|
+
* 🛑 A separate object rather than `shipmentItemBaseSchema.pick(...)`, for two
|
|
270
|
+
* reasons. `.pick()` returns NEW field instances and the annotation registry is
|
|
271
|
+
* a WeakMap keyed by instance, so the meta would not survive. And deriving the
|
|
272
|
+
* form from the full base schema would render `categorySlugs` (an array) and
|
|
273
|
+
* `condition` as controls the add-item row has never had — an array has no
|
|
274
|
+
* `FieldKind`, so it would appear as a disabled placeholder telling the user to
|
|
275
|
+
* supply a renderer.
|
|
276
|
+
*
|
|
277
|
+
* The base schema stays the API contract; this is the form's subset of it.
|
|
278
|
+
*/
|
|
279
|
+
export declare const shipmentItemFormSchema: z.ZodEffects<z.ZodObject<{
|
|
280
|
+
title: z.ZodString;
|
|
281
|
+
quantity: z.ZodNumber;
|
|
282
|
+
isForSelfUse: z.ZodBoolean;
|
|
283
|
+
price: z.ZodOptional<z.ZodNumber>;
|
|
284
|
+
}, "strip", z.ZodTypeAny, {
|
|
285
|
+
title: string;
|
|
286
|
+
quantity: number;
|
|
287
|
+
isForSelfUse: boolean;
|
|
288
|
+
price?: number | undefined;
|
|
289
|
+
}, {
|
|
290
|
+
title: string;
|
|
291
|
+
quantity: number;
|
|
292
|
+
isForSelfUse: boolean;
|
|
293
|
+
price?: number | undefined;
|
|
294
|
+
}>, {
|
|
295
|
+
title: string;
|
|
296
|
+
quantity: number;
|
|
297
|
+
isForSelfUse: boolean;
|
|
298
|
+
price?: number | undefined;
|
|
299
|
+
}, {
|
|
300
|
+
title: string;
|
|
301
|
+
quantity: number;
|
|
302
|
+
isForSelfUse: boolean;
|
|
303
|
+
price?: number | undefined;
|
|
304
|
+
}>;
|
|
305
|
+
export type ShipmentItemFormValues = z.infer<typeof shipmentItemFormSchema>;
|
|
265
306
|
export declare const updateShipmentItemSchema: z.ZodObject<{
|
|
266
307
|
title: z.ZodOptional<z.ZodString>;
|
|
267
308
|
quantity: z.ZodOptional<z.ZodNumber>;
|
|
@@ -103,6 +103,55 @@ const shipmentItemBaseSchema = z.object({
|
|
|
103
103
|
notes: z.string().optional(),
|
|
104
104
|
});
|
|
105
105
|
export const createShipmentItemSchema = shipmentItemBaseSchema.refine((item) => item.isForSelfUse || typeof item.price === "number", { message: "Projected sale price is required unless the item is for self use", path: ["price"] });
|
|
106
|
+
/**
|
|
107
|
+
* The four fields the add-item form actually offers, annotated for
|
|
108
|
+
* `buildSectionsFromSchema`.
|
|
109
|
+
*
|
|
110
|
+
* 🛑 A separate object rather than `shipmentItemBaseSchema.pick(...)`, for two
|
|
111
|
+
* reasons. `.pick()` returns NEW field instances and the annotation registry is
|
|
112
|
+
* a WeakMap keyed by instance, so the meta would not survive. And deriving the
|
|
113
|
+
* form from the full base schema would render `categorySlugs` (an array) and
|
|
114
|
+
* `condition` as controls the add-item row has never had — an array has no
|
|
115
|
+
* `FieldKind`, so it would appear as a disabled placeholder telling the user to
|
|
116
|
+
* supply a renderer.
|
|
117
|
+
*
|
|
118
|
+
* The base schema stays the API contract; this is the form's subset of it.
|
|
119
|
+
*/
|
|
120
|
+
export const shipmentItemFormSchema = z
|
|
121
|
+
.object({
|
|
122
|
+
title: annotate(z.string().min(1, "Item title is required"), {
|
|
123
|
+
section: "item", sectionLabel: "Add item", sectionRequired: true,
|
|
124
|
+
order: 1, row: "pair", label: "Title",
|
|
125
|
+
}),
|
|
126
|
+
quantity: annotate(z.coerce.number().int().min(1, "At least one"), {
|
|
127
|
+
section: "item", order: 2, row: "quarter", kind: "number", label: "Quantity",
|
|
128
|
+
}),
|
|
129
|
+
isForSelfUse: annotate(z.boolean(), {
|
|
130
|
+
section: "item", order: 3, row: "quarter", kind: "toggle",
|
|
131
|
+
label: "For self use (no resale)",
|
|
132
|
+
}),
|
|
133
|
+
/*
|
|
134
|
+
* Hidden — not disabled — when the item is for self use. A self-use item has
|
|
135
|
+
* no projected sale price by definition, and `visibleValues()` then drops
|
|
136
|
+
* the value so a price typed before ticking the box cannot be submitted.
|
|
137
|
+
*/
|
|
138
|
+
price: annotate(z.coerce.number().min(0).optional(), {
|
|
139
|
+
section: "item", order: 4, row: "pair", kind: "number",
|
|
140
|
+
label: "Projected sale price (₹)",
|
|
141
|
+
when: (v) => !v.isForSelfUse,
|
|
142
|
+
}),
|
|
143
|
+
})
|
|
144
|
+
.superRefine((item, ctx) => {
|
|
145
|
+
// Mirrors `createShipmentItemSchema`'s refine. `when` hides the control; it
|
|
146
|
+
// does NOT relax the rule, so the rule has to tolerate the hidden case.
|
|
147
|
+
if (!item.isForSelfUse && typeof item.price !== "number") {
|
|
148
|
+
ctx.addIssue({
|
|
149
|
+
code: z.ZodIssueCode.custom,
|
|
150
|
+
path: ["price"],
|
|
151
|
+
message: "Projected sale price is required unless the item is for self use",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
});
|
|
106
155
|
export const updateShipmentItemSchema = shipmentItemBaseSchema.partial().extend({
|
|
107
156
|
// Link/unlink fields — written by the link route and by the unlink action
|
|
108
157
|
// (PATCH with `null` to clear). Not part of item creation, so kept out of
|
|
@@ -18,6 +18,42 @@ export declare const storeGoogleConfigUpdateSchema: z.ZodObject<{
|
|
|
18
18
|
businessName?: string | undefined;
|
|
19
19
|
}>;
|
|
20
20
|
export type StoreGoogleConfigFormValues = z.infer<typeof storeGoogleConfigUpdateSchema>;
|
|
21
|
+
/**
|
|
22
|
+
* The seller-facing form schema — the same annotated fields, plus the one rule
|
|
23
|
+
* the server does not need.
|
|
24
|
+
*
|
|
25
|
+
* 🛑 `.superRefine()` wraps and returns a NEW schema instance, which is exactly
|
|
26
|
+
* why the annotations live on the FIELDS above rather than being applied here:
|
|
27
|
+
* the registry is a WeakMap keyed by instance, so `annotate()` must be the
|
|
28
|
+
* outermost call on each field, and any wrapper around the whole object is
|
|
29
|
+
* transparent to it. `objectShape()` sees through `.superRefine()` when
|
|
30
|
+
* `buildSectionsFromSchema` resolves the fields.
|
|
31
|
+
*
|
|
32
|
+
* The rule itself is a form-level one: a Place ID is only required once the
|
|
33
|
+
* seller has actually switched the feature on. Requiring it unconditionally
|
|
34
|
+
* would make an untouched, disconnected settings page fail validation on load.
|
|
35
|
+
*/
|
|
36
|
+
export declare const sellerGoogleReviewsFormSchema: z.ZodEffects<z.ZodObject<{
|
|
37
|
+
isConnected: z.ZodOptional<z.ZodBoolean>;
|
|
38
|
+
placeId: z.ZodOptional<z.ZodString>;
|
|
39
|
+
businessName: z.ZodOptional<z.ZodString>;
|
|
40
|
+
}, "strict", z.ZodTypeAny, {
|
|
41
|
+
placeId?: string | undefined;
|
|
42
|
+
isConnected?: boolean | undefined;
|
|
43
|
+
businessName?: string | undefined;
|
|
44
|
+
}, {
|
|
45
|
+
placeId?: string | undefined;
|
|
46
|
+
isConnected?: boolean | undefined;
|
|
47
|
+
businessName?: string | undefined;
|
|
48
|
+
}>, {
|
|
49
|
+
placeId?: string | undefined;
|
|
50
|
+
isConnected?: boolean | undefined;
|
|
51
|
+
businessName?: string | undefined;
|
|
52
|
+
}, {
|
|
53
|
+
placeId?: string | undefined;
|
|
54
|
+
isConnected?: boolean | undefined;
|
|
55
|
+
businessName?: string | undefined;
|
|
56
|
+
}>;
|
|
21
57
|
/**
|
|
22
58
|
* What the seller's own settings page may see.
|
|
23
59
|
*
|
|
@@ -69,6 +69,30 @@ export const storeGoogleConfigUpdateSchema = z
|
|
|
69
69
|
}),
|
|
70
70
|
})
|
|
71
71
|
.strict();
|
|
72
|
+
/**
|
|
73
|
+
* The seller-facing form schema — the same annotated fields, plus the one rule
|
|
74
|
+
* the server does not need.
|
|
75
|
+
*
|
|
76
|
+
* 🛑 `.superRefine()` wraps and returns a NEW schema instance, which is exactly
|
|
77
|
+
* why the annotations live on the FIELDS above rather than being applied here:
|
|
78
|
+
* the registry is a WeakMap keyed by instance, so `annotate()` must be the
|
|
79
|
+
* outermost call on each field, and any wrapper around the whole object is
|
|
80
|
+
* transparent to it. `objectShape()` sees through `.superRefine()` when
|
|
81
|
+
* `buildSectionsFromSchema` resolves the fields.
|
|
82
|
+
*
|
|
83
|
+
* The rule itself is a form-level one: a Place ID is only required once the
|
|
84
|
+
* seller has actually switched the feature on. Requiring it unconditionally
|
|
85
|
+
* would make an untouched, disconnected settings page fail validation on load.
|
|
86
|
+
*/
|
|
87
|
+
export const sellerGoogleReviewsFormSchema = storeGoogleConfigUpdateSchema.superRefine((v, ctx) => {
|
|
88
|
+
if (v.isConnected && !(v.placeId ?? "").trim()) {
|
|
89
|
+
ctx.addIssue({
|
|
90
|
+
code: z.ZodIssueCode.custom,
|
|
91
|
+
path: ["placeId"],
|
|
92
|
+
message: "A Place ID is required to show Google reviews on your store page",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
72
96
|
/**
|
|
73
97
|
* What the seller's own settings page may see.
|
|
74
98
|
*
|