Skip to main content

👤 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:

  1. Degree Certificate (Required) - Educational qualification proof
  2. Professional License (Required) - Practice license/certification
  3. Aadhaar Card (Required) - Identity proof
  4. PAN Card (Required) - Tax identification
  5. 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_PRIVATE endpoint
  • 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:

  1. Check minimum withdrawal amount (₹500)
  2. Verify bank details are added
  3. Validate available balance
  4. Submit withdrawal request
  5. Admin approval (1-3 business days)
  6. 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