👤 Therapist Profile Management
This document covers the comprehensive profile management system for therapists in the Com DEALL mobile application, including profile setup, document management, earnings tracking, and professional details.
🎯 Overview
The therapist profile management system enables therapists to:
- Manage professional information and credentials
- Upload and verify documents (degree, license, identity proofs)
- Track earnings and manage wallet operations
- Configure preferences and notification settings
- Complete onboarding process
🏗️ Architecture
System Components
Profile Management System
├── Profile Information (Basic details, specializations, experience)
├── Document Management (Upload, verification, status tracking)
├── Earnings Dashboard (Revenue tracking, transaction history)
├── Wallet Management (Balance, withdrawals, bank details)
├── Professional Details (Qualifications, languages, availability)
└── Settings & Preferences (Notifications, privacy, language)
Data Flow
User Input → Validation → Processing → Storage → Verification → UI Update
↓ ↓ ↓ ↓ ↓ ↓
Forms → Business Rules → API Call → Database → Admin Review → Real-time Sync
📱 Key Features
1. Profile Information Management
Screens: TherapistProfile.tsx, CompleteTherapistProfile.tsx
Features:
- Basic information (name, email, phone, date of birth, gender)
- Professional details (degree, experience years, specializations)
- Location details (address, city, state, country, pincode)
- Consultation fees configuration
- Profile picture upload with image optimization
- Preferred languages selection (multi-select)
Data Fields:
interface TherapistProfile {
user_id: string;
consultation_fees: number;
experience: number; // Overall experience in years
experience_comdeall: number; // Experience with Com DEALL platform
gender: 'MALE' | 'FEMALE' | 'OTHER';
date_of_birth: string;
degree_name: string;
degree_completion: string;
address: string;
city: string;
state: string;
country: string;
pincode: string;
aadhaar_card: string;
pan_card: string;
payment_share: number; // Default: 25% platform commission
preferred_languages: string[];
speciality: string[];
tranning: boolean;
}
GraphQL Implementation:
// Profile Query
const {
data: profileData,
loading: profileLoading,
error: profileError,
refetch: refetchProfile,
} = useGetTherapistProfileQuery({
variables: {
therapist_id: therapistId,
},
fetchPolicy: 'cache-and-network',
});
// Therapist Specialities Query
const { data: therapist_specialities, loading: specialitiesLoading } =
useGetTherapistSpecialitiesByIdQuery({
variables: { id: profileData?.therapist_specialities[0]?.id },
fetchPolicy: 'cache-and-network',
skip: !profileData?.therapist_specialities[0]?.id,
});
// Profile Image Query
const {
data: profileImage,
refetch,
loading: profileImageLoading,
} = useGetMediaPathQuery({
variables: { id: profileData?.therapist[0]?.user?.profile_img },
skip: !profileData?.therapist[0]?.user?.profile_img,
});
// FAQ/Content Query
const {
data: contentData,
loading: contentLoading,
error: contentError,
refetch: refetchContent,
} = useGetContentQuery({
variables: { title: "FAQ's" },
fetchPolicy: 'cache-and-network',
});
Profile Image Upload:
// Profile Picture Upload Mutation
const [setProfileImage, { loading: imageSetting }] =
useUploadProfilePictureMutation({
errorPolicy: 'all',
onError: imageErr => {
console.warn(imageErr?.message);
},
});
// Upload profile image function
const uploadProfileImage = async (value: Asset | undefined) => {
const token = mmkvStorage.get('accessToken');
const formData = new FormData();
if (value?.uri) {
formData.append('files', {
name: 'image',
uri: value?.uri,
type: value?.type,
});
}
try {
let response;
if (formData.getParts().length > 0) {
setFileUploading(true);
try {
// Upload to media server
response = await axios.post(
process.env.EXPO_PUBLIC_MEDIA_UPLOAD_PUBLIC!,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${token}`,
},
},
);
} catch (e) {
console.log(e);
}
setFileUploading(false);
// Update profile with new image ID
setProfileImage({
variables: {
userId: profileData?.therapist[0]?.user_id,
profileImg: response?.data?.data[0]?.id,
},
});
}
} catch {
toast.show({
text: tError('error.error'),
});
}
setFileUploading(false);
};
Logout Implementation:
const [modalVisible, setModalVisible] = useState(false);
// Logout API call
const logout = async () => {
const url = process.env.EXPO_PUBLIC_API_URL!;
const AUTH_API = url + '/api/auth';
const SIGNOUT_URL = AUTH_API + '/signout/user';
await fetch(SIGNOUT_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${mmkvStorage.get('accessToken')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
device_token: mmkvStorage.get('fcmToken'),
refresh_token: mmkvStorage.get('refreshToken'),
}),
});
};
// Logout modal with confirmation
<LogoutModal
modalVisible={modalVisible}
setModalVisible={setModalVisible}
title={t('logout-modal.title')}
primaryCtaText={t('logout-modal.primaryCta')}
secondaryCtaText={t('logout-modal.secondaryCta')}
onPress={async () => {
await logout();
dispatch(setSignOut());
i18n.changeLanguage(LanguageCodes.English);
setActiveChildId(undefined);
setModalVisible(false);
}}
image={<LOGOUT_ILLUSTRATION />}
/>
Implementation Notes:
- Profile picture upload uses multipart/form-data to
EXPO_PUBLIC_MEDIA_UPLOAD_PUBLIC - Image compression before upload to optimize bandwidth
- Real-time validation using Yup schema
- Form state management with Formik
- Cache-and-network fetch policy for latest data
2. Document Management
Screen: TherapistDocuments.tsx
Document Types:
- Degree Certificate (Required) - Educational qualification proof
- Professional License (Required) - Practice license/certification
- Aadhaar Card (Required) - Identity proof
- PAN Card (Required) - Tax identification
- Experience Certificate (Optional) - Previous work experience
Document Status Workflow:
Pending → Under Review → Approved / Rejected
↓ ↓ ↓ ↓
Upload → Admin Review → Verified → Needs Reupload
Implementation:
- Document picker for selecting files from device
- File type validation (PDF, JPG, PNG)
- File size validation (max 5MB per document)
- Upload progress tracking
- Document status badges (Pending, Verified, Rejected)
- Reupload capability for rejected documents
GraphQL Mutations:
// Update therapist profile mutation
const [updateTherapistProfile, { loading }] =
useUpdateTherapistProfileMutation({
errorPolicy: 'all',
onError: () => {
toast.show({
text: tError('error.error'),
});
},
onCompleted: response => {
if (response?.updateTherapist?.success) {
toast.show({ text: t('documents-tab.success') });
navigation.goBack();
} else {
toast.show({ text: response?.updateTherapist?.message });
}
},
});
// Add therapist media mutation
const [addTherapistMedia, { loading: mediaLoading }] =
useAddTherapistMediaMutation({
errorPolicy: 'all',
onError: error => {
toast.show({
text: tError('error.error'),
});
},
onCompleted: response => {
if (response?.insert_therapist_media?.returning?.length > 0) {
toast.show({ text: t('documents-tab.success') });
navigation.goBack();
}
},
});
Document Picker Setup:
import DocumentPicker, {
DocumentPickerResponse,
isCancel,
isInProgress,
types,
} from 'react-native-document-picker';
// State management for documents
const [itemOne, setItemOne] = useState
DocumentPickerResponse | undefined | null
>();
const [itemTwo, setItemTwo] = useState
DocumentPickerResponse | undefined | null
>();
const [itemThree, setItemThree] = useState
DocumentPickerResponse | undefined | null
>();
const [itemFour, setItemFour] = useState
DocumentPickerResponse | undefined | null
>();
const [moreItems, setMoreItems] = useState<DocumentPickerResponse[]>([]);
// Error handling for document picker
const handleError = (err: unknown) => {
if (isCancel(err)) {
console.warn('cancelled');
// User cancelled the picker, exit any dialogs or menus and move on
} else if (isInProgress(err)) {
console.warn(
'multiple pickers were opened, only the last will be considered',
);
} else {
throw err;
}
};
// Document picker function
const handlePicker = async () => {
try {
const pickerResult = await DocumentPicker.pickSingle({
presentationStyle: 'fullScreen',
copyTo: 'cachesDirectory',
type: [types.pdf, types.images], // Only PDF and images allowed
});
return pickerResult;
} catch (e) {
handleError(e);
}
};
Document Upload Implementation:
// Upload documents to media server
const uploadDocumentsToMedia = async (
documents: (DocumentPickerResponse | null | undefined)[],
): Promise<string[]> => {
// Filter out null/undefined documents
const validDocuments = documents.filter(
doc => doc && doc.uri,
) as DocumentPickerResponse[];
if (validDocuments.length === 0) {
return [];
}
const token = mmkvStorage.get('accessToken');
const formData = new FormData();
// Append all documents to FormData
validDocuments.forEach((doc, index) => {
formData.append('files', {
name: doc.name || `document-${index}`,
uri: doc.uri,
type: doc.type,
});
});
try {
// Upload to private media endpoint
const response = await axios.post(
process.env.EXPO_PUBLIC_MEDIA_UPLOAD_PRIVATE!,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${token}`,
},
},
);
if (response.data.data) {
// Return array of media IDs
return response.data.data.map(file => file.id);
} else {
throw new Error(response.data.message);
}
} catch (error) {
console.error('Error details:', {
message: error?.message,
response: error?.response?.data,
status: error?.response?.status,
});
// Handle case where upload succeeded but returned in error response
if (error.response?.data?.success && error.response?.data?.data) {
return error.response.data.data.map(file => file.id);
}
if (error.response?.data) {
throw new Error(error.response.data.message);
}
throw error;
}
};
Document Validation:
// File type validation (handled by DocumentPicker)
type: [types.pdf, types.images] // Only PDF and images
// File requirements
const documentRequirements = {
formats: ['PDF', 'JPG', 'JPEG', 'PNG'],
maxSize: '5MB',
minResolution: '300 DPI',
};
// Count validation
const MAX_FIXED_DOCUMENTS = 4;
const MAX_ADDITIONAL_DOCUMENTS = 3;
const MAX_TOTAL_DOCUMENTS = MAX_FIXED_DOCUMENTS + MAX_ADDITIONAL_DOCUMENTS; // 7
Upload Flow:
1. User taps document slot
↓
2. Document picker opens (PDF/Images only)
↓
3. User selects file from device
↓
4. File copied to cache directory
↓
5. Document displayed in slot with name
↓
6. User can remove document (tap X)
↓
7. On Save:
- Validate at least one document
- Upload all documents to media server
- Receive media IDs
- Create database entries
- Show success/error toast
- Navigate back
Document Upload Specifications
1. Fixed + Dynamic Slots
- 4 fixed document slots for required documents
- Up to 3 additional slots for supplementary documents
- Total maximum: 7 documents
2. File Type Restrictions
- PDF files for official documents
- Image files (
.jpg,.jpeg,.png) for scanned copies - Validation enforced at the picker level
3. Visual Feedback
- Empty slot: Displays placeholder text
- Selected document: Shows filename with a remove button
- Floating label: Appears above filled slots
- Loading indicator: Visible during upload
4. Batch Upload
- All documents are uploaded in a single API call
- Reduces network overhead
- Response: Returns an array of media IDs
5. State Management
- Individual state for the first 4 required documents
- Array-based state for additional (dynamic) documents
- Simplifies add/remove logic
6. Private Storage
- Uses
EXPO_PUBLIC_MEDIA_UPLOAD_PRIVATEendpoint - Documents are not publicly accessible
- Authentication token required for access
3. Earnings Management
Screen: EarningsDashboard.tsx
Metrics Displayed:
- Total Earnings: Cumulative revenue since registration
- This Month: Current month earnings
- Last Month: Previous month earnings
- Pending Amount: Earnings not yet released
- Paid Amount: Successfully transferred earnings
- Average per Session: Total earnings ÷ completed sessions
Earnings Calculation Formula:
Session Earnings = Consultation Fees × (100 - Payment Share) / 100
Example: ₹1000 × (100 - 25) / 100 = ₹750 (therapist receives 75%)
Transaction Details:
- Date and time of appointment
- Child name and session type
- Session duration
- Amount earned
- Payment status (Pending, Processing, Paid)
- Invoice download option
Period Filters:
- Today
- This Week
- This Month
- Last Month
- Custom Date Range
4. Wallet Management
Screen: Wallet.tsx
Wallet Operations:
Balance Display:
- Available Balance (withdrawable amount)
- Pending Balance (not yet released)
- Total Earnings (lifetime)
Withdrawal Process:
- Check minimum withdrawal amount (₹500)
- Verify bank details are added
- Validate available balance
- Submit withdrawal request
- Admin approval (1-3 business days)
- Bank transfer (3-5 business days)
Withdrawal Validation:
// Button disabled when balance is zero or negative
disabled={parseInt(data?.therapist_by_pk?.balance ?? 0) <= 0}
// Validation in WithdrawFromWallet component should check:
// - Minimum withdrawal amount: ₹500
// - Available balance >= withdrawal amount
// - Bank account details added
// - Valid bank_id provided
GraphQL Implementation:
// Wallet Queries
const { data: walletData, loading, error, refetch } = useGetTherapistWalletTransactionsQuery({
fetchPolicy: 'cache-and-network',
variables: {
therapist_id: therapistId,
},
skip: !therapistId,
});
const { data: walletHistory, loading: walletLoading, fetchMore } =
useGetTherapistWalletHistoryQuery({
fetchPolicy: 'cache-and-network',
variables: {
limit: ITEMS_PER_PAGE,
offset,
whereCondition: { therapist_id: { _eq: therapistId } },
},
});
// Bank Account Query
const { data: bankData } = useGetTherapistBankAccountQuery({
fetchPolicy: 'cache-and-network',
variables: {
therapist_id: therapistId,
},
});
Withdrawal Mutation:
// Minimum withdrawal amount
MIN_WITHDRAWAL_AMOUNT = 500
// Validation checks
if (withdrawAmount < MIN_WITHDRAWAL_AMOUNT) {
error = "Minimum withdrawal amount is ₹500"
}
if (withdrawAmount > availableBalance) {
error = "Insufficient balance"
}
if (!bankDetails) {
error = "Please add bank details first"
}
Withdrawal Mutation (Actual):
// Withdrawal mutation with error handling
const [withdrawWallet] = useWithdrawFromWalletMutation({
errorPolicy: 'all',
onCompleted: data => {
if (data?.withdraw?.success) {
setSuccessModalVisible(true);
} else {
setFiledModalVisible(true);
toast.show({ text: data?.withdraw?.message });
}
},
onError: e => {
toast.show({ text: tError('error.error') ?? '' });
},
});
// Withdrawal submission
const handleWithdrawSubmit = (values: any) => {
const variables = {
bank_id: values?.bank?.id,
amount: values?.amount,
};
withdrawWallet({ variables });
handleWithdrawModalClose();
};
Bank Details Required:
- Account holder name
- Account number
- IFSC code
- Bank name
- Branch name
5. Settings & Preferences
Screen: ProfileSettings.tsx
Configuration Options:
Notifications:
- Push notifications (On/Off)
- Email updates (On/Off)
- SMS notifications (On/Off)
- Appointment reminders (On/Off)
- New message alerts (On/Off)
Language & Region:
- Interface language selection
- Timezone configuration
- Date format preference
- Currency display
Privacy Settings:
- Profile visibility (Public/Private)
- Show contact information (Yes/No)
- Allow parent reviews (Yes/No)
Availability Settings:
- Working hours configuration
- Days available for appointments
- Automatic appointment acceptance
- Buffer time between sessions
📱 Mobile-Specific Features
Touch Interactions
Gesture Support:
- Long press on profile image → Change/Remove photo options
- Swipe left on document → Delete/Reupload options
- Pull to refresh → Reload profile data
- Tap on earning item → View transaction details
Offline Capabilities
Offline Profile Updates:
- Profile changes cached locally when offline
- Automatic sync when connection restored
- Conflict resolution for simultaneous updates
- Visual indicator for pending sync
Offline Storage:
// Store offline updates in MMKV
offlineUpdate = {
id: 'offline_' + timestamp,
type: 'profile_update',
data: {...changes},
isOffline: true,
createdAt: timestamp
}
Image Optimization
Profile Picture Processing:
- Automatic compression (max 500KB)
- Resize to standard dimensions (500x500px)
- Format conversion (JPEG with 85% quality)
- Thumbnail generation for list views
🔧 GraphQL Operations
Query Structure
# Profile Query
query GetTherapistProfile($therapist_id: String!) {
therapist(where: {id: {_eq: $therapist_id}}) {
id
user_id
consultation_fees
# ... other fields
user {
name
email
profile_img
}
therapist_specialities {
speciality { title }
}
}
}
# Earnings Query
query GetTherapistEarnings($therapist_id: String!, $period: String!) {
therapist_earnings(
where: {therapist_id: {_eq: $therapist_id}}
order_by: {created_at: desc}
) {
amount
type
status
appointment { date, child { name } }
}
}
Mutation Operations
# Profile Update
mutation UpdateTherapistProfile(
$therapist_id: String!,
$input: therapist_set_input!
) {
update_therapist_by_pk(
pk_columns: {id: $therapist_id},
_set: $input
) {
id
}
}
# Document Upload
mutation UploadDocument(
$therapist_id: String!,
$document_type: String!,
$document_url: String!
) {
insert_therapist_documents_one(object: {
therapist_id: $therapist_id
type: $document_type
document_url: $document_url
}) {
id
status
}
}
🎯 Validation Rules
Profile Validation
// Yup validation schema
const validationSchema = yup.object({
consultationFees: yup.number()
.required('Required')
.min(500, 'Minimum ₹500')
.max(10000, 'Maximum ₹10,000'),
overallExperience: yup.number()
.required('Required')
.min(0, 'Cannot be negative')
.max(50, 'Maximum 50 years'),
aadharCard: yup.string()
.required('Required')
.matches(/^\d{12}$/, 'Must be 12 digits'),
panCard: yup.string()
.required('Required')
.matches(/^[A-Z]{5}[0-9]{4}[A-Z]$/, 'Invalid PAN format'),
pincode: yup.string()
.required('Required')
.matches(/^\d{6}$/, 'Must be 6 digits'),
});
Document Validation
File Requirements:
- Formats: PDF, JPG, JPEG, PNG
- Size: Maximum 5MB per file
- Resolution: Minimum 300 DPI for documents
- Naming: No special characters in filename
🎨 UI/UX Guidelines
Profile Completion Progress
Progress Calculation:
totalFields = 15 // Total required fields
completedFields = fields.filter(field => field.value !== null).length
progressPercentage = (completedFields / totalFields) × 100
Progress Indicators:
- 0-33%: Red indicator "Complete your profile"
- 34-66%: Yellow indicator "Almost there"
- 67-99%: Blue indicator "Final steps"
- 100%: Green indicator "Profile complete"
Status Badges
Document Status Colors:
- Pending: Orange (#FFA500)
- Under Review: Blue (#2196F3)
- Approved: Green (#4CAF50)
- Rejected: Red (#F44336)
Payment Status Colors:
- Pending: Orange (#FFA500)
- Processing: Blue (#2196F3)
- Paid: Green (#4CAF50)
- Failed: Red (#F44336)
🔒 Security Considerations
Data Protection
Sensitive Data Handling:
- Aadhaar/PAN numbers encrypted in transit
- Documents stored on secure CDN
- Bank details encrypted at rest
- Access tokens in secure MMKV storage
API Security:
- JWT authentication on all requests
- Token refresh on 401 responses
- Rate limiting on document uploads
- File type verification server-side
📊 Performance Optimization
Caching Strategy
Apollo Cache:
- Profile data cached for 5 minutes
- Earnings data cached for 1 minute
- Documents cached until manual refresh
- Cache eviction on mutations
Image Caching:
- Profile images cached locally
- CDN caching for 24 hours
- Lazy loading for document thumbnails
Loading States
Skeleton Screens:
- Profile information skeleton
- Document list skeleton
- Earnings list skeleton
- Prevents layout shift
🎯 Best Practices
Error Handling
User-Friendly Messages:
- Network errors: "Check your internet connection"
- Validation errors: Specific field-level messages
- Server errors: "Something went wrong. Please try again"
- Success messages: Clear confirmation of actions
Accessibility
Mobile Accessibility:
- Minimum touch target size: 44x44 pts
- Sufficient color contrast (WCAG AA)
- Screen reader support for all elements
- Keyboard navigation support
🎯 Summary
The therapist profile management system provides a comprehensive solution for professional profile management with:
- Complete onboarding workflow with validation
- Document upload and verification system
- Real-time earnings tracking and wallet management
- Mobile-optimized touch interactions
- Offline capabilities with automatic sync
- Secure data handling and API integration