Compare commits
10 commits
products_s
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52a8423421 | ||
|
|
16c3b56a98 | ||
|
|
809c87e712 | ||
|
|
6ff1fd63e5 | ||
|
|
ea848992c9 | ||
|
|
5ed889a34f | ||
|
|
3ddc939a48 | ||
|
|
24252b717b | ||
|
|
78305e1670 | ||
|
|
1a3fe7826f |
976 changed files with 27748 additions and 596593 deletions
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
**/node_modules
|
||||
**/dist
|
||||
apps/users-ui/app
|
||||
apps/users-ui/src
|
||||
apps/admin-ui/app
|
||||
apps/users-ui/src
|
||||
**/package-lock.json
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -7,8 +7,8 @@ yarn-debug.log*
|
|||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
*.apk
|
||||
|
||||
**/.wrangler/*
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# Agent Instructions for Meat Farmer Monorepo
|
||||
|
||||
## Important instructions
|
||||
- Don't try to build the code or run or compile it. Just make changes and leave the rest for the user.
|
||||
- Don't run any drizzle migrations. User will handle it.
|
||||
|
||||
## Code Style Guidelines
|
||||
|
|
@ -47,4 +48,6 @@ react-native. They are available in the common-ui as MyText, MyTextInput, MyTouc
|
|||
- Database: Drizzle ORM with PostgreSQL
|
||||
|
||||
## Important Notes
|
||||
- **Do not run build, compile, or migration commands** - These should be handled manually by developers
|
||||
- Avoid running `npm run build`, `tsc`, `drizzle-kit generate`, or similar compilation/migration commands
|
||||
- Don't do anything with git. Don't do git add or git commit. That will be managed entirely by the user
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
- trpc.user.tags.getTagsByStore — apps/backend/src/trpc/apis/user-apis/apis/tags.ts
|
||||
- trpc.common.product.getAllProductsSummary — apps/backend/src/trpc/apis/common-apis/common.ts
|
||||
- remove slots from products cache
|
||||
- remove redundant product details like name, description etc from the slots api
|
||||
35
Dockerfile
35
Dockerfile
|
|
@ -1,36 +1,32 @@
|
|||
# Optimized Dockerfile for backend and fallback-ui services (project root)
|
||||
|
||||
# 1. ---- Base Bun image
|
||||
FROM oven/bun:1.3.10 AS base
|
||||
# 1. ---- Base Node image
|
||||
FROM node:20-slim AS base
|
||||
WORKDIR /app
|
||||
|
||||
# 2. ---- Pruner ----
|
||||
FROM base AS pruner
|
||||
WORKDIR /app
|
||||
# Copy config files first for better caching
|
||||
COPY package.json turbo.json ./
|
||||
COPY package.json package-lock.json turbo.json ./
|
||||
COPY apps/backend/package.json ./apps/backend/
|
||||
COPY apps/fallback-ui/package.json ./apps/fallback-ui/
|
||||
COPY packages/shared/ ./packages/shared
|
||||
COPY packages/ui/package.json ./packages/ui/
|
||||
RUN bun install -g turbo
|
||||
RUN npm install -g turbo
|
||||
COPY . .
|
||||
RUN turbo prune --scope=backend --scope=fallback-ui --scope=@packages/shared --docker
|
||||
# RUN find . -path "./node_modules" -prune -o -print
|
||||
RUN turbo prune --scope=backend --scope=fallback-ui --scope=common-ui --docker
|
||||
|
||||
# 3. ---- Builder ----
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
# Copy package files first to cache bun install
|
||||
# Copy package files first to cache npm install
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
#COPY --from=pruner /app/out/bun.lock ./bun.lock
|
||||
#RUN cat ./bun.lock
|
||||
COPY --from=pruner /app/out/package-lock.json ./package-lock.json
|
||||
COPY --from=pruner /app/turbo.json .
|
||||
RUN bun install
|
||||
RUN npm ci
|
||||
# Copy source code after dependencies are installed
|
||||
COPY --from=pruner /app/out/full/ .
|
||||
RUN bunx turbo run build --filter=fallback-ui... --filter=backend...
|
||||
RUN find . -path "./node_modules" -prune -o -print
|
||||
RUN npx turbo run build --filter=fallback-ui... --filter=backend...
|
||||
|
||||
# 4. ---- Runner ----
|
||||
FROM base AS runner
|
||||
|
|
@ -38,15 +34,12 @@ WORKDIR /app
|
|||
ENV NODE_ENV=production
|
||||
# Copy package files and install production deps
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
#COPY --from=pruner /app/out/bun.lock ./bun.lock
|
||||
RUN bun install --production
|
||||
COPY --from=pruner /app/out/package-lock.json ./package-lock.json
|
||||
RUN npm ci --production --omit=dev
|
||||
# Copy built applications
|
||||
COPY --from=builder /app/apps/backend/dist ./apps/backend/dist
|
||||
COPY --from=builder /app/apps/fallback-ui/dist ./apps/fallback-ui/dist
|
||||
COPY --from=builder /app/packages/shared ./packages/shared
|
||||
|
||||
# RUN ls -R
|
||||
RUN find . -path "./node_modules" -prune -o -print
|
||||
|
||||
EXPOSE 4000
|
||||
CMD ["bun", "apps/backend/dist/apps/backend/index.js"]
|
||||
RUN npm i -g bun
|
||||
CMD ["bun", "apps/backend/dist/index.js"]
|
||||
# CMD ["node", "apps/backend/dist/index.js"]
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
/** @type {Detox.DetoxConfig} */
|
||||
module.exports = {
|
||||
testRunner: {
|
||||
args: {
|
||||
'$0': 'jest',
|
||||
config: 'e2e/jest.config.js'
|
||||
},
|
||||
jest: {
|
||||
setupTimeout: 120000
|
||||
}
|
||||
},
|
||||
apps: {
|
||||
'ios.debug': {
|
||||
type: 'ios.app',
|
||||
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/YOUR_APP.app',
|
||||
build: 'xcodebuild -workspace ios/YOUR_APP.xcworkspace -scheme YOUR_APP -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build'
|
||||
},
|
||||
'ios.release': {
|
||||
type: 'ios.app',
|
||||
binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/YOUR_APP.app',
|
||||
build: 'xcodebuild -workspace ios/YOUR_APP.xcworkspace -scheme YOUR_APP -configuration Release -sdk iphonesimulator -derivedDataPath ios/build'
|
||||
},
|
||||
'android.debug': {
|
||||
type: 'android.apk',
|
||||
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
|
||||
build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
|
||||
reversePorts: [
|
||||
8081
|
||||
]
|
||||
},
|
||||
'android.release': {
|
||||
type: 'android.apk',
|
||||
binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
|
||||
build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release'
|
||||
}
|
||||
},
|
||||
devices: {
|
||||
simulator: {
|
||||
type: 'ios.simulator',
|
||||
device: {
|
||||
type: 'iPhone 15'
|
||||
}
|
||||
},
|
||||
attached: {
|
||||
type: 'android.attached',
|
||||
device: {
|
||||
adbName: '.*'
|
||||
}
|
||||
},
|
||||
emulator: {
|
||||
type: 'android.emulator',
|
||||
device: {
|
||||
avdName: 'Pixel_3a_API_30_x86'
|
||||
}
|
||||
}
|
||||
},
|
||||
configurations: {
|
||||
'ios.sim.debug': {
|
||||
device: 'simulator',
|
||||
app: 'ios.debug'
|
||||
},
|
||||
'ios.sim.release': {
|
||||
device: 'simulator',
|
||||
app: 'ios.release'
|
||||
},
|
||||
'android.att.debug': {
|
||||
device: 'attached',
|
||||
app: 'android.debug'
|
||||
},
|
||||
'android.att.release': {
|
||||
device: 'attached',
|
||||
app: 'android.release'
|
||||
},
|
||||
'android.emu.debug': {
|
||||
device: 'emulator',
|
||||
app: 'android.debug'
|
||||
},
|
||||
'android.emu.release': {
|
||||
device: 'emulator',
|
||||
app: 'android.release'
|
||||
}
|
||||
}
|
||||
};
|
||||
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
|
|
@ -63,21 +63,7 @@
|
|||
"backgroundColor": "#fff0f6"
|
||||
},
|
||||
"edgeToEdgeEnabled": true,
|
||||
"package": "in.freshyo.adminui",
|
||||
"intentFilters": [
|
||||
{
|
||||
"action": "VIEW",
|
||||
"autoVerify": true,
|
||||
"data": [
|
||||
{
|
||||
"scheme": "https",
|
||||
"host": "ui.freshyo.in",
|
||||
"pathPrefix": "/manage-orders/order-details"
|
||||
}
|
||||
],
|
||||
"category": ["BROWSABLE", "DEFAULT"]
|
||||
}
|
||||
]
|
||||
"package": "in.freshyo.adminui"
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
|
|
|
|||
|
|
@ -226,8 +226,9 @@ export default function Layout() {
|
|||
<Drawer.Screen name="coupons" options={{ title: "Coupons" }} />
|
||||
<Drawer.Screen name="slots" options={{ title: "Slots" }} />
|
||||
<Drawer.Screen name="vendor-snippets" options={{ title: "Vendor Snippets" }} />
|
||||
<Drawer.Screen name="stores" options={{ title: "Stores" }} />
|
||||
<Drawer.Screen name="product-tags" options={{ title: "Product Tags" }} />
|
||||
<Drawer.Screen name="stores" options={{ title: "Stores" }} />
|
||||
<Drawer.Screen name="address-management" options={{ title: "Address Management" }} />
|
||||
<Drawer.Screen name="product-tags" options={{ title: "Product Tags" }} />
|
||||
<Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
|
||||
<Drawer.Screen name="user-management" options={{ title: "User Management" }} />
|
||||
<Drawer.Screen name="send-notifications" options={{ title: "Send Notifications" }} />
|
||||
|
|
|
|||
108
apps/admin-ui/app/(drawer)/address-management/index.tsx
Normal file
108
apps/admin-ui/app/(drawer)/address-management/index.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import React, { useState } from 'react'
|
||||
import { View, Text, TouchableOpacity, ScrollView } from 'react-native'
|
||||
import { BottomDialog , tw } from 'common-ui'
|
||||
import { trpc } from '@/src/trpc-client'
|
||||
import AddressZoneForm from '@/components/AddressZoneForm'
|
||||
import AddressPlaceForm from '@/components/AddressPlaceForm'
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons'
|
||||
|
||||
const AddressManagement: React.FC = () => {
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [dialogType, setDialogType] = useState<'zone' | 'place' | null>(null)
|
||||
const [expandedZones, setExpandedZones] = useState<Set<number>>(new Set())
|
||||
|
||||
const { data: zones, refetch: refetchZones } = trpc.admin.address.getZones.useQuery()
|
||||
const { data: areas, refetch: refetchAreas } = trpc.admin.address.getAreas.useQuery()
|
||||
|
||||
const createZone = trpc.admin.address.createZone.useMutation({
|
||||
onSuccess: () => {
|
||||
refetchZones()
|
||||
setDialogOpen(false)
|
||||
},
|
||||
})
|
||||
|
||||
const createArea = trpc.admin.address.createArea.useMutation({
|
||||
onSuccess: () => {
|
||||
refetchAreas()
|
||||
setDialogOpen(false)
|
||||
},
|
||||
})
|
||||
|
||||
const handleAddZone = () => {
|
||||
setDialogType('zone')
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleAddPlace = () => {
|
||||
setDialogType('place')
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
const toggleZone = (zoneId: number) => {
|
||||
setExpandedZones(prev => {
|
||||
const newSet = new Set(prev)
|
||||
if (newSet.has(zoneId)) {
|
||||
newSet.delete(zoneId)
|
||||
} else {
|
||||
newSet.add(zoneId)
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
}
|
||||
|
||||
const groupedAreas = areas?.reduce((acc, area) => {
|
||||
if (area.zoneId) {
|
||||
if (!acc[area.zoneId]) acc[area.zoneId] = []
|
||||
acc[area.zoneId].push(area)
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<number, typeof areas[0][]>) || {}
|
||||
|
||||
const unzonedAreas = areas?.filter(a => !a.zoneId) || []
|
||||
|
||||
return (
|
||||
<View style={tw`flex-1 bg-white`}>
|
||||
<View style={tw`flex-row justify-between p-4`}>
|
||||
<TouchableOpacity style={tw`bg-blue1 px-4 py-2 rounded`} onPress={handleAddZone}>
|
||||
<Text style={tw`text-white`}>Add Zone</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={tw`bg-green1 px-4 py-2 rounded`} onPress={handleAddPlace}>
|
||||
<Text style={tw`text-white`}>Add Place</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView style={tw`flex-1 p-4`}>
|
||||
{zones?.map(zone => (
|
||||
<View key={zone.id} style={tw`mb-4 border border-gray-300 rounded`}>
|
||||
<TouchableOpacity style={tw`flex-row items-center p-3 bg-gray-100`} onPress={() => toggleZone(zone.id)}>
|
||||
<Text style={tw`flex-1 text-lg font-semibold`}>{zone.zoneName}</Text>
|
||||
<MaterialIcons name={expandedZones.has(zone.id) ? 'expand-less' : 'expand-more'} size={24} />
|
||||
</TouchableOpacity>
|
||||
{expandedZones.has(zone.id) && (
|
||||
<View style={tw`p-3`}>
|
||||
{groupedAreas[zone.id]?.map(area => (
|
||||
<Text key={area.id} style={tw`text-base mb-1`}>- {area.placeName}</Text>
|
||||
)) || <Text style={tw`text-gray-500`}>No places in this zone</Text>}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
|
||||
<View style={tw`mt-6`}>
|
||||
<Text style={tw`text-xl font-bold mb-2`}>Unzoned Places</Text>
|
||||
{unzonedAreas.map(area => (
|
||||
<Text key={area.id} style={tw`text-base mb-1`}>- {area.placeName}</Text>
|
||||
))}
|
||||
{unzonedAreas.length === 0 && <Text style={tw`text-gray-500`}>No unzoned places</Text>}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<BottomDialog open={dialogOpen} onClose={() => setDialogOpen(false)}>
|
||||
{dialogType === 'zone' && <AddressZoneForm onSubmit={createZone.mutate} onClose={() => setDialogOpen(false)} />}
|
||||
{dialogType === 'place' && <AddressPlaceForm onSubmit={createArea.mutate} onClose={() => setDialogOpen(false)} />}
|
||||
</BottomDialog>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddressManagement
|
||||
|
|
@ -9,20 +9,6 @@ export default function CreateCoupon() {
|
|||
const router = useRouter();
|
||||
const createCoupon = trpc.admin.coupon.create.useMutation();
|
||||
const createReservedCoupon = trpc.admin.coupon.createReservedCoupon.useMutation();
|
||||
const { refetch: refetchCoupons } = trpc.admin.coupon.getAll.useInfiniteQuery(
|
||||
{ limit: 20, search: '' },
|
||||
{
|
||||
enabled: false,
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
}
|
||||
)
|
||||
const { refetch: refetchReservedCoupons } = trpc.admin.coupon.getReservedCoupons.useInfiniteQuery(
|
||||
{ limit: 20, search: '' },
|
||||
{
|
||||
enabled: false,
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
}
|
||||
)
|
||||
|
||||
const handleCreateCoupon = (values: any) => {
|
||||
console.log('Form values:', values); // Debug log
|
||||
|
|
@ -41,9 +27,7 @@ export default function CreateCoupon() {
|
|||
if (isLoading) return; // Prevent double submission
|
||||
|
||||
mutation.mutate(payload, {
|
||||
onSuccess: async () => {
|
||||
await refetchCoupons()
|
||||
await refetchReservedCoupons()
|
||||
onSuccess: () => {
|
||||
Alert.alert('Success', `${isReservedCoupon ? 'Reserved coupon' : 'Coupon'} created successfully`, [
|
||||
{ text: 'OK', onPress: () => router.back() }
|
||||
]);
|
||||
|
|
@ -62,4 +46,4 @@ export default function CreateCoupon() {
|
|||
/>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,21 +12,7 @@ export default function EditCoupon() {
|
|||
const { id } = useLocalSearchParams();
|
||||
const couponId = parseInt(id as string);
|
||||
|
||||
const { data: coupon, isLoading, refetch } = trpc.admin.coupon.getById.useQuery({ id: couponId });
|
||||
const { refetch: refetchCoupons } = trpc.admin.coupon.getAll.useInfiniteQuery(
|
||||
{ limit: 20, search: '' },
|
||||
{
|
||||
enabled: false,
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
}
|
||||
)
|
||||
const { refetch: refetchReservedCoupons } = trpc.admin.coupon.getReservedCoupons.useInfiniteQuery(
|
||||
{ limit: 20, search: '' },
|
||||
{
|
||||
enabled: false,
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
}
|
||||
)
|
||||
const { data: coupon, isLoading } = trpc.admin.coupon.getById.useQuery({ id: couponId });
|
||||
const updateCoupon = trpc.admin.coupon.update.useMutation();
|
||||
|
||||
const handleUpdateCoupon = (values: CreateCouponPayload & { isReservedCoupon?: boolean }) => {
|
||||
|
|
@ -38,10 +24,7 @@ export default function EditCoupon() {
|
|||
delete updates.targetUsers;
|
||||
|
||||
updateCoupon.mutate({ id: couponId, updates }, {
|
||||
onSuccess: async () => {
|
||||
await refetch()
|
||||
await refetchCoupons()
|
||||
await refetchReservedCoupons()
|
||||
onSuccess: () => {
|
||||
Alert.alert('Success', 'Coupon updated successfully', [
|
||||
{ text: 'OK', onPress: () => router.back() }
|
||||
]);
|
||||
|
|
@ -97,4 +80,4 @@ export default function EditCoupon() {
|
|||
/>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,14 @@ import { trpc } from '../../../src/trpc-client';
|
|||
import { useRouter } from 'expo-router';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
|
||||
type ConstantFormData = Record<string, any>
|
||||
interface ConstantFormData {
|
||||
constants: ConstantItem[];
|
||||
}
|
||||
|
||||
interface ConstantItem {
|
||||
key: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
const CONST_LABELS: Record<string, string> = {
|
||||
minRegularOrderValue: 'Minimum Regular Order Value',
|
||||
|
|
@ -30,45 +37,23 @@ const CONST_LABELS: Record<string, string> = {
|
|||
supportEmail: 'Support Email',
|
||||
};
|
||||
|
||||
const CONST_VISIBILITY: Record<string, boolean> = {
|
||||
minRegularOrderValue: true,
|
||||
freeDeliveryThreshold: true,
|
||||
deliveryCharge: true,
|
||||
flashFreeDeliveryThreshold: true,
|
||||
flashDeliveryCharge: true,
|
||||
platformFeePercent: true,
|
||||
taxRate: false,
|
||||
minOrderAmountForCoupon: true,
|
||||
maxCouponDiscount: false,
|
||||
flashDeliverySlotId: true,
|
||||
readableOrderId: false,
|
||||
versionNum: true,
|
||||
playStoreUrl: true,
|
||||
appStoreUrl: true,
|
||||
popularItems: true,
|
||||
allItemsOrder: true,
|
||||
isFlashDeliveryEnabled: true,
|
||||
supportMobile: true,
|
||||
supportEmail: true,
|
||||
tester: false,
|
||||
};
|
||||
|
||||
interface ConstantInputProps {
|
||||
constantKey: string;
|
||||
value: any;
|
||||
constant: ConstantItem;
|
||||
setFieldValue: (field: string, value: any) => void;
|
||||
index: number;
|
||||
router: any;
|
||||
}
|
||||
|
||||
const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFieldValue, router }) => {
|
||||
const fieldName = constantKey
|
||||
const ConstantInput: React.FC<ConstantInputProps> = ({ constant, setFieldValue, index, router }) => {
|
||||
const fieldName = `constants.${index}.value`;
|
||||
|
||||
// Special handling for popularItems - show navigation button instead of input
|
||||
if (constantKey === 'popularItems') {
|
||||
if (constant.key === 'popularItems') {
|
||||
console.log('key is allItemsOrder')
|
||||
return (
|
||||
<View>
|
||||
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
|
||||
{CONST_LABELS[constantKey] || constantKey}
|
||||
{CONST_LABELS[constant.key] || constant.key}
|
||||
</MyText>
|
||||
<MyTouchableOpacity
|
||||
onPress={() => router.push('/(drawer)/customize-app/popular-items')}
|
||||
|
|
@ -76,7 +61,7 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFi
|
|||
>
|
||||
<MaterialIcons name="edit" size={20} color="#3b82f6" style={tw`mr-2`} />
|
||||
<MyText style={tw`text-blue-700 font-medium`}>
|
||||
Manage Popular Items ({Array.isArray(value) ? value.length : 0} items)
|
||||
Manage Popular Items ({Array.isArray(constant.value) ? constant.value.length : 0} items)
|
||||
</MyText>
|
||||
<MaterialIcons name="chevron-right" size={20} color="#3b82f6" style={tw`ml-2`} />
|
||||
</MyTouchableOpacity>
|
||||
|
|
@ -85,12 +70,12 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFi
|
|||
}
|
||||
|
||||
// Special handling for allItemsOrder - show navigation button instead of input
|
||||
if (constantKey === 'allItemsOrder') {
|
||||
if (constant.key === 'allItemsOrder') {
|
||||
|
||||
return (
|
||||
<View>
|
||||
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
|
||||
{CONST_LABELS[constantKey] || constantKey}
|
||||
{CONST_LABELS[constant.key] || constant.key}
|
||||
</MyText>
|
||||
<MyTouchableOpacity
|
||||
onPress={() => router.push('/(drawer)/customize-app/all-items-order')}
|
||||
|
|
@ -98,7 +83,7 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFi
|
|||
>
|
||||
<MaterialIcons name="reorder" size={20} color="#16a34a" style={tw`mr-2`} />
|
||||
<MyText style={tw`text-green-700 font-medium`}>
|
||||
Manage All Visible Items ({Array.isArray(value) ? value.length : 0} items)
|
||||
Manage All Visible Items ({Array.isArray(constant.value) ? constant.value.length : 0} items)
|
||||
</MyText>
|
||||
<MaterialIcons name="chevron-right" size={20} color="#16a34a" style={tw`ml-2`} />
|
||||
</MyTouchableOpacity>
|
||||
|
|
@ -107,20 +92,20 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFi
|
|||
}
|
||||
|
||||
// Handle boolean values - show checkbox
|
||||
if (typeof value === 'boolean') {
|
||||
if (typeof constant.value === 'boolean') {
|
||||
return (
|
||||
<View>
|
||||
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
|
||||
{CONST_LABELS[constantKey] || constantKey}
|
||||
{CONST_LABELS[constant.key] || constant.key}
|
||||
</MyText>
|
||||
<View style={tw`flex-row items-center`}>
|
||||
<Checkbox
|
||||
checked={value}
|
||||
onPress={() => setFieldValue(fieldName, !value)}
|
||||
checked={constant.value}
|
||||
onPress={() => setFieldValue(fieldName, !constant.value)}
|
||||
size={28}
|
||||
/>
|
||||
<MyText style={tw`ml-3 text-gray-700`}>
|
||||
{value ? 'Enabled' : 'Disabled'}
|
||||
{constant.value ? 'Enabled' : 'Disabled'}
|
||||
</MyText>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -128,11 +113,11 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFi
|
|||
}
|
||||
|
||||
// Handle different value types
|
||||
if (typeof value === 'number') {
|
||||
if (typeof constant.value === 'number') {
|
||||
return (
|
||||
<MyTextInput
|
||||
topLabel={CONST_LABELS[constantKey] || constantKey}
|
||||
value={value.toString()}
|
||||
topLabel={CONST_LABELS[constant.key] || constant.key}
|
||||
value={constant.value.toString()}
|
||||
onChangeText={(value) => {
|
||||
const numValue = parseFloat(value);
|
||||
setFieldValue(fieldName, isNaN(numValue) ? 0 : numValue);
|
||||
|
|
@ -143,11 +128,11 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFi
|
|||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (Array.isArray(constant.value)) {
|
||||
return (
|
||||
<MyTextInput
|
||||
topLabel={CONST_LABELS[constantKey] || constantKey}
|
||||
value={value.join(', ')}
|
||||
topLabel={CONST_LABELS[constant.key] || constant.key}
|
||||
value={constant.value.join(', ')}
|
||||
onChangeText={(value) => {
|
||||
const arrayValue = value.split(',').map(s => s.trim()).filter(s => s.length > 0);
|
||||
setFieldValue(fieldName, arrayValue);
|
||||
|
|
@ -160,12 +145,9 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFi
|
|||
// Default to string
|
||||
return (
|
||||
<MyTextInput
|
||||
topLabel={CONST_LABELS[constantKey] || constantKey}
|
||||
// value={value === null || value === undefined ? '' : String(value)}
|
||||
value={value}
|
||||
onChangeText={(value) => {
|
||||
setFieldValue(fieldName, value)
|
||||
}}
|
||||
topLabel={CONST_LABELS[constant.key] || constant.key}
|
||||
value={String(constant.value)}
|
||||
onChangeText={(value) => setFieldValue(fieldName, value)}
|
||||
placeholder="Enter value"
|
||||
/>
|
||||
);
|
||||
|
|
@ -179,13 +161,10 @@ export default function CustomizeApp() {
|
|||
|
||||
const handleSubmit = (values: ConstantFormData) => {
|
||||
// Filter out constants that haven't changed
|
||||
const changedConstants = (constants || []).filter((constant) => {
|
||||
const nextValue = values[constant.key]
|
||||
return JSON.stringify(nextValue) !== JSON.stringify(constant.value)
|
||||
}).map((constant) => ({
|
||||
key: constant.key,
|
||||
value: values[constant.key],
|
||||
}))
|
||||
const changedConstants = values.constants.filter((constant, index) => {
|
||||
const original = constants?.[index];
|
||||
return original && JSON.stringify(constant.value) !== JSON.stringify(original.value);
|
||||
});
|
||||
|
||||
if (changedConstants.length === 0) {
|
||||
Alert.alert('No Changes', 'No constants were modified.');
|
||||
|
|
@ -223,10 +202,9 @@ export default function CustomizeApp() {
|
|||
);
|
||||
}
|
||||
|
||||
const initialValues: ConstantFormData = constants.reduce((acc, constant) => {
|
||||
acc[constant.key] = constant.value ?? ''
|
||||
return acc
|
||||
}, {} as ConstantFormData)
|
||||
const initialValues: ConstantFormData = {
|
||||
constants: constants.map(c => ({ key: c.key, value: c.value ?? '' } as ConstantItem)),
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
|
@ -241,22 +219,11 @@ export default function CustomizeApp() {
|
|||
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||
{({ handleSubmit, values, setFieldValue }) => (
|
||||
<View>
|
||||
{constants.map((constant) => {
|
||||
if (!CONST_VISIBILITY[constant.key]) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View key={constant.key} style={tw`mb-4`}>
|
||||
<ConstantInput
|
||||
constantKey={constant.key}
|
||||
value={values[constant.key]}
|
||||
setFieldValue={setFieldValue}
|
||||
router={router}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
{values.constants.map((constant, index) => (
|
||||
<View key={constant.key} style={tw`mb-4`}>
|
||||
<ConstantInput constant={constant} setFieldValue={setFieldValue} index={index} router={router} />
|
||||
</View>
|
||||
))}
|
||||
|
||||
<MyTouchableOpacity
|
||||
onPress={() => handleSubmit()}
|
||||
|
|
@ -273,4 +240,4 @@ export default function CustomizeApp() {
|
|||
</View>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,9 +21,6 @@ export default function CreateBanner() {
|
|||
};
|
||||
|
||||
const createBannerMutation = trpc.admin.banner.createBanner.useMutation();
|
||||
const { refetch: refetchBanners } = trpc.admin.banner.getBanners.useQuery(undefined, {
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const handleSubmit = async (values: BannerFormData, imageUrl?: string) => {
|
||||
if (!imageUrl) {
|
||||
|
|
@ -42,7 +39,6 @@ export default function CreateBanner() {
|
|||
redirectUrl: values.redirectUrl || undefined,
|
||||
});
|
||||
|
||||
await refetchBanners()
|
||||
Alert.alert('Success', 'Banner created successfully', [
|
||||
{
|
||||
text: 'OK',
|
||||
|
|
@ -83,4 +79,4 @@ export default function CreateBanner() {
|
|||
</View>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -31,9 +31,6 @@ export default function EditBanner() {
|
|||
const {data: bannerData } = trpc.admin.banner.getBanner.useQuery({
|
||||
id: parseInt(bannerId)
|
||||
});
|
||||
const { refetch: refetchBanners } = trpc.admin.banner.getBanners.useQuery(undefined, {
|
||||
enabled: false,
|
||||
});
|
||||
const [banner, setBanner] = useState<typeof bannerData>(undefined);
|
||||
|
||||
|
||||
|
|
@ -103,7 +100,6 @@ export default function EditBanner() {
|
|||
redirectUrl: values.redirectUrl || undefined,
|
||||
});
|
||||
|
||||
await refetchBanners()
|
||||
Alert.alert('Success', 'Banner updated successfully', [
|
||||
{
|
||||
text: 'OK',
|
||||
|
|
@ -164,4 +160,4 @@ export default function EditBanner() {
|
|||
</View>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -464,4 +464,4 @@ export default function DashboardBanners() {
|
|||
</View>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ export default function Dashboard() {
|
|||
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
title: 'Manage Orderss',
|
||||
title: 'Manage Orders',
|
||||
icon: 'shopping-bag',
|
||||
description: 'View and manage customer orders',
|
||||
route: '/(drawer)/manage-orders',
|
||||
|
|
@ -176,6 +176,15 @@ export default function Dashboard() {
|
|||
iconColor: '#F97316',
|
||||
iconBg: '#FFEDD5',
|
||||
},
|
||||
{
|
||||
title: 'Address Management',
|
||||
icon: 'location-on',
|
||||
description: 'Manage service areas',
|
||||
route: '/(drawer)/address-management',
|
||||
category: 'settings',
|
||||
iconColor: '#EAB308',
|
||||
iconBg: '#FEF9C3',
|
||||
},
|
||||
{
|
||||
title: 'App Constants',
|
||||
icon: 'settings-applications',
|
||||
|
|
@ -285,4 +294,4 @@ export default function Dashboard() {
|
|||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -63,8 +63,7 @@ export default function OrderDetails() {
|
|||
onSuccess: (result) => {
|
||||
Alert.alert(
|
||||
"Success",
|
||||
`Refund initiated successfully!\n\nAmount: `
|
||||
// `Refund initiated successfully!\n\nAmount: ₹${result.amount}\nStatus: ${result.status}`
|
||||
`Refund initiated successfully!\n\nAmount: ₹${result.amount}\nStatus: ${result.status}`
|
||||
);
|
||||
setInitiateRefundDialogOpen(false);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import { View, Alert } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { AppContainer, MyText, tw, type ImageUploaderNeoItem } from 'common-ui';
|
||||
import { AppContainer, MyText, tw } from 'common-ui';
|
||||
import TagForm from '@/src/components/TagForm';
|
||||
import { useCreateTag } from '@/src/api-hooks/tag.api';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
||||
|
||||
interface TagFormData {
|
||||
tagName: string;
|
||||
|
|
@ -15,51 +15,50 @@ interface TagFormData {
|
|||
|
||||
export default function AddTag() {
|
||||
const router = useRouter();
|
||||
const createTag = trpc.admin.product.createProductTag.useMutation();
|
||||
const { refetch: refetchTags } = trpc.admin.product.getProductTags.useQuery(undefined, {
|
||||
enabled: false,
|
||||
});
|
||||
const { mutate: createTag, isPending: isCreating } = useCreateTag();
|
||||
const { data: storesData } = trpc.admin.store.getStores.useQuery();
|
||||
const { upload, isUploading } = useUploadToObjectStorage();
|
||||
|
||||
const handleSubmit = async (values: TagFormData, images: ImageUploaderNeoItem[], _removedExisting: boolean) => {
|
||||
try {
|
||||
let imageUrl: string | null | undefined;
|
||||
let uploadUrls: string[] = []
|
||||
const handleSubmit = (values: TagFormData, image?: { uri?: string }) => {
|
||||
const formData = new FormData();
|
||||
|
||||
const newImage = images.find((image) => image.mimeType !== null)
|
||||
if (newImage) {
|
||||
const response = await fetch(newImage.imgUrl)
|
||||
const blob = await response.blob()
|
||||
const result = await upload({
|
||||
images: [{ blob, mimeType: newImage.mimeType || 'image/jpeg' }],
|
||||
contextString: 'tags',
|
||||
})
|
||||
imageUrl = result.keys[0]
|
||||
uploadUrls = result.presignedUrls
|
||||
}
|
||||
|
||||
await createTag.mutateAsync({
|
||||
tagName: values.tagName,
|
||||
tagDescription: values.tagDescription || undefined,
|
||||
imageUrl,
|
||||
isDashboardTag: values.isDashboardTag,
|
||||
relatedStores: values.relatedStores,
|
||||
uploadUrls,
|
||||
})
|
||||
|
||||
await refetchTags()
|
||||
Alert.alert('Success', 'Tag created successfully', [
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => router.back(),
|
||||
},
|
||||
])
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.message || 'Failed to create tag'
|
||||
Alert.alert('Error', errorMessage)
|
||||
// Add text fields
|
||||
formData.append('tagName', values.tagName);
|
||||
if (values.tagDescription) {
|
||||
formData.append('tagDescription', values.tagDescription);
|
||||
}
|
||||
}
|
||||
formData.append('isDashboardTag', values.isDashboardTag.toString());
|
||||
|
||||
// Add related stores
|
||||
formData.append('relatedStores', JSON.stringify(values.relatedStores));
|
||||
|
||||
// Add image if uploaded
|
||||
if (image?.uri) {
|
||||
const filename = image.uri.split('/').pop() || 'image.jpg';
|
||||
const match = /\.(\w+)$/.exec(filename);
|
||||
const type = match ? `image/${match[1]}` : 'image/jpeg';
|
||||
|
||||
formData.append('image', {
|
||||
uri: image.uri,
|
||||
name: filename,
|
||||
type,
|
||||
} as any);
|
||||
}
|
||||
|
||||
createTag(formData, {
|
||||
onSuccess: (data) => {
|
||||
Alert.alert('Success', 'Tag created successfully', [
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => router.back(),
|
||||
},
|
||||
]);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
const errorMessage = error.message || 'Failed to create tag';
|
||||
Alert.alert('Error', errorMessage);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const initialValues: TagFormData = {
|
||||
tagName: '',
|
||||
|
|
@ -77,10 +76,10 @@ export default function AddTag() {
|
|||
mode="create"
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
isLoading={createTag.isPending || isUploading}
|
||||
stores={storesData?.stores.map((store: { id: number; name: string }) => ({ id: store.id, name: store.name })) || []}
|
||||
isLoading={isCreating}
|
||||
stores={storesData?.stores.map(store => ({ id: store.id, name: store.name })) || []}
|
||||
/>
|
||||
</View>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import { View, Alert } from 'react-native';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { AppContainer, MyText, tw, type ImageUploaderNeoItem } from 'common-ui';
|
||||
import { AppContainer, MyText, tw } from 'common-ui';
|
||||
import TagForm from '@/src/components/TagForm';
|
||||
import { useGetTag, useUpdateTag } from '@/src/api-hooks/tag.api';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
||||
|
||||
interface TagFormData {
|
||||
tagName: string;
|
||||
|
|
@ -19,60 +19,53 @@ export default function EditTag() {
|
|||
const { tagId } = useLocalSearchParams<{ tagId: string }>();
|
||||
const tagIdNum = tagId ? parseInt(tagId) : null;
|
||||
|
||||
const { data: tagData, isLoading: isLoadingTag, error: tagError } = trpc.admin.product.getProductTagById.useQuery(
|
||||
{ id: tagIdNum || 0 },
|
||||
{ enabled: !!tagIdNum }
|
||||
)
|
||||
const { refetch: refetchTags } = trpc.admin.product.getProductTags.useQuery(undefined, {
|
||||
enabled: false,
|
||||
});
|
||||
const updateTag = trpc.admin.product.updateProductTag.useMutation();
|
||||
const { data: tagData, isLoading: isLoadingTag, error: tagError } = useGetTag(tagIdNum!);
|
||||
const { mutate: updateTag, isPending: isUpdating } = useUpdateTag();
|
||||
const { data: storesData } = trpc.admin.store.getStores.useQuery();
|
||||
const { upload, isUploading } = useUploadToObjectStorage();
|
||||
|
||||
const handleSubmit = async (values: TagFormData, images: ImageUploaderNeoItem[], removedExisting: boolean) => {
|
||||
const handleSubmit = (values: TagFormData, image?: { uri?: string }) => {
|
||||
if (!tagIdNum) return;
|
||||
|
||||
try {
|
||||
let imageUrl: string | null | undefined
|
||||
let uploadUrls: string[] = []
|
||||
const formData = new FormData();
|
||||
|
||||
const newImage = images.find((image) => image.mimeType !== null)
|
||||
if (newImage) {
|
||||
const response = await fetch(newImage.imgUrl)
|
||||
const blob = await response.blob()
|
||||
const result = await upload({
|
||||
images: [{ blob, mimeType: newImage.mimeType || 'image/jpeg' }],
|
||||
contextString: 'tags',
|
||||
})
|
||||
imageUrl = result.keys[0]
|
||||
uploadUrls = result.presignedUrls
|
||||
} else if (removedExisting) {
|
||||
imageUrl = null
|
||||
}
|
||||
|
||||
await updateTag.mutateAsync({
|
||||
id: tagIdNum,
|
||||
tagName: values.tagName,
|
||||
tagDescription: values.tagDescription || undefined,
|
||||
imageUrl,
|
||||
isDashboardTag: values.isDashboardTag,
|
||||
relatedStores: values.relatedStores,
|
||||
uploadUrls,
|
||||
})
|
||||
|
||||
await refetchTags()
|
||||
Alert.alert('Success', 'Tag updated successfully', [
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => router.back(),
|
||||
},
|
||||
])
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.message || 'Failed to update tag'
|
||||
Alert.alert('Error', errorMessage)
|
||||
// Add text fields
|
||||
formData.append('tagName', values.tagName);
|
||||
if (values.tagDescription) {
|
||||
formData.append('tagDescription', values.tagDescription);
|
||||
}
|
||||
}
|
||||
formData.append('isDashboardTag', values.isDashboardTag.toString());
|
||||
|
||||
// Add related stores
|
||||
formData.append('relatedStores', JSON.stringify(values.relatedStores));
|
||||
|
||||
// Add image if uploaded
|
||||
if (image?.uri) {
|
||||
const filename = image.uri.split('/').pop() || 'image.jpg';
|
||||
const match = /\.(\w+)$/.exec(filename);
|
||||
const type = match ? `image/${match[1]}` : 'image/jpeg';
|
||||
|
||||
formData.append('image', {
|
||||
uri: image.uri,
|
||||
name: filename,
|
||||
type,
|
||||
} as any);
|
||||
}
|
||||
|
||||
updateTag({ id: tagIdNum, formData }, {
|
||||
onSuccess: (data) => {
|
||||
Alert.alert('Success', 'Tag updated successfully', [
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => router.back(),
|
||||
},
|
||||
]);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
const errorMessage = error.message || 'Failed to update tag';
|
||||
Alert.alert('Error', errorMessage);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoadingTag) {
|
||||
return (
|
||||
|
|
@ -99,7 +92,7 @@ export default function EditTag() {
|
|||
tagName: tag.tagName,
|
||||
tagDescription: tag.tagDescription || '',
|
||||
isDashboardTag: tag.isDashboardTag,
|
||||
relatedStores: Array.isArray(tag.relatedStores) ? tag.relatedStores : [],
|
||||
relatedStores: tag.relatedStores || [],
|
||||
existingImageUrl: tag.imageUrl || undefined,
|
||||
};
|
||||
|
||||
|
|
@ -113,10 +106,10 @@ export default function EditTag() {
|
|||
initialValues={initialValues}
|
||||
existingImageUrl={tag.imageUrl || undefined}
|
||||
onSubmit={handleSubmit}
|
||||
isLoading={updateTag.isPending || isUploading}
|
||||
stores={storesData?.stores.map((store: { id: number; name: string }) => ({ id: store.id, name: store.name })) || []}
|
||||
isLoading={isUpdating}
|
||||
stores={storesData?.stores.map(store => ({ id: store.id, name: store.name })) || []}
|
||||
/>
|
||||
</View>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,20 +5,10 @@ import { useRouter } from 'expo-router';
|
|||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { tw, MyText, useManualRefresh, useMarkDataFetchers, MyFlatList } from 'common-ui';
|
||||
import { TagMenu } from '@/src/components/TagMenu';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
|
||||
interface TagItemData {
|
||||
id: number;
|
||||
tagName: string;
|
||||
tagDescription: string | null;
|
||||
imageUrl: string | null;
|
||||
isDashboardTag: boolean;
|
||||
relatedStores?: unknown;
|
||||
createdAt: string | Date;
|
||||
}
|
||||
import { useGetTags, Tag } from '@/src/api-hooks/tag.api';
|
||||
|
||||
interface TagItemProps {
|
||||
item: TagItemData;
|
||||
item: Tag;
|
||||
onDeleteSuccess: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +60,7 @@ const TagHeader: React.FC<TagHeaderProps> = ({ onAddNewTag }) => (
|
|||
|
||||
export default function ProductTags() {
|
||||
const router = useRouter();
|
||||
const { data: tagsData, isLoading, error, refetch } = trpc.admin.product.getProductTags.useQuery();
|
||||
const { data: tagsData, isLoading, error, refetch } = useGetTags();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const tags = tagsData?.tags || [];
|
||||
|
|
@ -139,4 +129,4 @@ export default function ProductTags() {
|
|||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +1,62 @@
|
|||
import React from 'react';
|
||||
import { Alert } from 'react-native';
|
||||
import { AppContainer, ImageUploaderNeoPayload } from 'common-ui';
|
||||
import { AppContainer } from 'common-ui';
|
||||
import ProductForm from '@/src/components/ProductForm';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
||||
import { useCreateProduct, CreateProductPayload } from '@/src/api-hooks/product.api';
|
||||
|
||||
export default function AddProduct() {
|
||||
const createProduct = trpc.admin.product.createProduct.useMutation();
|
||||
const { refetch: refetchProducts } = trpc.admin.product.getProducts.useQuery(undefined, {
|
||||
enabled: false,
|
||||
});
|
||||
const { upload, isUploading } = useUploadToObjectStorage();
|
||||
const { mutate: createProduct, isPending: isCreating } = useCreateProduct();
|
||||
|
||||
const handleSubmit = async (values: any, images: ImageUploaderNeoPayload[]) => {
|
||||
try {
|
||||
let uploadUrls: string[] = [];
|
||||
const handleSubmit = (values: any, images?: { uri?: string, mimeType?: string }[]) => {
|
||||
const payload: CreateProductPayload = {
|
||||
name: values.name,
|
||||
shortDescription: values.shortDescription,
|
||||
longDescription: values.longDescription,
|
||||
unitId: parseInt(values.unitId),
|
||||
storeId: parseInt(values.storeId),
|
||||
price: parseFloat(values.price),
|
||||
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
|
||||
incrementStep: 1,
|
||||
productQuantity: values.productQuantity || 1,
|
||||
};
|
||||
|
||||
if (images.length > 0) {
|
||||
const blobs = await Promise.all(
|
||||
images.map(async (img) => {
|
||||
const response = await fetch(img.url);
|
||||
const blob = await response.blob();
|
||||
return { blob, mimeType: img.mimeType || 'image/jpeg' };
|
||||
})
|
||||
);
|
||||
|
||||
const result = await upload({ images: blobs, contextString: 'product_info' });
|
||||
uploadUrls = result.presignedUrls;
|
||||
const formData = new FormData();
|
||||
Object.entries(payload).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
formData.append(key, value as string);
|
||||
}
|
||||
});
|
||||
|
||||
await createProduct.mutateAsync({
|
||||
name: values.name,
|
||||
shortDescription: values.shortDescription,
|
||||
longDescription: values.longDescription,
|
||||
unitId: parseInt(values.unitId),
|
||||
storeId: parseInt(values.storeId),
|
||||
price: parseFloat(values.price),
|
||||
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
|
||||
incrementStep: 1,
|
||||
productQuantity: values.productQuantity || 1,
|
||||
isSuspended: values.isSuspended || false,
|
||||
isFlashAvailable: values.isFlashAvailable || false,
|
||||
flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : undefined,
|
||||
uploadUrls,
|
||||
tagIds: values.tagIds || [],
|
||||
// Append tag IDs
|
||||
if (values.tagIds && values.tagIds.length > 0) {
|
||||
values.tagIds.forEach((tagId: number) => {
|
||||
formData.append('tagIds', tagId.toString());
|
||||
});
|
||||
|
||||
await refetchProducts();
|
||||
Alert.alert('Success', 'Product created successfully!');
|
||||
} catch (error: any) {
|
||||
Alert.alert('Error', error.message || 'Failed to create product');
|
||||
}
|
||||
|
||||
// Append images
|
||||
if (images) {
|
||||
images.forEach((image, index) => {
|
||||
if (image.uri) {
|
||||
formData.append('images', {
|
||||
uri: image.uri,
|
||||
name: `image-${index}.jpg`,
|
||||
// type: 'image/jpeg',
|
||||
type: image.mimeType as any,
|
||||
} as any);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createProduct(formData, {
|
||||
onSuccess: (data) => {
|
||||
Alert.alert('Success', 'Product created successfully!');
|
||||
// Reset form or navigate
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Alert.alert('Error', error.message || 'Failed to create product');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const initialValues = {
|
||||
|
|
@ -75,8 +81,9 @@ export default function AddProduct() {
|
|||
mode="create"
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
isLoading={createProduct.isPending || isUploading}
|
||||
isLoading={isCreating}
|
||||
existingImages={[]}
|
||||
/>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ import { tw, AppContainer, MyText, useMarkDataFetchers, BottomDialog, ImageUploa
|
|||
import { MaterialIcons, FontAwesome5, Ionicons, Feather, MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
||||
import { Formik } from 'formik';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { BlurView } from 'expo-blur';
|
||||
|
|
@ -24,9 +23,10 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
|
|||
const [adminResponse, setAdminResponse] = useState('');
|
||||
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
|
||||
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
|
||||
const [uploadUrls, setUploadUrls] = useState<string[]>([]);
|
||||
|
||||
const respondToReview = trpc.admin.product.respondToReview.useMutation();
|
||||
const { upload } = useUploadToObjectStorage();
|
||||
const generateUploadUrls = trpc.user.fileUpload.generateUploadUrls.useMutation();
|
||||
|
||||
const handleImagePick = usePickImage({
|
||||
setFile: async (assets: any) => {
|
||||
|
|
@ -62,16 +62,37 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
|
|||
|
||||
const handleSubmit = async (adminResponse: string) => {
|
||||
try {
|
||||
const { keys, presignedUrls } = await upload({
|
||||
images: selectedImages,
|
||||
const mimeTypes = selectedImages.map(s => s.mimeType);
|
||||
const { uploadUrls: generatedUrls } = await generateUploadUrls.mutateAsync({
|
||||
contextString: 'review',
|
||||
mimeTypes,
|
||||
});
|
||||
const keys = generatedUrls.map(url => {
|
||||
const u = new URL(url);
|
||||
const rawKey = u.pathname.replace(/^\/+/, "");
|
||||
const decodedKey = decodeURIComponent(rawKey);
|
||||
const parts = decodedKey.split('/');
|
||||
parts.shift();
|
||||
return parts.join('/');
|
||||
});
|
||||
setUploadUrls(generatedUrls);
|
||||
|
||||
for (let i = 0; i < generatedUrls.length; i++) {
|
||||
const uploadUrl = generatedUrls[i];
|
||||
const { blob, mimeType } = selectedImages[i];
|
||||
const uploadResponse = await fetch(uploadUrl, {
|
||||
method: 'PUT',
|
||||
body: blob,
|
||||
headers: { 'Content-Type': mimeType },
|
||||
});
|
||||
if (!uploadResponse.ok) throw new Error(`Upload failed with status ${uploadResponse.status}`);
|
||||
}
|
||||
|
||||
await respondToReview.mutateAsync({
|
||||
reviewId,
|
||||
adminResponse,
|
||||
adminResponseImages: keys,
|
||||
uploadUrls: presignedUrls,
|
||||
uploadUrls: generatedUrls,
|
||||
});
|
||||
|
||||
Alert.alert('Success', 'Response submitted');
|
||||
|
|
@ -79,7 +100,9 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
|
|||
setAdminResponse('');
|
||||
setSelectedImages([]);
|
||||
setDisplayImages([]);
|
||||
setUploadUrls([]);
|
||||
} catch (error:any) {
|
||||
|
||||
Alert.alert('Error', error.message || 'Failed to submit response.');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,74 +1,95 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { View, Alert } from 'react-native';
|
||||
import { View, Text, Alert } from 'react-native';
|
||||
import { useLocalSearchParams } from 'expo-router';
|
||||
import { AppContainer, useManualRefresh, MyText, tw, ImageUploaderNeoItem, ImageUploaderNeoPayload } from 'common-ui';
|
||||
import { AppContainer, useManualRefresh, MyText, tw } from 'common-ui';
|
||||
import ProductForm, { ProductFormRef } from '@/src/components/ProductForm';
|
||||
import { useUpdateProduct } from '@/src/api-hooks/product.api';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
||||
|
||||
export default function EditProduct() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const productId = Number(id);
|
||||
const productFormRef = useRef<ProductFormRef>(null);
|
||||
|
||||
// const { data: product, isLoading: isFetching, refetch } = useGetProduct(productId);
|
||||
const { data: product, isLoading: isFetching, refetch } = trpc.admin.product.getProductById.useQuery(
|
||||
{ id: productId },
|
||||
{ enabled: !!productId }
|
||||
);
|
||||
const { refetch: refetchProducts } = trpc.admin.product.getProducts.useQuery(undefined, {
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const updateProduct = trpc.admin.product.updateProduct.useMutation();
|
||||
const { upload, isUploading } = useUploadToObjectStorage();
|
||||
//
|
||||
const { mutate: updateProduct, isPending: isUpdating } = useUpdateProduct();
|
||||
|
||||
useManualRefresh(() => refetch());
|
||||
|
||||
const handleSubmit = async (values: any, images: ImageUploaderNeoPayload[], imagesToDelete: string[]) => {
|
||||
try {
|
||||
// New images have mimeType !== null, existing images have mimeType === null
|
||||
const newImages = images.filter(img => img.mimeType !== null);
|
||||
let uploadUrls: string[] = [];
|
||||
|
||||
if (newImages.length > 0) {
|
||||
const blobs = await Promise.all(
|
||||
newImages.map(async (img) => {
|
||||
const response = await fetch(img.url);
|
||||
const blob = await response.blob();
|
||||
return { blob, mimeType: img.mimeType || 'image/jpeg' };
|
||||
})
|
||||
);
|
||||
|
||||
const result = await upload({ images: blobs, contextString: 'product_info' });
|
||||
uploadUrls = result.presignedUrls;
|
||||
}
|
||||
|
||||
await updateProduct.mutateAsync({
|
||||
id: productId,
|
||||
const handleSubmit = (values: any, newImages?: { uri?: string }[], imagesToDelete?: string[]) => {
|
||||
const payload = {
|
||||
name: values.name,
|
||||
shortDescription: values.shortDescription,
|
||||
longDescription: values.longDescription,
|
||||
unitId: parseInt(values.unitId),
|
||||
storeId: parseInt(values.storeId),
|
||||
price: parseFloat(values.price),
|
||||
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
|
||||
incrementStep: 1,
|
||||
productQuantity: values.productQuantity || 1,
|
||||
isSuspended: values.isSuspended || false,
|
||||
isFlashAvailable: values.isFlashAvailable || false,
|
||||
flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : null,
|
||||
uploadUrls,
|
||||
imagesToDelete,
|
||||
tagIds: values.tagIds || [],
|
||||
});
|
||||
price: parseFloat(values.price),
|
||||
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
|
||||
incrementStep: 1,
|
||||
productQuantity: values.productQuantity || 1,
|
||||
deals: values.deals?.filter((deal: any) =>
|
||||
deal.quantity && deal.price && deal.validTill
|
||||
).map((deal: any) => ({
|
||||
quantity: parseInt(deal.quantity),
|
||||
price: parseFloat(deal.price),
|
||||
validTill: deal.validTill instanceof Date
|
||||
? deal.validTill.toISOString().split('T')[0]
|
||||
: deal.validTill, // Convert Date to YYYY-MM-DD string
|
||||
})),
|
||||
tagIds: values.tagIds,
|
||||
};
|
||||
|
||||
await refetch();
|
||||
await refetchProducts();
|
||||
Alert.alert('Success', 'Product updated successfully!');
|
||||
productFormRef.current?.clearImages();
|
||||
} catch (error: any) {
|
||||
Alert.alert('Error', error.message || 'Failed to update product');
|
||||
|
||||
const formData = new FormData();
|
||||
Object.entries(payload).forEach(([key, value]) => {
|
||||
if (key === 'deals' && Array.isArray(value)) {
|
||||
formData.append(key, JSON.stringify(value));
|
||||
} else if (key === 'tagIds' && Array.isArray(value)) {
|
||||
value.forEach(tagId => {
|
||||
formData.append('tagIds', tagId.toString());
|
||||
});
|
||||
} else if (value !== undefined && value !== null) {
|
||||
formData.append(key, value as string);
|
||||
}
|
||||
});
|
||||
|
||||
// Add new images
|
||||
if (newImages && newImages.length > 0) {
|
||||
newImages.forEach((image, index) => {
|
||||
if (image.uri) {
|
||||
const fileName = image.uri.split('/').pop() || `image_${index}.jpg`;
|
||||
formData.append('images', {
|
||||
uri: image.uri,
|
||||
name: fileName,
|
||||
type: 'image/jpeg',
|
||||
} as any);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add images to delete
|
||||
if (imagesToDelete && imagesToDelete.length > 0) {
|
||||
formData.append('imagesToDelete', JSON.stringify(imagesToDelete));
|
||||
}
|
||||
|
||||
updateProduct(
|
||||
{ id: productId, formData },
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
Alert.alert('Success', 'Product updated successfully!');
|
||||
// Clear newly added images after successful update
|
||||
productFormRef.current?.clearImages();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Alert.alert('Error', error.message || 'Failed to update product');
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (isFetching) {
|
||||
|
|
@ -91,13 +112,7 @@ export default function EditProduct() {
|
|||
);
|
||||
}
|
||||
|
||||
const productData = product.product;
|
||||
|
||||
const existingImages: ImageUploaderNeoItem[] = (productData.images || []).map((url) => ({
|
||||
imgUrl: url,
|
||||
mimeType: null,
|
||||
}));
|
||||
const existingImageKeys = productData.imageKeys || [];
|
||||
const productData = product.product; // The API returns { product: Product }
|
||||
|
||||
const initialValues = {
|
||||
name: productData.name,
|
||||
|
|
@ -110,7 +125,7 @@ export default function EditProduct() {
|
|||
deals: productData.deals?.map(deal => ({
|
||||
quantity: deal.quantity,
|
||||
price: deal.price,
|
||||
validTill: deal.validTill ? new Date(deal.validTill) : null,
|
||||
validTill: deal.validTill ? new Date(deal.validTill) : null, // Convert to Date object
|
||||
})) || [{ quantity: '', price: '', validTill: null }],
|
||||
tagIds: productData.tags?.map((tag: any) => tag.id) || [],
|
||||
isSuspended: productData.isSuspended || false,
|
||||
|
|
@ -126,10 +141,9 @@ export default function EditProduct() {
|
|||
mode="edit"
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
isLoading={updateProduct.isPending || isUploading}
|
||||
existingImages={existingImages}
|
||||
existingImageKeys={existingImageKeys}
|
||||
isLoading={isUpdating}
|
||||
existingImages={productData.images || []}
|
||||
/>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
|||
import { AppContainer, MyText, tw, MyButton, useManualRefresh, MyTextInput, SearchBar, useMarkDataFetchers } from 'common-ui';
|
||||
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import type { AdminProduct } from '@packages/shared';
|
||||
import { Product } from '@/src/api-hooks/product.api';
|
||||
|
||||
type FilterType = 'all' | 'in-stock' | 'out-of-stock';
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ export default function Products() {
|
|||
|
||||
|
||||
// const handleToggleStock = (product: any) => {
|
||||
const handleToggleStock = (product: Pick<AdminProduct, 'id' | 'name' | 'isOutOfStock'>) => {
|
||||
const handleToggleStock = (product: Pick<Product, 'id' | 'name' | 'isOutOfStock'>) => {
|
||||
const action = product.isOutOfStock ? 'mark as in stock' : 'mark as out of stock';
|
||||
Alert.alert(
|
||||
'Update Stock Status',
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import {
|
|||
} from 'common-ui';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
|
|
@ -27,6 +26,12 @@ interface User {
|
|||
isEligibleForNotif: boolean;
|
||||
}
|
||||
|
||||
const extractKeyFromUrl = (url: string): string => {
|
||||
const u = new URL(url);
|
||||
const rawKey = u.pathname.replace(/^\/+/, '');
|
||||
return decodeURIComponent(rawKey);
|
||||
};
|
||||
|
||||
export default function SendNotifications() {
|
||||
const router = useRouter();
|
||||
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
|
||||
|
|
@ -41,7 +46,8 @@ export default function SendNotifications() {
|
|||
search: searchQuery,
|
||||
});
|
||||
|
||||
const { uploadSingle } = useUploadToObjectStorage();
|
||||
// Generate upload URLs mutation
|
||||
const generateUploadUrls = trpc.user.fileUpload.generateUploadUrls.useMutation();
|
||||
|
||||
// Send notification mutation
|
||||
const sendNotification = trpc.admin.user.sendNotification.useMutation({
|
||||
|
|
@ -121,8 +127,28 @@ export default function SendNotifications() {
|
|||
|
||||
// Upload image if selected
|
||||
if (selectedImage) {
|
||||
const { key } = await uploadSingle(selectedImage.blob, selectedImage.mimeType, 'notification');
|
||||
imageUrl = key;
|
||||
const { uploadUrls } = await generateUploadUrls.mutateAsync({
|
||||
contextString: 'notification',
|
||||
mimeTypes: [selectedImage.mimeType],
|
||||
});
|
||||
|
||||
if (uploadUrls.length > 0) {
|
||||
const uploadUrl = uploadUrls[0];
|
||||
imageUrl = extractKeyFromUrl(uploadUrl);
|
||||
|
||||
// Upload image
|
||||
const uploadResponse = await fetch(uploadUrl, {
|
||||
method: 'PUT',
|
||||
body: selectedImage.blob,
|
||||
headers: {
|
||||
'Content-Type': selectedImage.mimeType,
|
||||
},
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error(`Upload failed with status ${uploadResponse.status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send notification
|
||||
|
|
|
|||
|
|
@ -9,14 +9,10 @@ export default function AddStore() {
|
|||
const router = useRouter();
|
||||
|
||||
const createStoreMutation = trpc.admin.store.createStore.useMutation();
|
||||
const { refetch: refetchStores } = trpc.admin.store.getStores.useQuery(undefined, {
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const handleSubmit = (values: StoreFormData) => {
|
||||
createStoreMutation.mutate(values, {
|
||||
onSuccess: async (data) => {
|
||||
await refetchStores();
|
||||
onSuccess: (data) => {
|
||||
Alert.alert('Success', data.message);
|
||||
router.push('/stores' as any); // Navigate back to stores list
|
||||
},
|
||||
|
|
@ -39,4 +35,4 @@ export default function AddStore() {
|
|||
</View>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
|
|||
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 MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
|
||||
export interface BannerFormData {
|
||||
|
|
@ -53,10 +52,10 @@ export default function BannerForm({
|
|||
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
|
||||
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
|
||||
|
||||
const { uploadSingle } = useUploadToObjectStorage();
|
||||
const generateUploadUrls = trpc.common.generateUploadUrls.useMutation();
|
||||
|
||||
// Fetch products for dropdown
|
||||
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery();
|
||||
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
|
||||
const products = productsData?.products || [];
|
||||
|
||||
|
||||
|
|
@ -98,11 +97,33 @@ export default function BannerForm({
|
|||
let imageUrl: string | undefined;
|
||||
|
||||
if (selectedImages.length > 0) {
|
||||
// Generate upload URLs
|
||||
const mimeTypes = selectedImages.map(s => s.mimeType);
|
||||
const { uploadUrls } = await generateUploadUrls.mutateAsync({
|
||||
contextString: 'store', // Using 'store' for now
|
||||
mimeTypes,
|
||||
});
|
||||
|
||||
// Upload image
|
||||
const uploadUrl = uploadUrls[0];
|
||||
const { blob, mimeType } = selectedImages[0];
|
||||
const { presignedUrl } = await uploadSingle(blob, mimeType, 'store');
|
||||
imageUrl = presignedUrl;
|
||||
|
||||
const uploadResponse = await fetch(uploadUrl, {
|
||||
method: 'PUT',
|
||||
body: blob,
|
||||
headers: {
|
||||
'Content-Type': mimeType,
|
||||
},
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error(`Upload failed with status ${uploadResponse.status}`);
|
||||
}
|
||||
|
||||
imageUrl = uploadUrl;
|
||||
}
|
||||
|
||||
// Call onSubmit with form values and imageUrl
|
||||
await onSubmit(values, imageUrl);
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
|
|
@ -235,4 +256,4 @@ export default function BannerForm({
|
|||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
}
|
||||
197
apps/admin-ui/components/FullOrderView.tsx
Normal file
197
apps/admin-ui/components/FullOrderView.tsx
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
import React from 'react';
|
||||
import { View, ScrollView, Dimensions } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { MyText, tw } from 'common-ui';
|
||||
import { trpc } from '../src/trpc-client';
|
||||
|
||||
interface FullOrderViewProps {
|
||||
orderId: number;
|
||||
}
|
||||
|
||||
export const FullOrderView: React.FC<FullOrderViewProps> = ({ orderId }) => {
|
||||
const { data: order, isLoading, error } = trpc.admin.order.getFullOrder.useQuery({ orderId });
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={tw`p-6`}>
|
||||
<MyText style={tw`text-center text-gray-600`}>Loading order details...</MyText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !order) {
|
||||
return (
|
||||
<View style={tw`p-6`}>
|
||||
<MyText style={tw`text-center text-red-600`}>Failed to load order details</MyText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const totalAmount = order.items.reduce((sum, item) => sum + item.amount, 0);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={[tw`flex-1`, { maxHeight: Dimensions.get('window').height * 0.8 }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={tw`p-6`}>
|
||||
<MyText style={tw`text-2xl font-bold text-gray-800 mb-6`}>Order #{order.readableId}</MyText>
|
||||
|
||||
{/* Customer Information */}
|
||||
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
|
||||
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Customer Details</MyText>
|
||||
<View style={tw`space-y-2`}>
|
||||
<View style={tw`flex-row justify-between`}>
|
||||
<MyText style={tw`text-gray-600`}>Name:</MyText>
|
||||
<MyText style={tw`font-medium`}>{order.customerName}</MyText>
|
||||
</View>
|
||||
{order.customerEmail && (
|
||||
<View style={tw`flex-row justify-between`}>
|
||||
<MyText style={tw`text-gray-600`}>Email:</MyText>
|
||||
<MyText style={tw`font-medium`}>{order.customerEmail}</MyText>
|
||||
</View>
|
||||
)}
|
||||
<View style={tw`flex-row justify-between`}>
|
||||
<MyText style={tw`text-gray-600`}>Mobile:</MyText>
|
||||
<MyText style={tw`font-medium`}>{order.customerMobile}</MyText>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Delivery Address */}
|
||||
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
|
||||
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Delivery Address</MyText>
|
||||
<View style={tw`space-y-1`}>
|
||||
<MyText style={tw`text-gray-800`}>{order.address.line1}</MyText>
|
||||
{order.address.line2 && <MyText style={tw`text-gray-800`}>{order.address.line2}</MyText>}
|
||||
<MyText style={tw`text-gray-800`}>
|
||||
{order.address.city}, {order.address.state} - {order.address.pincode}
|
||||
</MyText>
|
||||
<MyText style={tw`text-gray-800`}>Phone: {order.address.phone}</MyText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Order Details */}
|
||||
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
|
||||
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Order Details</MyText>
|
||||
<View style={tw`space-y-2`}>
|
||||
<View style={tw`flex-row justify-between`}>
|
||||
<MyText style={tw`text-gray-600`}>Order Date:</MyText>
|
||||
<MyText style={tw`font-medium`}>
|
||||
{new Date(order.createdAt).toLocaleDateString('en-IN', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</MyText>
|
||||
</View>
|
||||
<View style={tw`flex-row justify-between`}>
|
||||
<MyText style={tw`text-gray-600`}>Payment Method:</MyText>
|
||||
<MyText style={tw`font-medium`}>
|
||||
{order.isCod ? 'Cash on Delivery' : 'Online Payment'}
|
||||
</MyText>
|
||||
</View>
|
||||
{order.slotInfo && (
|
||||
<View style={tw`flex-row justify-between`}>
|
||||
<MyText style={tw`text-gray-600`}>Delivery Slot:</MyText>
|
||||
<MyText style={tw`font-medium`}>
|
||||
{new Date(order.slotInfo.time).toLocaleDateString('en-IN', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</MyText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Items */}
|
||||
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
|
||||
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Items ({order.items.length})</MyText>
|
||||
{order.items.map((item, index) => (
|
||||
<View key={item.id} style={tw`flex-row items-center py-3 ${index !== order.items.length - 1 ? 'border-b border-gray-100' : ''}`}>
|
||||
<View style={tw`flex-1`}>
|
||||
<MyText style={tw`font-medium text-gray-800`} numberOfLines={2}>
|
||||
{item.productName}
|
||||
</MyText>
|
||||
<MyText style={tw`text-sm text-gray-600`}>
|
||||
Qty: {item.quantity} {item.unit} × ₹{parseFloat(item.price.toString()).toFixed(2)}
|
||||
</MyText>
|
||||
</View>
|
||||
<MyText style={tw`font-semibold text-gray-800`}>₹{item.amount.toFixed(2)}</MyText>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Payment Information */}
|
||||
{(order.payment || order.paymentInfo) && (
|
||||
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
|
||||
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Payment Information</MyText>
|
||||
{order.payment && (
|
||||
<View style={tw`space-y-2 mb-3`}>
|
||||
<MyText style={tw`text-sm font-medium text-gray-700`}>Payment Details:</MyText>
|
||||
<View style={tw`flex-row justify-between`}>
|
||||
<MyText style={tw`text-gray-600`}>Status:</MyText>
|
||||
<MyText style={tw`font-medium capitalize`}>{order.payment.status}</MyText>
|
||||
</View>
|
||||
<View style={tw`flex-row justify-between`}>
|
||||
<MyText style={tw`text-gray-600`}>Gateway:</MyText>
|
||||
<MyText style={tw`font-medium`}>{order.payment.gateway}</MyText>
|
||||
</View>
|
||||
<View style={tw`flex-row justify-between`}>
|
||||
<MyText style={tw`text-gray-600`}>Order ID:</MyText>
|
||||
<MyText style={tw`font-medium`}>{order.payment.merchantOrderId}</MyText>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{order.paymentInfo && (
|
||||
<View style={tw`space-y-2`}>
|
||||
<MyText style={tw`text-sm font-medium text-gray-700`}>Payment Info:</MyText>
|
||||
<View style={tw`flex-row justify-between`}>
|
||||
<MyText style={tw`text-gray-600`}>Status:</MyText>
|
||||
<MyText style={tw`font-medium capitalize`}>{order.paymentInfo.status}</MyText>
|
||||
</View>
|
||||
<View style={tw`flex-row justify-between`}>
|
||||
<MyText style={tw`text-gray-600`}>Gateway:</MyText>
|
||||
<MyText style={tw`font-medium`}>{order.paymentInfo.gateway}</MyText>
|
||||
</View>
|
||||
<View style={tw`flex-row justify-between`}>
|
||||
<MyText style={tw`text-gray-600`}>Order ID:</MyText>
|
||||
<MyText style={tw`font-medium`}>{order.paymentInfo.merchantOrderId}</MyText>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* User Notes */}
|
||||
{order.userNotes && (
|
||||
<View style={tw`bg-blue-50 rounded-xl p-4 mb-4`}>
|
||||
<MyText style={tw`text-lg font-semibold text-gray-800 mb-2`}>Customer Notes</MyText>
|
||||
<MyText style={tw`text-gray-700`}>{order.userNotes}</MyText>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Admin Notes */}
|
||||
{order.adminNotes && (
|
||||
<View style={tw`bg-yellow-50 rounded-xl p-4 mb-4`}>
|
||||
<MyText style={tw`text-lg font-semibold text-gray-800 mb-2`}>Admin Notes</MyText>
|
||||
<MyText style={tw`text-gray-700`}>{order.adminNotes}</MyText>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Total */}
|
||||
<View style={tw`bg-blue-50 rounded-xl p-4`}>
|
||||
<View style={tw`flex-row justify-between items-center`}>
|
||||
<MyText style={tw`text-xl font-bold text-gray-800`}>Total Amount</MyText>
|
||||
<MyText style={tw`text-2xl font-bold text-blue-600`}>₹{parseFloat(order.totalAmount.toString()).toFixed(2)}</MyText>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
|
@ -6,7 +6,6 @@ import { MyTextInput, BottomDropdown, MyText, tw, ImageUploader } from 'common-u
|
|||
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';
|
||||
|
||||
export interface StoreFormData {
|
||||
name: string;
|
||||
|
|
@ -60,19 +59,14 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
|
|||
});
|
||||
}, [initialValues, initialSelectedProducts]);
|
||||
|
||||
const existingImageUrls = useMemo(
|
||||
() => (formInitialValues.imageUrl ? [formInitialValues.imageUrl] : []),
|
||||
[formInitialValues.imageUrl]
|
||||
)
|
||||
|
||||
const staffOptions = staffData?.staff.map((staff: { id: number; name: string }) => ({
|
||||
const staffOptions = staffData?.staff.map(staff => ({
|
||||
label: staff.name,
|
||||
value: staff.id,
|
||||
})) || [];
|
||||
|
||||
|
||||
|
||||
const { uploadSingle, isUploading } = useUploadToObjectStorage();
|
||||
const generateUploadUrls = trpc.common.generateUploadUrls.useMutation();
|
||||
|
||||
const handleImagePick = usePickImage({
|
||||
setFile: async (assets: any) => {
|
||||
|
|
@ -119,11 +113,39 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
|
|||
let imageUrl: string | undefined;
|
||||
|
||||
if (selectedImages.length > 0) {
|
||||
const { blob, mimeType } = selectedImages[0];
|
||||
const { presignedUrl } = await uploadSingle(blob, mimeType, 'store');
|
||||
imageUrl = presignedUrl;
|
||||
// Generate upload URLs
|
||||
const mimeTypes = selectedImages.map(s => s.mimeType);
|
||||
const { uploadUrls } = await generateUploadUrls.mutateAsync({
|
||||
contextString: 'store',
|
||||
mimeTypes,
|
||||
});
|
||||
|
||||
// Upload images
|
||||
for (let i = 0; i < uploadUrls.length; i++) {
|
||||
const uploadUrl = uploadUrls[i];
|
||||
const { blob, mimeType } = selectedImages[i];
|
||||
|
||||
const uploadResponse = await fetch(uploadUrl, {
|
||||
method: 'PUT',
|
||||
body: blob,
|
||||
headers: {
|
||||
'Content-Type': mimeType,
|
||||
},
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error(`Upload failed with status ${uploadResponse.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract key from first upload URL
|
||||
// const u = new URL(uploadUrls[0]);
|
||||
// const rawKey = u.pathname.replace(/^\/+/, "");
|
||||
// imageUrl = decodeURIComponent(rawKey);
|
||||
imageUrl = uploadUrls[0];
|
||||
}
|
||||
|
||||
// Submit form with imageUrl
|
||||
onSubmit({ ...values, imageUrl });
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
|
|
@ -173,25 +195,20 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
|
|||
<MyText style={tw`text-sm font-bold text-gray-700 mb-3 uppercase tracking-wider`}>Store Image</MyText>
|
||||
<ImageUploader
|
||||
images={displayImages}
|
||||
existingImageUrls={existingImageUrls}
|
||||
existingImageUrls={formInitialValues.imageUrl ? [formInitialValues.imageUrl] : []}
|
||||
onAddImage={handleImagePick}
|
||||
onRemoveImage={handleRemoveImage}
|
||||
onRemoveExistingImage={() =>
|
||||
setFormInitialValues((prev) => ({
|
||||
...prev,
|
||||
imageUrl: undefined,
|
||||
}))
|
||||
}
|
||||
onRemoveExistingImage={() => setFormInitialValues({ ...formInitialValues, imageUrl: undefined })}
|
||||
allowMultiple={false}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={submit}
|
||||
disabled={isLoading || isUploading}
|
||||
style={tw`px-4 py-2 rounded-lg shadow-lg items-center mt-2 ${isLoading || isUploading ? 'bg-gray-400' : 'bg-blue-500'}`}
|
||||
disabled={isLoading || generateUploadUrls.isPending}
|
||||
style={tw`px-4 py-2 rounded-lg shadow-lg items-center mt-2 ${isLoading || generateUploadUrls.isPending ? 'bg-gray-400' : 'bg-blue-500'}`}
|
||||
>
|
||||
<MyText style={tw`text-white text-lg font-bold`}>
|
||||
{isUploading ? 'Uploading...' : isLoading ? (mode === 'create' ? 'Creating...' : 'Updating...') : (mode === 'create' ? 'Create Store' : 'Update Store')}
|
||||
{generateUploadUrls.isPending ? 'Uploading...' : isLoading ? (mode === 'create' ? 'Creating...' : 'Updating...') : (mode === 'create' ? 'Create Store' : 'Update Store')}
|
||||
</MyText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
@ -203,4 +220,4 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
|
|||
|
||||
StoreForm.displayName = 'StoreForm';
|
||||
|
||||
export default StoreForm;
|
||||
export default StoreForm;
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/** @type {import('@jest/types').Config.InitialOptions} */
|
||||
module.exports = {
|
||||
rootDir: '..',
|
||||
testMatch: ['<rootDir>/e2e/**/*.test.js'],
|
||||
testTimeout: 120000,
|
||||
maxWorkers: 1,
|
||||
globalSetup: 'detox/runners/jest/globalSetup',
|
||||
globalTeardown: 'detox/runners/jest/globalTeardown',
|
||||
reporters: ['detox/runners/jest/reporter'],
|
||||
testEnvironment: 'detox/runners/jest/testEnvironment',
|
||||
verbose: true,
|
||||
};
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
describe('Example', () => {
|
||||
beforeAll(async () => {
|
||||
await device.launchApp();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await device.reloadReactNative();
|
||||
});
|
||||
|
||||
it('should have welcome screen', async () => {
|
||||
await expect(element(by.id('welcome'))).toBeVisible();
|
||||
});
|
||||
|
||||
it('should show hello screen after tap', async () => {
|
||||
await element(by.id('hello_button')).tap();
|
||||
await expect(element(by.text('Hello!!!'))).toBeVisible();
|
||||
});
|
||||
|
||||
it('should show world screen after tap', async () => {
|
||||
await element(by.id('world_button')).tap();
|
||||
await expect(element(by.text('World!!!'))).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -5,8 +5,8 @@
|
|||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"distribution": "internal",
|
||||
"channel": "development"
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal",
|
||||
|
|
|
|||
|
|
@ -1,118 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import { trpc } from '../src/trpc-client';
|
||||
|
||||
type ContextString = 'review' | 'product_info' | 'notification' | 'store' | 'complaint' | 'profile' | 'tags';
|
||||
|
||||
interface UploadInput {
|
||||
blob: Blob;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
interface UploadBatchInput {
|
||||
images: UploadInput[];
|
||||
contextString: ContextString;
|
||||
}
|
||||
|
||||
interface UploadResult {
|
||||
keys: string[];
|
||||
presignedUrls: string[];
|
||||
}
|
||||
|
||||
export function useUploadToObjectStorage() {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [progress, setProgress] = useState<{ completed: number; total: number } | null>(null);
|
||||
|
||||
const generateUploadUrls = trpc.common.generateUploadUrls.useMutation();
|
||||
|
||||
const upload = async (input: UploadBatchInput): Promise<UploadResult> => {
|
||||
setIsUploading(true);
|
||||
setError(null);
|
||||
setProgress({ completed: 0, total: input.images.length });
|
||||
|
||||
try {
|
||||
const { images, contextString } = input;
|
||||
|
||||
if (images.length === 0) {
|
||||
return { keys: [], presignedUrls: [] };
|
||||
}
|
||||
|
||||
// 1. Get presigned URLs from backend (one call for all images)
|
||||
const mimeTypes = images.map(img => img.mimeType);
|
||||
const { uploadUrls } = await generateUploadUrls.mutateAsync({
|
||||
contextString,
|
||||
mimeTypes,
|
||||
});
|
||||
|
||||
if (uploadUrls.length !== images.length) {
|
||||
throw new Error(`Expected ${images.length} URLs, got ${uploadUrls.length}`);
|
||||
}
|
||||
|
||||
// 2. Upload all images in parallel
|
||||
const uploadPromises = images.map(async (image, index) => {
|
||||
const presignedUrl = uploadUrls[index];
|
||||
const { blob, mimeType } = image;
|
||||
|
||||
const response = await fetch(presignedUrl, {
|
||||
method: 'PUT',
|
||||
body: blob,
|
||||
headers: { 'Content-Type': mimeType },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload ${index + 1} failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
// Update progress
|
||||
setProgress(prev => prev ? { ...prev, completed: prev.completed + 1 } : null);
|
||||
|
||||
return {
|
||||
key: extractKeyFromPresignedUrl(presignedUrl),
|
||||
presignedUrl,
|
||||
};
|
||||
});
|
||||
|
||||
// Use Promise.all - if any fails, entire batch fails
|
||||
const results = await Promise.all(uploadPromises);
|
||||
|
||||
return {
|
||||
keys: results.map(r => r.key),
|
||||
presignedUrls: results.map(r => r.presignedUrl),
|
||||
};
|
||||
} catch (err) {
|
||||
const uploadError = err instanceof Error ? err : new Error('Upload failed');
|
||||
setError(uploadError);
|
||||
throw uploadError;
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
setProgress(null);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadSingle = async (blob: Blob, mimeType: string, contextString: ContextString): Promise<{ key: string; presignedUrl: string }> => {
|
||||
const result = await upload({
|
||||
images: [{ blob, mimeType }],
|
||||
contextString,
|
||||
});
|
||||
return {
|
||||
key: result.keys[0],
|
||||
presignedUrl: result.presignedUrls[0],
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
upload,
|
||||
uploadSingle,
|
||||
isUploading,
|
||||
error,
|
||||
progress,
|
||||
isPending: generateUploadUrls.isPending
|
||||
};
|
||||
}
|
||||
|
||||
function extractKeyFromPresignedUrl(url: string): string {
|
||||
const u = new URL(url);
|
||||
let rawKey = u.pathname.replace(/^\/+/, '');
|
||||
rawKey = rawKey.split('/').slice(1).join('/'); // make meatfarmer/product-images/asdf as product-images/asdf
|
||||
return decodeURIComponent(rawKey);
|
||||
}
|
||||
111
apps/admin-ui/src/api-hooks/product.api.ts
Normal file
111
apps/admin-ui/src/api-hooks/product.api.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import axios from '../../services/axios-admin-ui';
|
||||
|
||||
// Types
|
||||
export interface CreateProductPayload {
|
||||
name: string;
|
||||
shortDescription?: string;
|
||||
longDescription?: string;
|
||||
unitId: number;
|
||||
storeId: number;
|
||||
price: number;
|
||||
marketPrice?: number;
|
||||
incrementStep?: number;
|
||||
productQuantity?: number;
|
||||
isOutOfStock?: boolean;
|
||||
deals?: {
|
||||
quantity: number;
|
||||
price: number;
|
||||
validTill: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface UpdateProductPayload {
|
||||
name: string;
|
||||
shortDescription?: string;
|
||||
longDescription?: string;
|
||||
unitId: number;
|
||||
storeId: number;
|
||||
price: number;
|
||||
marketPrice?: number;
|
||||
incrementStep?: number;
|
||||
productQuantity?: number;
|
||||
isOutOfStock?: boolean;
|
||||
deals?: {
|
||||
quantity: number;
|
||||
price: number;
|
||||
validTill: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: number;
|
||||
name: string;
|
||||
shortDescription?: string | null;
|
||||
longDescription?: string;
|
||||
unitId: number;
|
||||
storeId: number;
|
||||
price: number;
|
||||
marketPrice?: number;
|
||||
productQuantity?: number;
|
||||
isOutOfStock?: boolean;
|
||||
images?: string[];
|
||||
createdAt: string;
|
||||
unit?: {
|
||||
id: number;
|
||||
shortNotation: string;
|
||||
fullName: string;
|
||||
};
|
||||
deals?: {
|
||||
id: number;
|
||||
quantity: string;
|
||||
price: string;
|
||||
validTill: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface CreateProductResponse {
|
||||
product: Product;
|
||||
deals?: any[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
// API functions
|
||||
const createProductApi = async (formData: FormData): Promise<CreateProductResponse> => {
|
||||
const response = await axios.post('/av/products', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updateProductApi = async ({ id, formData }: { id: number; formData: FormData }): Promise<CreateProductResponse> => {
|
||||
const response = await axios.put(`/av/products/${id}`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Hooks
|
||||
export const useCreateProduct = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: createProductApi,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['products'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateProduct = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: updateProductApi,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['products'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
119
apps/admin-ui/src/api-hooks/tag.api.ts
Normal file
119
apps/admin-ui/src/api-hooks/tag.api.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import axios from '../../services/axios-admin-ui';
|
||||
|
||||
// Types
|
||||
export interface CreateTagPayload {
|
||||
tagName: string;
|
||||
tagDescription?: string;
|
||||
imageUrl?: string;
|
||||
isDashboardTag: boolean;
|
||||
relatedStores?: number[];
|
||||
}
|
||||
|
||||
export interface UpdateTagPayload {
|
||||
tagName: string;
|
||||
tagDescription?: string;
|
||||
imageUrl?: string;
|
||||
isDashboardTag: boolean;
|
||||
relatedStores?: number[];
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
id: number;
|
||||
tagName: string;
|
||||
tagDescription: string | null;
|
||||
imageUrl: string | null;
|
||||
isDashboardTag: boolean;
|
||||
relatedStores: number[];
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface CreateTagResponse {
|
||||
tag: Tag;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface GetTagsResponse {
|
||||
tags: Tag[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
// API functions
|
||||
const createTagApi = async (formData: FormData): Promise<CreateTagResponse> => {
|
||||
const response = await axios.post('/av/product-tags', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updateTagApi = async ({ id, formData }: { id: number; formData: FormData }): Promise<CreateTagResponse> => {
|
||||
const response = await axios.put(`/av/product-tags/${id}`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const deleteTagApi = async (id: number): Promise<{ message: string }> => {
|
||||
const response = await axios.delete(`/av/product-tags/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getTagsApi = async (): Promise<GetTagsResponse> => {
|
||||
const response = await axios.get('/av/product-tags');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getTagApi = async (id: number): Promise<{ tag: Tag }> => {
|
||||
const response = await axios.get(`/av/product-tags/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Hooks
|
||||
export const useCreateTag = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: createTagApi,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tags'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateTag = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: updateTagApi,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tags'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteTag = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: deleteTagApi,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tags'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetTags = () => {
|
||||
return useQuery({
|
||||
queryKey: ['tags'],
|
||||
queryFn: getTagsApi,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetTag = (id: number) => {
|
||||
return useQuery({
|
||||
queryKey: ['tags', id],
|
||||
queryFn: () => getTagApi(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle } from 'react';
|
||||
import { View, TouchableOpacity } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { Formik, FieldArray } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { MyTextInput, BottomDropdown, MyText, useTheme, DatePicker, tw, useFocusCallback, Checkbox, ImageUploaderNeo, ImageUploaderNeoItem, ImageUploaderNeoPayload } from 'common-ui';
|
||||
import { MyTextInput, BottomDropdown, MyText, ImageUploader, ImageGalleryWithDelete, useTheme, DatePicker, tw, useFocusCallback, Checkbox } from 'common-ui';
|
||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { trpc } from '../trpc-client';
|
||||
import { useGetTags } from '../api-hooks/tag.api';
|
||||
|
||||
interface ProductFormData {
|
||||
name: string;
|
||||
|
|
@ -35,10 +38,9 @@ export interface ProductFormRef {
|
|||
interface ProductFormProps {
|
||||
mode: 'create' | 'edit';
|
||||
initialValues: ProductFormData;
|
||||
onSubmit: (values: ProductFormData, images: ImageUploaderNeoPayload[], imagesToDelete: string[]) => void;
|
||||
onSubmit: (values: ProductFormData, images?: { uri?: string }[], imagesToDelete?: string[]) => void;
|
||||
isLoading: boolean;
|
||||
existingImages?: ImageUploaderNeoItem[];
|
||||
existingImageKeys?: string[];
|
||||
existingImages?: string[];
|
||||
}
|
||||
|
||||
const unitOptions = [
|
||||
|
|
@ -48,22 +50,18 @@ const unitOptions = [
|
|||
{ label: 'Unit Piece', value: 4 },
|
||||
];
|
||||
|
||||
|
||||
|
||||
const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||
mode,
|
||||
initialValues,
|
||||
onSubmit,
|
||||
isLoading,
|
||||
existingImages:existingImagesRaw,
|
||||
existingImageKeys = [],
|
||||
existingImages = []
|
||||
}, ref) => {
|
||||
const { theme } = useTheme();
|
||||
const [images, setImages] = useState<ImageUploaderNeoItem[]>([]);
|
||||
|
||||
const existingImages = existingImagesRaw || []
|
||||
// Sync images state when existingImages prop changes (e.g., when async query data arrives)
|
||||
useEffect(() => {
|
||||
setImages(existingImages);
|
||||
}, [existingImagesRaw]);
|
||||
const [images, setImages] = useState<{ uri?: string }[]>([]);
|
||||
const [existingImagesState, setExistingImagesState] = useState<string[]>(existingImages);
|
||||
|
||||
const { data: storesData } = trpc.common.getStoresSummary.useQuery();
|
||||
const storeOptions = storesData?.stores.map(store => ({
|
||||
|
|
@ -71,50 +69,44 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
value: store.id,
|
||||
})) || [];
|
||||
|
||||
const { data: tagsData } = trpc.admin.product.getProductTags.useQuery();
|
||||
const { data: tagsData } = useGetTags();
|
||||
const tagOptions = tagsData?.tags.map(tag => ({
|
||||
label: tag.tagName,
|
||||
value: tag.id.toString(),
|
||||
})) || [];
|
||||
|
||||
// Build signed URL -> S3 key mapping for existing images
|
||||
const signedUrlToKey = useMemo(() => {
|
||||
const map: Record<string, string> = {};
|
||||
existingImages.forEach((img, i) => {
|
||||
if (existingImageKeys[i]) {
|
||||
map[img.imgUrl] = existingImageKeys[i];
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}, [existingImages, existingImageKeys]);
|
||||
// Initialize existing images state when existingImages prop changes
|
||||
useEffect(() => {
|
||||
console.log('changing existing imaes statte')
|
||||
|
||||
setExistingImagesState(existingImages);
|
||||
}, [existingImages]);
|
||||
|
||||
const pickImage = usePickImage({
|
||||
setFile: (files) => setImages(prev => [...prev, ...files]),
|
||||
multiple: true,
|
||||
});
|
||||
|
||||
// Calculate which existing images were deleted
|
||||
const deletedImages = existingImages.filter(img => !existingImagesState.includes(img));
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={(values) => {
|
||||
// New images have mimeType set, existing images have mimeType === null
|
||||
const newImages = images.filter(img => img.mimeType !== null);
|
||||
const deletedImageKeys = existingImages
|
||||
.filter(existing => !images.some(current => current.imgUrl === existing.imgUrl))
|
||||
.map(deleted => signedUrlToKey[deleted.imgUrl])
|
||||
.filter(Boolean);
|
||||
|
||||
onSubmit(
|
||||
values,
|
||||
newImages.map(img => ({ url: img.imgUrl, mimeType: img.mimeType })),
|
||||
deletedImageKeys,
|
||||
);
|
||||
}}
|
||||
onSubmit={(values) => onSubmit(values, images, deletedImages)}
|
||||
enableReinitialize
|
||||
>
|
||||
{({ handleChange, handleSubmit, values, setFieldValue, resetForm }) => {
|
||||
// Clear form when screen comes into focus
|
||||
const clearForm = useCallback(() => {
|
||||
setImages([]);
|
||||
setExistingImagesState([]);
|
||||
resetForm();
|
||||
}, [resetForm]);
|
||||
|
||||
useFocusCallback(clearForm);
|
||||
|
||||
// Update ref with current clearForm function
|
||||
useImperativeHandle(ref, () => ({
|
||||
clearImages: clearForm,
|
||||
}), [clearForm]);
|
||||
|
|
@ -149,18 +141,44 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<ImageUploaderNeo
|
||||
images={images}
|
||||
onImageAdd={(payloads) => setImages(prev => [...prev, ...payloads.map(p => ({ imgUrl: p.url, mimeType: p.mimeType }))])}
|
||||
onImageRemove={(payload) => setImages(prev => prev.filter(img => img.imgUrl !== payload.url))}
|
||||
allowMultiple={true}
|
||||
/>
|
||||
{mode === 'create' && (
|
||||
<ImageUploader
|
||||
images={images}
|
||||
onAddImage={pickImage}
|
||||
onRemoveImage={(uri) => setImages(prev => prev.filter(img => img.uri !== uri))}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode === 'edit' && existingImagesState.length > 0 && (
|
||||
<View style={{ marginBottom: 16 }}>
|
||||
<MyText style={tw`text-lg font-bold mb-2 text-gray-800`}>Current Images</MyText>
|
||||
<ImageGalleryWithDelete
|
||||
imageUrls={existingImagesState}
|
||||
setImageUrls={setExistingImagesState}
|
||||
imageHeight={100}
|
||||
imageWidth={100}
|
||||
columns={3}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{mode === 'edit' && (
|
||||
<View style={{ marginBottom: 16 }}>
|
||||
<MyText style={tw`text-lg font-bold mb-2 text-gray-800`}>Add New Images</MyText>
|
||||
<ImageUploader
|
||||
images={images}
|
||||
onAddImage={pickImage}
|
||||
onRemoveImage={(uri) => setImages(prev => prev.filter(img => img.uri !== uri))}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<BottomDropdown
|
||||
topLabel='Unit'
|
||||
label="Unit"
|
||||
value={values.unitId}
|
||||
options={unitOptions}
|
||||
// onValueChange={(value) => handleChange('unitId')(value+'')}
|
||||
onValueChange={(value) => setFieldValue('unitId', value)}
|
||||
placeholder="Select unit"
|
||||
style={{ marginBottom: 16 }}
|
||||
|
|
@ -170,7 +188,18 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
placeholder="Enter product quantity"
|
||||
keyboardType="numeric"
|
||||
value={values.productQuantity.toString()}
|
||||
onChangeText={(text) => setFieldValue('productQuantity', text)}
|
||||
onChangeText={(text) => {
|
||||
// if(text)
|
||||
// setFieldValue('productQuantity', text);
|
||||
// else
|
||||
setFieldValue('productQuantity', text);
|
||||
// if (text === '' || text === null || text === undefined) {
|
||||
// setFieldValue('productQuantity', 1);
|
||||
// } else {
|
||||
// const num = parseFloat(text);
|
||||
// setFieldValue('productQuantity', isNaN(num) ? 1 : num);
|
||||
// }
|
||||
}}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<BottomDropdown
|
||||
|
|
@ -209,6 +238,8 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
<View style={tw`flex-row items-center mb-4`}>
|
||||
<Checkbox
|
||||
checked={values.isSuspended}
|
||||
|
|
@ -223,7 +254,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
checked={values.isFlashAvailable}
|
||||
onPress={() => {
|
||||
setFieldValue('isFlashAvailable', !values.isFlashAvailable);
|
||||
if (values.isFlashAvailable) setFieldValue('flashPrice', '');
|
||||
if (values.isFlashAvailable) setFieldValue('flashPrice', ''); // Clear price when disabled
|
||||
}}
|
||||
style={tw`mr-3`}
|
||||
/>
|
||||
|
|
@ -241,6 +272,87 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* <FieldArray name="deals">
|
||||
{({ push, remove, form }) => (
|
||||
<View style={{ marginBottom: 16 }}>
|
||||
<View style={tw`flex-row items-center mb-4`}>
|
||||
<MaterialIcons name="local-offer" size={20} color="#3B82F6" />
|
||||
<MyText style={tw`text-lg font-bold text-gray-800 ml-2`}>
|
||||
Special Package Deals
|
||||
</MyText>
|
||||
<MyText style={tw`text-sm text-gray-500 ml-1`}>(Optional)</MyText>
|
||||
</View>
|
||||
{(form.values.deals || []).map((deal: any, index: number) => (
|
||||
<View key={index} style={tw`bg-white p-4 rounded-2xl shadow-lg mb-4 border border-gray-100`}>
|
||||
<View style={tw`mb-3`}>
|
||||
<View style={tw`flex-row items-end gap-3 mb-3`}>
|
||||
<View style={tw`flex-1`}>
|
||||
<MyTextInput
|
||||
topLabel="Quantity"
|
||||
placeholder="Enter quantity"
|
||||
keyboardType="numeric"
|
||||
value={deal.quantity || ''}
|
||||
onChangeText={form.handleChange(`deals.${index}.quantity`)}
|
||||
fullWidth={false}
|
||||
/>
|
||||
</View>
|
||||
<View style={tw`flex-1`}>
|
||||
<MyTextInput
|
||||
topLabel="Price"
|
||||
placeholder="Enter price"
|
||||
keyboardType="numeric"
|
||||
value={deal.price || ''}
|
||||
onChangeText={form.handleChange(`deals.${index}.price`)}
|
||||
fullWidth={false}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={tw`flex-row items-end gap-3`}>
|
||||
<View style={tw`flex-1`}>
|
||||
<DatePicker
|
||||
value={deal.validTill}
|
||||
setValue={(date) => form.setFieldValue(`deals.${index}.validTill`, date)}
|
||||
showLabel={true}
|
||||
placeholder="Valid Till"
|
||||
/>
|
||||
</View>
|
||||
<View style={tw`flex-1`}>
|
||||
<TouchableOpacity
|
||||
onPress={() => remove(index)}
|
||||
style={tw`bg-red-500 p-3 rounded-lg shadow-md flex-row items-center justify-center`}
|
||||
>
|
||||
<MaterialIcons name="delete" size={16} color="white" />
|
||||
<MyText style={tw`text-white font-semibold ml-1`}>Remove</MyText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{(form.values.deals || []).length === 0 && (
|
||||
<View style={tw`bg-gray-50 p-6 rounded-2xl border-2 border-dashed border-gray-300 items-center mb-4`}>
|
||||
<MaterialIcons name="local-offer" size={32} color="#9CA3AF" />
|
||||
<MyText style={tw`text-gray-500 text-center mt-2`}>
|
||||
No package deals added yet
|
||||
</MyText>
|
||||
<MyText style={tw`text-gray-400 text-sm text-center mt-1`}>
|
||||
Add special pricing for bulk purchases
|
||||
</MyText>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => push({ quantity: '', price: '', validTill: null })}
|
||||
style={tw`bg-green-500 px-4 py-2 rounded-lg shadow-lg flex-row items-center justify-center mt-4`}
|
||||
>
|
||||
<MaterialIcons name="add" size={20} color="white" />
|
||||
<MyText style={tw`text-white font-bold text-lg ml-2`}>Add Package Deal</MyText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</FieldArray> */}
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={submit}
|
||||
disabled={isLoading}
|
||||
|
|
@ -259,4 +371,4 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
|
||||
ProductForm.displayName = 'ProductForm';
|
||||
|
||||
export default ProductForm;
|
||||
export default ProductForm;
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import React, { useState, useEffect, forwardRef, useCallback } from 'react';
|
||||
import { View, TouchableOpacity } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { Formik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { MyTextInput, MyText, Checkbox, ImageUploaderNeo, tw, useFocusCallback, BottomDropdown, type ImageUploaderNeoItem, type ImageUploaderNeoPayload } from 'common-ui';
|
||||
import { MyTextInput, MyText, Checkbox, ImageUploader, tw, useFocusCallback, BottomDropdown } from 'common-ui';
|
||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
|
||||
interface StoreOption {
|
||||
|
|
@ -21,7 +23,7 @@ interface TagFormProps {
|
|||
mode: 'create' | 'edit';
|
||||
initialValues: TagFormData;
|
||||
existingImageUrl?: string;
|
||||
onSubmit: (values: TagFormData, images: ImageUploaderNeoItem[], removedExisting: boolean) => void;
|
||||
onSubmit: (values: TagFormData, image?: { uri?: string }) => void;
|
||||
isLoading: boolean;
|
||||
stores?: StoreOption[];
|
||||
}
|
||||
|
|
@ -29,28 +31,27 @@ interface TagFormProps {
|
|||
const TagForm = forwardRef<any, TagFormProps>(({
|
||||
mode,
|
||||
initialValues,
|
||||
existingImageUrl: existingImageUrlRaw,
|
||||
existingImageUrl = '',
|
||||
onSubmit,
|
||||
isLoading,
|
||||
stores: storesRaw,
|
||||
stores = [],
|
||||
}, ref) => {
|
||||
const [images, setImages] = useState<ImageUploaderNeoItem[]>([])
|
||||
const [removedExisting, setRemovedExisting] = useState(false)
|
||||
const [image, setImage] = useState<{ uri?: string } | null>(null);
|
||||
const [isDashboardTagChecked, setIsDashboardTagChecked] = useState<boolean>(Boolean(initialValues.isDashboardTag));
|
||||
|
||||
const existingImageUrl = existingImageUrlRaw || ''
|
||||
const stores = storesRaw || []
|
||||
|
||||
// Update checkbox when initial values change
|
||||
useEffect(() => {
|
||||
setIsDashboardTagChecked(Boolean(initialValues.isDashboardTag));
|
||||
if (existingImageUrl) {
|
||||
setImages([{ imgUrl: existingImageUrl, mimeType: null }])
|
||||
} else {
|
||||
setImages([])
|
||||
}
|
||||
setRemovedExisting(false)
|
||||
}, [existingImageUrlRaw, initialValues.isDashboardTag]);
|
||||
existingImageUrl && setImage({uri:existingImageUrl})
|
||||
}, [initialValues.isDashboardTag]);
|
||||
|
||||
const pickImage = usePickImage({
|
||||
setFile: (files) => {
|
||||
|
||||
setImage(files || null)
|
||||
},
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
|
|
@ -66,17 +67,17 @@ const TagForm = forwardRef<any, TagFormProps>(({
|
|||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(values) => onSubmit(values, images, removedExisting)}
|
||||
onSubmit={(values) => onSubmit(values, image || undefined)}
|
||||
enableReinitialize
|
||||
>
|
||||
{({ handleChange, handleSubmit, values, setFieldValue, errors, touched, setFieldValue: formikSetFieldValue, resetForm }) => {
|
||||
// Clear form when screen comes into focus
|
||||
const clearForm = useCallback(() => {
|
||||
setImages([])
|
||||
setRemovedExisting(false)
|
||||
setIsDashboardTagChecked(false);
|
||||
resetForm();
|
||||
}, [resetForm]);
|
||||
const clearForm = useCallback(() => {
|
||||
setImage(null);
|
||||
|
||||
setIsDashboardTagChecked(false);
|
||||
resetForm();
|
||||
}, [resetForm]);
|
||||
|
||||
useFocusCallback(clearForm);
|
||||
|
||||
|
|
@ -107,21 +108,10 @@ const TagForm = forwardRef<any, TagFormProps>(({
|
|||
</MyText>
|
||||
|
||||
|
||||
<ImageUploaderNeo
|
||||
images={images}
|
||||
onImageAdd={(payload: ImageUploaderNeoPayload[]) => {
|
||||
setImages((prev) => [...prev, ...payload.map((img) => ({
|
||||
imgUrl: img.url,
|
||||
mimeType: img.mimeType,
|
||||
}))])
|
||||
}}
|
||||
onImageRemove={(payload) => {
|
||||
if (payload.mimeType === null) {
|
||||
setRemovedExisting(true)
|
||||
}
|
||||
setImages((prev) => prev.filter((item) => item.imgUrl !== payload.url))
|
||||
}}
|
||||
allowMultiple={false}
|
||||
<ImageUploader
|
||||
images={image ? [image] : []}
|
||||
onAddImage={pickImage}
|
||||
onRemoveImage={() => setImage(null)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
|
@ -177,4 +167,4 @@ const TagForm = forwardRef<any, TagFormProps>(({
|
|||
|
||||
TagForm.displayName = 'TagForm';
|
||||
|
||||
export default TagForm;
|
||||
export default TagForm;
|
||||
|
|
@ -3,7 +3,7 @@ import { View, TouchableOpacity, Alert } from 'react-native';
|
|||
import { Entypo } from '@expo/vector-icons';
|
||||
import { MyText, tw, BottomDialog } from 'common-ui';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import { useDeleteTag } from '../api-hooks/tag.api';
|
||||
|
||||
export interface TagMenuProps {
|
||||
tagId: number;
|
||||
|
|
@ -22,7 +22,7 @@ export const TagMenu: React.FC<TagMenuProps> = ({
|
|||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const deleteTag = trpc.admin.product.deleteProductTag.useMutation();
|
||||
const { mutate: deleteTag, isPending: isDeleting } = useDeleteTag();
|
||||
|
||||
const handleOpenMenu = () => {
|
||||
setIsOpen(true);
|
||||
|
|
@ -54,7 +54,7 @@ export const TagMenu: React.FC<TagMenuProps> = ({
|
|||
};
|
||||
|
||||
const performDelete = () => {
|
||||
deleteTag.mutate({ id: tagId }, {
|
||||
deleteTag(tagId, {
|
||||
onSuccess: () => {
|
||||
Alert.alert('Success', 'Tag deleted successfully');
|
||||
onDeleteSuccess?.();
|
||||
|
|
@ -63,7 +63,7 @@ export const TagMenu: React.FC<TagMenuProps> = ({
|
|||
const errorMessage = error.message || 'Failed to delete tag';
|
||||
Alert.alert('Error', errorMessage);
|
||||
},
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
const options = [
|
||||
|
|
@ -116,4 +116,4 @@ export const TagMenu: React.FC<TagMenuProps> = ({
|
|||
</BottomDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
8
packages/db_helper_postgres/.env → apps/backend/.env
Normal file → Executable file
8
packages/db_helper_postgres/.env → apps/backend/.env
Normal file → Executable file
|
|
@ -1,6 +1,6 @@
|
|||
ENV_MODE=PROD
|
||||
DATABASE_URL=postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer #technocracy
|
||||
# DATABASE_URL=postgres://postgres:meatfarmer_master_password@5.223.55.14:7447/meatfarmer #hetzner
|
||||
# DATABASE_URL=postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer #technocracy
|
||||
DATABASE_URL=postgres://postgres:meatfarmer_master_password@5.223.55.14:7447/meatfarmer #hetzner
|
||||
PHONE_PE_BASE_URL=https://api-preprod.phonepe.com/
|
||||
PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090
|
||||
PHONE_PE_CLIENT_VERSION=1
|
||||
|
|
@ -21,10 +21,6 @@ S3_BUCKET_NAME=meatfarmer
|
|||
EXPO_ACCESS_TOKEN=Asvpy8cByRh6T4ksnWScO6PLcio2n35-BwES5zK-
|
||||
JWT_SECRET=my_meatfarmer_jwt_secret_key
|
||||
ASSETS_DOMAIN=https://assets.freshyo.in/
|
||||
API_CACHE_KEY=api-cache-dev
|
||||
# CLOUDFLARE_API_TOKEN=I8Vp4E9TX58E8qEDeH0nTFDS2d2zXNYiXvbs4Ckj
|
||||
CLOUDFLARE_API_TOKEN=N7jAg5X-RUj_fVfMW6zbfJ8qIYc81TSIKKlbZ6oh
|
||||
CLOUDFLARE_ZONE_ID=edefbf750bfc3ff26ccd11e8e28dc8d7
|
||||
# REDIS_URL=redis://default:redis_shafi_password@5.223.55.14:6379
|
||||
REDIS_URL=redis://default:redis_shafi_password@57.128.212.174:6379
|
||||
APP_URL=http://localhost:4000
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
ENV_MODE=PROD
|
||||
DATABASE_URL=postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer #technocracy
|
||||
# DATABASE_URL=postgres://postgres:meatfarmer_master_password@5.223.55.14:7447/meatfarmer #hetzner
|
||||
PHONE_PE_BASE_URL=https://api-preprod.phonepe.com/
|
||||
PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090
|
||||
PHONE_PE_CLIENT_VERSION=1
|
||||
PHONE_PE_CLIENT_SECRET=MTU1MmIzOTgtM2Q0Mi00N2M5LTkyMWUtNzBiMjdmYzVmZWUy
|
||||
PHONE_PE_MERCHANT_ID=M23F2IGP34ZAR
|
||||
|
||||
# S3_REGION=ap-hyderabad-1
|
||||
# S3_REGION=sgp
|
||||
# S3_ACCESS_KEY_ID=52932a33abce40b38b559dadccab640f
|
||||
# S3_SECRET_ACCESS_KEY=d287998b696d4a1c912e727f6394e53b
|
||||
# S3_URL=https://s3.sgp.io.cloud.ovh.net/
|
||||
# S3_BUCKET_NAME=theobjectstore
|
||||
S3_REGION=apac
|
||||
S3_ACCESS_KEY_ID=8fab47503efb9547b50e4fb317e35cc7
|
||||
S3_SECRET_ACCESS_KEY=47c2eb5636843cf568dda7ad0959a3e42071303f26dbdff94bd45a3c33dcd950
|
||||
S3_URL=https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com
|
||||
S3_BUCKET_NAME=meatfarmer-dev
|
||||
EXPO_ACCESS_TOKEN=Asvpy8cByRh6T4ksnWScO6PLcio2n35-BwES5zK-
|
||||
JWT_SECRET=my_meatfarmer_jwt_secret_key
|
||||
ASSETS_DOMAIN=https://assets2.freshyo.in/
|
||||
API_CACHE_KEY=api-cache-dev
|
||||
# CLOUDFLARE_API_TOKEN=I8Vp4E9TX58E8qEDeH0nTFDS2d2zXNYiXvbs4Ckj
|
||||
CLOUDFLARE_API_TOKEN=N7jAg5X-RUj_fVfMW6zbfJ8qIYc81TSIKKlbZ6oh
|
||||
CLOUDFLARE_ZONE_ID=edefbf750bfc3ff26ccd11e8e28dc8d7
|
||||
# REDIS_URL=redis://default:redis_shafi_password@5.223.55.14:6379
|
||||
REDIS_URL=redis://default:redis_shafi_password@57.128.212.174:6379
|
||||
APP_URL=http://localhost:4000
|
||||
RAZORPAY_KEY=rzp_test_RdCBBUJ56NLaJK
|
||||
RAZORPAY_SECRET=namEwKBE1ypWxH0QDVg6fWOe
|
||||
OTP_SENDER_AUTH_TOKEN=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJDLTM5OENEMkJDRTM0MjQ4OCIsImlhdCI6MTc0Nzg0MTEwMywiZXhwIjoxOTA1NTIxMTAzfQ.IV64ofVKjcwveIanxu_P2XlACtPeA9sJQ74uM53osDeyUXsFv0rwkCl6NNBIX93s_wnh4MKITLbcF_ClwmFQ0A
|
||||
|
||||
MIN_ORDER_VALUE=300
|
||||
DELIVERY_CHARGE=20
|
||||
|
||||
# Telegram Configuration
|
||||
TELEGRAM_BOT_TOKEN=8410461852:AAGXQCwRPFbndqwTgLJh8kYxST4Z0vgh72U
|
||||
TELEGRAM_CHAT_IDS=5147760058
|
||||
# TELEGRAM_BOT_TOKEN=8410461852:AAGXQCwRPFbndqwTgLJh8kYxST4Z0vgh72U
|
||||
# TELEGRAM_CHAT_IDS=-5075171894
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
0
packages/db_helper_postgres/drizzle.config.ts → apps/backend/drizzle.config.ts
Normal file → Executable file
0
packages/db_helper_postgres/drizzle.config.ts → apps/backend/drizzle.config.ts
Normal file → Executable file
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue