Compare commits

..

16 commits

Author SHA1 Message Date
shafi54
17e2644759 enh 2026-03-24 18:48:31 +05:30
shafi54
23be301cc0 enh 2026-03-23 11:17:38 +05:30
shafi54
95d2c861c0 enh 2026-03-22 21:29:04 +05:30
shafi54
a23d3bf5b8 enh 2026-03-22 20:20:28 +05:30
shafi54
56b606ebcf enh 2026-03-22 20:20:18 +05:30
shafi54
cd5ab79f44 enh 2026-03-22 16:52:25 +05:30
shafi54
b49015b446 enh 2026-03-22 16:38:37 +05:30
shafi54
a0a05615b1 enh 2026-03-22 16:35:39 +05:30
shafi54
501667a4d2 enh 2026-03-22 16:11:01 +05:30
shafi54
1122159552 enh 2026-03-22 14:57:53 +05:30
shafi54
8f4cddee1a enh 2026-03-21 22:28:45 +05:30
shafi54
77e3eb21d6 enh 2026-03-21 20:59:45 +05:30
shafi54
b38ff13950 enh 2026-03-20 14:48:31 +05:30
shafi54
e2abc7cb02 enh 2026-03-20 00:41:36 +05:30
shafi54
4f1f52ffee enh 2026-03-20 00:40:31 +05:30
shafi54
71cad727fd enh 2026-03-20 00:39:48 +05:30
1057 changed files with 27228 additions and 608848 deletions

View file

@ -6,3 +6,4 @@ apps/users-ui/src
apps/admin-ui/app
apps/users-ui/src
**/package-lock.json
test/

4
.gitignore vendored
View file

