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