vorqard-ai-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +209 -0
- package/assets/Ai_icon.png +0 -0
- package/package.json +34 -0
- package/src/api/sdkClient.js +22 -0
- package/src/components/ChatCards.js +5 -0
- package/src/components/ChatComponents.js +208 -0
- package/src/components/ChatStyles.js +759 -0
- package/src/components/ChatUtils.js +33 -0
- package/src/components/ReportAnalysisComponents.js +170 -0
- package/src/components/cards/BookingConfirmationCard.js +69 -0
- package/src/components/cards/DoctorCards.js +152 -0
- package/src/components/cards/DoctorTypeSelectionCard.js +40 -0
- package/src/components/cards/HospitalCards.js +136 -0
- package/src/components/cards/SlotSelectionCard.js +86 -0
- package/src/hooks/useHealthChat.js +145 -0
- package/src/hooks/useReportAnalysis.js +93 -0
- package/src/index.js +30 -0
- package/src/screens/AIHealthChatScreen.js +610 -0
- package/src/screens/AIReportAnalysisScreen.js +77 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getSDKClient } from '../api/sdkClient';
|
|
2
|
+
|
|
3
|
+
export const getAbsoluteProfilePicture = (path) => {
|
|
4
|
+
if (!path || String(path).trim() === '' || String(path).trim() === 'null' || String(path).trim() === 'undefined') {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const cleanPath = String(path).trim();
|
|
8
|
+
if (cleanPath.startsWith('http://') || cleanPath.startsWith('https://')) {
|
|
9
|
+
return cleanPath;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const sdkClient = getSDKClient();
|
|
13
|
+
const baseURL = sdkClient?.defaults?.baseURL;
|
|
14
|
+
if (baseURL) {
|
|
15
|
+
const cleanBase = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
|
|
16
|
+
const cleanSub = cleanPath.startsWith('/') ? cleanPath : `/${cleanPath}`;
|
|
17
|
+
return `${cleanBase}${cleanSub}`;
|
|
18
|
+
}
|
|
19
|
+
} catch (e) {
|
|
20
|
+
console.warn('Error resolving base URL for profile picture:', e);
|
|
21
|
+
}
|
|
22
|
+
return cleanPath;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const getFormattedDoctorName = (name) => {
|
|
26
|
+
if (!name) return "";
|
|
27
|
+
const nameStr = String(name).trim();
|
|
28
|
+
if (nameStr.toLowerCase().startsWith("dr.")) {
|
|
29
|
+
return nameStr;
|
|
30
|
+
}
|
|
31
|
+
return `Dr. ${nameStr}`;
|
|
32
|
+
};
|
|
33
|
+
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity, Image, ActivityIndicator, Animated, StyleSheet } from 'react-native';
|
|
3
|
+
import LinearGradient from 'react-native-linear-gradient';
|
|
4
|
+
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
5
|
+
|
|
6
|
+
export const ScoreRing = ({ score, condition }) => {
|
|
7
|
+
const getColor = () => {
|
|
8
|
+
if (score >= 80) return ['#10B981', '#059669'];
|
|
9
|
+
if (score >= 60) return ['#F59E0B', '#D97706'];
|
|
10
|
+
if (score >= 40) return ['#EF4444', '#DC2626'];
|
|
11
|
+
return ['#7C3AED', '#6D28D9'];
|
|
12
|
+
};
|
|
13
|
+
const [c1, c2] = getColor();
|
|
14
|
+
return (
|
|
15
|
+
<LinearGradient colors={[c1 + '22', c2 + '11']} style={styles.scoreRingWrap}>
|
|
16
|
+
<View style={[styles.scoreRingOuter, { borderColor: c1 }]}>
|
|
17
|
+
<View style={styles.scoreRingInner}>
|
|
18
|
+
<Text style={[styles.scoreNumber, { color: c1 }]}>{score}</Text>
|
|
19
|
+
<Text style={styles.scoreLabel}>/ 100</Text>
|
|
20
|
+
</View>
|
|
21
|
+
</View>
|
|
22
|
+
<Text style={[styles.conditionBadge, { color: c1, borderColor: c1 + '44', backgroundColor: c1 + '15' }]}>
|
|
23
|
+
{condition}
|
|
24
|
+
</Text>
|
|
25
|
+
</LinearGradient>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const UploadArea = ({ captureWithCamera, pickFromGallery }) => (
|
|
30
|
+
<View style={styles.uploadArea}>
|
|
31
|
+
<LinearGradient colors={['#EFF6FF', '#DBEAFE']} style={styles.uploadBox}>
|
|
32
|
+
<Ionicons name="cloud-upload-outline" size={52} color="#2563EB" />
|
|
33
|
+
<Text style={styles.uploadTitle}>Upload Your Test Report</Text>
|
|
34
|
+
<Text style={styles.uploadSub}>Blood test, X-ray, CBC, Lipid panel — any report</Text>
|
|
35
|
+
<View style={styles.uploadBtns}>
|
|
36
|
+
<TouchableOpacity style={styles.uploadBtn} onPress={captureWithCamera}>
|
|
37
|
+
<Ionicons name="camera" size={20} color="#fff" />
|
|
38
|
+
<Text style={styles.uploadBtnText}>Camera</Text>
|
|
39
|
+
</TouchableOpacity>
|
|
40
|
+
<TouchableOpacity style={[styles.uploadBtn, { backgroundColor: '#10B981' }]} onPress={pickFromGallery}>
|
|
41
|
+
<Ionicons name="images" size={20} color="#fff" />
|
|
42
|
+
<Text style={styles.uploadBtnText}>Gallery</Text>
|
|
43
|
+
</TouchableOpacity>
|
|
44
|
+
</View>
|
|
45
|
+
</LinearGradient>
|
|
46
|
+
</View>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
export const ImagePreview = ({ selectedImage, clearImage, analyzeReport, loading }) => (
|
|
50
|
+
<View style={styles.previewSection}>
|
|
51
|
+
<Image source={{ uri: selectedImage.uri }} style={styles.previewImage} resizeMode="cover" />
|
|
52
|
+
<View style={styles.previewActions}>
|
|
53
|
+
<TouchableOpacity style={styles.retakeBtn} onPress={clearImage}>
|
|
54
|
+
<Ionicons name="refresh" size={16} color="#64748B" />
|
|
55
|
+
<Text style={styles.retakeBtnText}>Change</Text>
|
|
56
|
+
</TouchableOpacity>
|
|
57
|
+
<TouchableOpacity style={styles.analyzeBtn} onPress={analyzeReport} disabled={loading}>
|
|
58
|
+
<LinearGradient colors={['#2563EB', '#1D4ED8']} style={styles.analyzeBtnGrad}>
|
|
59
|
+
{loading ? <ActivityIndicator color="#fff" size="small" /> : (
|
|
60
|
+
<>
|
|
61
|
+
<Ionicons name="sparkles" size={18} color="#fff" />
|
|
62
|
+
<Text style={styles.analyzeBtnText}>Analyze with AI</Text>
|
|
63
|
+
</>
|
|
64
|
+
)}
|
|
65
|
+
</LinearGradient>
|
|
66
|
+
</TouchableOpacity>
|
|
67
|
+
</View>
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
export const ResultsPanel = ({ result, fadeAnim, onChatWithAIPress }) => (
|
|
72
|
+
<Animated.View style={{ opacity: fadeAnim }}>
|
|
73
|
+
<View style={styles.card}>
|
|
74
|
+
<Text style={styles.cardTitle}>Health Assessment</Text>
|
|
75
|
+
<ScoreRing score={result.health_score} condition={result.condition} />
|
|
76
|
+
<Text style={styles.summaryText}>{result.summary}</Text>
|
|
77
|
+
</View>
|
|
78
|
+
|
|
79
|
+
{result.see_doctor && (
|
|
80
|
+
<View style={styles.doctorAlert}>
|
|
81
|
+
<Ionicons name="medical" size={18} color="#EF4444" />
|
|
82
|
+
<Text style={styles.doctorAlertText}>Your report may need a doctor's review. Book a consultation.</Text>
|
|
83
|
+
</View>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{result.findings?.length > 0 && (
|
|
87
|
+
<View style={styles.card}>
|
|
88
|
+
<View style={styles.cardHeader}>
|
|
89
|
+
<Ionicons name="analytics-outline" size={18} color="#EF4444" />
|
|
90
|
+
<Text style={styles.cardTitle}>Notable Findings</Text>
|
|
91
|
+
</View>
|
|
92
|
+
{result.findings.map((f, i) => (
|
|
93
|
+
<View key={i} style={styles.findingItem}>
|
|
94
|
+
<View style={styles.findingDot} />
|
|
95
|
+
<Text style={styles.findingText}>{f}</Text>
|
|
96
|
+
</View>
|
|
97
|
+
))}
|
|
98
|
+
</View>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{result.suggestions?.length > 0 && (
|
|
102
|
+
<View style={styles.card}>
|
|
103
|
+
<View style={styles.cardHeader}>
|
|
104
|
+
<Ionicons name="bulb-outline" size={18} color="#10B981" />
|
|
105
|
+
<Text style={styles.cardTitle}>AI Suggestions</Text>
|
|
106
|
+
</View>
|
|
107
|
+
{result.suggestions.map((s, i) => (
|
|
108
|
+
<View key={i} style={styles.suggestionItem}>
|
|
109
|
+
<View style={styles.suggestionNum}><Text style={styles.suggestionNumText}>{i + 1}</Text></View>
|
|
110
|
+
<Text style={styles.suggestionText}>{s}</Text>
|
|
111
|
+
</View>
|
|
112
|
+
))}
|
|
113
|
+
</View>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{onChatWithAIPress && (
|
|
117
|
+
<TouchableOpacity style={styles.chatBtn} onPress={onChatWithAIPress}>
|
|
118
|
+
<LinearGradient colors={['#2563EB', '#1D4ED8']} style={styles.chatBtnGrad}>
|
|
119
|
+
<Ionicons name="chatbubble-ellipses" size={20} color="#fff" />
|
|
120
|
+
<Text style={styles.chatBtnText}>Ask AI about this report</Text>
|
|
121
|
+
</LinearGradient>
|
|
122
|
+
</TouchableOpacity>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
<Text style={styles.disclaimer}>
|
|
126
|
+
⚠ AI analysis is for informational purposes only. Always consult your VORQARD doctor for medical decisions.
|
|
127
|
+
</Text>
|
|
128
|
+
</Animated.View>
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const styles = StyleSheet.create({
|
|
132
|
+
scoreRingWrap: { borderRadius: 16, padding: 20, alignItems: 'center', marginBottom: 14, marginTop: 4 },
|
|
133
|
+
scoreRingOuter: { width: 110, height: 110, borderRadius: 55, borderWidth: 5, justifyContent: 'center', alignItems: 'center', marginBottom: 12 },
|
|
134
|
+
scoreRingInner: { alignItems: 'center' },
|
|
135
|
+
scoreNumber: { fontSize: 34, fontWeight: 'bold' },
|
|
136
|
+
scoreLabel: { fontSize: 12, color: '#94A3B8', marginTop: -2 },
|
|
137
|
+
conditionBadge: { paddingHorizontal: 16, paddingVertical: 6, borderRadius: 20, fontSize: 13, fontWeight: 'bold', borderWidth: 1 },
|
|
138
|
+
uploadArea: { marginBottom: 20 },
|
|
139
|
+
uploadBox: { borderRadius: 20, padding: 30, alignItems: 'center', borderWidth: 2, borderColor: '#BFDBFE', borderStyle: 'dashed' },
|
|
140
|
+
uploadTitle: { fontSize: 18, fontWeight: 'bold', color: '#1E293B', marginTop: 16, marginBottom: 6 },
|
|
141
|
+
uploadSub: { fontSize: 13, color: '#64748B', textAlign: 'center', marginBottom: 20 },
|
|
142
|
+
uploadBtns: { flexDirection: 'row', gap: 12 },
|
|
143
|
+
uploadBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, backgroundColor: '#2563EB', paddingHorizontal: 20, paddingVertical: 12, borderRadius: 12 },
|
|
144
|
+
uploadBtnText: { color: '#fff', fontWeight: 'bold', fontSize: 14 },
|
|
145
|
+
previewSection: { marginBottom: 16 },
|
|
146
|
+
previewImage: { width: '100%', height: 220, borderRadius: 16, marginBottom: 12 },
|
|
147
|
+
previewActions: { flexDirection: 'row', gap: 10 },
|
|
148
|
+
retakeBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, borderWidth: 1, borderColor: '#E2E8F0', borderRadius: 12, paddingHorizontal: 16, paddingVertical: 12, backgroundColor: '#fff' },
|
|
149
|
+
retakeBtnText: { color: '#64748B', fontWeight: '600', fontSize: 14 },
|
|
150
|
+
analyzeBtn: { flex: 1, borderRadius: 12, overflow: 'hidden' },
|
|
151
|
+
analyzeBtnGrad: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, paddingVertical: 14 },
|
|
152
|
+
analyzeBtnText: { color: '#fff', fontWeight: 'bold', fontSize: 15 },
|
|
153
|
+
card: { backgroundColor: '#fff', borderRadius: 16, padding: 20, marginBottom: 12, elevation: 2 },
|
|
154
|
+
cardHeader: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 14 },
|
|
155
|
+
cardTitle: { fontSize: 15, fontWeight: 'bold', color: '#1E293B' },
|
|
156
|
+
summaryText: { fontSize: 14, color: '#475569', lineHeight: 22, textAlign: 'center' },
|
|
157
|
+
doctorAlert: { flexDirection: 'row', alignItems: 'center', gap: 10, backgroundColor: '#FEF2F2', borderRadius: 12, padding: 14, borderWidth: 1, borderColor: '#FCA5A5', marginBottom: 12 },
|
|
158
|
+
doctorAlertText: { flex: 1, fontSize: 13, color: '#DC2626', fontWeight: '500' },
|
|
159
|
+
findingItem: { flexDirection: 'row', alignItems: 'flex-start', gap: 10, marginBottom: 10 },
|
|
160
|
+
findingDot: { width: 8, height: 8, borderRadius: 4, backgroundColor: '#EF4444', marginTop: 6 },
|
|
161
|
+
findingText: { flex: 1, fontSize: 14, color: '#475569', lineHeight: 20 },
|
|
162
|
+
suggestionItem: { flexDirection: 'row', alignItems: 'flex-start', gap: 12, marginBottom: 12 },
|
|
163
|
+
suggestionNum: { width: 26, height: 26, borderRadius: 13, backgroundColor: '#DCFCE7', justifyContent: 'center', alignItems: 'center' },
|
|
164
|
+
suggestionNumText: { fontSize: 12, fontWeight: 'bold', color: '#10B981' },
|
|
165
|
+
suggestionText: { flex: 1, fontSize: 14, color: '#475569', lineHeight: 20 },
|
|
166
|
+
chatBtn: { borderRadius: 14, overflow: 'hidden', marginBottom: 16 },
|
|
167
|
+
chatBtnGrad: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10, paddingVertical: 15 },
|
|
168
|
+
chatBtnText: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
|
|
169
|
+
disclaimer: { fontSize: 11, color: '#94A3B8', textAlign: 'center', lineHeight: 16, paddingHorizontal: 10 },
|
|
170
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity, Image } from 'react-native';
|
|
3
|
+
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
4
|
+
import { styles } from '../ChatStyles';
|
|
5
|
+
import { getAbsoluteProfilePicture, getFormattedDoctorName } from '../ChatUtils';
|
|
6
|
+
|
|
7
|
+
// 7. Booking Confirmation
|
|
8
|
+
export const BookingConfirmationCard = ({ ui, onOptionPress }) => {
|
|
9
|
+
const [booked, setBooked] = useState(false);
|
|
10
|
+
const { doctor, date, slot, options } = ui;
|
|
11
|
+
return (
|
|
12
|
+
<View style={styles.confirmCard}>
|
|
13
|
+
<Text style={styles.confirmHeading}>Confirm Your Appointment</Text>
|
|
14
|
+
<View style={styles.confirmDocRow}>
|
|
15
|
+
{(() => {
|
|
16
|
+
const picUri = getAbsoluteProfilePicture(doctor.profile_picture);
|
|
17
|
+
return picUri ? (
|
|
18
|
+
<Image source={{ uri: picUri }} style={styles.confirmDocAvatar} />
|
|
19
|
+
) : (
|
|
20
|
+
<View style={[styles.confirmDocAvatar, { backgroundColor: '#FFFFFF', borderWidth: 1, borderColor: '#E2E8F0', justifyContent: 'center', alignItems: 'center' }]}>
|
|
21
|
+
<Ionicons name="person" size={14} color="#CBD5E1" />
|
|
22
|
+
</View>
|
|
23
|
+
);
|
|
24
|
+
})()}
|
|
25
|
+
<View style={{ flex: 1, marginLeft: 12 }}>
|
|
26
|
+
<Text style={styles.confirmDocName}>{getFormattedDoctorName(doctor.name)}</Text>
|
|
27
|
+
<Text style={styles.confirmDocSpecialty}>{doctor.specialty}</Text>
|
|
28
|
+
</View>
|
|
29
|
+
</View>
|
|
30
|
+
<View style={styles.confirmDetailsBox}>
|
|
31
|
+
<View style={styles.confirmDetailItem}>
|
|
32
|
+
<Ionicons name="calendar-outline" size={14} color="#4F46E5" />
|
|
33
|
+
<Text style={styles.confirmDetailText}>{date}</Text>
|
|
34
|
+
</View>
|
|
35
|
+
<View style={styles.confirmDetailItem}>
|
|
36
|
+
<Ionicons name="time-outline" size={14} color="#4F46E5" />
|
|
37
|
+
<Text style={styles.confirmDetailText}>{slot}</Text>
|
|
38
|
+
</View>
|
|
39
|
+
</View>
|
|
40
|
+
<View style={styles.confirmActionRow}>
|
|
41
|
+
{booked ? (
|
|
42
|
+
<View style={styles.bookedBanner}>
|
|
43
|
+
<Ionicons name="checkmark-circle" size={18} color="#10B981" />
|
|
44
|
+
<Text style={styles.bookedBannerText}>Appointment Confirmed!</Text>
|
|
45
|
+
</View>
|
|
46
|
+
) : (
|
|
47
|
+
options.map((opt) => {
|
|
48
|
+
const isConfirm = opt.id === 'confirm';
|
|
49
|
+
return (
|
|
50
|
+
<TouchableOpacity
|
|
51
|
+
key={opt.id}
|
|
52
|
+
style={[styles.confirmBtn, isConfirm ? styles.confirmBtnOk : styles.confirmBtnCancel]}
|
|
53
|
+
onPress={() => {
|
|
54
|
+
if (isConfirm) setBooked(true);
|
|
55
|
+
onOptionPress?.(opt);
|
|
56
|
+
}}
|
|
57
|
+
activeOpacity={0.8}
|
|
58
|
+
>
|
|
59
|
+
<Text style={[styles.confirmBtnText, { color: isConfirm ? '#FFF' : '#EF4444' }]}>
|
|
60
|
+
{opt.label}
|
|
61
|
+
</Text>
|
|
62
|
+
</TouchableOpacity>
|
|
63
|
+
);
|
|
64
|
+
})
|
|
65
|
+
)}
|
|
66
|
+
</View>
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity, Image } from 'react-native';
|
|
3
|
+
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
4
|
+
import { styles } from '../ChatStyles';
|
|
5
|
+
import { getAbsoluteProfilePicture, getFormattedDoctorName } from '../ChatUtils';
|
|
6
|
+
|
|
7
|
+
// 4. Doctor Selection List
|
|
8
|
+
export const DoctorSelectionList = ({ options, onOptionPress }) => {
|
|
9
|
+
const [expanded, setExpanded] = useState(false);
|
|
10
|
+
const limit = 7;
|
|
11
|
+
const showViewAll = options.length > limit;
|
|
12
|
+
const visibleOptions = expanded ? options : options.slice(0, limit);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<View style={styles.doctorListContainer}>
|
|
16
|
+
{visibleOptions.map((opt) => (
|
|
17
|
+
<TouchableOpacity
|
|
18
|
+
key={opt.id}
|
|
19
|
+
style={styles.doctorCard}
|
|
20
|
+
onPress={() => onOptionPress?.(opt)}
|
|
21
|
+
activeOpacity={0.8}
|
|
22
|
+
>
|
|
23
|
+
{(() => {
|
|
24
|
+
const picUri = getAbsoluteProfilePicture(opt.profile_picture);
|
|
25
|
+
return picUri ? (
|
|
26
|
+
<Image
|
|
27
|
+
source={{ uri: picUri }}
|
|
28
|
+
style={styles.doctorAvatar}
|
|
29
|
+
/>
|
|
30
|
+
) : (
|
|
31
|
+
<View style={[styles.doctorAvatar, { backgroundColor: '#FFFFFF', borderWidth: 1, borderColor: '#E2E8F0', justifyContent: 'center', alignItems: 'center' }]}>
|
|
32
|
+
<Ionicons name="person" size={20} color="#CBD5E1" />
|
|
33
|
+
</View>
|
|
34
|
+
);
|
|
35
|
+
})()}
|
|
36
|
+
<View style={styles.doctorDetails}>
|
|
37
|
+
<Text style={styles.doctorName} numberOfLines={1}>{getFormattedDoctorName(opt.name)}</Text>
|
|
38
|
+
<Text style={styles.doctorSpecialty} numberOfLines={1}>{opt.specialty}</Text>
|
|
39
|
+
<Text style={styles.doctorExp}>{opt.experience ? `${opt.experience} Experience` : "No experience listed"}</Text>
|
|
40
|
+
</View>
|
|
41
|
+
<View style={styles.doctorRatingCol}>
|
|
42
|
+
<View style={styles.doctorRatingRow}>
|
|
43
|
+
<Ionicons name="star" size={10} color="#F59E0B" />
|
|
44
|
+
<Text style={styles.doctorRatingValue}>{opt.rating || "0.0"}</Text>
|
|
45
|
+
</View>
|
|
46
|
+
<Text style={styles.doctorReviewsCount}>({opt.reviews || "0"})</Text>
|
|
47
|
+
</View>
|
|
48
|
+
<Ionicons name="chevron-forward" size={16} color="#94A3B8" style={{ marginLeft: 4 }} />
|
|
49
|
+
</TouchableOpacity>
|
|
50
|
+
))}
|
|
51
|
+
|
|
52
|
+
{showViewAll && !expanded && (
|
|
53
|
+
<TouchableOpacity
|
|
54
|
+
style={styles.viewAllButton}
|
|
55
|
+
onPress={() => setExpanded(true)}
|
|
56
|
+
activeOpacity={0.7}
|
|
57
|
+
>
|
|
58
|
+
<Text style={styles.viewAllText}>View all {options.length} doctors</Text>
|
|
59
|
+
<Ionicons name="chevron-forward" size={12} color="#6366F1" />
|
|
60
|
+
</TouchableOpacity>
|
|
61
|
+
)}
|
|
62
|
+
</View>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// 5. Doctor Profile Card
|
|
67
|
+
export const DoctorProfileCard = ({ doctor, options, onOptionPress }) => {
|
|
68
|
+
return (
|
|
69
|
+
<View style={styles.profileCard}>
|
|
70
|
+
<View style={styles.profileHeader}>
|
|
71
|
+
{(() => {
|
|
72
|
+
const picUri = getAbsoluteProfilePicture(doctor.profile_picture);
|
|
73
|
+
return picUri ? (
|
|
74
|
+
<Image
|
|
75
|
+
source={{ uri: picUri }}
|
|
76
|
+
style={styles.profileAvatar}
|
|
77
|
+
/>
|
|
78
|
+
) : (
|
|
79
|
+
<View style={[styles.profileAvatar, { backgroundColor: '#FFFFFF', borderWidth: 1, borderColor: '#E2E8F0', justifyContent: 'center', alignItems: 'center' }]}>
|
|
80
|
+
<Ionicons name="person" size={30} color="#CBD5E1" />
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
})()}
|
|
84
|
+
<Text style={styles.profileName}>{getFormattedDoctorName(doctor.name)}</Text>
|
|
85
|
+
<Text style={styles.profileTitleSub}>
|
|
86
|
+
{doctor.qualification} • {doctor.specialty}
|
|
87
|
+
</Text>
|
|
88
|
+
<Text style={styles.profileExperienceText}>
|
|
89
|
+
{doctor.experience ? `${doctor.experience} Experience` : "No experience listed"}
|
|
90
|
+
</Text>
|
|
91
|
+
</View>
|
|
92
|
+
|
|
93
|
+
<View style={styles.badgeRow}>
|
|
94
|
+
<View style={styles.badgeItem}>
|
|
95
|
+
<Ionicons name="star" size={12} color="#F59E0B" />
|
|
96
|
+
<Text style={styles.badgeVal}>{doctor.rating || "0.0"}</Text>
|
|
97
|
+
<Text style={styles.badgeLabel}>({doctor.reviews || "0"} reviews)</Text>
|
|
98
|
+
</View>
|
|
99
|
+
<View style={styles.badgeDivider} />
|
|
100
|
+
<View style={styles.badgeItem}>
|
|
101
|
+
<Ionicons name="wallet-outline" size={12} color="#10B981" />
|
|
102
|
+
<Text style={styles.badgeVal}>Rs. {doctor.consultation_fee}</Text>
|
|
103
|
+
<Text style={styles.badgeLabel}>Consult Fee</Text>
|
|
104
|
+
</View>
|
|
105
|
+
<View style={styles.badgeDivider} />
|
|
106
|
+
<View style={styles.badgeItem}>
|
|
107
|
+
<Ionicons name="people-outline" size={12} color="#6366F1" />
|
|
108
|
+
<Text style={styles.badgeVal}>{doctor.patients || "0"}</Text>
|
|
109
|
+
<Text style={styles.badgeLabel}>Patients</Text>
|
|
110
|
+
</View>
|
|
111
|
+
</View>
|
|
112
|
+
|
|
113
|
+
<View style={styles.aboutSection}>
|
|
114
|
+
<Text style={styles.aboutTitle}>About Doctor</Text>
|
|
115
|
+
<Text style={styles.aboutBody} numberOfLines={4}>
|
|
116
|
+
{doctor.bio || "Highly experienced practitioner committed to delivering high-quality healthcare and compassionate treatment for patients."}
|
|
117
|
+
</Text>
|
|
118
|
+
</View>
|
|
119
|
+
|
|
120
|
+
<View style={styles.hospitalSection}>
|
|
121
|
+
<Text style={styles.aboutTitle}>Available At</Text>
|
|
122
|
+
<View style={styles.profileHospitalCard}>
|
|
123
|
+
<View style={styles.hospitalIconCircle}>
|
|
124
|
+
<Ionicons name="business" size={16} color="#4F46E5" />
|
|
125
|
+
</View>
|
|
126
|
+
<View style={{ flex: 1, marginLeft: 10 }}>
|
|
127
|
+
<Text style={styles.profileHospitalName}>{doctor.hospital_name || "Independent Practice"}</Text>
|
|
128
|
+
<Text style={styles.profileHospitalAddress}>In-Clinic / Video Consultation</Text>
|
|
129
|
+
</View>
|
|
130
|
+
</View>
|
|
131
|
+
</View>
|
|
132
|
+
|
|
133
|
+
<View style={styles.profileActionRow}>
|
|
134
|
+
{options.map((opt) => {
|
|
135
|
+
const isYes = opt.id === 'yes' || opt.id === 'confirm';
|
|
136
|
+
return (
|
|
137
|
+
<TouchableOpacity
|
|
138
|
+
key={opt.id}
|
|
139
|
+
style={[styles.profileBtn, isYes ? styles.profileBtnConfirm : styles.profileBtnCancel]}
|
|
140
|
+
onPress={() => onOptionPress?.(opt)}
|
|
141
|
+
activeOpacity={0.8}
|
|
142
|
+
>
|
|
143
|
+
<Text style={[styles.profileBtnText, { color: isYes ? '#FFF' : '#EF4444' }]}>
|
|
144
|
+
{opt.label}
|
|
145
|
+
</Text>
|
|
146
|
+
</TouchableOpacity>
|
|
147
|
+
);
|
|
148
|
+
})}
|
|
149
|
+
</View>
|
|
150
|
+
</View>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity } from 'react-native';
|
|
3
|
+
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
4
|
+
import { styles } from '../ChatStyles';
|
|
5
|
+
|
|
6
|
+
// 1. Doctor Type Selection (Side-by-side cards)
|
|
7
|
+
export const DoctorTypeSelectionCard = ({ options, onOptionPress }) => {
|
|
8
|
+
return (
|
|
9
|
+
<View style={styles.typeSelectionGrid}>
|
|
10
|
+
{options.map((opt) => {
|
|
11
|
+
const isHospital = opt.id === 'hospital';
|
|
12
|
+
return (
|
|
13
|
+
<TouchableOpacity
|
|
14
|
+
key={opt.id}
|
|
15
|
+
style={[styles.typeCard, isHospital ? styles.typeCardHospital : styles.typeCardIndividual]}
|
|
16
|
+
onPress={() => onOptionPress?.(opt)}
|
|
17
|
+
activeOpacity={0.8}
|
|
18
|
+
>
|
|
19
|
+
<View style={[styles.typeIconContainer, { backgroundColor: isHospital ? '#EFF6FF' : '#ECFDF5' }]}>
|
|
20
|
+
<Ionicons
|
|
21
|
+
name={isHospital ? "business" : "person"}
|
|
22
|
+
size={28}
|
|
23
|
+
color={isHospital ? "#3B82F6" : "#10B981"}
|
|
24
|
+
/>
|
|
25
|
+
</View>
|
|
26
|
+
<Text style={[styles.typeTitle, { color: isHospital ? "#1E3A8A" : "#065F46" }]}>
|
|
27
|
+
{isHospital ? "Hospital Doctors" : "Individual Doctors"}
|
|
28
|
+
</Text>
|
|
29
|
+
<Text style={styles.typeSub}>
|
|
30
|
+
{isHospital ? "Consult with doctors from top hospitals" : "Consult with individual doctors"}
|
|
31
|
+
</Text>
|
|
32
|
+
<View style={[styles.typeArrowCircle, { backgroundColor: isHospital ? '#3B82F6' : '#10B981' }]}>
|
|
33
|
+
<Ionicons name="arrow-forward" size={12} color="#FFF" />
|
|
34
|
+
</View>
|
|
35
|
+
</TouchableOpacity>
|
|
36
|
+
);
|
|
37
|
+
})}
|
|
38
|
+
</View>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity, Image, Linking, Platform } from 'react-native';
|
|
3
|
+
import Ionicons from 'react-native-vector-icons/Ionicons';
|
|
4
|
+
import { styles } from '../ChatStyles';
|
|
5
|
+
import { getAbsoluteProfilePicture } from '../ChatUtils';
|
|
6
|
+
|
|
7
|
+
// 2. Hospital Selection List
|
|
8
|
+
export const HospitalSelectionList = ({ options, onOptionPress }) => {
|
|
9
|
+
return (
|
|
10
|
+
<View style={styles.hospitalListContainer}>
|
|
11
|
+
{options.map((opt) => {
|
|
12
|
+
const logoUri = getAbsoluteProfilePicture(opt.logo_url);
|
|
13
|
+
return (
|
|
14
|
+
<TouchableOpacity
|
|
15
|
+
key={opt.id}
|
|
16
|
+
style={styles.hospitalCard}
|
|
17
|
+
onPress={() => onOptionPress?.(opt)}
|
|
18
|
+
activeOpacity={0.8}
|
|
19
|
+
>
|
|
20
|
+
<View style={styles.hospitalLogoBox}>
|
|
21
|
+
{logoUri ? (
|
|
22
|
+
<Image source={{ uri: logoUri }} style={styles.hospitalLogoImage} resizeMode="cover" />
|
|
23
|
+
) : (
|
|
24
|
+
<Ionicons name="business" size={20} color="#3B82F6" />
|
|
25
|
+
)}
|
|
26
|
+
</View>
|
|
27
|
+
<View style={styles.hospitalDetails}>
|
|
28
|
+
<Text style={styles.hospitalName} numberOfLines={1}>{opt.label}</Text>
|
|
29
|
+
<Text style={styles.hospitalAddress} numberOfLines={1}>{opt.address || 'India'}</Text>
|
|
30
|
+
{opt.rating != null && (
|
|
31
|
+
<View style={styles.hospitalRatingRow}>
|
|
32
|
+
<Ionicons name="star" size={11} color="#F59E0B" />
|
|
33
|
+
<Text style={styles.hospitalRatingText}>
|
|
34
|
+
{opt.rating} ({opt.reviews || 0} reviews)
|
|
35
|
+
</Text>
|
|
36
|
+
</View>
|
|
37
|
+
)}
|
|
38
|
+
</View>
|
|
39
|
+
<Ionicons name="chevron-forward" size={18} color="#6366F1" style={styles.chevronRight} />
|
|
40
|
+
</TouchableOpacity>
|
|
41
|
+
);
|
|
42
|
+
})}
|
|
43
|
+
</View>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// 3. Hospital Profile Card
|
|
48
|
+
export const HospitalProfileCard = ({ hospital, options, onOptionPress }) => {
|
|
49
|
+
return (
|
|
50
|
+
<View style={styles.profileCard}>
|
|
51
|
+
<View style={styles.profileHeader}>
|
|
52
|
+
{(() => {
|
|
53
|
+
const picUri = getAbsoluteProfilePicture(hospital.logo_url);
|
|
54
|
+
return picUri ? (
|
|
55
|
+
<Image
|
|
56
|
+
source={{ uri: picUri }}
|
|
57
|
+
style={styles.profileAvatar}
|
|
58
|
+
resizeMode="cover"
|
|
59
|
+
/>
|
|
60
|
+
) : (
|
|
61
|
+
<View style={[styles.profileAvatar, { backgroundColor: '#EFF6FF', borderWidth: 1, borderColor: '#BFDBFE', justifyContent: 'center', alignItems: 'center' }]}>
|
|
62
|
+
<Ionicons name="business" size={30} color="#3B82F6" />
|
|
63
|
+
</View>
|
|
64
|
+
);
|
|
65
|
+
})()}
|
|
66
|
+
<View style={styles.profileHeaderRight}>
|
|
67
|
+
<Text style={styles.profileName} numberOfLines={1}>{hospital.name}</Text>
|
|
68
|
+
<Text style={styles.profileSpecialty} numberOfLines={2}>{hospital.address || 'India'}</Text>
|
|
69
|
+
{hospital.rating != null && (
|
|
70
|
+
<View style={styles.profileRatingBadge}>
|
|
71
|
+
<Ionicons name="star" size={11} color="#FFF" />
|
|
72
|
+
<Text style={styles.profileRatingText}>{hospital.rating}</Text>
|
|
73
|
+
<Text style={styles.profileReviewText}>({hospital.reviews || 0})</Text>
|
|
74
|
+
</View>
|
|
75
|
+
)}
|
|
76
|
+
</View>
|
|
77
|
+
</View>
|
|
78
|
+
|
|
79
|
+
{/* Hospital Extra Info */}
|
|
80
|
+
<View style={{ marginBottom: 12, gap: 4 }}>
|
|
81
|
+
{hospital.phone && (
|
|
82
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
|
83
|
+
<Ionicons name="call" size={12} color="#64748B" />
|
|
84
|
+
<Text style={{ fontSize: 11, color: '#64748B' }}>{hospital.phone}</Text>
|
|
85
|
+
</View>
|
|
86
|
+
)}
|
|
87
|
+
{hospital.email && (
|
|
88
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
|
89
|
+
<Ionicons name="mail" size={12} color="#64748B" />
|
|
90
|
+
<Text style={{ fontSize: 11, color: '#64748B' }}>{hospital.email}</Text>
|
|
91
|
+
</View>
|
|
92
|
+
)}
|
|
93
|
+
{hospital.website && (
|
|
94
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
|
95
|
+
<Ionicons name="globe" size={12} color="#64748B" />
|
|
96
|
+
<Text style={{ fontSize: 11, color: '#64748B' }}>{hospital.website}</Text>
|
|
97
|
+
</View>
|
|
98
|
+
)}
|
|
99
|
+
</View>
|
|
100
|
+
|
|
101
|
+
<View style={styles.profileStatsRow}>
|
|
102
|
+
<TouchableOpacity
|
|
103
|
+
style={[styles.profileStatBox, { flex: 1, backgroundColor: '#EFF6FF' }]}
|
|
104
|
+
onPress={() => {
|
|
105
|
+
const address = hospital.address || hospital.name;
|
|
106
|
+
const url = Platform.OS === 'ios'
|
|
107
|
+
? `http://maps.apple.com/?daddr=${encodeURIComponent(address)}&dirflg=d`
|
|
108
|
+
: `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(address)}`;
|
|
109
|
+
Linking.openURL(url);
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<Ionicons name="location" size={20} color="#3B82F6" />
|
|
113
|
+
<Text style={[styles.profileStatLabel, { color: '#3B82F6' }]}>Get Directions</Text>
|
|
114
|
+
</TouchableOpacity>
|
|
115
|
+
</View>
|
|
116
|
+
|
|
117
|
+
<View style={styles.profileActionRow}>
|
|
118
|
+
{options.map((opt) => {
|
|
119
|
+
const isConfirm = opt.id === 'view_doctors';
|
|
120
|
+
return (
|
|
121
|
+
<TouchableOpacity
|
|
122
|
+
key={opt.id}
|
|
123
|
+
style={[styles.profileBtn, isConfirm ? styles.profileBtnConfirm : styles.profileBtnCancel]}
|
|
124
|
+
onPress={() => onOptionPress?.(opt)}
|
|
125
|
+
activeOpacity={0.8}
|
|
126
|
+
>
|
|
127
|
+
<Text style={[styles.profileBtnText, { color: isConfirm ? '#FFF' : '#EF4444' }]}>
|
|
128
|
+
{opt.label}
|
|
129
|
+
</Text>
|
|
130
|
+
</TouchableOpacity>
|
|
131
|
+
);
|
|
132
|
+
})}
|
|
133
|
+
</View>
|
|
134
|
+
</View>
|
|
135
|
+
);
|
|
136
|
+
};
|