@mohasinac/appkit 4.17.0 → 4.18.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/client.d.ts +5 -0
- package/dist/client.js +5 -0
- package/dist/features/admin/components/AdminAdEditorView.js +4 -2
- package/dist/features/admin/components/AdminAddressEditorView.js +4 -2
- package/dist/features/admin/components/AdminEmployeeEditorView.js +3 -1
- package/dist/features/admin/components/AdminFeatureFlagsView.js +4 -2
- package/dist/features/admin/components/AdminOrderEditorView.js +4 -2
- package/dist/features/admin/components/AdminSiteView.js +4 -2
- package/dist/features/admin/components/AdminStoreEditorView.js +11 -9
- package/dist/features/admin/components/AdminUsersView.js +4 -2
- package/dist/features/admin/schemas/admin-editor-forms.d.ts +119 -0
- package/dist/features/admin/schemas/admin-editor-forms.js +141 -0
- package/dist/features/admin/schemas/admin-ops-forms.d.ts +123 -0
- package/dist/features/admin/schemas/admin-ops-forms.js +147 -0
- package/dist/features/orders/schemas/buyer-forms.d.ts +46 -0
- package/dist/features/orders/schemas/buyer-forms.js +81 -0
- package/dist/features/scams/schemas/report-form.d.ts +63 -0
- package/dist/features/scams/schemas/report-form.js +88 -0
- package/dist/features/store-extensions/schemas/sublisting-category-form.d.ts +15 -0
- package/dist/features/store-extensions/schemas/sublisting-category-form.js +41 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -436,3 +436,8 @@ export { PRODUCT_FIELDS, PRODUCT_STATUS_TRANSITIONS, ORDER_FIELDS, REVIEW_FIELDS
|
|
|
436
436
|
export { CART_LANE, CART_LANE_PRIORITY, CART_LANE_LABELS, laneOf, activeLane, laneItems, laneCounts, isLaneCheckoutable, laneBlockReason, isLockedLane, canAddNewItems, type CartLane, type LaneAssignable, } from "./_internal/shared/checkout/lanes";
|
|
437
437
|
export { newsletterSubscribeSchema, quickCreateTaxonomySchema, payoutMarkPaidSchema, } from "./features/admin/schemas/small-forms";
|
|
438
438
|
export { raffleEntrySchema } from "./features/contact/schemas/inquiry-forms";
|
|
439
|
+
export { orderCancelSchema, paymentProofSchema, } from "./features/orders/schemas/buyer-forms";
|
|
440
|
+
export { scamReportFormSchema } from "./features/scams/schemas/report-form";
|
|
441
|
+
export { sublistingCategoryFormSchema } from "./features/store-extensions/schemas/sublisting-category-form";
|
|
442
|
+
export { adminAddressFormSchema, adminStoreUpdateSchema, announcementBarSchema, } from "./features/admin/schemas/admin-editor-forms";
|
|
443
|
+
export { adminOrderUpdateSchema, employeeInviteSchema, hardBanReasonSchema, featureFlagsUpdateSchema, adminAdFormSchema, } from "./features/admin/schemas/admin-ops-forms";
|
package/dist/client.js
CHANGED
|
@@ -422,3 +422,8 @@ export { PRODUCT_FIELDS, PRODUCT_STATUS_TRANSITIONS, ORDER_FIELDS, REVIEW_FIELDS
|
|
|
422
422
|
export { CART_LANE, CART_LANE_PRIORITY, CART_LANE_LABELS, laneOf, activeLane, laneItems, laneCounts, isLaneCheckoutable, laneBlockReason, isLockedLane, canAddNewItems, } from "./_internal/shared/checkout/lanes";
|
|
423
423
|
export { newsletterSubscribeSchema, quickCreateTaxonomySchema, payoutMarkPaidSchema, } from "./features/admin/schemas/small-forms";
|
|
424
424
|
export { raffleEntrySchema } from "./features/contact/schemas/inquiry-forms";
|
|
425
|
+
export { orderCancelSchema, paymentProofSchema, } from "./features/orders/schemas/buyer-forms";
|
|
426
|
+
export { scamReportFormSchema } from "./features/scams/schemas/report-form";
|
|
427
|
+
export { sublistingCategoryFormSchema } from "./features/store-extensions/schemas/sublisting-category-form";
|
|
428
|
+
export { adminAddressFormSchema, adminStoreUpdateSchema, announcementBarSchema, } from "./features/admin/schemas/admin-editor-forms";
|
|
429
|
+
export { adminOrderUpdateSchema, employeeInviteSchema, hardBanReasonSchema, featureFlagsUpdateSchema, adminAdFormSchema, } from "./features/admin/schemas/admin-ops-forms";
|
|
@@ -7,6 +7,8 @@ import { useQuery } from "@tanstack/react-query";
|
|
|
7
7
|
import { Alert, Button, Checkbox, Div, Form, FormActions, Input, Select, Span, Stack, StackedViewShell, Text, Toggle, useToast } from "../../../ui";
|
|
8
8
|
import { apiClient } from "../../../http";
|
|
9
9
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
10
|
+
import { adminAdFormSchema } from "../schemas/admin-ops-forms";
|
|
11
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
10
12
|
const __P = {
|
|
11
13
|
p3: "p-[var(--appkit-space-3)]",
|
|
12
14
|
};
|
|
@@ -223,14 +225,14 @@ export function AdminAdEditorView({ adId, endpointBase = ADMIN_ENDPOINTS.ADS, la
|
|
|
223
225
|
return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: labels.title ?? (adId ? "Edit Ad" : "Create Ad"), sections: [
|
|
224
226
|
metaQuery.error ? (_jsx(Alert, { variant: "error", title: "Could not load ad metadata", children: metaQuery.error instanceof Error ? metaQuery.error.message : "Unknown error" })) : null,
|
|
225
227
|
adQuery.error ? (_jsx(Alert, { variant: "error", title: "Could not load ad", children: adQuery.error instanceof Error ? adQuery.error.message : "Unknown error" })) : null,
|
|
226
|
-
_jsxs(Form, { onSubmit: (event) => {
|
|
228
|
+
_jsxs(Form, { schema: adminAdFormSchema, onSubmit: (event) => {
|
|
227
229
|
event.preventDefault();
|
|
228
230
|
if (blockedByPublishValidation) {
|
|
229
231
|
showToast("Cannot save in publish-ready status until validation issues are fixed.", "warning");
|
|
230
232
|
return;
|
|
231
233
|
}
|
|
232
234
|
saveMutation.mutate();
|
|
233
|
-
}, spacing: "md", children: [_jsx(Input, { label: "Ad name", value: name, onChange: (event) => setName(event.target.value), placeholder: "Summer Campaign" }), _jsxs(Div, { layout: "grid", gap: "3", className: "grid-cols-1 md:grid-cols-3", children: [_jsx(Select, { label: "Provider", value: provider, options: [
|
|
235
|
+
}, spacing: "md", children: [_jsx(FormErrorSummary, {}), _jsx(Input, { label: "Ad name", value: name, onChange: (event) => setName(event.target.value), placeholder: "Summer Campaign" }), _jsxs(Div, { layout: "grid", gap: "3", className: "grid-cols-1 md:grid-cols-3", children: [_jsx(Select, { label: "Provider", value: provider, options: [
|
|
234
236
|
{ label: "Manual", value: "manual" },
|
|
235
237
|
{ label: "AdSense", value: "adsense" },
|
|
236
238
|
{ label: "Third Party", value: "thirdParty" },
|
|
@@ -9,6 +9,8 @@ import { apiClient } from "../../../http";
|
|
|
9
9
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
10
10
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
11
11
|
import { INDIAN_STATES } from "../../account/constants/addresses";
|
|
12
|
+
import { adminAddressFormSchema } from "../schemas/admin-editor-forms";
|
|
13
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
12
14
|
const STATE_OPTIONS = INDIAN_STATES.map((s) => ({ value: s, label: s }));
|
|
13
15
|
// ── Section heading helper ─────────────────────────────────────────────────────
|
|
14
16
|
function SectionHeading({ children }) {
|
|
@@ -105,10 +107,10 @@ export function AdminAddressEditorView({ addressId, onSaved, onDeleted, ...rest
|
|
|
105
107
|
// ── Action sidebar (desktop) ───────────────────────────────────────────────
|
|
106
108
|
const actionSidebar = (_jsxs(Card, { variant: "outlined", padding: "md", spacing: "sm", children: [_jsx(Button, { type: "submit", form: "address-editor-form", className: "w-full", isLoading: isSubmitting, disabled: !canSave || isSubmitting, children: isEdit ? ACTIONS.ADMIN["save-changes"].label : "Create address" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", className: "w-full", isLoading: deleteMutation.isPending, onClick: () => setDeleteOpen(true), children: "Delete address" }))] }));
|
|
107
109
|
// ── Form sections ──────────────────────────────────────────────────────────
|
|
108
|
-
const formContent = (_jsxs(Form, { id: "address-editor-form", onSubmit: (e) => {
|
|
110
|
+
const formContent = (_jsxs(Form, { schema: adminAddressFormSchema, id: "address-editor-form", onSubmit: (e) => {
|
|
109
111
|
e.preventDefault();
|
|
110
112
|
saveMutation.mutate();
|
|
111
|
-
}, spacing: "lg", children: [_jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(SectionHeading, { children: "Ownership" }), _jsxs(Stack, { gap: "md", children: [_jsx(Row, { gap: "lg", children: ["user", "store"].map((type) => (_jsxs("label", { className: "flex cursor-pointer items-center gap-[var(--appkit-space-2)]", children: [_jsx(Checkbox, { bare: true, type: "radio", name: "ownerType", value: type, checked: form.ownerType === type, onChange: () => {
|
|
113
|
+
}, spacing: "lg", children: [_jsx(FormErrorSummary, {}), _jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(SectionHeading, { children: "Ownership" }), _jsxs(Stack, { gap: "md", children: [_jsx(Row, { gap: "lg", children: ["user", "store"].map((type) => (_jsxs("label", { className: "flex cursor-pointer items-center gap-[var(--appkit-space-2)]", children: [_jsx(Checkbox, { bare: true, type: "radio", name: "ownerType", value: type, checked: form.ownerType === type, onChange: () => {
|
|
112
114
|
set("ownerType", type);
|
|
113
115
|
set("ownerId", "");
|
|
114
116
|
}, className: "accent-[var(--appkit-color-primary)]" }), _jsx(Span, { size: "sm", className: "text-[var(--appkit-color-text)]", transform: "capitalize", children: type })] }, type))) }), _jsx(Input, { label: form.ownerType === "user" ? "User ID" : "Store ID (slug)", value: form.ownerId, onChange: (e) => set("ownerId", e.target.value), required: true, placeholder: form.ownerType === "user" ? "user-mohsin-c" : "store-pokemon-palace", helperText: form.ownerType === "user"
|
|
@@ -7,6 +7,8 @@ import { Button, Checkbox, ConfirmDeleteModal, Details, Div, Form, FormActions,
|
|
|
7
7
|
import { apiClient } from "../../../http";
|
|
8
8
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
9
9
|
import { PERMISSION_GROUPS, PERMISSION_DOMAINS, getPermissionsForDomain, formatPermLabel, } from "../../auth/permissions/constants";
|
|
10
|
+
import { employeeInviteSchema } from "../schemas/admin-ops-forms";
|
|
11
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
10
12
|
const __O = {
|
|
11
13
|
yAuto: "overflow-y-auto",
|
|
12
14
|
};
|
|
@@ -130,5 +132,5 @@ export function AdminEmployeeEditorView({ open, onClose, mode, userId, displayNa
|
|
|
130
132
|
else
|
|
131
133
|
updateMutation.mutate();
|
|
132
134
|
};
|
|
133
|
-
return (_jsxs(_Fragment, { children: [_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: mode === "invite" ? "Invite Employee" : `Edit — ${displayName ?? "Employee"}`, children: _jsxs(Form, { onSubmit: handleSubmit, spacing: "lg", padding: "md", children: [mode === "invite" && (_jsxs(Stack, { gap: "xs", className: "", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Email address" }), _jsx(Input, { type: "email", required: true, value: email, onChange: (e) => setEmail(e.target.value), placeholder: "employee@example.com" })] })), _jsxs(Stack, { gap: "xs", className: "", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Permission group" }), _jsx(Select, { value: group, onChange: (e) => applyGroupPreset(e.target.value), options: GROUP_OPTIONS }), _jsx(Text, { size: "xs", color: "muted", children: "Selecting a group auto-fills the permissions below. You can still customise individual permissions." })] }), _jsx(PermissionDomainsPanel, { permissions: permissions, togglePerm: togglePerm }), _jsxs(FormActions, { children: [_jsx(Button, { type: "submit", variant: "primary", disabled: isBusy, isLoading: isBusy, children: mode === "invite" ? "Send Invite" : "Save" }), mode === "edit" && userId && (_jsx(Button, { type: "button", variant: "danger", disabled: isBusy, onClick: () => setRevokeOpen(true), children: "Revoke Access" }))] })] }) }), mode === "edit" && (_jsx(ConfirmDeleteModal, { isOpen: revokeOpen, onClose: () => setRevokeOpen(false), onConfirm: () => revokeMutation.mutate(), isDeleting: revokeMutation.isPending, title: "Revoke employee access?", message: `${displayName ?? "This employee"} will lose admin panel access immediately. Their user account remains active — only their role is reset to user.`, confirmText: "Revoke Access", variant: "danger" }))] }));
|
|
135
|
+
return (_jsxs(_Fragment, { children: [_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: mode === "invite" ? "Invite Employee" : `Edit — ${displayName ?? "Employee"}`, children: _jsxs(Form, { schema: employeeInviteSchema, onSubmit: handleSubmit, spacing: "lg", padding: "md", children: [_jsx(FormErrorSummary, {}), mode === "invite" && (_jsxs(Stack, { gap: "xs", className: "", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Email address" }), _jsx(Input, { type: "email", required: true, value: email, onChange: (e) => setEmail(e.target.value), placeholder: "employee@example.com" })] })), _jsxs(Stack, { gap: "xs", className: "", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Permission group" }), _jsx(Select, { value: group, onChange: (e) => applyGroupPreset(e.target.value), options: GROUP_OPTIONS }), _jsx(Text, { size: "xs", color: "muted", children: "Selecting a group auto-fills the permissions below. You can still customise individual permissions." })] }), _jsx(PermissionDomainsPanel, { permissions: permissions, togglePerm: togglePerm }), _jsxs(FormActions, { children: [_jsx(Button, { type: "submit", variant: "primary", disabled: isBusy, isLoading: isBusy, children: mode === "invite" ? "Send Invite" : "Save" }), mode === "edit" && userId && (_jsx(Button, { type: "button", variant: "danger", disabled: isBusy, onClick: () => setRevokeOpen(true), children: "Revoke Access" }))] })] }) }), mode === "edit" && (_jsx(ConfirmDeleteModal, { isOpen: revokeOpen, onClose: () => setRevokeOpen(false), onConfirm: () => revokeMutation.mutate(), isDeleting: revokeMutation.isPending, title: "Revoke employee access?", message: `${displayName ?? "This employee"} will lose admin panel access immediately. Their user account remains active — only their role is reset to user.`, confirmText: "Revoke Access", variant: "danger" }))] }));
|
|
134
136
|
}
|
|
@@ -6,6 +6,8 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
6
6
|
import { Alert, Button, Div, Form, FormActions, Input, Row, Stack, StackedViewShell, Text, Toggle, useToast } from "../../../ui";
|
|
7
7
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
8
|
import { apiClient } from "../../../http";
|
|
9
|
+
import { featureFlagsUpdateSchema } from "../schemas/admin-ops-forms";
|
|
10
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
9
11
|
const __O = {
|
|
10
12
|
hidden: "overflow-hidden",
|
|
11
13
|
};
|
|
@@ -96,10 +98,10 @@ export function AdminFeatureFlagsView({ labels = {}, renderFlags, ...rest }) {
|
|
|
96
98
|
showToast("Failed to save feature flags.", "error");
|
|
97
99
|
},
|
|
98
100
|
});
|
|
99
|
-
const defaultFlags = () => (_jsxs(Form, { onSubmit: (event) => {
|
|
101
|
+
const defaultFlags = () => (_jsxs(Form, { schema: featureFlagsUpdateSchema, onSubmit: (event) => {
|
|
100
102
|
event.preventDefault();
|
|
101
103
|
saveFlags.mutate();
|
|
102
|
-
}, spacing: "md", children: [_jsx(AccordionSection, { title: "Platform Features", children: PLATFORM_FLAGS.map(({ key, label, desc }) => (_jsx(FlagRow, { label: label, desc: desc, checked: getPlatformFlag(key), onChange: (val) => setPlatformFlag(key, val), rollout: rollouts[key], onRolloutChange: (val) => setRollouts((prev) => ({ ...prev, [key]: val })), showRollout: true }, key))) }), _jsx(AccordionSection, { title: "Listing Types", children: LISTING_TYPE_FLAGS.map(({ key, label, desc }) => (_jsx(FlagRow, { label: label, desc: desc, checked: getListingTypeFlag(key), onChange: (val) => setListingTypeFlag(key, val), showRollout: false }, key))) }), _jsx(AccordionSection, { title: "Category Types", children: CATEGORY_TYPE_FLAGS.map(({ key, label, desc }) => (_jsx(FlagRow, { label: label, desc: desc, checked: getCategoryTypeFlag(key), onChange: (val) => setCategoryTypeFlag(key, val), showRollout: false }, key))) }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "secondary", onClick: () => {
|
|
104
|
+
}, spacing: "md", children: [_jsx(FormErrorSummary, {}), _jsx(AccordionSection, { title: "Platform Features", children: PLATFORM_FLAGS.map(({ key, label, desc }) => (_jsx(FlagRow, { label: label, desc: desc, checked: getPlatformFlag(key), onChange: (val) => setPlatformFlag(key, val), rollout: rollouts[key], onRolloutChange: (val) => setRollouts((prev) => ({ ...prev, [key]: val })), showRollout: true }, key))) }), _jsx(AccordionSection, { title: "Listing Types", children: LISTING_TYPE_FLAGS.map(({ key, label, desc }) => (_jsx(FlagRow, { label: label, desc: desc, checked: getListingTypeFlag(key), onChange: (val) => setListingTypeFlag(key, val), showRollout: false }, key))) }), _jsx(AccordionSection, { title: "Category Types", children: CATEGORY_TYPE_FLAGS.map(({ key, label, desc }) => (_jsx(FlagRow, { label: label, desc: desc, checked: getCategoryTypeFlag(key), onChange: (val) => setCategoryTypeFlag(key, val), showRollout: false }, key))) }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "secondary", onClick: () => {
|
|
103
105
|
setFlags(data?.flags ?? {});
|
|
104
106
|
setRollouts(data?.rollouts ?? {});
|
|
105
107
|
}, disabled: saveFlags.isPending, children: "Reset" }), _jsx(Button, { type: "submit", isLoading: saveFlags.isPending, disabled: saveFlags.isPending, children: "Save All Flags" })] })] }));
|
|
@@ -12,6 +12,8 @@ import { apiClient } from "../../../http";
|
|
|
12
12
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
13
13
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
14
14
|
import { toCurrency } from "../hooks/useAdminListingData";
|
|
15
|
+
import { adminOrderUpdateSchema } from "../schemas/admin-ops-forms";
|
|
16
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
15
17
|
const STATUS_OPTIONS = [
|
|
16
18
|
{ label: "Pending", value: "pending" },
|
|
17
19
|
{ label: "Processing", value: "processing" },
|
|
@@ -146,8 +148,8 @@ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, curre
|
|
|
146
148
|
setIsRejectingFraud(false);
|
|
147
149
|
}
|
|
148
150
|
};
|
|
149
|
-
return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: orderLabel ? `Order: ${orderLabel}` : "Update Order", children: _jsxs(Form, { onSubmit: (e) => {
|
|
151
|
+
return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: orderLabel ? `Order: ${orderLabel}` : "Update Order", children: _jsxs(Form, { schema: adminOrderUpdateSchema, onSubmit: (e) => {
|
|
150
152
|
e.preventDefault();
|
|
151
153
|
saveMutation.mutate();
|
|
152
|
-
}, spacing: "md", padding: "md", children: [addons && _jsx(OrderAddonBadges, { order: addons, variant: "detail" }), items && items.length > 0 && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Label, { size: "sm", weight: "medium", color: "primary", children: ["Items (", items.length, ")"] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: items.map((item, i) => (_jsxs(Div, { layout: "flex", align: "center", gap: "3", padding: "sm", children: [_jsx(Div, { className: "h-10 w-10 shrink-0", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: item.image, alt: item.title, size: "thumbnail" }) }), _jsxs(Div, { className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", className: "truncate", weight: "medium", children: item.title }), _jsxs(Text, { size: "xs", color: "muted", children: ["Qty: ", item.quantity, " \u00D7 ", toCurrency(item.unitPrice)] })] }), _jsx(Text, { size: "sm", className: "shrink-0", weight: "semibold", children: toCurrency(item.totalPrice) })] }, item.productId || i))) })] })), _jsx(Select, { label: "Order status", options: STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsx(Input, { label: "Tracking number (optional)", value: trackingNumber, onChange: (e) => setTrackingNumber(e.target.value), placeholder: "e.g. DEL1234567890IN" }), _jsx(Select, { label: "Carrier (optional)", options: CARRIER_OPTIONS, value: carrier, onValueChange: setCarrier }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Internal note (optional)" }), _jsx(Textarea, { value: notes, onChange: (e) => setNotes(e.target.value), rows: 3, placeholder: "Reason for status change, escalation notes\u2026" })] }), (status === "refunded" || status === "return_requested") && (_jsx(Input, { label: "Refund amount \u20B9 (optional)", type: "number", min: "0", step: "0.01", value: refundAmount, onChange: (e) => setRefundAmount(e.target.value), placeholder: "e.g. 499.00" })), isManualPayment && (_jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Payment Proof" }), isRejected && (_jsx(Text, { size: "xs", className: "text-error", weight: "medium", children: "Payment rejected as fraud \u2014 no further action available here." })), isReuploadRequested && !isVerified && (_jsx(Text, { size: "xs", className: "text-warning", weight: "medium", children: "Re-upload requested \u2014 waiting for the buyer to submit a new proof." })), paymentProofUrl ? (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { border: "default", rounded: "lg", overflow: "hidden", children: _jsx(MediaImage, { src: paymentProofUrl, alt: "Payment screenshot", size: "card" }) }), paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", children: ["UTR: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: paymentTransactionId })] })), needsVerification && (_jsxs(Stack, { gap: "xs", children: [(displayedUpiId || buyerReportedUpiId) && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "xs", color: "muted", children: ["Expected UPI: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: displayedUpiId || "—" }), " · ", "Buyer reported: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerReportedUpiId || "—" })] }), paymentUpiMismatch && (_jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-error/20", surface: "danger-surface", children: _jsx(Text, { size: "xs", className: "text-error", weight: "semibold", children: "UPI mismatch \u2014 buyer-reported ID doesn't match the one shown for this order." }) }))] })), _jsxs(Text, { size: "xs", color: "muted", children: ["Buyer marked as paid: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerMarkedPaid ? "Yes" : "No" }), " · ", "Fraud agreement accepted: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerFraudAgreementAccepted ? "Yes" : "No" })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Review note (required for re-upload / reject)" }), _jsx(Textarea, { value: reviewNote, onChange: (e) => setReviewNote(e.target.value), rows: 2, placeholder: "e.g. Screenshot is blurry \u2014 amount not readable / UPI ID doesn't match order" })] }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["verify-payment"], onClick: handleVerifyPayment, isLoading: isVerifyingPayment, disabled: isVerifyingPayment, variant: "primary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["request-payment-reupload"], onClick: handleRequestReupload, isLoading: isRequestingReupload, disabled: isRequestingReupload || !reviewNote.trim(), variant: "secondary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["reject-payment-fraud"], onClick: handleRejectFraud, isLoading: isRejectingFraud, disabled: isRejectingFraud || !reviewNote.trim(), variant: "danger", className: "w-full" })] })), isVerified && (_jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-success/20", surface: "success-surface", children: _jsx(Text, { size: "xs", className: "text-success", weight: "medium", children: "Payment verified" }) }))] })) : (_jsx(Text, { size: "xs", color: "faint", children: "No proof uploaded yet." }))] })), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", isLoading: saveMutation.isPending, disabled: !orderId || saveMutation.isPending, children: "Save changes" })] })] }) }));
|
|
154
|
+
}, spacing: "md", padding: "md", children: [_jsx(FormErrorSummary, {}), addons && _jsx(OrderAddonBadges, { order: addons, variant: "detail" }), items && items.length > 0 && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Label, { size: "sm", weight: "medium", color: "primary", children: ["Items (", items.length, ")"] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: items.map((item, i) => (_jsxs(Div, { layout: "flex", align: "center", gap: "3", padding: "sm", children: [_jsx(Div, { className: "h-10 w-10 shrink-0", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: item.image, alt: item.title, size: "thumbnail" }) }), _jsxs(Div, { className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", className: "truncate", weight: "medium", children: item.title }), _jsxs(Text, { size: "xs", color: "muted", children: ["Qty: ", item.quantity, " \u00D7 ", toCurrency(item.unitPrice)] })] }), _jsx(Text, { size: "sm", className: "shrink-0", weight: "semibold", children: toCurrency(item.totalPrice) })] }, item.productId || i))) })] })), _jsx(Select, { label: "Order status", options: STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsx(Input, { label: "Tracking number (optional)", value: trackingNumber, onChange: (e) => setTrackingNumber(e.target.value), placeholder: "e.g. DEL1234567890IN" }), _jsx(Select, { label: "Carrier (optional)", options: CARRIER_OPTIONS, value: carrier, onValueChange: setCarrier }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Internal note (optional)" }), _jsx(Textarea, { value: notes, onChange: (e) => setNotes(e.target.value), rows: 3, placeholder: "Reason for status change, escalation notes\u2026" })] }), (status === "refunded" || status === "return_requested") && (_jsx(Input, { label: "Refund amount \u20B9 (optional)", type: "number", min: "0", step: "0.01", value: refundAmount, onChange: (e) => setRefundAmount(e.target.value), placeholder: "e.g. 499.00" })), isManualPayment && (_jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Payment Proof" }), isRejected && (_jsx(Text, { size: "xs", className: "text-error", weight: "medium", children: "Payment rejected as fraud \u2014 no further action available here." })), isReuploadRequested && !isVerified && (_jsx(Text, { size: "xs", className: "text-warning", weight: "medium", children: "Re-upload requested \u2014 waiting for the buyer to submit a new proof." })), paymentProofUrl ? (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { border: "default", rounded: "lg", overflow: "hidden", children: _jsx(MediaImage, { src: paymentProofUrl, alt: "Payment screenshot", size: "card" }) }), paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", children: ["UTR: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: paymentTransactionId })] })), needsVerification && (_jsxs(Stack, { gap: "xs", children: [(displayedUpiId || buyerReportedUpiId) && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "xs", color: "muted", children: ["Expected UPI: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: displayedUpiId || "—" }), " · ", "Buyer reported: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerReportedUpiId || "—" })] }), paymentUpiMismatch && (_jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-error/20", surface: "danger-surface", children: _jsx(Text, { size: "xs", className: "text-error", weight: "semibold", children: "UPI mismatch \u2014 buyer-reported ID doesn't match the one shown for this order." }) }))] })), _jsxs(Text, { size: "xs", color: "muted", children: ["Buyer marked as paid: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerMarkedPaid ? "Yes" : "No" }), " · ", "Fraud agreement accepted: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerFraudAgreementAccepted ? "Yes" : "No" })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Review note (required for re-upload / reject)" }), _jsx(Textarea, { value: reviewNote, onChange: (e) => setReviewNote(e.target.value), rows: 2, placeholder: "e.g. Screenshot is blurry \u2014 amount not readable / UPI ID doesn't match order" })] }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["verify-payment"], onClick: handleVerifyPayment, isLoading: isVerifyingPayment, disabled: isVerifyingPayment, variant: "primary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["request-payment-reupload"], onClick: handleRequestReupload, isLoading: isRequestingReupload, disabled: isRequestingReupload || !reviewNote.trim(), variant: "secondary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["reject-payment-fraud"], onClick: handleRejectFraud, isLoading: isRejectingFraud, disabled: isRejectingFraud || !reviewNote.trim(), variant: "danger", className: "w-full" })] })), isVerified && (_jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-success/20", surface: "success-surface", children: _jsx(Text, { size: "xs", className: "text-success", weight: "medium", children: "Payment verified" }) }))] })) : (_jsx(Text, { size: "xs", color: "faint", children: "No proof uploaded yet." }))] })), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", isLoading: saveMutation.isPending, disabled: !orderId || saveMutation.isPending, children: "Save changes" })] })] }) }));
|
|
153
155
|
}
|
|
@@ -7,6 +7,8 @@ import { Alert, Button, Form, FormActions, Input, StackedViewShell, Toggle, } fr
|
|
|
7
7
|
import { useSiteSettings } from "../../../core/hooks/useSiteSettings";
|
|
8
8
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
9
9
|
import { apiClient } from "../../../http";
|
|
10
|
+
import { announcementBarSchema } from "../schemas/admin-editor-forms";
|
|
11
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
10
12
|
export function AdminSiteView({ labels = {}, renderTabs, renderForm, ...rest }) {
|
|
11
13
|
const queryClient = useQueryClient();
|
|
12
14
|
const { data, isLoading, error } = useSiteSettings();
|
|
@@ -40,10 +42,10 @@ export function AdminSiteView({ labels = {}, renderTabs, renderForm, ...rest })
|
|
|
40
42
|
setSaveMessage("Failed to save announcement settings.");
|
|
41
43
|
},
|
|
42
44
|
});
|
|
43
|
-
const defaultForm = () => (_jsxs(Form, { onSubmit: (event) => {
|
|
45
|
+
const defaultForm = () => (_jsxs(Form, { schema: announcementBarSchema, onSubmit: (event) => {
|
|
44
46
|
event.preventDefault();
|
|
45
47
|
saveAnnouncement.mutate();
|
|
46
|
-
}, spacing: "md", children: [_jsx(Toggle, { checked: announcementEnabled, onChange: setAnnouncementEnabled, label: "Show announcement bar on homepage" }), _jsx(Input, { label: "Announcement message", value: announcementMessage, onChange: (event) => setAnnouncementMessage(event.target.value), placeholder: "Enter homepage announcement", disabled: !announcementEnabled, helperText: "This message is shown at the top of the homepage when enabled." }), _jsx(FormActions, { align: "right", children: _jsx(Button, { type: "submit", disabled: saveAnnouncement.isPending || (announcementEnabled && !announcementMessage.trim()), children: saveAnnouncement.isPending ? "Saving..." : "Save announcement" }) }), saveMessage ? (_jsx(Alert, { variant: saveMessage.startsWith("Failed") ? "error" : "success", title: saveMessage.startsWith("Failed") ? "Save failed" : "Saved", children: saveMessage })) : null] }));
|
|
48
|
+
}, spacing: "md", children: [_jsx(FormErrorSummary, {}), _jsx(Toggle, { checked: announcementEnabled, onChange: setAnnouncementEnabled, label: "Show announcement bar on homepage" }), _jsx(Input, { label: "Announcement message", value: announcementMessage, onChange: (event) => setAnnouncementMessage(event.target.value), placeholder: "Enter homepage announcement", disabled: !announcementEnabled, helperText: "This message is shown at the top of the homepage when enabled." }), _jsx(FormActions, { align: "right", children: _jsx(Button, { type: "submit", disabled: saveAnnouncement.isPending || (announcementEnabled && !announcementMessage.trim()), children: saveAnnouncement.isPending ? "Saving..." : "Save announcement" }) }), saveMessage ? (_jsx(Alert, { variant: saveMessage.startsWith("Failed") ? "error" : "success", title: saveMessage.startsWith("Failed") ? "Save failed" : "Saved", children: saveMessage })) : null] }));
|
|
47
49
|
const loadingAlert = isLoading ? (_jsx(Alert, { variant: "info", title: "Loading settings", children: "Fetching site settings..." })) : null;
|
|
48
50
|
const errorAlert = error ? (_jsx(Alert, { variant: "error", title: "Could not load settings", children: error instanceof Error ? error.message : "Unknown error" })) : null;
|
|
49
51
|
return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: labels.title ?? "Site Settings", sections: [renderTabs?.(), loadingAlert, errorAlert, renderForm?.() ?? defaultForm()] }));
|
|
@@ -6,6 +6,8 @@ import { useQueryClient } from "@tanstack/react-query";
|
|
|
6
6
|
import { Button, Checkbox, Details, Div, Form, FormActions, Label, Select, SideDrawer, Span, Stack, StackedViewShell, Summary, Textarea, Toggle, useToast } from "../../../ui";
|
|
7
7
|
import { apiClient } from "../../../http";
|
|
8
8
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
9
|
+
import { adminStoreUpdateSchema } from "../schemas/admin-editor-forms";
|
|
10
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
9
11
|
const CAPABILITY_GROUPS = [
|
|
10
12
|
{
|
|
11
13
|
label: "Listings",
|
|
@@ -106,15 +108,15 @@ export function AdminStoreEditorView({ open, onClose, storeId, storeName, curren
|
|
|
106
108
|
return (_jsxs(Details, { className: "group", children: [_jsxs(Summary, { paddingX: "x-sm", paddingY: "y-xs", weight: "semibold", color: "muted", layout: "flex", align: "center", justify: "between", className: "uppercase tracking-wide hover:bg-[var(--appkit-color-bg)] transition-colors", children: [_jsx(Span, { size: "xs", children: group.label }), _jsxs(Span, { size: "xs", weight: "normal", className: "normal-case", color: "faint", children: [checked, "/", group.caps.length] })] }), _jsx(Div, { layout: "grid", paddingY: "y-xs-tall", className: "grid-cols-2 gap-x-2 gap-y-1.5", surface: "muted", padding: "x-sm", children: group.caps.map((cap) => (_jsxs("label", { className: "flex items-center gap-[var(--appkit-space-2)] cursor-pointer text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]", children: [_jsx(Checkbox, { bare: true, checked: capabilities.has(cap.key), onChange: () => toggleCapability(cap.key), className: "h-3.5 w-3.5 rounded border-[var(--appkit-color-border)] accent-primary" }), cap.label] }, cap.key))) })] }, group.label));
|
|
107
109
|
}) })] }));
|
|
108
110
|
const renderActionsSection = () => (_jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", isLoading: saveMutation.isPending, disabled: !storeId || saveMutation.isPending, children: "Save changes" })] }));
|
|
109
|
-
return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: storeName ? `Manage: ${storeName}` : "Manage Store", children:
|
|
111
|
+
return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: storeName ? `Manage: ${storeName}` : "Manage Store", children: _jsxs(Form, { schema: adminStoreUpdateSchema, onSubmit: (e) => {
|
|
110
112
|
e.preventDefault();
|
|
111
113
|
saveMutation.mutate();
|
|
112
|
-
}, padding: "md", children: _jsx(StackedViewShell, { portal: "admin", className: "space-y-4", sections: [
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
114
|
+
}, padding: "md", children: [_jsx(FormErrorSummary, {}), _jsx(StackedViewShell, { portal: "admin", className: "space-y-4", sections: [
|
|
115
|
+
renderStatusSection,
|
|
116
|
+
renderNotesSection,
|
|
117
|
+
renderTogglesSection,
|
|
118
|
+
renderSuspensionSection,
|
|
119
|
+
renderCapabilitiesSection,
|
|
120
|
+
renderActionsSection,
|
|
121
|
+
] })] }) }));
|
|
120
122
|
}
|
|
@@ -18,6 +18,8 @@ import { useBulkAction } from "../../../react";
|
|
|
18
18
|
import { useBulkEvent } from "../../events/hooks/useBulkEvent";
|
|
19
19
|
import { RTDB_PATHS } from "../../../providers/db-firebase/rtdb-paths";
|
|
20
20
|
import { RealtimeEventStatus } from "../../../react/hooks/useRealtimeEvent";
|
|
21
|
+
import { hardBanReasonSchema } from "../schemas/admin-ops-forms";
|
|
22
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
21
23
|
export function AdminUsersView({ children, ...props }) {
|
|
22
24
|
const toast = useToast();
|
|
23
25
|
const queryClient = useQueryClient();
|
|
@@ -247,10 +249,10 @@ export function AdminUsersView({ children, ...props }) {
|
|
|
247
249
|
setBanModalOpen(false);
|
|
248
250
|
setBanTargetId(null);
|
|
249
251
|
setBanReason("");
|
|
250
|
-
}, title: ACTIONS.ADMIN["ban-user"].confirmation.title, children: [_jsx(AppText, { size: "sm", color: "muted", className: "mb-4", children: ACTIONS.ADMIN["ban-user"].confirmation.body }), _jsxs(Form, { onSubmit: (e) => {
|
|
252
|
+
}, title: ACTIONS.ADMIN["ban-user"].confirmation.title, children: [_jsx(AppText, { size: "sm", color: "muted", className: "mb-4", children: ACTIONS.ADMIN["ban-user"].confirmation.body }), _jsxs(Form, { schema: hardBanReasonSchema, onSubmit: (e) => {
|
|
251
253
|
e.preventDefault();
|
|
252
254
|
banUser.mutate();
|
|
253
|
-
}, children: [_jsx(Input, { label: "Reason", value: banReason, onChange: (e) => setBanReason(e.target.value), placeholder: "e.g. Repeated fraud, scam activity\u2026", required: true }), _jsxs(FormActions, { children: [_jsx(Button, { type: "button", variant: "secondary", onClick: () => {
|
|
255
|
+
}, children: [_jsx(FormErrorSummary, {}), _jsx(Input, { label: "Reason", value: banReason, onChange: (e) => setBanReason(e.target.value), placeholder: "e.g. Repeated fraud, scam activity\u2026", required: true }), _jsxs(FormActions, { children: [_jsx(Button, { type: "button", variant: "secondary", onClick: () => {
|
|
254
256
|
setBanModalOpen(false);
|
|
255
257
|
setBanTargetId(null);
|
|
256
258
|
setBanReason("");
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* An address written by an admin on someone's behalf.
|
|
4
|
+
*
|
|
5
|
+
* Shapes match `AddressDocument` (`addressLine1`, not `line1`) — deliberately
|
|
6
|
+
* NOT `userAddressSchema`, which is the account-side view of the same data
|
|
7
|
+
* under different field names. Reusing that one here would have meant
|
|
8
|
+
* translating on every write.
|
|
9
|
+
*/
|
|
10
|
+
export declare const adminAddressFormSchema: z.ZodObject<{
|
|
11
|
+
ownerType: z.ZodEnum<["user", "store"]>;
|
|
12
|
+
ownerId: z.ZodString;
|
|
13
|
+
label: z.ZodOptional<z.ZodString>;
|
|
14
|
+
fullName: z.ZodString;
|
|
15
|
+
phone: z.ZodString;
|
|
16
|
+
addressLine1: z.ZodString;
|
|
17
|
+
addressLine2: z.ZodOptional<z.ZodString>;
|
|
18
|
+
landmark: z.ZodOptional<z.ZodString>;
|
|
19
|
+
city: z.ZodString;
|
|
20
|
+
state: z.ZodString;
|
|
21
|
+
postalCode: z.ZodString;
|
|
22
|
+
country: z.ZodString;
|
|
23
|
+
isDefault: z.ZodOptional<z.ZodBoolean>;
|
|
24
|
+
}, "strip", z.ZodTypeAny, {
|
|
25
|
+
country: string;
|
|
26
|
+
ownerId: string;
|
|
27
|
+
ownerType: "user" | "store";
|
|
28
|
+
fullName: string;
|
|
29
|
+
phone: string;
|
|
30
|
+
addressLine1: string;
|
|
31
|
+
city: string;
|
|
32
|
+
state: string;
|
|
33
|
+
postalCode: string;
|
|
34
|
+
label?: string | undefined;
|
|
35
|
+
isDefault?: boolean | undefined;
|
|
36
|
+
addressLine2?: string | undefined;
|
|
37
|
+
landmark?: string | undefined;
|
|
38
|
+
}, {
|
|
39
|
+
country: string;
|
|
40
|
+
ownerId: string;
|
|
41
|
+
ownerType: "user" | "store";
|
|
42
|
+
fullName: string;
|
|
43
|
+
phone: string;
|
|
44
|
+
addressLine1: string;
|
|
45
|
+
city: string;
|
|
46
|
+
state: string;
|
|
47
|
+
postalCode: string;
|
|
48
|
+
label?: string | undefined;
|
|
49
|
+
isDefault?: boolean | undefined;
|
|
50
|
+
addressLine2?: string | undefined;
|
|
51
|
+
landmark?: string | undefined;
|
|
52
|
+
}>;
|
|
53
|
+
export type AdminAddressFormValues = z.infer<typeof adminAddressFormSchema>;
|
|
54
|
+
/**
|
|
55
|
+
* An admin changing a store's standing.
|
|
56
|
+
*
|
|
57
|
+
* `suspensionReason` is required when — and only when — the status is
|
|
58
|
+
* `suspended`: suspending a seller without a recorded reason leaves them, and
|
|
59
|
+
* the next admin, with nothing to appeal or review.
|
|
60
|
+
*/
|
|
61
|
+
export declare const adminStoreUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
62
|
+
storeStatus: z.ZodEnum<["pending", "active", "suspended", "closed"]>;
|
|
63
|
+
isVerified: z.ZodOptional<z.ZodBoolean>;
|
|
64
|
+
isFeatured: z.ZodOptional<z.ZodBoolean>;
|
|
65
|
+
suspensionReason: z.ZodOptional<z.ZodString>;
|
|
66
|
+
capabilities: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
67
|
+
adminNotes: z.ZodOptional<z.ZodString>;
|
|
68
|
+
}, "strip", z.ZodTypeAny, {
|
|
69
|
+
storeStatus: "closed" | "pending" | "active" | "suspended";
|
|
70
|
+
isVerified?: boolean | undefined;
|
|
71
|
+
isFeatured?: boolean | undefined;
|
|
72
|
+
adminNotes?: string | undefined;
|
|
73
|
+
suspensionReason?: string | undefined;
|
|
74
|
+
capabilities?: string[] | undefined;
|
|
75
|
+
}, {
|
|
76
|
+
storeStatus: "closed" | "pending" | "active" | "suspended";
|
|
77
|
+
isVerified?: boolean | undefined;
|
|
78
|
+
isFeatured?: boolean | undefined;
|
|
79
|
+
adminNotes?: string | undefined;
|
|
80
|
+
suspensionReason?: string | undefined;
|
|
81
|
+
capabilities?: string[] | undefined;
|
|
82
|
+
}>, {
|
|
83
|
+
storeStatus: "closed" | "pending" | "active" | "suspended";
|
|
84
|
+
isVerified?: boolean | undefined;
|
|
85
|
+
isFeatured?: boolean | undefined;
|
|
86
|
+
adminNotes?: string | undefined;
|
|
87
|
+
suspensionReason?: string | undefined;
|
|
88
|
+
capabilities?: string[] | undefined;
|
|
89
|
+
}, {
|
|
90
|
+
storeStatus: "closed" | "pending" | "active" | "suspended";
|
|
91
|
+
isVerified?: boolean | undefined;
|
|
92
|
+
isFeatured?: boolean | undefined;
|
|
93
|
+
adminNotes?: string | undefined;
|
|
94
|
+
suspensionReason?: string | undefined;
|
|
95
|
+
capabilities?: string[] | undefined;
|
|
96
|
+
}>;
|
|
97
|
+
export type AdminStoreUpdateValues = z.infer<typeof adminStoreUpdateSchema>;
|
|
98
|
+
/**
|
|
99
|
+
* The homepage announcement bar.
|
|
100
|
+
*
|
|
101
|
+
* 🛑 `message` is the canonical key. The main admin editor saved this copy
|
|
102
|
+
* under `text` until 2026-08-24 — a key no renderer has ever read — so
|
|
103
|
+
* admin-authored announcements saved successfully and never appeared. Naming
|
|
104
|
+
* it here is what stops that recurring.
|
|
105
|
+
*/
|
|
106
|
+
export declare const announcementBarSchema: z.ZodObject<{
|
|
107
|
+
message: z.ZodOptional<z.ZodString>;
|
|
108
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
109
|
+
link: z.ZodOptional<z.ZodString>;
|
|
110
|
+
}, "strip", z.ZodTypeAny, {
|
|
111
|
+
message?: string | undefined;
|
|
112
|
+
link?: string | undefined;
|
|
113
|
+
enabled?: boolean | undefined;
|
|
114
|
+
}, {
|
|
115
|
+
message?: string | undefined;
|
|
116
|
+
link?: string | undefined;
|
|
117
|
+
enabled?: boolean | undefined;
|
|
118
|
+
}>;
|
|
119
|
+
export type AnnouncementBarValues = z.infer<typeof announcementBarSchema>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: A cluster of admin editors rendered `<Form>` with no schema. Two of
|
|
3
|
+
* them write PII, and one changes a store's standing on the platform.
|
|
4
|
+
*
|
|
5
|
+
* `AdminAddressEditorView` is the PII one: `fullName`, `phone` and the
|
|
6
|
+
* address lines are all encrypted at rest, and none was checked — a
|
|
7
|
+
* postal code could be any string, and `ownerId` (which decides WHOSE
|
|
8
|
+
* address this is) was free text.
|
|
9
|
+
*
|
|
10
|
+
* `AdminStoreEditorView` sets `storeStatus`, `isVerified`, `isFeatured`
|
|
11
|
+
* and `capabilities`. Recurrent Root Cause #38 lived here: saving any
|
|
12
|
+
* field re-sent every other one, so an unrelated edit could silently
|
|
13
|
+
* un-verify a store.
|
|
14
|
+
*
|
|
15
|
+
* WHAT: One schema per editor.
|
|
16
|
+
*
|
|
17
|
+
* EXPORTS:
|
|
18
|
+
* adminAddressFormSchema, adminStoreUpdateSchema, announcementBarSchema,
|
|
19
|
+
* and their inferred value types
|
|
20
|
+
*
|
|
21
|
+
* @tag domain:admin
|
|
22
|
+
* @tag layer:schema
|
|
23
|
+
* @tag pattern:none
|
|
24
|
+
* @tag access:isomorphic
|
|
25
|
+
* @tag consumers:AdminAddressEditorView,AdminStoreEditorView,AdminSiteView
|
|
26
|
+
* @tag sideEffects:none
|
|
27
|
+
*/
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
30
|
+
const PHONE_RE = /^(\+?91[-\s]?)?[6-9]\d{9}$/;
|
|
31
|
+
/** Indian PIN: six digits, never starting at zero. */
|
|
32
|
+
const PIN_RE = /^[1-9]\d{5}$/;
|
|
33
|
+
/**
|
|
34
|
+
* An address written by an admin on someone's behalf.
|
|
35
|
+
*
|
|
36
|
+
* Shapes match `AddressDocument` (`addressLine1`, not `line1`) — deliberately
|
|
37
|
+
* NOT `userAddressSchema`, which is the account-side view of the same data
|
|
38
|
+
* under different field names. Reusing that one here would have meant
|
|
39
|
+
* translating on every write.
|
|
40
|
+
*/
|
|
41
|
+
export const adminAddressFormSchema = z.object({
|
|
42
|
+
ownerType: annotate(z.enum(["user", "store"]), {
|
|
43
|
+
section: "owner", sectionLabel: "Belongs to", sectionRequired: true, quick: true, order: 1, row: "pair",
|
|
44
|
+
}),
|
|
45
|
+
ownerId: annotate(z.string().trim().min(1, "Choose who this address belongs to."), {
|
|
46
|
+
section: "owner", quick: true, order: 2, row: "pair",
|
|
47
|
+
}),
|
|
48
|
+
label: annotate(z.string().trim().max(60, "Keep the label short — e.g. Home, Warehouse.").optional(), {
|
|
49
|
+
section: "address", sectionLabel: "Address", order: 1, row: "pair",
|
|
50
|
+
}),
|
|
51
|
+
fullName: annotate(z.string().trim().min(2, "A recipient name is required.").max(120), {
|
|
52
|
+
section: "address", sectionRequired: true, quick: true, order: 2, row: "pair",
|
|
53
|
+
}),
|
|
54
|
+
phone: annotate(z.string().trim().regex(PHONE_RE, "Enter a 10-digit Indian mobile number."), {
|
|
55
|
+
section: "address", quick: true, order: 3, row: "pair",
|
|
56
|
+
}),
|
|
57
|
+
addressLine1: annotate(z.string().trim().min(3, "Street address is required.").max(200), {
|
|
58
|
+
section: "address", quick: true, order: 4, row: "full",
|
|
59
|
+
}),
|
|
60
|
+
addressLine2: annotate(z.string().trim().max(200).optional(), {
|
|
61
|
+
section: "address", order: 5, row: "full",
|
|
62
|
+
}),
|
|
63
|
+
landmark: annotate(z.string().trim().max(120).optional(), {
|
|
64
|
+
section: "address", order: 6, row: "pair",
|
|
65
|
+
}),
|
|
66
|
+
city: annotate(z.string().trim().min(2, "City is required.").max(80), {
|
|
67
|
+
section: "address", order: 7, row: "pair",
|
|
68
|
+
}),
|
|
69
|
+
state: annotate(z.string().trim().min(2, "State is required.").max(80), {
|
|
70
|
+
section: "address", order: 8, row: "pair",
|
|
71
|
+
}),
|
|
72
|
+
// A wrong PIN is the single most common reason a parcel is undeliverable,
|
|
73
|
+
// and it was previously any string at all.
|
|
74
|
+
postalCode: annotate(z.string().trim().regex(PIN_RE, "Enter a 6-digit PIN code."), {
|
|
75
|
+
section: "address", order: 9, row: "pair",
|
|
76
|
+
}),
|
|
77
|
+
country: annotate(z.string().trim().min(2).max(60), {
|
|
78
|
+
section: "address", order: 10, row: "pair",
|
|
79
|
+
}),
|
|
80
|
+
isDefault: annotate(z.boolean().optional(), {
|
|
81
|
+
section: "address", order: 11, row: "quarter",
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
/**
|
|
85
|
+
* An admin changing a store's standing.
|
|
86
|
+
*
|
|
87
|
+
* `suspensionReason` is required when — and only when — the status is
|
|
88
|
+
* `suspended`: suspending a seller without a recorded reason leaves them, and
|
|
89
|
+
* the next admin, with nothing to appeal or review.
|
|
90
|
+
*/
|
|
91
|
+
export const adminStoreUpdateSchema = z
|
|
92
|
+
.object({
|
|
93
|
+
storeStatus: annotate(z.enum(["pending", "active", "suspended", "closed"]), {
|
|
94
|
+
section: "standing", sectionLabel: "Store standing", sectionRequired: true, quick: true, order: 1, row: "pair",
|
|
95
|
+
}),
|
|
96
|
+
isVerified: annotate(z.boolean().optional(), {
|
|
97
|
+
section: "standing", order: 2, row: "quarter",
|
|
98
|
+
}),
|
|
99
|
+
isFeatured: annotate(z.boolean().optional(), {
|
|
100
|
+
section: "standing", order: 3, row: "quarter",
|
|
101
|
+
}),
|
|
102
|
+
suspensionReason: annotate(z.string().trim().max(500).optional(), {
|
|
103
|
+
section: "standing", order: 4, row: "full", kind: "textarea",
|
|
104
|
+
}),
|
|
105
|
+
capabilities: annotate(z.array(z.string().min(1)).max(50).optional(), {
|
|
106
|
+
section: "capabilities", sectionLabel: "Capabilities", order: 1, row: "full", kind: "list",
|
|
107
|
+
}),
|
|
108
|
+
adminNotes: annotate(z.string().trim().max(2000).optional(), {
|
|
109
|
+
section: "capabilities", order: 2, row: "full", kind: "textarea",
|
|
110
|
+
help: "Internal only — never shown to the store owner or the public.",
|
|
111
|
+
}),
|
|
112
|
+
})
|
|
113
|
+
.superRefine((v, ctx) => {
|
|
114
|
+
if (v.storeStatus === "suspended" && !v.suspensionReason?.trim()) {
|
|
115
|
+
ctx.addIssue({
|
|
116
|
+
code: z.ZodIssueCode.custom,
|
|
117
|
+
path: ["suspensionReason"],
|
|
118
|
+
message: "Record why this store is being suspended.",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
/**
|
|
123
|
+
* The homepage announcement bar.
|
|
124
|
+
*
|
|
125
|
+
* 🛑 `message` is the canonical key. The main admin editor saved this copy
|
|
126
|
+
* under `text` until 2026-08-24 — a key no renderer has ever read — so
|
|
127
|
+
* admin-authored announcements saved successfully and never appeared. Naming
|
|
128
|
+
* it here is what stops that recurring.
|
|
129
|
+
*/
|
|
130
|
+
export const announcementBarSchema = z.object({
|
|
131
|
+
message: annotate(z.string().trim().max(300, "Keep the announcement under 300 characters.").optional(), {
|
|
132
|
+
section: "announcement", sectionLabel: "Announcement bar", sectionRequired: true,
|
|
133
|
+
quick: true, order: 1, row: "full",
|
|
134
|
+
}),
|
|
135
|
+
enabled: annotate(z.boolean().optional(), {
|
|
136
|
+
section: "announcement", order: 2, row: "quarter",
|
|
137
|
+
}),
|
|
138
|
+
link: annotate(z.string().trim().max(300).optional(), {
|
|
139
|
+
section: "announcement", order: 3, row: "full",
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const adminOrderUpdateSchema: z.ZodObject<{
|
|
3
|
+
status: z.ZodEnum<[string, ...string[]]>;
|
|
4
|
+
trackingNumber: z.ZodOptional<z.ZodString>;
|
|
5
|
+
carrier: z.ZodOptional<z.ZodString>;
|
|
6
|
+
refundAmount: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
status: string;
|
|
10
|
+
notes?: string | undefined;
|
|
11
|
+
trackingNumber?: string | undefined;
|
|
12
|
+
refundAmount?: number | undefined;
|
|
13
|
+
carrier?: string | undefined;
|
|
14
|
+
}, {
|
|
15
|
+
status: string;
|
|
16
|
+
notes?: string | undefined;
|
|
17
|
+
trackingNumber?: string | undefined;
|
|
18
|
+
refundAmount?: number | undefined;
|
|
19
|
+
carrier?: string | undefined;
|
|
20
|
+
}>;
|
|
21
|
+
export type AdminOrderUpdateValues = z.infer<typeof adminOrderUpdateSchema>;
|
|
22
|
+
/** Inviting a team member — an email plus the permissions it grants. */
|
|
23
|
+
export declare const employeeInviteSchema: z.ZodObject<{
|
|
24
|
+
email: z.ZodString;
|
|
25
|
+
permissionGroup: z.ZodString;
|
|
26
|
+
permissions: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
27
|
+
}, "strip", z.ZodTypeAny, {
|
|
28
|
+
email: string;
|
|
29
|
+
permissionGroup: string;
|
|
30
|
+
permissions?: string[] | undefined;
|
|
31
|
+
}, {
|
|
32
|
+
email: string;
|
|
33
|
+
permissionGroup: string;
|
|
34
|
+
permissions?: string[] | undefined;
|
|
35
|
+
}>;
|
|
36
|
+
export type EmployeeInviteValues = z.infer<typeof employeeInviteSchema>;
|
|
37
|
+
/**
|
|
38
|
+
* The reason recorded against a hard ban.
|
|
39
|
+
*
|
|
40
|
+
* A hard ban runs an eight-stage destructive cascade; this string is the only
|
|
41
|
+
* durable record of why. A 10-character floor, matching the admin offer-cancel
|
|
42
|
+
* reason, because "spam" is not an audit trail.
|
|
43
|
+
*/
|
|
44
|
+
export declare const hardBanReasonSchema: z.ZodObject<{
|
|
45
|
+
reason: z.ZodString;
|
|
46
|
+
}, "strip", z.ZodTypeAny, {
|
|
47
|
+
reason: string;
|
|
48
|
+
}, {
|
|
49
|
+
reason: string;
|
|
50
|
+
}>;
|
|
51
|
+
export type HardBanReasonValues = z.infer<typeof hardBanReasonSchema>;
|
|
52
|
+
/**
|
|
53
|
+
* Feature flags.
|
|
54
|
+
*
|
|
55
|
+
* Deliberately open records: flags are added and removed constantly, and a
|
|
56
|
+
* closed schema here would silently STRIP a newly-added flag on the next save
|
|
57
|
+
* — the `productBaseSchema` failure applied to configuration. The values are
|
|
58
|
+
* still constrained, which is what stops a flag being set to an object.
|
|
59
|
+
*/
|
|
60
|
+
export declare const featureFlagsUpdateSchema: z.ZodObject<{
|
|
61
|
+
flags: z.ZodRecord<z.ZodString, z.ZodBoolean>;
|
|
62
|
+
rollouts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
63
|
+
}, "strip", z.ZodTypeAny, {
|
|
64
|
+
flags: Record<string, boolean>;
|
|
65
|
+
rollouts?: Record<string, number> | undefined;
|
|
66
|
+
}, {
|
|
67
|
+
flags: Record<string, boolean>;
|
|
68
|
+
rollouts?: Record<string, number> | undefined;
|
|
69
|
+
}>;
|
|
70
|
+
export type FeatureFlagsUpdateValues = z.infer<typeof featureFlagsUpdateSchema>;
|
|
71
|
+
/** An ad placement. `priority` orders competing ads in the same slot. */
|
|
72
|
+
export declare const adminAdFormSchema: z.ZodEffects<z.ZodObject<{
|
|
73
|
+
name: z.ZodString;
|
|
74
|
+
provider: z.ZodEnum<["manual", "adsense", "thirdParty"]>;
|
|
75
|
+
status: z.ZodEnum<["draft", "active", "scheduled", "paused"]>;
|
|
76
|
+
priority: z.ZodNumber;
|
|
77
|
+
placementIds: z.ZodArray<z.ZodString, "many">;
|
|
78
|
+
requiresConsent: z.ZodBoolean;
|
|
79
|
+
startAt: z.ZodOptional<z.ZodString>;
|
|
80
|
+
endAt: z.ZodOptional<z.ZodString>;
|
|
81
|
+
creative: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<import("../../..").FirestoreValue, z.ZodTypeDef, import("../../..").FirestoreValue>>>;
|
|
82
|
+
}, "strip", z.ZodTypeAny, {
|
|
83
|
+
status: "draft" | "active" | "scheduled" | "paused";
|
|
84
|
+
name: string;
|
|
85
|
+
priority: number;
|
|
86
|
+
provider: "manual" | "adsense" | "thirdParty";
|
|
87
|
+
placementIds: string[];
|
|
88
|
+
requiresConsent: boolean;
|
|
89
|
+
startAt?: string | undefined;
|
|
90
|
+
endAt?: string | undefined;
|
|
91
|
+
creative?: Record<string, import("../../..").FirestoreValue> | undefined;
|
|
92
|
+
}, {
|
|
93
|
+
status: "draft" | "active" | "scheduled" | "paused";
|
|
94
|
+
name: string;
|
|
95
|
+
priority: number;
|
|
96
|
+
provider: "manual" | "adsense" | "thirdParty";
|
|
97
|
+
placementIds: string[];
|
|
98
|
+
requiresConsent: boolean;
|
|
99
|
+
startAt?: string | undefined;
|
|
100
|
+
endAt?: string | undefined;
|
|
101
|
+
creative?: Record<string, import("../../..").FirestoreValue> | undefined;
|
|
102
|
+
}>, {
|
|
103
|
+
status: "draft" | "active" | "scheduled" | "paused";
|
|
104
|
+
name: string;
|
|
105
|
+
priority: number;
|
|
106
|
+
provider: "manual" | "adsense" | "thirdParty";
|
|
107
|
+
placementIds: string[];
|
|
108
|
+
requiresConsent: boolean;
|
|
109
|
+
startAt?: string | undefined;
|
|
110
|
+
endAt?: string | undefined;
|
|
111
|
+
creative?: Record<string, import("../../..").FirestoreValue> | undefined;
|
|
112
|
+
}, {
|
|
113
|
+
status: "draft" | "active" | "scheduled" | "paused";
|
|
114
|
+
name: string;
|
|
115
|
+
priority: number;
|
|
116
|
+
provider: "manual" | "adsense" | "thirdParty";
|
|
117
|
+
placementIds: string[];
|
|
118
|
+
requiresConsent: boolean;
|
|
119
|
+
startAt?: string | undefined;
|
|
120
|
+
endAt?: string | undefined;
|
|
121
|
+
creative?: Record<string, import("../../..").FirestoreValue> | undefined;
|
|
122
|
+
}>;
|
|
123
|
+
export type AdminAdFormValues = z.infer<typeof adminAdFormSchema>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Four more admin editors rendered `<Form>` with no schema. Each writes
|
|
3
|
+
* something an admin cannot easily undo.
|
|
4
|
+
*
|
|
5
|
+
* `AdminOrderEditorView` is the one that touches money: `refundAmount`
|
|
6
|
+
* was parsed with a bare `parseFloat` and guarded only by
|
|
7
|
+
* `!isNaN(amount) && amount > 0`, with no upper bound and no check
|
|
8
|
+
* against the order — so a mistyped figure went straight through as a
|
|
9
|
+
* refund instruction.
|
|
10
|
+
*
|
|
11
|
+
* `AdminEmployeeEditorView` grants permissions, and `AdminUsersView`'s
|
|
12
|
+
* hard-ban takes a reason that is the entire record of why an account
|
|
13
|
+
* was destroyed.
|
|
14
|
+
*
|
|
15
|
+
* WHAT: One schema per editor.
|
|
16
|
+
*
|
|
17
|
+
* EXPORTS:
|
|
18
|
+
* adminOrderUpdateSchema, employeeInviteSchema, hardBanReasonSchema,
|
|
19
|
+
* featureFlagsUpdateSchema, adminAdFormSchema, and their value types
|
|
20
|
+
*
|
|
21
|
+
* @tag domain:admin
|
|
22
|
+
* @tag layer:schema
|
|
23
|
+
* @tag pattern:none
|
|
24
|
+
* @tag access:isomorphic
|
|
25
|
+
* @tag consumers:AdminOrderEditorView,AdminEmployeeEditorView,AdminUsersView,AdminFeatureFlagsView,AdminAdEditorView
|
|
26
|
+
* @tag sideEffects:none
|
|
27
|
+
*/
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
30
|
+
import { firestoreValueSchema } from "../../../schemas/firestore-value";
|
|
31
|
+
import { OrderStatusValues } from "../../orders/schemas/firestore";
|
|
32
|
+
/** Derived from the real 9-value union, never restated (Root Cause #36). */
|
|
33
|
+
const orderStatusSchema = z.enum(Object.values(OrderStatusValues));
|
|
34
|
+
export const adminOrderUpdateSchema = z.object({
|
|
35
|
+
status: annotate(orderStatusSchema, {
|
|
36
|
+
section: "status", sectionLabel: "Order", sectionRequired: true, quick: true, order: 1, row: "pair",
|
|
37
|
+
}),
|
|
38
|
+
trackingNumber: annotate(z.string().trim().max(120).optional(), {
|
|
39
|
+
section: "shipping", sectionLabel: "Shipping", order: 1, row: "pair",
|
|
40
|
+
}),
|
|
41
|
+
carrier: annotate(z.string().trim().max(80).optional(), {
|
|
42
|
+
section: "shipping", order: 2, row: "pair",
|
|
43
|
+
}),
|
|
44
|
+
/*
|
|
45
|
+
* 🛑 Bounded. This was `parseFloat` + `> 0` with no ceiling, so a slipped
|
|
46
|
+
* decimal point issued a refund an order of magnitude too large. The cap is
|
|
47
|
+
* a sanity bound, not a business rule — the route still checks the amount
|
|
48
|
+
* against the order itself.
|
|
49
|
+
*/
|
|
50
|
+
refundAmount: annotate(z.coerce
|
|
51
|
+
.number({ invalid_type_error: "Enter the refund amount as a number." })
|
|
52
|
+
.positive("A refund must be greater than zero.")
|
|
53
|
+
.max(10000000, "That looks like a typo — check the decimal point.")
|
|
54
|
+
.optional(), { section: "shipping", order: 3, row: "pair", kind: "number" }),
|
|
55
|
+
notes: annotate(z.string().trim().max(2000).optional(), {
|
|
56
|
+
section: "shipping", order: 4, row: "full", kind: "textarea",
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
/** Inviting a team member — an email plus the permissions it grants. */
|
|
60
|
+
export const employeeInviteSchema = z.object({
|
|
61
|
+
email: annotate(z.string().trim().toLowerCase().min(1, "An email address is required.").email("That doesn't look like an email address."), { section: "member", sectionLabel: "Team member", sectionRequired: true, quick: true, order: 1, row: "pair" }),
|
|
62
|
+
permissionGroup: annotate(z.string().trim().min(1, "Choose a permission group."), {
|
|
63
|
+
section: "member", quick: true, order: 2, row: "pair",
|
|
64
|
+
}),
|
|
65
|
+
permissions: annotate(z.array(z.string().min(1)).max(200).optional(), {
|
|
66
|
+
section: "member", order: 3, row: "full", kind: "list",
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* The reason recorded against a hard ban.
|
|
71
|
+
*
|
|
72
|
+
* A hard ban runs an eight-stage destructive cascade; this string is the only
|
|
73
|
+
* durable record of why. A 10-character floor, matching the admin offer-cancel
|
|
74
|
+
* reason, because "spam" is not an audit trail.
|
|
75
|
+
*/
|
|
76
|
+
export const hardBanReasonSchema = z.object({
|
|
77
|
+
reason: annotate(z.string().trim().min(10, "Record why this account is being banned — at least 10 characters.").max(500), { section: "ban", sectionLabel: "Reason", sectionRequired: true, quick: true, order: 1, row: "full", kind: "textarea" }),
|
|
78
|
+
});
|
|
79
|
+
/**
|
|
80
|
+
* Feature flags.
|
|
81
|
+
*
|
|
82
|
+
* Deliberately open records: flags are added and removed constantly, and a
|
|
83
|
+
* closed schema here would silently STRIP a newly-added flag on the next save
|
|
84
|
+
* — the `productBaseSchema` failure applied to configuration. The values are
|
|
85
|
+
* still constrained, which is what stops a flag being set to an object.
|
|
86
|
+
*/
|
|
87
|
+
export const featureFlagsUpdateSchema = z.object({
|
|
88
|
+
flags: annotate(z.record(z.string(), z.boolean()), {
|
|
89
|
+
section: "flags", sectionLabel: "Feature flags", sectionRequired: true, order: 1, row: "full", kind: "list",
|
|
90
|
+
}),
|
|
91
|
+
rollouts: annotate(z.record(z.string(), z.coerce.number().min(0).max(100)).optional(), {
|
|
92
|
+
section: "flags", order: 2, row: "full", kind: "list",
|
|
93
|
+
help: "Percentage rollout per flag, 0–100.",
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
/** An ad placement. `priority` orders competing ads in the same slot. */
|
|
97
|
+
export const adminAdFormSchema = z
|
|
98
|
+
.object({
|
|
99
|
+
name: annotate(z.string().trim().min(2, "Give the ad a name.").max(120), {
|
|
100
|
+
section: "basics", sectionLabel: "Ad", sectionRequired: true, quick: true, order: 1, row: "pair",
|
|
101
|
+
}),
|
|
102
|
+
provider: annotate(z.enum(["manual", "adsense", "thirdParty"]), {
|
|
103
|
+
section: "basics", quick: true, order: 2, row: "pair",
|
|
104
|
+
}),
|
|
105
|
+
status: annotate(z.enum(["draft", "active", "scheduled", "paused"]), {
|
|
106
|
+
section: "basics", quick: true, order: 3, row: "pair",
|
|
107
|
+
}),
|
|
108
|
+
priority: annotate(z.coerce.number().int().min(0).max(1000), {
|
|
109
|
+
section: "basics", order: 4, row: "pair", kind: "number",
|
|
110
|
+
}),
|
|
111
|
+
placementIds: annotate(z.array(z.string().min(1)).min(1, "Choose at least one placement."), {
|
|
112
|
+
section: "placement", sectionLabel: "Placement", sectionRequired: true, order: 1, row: "full", kind: "list",
|
|
113
|
+
}),
|
|
114
|
+
requiresConsent: annotate(z.boolean(), {
|
|
115
|
+
section: "placement", order: 2, row: "quarter",
|
|
116
|
+
}),
|
|
117
|
+
startAt: annotate(z.string().optional(), {
|
|
118
|
+
section: "schedule", sectionLabel: "Schedule", order: 1, row: "pair", kind: "date",
|
|
119
|
+
}),
|
|
120
|
+
endAt: annotate(z.string().optional(), {
|
|
121
|
+
section: "schedule", order: 2, row: "pair", kind: "date",
|
|
122
|
+
}),
|
|
123
|
+
// Creative shape varies by provider, so it stays open — an AdSense slot and
|
|
124
|
+
// a manual banner share almost no fields. Open to a Firestore VALUE tree,
|
|
125
|
+
// not to anything: `z.unknown()` would pass a function straight to the
|
|
126
|
+
// driver, which then throws at write time rather than at parse time.
|
|
127
|
+
creative: annotate(z.record(firestoreValueSchema).optional(), {
|
|
128
|
+
section: "creative", sectionLabel: "Creative", order: 1, row: "full", kind: "list",
|
|
129
|
+
}),
|
|
130
|
+
})
|
|
131
|
+
.superRefine((v, ctx) => {
|
|
132
|
+
// A scheduled ad with no start date never runs, and nothing would say so.
|
|
133
|
+
if (v.status === "scheduled" && !v.startAt) {
|
|
134
|
+
ctx.addIssue({
|
|
135
|
+
code: z.ZodIssueCode.custom,
|
|
136
|
+
path: ["startAt"],
|
|
137
|
+
message: "A scheduled ad needs a start date.",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (v.startAt && v.endAt && v.endAt <= v.startAt) {
|
|
141
|
+
ctx.addIssue({
|
|
142
|
+
code: z.ZodIssueCode.custom,
|
|
143
|
+
path: ["endAt"],
|
|
144
|
+
message: "The end date must be after the start date.",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Cancelling an order.
|
|
4
|
+
*
|
|
5
|
+
* A reason floor of 5 characters, because this text is what the seller reads
|
|
6
|
+
* to decide whether to restock, refund early or dispute — and "x" tells them
|
|
7
|
+
* nothing. Deliberately low: the buyer is cancelling, not writing an essay,
|
|
8
|
+
* and a high floor would push them to type filler.
|
|
9
|
+
*/
|
|
10
|
+
export declare const orderCancelSchema: z.ZodObject<{
|
|
11
|
+
reason: z.ZodString;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
reason: string;
|
|
14
|
+
}, {
|
|
15
|
+
reason: string;
|
|
16
|
+
}>;
|
|
17
|
+
export type OrderCancelValues = z.infer<typeof orderCancelSchema>;
|
|
18
|
+
/**
|
|
19
|
+
* Uploading proof of a manual (cash / UPI / EMI) payment.
|
|
20
|
+
*
|
|
21
|
+
* ## Every field here is evidence
|
|
22
|
+
*
|
|
23
|
+
* An admin verifies or rejects on this, and a rejection carries a 7-day ban
|
|
24
|
+
* cascade — so the cost of a sloppy submission is real in both directions.
|
|
25
|
+
*
|
|
26
|
+
* `buyerMarkedPaid` and `buyerFraudAgreementAccepted` are `z.literal(true)`
|
|
27
|
+
* rather than `z.boolean()`: an unchecked declaration is not a submission with
|
|
28
|
+
* a `false` in it, it is a form that must not send.
|
|
29
|
+
*/
|
|
30
|
+
export declare const paymentProofSchema: z.ZodObject<{
|
|
31
|
+
transactionId: z.ZodString;
|
|
32
|
+
buyerReportedUpiId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
33
|
+
buyerMarkedPaid: z.ZodLiteral<true>;
|
|
34
|
+
buyerFraudAgreementAccepted: z.ZodLiteral<true>;
|
|
35
|
+
}, "strip", z.ZodTypeAny, {
|
|
36
|
+
transactionId: string;
|
|
37
|
+
buyerMarkedPaid: true;
|
|
38
|
+
buyerFraudAgreementAccepted: true;
|
|
39
|
+
buyerReportedUpiId?: string | undefined;
|
|
40
|
+
}, {
|
|
41
|
+
transactionId: string;
|
|
42
|
+
buyerMarkedPaid: true;
|
|
43
|
+
buyerFraudAgreementAccepted: true;
|
|
44
|
+
buyerReportedUpiId?: string | undefined;
|
|
45
|
+
}>;
|
|
46
|
+
export type PaymentProofValues = z.infer<typeof paymentProofSchema>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Two buyer-facing order forms rendered `<Form>` with no schema.
|
|
3
|
+
*
|
|
4
|
+
* The manual-payment one is the consequential case. It carries the
|
|
5
|
+
* buyer's own claim that they have paid — a UTR, the UPI ID they sent to,
|
|
6
|
+
* and a fraud declaration — and an admin decides on that evidence whether
|
|
7
|
+
* to release goods. `transactionId` was free text with no floor, so a
|
|
8
|
+
* single character satisfied it and reached the review queue looking like
|
|
9
|
+
* a reference.
|
|
10
|
+
*
|
|
11
|
+
* WHAT: The two schemas, plus a shared `cancellationReason`.
|
|
12
|
+
*
|
|
13
|
+
* EXPORTS:
|
|
14
|
+
* orderCancelSchema, paymentProofSchema, and their inferred value types
|
|
15
|
+
*
|
|
16
|
+
* @tag domain:orders
|
|
17
|
+
* @tag layer:schema
|
|
18
|
+
* @tag pattern:none
|
|
19
|
+
* @tag access:isomorphic
|
|
20
|
+
* @tag consumers:user/orders/[id]/cancel,user/orders/[id]/payment
|
|
21
|
+
* @tag sideEffects:none
|
|
22
|
+
*/
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
25
|
+
/**
|
|
26
|
+
* Cancelling an order.
|
|
27
|
+
*
|
|
28
|
+
* A reason floor of 5 characters, because this text is what the seller reads
|
|
29
|
+
* to decide whether to restock, refund early or dispute — and "x" tells them
|
|
30
|
+
* nothing. Deliberately low: the buyer is cancelling, not writing an essay,
|
|
31
|
+
* and a high floor would push them to type filler.
|
|
32
|
+
*/
|
|
33
|
+
export const orderCancelSchema = z.object({
|
|
34
|
+
reason: annotate(z
|
|
35
|
+
.string()
|
|
36
|
+
.trim()
|
|
37
|
+
.min(5, "Give a short reason — the seller sees this.")
|
|
38
|
+
.max(500, "Keep it under 500 characters."), {
|
|
39
|
+
section: "cancel",
|
|
40
|
+
sectionLabel: "Why are you cancelling?",
|
|
41
|
+
sectionRequired: true,
|
|
42
|
+
quick: true,
|
|
43
|
+
order: 1,
|
|
44
|
+
row: "full",
|
|
45
|
+
kind: "textarea",
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* Uploading proof of a manual (cash / UPI / EMI) payment.
|
|
50
|
+
*
|
|
51
|
+
* ## Every field here is evidence
|
|
52
|
+
*
|
|
53
|
+
* An admin verifies or rejects on this, and a rejection carries a 7-day ban
|
|
54
|
+
* cascade — so the cost of a sloppy submission is real in both directions.
|
|
55
|
+
*
|
|
56
|
+
* `buyerMarkedPaid` and `buyerFraudAgreementAccepted` are `z.literal(true)`
|
|
57
|
+
* rather than `z.boolean()`: an unchecked declaration is not a submission with
|
|
58
|
+
* a `false` in it, it is a form that must not send.
|
|
59
|
+
*/
|
|
60
|
+
export const paymentProofSchema = z.object({
|
|
61
|
+
transactionId: annotate(z
|
|
62
|
+
.string()
|
|
63
|
+
.trim()
|
|
64
|
+
.min(4, "Enter the UTR or reference number from your payment app.")
|
|
65
|
+
.max(120, "Keep the reference under 120 characters."), { section: "proof", sectionLabel: "Your payment", sectionRequired: true, quick: true, order: 1, row: "pair" }),
|
|
66
|
+
/*
|
|
67
|
+
* The UPI ID the buyer actually sent to. Compared against the store's
|
|
68
|
+
* displayed one so a mismatch is flagged to the admin — which is why the
|
|
69
|
+
* shape matters, not just its presence.
|
|
70
|
+
*/
|
|
71
|
+
buyerReportedUpiId: annotate(z
|
|
72
|
+
.string()
|
|
73
|
+
.trim()
|
|
74
|
+
.regex(/^[\w.\-_]{2,256}@[a-zA-Z]{2,64}$/, "Enter the UPI ID you paid to, e.g. name@bank.")
|
|
75
|
+
.optional()
|
|
76
|
+
.or(z.literal("")), { section: "proof", order: 2, row: "pair" }),
|
|
77
|
+
buyerMarkedPaid: annotate(z.literal(true, { errorMap: () => ({ message: "Confirm that you have sent the payment." }) }), { section: "declare", sectionLabel: "Declaration", sectionRequired: true, order: 1, row: "full" }),
|
|
78
|
+
buyerFraudAgreementAccepted: annotate(z.literal(true, {
|
|
79
|
+
errorMap: () => ({ message: "You must accept the fraud policy to submit proof." }),
|
|
80
|
+
}), { section: "declare", order: 2, row: "full" }),
|
|
81
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const scamReportFormSchema: z.ZodEffects<z.ZodObject<{
|
|
3
|
+
displayName: z.ZodString;
|
|
4
|
+
phones: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
5
|
+
upiIds: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
6
|
+
emails: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
7
|
+
scamType: z.ZodString;
|
|
8
|
+
scamPlatform: z.ZodOptional<z.ZodString>;
|
|
9
|
+
amountLost: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
itemInvolved: z.ZodOptional<z.ZodString>;
|
|
11
|
+
description: z.ZodString;
|
|
12
|
+
reportedByAnon: z.ZodOptional<z.ZodBoolean>;
|
|
13
|
+
agreed: z.ZodLiteral<true>;
|
|
14
|
+
}, "strip", z.ZodTypeAny, {
|
|
15
|
+
displayName: string;
|
|
16
|
+
description: string;
|
|
17
|
+
scamType: string;
|
|
18
|
+
agreed: true;
|
|
19
|
+
reportedByAnon?: boolean | undefined;
|
|
20
|
+
scamPlatform?: string | undefined;
|
|
21
|
+
amountLost?: number | undefined;
|
|
22
|
+
itemInvolved?: string | undefined;
|
|
23
|
+
phones?: string[] | undefined;
|
|
24
|
+
upiIds?: string[] | undefined;
|
|
25
|
+
emails?: string[] | undefined;
|
|
26
|
+
}, {
|
|
27
|
+
displayName: string;
|
|
28
|
+
description: string;
|
|
29
|
+
scamType: string;
|
|
30
|
+
agreed: true;
|
|
31
|
+
reportedByAnon?: boolean | undefined;
|
|
32
|
+
scamPlatform?: string | undefined;
|
|
33
|
+
amountLost?: number | undefined;
|
|
34
|
+
itemInvolved?: string | undefined;
|
|
35
|
+
phones?: string[] | undefined;
|
|
36
|
+
upiIds?: string[] | undefined;
|
|
37
|
+
emails?: string[] | undefined;
|
|
38
|
+
}>, {
|
|
39
|
+
displayName: string;
|
|
40
|
+
description: string;
|
|
41
|
+
scamType: string;
|
|
42
|
+
agreed: true;
|
|
43
|
+
reportedByAnon?: boolean | undefined;
|
|
44
|
+
scamPlatform?: string | undefined;
|
|
45
|
+
amountLost?: number | undefined;
|
|
46
|
+
itemInvolved?: string | undefined;
|
|
47
|
+
phones?: string[] | undefined;
|
|
48
|
+
upiIds?: string[] | undefined;
|
|
49
|
+
emails?: string[] | undefined;
|
|
50
|
+
}, {
|
|
51
|
+
displayName: string;
|
|
52
|
+
description: string;
|
|
53
|
+
scamType: string;
|
|
54
|
+
agreed: true;
|
|
55
|
+
reportedByAnon?: boolean | undefined;
|
|
56
|
+
scamPlatform?: string | undefined;
|
|
57
|
+
amountLost?: number | undefined;
|
|
58
|
+
itemInvolved?: string | undefined;
|
|
59
|
+
phones?: string[] | undefined;
|
|
60
|
+
upiIds?: string[] | undefined;
|
|
61
|
+
emails?: string[] | undefined;
|
|
62
|
+
}>;
|
|
63
|
+
export type ScamReportFormValues = z.infer<typeof scamReportFormSchema>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: The public scam-report form (`/scams/report`) rendered `<Form>` with no
|
|
3
|
+
* schema. It is the highest-consequence unvalidated form in the app: a
|
|
4
|
+
* submission names a real person or business by phone number, UPI ID or
|
|
5
|
+
* email and asks the platform to publish an accusation against them.
|
|
6
|
+
*
|
|
7
|
+
* Everything was free text. `amountLost` in particular was a string with
|
|
8
|
+
* no bound, and it is displayed on the public scammer profile.
|
|
9
|
+
*
|
|
10
|
+
* WHAT: The submit contract.
|
|
11
|
+
*
|
|
12
|
+
* ## At least one identifier is required, and no single one is
|
|
13
|
+
*
|
|
14
|
+
* A report naming nobody is unactionable, but demanding a phone number would
|
|
15
|
+
* reject a report about an email-only scammer. So the requirement is on the
|
|
16
|
+
* SET, checked in a `superRefine` — the same shape the payout-method schema
|
|
17
|
+
* uses for its per-type bank rules.
|
|
18
|
+
*
|
|
19
|
+
* ## `agreed` is `z.literal(true)`
|
|
20
|
+
*
|
|
21
|
+
* An unticked declaration is not a submission carrying `false`; it is a form
|
|
22
|
+
* that must not send. The user is asserting the accusation is truthful, and
|
|
23
|
+
* that assertion is what makes publishing it defensible.
|
|
24
|
+
*
|
|
25
|
+
* EXPORTS: scamReportFormSchema, type ScamReportFormValues
|
|
26
|
+
*
|
|
27
|
+
* @tag domain:scams
|
|
28
|
+
* @tag layer:schema
|
|
29
|
+
* @tag pattern:none
|
|
30
|
+
* @tag access:isomorphic
|
|
31
|
+
* @tag consumers:scams/report page
|
|
32
|
+
* @tag sideEffects:none
|
|
33
|
+
*/
|
|
34
|
+
import { z } from "zod";
|
|
35
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
36
|
+
const UPI_VPA_RE = /^[\w.\-_]{2,256}@[a-zA-Z]{2,64}$/;
|
|
37
|
+
const PHONE_RE = /^(\+?91[-\s]?)?[6-9]\d{9}$/;
|
|
38
|
+
export const scamReportFormSchema = z
|
|
39
|
+
.object({
|
|
40
|
+
displayName: annotate(z.string().trim().min(2, "Give the name or handle they used.").max(160), { section: "who", sectionLabel: "Who", sectionRequired: true, quick: true, order: 1, row: "full" }),
|
|
41
|
+
// Each entry is shape-checked; the SET is checked in the superRefine.
|
|
42
|
+
phones: annotate(z.array(z.string().trim().regex(PHONE_RE, "Enter a 10-digit Indian mobile number.")).max(10).optional(), { section: "who", order: 2, row: "full", kind: "list" }),
|
|
43
|
+
upiIds: annotate(z.array(z.string().trim().regex(UPI_VPA_RE, "Enter a valid UPI ID, e.g. name@bank.")).max(10).optional(), { section: "who", order: 3, row: "full", kind: "list" }),
|
|
44
|
+
emails: annotate(z.array(z.string().trim().toLowerCase().email("Enter a valid email address.")).max(10).optional(), { section: "who", order: 4, row: "full", kind: "list" }),
|
|
45
|
+
scamType: annotate(z.string().trim().min(1, "Choose what kind of scam this was."), {
|
|
46
|
+
section: "what", sectionLabel: "What happened", sectionRequired: true, quick: true, order: 1, row: "pair",
|
|
47
|
+
}),
|
|
48
|
+
scamPlatform: annotate(z.string().trim().max(120).optional(), {
|
|
49
|
+
section: "what", order: 2, row: "pair",
|
|
50
|
+
}),
|
|
51
|
+
/*
|
|
52
|
+
* Coerced and bounded. It arrives from a text input and is rendered on the
|
|
53
|
+
* public profile, so an unbounded string could publish "one MILLION crore"
|
|
54
|
+
* as a loss figure. Empty is allowed — plenty of reports are about an
|
|
55
|
+
* attempt that cost nothing.
|
|
56
|
+
*/
|
|
57
|
+
amountLost: annotate(z.coerce
|
|
58
|
+
.number({ invalid_type_error: "Enter the amount as a number." })
|
|
59
|
+
.min(0, "An amount cannot be negative.")
|
|
60
|
+
.max(100000000, "That looks like a typo.")
|
|
61
|
+
.optional(), { section: "what", order: 3, row: "pair", kind: "number" }),
|
|
62
|
+
itemInvolved: annotate(z.string().trim().max(200).optional(), {
|
|
63
|
+
section: "what", order: 4, row: "pair",
|
|
64
|
+
}),
|
|
65
|
+
description: annotate(z
|
|
66
|
+
.string()
|
|
67
|
+
.trim()
|
|
68
|
+
.min(30, "Describe what happened in at least 30 characters — this is the evidence.")
|
|
69
|
+
.max(5000, "Keep the description under 5000 characters."), { section: "what", order: 5, row: "full", kind: "textarea" }),
|
|
70
|
+
reportedByAnon: annotate(z.boolean().optional(), {
|
|
71
|
+
section: "declare", sectionLabel: "Declaration", order: 1, row: "quarter",
|
|
72
|
+
}),
|
|
73
|
+
agreed: annotate(z.literal(true, {
|
|
74
|
+
errorMap: () => ({ message: "You must confirm this report is truthful before submitting." }),
|
|
75
|
+
}), { section: "declare", order: 2, row: "full" }),
|
|
76
|
+
})
|
|
77
|
+
.superRefine((v, ctx) => {
|
|
78
|
+
const count = (v.phones?.length ?? 0) + (v.upiIds?.length ?? 0) + (v.emails?.length ?? 0);
|
|
79
|
+
if (count === 0) {
|
|
80
|
+
// Reported on `phones` so the message has a field to land on; the text
|
|
81
|
+
// names all three, because any one of them satisfies the rule.
|
|
82
|
+
ctx.addIssue({
|
|
83
|
+
code: z.ZodIssueCode.custom,
|
|
84
|
+
path: ["phones"],
|
|
85
|
+
message: "Add at least one phone number, UPI ID or email — a report with no identifier cannot be acted on.",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const sublistingCategoryFormSchema: z.ZodObject<{
|
|
3
|
+
name: z.ZodString;
|
|
4
|
+
itemCode: z.ZodOptional<z.ZodString>;
|
|
5
|
+
description: z.ZodOptional<z.ZodString>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
name: string;
|
|
8
|
+
description?: string | undefined;
|
|
9
|
+
itemCode?: string | undefined;
|
|
10
|
+
}, {
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string | undefined;
|
|
13
|
+
itemCode?: string | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
export type SublistingCategoryFormValues = z.infer<typeof sublistingCategoryFormSchema>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Both sub-listing-category pages (`new` and `[id]/edit`) rendered
|
|
3
|
+
* `<Form>` with no schema and guarded only on `!name.trim()`. A
|
|
4
|
+
* sub-listing category groups the same real-world collectible across
|
|
5
|
+
* grades and conditions, so its `itemCode` is the identifier buyers match
|
|
6
|
+
* against — and it was unbounded free text.
|
|
7
|
+
*
|
|
8
|
+
* WHAT: One schema for both pages.
|
|
9
|
+
*
|
|
10
|
+
* `slug`/`seo` are absent: the route derives them, and the sibling route
|
|
11
|
+
* already recomputes `seo.title`/`seo.description` on rename (verified, not
|
|
12
|
+
* assumed — it is one of the RC #39 instances that turned out already fixed).
|
|
13
|
+
*
|
|
14
|
+
* EXPORTS: sublistingCategoryFormSchema, type SublistingCategoryFormValues
|
|
15
|
+
*
|
|
16
|
+
* @tag domain:store-extensions
|
|
17
|
+
* @tag layer:schema
|
|
18
|
+
* @tag pattern:none
|
|
19
|
+
* @tag access:isomorphic
|
|
20
|
+
* @tag consumers:store/sublisting-categories pages
|
|
21
|
+
* @tag sideEffects:none
|
|
22
|
+
*/
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
25
|
+
export const sublistingCategoryFormSchema = z.object({
|
|
26
|
+
name: annotate(z.string().trim().min(2, "A name is required.").max(160), {
|
|
27
|
+
section: "basics", sectionLabel: "Sub-listing", sectionRequired: true,
|
|
28
|
+
quick: true, order: 1, row: "full",
|
|
29
|
+
}),
|
|
30
|
+
/*
|
|
31
|
+
* The catalogue identifier buyers match on — e.g. "108/120". Bounded and
|
|
32
|
+
* trimmed so it stays comparable; a stray space made two otherwise identical
|
|
33
|
+
* codes distinct.
|
|
34
|
+
*/
|
|
35
|
+
itemCode: annotate(z.string().trim().max(80, "Keep the item code under 80 characters.").optional(), {
|
|
36
|
+
section: "basics", quick: true, order: 2, row: "pair",
|
|
37
|
+
}),
|
|
38
|
+
description: annotate(z.string().trim().max(1000, "Keep the description under 1000 characters.").optional(), {
|
|
39
|
+
section: "basics", order: 3, row: "full", kind: "textarea",
|
|
40
|
+
}),
|
|
41
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -3286,3 +3286,8 @@ export type { NormalizedError, NormalizedApiError, NormalizedAppError, Normalize
|
|
|
3286
3286
|
export { CART_LANE, CART_LANE_PRIORITY, CART_LANE_LABELS, laneOf, activeLane, laneItems, laneCounts, isLaneCheckoutable, laneBlockReason, isLockedLane, canAddNewItems, type CartLane, type LaneAssignable, } from "./_internal/shared/checkout/lanes";
|
|
3287
3287
|
export { newsletterSubscribeSchema, quickCreateTaxonomySchema, payoutMarkPaidSchema, } from "./features/admin/schemas/small-forms";
|
|
3288
3288
|
export { raffleEntrySchema } from "./features/contact/schemas/inquiry-forms";
|
|
3289
|
+
export { orderCancelSchema, paymentProofSchema, } from "./features/orders/schemas/buyer-forms";
|
|
3290
|
+
export { scamReportFormSchema } from "./features/scams/schemas/report-form";
|
|
3291
|
+
export { sublistingCategoryFormSchema } from "./features/store-extensions/schemas/sublisting-category-form";
|
|
3292
|
+
export { adminAddressFormSchema, adminStoreUpdateSchema, announcementBarSchema, } from "./features/admin/schemas/admin-editor-forms";
|
|
3293
|
+
export { adminOrderUpdateSchema, employeeInviteSchema, hardBanReasonSchema, featureFlagsUpdateSchema, adminAdFormSchema, } from "./features/admin/schemas/admin-ops-forms";
|
package/dist/index.js
CHANGED
|
@@ -5695,3 +5695,8 @@ export { normalizeError, getErrorMessage, isApiNormalized, isAppNormalized, isFi
|
|
|
5695
5695
|
export { CART_LANE, CART_LANE_PRIORITY, CART_LANE_LABELS, laneOf, activeLane, laneItems, laneCounts, isLaneCheckoutable, laneBlockReason, isLockedLane, canAddNewItems, } from "./_internal/shared/checkout/lanes";
|
|
5696
5696
|
export { newsletterSubscribeSchema, quickCreateTaxonomySchema, payoutMarkPaidSchema, } from "./features/admin/schemas/small-forms";
|
|
5697
5697
|
export { raffleEntrySchema } from "./features/contact/schemas/inquiry-forms";
|
|
5698
|
+
export { orderCancelSchema, paymentProofSchema, } from "./features/orders/schemas/buyer-forms";
|
|
5699
|
+
export { scamReportFormSchema } from "./features/scams/schemas/report-form";
|
|
5700
|
+
export { sublistingCategoryFormSchema } from "./features/store-extensions/schemas/sublisting-category-form";
|
|
5701
|
+
export { adminAddressFormSchema, adminStoreUpdateSchema, announcementBarSchema, } from "./features/admin/schemas/admin-editor-forms";
|
|
5702
|
+
export { adminOrderUpdateSchema, employeeInviteSchema, hardBanReasonSchema, featureFlagsUpdateSchema, adminAdFormSchema, } from "./features/admin/schemas/admin-ops-forms";
|