@ -8,11 +8,13 @@ yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
*.apk
**/appBinaries
**/.wrangler/*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
test/appBinaries
# Runtime data
pids
*.pid

View file

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

View file

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

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

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

View file

@ -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 (
{values.constants.map((constant, index) => (
<View key={constant.key} style={tw`mb-4`}>
<ConstantInput
constantKey={constant.key}
value={values[constant.key]}
setFieldValue={setFieldValue}
router={router}
/>
<ConstantInput constant={constant} setFieldValue={setFieldValue} index={index} router={router} />
</View>
)
})}
))}
<MyTouchableOpacity
onPress={() => handleSubmit()}

View file

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

View file

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

View file

@ -158,6 +158,15 @@ export default function Dashboard() {
iconColor: '#8B5CF6',
iconBg: '#F3E8FF',
},
{
title: 'Stocking Schedules',
icon: 'schedule',
description: 'Manage product stocking schedules',
route: '/(drawer)/stocking-schedules',
category: 'products',
iconColor: '#0EA5E9',
iconBg: '#E0F2FE',
},
{
title: 'Stores',
icon: 'store',

View file

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

View file

@ -1,10 +1,9 @@
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 { trpc } from '@/src/trpc-client';
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
interface TagFormData {
tagName: string;
@ -15,51 +14,31 @@ 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 createTag = trpc.admin.tag.createTag.useMutation();
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 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({
const handleSubmit = (values: TagFormData, imageKey?: string, deleteExistingImage?: boolean) => {
createTag.mutate({
tagName: values.tagName,
tagDescription: values.tagDescription || undefined,
imageUrl,
tagDescription: values.tagDescription,
isDashboardTag: values.isDashboardTag,
relatedStores: values.relatedStores,
uploadUrls,
})
await refetchTags()
imageKey: imageKey,
}, {
onSuccess: (data) => {
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)
}
}
]);
},
onError: (error: any) => {
const errorMessage = error.message || 'Failed to create tag';
Alert.alert('Error', errorMessage);
},
});
};
const initialValues: TagFormData = {
tagName: '',
@ -77,8 +56,8 @@ 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={createTag.isPending}
stores={storesData?.stores.map(store => ({ id: store.id, name: store.name })) || []}
/>
</View>
</AppContainer>

View file

@ -1,17 +1,15 @@
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 { trpc } from '@/src/trpc-client';
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
interface TagFormData {
tagName: string;
tagDescription: string;
isDashboardTag: boolean;
relatedStores: number[];
existingImageUrl?: string;
}
export default function EditTag() {
@ -19,60 +17,39 @@ 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 },
const { data: tagData, isLoading: isLoadingTag, error: tagError } = trpc.admin.tag.getTagById.useQuery(
{ id: tagIdNum! },
{ enabled: !!tagIdNum }
)
const { refetch: refetchTags } = trpc.admin.product.getProductTags.useQuery(undefined, {
enabled: false,
});
const updateTag = trpc.admin.product.updateProductTag.useMutation();
);
const updateTag = trpc.admin.tag.updateTag.useMutation();
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, imageKey?: string, deleteExistingImage?: boolean) => {
if (!tagIdNum) return;
try {
let imageUrl: string | null | undefined
let uploadUrls: string[] = []
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({
updateTag.mutate({
id: tagIdNum,
tagName: values.tagName,
tagDescription: values.tagDescription || undefined,
imageUrl,
tagDescription: values.tagDescription,
isDashboardTag: values.isDashboardTag,
relatedStores: values.relatedStores,
uploadUrls,
})
await refetchTags()
imageKey: imageKey,
deleteExistingImage: deleteExistingImage,
}, {
onSuccess: (data) => {
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)
}
}
]);
},
onError: (error: any) => {
const errorMessage = error.message || 'Failed to update tag';
Alert.alert('Error', errorMessage);
},
});
};
if (isLoadingTag) {
return (
@ -100,7 +77,6 @@ export default function EditTag() {
tagDescription: tag.tagDescription || '',
isDashboardTag: tag.isDashboardTag,
relatedStores: Array.isArray(tag.relatedStores) ? tag.relatedStores : [],
existingImageUrl: tag.imageUrl || undefined,
};
return (
@ -113,8 +89,8 @@ 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={updateTag.isPending}
stores={storesData?.stores.map(store => ({ id: store.id, name: store.name })) || []}
/>
</View>
</AppContainer>

View file

@ -7,18 +7,18 @@ import { tw, MyText, useManualRefresh, useMarkDataFetchers, MyFlatList } from 'c
import { TagMenu } from '@/src/components/TagMenu';
import { trpc } from '@/src/trpc-client';
interface TagItemData {
interface Tag {
id: number;
tagName: string;
tagDescription: string | null;
imageUrl: string | null;
isDashboardTag: boolean;
relatedStores?: unknown;
createdAt: string | Date;
relatedStores?: any;
createdAt?: string;
}
interface TagItemProps {
item: TagItemData;
item: Tag;
onDeleteSuccess: () => void;
}
@ -70,7 +70,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 } = trpc.admin.tag.getTags.useQuery();
const [refreshing, setRefreshing] = useState(false);
const tags = tagsData?.tags || [];

View file

@ -1,35 +1,14 @@
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';
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 handleSubmit = async (values: any, images: ImageUploaderNeoPayload[]) => {
try {
let uploadUrls: string[] = [];
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;
}
await createProduct.mutateAsync({
const handleSubmit = (values: any, imageKeys?: string[]) => {
createProduct.mutate({
name: values.name,
shortDescription: values.shortDescription,
longDescription: values.longDescription,
@ -42,15 +21,17 @@ export default function AddProduct() {
isSuspended: values.isSuspended || false,
isFlashAvailable: values.isFlashAvailable || false,
flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : undefined,
uploadUrls,
tagIds: values.tagIds || [],
});
await refetchProducts();
imageKeys: imageKeys || [],
}, {
onSuccess: (data) => {
Alert.alert('Success', 'Product created successfully!');
} catch (error: any) {
// Reset form or navigate
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to create product');
}
},
});
};
const initialValues = {
@ -75,7 +56,8 @@ export default function AddProduct() {
mode="create"
initialValues={initialValues}
onSubmit={handleSubmit}
isLoading={createProduct.isPending || isUploading}
isLoading={createProduct.isPending}
existingImages={[]}
/>
</AppContainer>
);

View file

@ -6,7 +6,7 @@ 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 { useUploadToObjectStorage } from '../../../../hooks/useUploadToObjectStorage';
import { Formik } from 'formik';
import { LinearGradient } from 'expo-linear-gradient';
import { BlurView } from 'expo-blur';
@ -24,9 +24,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 { upload, isUploading } = useUploadToObjectStorage();
const handleImagePick = usePickImage({
setFile: async (assets: any) => {
@ -62,16 +63,23 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
const handleSubmit = async (adminResponse: string) => {
try {
const { keys, presignedUrls } = await upload({
images: selectedImages,
let keys: string[] = [];
let generatedUrls: string[] = [];
if (selectedImages.length > 0) {
const result = await upload({
images: selectedImages.map(img => ({ blob: img.blob, mimeType: img.mimeType })),
contextString: 'review',
});
keys = result.keys;
generatedUrls = result.presignedUrls;
}
await respondToReview.mutateAsync({
reviewId,
adminResponse,
adminResponseImages: keys,
uploadUrls: presignedUrls,
uploadUrls: generatedUrls,
});
Alert.alert('Success', 'Response submitted');
@ -79,6 +87,7 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
setAdminResponse('');
setSelectedImages([]);
setDisplayImages([]);
setUploadUrls([]);
} catch (error: any) {
Alert.alert('Error', error.message || 'Failed to submit response.');
}
@ -114,7 +123,7 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
<TouchableOpacity
onPress={() => formikSubmit()}
activeOpacity={0.8}
disabled={respondToReview.isPending}
disabled={respondToReview.isPending || isUploading}
>
<LinearGradient
colors={['#2563EB', '#1D4ED8']}
@ -122,7 +131,9 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
end={{ x: 1, y: 0 }}
style={tw`py-4 rounded-2xl items-center shadow-lg`}
>
{respondToReview.isPending ? (
{isUploading ? (
<ActivityIndicator color="white" />
) : respondToReview.isPending ? (
<ActivityIndicator color="white" />
) : (
<MyText style={tw`text-white font-bold text-lg`}>Submit Response</MyText>

View file

@ -1,10 +1,9 @@
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 { trpc } from '@/src/trpc-client';
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
export default function EditProduct() {
const { id } = useLocalSearchParams();
@ -15,35 +14,13 @@ export default function EditProduct() {
{ 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();
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({
const handleSubmit = (values: any, newImageKeys?: string[], imagesToDelete?: string[]) => {
updateProduct.mutate({
id: productId,
name: values.name,
shortDescription: values.shortDescription,
@ -54,21 +31,31 @@ export default function EditProduct() {
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 || [],
});
await refetch();
await refetchProducts();
isSuspended: values.isSuspended,
isFlashAvailable: values.isFlashAvailable,
flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : undefined,
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,
})),
tagIds: values.tagIds,
newImageKeys: newImageKeys || [],
imagesToDelete: imagesToDelete || [],
}, {
onSuccess: (data) => {
Alert.alert('Success', 'Product updated successfully!');
// Clear newly added images after successful update
productFormRef.current?.clearImages();
} catch (error: any) {
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to update product');
}
},
});
};
if (isFetching) {
@ -91,13 +78,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,
@ -126,9 +107,8 @@ export default function EditProduct() {
mode="edit"
initialValues={initialValues}
onSubmit={handleSubmit}
isLoading={updateProduct.isPending || isUploading}
existingImages={existingImages}
existingImageKeys={existingImageKeys}
isLoading={updateProduct.isPending}
existingImages={productData.images || []}
/>
</AppContainer>
);

View file

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

View file

@ -18,7 +18,7 @@ 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';
import { useUploadToObjectStorage } from '../../../hooks/useUploadToObjectStorage';
interface User {
id: number;
@ -41,7 +41,7 @@ export default function SendNotifications() {
search: searchQuery,
});
const { uploadSingle } = useUploadToObjectStorage();
const { uploadSingle, isUploading } = useUploadToObjectStorage();
// Send notification mutation
const sendNotification = trpc.admin.user.sendNotification.useMutation({
@ -230,15 +230,15 @@ export default function SendNotifications() {
{/* Submit Button */}
<TouchableOpacity
onPress={handleSend}
disabled={sendNotification.isPending || title.trim().length === 0 || message.trim().length === 0}
disabled={sendNotification.isPending || isUploading || title.trim().length === 0 || message.trim().length === 0}
style={tw`${
sendNotification.isPending || title.trim().length === 0 || message.trim().length === 0
sendNotification.isPending || isUploading || title.trim().length === 0 || message.trim().length === 0
? 'bg-gray-300'
: 'bg-blue-600'
} rounded-xl py-4 items-center shadow-sm`}
>
<MyText style={tw`text-white font-bold text-base`}>
{sendNotification.isPending ? 'Sending...' : selectedUserIds.length === 0 ? 'Send to All Users' : 'Send Notification'}
{isUploading ? 'Uploading...' : sendNotification.isPending ? 'Sending...' : selectedUserIds.length === 0 ? 'Send to All Users' : 'Send Notification'}
</MyText>
</TouchableOpacity>
</ScrollView>

View file

