enh
This commit is contained in:
parent
be40f0cffa
commit
cc540297bf
32 changed files with 535 additions and 1335 deletions
|
|
@ -8,7 +8,18 @@
|
|||
"Shell(cp:*)",
|
||||
"Shell(rm:*)",
|
||||
"Shell(ls:*)",
|
||||
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/apps/admin-ui && ls \"app/(drawer)\" 2>/dev/null; echo \"---\"; find app -name \"create.tsx\" -path \"*coupon*\" 2>/dev/null)"
|
||||
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/apps/admin-ui && ls \"app/(drawer)\" 2>/dev/null; echo \"---\"; find app -name \"create.tsx\" -path \"*coupon*\" 2>/dev/null)",
|
||||
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/apps/user-ui && echo \"=== verify zero external refs for each dead file ===\" && for f in \"ProfileChecker\" \"UserAddressHeader\" \"AddressSelector\" \"OrderAgainIcon\" \"usePhonepeSdk\" \"useHideDrawerHeader\" \"useCurrentUserId\" \"useThemeColor\" \"IconSymbol\" \"TabBarBackground\" \"axios-user-ui\"; do refs=$(grep -rn \"$f\" --include=\"*.tsx\" --include=\"*.ts\" . 2>/dev/null | grep -v node_modules | grep -vE \"components/ProfileChecker|components/UserAddressHeader|components/AddressSelector|icons/OrderAgainIcon|hooks/usePhonepeSdk|hooks/useHideDrawerHeader|hooks/useCurrentUserId|hooks/useThemeColor|components/ui/IconSymbol|components/ui/TabBarBackground|services/axios-user-ui\" | wc -l | tr -d ' '); echo \"$f: $refs external refs\"; done)",
|
||||
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo && echo \"=== usage count per export across apps (excluding packages/ui itself) ===\" && for name in RolesDropdown StorageService ROLE_NAMES ROLE_DISPLAY_NAMES ROLE_OPTIONS BUSINESS_ROLE_OPTIONS REFUND_STATUS MyButton MyTextButton MyTextInput BottomDialog LoadingDialog MyText MyTouchableOpacity ConfirmationDialog RawBottomDialog DatePicker BottomDropdown ImageViewerURI ImageCarousel ImageGallery ImageGalleryWithDelete ImageUploader ImageUploaderNeo ProfileImage Checkbox AppContainer SearchBar DataTable Quantifier MiniQuantifier TabViewWrapper MyFlatList useFocusCallback useManualRefresh useDrawerTitle useMarkDataFetchers useIsDevMode usePagination DateTimePickerMod RefreshProvider useRefresh MyStatusBar updateStatusBarColor useStatusBarStore; do count=$(grep -rn \"\\b$name\\b\" apps/user-ui apps/admin-ui apps/web-ui apps/fallback-ui --include=\"*.tsx\" --include=\"*.ts\" 2>/dev/null | grep -v node_modules | wc -l | tr -d ' '); echo \"$name: $count\"; done)",
|
||||
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo && echo \"=== check usage inside packages/ui (internal use) ===\" && for name in RolesDropdown ROLE_DISPLAY_NAMES ROLE_OPTIONS BUSINESS_ROLE_OPTIONS MyTextButton ImageGallery ImageGalleryWithDelete DataTable TabViewWrapper useIsDevMode usePagination useRefresh useStatusBarStore; do count=$(grep -rn \"\\b$name\\b\" packages/ui --include=\"*.tsx\" --include=\"*.ts\" 2>/dev/null | grep -v node_modules | grep -v \"index.ts\" | wc -l | tr -d ' '); echo \"$name: $count internal\"; done)",
|
||||
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/dumps && echo \"=== lines 7890-7910 (product_skus start) ===\" && sed -n '7890,7910p' local_8_aug.sql && echo \"=== lines 8330-8345 (product_info start) ===\" && sed -n '8330,8345p' local_8_aug.sql && echo \"=== total lines ===\" && wc -l local_8_aug.sql && echo \"=== where does product_info block end? next CREATE/other marker ===\" && grep -nE \"CREATE TABLE|^INSERT INTO \\\"product_info\\\"|^INSERT INTO \\`product_info\\`|^INSERT INTO product_info\" local_8_aug.sql | sed -n '25,40p')",
|
||||
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/dumps && echo \"=== product_info block: from 8334 to next CREATE TABLE after it ===\" && awk 'NR>=8334 && /^CREATE TABLE/ {print NR\": \"$0; exit}' local_8_aug.sql && echo \"=== where do product_skus INSERTs start/end ===\" && grep -nE \"^INSERT INTO \\`?product_skus\\`?|^INSERT INTO \\\"product_skus\\\"\" local_8_aug.sql | head && echo \"=== where do sku_features/product_market_stats/product_info INSERTs start ===\" && grep -nE \"^INSERT INTO \\`?(product_skus|sku_features|product_market_stats|product_info)\\`?\" local_8_aug.sql | head && echo \"=== what comes right after product_info INSERTs (next table or marker) ===\" && awk 'NR>8334 && /^CREATE TABLE/ {print NR\": \"$0}' local_8_aug.sql | head)",
|
||||
"Shell(awk:*)",
|
||||
"Shell(echo:*)",
|
||||
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/dumps && python3 - <<'PY' # Move the product_info block (CREATE + INSERTs) above product_skus. with open('local_8_aug.sql', 'r') as f: lines = f.readlines() # 1-indexed boundaries pi_start = 8334 - 1 # CREATE TABLE product_info pi_end = 8478 # exclusive (up to line 8478 inclusive) -> slice [8333:8478] sku_start = 7900 - 1 # line where product_skus CREATE begins block = lines[pi_start:pi_end] # Remove the product_info block new_lines = lines[:pi_start] + lines[pi_end:] # Re-insert before product_skus (its index may have shifted by -len(block)) insert_at = sku_start new_lines = new_lines[:insert_at] + block + new_lines[insert_at:] with open('local_8_aug.sql', 'w') as f: f.writelines(new_lines) print(f\"Moved product_info block (lines {pi_start+1}-{pi_end}) to before line {sku_start+1}\") PY echo \"=== verify new order ===\" && grep -nE 'CREATE TABLE .(product_info|product_skus|sku_features|product_market_stats|cart_items).' local_8_aug.sql)",
|
||||
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/dumps && echo \"=== order: product_info BEFORE product_skus? ===\" && grep -nE '^CREATE TABLE' local_8_aug.sql | grep -E \"product_info|product_skus|sku_features|product_market_stats|cart_items\" && echo \"=== doc check: every REFERENCES has parent CREATE before it ===\" && grep -nE '^CREATE TABLE|REFERENCES' local_8_aug.sql | grep -E \"REFERENCES\" | grep -oE \"REFERENCES \\`?[a-z_]+\" | sort | uniq -c)",
|
||||
"Shell(sqlite3:*)",
|
||||
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/apps/backend/dumps && sqlite3 :memory: <<'EOF' PRAGMA foreign_keys=ON; BEGIN; .read local_8_aug.sql COMMIT; EOF echo \"exit: $?\")"
|
||||
],
|
||||
"deny": [],
|
||||
"defaultMode": "default"
|
||||
|
|
|
|||
|
|
@ -19,5 +19,13 @@
|
|||
- Prefers test plans written for a non-technical audience, using plain-language, click-by-click instructions ("tap this", "type that", "check there") rather than technical terms or API names. Confidence: 0.9
|
||||
- When updating or creating a document, prefers the agent first compare it against existing source documents, identify missing items or gaps, and add them in the same established format. Confidence: 0.9
|
||||
- Dislikes nested headers in mobile/drawer navigation; prefers a single, shared header (e.g., the drawer header) and relies on device back buttons or gestures for returning to previous screens. Confidence: 0.9
|
||||
- After reviewing a presented plan, prefers brief, action-oriented approval (e.g., "nice. go ahead") before implementation proceeds. Confidence: 0.6
|
||||
- After reviewing a presented plan, prefers brief, action-oriented approval (e.g., "nice. go ahead") before implementation proceeds. Confidence: 0.7
|
||||
- Wants feature parity maintained between the web-ui and user-ui apps (port logic/data patterns, rebuild UI per platform). Confidence: 0.85
|
||||
- Prefers dead-code audits to be documented in a markdown file. Confidence: 0.8
|
||||
- Prefers detailed technical documentation of system architecture, data models, flows, and integrations in markdown format. Confidence: 0.8
|
||||
- Wants edge cases explicitly enumerated when documenting or analyzing existing code/systems. Confidence: 0.8
|
||||
- Values end-to-end analysis of features (architecture, data model, flows, integrations, and edge cases) when asked to explain how something works. Confidence: 0.7
|
||||
- Prefers unifying parallel data structures into a single instance rather than maintaining separate ones (e.g., one cart for flash and regular items instead of separate flash/regular carts). Confidence: 0.9
|
||||
- Prefers using sentinel values in existing fields (e.g., slotId = 0) to distinguish special-case items rather than creating separate fields or structures. Confidence: 0.9
|
||||
- Prefers handling special-case logic locally in the relevant component/file rather than globally or via parallel flows. Confidence: 0.85
|
||||
- Prefers lazy/virtualized list rendering (e.g., FlatList) for long lists, rather than rendering all items at once. Confidence: 0.8
|
||||
|
|
|
|||
6
apps/admin-ui/.expo/types/router.d.ts
vendored
6
apps/admin-ui/.expo/types/router.d.ts
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import { View, ScrollView, TouchableOpacity, RefreshControl } from 'react-native';
|
||||
import { View, TouchableOpacity, RefreshControl } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { useRouter } from 'expo-router';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { AppContainer, MyText, tw, MyButton, useManualRefresh, SearchBar, useMarkDataFetchers } from 'common-ui';
|
||||
import { AppContainer, MyText, tw, MyButton, useManualRefresh, SearchBar, useMarkDataFetchers, MyFlatList } from 'common-ui';
|
||||
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import type { AdminSku } from '@packages/shared';
|
||||
|
|
@ -98,46 +98,115 @@ export default function Products() {
|
|||
|
||||
const inStockCount = products.filter(p => getDefaultSku(p) && !getDefaultSku(p)!.isOutOfStock).length;
|
||||
const outOfStockCount = products.filter(p => getDefaultSku(p)?.isOutOfStock).length;
|
||||
|
||||
|
||||
const listHeader = (
|
||||
<>
|
||||
{/* Header */}
|
||||
<View style={tw`flex-row justify-between items-center mb-6`}>
|
||||
<MyText style={tw`text-2xl font-bold text-gray-800`}>Products</MyText>
|
||||
<MyButton onPress={() => router.push('/(drawer)/dashboard/products/add' as any)}>
|
||||
Add Product
|
||||
</MyButton>
|
||||
</View>
|
||||
|
||||
{/* Search Bar */}
|
||||
<View style={tw`mb-4`}>
|
||||
<SearchBar
|
||||
value={searchTerm}
|
||||
onChangeText={setSearchTerm}
|
||||
onSearch={() => {}}
|
||||
placeholder="Search products..."
|
||||
containerStyle={tw`mb-0`}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<View style={tw`flex-row gap-2 mb-6`}>
|
||||
<FilterButton filter="all" label="All" count={products.length} />
|
||||
<FilterButton filter="in-stock" label="In Stock" count={inStockCount} />
|
||||
<FilterButton filter="out-of-stock" label="Out of Stock" count={outOfStockCount} />
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderItem = ({ item: product }: { item: any }) => {
|
||||
const defaultSku = getDefaultSku(product)
|
||||
const skuImages = defaultSku?.images ?? null
|
||||
const isOut = defaultSku?.isOutOfStock ?? false
|
||||
|
||||
return (
|
||||
<View key={product.id} style={tw`bg-white rounded-2xl shadow-lg mb-4 overflow-hidden`}>
|
||||
{/* Product Image */}
|
||||
{skuImages && skuImages.length > 0 ? (
|
||||
<Image
|
||||
source={{ uri: skuImages[0] }}
|
||||
style={tw`w-full h-48`}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<View style={tw`w-full h-48 bg-gray-200 justify-center items-center`}>
|
||||
<MaterialIcons name="image" size={48} color="#9CA3AF" />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Product Info */}
|
||||
<View style={tw`p-4`}>
|
||||
<View style={tw`flex-row justify-between items-start mb-2`}>
|
||||
<MyText style={tw`text-lg font-bold text-gray-800 flex-1 mr-2`} numberOfLines={2}>
|
||||
{product.name}
|
||||
</MyText>
|
||||
<View style={tw`flex-row items-center`}>
|
||||
<View style={tw`w-3 h-3 rounded-full mr-1 ${isOut ? 'bg-red-500' : 'bg-green-500'}`} />
|
||||
<MyText style={tw`text-sm ${isOut ? 'text-red-500' : 'text-green-500'} font-semibold`}>
|
||||
{isOut ? 'Out' : 'In'}
|
||||
</MyText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{product.shortDescription && (
|
||||
<MyText style={tw`text-gray-600 mb-2`} numberOfLines={2}>
|
||||
{product.shortDescription}
|
||||
</MyText>
|
||||
)}
|
||||
|
||||
<View style={tw`flex-row justify-between items-center mb-3`}>
|
||||
<MyText style={tw`text-sm text-gray-500 font-medium`}>
|
||||
{product.skus?.length ?? 0} Variants
|
||||
</MyText>
|
||||
</View>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<View style={tw`flex-row gap-2`}>
|
||||
<TouchableOpacity
|
||||
onPress={() => handleViewDetails(product.id)}
|
||||
style={tw`flex-1 bg-gray-500 p-3 rounded-lg flex-row items-center justify-center`}
|
||||
>
|
||||
<MaterialIcons name="visibility" size={16} color="white" />
|
||||
<MyText style={tw`text-white font-semibold ml-1`}>View</MyText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => handleEdit(product.id)}
|
||||
style={tw`flex-1 bg-blue-500 p-3 rounded-lg flex-row items-center justify-center`}
|
||||
>
|
||||
<MaterialIcons name="edit" size={16} color="white" />
|
||||
<MyText style={tw`text-white font-semibold ml-1`}>Edit</MyText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
};
|
||||
|
||||
return (
|
||||
<AppContainer>
|
||||
<View style={tw`flex-1`}>
|
||||
{/* Header */}
|
||||
<View style={tw`flex-row justify-between items-center mb-6`}>
|
||||
<MyText style={tw`text-2xl font-bold text-gray-800`}>Products</MyText>
|
||||
<MyButton onPress={() => router.push('/(drawer)/dashboard/products/add' as any)}>
|
||||
Add Product
|
||||
</MyButton>
|
||||
</View>
|
||||
|
||||
{/* Search Bar */}
|
||||
<View style={tw`mb-4`}>
|
||||
<SearchBar
|
||||
value={searchTerm}
|
||||
onChangeText={setSearchTerm}
|
||||
onSearch={() => {}}
|
||||
placeholder="Search products..."
|
||||
containerStyle={tw`mb-0`}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<View style={tw`flex-row gap-2 mb-6`}>
|
||||
<FilterButton filter="all" label="All" count={products.length} />
|
||||
<FilterButton filter="in-stock" label="In Stock" count={inStockCount} />
|
||||
<FilterButton filter="out-of-stock" label="Out of Stock" count={outOfStockCount} />
|
||||
</View>
|
||||
|
||||
{/* Products List */}
|
||||
<ScrollView
|
||||
style={tw`flex-1`}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
||||
}
|
||||
>
|
||||
{filteredProducts.length === 0 ? (
|
||||
<MyFlatList
|
||||
data={filteredProducts}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
ListHeaderComponent={listHeader}
|
||||
ListEmptyComponent={
|
||||
<View style={tw`flex-1 justify-center items-center py-10`}>
|
||||
<MaterialIcons name="inventory" size={64} color="#D1D5DB" />
|
||||
<MyText style={tw`text-gray-500 text-center mt-4 text-lg`}>
|
||||
|
|
@ -158,79 +227,14 @@ export default function Products() {
|
|||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<View style={tw`pb-4`}>
|
||||
{filteredProducts.map(product => {
|
||||
const defaultSku = getDefaultSku(product)
|
||||
const skuImages = defaultSku?.images ?? null
|
||||
const isOut = defaultSku?.isOutOfStock ?? false
|
||||
|
||||
return (
|
||||
<View key={product.id} style={tw`bg-white rounded-2xl shadow-lg mb-4 overflow-hidden`}>
|
||||
{/* Product Image */}
|
||||
{skuImages && skuImages.length > 0 ? (
|
||||
<Image
|
||||
source={{ uri: skuImages[0] }}
|
||||
style={tw`w-full h-48`}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<View style={tw`w-full h-48 bg-gray-200 justify-center items-center`}>
|
||||
<MaterialIcons name="image" size={48} color="#9CA3AF" />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Product Info */}
|
||||
<View style={tw`p-4`}>
|
||||
<View style={tw`flex-row justify-between items-start mb-2`}>
|
||||
<MyText style={tw`text-lg font-bold text-gray-800 flex-1 mr-2`} numberOfLines={2}>
|
||||
{product.name}
|
||||
</MyText>
|
||||
<View style={tw`flex-row items-center`}>
|
||||
<View style={tw`w-3 h-3 rounded-full mr-1 ${isOut ? 'bg-red-500' : 'bg-green-500'}`} />
|
||||
<MyText style={tw`text-sm ${isOut ? 'text-red-500' : 'text-green-500'} font-semibold`}>
|
||||
{isOut ? 'Out' : 'In'}
|
||||
</MyText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{product.shortDescription && (
|
||||
<MyText style={tw`text-gray-600 mb-2`} numberOfLines={2}>
|
||||
{product.shortDescription}
|
||||
</MyText>
|
||||
)}
|
||||
|
||||
<View style={tw`flex-row justify-between items-center mb-3`}>
|
||||
<MyText style={tw`text-sm text-gray-500 font-medium`}>
|
||||
{product.skus?.length ?? 0} Variants
|
||||
</MyText>
|
||||
</View>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<View style={tw`flex-row gap-2`}>
|
||||
<TouchableOpacity
|
||||
onPress={() => handleViewDetails(product.id)}
|
||||
style={tw`flex-1 bg-gray-500 p-3 rounded-lg flex-row items-center justify-center`}
|
||||
>
|
||||
<MaterialIcons name="visibility" size={16} color="white" />
|
||||
<MyText style={tw`text-white font-semibold ml-1`}>View</MyText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => handleEdit(product.id)}
|
||||
style={tw`flex-1 bg-blue-500 p-3 rounded-lg flex-row items-center justify-center`}
|
||||
>
|
||||
<MaterialIcons name="edit" size={16} color="white" />
|
||||
<MyText style={tw`text-white font-semibold ml-1`}>Edit</MyText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
}
|
||||
contentContainerStyle={tw`pb-4`}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,164 +0,0 @@
|
|||
import React from "react";
|
||||
import { View, Dimensions } from "react-native";
|
||||
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
||||
import {
|
||||
BottomDropdown,
|
||||
MyTouchableOpacity,
|
||||
MyText,
|
||||
tw,
|
||||
theme,
|
||||
} from "common-ui";
|
||||
import { useAuth } from "@/src/contexts/AuthContext";
|
||||
import { trpc } from "@/src/trpc-client";
|
||||
import { useAddressStore } from "@/src/store/addressStore";
|
||||
|
||||
const { width: screenWidth } = Dimensions.get("window");
|
||||
|
||||
interface AddressSelectorProps {
|
||||
mode?: "home";
|
||||
deliveryTime?: string;
|
||||
}
|
||||
|
||||
const AddressSelector: React.FC<AddressSelectorProps> = ({
|
||||
mode = "home",
|
||||
deliveryTime,
|
||||
}) => {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { selectedAddressId, setSelectedAddressId } = useAddressStore();
|
||||
|
||||
const { data: defaultAddressData } =
|
||||
trpc.user.address.getDefaultAddress.useQuery();
|
||||
const { data: addressesData } = trpc.user.address.getUserAddresses.useQuery(
|
||||
undefined,
|
||||
{
|
||||
enabled: isAuthenticated,
|
||||
}
|
||||
);
|
||||
|
||||
const defaultAddress = defaultAddressData?.data;
|
||||
const addresses = addressesData?.data || [];
|
||||
|
||||
// Transform addresses for dropdown
|
||||
const addressOptions = addresses.map((address) => ({
|
||||
label: `${address.name} - ${address.addressLine1}, ${address.city}`,
|
||||
value: address.id,
|
||||
}));
|
||||
|
||||
const handleAddressChange = (
|
||||
value: string | number | string[] | number[]
|
||||
) => {
|
||||
const addressId = value as number;
|
||||
setSelectedAddressId(addressId);
|
||||
};
|
||||
|
||||
// Helper to get display parts
|
||||
const getDisplayParts = (displayText?: string) => {
|
||||
const fullText =
|
||||
displayText ||
|
||||
(defaultAddress
|
||||
? `${defaultAddress.name} - ${defaultAddress.addressLine1}, ${defaultAddress.city}`
|
||||
: "");
|
||||
let name = defaultAddress ? defaultAddress.name : "Home";
|
||||
let address = defaultAddress
|
||||
? `${defaultAddress.addressLine1}, ${defaultAddress.city}`
|
||||
: "Add your delivery address";
|
||||
|
||||
if (fullText) {
|
||||
const parts = fullText.split(" - ");
|
||||
if (parts.length >= 1) name = parts[0];
|
||||
if (parts.length > 1) address = parts.slice(1).join(" - ");
|
||||
}
|
||||
return { name, address };
|
||||
};
|
||||
|
||||
const { name: defaultName, address: defaultAddr } = getDisplayParts();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
// Static display for non-authenticated users (Home mode)
|
||||
return (
|
||||
<View style={[{ width: screenWidth * 0.65 }, tw`mr-4`]}>
|
||||
<View style={tw`flex-row items-center mb-1`}>
|
||||
<View style={tw`bg-white/20 p-1 rounded-full `}>
|
||||
<MaterialIcons
|
||||
name="location-pin"
|
||||
size={14}
|
||||
color={'#fff'}
|
||||
/>
|
||||
</View>
|
||||
<View style={tw``}>
|
||||
<MyText
|
||||
style={tw`text-white text-xs font-bold uppercase tracking-widest`}
|
||||
>
|
||||
Delivery to {defaultName}
|
||||
</MyText>
|
||||
<MyText
|
||||
style={tw`text-white text-sm font-medium opacity-90`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{defaultAddr}
|
||||
</MyText>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BottomDropdown
|
||||
style={{ width: screenWidth * 0.65 }}
|
||||
label="Select Delivery Address"
|
||||
value={selectedAddressId || defaultAddress?.id || ""}
|
||||
options={addressOptions}
|
||||
onValueChange={handleAddressChange}
|
||||
placeholder="Select delivery address"
|
||||
triggerComponent={({ onPress, displayText }) => {
|
||||
const { name, address } = getDisplayParts(displayText);
|
||||
|
||||
// Home styling
|
||||
return (
|
||||
<MyTouchableOpacity
|
||||
style={tw`flex-1 mr-4`}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View style={tw`flex-row items-center mb-1`}>
|
||||
<View style={tw`bg-white/20 p-1 rounded-full mr-2`}>
|
||||
<MaterialIcons
|
||||
name="location-pin"
|
||||
size={18}
|
||||
style={tw`text-neutral-800`}
|
||||
// color={theme.colors.gray900}
|
||||
// color={'#fff'}
|
||||
/>
|
||||
</View>
|
||||
<View style={tw``}>
|
||||
<View style={tw`flex-row`}>
|
||||
<MyText
|
||||
numberOfLines={1}
|
||||
style={tw`text-neutral-800 text-xs font-bold uppercase tracking-widest`}
|
||||
>
|
||||
to {name}
|
||||
</MyText>
|
||||
{/* <MaterialIcons
|
||||
name="keyboard-arrow-down"
|
||||
size={14}
|
||||
color={theme.colors.gray900}
|
||||
style={tw`ml-1`}
|
||||
/> */}
|
||||
</View>
|
||||
<MyText
|
||||
style={tw`text-neutral-800 text-sm font-medium opacity-90`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{address}
|
||||
</MyText>
|
||||
</View>
|
||||
</View>
|
||||
</MyTouchableOpacity>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddressSelector;
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { tw, theme, MyTouchableOpacity } from 'common-ui';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
|
||||
const CustomHeader = () => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<View style={[tw`flex-row items-center justify-between pt-8 pb-4 px-4 shadow-sm`, { backgroundColor: theme.colors.gray1 }]}>
|
||||
<MyTouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: theme.colors.gray2,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="chevron-left" size={24} color="black" />
|
||||
</MyTouchableOpacity>
|
||||
<View style={tw`flex-row`}>
|
||||
<MyTouchableOpacity
|
||||
onPress={() => router.push('/(drawer)/(tabs)/home/cart')}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: theme.colors.gray2,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="shopping-cart" size={24} color="black" />
|
||||
</MyTouchableOpacity>
|
||||
<MyTouchableOpacity
|
||||
onPress={() => router.push('/(drawer)/(tabs)/me')}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: theme.colors.gray2,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="person" size={24} color="black" />
|
||||
</MyTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomHeader;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { View, TouchableOpacity, ActivityIndicator, Linking } from 'react-native';
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { View, TouchableOpacity, ActivityIndicator, Linking, ScrollView, Dimensions, NativeSyntheticEvent, NativeScrollEvent } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { tw, MyText } from 'common-ui';
|
||||
import { useNavigationTarget } from 'common-ui/hooks/useNavigationTarget';
|
||||
|
|
@ -11,6 +11,10 @@ import { orderStatusManipulator } from '@/src/lib/string-manipulators';
|
|||
import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
|
||||
import { useUserDetails } from '@/src/contexts/AuthContext';
|
||||
|
||||
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 OrderItem {
|
||||
productName: string;
|
||||
quantity: number;
|
||||
|
|
@ -40,10 +44,115 @@ interface Order {
|
|||
createdAt: string;
|
||||
}
|
||||
|
||||
interface OrderCardProps {
|
||||
order: Order;
|
||||
onPress: () => void;
|
||||
supportMobile?: string | null;
|
||||
}
|
||||
|
||||
function OrderCard({ order, onPress, supportMobile }: OrderCardProps) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{ width: CARD_WIDTH, marginRight: 24 }}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View style={tw`bg-gradient-to-r from-amber-50 to-orange-50 rounded-2xl p-4 border-2 border-amber-300`}>
|
||||
<View style={tw`flex-row items-center justify-between mb-3`}>
|
||||
<View style={tw`flex-row items-center`}>
|
||||
<View style={tw`w-2 h-2 rounded-full bg-amber-500 mr-2`} />
|
||||
<MyText style={tw`text-[10px] font-bold text-amber-600 uppercase tracking-wider`}>Upcoming Order</MyText>
|
||||
</View>
|
||||
{order.isFlashDelivery && (
|
||||
<View style={tw`px-2 py-0.5 bg-amber-200 rounded-full flex-row items-center`}>
|
||||
<MaterialIcons name="bolt" size={10} color="#D97706" style={tw`mr-0.5`} />
|
||||
<MyText style={tw`text-[8px] font-black text-amber-700 uppercase`}>FLASH</MyText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={tw`flex-row justify-between items-start mb-3`}>
|
||||
<View>
|
||||
<MyText style={tw`text-lg font-extrabold text-gray-900`}>#{order.orderId}</MyText>
|
||||
<MyText style={tw`text-xs text-gray-500 mt-0.5`}>
|
||||
{order.items.length} {order.items.length === 1 ? 'item' : 'items'} • ₹{order.totalAmount}
|
||||
</MyText>
|
||||
</View>
|
||||
<View style={tw`bg-white px-2 py-1 rounded-lg border border-amber-200`}>
|
||||
<MyText style={tw`text-[10px] font-bold text-amber-700 uppercase`}>{orderStatusManipulator(order.deliveryStatus)}</MyText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={tw`flex-row items-center`}>
|
||||
<MaterialIcons
|
||||
name={order.isFlashDelivery ? "bolt" : "schedule"}
|
||||
size={14}
|
||||
color="#D97706"
|
||||
style={tw`mr-1.5`}
|
||||
/>
|
||||
<MyText style={tw`text-xs font-medium text-amber-800`}>
|
||||
{order.isFlashDelivery
|
||||
? "30-Min Delivery"
|
||||
: (() => {
|
||||
const startTime = dayjs(order.deliveryDate);
|
||||
const endTime = startTime.add(1, 'hour');
|
||||
return `${startTime.format("DD MMM, hh:mm A")} - ${endTime.format("hh:mm A")}`;
|
||||
})()
|
||||
}
|
||||
</MyText>
|
||||
</View>
|
||||
|
||||
{order.items.length > 0 && (
|
||||
<View style={tw`flex-row items-center mt-3 pt-3 border-t border-amber-200`}>
|
||||
<View style={tw`flex-row`}>
|
||||
{order.items.slice(0, 3).map((item, idx) => (
|
||||
<View
|
||||
key={idx}
|
||||
style={[
|
||||
tw`w-8 h-8 rounded-lg bg-white border border-amber-200 items-center justify-center`,
|
||||
{ marginLeft: idx > 0 ? -8 : 0 }
|
||||
]}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: item.image || undefined }}
|
||||
style={tw`w-full h-full rounded-lg`}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
{order.items.length > 3 && (
|
||||
<View style={tw`w-8 h-8 rounded-lg bg-amber-100 border border-amber-200 items-center justify-center ml-2`}>
|
||||
<MyText style={tw`text-[9px] font-bold text-amber-700`}>+{order.items.length - 3}</MyText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<MyText style={tw`text-xs text-amber-700 font-medium ml-2`}>Track Order</MyText>
|
||||
<Ionicons name="chevron-forward" size={14} color="#D97706" style={tw`ml-auto`} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Support Mobile Number */}
|
||||
{supportMobile && (
|
||||
<TouchableOpacity
|
||||
style={tw`flex-row items-center mt-3 pt-3 border-t border-amber-200`}
|
||||
onPress={() => Linking.openURL(`tel:${supportMobile}`)}
|
||||
>
|
||||
<MaterialIcons name="phone-in-talk" size={14} color="#D97706" style={tw`mr-1.5`} />
|
||||
<MyText style={tw`text-xs text-amber-700 font-medium`}>
|
||||
Need help? Call {supportMobile}
|
||||
</MyText>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NextOrderGlimpse() {
|
||||
const router = useRouter();
|
||||
const { setNavigationTarget } = useNavigationTarget();
|
||||
const userDetails = useUserDetails();
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
|
||||
const { data: ordersData, isLoading: ordersLoading } = trpc.user.order.getOrders.useQuery({
|
||||
page: 1,
|
||||
|
|
@ -78,7 +187,15 @@ export default function NextOrderGlimpse() {
|
|||
return 0;
|
||||
});
|
||||
|
||||
const nextOrder = upcomingOrders[0] || null;
|
||||
const handleScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const index = Math.round(event.nativeEvent.contentOffset.x / SNAP_INTERVAL);
|
||||
setCurrentIndex(Math.min(Math.max(index, 0), upcomingOrders.length - 1));
|
||||
}, [upcomingOrders.length]);
|
||||
|
||||
const handleOrderPress = useCallback((order: Order) => {
|
||||
setNavigationTarget(`/(drawer)/(tabs)/me/my-orders/${order.id}`);
|
||||
router.replace('/(drawer)/(tabs)/me');
|
||||
}, [router, setNavigationTarget]);
|
||||
|
||||
if (!userDetails) {
|
||||
return null;
|
||||
|
|
@ -94,104 +211,44 @@ export default function NextOrderGlimpse() {
|
|||
);
|
||||
}
|
||||
|
||||
if (!nextOrder) {
|
||||
if (upcomingOrders.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={tw`px-6 mb-4`}
|
||||
onPress={() => {
|
||||
setNavigationTarget(`/(drawer)/(tabs)/me/my-orders/${nextOrder.id}`);
|
||||
router.replace('/(drawer)/(tabs)/me');
|
||||
}}
|
||||
>
|
||||
<View style={tw`bg-gradient-to-r from-amber-50 to-orange-50 rounded-2xl p-4 border-2 border-amber-300`}>
|
||||
<View style={tw`flex-row items-center justify-between mb-3`}>
|
||||
<View style={tw`flex-row items-center`}>
|
||||
<View style={tw`w-2 h-2 rounded-full bg-amber-500 mr-2`} />
|
||||
<MyText style={tw`text-[10px] font-bold text-amber-600 uppercase tracking-wider`}>Upcoming Order</MyText>
|
||||
</View>
|
||||
{nextOrder.isFlashDelivery && (
|
||||
<View style={tw`px-2 py-0.5 bg-amber-200 rounded-full flex-row items-center`}>
|
||||
<MaterialIcons name="bolt" size={10} color="#D97706" style={tw`mr-0.5`} />
|
||||
<MyText style={tw`text-[8px] font-black text-amber-700 uppercase`}>FLASH</MyText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={tw`flex-row justify-between items-start mb-3`}>
|
||||
<View>
|
||||
<MyText style={tw`text-lg font-extrabold text-gray-900`}>#{nextOrder.orderId}</MyText>
|
||||
<MyText style={tw`text-xs text-gray-500 mt-0.5`}>
|
||||
{nextOrder.items.length} {nextOrder.items.length === 1 ? 'item' : 'items'} • ₹{nextOrder.totalAmount}
|
||||
</MyText>
|
||||
</View>
|
||||
<View style={tw`bg-white px-2 py-1 rounded-lg border border-amber-200`}>
|
||||
<MyText style={tw`text-[10px] font-bold text-amber-700 uppercase`}>{orderStatusManipulator(nextOrder.deliveryStatus)}</MyText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={tw`flex-row items-center`}>
|
||||
<MaterialIcons
|
||||
name={nextOrder.isFlashDelivery ? "bolt" : "schedule"}
|
||||
size={14}
|
||||
color="#D97706"
|
||||
style={tw`mr-1.5`}
|
||||
<View style={tw`mb-4`}>
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
decelerationRate="fast"
|
||||
snapToInterval={SNAP_INTERVAL}
|
||||
contentContainerStyle={tw`px-6`}
|
||||
onScroll={handleScroll}
|
||||
onMomentumScrollEnd={handleScroll}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
{upcomingOrders.map((order) => (
|
||||
<OrderCard
|
||||
key={order.id}
|
||||
order={order}
|
||||
onPress={() => handleOrderPress(order)}
|
||||
supportMobile={essentialConsts?.supportMobile}
|
||||
/>
|
||||
<MyText style={tw`text-xs font-medium text-amber-800`}>
|
||||
{nextOrder.isFlashDelivery
|
||||
? "30-Min Delivery"
|
||||
: (() => {
|
||||
const startTime = dayjs(nextOrder.deliveryDate);
|
||||
const endTime = startTime.add(1, 'hour');
|
||||
return `${startTime.format("DD MMM, hh:mm A")} - ${endTime.format("hh:mm A")}`;
|
||||
})()
|
||||
}
|
||||
</MyText>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{/* Pagination Dots */}
|
||||
{upcomingOrders.length > 1 && (
|
||||
<View style={tw`flex-row justify-center mt-2`}>
|
||||
{upcomingOrders.map((_, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={tw`mx-1 w-2 h-2 rounded-full ${index === currentIndex ? 'bg-amber-500' : 'bg-amber-200'}`}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{nextOrder.items.length > 0 && (
|
||||
<View style={tw`flex-row items-center mt-3 pt-3 border-t border-amber-200`}>
|
||||
<View style={tw`flex-row`}>
|
||||
{nextOrder.items.slice(0, 3).map((item, idx) => (
|
||||
<View
|
||||
key={idx}
|
||||
style={[
|
||||
tw`w-8 h-8 rounded-lg bg-white border border-amber-200 items-center justify-center`,
|
||||
{ marginLeft: idx > 0 ? -8 : 0 }
|
||||
]}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: item.image || undefined }}
|
||||
style={tw`w-full h-full rounded-lg`}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
{nextOrder.items.length > 3 && (
|
||||
<View style={tw`w-8 h-8 rounded-lg bg-amber-100 border border-amber-200 items-center justify-center ml-2`}>
|
||||
<MyText style={tw`text-[9px] font-bold text-amber-700`}>+{nextOrder.items.length - 3}</MyText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<MyText style={tw`text-xs text-amber-700 font-medium ml-2`}>Track Order</MyText>
|
||||
<Ionicons name="chevron-forward" size={14} color="#D97706" style={tw`ml-auto`} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Support Mobile Number */}
|
||||
{essentialConsts?.supportMobile && (
|
||||
<TouchableOpacity
|
||||
style={tw`flex-row items-center mt-3 pt-3 border-t border-amber-200`}
|
||||
onPress={() => Linking.openURL(`tel:${essentialConsts.supportMobile}`)}
|
||||
>
|
||||
<MaterialIcons name="phone-in-talk" size={14} color="#D97706" style={tw`mr-1.5`} />
|
||||
<MyText style={tw`text-xs text-amber-700 font-medium`}>
|
||||
Need help? Call {essentialConsts.supportMobile}
|
||||
</MyText>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -228,7 +228,6 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
|
||||
return (
|
||||
<View style={tw`flex-1 bg-gray-50`}>
|
||||
{/* <CustomHeader /> */}
|
||||
|
||||
<ScrollView
|
||||
style={tw`flex-1`}
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { BottomDialog, MyText, MyButton, useIsDevMode } from 'common-ui';
|
||||
import { trpc } from '../src/trpc-client';
|
||||
import { useRouter } from 'expo-router';
|
||||
|
||||
const ProfileChecker: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const isDevMode = useIsDevMode();
|
||||
const { data, isLoading } = trpc.user.user.checkProfileComplete.useQuery();
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isLoading && data && !data.isComplete) {
|
||||
setDialogOpen(true);
|
||||
}
|
||||
}, [data, isLoading]);
|
||||
|
||||
if (isLoading || isDevMode) return null;
|
||||
|
||||
return (
|
||||
<BottomDialog open={dialogOpen} onClose={() => setDialogOpen(false)}>
|
||||
<View style={{ padding: 20 }}>
|
||||
<MyText style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 16 }}>
|
||||
Complete Your Profile
|
||||
</MyText>
|
||||
<MyText style={{ marginBottom: 24 }}>
|
||||
Please complete your profile for a better experience.
|
||||
</MyText>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 24 }}>
|
||||
<MyButton
|
||||
textContent="Update Later"
|
||||
onPress={() => setDialogOpen(false)}
|
||||
fillColor="gray1"
|
||||
textColor="black1"
|
||||
/>
|
||||
<MyButton
|
||||
textContent="Update Now"
|
||||
onPress={() => { setDialogOpen(false); router.push('/(drawer)/(tabs)/me/edit-profile'); }}
|
||||
fillColor="brand500"
|
||||
textColor="white1"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</BottomDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileChecker;
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import { tw, MyText, MyTouchableOpacity } from 'common-ui';
|
||||
|
||||
export default function UserAddressHeader() {
|
||||
const router = useRouter();
|
||||
const { data: defaultAddressResponse } = trpc.user.address.getDefaultAddress.useQuery();
|
||||
const defaultAddress = defaultAddressResponse?.data;
|
||||
|
||||
// Use a cleaner, white aesthetic that doesn't dominate the eye but feels premium
|
||||
return (
|
||||
<View style={[
|
||||
tw`bg-white px-5 pt-3 pb-4 rounded-b-3xl z-50 border-b border-gray-50 shadow-sm`,
|
||||
{ shadowColor: '#2E90FA', shadowOpacity: 0.08, shadowRadius: 10, shadowOffset: { width: 0, height: 4 } }
|
||||
]}>
|
||||
<View style={tw`flex-row items-center mt-1`}>
|
||||
<MyTouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={() => router.push('/(drawer)/(tabs)/me/addresses')}
|
||||
style={tw`flex-1`}
|
||||
>
|
||||
<View style={tw`flex-row items-center mb-0.5`}>
|
||||
{/* Small subtle brand indicator */}
|
||||
<View style={tw`w-1.5 h-1.5 rounded-full bg-blue-500 mr-2`} />
|
||||
|
||||
<MyText style={tw`text-gray-500 text-[10px] font-bold uppercase tracking-widest`}>
|
||||
Delivering to
|
||||
</MyText>
|
||||
<MyText style={tw`text-gray-800 text-xs font-bold uppercase tracking-wide ml-1`}>
|
||||
{defaultAddress?.name ? defaultAddress.name.split(' ')[0] : 'Home'}
|
||||
</MyText>
|
||||
<MaterialIcons name="keyboard-arrow-down" size={14} color="#374151" style={tw`ml-0.5`} />
|
||||
</View>
|
||||
|
||||
<MyText style={tw`text-gray-500 text-sm font-medium ml-3.5`} numberOfLines={1}>
|
||||
{defaultAddress ? `${defaultAddress.addressLine1}, ${defaultAddress.city}` : 'Add a delivery address'}
|
||||
</MyText>
|
||||
</MyTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import React from 'react';
|
||||
import Svg, { Path } from 'react-native-svg';
|
||||
|
||||
interface OrderAgainIconProps {
|
||||
focused: boolean;
|
||||
size: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const OrderAgainIcon: React.FC<OrderAgainIconProps> = ({ focused, size, color }) => {
|
||||
const iconColor = focused ? '#2563EB' : '#2D264B'; // Use focused color when focused, original color when not
|
||||
|
||||
return (
|
||||
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<Path d="M13.7213 2.52876C13.461 2.26841 13.0389 2.26841 12.7785 2.52876C12.5182 2.78911 12.5182 3.21122 12.7785 3.47157L13.6405 4.3335L11.8521 4.33348C10.1322 4.33327 9.10354 4.33314 8.24885 4.61084C6.5237 5.17138 5.17116 6.52392 4.61062 8.24907C4.33292 9.10376 4.33305 10.1324 4.33326 11.8524V12.148C4.33305 13.868 4.33292 14.8966 4.61062 15.7513C4.7244 16.1014 5.1005 16.2931 5.45067 16.1793C5.80084 16.0655 5.99248 15.6894 5.8787 15.3392C5.67483 14.7118 5.66661 13.9133 5.66661 12.0002C5.66661 10.087 5.67483 9.28852 5.8787 8.66109C6.30734 7.34186 7.34164 6.30756 8.66087 5.87892C9.2883 5.67505 10.0868 5.66683 11.9999 5.66683H13.4086L12.5732 6.36302C12.2903 6.59872 12.2521 7.0191 12.4878 7.30195C12.7235 7.5848 13.1439 7.62302 13.4267 7.38731L14.8355 6.21333C15.2015 5.90835 15.4022 5.48067 15.4314 5.04485C15.4623 4.58514 15.3028 4.11024 14.947 3.75445L13.7213 2.52876Z" fill={iconColor}/>
|
||||
<Path d="M20.3917 7.8019C20.2822 7.45037 19.9085 7.25416 19.557 7.36366C19.2054 7.47315 19.0092 7.84689 19.1187 8.19842C19.3257 8.86279 19.3333 9.7066 19.3333 11.6982C19.3333 13.6898 19.3257 14.5336 19.1187 15.1979C18.685 16.5905 17.6431 17.6688 16.331 18.1135C15.7081 18.3246 14.915 18.3335 12.9999 18.3335H11.6094L12.4713 17.4716C12.7317 17.2112 12.7317 16.7891 12.4713 16.5288C12.211 16.2684 11.7889 16.2684 11.5285 16.5288L10.3028 17.7545C9.94705 18.1102 9.78756 18.5851 9.81845 19.0448C9.84772 19.4807 10.0484 19.9083 10.4144 20.2133L11.8232 21.3873C12.106 21.623 12.5264 21.5848 12.7621 21.302C12.9978 21.0191 12.9596 20.5987 12.6767 20.363L11.8413 19.6668L13.1517 19.6668C14.8677 19.6671 15.9006 19.6672 16.759 19.3763C18.4914 18.7891 19.8363 17.3776 20.3917 15.5945C20.6669 14.711 20.6668 13.6464 20.6666 11.8433V11.553C20.6668 9.74996 20.6669 8.68539 20.3917 7.8019Z" fill={iconColor}/>
|
||||
</Svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderAgainIcon;
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols';
|
||||
import { StyleProp, ViewStyle } from 'react-native';
|
||||
|
||||
export function IconSymbol({
|
||||
name,
|
||||
size = 24,
|
||||
color,
|
||||
style,
|
||||
weight = 'regular',
|
||||
}: {
|
||||
name: SymbolViewProps['name'];
|
||||
size?: number;
|
||||
color: string;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
weight?: SymbolWeight;
|
||||
}) {
|
||||
return (
|
||||
<SymbolView
|
||||
weight={weight}
|
||||
tintColor={color}
|
||||
resizeMode="scaleAspectFit"
|
||||
name={name}
|
||||
style={[
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
// Fallback for using MaterialIcons on Android and web.
|
||||
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { SymbolWeight, SymbolViewProps } from 'expo-symbols';
|
||||
import { ComponentProps } from 'react';
|
||||
import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native';
|
||||
|
||||
type IconMapping = Record<SymbolViewProps['name'], ComponentProps<typeof MaterialIcons>['name']>;
|
||||
type IconSymbolName = keyof typeof MAPPING;
|
||||
|
||||
/**
|
||||
* Add your SF Symbols to Material Icons mappings here.
|
||||
* - see Material Icons in the [Icons Directory](https://icons.expo.fyi).
|
||||
* - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app.
|
||||
*/
|
||||
const MAPPING = {
|
||||
'house.fill': 'home',
|
||||
'paperplane.fill': 'send',
|
||||
'chevron.left.forwardslash.chevron.right': 'code',
|
||||
'chevron.right': 'chevron-right',
|
||||
} as IconMapping;
|
||||
|
||||
/**
|
||||
* An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web.
|
||||
* This ensures a consistent look across platforms, and optimal resource usage.
|
||||
* Icon `name`s are based on SF Symbols and require manual mapping to Material Icons.
|
||||
*/
|
||||
export function IconSymbol({
|
||||
name,
|
||||
size = 24,
|
||||
color,
|
||||
style,
|
||||
}: {
|
||||
name: IconSymbolName;
|
||||
size?: number;
|
||||
color: string | OpaqueColorValue;
|
||||
style?: StyleProp<TextStyle>;
|
||||
weight?: SymbolWeight;
|
||||
}) {
|
||||
return <MaterialIcons color={color} size={size} name={MAPPING[name]} style={style} />;
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export default function BlurTabBarBackground() {
|
||||
return (
|
||||
<BlurView
|
||||
// System chrome material automatically adapts to the system's theme
|
||||
// and matches the native tab bar appearance on iOS.
|
||||
tint="systemChromeMaterial"
|
||||
intensity={100}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function useBottomTabOverflow() {
|
||||
return useBottomTabBarHeight();
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// This is a shim for web and Android where the tab bar is generally opaque.
|
||||
export default undefined;
|
||||
|
||||
export function useBottomTabOverflow() {
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { getCurrentUserId } from '@/utils/getCurrentUserId';
|
||||
|
||||
export function useCurrentUserId(): {userId:number|null, refetchUserId: () => void} {
|
||||
const [userId, setUserId] = useState<number | null>(null);
|
||||
const refetchUserId = React.useCallback(() => {
|
||||
getCurrentUserId().then(setUserId);
|
||||
},[])
|
||||
useEffect(() => {
|
||||
getCurrentUserId().then(setUserId);
|
||||
}, []);
|
||||
return {userId,refetchUserId};
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { useFocusEffect, useNavigation } from "expo-router";
|
||||
import React from "react";
|
||||
|
||||
|
||||
function useHideDrawerHeader() {
|
||||
|
||||
const navigation = useNavigation();
|
||||
useFocusEffect(() => {
|
||||
let drawerNav = navigation.getParent();
|
||||
const drawerNavList:any = [];
|
||||
// Collect all parent navigators
|
||||
while (drawerNav) {
|
||||
drawerNavList.push(drawerNav);
|
||||
drawerNav = drawerNav.getParent();
|
||||
}
|
||||
|
||||
drawerNavList.at(-3)?.setOptions({ headerShown: false });
|
||||
|
||||
return () => {
|
||||
drawerNavList.at(-3)?.setOptions({ headerShown: true });
|
||||
};
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
export default useHideDrawerHeader;
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
|
||||
import { useEffect } from 'react';
|
||||
import PhonePePaymentSDK from 'react-native-phonepe-pg';
|
||||
|
||||
export function usePhonepeSdk() {
|
||||
// const { data: creds, isLoading, isError } = usePhonepeCreds();
|
||||
const creds: any = {};
|
||||
const isError = false;
|
||||
const isLoading = false;
|
||||
|
||||
useEffect(() => {
|
||||
if (creds && creds.clientId && creds.clientVersion) {
|
||||
PhonePePaymentSDK.init('SANDBOX', creds.clientId, creds.clientId, true);
|
||||
}
|
||||
}, [creds]);
|
||||
|
||||
const startTransaction = async (orderId: string, token: string) => {
|
||||
try {
|
||||
const request = {
|
||||
orderId,
|
||||
token,
|
||||
merchantId: creds?.merchantId,
|
||||
paymentMode: { type: 'PAY_PAGE' }
|
||||
};
|
||||
const stringReq = JSON.stringify(request);
|
||||
const response = await PhonePePaymentSDK.startTransaction(stringReq, null);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return { startTransaction, isLoading, isError, creds };
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
/**
|
||||
* Learn more about light and dark modes:
|
||||
* https://docs.expo.dev/guides/color-schemes/
|
||||
*/
|
||||
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
|
||||
export function useThemeColor(
|
||||
props: { light?: string; dark?: string },
|
||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
||||
) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const colorFromProps = props[theme];
|
||||
|
||||
if (colorFromProps) {
|
||||
return colorFromProps;
|
||||
} else {
|
||||
return Colors[theme][colorName];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import axiosParent from 'axios';
|
||||
import { FORCE_LOGOUT_EVENT } from 'common-ui/src/lib/const-strs';
|
||||
import { DeviceEventEmitter } from 'react-native'
|
||||
// import { getJWT } from '@/hooks/useJWT';
|
||||
import { BASE_API_URL } from 'common-ui';
|
||||
import { getAuthToken } from '@/hooks/useJWT';
|
||||
|
||||
// export const API_BASE_URL = 'http://192.168.100.95:4000'; // Change to your API base URL
|
||||
// const API_BASE_URL = 'https://www.technocracy.ovh/mf'; // Change to your API base URL
|
||||
// const API_BASE_URL = 'http://10.195.26.42:4000'; // Change to your API base URL
|
||||
// const API_BASE_URL = 'http://localhost:4000/api/mobile/'; // Change to your API base URL
|
||||
// const API_BASE_URL = 'https://car-safar.com/api/mobile/'; // Change to your API base URL
|
||||
|
||||
const axios = axiosParent.create({
|
||||
baseURL: BASE_API_URL + '/api/v1',
|
||||
timeout: 60000,
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
});
|
||||
|
||||
|
||||
axios.interceptors.request.use(
|
||||
async (config) => {
|
||||
// const token = await getJWT();
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (token) {
|
||||
config.headers = config.headers || {};
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
|
||||
const status = error?.status;
|
||||
const msg = error.response?.data?.error;
|
||||
|
||||
if (status === 401 && msg.startsWith('Access denied')) {
|
||||
// Handle unauthorized access
|
||||
DeviceEventEmitter.emit(FORCE_LOGOUT_EVENT);
|
||||
}
|
||||
const message = error?.response?.data?.error;
|
||||
|
||||
if (msg) {
|
||||
// Optionally, you can attach the message to the error object or throw a new error
|
||||
const err = new Error(msg);
|
||||
// Optionally attach the original error for debugging
|
||||
(err as any).original = error;
|
||||
return Promise.reject(err);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default axios;
|
||||
55
docs/common-ui-dependency-audit.md
Normal file
55
docs/common-ui-dependency-audit.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Dependency Audit — packages/ui (common-ui)
|
||||
|
||||
Audited 2026-08-16. Packages listed below are declared in `package.json` but **never imported** anywhere in this package's own source (`src/`, `hooks/`, `index.ts`).
|
||||
|
||||
## Never used in this package
|
||||
|
||||
- `@react-native-google-signin/google-signin`
|
||||
- `@react-native-picker/picker`
|
||||
- `@react-navigation/bottom-tabs`
|
||||
- `@react-navigation/drawer`
|
||||
- `@react-navigation/elements`
|
||||
- `buffer`
|
||||
- `dayjs`
|
||||
- `expo`
|
||||
- `expo-blur`
|
||||
- `expo-dev-client`
|
||||
- `expo-device`
|
||||
- `expo-font`
|
||||
- `expo-haptics`
|
||||
- `expo-linear-gradient`
|
||||
- `expo-linking`
|
||||
- `expo-notifications`
|
||||
- `expo-router`
|
||||
- `expo-splash-screen`
|
||||
- `expo-symbols`
|
||||
- `expo-system-ui`
|
||||
- `formik`
|
||||
- `jwt-decode`
|
||||
- `react-dom`
|
||||
- `react-hook-form`
|
||||
- `react-native-gesture-handler`
|
||||
- `react-native-pager-view`
|
||||
- `react-native-phonepe-pg`
|
||||
- `react-native-razorpay`
|
||||
- `react-native-reanimated`
|
||||
- `react-native-safe-area-context`
|
||||
- `react-native-screens`
|
||||
- `react-native-tab-view`
|
||||
- `react-native-toast-message`
|
||||
- `react-native-web`
|
||||
- `react-native-webview`
|
||||
- `yup`
|
||||
|
||||
## Caveats
|
||||
|
||||
- Several of the above (`react-native-gesture-handler`, `react-native-reanimated`, `react-native-safe-area-context`, `react-native-screens`, `react-navigation/*`, `expo-router`, etc.) are runtime peer/transitive requirements of `@react-navigation/native` and the consuming Expo apps, which declare them in their own `package.json`. They are unused in `packages/ui` but not removable from the workspace if the apps rely on hoisting.
|
||||
- `expo` (base) is not imported directly but is required by the `expo-*` modules that are used.
|
||||
- Safest candidates for removal (not used by apps either): `expo-dev-client`, `react-native-phonepe-pg`.
|
||||
|
||||
## Imported but not declared (opposite problem)
|
||||
|
||||
The package imports these without listing them in `package.json`; they currently resolve via hoisting:
|
||||
|
||||
- `expo-auth-session`
|
||||
- `react-native-svg`
|
||||
182
docs/user-ui-cart-management.md
Normal file
182
docs/user-ui-cart-management.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
# Cart Management in `apps/user-ui` — Detailed Documentation
|
||||
|
||||
> How the customer cart works end-to-end: local storage, React Query hooks, the two cart types (regular + flash), pricing, slots, coupons, checkout handoff, and all edge cases.
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture Overview
|
||||
|
||||
The cart is **100% client-side / local-first**. There is **no server cart** — items live in local storage (SecureStore on native, localStorage on web via `StorageServiceCasual`), and are resolved against the cached product catalog + slot/availability data on the device.
|
||||
|
||||
```
|
||||
AddToCartDialog / ProductCard / FloatingCartBar
|
||||
│ useCartStore.setAddedToCartProduct() (trigger dialog)
|
||||
▼
|
||||
useAddToCart / useUpdateCartItem / useRemoveFromCart (cart-query-hooks.tsx)
|
||||
│ mutations write to local storage
|
||||
▼
|
||||
StorageServiceCasual (key: "cart_items" | "flash_cart_items")
|
||||
▲
|
||||
useGetCart() (query reads local storage + merges product/availability data)
|
||||
│
|
||||
cart-page.tsx / floating-cart-bar.tsx / checkout-page.tsx
|
||||
```
|
||||
|
||||
### Key files
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `hooks/cart-query-hooks.tsx` | The core: local-storage CRUD + React Query hooks (`useGetCart`, `useAddToCart`, `useUpdateCartItem`, `useRemoveFromCart`, `clearLocalCart`). |
|
||||
| `src/store/cartStore.ts` | Zustand: `addedToCartProduct` — which product opened the AddToCart dialog. |
|
||||
| `src/store/flashCartStore.ts` | Zustand: `addedFlashProduct` — which product was routed to the flash flow. |
|
||||
| `src/store/centralProductStore.ts` | Zustand: cached products + `productsById` map (from `useAllProducts`). |
|
||||
| `src/store/centralSlotStore.ts` | Zustand: slots + `productSlotsMap` (per-product availability/out-of-stock/flash). |
|
||||
| `components/cart-page.tsx` | The full cart screen (regular + flash via prop). |
|
||||
| `components/floating-cart-bar.tsx` | Collapsible cart summary bar (count, total, free-delivery progress). |
|
||||
| `src/components/AddToCartDialog.tsx` | Slot/quantity/flash chooser when adding/updating an item. |
|
||||
| `components/checkout-page.tsx` | Reads cart via URL params (`slots=`, `coupons=`, `deliveryPrice=`) — the cart is passed by **reference**, not re-read. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Data Model
|
||||
|
||||
### Local storage item (`LocalCartItem`)
|
||||
```ts
|
||||
{
|
||||
id: number, // local incrementing id (max existing id + 1)
|
||||
skuId: number, // the product/SKU id
|
||||
quantity: number,
|
||||
slotId: number, // delivery slot id; 0 = flash/unspecified
|
||||
addedAt: string, // ISO timestamp
|
||||
}
|
||||
```
|
||||
Stored under `cart_items` (regular) or `flash_cart_items` (flash) via `StorageServiceCasual` (SecureStore native / localStorage web).
|
||||
|
||||
### Computed cart (`CartData`)
|
||||
```ts
|
||||
{
|
||||
items: CartItem[],
|
||||
totalItems: number, // item count (not quantity)
|
||||
totalAmount: number, // Σ(price × quantity)
|
||||
}
|
||||
```
|
||||
`CartItem` = local item + `subtotal` (computed from `productBasic.price`).
|
||||
|
||||
### Product resolution
|
||||
`useGetCart` builds `items` by looking up each `skuId` in `productsById` (central product store) **and** requiring `productSlotsMap[skuId]` to exist — **if either is missing, the item is silently dropped** from the cart view (not deleted from storage).
|
||||
|
||||
---
|
||||
|
||||
## 3. Flow: Add to Cart
|
||||
|
||||
1. **Trigger**: `ProductCard` / home / store / search call `useCartStore.setAddedToCartProduct({ productId, product })` → the `AddToCartDialog` opens (regular) OR `useFlashCartStore.setAddedFlashProduct` (flash).
|
||||
2. **Dialog** (`AddToCartDialog.tsx`):
|
||||
- Lists **available slots** for the product (from `useSlots()` → `productSlotIdsMap`, filtered to future `freezeTime`).
|
||||
- Shows **"1 hr Delivery"** option if `productSlotsMap[id].isFlashAvailable` AND flash is enabled.
|
||||
- Quantity via `Quantifier` (step 1 — not the product's `incrementStep`!).
|
||||
- **Pre-fills** from existing cart item: if the product is already in the cart, it loads the existing quantity + slotId.
|
||||
- "Add to Cart" / "Update Item" (if already in cart) / "Remove" (if updating).
|
||||
3. **Mutation** (`useAddToCart` / `useUpdateCartItem`):
|
||||
- `addToLocalCart`: if `skuId` exists → **merge quantity** (add to existing) + optionally update slotId; else push new item.
|
||||
- Requires `slotId` (throws if null).
|
||||
- `updateLocalCartItem`: set `quantity` on the item (by local `id`).
|
||||
- `removeFromLocalCart`: filter out by local `id`.
|
||||
4. **Flash special-case**: choosing "1 hr Delivery" sets `useFlashCartStore.setAddedFlashProduct(...)` and navigates to the flash products page — it does **NOT** add to the regular cart via the dialog; the flash cart is a separate local list.
|
||||
|
||||
---
|
||||
|
||||
## 4. Flow: Cart Page
|
||||
|
||||
`cart-page.tsx` (`isFlashDelivery` prop selects `cartType`):
|
||||
- **Loading**: blocks on `useGetCart` loading + (regular only) `getCartSlots` loading.
|
||||
- **Per-item**:
|
||||
- Image, name, unit, `Quantifier` (step = product `incrementStep`).
|
||||
- Quantity to **0** → confirmation alert → remove.
|
||||
- **Slot selection** (regular only): `BottomDropdown` from `trpc.user.cart.getCartSlots({ productIds })`, filtered to future slots. Flash forces `slotId = 0`.
|
||||
- Price: regular uses `product.price`; flash uses `product.flashPrice ?? product.price`.
|
||||
- **Availability** logic per item:
|
||||
- `productSlotInfo.isOutOfStock` → unavailable "Out of Stock".
|
||||
- Flash: not flash-eligible → "Not available for flash delivery. Please remove".
|
||||
- Regular: no slots → "No delivery slots available".
|
||||
- **Auto-slot effect**: on `slotsData`/`cartItems` change, auto-selects each item's slot (keeps existing if still valid, else first upcoming; flash = 0).
|
||||
- **Coupons**: `trpc.user.coupon.getEligible` → eligible list; min-order + usage-limit filtering client-side; discount = percent (capped at `maxValue`) or flat (capped at total); applied coupon id passed to checkout via URL.
|
||||
- **Bill**: Item Total → Product Discount → Coupon Applied → Delivery Fee (0 if `finalTotal >= threshold`) → To Pay. Free-delivery nudge + "You saved ₹X" banner.
|
||||
- **Checkout handoff**: builds `slots=<slotId>:<itemId,itemId>;...` + `coupons=<id>` + `deliveryPrice=<charge>` URL params → navigates to `/home/checkout` or `/flash-delivery/checkout`.
|
||||
- **Guard**: if no available items → alert (all OOS / none flash-eligible / missing slots) and blocks checkout.
|
||||
|
||||
---
|
||||
|
||||
## 5. Flow: Floating Cart Bar
|
||||
|
||||
- Reads `useGetCart` for the active cart type; shows count + `totalCartValue` + free-delivery progress (regular vs flash thresholds).
|
||||
- **Expanded dialog**: list with `MiniQuantifier` (0 = remove), per-item slot dropdown (regular), subtotal, "Go to cart" → cart page.
|
||||
- **Slot-reassignment effect** (runs once on mount): for regular items whose `slotId` is no longer in `productSlotsMap[skuId].slots`, it **removes + re-adds** the item with the nearest slot (`removeFromCart.mutate` + `addToCartHook.addToCart`). (Note: `addToCart(item.skuId, item.quantity, nearestSlotId)` — passing `quantity` as the *quantity* arg.)
|
||||
|
||||
---
|
||||
|
||||
## 6. Checkout integration
|
||||
|
||||
- Checkout does **not** re-read local storage directly — it receives the cart items **by URL params** (`slots=`, `coupons=`, `deliveryPrice=`) from the cart page.
|
||||
- This means the checkout's item set/quantities/slots are **frozen at navigation time**; if the cart changes after, checkout uses the snapshot.
|
||||
|
||||
---
|
||||
|
||||
## 7. Edge Cases & Observations
|
||||
|
||||
### Data / storage
|
||||
1. **Missing product or slot data → item dropped from view** (`useGetCart` returns `null` for items lacking `productBasic` or `productAvailability`). The item stays in storage but is invisible — potential "phantom cart" if product is later removed from catalog.
|
||||
2. **Duplicate add merges quantity** (same `skuId` → `quantity +=`). No separate line items.
|
||||
3. **`slotId: 0` sentinel** for flash — 0 is not a real slot; code must treat 0 as "flash/unspecified".
|
||||
4. **Local `id` generation** = `max(existing ids) + 1`. Deleting the highest-id item then adding reuses ids (fine) — but concurrent adds (two rapid taps) could race since id is computed from the read-then-write sequence (no lock).
|
||||
5. **Storage failures** (`StorageServiceCasual.setItem` error) are caught by the service and return false — the mutation still resolves, but the write may not have persisted (silent data loss risk).
|
||||
|
||||
### Slot / availability
|
||||
6. **Auto-slot reassignment**: if a cart item's saved slot becomes invalid, the cart page picks the first upcoming slot silently; the floating bar instead does remove+re-add. Two different behaviors for the same situation.
|
||||
7. **Slot changes in cart page are local-only** (`selectedSlots` state) — they're NOT written back to `LocalCartItem.slotId` until checkout builds the URL. (The `useEffect` reads `cartData` slotId but the dropdown only updates `selectedSlots`.) → **If the user leaves the cart without checking out, the new slot choice is lost.**
|
||||
8. **`AddToCartDialog` requires a slot** — if a product has no future slots, the dialog can't add it (button disabled). Good, but the flash path can bypass via "1 hr Delivery".
|
||||
9. **Flash eligibility** is from `productSlotsMap[id].isFlashAvailable` — but the flash cart page also checks `flashEligibleProductIds` (derived from `products` + `productSlotsMap`). Two sources that should agree.
|
||||
10. **Out-of-stock mid-cart**: if a product becomes OOS after adding, cart page shows "Out of Stock" and excludes it from totals/checkout, but does NOT auto-remove it.
|
||||
|
||||
### Quantity / pricing
|
||||
11. **Dialog step = 1** vs **cart page step = product.incrementStep** — inconsistent stepping between the two add/update UIs. (A product with 500g increment could be set to any integer in the dialog.)
|
||||
12. **`baseTotalPrice` (coupon eligibility) uses `product.price`** (not flash price) even in flash cart — coupon min-order check may be wrong for flash.
|
||||
13. **`totalPrice` filters OOS items** — good — but the `Quantifier` still shows them; quantity edits on OOS items are allowed.
|
||||
14. **Flash pricing**: `flashPrice ?? price` — if `flashPrice` is `0` or `null`, falls back to base price; if `flashPrice` is a valid string, used. (Floating bar + cart page consistent.)
|
||||
15. **Discount cap for flat coupons**: `Math.min(flatDiscount, maxValue || totalPrice)` — reasonable; percent capped at `maxValue || Infinity`.
|
||||
|
||||
### Coupons
|
||||
16. **Usage-limit coupons are filtered out entirely** from the dropdown (`.filter(coupon => coupon.ineligibilityReason !== "Usage limit exceeded")`) — a used-up coupon disappears rather than showing "used up".
|
||||
17. **Min-order coupons show disabled** with reason — good.
|
||||
18. **Selected coupon is only applied to available items' total** — matches backend intent (coupon applies to non-OOS items).
|
||||
19. **Coupon passed by id to checkout** — checkout re-validates on the backend; client discount is a preview.
|
||||
|
||||
### Checkout / navigation
|
||||
20. **Cart is snapshotted into URL params** — long carts → long URLs; quantities/slots frozen at navigation.
|
||||
21. **`availableItems.length === 0` guard** produces specific alerts (all OOS / none flash-eligible / missing slots) — good UX.
|
||||
22. **Partial availability**: if only some items are available, checkout proceeds with only the available ones — the unavailable ones stay in the cart (not removed).
|
||||
23. **Cart type switcher** navigates between regular/flash flows, but does NOT move items — each cart type is independent.
|
||||
|
||||
### Flash-specific
|
||||
24. **Flash cart items have `slotId: 0`** — the cart page forces `selectedSlots[item.id] = 0` for flash.
|
||||
25. **Adding via "1 hr Delivery" in the dialog does NOT add to the regular cart** — it sets `addedFlashProduct` and navigates to flash products; the actual add happens on the flash page. (If the user abandons the flash page, nothing was added.)
|
||||
26. **`isFlashDeliveryEnabled` gate** — flash option only shows if the const is true.
|
||||
|
||||
### Concurrency / timing
|
||||
27. **`useGetCart` enabled only when `productsById` is populated** — before the central product store loads, the cart query is disabled (no stale empty cart flash). Good.
|
||||
28. **`refetchOnWindowFocus`** default true → returning to the app refreshes cart totals (from local storage) but prices come from cached products — price updates in the catalog reflect on next product refetch.
|
||||
29. **Floating bar slot-reassignment runs once (`[]` deps)** — on mount only; slot validity isn't re-checked on later slot-data changes.
|
||||
30. **Remove-then-add in floating bar** for invalid slots changes the local item `id` (new id) — any UI holding the old id could mismatch.
|
||||
|
||||
---
|
||||
|
||||
## 8. Potential improvements (suggestions, not required)
|
||||
|
||||
- **Persist slot changes**: write `selectedSlots` back to `LocalCartItem.slotId` so slot choices survive leaving the cart.
|
||||
- **Unify quantity step** (dialog step=1 vs cart incrementStep).
|
||||
- **Fix flash coupon min-order** to use flash price.
|
||||
- **Handle "phantom items"**: purge local cart entries whose product no longer exists (or show them as unavailable).
|
||||
- **Serialize add-to-cart writes** (mutex/queue) to avoid id races on rapid taps.
|
||||
- **Surface storage write failures** instead of silently succeeding.
|
||||
|
||||
---
|
||||
|
||||
*Documentation based on code reading of `cart-query-hooks.tsx`, `cartStore.ts`, `flashCartStore.ts`, `centralProductStore.ts`, `centralSlotStore.ts`, `cart-page.tsx`, `floating-cart-bar.tsx`, `AddToCartDialog.tsx`, and `checkout-page.tsx`.*
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { useMemo } from 'react';
|
||||
import Constants from 'expo-constants';
|
||||
|
||||
export const useIsDevMode = () => {
|
||||
return useMemo(() => {
|
||||
return !Constants.isDevice;
|
||||
}, []);
|
||||
};
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import { TouchableOpacity, Text, View } from 'react-native';
|
||||
import { tw } from '../index';
|
||||
|
||||
export const usePagination = (initialPageSize: number = 10) => {
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
const pageSize = initialPageSize;
|
||||
|
||||
const PaginationComponent = ({ totalCount }: { totalCount: number }) => {
|
||||
const totalPages = Math.ceil(totalCount / pageSize);
|
||||
|
||||
if (totalCount === 0 || totalPages <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getVisiblePages = () => {
|
||||
const pages: (number | string)[] = [];
|
||||
const maxVisible = 3;
|
||||
const half = Math.floor(maxVisible / 2);
|
||||
|
||||
let start = Math.max(1, currentPage - half);
|
||||
let end = Math.min(totalPages, start + maxVisible - 1);
|
||||
|
||||
if (end - start + 1 < maxVisible) {
|
||||
start = Math.max(1, end - maxVisible + 1);
|
||||
}
|
||||
|
||||
if (start > 1) {
|
||||
pages.push(1);
|
||||
if (start > 2) pages.push('...');
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
if (end < totalPages) {
|
||||
if (end < totalPages - 1) pages.push('...');
|
||||
pages.push(totalPages);
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
const visiblePages = getVisiblePages();
|
||||
|
||||
return (
|
||||
<View style={tw`w-full bg-white border-t border-gray-200 px-4 py-3`}>
|
||||
<View style={tw`flex-row items-center justify-between`}>
|
||||
<TouchableOpacity
|
||||
disabled={currentPage === 1}
|
||||
onPress={() => setCurrentPage(currentPage - 1)}
|
||||
style={tw`px-4 py-2 rounded-lg ${currentPage === 1 ? 'bg-gray-100' : 'bg-blue-500'}`}
|
||||
>
|
||||
<Text style={tw`${currentPage === 1 ? 'text-gray-400' : 'text-white'} text-sm font-medium`}>
|
||||
Previous
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={tw`flex-row items-center`}>
|
||||
{visiblePages.map((page, index) => (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
disabled={typeof page === 'string'}
|
||||
onPress={() => typeof page === 'number' && setCurrentPage(page)}
|
||||
style={tw`px-3 py-2 mx-1 rounded-lg ${
|
||||
typeof page === 'number' && currentPage === page ? 'bg-blue-500' : 'bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<Text
|
||||
style={tw`${
|
||||
typeof page === 'number' && currentPage === page ? 'text-white font-semibold' : 'text-gray-600'
|
||||
} text-sm`}
|
||||
>
|
||||
{page}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
disabled={currentPage === totalPages}
|
||||
onPress={() => setCurrentPage(currentPage + 1)}
|
||||
style={tw`px-4 py-2 rounded-lg ${currentPage === totalPages ? 'bg-gray-100' : 'bg-blue-500'}`}
|
||||
>
|
||||
<Text style={tw`${currentPage === totalPages ? 'text-gray-400' : 'text-white'} text-sm font-medium`}>
|
||||
Next
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return { currentPage, pageSize, PaginationComponent };
|
||||
};
|
||||
|
|
@ -1,17 +1,13 @@
|
|||
import Constants from "expo-constants";
|
||||
import { Platform } from "react-native";
|
||||
import RolesDropdown from "./src/roles-dropdown";
|
||||
import { StorageService } from "./src/services/StorageService";
|
||||
import {
|
||||
ROLE_NAMES,
|
||||
ROLE_DISPLAY_NAMES,
|
||||
ROLE_OPTIONS,
|
||||
BUSINESS_ROLE_OPTIONS,
|
||||
} from "./src/lib/constants";
|
||||
import { REFUND_STATUS } from "./src/lib/const-strs";
|
||||
import { colors, colorsType } from "./src/lib/theme-colors";
|
||||
import { theme } from "./src/theme";
|
||||
import MyButton, { MyTextButton } from "./src/components/button";
|
||||
import MyButton from "./src/components/button";
|
||||
import { useTheme, Theme } from "./hooks/theme-context";
|
||||
import MyTextInput from "./src/components/textinput";
|
||||
import BottomDialog, { ConfirmationDialog, RawBottomDialog } from "./src/components/dialog";
|
||||
|
|
@ -22,8 +18,6 @@ import MyTouchableOpacity from "./src/components/touchable-opacity";
|
|||
import BottomDropdown from "./src/components/bottom-dropdown";
|
||||
import ImageViewerURI from "./src/components/image-viewer";
|
||||
import ImageCarousel from "./src/components/ImageCarousel";
|
||||
import ImageGallery from "./src/components/ImageGallery";
|
||||
import ImageGalleryWithDelete from "./src/components/ImageGalleryWithDelete";
|
||||
import ImageUploader from "./src/components/ImageUploader";
|
||||
import ImageUploaderNeo, { ImageUploaderNeoItem, ImageUploaderNeoPayload } from "./src/components/ImageUploaderNeo";
|
||||
import ProfileImage from "./src/components/profile-image";
|
||||
|
|
@ -31,21 +25,18 @@ import Checkbox from "./src/components/checkbox";
|
|||
import AppContainer from "./src/components/app-container";
|
||||
import tw from "./src/lib/tailwind";
|
||||
import SearchBar from "./src/components/search-bar";
|
||||
import DataTable from "./src/components/data-table";
|
||||
import Quantifier from "./src/components/quantifier";
|
||||
import MiniQuantifier from "./src/components/mini-quantifier";
|
||||
import TabViewWrapper from "./src/components/tab-view";
|
||||
import MyFlatList from "./src/components/flat-list";
|
||||
import useFocusCallback from "./hooks/useFocusCallback";
|
||||
import useManualRefresh from "./hooks/useManualRefresh";
|
||||
import { useDrawerTitle } from "./hooks/useDrawerTitle";
|
||||
import { useMarkDataFetchers } from "./hooks/useMarkDataFetchers";
|
||||
import { useIsDevMode } from "./hooks/useIsDevMode";
|
||||
import { usePagination } from "./hooks/usePagination";
|
||||
import { StorageServiceCasual } from "./src/services/StorageServiceCasual";
|
||||
import DateTimePickerMod from "./src/components/date-time-picker";
|
||||
import { RefreshProvider, useRefresh } from "./src/lib/refresh-context";
|
||||
import MyStatusBar from "./src/components/MyStatusBar";
|
||||
import { InfoDialog } from "./src/components/info-dialog";
|
||||
import { updateStatusBarColor, useStatusBarStore } from "./src/lib/status-bar-store";
|
||||
|
||||
const isDevMode = Constants.executionEnvironment !== "standalone";
|
||||
|
|
@ -69,24 +60,19 @@ const isDevMode = Constants.executionEnvironment !== "standalone";
|
|||
// let BASE_API_URL = "https://raw.freshyo.in";
|
||||
let BASE_API_URL = "https://worker.freshyo.in";
|
||||
// let BASE_API_URL = "https://freshyo.technocracy.ovh";
|
||||
// let BASE_API_URL = 'http://192.168.100.111:8787';
|
||||
// let BASE_API_URL = 'http://192.168.100.120:8787';
|
||||
// let BASE_API_URL = 'http://192.168.29.176:4000';
|
||||
|
||||
// if(isDevMode) {
|
||||
// }
|
||||
|
||||
export {
|
||||
RolesDropdown,
|
||||
StorageService,
|
||||
ROLE_NAMES,
|
||||
ROLE_DISPLAY_NAMES,
|
||||
ROLE_OPTIONS,
|
||||
BUSINESS_ROLE_OPTIONS,
|
||||
colors,
|
||||
colorsType,
|
||||
theme,
|
||||
MyButton,
|
||||
MyTextButton,
|
||||
useTheme,
|
||||
Theme,
|
||||
MyTextInput,
|
||||
|
|
@ -100,8 +86,6 @@ export {
|
|||
BottomDropdown,
|
||||
ImageViewerURI,
|
||||
ImageCarousel,
|
||||
ImageGallery,
|
||||
ImageGalleryWithDelete,
|
||||
ImageUploader,
|
||||
ImageUploaderNeo,
|
||||
ImageUploaderNeoItem,
|
||||
|
|
@ -111,10 +95,8 @@ export {
|
|||
AppContainer,
|
||||
tw,
|
||||
SearchBar,
|
||||
DataTable,
|
||||
Quantifier,
|
||||
MiniQuantifier,
|
||||
TabViewWrapper,
|
||||
MyFlatList,
|
||||
useFocusCallback,
|
||||
useManualRefresh,
|
||||
|
|
@ -122,13 +104,12 @@ export {
|
|||
BASE_API_URL,
|
||||
useMarkDataFetchers,
|
||||
StorageServiceCasual,
|
||||
useIsDevMode,
|
||||
usePagination,
|
||||
REFUND_STATUS,
|
||||
DateTimePickerMod,
|
||||
RefreshProvider,
|
||||
useRefresh,
|
||||
MyStatusBar,
|
||||
InfoDialog,
|
||||
updateStatusBarColor,
|
||||
useStatusBarStore,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
import React from 'react';
|
||||
import { View, FlatList, Dimensions, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import ImageViewerURI from './image-viewer';
|
||||
|
||||
interface ImageGalleryProps {
|
||||
urls: string[];
|
||||
imageHeight?: number;
|
||||
imageWidth?: number;
|
||||
columns?: number;
|
||||
}
|
||||
|
||||
const { width: screenWidth } = Dimensions.get('window');
|
||||
|
||||
const ImageGallery: React.FC<ImageGalleryProps> = ({
|
||||
urls,
|
||||
imageHeight = 150,
|
||||
imageWidth = screenWidth / 3 - 8, // Default to 3 columns with some margin
|
||||
columns = 3,
|
||||
}) => {
|
||||
if (!urls || urls.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const numColumns = columns;
|
||||
const containerPadding = 16;
|
||||
|
||||
const renderItem = ({ item }: { item: string }) => (
|
||||
<TouchableOpacity style={[styles.imageContainer, { width: imageWidth, height: imageHeight }]}>
|
||||
<ImageViewerURI
|
||||
uri={item}
|
||||
style={[
|
||||
styles.image,
|
||||
{
|
||||
height: imageHeight,
|
||||
width: imageWidth,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { paddingHorizontal: containerPadding / 2 }]}>
|
||||
<FlatList
|
||||
data={urls}
|
||||
numColumns={numColumns}
|
||||
keyExtractor={(_, index) => index.toString()}
|
||||
renderItem={renderItem}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
imageContainer: {
|
||||
margin: 4,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
image: {
|
||||
resizeMode: 'cover',
|
||||
borderRadius: 8,
|
||||
},
|
||||
});
|
||||
|
||||
export default ImageGallery;
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
import React from 'react';
|
||||
import { View, FlatList, Dimensions, StyleSheet, TouchableOpacity, Alert } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import ImageViewerURI from './image-viewer';
|
||||
|
||||
interface ImageGalleryWithDeleteProps {
|
||||
imageUrls: string[];
|
||||
setImageUrls: (urls: string[]) => void;
|
||||
imageHeight?: number;
|
||||
imageWidth?: number;
|
||||
columns?: number;
|
||||
}
|
||||
|
||||
const { width: screenWidth } = Dimensions.get('window');
|
||||
|
||||
const ImageGalleryWithDelete: React.FC<ImageGalleryWithDeleteProps> = ({
|
||||
imageUrls,
|
||||
setImageUrls,
|
||||
imageHeight = 150,
|
||||
imageWidth = screenWidth / 3 - 8, // Default to 3 columns with some margin
|
||||
columns = 3,
|
||||
}) => {
|
||||
if (!imageUrls || imageUrls.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const numColumns = columns;
|
||||
const containerPadding = 16;
|
||||
|
||||
const handleDeleteImage = (index: number) => {
|
||||
Alert.alert(
|
||||
'Delete Image',
|
||||
'Are you sure you want to delete this image?',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Delete',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
const newUrls = imageUrls.filter((_, i) => i !== index);
|
||||
setImageUrls(newUrls);
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const renderItem = ({ item, index }: { item: string; index: number }) => (
|
||||
<View style={[styles.imageContainer, { width: imageWidth, height: imageHeight }]}>
|
||||
<ImageViewerURI
|
||||
uri={item}
|
||||
style={[
|
||||
styles.image,
|
||||
{
|
||||
height: imageHeight,
|
||||
width: imageWidth,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButton}
|
||||
onPress={() => handleDeleteImage(index)}
|
||||
>
|
||||
<Ionicons name="close-circle" size={24} color="#ff4444" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { paddingHorizontal: containerPadding / 2 }]}>
|
||||
<FlatList
|
||||
data={imageUrls}
|
||||
numColumns={numColumns}
|
||||
keyExtractor={(_, index) => index.toString()}
|
||||
renderItem={renderItem}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
imageContainer: {
|
||||
margin: 4,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
},
|
||||
image: {
|
||||
resizeMode: 'cover',
|
||||
borderRadius: 8,
|
||||
},
|
||||
deleteButton: {
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
right: 4,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
borderRadius: 12,
|
||||
padding: 2,
|
||||
},
|
||||
});
|
||||
|
||||
export default ImageGalleryWithDelete;
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import React from 'react';
|
||||
import { DataTable } from 'react-native-paper';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// Define the shape of a single token item
|
||||
interface Token {
|
||||
id: number;
|
||||
queueNum: number;
|
||||
tokenDate: string;
|
||||
doctorName: string;
|
||||
patientName: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
// Define the props for the component
|
||||
interface PaginatedDataTableProps {
|
||||
tokens: Token[];
|
||||
page: number;
|
||||
numberOfPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
from: number;
|
||||
to: number;
|
||||
totalCount: number;
|
||||
numberOfItemsPerPageList: number[];
|
||||
itemsPerPage: number;
|
||||
onItemsPerPageChange: (itemsPerPage: number) => void;
|
||||
}
|
||||
|
||||
const PaginatedDataTable: React.FC<PaginatedDataTableProps> = ({
|
||||
tokens,
|
||||
page,
|
||||
numberOfPages,
|
||||
onPageChange,
|
||||
from,
|
||||
to,
|
||||
totalCount,
|
||||
numberOfItemsPerPageList,
|
||||
itemsPerPage,
|
||||
onItemsPerPageChange,
|
||||
}) => {
|
||||
return (
|
||||
<DataTable>
|
||||
<DataTable.Header>
|
||||
<DataTable.Title style={{ flex: 0.6 }}>Q.No</DataTable.Title>
|
||||
<DataTable.Title style={{ flex: 1 }}>Date</DataTable.Title>
|
||||
<DataTable.Title style={{ flex: 1.5 }}>Doctor</DataTable.Title>
|
||||
<DataTable.Title style={{ flex: 1.5 }}>Patient</DataTable.Title>
|
||||
<DataTable.Title style={{ flex: 1 }}>Status</DataTable.Title>
|
||||
</DataTable.Header>
|
||||
|
||||
{tokens.map((token) => (
|
||||
<DataTable.Row key={token.id}>
|
||||
<DataTable.Cell style={{ flex: 0.6 }}>{token.queueNum}</DataTable.Cell>
|
||||
<DataTable.Cell style={{ flex: 1 }}>{dayjs(token.tokenDate).format('DD-MM-YY')}</DataTable.Cell>
|
||||
<DataTable.Cell style={{ flex: 1.5 }}>{token.doctorName}</DataTable.Cell>
|
||||
<DataTable.Cell style={{ flex: 1.5 }}>{token.patientName}</DataTable.Cell>
|
||||
<DataTable.Cell style={{ flex: 1 }}>{token.status}</DataTable.Cell>
|
||||
</DataTable.Row>
|
||||
))}
|
||||
|
||||
<DataTable.Pagination
|
||||
page={page}
|
||||
numberOfPages={numberOfPages}
|
||||
onPageChange={onPageChange}
|
||||
label={`${from + 1}-${to} of ${totalCount}`}
|
||||
numberOfItemsPerPageList={numberOfItemsPerPageList}
|
||||
numberOfItemsPerPage={itemsPerPage}
|
||||
onItemsPerPageChange={onItemsPerPageChange}
|
||||
showFastPaginationControls
|
||||
selectPageDropdownLabel={'Rows per page'}
|
||||
/>
|
||||
</DataTable>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaginatedDataTable;
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import React from 'react';
|
||||
import { StyleProp, ViewStyle } from 'react-native';
|
||||
import { DataTable as PaperDataTable } from 'react-native-paper';
|
||||
|
||||
// Define a generic column configuration
|
||||
export interface Column<T> {
|
||||
header: string;
|
||||
accessor: keyof T;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
render?: (row: T) => React.ReactNode;
|
||||
}
|
||||
|
||||
// Define the props for the component, using a generic type T
|
||||
interface DataTableProps<T> {
|
||||
data: T[];
|
||||
columns: Column<T>[];
|
||||
page: number;
|
||||
numberOfPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
from: number;
|
||||
to: number;
|
||||
totalCount: number;
|
||||
numberOfItemsPerPageList: number[];
|
||||
itemsPerPage: number;
|
||||
onItemsPerPageChange: (itemsPerPage: number) => void;
|
||||
}
|
||||
|
||||
const DataTable = <T extends { id: any }>({ // The data objects must have a unique 'id' property
|
||||
data,
|
||||
columns,
|
||||
page,
|
||||
numberOfPages,
|
||||
onPageChange,
|
||||
from,
|
||||
to,
|
||||
totalCount,
|
||||
numberOfItemsPerPageList,
|
||||
itemsPerPage,
|
||||
onItemsPerPageChange,
|
||||
}: DataTableProps<T>) => {
|
||||
return (
|
||||
<PaperDataTable>
|
||||
<PaperDataTable.Header>
|
||||
{columns.map((col, index) => (
|
||||
<PaperDataTable.Title key={index} style={col.style}>
|
||||
{col.header}
|
||||
</PaperDataTable.Title>
|
||||
))}
|
||||
</PaperDataTable.Header>
|
||||
|
||||
{data.map((row) => (
|
||||
<PaperDataTable.Row key={row.id}>
|
||||
{columns.map((col, index) => (
|
||||
<PaperDataTable.Cell key={index} style={col.style}>
|
||||
{col.render ? col.render(row) : String(row[col.accessor] ?? '')}
|
||||
</PaperDataTable.Cell>
|
||||
))}
|
||||
</PaperDataTable.Row>
|
||||
))}
|
||||
|
||||
<PaperDataTable.Pagination
|
||||
page={page}
|
||||
numberOfPages={numberOfPages}
|
||||
onPageChange={onPageChange}
|
||||
label={`${from + 1}-${to} of ${totalCount}`}
|
||||
numberOfItemsPerPageList={numberOfItemsPerPageList}
|
||||
numberOfItemsPerPage={itemsPerPage}
|
||||
onItemsPerPageChange={onItemsPerPageChange}
|
||||
showFastPaginationControls
|
||||
selectPageDropdownLabel={'Rows per page'}
|
||||
/>
|
||||
</PaperDataTable>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataTable;
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import React from 'react';
|
||||
import { TabView, TabViewProps, Route, TabBar } from 'react-native-tab-view';
|
||||
import { useTheme } from 'common-ui';
|
||||
|
||||
interface TabViewWrapperProps extends TabViewProps<Route> {
|
||||
// Additional props can be added here if needed
|
||||
}
|
||||
|
||||
const TabViewWrapper: React.FC<TabViewWrapperProps> = (props) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const renderTabBar = (tabBarProps: any) => (
|
||||
<TabBar
|
||||
{...tabBarProps}
|
||||
indicatorStyle={{ backgroundColor: theme.colors.brand500 }}
|
||||
style={{ backgroundColor: theme.colors.white1 }}
|
||||
labelStyle={{ color: theme.colors.black1 }}
|
||||
activeColor={theme.colors.brand500}
|
||||
inactiveColor={theme.colors.black1}
|
||||
/>
|
||||
);
|
||||
|
||||
return <TabView {...props} renderTabBar={renderTabBar} />;
|
||||
};
|
||||
|
||||
export default TabViewWrapper;
|
||||
|
|
@ -10,36 +10,4 @@ export const ROLE_NAMES = {
|
|||
DOCTOR_SECRETARY: 'doctor_secretary'
|
||||
};
|
||||
|
||||
/**
|
||||
* Constants for display names of roles for UI presentation
|
||||
*/
|
||||
export const ROLE_DISPLAY_NAMES = {
|
||||
[ROLE_NAMES.ADMIN]: 'Administrator',
|
||||
[ROLE_NAMES.GENERAL_USER]: 'General User',
|
||||
[ROLE_NAMES.HOSPITAL_ADMIN]: 'Hospital Admin',
|
||||
[ROLE_NAMES.DOCTOR]: 'Doctor',
|
||||
[ROLE_NAMES.HOSPITAL_ASSISTANT]: 'Hospital Assistant',
|
||||
[ROLE_NAMES.DOCTOR_SECRETARY]: 'Doctor Secretary'
|
||||
};
|
||||
|
||||
/**
|
||||
* Role options for dropdowns in forms
|
||||
*/
|
||||
export const ROLE_OPTIONS = [
|
||||
{ label: ROLE_DISPLAY_NAMES[ROLE_NAMES.HOSPITAL_ADMIN], value: ROLE_NAMES.HOSPITAL_ADMIN },
|
||||
{ label: ROLE_DISPLAY_NAMES[ROLE_NAMES.DOCTOR], value: ROLE_NAMES.DOCTOR },
|
||||
{ label: ROLE_DISPLAY_NAMES[ROLE_NAMES.HOSPITAL_ASSISTANT], value: ROLE_NAMES.HOSPITAL_ASSISTANT },
|
||||
{ label: ROLE_DISPLAY_NAMES[ROLE_NAMES.DOCTOR_SECRETARY], value: ROLE_NAMES.DOCTOR_SECRETARY }
|
||||
];
|
||||
|
||||
/**
|
||||
* Business role options (subset of roles that can be assigned to business users)
|
||||
*/
|
||||
export const BUSINESS_ROLE_OPTIONS = [
|
||||
{ label: ROLE_DISPLAY_NAMES[ROLE_NAMES.HOSPITAL_ADMIN], value: ROLE_NAMES.HOSPITAL_ADMIN },
|
||||
{ label: ROLE_DISPLAY_NAMES[ROLE_NAMES.DOCTOR], value: ROLE_NAMES.DOCTOR },
|
||||
{ label: ROLE_DISPLAY_NAMES[ROLE_NAMES.HOSPITAL_ASSISTANT], value: ROLE_NAMES.HOSPITAL_ASSISTANT },
|
||||
{ label: ROLE_DISPLAY_NAMES[ROLE_NAMES.DOCTOR_SECRETARY], value: ROLE_NAMES.DOCTOR_SECRETARY }
|
||||
];
|
||||
|
||||
export const GENDERS = ['Male', 'Female']
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import React from 'react'
|
||||
import {View,Text} from 'react-native';
|
||||
|
||||
interface Props {}
|
||||
|
||||
function RolesDropdown(props: Props) {
|
||||
const {} = props
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>RolesDropdown</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default RolesDropdown
|
||||
Loading…
Add table
Reference in a new issue