374 lines
12 KiB
TypeScript
374 lines
12 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
||
import {
|
||
View,
|
||
Text,
|
||
TextInput,
|
||
TouchableOpacity,
|
||
FlatList,
|
||
Image,
|
||
ActivityIndicator,
|
||
KeyboardAvoidingView,
|
||
Platform,
|
||
Alert,
|
||
Modal,
|
||
} from 'react-native';
|
||
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
|
||
import { StackNavigationProp } from '@react-navigation/stack';
|
||
import { Header } from '@components';
|
||
import { useAppTheme } from '@theme';
|
||
import { useCustomerSupport } from '@hooks';
|
||
import { getStyles } from './supportChatScreen.styles';
|
||
import { AppStackParamList } from '../../../navigation/appStack';
|
||
import { formatTime, getFullUrl } from '../../../utils/helper';
|
||
|
||
type SupportChatRouteProp = RouteProp<AppStackParamList, 'SupportChatScreen'>;
|
||
type SupportChatNavProp = StackNavigationProp<AppStackParamList>;
|
||
|
||
// Mock attachments that users can select to simulate image attachments
|
||
const MOCK_ATTACHMENTS = [
|
||
{
|
||
id: 'att1',
|
||
label: 'Receipt Invoice',
|
||
url: 'https://images.unsplash.com/photo-1554415707-6e8cfc93fe23?w=500&auto=format&fit=crop&q=60',
|
||
},
|
||
{
|
||
id: 'att2',
|
||
label: 'Damaged Packing',
|
||
url: 'https://images.unsplash.com/photo-1607344645866-009c320c5ab8?w=500&auto=format&fit=crop&q=60',
|
||
},
|
||
{
|
||
id: 'att3',
|
||
label: 'Wrong Item Delivered',
|
||
url: 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=500&auto=format&fit=crop&q=60',
|
||
},
|
||
];
|
||
|
||
export const SupportChatScreen: React.FC = () => {
|
||
const { colors } = useAppTheme();
|
||
const styles = getStyles(colors);
|
||
const route = useRoute<SupportChatRouteProp>();
|
||
const navigation = useNavigation<SupportChatNavProp>();
|
||
const { ticketId } = route.params;
|
||
|
||
const {
|
||
currentTicket,
|
||
messages,
|
||
loading,
|
||
isAgentTyping,
|
||
fetchTicketDetails,
|
||
sendMessage,
|
||
reopenTicket,
|
||
emitTypingStart,
|
||
emitTypingStop,
|
||
} = useCustomerSupport(ticketId);
|
||
|
||
const [inputText, setInputText] = useState('');
|
||
const [selectedAttachmentUrl, setSelectedAttachmentUrl] = useState<string | null>(null);
|
||
const [attachmentModalVisible, setAttachmentModalVisible] = useState(false);
|
||
const flatListRef = useRef<FlatList>(null);
|
||
const typingTimeoutRef = useRef<any>(null);
|
||
|
||
useEffect(() => {
|
||
if (ticketId) {
|
||
fetchTicketDetails(ticketId);
|
||
}
|
||
}, [ticketId, fetchTicketDetails]);
|
||
|
||
// Auto scroll to bottom when messages change
|
||
useEffect(() => {
|
||
if (messages.length > 0) {
|
||
setTimeout(() => {
|
||
flatListRef.current?.scrollToEnd({ animated: true });
|
||
}, 100);
|
||
}
|
||
}, [messages]);
|
||
|
||
const handleSend = async () => {
|
||
if (!inputText.trim() && !selectedAttachmentUrl) return;
|
||
|
||
try {
|
||
const textToSend = inputText.trim();
|
||
const attachmentsToSend = selectedAttachmentUrl ? [selectedAttachmentUrl] : undefined;
|
||
|
||
setInputText('');
|
||
setSelectedAttachmentUrl(null);
|
||
emitTypingStop();
|
||
|
||
await sendMessage(ticketId, textToSend, attachmentsToSend);
|
||
} catch (error) {
|
||
Alert.alert('Error', 'Failed to send message. Please try again.');
|
||
}
|
||
};
|
||
|
||
const handleInputChange = (text: string) => {
|
||
setInputText(text);
|
||
|
||
// Emit typing start and manage typing stop timeout
|
||
emitTypingStart();
|
||
if (typingTimeoutRef.current) {
|
||
clearTimeout(typingTimeoutRef.current);
|
||
}
|
||
typingTimeoutRef.current = setTimeout(() => {
|
||
emitTypingStop();
|
||
}, 2000);
|
||
};
|
||
|
||
const handleReopen = async () => {
|
||
try {
|
||
await reopenTicket(ticketId);
|
||
Alert.alert('Success', 'Ticket has been reopened.');
|
||
} catch (error) {
|
||
Alert.alert('Error', 'Failed to reopen ticket.');
|
||
}
|
||
};
|
||
|
||
const getStatusColor = (status?: string) => {
|
||
switch (status) {
|
||
case 'OPEN':
|
||
return { bg: '#E3F2FD', text: '#1E88E5' }; // Blue
|
||
case 'IN_PROGRESS':
|
||
return { bg: '#FFF3E0', text: '#FB8C00' }; // Amber
|
||
case 'RESOLVED':
|
||
return { bg: '#E8F5E9', text: '#43A047' }; // Green
|
||
case 'CLOSED':
|
||
return { bg: '#ECEFF1', text: '#546E7A' }; // Grey
|
||
default:
|
||
return { bg: '#F5F5F5', text: '#9E9E9E' };
|
||
}
|
||
};
|
||
|
||
const statusStyle = getStatusColor(currentTicket?.status);
|
||
|
||
if (loading && !currentTicket) {
|
||
return (
|
||
<View style={styles.container}>
|
||
<Header title="Support Ticket" onBack={() => navigation.goBack()} />
|
||
<View style={styles.loadingContainer}>
|
||
<ActivityIndicator size="large" color={colors.primary} />
|
||
<Text style={styles.loadingText}>Loading support ticket...</Text>
|
||
</View>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const isClosed = currentTicket?.status === 'RESOLVED' || currentTicket?.status === 'CLOSED';
|
||
|
||
return (
|
||
<KeyboardAvoidingView
|
||
style={styles.container}
|
||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||
>
|
||
<Header
|
||
title={`Support Chat`}
|
||
onBack={() => navigation.goBack()}
|
||
rightComponent={
|
||
currentTicket && (
|
||
<View style={[styles.statusBadge, { backgroundColor: statusStyle.bg }]}>
|
||
<Text style={[styles.statusText, { color: statusStyle.text }]}>
|
||
{currentTicket.status.replace('_', ' ')}
|
||
</Text>
|
||
</View>
|
||
)
|
||
}
|
||
/>
|
||
|
||
{currentTicket && (
|
||
<View style={styles.ticketInfoBar}>
|
||
<Text style={styles.ticketSubject}>{currentTicket.subject}</Text>
|
||
<Text style={styles.ticketMeta}>
|
||
ID: #{currentTicket.ticketNumber} | Category: {currentTicket.category.replace('_', ' ')}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
|
||
<FlatList
|
||
ref={flatListRef}
|
||
data={messages}
|
||
keyExtractor={(item) => item.id}
|
||
contentContainerStyle={styles.chatList}
|
||
renderItem={({ item }) => {
|
||
const isCustomer = item.sender.roleEnum === 'CUSTOMER';
|
||
return (
|
||
<View
|
||
style={[
|
||
styles.messageRow,
|
||
isCustomer ? styles.customerRow : styles.agentRow,
|
||
]}
|
||
>
|
||
<View
|
||
style={[
|
||
styles.bubble,
|
||
isCustomer ? styles.customerBubble : styles.agentBubble,
|
||
]}
|
||
>
|
||
{!isCustomer && (
|
||
<Text style={styles.senderName}>{item.sender.name} (Support)</Text>
|
||
)}
|
||
{item.attachments && item.attachments.map((att: string, idx: number) => (
|
||
<Image
|
||
key={idx}
|
||
source={{ uri: getFullUrl(att) }}
|
||
style={styles.attachmentImage}
|
||
resizeMode="cover"
|
||
/>
|
||
))}
|
||
{item.message.trim() ? (
|
||
<Text
|
||
style={[
|
||
styles.messageText,
|
||
isCustomer ? styles.customerText : styles.agentText,
|
||
]}
|
||
>
|
||
{item.message}
|
||
</Text>
|
||
) : null}
|
||
<Text
|
||
style={[
|
||
styles.timeText,
|
||
isCustomer ? styles.customerTime : styles.agentTime,
|
||
]}
|
||
>
|
||
{formatTime(item.createdAt)}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
);
|
||
}}
|
||
onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: true })}
|
||
/>
|
||
|
||
{isAgentTyping && (
|
||
<View style={styles.typingContainer}>
|
||
<ActivityIndicator size="small" color={colors.textSecondary} />
|
||
<Text style={styles.typingText}>Agent is typing...</Text>
|
||
</View>
|
||
)}
|
||
|
||
{/* Selected attachment preview */}
|
||
{selectedAttachmentUrl && (
|
||
<View style={styles.previewContainer}>
|
||
<View style={styles.previewImageWrapper}>
|
||
<Image source={{ uri: selectedAttachmentUrl }} style={styles.previewImage} />
|
||
<TouchableOpacity
|
||
style={styles.removePreviewBtn}
|
||
onPress={() => setSelectedAttachmentUrl(null)}
|
||
>
|
||
<Text style={styles.removePreviewText}>✕</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{/* Input or Reopen prompt */}
|
||
{isClosed ? (
|
||
<View style={styles.reopenContainer}>
|
||
<Text style={styles.reopenTitle}>
|
||
This ticket has been resolved or closed.
|
||
</Text>
|
||
<TouchableOpacity style={styles.reopenBtn} onPress={handleReopen} activeOpacity={0.8}>
|
||
<Text style={styles.reopenBtnText}>Reopen Ticket</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
) : (
|
||
<View style={styles.inputContainer}>
|
||
<TouchableOpacity
|
||
style={styles.attachmentBtn}
|
||
onPress={() => setAttachmentModalVisible(true)}
|
||
>
|
||
<Text style={{ fontSize: 22 }}>📎</Text>
|
||
</TouchableOpacity>
|
||
<TextInput
|
||
style={styles.input}
|
||
value={inputText}
|
||
onChangeText={handleInputChange}
|
||
placeholder="Type a message..."
|
||
placeholderTextColor={colors.textSecondary}
|
||
multiline
|
||
onBlur={emitTypingStop}
|
||
/>
|
||
<TouchableOpacity
|
||
style={[
|
||
styles.sendBtn,
|
||
(!inputText.trim() && !selectedAttachmentUrl) && styles.sendBtnDisabled,
|
||
]}
|
||
onPress={handleSend}
|
||
disabled={!inputText.trim() && !selectedAttachmentUrl}
|
||
activeOpacity={0.8}
|
||
>
|
||
<Text style={styles.sendIcon}>➤</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
)}
|
||
|
||
{/* Attachment Modal */}
|
||
<Modal
|
||
visible={attachmentModalVisible}
|
||
transparent
|
||
animationType="fade"
|
||
onRequestClose={() => setAttachmentModalVisible(false)}
|
||
>
|
||
<View
|
||
style={{
|
||
flex: 1,
|
||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||
justifyContent: 'flex-end',
|
||
}}
|
||
>
|
||
<View
|
||
style={{
|
||
backgroundColor: colors.cardBg,
|
||
borderTopLeftRadius: 20,
|
||
borderTopRightRadius: 20,
|
||
padding: 20,
|
||
}}
|
||
>
|
||
<Text
|
||
style={{
|
||
fontSize: 18,
|
||
fontWeight: 'bold',
|
||
color: colors.text,
|
||
marginBottom: 16,
|
||
textAlign: 'center',
|
||
}}
|
||
>
|
||
Select Mock Attachment
|
||
</Text>
|
||
{MOCK_ATTACHMENTS.map((item) => (
|
||
<TouchableOpacity
|
||
key={item.id}
|
||
style={{
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
paddingVertical: 12,
|
||
borderBottomWidth: 1,
|
||
borderBottomColor: colors.border,
|
||
}}
|
||
onPress={() => {
|
||
setSelectedAttachmentUrl(item.url);
|
||
setAttachmentModalVisible(false);
|
||
}}
|
||
>
|
||
<Text style={{ fontSize: 24, marginRight: 12 }}>🖼️</Text>
|
||
<Text style={{ fontSize: 16, color: colors.text }}>{item.label}</Text>
|
||
</TouchableOpacity>
|
||
))}
|
||
<TouchableOpacity
|
||
style={{
|
||
marginTop: 16,
|
||
paddingVertical: 12,
|
||
backgroundColor: colors.border,
|
||
borderRadius: 10,
|
||
alignItems: 'center',
|
||
}}
|
||
onPress={() => setAttachmentModalVisible(false)}
|
||
>
|
||
<Text style={{ fontSize: 16, fontWeight: 'bold', color: colors.text }}>Cancel</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
</Modal>
|
||
</KeyboardAvoidingView>
|
||
);
|
||
};
|
||
export default SupportChatScreen;
|