@ -0,0 +1,443 @@
import React, { useState } from 'react';
import { View, ScrollView, Alert, FlatList, TouchableOpacity } from 'react-native';
import {
theme,
AppContainer,
MyText,
tw,
useManualRefresh,
useMarkDataFetchers,
MyTouchableOpacity,
RawBottomDialog,
BottomDialog,
} from 'common-ui';
import { trpc } from '../../../src/trpc-client';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { Ionicons, Entypo } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import AvailabilityScheduleForm from '../../../components/AvailabilityScheduleForm';
interface Schedule {
id: number;
scheduleName: string;
time: string;
action: 'in' | 'out';
createdAt: string;
lastUpdated: string;
productIds: number[];
groupIds: number[];
productCount: number;
groupCount: number;
}
const ScheduleItem = ({
schedule,
onDelete,
index,
onViewProducts,
onViewGroups,
onReplicate,
}: {
schedule: Schedule;
onDelete: (id: number) => void;
index: number;
onViewProducts: (productIds: number[]) => void;
onViewGroups: (groupIds: number[]) => void;
onReplicate: (schedule: Schedule) => void;
}) => {
const isIn = schedule.action === 'in';
const [menuOpen, setMenuOpen] = useState(false);
return (
<View style={tw``}>
<View style={tw`p-6`}>
{/* Top Header: Name & Action Badge */}
<View style={tw`flex-row justify-between items-start mb-4`}>
<View style={tw`flex-row items-center flex-1`}>
<View
style={tw`w-12 h-12 rounded-2xl bg-brand50 items-center justify-center mr-4`}
>
<MaterialIcons
name="schedule"
size={24}
color={theme.colors.brand600}
/>
</View>
<View style={tw`flex-1`}>
<MyText
style={tw`text-slate-400 text-[10px] font-black uppercase tracking-widest`}
>
Schedule Name
</MyText>
<MyText
style={tw`text-xl font-black text-slate-900`}
numberOfLines={1}
>
{schedule.scheduleName}
</MyText>
</View>
</View>
<View style={tw`flex-row items-center`}>
<View
style={[
tw`px-3 py-1.5 rounded-full flex-row items-center mr-2`,
{ backgroundColor: isIn ? '#F0FDF4' : '#FFF1F2' },
]}
>
<View
style={[
tw`w-1.5 h-1.5 rounded-full mr-2`,
{ backgroundColor: isIn ? '#10B981' : '#E11D48' },
]}
/>
<MyText
style={[
tw`text-[10px] font-black uppercase tracking-tighter`,
{ color: isIn ? '#10B981' : '#E11D48' },
]}
>
{isIn ? 'In Stock' : 'Out of Stock'}
</MyText>
</View>
<TouchableOpacity
onPress={() => setMenuOpen(true)}
style={tw`p-1`}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Entypo name="dots-three-vertical" size={20} color="#9CA3AF" />
</TouchableOpacity>
</View>
</View>
{/* Menu Dialog */}
<BottomDialog open={menuOpen} onClose={() => setMenuOpen(false)}>
<View style={tw`p-4`}>
<MyText style={tw`text-lg font-bold mb-4`}>{schedule.scheduleName}</MyText>
<TouchableOpacity
onPress={() => {
setMenuOpen(false);
onReplicate(schedule);
}}
style={tw`py-4 border-b border-gray-200`}
>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="content-copy" size={20} color="#4B5563" style={tw`mr-3`} />
<MyText style={tw`text-base text-gray-800`}>Replicate items</MyText>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setMenuOpen(false);
Alert.alert('Coming Soon', 'Edit functionality will be available soon');
}}
style={tw`py-4 border-b border-gray-200`}
>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="edit" size={20} color="#4B5563" style={tw`mr-3`} />
<MyText style={tw`text-base text-gray-800`}>Edit</MyText>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setMenuOpen(false);
onDelete(schedule.id);
}}
style={tw`py-4 border-b border-gray-200`}
>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="delete" size={20} color="#E11D48" style={tw`mr-3`} />
<MyText style={tw`text-base text-red-500`}>Delete</MyText>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setMenuOpen(false)}
style={tw`py-4 mt-2`}
>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="close" size={20} color="#6B7280" style={tw`mr-3`} />
<MyText style={tw`text-base text-gray-600`}>Cancel</MyText>
</View>
</TouchableOpacity>
</View>
</BottomDialog>
{/* Middle: Time Banner */}
<View
style={tw`bg-slate-50 rounded-3xl p-4 flex-row items-center mb-4 border border-slate-100`}
>
<View
style={tw`bg-white w-10 h-10 rounded-2xl items-center justify-center shadow-sm`}
>
<MaterialIcons name="access-time" size={20} color="#64748B" />
</View>
<View style={tw`ml-4 flex-1`}>
<MyText style={tw`text-slate-900 font-extrabold text-sm`}>
{schedule.time}
</MyText>
<MyText style={tw`text-slate-500 text-[10px] font-bold uppercase`}>
Daily at this time
</MyText>
</View>
</View>
{/* Stats & Actions */}
<View style={tw`flex-row items-center justify-between`}>
<View style={tw`flex-row items-center`}>
<MyTouchableOpacity
onPress={() => onViewProducts(schedule.productIds)}
style={tw`flex-row items-center mr-4`}
>
<MaterialIcons name="shopping-bag" size={14} color="#94A3B8" />
<MyText style={tw`text-xs font-bold text-brand600 ml-1.5`}>
{schedule.productCount} Products
</MyText>
</MyTouchableOpacity>
{schedule.groupCount > 0 && (
<MyTouchableOpacity
onPress={() => onViewGroups(schedule.groupIds)}
style={tw`flex-row items-center`}
>
<MaterialIcons name="category" size={14} color="#94A3B8" />
<MyText style={tw`text-xs font-bold text-brand600 ml-1.5`}>
{schedule.groupCount} Groups
</MyText>
</MyTouchableOpacity>
)}
</View>
</View>
</View>
</View>
);
};
export default function StockingSchedules() {
const {
data: schedules,
isLoading,
error,
refetch,
} = trpc.admin.productAvailabilitySchedules.getAll.useQuery();
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
const { data: groupsData } = trpc.admin.product.getGroups.useQuery();
const deleteSchedule = trpc.admin.productAvailabilitySchedules.delete.useMutation();
const [showCreateForm, setShowCreateForm] = useState(false);
// Dialog state
const [dialogOpen, setDialogOpen] = useState(false);
const [dialogType, setDialogType] = useState<'products' | 'groups'>('products');
const [selectedIds, setSelectedIds] = useState<number[]>([]);
// Replication state
const [replicatingSchedule, setReplicatingSchedule] = useState<Schedule | null>(null);
useManualRefresh(refetch);
useMarkDataFetchers(() => {
refetch();
});
const handleCreate = () => {
setShowCreateForm(true);
};
const handleDelete = (id: number) => {
Alert.alert(
'Delete Schedule',
'Are you sure you want to delete this schedule? This action cannot be undone.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
style: 'destructive',
onPress: () => {
deleteSchedule.mutate(
{ id },
{
onSuccess: () => {
refetch();
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to delete schedule');
},
},
);
},
},
],
);
};
const handleViewProducts = (productIds: number[]) => {
setDialogType('products');
setSelectedIds(productIds);
setDialogOpen(true);
};
const handleViewGroups = (groupIds: number[]) => {
setDialogType('groups');
setSelectedIds(groupIds);
setDialogOpen(true);
};
const handleReplicate = (schedule: Schedule) => {
setReplicatingSchedule(schedule);
setShowCreateForm(true);
};
const handleCloseForm = () => {
setShowCreateForm(false);
setReplicatingSchedule(null);
};
// Get product/group names from IDs
const getProductNames = () => {
const allProducts = productsData?.products || [];
return selectedIds.map(id => {
const product = allProducts.find(p => p.id === id);
return product?.name || `Product #${id}`;
});
};
const getGroupNames = () => {
const allGroups = groupsData?.groups || [];
return selectedIds.map(id => {
const group = allGroups.find(g => g.id === id);
return group?.groupName || `Group #${id}`;
});
};
if (showCreateForm) {
return (
<AvailabilityScheduleForm
onClose={handleCloseForm}
onSuccess={() => {
refetch();
handleCloseForm();
}}
initialProductIds={replicatingSchedule?.productIds}
initialGroupIds={replicatingSchedule?.groupIds}
/>
);
}
if (isLoading) {
return (
<AppContainer>
<View style={tw`flex-1 justify-center items-center`}>
<MyText style={tw`text-gray-600`}>Loading schedules...</MyText>
</View>
</AppContainer>
);
}
if (error) {
return (
<AppContainer>
<View style={tw`flex-1 justify-center items-center`}>
<MyText style={tw`text-red-600`}>Error loading schedules</MyText>
</View>
</AppContainer>
);
}
return (
<>
<AppContainer>
<View style={tw`flex-1 bg-white h-full`}>
<ScrollView
style={tw`flex-1`}
contentContainerStyle={tw`pt-2 pb-32`}
showsVerticalScrollIndicator={false}
>
{schedules && schedules.length === 0 ? (
<View style={tw`flex-1 justify-center items-center py-20`}>
<View
style={tw`w-24 h-24 bg-slate-50 rounded-full items-center justify-center mb-6`}
>
<Ionicons name="time-outline" size={48} color="#94A3B8" />
</View>
<MyText
style={tw`text-slate-900 text-xl font-black tracking-tight`}
>
No Schedules Yet
</MyText>
<MyText
style={tw`text-slate-500 text-center mt-2 font-medium px-8`}
>
Start by creating your first availability schedule using the
button below.
</MyText>
</View>
) : (
schedules?.map((schedule, index) => (
<React.Fragment key={schedule.id}>
<ScheduleItem
schedule={schedule}
index={index}
onDelete={handleDelete}
onViewProducts={handleViewProducts}
onViewGroups={handleViewGroups}
onReplicate={handleReplicate}
/>
{index < schedules.length - 1 && (
<View style={tw`h-px bg-slate-200 w-full`} />
)}
</React.Fragment>
))
)}
</ScrollView>
</View>
</AppContainer>
<MyTouchableOpacity
onPress={handleCreate}
activeOpacity={0.95}
style={tw`absolute bottom-8 right-6 shadow-2xl z-50`}
>
<LinearGradient
colors={['#1570EF', '#194185']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={tw`w-16 h-16 rounded-[24px] items-center justify-center shadow-lg shadow-brand300`}
>
<MaterialIcons name="add" size={32} color="white" />
</LinearGradient>
</MyTouchableOpacity>
{/* Products/Groups Dialog */}
<RawBottomDialog open={dialogOpen} onClose={() => setDialogOpen(false)}>
<View style={tw`p-4`}>
<MyText style={tw`text-lg font-bold mb-4`}>
{dialogType === 'products' ? 'Products' : 'Groups'}
</MyText>
<FlatList
data={dialogType === 'products' ? getProductNames() : getGroupNames()}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => (
<View style={tw`py-3 border-b border-gray-100`}>
<MyText style={tw`text-base text-gray-800`}>{item}</MyText>
</View>
)}
showsVerticalScrollIndicator={false}
style={tw`max-h-80`}
ListEmptyComponent={
<View style={tw`py-8 items-center`}>
<MyText style={tw`text-gray-500`}>
No {dialogType} found
</MyText>
</View>
}
/>
</View>
</RawBottomDialog>
</>
);
}

