diff --git a/apps/admin-ui/app/(drawer)/dashboard/customize-app/all-items-order.tsx b/apps/admin-ui/app/(drawer)/dashboard/customize-app/all-items-order.tsx index aad9ae1..9930cc0 100644 --- a/apps/admin-ui/app/(drawer)/dashboard/customize-app/all-items-order.tsx +++ b/apps/admin-ui/app/(drawer)/dashboard/customize-app/all-items-order.tsx @@ -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 -interface ProductItemProps { - item: Product; - drag: () => void; - isActive: boolean; -} +import type { DragOrderItemProps } from '@/types/drag-order-item'; -const ProductItem: React.FC = ({ +const ProductItem: React.FC> = ({ item, drag, isActive, diff --git a/apps/admin-ui/app/(drawer)/dashboard/customize-app/popular-items.tsx b/apps/admin-ui/app/(drawer)/dashboard/customize-app/popular-items.tsx index 15137d1..f0d8830 100644 --- a/apps/admin-ui/app/(drawer)/dashboard/customize-app/popular-items.tsx +++ b/apps/admin-ui/app/(drawer)/dashboard/customize-app/popular-items.tsx @@ -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 { onDelete: (id: number) => void; } diff --git a/apps/admin-ui/app/(drawer)/dashboard/dashboard-banners/edit/[id].tsx b/apps/admin-ui/app/(drawer)/dashboard/dashboard-banners/edit/[id].tsx index 4e86bbc..cdbc9e0 100644 --- a/apps/admin-ui/app/(drawer)/dashboard/dashboard-banners/edit/[id].tsx +++ b/apps/admin-ui/app/(drawer)/dashboard/dashboard-banners/edit/[id].tsx @@ -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(); diff --git a/apps/admin-ui/app/(drawer)/dashboard/dashboard-banners/index.tsx b/apps/admin-ui/app/(drawer)/dashboard/dashboard-banners/index.tsx index dbc31d9..6d68a57 100644 --- a/apps/admin-ui/app/(drawer)/dashboard/dashboard-banners/index.tsx +++ b/apps/admin-ui/app/(drawer)/dashboard/dashboard-banners/index.tsx @@ -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 & { createdAt: string; lastUpdated: string; -} +}; export default function DashboardBanners() { const router = useRouter(); diff --git a/apps/admin-ui/app/(drawer)/dashboard/manage-orders/orders/index.tsx b/apps/admin-ui/app/(drawer)/dashboard/manage-orders/orders/index.tsx index 1374b0f..b733918 100644 --- a/apps/admin-ui/app/(drawer)/dashboard/manage-orders/orders/index.tsx +++ b/apps/admin-ui/app/(drawer)/dashboard/manage-orders/orders/index.tsx @@ -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; diff --git a/apps/admin-ui/app/(drawer)/dashboard/product-tags/edit/index.tsx b/apps/admin-ui/app/(drawer)/dashboard/product-tags/edit/index.tsx index 28d7d63..8750046 100644 --- a/apps/admin-ui/app/(drawer)/dashboard/product-tags/edit/index.tsx +++ b/apps/admin-ui/app/(drawer)/dashboard/product-tags/edit/index.tsx @@ -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(); diff --git a/apps/admin-ui/app/(drawer)/dashboard/product-tags/index.tsx b/apps/admin-ui/app/(drawer)/dashboard/product-tags/index.tsx index a1eae54..1991455 100644 --- a/apps/admin-ui/app/(drawer)/dashboard/product-tags/index.tsx +++ b/apps/admin-ui/app/(drawer)/dashboard/product-tags/index.tsx @@ -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; } diff --git a/apps/admin-ui/app/(drawer)/dashboard/product-tags/order.tsx b/apps/admin-ui/app/(drawer)/dashboard/product-tags/order.tsx index 06105a0..8b82d5c 100644 --- a/apps/admin-ui/app/(drawer)/dashboard/product-tags/order.tsx +++ b/apps/admin-ui/app/(drawer)/dashboard/product-tags/order.tsx @@ -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 = ({ item, drag, isActive }) => { +const TagItem: React.FC> = ({ item, drag, isActive }) => { return ( { 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() 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: [], }, ], } diff --git a/apps/admin-ui/app/(drawer)/dashboard/products/edit.tsx b/apps/admin-ui/app/(drawer)/dashboard/products/edit.tsx index 27011b7..3a19620 100644 --- a/apps/admin-ui/app/(drawer)/dashboard/products/edit.tsx +++ b/apps/admin-ui/app/(drawer)/dashboard/products/edit.tsx @@ -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() 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, })), } }) diff --git a/apps/admin-ui/app/(drawer)/dashboard/send-notifications/index.tsx b/apps/admin-ui/app/(drawer)/dashboard/send-notifications/index.tsx index f892d1b..6dd3118 100644 --- a/apps/admin-ui/app/(drawer)/dashboard/send-notifications/index.tsx +++ b/apps/admin-ui/app/(drawer)/dashboard/send-notifications/index.tsx @@ -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 & { name: string | null; - mobile: string | null; isEligibleForNotif: boolean; } diff --git a/apps/admin-ui/app/(drawer)/dashboard/user-management/[id].tsx b/apps/admin-ui/app/(drawer)/dashboard/user-management/[id].tsx index 3f9ed1e..31925ea 100644 --- a/apps/admin-ui/app/(drawer)/dashboard/user-management/[id].tsx +++ b/apps/admin-ui/app/(drawer)/dashboard/user-management/[id].tsx @@ -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 { } const getStatusColor = (status: string) => { diff --git a/apps/admin-ui/components/CancelOrderDialog.tsx b/apps/admin-ui/components/CancelOrderDialog.tsx index c9805fe..854efc3 100644 --- a/apps/admin-ui/components/CancelOrderDialog.tsx +++ b/apps/admin-ui/components/CancelOrderDialog.tsx @@ -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; } diff --git a/apps/admin-ui/components/ProductGroupForm.tsx b/apps/admin-ui/components/ProductGroupForm.tsx index d8fbd12..4290085 100644 --- a/apps/admin-ui/components/ProductGroupForm.tsx +++ b/apps/admin-ui/components/ProductGroupForm.tsx @@ -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 & { createdAt: string; products: any[]; - productCount: number; -} +}; interface ProductGroupFormProps { group?: ProductGroup | null; diff --git a/apps/admin-ui/components/ProductsSelector.tsx b/apps/admin-ui/components/ProductsSelector.tsx index 254f02f..d743cc9 100644 --- a/apps/admin-ui/components/ProductsSelector.tsx +++ b/apps/admin-ui/components/ProductsSelector.tsx @@ -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 interface Group { id: number; diff --git a/apps/admin-ui/components/SlotForm.tsx b/apps/admin-ui/components/SlotForm.tsx index 296b4ba..56fec03 100644 --- a/apps/admin-ui/components/SlotForm.tsx +++ b/apps/admin-ui/components/SlotForm.tsx @@ -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, })), }; diff --git a/apps/admin-ui/components/SnippetOrdersView.tsx b/apps/admin-ui/components/SnippetOrdersView.tsx index f48b6ef..feddfae 100644 --- a/apps/admin-ui/components/SnippetOrdersView.tsx +++ b/apps/admin-ui/components/SnippetOrdersView.tsx @@ -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 & { + totalAmount: string slotInfo: { time: string; sequence: any[]; } | null; products: OrderProduct[]; - matchedProducts: number[]; - snippetCode: string; } interface SnippetOrdersViewProps { diff --git a/apps/admin-ui/components/StoreForm.tsx b/apps/admin-ui/components/StoreForm.tsx index da48a60..95c4308 100644 --- a/apps/admin-ui/components/StoreForm.tsx +++ b/apps/admin-ui/components/StoreForm.tsx @@ -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 & { description: string; - imageUrl?: string; - owner: number; products: number[]; -} +}; interface StoreFormProps { mode: 'create' | 'edit'; diff --git a/apps/admin-ui/src/components/ProductForm.tsx b/apps/admin-ui/src/components/ProductForm.tsx index 99b315d..cdeb9f3 100644 --- a/apps/admin-ui/src/components/ProductForm.tsx +++ b/apps/admin-ui/src/components/ProductForm.tsx @@ -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() 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(({ 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(({ 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(({ ...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(({ style={{ marginBottom: 12 }} /> - + {({ push: pushAttr, remove: removeAttr }) => ( @@ -360,12 +365,12 @@ const ProductForm = forwardRef(({ Add - {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 ( @@ -374,7 +379,7 @@ const ProductForm = forwardRef(({ 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(({ {canDelete && ( @@ -420,7 +425,7 @@ const ProductForm = forwardRef(({ topLabel="Market Price" placeholder="MRP" keyboardType="numeric" - value={variant.marketPrice} + value={variant.marketPrice?.toString() ?? ''} onChangeText={handleChange(`variants.${vIndex}.marketPrice`)} /> @@ -429,7 +434,7 @@ const ProductForm = forwardRef(({ topLabel="Our Price" placeholder="Selling price" keyboardType="numeric" - value={variant.price} + value={variant.price?.toString() ?? ''} onChangeText={handleChange(`variants.${vIndex}.price`)} /> @@ -440,7 +445,7 @@ const ProductForm = forwardRef(({ 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(({ 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(({ { 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(({ ({ + 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(({ 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" /> diff --git a/apps/admin-ui/types/drag-order-item.ts b/apps/admin-ui/types/drag-order-item.ts new file mode 100644 index 0000000..447013f --- /dev/null +++ b/apps/admin-ui/types/drag-order-item.ts @@ -0,0 +1,6 @@ +// Common props for draggable reorder rows (customize-app / product-tags order screens) +export interface DragOrderItemProps { + item: T + drag: () => void + isActive: boolean +} diff --git a/apps/admin-ui/types/vendor-snippets.ts b/apps/admin-ui/types/vendor-snippets.ts index 6285c02..cf433c1 100644 --- a/apps/admin-ui/types/vendor-snippets.ts +++ b/apps/admin-ui/types/vendor-snippets.ts @@ -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 & { id: number; - snippetCode: string; slotId: number; - isPermanent: boolean; - skuIds: number[]; - validTill: string | null; createdAt: string; } \ No newline at end of file diff --git a/apps/backend/src/dbService.ts b/apps/backend/src/dbService.ts index 3a870cc..d2c5c1e 100644 --- a/apps/backend/src/dbService.ts +++ b/apps/backend/src/dbService.ts @@ -82,8 +82,7 @@ export type { AdminVendorSnippetWithSlot, AdminVendorSnippetProduct, AdminVendorSnippetWithProducts, - AdminVendorSnippetCreateInput, - AdminVendorSnippetUpdateInput, + AdminVendorSnippetInput, AdminVendorSnippetDeleteResult, AdminVendorSnippetOrderProduct, AdminVendorSnippetOrderSummary, diff --git a/apps/backend/src/stores/product-store.ts b/apps/backend/src/stores/product-store.ts index f3b41e6..a044bee 100644 --- a/apps/backend/src/stores/product-store.ts +++ b/apps/backend/src/stores/product-store.ts @@ -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 diff --git a/apps/backend/src/stores/product-tag-store.ts b/apps/backend/src/stores/product-tag-store.ts index 1241af6..814b71b 100644 --- a/apps/backend/src/stores/product-tag-store.ts +++ b/apps/backend/src/stores/product-tag-store.ts @@ -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 diff --git a/apps/backend/src/stores/slot-store.ts b/apps/backend/src/stores/slot-store.ts index 80c1590..ceb6215 100644 --- a/apps/backend/src/stores/slot-store.ts +++ b/apps/backend/src/stores/slot-store.ts @@ -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 } diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts b/apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts index 703cd2a..f475a86 100644 --- a/apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts +++ b/apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts @@ -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( diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/product.ts b/apps/backend/src/trpc/apis/admin-apis/apis/product.ts index f4475ff..bc66c67 100644 --- a/apps/backend/src/trpc/apis/admin-apis/apis/product.ts +++ b/apps/backend/src/trpc/apis/admin-apis/apis/product.ts @@ -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( diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/slots.ts b/apps/backend/src/trpc/apis/admin-apis/apis/slots.ts index 7c7932d..43b65e2 100644 --- a/apps/backend/src/trpc/apis/admin-apis/apis/slots.ts +++ b/apps/backend/src/trpc/apis/admin-apis/apis/slots.ts @@ -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, }) diff --git a/apps/backend/src/trpc/apis/user-apis/apis/product.ts b/apps/backend/src/trpc/apis/user-apis/apis/product.ts index e7fd996..a4e439c 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/product.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/product.ts @@ -51,6 +51,7 @@ export const productRouter = router({ return { ...cachedProduct, + images: cachedProduct.images as string[], deliverySlots: filteredSlots }; } diff --git a/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx b/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx index fd51a53..986629e 100755 --- a/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx +++ b/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx @@ -365,13 +365,7 @@ const ProductItem = memo(({ item, onPress }: ProductItemProps) => { ); }); -interface ListHeaderProps { - dashboardTags: any[]; - activeTagId: number | null; - productsByTagId: Record; - expandedByTagId: Record; - onToggleExpand: (tagId: number) => void; - onProductPress: (id: number) => void; +interface ListHeaderProps extends TagSceneProps { onSelectTag: (id: number) => void; storesData: any; sortedSlots: any[]; diff --git a/apps/user-ui/app/(drawer)/(tabs)/me/addresses/index.tsx b/apps/user-ui/app/(drawer)/(tabs)/me/addresses/index.tsx index cb53e50..055436b 100644 --- a/apps/user-ui/app/(drawer)/(tabs)/me/addresses/index.tsx +++ b/apps/user-ui/app/(drawer)/(tabs)/me/addresses/index.tsx @@ -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 }: { diff --git a/apps/user-ui/components/NextOrderGlimpse.tsx b/apps/user-ui/components/NextOrderGlimpse.tsx index c122dff..c459f4a 100644 --- a/apps/user-ui/components/NextOrderGlimpse.tsx +++ b/apps/user-ui/components/NextOrderGlimpse.tsx @@ -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 { supportMobile?: string | null; } diff --git a/apps/user-ui/components/SlotSpecificView.tsx b/apps/user-ui/components/SlotSpecificView.tsx index 7880df4..b61c0eb 100644 --- a/apps/user-ui/components/SlotSpecificView.tsx +++ b/apps/user-ui/components/SlotSpecificView.tsx @@ -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; } diff --git a/apps/user-ui/components/stores/flashNavigationStore.ts b/apps/user-ui/components/stores/flashNavigationStore.ts index 17e2e95..40caf22 100644 --- a/apps/user-ui/components/stores/flashNavigationStore.ts +++ b/apps/user-ui/components/stores/flashNavigationStore.ts @@ -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((set) => ({ shouldNavigateToCart: false, diff --git a/apps/user-ui/hooks/cart-query-hooks.tsx b/apps/user-ui/hooks/cart-query-hooks.tsx index a35af89..43fef88 100644 --- a/apps/user-ui/hooks/cart-query-hooks.tsx +++ b/apps/user-ui/hooks/cart-query-hooks.tsx @@ -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 -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 { diff --git a/apps/user-ui/hooks/useAuthenticatedRoute.ts b/apps/user-ui/hooks/useAuthenticatedRoute.ts index e90af7d..41194b2 100644 --- a/apps/user-ui/hooks/useAuthenticatedRoute.ts +++ b/apps/user-ui/hooks/useAuthenticatedRoute.ts @@ -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; +interface AuthenticatedRouteOptions extends AuthRedirectOptions { } export function useAuthenticatedRoute(options: AuthenticatedRouteOptions = {}) { diff --git a/apps/user-ui/src/contexts/AuthContext.tsx b/apps/user-ui/src/contexts/AuthContext.tsx index def78c5..dd1f094 100644 --- a/apps/user-ui/src/contexts/AuthContext.tsx +++ b/apps/user-ui/src/contexts/AuthContext.tsx @@ -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; timestamp: number; diff --git a/apps/user-ui/src/hooks/prominent-api-hooks.ts b/apps/user-ui/src/hooks/prominent-api-hooks.ts index 3f078fb..c197465 100644 --- a/apps/user-ui/src/hooks/prominent-api-hooks.ts +++ b/apps/user-ui/src/hooks/prominent-api-hooks.ts @@ -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 } diff --git a/apps/user-ui/src/store/addressStore.ts b/apps/user-ui/src/store/addressStore.ts index fba96ae..5bb8bdd 100644 --- a/apps/user-ui/src/store/addressStore.ts +++ b/apps/user-ui/src/store/addressStore.ts @@ -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((set) => ({ +export const useAddressStore = create((set) => ({ selectedAddressId: null, setSelectedAddressId: (addressId) => set({ selectedAddressId: addressId }), })); \ No newline at end of file diff --git a/apps/user-ui/src/store/centralProductStore.ts b/apps/user-ui/src/store/centralProductStore.ts index 3d4499a..ad38a4c 100644 --- a/apps/user-ui/src/store/centralProductStore.ts +++ b/apps/user-ui/src/store/centralProductStore.ts @@ -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 - refetchProducts: (() => Promise) | null - setProducts: (products: Product[]) => void - setRefetchProducts: (refetch: () => Promise) => void -} +type CentralProductState = CentralProductsState export const useCentralProductStore = create((set) => ({ products: [], diff --git a/apps/user-ui/src/store/navigationStore.ts b/apps/user-ui/src/store/navigationStore.ts index 02e19fc..5d507c4 100644 --- a/apps/user-ui/src/store/navigationStore.ts +++ b/apps/user-ui/src/store/navigationStore.ts @@ -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((set) => ({ isNavigatedFromHome: false, diff --git a/apps/user-ui/src/store/quickDeliveryStore.ts b/apps/user-ui/src/store/quickDeliveryStore.ts index 2f8d9ce..5784c8e 100644 --- a/apps/user-ui/src/store/quickDeliveryStore.ts +++ b/apps/user-ui/src/store/quickDeliveryStore.ts @@ -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 export const useQuickDeliveryStore = create((set) => ({ isDrawerHidden: false, diff --git a/apps/user-ui/src/types/auth.ts b/apps/user-ui/src/types/auth.ts index 5f02fef..8dc69e2 100644 --- a/apps/user-ui/src/types/auth.ts +++ b/apps/user-ui/src/types/auth.ts @@ -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 & + Partial> & { + mobile: string | null + profileImage?: string | null + createdAt: string + } -export interface AuthState { +// Profile details — required id/email/mobile, everything else optional +export type UserDetails = Pick & + Partial> + +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' diff --git a/apps/user-ui/src/types/orders.ts b/apps/user-ui/src/types/orders.ts index 35f88f7..6fb8457 100644 --- a/apps/user-ui/src/types/orders.ts +++ b/apps/user-ui/src/types/orders.ts @@ -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 & { + deliveryDate?: string + items: OrderItem[] + discountAmount?: number } diff --git a/apps/web-ui/src/components/Dialog.tsx b/apps/web-ui/src/components/Dialog.tsx index 639487e..a587ffd 100644 --- a/apps/web-ui/src/components/Dialog.tsx +++ b/apps/web-ui/src/components/Dialog.tsx @@ -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 diff --git a/apps/web-ui/src/hooks/prominent-api-hooks.ts b/apps/web-ui/src/hooks/prominent-api-hooks.ts index 8b809fd..b8cdfd5 100644 --- a/apps/web-ui/src/hooks/prominent-api-hooks.ts +++ b/apps/web-ui/src/hooks/prominent-api-hooks.ts @@ -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() diff --git a/apps/web-ui/src/lib/auth-context.tsx b/apps/web-ui/src/lib/auth-context.tsx index 1195b6c..1e95cbe 100644 --- a/apps/web-ui/src/lib/auth-context.tsx +++ b/apps/web-ui/src/lib/auth-context.tsx @@ -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 & + Partial> & { + 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 { diff --git a/apps/web-ui/src/lib/stores/address-store.ts b/apps/web-ui/src/lib/stores/address-store.ts index e6aa606..64238f6 100644 --- a/apps/web-ui/src/lib/stores/address-store.ts +++ b/apps/web-ui/src/lib/stores/address-store.ts @@ -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((set) => ({ selectedAddressId: null, diff --git a/apps/web-ui/src/lib/stores/central-product-store.ts b/apps/web-ui/src/lib/stores/central-product-store.ts index 6e858e5..e7c9663 100644 --- a/apps/web-ui/src/lib/stores/central-product-store.ts +++ b/apps/web-ui/src/lib/stores/central-product-store.ts @@ -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 - setProducts: (products: Product[]) => void - refetchProducts: (() => void) | null - setRefetchProducts: (fn: (() => void) | null) => void -} +type CentralProductStore = CentralProductsState export const useCentralProductStore = create((set) => ({ products: [], diff --git a/apps/web-ui/src/lib/stores/flash-navigation-store.ts b/apps/web-ui/src/lib/stores/flash-navigation-store.ts index 6b5c0d6..35acfc2 100644 --- a/apps/web-ui/src/lib/stores/flash-navigation-store.ts +++ b/apps/web-ui/src/lib/stores/flash-navigation-store.ts @@ -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((set) => ({ shouldNavigateToCart: false, diff --git a/apps/web-ui/src/lib/stores/navigation-store.ts b/apps/web-ui/src/lib/stores/navigation-store.ts index 97675d7..50ff035 100644 --- a/apps/web-ui/src/lib/stores/navigation-store.ts +++ b/apps/web-ui/src/lib/stores/navigation-store.ts @@ -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((set) => ({ isNavigatedFromHome: false, diff --git a/apps/web-ui/src/lib/stores/quick-delivery-store.ts b/apps/web-ui/src/lib/stores/quick-delivery-store.ts index 845592b..c12aa20 100644 --- a/apps/web-ui/src/lib/stores/quick-delivery-store.ts +++ b/apps/web-ui/src/lib/stores/quick-delivery-store.ts @@ -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((set) => ({ isDrawerHidden: false, diff --git a/apps/web-ui/src/routes/me.coupons.tsx b/apps/web-ui/src/routes/me.coupons.tsx index 6c4b624..f3c55a7 100644 --- a/apps/web-ui/src/routes/me.coupons.tsx +++ b/apps/web-ui/src/routes/me.coupons.tsx @@ -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 { diff --git a/change-log.txt b/change-log.txt index fe3aae5..704610c 100644 --- a/change-log.txt +++ b/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 (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 & { 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 & { 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 { products: Product[]; productsById: Record; refetchProducts: (() => void | Promise) | null; setProducts(products); setRefetchProducts(refetch) } (c28; refetch union accepts void AND Promise 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 & { createdAt: string; lastUpdated: string } +- dashboard-banners/edit/[id].tsx (c6): interface Banner → Omit & { description?; skuIds?; redirectUrl?; serialNum: number; createdAt: string; lastUpdated: string } +- components/ProductGroupForm.tsx (c8): ProductGroup → Omit & { createdAt: string; products: any[] } +- manage-orders/orders/index.tsx (c20): OrderType → Omit & { customerName: string|null; customerMobile?: string|null; items: OrderItemRow[]; createdAt: string; adminNotes?; userNotes?; userNegativityScore?; couponCode?; couponDescription?; discountAmount? } where OrderItemRow = Omit & { id?: number; isPackaged?: boolean; isPackageVerified?: boolean } +- components/SnippetOrdersView.tsx (c11+24): OrderProduct → Pick; SnippetOrder → Omit & { totalAmount: string; slotInfo: { time: string; sequence: any[] } | null; products: OrderProduct[] } +- components/StoreForm.tsx (c14): StoreFormData → Omit & { description: string; products: number[] } +- components/ProductsSelector.tsx (c43): SkuSummary → Omit +- send-notifications/index.tsx (c55): User → Omit & { name: string|null; isEligibleForNotif: boolean } +- NEW types/drag-order-item.ts (c16): export interface DragOrderItemProps { item: T; drag: () => void; isActive: boolean } +- customize-app/all-items-order.tsx (c16): ProductItemProps deleted → React.FC> +- customize-app/popular-items.tsx (c16+c4): ProductItemProps → interface extends DragOrderItemProps { onDelete: (id:number)=>void }; PopularProduct → Omit & { price: number; marketPrice: number|null; images: string[]; storeId: number|null; nextDeliveryDate: string|null } +- product-tags/order.tsx (c16): TagItemProps deleted → React.FC> + +apps/user-ui: +- src/types/auth.ts (c17+53+26): User → Pick & Partial> & { mobile: string|null; profileImage?: string|null; createdAt: string }; UserDetails → Pick & Partial>; AuthState → interface extends AuthStateCore { user: User|null; userDetails: UserDetails|null } +- src/store/addressStore.ts (c19): interface AddressState deleted → create +- src/store/navigationStore.ts (c37): interface NavigationState deleted → create (imported from shared) +- components/stores/flashNavigationStore.ts (c34): → type FlashNavigationState = FlashNavigationState (shared) & { reset: () => void } +- src/store/quickDeliveryStore.ts (c42): → type QuickDeliveryState = Omit (user-ui store has no setDrawerHidden action) +- src/store/centralProductStore.ts (c28): interface CentralProductState → type CentralProductState = CentralProductState (shared generic; local Product = MergedProduct) +- hooks/cart-query-hooks.tsx (c36): interface LocalCartItem → type LocalCartItem = Omit +- src/types/orders.ts (c56+57): OrderItem → Pick; Order → Omit & { deliveryDate?: string; items: OrderItem[]; discountAmount?: number } + +apps/web-ui: +- src/lib/auth-context.tsx (c17+26): User → Pick & Partial> & { 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 (shared generic, local Product) +- src/routes/me.coupons.tsx (c54): Coupon → Omit & { 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>` 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` (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 +- CouponValidationResult.coupon?: Partial 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 & { 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; 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 { id: number; images: I } + UserStoreSummaryCore

{ id; name; description: string|null; productCount: number; sampleProducts: P[] }; UserStoreSampleProduct = Core<{signedImageUrl: string|null}>; UserStoreSampleProductData = Core<{images: string[]|null}>; UserStoreSummary extends SummaryCore { signedImageUrl: string|null }; UserStoreSummaryData extends SummaryCore { 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 } ++ OrderCardActionProps { 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 +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 { supportMobile: string|null } +apps/admin-ui/app/(drawer)/dashboard/user-management/[id].tsx (c39): OrderItemProps → extends OrderCardActionProps +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 (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 & { 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 `

` 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`. diff --git a/package.json b/package.json index 30ae199..6594969 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/db_helper_sqlite/src/admin-apis/coupon.ts b/packages/db_helper_sqlite/src/admin-apis/coupon.ts index 1c2a596..d92a0ec 100644 --- a/packages/db_helper_sqlite/src/admin-apis/coupon.ts +++ b/packages/db_helper_sqlite/src/admin-apis/coupon.ts @@ -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 { }) } -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 { @@ -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 { 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() diff --git a/packages/db_helper_sqlite/src/admin-apis/product.ts b/packages/db_helper_sqlite/src/admin-apis/product.ts index 447fd29..4a7bcb4 100644 --- a/packages/db_helper_sqlite/src/admin-apis/product.ts +++ b/packages/db_helper_sqlite/src/admin-apis/product.ts @@ -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 type SkuRow = InferSelectModel type MarketStatsRow = InferSelectModel type SkuFeatureRow = InferSelectModel - -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 type SpecialDealRow = InferSelectModel type ProductTagInfoRow = InferSelectModel @@ -677,12 +651,7 @@ export async function getProductTagInfoById(tagId: number): Promise & { tagName?: string - tagDescription?: string | null - imageUrl?: string | null - isDashboardTag?: boolean - relatedStores?: number[] sortOrder?: number[] } diff --git a/packages/db_helper_sqlite/src/admin-apis/slots.ts b/packages/db_helper_sqlite/src/admin-apis/slots.ts index 7c56140..9112a06 100644 --- a/packages/db_helper_sqlite/src/admin-apis/slots.ts +++ b/packages/db_helper_sqlite/src/admin-apis/slots.ts @@ -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 diff --git a/packages/db_helper_sqlite/src/stores/store-helpers.ts b/packages/db_helper_sqlite/src/stores/store-helpers.ts index fe4ca2f..289ac0f 100644 --- a/packages/db_helper_sqlite/src/stores/store-helpers.ts +++ b/packages/db_helper_sqlite/src/stores/store-helpers.ts @@ -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 { // 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 { @@ -129,7 +102,7 @@ export async function getAllProductsForCache(): Promise { 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 !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 { // 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 { @@ -242,7 +208,7 @@ export async function getAllProductCombosForCache(): Promise { // 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 { const skus = await db.query.productSkus.findMany({ @@ -339,15 +333,7 @@ export async function getAllSkusSummary(): Promise { }) } -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, } diff --git a/packages/shared/types/admin.ts b/packages/shared/types/admin.ts index df008bf..b0ff93b 100644 --- a/packages/shared/types/admin.ts +++ b/packages/shared/types/admin.ts @@ -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 & { + 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; -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 & { + 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 { diff --git a/packages/shared/types/app-common.types.ts b/packages/shared/types/app-common.types.ts index f3a84ba..5d02d5c 100644 --- a/packages/shared/types/app-common.types.ts +++ b/packages/shared/types/app-common.types.ts @@ -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 +} + +// Card that shows one order with a press action (user-ui glimpse, admin-ui history) +export interface OrderCardActionProps { + 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 { + products: Product[] + productsById: Record + refetchProducts: (() => void | Promise) | null + setProducts: (products: Product[]) => void + setRefetchProducts: (refetch: (() => void | Promise) | null) => void +} diff --git a/packages/shared/types/coupon.types.ts b/packages/shared/types/coupon.types.ts index c89afe2..63ef7f8 100644 --- a/packages/shared/types/coupon.types.ts +++ b/packages/shared/types/coupon.types.ts @@ -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 + export interface CouponValidationResult { valid: boolean; message?: string; diff --git a/packages/shared/types/index.ts b/packages/shared/types/index.ts index 306a305..ec36da4 100644 --- a/packages/shared/types/index.ts +++ b/packages/shared/types/index.ts @@ -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 diff --git a/packages/shared/types/staff-user.types.ts b/packages/shared/types/staff-user.types.ts index 6b7d20c..44c5209 100644 --- a/packages/shared/types/staff-user.types.ts +++ b/packages/shared/types/staff-user.types.ts @@ -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; diff --git a/packages/shared/types/store.types.ts b/packages/shared/types/store.types.ts index 52b3d3a..8cc1a95 100644 --- a/packages/shared/types/store.types.ts +++ b/packages/shared/types/store.types.ts @@ -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 & { productIds?: number[] } /** * Chip props for tag selection on store-detail pages. diff --git a/packages/shared/types/user.ts b/packages/shared/types/user.ts index d649ff9..2f606ea 100644 --- a/packages/shared/types/user.ts +++ b/packages/shared/types/user.ts @@ -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

{ 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 { + signedImageUrl: string | null; +} + +export interface UserStoreSummaryData extends UserStoreSummaryCore { 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 & { 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 { diff --git a/packages/ui/src/components/bottom-dropdown.tsx b/packages/ui/src/components/bottom-dropdown.tsx index 2d4fbd9..0893ef7 100644 --- a/packages/ui/src/components/bottom-dropdown.tsx +++ b/packages/ui/src/components/bottom-dropdown.tsx @@ -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 { 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; diff --git a/packages/ui/src/components/dropdown.tsx b/packages/ui/src/components/dropdown.tsx index b44b43c..a7b1aa8 100755 --- a/packages/ui/src/components/dropdown.tsx +++ b/packages/ui/src/components/dropdown.tsx @@ -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 { 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 = ({ label, value, diff --git a/packages/web-components/src/components/image-gallery-with-delete.tsx b/packages/web-components/src/components/image-gallery-with-delete.tsx index 265ac4e..a2fd27f 100644 --- a/packages/web-components/src/components/image-gallery-with-delete.tsx +++ b/packages/web-components/src/components/image-gallery-with-delete.tsx @@ -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({ diff --git a/packages/web-components/tsconfig.json b/packages/web-components/tsconfig.json index abc53d9..0bf69c8 100644 --- a/packages/web-components/tsconfig.json +++ b/packages/web-components/tsconfig.json @@ -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"], diff --git a/typecheck b/typecheck new file mode 100755 index 0000000..62f85ac --- /dev/null +++ b/typecheck @@ -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'