274 lines
9 KiB
TypeScript
274 lines
9 KiB
TypeScript
import React, { useState, useCallback } from 'react';
|
|
import {
|
|
View,
|
|
TouchableOpacity,
|
|
ActivityIndicator,
|
|
ScrollView,
|
|
Alert,
|
|
} from 'react-native';
|
|
import { useRouter } from 'expo-router';
|
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
|
import {
|
|
AppContainer,
|
|
MyText,
|
|
tw,
|
|
MyTextInput,
|
|
BottomDropdown,
|
|
ImageUploader,
|
|
} from 'common-ui';
|
|
import { trpc } from '@/src/trpc-client';
|
|
import usePickImage from 'common-ui/src/components/use-pick-image';
|
|
|
|
interface User {
|
|
id: number;
|
|
name: string | null;
|
|
mobile: string | null;
|
|
isEligibleForNotif: boolean;
|
|
}
|
|
|
|
const extractKeyFromUrl = (url: string): string => {
|
|
const u = new URL(url);
|
|
const rawKey = u.pathname.replace(/^\/+/, '');
|
|
return decodeURIComponent(rawKey);
|
|
};
|
|
|
|
export default function SendNotifications() {
|
|
const router = useRouter();
|
|
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
|
|
const [title, setTitle] = useState('');
|
|
const [message, setMessage] = useState('');
|
|
const [selectedImage, setSelectedImage] = useState<{ blob: Blob; mimeType: string } | null>(null);
|
|
const [displayImage, setDisplayImage] = useState<{ uri?: string } | null>(null);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
// Query users eligible for notifications
|
|
const { data: usersData, isLoading: isLoadingUsers } = trpc.admin.user.getUsersForNotification.useQuery({
|
|
search: searchQuery,
|
|
});
|
|
|
|
// Generate upload URLs mutation
|
|
const generateUploadUrls = trpc.user.fileUpload.generateUploadUrls.useMutation();
|
|
|
|
// Send notification mutation
|
|
const sendNotification = trpc.admin.user.sendNotification.useMutation({
|
|
onSuccess: () => {
|
|
Alert.alert('Success', 'Notification sent successfully!');
|
|
// Reset form
|
|
setSelectedUserIds([]);
|
|
setTitle('');
|
|
setMessage('');
|
|
setSelectedImage(null);
|
|
setDisplayImage(null);
|
|
},
|
|
onError: (error: any) => {
|
|
Alert.alert('Error', error.message || 'Failed to send notification');
|
|
},
|
|
});
|
|
|
|
const eligibleUsers = usersData?.users.filter((u: User) => u.isEligibleForNotif) || [];
|
|
|
|
const dropdownOptions = eligibleUsers.map((user: User) => ({
|
|
label: `${user.mobile || 'No Mobile'}${user.name ? ` - ${user.name}` : ''}`,
|
|
value: user.id,
|
|
}));
|
|
|
|
const handleImagePick = usePickImage({
|
|
setFile: async (assets: any) => {
|
|
if (!assets || (Array.isArray(assets) && assets.length === 0)) {
|
|
setSelectedImage(null);
|
|
setDisplayImage(null);
|
|
return;
|
|
}
|
|
|
|
const file = Array.isArray(assets) ? assets[0] : assets;
|
|
const response = await fetch(file.uri);
|
|
const blob = await response.blob();
|
|
|
|
setSelectedImage({ blob, mimeType: file.mimeType || 'image/jpeg' });
|
|
setDisplayImage({ uri: file.uri });
|
|
},
|
|
multiple: false,
|
|
});
|
|
|
|
const handleRemoveImage = () => {
|
|
setSelectedImage(null);
|
|
setDisplayImage(null);
|
|
};
|
|
|
|
const handleSend = async () => {
|
|
if (title.trim().length === 0) {
|
|
Alert.alert('Error', 'Please enter a title');
|
|
return;
|
|
}
|
|
|
|
if (message.trim().length === 0) {
|
|
Alert.alert('Error', 'Please enter a message');
|
|
return;
|
|
}
|
|
|
|
// Check if sending to all users
|
|
const isSendingToAll = selectedUserIds.length === 0;
|
|
if (isSendingToAll) {
|
|
const confirmed = await new Promise<boolean>((resolve) => {
|
|
Alert.alert(
|
|
'Send to All Users?',
|
|
'This will send the notification to all users with push tokens. Continue?',
|
|
[
|
|
{ text: 'Cancel', style: 'cancel', onPress: () => resolve(false) },
|
|
{ text: 'Send', style: 'default', onPress: () => resolve(true) },
|
|
]
|
|
);
|
|
});
|
|
if (!confirmed) return;
|
|
}
|
|
|
|
try {
|
|
let imageUrl: string | undefined;
|
|
|
|
// Upload image if selected
|
|
if (selectedImage) {
|
|
const { uploadUrls } = await generateUploadUrls.mutateAsync({
|
|
contextString: 'notification',
|
|
mimeTypes: [selectedImage.mimeType],
|
|
});
|
|
|
|
if (uploadUrls.length > 0) {
|
|
const uploadUrl = uploadUrls[0];
|
|
imageUrl = extractKeyFromUrl(uploadUrl);
|
|
|
|
// Upload image
|
|
const uploadResponse = await fetch(uploadUrl, {
|
|
method: 'PUT',
|
|
body: selectedImage.blob,
|
|
headers: {
|
|
'Content-Type': selectedImage.mimeType,
|
|
},
|
|
});
|
|
|
|
if (!uploadResponse.ok) {
|
|
throw new Error(`Upload failed with status ${uploadResponse.status}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send notification
|
|
await sendNotification.mutateAsync({
|
|
userIds: selectedUserIds,
|
|
title: title.trim(),
|
|
text: message.trim(),
|
|
imageUrl,
|
|
});
|
|
} catch (error: any) {
|
|
Alert.alert('Error', error.message || 'Failed to send notification');
|
|
}
|
|
};
|
|
|
|
const getDisplayText = () => {
|
|
if (selectedUserIds.length === 0) return 'All Users';
|
|
if (selectedUserIds.length === 1) {
|
|
const user = eligibleUsers.find((u: User) => u.id === selectedUserIds[0]);
|
|
return user ? `${user.mobile}${user.name ? ` - ${user.name}` : ''}` : '1 user selected';
|
|
}
|
|
return `${selectedUserIds.length} users selected`;
|
|
};
|
|
|
|
if (isLoadingUsers) {
|
|
return (
|
|
<AppContainer>
|
|
<View style={tw`flex-1 justify-center items-center`}>
|
|
<ActivityIndicator size="large" color="#3b82f6" />
|
|
<MyText style={tw`text-gray-500 mt-4`}>Loading users...</MyText>
|
|
</View>
|
|
</AppContainer>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AppContainer>
|
|
<View style={tw`flex-1 bg-gray-50`}>
|
|
{/* Header */}
|
|
<View style={tw`bg-white px-4 py-4 border-b border-gray-200 flex-row items-center`}>
|
|
<TouchableOpacity
|
|
onPress={() => router.back()}
|
|
style={tw`p-2 -ml-4`}
|
|
>
|
|
<MaterialIcons name="chevron-left" size={24} color="#374151" />
|
|
</TouchableOpacity>
|
|
<MyText style={tw`text-xl font-bold text-gray-900 ml-2`}>Send Notifications</MyText>
|
|
</View>
|
|
|
|
<ScrollView style={tw`flex-1`} contentContainerStyle={tw`p-4`}>
|
|
{/* Title Input */}
|
|
<View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
|
|
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Title</MyText>
|
|
<MyTextInput
|
|
value={title}
|
|
onChangeText={setTitle}
|
|
placeholder="Enter notification title..."
|
|
style={tw`text-gray-900`}
|
|
/>
|
|
</View>
|
|
|
|
{/* Message Input */}
|
|
<View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
|
|
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Message</MyText>
|
|
<MyTextInput
|
|
value={message}
|
|
onChangeText={setMessage}
|
|
placeholder="Enter notification message..."
|
|
multiline
|
|
numberOfLines={4}
|
|
style={tw`text-gray-900`}
|
|
/>
|
|
</View>
|
|
|
|
{/* Image Upload - Hidden for now */}
|
|
{/* <View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
|
|
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Image (Optional)</MyText>
|
|
<ImageUploader
|
|
images={displayImage ? [displayImage] : []}
|
|
existingImageUrls={[]}
|
|
onAddImage={handleImagePick}
|
|
onRemoveImage={handleRemoveImage}
|
|
/>
|
|
</View> */}
|
|
|
|
{/* User Selection */}
|
|
<View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
|
|
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Select Users (Optional)</MyText>
|
|
<BottomDropdown
|
|
label="Select Users"
|
|
value={selectedUserIds}
|
|
options={dropdownOptions}
|
|
onValueChange={(value) => setSelectedUserIds(value as number[])}
|
|
multiple={true}
|
|
placeholder="Select users"
|
|
onSearch={(query) => setSearchQuery(query)}
|
|
/>
|
|
<MyText style={tw`text-gray-500 text-sm mt-2`}>
|
|
{getDisplayText()}
|
|
</MyText>
|
|
<MyText style={tw`text-blue-600 text-xs mt-1`}>
|
|
Leave empty to send to all users
|
|
</MyText>
|
|
</View>
|
|
|
|
{/* Submit Button */}
|
|
<TouchableOpacity
|
|
onPress={handleSend}
|
|
disabled={sendNotification.isPending || title.trim().length === 0 || message.trim().length === 0}
|
|
style={tw`${
|
|
sendNotification.isPending || title.trim().length === 0 || message.trim().length === 0
|
|
? 'bg-gray-300'
|
|
: 'bg-blue-600'
|
|
} rounded-xl py-4 items-center shadow-sm`}
|
|
>
|
|
<MyText style={tw`text-white font-bold text-base`}>
|
|
{sendNotification.isPending ? 'Sending...' : selectedUserIds.length === 0 ? 'Send to All Users' : 'Send Notification'}
|
|
</MyText>
|
|
</TouchableOpacity>
|
|
</ScrollView>
|
|
</View>
|
|
</AppContainer>
|
|
);
|
|
}
|