View file

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

View file

@ -1,64 +0,0 @@
import React from 'react'
import { Formik } from 'formik'
import * as Yup from 'yup'
import { View, Text, TouchableOpacity } from 'react-native'
import { MyTextInput, BottomDropdown, tw } from 'common-ui'
import { trpc } from '@/src/trpc-client'
interface AddressPlaceFormProps {
onSubmit: (values: { placeName: string; zoneId: number | null }) => void
onClose: () => void
}
const AddressPlaceForm: React.FC<AddressPlaceFormProps> = ({ onSubmit, onClose }) => {
const { data: zones } = trpc.admin.address.getZones.useQuery()
const validationSchema = Yup.object({
placeName: Yup.string().required('Place name is required'),
zoneId: Yup.number().optional(),
})
const zoneOptions = zones?.map(z => ({ label: z.zoneName, value: z.id })) || []
return (
<View style={tw`p-4`}>
<Text style={tw`text-lg font-semibold mb-4`}>Add Place</Text>
<Formik
initialValues={{ placeName: '', zoneId: null as number | null }}
validationSchema={validationSchema}
onSubmit={(values) => {
onSubmit(values)
onClose()
}}
>
{({ handleChange, setFieldValue, handleSubmit, values, errors, touched }) => (
<View>
<MyTextInput
label="Place Name"
value={values.placeName}
onChangeText={handleChange('placeName')}
error={!!(touched.placeName && errors.placeName)}
/>
<BottomDropdown
label="Zone (Optional)"
value={values.zoneId as any}
options={zoneOptions}
onValueChange={(value) => setFieldValue('zoneId', value as number | undefined)}
placeholder="Select Zone"
/>
<View style={tw`flex-row justify-between mt-4`}>
<TouchableOpacity style={tw`bg-gray2 px-4 py-2 rounded`} onPress={onClose}>
<Text style={tw`text-gray-900`}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={tw`bg-blue1 px-4 py-2 rounded`} onPress={() => handleSubmit()}>
<Text style={tw`text-white`}>Create</Text>
</TouchableOpacity>
</View>
</View>
)}
</Formik>
</View>
)
}
export default AddressPlaceForm

View file

@ -1,51 +0,0 @@
import React from 'react'
import { Formik } from 'formik'
import * as Yup from 'yup'
import { View, Text, TouchableOpacity } from 'react-native'
import { MyTextInput, tw } from 'common-ui'
interface AddressZoneFormProps {
onSubmit: (values: { zoneName: string }) => void
onClose: () => void
}
const AddressZoneForm: React.FC<AddressZoneFormProps> = ({ onSubmit, onClose }) => {
const validationSchema = Yup.object({
zoneName: Yup.string().required('Zone name is required'),
})
return (
<View style={tw`p-4`}>
<Text style={tw`text-lg font-semibold mb-4`}>Add Zone</Text>
<Formik
initialValues={{ zoneName: '' }}
validationSchema={validationSchema}
onSubmit={(values) => {
onSubmit(values)
onClose()
}}
>
{({ handleChange, handleSubmit, values, errors, touched }) => (
<View>
<MyTextInput
label="Zone Name"
value={values.zoneName}
onChangeText={handleChange('zoneName')}
error={!!(touched.zoneName && errors.zoneName)}
/>
<View style={tw`flex-row justify-between mt-4`}>
<TouchableOpacity style={tw`bg-gray2 px-4 py-2 rounded`} onPress={onClose}>
<Text style={tw`text-gray-900`}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={tw`bg-blue1 px-4 py-2 rounded`} onPress={() => handleSubmit()}>
<Text style={tw`text-white`}>Create</Text>
</TouchableOpacity>
</View>
</View>
)}
</Formik>
</View>
)
}
export default AddressZoneForm

