enh
This commit is contained in:
parent
1ff771355e
commit
ffcab51d98
72 changed files with 875 additions and 844 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import type { StoreProductCard } from '@packages/shared'
|
||||
import {
|
||||
View,
|
||||
Alert,
|
||||
|
|
@ -27,20 +28,12 @@ const { width: screenWidth } = Dimensions.get("window");
|
|||
const itemWidth = screenWidth - 48; // 24px padding each side
|
||||
const itemHeight = 80;
|
||||
|
||||
interface Product {
|
||||
id: number;
|
||||
name: string;
|
||||
images: string[];
|
||||
isOutOfStock: boolean;
|
||||
}
|
||||
// Reorder row — anchored to the shared store-card core
|
||||
type Product = Pick<StoreProductCard, 'id' | 'name' | 'images' | 'isOutOfStock'>
|
||||
|
||||
interface ProductItemProps {
|
||||
item: Product;
|
||||
drag: () => void;
|
||||
isActive: boolean;
|
||||
}
|
||||
import type { DragOrderItemProps } from '@/types/drag-order-item';
|
||||
|
||||
const ProductItem: React.FC<ProductItemProps> = ({
|
||||
const ProductItem: React.FC<DragOrderItemProps<Product>> = ({
|
||||
item,
|
||||
drag,
|
||||
isActive,
|
||||
|
|
|
|||
|
|
@ -24,26 +24,22 @@ import { useRouter } from "expo-router";
|
|||
import { trpc } from "../../../../src/trpc-client";
|
||||
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { StoreProductCard } from '@packages/shared';
|
||||
import type { DragOrderItemProps } from '@/types/drag-order-item';
|
||||
|
||||
interface PopularProduct {
|
||||
id: number;
|
||||
name: string;
|
||||
shortDescription: string | null;
|
||||
// Popular-items row = shared store card core with numeric (form) prices
|
||||
type PopularProduct = Omit<
|
||||
StoreProductCard,
|
||||
'price' | 'marketPrice' | 'unitNotation' | 'images'
|
||||
> & {
|
||||
price: number;
|
||||
marketPrice: number | null;
|
||||
unit: string;
|
||||
incrementStep: number;
|
||||
productQuantity: number;
|
||||
storeId: number | null;
|
||||
isOutOfStock: boolean;
|
||||
nextDeliveryDate: string | null;
|
||||
images: string[];
|
||||
}
|
||||
storeId: number | null;
|
||||
nextDeliveryDate: string | null;
|
||||
};
|
||||
|
||||
interface ProductItemProps {
|
||||
item: PopularProduct;
|
||||
drag: () => void;
|
||||
isActive: boolean;
|
||||
interface ProductItemProps extends DragOrderItemProps<PopularProduct> {
|
||||
onDelete: (id: number) => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,19 +6,20 @@ import { useRouter, useLocalSearchParams } from 'expo-router';
|
|||
import { FormikHelpers } from 'formik';
|
||||
import BannerForm, { BannerFormData } from '@/components/BannerForm';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import type { Banner as SharedBanner } from '@packages/shared';
|
||||
|
||||
interface Banner {
|
||||
id: number;
|
||||
name: string;
|
||||
imageUrl: string;
|
||||
// Form/hardened view of the shared banner (string dates, non-null editable fields)
|
||||
type Banner = Omit<
|
||||
SharedBanner,
|
||||
'description' | 'skuIds' | 'redirectUrl' | 'serialNum' | 'createdAt' | 'lastUpdated'
|
||||
> & {
|
||||
description?: string;
|
||||
skuIds?: number[];
|
||||
redirectUrl?: string;
|
||||
serialNum: number;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
lastUpdated: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default function EditBanner() {
|
||||
const router = useRouter();
|
||||
|
|
|
|||
|
|
@ -4,19 +4,13 @@ import { AppContainer, MyText, tw, MyTouchableOpacity } from 'common-ui';
|
|||
import { trpc } from '../../../../src/trpc-client';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import type { Banner as SharedBanner } from '@packages/shared';
|
||||
|
||||
interface Banner {
|
||||
id: number;
|
||||
name: string;
|
||||
imageUrl: string;
|
||||
description: string | null;
|
||||
skuIds: number[] | null;
|
||||
redirectUrl: string | null;
|
||||
serialNum: number | null;
|
||||
isActive: boolean;
|
||||
// Serialized banner row (API JSON carries string dates) anchored to the shared shape
|
||||
type Banner = Omit<SharedBanner, 'createdAt' | 'lastUpdated'> & {
|
||||
createdAt: string;
|
||||
lastUpdated: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default function DashboardBanners() {
|
||||
const router = useRouter();
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Entypo } from '@expo/vector-icons';
|
|||
import CancelOrderDialog from '@/components/CancelOrderDialog';
|
||||
import { OrderOptionsMenu } from '@/components/OrderOptionsMenu';
|
||||
import * as Location from 'expo-location';
|
||||
import type { AdminOrderListItem, AdminOrderListItemProduct } from '@packages/shared';
|
||||
|
||||
const AdminNotesForm = ({ orderId, existingNotes, onClose, refetch }: { orderId: string; existingNotes?: string | null; onClose: () => void; refetch: () => void }) => {
|
||||
const [notesText, setNotesText] = useState(existingNotes || '');
|
||||
|
|
@ -51,43 +52,32 @@ const AdminNotesForm = ({ orderId, existingNotes, onClose, refetch }: { orderId:
|
|||
};
|
||||
|
||||
|
||||
interface OrderType {
|
||||
id: number;
|
||||
orderId: string;
|
||||
readableId: number;
|
||||
// Order list row — derived from shared AdminOrderListItem (serialized dates,
|
||||
// coupon display fields, optional flags)
|
||||
type OrderItemRow = Omit<
|
||||
AdminOrderListItemProduct,
|
||||
'id' | 'skuName' | 'features' | 'isPackaged' | 'isPackageVerified'
|
||||
> & {
|
||||
id?: number;
|
||||
isPackaged?: boolean;
|
||||
isPackageVerified?: boolean;
|
||||
};
|
||||
|
||||
type OrderType = Omit<
|
||||
AdminOrderListItem,
|
||||
'customerName' | 'customerMobile' | 'items' | 'createdAt' | 'adminNotes' | 'userNotes' | 'userNegativityScore'
|
||||
> & {
|
||||
customerName: string | null;
|
||||
customerMobile?: string | null;
|
||||
address: string;
|
||||
addressId: number;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
totalAmount: number;
|
||||
deliveryCharge: number;
|
||||
items: {
|
||||
id?: number;
|
||||
name: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
amount: number;
|
||||
unit: string;
|
||||
isPackaged?: boolean;
|
||||
isPackageVerified?: boolean;
|
||||
productSize: number;
|
||||
}[];
|
||||
items: OrderItemRow[];
|
||||
createdAt: string;
|
||||
deliveryTime: string | null;
|
||||
status: 'pending' | 'delivered' | 'cancelled';
|
||||
isPackaged: boolean;
|
||||
isDelivered: boolean;
|
||||
isCod: boolean;
|
||||
isFlashDelivery: boolean;
|
||||
couponCode?: string;
|
||||
couponDescription?: string;
|
||||
discountAmount?: number;
|
||||
adminNotes?: string | null;
|
||||
userNotes?: string | null;
|
||||
userNegativityScore?: number;
|
||||
}
|
||||
couponCode?: string;
|
||||
couponDescription?: string;
|
||||
discountAmount?: number;
|
||||
};
|
||||
|
||||
const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }) => {
|
||||
const id = order.orderId;
|
||||
|
|
|
|||
|
|
@ -3,17 +3,12 @@ import { View, Alert } from 'react-native';
|
|||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { AppContainer, MyText, tw, type ImageUploaderNeoItem } from 'common-ui';
|
||||
import TagForm from '@/src/components/TagForm';
|
||||
import type { TagFormData as BaseTagFormData } from '@/src/components/TagForm';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
||||
|
||||
interface TagFormData {
|
||||
tagName: string;
|
||||
tagDescription: string;
|
||||
isDashboardTag: boolean;
|
||||
relatedStores: number[];
|
||||
productIds: number[];
|
||||
existingImageUrl?: string;
|
||||
}
|
||||
// Edit screen adds the current image to the canonical TagForm data
|
||||
type TagFormData = BaseTagFormData & { existingImageUrl?: string };
|
||||
|
||||
export default function EditTag() {
|
||||
const router = useRouter();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import type { ProductTagCore } from '@packages/shared'
|
||||
import { View, TouchableOpacity, Alert, RefreshControl } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { useRouter } from 'expo-router';
|
||||
|
|
@ -7,13 +8,12 @@ import { tw, MyText, useManualRefresh, useMarkDataFetchers, MyFlatList } from 'c
|
|||
import { TagMenu } from '@/src/components/TagMenu';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
|
||||
interface TagItemData {
|
||||
// Tag list row — anchored to the shared tag core (relatedStores now typed)
|
||||
interface TagItemData extends ProductTagCore {
|
||||
id: number;
|
||||
tagName: string;
|
||||
tagDescription: string | null;
|
||||
imageUrl: string | null;
|
||||
isDashboardTag: boolean;
|
||||
relatedStores?: unknown;
|
||||
createdAt: string | Date;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { useRouter } from 'expo-router';
|
|||
import { trpc } from '../../../../src/trpc-client';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import type { DragOrderItemProps } from '@/types/drag-order-item';
|
||||
|
||||
const { width: screenWidth } = Dimensions.get('window');
|
||||
const itemWidth = screenWidth - 48;
|
||||
|
|
@ -32,13 +33,7 @@ interface Tag {
|
|||
imageUrl: string | null;
|
||||
}
|
||||
|
||||
interface TagItemProps {
|
||||
item: Tag;
|
||||
drag: () => void;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const TagItem: React.FC<TagItemProps> = ({ item, drag, isActive }) => {
|
||||
const TagItem: React.FC<DragOrderItemProps<Tag>> = ({ item, drag, isActive }) => {
|
||||
return (
|
||||
<ScaleDecorator>
|
||||
<TouchableOpacity
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ export default function AddProduct() {
|
|||
const handleSubmit = async (values: any, variantImages: ImageUploaderNeoPayload[][], _deletedImageKeys?: string[]) => {
|
||||
try {
|
||||
for (const variant of values.variants) {
|
||||
const price = parseFloat(variant.price)
|
||||
if (isNaN(price) || price <= 0) {
|
||||
const price = variant.price
|
||||
if (price == null || price <= 0) {
|
||||
Alert.alert('Error', 'Please enter a valid price for every variant')
|
||||
return
|
||||
}
|
||||
|
|
@ -24,15 +24,15 @@ export default function AddProduct() {
|
|||
|
||||
const seenSignatures = new Set<string>()
|
||||
for (const variant of values.variants) {
|
||||
const attributes = variant.attributes || []
|
||||
const hasQuantity = attributes.some(
|
||||
const features = variant.features || []
|
||||
const hasQuantity = features.some(
|
||||
(a: any) => (a.featureName ?? '').trim().toLowerCase() === 'quantity'
|
||||
)
|
||||
if (!hasQuantity) {
|
||||
Alert.alert('Error', 'Every SKU must have a quantity feature')
|
||||
return
|
||||
}
|
||||
const signature = attributes
|
||||
const signature = features
|
||||
.map((a: any) => `${(a.featureName ?? '').trim().toLowerCase()}::${a.featureValue.trim().toLowerCase()}`)
|
||||
.sort()
|
||||
.join('|')
|
||||
|
|
@ -68,21 +68,21 @@ export default function AddProduct() {
|
|||
|
||||
return {
|
||||
name: variant.name || null,
|
||||
price: parseFloat(variant.price),
|
||||
marketPrice: variant.marketPrice ? parseFloat(variant.marketPrice) : undefined,
|
||||
price: variant.price as number,
|
||||
marketPrice: variant.marketPrice ?? undefined,
|
||||
images: variantUrls,
|
||||
isFlashAvailable: variant.isFlashAvailable || false,
|
||||
flashPrice: variant.flashPrice ? parseFloat(variant.flashPrice) : undefined,
|
||||
flashPrice: variant.flashPrice ?? undefined,
|
||||
isOffer: variant.isOffer || false,
|
||||
isComboOnly: variant.isComboOnly || false,
|
||||
isDeleted: variant.isDeleted || false,
|
||||
isSuspended: variant.isSuspended || false,
|
||||
features: variant.attributes.map((attr: any) => ({
|
||||
features: variant.features.map((attr: any) => ({
|
||||
featureName: attr.featureName,
|
||||
featureValue: attr.featureValue,
|
||||
})),
|
||||
comboItems: (variant.comboItems || []).map((ci: any) => ({
|
||||
skuId: typeof ci.skuId === 'string' ? parseInt(ci.skuId) : ci.skuId,
|
||||
skuId: ci.skuId,
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
|
@ -114,16 +114,16 @@ export default function AddProduct() {
|
|||
{
|
||||
id: undefined as number | undefined,
|
||||
name: '',
|
||||
price: '',
|
||||
marketPrice: '',
|
||||
price: undefined,
|
||||
marketPrice: null,
|
||||
isFlashAvailable: false,
|
||||
flashPrice: '',
|
||||
flashPrice: null,
|
||||
isOffer: false,
|
||||
isComboOnly: false,
|
||||
isDeleted: false,
|
||||
isSuspended: false,
|
||||
attributes: [{ featureName: 'quantity', featureValue: '' }],
|
||||
comboItems: [] as { skuId: number | string }[],
|
||||
features: [{ featureName: 'quantity', featureValue: '' }],
|
||||
comboItems: [],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,20 +45,20 @@ export default function EditProduct() {
|
|||
variants: (productData.skus || []).map((sku) => ({
|
||||
id: sku.id,
|
||||
name: sku.name || '',
|
||||
price: sku.price || '',
|
||||
marketPrice: sku.marketPrice || '',
|
||||
price: sku.price ? parseFloat(sku.price) : undefined,
|
||||
marketPrice: sku.marketPrice ? parseFloat(sku.marketPrice) : null,
|
||||
isFlashAvailable: sku.isFlashAvailable || false,
|
||||
flashPrice: sku.flashPrice || '',
|
||||
flashPrice: sku.flashPrice ? parseFloat(sku.flashPrice) : null,
|
||||
isOffer: sku.isOffer || false,
|
||||
isComboOnly: sku.isComboOnly || false,
|
||||
isDeleted: sku.isDeleted || false,
|
||||
isSuspended: sku.isSuspended || false,
|
||||
attributes: (sku.features || []).map((f) => ({
|
||||
features: (sku.features || []).map((f) => ({
|
||||
featureName: f.featureName,
|
||||
featureValue: f.featureValue,
|
||||
})),
|
||||
comboItems: (sku.comboItems || []).map((ci: any) => ({
|
||||
skuId: ci.skuId.toString(),
|
||||
skuId: ci.skuId,
|
||||
})),
|
||||
})),
|
||||
}
|
||||
|
|
@ -81,8 +81,8 @@ export default function EditProduct() {
|
|||
const handleSubmit = async (values: any, variantImages: ImageUploaderNeoPayload[][], deletedImageKeys: string[]) => {
|
||||
try {
|
||||
for (const variant of values.variants) {
|
||||
const price = parseFloat(variant.price)
|
||||
if (isNaN(price) || price <= 0) {
|
||||
const price = variant.price
|
||||
if (price == null || price <= 0) {
|
||||
Alert.alert('Error', 'Please enter a valid price for every variant')
|
||||
return
|
||||
}
|
||||
|
|
@ -90,15 +90,15 @@ export default function EditProduct() {
|
|||
|
||||
const seenSignatures = new Set<string>()
|
||||
for (const variant of values.variants) {
|
||||
const attributes = variant.attributes || []
|
||||
const hasQuantity = attributes.some(
|
||||
const features = variant.features || []
|
||||
const hasQuantity = features.some(
|
||||
(a: any) => (a.featureName ?? '').trim().toLowerCase() === 'quantity'
|
||||
)
|
||||
if (!hasQuantity) {
|
||||
Alert.alert('Error', 'Every SKU must have a quantity feature')
|
||||
return
|
||||
}
|
||||
const signature = attributes
|
||||
const signature = features
|
||||
.map((a: any) => `${(a.featureName ?? '').trim().toLowerCase()}::${a.featureValue.trim().toLowerCase()}`)
|
||||
.sort()
|
||||
.join('|')
|
||||
|
|
@ -146,21 +146,21 @@ export default function EditProduct() {
|
|||
return {
|
||||
id: variant.id,
|
||||
name: variant.name || null,
|
||||
price: parseFloat(variant.price),
|
||||
marketPrice: variant.marketPrice ? parseFloat(variant.marketPrice) : undefined,
|
||||
price: variant.price as number,
|
||||
marketPrice: variant.marketPrice ?? undefined,
|
||||
images: allUrls,
|
||||
isFlashAvailable: variant.isFlashAvailable || false,
|
||||
flashPrice: variant.flashPrice ? parseFloat(variant.flashPrice) : undefined,
|
||||
flashPrice: variant.flashPrice ?? undefined,
|
||||
isOffer: variant.isOffer || false,
|
||||
isComboOnly: variant.isComboOnly || false,
|
||||
isDeleted: variant.isDeleted || false,
|
||||
isSuspended: variant.isSuspended || false,
|
||||
features: variant.attributes.map((attr: any) => ({
|
||||
features: variant.features.map((attr: any) => ({
|
||||
featureName: attr.featureName,
|
||||
featureValue: attr.featureValue,
|
||||
})),
|
||||
comboItems: (variant.comboItems || []).map((ci: any) => ({
|
||||
skuId: typeof ci.skuId === 'string' ? parseInt(ci.skuId) : ci.skuId,
|
||||
skuId: ci.skuId,
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ import {
|
|||
BottomDropdown,
|
||||
} from 'common-ui';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import type { UserMiniInfo } from '@packages/shared';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
// Notification recipient = shared mini user + eligibility flag
|
||||
type User = Omit<UserMiniInfo, 'name'> & {
|
||||
name: string | null;
|
||||
mobile: string | null;
|
||||
isEligibleForNotif: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import type { OrderCardActionProps } from '@packages/shared'
|
||||
import {
|
||||
View,
|
||||
TouchableOpacity,
|
||||
|
|
@ -28,9 +29,7 @@ interface Order {
|
|||
itemCount: number;
|
||||
}
|
||||
|
||||
interface OrderItemProps {
|
||||
order: Order;
|
||||
onPress: () => void;
|
||||
interface OrderItemProps extends OrderCardActionProps<Order> {
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import React, { useState } from 'react';
|
||||
import type { OrderDialogBaseProps } from '@packages/shared'
|
||||
import { View, TouchableOpacity } from 'react-native';
|
||||
import { MyText, tw, BottomDialog, MyTextInput } from 'common-ui';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { Alert } from 'react-native';
|
||||
|
||||
interface CancelOrderDialogProps {
|
||||
orderId: number;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
interface CancelOrderDialogProps extends OrderDialogBaseProps {
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,15 +5,13 @@ import { MyText, tw, MyTextInput, MyTouchableOpacity, theme, BottomDropdown } fr
|
|||
import ProductsSelector from './ProductsSelector';
|
||||
import { trpc } from '../src/trpc-client';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import type { AdminProductGroup as SharedAdminProductGroup } from '@packages/shared';
|
||||
|
||||
export interface ProductGroup {
|
||||
id: number;
|
||||
groupName: string;
|
||||
description: string | null;
|
||||
// Serialized product group (string date, loosely typed products) anchored to shared
|
||||
export type ProductGroup = Omit<SharedAdminProductGroup, 'createdAt' | 'products'> & {
|
||||
createdAt: string;
|
||||
products: any[];
|
||||
productCount: number;
|
||||
}
|
||||
};
|
||||
|
||||
interface ProductGroupFormProps {
|
||||
group?: ProductGroup | null;
|
||||
|
|
|
|||
|
|
@ -3,15 +3,10 @@ import { View } from 'react-native';
|
|||
import BottomDropdown, { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
|
||||
import { trpc } from '../src/trpc-client';
|
||||
import { tw } from 'common-ui';
|
||||
import type { SkuSummary as SharedSkuSummary } from '@packages/shared';
|
||||
|
||||
interface SkuSummary {
|
||||
id: number;
|
||||
productId: number;
|
||||
productName: string;
|
||||
label: string;
|
||||
storeId: number | null;
|
||||
price: string;
|
||||
}
|
||||
// Selector works with skus that may not have images resolved yet
|
||||
type SkuSummary = Omit<SharedSkuSummary, 'images'>
|
||||
|
||||
interface Group {
|
||||
id: number;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import type { SlotSnippetInput } from '@packages/shared'
|
||||
import { View, Text, TouchableOpacity, Alert } from 'react-native';
|
||||
import { Formik, FieldArray } from 'formik';
|
||||
import DateTimePickerMod from 'common-ui/src/components/date-time-picker';
|
||||
|
|
@ -6,12 +7,8 @@ import { tw, MyTextInput } from 'common-ui';
|
|||
import { trpc } from '../src/trpc-client';
|
||||
import ProductsSelector from '../components/ProductsSelector';
|
||||
|
||||
interface VendorSnippet {
|
||||
name: string;
|
||||
groupIds: number[];
|
||||
skuIds: number[];
|
||||
validTill?: string;
|
||||
}
|
||||
// Snippet form row — single source in @packages/shared (complete payload)
|
||||
type VendorSnippet = SlotSnippetInput
|
||||
|
||||
interface SlotFormProps {
|
||||
onSlotAdded?: () => void;
|
||||
|
|
@ -41,7 +38,7 @@ export default function SlotForm({
|
|||
name: snippet.name || '',
|
||||
groupIds: snippet.groupIds || [],
|
||||
skuIds: snippet.skuIds || [],
|
||||
validTill: snippet.validTill || undefined,
|
||||
validTill: snippet.validTill || null,
|
||||
})) as VendorSnippet[];
|
||||
|
||||
const initialValues = {
|
||||
|
|
@ -80,7 +77,7 @@ export default function SlotForm({
|
|||
vendorSnippets: values.vendorSnippetList.map((snippet: VendorSnippet) => ({
|
||||
name: snippet.name,
|
||||
skuIds: snippet.skuIds,
|
||||
validTill: snippet.validTill,
|
||||
validTill: snippet.validTill ?? undefined,
|
||||
})),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,28 +2,25 @@ import React from 'react';
|
|||
import { View, ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { MyText, tw, AppContainer } from 'common-ui';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import type {
|
||||
AdminVendorSnippetOrderProduct,
|
||||
AdminVendorSnippetOrderSummary as SharedSnippetOrderSummary,
|
||||
} from '@packages/shared';
|
||||
|
||||
interface OrderProduct {
|
||||
productId: number;
|
||||
productName: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
unit: string;
|
||||
subtotal: number;
|
||||
}
|
||||
// Line item + order summary anchored to the shared vendor-order contract
|
||||
// (string totalAmount + any[] sequence at this view layer)
|
||||
type OrderProduct = Pick<
|
||||
AdminVendorSnippetOrderProduct,
|
||||
'productId' | 'productName' | 'quantity' | 'price' | 'unit' | 'subtotal'
|
||||
>
|
||||
|
||||
interface SnippetOrder {
|
||||
orderId: string;
|
||||
orderDate: string;
|
||||
customerName: string;
|
||||
totalAmount: string;
|
||||
type SnippetOrder = Omit<SharedSnippetOrderSummary, 'totalAmount' | 'slotInfo' | 'products'> & {
|
||||
totalAmount: string
|
||||
slotInfo: {
|
||||
time: string;
|
||||
sequence: any[];
|
||||
} | null;
|
||||
products: OrderProduct[];
|
||||
matchedProducts: number[];
|
||||
snippetCode: string;
|
||||
}
|
||||
|
||||
interface SnippetOrdersViewProps {
|
||||
|
|
|
|||
|
|
@ -7,14 +7,13 @@ import ProductsSelector from './ProductsSelector';
|
|||
import { trpc } from '../src/trpc-client';
|
||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStore';
|
||||
import type { CreateStoreInput } from '@packages/shared';
|
||||
|
||||
export interface StoreFormData {
|
||||
name: string;
|
||||
// Store form values — required description + product selection on top of CreateStoreInput
|
||||
export type StoreFormData = Omit<CreateStoreInput, 'description'> & {
|
||||
description: string;
|
||||
imageUrl?: string;
|
||||
owner: number;
|
||||
products: number[];
|
||||
}
|
||||
};
|
||||
|
||||
interface StoreFormProps {
|
||||
mode: 'create' | 'edit';
|
||||
|
|
|
|||
|
|
@ -5,24 +5,29 @@ import * as Yup from 'yup'
|
|||
import { MyTextInput, BottomDropdown, MyText, tw, ImageUploaderNeo, ImageUploaderNeoItem, ImageUploaderNeoPayload, Checkbox, InfoDialog } from 'common-ui'
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons'
|
||||
import { trpc } from '../trpc-client'
|
||||
import type { CreateComboItemInput, CreateSkuInput } from '@packages/shared'
|
||||
import type { SkuFeatureLike } from '@packages/shared'
|
||||
|
||||
// Feature input shape — single source: shared SkuFeatureLike
|
||||
type Attribute = SkuFeatureLike
|
||||
|
||||
interface Variant {
|
||||
// Variant form state — anchored to the shared CreateSkuInput (numeric prices,
|
||||
// same field names). Images are managed separately via variantImages state.
|
||||
type Variant = Omit<
|
||||
CreateSkuInput,
|
||||
'name' | 'price' | 'marketPrice' | 'flashPrice' | 'images' | 'features' | 'comboItems'
|
||||
> & {
|
||||
id?: number
|
||||
name: string
|
||||
price: string
|
||||
marketPrice: string
|
||||
price?: number
|
||||
marketPrice: number | null
|
||||
flashPrice: number | null
|
||||
isFlashAvailable: boolean
|
||||
flashPrice: string
|
||||
isOffer: boolean
|
||||
isComboOnly: boolean
|
||||
isDeleted?: boolean
|
||||
isSuspended: boolean
|
||||
attributes: Attribute[]
|
||||
comboItems: { skuId: number | string }[]
|
||||
features: Attribute[]
|
||||
comboItems: CreateComboItemInput[]
|
||||
}
|
||||
|
||||
interface ProductFormData {
|
||||
|
|
@ -58,15 +63,15 @@ const isQuantityFeature = (attr: Attribute): boolean =>
|
|||
const defaultVariant = (): Variant => ({
|
||||
id: undefined,
|
||||
name: '',
|
||||
price: '',
|
||||
marketPrice: '',
|
||||
price: undefined,
|
||||
marketPrice: null,
|
||||
isFlashAvailable: false,
|
||||
flashPrice: '',
|
||||
flashPrice: null,
|
||||
isOffer: false,
|
||||
isComboOnly: false,
|
||||
isDeleted: false,
|
||||
isSuspended: false,
|
||||
attributes: [quantityAttribute()],
|
||||
features: [quantityAttribute()],
|
||||
comboItems: [],
|
||||
})
|
||||
|
||||
|
|
@ -100,7 +105,7 @@ const productValidationSchema = Yup.object().shape({
|
|||
.nullable()
|
||||
.transform((value, originalValue) => (originalValue === '' ? null : value))
|
||||
.optional(),
|
||||
attributes: Yup.array()
|
||||
features: Yup.array()
|
||||
.min(1, 'At least one attribute is required')
|
||||
.of(
|
||||
Yup.object().shape({
|
||||
|
|
@ -113,7 +118,7 @@ const productValidationSchema = Yup.object().shape({
|
|||
if (!Array.isArray(variants)) return true
|
||||
const seen = new Set<string>()
|
||||
for (const variant of variants) {
|
||||
const attrs = variant?.attributes || []
|
||||
const attrs = variant?.features || []
|
||||
const signature = variantSignature(attrs as Attribute[])
|
||||
if (seen.has(signature)) {
|
||||
return this.createError({ message: 'Two variants have the same attributes' })
|
||||
|
|
@ -125,7 +130,7 @@ const productValidationSchema = Yup.object().shape({
|
|||
.test('quantity-feature', 'Each SKU must have exactly one quantity feature', function (variants) {
|
||||
if (!Array.isArray(variants)) return true
|
||||
for (const variant of variants) {
|
||||
const attrs = variant?.attributes || []
|
||||
const attrs = variant?.features || []
|
||||
const quantityCount = attrs.filter(
|
||||
(a) => ((a as Attribute)?.featureName ?? '').trim().toLowerCase() === 'quantity'
|
||||
).length
|
||||
|
|
@ -180,7 +185,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
const { data: skusData } = trpc.common.product.getAllSkusSummary.useQuery({})
|
||||
const skuOptions = (skusData?.skus || []).map((sku) => ({
|
||||
label: sku.label,
|
||||
value: sku.id.toString(),
|
||||
value: sku.id,
|
||||
}))
|
||||
|
||||
// Make sure every SKU starts with a 'quantity' feature (auto-added at the
|
||||
|
|
@ -189,11 +194,11 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
return {
|
||||
...initialValues,
|
||||
variants: (initialValues.variants || []).map((v) => {
|
||||
const hasQuantity = (v.attributes || []).some(
|
||||
const hasQuantity = (v.features || []).some(
|
||||
(a) => (a.featureName ?? '').trim().toLowerCase() === 'quantity'
|
||||
)
|
||||
if (hasQuantity) return v
|
||||
return { ...v, attributes: [quantityAttribute(), ...(v.attributes || [])] }
|
||||
return { ...v, features: [quantityAttribute(), ...(v.features || [])] }
|
||||
}),
|
||||
}
|
||||
}, [initialValues])
|
||||
|
|
@ -211,7 +216,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
...values,
|
||||
variants: values.variants.map((v) => ({
|
||||
...v,
|
||||
attributes: v.attributes.map((a) => ({
|
||||
features: v.features.map((a) => ({
|
||||
...a,
|
||||
featureName: isQuantityFeature(a) ? 'quantity' : (a.featureName ?? '').trim() || null,
|
||||
})),
|
||||
|
|
@ -347,7 +352,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
|
||||
<FieldArray name={`variants.${vIndex}.attributes`}>
|
||||
<FieldArray name={`variants.${vIndex}.features`}>
|
||||
{({ push: pushAttr, remove: removeAttr }) => (
|
||||
<View style={tw`mb-3`}>
|
||||
<View style={tw`flex-row justify-between items-center mb-2`}>
|
||||
|
|
@ -360,12 +365,12 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
<MyText style={tw`text-gray-600 text-xs ml-0.5`}>Add</MyText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{variant.attributes.map((attr, aIndex) => {
|
||||
const quantityCount = variant.attributes.filter(isQuantityFeature).length
|
||||
{variant.features.map((attr, aIndex) => {
|
||||
const quantityCount = variant.features.filter(isQuantityFeature).length
|
||||
// Quantity features are locked (non-editable), but if there
|
||||
// are duplicates (e.g. 'quantity' + 'Quantity') the user must
|
||||
// be able to delete the extras to fix the form.
|
||||
const canDelete = variant.attributes.length > 1 && (!isQuantityFeature(attr) || quantityCount > 1)
|
||||
const canDelete = variant.features.length > 1 && (!isQuantityFeature(attr) || quantityCount > 1)
|
||||
return (
|
||||
<View key={aIndex} style={tw`flex-row items-center mb-2 gap-2`}>
|
||||
<View style={tw`flex-1`}>
|
||||
|
|
@ -374,7 +379,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
value={attr.featureName ?? ''}
|
||||
onChangeText={(text) =>
|
||||
setFieldValue(
|
||||
`variants.${vIndex}.attributes.${aIndex}.featureName`,
|
||||
`variants.${vIndex}.features.${aIndex}.featureName`,
|
||||
text.trim().toLowerCase() === 'quantity' ? 'quantity' : text
|
||||
)
|
||||
}
|
||||
|
|
@ -386,7 +391,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
<MyTextInput
|
||||
placeholder={isQuantityFeature(attr) ? 'e.g. 0.5 kg' : 'Value'}
|
||||
value={attr.featureValue}
|
||||
onChangeText={handleChange(`variants.${vIndex}.attributes.${aIndex}.featureValue`)}
|
||||
onChangeText={handleChange(`variants.${vIndex}.features.${aIndex}.featureValue`)}
|
||||
/>
|
||||
</View>
|
||||
{canDelete && (
|
||||
|
|
@ -420,7 +425,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
topLabel="Market Price"
|
||||
placeholder="MRP"
|
||||
keyboardType="numeric"
|
||||
value={variant.marketPrice}
|
||||
value={variant.marketPrice?.toString() ?? ''}
|
||||
onChangeText={handleChange(`variants.${vIndex}.marketPrice`)}
|
||||
/>
|
||||
</View>
|
||||
|
|
@ -429,7 +434,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
topLabel="Our Price"
|
||||
placeholder="Selling price"
|
||||
keyboardType="numeric"
|
||||
value={variant.price}
|
||||
value={variant.price?.toString() ?? ''}
|
||||
onChangeText={handleChange(`variants.${vIndex}.price`)}
|
||||
/>
|
||||
</View>
|
||||
|
|
@ -440,7 +445,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
checked={variant.isFlashAvailable}
|
||||
onPress={() => {
|
||||
setFieldValue(`variants.${vIndex}.isFlashAvailable`, !variant.isFlashAvailable)
|
||||
if (variant.isFlashAvailable) setFieldValue(`variants.${vIndex}.flashPrice`, '')
|
||||
if (variant.isFlashAvailable) setFieldValue(`variants.${vIndex}.flashPrice`, null)
|
||||
}}
|
||||
style={tw`mr-3`}
|
||||
/>
|
||||
|
|
@ -483,7 +488,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
topLabel="Flash Price"
|
||||
placeholder="Enter flash price"
|
||||
keyboardType="numeric"
|
||||
value={variant.flashPrice}
|
||||
value={variant.flashPrice?.toString() ?? ''}
|
||||
onChangeText={handleChange(`variants.${vIndex}.flashPrice`)}
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
|
|
@ -515,7 +520,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
const items = variant.comboItems || []
|
||||
items.push({ skuId: '' })
|
||||
items.push({ skuId: 0 })
|
||||
setFieldValue(`variants.${vIndex}.comboItems`, items)
|
||||
}}
|
||||
style={tw`bg-gray-200 px-2 py-0.5 rounded flex-row items-center`}
|
||||
|
|
@ -530,7 +535,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
<BottomDropdown
|
||||
label="SKU"
|
||||
value={ci.skuId}
|
||||
options={skuOptions.map((opt: { label: string; value: string }) => ({
|
||||
options={skuOptions.map((opt: { label: string; value: number }) => ({
|
||||
...opt,
|
||||
// Disable SKUs already picked in another row of this combo.
|
||||
disabled: (variant.comboItems || []).some(
|
||||
|
|
@ -546,7 +551,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
Alert.alert('Duplicate', 'This product is already in the combo')
|
||||
return
|
||||
}
|
||||
setFieldValue(`variants.${vIndex}.comboItems.${cIndex}.skuId`, val)
|
||||
setFieldValue(`variants.${vIndex}.comboItems.${cIndex}.skuId`, Number(val))
|
||||
}}
|
||||
placeholder="Select SKU"
|
||||
/>
|
||||
|
|
|
|||
6
apps/admin-ui/types/drag-order-item.ts
Normal file
6
apps/admin-ui/types/drag-order-item.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Common props for draggable reorder rows (customize-app / product-tags order screens)
|
||||
export interface DragOrderItemProps<T> {
|
||||
item: T
|
||||
drag: () => void
|
||||
isActive: boolean
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import type { IdName } from '@packages/shared';
|
||||
import type { AdminVendorSnippetInput } from '@packages/shared'
|
||||
|
||||
export type VendorSnippetProduct = IdName;
|
||||
|
||||
|
|
@ -22,12 +23,10 @@ export interface VendorSnippet {
|
|||
} | null;
|
||||
}
|
||||
|
||||
export interface VendorSnippetForm {
|
||||
// Vendor-snippet form row — anchored to the shared input (wire validTill);
|
||||
// form requires slotId and carries the serialized createdAt.
|
||||
export type VendorSnippetForm = Omit<AdminVendorSnippetInput, 'slotId'> & {
|
||||
id: number;
|
||||
snippetCode: string;
|
||||
slotId: number;
|
||||
isPermanent: boolean;
|
||||
skuIds: number[];
|
||||
validTill: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
|
@ -82,8 +82,7 @@ export type {
|
|||
AdminVendorSnippetWithSlot,
|
||||
AdminVendorSnippetProduct,
|
||||
AdminVendorSnippetWithProducts,
|
||||
AdminVendorSnippetCreateInput,
|
||||
AdminVendorSnippetUpdateInput,
|
||||
AdminVendorSnippetInput,
|
||||
AdminVendorSnippetDeleteResult,
|
||||
AdminVendorSnippetOrderProduct,
|
||||
AdminVendorSnippetOrderSummary,
|
||||
|
|
|
|||
|
|
@ -14,31 +14,18 @@ import {
|
|||
type ProductTagData,
|
||||
type ProductComboCacheData,
|
||||
} from '@/src/dbService'
|
||||
import type { ProductDetailCore } from '@packages/shared'
|
||||
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
|
||||
|
||||
// Uniform Product Type (matches getProductDetails return)
|
||||
interface Product {
|
||||
id: number
|
||||
productId: number
|
||||
name: string
|
||||
shortDescription: string | null
|
||||
longDescription: string | null
|
||||
price: string
|
||||
marketPrice: string | null
|
||||
unitNotation: string
|
||||
images: string[]
|
||||
isOutOfStock: boolean
|
||||
// Uniform Product Type (matches getProductDetails return) — anchored to the
|
||||
// shared ProductDetailCore; cache-specific relations stay local.
|
||||
interface Product extends ProductDetailCore {
|
||||
store: { id: number; name: string; description: string | null } | null
|
||||
incrementStep: number
|
||||
productQuantity: number
|
||||
isFlashAvailable: boolean
|
||||
flashPrice: string | null
|
||||
deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date; isCapacityFull: boolean }>
|
||||
specialDeals: Array<{ quantity: string; price: string; validTill: Date }>
|
||||
productTags: string[]
|
||||
productType: string
|
||||
isComboOnly: boolean
|
||||
isOffer: boolean
|
||||
isComboOnly: boolean
|
||||
comboItems: Array<{
|
||||
skuId: number
|
||||
skuName: string | null
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ import {
|
|||
type TagProductMapping,
|
||||
} from '@/src/dbService'
|
||||
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
|
||||
import type { ProductTagCore } from '@packages/shared'
|
||||
|
||||
// Tag Type (matches getDashboardTags return)
|
||||
interface Tag {
|
||||
// Tag Type (matches getDashboardTags return) — anchored to the shared tag core
|
||||
interface Tag extends ProductTagCore {
|
||||
id: number
|
||||
tagName: string
|
||||
tagDescription: string | null
|
||||
imageUrl: string | null
|
||||
isDashboardTag: boolean
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@ import {
|
|||
getAllSlotsWithProductsForCache,
|
||||
type SlotWithProductsData,
|
||||
} from '@/src/dbService'
|
||||
import type { UserProductDeliverySlot } from '@packages/shared'
|
||||
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// Define the structure for slot with products
|
||||
interface SlotWithProducts {
|
||||
id: number
|
||||
deliveryTime: Date
|
||||
freezeTime: Date
|
||||
// Define the structure for slot with products — anchored to the shared slot core
|
||||
interface SlotWithProducts extends UserProductDeliverySlot {
|
||||
isActive: boolean
|
||||
isCapacityFull: boolean
|
||||
products: Array<{
|
||||
|
|
@ -28,10 +26,7 @@ interface SlotWithProducts {
|
|||
}>
|
||||
}
|
||||
|
||||
interface SlotInfo {
|
||||
id: number
|
||||
deliveryTime: Date
|
||||
freezeTime: Date
|
||||
interface SlotInfo extends UserProductDeliverySlot {
|
||||
isCapacityFull: boolean
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import {
|
|||
checkReservedCouponExists,
|
||||
getOrderWithUser,
|
||||
} from '@/src/dbService'
|
||||
import type { Coupon, CouponValidationResult, UserMiniInfo } from '@packages/shared'
|
||||
import type { Coupon, CouponFormInput, CouponValidationResult, UserMiniInfo } from '@packages/shared'
|
||||
|
||||
const createCouponBodySchema = z.object({
|
||||
couponCode: z.string().optional(),
|
||||
|
|
@ -100,20 +100,22 @@ export const couponRouter = router({
|
|||
throw new Error(`Coupon code already exists: ${finalCouponCode}`);
|
||||
}
|
||||
|
||||
// Complete CouponFormInput — every field sent (nullable columns get null)
|
||||
const coupon = await createCouponWithRelations(
|
||||
{
|
||||
couponCode: finalCouponCode,
|
||||
isUserBased: isUserBased || false,
|
||||
discountPercent: discountPercent?.toString(),
|
||||
flatDiscount: flatDiscount?.toString(),
|
||||
minOrder: minOrder?.toString(),
|
||||
discountPercent: discountPercent?.toString() ?? null,
|
||||
flatDiscount: flatDiscount?.toString() ?? null,
|
||||
minOrder: minOrder?.toString() ?? null,
|
||||
skuIds: skuIds || null,
|
||||
createdBy: staffUserId,
|
||||
maxValue: maxValue?.toString(),
|
||||
maxValue: maxValue?.toString() ?? null,
|
||||
isApplyForAll: isApplyForAll || false,
|
||||
validTill: validTill ? dayjs(validTill).toDate() : undefined,
|
||||
maxLimitForUser,
|
||||
validTill: validTill ? dayjs(validTill).toDate() : null,
|
||||
maxLimitForUser: maxLimitForUser ?? null,
|
||||
exclusiveApply: exclusiveApply || false,
|
||||
isInvalidated: false,
|
||||
},
|
||||
applicableUsers,
|
||||
applicableProducts
|
||||
|
|
@ -177,21 +179,29 @@ export const couponRouter = router({
|
|||
}
|
||||
}
|
||||
|
||||
// Prepare update data
|
||||
const updateData: any = {};
|
||||
// Prepare update data — always a COMPLETE CouponFormInput: fields present
|
||||
// in the payload win, everything else keeps its current value (the data
|
||||
// is still sent even where the caller doesn't need it).
|
||||
const existing = await getCouponByIdFromDb(id);
|
||||
if (!existing) {
|
||||
throw new Error("Coupon not found");
|
||||
}
|
||||
const updatedCode = updates.couponCodes?.[0]?.trim() || updates.couponCode;
|
||||
if (updatedCode !== undefined && updatedCode !== '') updateData.couponCode = updatedCode.trim().toUpperCase();
|
||||
if (updates.isUserBased !== undefined) updateData.isUserBased = updates.isUserBased;
|
||||
if (updates.discountPercent !== undefined) updateData.discountPercent = updates.discountPercent?.toString();
|
||||
if (updates.flatDiscount !== undefined) updateData.flatDiscount = updates.flatDiscount?.toString();
|
||||
if (updates.minOrder !== undefined) updateData.minOrder = updates.minOrder?.toString();
|
||||
if (updates.maxValue !== undefined) updateData.maxValue = updates.maxValue?.toString();
|
||||
if (updates.isApplyForAll !== undefined) updateData.isApplyForAll = updates.isApplyForAll;
|
||||
if (updates.validTill !== undefined) updateData.validTill = updates.validTill ? dayjs(updates.validTill).toDate() : null;
|
||||
if (updates.maxLimitForUser !== undefined) updateData.maxLimitForUser = updates.maxLimitForUser;
|
||||
if (updates.exclusiveApply !== undefined) updateData.exclusiveApply = updates.exclusiveApply;
|
||||
if (updates.isInvalidated !== undefined) updateData.isInvalidated = updates.isInvalidated;
|
||||
if (updates.skuIds !== undefined) updateData.skuIds = updates.skuIds;
|
||||
const updateData: CouponFormInput = {
|
||||
couponCode: updatedCode !== undefined && updatedCode !== '' ? updatedCode.trim().toUpperCase() : existing.couponCode,
|
||||
isUserBased: updates.isUserBased ?? existing.isUserBased,
|
||||
discountPercent: updates.discountPercent !== undefined ? (updates.discountPercent?.toString() ?? null) : existing.discountPercent,
|
||||
flatDiscount: updates.flatDiscount !== undefined ? (updates.flatDiscount?.toString() ?? null) : existing.flatDiscount,
|
||||
minOrder: updates.minOrder !== undefined ? (updates.minOrder?.toString() ?? null) : existing.minOrder,
|
||||
skuIds: updates.skuIds !== undefined ? updates.skuIds : existing.skuIds ?? null,
|
||||
maxValue: updates.maxValue !== undefined ? (updates.maxValue?.toString() ?? null) : existing.maxValue,
|
||||
isApplyForAll: updates.isApplyForAll ?? existing.isApplyForAll,
|
||||
validTill: updates.validTill !== undefined ? (updates.validTill ? dayjs(updates.validTill).toDate() : null) : existing.validTill,
|
||||
maxLimitForUser: updates.maxLimitForUser ?? existing.maxLimitForUser,
|
||||
exclusiveApply: updates.exclusiveApply ?? existing.exclusiveApply,
|
||||
isInvalidated: updates.isInvalidated ?? existing.isInvalidated,
|
||||
createdBy: existing.createdBy,
|
||||
};
|
||||
|
||||
// Using dbService helper (new implementation)
|
||||
const coupon = await updateCouponWithRelations(
|
||||
|
|
|
|||
|
|
@ -849,7 +849,7 @@ export const productRouter = router({
|
|||
}),
|
||||
|
||||
getProductTags: protectedProcedure
|
||||
.query(async (): Promise<{ tags: Array<{ id: number; tagName: string; tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; relatedStores: unknown; createdAt: Date }>; message: string }> => {
|
||||
.query(async (): Promise<{ tags: Array<{ id: number; tagName: string; tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; relatedStores: number[] | null; createdAt: Date }>; message: string }> => {
|
||||
const tags = await getAllProductTagInfosInDb()
|
||||
|
||||
const tagsWithSignedUrls = await Promise.all(
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ const createSlotSchema = z.object({
|
|||
name: z.string().min(1),
|
||||
skuIds: z.array(z.number().int().positive()).min(1),
|
||||
validTill: z.string().optional(),
|
||||
groupIds: z.array(z.number()).optional(),
|
||||
})).optional(),
|
||||
groupIds: z.array(z.number()).optional(),
|
||||
});
|
||||
|
|
@ -70,6 +71,7 @@ const updateSlotSchema = z.object({
|
|||
name: z.string().min(1),
|
||||
skuIds: z.array(z.number().int().positive()).min(1),
|
||||
validTill: z.string().optional(),
|
||||
groupIds: z.array(z.number()).optional(),
|
||||
})).optional(),
|
||||
groupIds: z.array(z.number()).optional(),
|
||||
});
|
||||
|
|
@ -297,7 +299,13 @@ export const slotsRouter = router({
|
|||
freezeTime,
|
||||
isActive,
|
||||
skuIds,
|
||||
vendorSnippets: snippets,
|
||||
// Complete SlotSnippetInput payloads — groupIds unused by sqlite but
|
||||
// always sent; absent validTill becomes null.
|
||||
vendorSnippets: snippets?.map((snippet) => ({
|
||||
...snippet,
|
||||
groupIds: snippet.groupIds ?? [],
|
||||
validTill: snippet.validTill ?? null,
|
||||
})),
|
||||
groupIds,
|
||||
})
|
||||
|
||||
|
|
@ -462,7 +470,13 @@ export const slotsRouter = router({
|
|||
freezeTime,
|
||||
isActive,
|
||||
skuIds,
|
||||
vendorSnippets: snippets,
|
||||
// Complete SlotSnippetInput payloads — groupIds unused by sqlite but
|
||||
// always sent; absent validTill becomes null.
|
||||
vendorSnippets: snippets?.map((snippet) => ({
|
||||
...snippet,
|
||||
groupIds: snippet.groupIds ?? [],
|
||||
validTill: snippet.validTill ?? null,
|
||||
})),
|
||||
groupIds,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export const productRouter = router({
|
|||
|
||||
return {
|
||||
...cachedProduct,
|
||||
images: cachedProduct.images as string[],
|
||||
deliverySlots: filteredSlots
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -365,13 +365,7 @@ const ProductItem = memo(({ item, onPress }: ProductItemProps) => {
|
|||
);
|
||||
});
|
||||
|
||||
interface ListHeaderProps {
|
||||
dashboardTags: any[];
|
||||
activeTagId: number | null;
|
||||
productsByTagId: Record<number, any[]>;
|
||||
expandedByTagId: Record<number, boolean>;
|
||||
onToggleExpand: (tagId: number) => void;
|
||||
onProductPress: (id: number) => void;
|
||||
interface ListHeaderProps extends TagSceneProps {
|
||||
onSelectTag: (id: number) => void;
|
||||
storesData: any;
|
||||
sortedSlots: any[];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import type { UserAddress } from '@packages/shared'
|
||||
import { View, Alert } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { AppContainer, MyText, tw, useMarkDataFetchers, MyFlatList, MyTouchableOpacity, BottomDialog, RawBottomDialog } from 'common-ui';
|
||||
|
|
@ -6,19 +7,14 @@ import { trpc } from '@/src/trpc-client';
|
|||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import AddressForm from '@/src/components/AddressForm';
|
||||
|
||||
interface Address {
|
||||
id: number;
|
||||
name: string;
|
||||
phone: string;
|
||||
addressLine1: string;
|
||||
addressLine2: string | null;
|
||||
city: string;
|
||||
state: string;
|
||||
pincode: string;
|
||||
isDefault: boolean;
|
||||
latitude?: number | null;
|
||||
longitude?: number | null;
|
||||
googleMapsUrl?: string | null;
|
||||
// Address view — required core of the shared UserAddress + optional geo fields
|
||||
type Address = Pick<
|
||||
UserAddress,
|
||||
'id' | 'name' | 'phone' | 'addressLine1' | 'addressLine2' | 'city' | 'state' | 'pincode' | 'isDefault'
|
||||
> & {
|
||||
latitude?: number | null
|
||||
longitude?: number | null
|
||||
googleMapsUrl?: string | null
|
||||
}
|
||||
|
||||
function AddressCard({ address, onEdit, onDelete, onSetDefault, isDeleting }: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import type { OrderCardActionProps } from '@packages/shared'
|
||||
import { View, TouchableOpacity, ActivityIndicator, Linking, ScrollView, Dimensions, NativeSyntheticEvent, NativeScrollEvent } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { tw, MyText } from 'common-ui';
|
||||
|
|
@ -16,9 +17,7 @@ const { width: screenWidth } = Dimensions.get('window');
|
|||
const CARD_WIDTH = (screenWidth - 48) * 0.95; // card width (24px padding each side), reduced 5%
|
||||
const SNAP_INTERVAL = CARD_WIDTH + 24; // card + right margin = page width
|
||||
|
||||
interface OrderCardProps {
|
||||
order: Order;
|
||||
onPress: () => void;
|
||||
interface OrderCardProps extends OrderCardActionProps<Order> {
|
||||
supportMobile?: string | null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,10 +26,7 @@ const { width: screenWidth } = Dimensions.get("window");
|
|||
const drawerWidth = 85; // From layout drawerStyle
|
||||
const itemWidth = (screenWidth - drawerWidth - 48) / 2; // Account for drawer width
|
||||
|
||||
interface SlotLayoutProps {
|
||||
slotId?: number;
|
||||
storeId?: number;
|
||||
baseUrl: string;
|
||||
interface SlotLayoutProps extends SlotProductsProps {
|
||||
isForFlashDelivery?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -330,7 +327,7 @@ const CompactProductCard = ({
|
|||
|
||||
interface SlotProductsProps {
|
||||
slotId?: number;
|
||||
storeId?: number;
|
||||
storeId?: number;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import { create } from 'zustand';
|
||||
import type { FlashNavigationState as SharedFlashNavigationState } from '@packages/shared';
|
||||
|
||||
interface FlashNavigationState {
|
||||
shouldNavigateToCart: boolean;
|
||||
setShouldNavigateToCart: (value: boolean) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
type FlashNavigationState = SharedFlashNavigationState & { reset: () => void }
|
||||
|
||||
export const useFlashNavigationStore = create<FlashNavigationState>((set) => ({
|
||||
shouldNavigateToCart: false,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useCentralProductStore } from '@/src/store/centralProductStore';
|
|||
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
|
||||
import { Alert } from 'react-native';
|
||||
import { useQuery, useMutation, useQueryClient, UseQueryResult, UseMutationResult } from '@tanstack/react-query';
|
||||
import type { CartData as SharedCartData } from '@packages/shared';
|
||||
import type { CartData as SharedCartData, ProductSummaryCore } from '@packages/shared';
|
||||
import { StorageServiceCasual } from 'common-ui/src/services/StorageServiceCasual';
|
||||
|
||||
// Cart type definition
|
||||
|
|
@ -12,26 +12,21 @@ const getCartStorageKey = (cartType: CartType = "regular"): string => {
|
|||
return cartType === "flash" ? "flash_cart_items" : "cart_items";
|
||||
};
|
||||
|
||||
interface LocalCartItem {
|
||||
id: number;
|
||||
skuId: number;
|
||||
quantity: number;
|
||||
slotId: number;
|
||||
addedAt: string;
|
||||
}
|
||||
type LocalCartItem = Omit<CartItem, 'subtotal'>
|
||||
|
||||
interface ProductSummary {
|
||||
id: number;
|
||||
price: string;
|
||||
incrementStep: number;
|
||||
isOutOfStock: boolean;
|
||||
isFlashAvailable: boolean;
|
||||
name?: string;
|
||||
flashPrice?: string | null;
|
||||
images?: string[];
|
||||
productQuantity?: number;
|
||||
unitNotation?: string;
|
||||
marketPrice?: string | null;
|
||||
// Cart product summary — anchored to the shared ProductSummaryCore; the cart
|
||||
// view keeps most display fields optional and adds the flash flags.
|
||||
type ProductSummary = Omit<
|
||||
ProductSummaryCore,
|
||||
'name' | 'images' | 'unitNotation' | 'marketPrice'
|
||||
> & {
|
||||
isFlashAvailable: boolean
|
||||
name?: string
|
||||
flashPrice?: string | null
|
||||
images?: string[] | null
|
||||
productQuantity?: number
|
||||
unitNotation?: string
|
||||
marketPrice?: string | null
|
||||
}
|
||||
|
||||
export interface CartItem {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import type { AuthRedirectOptions } from '@packages/shared'
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useAuth } from '@/src/contexts/AuthContext';
|
||||
import type { RedirectState } from '@/src/contexts/AuthContext';
|
||||
import { StorageServiceCasual } from 'common-ui';
|
||||
import constants from '@/src/constants';
|
||||
|
||||
interface AuthenticatedRouteOptions {
|
||||
targetUrl?: string;
|
||||
queryParams?: Record<string, any>;
|
||||
interface AuthenticatedRouteOptions extends AuthRedirectOptions {
|
||||
}
|
||||
|
||||
export function useAuthenticatedRoute(options: AuthenticatedRouteOptions = {}) {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import { trpc } from '@/src/trpc-client';
|
|||
import { StorageServiceCasual } from 'common-ui';
|
||||
import { useRouter } from 'expo-router';
|
||||
import constants from '@/src/constants';
|
||||
import type { ReactParentComponent } from '@packages/shared';
|
||||
import type { AuthRedirectOptions, ReactParentComponent } from '@packages/shared';
|
||||
|
||||
export interface RedirectState {
|
||||
export interface RedirectState extends AuthRedirectOptions {
|
||||
targetUrl: string;
|
||||
queryParams: Record<string, any>;
|
||||
timestamp: number;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'
|
|||
import { useQuery } from '@tanstack/react-query'
|
||||
import axios from 'axios'
|
||||
import { trpc } from '@/src/trpc-client'
|
||||
import type { ProductPriceFlags } from '@packages/shared'
|
||||
import { AllProductsApiType, StoresApiType, SlotsApiType, EssentialConstsApiType, StoreWithProductsApiType, AvailabilityApiType } from "@backend/trpc/router";
|
||||
import { CACHE_FILENAMES } from "@packages/shared";
|
||||
import { StorageServiceCasual } from 'common-ui';
|
||||
|
|
@ -58,13 +59,7 @@ type AvailabilityResponse = AvailabilityApiType;
|
|||
type BaseProduct = AllProductsApiType['products'][number]
|
||||
export type AvailabilityEntry = AvailabilityApiType['availability'][number]
|
||||
|
||||
export type MergedProduct = BaseProduct & {
|
||||
price: number
|
||||
marketPrice: number | null
|
||||
flashPrice: string | null
|
||||
isFlashAvailable: boolean
|
||||
isOutOfStock: boolean
|
||||
isSuspended: boolean
|
||||
export type MergedProduct = BaseProduct & ProductPriceFlags & {
|
||||
isComboOnly?: boolean
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import { create } from 'zustand';
|
||||
import type { AddressSelectionState } from '@packages/shared';
|
||||
|
||||
interface AddressState {
|
||||
selectedAddressId: number | null;
|
||||
setSelectedAddressId: (addressId: number | null) => void;
|
||||
}
|
||||
|
||||
export const useAddressStore = create<AddressState>((set) => ({
|
||||
export const useAddressStore = create<AddressSelectionState>((set) => ({
|
||||
selectedAddressId: null,
|
||||
setSelectedAddressId: (addressId) => set({ selectedAddressId: addressId }),
|
||||
}));
|
||||
|
|
@ -1,16 +1,11 @@
|
|||
import { create } from 'zustand'
|
||||
import { useEffect } from 'react'
|
||||
import { useAllProducts, type MergedProduct } from '@/src/hooks/prominent-api-hooks'
|
||||
import type { CentralProductState as CentralProductsState } from '@packages/shared'
|
||||
|
||||
export type Product = MergedProduct
|
||||
|
||||
interface CentralProductState {
|
||||
products: Product[]
|
||||
productsById: Record<number, Product>
|
||||
refetchProducts: (() => Promise<void>) | null
|
||||
setProducts: (products: Product[]) => void
|
||||
setRefetchProducts: (refetch: () => Promise<void>) => void
|
||||
}
|
||||
type CentralProductState = CentralProductsState<Product>
|
||||
|
||||
export const useCentralProductStore = create<CentralProductState>((set) => ({
|
||||
products: [],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
import { create } from 'zustand';
|
||||
|
||||
interface NavigationState {
|
||||
isNavigatedFromHome: boolean;
|
||||
setNavigatedFromHome: (value: boolean) => void;
|
||||
selectedStoreId: number | null;
|
||||
setSelectedStoreId: (storeId: number | null) => void;
|
||||
}
|
||||
import type { NavigationState } from '@packages/shared';
|
||||
|
||||
export const useNavigationStore = create<NavigationState>((set) => ({
|
||||
isNavigatedFromHome: false,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import { create } from 'zustand';
|
||||
import type { QuickDeliveryState as SharedQuickDeliveryState } from '@packages/shared';
|
||||
|
||||
interface QuickDeliveryState {
|
||||
isDrawerHidden: boolean;
|
||||
selectedSlotId: number | null;
|
||||
setSelectedSlotId: (slotId: number | null) => void;
|
||||
}
|
||||
// user-ui only reads/hides the drawer — no setDrawerHidden action here
|
||||
type QuickDeliveryState = Omit<SharedQuickDeliveryState, 'setDrawerHidden'>
|
||||
|
||||
export const useQuickDeliveryStore = create<QuickDeliveryState>((set) => ({
|
||||
isDrawerHidden: false,
|
||||
|
|
|
|||
|
|
@ -1,30 +1,20 @@
|
|||
export interface User {
|
||||
id: number;
|
||||
name?: string | null;
|
||||
email: string | null;
|
||||
mobile: string | null;
|
||||
profileImage?: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
import type { User as SharedUser, UserAuthProfile, AuthStateCore } from '@packages/shared'
|
||||
|
||||
export interface UserDetails {
|
||||
id: number;
|
||||
name?: string | null;
|
||||
email: string | null;
|
||||
mobile: string | null;
|
||||
profileImage?: string | null;
|
||||
bio?: string | null;
|
||||
dateOfBirth?: string | null;
|
||||
gender?: string | null;
|
||||
occupation?: string | null;
|
||||
}
|
||||
// Auth user — serialized view of the shared User (string dates, nullable mobile)
|
||||
export type User = Pick<SharedUser, 'id' | 'email'> &
|
||||
Partial<Omit<SharedUser, 'id' | 'email' | 'createdAt' | 'mobile'>> & {
|
||||
mobile: string | null
|
||||
profileImage?: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
// Profile details — required id/email/mobile, everything else optional
|
||||
export type UserDetails = Pick<UserAuthProfile, 'id' | 'email' | 'mobile'> &
|
||||
Partial<Omit<UserAuthProfile, 'id' | 'email' | 'mobile' | 'createdAt'>>
|
||||
|
||||
export interface AuthState extends AuthStateCore {
|
||||
user: User | null;
|
||||
userDetails: UserDetails | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
token: string | null;
|
||||
}
|
||||
|
||||
import type { LoginRequest, RegisterRequest } from '@packages/shared'
|
||||
|
|
|
|||
|
|
@ -1,31 +1,15 @@
|
|||
// Shared order view-model types for user-ui
|
||||
// (Were identically duplicated in me/my-orders/index.tsx and NextOrderGlimpse.tsx)
|
||||
import type { UserOrderItemSummary, UserOrderSummary } from '@packages/shared'
|
||||
|
||||
export interface OrderItem {
|
||||
productName: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
amount: number;
|
||||
image: string | null;
|
||||
}
|
||||
// Order line item = shared summary minus the discounted price column
|
||||
export type OrderItem = Pick<
|
||||
UserOrderItemSummary,
|
||||
'productName' | 'quantity' | 'price' | 'amount' | 'image'
|
||||
>
|
||||
|
||||
export interface Order {
|
||||
id: number;
|
||||
orderId: string;
|
||||
orderDate: string;
|
||||
deliveryStatus: string;
|
||||
deliveryDate?: string;
|
||||
orderStatus: string;
|
||||
cancelReason: string | null;
|
||||
totalAmount: number;
|
||||
deliveryCharge: number;
|
||||
paymentMode: string;
|
||||
paymentStatus: string;
|
||||
refundStatus: string;
|
||||
refundAmount: number | null;
|
||||
userNotes: string | null;
|
||||
items: OrderItem[];
|
||||
discountAmount?: number;
|
||||
isFlashDelivery: boolean;
|
||||
createdAt: string;
|
||||
export type Order = Omit<UserOrderSummary, 'deliveryDate' | 'items' | 'discountAmount'> & {
|
||||
deliveryDate?: string
|
||||
items: OrderItem[]
|
||||
discountAmount?: number
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
interface DialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
children: React.ReactNode
|
||||
title?: string
|
||||
}
|
||||
import type { DialogProps } from '@packages/shared'
|
||||
|
||||
export function Dialog({ open, onClose, children, title }: DialogProps) {
|
||||
// Close on escape key
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useMemo, useEffect, useState } from 'react'
|
|||
import { useQuery } from '@tanstack/react-query'
|
||||
import axios from 'axios'
|
||||
import { trpc } from '../lib/trpc-client'
|
||||
import type { ProductPriceFlags } from '@packages/shared'
|
||||
import type {
|
||||
AllProductsApiType,
|
||||
AvailabilityApiType,
|
||||
|
|
@ -92,14 +93,7 @@ type AvailabilityResponse = AvailabilityApiType
|
|||
type BaseProduct = AllProductsApiType['products'][number]
|
||||
type AvailabilityEntry = AvailabilityApiType['availability'][number]
|
||||
|
||||
export type MergedProduct = BaseProduct & {
|
||||
price: number
|
||||
marketPrice: number | null
|
||||
flashPrice: string | null
|
||||
isFlashAvailable: boolean
|
||||
isOutOfStock: boolean
|
||||
isSuspended: boolean
|
||||
}
|
||||
export type MergedProduct = BaseProduct & ProductPriceFlags
|
||||
|
||||
function useCacheUrl(filename: string): string | null {
|
||||
const { data: essentialConsts } = useGetEssentialConsts()
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'
|
||||
import { trpc } from './trpc-client'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import type { User as SharedUser, AuthStateCore } from '@packages/shared'
|
||||
|
||||
interface User {
|
||||
id: number
|
||||
name?: string | null | undefined
|
||||
mobile: string | null
|
||||
email: string | null
|
||||
profileImage: string | null
|
||||
// Auth user — serialized view of the shared User (optional name, no createdAt)
|
||||
type User = Pick<SharedUser, 'id' | 'email'> &
|
||||
Partial<Omit<SharedUser, 'id' | 'email' | 'createdAt' | 'mobile'>> & {
|
||||
mobile: string | null
|
||||
profileImage: string | null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
interface AuthState extends AuthStateCore {
|
||||
user: User | null
|
||||
userDetails: any | null
|
||||
isAuthenticated: boolean
|
||||
isLoading: boolean
|
||||
token: string | null
|
||||
}
|
||||
|
||||
interface AuthContextType extends AuthState {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import { create } from 'zustand'
|
||||
import type { AddressSelectionState } from '@packages/shared'
|
||||
|
||||
interface AddressState {
|
||||
selectedAddressId: number | null
|
||||
setSelectedAddressId: (addressId: number | null) => void
|
||||
clearSelectedAddress: () => void
|
||||
}
|
||||
type AddressState = AddressSelectionState & { clearSelectedAddress: () => void }
|
||||
|
||||
export const useAddressStore = create<AddressState>((set) => ({
|
||||
selectedAddressId: null,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { create } from 'zustand'
|
||||
import type { CentralProductState as CentralProductsState } from '@packages/shared'
|
||||
|
||||
interface Product {
|
||||
id: number
|
||||
|
|
@ -14,13 +15,7 @@ interface Product {
|
|||
description: string | null
|
||||
}
|
||||
|
||||
interface CentralProductStore {
|
||||
products: Product[]
|
||||
productsById: Record<number, Product>
|
||||
setProducts: (products: Product[]) => void
|
||||
refetchProducts: (() => void) | null
|
||||
setRefetchProducts: (fn: (() => void) | null) => void
|
||||
}
|
||||
type CentralProductStore = CentralProductsState<Product>
|
||||
|
||||
export const useCentralProductStore = create<CentralProductStore>((set) => ({
|
||||
products: [],
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { create } from 'zustand'
|
||||
import type { FlashNavigationState } from '@packages/shared'
|
||||
|
||||
interface FlashNavigationStore {
|
||||
shouldNavigateToCart: boolean
|
||||
setShouldNavigateToCart: (value: boolean) => void
|
||||
}
|
||||
type FlashNavigationStore = FlashNavigationState
|
||||
|
||||
export const useFlashNavigationStore = create<FlashNavigationStore>((set) => ({
|
||||
shouldNavigateToCart: false,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import { create } from 'zustand'
|
||||
import type { NavigationState } from '@packages/shared'
|
||||
|
||||
interface NavigationStore {
|
||||
isNavigatedFromHome: boolean
|
||||
selectedStoreId: number | null
|
||||
setNavigatedFromHome: (value: boolean) => void
|
||||
setSelectedStoreId: (id: number | null) => void
|
||||
}
|
||||
type NavigationStore = NavigationState
|
||||
|
||||
export const useNavigationStore = create<NavigationStore>((set) => ({
|
||||
isNavigatedFromHome: false,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import { create } from 'zustand'
|
||||
import type { QuickDeliveryState } from '@packages/shared'
|
||||
|
||||
interface QuickDeliveryStore {
|
||||
isDrawerHidden: boolean
|
||||
selectedSlotId: number | null
|
||||
setDrawerHidden: (hidden: boolean) => void
|
||||
setSelectedSlotId: (id: number | null) => void
|
||||
}
|
||||
type QuickDeliveryStore = QuickDeliveryState
|
||||
|
||||
export const useQuickDeliveryStore = create<QuickDeliveryStore>((set) => ({
|
||||
isDrawerHidden: false,
|
||||
|
|
|
|||
|
|
@ -4,22 +4,19 @@ import { useState } from 'react'
|
|||
import { p, MyButton, AppContainer, div } from 'web-components'
|
||||
import { Ticket, AlertCircle } from 'lucide-react'
|
||||
import dayjs from 'dayjs'
|
||||
import type { UserCouponDisplay } from '@packages/shared'
|
||||
|
||||
export const Route = createFileRoute('/me/coupons')({ component: CouponsPage })
|
||||
|
||||
interface Coupon {
|
||||
id: number
|
||||
code: string
|
||||
discountType: 'percentage' | 'flat'
|
||||
discountValue: number
|
||||
// Coupons screen view-model — relaxed optionality over the shared display type
|
||||
type Coupon = Omit<
|
||||
UserCouponDisplay,
|
||||
'maxValue' | 'minOrder' | 'validTill' | 'maxLimitForUser'
|
||||
> & {
|
||||
maxValue?: number
|
||||
minOrder?: number
|
||||
description: string
|
||||
validTill?: string | Date
|
||||
usageCount: number
|
||||
maxLimitForUser?: number
|
||||
isExpired: boolean
|
||||
isUsedUp: boolean
|
||||
}
|
||||
|
||||
interface CouponCardProps {
|
||||
|
|
|
|||
204
change-log.txt
204
change-log.txt
|
|
@ -1212,3 +1212,207 @@ apps/fallback-ui/src/components/3d/Characters.tsx + 3d/Scene.tsx (c48):
|
|||
|
||||
[2026-09-05 12:55:00] COMPLETED type-cluster unification pass (clusters 3,7,8,9,11,12,13,14,17,19,20,21,44,47,48,49,50,51,52,53,54,56 → @packages/shared).
|
||||
Verification (tsc --noEmit error counts vs pre-change baselines): backend 11=11, web-ui 150=150, user-ui 106=106, admin-ui 122=122, fallback-ui 107=107, db_helper_sqlite 4=4. Diffs are line-number shifts only (deleted lines) — zero new errors. `apps/web-ui` vite build exit 0. db_helper_postgres: untouched. Remaining same-name locals verified genuinely different shapes (bottom-dropdown/multi-select DropdownOption variants, web-ui Dialog.tsx DialogProps, user-ui floating-cart-bar props) — intentionally left. type-clusters/README.md verdicts updated to ✅ for the resolved clusters with a pass note at top.
|
||||
|
||||
================================================================================
|
||||
2026-09-05 13:40:00 — IMPLEMENT all 🟢 "unify now" clusters from similar_types.md (29 clusters; 🟡/🟣/⚪/🔵 untouched). db_helper_postgres still excluded. Derived types are shape-EXACT replacements of the locals they replace (verified per-cluster; tsc baselines re-checked after).
|
||||
|
||||
packages/shared/types/admin.ts (clusters 8,10,22,23,43,51):
|
||||
- import type { UnitSeedData } from './seed.types' (top import block)
|
||||
- AdminRebalanceSlotsResult {success,updatedOrders,message} → extends MessageResponse { updatedOrders: number[] } (c23)
|
||||
- AdminUnit {id,shortNotation,fullName} → extends UnitSeedData { id: number } (c51)
|
||||
- AdminSpecialDeal → NEW `export interface SpecialDealCore { quantity: string; price: string; validTill: Date }` + `AdminSpecialDeal extends SpecialDealCore { id: number; skuId: number }` (c10)
|
||||
- AdminProductReview → NEW `export interface ProductReviewCore { id: number; reviewBody: string; ratings: number; reviewTime: Date; userName: string|null }` + `AdminProductReview extends ProductReviewCore { imageUrls: unknown; adminResponse: string|null; adminResponseImages: unknown }` (c22)
|
||||
- AdminProductGroup {id,groupName,description,createdAt,products,productCount} → extends AdminProductGroupInfo { products: AdminSku[]; productCount: number } (c8)
|
||||
- NEW SkuSummary { id: number; productId: number; productName: string; label: string; images: unknown; storeId: number|null; price: string } (after AdminSlotsProductIdsResult) (c43)
|
||||
|
||||
packages/shared/types/user.ts (clusters 4,10,22,29):
|
||||
- './admin' import adds ProductReviewCore, SpecialDealCore; NEW import type { Complaint } from './complaint.types'
|
||||
- UserComplaint {6 fields} → type UserComplaint = Pick<Complaint, 'id'|'complaintBody'|'response'|'isResolved'|'createdAt'|'orderId'> (c29; shape identical)
|
||||
- NEW `export interface StoreProductCard { id; name; shortDescription: string|null; price: string; marketPrice: string|null; incrementStep: number; unit: string; unitNotation: string; images: string[]|null; isOutOfStock: boolean; productQuantity: number }` (c4)
|
||||
- UserStoreProduct → type UserStoreProduct = Omit<StoreProductCard,'images'> & { images: string[] } (c4)
|
||||
- UserStoreProductData → type UserStoreProductData = StoreProductCard (c4)
|
||||
- UserProductSpecialDeal {quantity,price,validTill} → type UserProductSpecialDeal = SpecialDealCore (c10)
|
||||
- UserProductReview {id,reviewBody,ratings,imageUrls:string[]|null,reviewTime,userName} → interface UserProductReview extends ProductReviewCore { imageUrls: string[] | null } (c22)
|
||||
|
||||
packages/shared/types/store.types.ts (clusters 47,48):
|
||||
- + import type { IdName } from './primitives.types'; + import type { UserTagSummary } from './user' (type-only cycle, erased)
|
||||
- StoreSummary {id,name,description} → interface StoreSummary extends IdName { description: string | null } (c47)
|
||||
- StoreTag {id,tagName,productIds?} → type StoreTag = Pick<UserTagSummary,'id'|'tagName'> & { productIds?: number[] } (c48; productIds stays OPTIONAL)
|
||||
|
||||
packages/shared/types/app-common.types.ts (clusters 19,26,28,34,37,42):
|
||||
+ AuthStateCore { isAuthenticated: boolean; isLoading: boolean; token: string | null } (c26)
|
||||
+ AddressSelectionState { selectedAddressId: number|null; setSelectedAddressId: (addressId: number|null) => void } (c19)
|
||||
+ NavigationState { isNavigatedFromHome: boolean; selectedStoreId: number|null; setNavigatedFromHome(value); setSelectedStoreId(id: number|null) } (c37)
|
||||
+ FlashNavigationState { shouldNavigateToCart: boolean; setShouldNavigateToCart(value) } (c34)
|
||||
+ QuickDeliveryState { isDrawerHidden: boolean; selectedSlotId: number|null; setDrawerHidden(hidden); setSelectedSlotId(id: number|null) } (c42)
|
||||
+ CentralProductState<Product = unknown> { products: Product[]; productsById: Record<number,Product>; refetchProducts: (() => void | Promise<void>) | null; setProducts(products); setRefetchProducts(refetch) } (c28; refetch union accepts void AND Promise<void> to cover both apps)
|
||||
|
||||
packages/shared/types/index.ts: app-common comment line updated to list new types.
|
||||
|
||||
packages/db_helper_sqlite/src/stores/store-helpers.ts (c10):
|
||||
- SpecialDealData {skuId,quantity,price,validTill} → interface SpecialDealData extends SpecialDealCore { skuId: number } (SpecialDealCore added to existing @packages/shared import; index.ts:318 re-export unaffected)
|
||||
|
||||
packages/db_helper_sqlite/src/user-apis/product.ts (c43):
|
||||
- SkuSummary {7 fields} → import type { SkuSummary } from '@packages/shared' + export type { SkuSummary } (module export surface kept; getAllSkusSummary signature unchanged)
|
||||
|
||||
apps/admin-ui:
|
||||
- dashboard-banners/index.tsx (c6): interface Banner → type Banner = Omit<SharedBanner,'createdAt'|'lastUpdated'> & { createdAt: string; lastUpdated: string }
|
||||
- dashboard-banners/edit/[id].tsx (c6): interface Banner → Omit<SharedBanner,'description'|'skuIds'|'redirectUrl'|'serialNum'|'createdAt'|'lastUpdated'> & { description?; skuIds?; redirectUrl?; serialNum: number; createdAt: string; lastUpdated: string }
|
||||
- components/ProductGroupForm.tsx (c8): ProductGroup → Omit<AdminProductGroup,'createdAt'|'products'> & { createdAt: string; products: any[] }
|
||||
- manage-orders/orders/index.tsx (c20): OrderType → Omit<AdminOrderListItem,'customerName'|'customerMobile'|'items'|'createdAt'|'adminNotes'|'userNotes'|'userNegativityScore'> & { customerName: string|null; customerMobile?: string|null; items: OrderItemRow[]; createdAt: string; adminNotes?; userNotes?; userNegativityScore?; couponCode?; couponDescription?; discountAmount? } where OrderItemRow = Omit<AdminOrderListItemProduct,'id'|'skuName'|'features'|'isPackaged'|'isPackageVerified'> & { id?: number; isPackaged?: boolean; isPackageVerified?: boolean }
|
||||
- components/SnippetOrdersView.tsx (c11+24): OrderProduct → Pick<AdminVendorSnippetOrderProduct,'productId'|'productName'|'quantity'|'price'|'unit'|'subtotal'>; SnippetOrder → Omit<AdminVendorSnippetOrderSummary,'totalAmount'|'slotInfo'|'products'> & { totalAmount: string; slotInfo: { time: string; sequence: any[] } | null; products: OrderProduct[] }
|
||||
- components/StoreForm.tsx (c14): StoreFormData → Omit<CreateStoreInput,'description'> & { description: string; products: number[] }
|
||||
- components/ProductsSelector.tsx (c43): SkuSummary → Omit<SharedSkuSummary,'images'>
|
||||
- send-notifications/index.tsx (c55): User → Omit<UserMiniInfo,'name'> & { name: string|null; isEligibleForNotif: boolean }
|
||||
- NEW types/drag-order-item.ts (c16): export interface DragOrderItemProps<T> { item: T; drag: () => void; isActive: boolean }
|
||||
- customize-app/all-items-order.tsx (c16): ProductItemProps deleted → React.FC<DragOrderItemProps<Product>>
|
||||
- customize-app/popular-items.tsx (c16+c4): ProductItemProps → interface extends DragOrderItemProps<PopularProduct> { onDelete: (id:number)=>void }; PopularProduct → Omit<StoreProductCard,'price'|'marketPrice'|'unitNotation'|'images'> & { price: number; marketPrice: number|null; images: string[]; storeId: number|null; nextDeliveryDate: string|null }
|
||||
- product-tags/order.tsx (c16): TagItemProps deleted → React.FC<DragOrderItemProps<Tag>>
|
||||
|
||||
apps/user-ui:
|
||||
- src/types/auth.ts (c17+53+26): User → Pick<SharedUser,'id'|'email'> & Partial<Omit<SharedUser,'id'|'email'|'createdAt'>> & { mobile: string|null; profileImage?: string|null; createdAt: string }; UserDetails → Pick<UserAuthProfile,'id'|'email'|'mobile'> & Partial<Omit<UserAuthProfile,'id'|'email'|'mobile'|'createdAt'>>; AuthState → interface extends AuthStateCore { user: User|null; userDetails: UserDetails|null }
|
||||
- src/store/addressStore.ts (c19): interface AddressState deleted → create<AddressSelectionState>
|
||||
- src/store/navigationStore.ts (c37): interface NavigationState deleted → create<NavigationState> (imported from shared)
|
||||
- components/stores/flashNavigationStore.ts (c34): → type FlashNavigationState = FlashNavigationState (shared) & { reset: () => void }
|
||||
- src/store/quickDeliveryStore.ts (c42): → type QuickDeliveryState = Omit<QuickDeliveryState(shared),'setDrawerHidden'> (user-ui store has no setDrawerHidden action)
|
||||
- src/store/centralProductStore.ts (c28): interface CentralProductState → type CentralProductState = CentralProductState<Product> (shared generic; local Product = MergedProduct)
|
||||
- hooks/cart-query-hooks.tsx (c36): interface LocalCartItem → type LocalCartItem = Omit<CartItem,'subtotal'>
|
||||
- src/types/orders.ts (c56+57): OrderItem → Pick<UserOrderItemSummary,'productName'|'quantity'|'price'|'amount'|'image'>; Order → Omit<UserOrderSummary,'deliveryDate'|'items'|'discountAmount'> & { deliveryDate?: string; items: OrderItem[]; discountAmount?: number }
|
||||
|
||||
apps/web-ui:
|
||||
- src/lib/auth-context.tsx (c17+26): User → Pick<SharedUser,'id'|'email'> & Partial<Omit<SharedUser,'id'|'email'|'createdAt'>> & { mobile: string|null; profileImage: string|null }; AuthState → interface extends AuthStateCore { user: User|null; userDetails: any }
|
||||
- src/lib/stores/address-store.ts (c19): → type AddressState = AddressSelectionState & { clearSelectedAddress: () => void }
|
||||
- src/lib/stores/navigation-store.ts (c37): interface NavigationStore → type NavigationStore = NavigationState (shared)
|
||||
- src/lib/stores/flash-navigation-store.ts (c34): → type FlashNavigationStore = FlashNavigationState (shared)
|
||||
- src/lib/stores/quick-delivery-store.ts (c42): → type QuickDeliveryStore = QuickDeliveryState (shared)
|
||||
- src/lib/stores/central-product-store.ts (c28): interface CentralProductStore → type CentralProductStore = CentralProductState<Product> (shared generic, local Product)
|
||||
- src/routes/me.coupons.tsx (c54): Coupon → Omit<UserCouponDisplay,'maxValue'|'minOrder'|'validTill'|'maxLimitForUser'> & { maxValue?: number; minOrder?: number; validTill?: string | Date; maxLimitForUser?: number }
|
||||
|
||||
[2026-09-05 14:05:00] COMPLETED all 29 🟢 "unify now" clusters from similar_types.md (clusters 4,6,8,10,11,14,16,17,19,20,22,23,24,26,28,29,34,36,37,42,43,47,48,51,53,54,55,56,57). 33 files touched (5 shared, 12 admin-ui incl. 1 new types/drag-order-item.ts, 8 user-ui, 7 web-ui, 2 db_helper_sqlite). Every replacement is shape-exact vs the local it replaced. One derivation fix during verification: user/web auth `User` originally intersected `Partial<Omit<SharedUser,...>>` with `{mobile: string|null}` — object intersection collapsed mobile to `string`; fixed by excluding 'mobile' from the Omit and overriding it directly.
|
||||
Verification (tsc --noEmit error counts vs pre-change state): backend 11=11, web-ui 150=150, user-ui 106=106 (login.tsx lost 2 pre-existing errors, AuthContext regained symmetric — net 0), admin-ui 122=122, fallback-ui 107=107, db_helper_sqlite 4=4. Error-code distributions identical per project (line-shift only). db_helper_postgres: untouched. similar_types.md: 29 clusters marked ✅ done.
|
||||
Remaining (documented, not in scope): 🟡 worth-doing (1,5,27,31,32,33,38,45,52,61), 🟣 shared-internal (2,7,21,46), ⚪ optional (25,30,35,39,44,50,59,60), 🔵 schema-bound/intentional (3,9,12,13,15,18,40,41,58).
|
||||
|
||||
2026-09-05 14:20:00 — Missed 🟢 cluster 49 (TagFormData ×2 in admin-ui) from the unify-now batch; implementing now so similar_types.md can drop ALL done clusters.
|
||||
- apps/admin-ui/app/(drawer)/dashboard/product-tags/edit/index.tsx: local `interface TagFormData {…5 fields, existingImageUrl?}` → `type TagFormData = BaseTagFormData & { existingImageUrl?: string }` importing the canonical TagForm.tsx declaration (name alias import to avoid self-shadowing).
|
||||
|
||||
[2026-09-05 14:35:00] Pruned similar_types.md: all 30 implemented 🟢 clusters (29 batch + follow-up 49) removed. File now lists the 31 remaining candidates / 84 types (🟡 10 · 🟣 4 · ⚪ 8 · 🔵 9), original cluster IDs kept for change-log traceability; header, top-recommendations, summary table and counts rewritten. Verified: admin-ui tsc 122 = baseline after cluster-49 fix.
|
||||
|
||||
================================================================================
|
||||
2026-09-05 15:00:00 — similar_types.md §B cluster 2 (Coupon family): reduce to TWO types — canonical `Coupon` (entity, skuIds) + `CouponFormInput = Omit<Coupon,'id'|'createdAt'>` (single form for create+update; complete data everywhere). Decisions: canonical field skuIds (API zod + admin form + sqlite schema already use it); db_helper_postgres stays UNTOUCHED (its local Create/UpdateCouponInput documented exception); update path sends complete data, helper strips createdBy from the DB write (authorship preserved).
|
||||
|
||||
packages/shared/types/coupon.types.ts:
|
||||
- drifted Coupon {productIds: number[]|null} → canonical {skuIds: number[]|null} (all other 14 fields identical to admin.ts's shape)
|
||||
+ export type CouponFormInput = Omit<Coupon, 'id' | 'createdAt'>
|
||||
- CouponValidationResult.coupon?: Partial<Coupon> now references the canonical type (verified: no read-side consumers of `.coupon`)
|
||||
|
||||
packages/shared/types/admin.ts:
|
||||
- local interface Coupon (lines ~16-31) deleted
|
||||
- re-export line: export type { Coupon, CouponFormInput, CouponValidationResult, UserMiniInfo } from './coupon.types' (admin.ts has no other internal Coupon references — verified grep)
|
||||
|
||||
packages/db_helper_sqlite/src/admin-apis/coupon.ts:
|
||||
- local CreateCouponInput + UpdateCouponInput deleted
|
||||
- import adds CouponFormInput (from '@packages/shared')
|
||||
- createCouponWithRelations(input: CouponFormInput, ...): insert mapping unchanged (field names match; previously-optional fields now arrive complete; NULL ≡ omitted for the nullable columns)
|
||||
- updateCouponWithRelations(id, input: CouponFormInput, ...): const { createdBy: _createdBy, ...updateFields } = input; .set(updateFields) — createdBy stripped from write only
|
||||
|
||||
apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts (only caller of both helpers; type-only + payload-completeness changes):
|
||||
- create: call object completed — discountPercent/flatDiscount/minOrder/maxValue/maxLimitForUser get `?? null`, validTill `: null` when absent, isInvalidated: false added (DB result identical: all nullable columns; NULL ≡ omitted)
|
||||
- update: updateData becomes a COMPLETE CouponFormInput by merging partial `updates` over the existing coupon via getCouponByIdFromDb(id) (already imported) — sent fields win, unsent fields keep current values (exclusiveApply/isInvalidated/createdBy from existing); net DB write identical to today's partial update
|
||||
|
||||
db_helper_postgres: untouched (user decision). admin-ui: zero changes (Formik→zod→backend flow untouched).
|
||||
|
||||
[2026-09-05 15:20:00] COMPLETED §B cluster 2 (Coupon family → 2 types). 4 files changed: coupon.types.ts (canonical Coupon w/ skuIds + CouponFormInput + CouponValidationResult.coupon re-anchored), admin.ts (local Coupon deleted; combined coupon re-export), sqlite admin-apis/coupon.ts (CouponFormInput signatures; update strips createdBy from write), backend admin coupon router (create sends complete payload incl. isInvalidated: false + nulls; update merges partial updates over existing coupon → complete CouponFormInput; getCouponByIdFromDb fetch added). pg untouched (45 = baseline); admin-ui untouched. Verification (tsc error counts vs pre-change): backend 11=11, web-ui 150=150, user-ui 106=106, admin-ui 122=122, fallback-ui 107=107, sqlite 4=4. similar_types.md: cluster 2 removed → 30 clusters / 78 types remain (🟡 10 · 🟣 3 · ⚪ 8 · 🔵 9).
|
||||
|
||||
================================================================================
|
||||
2026-09-05 15:40:00 — similar_types.md §B cluster 7 (Admin order item trio, all in shared admin.ts): extract common base, chain with extends; all three names + shapes preserved exactly (contract intact).
|
||||
packages/shared/types/admin.ts:
|
||||
+ NEW `export interface AdminOrderItemCore { id: number; name: string; amount: number; isPackaged: boolean; isPackageVerified: boolean; skuName?: string | null; features?: { featureName: string | null; featureValue: string }[] }` (declared before AdminOrderDetailsItem)
|
||||
- AdminOrderDetailsItem: common 7 fields deleted → extends AdminOrderItemCore { quantity: string; price: string; productSize: number; unit?: string }
|
||||
- AdminSlotOrderItem: common 7 fields deleted → extends AdminOrderItemCore { quantity: number; price: number; unit: string }
|
||||
- AdminOrderListItemProduct: common fields deleted → extends AdminSlotOrderItem { productSize: number } (Slot/List share numeric quantity+price; List adds productSize)
|
||||
Consumers unchanged (backend getOrderDetails / slot-orders / orders-list + admin-ui derivations — shapes byte-equivalent). db_helper_postgres untouched.
|
||||
|
||||
[2026-09-05 15:50:00] COMPLETED §B cluster 7 (Admin order item trio). 1 file changed: packages/shared/types/admin.ts — NEW AdminOrderItemCore {id, name, amount, isPackaged, isPackageVerified, skuName?, features?}; AdminOrderDetailsItem extends it {quantity: string; price: string; productSize: number; unit?}; AdminSlotOrderItem extends it {quantity: number; price: number; unit: string}; AdminOrderListItemProduct extends AdminSlotOrderItem {productSize: number}. All names + shapes preserved (contract intact); backend identical tsc output. Verification (tsc error counts vs pre-change): backend 11=11, web-ui 150=150, user-ui 106=106, admin-ui 122=122, fallback-ui 107=107, sqlite 4=4, pg 45=45. similar_types.md: cluster 7 removed → 29 clusters / 75 types remain (🟡 10 · 🟣 2 · ⚪ 8 · 🔵 9).
|
||||
|
||||
================================================================================
|
||||
2026-09-05 16:00:00 — FULL UNIFICATION PASS (remaining 29 clusters of similar_types.md). User rules: (1) add unneeded fields rather than fragment types, (2) make differing types identical + cast (numbers on inputs/cache; DB rows keep DB strings; images → string[]|null; relatedStores → number[]|null), (3) base types via extends. pg stays untouched → clusters 13/15/40/41 (pure pg↔sqlite pairs) = decided-skips. GROUP 1: packages/shared.
|
||||
|
||||
packages/shared/types/admin.ts:
|
||||
- (c21) AdminOrderRow {16 fields} → type AdminOrderRow = Omit<PlacedOrder,'orderGroupId'|'orderGroupProportion'> & { adminNotes: string|null; orderGroupId: string|null; orderGroupProportion: string|null }; + import type { PlacedOrder } from './order.types' (shape byte-equal)
|
||||
- (c9) NEW SkuFlagCore { name?: string|null; isOutOfStock?; isSuspended?; isFlashAvailable?; isOffer?; isComboOnly?; isDeleted? } (all optional); AdminSku → extends SkuFlagCore re-tightening name: string|null + all 6 flags required (shape unchanged)
|
||||
- (c9/31) DEAD types replaced by the live sqlite contracts (verified zero consumers of the old shapes repo-wide):
|
||||
- DELETE CreateSkuVariantInput (dead)
|
||||
- NEW CreateComboItemInput { skuId: number } (was sqlite-local)
|
||||
- CreateSkuInput (dead: skuCode/displayName/unitId/variants…) → live shape extends SkuFlagCore { price: number; marketPrice?: number|null; images?: string[]|null; flashPrice?: number|null; features: SkuFeatureLike[]; comboItems?: CreateComboItemInput[] }
|
||||
- CreateProductInput (dead: storeId: number, no productType) → live shape { name: string; shortDescription?: string|null; longDescription?: string|null; storeId: number|null; incrementStep?: number; productType?: 'item'|'combo'; skus: CreateSkuInput[] }
|
||||
- (c1) NEW ProductTagCore { tagName: string; tagDescription: string|null; imageUrl: string|null; isDashboardTag: boolean; relatedStores: number[]|null }; AdminProductTagInfo → extends ProductTagCore { id; sortOrder: number[]; createdAt: Date } (relatedStores unknown → number[]|null)
|
||||
- (c5) AdminVendorSnippetCreateInput + AdminVendorSnippetUpdateInput (both dead, verified) → single export type AdminVendorSnippetInput = Omit<AdminVendorSnippet,'id'|'createdAt'>; dbService.ts re-export list updated (2 lines → 1)
|
||||
|
||||
packages/shared/types/staff-user.types.ts (c46):
|
||||
- local StaffUser (staffRoleId: number, non-null — unreachable duplicate; barrel only exports StaffRole from this file) deleted → single source = admin.ts StaffUser (staffRoleId: number|null)
|
||||
|
||||
packages/shared/types/user.ts (c12/18/38/59/61/3):
|
||||
- (c12) NEW ProductPriceFlags { price: number; marketPrice: number|null; flashPrice: string|null; isFlashAvailable: boolean; isOutOfStock: boolean; isSuspended: boolean } — MergedProduct (both apps) and sqlite AvailabilityCacheData anchor here
|
||||
- (c18) NEW ProductDetailCore { id; productId; name; shortDescription: string|null; longDescription: string|null; price: string; marketPrice: string|null; unitNotation: string; images: string[]|null; isOutOfStock: boolean; incrementStep: number; productQuantity: number; isFlashAvailable: boolean; flashPrice: string|null; productType: string; isOffer?: boolean; isComboOnly?: boolean }; UserProductDetailData → extends ProductDetailCore { store: UserProductStoreInfo|null; deliverySlots: UserProductDeliverySlot[]; specialDeals: UserProductSpecialDeal[]; comboItems: UserProductComboItem[] } (isOffer/isComboOnly gained optional — rule 1)
|
||||
- (c38) NEW ProductSummaryCore { id; name; price: string; marketPrice: string|null; unitNotation: string; images: string[]|null; isOutOfStock: boolean; incrementStep: number }
|
||||
- (c59) UserSlotsWithProductsResponse → extends UserSlotsResponse { productAvailability: UserSlotAvailability[] }
|
||||
- (c61) NEW UserStoreSampleProductCore<I> { id: number; images: I } + UserStoreSummaryCore<P> { id; name; description: string|null; productCount: number; sampleProducts: P[] }; UserStoreSampleProduct = Core<{signedImageUrl: string|null}>; UserStoreSampleProductData = Core<{images: string[]|null}>; UserStoreSummary extends SummaryCore<UserStoreSampleProduct> { signedImageUrl: string|null }; UserStoreSummaryData extends SummaryCore<UserStoreSampleProductData> { imageUrl: string|null }
|
||||
- (c3) UserSlotWithProducts → extends UserProductDeliverySlot { products: UserSlotProduct[] }
|
||||
|
||||
packages/shared/types/app-common.types.ts (c25/30/39):
|
||||
+ OrderDialogBaseProps { open: boolean; onClose: () => void; orderId: number }; ComplaintFormProps → extends it
|
||||
+ AuthRedirectOptions { targetUrl: string; queryParams: Record<string, any> }
|
||||
+ OrderCardActionProps<T> { order: T; onPress: () => void }
|
||||
|
||||
GROUP 2 (2026-09-05 16:10) — sqlite helpers + slots adopt shared bases (clusters 1,3,9,12,18,31,38,45,58):
|
||||
|
||||
packages/db_helper_sqlite/src/stores/store-helpers.ts:
|
||||
- (c18) ProductBasicData {20 fields} → extends ProductDetailCore (shared) { skuName: string|null; storeId: number|null } + re-tightens isOffer/isComboOnly to required; images unknown → string[]|null via core; producer (getAllProductsForCache:129) images: sku.images → cast `as string[] | null`
|
||||
- (c12) AvailabilityCacheData {7 fields} → extends ProductPriceFlags { id: number }; producer getAvailabilityForCache now writes NUMBERS: price: Number(stat.ourPrice ?? 0), marketPrice: stat.marketPrice != null ? Number(stat.marketPrice) : null, flashPrice unchanged string|null; downstream apps already Number()-cast (verified user-ui:143-144, web-ui:181-182) — no app changes
|
||||
- (c58) ProductComboCacheData {8 fields} → extends UserProductComboItem { comboSkuId: number }; producer (getAllProductCombosForCache:242) images: ci.sku?.images → cast `as string[] | null`
|
||||
- (c1) TagBasicData {7 fields} → extends ProductTagCore { id: number; sortOrder: number[] | null }; producer (:272) relatedStores: productTagInfo.relatedStores → cast `as number[] | null`
|
||||
- (c3) DeliverySlotData {5 fields} → extends UserProductDeliverySlot { isCapacityFull: boolean; skuId: number }; SlotWithProductsData → extends UserProductDeliverySlot { isActive: boolean; isCapacityFull: boolean; products: Array<{…same inline…}> }; import adds UserProductDeliverySlot
|
||||
|
||||
packages/db_helper_sqlite/src/admin-apis/product.ts (c9/31/38):
|
||||
- local CreateComboItemInput / CreateSkuInput / CreateProductInput deleted → imported from '@packages/shared' (createProduct's ?? null / ?? 'item' defaults unchanged)
|
||||
- (c38) OffersPageProductData {8 fields} → extends ProductSummaryCore; producer mapOffersPageProduct images cast `as string[] | null`
|
||||
|
||||
packages/db_helper_sqlite/src/admin-apis/slots.ts (c45):
|
||||
- local SlotSnippetInput {name, skuIds, validTill} deleted → import shared SlotSnippetInput {name; groupIds: number[]; skuIds: number[]; validTill: string} (groupIds unused in sqlite — rule 1)
|
||||
|
||||
packages/db_helper_postgres/src/admin-apis/product.ts: UNTOUCHED (pg-side tag input/sortOrder drift remains the documented exception).
|
||||
|
||||
GROUP 3 (2026-09-05 16:25) — component prop families (clusters 27, 33, 35):
|
||||
packages/ui/src/components/dropdown.tsx (c27): NEW exported DropdownBaseProps { label: string; options: DropdownOption[]; error?: boolean; style?: any; placeholder?: string; disabled?: boolean; className?: string } (the truly common scaffold — value/onValueChange intentionally excluded because the two dropdowns declare incompatible value widths: string|number vs string|number|string[]|number[]); Props → extends DropdownBaseProps { value; onValueChange }
|
||||
packages/ui/src/components/bottom-dropdown.tsx (c27+33): DropdownOption → interface extends shared DropdownOption (import alias BaseDropdownOption) + disabled?: boolean (public export kept); BottomDropdownProps → extends DropdownBaseProps + topLabel/value(wide)/onValueChange(wide)/multiple/triggerComponent/onSearch/testID
|
||||
packages/web-components/src/components/image-gallery-with-delete.tsx (c35): ImageGalleryWithDeleteProps → extends ImageCarouselProps { onRemove: (uri: string) => void }
|
||||
|
||||
GROUP 4 (2026-09-05 16:35) — apps adopt the shared bases (clusters 12, 32, 38, 39, 44, 45, 50, 52, 60) + admin-ui ProductForm unification (cluster 9):
|
||||
|
||||
apps/user-ui/src/hooks/prominent-api-hooks.ts (c12): MergedProduct → BaseProduct & ProductPriceFlags & { isComboOnly?: boolean } (shared ProductPriceFlags imported; fields identical)
|
||||
apps/web-ui/src/hooks/prominent-api-hooks.ts (c12): MergedProduct → BaseProduct & ProductPriceFlags
|
||||
apps/user-ui/app/(drawer)/(tabs)/me/addresses/index.tsx (c52): local Address {12 fields} → type Address = Pick<UserAddress, 'id'|'name'|'phone'|'addressLine1'|'addressLine2'|'city'|'state'|'pincode'|'isDefault'|'latitude'|'longitude'|'googleMapsUrl'>
|
||||
apps/user-ui/hooks/cart-query-hooks.tsx (c38): ProductSummary → extends ProductSummaryCore { isFlashAvailable: boolean; flashPrice: string|null; productQuantity: number } (images string[] → string[]|null via core)
|
||||
apps/user-ui/components/NextOrderGlimpse.tsx (c39): OrderCardProps → extends OrderCardActionProps<Order> { supportMobile: string|null }
|
||||
apps/admin-ui/app/(drawer)/dashboard/user-management/[id].tsx (c39): OrderItemProps → extends OrderCardActionProps<Order>
|
||||
apps/user-ui/components/SlotSpecificView.tsx (c44): SlotLayoutProps → extends SlotProductsProps { isForFlashDelivery: boolean }
|
||||
apps/user-ui/app/(drawer)/(tabs)/home/index.tsx (c50): ListHeaderProps → extends TagSceneProps { onSelectTag: (id:number)=>void; storesData: any; sortedSlots: any[] }
|
||||
apps/web-ui/src/components/Dialog.tsx (c32): local DialogProps deleted → import shared DialogProps (now carries title?)
|
||||
apps/admin-ui/app/(drawer)/dashboard/product-tags/order.tsx (c1): TagItemData? no — admin-ui TagItemData lives in product-tags/index.tsx → extends ProductTagCore { id; createdAt: string|Date }
|
||||
apps/admin-ui/app/(drawer)/dashboard/customize-app/all-items-order.tsx (c60): Product → Pick<StoreProductCard,'id'|'name'|'images'|'isOutOfStock'> (images string[] → string[]|null)
|
||||
apps/admin-ui/components/SlotForm.tsx (c45): local VendorSnippet → type VendorSnippet = SlotSnippetInput (shared; validTill string → string|null)
|
||||
apps/admin-ui/src/components/ProductForm.tsx + products/add.tsx + products/edit.tsx (c9): Variant → Omit<CreateSkuInput,'features'|'comboItems'|'flashPrice'|'marketPrice'|'name'> & { name: string; marketPrice: number; flashPrice: number|null; features: Attribute[]; comboItems: CreateComboItemInput[] }; attributes→features rename across the form; price/marketPrice/flashPrice become numbers (TextInput display casts via String(... ?? ''), combo picker pushes {skuId: 0} + Number(val) on select); add/edit screens drop their parseFloat boundary casts (values already numeric)
|
||||
|
||||
[2026-09-05 17:30:00] COMPLETED the FINAL unification pass — all 25 remaining actionable clusters (1,3,5,9,12,18,21,25,27,30,31,32,33,35,38,39,44,45,46,50,52,58,59,60,61). ~30 files across shared/admin.ts+user.ts+app-common, sqlite helpers/product/slots, backend (slot-store, product-store, product-tag-store, admin coupon/slots/product trpc), packages/ui (dropdowns), web-components, user-ui, web-ui, admin-ui (incl. ProductForm numeric/features unification). Notable standardizations per user rules: availability cache now writes NUMERIC prices (apps already Number()-cast — zero app changes); SKU/form prices numeric end-to-end (ProductForm casts at display, add/edit drop parseFloat); form field attributes→features renamed; images → string[]|null everywhere; relatedStores → number[]|null (trpc getProductTags annotation updated); vendor-snippet input single-sourced with wire (string) validTill; SlotSnippetInput shared with groupIds (unused in sqlite); dead shared CreateProductInput/CreateSkuInput/CreateSkuVariantInput/AdminVendorSnippet{Create,Update}Input/staff-user StaffUser deleted and replaced by live-shape single sources. Decided-skips (13,15,40,41) documented in similar_types.md (4 clusters / 8+1 detail remain — 13, 15, 40, 41).
|
||||
FINAL VERIFICATION (tsc error counts vs baselines): backend 11=11, admin-ui 122=122, user-ui 106=106, web-ui 150=150, fallback-ui 107=107, db_helper_sqlite 4=4, db_helper_postgres 45=45. All diffs are line-number/message-render shifts of pre-existing errors only.
|
||||
|
||||
================================================================================
|
||||
2026-09-05 17:45:00 — Add root typecheck script (runs every package/app with a tsconfig).
|
||||
|
||||
[NEW FILE] ./typecheck (executable bash): iterates apps/backend, apps/user-ui, apps/admin-ui, apps/web-ui, apps/fallback-ui, packages/db_helper_sqlite, packages/db_helper_postgres, packages/migrator, packages/web-components — runs `npx tsc --noEmit` per target, prints ✅/❌ with error counts, prints first errors on failure, exits non-zero if any target fails. Targets without a tsconfig (packages/shared, packages/ui) are typechecked transitively via consumers.
|
||||
|
||||
packages/web-components/tsconfig.json:
|
||||
- removed "declaration": true, "declarationMap": true, "outDir": "./dist", "rootDir": "./src" — the package is consumed as source (main: ./src/index.ts) and never emitted standalone; with the unified @packages/shared type imports now in its sources, declaration emit forced TS6059 rootDir violations (all 21 standalone errors were exactly this). Checking-only tsconfig → 0 errors.
|
||||
|
||||
package.json (root): + "typecheck": "bash typecheck"
|
||||
[2026-09-05 17:55:00] VERIFIED ./typecheck: all 9 targets report exactly their known baselines — backend 11, user-ui 106, admin-ui 122, web-ui 150, fallback-ui 107, db_helper_sqlite 4, db_helper_postgres 45, migrator 0 ✅, web-components 3 (the pre-existing `<p weight>` custom-prop errors; standalone count went 21 → 3 after removing the declaration-emit settings). Script exits 1 while any target fails; run via `./typecheck` or `npm run typecheck` / `bun run typecheck`.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"build": "turbo run build",
|
||||
"dev": "turbo run dev --parallel",
|
||||
"lint": "turbo run lint",
|
||||
"typecheck": "bash typecheck",
|
||||
"test": "turbo run test",
|
||||
"web-ui": "bun run --filter web-ui",
|
||||
"web-ui:dev": "bun run web-ui dev",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { coupons, reservedCoupons, users, orders, orderStatus, couponApplicableUsers, couponApplicableProducts } from '../db/schema'
|
||||
import { eq, and, like, or, inArray, lt, desc, asc } from 'drizzle-orm'
|
||||
import type { Coupon, CouponValidationResult, UserMiniInfo } from '@packages/shared'
|
||||
import type { Coupon, CouponFormInput, CouponValidationResult, UserMiniInfo } from '@packages/shared'
|
||||
|
||||
export async function getAllCoupons(
|
||||
cursor?: number,
|
||||
|
|
@ -71,23 +71,8 @@ export async function getCouponById(id: number): Promise<any | null> {
|
|||
})
|
||||
}
|
||||
|
||||
export interface CreateCouponInput {
|
||||
couponCode: string
|
||||
isUserBased: boolean
|
||||
discountPercent?: string
|
||||
flatDiscount?: string
|
||||
minOrder?: string
|
||||
skuIds?: number[] | null
|
||||
maxValue?: string
|
||||
isApplyForAll: boolean
|
||||
validTill?: Date
|
||||
maxLimitForUser?: number
|
||||
exclusiveApply: boolean
|
||||
createdBy: number
|
||||
}
|
||||
|
||||
export async function createCouponWithRelations(
|
||||
input: CreateCouponInput,
|
||||
input: CouponFormInput,
|
||||
applicableUsers?: number[],
|
||||
applicableProducts?: number[]
|
||||
): Promise<Coupon> {
|
||||
|
|
@ -129,31 +114,19 @@ export async function createCouponWithRelations(
|
|||
})
|
||||
}
|
||||
|
||||
export interface UpdateCouponInput {
|
||||
couponCode?: string
|
||||
isUserBased?: boolean
|
||||
discountPercent?: string
|
||||
flatDiscount?: string
|
||||
minOrder?: string
|
||||
skuIds?: number[] | null
|
||||
maxValue?: string
|
||||
isApplyForAll?: boolean
|
||||
validTill?: Date | null
|
||||
maxLimitForUser?: number
|
||||
exclusiveApply?: boolean
|
||||
isInvalidated?: boolean
|
||||
}
|
||||
|
||||
export async function updateCouponWithRelations(
|
||||
id: number,
|
||||
input: UpdateCouponInput,
|
||||
input: CouponFormInput,
|
||||
applicableUsers?: number[],
|
||||
applicableProducts?: number[]
|
||||
): Promise<Coupon> {
|
||||
return await db.transaction(async (tx) => {
|
||||
// createdBy is always sent (complete payload) but never rewritten on update
|
||||
const { createdBy: _createdBy, ...updateFields } = input
|
||||
|
||||
const [coupon] = await tx.update(coupons)
|
||||
.set({
|
||||
...input,
|
||||
...updateFields,
|
||||
})
|
||||
.where(eq(coupons.id, id))
|
||||
.returning()
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ import type {
|
|||
AdminSpecialDeal,
|
||||
AdminUnit,
|
||||
AdminUpdateSlotProductsResult,
|
||||
CreateComboItemInput,
|
||||
CreateProductInput,
|
||||
CreateSkuInput,
|
||||
ProductTagCore,
|
||||
SkuFeatureLike,
|
||||
Store,
|
||||
} from '@packages/shared'
|
||||
|
|
@ -66,36 +70,6 @@ type ProductRow = InferSelectModel<typeof productInfo>
|
|||
type SkuRow = InferSelectModel<typeof productSkus>
|
||||
type MarketStatsRow = InferSelectModel<typeof productMarketStats>
|
||||
type SkuFeatureRow = InferSelectModel<typeof skuFeatures>
|
||||
|
||||
interface CreateComboItemInput {
|
||||
skuId: number
|
||||
}
|
||||
|
||||
interface CreateSkuInput {
|
||||
name?: string | null
|
||||
price: number
|
||||
marketPrice?: number | null
|
||||
images?: string[] | null
|
||||
isFlashAvailable?: boolean
|
||||
flashPrice?: number | null
|
||||
isOutOfStock?: boolean
|
||||
isSuspended?: boolean
|
||||
isOffer?: boolean
|
||||
isComboOnly?: boolean
|
||||
isDeleted?: boolean
|
||||
features: SkuFeatureLike[]
|
||||
comboItems?: CreateComboItemInput[]
|
||||
}
|
||||
|
||||
interface CreateProductInput {
|
||||
name: string
|
||||
shortDescription?: string | null
|
||||
longDescription?: string | null
|
||||
storeId?: number | null
|
||||
incrementStep?: number
|
||||
productType?: 'item' | 'combo'
|
||||
skus: CreateSkuInput[]
|
||||
}
|
||||
type StoreRow = InferSelectModel<typeof storeInfo>
|
||||
type SpecialDealRow = InferSelectModel<typeof specialDeals>
|
||||
type ProductTagInfoRow = InferSelectModel<typeof productTagInfo>
|
||||
|
|
@ -677,12 +651,7 @@ export async function getProductTagInfoById(tagId: number): Promise<AdminProduct
|
|||
return mapTagInfo(tag)
|
||||
}
|
||||
|
||||
export interface CreateProductTagInput {
|
||||
tagName: string
|
||||
tagDescription?: string | null
|
||||
imageUrl?: string | null
|
||||
isDashboardTag?: boolean
|
||||
relatedStores?: number[]
|
||||
export interface CreateProductTagInput extends ProductTagCore {
|
||||
sortOrder?: number[]
|
||||
}
|
||||
|
||||
|
|
@ -733,12 +702,8 @@ export async function getProductTagById(tagId: number): Promise<AdminProductTagW
|
|||
}
|
||||
}
|
||||
|
||||
export interface UpdateProductTagInput {
|
||||
export type UpdateProductTagInput = Omit<ProductTagCore, 'tagName'> & {
|
||||
tagName?: string
|
||||
tagDescription?: string | null
|
||||
imageUrl?: string | null
|
||||
isDashboardTag?: boolean
|
||||
relatedStores?: number[]
|
||||
sortOrder?: number[]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,11 +20,8 @@ import type {
|
|||
} from '@packages/shared'
|
||||
import { coerceDate } from '../lib/date'
|
||||
|
||||
type SlotSnippetInput = {
|
||||
name: string
|
||||
skuIds: number[]
|
||||
validTill?: string
|
||||
}
|
||||
// Single source in @packages/shared (admin.ts); groupIds unused in sqlite.
|
||||
import type { SlotSnippetInput } from '@packages/shared'
|
||||
|
||||
const getStringArray = (value: unknown): string[] | null => {
|
||||
if (!Array.isArray(value)) return null
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from '../db/schema'
|
||||
import { eq, and, gt, sql, isNotNull, asc } from 'drizzle-orm'
|
||||
import { composeSkuName, composeUnitNotation } from '../lib/sku-features'
|
||||
import type { ProductTagData, TagProductMapping, UserNegativityData, StoreSummary, BannerData } from '@packages/shared'
|
||||
import type { ProductTagData, TagProductMapping, UserNegativityData, StoreSummary, BannerData, SpecialDealCore, ProductDetailCore, ProductPriceFlags, ProductTagCore, UserProductDeliverySlot, UserProductComboItem } from '@packages/shared'
|
||||
|
||||
// Re-exported so the package index (`export { ... } from './src/stores/store-helpers'`)
|
||||
// keeps resolving to the single shared source.
|
||||
|
|
@ -41,51 +41,24 @@ export async function getAllBannersForCache(): Promise<BannerData[]> {
|
|||
// PRODUCT STORE HELPERS
|
||||
// ============================================================================
|
||||
|
||||
export interface ProductBasicData {
|
||||
id: number
|
||||
productId: number
|
||||
name: string
|
||||
export interface ProductBasicData extends ProductDetailCore {
|
||||
skuName: string | null
|
||||
shortDescription: string | null
|
||||
longDescription: string | null
|
||||
price: string
|
||||
marketPrice: string | null
|
||||
images: unknown
|
||||
isOutOfStock: boolean
|
||||
storeId: number | null
|
||||
unitNotation: string
|
||||
incrementStep: number
|
||||
productQuantity: number
|
||||
isFlashAvailable: boolean
|
||||
flashPrice: string | null
|
||||
productType: string
|
||||
isComboOnly: boolean
|
||||
isOffer: boolean
|
||||
isComboOnly: boolean
|
||||
}
|
||||
|
||||
export interface AvailabilityCacheData {
|
||||
export interface AvailabilityCacheData extends ProductPriceFlags {
|
||||
id: number
|
||||
price: string
|
||||
marketPrice: string | null
|
||||
flashPrice: string | null
|
||||
isFlashAvailable: boolean
|
||||
isOutOfStock: boolean
|
||||
isSuspended: boolean
|
||||
}
|
||||
|
||||
export interface DeliverySlotData {
|
||||
export interface DeliverySlotData extends UserProductDeliverySlot {
|
||||
skuId: number
|
||||
id: number
|
||||
deliveryTime: Date
|
||||
freezeTime: Date
|
||||
isCapacityFull: boolean
|
||||
}
|
||||
|
||||
export interface SpecialDealData {
|
||||
export interface SpecialDealData extends SpecialDealCore {
|
||||
skuId: number
|
||||
quantity: string
|
||||
price: string
|
||||
validTill: Date
|
||||
}
|
||||
|
||||
export async function getAllSpecialDealsForCache(): Promise<SpecialDealData[]> {
|
||||
|
|
@ -129,7 +102,7 @@ export async function getAllProductsForCache(): Promise<ProductBasicData[]> {
|
|||
longDescription: sku.product?.longDescription ?? null,
|
||||
price: marketStats?.ourPrice ? String(marketStats.ourPrice) : '0',
|
||||
marketPrice: marketStats?.marketPrice ? String(marketStats.marketPrice) : null,
|
||||
images: sku.images,
|
||||
images: sku.images as string[] | null,
|
||||
isOutOfStock: marketStats?.isOutOfStock ?? false,
|
||||
storeId: sku.product?.storeId ?? null,
|
||||
unitNotation: composeUnitNotation(features),
|
||||
|
|
@ -153,8 +126,8 @@ export async function getAvailabilityForCache(): Promise<AvailabilityCacheData[]
|
|||
.filter((stat) => !stat.isSuspended && !stat.sku?.isDeleted)
|
||||
.map((stat) => ({
|
||||
id: stat.skuId,
|
||||
price: stat.ourPrice ? String(stat.ourPrice) : '0',
|
||||
marketPrice: stat.marketPrice ? String(stat.marketPrice) : null,
|
||||
price: Number(stat.ourPrice ?? 0),
|
||||
marketPrice: stat.marketPrice != null ? Number(stat.marketPrice) : null,
|
||||
flashPrice: stat.flashPrice ? String(stat.flashPrice) : null,
|
||||
isFlashAvailable: stat.isFlashAvailable,
|
||||
isOutOfStock: stat.isOutOfStock,
|
||||
|
|
@ -208,15 +181,8 @@ export async function getAllProductTagsForCache(): Promise<ProductTagData[]> {
|
|||
// PRODUCT COMBO STORE HELPERS
|
||||
// ============================================================================
|
||||
|
||||
export interface ProductComboCacheData {
|
||||
export interface ProductComboCacheData extends UserProductComboItem {
|
||||
comboSkuId: number
|
||||
skuId: number
|
||||
productName: string
|
||||
skuName: string | null
|
||||
images: unknown
|
||||
unitNotation: string
|
||||
price: string
|
||||
isOffer: boolean
|
||||
}
|
||||
|
||||
export async function getAllProductCombosForCache(): Promise<ProductComboCacheData[]> {
|
||||
|
|
@ -242,7 +208,7 @@ export async function getAllProductCombosForCache(): Promise<ProductComboCacheDa
|
|||
skuId: ci.skuId,
|
||||
productName: composeSkuName(ci.sku?.product?.name ?? 'Unknown', features, ci.sku?.name),
|
||||
skuName: ci.sku?.name ?? null,
|
||||
images: ci.sku?.images,
|
||||
images: (ci.sku?.images ?? null) as string[] | null,
|
||||
unitNotation: composeUnitNotation(features),
|
||||
price: ci.sku?.marketStats?.ourPrice ? String(ci.sku.marketStats.ourPrice) : '0',
|
||||
isOffer: ci.sku?.isOffer ?? false,
|
||||
|
|
@ -254,13 +220,11 @@ export async function getAllProductCombosForCache(): Promise<ProductComboCacheDa
|
|||
// PRODUCT TAG STORE HELPERS
|
||||
// ============================================================================
|
||||
|
||||
export interface TagBasicData {
|
||||
export interface TagBasicData extends ProductTagCore {
|
||||
id: number
|
||||
tagName: string
|
||||
tagDescription: string | null
|
||||
imageUrl: string | null
|
||||
isDashboardTag: boolean
|
||||
relatedStores: unknown
|
||||
sortOrder: number[] | null
|
||||
}
|
||||
|
||||
|
|
@ -291,10 +255,7 @@ export async function getAllTagProductMappings(): Promise<TagProductMapping[]> {
|
|||
// SLOT STORE HELPERS
|
||||
// ============================================================================
|
||||
|
||||
export interface SlotWithProductsData {
|
||||
id: number
|
||||
deliveryTime: Date
|
||||
freezeTime: Date
|
||||
export interface SlotWithProductsData extends UserProductDeliverySlot {
|
||||
isActive: boolean
|
||||
isCapacityFull: boolean
|
||||
products: Array<{
|
||||
|
|
@ -380,7 +341,7 @@ export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProduct
|
|||
name: sku.product.store.name,
|
||||
description: sku.product.store.description
|
||||
} : null,
|
||||
images: sku.images,
|
||||
images: sku.images as string[] | null,
|
||||
isOutOfStock: marketStats?.isOutOfStock ?? false,
|
||||
storeId: sku.product?.storeId ?? null,
|
||||
isFlashAvailable: marketStats?.isFlashAvailable ?? false,
|
||||
|
|
|
|||
|
|
@ -301,15 +301,9 @@ export async function getNextDeliveryDateWithCapacity(skuId: number): Promise<Da
|
|||
return null
|
||||
}
|
||||
|
||||
export interface SkuSummary {
|
||||
id: number
|
||||
productId: number
|
||||
productName: string
|
||||
label: string
|
||||
images: unknown
|
||||
storeId: number | null
|
||||
price: string
|
||||
}
|
||||
// Single source in @packages/shared (admin.ts); re-exported for module compat.
|
||||
import type { ProductSummaryCore, SkuSummary } from '@packages/shared'
|
||||
export type { SkuSummary } from '@packages/shared'
|
||||
|
||||
export async function getAllSkusSummary(): Promise<SkuSummary[]> {
|
||||
const skus = await db.query.productSkus.findMany({
|
||||
|
|
@ -339,15 +333,7 @@ export async function getAllSkusSummary(): Promise<SkuSummary[]> {
|
|||
})
|
||||
}
|
||||
|
||||
export interface OffersPageProductData {
|
||||
id: number
|
||||
name: string
|
||||
price: string
|
||||
marketPrice: string | null
|
||||
unitNotation: string
|
||||
images: unknown
|
||||
isOutOfStock: boolean
|
||||
incrementStep: number
|
||||
export interface OffersPageProductData extends ProductSummaryCore {
|
||||
}
|
||||
|
||||
export interface OffersPageData {
|
||||
|
|
@ -373,7 +359,7 @@ const mapOffersPageProduct = (sku: {
|
|||
price: sku.marketStats?.ourPrice ? String(sku.marketStats.ourPrice) : '0',
|
||||
marketPrice: sku.marketStats?.marketPrice ? String(sku.marketStats.marketPrice) : null,
|
||||
unitNotation: composeUnitNotation(features),
|
||||
images: sku.images,
|
||||
images: (sku.images ?? null) as string[] | null,
|
||||
isOutOfStock: sku.marketStats?.isOutOfStock ?? false,
|
||||
incrementStep: sku.product?.incrementStep ?? 1,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,33 +3,19 @@
|
|||
// so existing `@packages/shared` consumers keep working from one surface.)
|
||||
|
||||
import type { MessageResponse, BasicSuccessResponse, IdName } from './primitives.types'
|
||||
import type { UnitSeedData } from './seed.types'
|
||||
import type { PlacedOrder } from './order.types'
|
||||
export type { Banner } from './banner.types'
|
||||
export type { Complaint, ComplaintWithUser } from './complaint.types'
|
||||
export type { Constant, ConstantUpdateResult, ConstValueType } from './const.types'
|
||||
export type { CouponValidationResult, UserMiniInfo } from './coupon.types'
|
||||
export type { StaffRole } from './staff-user.types'
|
||||
export type { MessageResponse, IdName } from './primitives.types'
|
||||
|
||||
// NOTE: Coupon and StaffUser intentionally keep LOCAL declarations below —
|
||||
// the dedicated files drifted (productIds vs skuIds; staffRoleId nullable vs
|
||||
// not) and sqlite consumers depend on these exact shapes.
|
||||
export interface Coupon {
|
||||
id: number;
|
||||
couponCode: string;
|
||||
isUserBased: boolean;
|
||||
discountPercent: string | null;
|
||||
flatDiscount: string | null;
|
||||
minOrder: string | null;
|
||||
skuIds: number[] | null;
|
||||
maxValue: string | null;
|
||||
isApplyForAll: boolean;
|
||||
validTill: Date | null;
|
||||
maxLimitForUser: number | null;
|
||||
exclusiveApply: boolean;
|
||||
isInvalidated: boolean;
|
||||
createdAt: Date;
|
||||
createdBy: number;
|
||||
}
|
||||
// NOTE: Coupon + CouponFormInput are single-sourced in coupon.types.ts
|
||||
// (canonical skuIds shape); StaffUser intentionally keeps its LOCAL declaration
|
||||
// below — the dedicated staff-user.types.ts copy drifted (staffRoleId nullable
|
||||
// vs not) and sqlite consumers depend on these exact shapes.
|
||||
export type { Coupon, CouponFormInput, CouponValidationResult, UserMiniInfo } from './coupon.types'
|
||||
|
||||
export interface StaffUser {
|
||||
id: number;
|
||||
|
|
@ -49,23 +35,12 @@ export interface Store {
|
|||
// updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface AdminOrderRow {
|
||||
id: number;
|
||||
userId: number;
|
||||
addressId: number;
|
||||
slotId: number | null;
|
||||
isCod: boolean;
|
||||
isOnlinePayment: boolean;
|
||||
paymentInfoId: number | null;
|
||||
totalAmount: string;
|
||||
deliveryCharge: string;
|
||||
readableId: number;
|
||||
adminNotes: string | null;
|
||||
userNotes: string | null;
|
||||
orderGroupId: string | null;
|
||||
orderGroupProportion: string | null;
|
||||
isFlashDelivery: boolean;
|
||||
createdAt: Date;
|
||||
// Order row — anchored to the shared PlacedOrder shape (single source); the
|
||||
// admin variant widens the group fields to nullable and adds adminNotes.
|
||||
export type AdminOrderRow = Omit<PlacedOrder, 'orderGroupId' | 'orderGroupProportion'> & {
|
||||
adminNotes: string | null
|
||||
orderGroupId: string | null
|
||||
orderGroupProportion: string | null
|
||||
}
|
||||
|
||||
export type PaymentStatus = 'pending' | 'success' | 'cod' | 'failed'
|
||||
|
|
@ -115,13 +90,11 @@ export interface AdminOrderDetailsSlotInfo {
|
|||
sequence: unknown;
|
||||
}
|
||||
|
||||
export interface AdminOrderDetailsItem {
|
||||
// Common scaffolding for the three admin order-item views. quantity/price stay
|
||||
// per-view (Details: string, Slot/List: number) since they mirror different queries.
|
||||
export interface AdminOrderItemCore {
|
||||
id: number;
|
||||
name: string;
|
||||
quantity: string;
|
||||
productSize: number;
|
||||
price: string;
|
||||
unit?: string;
|
||||
amount: number;
|
||||
isPackaged: boolean;
|
||||
isPackageVerified: boolean;
|
||||
|
|
@ -129,6 +102,13 @@ export interface AdminOrderDetailsItem {
|
|||
features?: { featureName: string | null; featureValue: string }[];
|
||||
}
|
||||
|
||||
export interface AdminOrderDetailsItem extends AdminOrderItemCore {
|
||||
quantity: string;
|
||||
price: string;
|
||||
productSize: number;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export interface AdminOrderDetailsPayment {
|
||||
status: string;
|
||||
gateway: string;
|
||||
|
|
@ -191,17 +171,10 @@ export type AdminOrderMessageResult = MessageResponse
|
|||
|
||||
export type AdminOrderBasicResult = BasicSuccessResponse
|
||||
|
||||
export interface AdminSlotOrderItem {
|
||||
id: number;
|
||||
name: string;
|
||||
export interface AdminSlotOrderItem extends AdminOrderItemCore {
|
||||
quantity: number;
|
||||
price: number;
|
||||
amount: number;
|
||||
unit: string;
|
||||
isPackaged: boolean;
|
||||
isPackageVerified: boolean;
|
||||
skuName?: string | null;
|
||||
features?: { featureName: string | null; featureValue: string }[];
|
||||
}
|
||||
|
||||
export interface AdminSlotOrder {
|
||||
|
|
@ -231,18 +204,8 @@ export interface AdminGetSlotOrdersResult {
|
|||
data: AdminSlotOrder[];
|
||||
}
|
||||
|
||||
export interface AdminOrderListItemProduct {
|
||||
id: number;
|
||||
name: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
amount: number;
|
||||
unit: string;
|
||||
export interface AdminOrderListItemProduct extends AdminSlotOrderItem {
|
||||
productSize: number;
|
||||
isPackaged: boolean;
|
||||
isPackageVerified: boolean;
|
||||
skuName?: string | null;
|
||||
features?: { featureName: string | null; featureValue: string }[];
|
||||
}
|
||||
|
||||
export interface AdminOrderListItem {
|
||||
|
|
@ -284,10 +247,8 @@ export interface AdminGetAllOrdersResultWithUserId {
|
|||
nextCursor?: number;
|
||||
}
|
||||
|
||||
export interface AdminRebalanceSlotsResult {
|
||||
success: boolean;
|
||||
export interface AdminRebalanceSlotsResult extends MessageResponse {
|
||||
updatedOrders: number[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type AdminCancelOrderError =
|
||||
|
|
@ -304,10 +265,8 @@ export interface AdminCancelOrderResult {
|
|||
error?: AdminCancelOrderError;
|
||||
}
|
||||
|
||||
export interface AdminUnit {
|
||||
export interface AdminUnit extends UnitSeedData {
|
||||
id: number;
|
||||
shortNotation: string;
|
||||
fullName: string;
|
||||
}
|
||||
|
||||
export interface AdminSkuFeature {
|
||||
|
|
@ -333,7 +292,19 @@ export interface AdminProductComboItem {
|
|||
price: string;
|
||||
}
|
||||
|
||||
export interface AdminSku {
|
||||
// Flag scaffold shared by the SKU row (AdminSku), the insert input
|
||||
// (CreateSkuInput) and the admin product-form variant state.
|
||||
export interface SkuFlagCore {
|
||||
name?: string | null
|
||||
isOutOfStock?: boolean
|
||||
isSuspended?: boolean
|
||||
isFlashAvailable?: boolean
|
||||
isOffer?: boolean
|
||||
isComboOnly?: boolean
|
||||
isDeleted?: boolean
|
||||
}
|
||||
|
||||
export interface AdminSku extends SkuFlagCore {
|
||||
id: number
|
||||
productId: number
|
||||
name: string | null
|
||||
|
|
@ -369,50 +340,46 @@ export interface AdminProductWithRelations extends AdminProduct {
|
|||
skus: AdminSku[];
|
||||
}
|
||||
|
||||
export interface CreateSkuVariantInput {
|
||||
name: string
|
||||
value: string
|
||||
unitId?: number | null
|
||||
sortOrder?: number
|
||||
export interface CreateComboItemInput {
|
||||
skuId: number
|
||||
}
|
||||
|
||||
export interface CreateSkuInput {
|
||||
skuCode?: string | null
|
||||
displayName: string
|
||||
unitId: number
|
||||
productQuantity?: number
|
||||
incrementStep?: number
|
||||
price: number | string
|
||||
marketPrice?: number | string | null
|
||||
export interface CreateSkuInput extends SkuFlagCore {
|
||||
price: number
|
||||
marketPrice?: number | null
|
||||
images?: string[] | null
|
||||
isOutOfStock?: boolean
|
||||
isSuspended?: boolean
|
||||
isDeleted?: boolean
|
||||
isFlashAvailable?: boolean
|
||||
flashPrice?: number | string | null
|
||||
isOffer?: boolean
|
||||
isComboOnly?: boolean
|
||||
sortOrder?: number
|
||||
isDefault?: boolean
|
||||
variants?: CreateSkuVariantInput[]
|
||||
flashPrice?: number | null
|
||||
features: SkuFeatureLike[]
|
||||
comboItems?: CreateComboItemInput[]
|
||||
}
|
||||
|
||||
export interface CreateProductInput {
|
||||
name: string
|
||||
shortDescription?: string | null
|
||||
longDescription?: string | null
|
||||
storeId: number
|
||||
storeId: number | null
|
||||
incrementStep?: number
|
||||
productType?: 'item' | 'combo'
|
||||
skus: CreateSkuInput[]
|
||||
}
|
||||
|
||||
export interface AdminProductTagInfo {
|
||||
// Tag core — shared by the cache row (TagBasicData), the create/update inputs,
|
||||
// the admin tag-info row, the backend store tag and the admin-ui list item.
|
||||
// Non-key fields are optional here; row-shaped variants re-tighten them.
|
||||
export interface ProductTagCore {
|
||||
tagName: string
|
||||
tagDescription?: string | null
|
||||
imageUrl?: string | null
|
||||
isDashboardTag?: boolean
|
||||
relatedStores?: number[] | null
|
||||
}
|
||||
|
||||
export interface AdminProductTagInfo extends ProductTagCore {
|
||||
id: number;
|
||||
tagName: string;
|
||||
tagDescription: string | null;
|
||||
imageUrl: string | null;
|
||||
isDashboardTag: boolean;
|
||||
relatedStores: unknown;
|
||||
relatedStores: number[] | null;
|
||||
sortOrder: number[];
|
||||
createdAt: Date;
|
||||
}
|
||||
|
|
@ -429,14 +396,19 @@ export interface AdminProductTagWithProducts extends AdminProductTagInfo {
|
|||
productIds: number[];
|
||||
}
|
||||
|
||||
export interface AdminSpecialDeal {
|
||||
id: number;
|
||||
skuId: number;
|
||||
// Special-deal chain — UserProductSpecialDeal (user.ts) and sqlite SpecialDealData
|
||||
// extend this core; AdminSpecialDeal adds the row fields.
|
||||
export interface SpecialDealCore {
|
||||
quantity: string;
|
||||
price: string;
|
||||
validTill: Date;
|
||||
}
|
||||
|
||||
export interface AdminSpecialDeal extends SpecialDealCore {
|
||||
id: number;
|
||||
skuId: number;
|
||||
}
|
||||
|
||||
export interface AdminProductWithDetails extends AdminProduct {
|
||||
store: Store | null;
|
||||
skus: AdminSku[];
|
||||
|
|
@ -465,15 +437,32 @@ export interface AdminUpdateSlotProductsResult {
|
|||
|
||||
export type AdminSlotsProductIdsResult = Record<number, number[]>;
|
||||
|
||||
export interface AdminProductReview {
|
||||
// SKU summary row — sqlite user-apis product.ts + admin-ui ProductsSelector
|
||||
// single source (admin-ui drops images; the helper keeps it unknown).
|
||||
export interface SkuSummary {
|
||||
id: number;
|
||||
productId: number;
|
||||
productName: string;
|
||||
label: string;
|
||||
images: unknown;
|
||||
storeId: number | null;
|
||||
price: string;
|
||||
}
|
||||
|
||||
// Review core — shared between the admin row (adds response fields) and the
|
||||
// user-facing review (user.ts adds typed imageUrls).
|
||||
export interface ProductReviewCore {
|
||||
id: number;
|
||||
reviewBody: string;
|
||||
ratings: number;
|
||||
imageUrls: unknown;
|
||||
reviewTime: Date;
|
||||
userName: string | null;
|
||||
}
|
||||
|
||||
export interface AdminProductReview extends ProductReviewCore {
|
||||
imageUrls: unknown;
|
||||
adminResponse: string | null;
|
||||
adminResponseImages: unknown;
|
||||
userName: string | null;
|
||||
}
|
||||
|
||||
export interface AdminProductReviewWithSignedUrls extends AdminProductReview {
|
||||
|
|
@ -491,11 +480,7 @@ export interface AdminProductReviewResponse {
|
|||
review: AdminProductReview;
|
||||
}
|
||||
|
||||
export interface AdminProductGroup {
|
||||
id: number;
|
||||
groupName: string;
|
||||
description: string | null;
|
||||
createdAt: Date;
|
||||
export interface AdminProductGroup extends AdminProductGroupInfo {
|
||||
products: AdminSku[];
|
||||
productCount: number;
|
||||
}
|
||||
|
|
@ -626,20 +611,20 @@ export interface AdminUpdateSlotCapacityResult {
|
|||
message: string;
|
||||
}
|
||||
|
||||
export interface AdminVendorSnippetCreateInput {
|
||||
snippetCode: string;
|
||||
slotId?: number;
|
||||
skuIds: number[];
|
||||
validTill?: string;
|
||||
isPermanent: boolean;
|
||||
// Single complete form input for vendor-snippet create + update — callers
|
||||
// always send every field (no partial shapes). Derived from the row; validTill
|
||||
// is the wire type (ISO string) — the row keeps Date.
|
||||
export type AdminVendorSnippetInput = Omit<AdminVendorSnippet, 'id' | 'createdAt' | 'validTill'> & {
|
||||
validTill: string | null
|
||||
}
|
||||
|
||||
export interface AdminVendorSnippetUpdateInput {
|
||||
snippetCode?: string;
|
||||
slotId?: number;
|
||||
skuIds?: number[];
|
||||
validTill?: string | null;
|
||||
isPermanent?: boolean;
|
||||
// Slot-level snippet input (group-model) — sqlite carries skuIds and leaves
|
||||
// groupIds unused; the admin-ui SlotForm uses both.
|
||||
export interface SlotSnippetInput {
|
||||
name: string
|
||||
groupIds: number[]
|
||||
skuIds: number[]
|
||||
validTill: string | null
|
||||
}
|
||||
|
||||
export interface AdminVendorSnippetDeleteResult {
|
||||
|
|
|
|||
|
|
@ -5,16 +5,34 @@
|
|||
*/
|
||||
|
||||
// --- Forms / shared UI ---
|
||||
|
||||
// Dialog anchored to a specific order (complaint form, cancel-order dialog)
|
||||
export interface OrderDialogBaseProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
orderId: number
|
||||
}
|
||||
|
||||
// Auth redirect plumbing (route guard options ↔ redirect state); the state
|
||||
// variant re-tightens both fields to required.
|
||||
export interface AuthRedirectOptions {
|
||||
targetUrl?: string
|
||||
queryParams?: Record<string, any>
|
||||
}
|
||||
|
||||
// Card that shows one order with a press action (user-ui glimpse, admin-ui history)
|
||||
export interface OrderCardActionProps<T> {
|
||||
order: T
|
||||
onPress: () => void
|
||||
}
|
||||
|
||||
export interface LoginFormInputs {
|
||||
mobile: string
|
||||
otp?: string
|
||||
password?: string
|
||||
}
|
||||
|
||||
export interface ComplaintFormProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
orderId: number
|
||||
export interface ComplaintFormProps extends OrderDialogBaseProps {
|
||||
}
|
||||
|
||||
// Address form props — identical in user-ui + web-ui AddressForm components
|
||||
|
|
@ -89,3 +107,44 @@ export interface StoreHeaderState {
|
|||
title: string
|
||||
setTitle: (title: string) => void
|
||||
}
|
||||
|
||||
// --- Auth shell (identical user-ui ↔ web-ui; apps add their own user/userDetails) ---
|
||||
export interface AuthStateCore {
|
||||
isAuthenticated: boolean
|
||||
isLoading: boolean
|
||||
token: string | null
|
||||
}
|
||||
|
||||
// --- Zustand store shapes duplicated across user-ui ↔ web-ui ---
|
||||
export interface AddressSelectionState {
|
||||
selectedAddressId: number | null
|
||||
setSelectedAddressId: (addressId: number | null) => void
|
||||
}
|
||||
|
||||
export interface NavigationState {
|
||||
isNavigatedFromHome: boolean
|
||||
selectedStoreId: number | null
|
||||
setNavigatedFromHome: (value: boolean) => void
|
||||
setSelectedStoreId: (id: number | null) => void
|
||||
}
|
||||
|
||||
export interface FlashNavigationState {
|
||||
shouldNavigateToCart: boolean
|
||||
setShouldNavigateToCart: (value: boolean) => void
|
||||
}
|
||||
|
||||
export interface QuickDeliveryState {
|
||||
isDrawerHidden: boolean
|
||||
selectedSlotId: number | null
|
||||
setDrawerHidden: (hidden: boolean) => void
|
||||
setSelectedSlotId: (id: number | null) => void
|
||||
}
|
||||
|
||||
// Central product cache — element type differs per app (MergedProduct vs local Product)
|
||||
export interface CentralProductState<Product = unknown> {
|
||||
products: Product[]
|
||||
productsById: Record<number, Product>
|
||||
refetchProducts: (() => void | Promise<void>) | null
|
||||
setProducts: (products: Product[]) => void
|
||||
setRefetchProducts: (refetch: (() => void | Promise<void>) | null) => void
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
* Central type definitions for coupon-related data structures
|
||||
*/
|
||||
|
||||
// Canonical coupon row — `skuIds` is the canonical reserved-items field
|
||||
// (matches the API schema, admin form and sqlite schema `sku_ids`;
|
||||
// postgres maps it to its product_ids column at the boundary).
|
||||
export interface Coupon {
|
||||
id: number;
|
||||
couponCode: string;
|
||||
|
|
@ -10,7 +13,7 @@ export interface Coupon {
|
|||
discountPercent: string | null;
|
||||
flatDiscount: string | null;
|
||||
minOrder: string | null;
|
||||
productIds: number[] | null;
|
||||
skuIds: number[] | null;
|
||||
maxValue: string | null;
|
||||
isApplyForAll: boolean;
|
||||
validTill: Date | null;
|
||||
|
|
@ -21,6 +24,10 @@ export interface Coupon {
|
|||
createdBy: number;
|
||||
}
|
||||
|
||||
// Complete form payload for BOTH create and update — callers always send
|
||||
// every field (create sends isInvalidated: false, update re-sends createdBy).
|
||||
export type CouponFormInput = Omit<Coupon, 'id' | 'createdAt'>
|
||||
|
||||
export interface CouponValidationResult {
|
||||
valid: boolean;
|
||||
message?: string;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export type * from './store.types';
|
|||
// Shared cross-cutting types — import from '@packages/shared' anywhere:
|
||||
export type * from './primitives.types'; // MessageResponse, BasicSuccessResponse, IdName, ReactParentComponent, TabIconProps
|
||||
export type * from './upload.types'; // ContextString, UploadInput, UploadBatchInput, UploadResult
|
||||
export type * from './app-common.types'; // LoginFormInputs, ComplaintItemProps/ListItemProps, FlashDeliveryProps, CelebrationProps
|
||||
export type * from './app-common.types'; // LoginFormInputs, ListItemProps, FlashDeliveryProps, CelebrationProps, AuthStateCore + zustand store shapes (Navigation/Address/QuickDelivery/FlashNavigation/CentralProductState)
|
||||
export type * from './ui-common.types'; // DropdownOption, DialogProps, ConfirmationDialogProps, LoadingDialogProps, QuantifierProps, ImageUploader*, TextButtonProps
|
||||
export type * from './order.types'; // PlacedOrder, CouponUsageWithCoupon, GetAllOrdersInput, OrderStatus, DeliveryStatus, CartData
|
||||
export type * from './seed.types'; // UnitSeedData, StaffRoleName, StaffPermissionName, RolePermissionAssignment, KeyValSeedData
|
||||
|
|
|
|||
|
|
@ -1,16 +1,10 @@
|
|||
/**
|
||||
* Staff User Types
|
||||
* Central type definitions for staff user-related data structures
|
||||
* NOTE: StaffUser is single-sourced in admin.ts (nullable staffRoleId row
|
||||
* shape); the strict non-null copy that lived here was an unreachable dup.
|
||||
*/
|
||||
|
||||
export interface StaffUser {
|
||||
id: number;
|
||||
name: string;
|
||||
password: string;
|
||||
staffRoleId: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface StaffRole {
|
||||
id: number;
|
||||
roleName: string;
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@
|
|||
* Note: Store interface is defined in admin.ts to avoid duplication
|
||||
*/
|
||||
|
||||
import type { IdName } from './primitives.types'
|
||||
import type { UserTagSummary } from './user'
|
||||
|
||||
/**
|
||||
* Store summary for dropdowns/forms
|
||||
* Minimal data for store selection UI
|
||||
*/
|
||||
export interface StoreSummary {
|
||||
id: number;
|
||||
name: string;
|
||||
export interface StoreSummary extends IdName {
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
|
|
@ -24,12 +25,9 @@ export interface StoresSummaryResponse {
|
|||
/**
|
||||
* Lightweight tag row used on store-detail pages (user-ui + web-ui).
|
||||
* Named StoreTag to avoid clashing with the richer backend Tag shape.
|
||||
* Derived from UserTagSummary so id/tagName stay in lockstep.
|
||||
*/
|
||||
export interface StoreTag {
|
||||
id: number;
|
||||
tagName: string;
|
||||
productIds?: number[];
|
||||
}
|
||||
export type StoreTag = Pick<UserTagSummary, 'id' | 'tagName'> & { productIds?: number[] }
|
||||
|
||||
/**
|
||||
* Chip props for tag selection on store-detail pages.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// User-related types
|
||||
|
||||
import type { MessageResponse, BasicSuccessResponse } from './primitives.types'
|
||||
import type { AdminDeliverySlot } from './admin'
|
||||
import type { AdminDeliverySlot, ProductReviewCore, SpecialDealCore } from './admin'
|
||||
import type { Complaint } from './complaint.types'
|
||||
|
||||
import type { Banner } from './banner.types'
|
||||
|
||||
|
|
@ -125,14 +126,10 @@ export interface UserBannersResponse {
|
|||
banners: UserBanner[];
|
||||
}
|
||||
|
||||
export interface UserComplaint {
|
||||
id: number;
|
||||
complaintBody: string;
|
||||
response: string | null;
|
||||
isResolved: boolean;
|
||||
createdAt: Date;
|
||||
orderId: number | null;
|
||||
}
|
||||
export type UserComplaint = Pick<
|
||||
Complaint,
|
||||
'id' | 'complaintBody' | 'response' | 'isResolved' | 'createdAt' | 'orderId'
|
||||
>
|
||||
|
||||
export interface UserComplaintsResponse {
|
||||
complaints: UserComplaint[];
|
||||
|
|
@ -148,55 +145,42 @@ export interface UserTagSummary {
|
|||
productIds: number[];
|
||||
}
|
||||
|
||||
export interface UserStoreSampleProduct {
|
||||
// Store-sample-product cores — signed (authed API) vs plain (cache) variants
|
||||
// share id/name; only the image carrier differs.
|
||||
export interface UserStoreSampleProductCore {
|
||||
id: number;
|
||||
name: string;
|
||||
signedImageUrl: string | null;
|
||||
}
|
||||
|
||||
export interface UserStoreSampleProductData {
|
||||
id: number;
|
||||
name: string;
|
||||
images: string[] | null;
|
||||
}
|
||||
export type UserStoreSampleProduct = UserStoreSampleProductCore & { signedImageUrl: string | null }
|
||||
|
||||
export interface UserStoreSummary {
|
||||
export type UserStoreSampleProductData = UserStoreSampleProductCore & { images: string[] | null }
|
||||
|
||||
// Store-summary cores — same split (signedImageUrl vs imageUrl) over shared
|
||||
// id/name/description/count/sampleProducts.
|
||||
export interface UserStoreSummaryCore<P> {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
signedImageUrl: string | null;
|
||||
productCount: number;
|
||||
sampleProducts: UserStoreSampleProduct[];
|
||||
sampleProducts: P[];
|
||||
}
|
||||
|
||||
export interface UserStoreSummaryData {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
export interface UserStoreSummary extends UserStoreSummaryCore<UserStoreSampleProduct> {
|
||||
signedImageUrl: string | null;
|
||||
}
|
||||
|
||||
export interface UserStoreSummaryData extends UserStoreSummaryCore<UserStoreSampleProductData> {
|
||||
imageUrl: string | null;
|
||||
productCount: number;
|
||||
sampleProducts: UserStoreSampleProductData[];
|
||||
}
|
||||
|
||||
export interface UserStoresResponse {
|
||||
stores: UserStoreSummary[];
|
||||
}
|
||||
|
||||
export interface UserStoreProduct {
|
||||
id: number;
|
||||
name: string;
|
||||
shortDescription: string | null;
|
||||
price: string;
|
||||
marketPrice: string | null;
|
||||
incrementStep: number;
|
||||
unit: string;
|
||||
unitNotation: string;
|
||||
images: string[];
|
||||
isOutOfStock: boolean;
|
||||
productQuantity: number;
|
||||
}
|
||||
|
||||
export interface UserStoreProductData {
|
||||
// Product card row — single source for store product cards everywhere
|
||||
// (user store pages, admin customize-app, helper caches).
|
||||
export interface StoreProductCard {
|
||||
id: number;
|
||||
name: string;
|
||||
shortDescription: string | null;
|
||||
|
|
@ -210,6 +194,10 @@ export interface UserStoreProductData {
|
|||
productQuantity: number;
|
||||
}
|
||||
|
||||
export type UserStoreProduct = Omit<StoreProductCard, 'images'> & { images: string[] }
|
||||
|
||||
export type UserStoreProductData = StoreProductCard
|
||||
|
||||
export interface UserStoreDetail {
|
||||
store: {
|
||||
id: number;
|
||||
|
|
@ -241,11 +229,7 @@ export interface UserProductDeliverySlot {
|
|||
freezeTime: Date;
|
||||
}
|
||||
|
||||
export interface UserProductSpecialDeal {
|
||||
quantity: string;
|
||||
price: string;
|
||||
validTill: Date;
|
||||
}
|
||||
export type UserProductSpecialDeal = SpecialDealCore
|
||||
|
||||
export interface UserProductComboItem {
|
||||
skuId: number;
|
||||
|
|
@ -257,7 +241,10 @@ export interface UserProductComboItem {
|
|||
isOffer: boolean;
|
||||
}
|
||||
|
||||
export interface UserProductDetailData {
|
||||
// Product-detail core — shared by the user API detail payload, the backend
|
||||
// cache row (product-store Product) and the sqlite cache row (ProductBasicData).
|
||||
// isOffer/isComboOnly are optional here: variants that carry them re-tighten.
|
||||
export interface ProductDetailCore {
|
||||
id: number;
|
||||
productId: number;
|
||||
name: string;
|
||||
|
|
@ -268,14 +255,19 @@ export interface UserProductDetailData {
|
|||
unitNotation: string;
|
||||
images: string[] | null;
|
||||
isOutOfStock: boolean;
|
||||
store: UserProductStoreInfo | null;
|
||||
incrementStep: number;
|
||||
productQuantity: number;
|
||||
isFlashAvailable: boolean;
|
||||
flashPrice: string | null;
|
||||
productType: string;
|
||||
isOffer?: boolean;
|
||||
isComboOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface UserProductDetailData extends ProductDetailCore {
|
||||
store: UserProductStoreInfo | null;
|
||||
deliverySlots: UserProductDeliverySlot[];
|
||||
specialDeals: UserProductSpecialDeal[];
|
||||
productType: string;
|
||||
comboItems: UserProductComboItem[];
|
||||
}
|
||||
|
||||
|
|
@ -283,13 +275,32 @@ export interface UserProductDetail extends UserProductDetailData {
|
|||
images: string[];
|
||||
}
|
||||
|
||||
export interface UserProductReview {
|
||||
// Price/availability flag block — anchors the sqlite availability cache row and
|
||||
// the apps' MergedProduct intersection (both previously hand-rolled).
|
||||
export interface ProductPriceFlags {
|
||||
price: number;
|
||||
marketPrice: number | null;
|
||||
flashPrice: string | null;
|
||||
isFlashAvailable: boolean;
|
||||
isOutOfStock: boolean;
|
||||
isSuspended: boolean;
|
||||
}
|
||||
|
||||
// Offers-page product card core — sqlite offers/offers-cache row and the
|
||||
// user-ui cart ProductSummary anchor here.
|
||||
export interface ProductSummaryCore {
|
||||
id: number;
|
||||
reviewBody: string;
|
||||
ratings: number;
|
||||
name: string;
|
||||
price: string;
|
||||
marketPrice: string | null;
|
||||
unitNotation: string;
|
||||
images: string[] | null;
|
||||
isOutOfStock: boolean;
|
||||
incrementStep: number;
|
||||
}
|
||||
|
||||
export interface UserProductReview extends ProductReviewCore {
|
||||
imageUrls: string[] | null;
|
||||
reviewTime: Date;
|
||||
userName: string | null;
|
||||
}
|
||||
|
||||
export interface UserProductReviewWithSignedUrls extends UserProductReview {
|
||||
|
|
@ -311,10 +322,7 @@ export interface UserSlotProduct {
|
|||
images: string[] | null;
|
||||
}
|
||||
|
||||
export interface UserSlotWithProducts {
|
||||
id: number;
|
||||
deliveryTime: Date;
|
||||
freezeTime: Date;
|
||||
export interface UserSlotWithProducts extends UserProductDeliverySlot {
|
||||
products: UserSlotProduct[];
|
||||
}
|
||||
|
||||
|
|
@ -335,10 +343,8 @@ export interface UserSlotsListResponse {
|
|||
count: number;
|
||||
}
|
||||
|
||||
export interface UserSlotsWithProductsResponse {
|
||||
slots: UserSlotWithProducts[];
|
||||
export interface UserSlotsWithProductsResponse extends UserSlotsResponse {
|
||||
productAvailability: UserSlotAvailability[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface UserPaymentOrderResponse {
|
||||
|
|
|
|||
|
|
@ -6,25 +6,19 @@ import MyText from './text';
|
|||
import { useTheme } from '../../hooks/theme-context';
|
||||
import tw from '../lib/tailwind';
|
||||
import { colors } from '../lib/theme-colors';
|
||||
import type { DropdownOption as BaseDropdownOption } from '@packages/shared';
|
||||
import type { DropdownBaseProps } from './dropdown';
|
||||
|
||||
export interface DropdownOption {
|
||||
label: string;
|
||||
value: string | number;
|
||||
// Bottom/multi dropdown option — shared base + per-option disabled flag
|
||||
export interface DropdownOption extends BaseDropdownOption {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface BottomDropdownProps {
|
||||
label: string;
|
||||
interface BottomDropdownProps extends DropdownBaseProps<DropdownOption> {
|
||||
topLabel?: string;
|
||||
value: string | number | string[] | number[];
|
||||
options: DropdownOption[];
|
||||
onValueChange: (value: string | number | string[] | number[]) => void;
|
||||
multiple?: boolean;
|
||||
error?: boolean;
|
||||
style?: any;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
triggerComponent?: React.ComponentType<{
|
||||
onPress: () => void;
|
||||
disabled?: boolean;
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ import type { DropdownOption } from '@packages/shared';
|
|||
|
||||
export type { DropdownOption } from '@packages/shared';
|
||||
|
||||
interface Props {
|
||||
// Common dropdown scaffold — value/onValueChange stay per-variant because the
|
||||
// single dropdown carries a scalar and the bottom/multi one carries arrays.
|
||||
// The option element type is generic: the bottom/multi dropdown extends the
|
||||
// shared DropdownOption with a per-option disabled flag.
|
||||
export interface DropdownBaseProps<O = DropdownOption> {
|
||||
label: string;
|
||||
value: string | number;
|
||||
options: DropdownOption[];
|
||||
onValueChange: (value: string | number) => void;
|
||||
options: O[];
|
||||
error?: boolean;
|
||||
style?: any;
|
||||
placeholder?: string;
|
||||
|
|
@ -18,6 +20,11 @@ interface Props {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
interface Props extends DropdownBaseProps {
|
||||
value: string | number;
|
||||
onValueChange: (value: string | number) => void;
|
||||
}
|
||||
|
||||
const CustomDropdown: React.FC<Props> = ({
|
||||
label,
|
||||
value,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import React from 'react'
|
||||
import { cn } from '../lib/utils'
|
||||
import { X } from 'lucide-react'
|
||||
import type { ImageCarouselProps } from './image-carousel'
|
||||
|
||||
interface ImageGalleryWithDeleteProps {
|
||||
images: { uri?: string }[]
|
||||
interface ImageGalleryWithDeleteProps extends ImageCarouselProps {
|
||||
onRemove: (uri: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function ImageGalleryWithDelete({
|
||||
|
|
|
|||
|
|
@ -9,10 +9,7 @@
|
|||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@packages/shared": ["../shared"],
|
||||
|
|
|
|||
43
typecheck
Executable file
43
typecheck
Executable file
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env bash
|
||||
# Typechecks every app/package that has a tsconfig.
|
||||
# packages/shared and packages/ui have no tsconfig of their own — they are
|
||||
# typechecked transitively via the consumers below.
|
||||
set -u
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
targets=(
|
||||
apps/backend
|
||||
apps/user-ui
|
||||
apps/admin-ui
|
||||
apps/web-ui
|
||||
apps/fallback-ui
|
||||
packages/db_helper_sqlite
|
||||
packages/db_helper_postgres
|
||||
packages/migrator
|
||||
packages/web-components
|
||||
)
|
||||
|
||||
log="$(mktemp)"
|
||||
failed=0
|
||||
for target in "${targets[@]}"; do
|
||||
if [ ! -f "$target/tsconfig.json" ]; then
|
||||
printf '⏭ %-32s (no tsconfig)\n' "$target"
|
||||
continue
|
||||
fi
|
||||
printf '▶ %-32s ' "$target"
|
||||
if (cd "$target" && npx tsc --noEmit >"$log" 2>&1); then
|
||||
echo '✅ clean'
|
||||
else
|
||||
errors=$(grep -c 'error TS' "$log" || true)
|
||||
echo "❌ ${errors} error(s)"
|
||||
head -20 "$log"
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
rm -f "$log"
|
||||
|
||||
if [ "$failed" -ne 0 ]; then
|
||||
echo '❌ typecheck failed'
|
||||
exit 1
|
||||
fi
|
||||
echo '✅ all packages typecheck clean'
|
||||
Loading…
Add table
Reference in a new issue