This commit is contained in:
shafi54 2026-09-06 18:05:20 +05:30
parent 1ff771355e
commit ffcab51d98
72 changed files with 875 additions and 844 deletions

View file

@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback } from "react";
import type { StoreProductCard } from '@packages/shared'
import {
View,
Alert,
@ -27,20 +28,12 @@ const { width: screenWidth } = Dimensions.get("window");
const itemWidth = screenWidth - 48; // 24px padding each side
const itemHeight = 80;
interface Product {
id: number;
name: string;
images: string[];
isOutOfStock: boolean;
}
// Reorder row — anchored to the shared store-card core
type Product = Pick<StoreProductCard, 'id' | 'name' | 'images' | 'isOutOfStock'>
interface ProductItemProps {
item: Product;
drag: () => void;
isActive: boolean;
}
import type { DragOrderItemProps } from '@/types/drag-order-item';
const ProductItem: React.FC<ProductItemProps> = ({
const ProductItem: React.FC<DragOrderItemProps<Product>> = ({
item,
drag,
isActive,

View file

@ -24,26 +24,22 @@ import { useRouter } from "expo-router";
import { trpc } from "../../../../src/trpc-client";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import { useQueryClient } from "@tanstack/react-query";
import type { StoreProductCard } from '@packages/shared';
import type { DragOrderItemProps } from '@/types/drag-order-item';
interface PopularProduct {
id: number;
name: string;
shortDescription: string | null;
// Popular-items row = shared store card core with numeric (form) prices
type PopularProduct = Omit<
StoreProductCard,
'price' | 'marketPrice' | 'unitNotation' | 'images'
> & {
price: number;
marketPrice: number | null;
unit: string;
incrementStep: number;
productQuantity: number;
storeId: number | null;
isOutOfStock: boolean;
nextDeliveryDate: string | null;
images: string[];
}
storeId: number | null;
nextDeliveryDate: string | null;
};
interface ProductItemProps {
item: PopularProduct;
drag: () => void;
isActive: boolean;
interface ProductItemProps extends DragOrderItemProps<PopularProduct> {
onDelete: (id: number) => void;
}

View file

@ -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();

View file

@ -4,19 +4,13 @@ import { AppContainer, MyText, tw, MyTouchableOpacity } from 'common-ui';
import { trpc } from '../../../../src/trpc-client';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useRouter } from 'expo-router';
import type { Banner as SharedBanner } from '@packages/shared';
interface Banner {
id: number;
name: string;
imageUrl: string;
description: string | null;
skuIds: number[] | null;
redirectUrl: string | null;
serialNum: number | null;
isActive: boolean;
// Serialized banner row (API JSON carries string dates) anchored to the shared shape
type Banner = Omit<SharedBanner, 'createdAt' | 'lastUpdated'> & {
createdAt: string;
lastUpdated: string;
}
};
export default function DashboardBanners() {
const router = useRouter();

View file

@ -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;

View file

@ -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();

View file

@ -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;
}

View file

@ -21,6 +21,7 @@ import { useRouter } from 'expo-router';
import { trpc } from '../../../../src/trpc-client';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useQueryClient } from '@tanstack/react-query';
import type { DragOrderItemProps } from '@/types/drag-order-item';
const { width: screenWidth } = Dimensions.get('window');
const itemWidth = screenWidth - 48;
@ -32,13 +33,7 @@ interface Tag {
imageUrl: string | null;
}
interface TagItemProps {
item: Tag;
drag: () => void;
isActive: boolean;
}
const TagItem: React.FC<TagItemProps> = ({ item, drag, isActive }) => {
const TagItem: React.FC<DragOrderItemProps<Tag>> = ({ item, drag, isActive }) => {
return (
<ScaleDecorator>
<TouchableOpacity

View file

@ -15,8 +15,8 @@ export default function AddProduct() {
const handleSubmit = async (values: any, variantImages: ImageUploaderNeoPayload[][], _deletedImageKeys?: string[]) => {
try {
for (const variant of values.variants) {
const price = parseFloat(variant.price)
if (isNaN(price) || price <= 0) {
const price = variant.price
if (price == null || price <= 0) {
Alert.alert('Error', 'Please enter a valid price for every variant')
return
}
@ -24,15 +24,15 @@ export default function AddProduct() {
const seenSignatures = new Set<string>()
for (const variant of values.variants) {
const attributes = variant.attributes || []
const hasQuantity = attributes.some(
const features = variant.features || []
const hasQuantity = features.some(
(a: any) => (a.featureName ?? '').trim().toLowerCase() === 'quantity'
)
if (!hasQuantity) {
Alert.alert('Error', 'Every SKU must have a quantity feature')
return
}
const signature = attributes
const signature = features
.map((a: any) => `${(a.featureName ?? '').trim().toLowerCase()}::${a.featureValue.trim().toLowerCase()}`)
.sort()
.join('|')
@ -68,21 +68,21 @@ export default function AddProduct() {
return {
name: variant.name || null,
price: parseFloat(variant.price),
marketPrice: variant.marketPrice ? parseFloat(variant.marketPrice) : undefined,
price: variant.price as number,
marketPrice: variant.marketPrice ?? undefined,
images: variantUrls,
isFlashAvailable: variant.isFlashAvailable || false,
flashPrice: variant.flashPrice ? parseFloat(variant.flashPrice) : undefined,
flashPrice: variant.flashPrice ?? undefined,
isOffer: variant.isOffer || false,
isComboOnly: variant.isComboOnly || false,
isDeleted: variant.isDeleted || false,
isSuspended: variant.isSuspended || false,
features: variant.attributes.map((attr: any) => ({
features: variant.features.map((attr: any) => ({
featureName: attr.featureName,
featureValue: attr.featureValue,
})),
comboItems: (variant.comboItems || []).map((ci: any) => ({
skuId: typeof ci.skuId === 'string' ? parseInt(ci.skuId) : ci.skuId,
skuId: ci.skuId,
})),
}
})
@ -114,16 +114,16 @@ export default function AddProduct() {
{
id: undefined as number | undefined,
name: '',
price: '',
marketPrice: '',
price: undefined,
marketPrice: null,
isFlashAvailable: false,
flashPrice: '',
flashPrice: null,
isOffer: false,
isComboOnly: false,
isDeleted: false,
isSuspended: false,
attributes: [{ featureName: 'quantity', featureValue: '' }],
comboItems: [] as { skuId: number | string }[],
features: [{ featureName: 'quantity', featureValue: '' }],
comboItems: [],
},
],
}

View file

@ -45,20 +45,20 @@ export default function EditProduct() {
variants: (productData.skus || []).map((sku) => ({
id: sku.id,
name: sku.name || '',
price: sku.price || '',
marketPrice: sku.marketPrice || '',
price: sku.price ? parseFloat(sku.price) : undefined,
marketPrice: sku.marketPrice ? parseFloat(sku.marketPrice) : null,
isFlashAvailable: sku.isFlashAvailable || false,
flashPrice: sku.flashPrice || '',
flashPrice: sku.flashPrice ? parseFloat(sku.flashPrice) : null,
isOffer: sku.isOffer || false,
isComboOnly: sku.isComboOnly || false,
isDeleted: sku.isDeleted || false,
isSuspended: sku.isSuspended || false,
attributes: (sku.features || []).map((f) => ({
features: (sku.features || []).map((f) => ({
featureName: f.featureName,
featureValue: f.featureValue,
})),
comboItems: (sku.comboItems || []).map((ci: any) => ({
skuId: ci.skuId.toString(),
skuId: ci.skuId,
})),
})),
}
@ -81,8 +81,8 @@ export default function EditProduct() {
const handleSubmit = async (values: any, variantImages: ImageUploaderNeoPayload[][], deletedImageKeys: string[]) => {
try {
for (const variant of values.variants) {
const price = parseFloat(variant.price)
if (isNaN(price) || price <= 0) {
const price = variant.price
if (price == null || price <= 0) {
Alert.alert('Error', 'Please enter a valid price for every variant')
return
}
@ -90,15 +90,15 @@ export default function EditProduct() {
const seenSignatures = new Set<string>()
for (const variant of values.variants) {
const attributes = variant.attributes || []
const hasQuantity = attributes.some(
const features = variant.features || []
const hasQuantity = features.some(
(a: any) => (a.featureName ?? '').trim().toLowerCase() === 'quantity'
)
if (!hasQuantity) {
Alert.alert('Error', 'Every SKU must have a quantity feature')
return
}
const signature = attributes
const signature = features
.map((a: any) => `${(a.featureName ?? '').trim().toLowerCase()}::${a.featureValue.trim().toLowerCase()}`)
.sort()
.join('|')
@ -146,21 +146,21 @@ export default function EditProduct() {
return {
id: variant.id,
name: variant.name || null,
price: parseFloat(variant.price),
marketPrice: variant.marketPrice ? parseFloat(variant.marketPrice) : undefined,
price: variant.price as number,
marketPrice: variant.marketPrice ?? undefined,
images: allUrls,
isFlashAvailable: variant.isFlashAvailable || false,
flashPrice: variant.flashPrice ? parseFloat(variant.flashPrice) : undefined,
flashPrice: variant.flashPrice ?? undefined,
isOffer: variant.isOffer || false,
isComboOnly: variant.isComboOnly || false,
isDeleted: variant.isDeleted || false,
isSuspended: variant.isSuspended || false,
features: variant.attributes.map((attr: any) => ({
features: variant.features.map((attr: any) => ({
featureName: attr.featureName,
featureValue: attr.featureValue,
})),
comboItems: (variant.comboItems || []).map((ci: any) => ({
skuId: typeof ci.skuId === 'string' ? parseInt(ci.skuId) : ci.skuId,
skuId: ci.skuId,
})),
}
})

View file

@ -16,11 +16,11 @@ import {
BottomDropdown,
} from 'common-ui';
import { trpc } from '@/src/trpc-client';
import type { UserMiniInfo } from '@packages/shared';
interface User {
id: number;
// Notification recipient = shared mini user + eligibility flag
type User = Omit<UserMiniInfo, 'name'> & {
name: string | null;
mobile: string | null;
isEligibleForNotif: boolean;
}

View file

@ -1,4 +1,5 @@
import React, { useCallback } from 'react';
import type { OrderCardActionProps } from '@packages/shared'
import {
View,
TouchableOpacity,
@ -28,9 +29,7 @@ interface Order {
itemCount: number;
}
interface OrderItemProps {
order: Order;
onPress: () => void;
interface OrderItemProps extends OrderCardActionProps<Order> {
}
const getStatusColor = (status: string) => {

View file

@ -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;
}

View file

@ -5,15 +5,13 @@ import { MyText, tw, MyTextInput, MyTouchableOpacity, theme, BottomDropdown } fr
import ProductsSelector from './ProductsSelector';
import { trpc } from '../src/trpc-client';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import type { AdminProductGroup as SharedAdminProductGroup } from '@packages/shared';
export interface ProductGroup {
id: number;
groupName: string;
description: string | null;
// Serialized product group (string date, loosely typed products) anchored to shared
export type ProductGroup = Omit<SharedAdminProductGroup, 'createdAt' | 'products'> & {
createdAt: string;
products: any[];
productCount: number;
}
};
interface ProductGroupFormProps {
group?: ProductGroup | null;

View file

@ -3,15 +3,10 @@ import { View } from 'react-native';
import BottomDropdown, { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
import { trpc } from '../src/trpc-client';
import { tw } from 'common-ui';
import type { SkuSummary as SharedSkuSummary } from '@packages/shared';
interface SkuSummary {
id: number;
productId: number;
productName: string;
label: string;
storeId: number | null;
price: string;
}
// Selector works with skus that may not have images resolved yet
type SkuSummary = Omit<SharedSkuSummary, 'images'>
interface Group {
id: number;

View file

@ -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,
})),
};

View file

@ -2,28 +2,25 @@ import React from 'react';
import { View, ScrollView, TouchableOpacity } from 'react-native';
import { MyText, tw, AppContainer } from 'common-ui';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import type {
AdminVendorSnippetOrderProduct,
AdminVendorSnippetOrderSummary as SharedSnippetOrderSummary,
} from '@packages/shared';
interface OrderProduct {
productId: number;
productName: string;
quantity: number;
price: number;
unit: string;
subtotal: number;
}
// Line item + order summary anchored to the shared vendor-order contract
// (string totalAmount + any[] sequence at this view layer)
type OrderProduct = Pick<
AdminVendorSnippetOrderProduct,
'productId' | 'productName' | 'quantity' | 'price' | 'unit' | 'subtotal'
>
interface SnippetOrder {
orderId: string;
orderDate: string;
customerName: string;
totalAmount: string;
type SnippetOrder = Omit<SharedSnippetOrderSummary, 'totalAmount' | 'slotInfo' | 'products'> & {
totalAmount: string
slotInfo: {
time: string;
sequence: any[];
} | null;
products: OrderProduct[];
matchedProducts: number[];
snippetCode: string;
}
interface SnippetOrdersViewProps {

View file

@ -7,14 +7,13 @@ import ProductsSelector from './ProductsSelector';
import { trpc } from '../src/trpc-client';
import usePickImage from 'common-ui/src/components/use-pick-image';
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStore';
import type { CreateStoreInput } from '@packages/shared';
export interface StoreFormData {
name: string;
// Store form values — required description + product selection on top of CreateStoreInput
export type StoreFormData = Omit<CreateStoreInput, 'description'> & {
description: string;
imageUrl?: string;
owner: number;
products: number[];
}
};
interface StoreFormProps {
mode: 'create' | 'edit';

View file

@ -5,24 +5,29 @@ import * as Yup from 'yup'
import { MyTextInput, BottomDropdown, MyText, tw, ImageUploaderNeo, ImageUploaderNeoItem, ImageUploaderNeoPayload, Checkbox, InfoDialog } from 'common-ui'
import MaterialIcons from '@expo/vector-icons/MaterialIcons'
import { trpc } from '../trpc-client'
import type { CreateComboItemInput, CreateSkuInput } from '@packages/shared'
import type { SkuFeatureLike } from '@packages/shared'
// Feature input shape — single source: shared SkuFeatureLike
type Attribute = SkuFeatureLike
interface Variant {
// Variant form state — anchored to the shared CreateSkuInput (numeric prices,
// same field names). Images are managed separately via variantImages state.
type Variant = Omit<
CreateSkuInput,
'name' | 'price' | 'marketPrice' | 'flashPrice' | 'images' | 'features' | 'comboItems'
> & {
id?: number
name: string
price: string
marketPrice: string
price?: number
marketPrice: number | null
flashPrice: number | null
isFlashAvailable: boolean
flashPrice: string
isOffer: boolean
isComboOnly: boolean
isDeleted?: boolean
isSuspended: boolean
attributes: Attribute[]
comboItems: { skuId: number | string }[]
features: Attribute[]
comboItems: CreateComboItemInput[]
}
interface ProductFormData {
@ -58,15 +63,15 @@ const isQuantityFeature = (attr: Attribute): boolean =>
const defaultVariant = (): Variant => ({
id: undefined,
name: '',
price: '',
marketPrice: '',
price: undefined,
marketPrice: null,
isFlashAvailable: false,
flashPrice: '',
flashPrice: null,
isOffer: false,
isComboOnly: false,
isDeleted: false,
isSuspended: false,
attributes: [quantityAttribute()],
features: [quantityAttribute()],
comboItems: [],
})
@ -100,7 +105,7 @@ const productValidationSchema = Yup.object().shape({
.nullable()
.transform((value, originalValue) => (originalValue === '' ? null : value))
.optional(),
attributes: Yup.array()
features: Yup.array()
.min(1, 'At least one attribute is required')
.of(
Yup.object().shape({
@ -113,7 +118,7 @@ const productValidationSchema = Yup.object().shape({
if (!Array.isArray(variants)) return true
const seen = new Set<string>()
for (const variant of variants) {
const attrs = variant?.attributes || []
const attrs = variant?.features || []
const signature = variantSignature(attrs as Attribute[])
if (seen.has(signature)) {
return this.createError({ message: 'Two variants have the same attributes' })
@ -125,7 +130,7 @@ const productValidationSchema = Yup.object().shape({
.test('quantity-feature', 'Each SKU must have exactly one quantity feature', function (variants) {
if (!Array.isArray(variants)) return true
for (const variant of variants) {
const attrs = variant?.attributes || []
const attrs = variant?.features || []
const quantityCount = attrs.filter(
(a) => ((a as Attribute)?.featureName ?? '').trim().toLowerCase() === 'quantity'
).length
@ -180,7 +185,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
const { data: skusData } = trpc.common.product.getAllSkusSummary.useQuery({})
const skuOptions = (skusData?.skus || []).map((sku) => ({
label: sku.label,
value: sku.id.toString(),
value: sku.id,
}))
// Make sure every SKU starts with a 'quantity' feature (auto-added at the
@ -189,11 +194,11 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
return {
...initialValues,
variants: (initialValues.variants || []).map((v) => {
const hasQuantity = (v.attributes || []).some(
const hasQuantity = (v.features || []).some(
(a) => (a.featureName ?? '').trim().toLowerCase() === 'quantity'
)
if (hasQuantity) return v
return { ...v, attributes: [quantityAttribute(), ...(v.attributes || [])] }
return { ...v, features: [quantityAttribute(), ...(v.features || [])] }
}),
}
}, [initialValues])
@ -211,7 +216,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
...values,
variants: values.variants.map((v) => ({
...v,
attributes: v.attributes.map((a) => ({
features: v.features.map((a) => ({
...a,
featureName: isQuantityFeature(a) ? 'quantity' : (a.featureName ?? '').trim() || null,
})),
@ -347,7 +352,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
style={{ marginBottom: 12 }}
/>
<FieldArray name={`variants.${vIndex}.attributes`}>
<FieldArray name={`variants.${vIndex}.features`}>
{({ push: pushAttr, remove: removeAttr }) => (
<View style={tw`mb-3`}>
<View style={tw`flex-row justify-between items-center mb-2`}>
@ -360,12 +365,12 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
<MyText style={tw`text-gray-600 text-xs ml-0.5`}>Add</MyText>
</TouchableOpacity>
</View>
{variant.attributes.map((attr, aIndex) => {
const quantityCount = variant.attributes.filter(isQuantityFeature).length
{variant.features.map((attr, aIndex) => {
const quantityCount = variant.features.filter(isQuantityFeature).length
// Quantity features are locked (non-editable), but if there
// are duplicates (e.g. 'quantity' + 'Quantity') the user must
// be able to delete the extras to fix the form.
const canDelete = variant.attributes.length > 1 && (!isQuantityFeature(attr) || quantityCount > 1)
const canDelete = variant.features.length > 1 && (!isQuantityFeature(attr) || quantityCount > 1)
return (
<View key={aIndex} style={tw`flex-row items-center mb-2 gap-2`}>
<View style={tw`flex-1`}>
@ -374,7 +379,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
value={attr.featureName ?? ''}
onChangeText={(text) =>
setFieldValue(
`variants.${vIndex}.attributes.${aIndex}.featureName`,
`variants.${vIndex}.features.${aIndex}.featureName`,
text.trim().toLowerCase() === 'quantity' ? 'quantity' : text
)
}
@ -386,7 +391,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
<MyTextInput
placeholder={isQuantityFeature(attr) ? 'e.g. 0.5 kg' : 'Value'}
value={attr.featureValue}
onChangeText={handleChange(`variants.${vIndex}.attributes.${aIndex}.featureValue`)}
onChangeText={handleChange(`variants.${vIndex}.features.${aIndex}.featureValue`)}
/>
</View>
{canDelete && (
@ -420,7 +425,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
topLabel="Market Price"
placeholder="MRP"
keyboardType="numeric"
value={variant.marketPrice}
value={variant.marketPrice?.toString() ?? ''}
onChangeText={handleChange(`variants.${vIndex}.marketPrice`)}
/>
</View>
@ -429,7 +434,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
topLabel="Our Price"
placeholder="Selling price"
keyboardType="numeric"
value={variant.price}
value={variant.price?.toString() ?? ''}
onChangeText={handleChange(`variants.${vIndex}.price`)}
/>
</View>
@ -440,7 +445,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
checked={variant.isFlashAvailable}
onPress={() => {
setFieldValue(`variants.${vIndex}.isFlashAvailable`, !variant.isFlashAvailable)
if (variant.isFlashAvailable) setFieldValue(`variants.${vIndex}.flashPrice`, '')
if (variant.isFlashAvailable) setFieldValue(`variants.${vIndex}.flashPrice`, null)
}}
style={tw`mr-3`}
/>
@ -483,7 +488,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
topLabel="Flash Price"
placeholder="Enter flash price"
keyboardType="numeric"
value={variant.flashPrice}
value={variant.flashPrice?.toString() ?? ''}
onChangeText={handleChange(`variants.${vIndex}.flashPrice`)}
style={{ marginBottom: 12 }}
/>
@ -515,7 +520,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
<TouchableOpacity
onPress={() => {
const items = variant.comboItems || []
items.push({ skuId: '' })
items.push({ skuId: 0 })
setFieldValue(`variants.${vIndex}.comboItems`, items)
}}
style={tw`bg-gray-200 px-2 py-0.5 rounded flex-row items-center`}
@ -530,7 +535,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
<BottomDropdown
label="SKU"
value={ci.skuId}
options={skuOptions.map((opt: { label: string; value: string }) => ({
options={skuOptions.map((opt: { label: string; value: number }) => ({
...opt,
// Disable SKUs already picked in another row of this combo.
disabled: (variant.comboItems || []).some(
@ -546,7 +551,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
Alert.alert('Duplicate', 'This product is already in the combo')
return
}
setFieldValue(`variants.${vIndex}.comboItems.${cIndex}.skuId`, val)
setFieldValue(`variants.${vIndex}.comboItems.${cIndex}.skuId`, Number(val))
}}
placeholder="Select SKU"
/>

View file

@ -0,0 +1,6 @@
// Common props for draggable reorder rows (customize-app / product-tags order screens)
export interface DragOrderItemProps<T> {
item: T
drag: () => void
isActive: boolean
}

View file

@ -1,4 +1,5 @@
import type { IdName } from '@packages/shared';
import type { AdminVendorSnippetInput } from '@packages/shared'
export type VendorSnippetProduct = IdName;
@ -22,12 +23,10 @@ export interface VendorSnippet {
} | null;
}
export interface VendorSnippetForm {
// Vendor-snippet form row — anchored to the shared input (wire validTill);
// form requires slotId and carries the serialized createdAt.
export type VendorSnippetForm = Omit<AdminVendorSnippetInput, 'slotId'> & {
id: number;
snippetCode: string;
slotId: number;
isPermanent: boolean;
skuIds: number[];
validTill: string | null;
createdAt: string;
}

View file

@ -82,8 +82,7 @@ export type {
AdminVendorSnippetWithSlot,
AdminVendorSnippetProduct,
AdminVendorSnippetWithProducts,
AdminVendorSnippetCreateInput,
AdminVendorSnippetUpdateInput,
AdminVendorSnippetInput,
AdminVendorSnippetDeleteResult,
AdminVendorSnippetOrderProduct,
AdminVendorSnippetOrderSummary,

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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(

View file

@ -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(

View file

@ -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,
})

View file

@ -51,6 +51,7 @@ export const productRouter = router({
return {
...cachedProduct,
images: cachedProduct.images as string[],
deliverySlots: filteredSlots
};
}

View file

@ -365,13 +365,7 @@ const ProductItem = memo(({ item, onPress }: ProductItemProps) => {
);
});
interface ListHeaderProps {
dashboardTags: any[];
activeTagId: number | null;
productsByTagId: Record<number, any[]>;
expandedByTagId: Record<number, boolean>;
onToggleExpand: (tagId: number) => void;
onProductPress: (id: number) => void;
interface ListHeaderProps extends TagSceneProps {
onSelectTag: (id: number) => void;
storesData: any;
sortedSlots: any[];

View file

@ -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 }: {

View file

@ -1,4 +1,5 @@
import React, { useState, useCallback, useRef } from 'react';
import type { OrderCardActionProps } from '@packages/shared'
import { View, TouchableOpacity, ActivityIndicator, Linking, ScrollView, Dimensions, NativeSyntheticEvent, NativeScrollEvent } from 'react-native';
import { useRouter } from 'expo-router';
import { tw, MyText } from 'common-ui';
@ -16,9 +17,7 @@ const { width: screenWidth } = Dimensions.get('window');
const CARD_WIDTH = (screenWidth - 48) * 0.95; // card width (24px padding each side), reduced 5%
const SNAP_INTERVAL = CARD_WIDTH + 24; // card + right margin = page width
interface OrderCardProps {
order: Order;
onPress: () => void;
interface OrderCardProps extends OrderCardActionProps<Order> {
supportMobile?: string | null;
}

View file

@ -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;
}

View file

@ -1,10 +1,7 @@
import { create } from 'zustand';
import type { FlashNavigationState as SharedFlashNavigationState } from '@packages/shared';
interface FlashNavigationState {
shouldNavigateToCart: boolean;
setShouldNavigateToCart: (value: boolean) => void;
reset: () => void;
}
type FlashNavigationState = SharedFlashNavigationState & { reset: () => void }
export const useFlashNavigationStore = create<FlashNavigationState>((set) => ({
shouldNavigateToCart: false,

View file

@ -2,7 +2,7 @@ import { useCentralProductStore } from '@/src/store/centralProductStore';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { Alert } from 'react-native';
import { useQuery, useMutation, useQueryClient, UseQueryResult, UseMutationResult } from '@tanstack/react-query';
import type { CartData as SharedCartData } from '@packages/shared';
import type { CartData as SharedCartData, ProductSummaryCore } from '@packages/shared';
import { StorageServiceCasual } from 'common-ui/src/services/StorageServiceCasual';
// Cart type definition
@ -12,26 +12,21 @@ const getCartStorageKey = (cartType: CartType = "regular"): string => {
return cartType === "flash" ? "flash_cart_items" : "cart_items";
};
interface LocalCartItem {
id: number;
skuId: number;
quantity: number;
slotId: number;
addedAt: string;
}
type LocalCartItem = Omit<CartItem, 'subtotal'>
interface ProductSummary {
id: number;
price: string;
incrementStep: number;
isOutOfStock: boolean;
isFlashAvailable: boolean;
name?: string;
flashPrice?: string | null;
images?: string[];
productQuantity?: number;
unitNotation?: string;
marketPrice?: string | null;
// Cart product summary — anchored to the shared ProductSummaryCore; the cart
// view keeps most display fields optional and adds the flash flags.
type ProductSummary = Omit<
ProductSummaryCore,
'name' | 'images' | 'unitNotation' | 'marketPrice'
> & {
isFlashAvailable: boolean
name?: string
flashPrice?: string | null
images?: string[] | null
productQuantity?: number
unitNotation?: string
marketPrice?: string | null
}
export interface CartItem {

View file

@ -1,13 +1,12 @@
import { useFocusEffect } from '@react-navigation/native';
import type { AuthRedirectOptions } from '@packages/shared'
import { useRouter } from 'expo-router';
import { useAuth } from '@/src/contexts/AuthContext';
import type { RedirectState } from '@/src/contexts/AuthContext';
import { StorageServiceCasual } from 'common-ui';
import constants from '@/src/constants';
interface AuthenticatedRouteOptions {
targetUrl?: string;
queryParams?: Record<string, any>;
interface AuthenticatedRouteOptions extends AuthRedirectOptions {
}
export function useAuthenticatedRoute(options: AuthenticatedRouteOptions = {}) {

View file

@ -7,9 +7,9 @@ import { trpc } from '@/src/trpc-client';
import { StorageServiceCasual } from 'common-ui';
import { useRouter } from 'expo-router';
import constants from '@/src/constants';
import type { ReactParentComponent } from '@packages/shared';
import type { AuthRedirectOptions, ReactParentComponent } from '@packages/shared';
export interface RedirectState {
export interface RedirectState extends AuthRedirectOptions {
targetUrl: string;
queryParams: Record<string, any>;
timestamp: number;

View file

@ -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
}

View file

@ -1,11 +1,7 @@
import { create } from 'zustand';
import type { AddressSelectionState } from '@packages/shared';
interface AddressState {
selectedAddressId: number | null;
setSelectedAddressId: (addressId: number | null) => void;
}
export const useAddressStore = create<AddressState>((set) => ({
export const useAddressStore = create<AddressSelectionState>((set) => ({
selectedAddressId: null,
setSelectedAddressId: (addressId) => set({ selectedAddressId: addressId }),
}));

View file

@ -1,16 +1,11 @@
import { create } from 'zustand'
import { useEffect } from 'react'
import { useAllProducts, type MergedProduct } from '@/src/hooks/prominent-api-hooks'
import type { CentralProductState as CentralProductsState } from '@packages/shared'
export type Product = MergedProduct
interface CentralProductState {
products: Product[]
productsById: Record<number, Product>
refetchProducts: (() => Promise<void>) | null
setProducts: (products: Product[]) => void
setRefetchProducts: (refetch: () => Promise<void>) => void
}
type CentralProductState = CentralProductsState<Product>
export const useCentralProductStore = create<CentralProductState>((set) => ({
products: [],

View file

@ -1,11 +1,5 @@
import { create } from 'zustand';
interface NavigationState {
isNavigatedFromHome: boolean;
setNavigatedFromHome: (value: boolean) => void;
selectedStoreId: number | null;
setSelectedStoreId: (storeId: number | null) => void;
}
import type { NavigationState } from '@packages/shared';
export const useNavigationStore = create<NavigationState>((set) => ({
isNavigatedFromHome: false,

View file

@ -1,10 +1,8 @@
import { create } from 'zustand';
import type { QuickDeliveryState as SharedQuickDeliveryState } from '@packages/shared';
interface QuickDeliveryState {
isDrawerHidden: boolean;
selectedSlotId: number | null;
setSelectedSlotId: (slotId: number | null) => void;
}
// user-ui only reads/hides the drawer — no setDrawerHidden action here
type QuickDeliveryState = Omit<SharedQuickDeliveryState, 'setDrawerHidden'>
export const useQuickDeliveryStore = create<QuickDeliveryState>((set) => ({
isDrawerHidden: false,

View file

@ -1,30 +1,20 @@
export interface User {
id: number;
name?: string | null;
email: string | null;
mobile: string | null;
profileImage?: string | null;
createdAt: string;
}
import type { User as SharedUser, UserAuthProfile, AuthStateCore } from '@packages/shared'
export interface UserDetails {
id: number;
name?: string | null;
email: string | null;
mobile: string | null;
profileImage?: string | null;
bio?: string | null;
dateOfBirth?: string | null;
gender?: string | null;
occupation?: string | null;
}
// Auth user — serialized view of the shared User (string dates, nullable mobile)
export type User = Pick<SharedUser, 'id' | 'email'> &
Partial<Omit<SharedUser, 'id' | 'email' | 'createdAt' | 'mobile'>> & {
mobile: string | null
profileImage?: string | null
createdAt: string
}
export interface AuthState {
// Profile details — required id/email/mobile, everything else optional
export type UserDetails = Pick<UserAuthProfile, 'id' | 'email' | 'mobile'> &
Partial<Omit<UserAuthProfile, 'id' | 'email' | 'mobile' | 'createdAt'>>
export interface AuthState extends AuthStateCore {
user: User | null;
userDetails: UserDetails | null;
isAuthenticated: boolean;
isLoading: boolean;
token: string | null;
}
import type { LoginRequest, RegisterRequest } from '@packages/shared'

View file

@ -1,31 +1,15 @@
// Shared order view-model types for user-ui
// (Were identically duplicated in me/my-orders/index.tsx and NextOrderGlimpse.tsx)
import type { UserOrderItemSummary, UserOrderSummary } from '@packages/shared'
export interface OrderItem {
productName: string;
quantity: number;
price: number;
amount: number;
image: string | null;
}
// Order line item = shared summary minus the discounted price column
export type OrderItem = Pick<
UserOrderItemSummary,
'productName' | 'quantity' | 'price' | 'amount' | 'image'
>
export interface Order {
id: number;
orderId: string;
orderDate: string;
deliveryStatus: string;
deliveryDate?: string;
orderStatus: string;
cancelReason: string | null;
totalAmount: number;
deliveryCharge: number;
paymentMode: string;
paymentStatus: string;
refundStatus: string;
refundAmount: number | null;
userNotes: string | null;
items: OrderItem[];
discountAmount?: number;
isFlashDelivery: boolean;
createdAt: string;
export type Order = Omit<UserOrderSummary, 'deliveryDate' | 'items' | 'discountAmount'> & {
deliveryDate?: string
items: OrderItem[]
discountAmount?: number
}

View file

@ -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

View file

@ -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()

View file

@ -1,22 +1,18 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'
import { trpc } from './trpc-client'
import { useNavigate } from '@tanstack/react-router'
import type { User as SharedUser, AuthStateCore } from '@packages/shared'
interface User {
id: number
name?: string | null | undefined
mobile: string | null
email: string | null
profileImage: string | null
// Auth user — serialized view of the shared User (optional name, no createdAt)
type User = Pick<SharedUser, 'id' | 'email'> &
Partial<Omit<SharedUser, 'id' | 'email' | 'createdAt' | 'mobile'>> & {
mobile: string | null
profileImage: string | null
}
}
interface AuthState {
interface AuthState extends AuthStateCore {
user: User | null
userDetails: any | null
isAuthenticated: boolean
isLoading: boolean
token: string | null
}
interface AuthContextType extends AuthState {

View file

@ -1,10 +1,7 @@
import { create } from 'zustand'
import type { AddressSelectionState } from '@packages/shared'
interface AddressState {
selectedAddressId: number | null
setSelectedAddressId: (addressId: number | null) => void
clearSelectedAddress: () => void
}
type AddressState = AddressSelectionState & { clearSelectedAddress: () => void }
export const useAddressStore = create<AddressState>((set) => ({
selectedAddressId: null,

View file

@ -1,4 +1,5 @@
import { create } from 'zustand'
import type { CentralProductState as CentralProductsState } from '@packages/shared'
interface Product {
id: number
@ -14,13 +15,7 @@ interface Product {
description: string | null
}
interface CentralProductStore {
products: Product[]
productsById: Record<number, Product>
setProducts: (products: Product[]) => void
refetchProducts: (() => void) | null
setRefetchProducts: (fn: (() => void) | null) => void
}
type CentralProductStore = CentralProductsState<Product>
export const useCentralProductStore = create<CentralProductStore>((set) => ({
products: [],

View file

@ -1,9 +1,7 @@
import { create } from 'zustand'
import type { FlashNavigationState } from '@packages/shared'
interface FlashNavigationStore {
shouldNavigateToCart: boolean
setShouldNavigateToCart: (value: boolean) => void
}
type FlashNavigationStore = FlashNavigationState
export const useFlashNavigationStore = create<FlashNavigationStore>((set) => ({
shouldNavigateToCart: false,

View file

@ -1,11 +1,7 @@
import { create } from 'zustand'
import type { NavigationState } from '@packages/shared'
interface NavigationStore {
isNavigatedFromHome: boolean
selectedStoreId: number | null
setNavigatedFromHome: (value: boolean) => void
setSelectedStoreId: (id: number | null) => void
}
type NavigationStore = NavigationState
export const useNavigationStore = create<NavigationStore>((set) => ({
isNavigatedFromHome: false,

View file

@ -1,11 +1,7 @@
import { create } from 'zustand'
import type { QuickDeliveryState } from '@packages/shared'
interface QuickDeliveryStore {
isDrawerHidden: boolean
selectedSlotId: number | null
setDrawerHidden: (hidden: boolean) => void
setSelectedSlotId: (id: number | null) => void
}
type QuickDeliveryStore = QuickDeliveryState
export const useQuickDeliveryStore = create<QuickDeliveryStore>((set) => ({
isDrawerHidden: false,

View file

@ -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 {

View file

@ -1212,3 +1212,207 @@ apps/fallback-ui/src/components/3d/Characters.tsx + 3d/Scene.tsx (c48):
[2026-09-05 12:55:00] COMPLETED type-cluster unification pass (clusters 3,7,8,9,11,12,13,14,17,19,20,21,44,47,48,49,50,51,52,53,54,56 → @packages/shared).
Verification (tsc --noEmit error counts vs pre-change baselines): backend 11=11, web-ui 150=150, user-ui 106=106, admin-ui 122=122, fallback-ui 107=107, db_helper_sqlite 4=4. Diffs are line-number shifts only (deleted lines) — zero new errors. `apps/web-ui` vite build exit 0. db_helper_postgres: untouched. Remaining same-name locals verified genuinely different shapes (bottom-dropdown/multi-select DropdownOption variants, web-ui Dialog.tsx DialogProps, user-ui floating-cart-bar props) — intentionally left. type-clusters/README.md verdicts updated to ✅ for the resolved clusters with a pass note at top.
================================================================================
2026-09-05 13:40:00 — IMPLEMENT all 🟢 "unify now" clusters from similar_types.md (29 clusters; 🟡/🟣/⚪/🔵 untouched). db_helper_postgres still excluded. Derived types are shape-EXACT replacements of the locals they replace (verified per-cluster; tsc baselines re-checked after).
packages/shared/types/admin.ts (clusters 8,10,22,23,43,51):
- import type { UnitSeedData } from './seed.types' (top import block)
- AdminRebalanceSlotsResult {success,updatedOrders,message} → extends MessageResponse { updatedOrders: number[] } (c23)
- AdminUnit {id,shortNotation,fullName} → extends UnitSeedData { id: number } (c51)
- AdminSpecialDeal → NEW `export interface SpecialDealCore { quantity: string; price: string; validTill: Date }` + `AdminSpecialDeal extends SpecialDealCore { id: number; skuId: number }` (c10)
- AdminProductReview → NEW `export interface ProductReviewCore { id: number; reviewBody: string; ratings: number; reviewTime: Date; userName: string|null }` + `AdminProductReview extends ProductReviewCore { imageUrls: unknown; adminResponse: string|null; adminResponseImages: unknown }` (c22)
- AdminProductGroup {id,groupName,description,createdAt,products,productCount} → extends AdminProductGroupInfo { products: AdminSku[]; productCount: number } (c8)
- NEW SkuSummary { id: number; productId: number; productName: string; label: string; images: unknown; storeId: number|null; price: string } (after AdminSlotsProductIdsResult) (c43)
packages/shared/types/user.ts (clusters 4,10,22,29):
- './admin' import adds ProductReviewCore, SpecialDealCore; NEW import type { Complaint } from './complaint.types'
- UserComplaint {6 fields} → type UserComplaint = Pick<Complaint, 'id'|'complaintBody'|'response'|'isResolved'|'createdAt'|'orderId'> (c29; shape identical)
- NEW `export interface StoreProductCard { id; name; shortDescription: string|null; price: string; marketPrice: string|null; incrementStep: number; unit: string; unitNotation: string; images: string[]|null; isOutOfStock: boolean; productQuantity: number }` (c4)
- UserStoreProduct → type UserStoreProduct = Omit<StoreProductCard,'images'> & { images: string[] } (c4)
- UserStoreProductData → type UserStoreProductData = StoreProductCard (c4)
- UserProductSpecialDeal {quantity,price,validTill} → type UserProductSpecialDeal = SpecialDealCore (c10)
- UserProductReview {id,reviewBody,ratings,imageUrls:string[]|null,reviewTime,userName} → interface UserProductReview extends ProductReviewCore { imageUrls: string[] | null } (c22)
packages/shared/types/store.types.ts (clusters 47,48):
- + import type { IdName } from './primitives.types'; + import type { UserTagSummary } from './user' (type-only cycle, erased)
- StoreSummary {id,name,description} → interface StoreSummary extends IdName { description: string | null } (c47)
- StoreTag {id,tagName,productIds?} → type StoreTag = Pick<UserTagSummary,'id'|'tagName'> & { productIds?: number[] } (c48; productIds stays OPTIONAL)
packages/shared/types/app-common.types.ts (clusters 19,26,28,34,37,42):
+ AuthStateCore { isAuthenticated: boolean; isLoading: boolean; token: string | null } (c26)
+ AddressSelectionState { selectedAddressId: number|null; setSelectedAddressId: (addressId: number|null) => void } (c19)
+ NavigationState { isNavigatedFromHome: boolean; selectedStoreId: number|null; setNavigatedFromHome(value); setSelectedStoreId(id: number|null) } (c37)
+ FlashNavigationState { shouldNavigateToCart: boolean; setShouldNavigateToCart(value) } (c34)
+ QuickDeliveryState { isDrawerHidden: boolean; selectedSlotId: number|null; setDrawerHidden(hidden); setSelectedSlotId(id: number|null) } (c42)
+ CentralProductState<Product = unknown> { products: Product[]; productsById: Record<number,Product>; refetchProducts: (() => void | Promise<void>) | null; setProducts(products); setRefetchProducts(refetch) } (c28; refetch union accepts void AND Promise<void> to cover both apps)
packages/shared/types/index.ts: app-common comment line updated to list new types.
packages/db_helper_sqlite/src/stores/store-helpers.ts (c10):
- SpecialDealData {skuId,quantity,price,validTill} → interface SpecialDealData extends SpecialDealCore { skuId: number } (SpecialDealCore added to existing @packages/shared import; index.ts:318 re-export unaffected)
packages/db_helper_sqlite/src/user-apis/product.ts (c43):
- SkuSummary {7 fields} → import type { SkuSummary } from '@packages/shared' + export type { SkuSummary } (module export surface kept; getAllSkusSummary signature unchanged)
apps/admin-ui:
- dashboard-banners/index.tsx (c6): interface Banner → type Banner = Omit<SharedBanner,'createdAt'|'lastUpdated'> & { createdAt: string; lastUpdated: string }
- dashboard-banners/edit/[id].tsx (c6): interface Banner → Omit<SharedBanner,'description'|'skuIds'|'redirectUrl'|'serialNum'|'createdAt'|'lastUpdated'> & { description?; skuIds?; redirectUrl?; serialNum: number; createdAt: string; lastUpdated: string }
- components/ProductGroupForm.tsx (c8): ProductGroup → Omit<AdminProductGroup,'createdAt'|'products'> & { createdAt: string; products: any[] }
- manage-orders/orders/index.tsx (c20): OrderType → Omit<AdminOrderListItem,'customerName'|'customerMobile'|'items'|'createdAt'|'adminNotes'|'userNotes'|'userNegativityScore'> & { customerName: string|null; customerMobile?: string|null; items: OrderItemRow[]; createdAt: string; adminNotes?; userNotes?; userNegativityScore?; couponCode?; couponDescription?; discountAmount? } where OrderItemRow = Omit<AdminOrderListItemProduct,'id'|'skuName'|'features'|'isPackaged'|'isPackageVerified'> & { id?: number; isPackaged?: boolean; isPackageVerified?: boolean }
- components/SnippetOrdersView.tsx (c11+24): OrderProduct → Pick<AdminVendorSnippetOrderProduct,'productId'|'productName'|'quantity'|'price'|'unit'|'subtotal'>; SnippetOrder → Omit<AdminVendorSnippetOrderSummary,'totalAmount'|'slotInfo'|'products'> & { totalAmount: string; slotInfo: { time: string; sequence: any[] } | null; products: OrderProduct[] }
- components/StoreForm.tsx (c14): StoreFormData → Omit<CreateStoreInput,'description'> & { description: string; products: number[] }
- components/ProductsSelector.tsx (c43): SkuSummary → Omit<SharedSkuSummary,'images'>
- send-notifications/index.tsx (c55): User → Omit<UserMiniInfo,'name'> & { name: string|null; isEligibleForNotif: boolean }
- NEW types/drag-order-item.ts (c16): export interface DragOrderItemProps<T> { item: T; drag: () => void; isActive: boolean }
- customize-app/all-items-order.tsx (c16): ProductItemProps deleted → React.FC<DragOrderItemProps<Product>>
- customize-app/popular-items.tsx (c16+c4): ProductItemProps → interface extends DragOrderItemProps<PopularProduct> { onDelete: (id:number)=>void }; PopularProduct → Omit<StoreProductCard,'price'|'marketPrice'|'unitNotation'|'images'> & { price: number; marketPrice: number|null; images: string[]; storeId: number|null; nextDeliveryDate: string|null }
- product-tags/order.tsx (c16): TagItemProps deleted → React.FC<DragOrderItemProps<Tag>>
apps/user-ui:
- src/types/auth.ts (c17+53+26): User → Pick<SharedUser,'id'|'email'> & Partial<Omit<SharedUser,'id'|'email'|'createdAt'>> & { mobile: string|null; profileImage?: string|null; createdAt: string }; UserDetails → Pick<UserAuthProfile,'id'|'email'|'mobile'> & Partial<Omit<UserAuthProfile,'id'|'email'|'mobile'|'createdAt'>>; AuthState → interface extends AuthStateCore { user: User|null; userDetails: UserDetails|null }
- src/store/addressStore.ts (c19): interface AddressState deleted → create<AddressSelectionState>
- src/store/navigationStore.ts (c37): interface NavigationState deleted → create<NavigationState> (imported from shared)
- components/stores/flashNavigationStore.ts (c34): → type FlashNavigationState = FlashNavigationState (shared) & { reset: () => void }
- src/store/quickDeliveryStore.ts (c42): → type QuickDeliveryState = Omit<QuickDeliveryState(shared),'setDrawerHidden'> (user-ui store has no setDrawerHidden action)
- src/store/centralProductStore.ts (c28): interface CentralProductState → type CentralProductState = CentralProductState<Product> (shared generic; local Product = MergedProduct)
- hooks/cart-query-hooks.tsx (c36): interface LocalCartItem → type LocalCartItem = Omit<CartItem,'subtotal'>
- src/types/orders.ts (c56+57): OrderItem → Pick<UserOrderItemSummary,'productName'|'quantity'|'price'|'amount'|'image'>; Order → Omit<UserOrderSummary,'deliveryDate'|'items'|'discountAmount'> & { deliveryDate?: string; items: OrderItem[]; discountAmount?: number }
apps/web-ui:
- src/lib/auth-context.tsx (c17+26): User → Pick<SharedUser,'id'|'email'> & Partial<Omit<SharedUser,'id'|'email'|'createdAt'>> & { mobile: string|null; profileImage: string|null }; AuthState → interface extends AuthStateCore { user: User|null; userDetails: any }
- src/lib/stores/address-store.ts (c19): → type AddressState = AddressSelectionState & { clearSelectedAddress: () => void }
- src/lib/stores/navigation-store.ts (c37): interface NavigationStore → type NavigationStore = NavigationState (shared)
- src/lib/stores/flash-navigation-store.ts (c34): → type FlashNavigationStore = FlashNavigationState (shared)
- src/lib/stores/quick-delivery-store.ts (c42): → type QuickDeliveryStore = QuickDeliveryState (shared)
- src/lib/stores/central-product-store.ts (c28): interface CentralProductStore → type CentralProductStore = CentralProductState<Product> (shared generic, local Product)
- src/routes/me.coupons.tsx (c54): Coupon → Omit<UserCouponDisplay,'maxValue'|'minOrder'|'validTill'|'maxLimitForUser'> & { maxValue?: number; minOrder?: number; validTill?: string | Date; maxLimitForUser?: number }
[2026-09-05 14:05:00] COMPLETED all 29 🟢 "unify now" clusters from similar_types.md (clusters 4,6,8,10,11,14,16,17,19,20,22,23,24,26,28,29,34,36,37,42,43,47,48,51,53,54,55,56,57). 33 files touched (5 shared, 12 admin-ui incl. 1 new types/drag-order-item.ts, 8 user-ui, 7 web-ui, 2 db_helper_sqlite). Every replacement is shape-exact vs the local it replaced. One derivation fix during verification: user/web auth `User` originally intersected `Partial<Omit<SharedUser,...>>` with `{mobile: string|null}` — object intersection collapsed mobile to `string`; fixed by excluding 'mobile' from the Omit and overriding it directly.
Verification (tsc --noEmit error counts vs pre-change state): backend 11=11, web-ui 150=150, user-ui 106=106 (login.tsx lost 2 pre-existing errors, AuthContext regained symmetric — net 0), admin-ui 122=122, fallback-ui 107=107, db_helper_sqlite 4=4. Error-code distributions identical per project (line-shift only). db_helper_postgres: untouched. similar_types.md: 29 clusters marked ✅ done.
Remaining (documented, not in scope): 🟡 worth-doing (1,5,27,31,32,33,38,45,52,61), 🟣 shared-internal (2,7,21,46), ⚪ optional (25,30,35,39,44,50,59,60), 🔵 schema-bound/intentional (3,9,12,13,15,18,40,41,58).
2026-09-05 14:20:00 — Missed 🟢 cluster 49 (TagFormData ×2 in admin-ui) from the unify-now batch; implementing now so similar_types.md can drop ALL done clusters.
- apps/admin-ui/app/(drawer)/dashboard/product-tags/edit/index.tsx: local `interface TagFormData {…5 fields, existingImageUrl?}` → `type TagFormData = BaseTagFormData & { existingImageUrl?: string }` importing the canonical TagForm.tsx declaration (name alias import to avoid self-shadowing).
[2026-09-05 14:35:00] Pruned similar_types.md: all 30 implemented 🟢 clusters (29 batch + follow-up 49) removed. File now lists the 31 remaining candidates / 84 types (🟡 10 · 🟣 4 · ⚪ 8 · 🔵 9), original cluster IDs kept for change-log traceability; header, top-recommendations, summary table and counts rewritten. Verified: admin-ui tsc 122 = baseline after cluster-49 fix.
================================================================================
2026-09-05 15:00:00 — similar_types.md §B cluster 2 (Coupon family): reduce to TWO types — canonical `Coupon` (entity, skuIds) + `CouponFormInput = Omit<Coupon,'id'|'createdAt'>` (single form for create+update; complete data everywhere). Decisions: canonical field skuIds (API zod + admin form + sqlite schema already use it); db_helper_postgres stays UNTOUCHED (its local Create/UpdateCouponInput documented exception); update path sends complete data, helper strips createdBy from the DB write (authorship preserved).
packages/shared/types/coupon.types.ts:
- drifted Coupon {productIds: number[]|null} → canonical {skuIds: number[]|null} (all other 14 fields identical to admin.ts's shape)
+ export type CouponFormInput = Omit<Coupon, 'id' | 'createdAt'>
- CouponValidationResult.coupon?: Partial<Coupon> now references the canonical type (verified: no read-side consumers of `.coupon`)
packages/shared/types/admin.ts:
- local interface Coupon (lines ~16-31) deleted
- re-export line: export type { Coupon, CouponFormInput, CouponValidationResult, UserMiniInfo } from './coupon.types' (admin.ts has no other internal Coupon references — verified grep)
packages/db_helper_sqlite/src/admin-apis/coupon.ts:
- local CreateCouponInput + UpdateCouponInput deleted
- import adds CouponFormInput (from '@packages/shared')
- createCouponWithRelations(input: CouponFormInput, ...): insert mapping unchanged (field names match; previously-optional fields now arrive complete; NULL ≡ omitted for the nullable columns)
- updateCouponWithRelations(id, input: CouponFormInput, ...): const { createdBy: _createdBy, ...updateFields } = input; .set(updateFields) — createdBy stripped from write only
apps/backend/src/trpc/apis/admin-apis/apis/coupon.ts (only caller of both helpers; type-only + payload-completeness changes):
- create: call object completed — discountPercent/flatDiscount/minOrder/maxValue/maxLimitForUser get `?? null`, validTill `: null` when absent, isInvalidated: false added (DB result identical: all nullable columns; NULL ≡ omitted)
- update: updateData becomes a COMPLETE CouponFormInput by merging partial `updates` over the existing coupon via getCouponByIdFromDb(id) (already imported) — sent fields win, unsent fields keep current values (exclusiveApply/isInvalidated/createdBy from existing); net DB write identical to today's partial update
db_helper_postgres: untouched (user decision). admin-ui: zero changes (Formik→zod→backend flow untouched).
[2026-09-05 15:20:00] COMPLETED §B cluster 2 (Coupon family → 2 types). 4 files changed: coupon.types.ts (canonical Coupon w/ skuIds + CouponFormInput + CouponValidationResult.coupon re-anchored), admin.ts (local Coupon deleted; combined coupon re-export), sqlite admin-apis/coupon.ts (CouponFormInput signatures; update strips createdBy from write), backend admin coupon router (create sends complete payload incl. isInvalidated: false + nulls; update merges partial updates over existing coupon → complete CouponFormInput; getCouponByIdFromDb fetch added). pg untouched (45 = baseline); admin-ui untouched. Verification (tsc error counts vs pre-change): backend 11=11, web-ui 150=150, user-ui 106=106, admin-ui 122=122, fallback-ui 107=107, sqlite 4=4. similar_types.md: cluster 2 removed → 30 clusters / 78 types remain (🟡 10 · 🟣 3 · ⚪ 8 · 🔵 9).
================================================================================
2026-09-05 15:40:00 — similar_types.md §B cluster 7 (Admin order item trio, all in shared admin.ts): extract common base, chain with extends; all three names + shapes preserved exactly (contract intact).
packages/shared/types/admin.ts:
+ NEW `export interface AdminOrderItemCore { id: number; name: string; amount: number; isPackaged: boolean; isPackageVerified: boolean; skuName?: string | null; features?: { featureName: string | null; featureValue: string }[] }` (declared before AdminOrderDetailsItem)
- AdminOrderDetailsItem: common 7 fields deleted → extends AdminOrderItemCore { quantity: string; price: string; productSize: number; unit?: string }
- AdminSlotOrderItem: common 7 fields deleted → extends AdminOrderItemCore { quantity: number; price: number; unit: string }
- AdminOrderListItemProduct: common fields deleted → extends AdminSlotOrderItem { productSize: number } (Slot/List share numeric quantity+price; List adds productSize)
Consumers unchanged (backend getOrderDetails / slot-orders / orders-list + admin-ui derivations — shapes byte-equivalent). db_helper_postgres untouched.
[2026-09-05 15:50:00] COMPLETED §B cluster 7 (Admin order item trio). 1 file changed: packages/shared/types/admin.ts — NEW AdminOrderItemCore {id, name, amount, isPackaged, isPackageVerified, skuName?, features?}; AdminOrderDetailsItem extends it {quantity: string; price: string; productSize: number; unit?}; AdminSlotOrderItem extends it {quantity: number; price: number; unit: string}; AdminOrderListItemProduct extends AdminSlotOrderItem {productSize: number}. All names + shapes preserved (contract intact); backend identical tsc output. Verification (tsc error counts vs pre-change): backend 11=11, web-ui 150=150, user-ui 106=106, admin-ui 122=122, fallback-ui 107=107, sqlite 4=4, pg 45=45. similar_types.md: cluster 7 removed → 29 clusters / 75 types remain (🟡 10 · 🟣 2 · ⚪ 8 · 🔵 9).
================================================================================
2026-09-05 16:00:00 — FULL UNIFICATION PASS (remaining 29 clusters of similar_types.md). User rules: (1) add unneeded fields rather than fragment types, (2) make differing types identical + cast (numbers on inputs/cache; DB rows keep DB strings; images → string[]|null; relatedStores → number[]|null), (3) base types via extends. pg stays untouched → clusters 13/15/40/41 (pure pg↔sqlite pairs) = decided-skips. GROUP 1: packages/shared.
packages/shared/types/admin.ts:
- (c21) AdminOrderRow {16 fields} → type AdminOrderRow = Omit<PlacedOrder,'orderGroupId'|'orderGroupProportion'> & { adminNotes: string|null; orderGroupId: string|null; orderGroupProportion: string|null }; + import type { PlacedOrder } from './order.types' (shape byte-equal)
- (c9) NEW SkuFlagCore { name?: string|null; isOutOfStock?; isSuspended?; isFlashAvailable?; isOffer?; isComboOnly?; isDeleted? } (all optional); AdminSku → extends SkuFlagCore re-tightening name: string|null + all 6 flags required (shape unchanged)
- (c9/31) DEAD types replaced by the live sqlite contracts (verified zero consumers of the old shapes repo-wide):
- DELETE CreateSkuVariantInput (dead)
- NEW CreateComboItemInput { skuId: number } (was sqlite-local)
- CreateSkuInput (dead: skuCode/displayName/unitId/variants…) → live shape extends SkuFlagCore { price: number; marketPrice?: number|null; images?: string[]|null; flashPrice?: number|null; features: SkuFeatureLike[]; comboItems?: CreateComboItemInput[] }
- CreateProductInput (dead: storeId: number, no productType) → live shape { name: string; shortDescription?: string|null; longDescription?: string|null; storeId: number|null; incrementStep?: number; productType?: 'item'|'combo'; skus: CreateSkuInput[] }
- (c1) NEW ProductTagCore { tagName: string; tagDescription: string|null; imageUrl: string|null; isDashboardTag: boolean; relatedStores: number[]|null }; AdminProductTagInfo → extends ProductTagCore { id; sortOrder: number[]; createdAt: Date } (relatedStores unknown → number[]|null)
- (c5) AdminVendorSnippetCreateInput + AdminVendorSnippetUpdateInput (both dead, verified) → single export type AdminVendorSnippetInput = Omit<AdminVendorSnippet,'id'|'createdAt'>; dbService.ts re-export list updated (2 lines → 1)
packages/shared/types/staff-user.types.ts (c46):
- local StaffUser (staffRoleId: number, non-null — unreachable duplicate; barrel only exports StaffRole from this file) deleted → single source = admin.ts StaffUser (staffRoleId: number|null)
packages/shared/types/user.ts (c12/18/38/59/61/3):
- (c12) NEW ProductPriceFlags { price: number; marketPrice: number|null; flashPrice: string|null; isFlashAvailable: boolean; isOutOfStock: boolean; isSuspended: boolean } — MergedProduct (both apps) and sqlite AvailabilityCacheData anchor here
- (c18) NEW ProductDetailCore { id; productId; name; shortDescription: string|null; longDescription: string|null; price: string; marketPrice: string|null; unitNotation: string; images: string[]|null; isOutOfStock: boolean; incrementStep: number; productQuantity: number; isFlashAvailable: boolean; flashPrice: string|null; productType: string; isOffer?: boolean; isComboOnly?: boolean }; UserProductDetailData → extends ProductDetailCore { store: UserProductStoreInfo|null; deliverySlots: UserProductDeliverySlot[]; specialDeals: UserProductSpecialDeal[]; comboItems: UserProductComboItem[] } (isOffer/isComboOnly gained optional — rule 1)
- (c38) NEW ProductSummaryCore { id; name; price: string; marketPrice: string|null; unitNotation: string; images: string[]|null; isOutOfStock: boolean; incrementStep: number }
- (c59) UserSlotsWithProductsResponse → extends UserSlotsResponse { productAvailability: UserSlotAvailability[] }
- (c61) NEW UserStoreSampleProductCore<I> { id: number; images: I } + UserStoreSummaryCore<P> { id; name; description: string|null; productCount: number; sampleProducts: P[] }; UserStoreSampleProduct = Core<{signedImageUrl: string|null}>; UserStoreSampleProductData = Core<{images: string[]|null}>; UserStoreSummary extends SummaryCore<UserStoreSampleProduct> { signedImageUrl: string|null }; UserStoreSummaryData extends SummaryCore<UserStoreSampleProductData> { imageUrl: string|null }
- (c3) UserSlotWithProducts → extends UserProductDeliverySlot { products: UserSlotProduct[] }
packages/shared/types/app-common.types.ts (c25/30/39):
+ OrderDialogBaseProps { open: boolean; onClose: () => void; orderId: number }; ComplaintFormProps → extends it
+ AuthRedirectOptions { targetUrl: string; queryParams: Record<string, any> }
+ OrderCardActionProps<T> { order: T; onPress: () => void }
GROUP 2 (2026-09-05 16:10) — sqlite helpers + slots adopt shared bases (clusters 1,3,9,12,18,31,38,45,58):
packages/db_helper_sqlite/src/stores/store-helpers.ts:
- (c18) ProductBasicData {20 fields} → extends ProductDetailCore (shared) { skuName: string|null; storeId: number|null } + re-tightens isOffer/isComboOnly to required; images unknown → string[]|null via core; producer (getAllProductsForCache:129) images: sku.images → cast `as string[] | null`
- (c12) AvailabilityCacheData {7 fields} → extends ProductPriceFlags { id: number }; producer getAvailabilityForCache now writes NUMBERS: price: Number(stat.ourPrice ?? 0), marketPrice: stat.marketPrice != null ? Number(stat.marketPrice) : null, flashPrice unchanged string|null; downstream apps already Number()-cast (verified user-ui:143-144, web-ui:181-182) — no app changes
- (c58) ProductComboCacheData {8 fields} → extends UserProductComboItem { comboSkuId: number }; producer (getAllProductCombosForCache:242) images: ci.sku?.images → cast `as string[] | null`
- (c1) TagBasicData {7 fields} → extends ProductTagCore { id: number; sortOrder: number[] | null }; producer (:272) relatedStores: productTagInfo.relatedStores → cast `as number[] | null`
- (c3) DeliverySlotData {5 fields} → extends UserProductDeliverySlot { isCapacityFull: boolean; skuId: number }; SlotWithProductsData → extends UserProductDeliverySlot { isActive: boolean; isCapacityFull: boolean; products: Array<{…same inline…}> }; import adds UserProductDeliverySlot
packages/db_helper_sqlite/src/admin-apis/product.ts (c9/31/38):
- local CreateComboItemInput / CreateSkuInput / CreateProductInput deleted → imported from '@packages/shared' (createProduct's ?? null / ?? 'item' defaults unchanged)
- (c38) OffersPageProductData {8 fields} → extends ProductSummaryCore; producer mapOffersPageProduct images cast `as string[] | null`
packages/db_helper_sqlite/src/admin-apis/slots.ts (c45):
- local SlotSnippetInput {name, skuIds, validTill} deleted → import shared SlotSnippetInput {name; groupIds: number[]; skuIds: number[]; validTill: string} (groupIds unused in sqlite — rule 1)
packages/db_helper_postgres/src/admin-apis/product.ts: UNTOUCHED (pg-side tag input/sortOrder drift remains the documented exception).
GROUP 3 (2026-09-05 16:25) — component prop families (clusters 27, 33, 35):
packages/ui/src/components/dropdown.tsx (c27): NEW exported DropdownBaseProps { label: string; options: DropdownOption[]; error?: boolean; style?: any; placeholder?: string; disabled?: boolean; className?: string } (the truly common scaffold — value/onValueChange intentionally excluded because the two dropdowns declare incompatible value widths: string|number vs string|number|string[]|number[]); Props → extends DropdownBaseProps { value; onValueChange }
packages/ui/src/components/bottom-dropdown.tsx (c27+33): DropdownOption → interface extends shared DropdownOption (import alias BaseDropdownOption) + disabled?: boolean (public export kept); BottomDropdownProps → extends DropdownBaseProps + topLabel/value(wide)/onValueChange(wide)/multiple/triggerComponent/onSearch/testID
packages/web-components/src/components/image-gallery-with-delete.tsx (c35): ImageGalleryWithDeleteProps → extends ImageCarouselProps { onRemove: (uri: string) => void }
GROUP 4 (2026-09-05 16:35) — apps adopt the shared bases (clusters 12, 32, 38, 39, 44, 45, 50, 52, 60) + admin-ui ProductForm unification (cluster 9):
apps/user-ui/src/hooks/prominent-api-hooks.ts (c12): MergedProduct → BaseProduct & ProductPriceFlags & { isComboOnly?: boolean } (shared ProductPriceFlags imported; fields identical)
apps/web-ui/src/hooks/prominent-api-hooks.ts (c12): MergedProduct → BaseProduct & ProductPriceFlags
apps/user-ui/app/(drawer)/(tabs)/me/addresses/index.tsx (c52): local Address {12 fields} → type Address = Pick<UserAddress, 'id'|'name'|'phone'|'addressLine1'|'addressLine2'|'city'|'state'|'pincode'|'isDefault'|'latitude'|'longitude'|'googleMapsUrl'>
apps/user-ui/hooks/cart-query-hooks.tsx (c38): ProductSummary → extends ProductSummaryCore { isFlashAvailable: boolean; flashPrice: string|null; productQuantity: number } (images string[] → string[]|null via core)
apps/user-ui/components/NextOrderGlimpse.tsx (c39): OrderCardProps → extends OrderCardActionProps<Order> { supportMobile: string|null }
apps/admin-ui/app/(drawer)/dashboard/user-management/[id].tsx (c39): OrderItemProps → extends OrderCardActionProps<Order>
apps/user-ui/components/SlotSpecificView.tsx (c44): SlotLayoutProps → extends SlotProductsProps { isForFlashDelivery: boolean }
apps/user-ui/app/(drawer)/(tabs)/home/index.tsx (c50): ListHeaderProps → extends TagSceneProps { onSelectTag: (id:number)=>void; storesData: any; sortedSlots: any[] }
apps/web-ui/src/components/Dialog.tsx (c32): local DialogProps deleted → import shared DialogProps (now carries title?)
apps/admin-ui/app/(drawer)/dashboard/product-tags/order.tsx (c1): TagItemData? no — admin-ui TagItemData lives in product-tags/index.tsx → extends ProductTagCore { id; createdAt: string|Date }
apps/admin-ui/app/(drawer)/dashboard/customize-app/all-items-order.tsx (c60): Product → Pick<StoreProductCard,'id'|'name'|'images'|'isOutOfStock'> (images string[] → string[]|null)
apps/admin-ui/components/SlotForm.tsx (c45): local VendorSnippet → type VendorSnippet = SlotSnippetInput (shared; validTill string → string|null)
apps/admin-ui/src/components/ProductForm.tsx + products/add.tsx + products/edit.tsx (c9): Variant → Omit<CreateSkuInput,'features'|'comboItems'|'flashPrice'|'marketPrice'|'name'> & { name: string; marketPrice: number; flashPrice: number|null; features: Attribute[]; comboItems: CreateComboItemInput[] }; attributes→features rename across the form; price/marketPrice/flashPrice become numbers (TextInput display casts via String(... ?? ''), combo picker pushes {skuId: 0} + Number(val) on select); add/edit screens drop their parseFloat boundary casts (values already numeric)
[2026-09-05 17:30:00] COMPLETED the FINAL unification pass — all 25 remaining actionable clusters (1,3,5,9,12,18,21,25,27,30,31,32,33,35,38,39,44,45,46,50,52,58,59,60,61). ~30 files across shared/admin.ts+user.ts+app-common, sqlite helpers/product/slots, backend (slot-store, product-store, product-tag-store, admin coupon/slots/product trpc), packages/ui (dropdowns), web-components, user-ui, web-ui, admin-ui (incl. ProductForm numeric/features unification). Notable standardizations per user rules: availability cache now writes NUMERIC prices (apps already Number()-cast — zero app changes); SKU/form prices numeric end-to-end (ProductForm casts at display, add/edit drop parseFloat); form field attributes→features renamed; images → string[]|null everywhere; relatedStores → number[]|null (trpc getProductTags annotation updated); vendor-snippet input single-sourced with wire (string) validTill; SlotSnippetInput shared with groupIds (unused in sqlite); dead shared CreateProductInput/CreateSkuInput/CreateSkuVariantInput/AdminVendorSnippet{Create,Update}Input/staff-user StaffUser deleted and replaced by live-shape single sources. Decided-skips (13,15,40,41) documented in similar_types.md (4 clusters / 8+1 detail remain — 13, 15, 40, 41).
FINAL VERIFICATION (tsc error counts vs baselines): backend 11=11, admin-ui 122=122, user-ui 106=106, web-ui 150=150, fallback-ui 107=107, db_helper_sqlite 4=4, db_helper_postgres 45=45. All diffs are line-number/message-render shifts of pre-existing errors only.
================================================================================
2026-09-05 17:45:00 — Add root typecheck script (runs every package/app with a tsconfig).
[NEW FILE] ./typecheck (executable bash): iterates apps/backend, apps/user-ui, apps/admin-ui, apps/web-ui, apps/fallback-ui, packages/db_helper_sqlite, packages/db_helper_postgres, packages/migrator, packages/web-components — runs `npx tsc --noEmit` per target, prints ✅/❌ with error counts, prints first errors on failure, exits non-zero if any target fails. Targets without a tsconfig (packages/shared, packages/ui) are typechecked transitively via consumers.
packages/web-components/tsconfig.json:
- removed "declaration": true, "declarationMap": true, "outDir": "./dist", "rootDir": "./src" — the package is consumed as source (main: ./src/index.ts) and never emitted standalone; with the unified @packages/shared type imports now in its sources, declaration emit forced TS6059 rootDir violations (all 21 standalone errors were exactly this). Checking-only tsconfig → 0 errors.
package.json (root): + "typecheck": "bash typecheck"
[2026-09-05 17:55:00] VERIFIED ./typecheck: all 9 targets report exactly their known baselines — backend 11, user-ui 106, admin-ui 122, web-ui 150, fallback-ui 107, db_helper_sqlite 4, db_helper_postgres 45, migrator 0 ✅, web-components 3 (the pre-existing `<p weight>` custom-prop errors; standalone count went 21 → 3 after removing the declaration-emit settings). Script exits 1 while any target fails; run via `./typecheck` or `npm run typecheck` / `bun run typecheck`.

View file

@ -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",

View file

@ -1,7 +1,7 @@
import { db } from '../db/db_index'
import { coupons, reservedCoupons, users, orders, orderStatus, couponApplicableUsers, couponApplicableProducts } from '../db/schema'
import { eq, and, like, or, inArray, lt, desc, asc } from 'drizzle-orm'
import type { Coupon, CouponValidationResult, UserMiniInfo } from '@packages/shared'
import type { Coupon, CouponFormInput, CouponValidationResult, UserMiniInfo } from '@packages/shared'
export async function getAllCoupons(
cursor?: number,
@ -71,23 +71,8 @@ export async function getCouponById(id: number): Promise<any | null> {
})
}
export interface CreateCouponInput {
couponCode: string
isUserBased: boolean
discountPercent?: string
flatDiscount?: string
minOrder?: string
skuIds?: number[] | null
maxValue?: string
isApplyForAll: boolean
validTill?: Date
maxLimitForUser?: number
exclusiveApply: boolean
createdBy: number
}
export async function createCouponWithRelations(
input: CreateCouponInput,
input: CouponFormInput,
applicableUsers?: number[],
applicableProducts?: number[]
): Promise<Coupon> {
@ -129,31 +114,19 @@ export async function createCouponWithRelations(
})
}
export interface UpdateCouponInput {
couponCode?: string
isUserBased?: boolean
discountPercent?: string
flatDiscount?: string
minOrder?: string
skuIds?: number[] | null
maxValue?: string
isApplyForAll?: boolean
validTill?: Date | null
maxLimitForUser?: number
exclusiveApply?: boolean
isInvalidated?: boolean
}
export async function updateCouponWithRelations(
id: number,
input: UpdateCouponInput,
input: CouponFormInput,
applicableUsers?: number[],
applicableProducts?: number[]
): Promise<Coupon> {
return await db.transaction(async (tx) => {
// createdBy is always sent (complete payload) but never rewritten on update
const { createdBy: _createdBy, ...updateFields } = input
const [coupon] = await tx.update(coupons)
.set({
...input,
...updateFields,
})
.where(eq(coupons.id, id))
.returning()

View file

@ -58,6 +58,10 @@ import type {
AdminSpecialDeal,
AdminUnit,
AdminUpdateSlotProductsResult,
CreateComboItemInput,
CreateProductInput,
CreateSkuInput,
ProductTagCore,
SkuFeatureLike,
Store,
} from '@packages/shared'
@ -66,36 +70,6 @@ type ProductRow = InferSelectModel<typeof productInfo>
type SkuRow = InferSelectModel<typeof productSkus>
type MarketStatsRow = InferSelectModel<typeof productMarketStats>
type SkuFeatureRow = InferSelectModel<typeof skuFeatures>
interface CreateComboItemInput {
skuId: number
}
interface CreateSkuInput {
name?: string | null
price: number
marketPrice?: number | null
images?: string[] | null
isFlashAvailable?: boolean
flashPrice?: number | null
isOutOfStock?: boolean
isSuspended?: boolean
isOffer?: boolean
isComboOnly?: boolean
isDeleted?: boolean
features: SkuFeatureLike[]
comboItems?: CreateComboItemInput[]
}
interface CreateProductInput {
name: string
shortDescription?: string | null
longDescription?: string | null
storeId?: number | null
incrementStep?: number
productType?: 'item' | 'combo'
skus: CreateSkuInput[]
}
type StoreRow = InferSelectModel<typeof storeInfo>
type SpecialDealRow = InferSelectModel<typeof specialDeals>
type ProductTagInfoRow = InferSelectModel<typeof productTagInfo>
@ -677,12 +651,7 @@ export async function getProductTagInfoById(tagId: number): Promise<AdminProduct
return mapTagInfo(tag)
}
export interface CreateProductTagInput {
tagName: string
tagDescription?: string | null
imageUrl?: string | null
isDashboardTag?: boolean
relatedStores?: number[]
export interface CreateProductTagInput extends ProductTagCore {
sortOrder?: number[]
}
@ -733,12 +702,8 @@ export async function getProductTagById(tagId: number): Promise<AdminProductTagW
}
}
export interface UpdateProductTagInput {
export type UpdateProductTagInput = Omit<ProductTagCore, 'tagName'> & {
tagName?: string
tagDescription?: string | null
imageUrl?: string | null
isDashboardTag?: boolean
relatedStores?: number[]
sortOrder?: number[]
}

View file

@ -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

View file

@ -17,7 +17,7 @@ import {
} from '../db/schema'
import { eq, and, gt, sql, isNotNull, asc } from 'drizzle-orm'
import { composeSkuName, composeUnitNotation } from '../lib/sku-features'
import type { ProductTagData, TagProductMapping, UserNegativityData, StoreSummary, BannerData } from '@packages/shared'
import type { ProductTagData, TagProductMapping, UserNegativityData, StoreSummary, BannerData, SpecialDealCore, ProductDetailCore, ProductPriceFlags, ProductTagCore, UserProductDeliverySlot, UserProductComboItem } from '@packages/shared'
// Re-exported so the package index (`export { ... } from './src/stores/store-helpers'`)
// keeps resolving to the single shared source.
@ -41,51 +41,24 @@ export async function getAllBannersForCache(): Promise<BannerData[]> {
// PRODUCT STORE HELPERS
// ============================================================================
export interface ProductBasicData {
id: number
productId: number
name: string
export interface ProductBasicData extends ProductDetailCore {
skuName: string | null
shortDescription: string | null
longDescription: string | null
price: string
marketPrice: string | null
images: unknown
isOutOfStock: boolean
storeId: number | null
unitNotation: string
incrementStep: number
productQuantity: number
isFlashAvailable: boolean
flashPrice: string | null
productType: string
isComboOnly: boolean
isOffer: boolean
isComboOnly: boolean
}
export interface AvailabilityCacheData {
export interface AvailabilityCacheData extends ProductPriceFlags {
id: number
price: string
marketPrice: string | null
flashPrice: string | null
isFlashAvailable: boolean
isOutOfStock: boolean
isSuspended: boolean
}
export interface DeliverySlotData {
export interface DeliverySlotData extends UserProductDeliverySlot {
skuId: number
id: number
deliveryTime: Date
freezeTime: Date
isCapacityFull: boolean
}
export interface SpecialDealData {
export interface SpecialDealData extends SpecialDealCore {
skuId: number
quantity: string
price: string
validTill: Date
}
export async function getAllSpecialDealsForCache(): Promise<SpecialDealData[]> {
@ -129,7 +102,7 @@ export async function getAllProductsForCache(): Promise<ProductBasicData[]> {
longDescription: sku.product?.longDescription ?? null,
price: marketStats?.ourPrice ? String(marketStats.ourPrice) : '0',
marketPrice: marketStats?.marketPrice ? String(marketStats.marketPrice) : null,
images: sku.images,
images: sku.images as string[] | null,
isOutOfStock: marketStats?.isOutOfStock ?? false,
storeId: sku.product?.storeId ?? null,
unitNotation: composeUnitNotation(features),
@ -153,8 +126,8 @@ export async function getAvailabilityForCache(): Promise<AvailabilityCacheData[]
.filter((stat) => !stat.isSuspended && !stat.sku?.isDeleted)
.map((stat) => ({
id: stat.skuId,
price: stat.ourPrice ? String(stat.ourPrice) : '0',
marketPrice: stat.marketPrice ? String(stat.marketPrice) : null,
price: Number(stat.ourPrice ?? 0),
marketPrice: stat.marketPrice != null ? Number(stat.marketPrice) : null,
flashPrice: stat.flashPrice ? String(stat.flashPrice) : null,
isFlashAvailable: stat.isFlashAvailable,
isOutOfStock: stat.isOutOfStock,
@ -208,15 +181,8 @@ export async function getAllProductTagsForCache(): Promise<ProductTagData[]> {
// PRODUCT COMBO STORE HELPERS
// ============================================================================
export interface ProductComboCacheData {
export interface ProductComboCacheData extends UserProductComboItem {
comboSkuId: number
skuId: number
productName: string
skuName: string | null
images: unknown
unitNotation: string
price: string
isOffer: boolean
}
export async function getAllProductCombosForCache(): Promise<ProductComboCacheData[]> {
@ -242,7 +208,7 @@ export async function getAllProductCombosForCache(): Promise<ProductComboCacheDa
skuId: ci.skuId,
productName: composeSkuName(ci.sku?.product?.name ?? 'Unknown', features, ci.sku?.name),
skuName: ci.sku?.name ?? null,
images: ci.sku?.images,
images: (ci.sku?.images ?? null) as string[] | null,
unitNotation: composeUnitNotation(features),
price: ci.sku?.marketStats?.ourPrice ? String(ci.sku.marketStats.ourPrice) : '0',
isOffer: ci.sku?.isOffer ?? false,
@ -254,13 +220,11 @@ export async function getAllProductCombosForCache(): Promise<ProductComboCacheDa
// PRODUCT TAG STORE HELPERS
// ============================================================================
export interface TagBasicData {
export interface TagBasicData extends ProductTagCore {
id: number
tagName: string
tagDescription: string | null
imageUrl: string | null
isDashboardTag: boolean
relatedStores: unknown
sortOrder: number[] | null
}
@ -291,10 +255,7 @@ export async function getAllTagProductMappings(): Promise<TagProductMapping[]> {
// SLOT STORE HELPERS
// ============================================================================
export interface SlotWithProductsData {
id: number
deliveryTime: Date
freezeTime: Date
export interface SlotWithProductsData extends UserProductDeliverySlot {
isActive: boolean
isCapacityFull: boolean
products: Array<{
@ -380,7 +341,7 @@ export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProduct
name: sku.product.store.name,
description: sku.product.store.description
} : null,
images: sku.images,
images: sku.images as string[] | null,
isOutOfStock: marketStats?.isOutOfStock ?? false,
storeId: sku.product?.storeId ?? null,
isFlashAvailable: marketStats?.isFlashAvailable ?? false,

View file

@ -301,15 +301,9 @@ export async function getNextDeliveryDateWithCapacity(skuId: number): Promise<Da
return null
}
export interface SkuSummary {
id: number
productId: number
productName: string
label: string
images: unknown
storeId: number | null
price: string
}
// Single source in @packages/shared (admin.ts); re-exported for module compat.
import type { ProductSummaryCore, SkuSummary } from '@packages/shared'
export type { SkuSummary } from '@packages/shared'
export async function getAllSkusSummary(): Promise<SkuSummary[]> {
const skus = await db.query.productSkus.findMany({
@ -339,15 +333,7 @@ export async function getAllSkusSummary(): Promise<SkuSummary[]> {
})
}
export interface OffersPageProductData {
id: number
name: string
price: string
marketPrice: string | null
unitNotation: string
images: unknown
isOutOfStock: boolean
incrementStep: number
export interface OffersPageProductData extends ProductSummaryCore {
}
export interface OffersPageData {
@ -373,7 +359,7 @@ const mapOffersPageProduct = (sku: {
price: sku.marketStats?.ourPrice ? String(sku.marketStats.ourPrice) : '0',
marketPrice: sku.marketStats?.marketPrice ? String(sku.marketStats.marketPrice) : null,
unitNotation: composeUnitNotation(features),
images: sku.images,
images: (sku.images ?? null) as string[] | null,
isOutOfStock: sku.marketStats?.isOutOfStock ?? false,
incrementStep: sku.product?.incrementStep ?? 1,
}

View file

@ -3,33 +3,19 @@
// so existing `@packages/shared` consumers keep working from one surface.)
import type { MessageResponse, BasicSuccessResponse, IdName } from './primitives.types'
import type { UnitSeedData } from './seed.types'
import type { PlacedOrder } from './order.types'
export type { Banner } from './banner.types'
export type { Complaint, ComplaintWithUser } from './complaint.types'
export type { Constant, ConstantUpdateResult, ConstValueType } from './const.types'
export type { CouponValidationResult, UserMiniInfo } from './coupon.types'
export type { StaffRole } from './staff-user.types'
export type { MessageResponse, IdName } from './primitives.types'
// NOTE: Coupon and StaffUser intentionally keep LOCAL declarations below —
// the dedicated files drifted (productIds vs skuIds; staffRoleId nullable vs
// not) and sqlite consumers depend on these exact shapes.
export interface Coupon {
id: number;
couponCode: string;
isUserBased: boolean;
discountPercent: string | null;
flatDiscount: string | null;
minOrder: string | null;
skuIds: number[] | null;
maxValue: string | null;
isApplyForAll: boolean;
validTill: Date | null;
maxLimitForUser: number | null;
exclusiveApply: boolean;
isInvalidated: boolean;
createdAt: Date;
createdBy: number;
}
// NOTE: Coupon + CouponFormInput are single-sourced in coupon.types.ts
// (canonical skuIds shape); StaffUser intentionally keeps its LOCAL declaration
// below — the dedicated staff-user.types.ts copy drifted (staffRoleId nullable
// vs not) and sqlite consumers depend on these exact shapes.
export type { Coupon, CouponFormInput, CouponValidationResult, UserMiniInfo } from './coupon.types'
export interface StaffUser {
id: number;
@ -49,23 +35,12 @@ export interface Store {
// updatedAt: Date;
}
export interface AdminOrderRow {
id: number;
userId: number;
addressId: number;
slotId: number | null;
isCod: boolean;
isOnlinePayment: boolean;
paymentInfoId: number | null;
totalAmount: string;
deliveryCharge: string;
readableId: number;
adminNotes: string | null;
userNotes: string | null;
orderGroupId: string | null;
orderGroupProportion: string | null;
isFlashDelivery: boolean;
createdAt: Date;
// Order row — anchored to the shared PlacedOrder shape (single source); the
// admin variant widens the group fields to nullable and adds adminNotes.
export type AdminOrderRow = Omit<PlacedOrder, 'orderGroupId' | 'orderGroupProportion'> & {
adminNotes: string | null
orderGroupId: string | null
orderGroupProportion: string | null
}
export type PaymentStatus = 'pending' | 'success' | 'cod' | 'failed'
@ -115,13 +90,11 @@ export interface AdminOrderDetailsSlotInfo {
sequence: unknown;
}
export interface AdminOrderDetailsItem {
// Common scaffolding for the three admin order-item views. quantity/price stay
// per-view (Details: string, Slot/List: number) since they mirror different queries.
export interface AdminOrderItemCore {
id: number;
name: string;
quantity: string;
productSize: number;
price: string;
unit?: string;
amount: number;
isPackaged: boolean;
isPackageVerified: boolean;
@ -129,6 +102,13 @@ export interface AdminOrderDetailsItem {
features?: { featureName: string | null; featureValue: string }[];
}
export interface AdminOrderDetailsItem extends AdminOrderItemCore {
quantity: string;
price: string;
productSize: number;
unit?: string;
}
export interface AdminOrderDetailsPayment {
status: string;
gateway: string;
@ -191,17 +171,10 @@ export type AdminOrderMessageResult = MessageResponse
export type AdminOrderBasicResult = BasicSuccessResponse
export interface AdminSlotOrderItem {
id: number;
name: string;
export interface AdminSlotOrderItem extends AdminOrderItemCore {
quantity: number;
price: number;
amount: number;
unit: string;
isPackaged: boolean;
isPackageVerified: boolean;
skuName?: string | null;
features?: { featureName: string | null; featureValue: string }[];
}
export interface AdminSlotOrder {
@ -231,18 +204,8 @@ export interface AdminGetSlotOrdersResult {
data: AdminSlotOrder[];
}
export interface AdminOrderListItemProduct {
id: number;
name: string;
quantity: number;
price: number;
amount: number;
unit: string;
export interface AdminOrderListItemProduct extends AdminSlotOrderItem {
productSize: number;
isPackaged: boolean;
isPackageVerified: boolean;
skuName?: string | null;
features?: { featureName: string | null; featureValue: string }[];
}
export interface AdminOrderListItem {
@ -284,10 +247,8 @@ export interface AdminGetAllOrdersResultWithUserId {
nextCursor?: number;
}
export interface AdminRebalanceSlotsResult {
success: boolean;
export interface AdminRebalanceSlotsResult extends MessageResponse {
updatedOrders: number[];
message: string;
}
export type AdminCancelOrderError =
@ -304,10 +265,8 @@ export interface AdminCancelOrderResult {
error?: AdminCancelOrderError;
}
export interface AdminUnit {
export interface AdminUnit extends UnitSeedData {
id: number;
shortNotation: string;
fullName: string;
}
export interface AdminSkuFeature {
@ -333,7 +292,19 @@ export interface AdminProductComboItem {
price: string;
}
export interface AdminSku {
// Flag scaffold shared by the SKU row (AdminSku), the insert input
// (CreateSkuInput) and the admin product-form variant state.
export interface SkuFlagCore {
name?: string | null
isOutOfStock?: boolean
isSuspended?: boolean
isFlashAvailable?: boolean
isOffer?: boolean
isComboOnly?: boolean
isDeleted?: boolean
}
export interface AdminSku extends SkuFlagCore {
id: number
productId: number
name: string | null
@ -369,50 +340,46 @@ export interface AdminProductWithRelations extends AdminProduct {
skus: AdminSku[];
}
export interface CreateSkuVariantInput {
name: string
value: string
unitId?: number | null
sortOrder?: number
export interface CreateComboItemInput {
skuId: number
}
export interface CreateSkuInput {
skuCode?: string | null
displayName: string
unitId: number
productQuantity?: number
incrementStep?: number
price: number | string
marketPrice?: number | string | null
export interface CreateSkuInput extends SkuFlagCore {
price: number
marketPrice?: number | null
images?: string[] | null
isOutOfStock?: boolean
isSuspended?: boolean
isDeleted?: boolean
isFlashAvailable?: boolean
flashPrice?: number | string | null
isOffer?: boolean
isComboOnly?: boolean
sortOrder?: number
isDefault?: boolean
variants?: CreateSkuVariantInput[]
flashPrice?: number | null
features: SkuFeatureLike[]
comboItems?: CreateComboItemInput[]
}
export interface CreateProductInput {
name: string
shortDescription?: string | null
longDescription?: string | null
storeId: number
storeId: number | null
incrementStep?: number
productType?: 'item' | 'combo'
skus: CreateSkuInput[]
}
export interface AdminProductTagInfo {
// Tag core — shared by the cache row (TagBasicData), the create/update inputs,
// the admin tag-info row, the backend store tag and the admin-ui list item.
// Non-key fields are optional here; row-shaped variants re-tighten them.
export interface ProductTagCore {
tagName: string
tagDescription?: string | null
imageUrl?: string | null
isDashboardTag?: boolean
relatedStores?: number[] | null
}
export interface AdminProductTagInfo extends ProductTagCore {
id: number;
tagName: string;
tagDescription: string | null;
imageUrl: string | null;
isDashboardTag: boolean;
relatedStores: unknown;
relatedStores: number[] | null;
sortOrder: number[];
createdAt: Date;
}
@ -429,14 +396,19 @@ export interface AdminProductTagWithProducts extends AdminProductTagInfo {
productIds: number[];
}
export interface AdminSpecialDeal {
id: number;
skuId: number;
// Special-deal chain — UserProductSpecialDeal (user.ts) and sqlite SpecialDealData
// extend this core; AdminSpecialDeal adds the row fields.
export interface SpecialDealCore {
quantity: string;
price: string;
validTill: Date;
}
export interface AdminSpecialDeal extends SpecialDealCore {
id: number;
skuId: number;
}
export interface AdminProductWithDetails extends AdminProduct {
store: Store | null;
skus: AdminSku[];
@ -465,15 +437,32 @@ export interface AdminUpdateSlotProductsResult {
export type AdminSlotsProductIdsResult = Record<number, number[]>;
export interface AdminProductReview {
// SKU summary row — sqlite user-apis product.ts + admin-ui ProductsSelector
// single source (admin-ui drops images; the helper keeps it unknown).
export interface SkuSummary {
id: number;
productId: number;
productName: string;
label: string;
images: unknown;
storeId: number | null;
price: string;
}
// Review core — shared between the admin row (adds response fields) and the
// user-facing review (user.ts adds typed imageUrls).
export interface ProductReviewCore {
id: number;
reviewBody: string;
ratings: number;
imageUrls: unknown;
reviewTime: Date;
userName: string | null;
}
export interface AdminProductReview extends ProductReviewCore {
imageUrls: unknown;
adminResponse: string | null;
adminResponseImages: unknown;
userName: string | null;
}
export interface AdminProductReviewWithSignedUrls extends AdminProductReview {
@ -491,11 +480,7 @@ export interface AdminProductReviewResponse {
review: AdminProductReview;
}
export interface AdminProductGroup {
id: number;
groupName: string;
description: string | null;
createdAt: Date;
export interface AdminProductGroup extends AdminProductGroupInfo {
products: AdminSku[];
productCount: number;
}
@ -626,20 +611,20 @@ export interface AdminUpdateSlotCapacityResult {
message: string;
}
export interface AdminVendorSnippetCreateInput {
snippetCode: string;
slotId?: number;
skuIds: number[];
validTill?: string;
isPermanent: boolean;
// Single complete form input for vendor-snippet create + update — callers
// always send every field (no partial shapes). Derived from the row; validTill
// is the wire type (ISO string) — the row keeps Date.
export type AdminVendorSnippetInput = Omit<AdminVendorSnippet, 'id' | 'createdAt' | 'validTill'> & {
validTill: string | null
}
export interface AdminVendorSnippetUpdateInput {
snippetCode?: string;
slotId?: number;
skuIds?: number[];
validTill?: string | null;
isPermanent?: boolean;
// Slot-level snippet input (group-model) — sqlite carries skuIds and leaves
// groupIds unused; the admin-ui SlotForm uses both.
export interface SlotSnippetInput {
name: string
groupIds: number[]
skuIds: number[]
validTill: string | null
}
export interface AdminVendorSnippetDeleteResult {

View file

@ -5,16 +5,34 @@
*/
// --- Forms / shared UI ---
// Dialog anchored to a specific order (complaint form, cancel-order dialog)
export interface OrderDialogBaseProps {
open: boolean
onClose: () => void
orderId: number
}
// Auth redirect plumbing (route guard options ↔ redirect state); the state
// variant re-tightens both fields to required.
export interface AuthRedirectOptions {
targetUrl?: string
queryParams?: Record<string, any>
}
// Card that shows one order with a press action (user-ui glimpse, admin-ui history)
export interface OrderCardActionProps<T> {
order: T
onPress: () => void
}
export interface LoginFormInputs {
mobile: string
otp?: string
password?: string
}
export interface ComplaintFormProps {
open: boolean
onClose: () => void
orderId: number
export interface ComplaintFormProps extends OrderDialogBaseProps {
}
// Address form props — identical in user-ui + web-ui AddressForm components
@ -89,3 +107,44 @@ export interface StoreHeaderState {
title: string
setTitle: (title: string) => void
}
// --- Auth shell (identical user-ui ↔ web-ui; apps add their own user/userDetails) ---
export interface AuthStateCore {
isAuthenticated: boolean
isLoading: boolean
token: string | null
}
// --- Zustand store shapes duplicated across user-ui ↔ web-ui ---
export interface AddressSelectionState {
selectedAddressId: number | null
setSelectedAddressId: (addressId: number | null) => void
}
export interface NavigationState {
isNavigatedFromHome: boolean
selectedStoreId: number | null
setNavigatedFromHome: (value: boolean) => void
setSelectedStoreId: (id: number | null) => void
}
export interface FlashNavigationState {
shouldNavigateToCart: boolean
setShouldNavigateToCart: (value: boolean) => void
}
export interface QuickDeliveryState {
isDrawerHidden: boolean
selectedSlotId: number | null
setDrawerHidden: (hidden: boolean) => void
setSelectedSlotId: (id: number | null) => void
}
// Central product cache — element type differs per app (MergedProduct vs local Product)
export interface CentralProductState<Product = unknown> {
products: Product[]
productsById: Record<number, Product>
refetchProducts: (() => void | Promise<void>) | null
setProducts: (products: Product[]) => void
setRefetchProducts: (refetch: (() => void | Promise<void>) | null) => void
}

View file

@ -3,6 +3,9 @@
* Central type definitions for coupon-related data structures
*/
// Canonical coupon row — `skuIds` is the canonical reserved-items field
// (matches the API schema, admin form and sqlite schema `sku_ids`;
// postgres maps it to its product_ids column at the boundary).
export interface Coupon {
id: number;
couponCode: string;
@ -10,7 +13,7 @@ export interface Coupon {
discountPercent: string | null;
flatDiscount: string | null;
minOrder: string | null;
productIds: number[] | null;
skuIds: number[] | null;
maxValue: string | null;
isApplyForAll: boolean;
validTill: Date | null;
@ -21,6 +24,10 @@ export interface Coupon {
createdBy: number;
}
// Complete form payload for BOTH create and update — callers always send
// every field (create sends isInvalidated: false, update re-sends createdBy).
export type CouponFormInput = Omit<Coupon, 'id' | 'createdAt'>
export interface CouponValidationResult {
valid: boolean;
message?: string;

View file

@ -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

View file

@ -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;

View file

@ -4,13 +4,14 @@
* Note: Store interface is defined in admin.ts to avoid duplication
*/
import type { IdName } from './primitives.types'
import type { UserTagSummary } from './user'
/**
* Store summary for dropdowns/forms
* Minimal data for store selection UI
*/
export interface StoreSummary {
id: number;
name: string;
export interface StoreSummary extends IdName {
description: string | null;
}
@ -24,12 +25,9 @@ export interface StoresSummaryResponse {
/**
* Lightweight tag row used on store-detail pages (user-ui + web-ui).
* Named StoreTag to avoid clashing with the richer backend Tag shape.
* Derived from UserTagSummary so id/tagName stay in lockstep.
*/
export interface StoreTag {
id: number;
tagName: string;
productIds?: number[];
}
export type StoreTag = Pick<UserTagSummary, 'id' | 'tagName'> & { productIds?: number[] }
/**
* Chip props for tag selection on store-detail pages.

View file

@ -1,7 +1,8 @@
// User-related types
import type { MessageResponse, BasicSuccessResponse } from './primitives.types'
import type { AdminDeliverySlot } from './admin'
import type { AdminDeliverySlot, ProductReviewCore, SpecialDealCore } from './admin'
import type { Complaint } from './complaint.types'
import type { Banner } from './banner.types'
@ -125,14 +126,10 @@ export interface UserBannersResponse {
banners: UserBanner[];
}
export interface UserComplaint {
id: number;
complaintBody: string;
response: string | null;
isResolved: boolean;
createdAt: Date;
orderId: number | null;
}
export type UserComplaint = Pick<
Complaint,
'id' | 'complaintBody' | 'response' | 'isResolved' | 'createdAt' | 'orderId'
>
export interface UserComplaintsResponse {
complaints: UserComplaint[];
@ -148,55 +145,42 @@ export interface UserTagSummary {
productIds: number[];
}
export interface UserStoreSampleProduct {
// Store-sample-product cores — signed (authed API) vs plain (cache) variants
// share id/name; only the image carrier differs.
export interface UserStoreSampleProductCore {
id: number;
name: string;
signedImageUrl: string | null;
}
export interface UserStoreSampleProductData {
id: number;
name: string;
images: string[] | null;
}
export type UserStoreSampleProduct = UserStoreSampleProductCore & { signedImageUrl: string | null }
export interface UserStoreSummary {
export type UserStoreSampleProductData = UserStoreSampleProductCore & { images: string[] | null }
// Store-summary cores — same split (signedImageUrl vs imageUrl) over shared
// id/name/description/count/sampleProducts.
export interface UserStoreSummaryCore<P> {
id: number;
name: string;
description: string | null;
signedImageUrl: string | null;
productCount: number;
sampleProducts: UserStoreSampleProduct[];
sampleProducts: P[];
}
export interface UserStoreSummaryData {
id: number;
name: string;
description: string | null;
export interface UserStoreSummary extends UserStoreSummaryCore<UserStoreSampleProduct> {
signedImageUrl: string | null;
}
export interface UserStoreSummaryData extends UserStoreSummaryCore<UserStoreSampleProductData> {
imageUrl: string | null;
productCount: number;
sampleProducts: UserStoreSampleProductData[];
}
export interface UserStoresResponse {
stores: UserStoreSummary[];
}
export interface UserStoreProduct {
id: number;
name: string;
shortDescription: string | null;
price: string;
marketPrice: string | null;
incrementStep: number;
unit: string;
unitNotation: string;
images: string[];
isOutOfStock: boolean;
productQuantity: number;
}
export interface UserStoreProductData {
// Product card row — single source for store product cards everywhere
// (user store pages, admin customize-app, helper caches).
export interface StoreProductCard {
id: number;
name: string;
shortDescription: string | null;
@ -210,6 +194,10 @@ export interface UserStoreProductData {
productQuantity: number;
}
export type UserStoreProduct = Omit<StoreProductCard, 'images'> & { images: string[] }
export type UserStoreProductData = StoreProductCard
export interface UserStoreDetail {
store: {
id: number;
@ -241,11 +229,7 @@ export interface UserProductDeliverySlot {
freezeTime: Date;
}
export interface UserProductSpecialDeal {
quantity: string;
price: string;
validTill: Date;
}
export type UserProductSpecialDeal = SpecialDealCore
export interface UserProductComboItem {
skuId: number;
@ -257,7 +241,10 @@ export interface UserProductComboItem {
isOffer: boolean;
}
export interface UserProductDetailData {
// Product-detail core — shared by the user API detail payload, the backend
// cache row (product-store Product) and the sqlite cache row (ProductBasicData).
// isOffer/isComboOnly are optional here: variants that carry them re-tighten.
export interface ProductDetailCore {
id: number;
productId: number;
name: string;
@ -268,14 +255,19 @@ export interface UserProductDetailData {
unitNotation: string;
images: string[] | null;
isOutOfStock: boolean;
store: UserProductStoreInfo | null;
incrementStep: number;
productQuantity: number;
isFlashAvailable: boolean;
flashPrice: string | null;
productType: string;
isOffer?: boolean;
isComboOnly?: boolean;
}
export interface UserProductDetailData extends ProductDetailCore {
store: UserProductStoreInfo | null;
deliverySlots: UserProductDeliverySlot[];
specialDeals: UserProductSpecialDeal[];
productType: string;
comboItems: UserProductComboItem[];
}
@ -283,13 +275,32 @@ export interface UserProductDetail extends UserProductDetailData {
images: string[];
}
export interface UserProductReview {
// Price/availability flag block — anchors the sqlite availability cache row and
// the apps' MergedProduct intersection (both previously hand-rolled).
export interface ProductPriceFlags {
price: number;
marketPrice: number | null;
flashPrice: string | null;
isFlashAvailable: boolean;
isOutOfStock: boolean;
isSuspended: boolean;
}
// Offers-page product card core — sqlite offers/offers-cache row and the
// user-ui cart ProductSummary anchor here.
export interface ProductSummaryCore {
id: number;
reviewBody: string;
ratings: number;
name: string;
price: string;
marketPrice: string | null;
unitNotation: string;
images: string[] | null;
isOutOfStock: boolean;
incrementStep: number;
}
export interface UserProductReview extends ProductReviewCore {
imageUrls: string[] | null;
reviewTime: Date;
userName: string | null;
}
export interface UserProductReviewWithSignedUrls extends UserProductReview {
@ -311,10 +322,7 @@ export interface UserSlotProduct {
images: string[] | null;
}
export interface UserSlotWithProducts {
id: number;
deliveryTime: Date;
freezeTime: Date;
export interface UserSlotWithProducts extends UserProductDeliverySlot {
products: UserSlotProduct[];
}
@ -335,10 +343,8 @@ export interface UserSlotsListResponse {
count: number;
}
export interface UserSlotsWithProductsResponse {
slots: UserSlotWithProducts[];
export interface UserSlotsWithProductsResponse extends UserSlotsResponse {
productAvailability: UserSlotAvailability[];
count: number;
}
export interface UserPaymentOrderResponse {

View file

@ -6,25 +6,19 @@ import MyText from './text';
import { useTheme } from '../../hooks/theme-context';
import tw from '../lib/tailwind';
import { colors } from '../lib/theme-colors';
import type { DropdownOption as BaseDropdownOption } from '@packages/shared';
import type { DropdownBaseProps } from './dropdown';
export interface DropdownOption {
label: string;
value: string | number;
// Bottom/multi dropdown option — shared base + per-option disabled flag
export interface DropdownOption extends BaseDropdownOption {
disabled?: boolean;
}
interface BottomDropdownProps {
label: string;
interface BottomDropdownProps extends DropdownBaseProps<DropdownOption> {
topLabel?: string;
value: string | number | string[] | number[];
options: DropdownOption[];
onValueChange: (value: string | number | string[] | number[]) => void;
multiple?: boolean;
error?: boolean;
style?: any;
placeholder?: string;
disabled?: boolean;
className?: string;
triggerComponent?: React.ComponentType<{
onPress: () => void;
disabled?: boolean;

View file

@ -6,11 +6,13 @@ import type { DropdownOption } from '@packages/shared';
export type { DropdownOption } from '@packages/shared';
interface Props {
// Common dropdown scaffold — value/onValueChange stay per-variant because the
// single dropdown carries a scalar and the bottom/multi one carries arrays.
// The option element type is generic: the bottom/multi dropdown extends the
// shared DropdownOption with a per-option disabled flag.
export interface DropdownBaseProps<O = DropdownOption> {
label: string;
value: string | number;
options: DropdownOption[];
onValueChange: (value: string | number) => void;
options: O[];
error?: boolean;
style?: any;
placeholder?: string;
@ -18,6 +20,11 @@ interface Props {
className?: string;
}
interface Props extends DropdownBaseProps {
value: string | number;
onValueChange: (value: string | number) => void;
}
const CustomDropdown: React.FC<Props> = ({
label,
value,

View file

@ -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({

View file

@ -9,10 +9,7 @@
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src",
"noEmit": true,
"paths": {
"@/*": ["./src/*"],
"@packages/shared": ["../shared"],

43
typecheck Executable file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Typechecks every app/package that has a tsconfig.
# packages/shared and packages/ui have no tsconfig of their own — they are
# typechecked transitively via the consumers below.
set -u
cd "$(dirname "$0")"
targets=(
apps/backend
apps/user-ui
apps/admin-ui
apps/web-ui
apps/fallback-ui
packages/db_helper_sqlite
packages/db_helper_postgres
packages/migrator
packages/web-components
)
log="$(mktemp)"
failed=0
for target in "${targets[@]}"; do
if [ ! -f "$target/tsconfig.json" ]; then
printf '⏭ %-32s (no tsconfig)\n' "$target"
continue
fi
printf '▶ %-32s ' "$target"
if (cd "$target" && npx tsc --noEmit >"$log" 2>&1); then
echo '✅ clean'
else
errors=$(grep -c 'error TS' "$log" || true)
echo "❌ ${errors} error(s)"
head -20 "$log"
failed=1
fi
done
rm -f "$log"
if [ "$failed" -ne 0 ]; then
echo '❌ typecheck failed'
exit 1
fi
echo '✅ all packages typecheck clean'