View file

@ -0,0 +1,237 @@
import React, { useState } from 'react';
import { View, TouchableOpacity, Alert, ScrollView } from 'react-native';
import { useFormik } from 'formik';
import { MyText, tw, MyTextInput, MyTouchableOpacity, DateTimePickerMod } from 'common-ui';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import ProductsSelector from './ProductsSelector';
import { trpc } from '../src/trpc-client';
interface AvailabilityScheduleFormProps {
onClose: () => void;
onSuccess: () => void;
initialProductIds?: number[];
initialGroupIds?: number[];
}
const AvailabilityScheduleForm: React.FC<AvailabilityScheduleFormProps> = ({
onClose,
onSuccess,
initialProductIds,
initialGroupIds,
}) => {
const createSchedule = trpc.admin.productAvailabilitySchedules.create.useMutation();
const { data: groupsData } = trpc.admin.product.getGroups.useQuery();
// Map groups data to match ProductsSelector types (convert price from string to number)
const groups = (groupsData?.groups || []).map(group => ({
...group,
products: group.products.map(product => ({
...product,
price: parseFloat(product.price as unknown as string) || 0,
})),
}));
const formik = useFormik({
initialValues: {
scheduleName: '',
timeDate: null as Date | null,
action: 'in' as 'in' | 'out',
productIds: initialProductIds || ([] as number[]),
groupIds: initialGroupIds || ([] as number[]),
},
validate: (values) => {
const errors: {[key: string]: string} = {};
if (!values.scheduleName.trim()) {
errors.scheduleName = 'Schedule name is required';
}
if (!values.timeDate) {
errors.timeDate = 'Time is required';
}
if (!values.action) {
errors.action = 'Action is required';
}
if (values.productIds.length === 0) {
errors.productIds = 'At least one product must be selected';
}
return errors;
},
onSubmit: async (values) => {
try {
// Convert Date to HH:MM string
const hours = values.timeDate!.getHours().toString().padStart(2, '0');
const minutes = values.timeDate!.getMinutes().toString().padStart(2, '0');
const timeString = `${hours}:${minutes}`;
await createSchedule.mutateAsync({
scheduleName: values.scheduleName,
time: timeString,
action: values.action,
productIds: values.productIds,
groupIds: values.groupIds,
});
Alert.alert('Success', 'Schedule created successfully');
onSuccess();
onClose();
} catch (error: any) {
Alert.alert('Error', error.message || 'Failed to create schedule');
}
},
});
const actionOptions = [
{ label: 'In Stock', value: 'in' },
{ label: 'Out of Stock', value: 'out' },
];
return (
<View style={tw`flex-1 bg-white`}>
{/* Header */}
<View style={tw`flex-row items-center justify-between p-4 border-b border-gray-200 bg-white`}>
<MyText style={tw`text-xl font-bold text-gray-900`}>
Create Availability Schedule
</MyText>
<MyTouchableOpacity onPress={onClose}>
<MaterialIcons name="close" size={24} color="#6B7280" />
</MyTouchableOpacity>
</View>
<ScrollView style={tw`flex-1 p-4`} showsVerticalScrollIndicator={false}>
{/* Schedule Name */}
<View style={tw`mb-4`}>
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
Schedule Name
</MyText>
<MyTextInput
placeholder="Enter schedule name"
value={formik.values.scheduleName}
onChangeText={formik.handleChange('scheduleName')}
onBlur={formik.handleBlur('scheduleName')}
style={tw`border rounded-lg p-3 ${
formik.touched.scheduleName && formik.errors.scheduleName
? 'border-red-500'
: 'border-gray-300'
}`}
/>
{formik.touched.scheduleName && formik.errors.scheduleName && (
<MyText style={tw`text-red-500 text-xs mt-1`}>
{formik.errors.scheduleName}
</MyText>
)}
</View>
{/* Time */}
<View style={tw`mb-4`}>
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
Time
</MyText>
<DateTimePickerMod
value={formik.values.timeDate}
setValue={(date) => formik.setFieldValue('timeDate', date)}
timeOnly={true}
showLabels={false}
/>
{formik.touched.timeDate && formik.errors.timeDate && (
<MyText style={tw`text-red-500 text-xs mt-1`}>
{formik.errors.timeDate}
</MyText>
)}
</View>
{/* Action */}
<View style={tw`mb-4`}>
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
Action
</MyText>
<View style={tw`flex-row gap-3`}>
{actionOptions.map((option) => (
<TouchableOpacity
key={option.value}
onPress={() => formik.setFieldValue('action', option.value)}
style={tw`flex-1 flex-row items-center p-4 rounded-lg border ${
formik.values.action === option.value
? 'bg-blue-50 border-blue-500'
: 'bg-white border-gray-300'
}`}
>
<View
style={tw`w-5 h-5 rounded-full border-2 mr-3 items-center justify-center ${
formik.values.action === option.value
? 'border-blue-500'
: 'border-gray-300'
}`}
>
{formik.values.action === option.value && (
<View style={tw`w-3 h-3 rounded-full bg-blue-500`} />
)}
</View>
<MyText
style={tw`font-medium ${
formik.values.action === option.value
? 'text-blue-700'
: 'text-gray-700'
}`}
>
{option.label}
</MyText>
</TouchableOpacity>
))}
</View>
</View>
{/* Products and Groups */}
<View style={tw`mb-4`}>
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
Products & Groups
</MyText>
<ProductsSelector
value={formik.values.productIds}
onChange={(value) => formik.setFieldValue('productIds', value)}
groups={groups}
selectedGroupIds={formik.values.groupIds}
onGroupChange={(groupIds) => formik.setFieldValue('groupIds', groupIds)}
showGroups={true}
label="Select Products"
placeholder="Select products for this schedule"
/>
{formik.touched.productIds && formik.errors.productIds && (
<MyText style={tw`text-red-500 text-xs mt-1`}>
{formik.errors.productIds}
</MyText>
)}
</View>
{/* Spacer for bottom padding */}
<View style={tw`h-24`} />
</ScrollView>
{/* Footer Buttons */}
<View style={tw`p-4 border-t border-gray-200 bg-white flex-row gap-3`}>
<MyTouchableOpacity
onPress={onClose}
style={tw`flex-1 py-3 px-4 rounded-lg border border-gray-300 items-center`}
>
<MyText style={tw`font-medium text-gray-700`}>Cancel</MyText>
</MyTouchableOpacity>
<MyTouchableOpacity
onPress={() => formik.handleSubmit()}
disabled={formik.isSubmitting}
style={tw`flex-1 py-3 px-4 rounded-lg bg-blue-600 items-center ${
formik.isSubmitting ? 'opacity-50' : ''
}`}
>
<MyText style={tw`font-medium text-white`}>
{formik.isSubmitting ? 'Creating...' : 'Create Schedule'}
</MyText>
</MyTouchableOpacity>
</View>
</View>
);
};
export default AvailabilityScheduleForm;

View file

