Compare commits
16 commits
main
...
edge_redac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17e2644759 | ||
|
|
23be301cc0 | ||
|
|
95d2c861c0 | ||
|
|
a23d3bf5b8 | ||
|
|
56b606ebcf | ||
|
|
cd5ab79f44 | ||
|
|
b49015b446 | ||
|
|
a0a05615b1 | ||
|
|
501667a4d2 | ||
|
|
1122159552 | ||
|
|
8f4cddee1a | ||
|
|
77e3eb21d6 | ||
|
|
b38ff13950 | ||
|
|
e2abc7cb02 | ||
|
|
4f1f52ffee | ||
|
|
71cad727fd |
1057 changed files with 27228 additions and 608848 deletions
|
|
@ -6,3 +6,4 @@ apps/users-ui/src
|
||||||
apps/admin-ui/app
|
apps/admin-ui/app
|
||||||
apps/users-ui/src
|
apps/users-ui/src
|
||||||
**/package-lock.json
|
**/package-lock.json
|
||||||
|
test/
|
||||||
|
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -8,11 +8,13 @@ yarn-error.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
*.apk
|
*.apk
|
||||||
|
**/appBinaries
|
||||||
|
|
||||||
**/.wrangler/*
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
test/appBinaries
|
||||||
|
|
||||||
# Runtime data
|
# Runtime data
|
||||||
pids
|
pids
|
||||||
*.pid
|
*.pid
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# Agent Instructions for Meat Farmer Monorepo
|
# Agent Instructions for Meat Farmer Monorepo
|
||||||
|
|
||||||
## Important instructions
|
## 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.
|
- Don't run any drizzle migrations. User will handle it.
|
||||||
|
|
||||||
## Code Style Guidelines
|
## 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
|
- Database: Drizzle ORM with PostgreSQL
|
||||||
|
|
||||||
## Important Notes
|
## 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
|
- Don't do anything with git. Don't do git add or git commit. That will be managed entirely by the user
|
||||||
|
|
|
||||||
|
|
@ -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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
12
apps/admin-ui/.expo/types/router.d.ts
vendored
12
apps/admin-ui/.expo/types/router.d.ts
vendored
File diff suppressed because one or more lines are too long
|
|
@ -63,21 +63,7 @@
|
||||||
"backgroundColor": "#fff0f6"
|
"backgroundColor": "#fff0f6"
|
||||||
},
|
},
|
||||||
"edgeToEdgeEnabled": true,
|
"edgeToEdgeEnabled": true,
|
||||||
"package": "in.freshyo.adminui",
|
"package": "in.freshyo.adminui"
|
||||||
"intentFilters": [
|
|
||||||
{
|
|
||||||
"action": "VIEW",
|
|
||||||
"autoVerify": true,
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"scheme": "https",
|
|
||||||
"host": "ui.freshyo.in",
|
|
||||||
"pathPrefix": "/manage-orders/order-details"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"category": ["BROWSABLE", "DEFAULT"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"bundler": "metro",
|
"bundler": "metro",
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,7 @@ export default function Layout() {
|
||||||
<Drawer.Screen name="coupons" options={{ title: "Coupons" }} />
|
<Drawer.Screen name="coupons" options={{ title: "Coupons" }} />
|
||||||
<Drawer.Screen name="slots" options={{ title: "Slots" }} />
|
<Drawer.Screen name="slots" options={{ title: "Slots" }} />
|
||||||
<Drawer.Screen name="vendor-snippets" options={{ title: "Vendor Snippets" }} />
|
<Drawer.Screen name="vendor-snippets" options={{ title: "Vendor Snippets" }} />
|
||||||
<Drawer.Screen name="stores" options={{ title: "Stores" }} />
|
<Drawer.Screen name="stores" options={{ title: "Stores" }} />
|
||||||
<Drawer.Screen name="product-tags" options={{ title: "Product Tags" }} />
|
<Drawer.Screen name="product-tags" options={{ title: "Product Tags" }} />
|
||||||
<Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
|
<Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
|
||||||
<Drawer.Screen name="user-management" options={{ title: "User Management" }} />
|
<Drawer.Screen name="user-management" options={{ title: "User Management" }} />
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,6 @@ export default function CreateCoupon() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const createCoupon = trpc.admin.coupon.create.useMutation();
|
const createCoupon = trpc.admin.coupon.create.useMutation();
|
||||||
const createReservedCoupon = trpc.admin.coupon.createReservedCoupon.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) => {
|
const handleCreateCoupon = (values: any) => {
|
||||||
console.log('Form values:', values); // Debug log
|
console.log('Form values:', values); // Debug log
|
||||||
|
|
@ -41,9 +27,7 @@ export default function CreateCoupon() {
|
||||||
if (isLoading) return; // Prevent double submission
|
if (isLoading) return; // Prevent double submission
|
||||||
|
|
||||||
mutation.mutate(payload, {
|
mutation.mutate(payload, {
|
||||||
onSuccess: async () => {
|
onSuccess: () => {
|
||||||
await refetchCoupons()
|
|
||||||
await refetchReservedCoupons()
|
|
||||||
Alert.alert('Success', `${isReservedCoupon ? 'Reserved coupon' : 'Coupon'} created successfully`, [
|
Alert.alert('Success', `${isReservedCoupon ? 'Reserved coupon' : 'Coupon'} created successfully`, [
|
||||||
{ text: 'OK', onPress: () => router.back() }
|
{ text: 'OK', onPress: () => router.back() }
|
||||||
]);
|
]);
|
||||||
|
|
@ -62,4 +46,4 @@ export default function CreateCoupon() {
|
||||||
/>
|
/>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -12,21 +12,7 @@ export default function EditCoupon() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
const couponId = parseInt(id as string);
|
const couponId = parseInt(id as string);
|
||||||
|
|
||||||
const { data: coupon, isLoading, refetch } = trpc.admin.coupon.getById.useQuery({ id: couponId });
|
const { data: coupon, isLoading } = 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 updateCoupon = trpc.admin.coupon.update.useMutation();
|
const updateCoupon = trpc.admin.coupon.update.useMutation();
|
||||||
|
|
||||||
const handleUpdateCoupon = (values: CreateCouponPayload & { isReservedCoupon?: boolean }) => {
|
const handleUpdateCoupon = (values: CreateCouponPayload & { isReservedCoupon?: boolean }) => {
|
||||||
|
|
@ -38,10 +24,7 @@ export default function EditCoupon() {
|
||||||
delete updates.targetUsers;
|
delete updates.targetUsers;
|
||||||
|
|
||||||
updateCoupon.mutate({ id: couponId, updates }, {
|
updateCoupon.mutate({ id: couponId, updates }, {
|
||||||
onSuccess: async () => {
|
onSuccess: () => {
|
||||||
await refetch()
|
|
||||||
await refetchCoupons()
|
|
||||||
await refetchReservedCoupons()
|
|
||||||
Alert.alert('Success', 'Coupon updated successfully', [
|
Alert.alert('Success', 'Coupon updated successfully', [
|
||||||
{ text: 'OK', onPress: () => router.back() }
|
{ text: 'OK', onPress: () => router.back() }
|
||||||
]);
|
]);
|
||||||
|
|
@ -97,4 +80,4 @@ export default function EditCoupon() {
|
||||||
/>
|
/>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -6,7 +6,14 @@ import { trpc } from '../../../src/trpc-client';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
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> = {
|
const CONST_LABELS: Record<string, string> = {
|
||||||
minRegularOrderValue: 'Minimum Regular Order Value',
|
minRegularOrderValue: 'Minimum Regular Order Value',
|
||||||
|
|
@ -30,45 +37,23 @@ const CONST_LABELS: Record<string, string> = {
|
||||||
supportEmail: 'Support Email',
|
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 {
|
interface ConstantInputProps {
|
||||||
constantKey: string;
|
constant: ConstantItem;
|
||||||
value: any;
|
|
||||||
setFieldValue: (field: string, value: any) => void;
|
setFieldValue: (field: string, value: any) => void;
|
||||||
|
index: number;
|
||||||
router: any;
|
router: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFieldValue, router }) => {
|
const ConstantInput: React.FC<ConstantInputProps> = ({ constant, setFieldValue, index, router }) => {
|
||||||
const fieldName = constantKey
|
const fieldName = `constants.${index}.value`;
|
||||||
|
|
||||||
// Special handling for popularItems - show navigation button instead of input
|
// Special handling for popularItems - show navigation button instead of input
|
||||||
if (constantKey === 'popularItems') {
|
if (constant.key === 'popularItems') {
|
||||||
|
console.log('key is allItemsOrder')
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
|
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
|
||||||
{CONST_LABELS[constantKey] || constantKey}
|
{CONST_LABELS[constant.key] || constant.key}
|
||||||
</MyText>
|
</MyText>
|
||||||
<MyTouchableOpacity
|
<MyTouchableOpacity
|
||||||
onPress={() => router.push('/(drawer)/customize-app/popular-items')}
|
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`} />
|
<MaterialIcons name="edit" size={20} color="#3b82f6" style={tw`mr-2`} />
|
||||||
<MyText style={tw`text-blue-700 font-medium`}>
|
<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>
|
</MyText>
|
||||||
<MaterialIcons name="chevron-right" size={20} color="#3b82f6" style={tw`ml-2`} />
|
<MaterialIcons name="chevron-right" size={20} color="#3b82f6" style={tw`ml-2`} />
|
||||||
</MyTouchableOpacity>
|
</MyTouchableOpacity>
|
||||||
|
|
@ -85,12 +70,12 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFi
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special handling for allItemsOrder - show navigation button instead of input
|
// Special handling for allItemsOrder - show navigation button instead of input
|
||||||
if (constantKey === 'allItemsOrder') {
|
if (constant.key === 'allItemsOrder') {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
|
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
|
||||||
{CONST_LABELS[constantKey] || constantKey}
|
{CONST_LABELS[constant.key] || constant.key}
|
||||||
</MyText>
|
</MyText>
|
||||||
<MyTouchableOpacity
|
<MyTouchableOpacity
|
||||||
onPress={() => router.push('/(drawer)/customize-app/all-items-order')}
|
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`} />
|
<MaterialIcons name="reorder" size={20} color="#16a34a" style={tw`mr-2`} />
|
||||||
<MyText style={tw`text-green-700 font-medium`}>
|
<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>
|
</MyText>
|
||||||
<MaterialIcons name="chevron-right" size={20} color="#16a34a" style={tw`ml-2`} />
|
<MaterialIcons name="chevron-right" size={20} color="#16a34a" style={tw`ml-2`} />
|
||||||
</MyTouchableOpacity>
|
</MyTouchableOpacity>
|
||||||
|
|
@ -107,20 +92,20 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFi
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle boolean values - show checkbox
|
// Handle boolean values - show checkbox
|
||||||
if (typeof value === 'boolean') {
|
if (typeof constant.value === 'boolean') {
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
|
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
|
||||||
{CONST_LABELS[constantKey] || constantKey}
|
{CONST_LABELS[constant.key] || constant.key}
|
||||||
</MyText>
|
</MyText>
|
||||||
<View style={tw`flex-row items-center`}>
|
<View style={tw`flex-row items-center`}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={value}
|
checked={constant.value}
|
||||||
onPress={() => setFieldValue(fieldName, !value)}
|
onPress={() => setFieldValue(fieldName, !constant.value)}
|
||||||
size={28}
|
size={28}
|
||||||
/>
|
/>
|
||||||
<MyText style={tw`ml-3 text-gray-700`}>
|
<MyText style={tw`ml-3 text-gray-700`}>
|
||||||
{value ? 'Enabled' : 'Disabled'}
|
{constant.value ? 'Enabled' : 'Disabled'}
|
||||||
</MyText>
|
</MyText>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -128,11 +113,11 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFi
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle different value types
|
// Handle different value types
|
||||||
if (typeof value === 'number') {
|
if (typeof constant.value === 'number') {
|
||||||
return (
|
return (
|
||||||
<MyTextInput
|
<MyTextInput
|
||||||
topLabel={CONST_LABELS[constantKey] || constantKey}
|
topLabel={CONST_LABELS[constant.key] || constant.key}
|
||||||
value={value.toString()}
|
value={constant.value.toString()}
|
||||||
onChangeText={(value) => {
|
onChangeText={(value) => {
|
||||||
const numValue = parseFloat(value);
|
const numValue = parseFloat(value);
|
||||||
setFieldValue(fieldName, isNaN(numValue) ? 0 : numValue);
|
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 (
|
return (
|
||||||
<MyTextInput
|
<MyTextInput
|
||||||
topLabel={CONST_LABELS[constantKey] || constantKey}
|
topLabel={CONST_LABELS[constant.key] || constant.key}
|
||||||
value={value.join(', ')}
|
value={constant.value.join(', ')}
|
||||||
onChangeText={(value) => {
|
onChangeText={(value) => {
|
||||||
const arrayValue = value.split(',').map(s => s.trim()).filter(s => s.length > 0);
|
const arrayValue = value.split(',').map(s => s.trim()).filter(s => s.length > 0);
|
||||||
setFieldValue(fieldName, arrayValue);
|
setFieldValue(fieldName, arrayValue);
|
||||||
|
|
@ -160,12 +145,9 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFi
|
||||||
// Default to string
|
// Default to string
|
||||||
return (
|
return (
|
||||||
<MyTextInput
|
<MyTextInput
|
||||||
topLabel={CONST_LABELS[constantKey] || constantKey}
|
topLabel={CONST_LABELS[constant.key] || constant.key}
|
||||||
// value={value === null || value === undefined ? '' : String(value)}
|
value={String(constant.value)}
|
||||||
value={value}
|
onChangeText={(value) => setFieldValue(fieldName, value)}
|
||||||
onChangeText={(value) => {
|
|
||||||
setFieldValue(fieldName, value)
|
|
||||||
}}
|
|
||||||
placeholder="Enter value"
|
placeholder="Enter value"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -179,13 +161,10 @@ export default function CustomizeApp() {
|
||||||
|
|
||||||
const handleSubmit = (values: ConstantFormData) => {
|
const handleSubmit = (values: ConstantFormData) => {
|
||||||
// Filter out constants that haven't changed
|
// Filter out constants that haven't changed
|
||||||
const changedConstants = (constants || []).filter((constant) => {
|
const changedConstants = values.constants.filter((constant, index) => {
|
||||||
const nextValue = values[constant.key]
|
const original = constants?.[index];
|
||||||
return JSON.stringify(nextValue) !== JSON.stringify(constant.value)
|
return original && JSON.stringify(constant.value) !== JSON.stringify(original.value);
|
||||||
}).map((constant) => ({
|
});
|
||||||
key: constant.key,
|
|
||||||
value: values[constant.key],
|
|
||||||
}))
|
|
||||||
|
|
||||||
if (changedConstants.length === 0) {
|
if (changedConstants.length === 0) {
|
||||||
Alert.alert('No Changes', 'No constants were modified.');
|
Alert.alert('No Changes', 'No constants were modified.');
|
||||||
|
|
@ -223,10 +202,9 @@ export default function CustomizeApp() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialValues: ConstantFormData = constants.reduce((acc, constant) => {
|
const initialValues: ConstantFormData = {
|
||||||
acc[constant.key] = constant.value ?? ''
|
constants: constants.map(c => ({ key: c.key, value: c.value ?? '' } as ConstantItem)),
|
||||||
return acc
|
};
|
||||||
}, {} as ConstantFormData)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -241,22 +219,11 @@ export default function CustomizeApp() {
|
||||||
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||||
{({ handleSubmit, values, setFieldValue }) => (
|
{({ handleSubmit, values, setFieldValue }) => (
|
||||||
<View>
|
<View>
|
||||||
{constants.map((constant) => {
|
{values.constants.map((constant, index) => (
|
||||||
if (!CONST_VISIBILITY[constant.key]) {
|
<View key={constant.key} style={tw`mb-4`}>
|
||||||
return null
|
<ConstantInput constant={constant} setFieldValue={setFieldValue} index={index} router={router} />
|
||||||
}
|
</View>
|
||||||
|
))}
|
||||||
return (
|
|
||||||
<View key={constant.key} style={tw`mb-4`}>
|
|
||||||
<ConstantInput
|
|
||||||
constantKey={constant.key}
|
|
||||||
value={values[constant.key]}
|
|
||||||
setFieldValue={setFieldValue}
|
|
||||||
router={router}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
<MyTouchableOpacity
|
<MyTouchableOpacity
|
||||||
onPress={() => handleSubmit()}
|
onPress={() => handleSubmit()}
|
||||||
|
|
@ -273,4 +240,4 @@ export default function CustomizeApp() {
|
||||||
</View>
|
</View>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -21,9 +21,6 @@ export default function CreateBanner() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createBannerMutation = trpc.admin.banner.createBanner.useMutation();
|
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) => {
|
const handleSubmit = async (values: BannerFormData, imageUrl?: string) => {
|
||||||
if (!imageUrl) {
|
if (!imageUrl) {
|
||||||
|
|
@ -42,7 +39,6 @@ export default function CreateBanner() {
|
||||||
redirectUrl: values.redirectUrl || undefined,
|
redirectUrl: values.redirectUrl || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await refetchBanners()
|
|
||||||
Alert.alert('Success', 'Banner created successfully', [
|
Alert.alert('Success', 'Banner created successfully', [
|
||||||
{
|
{
|
||||||
text: 'OK',
|
text: 'OK',
|
||||||
|
|
@ -83,4 +79,4 @@ export default function CreateBanner() {
|
||||||
</View>
|
</View>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -31,9 +31,6 @@ export default function EditBanner() {
|
||||||
const {data: bannerData } = trpc.admin.banner.getBanner.useQuery({
|
const {data: bannerData } = trpc.admin.banner.getBanner.useQuery({
|
||||||
id: parseInt(bannerId)
|
id: parseInt(bannerId)
|
||||||
});
|
});
|
||||||
const { refetch: refetchBanners } = trpc.admin.banner.getBanners.useQuery(undefined, {
|
|
||||||
enabled: false,
|
|
||||||
});
|
|
||||||
const [banner, setBanner] = useState<typeof bannerData>(undefined);
|
const [banner, setBanner] = useState<typeof bannerData>(undefined);
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -103,7 +100,6 @@ export default function EditBanner() {
|
||||||
redirectUrl: values.redirectUrl || undefined,
|
redirectUrl: values.redirectUrl || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await refetchBanners()
|
|
||||||
Alert.alert('Success', 'Banner updated successfully', [
|
Alert.alert('Success', 'Banner updated successfully', [
|
||||||
{
|
{
|
||||||
text: 'OK',
|
text: 'OK',
|
||||||
|
|
@ -164,4 +160,4 @@ export default function EditBanner() {
|
||||||
</View>
|
</View>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -464,4 +464,4 @@ export default function DashboardBanners() {
|
||||||
</View>
|
</View>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -158,6 +158,15 @@ export default function Dashboard() {
|
||||||
iconColor: '#8B5CF6',
|
iconColor: '#8B5CF6',
|
||||||
iconBg: '#F3E8FF',
|
iconBg: '#F3E8FF',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Stocking Schedules',
|
||||||
|
icon: 'schedule',
|
||||||
|
description: 'Manage product stocking schedules',
|
||||||
|
route: '/(drawer)/stocking-schedules',
|
||||||
|
category: 'products',
|
||||||
|
iconColor: '#0EA5E9',
|
||||||
|
iconBg: '#E0F2FE',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Stores',
|
title: 'Stores',
|
||||||
icon: 'store',
|
icon: 'store',
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,7 @@ export default function OrderDetails() {
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"Success",
|
"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);
|
setInitiateRefundDialogOpen(false);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Alert } from 'react-native';
|
import { View, Alert } from 'react-native';
|
||||||
import { useRouter } from 'expo-router';
|
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 TagForm from '@/src/components/TagForm';
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
|
||||||
|
|
||||||
interface TagFormData {
|
interface TagFormData {
|
||||||
tagName: string;
|
tagName: string;
|
||||||
|
|
@ -15,51 +14,31 @@ interface TagFormData {
|
||||||
|
|
||||||
export default function AddTag() {
|
export default function AddTag() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const createTag = trpc.admin.product.createProductTag.useMutation();
|
const createTag = trpc.admin.tag.createTag.useMutation();
|
||||||
const { refetch: refetchTags } = trpc.admin.product.getProductTags.useQuery(undefined, {
|
|
||||||
enabled: false,
|
|
||||||
});
|
|
||||||
const { data: storesData } = trpc.admin.store.getStores.useQuery();
|
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) => {
|
||||||
try {
|
createTag.mutate({
|
||||||
let imageUrl: string | null | undefined;
|
tagName: values.tagName,
|
||||||
let uploadUrls: string[] = []
|
tagDescription: values.tagDescription,
|
||||||
|
isDashboardTag: values.isDashboardTag,
|
||||||
const newImage = images.find((image) => image.mimeType !== null)
|
relatedStores: values.relatedStores,
|
||||||
if (newImage) {
|
imageKey: imageKey,
|
||||||
const response = await fetch(newImage.imgUrl)
|
}, {
|
||||||
const blob = await response.blob()
|
onSuccess: (data) => {
|
||||||
const result = await upload({
|
Alert.alert('Success', 'Tag created successfully', [
|
||||||
images: [{ blob, mimeType: newImage.mimeType || 'image/jpeg' }],
|
{
|
||||||
contextString: 'tags',
|
text: 'OK',
|
||||||
})
|
onPress: () => router.back(),
|
||||||
imageUrl = result.keys[0]
|
},
|
||||||
uploadUrls = result.presignedUrls
|
]);
|
||||||
}
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
await createTag.mutateAsync({
|
const errorMessage = error.message || 'Failed to create tag';
|
||||||
tagName: values.tagName,
|
Alert.alert('Error', errorMessage);
|
||||||
tagDescription: values.tagDescription || undefined,
|
},
|
||||||
imageUrl,
|
});
|
||||||
isDashboardTag: values.isDashboardTag,
|
};
|
||||||
relatedStores: values.relatedStores,
|
|
||||||
uploadUrls,
|
|
||||||
})
|
|
||||||
|
|
||||||
await refetchTags()
|
|
||||||
Alert.alert('Success', 'Tag created successfully', [
|
|
||||||
{
|
|
||||||
text: 'OK',
|
|
||||||
onPress: () => router.back(),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
} catch (error: any) {
|
|
||||||
const errorMessage = error.message || 'Failed to create tag'
|
|
||||||
Alert.alert('Error', errorMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialValues: TagFormData = {
|
const initialValues: TagFormData = {
|
||||||
tagName: '',
|
tagName: '',
|
||||||
|
|
@ -77,8 +56,8 @@ export default function AddTag() {
|
||||||
mode="create"
|
mode="create"
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
isLoading={createTag.isPending || isUploading}
|
isLoading={createTag.isPending}
|
||||||
stores={storesData?.stores.map((store: { id: number; name: string }) => ({ id: store.id, name: store.name })) || []}
|
stores={storesData?.stores.map(store => ({ id: store.id, name: store.name })) || []}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Alert } from 'react-native';
|
import { View, Alert } from 'react-native';
|
||||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
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 TagForm from '@/src/components/TagForm';
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
|
||||||
|
|
||||||
interface TagFormData {
|
interface TagFormData {
|
||||||
tagName: string;
|
tagName: string;
|
||||||
tagDescription: string;
|
tagDescription: string;
|
||||||
isDashboardTag: boolean;
|
isDashboardTag: boolean;
|
||||||
relatedStores: number[];
|
relatedStores: number[];
|
||||||
existingImageUrl?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EditTag() {
|
export default function EditTag() {
|
||||||
|
|
@ -19,60 +17,39 @@ export default function EditTag() {
|
||||||
const { tagId } = useLocalSearchParams<{ tagId: string }>();
|
const { tagId } = useLocalSearchParams<{ tagId: string }>();
|
||||||
const tagIdNum = tagId ? parseInt(tagId) : null;
|
const tagIdNum = tagId ? parseInt(tagId) : null;
|
||||||
|
|
||||||
const { data: tagData, isLoading: isLoadingTag, error: tagError } = trpc.admin.product.getProductTagById.useQuery(
|
const { data: tagData, isLoading: isLoadingTag, error: tagError } = trpc.admin.tag.getTagById.useQuery(
|
||||||
{ id: tagIdNum || 0 },
|
{ id: tagIdNum! },
|
||||||
{ enabled: !!tagIdNum }
|
{ enabled: !!tagIdNum }
|
||||||
)
|
);
|
||||||
const { refetch: refetchTags } = trpc.admin.product.getProductTags.useQuery(undefined, {
|
const updateTag = trpc.admin.tag.updateTag.useMutation();
|
||||||
enabled: false,
|
|
||||||
});
|
|
||||||
const updateTag = trpc.admin.product.updateProductTag.useMutation();
|
|
||||||
const { data: storesData } = trpc.admin.store.getStores.useQuery();
|
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;
|
if (!tagIdNum) return;
|
||||||
|
|
||||||
try {
|
updateTag.mutate({
|
||||||
let imageUrl: string | null | undefined
|
id: tagIdNum,
|
||||||
let uploadUrls: string[] = []
|
tagName: values.tagName,
|
||||||
|
tagDescription: values.tagDescription,
|
||||||
const newImage = images.find((image) => image.mimeType !== null)
|
isDashboardTag: values.isDashboardTag,
|
||||||
if (newImage) {
|
relatedStores: values.relatedStores,
|
||||||
const response = await fetch(newImage.imgUrl)
|
imageKey: imageKey,
|
||||||
const blob = await response.blob()
|
deleteExistingImage: deleteExistingImage,
|
||||||
const result = await upload({
|
}, {
|
||||||
images: [{ blob, mimeType: newImage.mimeType || 'image/jpeg' }],
|
onSuccess: (data) => {
|
||||||
contextString: 'tags',
|
Alert.alert('Success', 'Tag updated successfully', [
|
||||||
})
|
{
|
||||||
imageUrl = result.keys[0]
|
text: 'OK',
|
||||||
uploadUrls = result.presignedUrls
|
onPress: () => router.back(),
|
||||||
} else if (removedExisting) {
|
},
|
||||||
imageUrl = null
|
]);
|
||||||
}
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
await updateTag.mutateAsync({
|
const errorMessage = error.message || 'Failed to update tag';
|
||||||
id: tagIdNum,
|
Alert.alert('Error', errorMessage);
|
||||||
tagName: values.tagName,
|
},
|
||||||
tagDescription: values.tagDescription || undefined,
|
});
|
||||||
imageUrl,
|
};
|
||||||
isDashboardTag: values.isDashboardTag,
|
|
||||||
relatedStores: values.relatedStores,
|
|
||||||
uploadUrls,
|
|
||||||
})
|
|
||||||
|
|
||||||
await refetchTags()
|
|
||||||
Alert.alert('Success', 'Tag updated successfully', [
|
|
||||||
{
|
|
||||||
text: 'OK',
|
|
||||||
onPress: () => router.back(),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
} catch (error: any) {
|
|
||||||
const errorMessage = error.message || 'Failed to update tag'
|
|
||||||
Alert.alert('Error', errorMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoadingTag) {
|
if (isLoadingTag) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -100,7 +77,6 @@ export default function EditTag() {
|
||||||
tagDescription: tag.tagDescription || '',
|
tagDescription: tag.tagDescription || '',
|
||||||
isDashboardTag: tag.isDashboardTag,
|
isDashboardTag: tag.isDashboardTag,
|
||||||
relatedStores: Array.isArray(tag.relatedStores) ? tag.relatedStores : [],
|
relatedStores: Array.isArray(tag.relatedStores) ? tag.relatedStores : [],
|
||||||
existingImageUrl: tag.imageUrl || undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -113,8 +89,8 @@ export default function EditTag() {
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
existingImageUrl={tag.imageUrl || undefined}
|
existingImageUrl={tag.imageUrl || undefined}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
isLoading={updateTag.isPending || isUploading}
|
isLoading={updateTag.isPending}
|
||||||
stores={storesData?.stores.map((store: { id: number; name: string }) => ({ id: store.id, name: store.name })) || []}
|
stores={storesData?.stores.map(store => ({ id: store.id, name: store.name })) || []}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,18 @@ import { tw, MyText, useManualRefresh, useMarkDataFetchers, MyFlatList } from 'c
|
||||||
import { TagMenu } from '@/src/components/TagMenu';
|
import { TagMenu } from '@/src/components/TagMenu';
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
|
|
||||||
interface TagItemData {
|
interface Tag {
|
||||||
id: number;
|
id: number;
|
||||||
tagName: string;
|
tagName: string;
|
||||||
tagDescription: string | null;
|
tagDescription: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
isDashboardTag: boolean;
|
isDashboardTag: boolean;
|
||||||
relatedStores?: unknown;
|
relatedStores?: any;
|
||||||
createdAt: string | Date;
|
createdAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TagItemProps {
|
interface TagItemProps {
|
||||||
item: TagItemData;
|
item: Tag;
|
||||||
onDeleteSuccess: () => void;
|
onDeleteSuccess: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,7 +70,7 @@ const TagHeader: React.FC<TagHeaderProps> = ({ onAddNewTag }) => (
|
||||||
|
|
||||||
export default function ProductTags() {
|
export default function ProductTags() {
|
||||||
const router = useRouter();
|
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 [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
const tags = tagsData?.tags || [];
|
const tags = tagsData?.tags || [];
|
||||||
|
|
@ -139,4 +139,4 @@ export default function ProductTags() {
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,56 +1,37 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Alert } from 'react-native';
|
import { Alert } from 'react-native';
|
||||||
import { AppContainer, ImageUploaderNeoPayload } from 'common-ui';
|
import { AppContainer } from 'common-ui';
|
||||||
import ProductForm from '@/src/components/ProductForm';
|
import ProductForm from '@/src/components/ProductForm';
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
|
||||||
|
|
||||||
export default function AddProduct() {
|
export default function AddProduct() {
|
||||||
const createProduct = trpc.admin.product.createProduct.useMutation();
|
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[]) => {
|
const handleSubmit = (values: any, imageKeys?: string[]) => {
|
||||||
try {
|
createProduct.mutate({
|
||||||
let uploadUrls: string[] = [];
|
name: values.name,
|
||||||
|
shortDescription: values.shortDescription,
|
||||||
if (images.length > 0) {
|
longDescription: values.longDescription,
|
||||||
const blobs = await Promise.all(
|
unitId: parseInt(values.unitId),
|
||||||
images.map(async (img) => {
|
storeId: parseInt(values.storeId),
|
||||||
const response = await fetch(img.url);
|
price: parseFloat(values.price),
|
||||||
const blob = await response.blob();
|
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
|
||||||
return { blob, mimeType: img.mimeType || 'image/jpeg' };
|
incrementStep: 1,
|
||||||
})
|
productQuantity: values.productQuantity || 1,
|
||||||
);
|
isSuspended: values.isSuspended || false,
|
||||||
|
isFlashAvailable: values.isFlashAvailable || false,
|
||||||
const result = await upload({ images: blobs, contextString: 'product_info' });
|
flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : undefined,
|
||||||
uploadUrls = result.presignedUrls;
|
tagIds: values.tagIds || [],
|
||||||
}
|
imageKeys: imageKeys || [],
|
||||||
|
}, {
|
||||||
await createProduct.mutateAsync({
|
onSuccess: (data) => {
|
||||||
name: values.name,
|
Alert.alert('Success', 'Product created successfully!');
|
||||||
shortDescription: values.shortDescription,
|
// Reset form or navigate
|
||||||
longDescription: values.longDescription,
|
},
|
||||||
unitId: parseInt(values.unitId),
|
onError: (error: any) => {
|
||||||
storeId: parseInt(values.storeId),
|
Alert.alert('Error', error.message || 'Failed to create product');
|
||||||
price: parseFloat(values.price),
|
},
|
||||||
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
|
});
|
||||||
incrementStep: 1,
|
|
||||||
productQuantity: values.productQuantity || 1,
|
|
||||||
isSuspended: values.isSuspended || false,
|
|
||||||
isFlashAvailable: values.isFlashAvailable || false,
|
|
||||||
flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : undefined,
|
|
||||||
uploadUrls,
|
|
||||||
tagIds: values.tagIds || [],
|
|
||||||
});
|
|
||||||
|
|
||||||
await refetchProducts();
|
|
||||||
Alert.alert('Success', 'Product created successfully!');
|
|
||||||
} catch (error: any) {
|
|
||||||
Alert.alert('Error', error.message || 'Failed to create product');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
|
|
@ -67,7 +48,7 @@ export default function AddProduct() {
|
||||||
isFlashAvailable: false,
|
isFlashAvailable: false,
|
||||||
flashPrice: '',
|
flashPrice: '',
|
||||||
productQuantity: 1,
|
productQuantity: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContainer>
|
<AppContainer>
|
||||||
|
|
@ -75,7 +56,8 @@ export default function AddProduct() {
|
||||||
mode="create"
|
mode="create"
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
isLoading={createProduct.isPending || isUploading}
|
isLoading={createProduct.isPending}
|
||||||
|
existingImages={[]}
|
||||||
/>
|
/>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { tw, AppContainer, MyText, useMarkDataFetchers, BottomDialog, ImageUploa
|
||||||
import { MaterialIcons, FontAwesome5, Ionicons, Feather, MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialIcons, FontAwesome5, Ionicons, Feather, MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
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 { Formik } from 'formik';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { BlurView } from 'expo-blur';
|
import { BlurView } from 'expo-blur';
|
||||||
|
|
@ -24,9 +24,10 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
|
||||||
const [adminResponse, setAdminResponse] = useState('');
|
const [adminResponse, setAdminResponse] = useState('');
|
||||||
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
|
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
|
||||||
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
|
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
|
||||||
|
const [uploadUrls, setUploadUrls] = useState<string[]>([]);
|
||||||
|
|
||||||
const respondToReview = trpc.admin.product.respondToReview.useMutation();
|
const respondToReview = trpc.admin.product.respondToReview.useMutation();
|
||||||
const { upload } = useUploadToObjectStorage();
|
const { upload, isUploading } = useUploadToObjectStorage();
|
||||||
|
|
||||||
const handleImagePick = usePickImage({
|
const handleImagePick = usePickImage({
|
||||||
setFile: async (assets: any) => {
|
setFile: async (assets: any) => {
|
||||||
|
|
@ -62,16 +63,23 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
|
||||||
|
|
||||||
const handleSubmit = async (adminResponse: string) => {
|
const handleSubmit = async (adminResponse: string) => {
|
||||||
try {
|
try {
|
||||||
const { keys, presignedUrls } = await upload({
|
let keys: string[] = [];
|
||||||
images: selectedImages,
|
let generatedUrls: string[] = [];
|
||||||
contextString: 'review',
|
|
||||||
});
|
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({
|
await respondToReview.mutateAsync({
|
||||||
reviewId,
|
reviewId,
|
||||||
adminResponse,
|
adminResponse,
|
||||||
adminResponseImages: keys,
|
adminResponseImages: keys,
|
||||||
uploadUrls: presignedUrls,
|
uploadUrls: generatedUrls,
|
||||||
});
|
});
|
||||||
|
|
||||||
Alert.alert('Success', 'Response submitted');
|
Alert.alert('Success', 'Response submitted');
|
||||||
|
|
@ -79,7 +87,8 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
|
||||||
setAdminResponse('');
|
setAdminResponse('');
|
||||||
setSelectedImages([]);
|
setSelectedImages([]);
|
||||||
setDisplayImages([]);
|
setDisplayImages([]);
|
||||||
} catch (error:any) {
|
setUploadUrls([]);
|
||||||
|
} catch (error: any) {
|
||||||
Alert.alert('Error', error.message || 'Failed to submit response.');
|
Alert.alert('Error', error.message || 'Failed to submit response.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -114,7 +123,7 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => formikSubmit()}
|
onPress={() => formikSubmit()}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
disabled={respondToReview.isPending}
|
disabled={respondToReview.isPending || isUploading}
|
||||||
>
|
>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={['#2563EB', '#1D4ED8']}
|
colors={['#2563EB', '#1D4ED8']}
|
||||||
|
|
@ -122,7 +131,9 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
|
||||||
end={{ x: 1, y: 0 }}
|
end={{ x: 1, y: 0 }}
|
||||||
style={tw`py-4 rounded-2xl items-center shadow-lg`}
|
style={tw`py-4 rounded-2xl items-center shadow-lg`}
|
||||||
>
|
>
|
||||||
{respondToReview.isPending ? (
|
{isUploading ? (
|
||||||
|
<ActivityIndicator color="white" />
|
||||||
|
) : respondToReview.isPending ? (
|
||||||
<ActivityIndicator color="white" />
|
<ActivityIndicator color="white" />
|
||||||
) : (
|
) : (
|
||||||
<MyText style={tw`text-white font-bold text-lg`}>Submit Response</MyText>
|
<MyText style={tw`text-white font-bold text-lg`}>Submit Response</MyText>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { View, Alert } from 'react-native';
|
import { View, Text, Alert } from 'react-native';
|
||||||
import { useLocalSearchParams } from 'expo-router';
|
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 ProductForm, { ProductFormRef } from '@/src/components/ProductForm';
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
|
||||||
|
|
||||||
export default function EditProduct() {
|
export default function EditProduct() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id } = useLocalSearchParams();
|
||||||
|
|
@ -15,60 +14,48 @@ export default function EditProduct() {
|
||||||
{ id: productId },
|
{ id: productId },
|
||||||
{ enabled: !!productId }
|
{ enabled: !!productId }
|
||||||
);
|
);
|
||||||
const { refetch: refetchProducts } = trpc.admin.product.getProducts.useQuery(undefined, {
|
|
||||||
enabled: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateProduct = trpc.admin.product.updateProduct.useMutation();
|
const updateProduct = trpc.admin.product.updateProduct.useMutation();
|
||||||
const { upload, isUploading } = useUploadToObjectStorage();
|
|
||||||
|
|
||||||
useManualRefresh(() => refetch());
|
useManualRefresh(() => refetch());
|
||||||
|
|
||||||
const handleSubmit = async (values: any, images: ImageUploaderNeoPayload[], imagesToDelete: string[]) => {
|
const handleSubmit = (values: any, newImageKeys?: string[], imagesToDelete?: string[]) => {
|
||||||
try {
|
updateProduct.mutate({
|
||||||
// New images have mimeType !== null, existing images have mimeType === null
|
id: productId,
|
||||||
const newImages = images.filter(img => img.mimeType !== null);
|
name: values.name,
|
||||||
let uploadUrls: string[] = [];
|
shortDescription: values.shortDescription,
|
||||||
|
longDescription: values.longDescription,
|
||||||
if (newImages.length > 0) {
|
unitId: parseInt(values.unitId),
|
||||||
const blobs = await Promise.all(
|
storeId: parseInt(values.storeId),
|
||||||
newImages.map(async (img) => {
|
price: parseFloat(values.price),
|
||||||
const response = await fetch(img.url);
|
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
|
||||||
const blob = await response.blob();
|
incrementStep: 1,
|
||||||
return { blob, mimeType: img.mimeType || 'image/jpeg' };
|
productQuantity: values.productQuantity || 1,
|
||||||
})
|
isSuspended: values.isSuspended,
|
||||||
);
|
isFlashAvailable: values.isFlashAvailable,
|
||||||
|
flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : undefined,
|
||||||
const result = await upload({ images: blobs, contextString: 'product_info' });
|
deals: values.deals?.filter((deal: any) =>
|
||||||
uploadUrls = result.presignedUrls;
|
deal.quantity && deal.price && deal.validTill
|
||||||
}
|
).map((deal: any) => ({
|
||||||
|
quantity: parseInt(deal.quantity),
|
||||||
await updateProduct.mutateAsync({
|
price: parseFloat(deal.price),
|
||||||
id: productId,
|
validTill: deal.validTill instanceof Date
|
||||||
name: values.name,
|
? deal.validTill.toISOString().split('T')[0]
|
||||||
shortDescription: values.shortDescription,
|
: deal.validTill,
|
||||||
longDescription: values.longDescription,
|
})),
|
||||||
unitId: parseInt(values.unitId),
|
tagIds: values.tagIds,
|
||||||
storeId: parseInt(values.storeId),
|
newImageKeys: newImageKeys || [],
|
||||||
price: parseFloat(values.price),
|
imagesToDelete: imagesToDelete || [],
|
||||||
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
|
}, {
|
||||||
incrementStep: 1,
|
onSuccess: (data) => {
|
||||||
productQuantity: values.productQuantity || 1,
|
Alert.alert('Success', 'Product updated successfully!');
|
||||||
isSuspended: values.isSuspended || false,
|
// Clear newly added images after successful update
|
||||||
isFlashAvailable: values.isFlashAvailable || false,
|
productFormRef.current?.clearImages();
|
||||||
flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : null,
|
},
|
||||||
uploadUrls,
|
onError: (error: any) => {
|
||||||
imagesToDelete,
|
Alert.alert('Error', error.message || 'Failed to update product');
|
||||||
tagIds: values.tagIds || [],
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await refetch();
|
|
||||||
await refetchProducts();
|
|
||||||
Alert.alert('Success', 'Product updated successfully!');
|
|
||||||
productFormRef.current?.clearImages();
|
|
||||||
} catch (error: any) {
|
|
||||||
Alert.alert('Error', error.message || 'Failed to update product');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isFetching) {
|
if (isFetching) {
|
||||||
|
|
@ -91,13 +78,7 @@ export default function EditProduct() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const productData = product.product;
|
const productData = product.product; // The API returns { product: Product }
|
||||||
|
|
||||||
const existingImages: ImageUploaderNeoItem[] = (productData.images || []).map((url) => ({
|
|
||||||
imgUrl: url,
|
|
||||||
mimeType: null,
|
|
||||||
}));
|
|
||||||
const existingImageKeys = productData.imageKeys || [];
|
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
name: productData.name,
|
name: productData.name,
|
||||||
|
|
@ -126,9 +107,8 @@ export default function EditProduct() {
|
||||||
mode="edit"
|
mode="edit"
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
isLoading={updateProduct.isPending || isUploading}
|
isLoading={updateProduct.isPending}
|
||||||
existingImages={existingImages}
|
existingImages={productData.images || []}
|
||||||
existingImageKeys={existingImageKeys}
|
|
||||||
/>
|
/>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
import { AppContainer, MyText, tw, MyButton, useManualRefresh, MyTextInput, SearchBar, useMarkDataFetchers } from 'common-ui';
|
import { AppContainer, MyText, tw, MyButton, useManualRefresh, MyTextInput, SearchBar, useMarkDataFetchers } from 'common-ui';
|
||||||
|
|
||||||
import { trpc } from '@/src/trpc-client';
|
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';
|
type FilterType = 'all' | 'in-stock' | 'out-of-stock';
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ export default function Products() {
|
||||||
|
|
||||||
|
|
||||||
// const handleToggleStock = (product: any) => {
|
// 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';
|
const action = product.isOutOfStock ? 'mark as in stock' : 'mark as out of stock';
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'Update Stock Status',
|
'Update Stock Status',
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import {
|
||||||
} from 'common-ui';
|
} from 'common-ui';
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||||
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
|
import { useUploadToObjectStorage } from '../../../hooks/useUploadToObjectStorage';
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -41,7 +41,7 @@ export default function SendNotifications() {
|
||||||
search: searchQuery,
|
search: searchQuery,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { uploadSingle } = useUploadToObjectStorage();
|
const { uploadSingle, isUploading } = useUploadToObjectStorage();
|
||||||
|
|
||||||
// Send notification mutation
|
// Send notification mutation
|
||||||
const sendNotification = trpc.admin.user.sendNotification.useMutation({
|
const sendNotification = trpc.admin.user.sendNotification.useMutation({
|
||||||
|
|
@ -230,15 +230,15 @@ export default function SendNotifications() {
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={handleSend}
|
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`${
|
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-gray-300'
|
||||||
: 'bg-blue-600'
|
: 'bg-blue-600'
|
||||||
} rounded-xl py-4 items-center shadow-sm`}
|
} rounded-xl py-4 items-center shadow-sm`}
|
||||||
>
|
>
|
||||||
<MyText style={tw`text-white font-bold text-base`}>
|
<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>
|
</MyText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
|
||||||
443
apps/admin-ui/app/(drawer)/stocking-schedules/index.tsx
Normal file
443
apps/admin-ui/app/(drawer)/stocking-schedules/index.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -9,14 +9,10 @@ export default function AddStore() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const createStoreMutation = trpc.admin.store.createStore.useMutation();
|
const createStoreMutation = trpc.admin.store.createStore.useMutation();
|
||||||
const { refetch: refetchStores } = trpc.admin.store.getStores.useQuery(undefined, {
|
|
||||||
enabled: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = (values: StoreFormData) => {
|
const handleSubmit = (values: StoreFormData) => {
|
||||||
createStoreMutation.mutate(values, {
|
createStoreMutation.mutate(values, {
|
||||||
onSuccess: async (data) => {
|
onSuccess: (data) => {
|
||||||
await refetchStores();
|
|
||||||
Alert.alert('Success', data.message);
|
Alert.alert('Success', data.message);
|
||||||
router.push('/stores' as any); // Navigate back to stores list
|
router.push('/stores' as any); // Navigate back to stores list
|
||||||
},
|
},
|
||||||
|
|
@ -39,4 +35,4 @@ export default function AddStore() {
|
||||||
</View>
|
</View>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
237
apps/admin-ui/components/AvailabilityScheduleForm.tsx
Normal file
237
apps/admin-ui/components/AvailabilityScheduleForm.tsx
Normal 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;
|
||||||
|
|
@ -7,8 +7,8 @@ import { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
|
||||||
import ProductsSelector from './ProductsSelector';
|
import ProductsSelector from './ProductsSelector';
|
||||||
import { trpc } from '../src/trpc-client';
|
import { trpc } from '../src/trpc-client';
|
||||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||||
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStore';
|
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
|
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStorage';
|
||||||
|
|
||||||
export interface BannerFormData {
|
export interface BannerFormData {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -53,14 +53,7 @@ export default function BannerForm({
|
||||||
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
|
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
|
||||||
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
|
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
|
||||||
|
|
||||||
const { uploadSingle } = useUploadToObjectStorage();
|
const { uploadSingle, isUploading } = useUploadToObjectStorage();
|
||||||
|
|
||||||
// Fetch products for dropdown
|
|
||||||
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery();
|
|
||||||
const products = productsData?.products || [];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleImagePick = usePickImage({
|
const handleImagePick = usePickImage({
|
||||||
setFile: async (assets: any) => {
|
setFile: async (assets: any) => {
|
||||||
|
|
@ -99,14 +92,14 @@ export default function BannerForm({
|
||||||
|
|
||||||
if (selectedImages.length > 0) {
|
if (selectedImages.length > 0) {
|
||||||
const { blob, mimeType } = selectedImages[0];
|
const { blob, mimeType } = selectedImages[0];
|
||||||
const { presignedUrl } = await uploadSingle(blob, mimeType, 'store');
|
const { key, presignedUrl } = await uploadSingle(blob, mimeType, 'store');
|
||||||
imageUrl = presignedUrl;
|
imageUrl = presignedUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
await onSubmit(values, imageUrl);
|
await onSubmit(values, imageUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', 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
|
<MyTouchableOpacity
|
||||||
onPress={() => handleSubmit()}
|
onPress={() => handleSubmit()}
|
||||||
disabled={isSubmitting || !isValid || !dirty}
|
disabled={isSubmitting || isUploading || !isValid || !dirty}
|
||||||
style={tw`flex-1 rounded-lg py-4 items-center ${
|
style={tw`flex-1 rounded-lg py-4 items-center ${
|
||||||
isSubmitting || !isValid || !dirty
|
isSubmitting || isUploading || !isValid || !dirty
|
||||||
? 'bg-blue-400'
|
? 'bg-blue-400'
|
||||||
: 'bg-blue-600'
|
: 'bg-blue-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<MyText style={tw`text-white font-semibold`}>
|
<MyText style={tw`text-white font-semibold`}>
|
||||||
{isSubmitting ? 'Saving...' : submitButtonText}
|
{isUploading ? 'Uploading...' : isSubmitting ? 'Saving...' : submitButtonText}
|
||||||
</MyText>
|
</MyText>
|
||||||
</MyTouchableOpacity>
|
</MyTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
197
apps/admin-ui/components/FullOrderView.tsx
Normal file
197
apps/admin-ui/components/FullOrderView.tsx
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { View, ScrollView, Dimensions } from 'react-native';
|
||||||
|
import { Image } from 'expo-image';
|
||||||
|
import { MyText, tw } from 'common-ui';
|
||||||
|
import { trpc } from '../src/trpc-client';
|
||||||
|
|
||||||
|
interface FullOrderViewProps {
|
||||||
|
orderId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FullOrderView: React.FC<FullOrderViewProps> = ({ orderId }) => {
|
||||||
|
const { data: order, isLoading, error } = trpc.admin.order.getFullOrder.useQuery({ orderId });
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<View style={tw`p-6`}>
|
||||||
|
<MyText style={tw`text-center text-gray-600`}>Loading order details...</MyText>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !order) {
|
||||||
|
return (
|
||||||
|
<View style={tw`p-6`}>
|
||||||
|
<MyText style={tw`text-center text-red-600`}>Failed to load order details</MyText>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalAmount = order.items.reduce((sum, item) => sum + item.amount, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
style={[tw`flex-1`, { maxHeight: Dimensions.get('window').height * 0.8 }]}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
<View style={tw`p-6`}>
|
||||||
|
<MyText style={tw`text-2xl font-bold text-gray-800 mb-6`}>Order #{order.readableId}</MyText>
|
||||||
|
|
||||||
|
{/* Customer Information */}
|
||||||
|
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
|
||||||
|
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Customer Details</MyText>
|
||||||
|
<View style={tw`space-y-2`}>
|
||||||
|
<View style={tw`flex-row justify-between`}>
|
||||||
|
<MyText style={tw`text-gray-600`}>Name:</MyText>
|
||||||
|
<MyText style={tw`font-medium`}>{order.customerName}</MyText>
|
||||||
|
</View>
|
||||||
|
{order.customerEmail && (
|
||||||
|
<View style={tw`flex-row justify-between`}>
|
||||||
|
<MyText style={tw`text-gray-600`}>Email:</MyText>
|
||||||
|
<MyText style={tw`font-medium`}>{order.customerEmail}</MyText>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View style={tw`flex-row justify-between`}>
|
||||||
|
<MyText style={tw`text-gray-600`}>Mobile:</MyText>
|
||||||
|
<MyText style={tw`font-medium`}>{order.customerMobile}</MyText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Delivery Address */}
|
||||||
|
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
|
||||||
|
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Delivery Address</MyText>
|
||||||
|
<View style={tw`space-y-1`}>
|
||||||
|
<MyText style={tw`text-gray-800`}>{order.address.line1}</MyText>
|
||||||
|
{order.address.line2 && <MyText style={tw`text-gray-800`}>{order.address.line2}</MyText>}
|
||||||
|
<MyText style={tw`text-gray-800`}>
|
||||||
|
{order.address.city}, {order.address.state} - {order.address.pincode}
|
||||||
|
</MyText>
|
||||||
|
<MyText style={tw`text-gray-800`}>Phone: {order.address.phone}</MyText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Order Details */}
|
||||||
|
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
|
||||||
|
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Order Details</MyText>
|
||||||
|
<View style={tw`space-y-2`}>
|
||||||
|
<View style={tw`flex-row justify-between`}>
|
||||||
|
<MyText style={tw`text-gray-600`}>Order Date:</MyText>
|
||||||
|
<MyText style={tw`font-medium`}>
|
||||||
|
{new Date(order.createdAt).toLocaleDateString('en-IN', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})}
|
||||||
|
</MyText>
|
||||||
|
</View>
|
||||||
|
<View style={tw`flex-row justify-between`}>
|
||||||
|
<MyText style={tw`text-gray-600`}>Payment Method:</MyText>
|
||||||
|
<MyText style={tw`font-medium`}>
|
||||||
|
{order.isCod ? 'Cash on Delivery' : 'Online Payment'}
|
||||||
|
</MyText>
|
||||||
|
</View>
|
||||||
|
{order.slotInfo && (
|
||||||
|
<View style={tw`flex-row justify-between`}>
|
||||||
|
<MyText style={tw`text-gray-600`}>Delivery Slot:</MyText>
|
||||||
|
<MyText style={tw`font-medium`}>
|
||||||
|
{new Date(order.slotInfo.time).toLocaleDateString('en-IN', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})}
|
||||||
|
</MyText>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Items */}
|
||||||
|
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
|
||||||
|
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Items ({order.items.length})</MyText>
|
||||||
|
{order.items.map((item, index) => (
|
||||||
|
<View key={item.id} style={tw`flex-row items-center py-3 ${index !== order.items.length - 1 ? 'border-b border-gray-100' : ''}`}>
|
||||||
|
<View style={tw`flex-1`}>
|
||||||
|
<MyText style={tw`font-medium text-gray-800`} numberOfLines={2}>
|
||||||
|
{item.productName}
|
||||||
|
</MyText>
|
||||||
|
<MyText style={tw`text-sm text-gray-600`}>
|
||||||
|
Qty: {item.quantity} {item.unit} × ₹{parseFloat(item.price.toString()).toFixed(2)}
|
||||||
|
</MyText>
|
||||||
|
</View>
|
||||||
|
<MyText style={tw`font-semibold text-gray-800`}>₹{item.amount.toFixed(2)}</MyText>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Payment Information */}
|
||||||
|
{(order.payment || order.paymentInfo) && (
|
||||||
|
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
|
||||||
|
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Payment Information</MyText>
|
||||||
|
{order.payment && (
|
||||||
|
<View style={tw`space-y-2 mb-3`}>
|
||||||
|
<MyText style={tw`text-sm font-medium text-gray-700`}>Payment Details:</MyText>
|
||||||
|
<View style={tw`flex-row justify-between`}>
|
||||||
|
<MyText style={tw`text-gray-600`}>Status:</MyText>
|
||||||
|
<MyText style={tw`font-medium capitalize`}>{order.payment.status}</MyText>
|
||||||
|
</View>
|
||||||
|
<View style={tw`flex-row justify-between`}>
|
||||||
|
<MyText style={tw`text-gray-600`}>Gateway:</MyText>
|
||||||
|
<MyText style={tw`font-medium`}>{order.payment.gateway}</MyText>
|
||||||
|
</View>
|
||||||
|
<View style={tw`flex-row justify-between`}>
|
||||||
|
<MyText style={tw`text-gray-600`}>Order ID:</MyText>
|
||||||
|
<MyText style={tw`font-medium`}>{order.payment.merchantOrderId}</MyText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{order.paymentInfo && (
|
||||||
|
<View style={tw`space-y-2`}>
|
||||||
|
<MyText style={tw`text-sm font-medium text-gray-700`}>Payment Info:</MyText>
|
||||||
|
<View style={tw`flex-row justify-between`}>
|
||||||
|
<MyText style={tw`text-gray-600`}>Status:</MyText>
|
||||||
|
<MyText style={tw`font-medium capitalize`}>{order.paymentInfo.status}</MyText>
|
||||||
|
</View>
|
||||||
|
<View style={tw`flex-row justify-between`}>
|
||||||
|
<MyText style={tw`text-gray-600`}>Gateway:</MyText>
|
||||||
|
<MyText style={tw`font-medium`}>{order.paymentInfo.gateway}</MyText>
|
||||||
|
</View>
|
||||||
|
<View style={tw`flex-row justify-between`}>
|
||||||
|
<MyText style={tw`text-gray-600`}>Order ID:</MyText>
|
||||||
|
<MyText style={tw`font-medium`}>{order.paymentInfo.merchantOrderId}</MyText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* User Notes */}
|
||||||
|
{order.userNotes && (
|
||||||
|
<View style={tw`bg-blue-50 rounded-xl p-4 mb-4`}>
|
||||||
|
<MyText style={tw`text-lg font-semibold text-gray-800 mb-2`}>Customer Notes</MyText>
|
||||||
|
<MyText style={tw`text-gray-700`}>{order.userNotes}</MyText>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Admin Notes */}
|
||||||
|
{order.adminNotes && (
|
||||||
|
<View style={tw`bg-yellow-50 rounded-xl p-4 mb-4`}>
|
||||||
|
<MyText style={tw`text-lg font-semibold text-gray-800 mb-2`}>Admin Notes</MyText>
|
||||||
|
<MyText style={tw`text-gray-700`}>{order.adminNotes}</MyText>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Total */}
|
||||||
|
<View style={tw`bg-blue-50 rounded-xl p-4`}>
|
||||||
|
<View style={tw`flex-row justify-between items-center`}>
|
||||||
|
<MyText style={tw`text-xl font-bold text-gray-800`}>Total Amount</MyText>
|
||||||
|
<MyText style={tw`text-2xl font-bold text-blue-600`}>₹{parseFloat(order.totalAmount.toString()).toFixed(2)}</MyText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -2,11 +2,11 @@ import React, { forwardRef, useState, useEffect, useMemo } from 'react';
|
||||||
import { View, TouchableOpacity, Alert } from 'react-native';
|
import { View, TouchableOpacity, Alert } from 'react-native';
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import * as Yup from 'yup';
|
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 ProductsSelector from './ProductsSelector';
|
||||||
import { trpc } from '../src/trpc-client';
|
import { trpc } from '../src/trpc-client';
|
||||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||||
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStore';
|
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStorage';
|
||||||
|
|
||||||
export interface StoreFormData {
|
export interface StoreFormData {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -16,6 +16,12 @@ export interface StoreFormData {
|
||||||
products: number[];
|
products: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface StoreImage {
|
||||||
|
uri: string;
|
||||||
|
mimeType: string;
|
||||||
|
isExisting: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StoreFormRef {
|
export interface StoreFormRef {
|
||||||
// Add methods if needed
|
// Add methods if needed
|
||||||
}
|
}
|
||||||
|
|
@ -28,6 +34,11 @@ interface StoreFormProps {
|
||||||
storeId?: number;
|
storeId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extend Formik values with images array
|
||||||
|
interface FormikStoreValues extends StoreFormData {
|
||||||
|
images: StoreImage[];
|
||||||
|
}
|
||||||
|
|
||||||
const validationSchema = Yup.object().shape({
|
const validationSchema = Yup.object().shape({
|
||||||
name: Yup.string().required('Name is required'),
|
name: Yup.string().required('Name is required'),
|
||||||
description: Yup.string(),
|
description: Yup.string(),
|
||||||
|
|
@ -41,9 +52,23 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
|
||||||
const { data: staffData } = trpc.admin.staffUser.getStaff.useQuery();
|
const { data: staffData } = trpc.admin.staffUser.getStaff.useQuery();
|
||||||
const { data: productsData } = trpc.admin.product.getProducts.useQuery();
|
const { data: productsData } = trpc.admin.product.getProducts.useQuery();
|
||||||
|
|
||||||
const [formInitialValues, setFormInitialValues] = useState<StoreFormData>(initialValues);
|
// Build initial form values with images array
|
||||||
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
|
const buildInitialValues = (): FormikStoreValues => {
|
||||||
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
|
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
|
// For edit mode, pre-select products belonging to this store
|
||||||
const initialSelectedProducts = useMemo(() => {
|
const initialSelectedProducts = useMemo(() => {
|
||||||
|
|
@ -55,57 +80,18 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFormInitialValues({
|
setFormInitialValues({
|
||||||
...initialValues,
|
...buildInitialValues(),
|
||||||
products: initialSelectedProducts,
|
products: initialSelectedProducts,
|
||||||
});
|
});
|
||||||
}, [initialValues, initialSelectedProducts]);
|
}, [initialValues, initialSelectedProducts]);
|
||||||
|
|
||||||
const existingImageUrls = useMemo(
|
const staffOptions = staffData?.staff.map(staff => ({
|
||||||
() => (formInitialValues.imageUrl ? [formInitialValues.imageUrl] : []),
|
|
||||||
[formInitialValues.imageUrl]
|
|
||||||
)
|
|
||||||
|
|
||||||
const staffOptions = staffData?.staff.map((staff: { id: number; name: string }) => ({
|
|
||||||
label: staff.name,
|
label: staff.name,
|
||||||
value: staff.id,
|
value: staff.id,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const { uploadSingle, isUploading } = useUploadToObjectStorage();
|
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 (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={formInitialValues}
|
initialValues={formInitialValues}
|
||||||
|
|
@ -114,23 +100,78 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
>
|
>
|
||||||
{({ handleChange, handleSubmit, values, setFieldValue, errors, touched }) => {
|
{({ 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 () => {
|
const submit = async () => {
|
||||||
try {
|
try {
|
||||||
let imageUrl: string | undefined;
|
let imageUrl: string | undefined;
|
||||||
|
|
||||||
if (selectedImages.length > 0) {
|
// Get new images that need to be uploaded
|
||||||
const { blob, mimeType } = selectedImages[0];
|
const newImages = values.images.filter(img => !img.isExisting);
|
||||||
const { presignedUrl } = await uploadSingle(blob, mimeType, 'store');
|
|
||||||
imageUrl = presignedUrl;
|
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) {
|
} catch (error) {
|
||||||
console.error('Upload error:', 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 (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<MyTextInput
|
<MyTextInput
|
||||||
|
|
@ -171,17 +212,11 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
|
||||||
/>
|
/>
|
||||||
<View style={tw`mb-6`}>
|
<View style={tw`mb-6`}>
|
||||||
<MyText style={tw`text-sm font-bold text-gray-700 mb-3 uppercase tracking-wider`}>Store Image</MyText>
|
<MyText style={tw`text-sm font-bold text-gray-700 mb-3 uppercase tracking-wider`}>Store Image</MyText>
|
||||||
<ImageUploader
|
|
||||||
images={displayImages}
|
<ImageUploaderNeo
|
||||||
existingImageUrls={existingImageUrls}
|
images={imagesForUploader}
|
||||||
onAddImage={handleImagePick}
|
onUploadImage={handleImagePick}
|
||||||
onRemoveImage={handleRemoveImage}
|
onRemoveImage={handleRemoveImage}
|
||||||
onRemoveExistingImage={() =>
|
|
||||||
setFormInitialValues((prev) => ({
|
|
||||||
...prev,
|
|
||||||
imageUrl: undefined,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
allowMultiple={false}
|
allowMultiple={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
/** @type {import('@jest/types').Config.InitialOptions} */
|
|
||||||
module.exports = {
|
|
||||||
rootDir: '..',
|
|
||||||
testMatch: ['<rootDir>/e2e/**/*.test.js'],
|
|
||||||
testTimeout: 120000,
|
|
||||||
maxWorkers: 1,
|
|
||||||
globalSetup: 'detox/runners/jest/globalSetup',
|
|
||||||
globalTeardown: 'detox/runners/jest/globalTeardown',
|
|
||||||
reporters: ['detox/runners/jest/reporter'],
|
|
||||||
testEnvironment: 'detox/runners/jest/testEnvironment',
|
|
||||||
verbose: true,
|
|
||||||
};
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
describe('Example', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
await device.launchApp();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await device.reloadReactNative();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have welcome screen', async () => {
|
|
||||||
await expect(element(by.id('welcome'))).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show hello screen after tap', async () => {
|
|
||||||
await element(by.id('hello_button')).tap();
|
|
||||||
await expect(element(by.text('Hello!!!'))).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show world screen after tap', async () => {
|
|
||||||
await element(by.id('world_button')).tap();
|
|
||||||
await expect(element(by.text('World!!!'))).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
"build": {
|
"build": {
|
||||||
"development": {
|
"development": {
|
||||||
"distribution": "internal",
|
"distribution": "internal",
|
||||||
"channel": "development"
|
"autoIncrement": true
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"distribution": "internal",
|
"distribution": "internal",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { trpc } from '../src/trpc-client';
|
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 {
|
interface UploadInput {
|
||||||
blob: Blob;
|
blob: Blob;
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle, useMemo } from 'react';
|
import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle } from 'react';
|
||||||
import { View, TouchableOpacity } from 'react-native';
|
import { View, TouchableOpacity, Alert } from 'react-native';
|
||||||
|
import { Image } from 'expo-image';
|
||||||
import { Formik, FieldArray } from 'formik';
|
import { Formik, FieldArray } from 'formik';
|
||||||
import * as Yup from 'yup';
|
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 MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
import { trpc } from '../trpc-client';
|
import { trpc } from '../trpc-client';
|
||||||
|
import { useUploadToObjectStorage } from '../../hooks/useUploadToObjectStorage';
|
||||||
|
|
||||||
interface ProductFormData {
|
interface ProductFormData {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -35,10 +38,9 @@ export interface ProductFormRef {
|
||||||
interface ProductFormProps {
|
interface ProductFormProps {
|
||||||
mode: 'create' | 'edit';
|
mode: 'create' | 'edit';
|
||||||
initialValues: ProductFormData;
|
initialValues: ProductFormData;
|
||||||
onSubmit: (values: ProductFormData, images: ImageUploaderNeoPayload[], imagesToDelete: string[]) => void;
|
onSubmit: (values: ProductFormData, imageKeys?: string[], imagesToDelete?: string[]) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
existingImages?: ImageUploaderNeoItem[];
|
existingImages?: string[];
|
||||||
existingImageKeys?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const unitOptions = [
|
const unitOptions = [
|
||||||
|
|
@ -48,22 +50,19 @@ const unitOptions = [
|
||||||
{ label: 'Unit Piece', value: 4 },
|
{ label: 'Unit Piece', value: 4 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
mode,
|
mode,
|
||||||
initialValues,
|
initialValues,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
isLoading,
|
isLoading,
|
||||||
existingImages:existingImagesRaw,
|
existingImages = []
|
||||||
existingImageKeys = [],
|
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const [images, setImages] = useState<ImageUploaderNeoItem[]>([]);
|
const [newImages, setNewImages] = useState<{ blob: Blob; mimeType: string; uri: string }[]>([]);
|
||||||
|
const [existingImagesState, setExistingImagesState] = useState<string[]>(existingImages);
|
||||||
const existingImages = existingImagesRaw || []
|
const { upload, isUploading } = useUploadToObjectStorage();
|
||||||
// Sync images state when existingImages prop changes (e.g., when async query data arrives)
|
|
||||||
useEffect(() => {
|
|
||||||
setImages(existingImages);
|
|
||||||
}, [existingImagesRaw]);
|
|
||||||
|
|
||||||
const { data: storesData } = trpc.common.getStoresSummary.useQuery();
|
const { data: storesData } = trpc.common.getStoresSummary.useQuery();
|
||||||
const storeOptions = storesData?.stores.map(store => ({
|
const storeOptions = storesData?.stores.map(store => ({
|
||||||
|
|
@ -71,50 +70,83 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
value: store.id,
|
value: store.id,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const { data: tagsData } = trpc.admin.product.getProductTags.useQuery();
|
const { data: tagsData } = trpc.admin.tag.getTags.useQuery();
|
||||||
const tagOptions = tagsData?.tags.map(tag => ({
|
const tagOptions = tagsData?.tags.map((tag: { tagName: string; id: number }) => ({
|
||||||
label: tag.tagName,
|
label: tag.tagName,
|
||||||
value: tag.id.toString(),
|
value: tag.id.toString(),
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
// Build signed URL -> S3 key mapping for existing images
|
// Initialize existing images state when existingImages prop changes
|
||||||
const signedUrlToKey = useMemo(() => {
|
useEffect(() => {
|
||||||
const map: Record<string, string> = {};
|
console.log('changing existing imaes statte')
|
||||||
existingImages.forEach((img, i) => {
|
|
||||||
if (existingImageKeys[i]) {
|
setExistingImagesState(existingImages);
|
||||||
map[img.imgUrl] = existingImageKeys[i];
|
}, [existingImages]);
|
||||||
|
|
||||||
|
const pickImage = usePickImage({
|
||||||
|
setFile: async (assets: any) => {
|
||||||
|
if (!assets || (Array.isArray(assets) && assets.length === 0)) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
return map;
|
const files = Array.isArray(assets) ? assets : [assets];
|
||||||
}, [existingImages, existingImageKeys]);
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={(values) => {
|
onSubmit={async (values) => {
|
||||||
// New images have mimeType set, existing images have mimeType === null
|
try {
|
||||||
const newImages = images.filter(img => img.mimeType !== null);
|
let imageKeys: string[] = [];
|
||||||
const deletedImageKeys = existingImages
|
|
||||||
.filter(existing => !images.some(current => current.imgUrl === existing.imgUrl))
|
|
||||||
.map(deleted => signedUrlToKey[deleted.imgUrl])
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
onSubmit(
|
// Upload new images if any
|
||||||
values,
|
if (newImages.length > 0) {
|
||||||
newImages.map(img => ({ url: img.imgUrl, mimeType: img.mimeType })),
|
const result = await upload({
|
||||||
deletedImageKeys,
|
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
|
enableReinitialize
|
||||||
>
|
>
|
||||||
{({ handleChange, handleSubmit, values, setFieldValue, resetForm }) => {
|
{({ handleChange, handleSubmit, values, setFieldValue, resetForm }) => {
|
||||||
|
// Clear form when screen comes into focus
|
||||||
const clearForm = useCallback(() => {
|
const clearForm = useCallback(() => {
|
||||||
setImages([]);
|
setNewImages([]);
|
||||||
|
setExistingImagesState([]);
|
||||||
resetForm();
|
resetForm();
|
||||||
}, [resetForm]);
|
}, [resetForm]);
|
||||||
|
|
||||||
useFocusCallback(clearForm);
|
useFocusCallback(clearForm);
|
||||||
|
|
||||||
|
// Update ref with current clearForm function
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
clearImages: clearForm,
|
clearImages: clearForm,
|
||||||
}), [clearForm]);
|
}), [clearForm]);
|
||||||
|
|
@ -149,18 +181,44 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ImageUploaderNeo
|
{mode === 'create' && (
|
||||||
images={images}
|
<ImageUploader
|
||||||
onImageAdd={(payloads) => setImages(prev => [...prev, ...payloads.map(p => ({ imgUrl: p.url, mimeType: p.mimeType }))])}
|
images={displayImages}
|
||||||
onImageRemove={(payload) => setImages(prev => prev.filter(img => img.imgUrl !== payload.url))}
|
onAddImage={pickImage}
|
||||||
allowMultiple={true}
|
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
|
<BottomDropdown
|
||||||
topLabel='Unit'
|
topLabel='Unit'
|
||||||
label="Unit"
|
label="Unit"
|
||||||
value={values.unitId}
|
value={values.unitId}
|
||||||
options={unitOptions}
|
options={unitOptions}
|
||||||
|
// onValueChange={(value) => handleChange('unitId')(value+'')}
|
||||||
onValueChange={(value) => setFieldValue('unitId', value)}
|
onValueChange={(value) => setFieldValue('unitId', value)}
|
||||||
placeholder="Select unit"
|
placeholder="Select unit"
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
|
|
@ -170,7 +228,18 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
placeholder="Enter product quantity"
|
placeholder="Enter product quantity"
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
value={values.productQuantity.toString()}
|
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 }}
|
style={{ marginBottom: 16 }}
|
||||||
/>
|
/>
|
||||||
<BottomDropdown
|
<BottomDropdown
|
||||||
|
|
@ -209,6 +278,8 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<View style={tw`flex-row items-center mb-4`}>
|
<View style={tw`flex-row items-center mb-4`}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={values.isSuspended}
|
checked={values.isSuspended}
|
||||||
|
|
@ -223,7 +294,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
checked={values.isFlashAvailable}
|
checked={values.isFlashAvailable}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setFieldValue('isFlashAvailable', !values.isFlashAvailable);
|
setFieldValue('isFlashAvailable', !values.isFlashAvailable);
|
||||||
if (values.isFlashAvailable) setFieldValue('flashPrice', '');
|
if (values.isFlashAvailable) setFieldValue('flashPrice', ''); // Clear price when disabled
|
||||||
}}
|
}}
|
||||||
style={tw`mr-3`}
|
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
|
<TouchableOpacity
|
||||||
onPress={submit}
|
onPress={submit}
|
||||||
disabled={isLoading}
|
disabled={isLoading || isUploading}
|
||||||
style={tw`px-4 py-2 rounded-lg shadow-lg items-center mt-2 ${isLoading ? 'bg-gray-400' : 'bg-blue-500'}`}
|
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`}>
|
<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>
|
</MyText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import React, { useState, useEffect, forwardRef, useCallback } from 'react';
|
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 { Formik } from 'formik';
|
||||||
import * as Yup from 'yup';
|
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 MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
|
import { useUploadToObjectStorage } from '../../hooks/useUploadToObjectStorage';
|
||||||
|
|
||||||
interface StoreOption {
|
interface StoreOption {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -21,7 +24,7 @@ interface TagFormProps {
|
||||||
mode: 'create' | 'edit';
|
mode: 'create' | 'edit';
|
||||||
initialValues: TagFormData;
|
initialValues: TagFormData;
|
||||||
existingImageUrl?: string;
|
existingImageUrl?: string;
|
||||||
onSubmit: (values: TagFormData, images: ImageUploaderNeoItem[], removedExisting: boolean) => void;
|
onSubmit: (values: TagFormData, imageKey?: string, deleteExistingImage?: boolean) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
stores?: StoreOption[];
|
stores?: StoreOption[];
|
||||||
}
|
}
|
||||||
|
|
@ -29,29 +32,39 @@ interface TagFormProps {
|
||||||
const TagForm = forwardRef<any, TagFormProps>(({
|
const TagForm = forwardRef<any, TagFormProps>(({
|
||||||
mode,
|
mode,
|
||||||
initialValues,
|
initialValues,
|
||||||
existingImageUrl: existingImageUrlRaw,
|
existingImageUrl = '',
|
||||||
onSubmit,
|
onSubmit,
|
||||||
isLoading,
|
isLoading,
|
||||||
stores: storesRaw,
|
stores = [],
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const [images, setImages] = useState<ImageUploaderNeoItem[]>([])
|
const [newImage, setNewImage] = useState<{ blob: Blob; mimeType: string; uri: string } | null>(null);
|
||||||
const [removedExisting, setRemovedExisting] = useState(false)
|
|
||||||
const [isDashboardTagChecked, setIsDashboardTagChecked] = useState<boolean>(Boolean(initialValues.isDashboardTag));
|
const [isDashboardTagChecked, setIsDashboardTagChecked] = useState<boolean>(Boolean(initialValues.isDashboardTag));
|
||||||
|
const { uploadSingle, isUploading } = useUploadToObjectStorage();
|
||||||
const existingImageUrl = existingImageUrlRaw || ''
|
|
||||||
const stores = storesRaw || []
|
|
||||||
|
|
||||||
// Update checkbox when initial values change
|
// Update checkbox when initial values change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsDashboardTagChecked(Boolean(initialValues.isDashboardTag));
|
setIsDashboardTagChecked(Boolean(initialValues.isDashboardTag));
|
||||||
if (existingImageUrl) {
|
}, [initialValues.isDashboardTag]);
|
||||||
setImages([{ imgUrl: existingImageUrl, mimeType: null }])
|
|
||||||
} else {
|
const pickImage = usePickImage({
|
||||||
setImages([])
|
setFile: async (assets: any) => {
|
||||||
}
|
if (!assets || (Array.isArray(assets) && assets.length === 0)) {
|
||||||
setRemovedExisting(false)
|
setNewImage(null);
|
||||||
}, [existingImageUrlRaw, initialValues.isDashboardTag]);
|
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({
|
const validationSchema = Yup.object().shape({
|
||||||
tagName: Yup.string()
|
tagName: Yup.string()
|
||||||
|
|
@ -62,21 +75,47 @@ const TagForm = forwardRef<any, TagFormProps>(({
|
||||||
.max(500, 'Description must be less than 500 characters'),
|
.max(500, 'Description must be less than 500 characters'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Display images for ImageUploader
|
||||||
|
const displayImages = newImage ? [{ uri: newImage.uri }] : [];
|
||||||
|
const existingImages = existingImageUrl ? [existingImageUrl] : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
validationSchema={validationSchema}
|
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
|
enableReinitialize
|
||||||
>
|
>
|
||||||
{({ handleChange, handleSubmit, values, setFieldValue, errors, touched, setFieldValue: formikSetFieldValue, resetForm }) => {
|
{({ handleChange, handleSubmit, values, setFieldValue, errors, touched, resetForm }) => {
|
||||||
// Clear form when screen comes into focus
|
// Clear form when screen comes into focus
|
||||||
const clearForm = useCallback(() => {
|
const clearForm = useCallback(() => {
|
||||||
setImages([])
|
setNewImage(null);
|
||||||
setRemovedExisting(false)
|
setIsDashboardTagChecked(false);
|
||||||
setIsDashboardTagChecked(false);
|
resetForm();
|
||||||
resetForm();
|
}, [resetForm]);
|
||||||
}, [resetForm]);
|
|
||||||
|
|
||||||
useFocusCallback(clearForm);
|
useFocusCallback(clearForm);
|
||||||
|
|
||||||
|
|
@ -106,22 +145,15 @@ const TagForm = forwardRef<any, TagFormProps>(({
|
||||||
Tag Image {mode === 'edit' ? '(Upload new to replace)' : '(Optional)'}
|
Tag Image {mode === 'edit' ? '(Upload new to replace)' : '(Optional)'}
|
||||||
</MyText>
|
</MyText>
|
||||||
|
|
||||||
|
<ImageUploader
|
||||||
<ImageUploaderNeo
|
images={displayImages}
|
||||||
images={images}
|
existingImageUrls={mode === 'edit' ? existingImages : []}
|
||||||
onImageAdd={(payload: ImageUploaderNeoPayload[]) => {
|
onAddImage={pickImage}
|
||||||
setImages((prev) => [...prev, ...payload.map((img) => ({
|
onRemoveImage={() => setNewImage(null)}
|
||||||
imgUrl: img.url,
|
onRemoveExistingImage={mode === 'edit' ? () => {
|
||||||
mimeType: img.mimeType,
|
// In edit mode, this would trigger deletion of existing image
|
||||||
}))])
|
// But we need to implement this logic in the parent
|
||||||
}}
|
} : undefined}
|
||||||
onImageRemove={(payload) => {
|
|
||||||
if (payload.mimeType === null) {
|
|
||||||
setRemovedExisting(true)
|
|
||||||
}
|
|
||||||
setImages((prev) => prev.filter((item) => item.imgUrl !== payload.url))
|
|
||||||
}}
|
|
||||||
allowMultiple={false}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -132,7 +164,7 @@ const TagForm = forwardRef<any, TagFormProps>(({
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
const newValue = !isDashboardTagChecked;
|
const newValue = !isDashboardTagChecked;
|
||||||
setIsDashboardTagChecked(newValue);
|
setIsDashboardTagChecked(newValue);
|
||||||
formikSetFieldValue('isDashboardTag', newValue);
|
setFieldValue('isDashboardTag', newValue);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<MyText style={tw`ml-3 text-gray-800`}>Mark as Dashboard Tag</MyText>
|
<MyText style={tw`ml-3 text-gray-800`}>Mark as Dashboard Tag</MyText>
|
||||||
|
|
@ -153,7 +185,7 @@ const TagForm = forwardRef<any, TagFormProps>(({
|
||||||
}))}
|
}))}
|
||||||
onValueChange={(selectedValues) => {
|
onValueChange={(selectedValues) => {
|
||||||
const numericValues = (selectedValues as string[]).map(v => parseInt(v));
|
const numericValues = (selectedValues as string[]).map(v => parseInt(v));
|
||||||
formikSetFieldValue('relatedStores', numericValues);
|
setFieldValue('relatedStores', numericValues);
|
||||||
}}
|
}}
|
||||||
multiple={true}
|
multiple={true}
|
||||||
/>
|
/>
|
||||||
|
|
@ -161,11 +193,11 @@ const TagForm = forwardRef<any, TagFormProps>(({
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => handleSubmit()}
|
onPress={() => handleSubmit()}
|
||||||
disabled={isLoading}
|
disabled={isLoading || isUploading}
|
||||||
style={tw`px-4 py-3 rounded-lg shadow-lg items-center ${isLoading ? 'bg-gray-400' : 'bg-blue-500'}`}
|
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`}>
|
<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>
|
</MyText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { View, TouchableOpacity, Alert } from 'react-native';
|
||||||
import { Entypo } from '@expo/vector-icons';
|
import { Entypo } from '@expo/vector-icons';
|
||||||
import { MyText, tw, BottomDialog } from 'common-ui';
|
import { MyText, tw, BottomDialog } from 'common-ui';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '../trpc-client';
|
||||||
|
|
||||||
export interface TagMenuProps {
|
export interface TagMenuProps {
|
||||||
tagId: number;
|
tagId: number;
|
||||||
|
|
@ -22,7 +22,7 @@ export const TagMenu: React.FC<TagMenuProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const deleteTag = trpc.admin.product.deleteProductTag.useMutation();
|
const deleteTag = trpc.admin.tag.deleteTag.useMutation();
|
||||||
|
|
||||||
const handleOpenMenu = () => {
|
const handleOpenMenu = () => {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
|
|
@ -63,7 +63,7 @@ export const TagMenu: React.FC<TagMenuProps> = ({
|
||||||
const errorMessage = error.message || 'Failed to delete tag';
|
const errorMessage = error.message || 'Failed to delete tag';
|
||||||
Alert.alert('Error', errorMessage);
|
Alert.alert('Error', errorMessage);
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
|
|
@ -116,4 +116,4 @@ export const TagMenu: React.FC<TagMenuProps> = ({
|
||||||
</BottomDialog>
|
</BottomDialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
9
packages/db_helper_postgres/.env → apps/backend/.env
Normal file → Executable file
9
packages/db_helper_postgres/.env → apps/backend/.env
Normal file → Executable file
|
|
@ -1,7 +1,10 @@
|
||||||
ENV_MODE=PROD
|
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
|
# 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_BASE_URL=https://api-preprod.phonepe.com/
|
||||||
|
|
||||||
PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090
|
PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090
|
||||||
PHONE_PE_CLIENT_VERSION=1
|
PHONE_PE_CLIENT_VERSION=1
|
||||||
PHONE_PE_CLIENT_SECRET=MTU1MmIzOTgtM2Q0Mi00N2M5LTkyMWUtNzBiMjdmYzVmZWUy
|
PHONE_PE_CLIENT_SECRET=MTU1MmIzOTgtM2Q0Mi00N2M5LTkyMWUtNzBiMjdmYzVmZWUy
|
||||||
|
|
@ -17,10 +20,10 @@ S3_REGION=apac
|
||||||
S3_ACCESS_KEY_ID=8fab47503efb9547b50e4fb317e35cc7
|
S3_ACCESS_KEY_ID=8fab47503efb9547b50e4fb317e35cc7
|
||||||
S3_SECRET_ACCESS_KEY=47c2eb5636843cf568dda7ad0959a3e42071303f26dbdff94bd45a3c33dcd950
|
S3_SECRET_ACCESS_KEY=47c2eb5636843cf568dda7ad0959a3e42071303f26dbdff94bd45a3c33dcd950
|
||||||
S3_URL=https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com
|
S3_URL=https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com
|
||||||
S3_BUCKET_NAME=meatfarmer
|
S3_BUCKET_NAME=meatfarmer-dev
|
||||||
EXPO_ACCESS_TOKEN=Asvpy8cByRh6T4ksnWScO6PLcio2n35-BwES5zK-
|
EXPO_ACCESS_TOKEN=Asvpy8cByRh6T4ksnWScO6PLcio2n35-BwES5zK-
|
||||||
JWT_SECRET=my_meatfarmer_jwt_secret_key
|
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
|
API_CACHE_KEY=api-cache-dev
|
||||||
# CLOUDFLARE_API_TOKEN=I8Vp4E9TX58E8qEDeH0nTFDS2d2zXNYiXvbs4Ckj
|
# CLOUDFLARE_API_TOKEN=I8Vp4E9TX58E8qEDeH0nTFDS2d2zXNYiXvbs4Ckj
|
||||||
CLOUDFLARE_API_TOKEN=N7jAg5X-RUj_fVfMW6zbfJ8qIYc81TSIKKlbZ6oh
|
CLOUDFLARE_API_TOKEN=N7jAg5X-RUj_fVfMW6zbfJ8qIYc81TSIKKlbZ6oh
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
ENV_MODE=PROD
|
|
||||||
DATABASE_URL=postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer #technocracy
|
|
||||||
# DATABASE_URL=postgres://postgres:meatfarmer_master_password@5.223.55.14:7447/meatfarmer #hetzner
|
|
||||||
PHONE_PE_BASE_URL=https://api-preprod.phonepe.com/
|
|
||||||
PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090
|
|
||||||
PHONE_PE_CLIENT_VERSION=1
|
|
||||||
PHONE_PE_CLIENT_SECRET=MTU1MmIzOTgtM2Q0Mi00N2M5LTkyMWUtNzBiMjdmYzVmZWUy
|
|
||||||
PHONE_PE_MERCHANT_ID=M23F2IGP34ZAR
|
|
||||||
|
|
||||||
# S3_REGION=ap-hyderabad-1
|
|
||||||
# S3_REGION=sgp
|
|
||||||
# S3_ACCESS_KEY_ID=52932a33abce40b38b559dadccab640f
|
|
||||||
# S3_SECRET_ACCESS_KEY=d287998b696d4a1c912e727f6394e53b
|
|
||||||
# S3_URL=https://s3.sgp.io.cloud.ovh.net/
|
|
||||||
# S3_BUCKET_NAME=theobjectstore
|
|
||||||
S3_REGION=apac
|
|
||||||
S3_ACCESS_KEY_ID=8fab47503efb9547b50e4fb317e35cc7
|
|
||||||
S3_SECRET_ACCESS_KEY=47c2eb5636843cf568dda7ad0959a3e42071303f26dbdff94bd45a3c33dcd950
|
|
||||||
S3_URL=https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com
|
|
||||||
S3_BUCKET_NAME=meatfarmer-dev
|
|
||||||
EXPO_ACCESS_TOKEN=Asvpy8cByRh6T4ksnWScO6PLcio2n35-BwES5zK-
|
|
||||||
JWT_SECRET=my_meatfarmer_jwt_secret_key
|
|
||||||
ASSETS_DOMAIN=https://assets2.freshyo.in/
|
|
||||||
API_CACHE_KEY=api-cache-dev
|
|
||||||
# CLOUDFLARE_API_TOKEN=I8Vp4E9TX58E8qEDeH0nTFDS2d2zXNYiXvbs4Ckj
|
|
||||||
CLOUDFLARE_API_TOKEN=N7jAg5X-RUj_fVfMW6zbfJ8qIYc81TSIKKlbZ6oh
|
|
||||||
CLOUDFLARE_ZONE_ID=edefbf750bfc3ff26ccd11e8e28dc8d7
|
|
||||||
# REDIS_URL=redis://default:redis_shafi_password@5.223.55.14:6379
|
|
||||||
REDIS_URL=redis://default:redis_shafi_password@57.128.212.174:6379
|
|
||||||
APP_URL=http://localhost:4000
|
|
||||||
RAZORPAY_KEY=rzp_test_RdCBBUJ56NLaJK
|
|
||||||
RAZORPAY_SECRET=namEwKBE1ypWxH0QDVg6fWOe
|
|
||||||
OTP_SENDER_AUTH_TOKEN=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJDLTM5OENEMkJDRTM0MjQ4OCIsImlhdCI6MTc0Nzg0MTEwMywiZXhwIjoxOTA1NTIxMTAzfQ.IV64ofVKjcwveIanxu_P2XlACtPeA9sJQ74uM53osDeyUXsFv0rwkCl6NNBIX93s_wnh4MKITLbcF_ClwmFQ0A
|
|
||||||
|
|
||||||
MIN_ORDER_VALUE=300
|
|
||||||
DELIVERY_CHARGE=20
|
|
||||||
|
|
||||||
# Telegram Configuration
|
|
||||||
TELEGRAM_BOT_TOKEN=8410461852:AAGXQCwRPFbndqwTgLJh8kYxST4Z0vgh72U
|
|
||||||
TELEGRAM_CHAT_IDS=5147760058
|
|
||||||
# TELEGRAM_BOT_TOKEN=8410461852:AAGXQCwRPFbndqwTgLJh8kYxST4Z0vgh72U
|
|
||||||
# TELEGRAM_CHAT_IDS=-5075171894
|
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6
apps/backend/drizzle.config.ts
Executable file
6
apps/backend/drizzle.config.ts
Executable 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
Loading…
Add table
Reference in a new issue