@mohasinac/appkit 3.2.20 → 3.3.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/checkout/actions.d.ts +2 -0
- package/dist/_internal/server/features/checkout/actions.js +239 -190
- package/dist/_internal/server/functions/scheduled.d.ts +2 -1
- package/dist/_internal/server/functions/scheduled.js +9 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +8 -11
- package/dist/_internal/server/jobs/core/emiInstallmentReminder.d.ts +8 -0
- package/dist/_internal/server/jobs/core/emiInstallmentReminder.js +78 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.d.ts +2 -2
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -10
- package/dist/_internal/server/jobs/handlers/emiInstallmentReminder.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/emiInstallmentReminder.js +2 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +2 -0
- package/dist/_internal/server/providers/index.d.ts +4 -4
- package/dist/_internal/server/providers/index.js +4 -4
- package/dist/_internal/server/providers/payment/razorpay-mock.d.ts +3 -2
- package/dist/_internal/server/providers/payment/razorpay-mock.js +3 -1
- package/dist/_internal/server/providers/resolve.d.ts +10 -13
- package/dist/_internal/server/providers/resolve.js +9 -15
- package/dist/_internal/shared/actions/action-registry.js +28 -0
- package/dist/_internal/shared/checkout/rules/_defaults.js +1 -0
- package/dist/_internal/shared/checkout/rules/_registry.js +4 -0
- package/dist/_internal/shared/checkout/rules/art.rule.d.ts +9 -0
- package/dist/_internal/shared/checkout/rules/art.rule.js +5 -0
- package/dist/_internal/shared/checkout/rules/auction.rule.js +1 -0
- package/dist/_internal/shared/checkout/rules/classified.rule.js +1 -0
- package/dist/_internal/shared/checkout/rules/index.d.ts +2 -0
- package/dist/_internal/shared/checkout/rules/index.js +2 -0
- package/dist/_internal/shared/checkout/rules/stickers.rule.d.ts +9 -0
- package/dist/_internal/shared/checkout/rules/stickers.rule.js +5 -0
- package/dist/_internal/shared/checkout/rules/types.d.ts +6 -0
- package/dist/_internal/shared/features/checkout/config.d.ts +16 -1
- package/dist/_internal/shared/features/checkout/config.js +16 -1
- package/dist/_internal/shared/features/emi/schedule.d.ts +63 -0
- package/dist/_internal/shared/features/emi/schedule.js +62 -0
- package/dist/_internal/shared/fees/calculator.d.ts +53 -0
- package/dist/_internal/shared/fees/calculator.js +40 -0
- package/dist/_internal/shared/listing-types/_registry.d.ts +22 -66
- package/dist/_internal/shared/listing-types/_registry.js +19 -2
- package/dist/_internal/shared/listing-types/art/config.d.ts +15 -0
- package/dist/_internal/shared/listing-types/art/config.js +14 -0
- package/dist/_internal/shared/listing-types/art/ctas.d.ts +1 -0
- package/dist/_internal/shared/listing-types/art/ctas.js +3 -0
- package/dist/_internal/shared/listing-types/art/og.d.ts +1 -0
- package/dist/_internal/shared/listing-types/art/og.js +1 -0
- package/dist/_internal/shared/listing-types/art/schema.d.ts +2 -0
- package/dist/_internal/shared/listing-types/art/schema.js +3 -0
- package/dist/_internal/shared/listing-types/art/seed-factory.d.ts +1 -0
- package/dist/_internal/shared/listing-types/art/seed-factory.js +1 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +8 -0
- package/dist/_internal/shared/listing-types/auction/config.js +6 -0
- package/dist/_internal/shared/listing-types/capabilities.js +17 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +8 -0
- package/dist/_internal/shared/listing-types/classified/config.js +6 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +8 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +6 -0
- package/dist/_internal/shared/listing-types/live/config.d.ts +8 -0
- package/dist/_internal/shared/listing-types/live/config.js +6 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +8 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +6 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +8 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +6 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +8 -0
- package/dist/_internal/shared/listing-types/standard/config.js +6 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +15 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +14 -0
- package/dist/_internal/shared/listing-types/stickers/ctas.d.ts +1 -0
- package/dist/_internal/shared/listing-types/stickers/ctas.js +3 -0
- package/dist/_internal/shared/listing-types/stickers/og.d.ts +1 -0
- package/dist/_internal/shared/listing-types/stickers/og.js +1 -0
- package/dist/_internal/shared/listing-types/stickers/schema.d.ts +2 -0
- package/dist/_internal/shared/listing-types/stickers/schema.js +3 -0
- package/dist/_internal/shared/listing-types/stickers/seed-factory.d.ts +1 -0
- package/dist/_internal/shared/listing-types/stickers/seed-factory.js +1 -0
- package/dist/client.d.ts +7 -2
- package/dist/client.js +4 -2
- package/dist/constants/field-names.d.ts +2 -6
- package/dist/constants/field-names.js +2 -6
- package/dist/contracts/index.d.ts +4 -2
- package/dist/contracts/index.js +6 -0
- package/dist/contracts/payment.d.ts +14 -9
- package/dist/contracts/payment.js +11 -1
- package/dist/contracts/shipping.d.ts +14 -9
- package/dist/contracts/shipping.js +11 -1
- package/dist/features/about/components/PublicProfileView.js +3 -6
- package/dist/features/admin/components/AdminAddressBookView.d.ts +11 -0
- package/dist/features/admin/components/AdminAddressBookView.js +30 -0
- package/dist/features/admin/components/AdminArtView.d.ts +3 -0
- package/dist/features/admin/components/AdminArtView.js +45 -0
- package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +14 -9
- package/dist/features/admin/components/AdminStickersView.d.ts +3 -0
- package/dist/features/admin/components/AdminStickersView.js +45 -0
- package/dist/features/admin/components/index.d.ts +6 -0
- package/dist/features/admin/components/index.js +4 -0
- package/dist/features/admin/schemas/firestore.d.ts +19 -11
- package/dist/features/admin/schemas/firestore.js +12 -2
- package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +3 -2
- package/dist/features/auth/hooks/useAuth.js +35 -9
- package/dist/features/auth/schemas/firestore.d.ts +2 -6
- package/dist/features/cart/schemas/firestore.d.ts +3 -2
- package/dist/features/consultation/schemas/index.d.ts +2 -2
- package/dist/features/loyalty/schemas/index.d.ts +6 -6
- package/dist/features/orders/repository/orders.repository.d.ts +20 -2
- package/dist/features/orders/repository/orders.repository.js +34 -5
- package/dist/features/orders/schemas/firestore.d.ts +33 -7
- package/dist/features/orders/schemas/firestore.js +1 -1
- package/dist/features/orders/schemas/index.d.ts +176 -31
- package/dist/features/orders/schemas/index.js +20 -6
- package/dist/features/orders/types/index.d.ts +3 -3
- package/dist/features/payments/repository/payout.repository.js +2 -2
- package/dist/features/payments/schemas/firestore.d.ts +3 -3
- package/dist/features/payments/schemas/firestore.js +2 -2
- package/dist/features/payments/schemas/index.d.ts +5 -5
- package/dist/features/payments/schemas/index.js +2 -2
- package/dist/features/products/api/[id]/route.js +4 -1
- package/dist/features/products/api/route.js +4 -1
- package/dist/features/products/components/CompareOverlay.js +7 -9
- package/dist/features/products/components/ProductDetailPageView.d.ts +3 -1
- package/dist/features/products/components/ProductDetailPageView.js +2 -2
- package/dist/features/products/components/ProductForm.js +18 -7
- package/dist/features/products/components/ProductGrid.js +7 -12
- package/dist/features/products/components/ShowGroupSection.js +3 -5
- package/dist/features/products/components/SublistingCarouselSection.js +3 -6
- package/dist/features/products/constants/listing-tabs.d.ts +32 -0
- package/dist/features/products/constants/listing-tabs.js +8 -0
- package/dist/features/products/index.d.ts +1 -1
- package/dist/features/products/index.js +3 -1
- package/dist/features/products/schemas/firestore.d.ts +20 -1
- package/dist/features/products/schemas/firestore.js +2 -0
- package/dist/features/products/schemas/index.d.ts +48 -0
- package/dist/features/products/schemas/index.js +9 -0
- package/dist/features/products/schemas/product-features.d.ts +1 -1
- package/dist/features/products/types/index.d.ts +5 -3
- package/dist/features/products/utils/listing-type.d.ts +6 -0
- package/dist/features/products/utils/listing-type.js +3 -0
- package/dist/features/reviews/api/[id]/route.js +5 -4
- package/dist/features/seller/actions/seller-actions.d.ts +29 -14
- package/dist/features/seller/actions/seller-actions.js +101 -27
- package/dist/features/seller/components/SellerArtView.d.ts +7 -0
- package/dist/features/seller/components/SellerArtView.js +114 -0
- package/dist/features/seller/components/SellerOrdersView.js +38 -1
- package/dist/features/seller/components/SellerProductShell.d.ts +5 -1
- package/dist/features/seller/components/SellerProductShell.js +69 -59
- package/dist/features/seller/components/SellerShippingView.js +6 -25
- package/dist/features/seller/components/SellerStickersView.d.ts +7 -0
- package/dist/features/seller/components/SellerStickersView.js +114 -0
- package/dist/features/seller/components/index.d.ts +4 -0
- package/dist/features/seller/components/index.js +2 -0
- package/dist/features/seller/messages/en.json +1 -1
- package/dist/features/seller/schemas/index.d.ts +7 -7
- package/dist/features/seller/schemas/index.js +1 -1
- package/dist/features/seller/types/index.d.ts +1 -1
- package/dist/features/stores/components/StoreDetailLayoutView.js +18 -2
- package/dist/features/stores/schemas/firestore.d.ts +7 -1
- package/dist/features/stores/schemas/index.d.ts +19 -16
- package/dist/features/stores/schemas/index.js +1 -1
- package/dist/index.d.ts +21 -36
- package/dist/index.js +27 -39
- package/dist/next/routing/route-map.d.ts +22 -0
- package/dist/next/routing/route-map.js +10 -0
- package/dist/providers/payment-manual/index.d.ts +40 -0
- package/dist/providers/payment-manual/index.js +91 -0
- package/dist/providers/payment-razorpay/index.d.ts +4 -2
- package/dist/providers/payment-razorpay/index.js +4 -1
- package/dist/providers/shipping-manual/index.d.ts +22 -0
- package/dist/providers/shipping-manual/index.js +47 -0
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +5 -3
- package/dist/schemas/registry.d.ts +129 -70
- package/dist/schemas/registry.js +0 -4
- package/dist/security/pii-encrypt.js +0 -7
- package/dist/security/pii-redact.js +0 -1
- package/dist/seed/conversations-seed-data.js +2 -2
- package/dist/seed/faq-seed-data.js +7 -7
- package/dist/seed/grouped-listings-seed-data.d.ts +2 -0
- package/dist/seed/index.d.ts +2 -0
- package/dist/seed/index.js +2 -0
- package/dist/seed/manifest.js +20 -16
- package/dist/seed/payouts-seed-data.js +5 -5
- package/dist/seed/products-art-seed-data.d.ts +11 -0
- package/dist/seed/products-art-seed-data.js +125 -0
- package/dist/seed/products-preorders-seed-data.d.ts +2 -0
- package/dist/seed/products-stickers-seed-data.d.ts +11 -0
- package/dist/seed/products-stickers-seed-data.js +125 -0
- package/dist/seed/site-settings-seed-data.js +13 -4
- package/dist/seed/stores-seed-data.js +11 -11
- package/dist/server.d.ts +7 -15
- package/dist/server.js +17 -42
- package/package.json +1 -1
|
@@ -5,10 +5,8 @@
|
|
|
5
5
|
* explicit params. Called by letitrip thin wrappers that handle auth,
|
|
6
6
|
* rate-limiting, and user-facing validation.
|
|
7
7
|
*
|
|
8
|
-
* Note:
|
|
9
|
-
*
|
|
10
|
-
* thin wrapper because they use the @/lib/shiprocket/client SDK which is
|
|
11
|
-
* a permanent letitrip-only dependency.
|
|
8
|
+
* Note: updateSellerShipping remains in the letitrip thin wrapper
|
|
9
|
+
* (src/actions/seller.actions.ts) alongside the other seller server actions.
|
|
12
10
|
*/
|
|
13
11
|
import { serverLogger } from "../../../monitoring";
|
|
14
12
|
import { isAdminUser, isSellerUser } from "../../auth/role-predicates";
|
|
@@ -23,7 +21,8 @@ import { payoutRepository } from "../../payments/repository/payout.repository";
|
|
|
23
21
|
import { PAYOUT_FIELDS } from "../../payments/schemas";
|
|
24
22
|
import { couponsRepository } from "../../promotions/repository/coupons.repository";
|
|
25
23
|
import { generateStoreSlug } from "../../stores/schemas/firestore";
|
|
26
|
-
import {
|
|
24
|
+
import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
|
|
25
|
+
import { computePayoutDeduction } from "../../../_internal/shared/fees/calculator";
|
|
27
26
|
import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
28
27
|
import { finalizeStagedMediaUrl, finalizeStagedMediaField, finalizeStagedMediaArray, } from "../../media/finalize";
|
|
29
28
|
// --- Helper -------------------------------------------------------------------
|
|
@@ -169,14 +168,17 @@ async function computeSellerEarnings(sellerId) {
|
|
|
169
168
|
const paidOutIds = await payoutRepository.getPaidOutOrderIds(sellerId);
|
|
170
169
|
const eligibleOrders = deliveredOrders.filter((o) => !paidOutIds.has(o.id));
|
|
171
170
|
const grossAmount = eligibleOrders.reduce((sum, o) => sum + (o.totalPrice ?? 0), 0);
|
|
172
|
-
const
|
|
173
|
-
const
|
|
171
|
+
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
172
|
+
const deduction = computePayoutDeduction(grossAmount, siteSettings.commissions);
|
|
174
173
|
return {
|
|
175
174
|
eligibleOrders,
|
|
176
175
|
grossAmount,
|
|
177
|
-
platformFee,
|
|
178
|
-
|
|
176
|
+
platformFee: deduction.platformFee,
|
|
177
|
+
gatewayFee: deduction.gatewayFee,
|
|
178
|
+
gstAmount: deduction.gstOnFee,
|
|
179
|
+
netAmount: deduction.netAmount,
|
|
179
180
|
productCount: products.length,
|
|
181
|
+
commissions: siteSettings.commissions,
|
|
180
182
|
};
|
|
181
183
|
}
|
|
182
184
|
export async function requestPayout(userId, userName, userEmail, input) {
|
|
@@ -195,7 +197,11 @@ export async function requestPayout(userId, userName, userEmail, input) {
|
|
|
195
197
|
amount: earnings.netAmount,
|
|
196
198
|
grossAmount: earnings.grossAmount,
|
|
197
199
|
platformFee: earnings.platformFee,
|
|
198
|
-
platformFeeRate:
|
|
200
|
+
platformFeeRate: earnings.commissions.platformFeePercent / 100,
|
|
201
|
+
gatewayFee: earnings.gatewayFee,
|
|
202
|
+
gatewayFeeRate: (earnings.commissions.gatewayFeePercent ?? 0) / 100,
|
|
203
|
+
gstAmount: earnings.gstAmount,
|
|
204
|
+
gstRate: earnings.commissions.gstPercent / 100,
|
|
199
205
|
currency: getDefaultCurrency(),
|
|
200
206
|
paymentMethod: input.paymentMethod,
|
|
201
207
|
...(input.bankAccount && { bankAccount: input.bankAccount }),
|
|
@@ -245,23 +251,32 @@ export async function bulkSellerOrder(userId, userRole, userDisplayName, userEma
|
|
|
245
251
|
skipped.push(id);
|
|
246
252
|
continue;
|
|
247
253
|
}
|
|
254
|
+
if (order.emiEnabled && !order.emiComplete) {
|
|
255
|
+
// Delivered via the seller's allowShipBeforeEmiComplete override, but the
|
|
256
|
+
// buyer hasn't finished paying — hold the payout until emiComplete.
|
|
257
|
+
skipped.push(id);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
248
260
|
eligible.push(order);
|
|
249
261
|
}
|
|
250
262
|
if (eligible.length === 0)
|
|
251
263
|
throw new ValidationError("No eligible orders found.");
|
|
252
|
-
const PLATFORM_COMMISSION_RATE = DEFAULT_PLATFORM_FEE_RATE;
|
|
253
264
|
const grossAmount = eligible.reduce((sum, o) => sum + (o.totalPrice ?? 0), 0);
|
|
254
|
-
const
|
|
255
|
-
const
|
|
265
|
+
const bulkSiteSettings = await siteSettingsRepository.getSingleton();
|
|
266
|
+
const bulkDeduction = computePayoutDeduction(grossAmount, bulkSiteSettings.commissions);
|
|
256
267
|
const payoutDoc = await payoutRepository.create({
|
|
257
268
|
storeId: userId,
|
|
258
269
|
sellerName: userDisplayName,
|
|
259
270
|
sellerEmail: userEmail,
|
|
260
271
|
orderIds: eligible.map((o) => o.id),
|
|
261
|
-
amount: netAmount,
|
|
272
|
+
amount: bulkDeduction.netAmount,
|
|
262
273
|
grossAmount,
|
|
263
|
-
platformFee,
|
|
264
|
-
platformFeeRate:
|
|
274
|
+
platformFee: bulkDeduction.platformFee,
|
|
275
|
+
platformFeeRate: bulkSiteSettings.commissions.platformFeePercent / 100,
|
|
276
|
+
gatewayFee: bulkDeduction.gatewayFee,
|
|
277
|
+
gatewayFeeRate: (bulkSiteSettings.commissions.gatewayFeePercent ?? 0) / 100,
|
|
278
|
+
gstAmount: bulkDeduction.gstOnFee,
|
|
279
|
+
gstRate: bulkSiteSettings.commissions.gstPercent / 100,
|
|
265
280
|
currency: getDefaultCurrency(),
|
|
266
281
|
paymentMethod: userDoc.payoutDetails.method === "upi"
|
|
267
282
|
? "upi"
|
|
@@ -281,7 +296,7 @@ export async function bulkSellerOrder(userId, userRole, userDisplayName, userEma
|
|
|
281
296
|
userId,
|
|
282
297
|
payoutId,
|
|
283
298
|
orderCount: eligible.length,
|
|
284
|
-
netAmount,
|
|
299
|
+
netAmount: bulkDeduction.netAmount,
|
|
285
300
|
});
|
|
286
301
|
return {
|
|
287
302
|
payoutId,
|
|
@@ -289,9 +304,9 @@ export async function bulkSellerOrder(userId, userRole, userDisplayName, userEma
|
|
|
289
304
|
skipped,
|
|
290
305
|
eligibleCount: eligible.length,
|
|
291
306
|
skippedCount: skipped.length,
|
|
292
|
-
netAmount,
|
|
307
|
+
netAmount: bulkDeduction.netAmount,
|
|
293
308
|
grossAmount,
|
|
294
|
-
platformFee,
|
|
309
|
+
platformFee: bulkDeduction.platformFee,
|
|
295
310
|
};
|
|
296
311
|
}
|
|
297
312
|
// --- Create Seller Product ----------------------------------------------------
|
|
@@ -314,14 +329,7 @@ export async function getSellerShipping(userId) {
|
|
|
314
329
|
const userData = await userRepository.findById(userId);
|
|
315
330
|
if (!userData)
|
|
316
331
|
return null;
|
|
317
|
-
|
|
318
|
-
if (!config)
|
|
319
|
-
return null;
|
|
320
|
-
if (config.method === "shiprocket" && config.shiprocketToken) {
|
|
321
|
-
const { shiprocketToken: _removed, ...safe } = config;
|
|
322
|
-
return safe;
|
|
323
|
-
}
|
|
324
|
-
return config;
|
|
332
|
+
return userData.shippingConfig ?? null;
|
|
325
333
|
}
|
|
326
334
|
export async function getSellerPayoutSettings(userId) {
|
|
327
335
|
const userData = await userRepository.findById(userId);
|
|
@@ -458,6 +466,23 @@ export async function sellerDeleteProduct(userId, userRole, productId) {
|
|
|
458
466
|
serverLogger.info("sellerDeleteProduct", { userId, productId });
|
|
459
467
|
}
|
|
460
468
|
// --- Custom Ship Order --------------------------------------------------------
|
|
469
|
+
/**
|
|
470
|
+
* EMI orders stay unshipped until every installment is paid, UNLESS every
|
|
471
|
+
* product in the order has its `allowShipBeforeEmiComplete` flag set. This
|
|
472
|
+
* is the single choke point for the "ship" transition, so it can't be
|
|
473
|
+
* bypassed by a different route/action reaching `orderRepository.update`
|
|
474
|
+
* directly with `status: "shipped"`.
|
|
475
|
+
*/
|
|
476
|
+
export async function assertEmiShippable(order) {
|
|
477
|
+
if (!order.emiEnabled || order.emiComplete)
|
|
478
|
+
return;
|
|
479
|
+
const productIds = (order.items ?? []).map((i) => i.productId).filter(Boolean);
|
|
480
|
+
const products = await Promise.all(productIds.map((id) => productRepository.findById(id)));
|
|
481
|
+
const allAllowEarlyShip = products.length > 0 && products.every((p) => p?.allowShipBeforeEmiComplete === true);
|
|
482
|
+
if (!allAllowEarlyShip) {
|
|
483
|
+
throw new ValidationError("This order is still on an EMI plan — it can't ship until all installments are paid, unless the seller has enabled early shipping for every item in the order.");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
461
486
|
export async function customShipOrder(userId, userRole, orderId, input) {
|
|
462
487
|
const order = await orderRepository.findById(orderId);
|
|
463
488
|
if (!order)
|
|
@@ -469,6 +494,7 @@ export async function customShipOrder(userId, userRole, orderId, input) {
|
|
|
469
494
|
throw new ValidationError("Order is already shipped");
|
|
470
495
|
if (order.status !== OrderStatusValues.CONFIRMED)
|
|
471
496
|
throw new ValidationError("Order must be confirmed before shipping");
|
|
497
|
+
await assertEmiShippable(order);
|
|
472
498
|
await orderRepository.update(orderId, {
|
|
473
499
|
status: OrderStatusValues.SHIPPED,
|
|
474
500
|
shippingMethod: "custom",
|
|
@@ -485,3 +511,51 @@ export async function customShipOrder(userId, userRole, orderId, input) {
|
|
|
485
511
|
});
|
|
486
512
|
return { orderId, method: "custom" };
|
|
487
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Records collection of one EMI installment. Seller/admin marks the
|
|
516
|
+
* installment paid after verifying the buyer's manual transfer (UTR +
|
|
517
|
+
* proof). Recomputes `emiRemainingBalance` from the still-pending
|
|
518
|
+
* installments and flips `emiComplete` once none remain — that flag is the
|
|
519
|
+
* single gate `assertEmiShippable` reads, so this is the only place an
|
|
520
|
+
* EMI order can become shippable.
|
|
521
|
+
*/
|
|
522
|
+
export async function markEmiInstallmentPaid(userId, userRole, orderId, input) {
|
|
523
|
+
const order = await orderRepository.findById(orderId);
|
|
524
|
+
if (!order)
|
|
525
|
+
throw new NotFoundError("Order not found");
|
|
526
|
+
if (userRole !== "admin" && order.storeId !== userId)
|
|
527
|
+
throw new AuthorizationError("You do not own this order");
|
|
528
|
+
if (!order.emiEnabled)
|
|
529
|
+
throw new ValidationError("This order is not on an EMI plan");
|
|
530
|
+
const installments = order.emiInstallments ?? [];
|
|
531
|
+
const target = installments.find((i) => i.index === input.installmentIndex);
|
|
532
|
+
if (!target)
|
|
533
|
+
throw new ValidationError(`Installment ${input.installmentIndex} not found on this order`);
|
|
534
|
+
if (target.status === "paid")
|
|
535
|
+
throw new ValidationError("This installment is already marked paid");
|
|
536
|
+
const updatedInstallments = installments.map((i) => i.index === input.installmentIndex
|
|
537
|
+
? {
|
|
538
|
+
...i,
|
|
539
|
+
status: "paid",
|
|
540
|
+
paidAt: new Date(),
|
|
541
|
+
transactionId: input.transactionId,
|
|
542
|
+
proofUrl: input.proofUrl,
|
|
543
|
+
}
|
|
544
|
+
: i);
|
|
545
|
+
const emiRemainingBalance = updatedInstallments
|
|
546
|
+
.filter((i) => i.status !== "paid")
|
|
547
|
+
.reduce((sum, i) => sum + i.amount, 0);
|
|
548
|
+
const emiComplete = updatedInstallments.every((i) => i.status === "paid");
|
|
549
|
+
const updated = await orderRepository.update(orderId, {
|
|
550
|
+
emiInstallments: updatedInstallments,
|
|
551
|
+
emiRemainingBalance,
|
|
552
|
+
emiComplete,
|
|
553
|
+
});
|
|
554
|
+
serverLogger.info("markEmiInstallmentPaid", {
|
|
555
|
+
orderId,
|
|
556
|
+
userId,
|
|
557
|
+
installmentIndex: input.installmentIndex,
|
|
558
|
+
emiComplete,
|
|
559
|
+
});
|
|
560
|
+
return updated;
|
|
561
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface SellerArtViewProps {
|
|
2
|
+
onCreateClick?: () => void;
|
|
3
|
+
onEditClick?: (id: string) => void;
|
|
4
|
+
onDelete?: (id: string) => Promise<void>;
|
|
5
|
+
onBulkDelete?: (ids: string[]) => Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare function SellerArtView({ onCreateClick, onEditClick, onDelete, onBulkDelete, }: SellerArtViewProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* SellerArtView — seller browse/manage of art print listings
|
|
5
|
+
* (EMI/art-stickers session). Mirrors SellerClassifiedView's pattern.
|
|
6
|
+
*/
|
|
7
|
+
import { Badge, sortBy } from "@mohasinac/appkit";
|
|
8
|
+
import { useState, useCallback } from "react";
|
|
9
|
+
import { useEntityDelete } from "../../../react/hooks/useEntityDelete";
|
|
10
|
+
import { ConfirmDeleteModal, RowActionMenu, Text } from "../../../ui";
|
|
11
|
+
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
12
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
13
|
+
import { ROUTES } from "../../..";
|
|
14
|
+
import { toRecordArray, toRelativeDate, toRupees, toStringValue, } from "../../admin/hooks/useAdminListingData";
|
|
15
|
+
import { DataListingView } from "../../admin/components/DataListingView";
|
|
16
|
+
const COLUMNS = [
|
|
17
|
+
{ key: "title", header: "Listing", render: (row) => _jsx(Text, { size: "sm", weight: "medium", children: row.title }) },
|
|
18
|
+
{ key: "price", header: "Price", render: (row) => _jsx(Text, { className: "tabular-nums", size: "sm", children: row.price }) },
|
|
19
|
+
{
|
|
20
|
+
key: "size",
|
|
21
|
+
header: "Size",
|
|
22
|
+
render: (row) => _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: row.size || "—" }),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
key: "status",
|
|
26
|
+
header: "Status",
|
|
27
|
+
render: (row) => (_jsx(Badge, { variant: row.status === "active" ? "active" : "inactive", size: "xs", className: "capitalize", children: row.status })),
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: "createdAt",
|
|
31
|
+
header: "Created",
|
|
32
|
+
render: (row) => _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: row.createdAt }),
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
export function SellerArtView({ onCreateClick, onEditClick, onDelete, onBulkDelete, }) {
|
|
36
|
+
const [deleteTargetId, setDeleteTargetId] = useState(null);
|
|
37
|
+
const { deletingId, handleDelete: performDelete } = useEntityDelete({
|
|
38
|
+
endpoint: (id) => `/api/store/products/${id}`,
|
|
39
|
+
deleteFn: onDelete,
|
|
40
|
+
successMessage: "Listing deleted.",
|
|
41
|
+
fetchOptions: { credentials: "include" },
|
|
42
|
+
});
|
|
43
|
+
const handleDelete = useCallback(async (id) => {
|
|
44
|
+
await performDelete(id);
|
|
45
|
+
setDeleteTargetId(null);
|
|
46
|
+
}, [performDelete]);
|
|
47
|
+
const handleEdit = useCallback((id) => {
|
|
48
|
+
if (onEditClick)
|
|
49
|
+
onEditClick(id);
|
|
50
|
+
else
|
|
51
|
+
window.location.href = String(ROUTES.STORE.ART_EDIT(id));
|
|
52
|
+
}, [onEditClick]);
|
|
53
|
+
const handleCreate = useCallback(() => {
|
|
54
|
+
if (onCreateClick)
|
|
55
|
+
onCreateClick();
|
|
56
|
+
else
|
|
57
|
+
window.location.href = String(ROUTES.STORE.ART_NEW);
|
|
58
|
+
}, [onCreateClick]);
|
|
59
|
+
const config = {
|
|
60
|
+
portal: "seller",
|
|
61
|
+
title: "Art",
|
|
62
|
+
searchPlaceholder: "Search art prints...",
|
|
63
|
+
emptyLabel: "No art listings yet — list your first print",
|
|
64
|
+
filterKeys: [],
|
|
65
|
+
defaultSort: sortBy("createdAt", "DESC"),
|
|
66
|
+
queryKey: ["seller", "art"],
|
|
67
|
+
endpoint: SELLER_ENDPOINTS.PRODUCTS,
|
|
68
|
+
sortOptions: [
|
|
69
|
+
{ value: sortBy("createdAt", "DESC"), label: "Newest" },
|
|
70
|
+
{ value: sortBy("createdAt", "ASC"), label: "Oldest" },
|
|
71
|
+
{ value: "productTitle", label: "Name A–Z" },
|
|
72
|
+
{ value: sortBy("price", "ASC"), label: "Price: Low–High" },
|
|
73
|
+
{ value: sortBy("price", "DESC"), label: "Price: High–Low" },
|
|
74
|
+
],
|
|
75
|
+
columns: COLUMNS,
|
|
76
|
+
mapRows: (response) => toRecordArray(response.products).map((item, index) => {
|
|
77
|
+
const printMeta = (item.printMeta ?? {});
|
|
78
|
+
return {
|
|
79
|
+
id: toStringValue(item.id, `art-${index}`),
|
|
80
|
+
title: toStringValue(item.productTitle ?? item.title, "Untitled"),
|
|
81
|
+
price: toRupees(item.price),
|
|
82
|
+
size: toStringValue(printMeta.size, ""),
|
|
83
|
+
status: toStringValue(item.status, "draft"),
|
|
84
|
+
createdAt: toRelativeDate(item.createdAt),
|
|
85
|
+
};
|
|
86
|
+
}),
|
|
87
|
+
getTotal: (response, mappedRows) => typeof response.meta?.total === "number" ? response.meta.total : mappedRows.length,
|
|
88
|
+
buildFilters: () => "listingType==art",
|
|
89
|
+
primaryAction: { label: "New Art Print", onClick: () => handleCreate() },
|
|
90
|
+
buildBulkActions: onBulkDelete
|
|
91
|
+
? (selection) => [
|
|
92
|
+
{
|
|
93
|
+
id: "bulk-delete",
|
|
94
|
+
label: ACTIONS.STORE["delete-listing"].label,
|
|
95
|
+
variant: "danger",
|
|
96
|
+
onClick: async () => {
|
|
97
|
+
await onBulkDelete(selection.selectedIds);
|
|
98
|
+
selection.clearSelection();
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
]
|
|
102
|
+
: undefined,
|
|
103
|
+
renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
|
|
104
|
+
{ label: ACTIONS.STORE["edit-listing"].label, onClick: () => handleEdit(row.id) },
|
|
105
|
+
{
|
|
106
|
+
label: ACTIONS.STORE["delete-listing"].label,
|
|
107
|
+
destructive: true,
|
|
108
|
+
onClick: () => setDeleteTargetId(row.id),
|
|
109
|
+
disabled: deletingId === row.id,
|
|
110
|
+
},
|
|
111
|
+
] })),
|
|
112
|
+
};
|
|
113
|
+
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), deleteTargetId && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Art Listing", message: "Are you sure you want to delete this art listing? This cannot be undone.", onConfirm: () => handleDelete(deleteTargetId), onClose: () => setDeleteTargetId(null), isDeleting: deletingId === deleteTargetId }))] }));
|
|
114
|
+
}
|
|
@@ -48,6 +48,11 @@ const UPDATE_STATUS_OPTIONS = [
|
|
|
48
48
|
{ value: "delivered", label: "Delivered" },
|
|
49
49
|
{ value: "cancelled", label: "Cancelled" },
|
|
50
50
|
];
|
|
51
|
+
const EMI_INSTALLMENT_BADGE_VARIANT = {
|
|
52
|
+
paid: "success",
|
|
53
|
+
pending: "warning",
|
|
54
|
+
overdue: "danger",
|
|
55
|
+
};
|
|
51
56
|
// ---------------------------------------------------------------------------
|
|
52
57
|
// Order Detail Drawer
|
|
53
58
|
// ---------------------------------------------------------------------------
|
|
@@ -61,6 +66,9 @@ function OrderDetailDrawer({ orderId, apiBase, onClose, }) {
|
|
|
61
66
|
const [trackingNumber, setTrackingNumber] = useState("");
|
|
62
67
|
const [carrier, setCarrier] = useState("");
|
|
63
68
|
const [trackingUrl, setTrackingUrl] = useState("");
|
|
69
|
+
const [markingPaidIndex, setMarkingPaidIndex] = useState(null);
|
|
70
|
+
const [emiError, setEmiError] = useState(null);
|
|
71
|
+
const { showToast } = useToast();
|
|
64
72
|
React.useEffect(() => {
|
|
65
73
|
setLoading(true);
|
|
66
74
|
setFetchError(null);
|
|
@@ -76,6 +84,31 @@ function OrderDetailDrawer({ orderId, apiBase, onClose, }) {
|
|
|
76
84
|
.catch(() => setFetchError("Failed to load order details"))
|
|
77
85
|
.finally(() => setLoading(false));
|
|
78
86
|
}, [orderId, apiBase]);
|
|
87
|
+
const handleMarkInstallmentPaid = async (installmentIndex) => {
|
|
88
|
+
setMarkingPaidIndex(installmentIndex);
|
|
89
|
+
setEmiError(null);
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(`${apiBase}/${orderId}/emi-installment`, {
|
|
92
|
+
method: "PATCH",
|
|
93
|
+
headers: { "Content-Type": "application/json" },
|
|
94
|
+
body: JSON.stringify({ installmentIndex }),
|
|
95
|
+
});
|
|
96
|
+
const json = await res.json().catch(() => ({}));
|
|
97
|
+
if (!res.ok)
|
|
98
|
+
throw new Error(json?.error ?? "Failed to mark installment paid");
|
|
99
|
+
setOrder(json?.data ?? json);
|
|
100
|
+
showToast("Installment marked paid.", "success");
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
void normalizeError(err);
|
|
104
|
+
const message = err instanceof Error ? err.message : "Failed to mark installment paid.";
|
|
105
|
+
setEmiError(message);
|
|
106
|
+
showToast(message, "error");
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
setMarkingPaidIndex(null);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
79
112
|
const handleSave = async () => {
|
|
80
113
|
if (!order)
|
|
81
114
|
return;
|
|
@@ -118,7 +151,11 @@ function OrderDetailDrawer({ orderId, apiBase, onClose, }) {
|
|
|
118
151
|
};
|
|
119
152
|
const addr = order?.shippingAddress ?? {};
|
|
120
153
|
const addrLine = [addr.addressLine1, addr.city, addr.state, addr.pincode].filter(Boolean).join(", ");
|
|
121
|
-
return (_jsxs(SideDrawer, { isOpen: true, title: `Order ${order?.id ?? orderId}`, onClose: onClose, children: [loading && (_jsx(Row, { align: "center", justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), fetchError && (_jsx(Div, { textSize: "sm", className: "mx-4 mt-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "lg", children: fetchError })), order && !loading && (_jsxs(Stack, { gap: "none", children: [_jsxs(Stack, { className: `flex-1 ${__O.yAuto}`, gap: "5", padding: "md", children: [_jsxs(Row, { align: "center", justify: "between", children: [_jsx(Badge, { variant: STATUS_BADGE_VARIANT[order.status?.toUpperCase()] ?? "default", children: order.status ?? "Unknown" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: toRelativeDate(order.createdAt) })] }), (order.items ?? []).length > 0 && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-primary)] mb-2", weight: "semibold", children: "Items" }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.items ?? []).map((item, i) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { size: "sm", className: "truncate", weight: "medium", children: item.title ?? item.productId ?? "Item" }), _jsxs(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: ["Qty: ", item.quantity ?? 1] })] }), _jsx(Text, { size: "sm", className: "shrink-0", weight: "medium", children: toRupees(item.price ?? 0) })] }, i))) })] })), _jsxs(Row, { surface: "muted", padding: "inline", align: "center", justify: "between", rounded: "lg", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Total" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-primary)]", weight: "bold", children: toRupees(order.totalAmount ?? 0) })] }), addrLine && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "mb-1", weight: "semibold", children: "Shipping address" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: [String(addr.fullName ?? ""), addrLine].filter(Boolean).join(" · ") })] })), order.paymentMethod && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "mb-1", weight: "semibold", children: "Payment" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", transform: "capitalize", children: order.paymentMethod })] })), _jsxs(
|
|
154
|
+
return (_jsxs(SideDrawer, { isOpen: true, title: `Order ${order?.id ?? orderId}`, onClose: onClose, children: [loading && (_jsx(Row, { align: "center", justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), fetchError && (_jsx(Div, { textSize: "sm", className: "mx-4 mt-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "lg", children: fetchError })), order && !loading && (_jsxs(Stack, { gap: "none", children: [_jsxs(Stack, { className: `flex-1 ${__O.yAuto}`, gap: "5", padding: "md", children: [_jsxs(Row, { align: "center", justify: "between", children: [_jsx(Badge, { variant: STATUS_BADGE_VARIANT[order.status?.toUpperCase()] ?? "default", children: order.status ?? "Unknown" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: toRelativeDate(order.createdAt) })] }), (order.items ?? []).length > 0 && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-primary)] mb-2", weight: "semibold", children: "Items" }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.items ?? []).map((item, i) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { size: "sm", className: "truncate", weight: "medium", children: item.title ?? item.productId ?? "Item" }), _jsxs(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: ["Qty: ", item.quantity ?? 1] })] }), _jsx(Text, { size: "sm", className: "shrink-0", weight: "medium", children: toRupees(item.price ?? 0) })] }, i))) })] })), _jsxs(Row, { surface: "muted", padding: "inline", align: "center", justify: "between", rounded: "lg", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Total" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-primary)]", weight: "bold", children: toRupees(order.totalAmount ?? 0) })] }), addrLine && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "mb-1", weight: "semibold", children: "Shipping address" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: [String(addr.fullName ?? ""), addrLine].filter(Boolean).join(" · ") })] })), order.paymentMethod && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "mb-1", weight: "semibold", children: "Payment" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", transform: "capitalize", children: order.paymentMethod })] })), order.emiEnabled && (_jsxs(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", children: [_jsxs(Row, { align: "center", justify: "between", className: "mb-2", children: [_jsxs(Text, { size: "sm", weight: "semibold", children: ["EMI plan ", order.emiTenureMonths ? `· ${order.emiTenureMonths} months` : ""] }), _jsx(Badge, { variant: order.emiComplete ? "success" : "warning", children: order.emiComplete ? "Fully paid" : "In progress" })] }), _jsxs(Row, { align: "center", justify: "between", className: "mb-2", children: [_jsx(Text, { size: "xs", color: "muted", children: "Token collected" }), _jsx(Text, { size: "xs", weight: "medium", children: toRupees(order.emiTokenAmount ?? 0) })] }), _jsxs(Row, { align: "center", justify: "between", className: "mb-3", children: [_jsx(Text, { size: "xs", color: "muted", children: "Remaining balance" }), _jsx(Text, { size: "xs", weight: "medium", children: toRupees(order.emiRemainingBalance ?? 0) })] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.emiInstallments ?? []).map((inst) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { className: "min-w-0", children: [_jsxs(Text, { size: "sm", weight: "medium", children: ["Installment ", inst.index, " \u00B7 ", toRupees(inst.amount)] }), _jsx(Text, { size: "xs", color: "muted", children: inst.status === "paid" && inst.paidAt
|
|
155
|
+
? `Paid ${toRelativeDate(inst.paidAt)}`
|
|
156
|
+
: inst.dueDate
|
|
157
|
+
? `Due ${toRelativeDate(inst.dueDate)}`
|
|
158
|
+
: "" })] }), _jsxs(Row, { align: "center", gap: "sm", className: "shrink-0", children: [_jsx(Badge, { variant: EMI_INSTALLMENT_BADGE_VARIANT[inst.status] ?? "default", children: inst.status }), inst.status !== "paid" && (_jsx(Button, { action: ACTIONS.STORE["mark-installment-paid"], size: "sm", variant: "outline", isLoading: markingPaidIndex === inst.index, disabled: markingPaidIndex !== null, onClick: () => handleMarkInstallmentPaid(inst.index), children: "Mark paid" }))] })] }, inst.index))) }), emiError && (_jsx(Div, { textSize: "xs", className: "mt-2 border border-error/20", color: "error", surface: "danger-surface", padding: "inlineSm", rounded: "lg", children: emiError }))] })), _jsxs(Stack, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", gap: "3", children: [_jsx(Heading, { level: 4, size: "sm", weight: "semibold", children: "Update order" }), _jsx(Select, { label: "New status", value: newStatus, options: UPDATE_STATUS_OPTIONS, onChange: (e) => setNewStatus(e.target.value) }), _jsx(Input, { label: "Tracking number", value: trackingNumber, onChange: (e) => setTrackingNumber(e.target.value), placeholder: "e.g. 12345678901234" }), _jsx(Input, { label: "Carrier", value: carrier, onChange: (e) => setCarrier(e.target.value), placeholder: "e.g. Delhivery, Bluedart" }), _jsx(Input, { label: "Tracking URL (optional)", value: trackingUrl, onChange: (e) => setTrackingUrl(e.target.value), placeholder: "https://...", type: "url" }), saveError && (_jsx(Div, { textSize: "xs", className: "border border-error/20", color: "error", surface: "danger-surface", padding: "inlineSm", rounded: "lg", children: saveError }))] })] }), _jsxs(Row, { border: "top", paddingY: "y-sm-tall", padding: "x-md", align: "center", justify: "end", gap: "3", children: [_jsx(Button, { variant: "outline", onClick: onClose, disabled: saving, children: "Close" }), _jsx(Button, { onClick: handleSave, isLoading: saving, disabled: saving, children: "Save" })] })] }))] }));
|
|
122
159
|
}
|
|
123
160
|
// ---------------------------------------------------------------------------
|
|
124
161
|
// Main view
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
export type ProductListingMode = "standard" | "auction" | "pre-order" | "prize-draw" | "bundle" | "classified" | "digital-code" | "live";
|
|
2
|
+
export type ProductListingMode = "standard" | "auction" | "pre-order" | "prize-draw" | "bundle" | "classified" | "digital-code" | "live" | "art" | "stickers";
|
|
3
3
|
export interface SellerProductDraft {
|
|
4
4
|
title?: string;
|
|
5
5
|
slug?: string;
|
|
@@ -61,6 +61,10 @@ export interface SellerProductDraft {
|
|
|
61
61
|
liveCites?: boolean;
|
|
62
62
|
/** Barcode ID of the physical sticker on this item. Auto-generated on save if blank. */
|
|
63
63
|
barcodeId?: string;
|
|
64
|
+
printSize?: string;
|
|
65
|
+
printMaterial?: string;
|
|
66
|
+
printFinish?: string;
|
|
67
|
+
printEditionSize?: number;
|
|
64
68
|
}
|
|
65
69
|
export interface SellerProductShellProps {
|
|
66
70
|
mode: "create" | "edit";
|
|
@@ -10,7 +10,21 @@ import { ImageUpload, MediaUploadField, MediaUploadList, useMediaUpload } from "
|
|
|
10
10
|
import { StoreAddressSelectorCreate } from "../../stores/components/StoreAddressSelectorCreate";
|
|
11
11
|
import { QuickProductForm } from "./QuickProductForm";
|
|
12
12
|
import { BarcodeField } from "./BarcodeField";
|
|
13
|
+
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
13
14
|
import { normalizeError } from "../../../errors/normalize";
|
|
15
|
+
/**
|
|
16
|
+
* "bundle" is a categoryType, not a ListingType, so it can't live in
|
|
17
|
+
* LISTING_TYPE_REGISTRY — this is the single, explicit escape hatch that
|
|
18
|
+
* lets the seller form's registry lookups also cover it.
|
|
19
|
+
*/
|
|
20
|
+
const BUNDLE_PLUGIN_FALLBACK = {
|
|
21
|
+
priceLabel: "Price (₹)",
|
|
22
|
+
typeLabel: "Bundle",
|
|
23
|
+
showsStockQuantity: false,
|
|
24
|
+
};
|
|
25
|
+
function pluginForMode(mode) {
|
|
26
|
+
return mode === "bundle" ? BUNDLE_PLUGIN_FALLBACK : pluginFor(mode);
|
|
27
|
+
}
|
|
14
28
|
const sellerProductSchema = z.object({
|
|
15
29
|
title: z.string().min(1, "Product title is required").max(200),
|
|
16
30
|
description: z.string().max(10000).optional().or(z.literal("")),
|
|
@@ -114,18 +128,59 @@ const LIVE_TRANSPORT_OPTIONS = [
|
|
|
114
128
|
function StepLiveItemSettings({ values, onChange, }) {
|
|
115
129
|
return (_jsxs(Stack, { gap: "md", children: [_jsx(Alert, { variant: "warning", children: "Live animal / plant listings must comply with all applicable laws, CITES regulations, and local jurisdiction rules. LetItRip is not responsible for legal compliance \u2014 this is solely the seller's responsibility." }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "liveSpecies", label: "Species / Common Name", type: "text", value: values.liveSpecies ?? "", onChange: (v) => onChange({ liveSpecies: v }), placeholder: "e.g. Axolotl (Ambystoma mexicanum)" }), _jsx(FormField, { name: "liveAgeMonths", label: "Age (months, optional)", type: "number", value: String(values.liveAgeMonths ?? ""), onChange: (v) => onChange({ liveAgeMonths: v ? Number(v) : undefined }), placeholder: "e.g. 6" })] }), _jsx(FormField, { name: "liveSex", label: "Sex", type: "select", value: values.liveSex ?? "unknown", onChange: (v) => onChange({ liveSex: v }), options: LIVE_SEX_OPTIONS }), _jsx(FormField, { name: "liveCareInfo", label: "Care Instructions (optional)", type: "textarea", value: values.liveCareInfo ?? "", onChange: (v) => onChange({ liveCareInfo: v }), placeholder: "Diet, temperature, habitat requirements\u2026" }), _jsx(FormField, { name: "liveTransportMethod", label: "Transport Method", type: "select", value: values.liveTransportMethod ?? "in-person", onChange: (v) => onChange({ liveTransportMethod: v }), options: LIVE_TRANSPORT_OPTIONS }), values.liveTransportMethod !== "in-person" && (_jsx(FormField, { name: "liveHandlingFee", label: "Handling / Transport Fee (\u20B9, optional)", type: "number", value: toRupees(values.liveHandlingFee), onChange: (v) => onChange({ liveHandlingFee: v ? toPaise(v) : undefined }), placeholder: "0" })), _jsx(FormField, { name: "liveJurisdictions", label: "Jurisdictions where sale is permitted (comma-separated)", type: "text", value: (values.liveJurisdictions ?? []).join(", "), onChange: (v) => onChange({ liveJurisdictions: v.split(",").map((s) => s.trim()).filter(Boolean) }), placeholder: "e.g. Maharashtra, Karnataka", helpText: "List all Indian states / UTs where you can legally sell" }), _jsx(Toggle, { checked: !!values.liveCites, onChange: (checked) => onChange({ liveCites: checked }), label: "This species requires CITES documentation" })] }));
|
|
116
130
|
}
|
|
131
|
+
// ── Step: Print Meta (art / stickers) ───────────────────────────────────────
|
|
132
|
+
// Shared by both "art" and "stickers" — identical field set, only the intro
|
|
133
|
+
// copy differs (set by the caller via `intro`).
|
|
134
|
+
function StepPrintMetaSettings({ values, onChange, intro, }) {
|
|
135
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsx(Alert, { variant: "info", children: intro }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "printSize", label: "Size", type: "text", value: values.printSize ?? "", onChange: (v) => onChange({ printSize: v }), placeholder: "e.g. A4, 12x18 in" }), _jsx(FormField, { name: "printMaterial", label: "Material", type: "text", value: values.printMaterial ?? "", onChange: (v) => onChange({ printMaterial: v }), placeholder: "e.g. Matte photo paper, Vinyl" })] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "printFinish", label: "Finish (optional)", type: "text", value: values.printFinish ?? "", onChange: (v) => onChange({ printFinish: v }), placeholder: "e.g. Glossy, Matte, Holographic" }), _jsx(FormField, { name: "printEditionSize", label: "Edition Size (optional)", type: "number", value: String(values.printEditionSize ?? ""), onChange: (v) => onChange({ printEditionSize: v ? Number(v) : undefined }), placeholder: "Leave blank for an open edition" })] })] }));
|
|
136
|
+
}
|
|
137
|
+
const TYPE_SPECIFIC_STEPS = {
|
|
138
|
+
auction: {
|
|
139
|
+
id: "auction",
|
|
140
|
+
label: "Auction Settings",
|
|
141
|
+
render: ({ values, onChange }) => (_jsx(StepAuctionSettings, { values: values, onChange: onChange })),
|
|
142
|
+
validate: (v) => (!v.startingBid ? "Starting bid is required" : !v.auctionEndDate ? "Auction end date is required" : null),
|
|
143
|
+
},
|
|
144
|
+
"pre-order": {
|
|
145
|
+
id: "preorder",
|
|
146
|
+
label: "Pre-Order",
|
|
147
|
+
sectionHeading: "Pre-Order Settings",
|
|
148
|
+
render: ({ values, onChange }) => (_jsx(StepPreOrderSettings, { values: values, onChange: onChange })),
|
|
149
|
+
validate: (v) => (!v.preOrderDeliveryDate ? "Estimated delivery date is required" : null),
|
|
150
|
+
},
|
|
151
|
+
classified: {
|
|
152
|
+
id: "classified",
|
|
153
|
+
label: "Meetup Details",
|
|
154
|
+
render: ({ values, onChange }) => (_jsx(StepClassifiedSettings, { values: values, onChange: onChange })),
|
|
155
|
+
validate: (v) => (!v.classifiedCity?.trim() ? "City is required for classified listings" : null),
|
|
156
|
+
},
|
|
157
|
+
"digital-code": {
|
|
158
|
+
id: "digitalcode",
|
|
159
|
+
label: "Code Details",
|
|
160
|
+
render: ({ values, onChange }) => (_jsx(StepDigitalCodeSettings, { values: values, onChange: onChange })),
|
|
161
|
+
},
|
|
162
|
+
live: {
|
|
163
|
+
id: "live",
|
|
164
|
+
label: "Live Item Details",
|
|
165
|
+
render: ({ values, onChange }) => (_jsx(StepLiveItemSettings, { values: values, onChange: onChange })),
|
|
166
|
+
validate: (v) => (!v.liveSpecies?.trim() ? "Species name is required" : null),
|
|
167
|
+
},
|
|
168
|
+
art: {
|
|
169
|
+
id: "printmeta",
|
|
170
|
+
label: "Print Details",
|
|
171
|
+
render: ({ values, onChange }) => (_jsx(StepPrintMetaSettings, { values: values, onChange: onChange, intro: "Art prints are sold as physical, printed-only reproductions \u2014 no original artwork or digital files." })),
|
|
172
|
+
},
|
|
173
|
+
stickers: {
|
|
174
|
+
id: "printmeta",
|
|
175
|
+
label: "Print Details",
|
|
176
|
+
render: ({ values, onChange }) => (_jsx(StepPrintMetaSettings, { values: values, onChange: onChange, intro: "Sticker listings are sold as physical, printed sheets or packs \u2014 no digital files." })),
|
|
177
|
+
},
|
|
178
|
+
};
|
|
117
179
|
// ── Step: Pricing ─────────────────────────────────────────────────────────
|
|
118
180
|
function StepPricing({ values, onChange, listingType, }) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
? "Pre-Order Price (₹)"
|
|
123
|
-
: listingType === "classified"
|
|
124
|
-
? "Asking Price (₹)"
|
|
125
|
-
: listingType === "digital-code"
|
|
126
|
-
? "Price per Code (₹)"
|
|
127
|
-
: "Price (₹)";
|
|
128
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsx(Alert, { variant: "info", children: "All prices in Indian Rupees (\u20B9)." }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "price", label: priceLabel, type: "number", value: toRupees(values.price), onChange: (v) => onChange({ price: toPaise(v) }), placeholder: "999" }), _jsx(FormField, { name: "compareAtPrice", label: "Compare-at Price (\u20B9, optional)", type: "number", value: toRupees(values.compareAtPrice), onChange: (v) => onChange({ compareAtPrice: v ? toPaise(v) : undefined }), placeholder: "1299", helpText: "Original price shown as strikethrough" })] }), (listingType === "standard" || listingType === "classified" || listingType === "live") && (_jsx(FormField, { name: "stockQuantity", label: "Stock Quantity", type: "number", value: String(values.stockQuantity ?? ""), onChange: (v) => onChange({ stockQuantity: Number(v) }), placeholder: "1" })), _jsxs(FormGroup, { columns: 2, children: [_jsx(Toggle, { checked: !!values.featured, onChange: (checked) => onChange({ featured: checked }), label: "Mark as Featured" }), _jsx(Toggle, { checked: !!values.isNew, onChange: (checked) => onChange({ isNew: checked }), label: "Mark as New Arrival" })] })] }));
|
|
181
|
+
const listingPlugin = pluginForMode(listingType);
|
|
182
|
+
const priceLabel = listingPlugin.priceLabel;
|
|
183
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsx(Alert, { variant: "info", children: "All prices in Indian Rupees (\u20B9)." }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "price", label: priceLabel, type: "number", value: toRupees(values.price), onChange: (v) => onChange({ price: toPaise(v) }), placeholder: "999" }), _jsx(FormField, { name: "compareAtPrice", label: "Compare-at Price (\u20B9, optional)", type: "number", value: toRupees(values.compareAtPrice), onChange: (v) => onChange({ compareAtPrice: v ? toPaise(v) : undefined }), placeholder: "1299", helpText: "Original price shown as strikethrough" })] }), listingPlugin.showsStockQuantity && (_jsx(FormField, { name: "stockQuantity", label: "Stock Quantity", type: "number", value: String(values.stockQuantity ?? ""), onChange: (v) => onChange({ stockQuantity: Number(v) }), placeholder: "1" })), _jsxs(FormGroup, { columns: 2, children: [_jsx(Toggle, { checked: !!values.featured, onChange: (checked) => onChange({ featured: checked }), label: "Mark as Featured" }), _jsx(Toggle, { checked: !!values.isNew, onChange: (checked) => onChange({ isNew: checked }), label: "Mark as New Arrival" })] })] }));
|
|
129
184
|
}
|
|
130
185
|
// ── Step: Shipping ────────────────────────────────────────────────────────
|
|
131
186
|
function StepShipping({ values, onChange, renderAddressSelector, }) {
|
|
@@ -235,49 +290,8 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
|
|
|
235
290
|
showToast(err instanceof Error ? err.message : "Failed to publish.", "error");
|
|
236
291
|
}
|
|
237
292
|
}, [draft, onPublish, markClean, showToast]);
|
|
238
|
-
const listingTypeLabel = listingType
|
|
239
|
-
|
|
240
|
-
: listingType === "pre-order"
|
|
241
|
-
? "Pre-Order"
|
|
242
|
-
: listingType === "bundle"
|
|
243
|
-
? "Bundle"
|
|
244
|
-
: listingType === "classified"
|
|
245
|
-
? "Classified"
|
|
246
|
-
: listingType === "digital-code"
|
|
247
|
-
? "Digital Code"
|
|
248
|
-
: listingType === "live"
|
|
249
|
-
? "Live Item"
|
|
250
|
-
: "Product";
|
|
251
|
-
const typeSpecificStep = listingType === "auction"
|
|
252
|
-
? {
|
|
253
|
-
label: "Auction Settings",
|
|
254
|
-
render: ({ values, onChange }) => (_jsx(StepAuctionSettings, { values: values, onChange: onChange })),
|
|
255
|
-
validate: (v) => (!v.startingBid ? "Starting bid is required" : !v.auctionEndDate ? "Auction end date is required" : null),
|
|
256
|
-
}
|
|
257
|
-
: listingType === "pre-order"
|
|
258
|
-
? {
|
|
259
|
-
label: "Pre-Order",
|
|
260
|
-
render: ({ values, onChange }) => (_jsx(StepPreOrderSettings, { values: values, onChange: onChange })),
|
|
261
|
-
validate: (v) => (!v.preOrderDeliveryDate ? "Estimated delivery date is required" : null),
|
|
262
|
-
}
|
|
263
|
-
: listingType === "classified"
|
|
264
|
-
? {
|
|
265
|
-
label: "Meetup Details",
|
|
266
|
-
render: ({ values, onChange }) => (_jsx(StepClassifiedSettings, { values: values, onChange: onChange })),
|
|
267
|
-
validate: (v) => (!v.classifiedCity?.trim() ? "City is required for classified listings" : null),
|
|
268
|
-
}
|
|
269
|
-
: listingType === "digital-code"
|
|
270
|
-
? {
|
|
271
|
-
label: "Code Details",
|
|
272
|
-
render: ({ values, onChange }) => (_jsx(StepDigitalCodeSettings, { values: values, onChange: onChange })),
|
|
273
|
-
}
|
|
274
|
-
: listingType === "live"
|
|
275
|
-
? {
|
|
276
|
-
label: "Live Item Details",
|
|
277
|
-
render: ({ values, onChange }) => (_jsx(StepLiveItemSettings, { values: values, onChange: onChange })),
|
|
278
|
-
validate: (v) => (!v.liveSpecies?.trim() ? "Species name is required" : null),
|
|
279
|
-
}
|
|
280
|
-
: null;
|
|
293
|
+
const listingTypeLabel = pluginForMode(listingType).typeLabel;
|
|
294
|
+
const typeSpecificStep = TYPE_SPECIFIC_STEPS[listingType] ?? null;
|
|
281
295
|
const steps = [
|
|
282
296
|
{
|
|
283
297
|
label: "Basic",
|
|
@@ -349,11 +363,7 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
|
|
|
349
363
|
// Edit mode — FormShell with section nav + full form
|
|
350
364
|
const editSections = [
|
|
351
365
|
...EDIT_SECTIONS,
|
|
352
|
-
...(
|
|
353
|
-
...(listingType === "pre-order" ? [{ id: "preorder", label: "Pre-Order" }] : []),
|
|
354
|
-
...(listingType === "classified" ? [{ id: "classified", label: "Meetup Details" }] : []),
|
|
355
|
-
...(listingType === "digital-code" ? [{ id: "digitalcode", label: "Code Details" }] : []),
|
|
356
|
-
...(listingType === "live" ? [{ id: "live", label: "Live Item" }] : []),
|
|
366
|
+
...(typeSpecificStep ? [{ id: typeSpecificStep.id, label: typeSpecificStep.label }] : []),
|
|
357
367
|
];
|
|
358
|
-
return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, sections: editSections, onSaveDraft: handleSave, onPublish: handlePublish, saveLabel: "Save Changes", publishLabel: "Update", previewSlot: previewSlot, children: _jsx(FormShellProvider, { isDirty: isDirty, values: draft, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Section, { id: "basic", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Basic Info" }), _jsx(StepBasic, { values: draft, onChange: update, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })] }), _jsxs(Section, { id: "media", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Media" }), _jsx(StepMedia, { values: draft, onChange: update, storeSlug: storeSlug })] }),
|
|
368
|
+
return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, sections: editSections, onSaveDraft: handleSave, onPublish: handlePublish, saveLabel: "Save Changes", publishLabel: "Update", previewSlot: previewSlot, children: _jsx(FormShellProvider, { isDirty: isDirty, values: draft, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Section, { id: "basic", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Basic Info" }), _jsx(StepBasic, { values: draft, onChange: update, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })] }), _jsxs(Section, { id: "media", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Media" }), _jsx(StepMedia, { values: draft, onChange: update, storeSlug: storeSlug })] }), typeSpecificStep && (_jsxs(Section, { id: typeSpecificStep.id, children: [_jsx(Heading, { level: 3, className: "mb-4", children: typeSpecificStep.sectionHeading ?? typeSpecificStep.label }), typeSpecificStep.render({ values: draft, onChange: update, errors: {} })] })), _jsxs(Section, { id: "pricing", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Pricing" }), _jsx(StepPricing, { values: draft, onChange: update, listingType: listingType })] }), _jsxs(Section, { id: "shipping", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Shipping" }), _jsx(StepShipping, { values: draft, onChange: update, renderAddressSelector: renderAddressSelector })] }), _jsxs(Section, { id: "publish", children: [_jsx(Heading, { level: 3, className: "mb-4", children: "Publish" }), _jsx(StepPublish, { values: draft, onChange: update }), onSaveAsTemplate && (_jsxs(Div, { className: "mt-4 border-t border-[var(--appkit-color-border)]", padding: "t-md", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onSaveAsTemplate(draft), children: "Save as Template" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-secondary-text)]", size: "xs", children: "Save these settings as a reusable template for future listings." })] }))] })] }) }) }));
|
|
359
369
|
}
|