@ -7,8 +7,8 @@ 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';
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStorage';
export interface BannerFormData {
name: string;
@ -53,14 +53,7 @@ export default function BannerForm({
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
const { uploadSingle } = useUploadToObjectStorage();
// Fetch products for dropdown
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery();
const products = productsData?.products || [];
const { uploadSingle, isUploading } = useUploadToObjectStorage();
const handleImagePick = usePickImage({
setFile: async (assets: any) => {
@ -99,14 +92,14 @@ export default function BannerForm({
if (selectedImages.length > 0) {
const { blob, mimeType } = selectedImages[0];
const { presignedUrl } = await uploadSingle(blob, mimeType, 'store');
const { key, presignedUrl } = await uploadSingle(blob, mimeType, 'store');
imageUrl = presignedUrl;
}
await onSubmit(values, imageUrl);
} catch (error) {
console.error('Upload error:', error);
Alert.alert('Error', 'Failed to upload image');
Alert.alert('Error', error instanceof Error ? error.message : 'Failed to upload image');
}
};
@ -218,15 +211,15 @@ export default function BannerForm({
<MyTouchableOpacity
onPress={() => handleSubmit()}
disabled={isSubmitting || !isValid || !dirty}
disabled={isSubmitting || isUploading || !isValid || !dirty}
style={tw`flex-1 rounded-lg py-4 items-center ${
isSubmitting || !isValid || !dirty
isSubmitting || isUploading || !isValid || !dirty
? 'bg-blue-400'
: 'bg-blue-600'
}`}
>
<MyText style={tw`text-white font-semibold`}>
{isSubmitting ? 'Saving...' : submitButtonText}
{isUploading ? 'Uploading...' : isSubmitting ? 'Saving...' : submitButtonText}
</MyText>
</MyTouchableOpacity>
</View>

View 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>
);
};

View file

@ -2,11 +2,11 @@ import React, { forwardRef, useState, useEffect, useMemo } from 'react';
import { View, TouchableOpacity, Alert } from 'react-native';
import { Formik } from 'formik';
import * as Yup from 'yup';
import { MyTextInput, BottomDropdown, MyText, tw, ImageUploader } from 'common-ui';
import { MyTextInput, BottomDropdown, MyText, tw, ImageUploaderNeo } from 'common-ui';
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 { useUploadToObjectStorage } from '../hooks/useUploadToObjectStorage';
export interface StoreFormData {
name: string;
@ -16,6 +16,12 @@ export interface StoreFormData {
products: number[];
}
interface StoreImage {
uri: string;
mimeType: string;
isExisting: boolean;
}
export interface StoreFormRef {
// Add methods if needed
}
@ -28,6 +34,11 @@ interface StoreFormProps {
storeId?: number;
}
// Extend Formik values with images array
interface FormikStoreValues extends StoreFormData {
images: StoreImage[];
}
const validationSchema = Yup.object().shape({
name: Yup.string().required('Name is required'),
description: Yup.string(),
@ -41,9 +52,23 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
const { data: staffData } = trpc.admin.staffUser.getStaff.useQuery();
const { data: productsData } = trpc.admin.product.getProducts.useQuery();
const [formInitialValues, setFormInitialValues] = useState<StoreFormData>(initialValues);
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
// Build initial form values with images array
const buildInitialValues = (): FormikStoreValues => {
const images: StoreImage[] = [];
if (initialValues.imageUrl) {
images.push({
uri: initialValues.imageUrl,
mimeType: 'image/jpeg',
isExisting: true,
});
}
return {
...initialValues,
images,
};
};
const [formInitialValues, setFormInitialValues] = useState<FormikStoreValues>(buildInitialValues());
// For edit mode, pre-select products belonging to this store
const initialSelectedProducts = useMemo(() => {
@ -55,57 +80,18 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
useEffect(() => {
setFormInitialValues({
...initialValues,
...buildInitialValues(),
products: initialSelectedProducts,
});
}, [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 handleImagePick = usePickImage({
setFile: async (assets: any) => {
if (!assets || (Array.isArray(assets) && assets.length === 0)) {
setSelectedImages([]);
setDisplayImages([]);
return;
}
const files = Array.isArray(assets) ? assets : [assets];
const blobPromises = files.map(async (asset) => {
const response = await fetch(asset.uri);
const blob = await response.blob();
return { blob, mimeType: asset.mimeType || 'image/jpeg' };
});
const blobArray = await Promise.all(blobPromises);
setSelectedImages(blobArray);
setDisplayImages(files.map(asset => ({ uri: asset.uri })));
},
multiple: false, // Single image for stores
});
const handleRemoveImage = (uri: string) => {
const index = displayImages.findIndex(img => img.uri === uri);
if (index !== -1) {
const newDisplay = displayImages.filter((_, i) => i !== index);
const newFiles = selectedImages.filter((_, i) => i !== index);
setDisplayImages(newDisplay);
setSelectedImages(newFiles);
}
};
return (
<Formik
initialValues={formInitialValues}
@ -114,23 +100,78 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
enableReinitialize
>
{({ handleChange, handleSubmit, values, setFieldValue, errors, touched }) => {
// Image picker that adds to Formik field
const handleImagePick = usePickImage({
setFile: async (assets: any) => {
if (!assets || (Array.isArray(assets) && assets.length === 0)) {
return;
}
const files = Array.isArray(assets) ? assets : [assets];
const newImages: StoreImage[] = files.map((asset) => ({
uri: asset.uri,
mimeType: asset.mimeType || 'image/jpeg',
isExisting: false,
}));
// Add to Formik images field
const currentImages = values.images || [];
setFieldValue('images', [...currentImages, ...newImages]);
},
multiple: false,
});
// Remove image - works for both existing and new
const handleRemoveImage = (image: { uri: string; mimeType: string }) => {
const currentImages = values.images || [];
const removedImage = currentImages.find(img => img.uri === image.uri);
const newImages = currentImages.filter(img => img.uri !== image.uri);
setFieldValue('images', newImages);
// If we removed an existing image, also clear the imageUrl
if (removedImage?.isExisting) {
setFieldValue('imageUrl', undefined);
}
};
const submit = async () => {
try {
let imageUrl: string | undefined;
if (selectedImages.length > 0) {
const { blob, mimeType } = selectedImages[0];
const { presignedUrl } = await uploadSingle(blob, mimeType, 'store');
imageUrl = presignedUrl;
// Get new images that need to be uploaded
const newImages = values.images.filter(img => !img.isExisting);
if (newImages.length > 0) {
// Upload the first new image (single image for stores)
const image = newImages[0];
const response = await fetch(image.uri);
const imageBlob = await response.blob();
const { key } = await uploadSingle(imageBlob, image.mimeType, 'store');
imageUrl = key;
} else {
// Check if there's an existing image remaining
const existingImage = values.images.find(img => img.isExisting);
if (existingImage) {
imageUrl = existingImage.uri;
}
}
onSubmit({ ...values, imageUrl });
// Submit form with imageUrl (without images array)
const { images, ...submitValues } = values;
onSubmit({ ...submitValues, imageUrl });
} catch (error) {
console.error('Upload error:', error);
Alert.alert('Error', 'Failed to upload image');
Alert.alert('Error', error instanceof Error ? error.message : 'Failed to upload image');
}
};
// Prepare images for ImageUploaderNeo (convert to expected format)
const imagesForUploader = (values.images || []).map(img => ({
uri: img.uri,
mimeType: img.mimeType,
}));
return (
<View>
<MyTextInput
@ -171,17 +212,11 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
/>
<View style={tw`mb-6`}>
<MyText style={tw`text-sm font-bold text-gray-700 mb-3 uppercase tracking-wider`}>Store Image</MyText>
<ImageUploader
images={displayImages}
existingImageUrls={existingImageUrls}
onAddImage={handleImagePick}
<ImageUploaderNeo
images={imagesForUploader}
onUploadImage={handleImagePick}
onRemoveImage={handleRemoveImage}
onRemoveExistingImage={() =>
setFormInitialValues((prev) => ({
...prev,
imageUrl: undefined,
}))
}
allowMultiple={false}
/>
</View>

View file

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

View file

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

View file

@ -6,7 +6,7 @@
"build": {
"development": {
"distribution": "internal",
"channel": "development"
"autoIncrement": true
},
"preview": {
"distribution": "internal",

View file

@ -1,7 +1,7 @@
import { useState } from 'react';
import { trpc } from '../src/trpc-client';
type ContextString = 'review' | 'product_info' | 'notification' | 'store' | 'complaint' | 'profile' | 'tags';
type ContextString = 'review' | 'product_info' | 'notification' | 'store';
interface UploadInput {
blob: Blob;

View file

@ -1,10 +1,13 @@
import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle, useMemo } from 'react';
import { View, TouchableOpacity } from 'react-native';
import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle } from 'react';
import { View, TouchableOpacity, Alert } 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 { useUploadToObjectStorage } from '../../hooks/useUploadToObjectStorage';
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, imageKeys?: string[], imagesToDelete?: string[]) => void;
isLoading: boolean;
existingImages?: ImageUploaderNeoItem[];
existingImageKeys?: string[];
existingImages?: string[];
}
const unitOptions = [
@ -48,22 +50,19 @@ 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 [newImages, setNewImages] = useState<{ blob: Blob; mimeType: string; uri: string }[]>([]);
const [existingImagesState, setExistingImagesState] = useState<string[]>(existingImages);
const { upload, isUploading } = useUploadToObjectStorage();
const { data: storesData } = trpc.common.getStoresSummary.useQuery();
const storeOptions = storesData?.stores.map(store => ({
@ -71,50 +70,83 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
value: store.id,
})) || [];
const { data: tagsData } = trpc.admin.product.getProductTags.useQuery();
const tagOptions = tagsData?.tags.map(tag => ({
const { data: tagsData } = trpc.admin.tag.getTags.useQuery();
const tagOptions = tagsData?.tags.map((tag: { tagName: string; id: number }) => ({
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];
// Initialize existing images state when existingImages prop changes
useEffect(() => {
console.log('changing existing imaes statte')
setExistingImagesState(existingImages);
}, [existingImages]);
const pickImage = usePickImage({
setFile: async (assets: any) => {
if (!assets || (Array.isArray(assets) && assets.length === 0)) {
return;
}
const files = Array.isArray(assets) ? assets : [assets];
const imageData = await Promise.all(
files.map(async (asset) => {
const response = await fetch(asset.uri);
const blob = await response.blob();
return {
blob,
mimeType: asset.mimeType || 'image/jpeg',
uri: asset.uri
};
})
);
setNewImages(prev => [...prev, ...imageData]);
},
multiple: true,
});
return map;
}, [existingImages, existingImageKeys]);
// Calculate which existing images were deleted
const deletedImages = existingImages.filter(img => !existingImagesState.includes(img));
// Display images for ImageUploader component
const displayImages = newImages.map(img => ({ uri: img.uri }));
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={async (values) => {
try {
let imageKeys: string[] = [];
onSubmit(
values,
newImages.map(img => ({ url: img.imgUrl, mimeType: img.mimeType })),
deletedImageKeys,
);
// Upload new images if any
if (newImages.length > 0) {
const result = await upload({
images: newImages.map(img => ({ blob: img.blob, mimeType: img.mimeType })),
contextString: 'product_info',
});
imageKeys = result.keys;
}
onSubmit(values, imageKeys, deletedImages);
} catch (error) {
Alert.alert('Error', error instanceof Error ? error.message : 'Failed to upload images');
}
}}
enableReinitialize
>
{({ handleChange, handleSubmit, values, setFieldValue, resetForm }) => {
// Clear form when screen comes into focus
const clearForm = useCallback(() => {
setImages([]);
setNewImages([]);
setExistingImagesState([]);
resetForm();
}, [resetForm]);
useFocusCallback(clearForm);
// Update ref with current clearForm function
useImperativeHandle(ref, () => ({
clearImages: clearForm,
}), [clearForm]);
@ -149,18 +181,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={displayImages}
onAddImage={pickImage}
onRemoveImage={(uri) => setNewImages(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={displayImages}
onAddImage={pickImage}
onRemoveImage={(uri) => setNewImages(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 +228,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 +278,8 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
style={{ marginBottom: 16 }}
/>
<View style={tw`flex-row items-center mb-4`}>
<Checkbox
checked={values.isSuspended}
@ -223,7 +294,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,13 +312,94 @@ 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}
style={tw`px-4 py-2 rounded-lg shadow-lg items-center mt-2 ${isLoading ? 'bg-gray-400' : 'bg-blue-500'}`}
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'}`}
>
<MyText style={tw`text-white text-lg font-bold`}>
{isLoading ? (mode === 'create' ? 'Creating...' : 'Updating...') : (mode === 'create' ? 'Create Product' : 'Update Product')}
{isUploading ? 'Uploading Images...' : isLoading ? (mode === 'create' ? 'Creating...' : 'Updating...') : (mode === 'create' ? 'Create Product' : 'Update Product')}
</MyText>
</TouchableOpacity>
</View>

View file

@ -1,9 +1,12 @@
import React, { useState, useEffect, forwardRef, useCallback } from 'react';
import { View, TouchableOpacity } from 'react-native';
import { View, TouchableOpacity, Alert } 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';
import { useUploadToObjectStorage } from '../../hooks/useUploadToObjectStorage';
interface StoreOption {
id: number;
@ -21,7 +24,7 @@ interface TagFormProps {
mode: 'create' | 'edit';
initialValues: TagFormData;
existingImageUrl?: string;
onSubmit: (values: TagFormData, images: ImageUploaderNeoItem[], removedExisting: boolean) => void;
onSubmit: (values: TagFormData, imageKey?: string, deleteExistingImage?: boolean) => void;
isLoading: boolean;
stores?: StoreOption[];
}
@ -29,29 +32,39 @@ 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 [newImage, setNewImage] = useState<{ blob: Blob; mimeType: string; uri: string } | null>(null);
const [isDashboardTagChecked, setIsDashboardTagChecked] = useState<boolean>(Boolean(initialValues.isDashboardTag));
const existingImageUrl = existingImageUrlRaw || ''
const stores = storesRaw || []
const { uploadSingle, isUploading } = useUploadToObjectStorage();
// 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]);
}, [initialValues.isDashboardTag]);
const pickImage = usePickImage({
setFile: async (assets: any) => {
if (!assets || (Array.isArray(assets) && assets.length === 0)) {
setNewImage(null);
return;
}
const asset = Array.isArray(assets) ? assets[0] : assets;
const response = await fetch(asset.uri);
const blob = await response.blob();
setNewImage({
blob,
mimeType: asset.mimeType || 'image/jpeg',
uri: asset.uri
});
},
multiple: false,
});
const validationSchema = Yup.object().shape({
tagName: Yup.string()
@ -62,18 +75,44 @@ const TagForm = forwardRef<any, TagFormProps>(({
.max(500, 'Description must be less than 500 characters'),
});
// Display images for ImageUploader
const displayImages = newImage ? [{ uri: newImage.uri }] : [];
const existingImages = existingImageUrl ? [existingImageUrl] : [];
return (
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={(values) => onSubmit(values, images, removedExisting)}
onSubmit={async (values) => {
try {
let imageKey: string | undefined;
let deleteExistingImage = false;
// Handle image upload
if (newImage) {
const result = await uploadSingle(newImage.blob, newImage.mimeType, 'product_info');
imageKey = result.key;
// If we're uploading a new image and there's an existing one, mark it for deletion
if (existingImageUrl) {
deleteExistingImage = true;
}
} else if (mode === 'edit' && !newImage && existingImageUrl) {
// In edit mode, if no new image and existing was removed
// This would need UI to explicitly remove image
// For now, we don't support explicit deletion without replacement
}
onSubmit(values, imageKey, deleteExistingImage);
} catch (error) {
Alert.alert('Error', error instanceof Error ? error.message : 'Failed to upload image');
}
}}
enableReinitialize
>
{({ handleChange, handleSubmit, values, setFieldValue, errors, touched, setFieldValue: formikSetFieldValue, resetForm }) => {
{({ handleChange, handleSubmit, values, setFieldValue, errors, touched, resetForm }) => {
// Clear form when screen comes into focus
const clearForm = useCallback(() => {
setImages([])
setRemovedExisting(false)
setNewImage(null);
setIsDashboardTagChecked(false);
resetForm();
}, [resetForm]);
@ -106,22 +145,15 @@ const TagForm = forwardRef<any, TagFormProps>(({
Tag Image {mode === 'edit' ? '(Upload new to replace)' : '(Optional)'}
</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={displayImages}
existingImageUrls={mode === 'edit' ? existingImages : []}
onAddImage={pickImage}
onRemoveImage={() => setNewImage(null)}
onRemoveExistingImage={mode === 'edit' ? () => {
// In edit mode, this would trigger deletion of existing image
// But we need to implement this logic in the parent
} : undefined}
/>
</View>
@ -132,7 +164,7 @@ const TagForm = forwardRef<any, TagFormProps>(({
onPress={() => {
const newValue = !isDashboardTagChecked;
setIsDashboardTagChecked(newValue);
formikSetFieldValue('isDashboardTag', newValue);
setFieldValue('isDashboardTag', newValue);
}}
/>
<MyText style={tw`ml-3 text-gray-800`}>Mark as Dashboard Tag</MyText>
@ -153,7 +185,7 @@ const TagForm = forwardRef<any, TagFormProps>(({
}))}
onValueChange={(selectedValues) => {
const numericValues = (selectedValues as string[]).map(v => parseInt(v));
formikSetFieldValue('relatedStores', numericValues);
setFieldValue('relatedStores', numericValues);
}}
multiple={true}
/>
@ -161,11 +193,11 @@ const TagForm = forwardRef<any, TagFormProps>(({
<TouchableOpacity
onPress={() => handleSubmit()}
disabled={isLoading}
style={tw`px-4 py-3 rounded-lg shadow-lg items-center ${isLoading ? 'bg-gray-400' : 'bg-blue-500'}`}
disabled={isLoading || isUploading}
style={tw`px-4 py-3 rounded-lg shadow-lg items-center ${isLoading || isUploading ? 'bg-gray-400' : 'bg-blue-500'}`}
>
<MyText style={tw`text-white text-lg font-bold`}>
{isLoading ? (mode === 'create' ? 'Creating...' : 'Updating...') : (mode === 'create' ? 'Create Tag' : 'Update Tag')}
{isUploading ? 'Uploading Image...' : isLoading ? (mode === 'create' ? 'Creating...' : 'Updating...') : (mode === 'create' ? 'Create Tag' : 'Update Tag')}
</MyText>
</TouchableOpacity>
</View>

View file

@ -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 { trpc } from '../trpc-client';
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 deleteTag = trpc.admin.tag.deleteTag.useMutation();
const handleOpenMenu = () => {
setIsOpen(true);
@ -63,7 +63,7 @@ export const TagMenu: React.FC<TagMenuProps> = ({
const errorMessage = error.message || 'Failed to delete tag';
Alert.alert('Error', errorMessage);
},
})
});
};
const options = [

9
packages/db_helper_postgres/.env → apps/backend/.env Normal file → Executable file
View file

@ -1,7 +1,10 @@
ENV_MODE=PROD
DATABASE_URL=postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer #technocracy
# 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
SQLITE_DB_PATH='./sqlite.db'
DB_DIALECT='sqlite'
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
@ -17,10 +20,10 @@ S3_REGION=apac
S3_ACCESS_KEY_ID=8fab47503efb9547b50e4fb317e35cc7
S3_SECRET_ACCESS_KEY=47c2eb5636843cf568dda7ad0959a3e42071303f26dbdff94bd45a3c33dcd950
S3_URL=https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com
S3_BUCKET_NAME=meatfarmer
S3_BUCKET_NAME=meatfarmer-dev
EXPO_ACCESS_TOKEN=Asvpy8cByRh6T4ksnWScO6PLcio2n35-BwES5zK-
JWT_SECRET=my_meatfarmer_jwt_secret_key
ASSETS_DOMAIN=https://assets.freshyo.in/
ASSETS_DOMAIN=https://assets2.freshyo.in/
API_CACHE_KEY=api-cache-dev
# CLOUDFLARE_API_TOKEN=I8Vp4E9TX58E8qEDeH0nTFDS2d2zXNYiXvbs4Ckj
CLOUDFLARE_API_TOKEN=N7jAg5X-RUj_fVfMW6zbfJ8qIYc81TSIKKlbZ6oh

View file

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

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

6
apps/backend/drizzle.config.ts Executable file
View file

@ -0,0 +1,6 @@
import postgresConfig from '../db-helper-postgres/drizzle.config'
import sqliteConfig from '../db-helper-sqlite/drizzle.config'
export default process.env.DB_DIALECT === 'sqlite'
? sqliteConfig
: postgresConfig

Some files were not shown because too many files have changed in this diff Show more