signup flow added for radiologist

This commit is contained in:
yashwin-foxy 2025-07-24 20:06:12 +05:30
parent df5616eaf5
commit 29940278e6
33 changed files with 2335 additions and 24 deletions

165
.cursor/rules/appflow.mdc Normal file
View File

@ -0,0 +1,165 @@
---
description:
globs:
alwaysApply: true
---
Radiologist_Rule1
1. RADIOLOGIST APP - DETAILED WORKFLOW
1.1 App Launch & Authentication
App Launch → Biometric Auth → Dashboard Loading
Load Case Queue → Priority Sort → Display Cases
Critical (RED) → Urgent (ORANGE) → Routine (GREEN)
1.2 Critical Finding Workflow (Acute Subdural Hemorrhage)
STEP 1: Alert Reception
Push Notification → "CRITICAL - Acute Subdural Hemorrhage"
Tap Notification → App Opens → Case Preview
Patient: John Doe, Age 45, Head Trauma
AI Confidence: 93%
Midline Shift: 7mm
STEP 2: Full Case Review
Case Preview → Open DICOM Viewer
AI Overlay ON → Hemorrhage Highlighted
Review Images → Confirm Finding → Add Observations
"Large acute subdural hemorrhage with mass effect"
STEP 3: Report Generation
Voice-to-Text → "Acute subdural hemorrhage..."
Review Text → Edit if needed → Submit Report
Preliminary Report Sent (< 5 minutes)
STEP 4: System Learning
System Logs → Radiologist Confirmation
Time Metrics Recorded → Model Improvement Data
1.3 Routine Scan Workflow (Negative Finding)
STEP 1: Queue Processing
Dashboard → Routine Queue → Select Case
Patient: Jane Smith, Age 30, Headache
AI: No acute abnormalities (99% confidence)
STEP 2: Confirmation Review
DICOM Viewer → Quick Review → Confirm Negative
"No acute intracranial abnormality"
STEP 3: Report & Documentation
Submit Report → System Logs Concordance
Next Case in Queue
1.4 System Uncertainty Protocol
AI Confidence 70-85% → "REVIEW RECOMMENDED"
Special Yellow Flag → Enhanced Attention Required
Detailed Review → Feedback to System
3. RADIOLOGIST APP WIREFRAMES
3.1 Login Screen
+------------------------+
| [HOSPITAL LOGO] |
| |
| Radiologist Portal |
| |
| [👤 Dr. Smith] |
| [🔒 Biometric Login] |
| |
| [Face ID Icon] |
| Touch to Login |
| |
| OR |
| |
| PIN: [****] |
| [LOGIN] |
| |
| Emergency Access |
+------------------------+
3.2 Dashboard - Case Queue
+------------------------+
| Dr. Smith [🔔3][⚙️] |
| On-Call Status: ACTIVE |
| |
| CRITICAL CASES |
| 🔴 Bed 3 - Hemorrhage |
| 📱 2 min ago |
| AI: 93% confidence |
| [REVIEW NOW] |
| |
| 🔴 Bed 7 - Stroke |
| 📱 5 min ago |
| AI: 87% confidence |
| [REVIEW NOW] |
| |
| URGENT CASES (4) |
| 🟡 Show All |
| |
| ROUTINE CASES (12) |
| 🟢 Show All |
+------------------------+
3.3 Critical Case Details
+------------------------+
| ← CRITICAL CASE |
| |
| Patient: John Doe, 45M |
| Bed: 3 | Time: 14:35 |
| Clinical: Head trauma |
| |
| AI ANALYSIS: |
| 🔴 Acute Subdural |
| Confidence: 93% |
| Midline Shift: 7mm |
| Mass Effect: Present |
| |
| [VIEW DICOM IMAGES] |
| [VOICE REPORT] |
| [QUICK REPORT] |
| |
| Status: PENDING REVIEW |
+------------------------+
3.4 DICOM Viewer
+------------------------+
| ← John Doe - CT Brain |
| |
| [ CT SCAN IMAGE ]|
| [ WITH AI OVERLAY ]|
| [ HEMORRHAGE ]|
| [ HIGHLIGHTED ]|
| |
| Tools: [🔍][📏][✏️] |
| AI: [ON] Conf: 93% |
| |
| Measurements: |
| • Hemorrhage: 3.2cm |
| • Midline: 7mm shift |
| |
| [🎤 START REPORT] |
+------------------------+
3.5 Voice Report Interface
+------------------------+
| ← Voice Report |
| |
| [🎤 RECORDING 01:23] |
| |
| "There is a large |
| acute subdural |
| hemorrhage in the |
| right frontoparietal |
| region..." |
| |
| [PAUSE] [STOP] [PLAY] |
| |
| [SAVE DRAFT] |
| [SUBMIT REPORT] |
| |
| Estimated: 2 min left |
+------------------------+

View File

@ -0,0 +1,34 @@
---
alwaysApply: true
---
Each file should contain this as header
/*
* File: FILE_NAME.tsx
* Description: Main chat screen component
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
file footer
/*
* End of File: ChatScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
and it should add proper comments in the file for better understanding the flow.
should follow rule file while generating code.Each file should contain this as header
/*
* File: FILE_NAME.tsx
* Description: Main chat screen component
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
file footer
/*
* End of File: ChatScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
and it should add proper comments in the file for better understanding the flow.
should follow rule file while generating code.

View File

@ -1,3 +0,0 @@
---
alwaysApply: true
---

View File

@ -0,0 +1,397 @@
---
description:
globs:
alwaysApply: true
---
Project Structure
🏥 RADIOLOGIST APP STRUCTURE
NeoScan_Radiologist/
├── app/
│ ├── modules/ # 🌐 Feature-wise modular architecture
│ │ ├── Auth/ # 🔐 Authentication Module
│ │ │ ├── components/
│ │ │ │ ├── BiometricLogin.tsx
│ │ │ │ ├── PinInput.tsx
│ │ │ │ ├── EmergencyAccess.tsx
│ │ │ │ └── index.ts
│ │ │ ├── screens/
│ │ │ │ ├── LoginScreen.tsx
│ │ │ │ ├── SetupBiometricScreen.tsx
│ │ │ │ └── index.ts
│ │ │ ├── hooks/
│ │ │ │ ├── useAuth.ts
│ │ │ │ ├── useBiometric.ts
│ │ │ │ └── index.ts
│ │ │ ├── redux/
│ │ │ │ ├── authSlice.ts
│ │ │ │ ├── authActions.ts
│ │ │ │ ├── authSelectors.ts
│ │ │ │ └── index.ts
│ │ │ ├── services/
│ │ │ │ ├── authAPI.ts
│ │ │ │ ├── biometricService.ts
│ │ │ │ └── index.ts
│ │ │ ├── __tests__/
│ │ │ │ ├── AuthService.test.ts
│ │ │ │ ├── LoginScreen.test.tsx
│ │ │ │ └── authSlice.test.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── Dashboard/ # 📊 Dashboard Module
│ │ │ ├── components/
│ │ │ │ ├── CaseQueue.tsx
│ │ │ │ ├── CaseCard.tsx
│ │ │ │ ├── PriorityIndicator.tsx
│ │ │ │ ├── StatsPanel.tsx
│ │ │ │ ├── FilterBar.tsx
│ │ │ │ └── index.ts
│ │ │ ├── screens/
│ │ │ │ ├── DashboardScreen.tsx
│ │ │ │ ├── CaseListScreen.tsx
│ │ │ │ └── index.ts
│ │ │ ├── hooks/
│ │ │ │ ├── useCaseQueue.ts
│ │ │ │ ├── useRealTimeUpdates.ts
│ │ │ │ ├── useFilterCases.ts
│ │ │ │ └── index.ts
│ │ │ ├── redux/
│ │ │ │ ├── dashboardSlice.ts
│ │ │ │ ├── caseQueueSlice.ts
│ │ │ │ ├── dashboardActions.ts
│ │ │ │ ├── dashboardSelectors.ts
│ │ │ │ └── index.ts
│ │ │ ├── services/
│ │ │ │ ├── caseAPI.ts
│ │ │ │ ├── websocketService.ts
│ │ │ │ ├── notificationService.ts
│ │ │ │ └── index.ts
│ │ │ ├── __tests__/
│ │ │ │ ├── CaseQueue.test.tsx
│ │ │ │ ├── dashboardSlice.test.ts
│ │ │ │ └── caseAPI.test.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── CaseReview/ # 🔍 Case Review Module
│ │ │ ├── components/
│ │ │ │ ├── DICOMViewer.tsx
│ │ │ │ ├── AIOverlay.tsx
│ │ │ │ ├── MeasurementTools.tsx
│ │ │ │ ├── AnnotationTools.tsx
│ │ │ │ ├── CaseMetadata.tsx
│ │ │ │ ├── PriorStudyComparison.tsx
│ │ │ │ └── index.ts
│ │ │ ├── screens/
│ │ │ │ ├── CaseDetailsScreen.tsx
│ │ │ │ ├── DICOMViewerScreen.tsx
│ │ │ │ ├── ComparisonScreen.tsx
│ │ │ │ └── index.ts
│ │ │ ├── hooks/
│ │ │ │ ├── useDICOMViewer.ts
│ │ │ │ ├── useAIOverlay.ts
│ │ │ │ ├── useMeasurements.ts
│ │ │ │ ├── useAnnotations.ts
│ │ │ │ └── index.ts
│ │ │ ├── redux/
│ │ │ │ ├── caseReviewSlice.ts
│ │ │ │ ├── dicomSlice.ts
│ │ │ │ ├── aiAnalysisSlice.ts
│ │ │ │ ├── caseReviewActions.ts
│ │ │ │ ├── caseReviewSelectors.ts
│ │ │ │ └── index.ts
│ │ │ ├── services/
│ │ │ │ ├── dicomAPI.ts
│ │ │ │ ├── aiAnalysisAPI.ts
│ │ │ │ ├── dicomParser.ts
│ │ │ │ ├── imageProcessor.ts
│ │ │ │ └── index.ts
│ │ │ ├── __tests__/
│ │ │ │ ├── DICOMViewer.test.tsx
│ │ │ │ ├── AIOverlay.test.tsx
│ │ │ │ ├── dicomParser.test.ts
│ │ │ │ └── caseReviewSlice.test.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── Reporting/ # 📝 Reporting Module
│ │ │ ├── components/
│ │ │ │ ├── VoiceRecorder.tsx
│ │ │ │ ├── ReportEditor.tsx
│ │ │ │ ├── ReportTemplate.tsx
│ │ │ │ ├── QuickReportButtons.tsx
│ │ │ │ ├── ReportPreview.tsx
│ │ │ │ └── index.ts
│ │ │ ├── screens/
│ │ │ │ ├── ReportingScreen.tsx
│ │ │ │ ├── VoiceReportScreen.tsx
│ │ │ │ ├── ReportHistoryScreen.tsx
│ │ │ │ └── index.ts
│ │ │ ├── hooks/
│ │ │ │ ├── useVoiceRecording.ts
│ │ │ │ ├── useReportGeneration.ts
│ │ │ │ ├── useSpeechToText.ts
│ │ │ │ └── index.ts
│ │ │ ├── redux/
│ │ │ │ ├── reportingSlice.ts
│ │ │ │ ├── voiceRecordingSlice.ts
│ │ │ │ ├── reportingActions.ts
│ │ │ │ ├── reportingSelectors.ts
│ │ │ │ └── index.ts
│ │ │ ├── services/
│ │ │ │ ├── reportAPI.ts
│ │ │ │ ├── voiceToTextAPI.ts
│ │ │ │ ├── reportTemplateService.ts
│ │ │ │ └── index.ts
│ │ │ ├── __tests__/
│ │ │ │ ├── VoiceRecorder.test.tsx
│ │ │ │ ├── reportingSlice.test.ts
│ │ │ │ └── voiceToTextAPI.test.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── Notifications/ # 🔔 Notifications Module
│ │ │ ├── components/
│ │ │ │ ├── NotificationPanel.tsx
│ │ │ │ ├── AlertBanner.tsx
│ │ │ │ ├── CriticalAlert.tsx
│ │ │ │ ├── NotificationSettings.tsx
│ │ │ │ └── index.ts
│ │ │ ├── screens/
│ │ │ │ ├── NotificationsScreen.tsx
│ │ │ │ ├── AlertDetailsScreen.tsx
│ │ │ │ └── index.ts
│ │ │ ├── hooks/
│ │ │ │ ├── useNotifications.ts
│ │ │ │ ├── usePushNotifications.ts
│ │ │ │ ├── useAlertHandling.ts
│ │ │ │ └── index.ts
│ │ │ ├── redux/
│ │ │ │ ├── notificationsSlice.ts
│ │ │ │ ├── alertsSlice.ts
│ │ │ │ ├── notificationActions.ts
│ │ │ │ ├── notificationSelectors.ts
│ │ │ │ └── index.ts
│ │ │ ├── services/
│ │ │ │ ├── pushNotificationService.ts
│ │ │ │ ├── alertService.ts
│ │ │ │ ├── notificationAPI.ts
│ │ │ │ └── index.ts
│ │ │ ├── __tests__/
│ │ │ │ ├── NotificationPanel.test.tsx
│ │ │ │ ├── alertService.test.ts
│ │ │ │ └── notificationsSlice.test.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── Analytics/ # 📈 Analytics Module
│ │ │ ├── components/
│ │ │ │ ├── PerformanceMetrics.tsx
│ │ │ │ ├── ResponseTimeChart.tsx
│ │ │ │ ├── ConcordanceStats.tsx
│ │ │ │ ├── WorkloadAnalysis.tsx
│ │ │ │ └── index.ts
│ │ │ ├── screens/
│ │ │ │ ├── AnalyticsScreen.tsx
│ │ │ │ ├── PerformanceScreen.tsx
│ │ │ │ └── index.ts
│ │ │ ├── hooks/
│ │ │ │ ├── useAnalytics.ts
│ │ │ │ ├── usePerformanceMetrics.ts
│ │ │ │ └── index.ts
│ │ │ ├── redux/
│ │ │ │ ├── analyticsSlice.ts
│ │ │ │ ├── analyticsActions.ts
│ │ │ │ ├── analyticsSelectors.ts
│ │ │ │ └── index.ts
│ │ │ ├── services/
│ │ │ │ ├── analyticsAPI.ts
│ │ │ │ ├── metricsCollector.ts
│ │ │ │ └── index.ts
│ │ │ ├── __tests__/
│ │ │ │ ├── PerformanceMetrics.test.tsx
│ │ │ │ ├── analyticsSlice.test.ts
│ │ │ │ └── metricsCollector.test.ts
│ │ │ └── index.ts
│ │ │
│ │ └── Profile/ # 👤 User Profile Module
│ │ ├── components/
│ │ │ ├── ProfileHeader.tsx
│ │ │ ├── SettingsPanel.tsx
│ │ │ ├── PreferencesForm.tsx
│ │ │ ├── SecuritySettings.tsx
│ │ │ └── index.ts
│ │ ├── screens/
│ │ │ ├── ProfileScreen.tsx
│ │ │ ├── SettingsScreen.tsx
│ │ │ ├── PreferencesScreen.tsx
│ │ │ └── index.ts
│ │ ├── hooks/
│ │ │ ├── useProfile.ts
│ │ │ ├── useSettings.ts
│ │ │ └── index.ts
│ │ ├── redux/
│ │ │ ├── profileSlice.ts
│ │ │ ├── settingsSlice.ts
│ │ │ ├── profileActions.ts
│ │ │ ├── profileSelectors.ts
│ │ │ └── index.ts
│ │ ├── services/
│ │ │ ├── profileAPI.ts
│ │ │ ├── settingsAPI.ts
│ │ │ └── index.ts
│ │ ├── __tests__/
│ │ │ ├── ProfileHeader.test.tsx
│ │ │ ├── profileSlice.test.ts
│ │ │ └── profileAPI.test.ts
│ │ └── index.ts
│ │
│ ├── navigation/ # 🧭 Navigation Setup
│ │ ├── AppNavigator.tsx
│ │ ├── AuthNavigator.tsx
│ │ ├── MainNavigator.tsx
│ │ ├── TabNavigator.tsx
│ │ ├── ModalNavigator.tsx
│ │ ├── navigationTypes.ts
│ │ └── index.ts
│ │
│ ├── redux/ # 🗃️ Redux Store Setup
│ │ ├── store.ts
│ │ ├── rootReducer.ts
│ │ ├── middleware.ts
│ │ ├── persistConfig.ts
│ │ └── index.ts
│ │
│ ├── constants/ # 📋 Global Constants
│ │ ├── apiEndpoints.ts
│ │ ├── appConstants.ts
│ │ ├── errorMessages.ts
│ │ ├── notificationTypes.ts
│ │ ├── priorityLevels.ts
│ │ └── index.ts
│ │
│ ├── App.tsx # 🎯 Root Component
│ └── index.ts # 🚀 App Bootstrap
├── shared/ # ✅ Shared Components & Utilities
│ ├── src/
│ │ ├── components/ # 🎨 Global UI Components
│ │ │ ├── Button/
│ │ │ │ ├── Button.tsx
│ │ │ │ ├── Button.styles.ts
│ │ │ │ ├── Button.types.ts
│ │ │ │ └── index.ts
│ │ │ ├── Input/
│ │ │ │ ├── TextInput.tsx
│ │ │ │ ├── SearchInput.tsx
│ │ │ │ ├── Input.styles.ts
│ │ │ │ ├── Input.types.ts
│ │ │ │ └── index.ts
│ │ │ ├── Modal/
│ │ │ │ ├── Modal.tsx
│ │ │ │ ├── ConfirmModal.tsx
│ │ │ │ ├── AlertModal.tsx
│ │ │ │ ├── Modal.styles.ts
│ │ │ │ ├── Modal.types.ts
│ │ │ │ └── index.ts
│ │ │ ├── Card/
│ │ │ │ ├── Card.tsx
│ │ │ │ ├── InfoCard.tsx
│ │ │ │ ├── Card.styles.ts
│ │ │ │ ├── Card.types.ts
│ │ │ │ └── index.ts
│ │ │ ├── Loading/
│ │ │ │ ├── Spinner.tsx
│ │ │ │ ├── LoadingOverlay.tsx
│ │ │ │ ├── Loading.styles.ts
│ │ │ │ └── index.ts
│ │ │ ├── Icons/
│ │ │ │ ├── CustomIcon.tsx
│ │ │ │ ├── IconButton.tsx
│ │ │ │ ├── Icons.types.ts
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── theme/ # 🎨 Design System
│ │ │ ├── colors.ts
│ │ │ ├── spacing.ts
│ │ │ ├── typography.ts
│ │ │ ├── shadows.ts
│ │ │ ├── borderRadius.ts
│ │ │ ├── animations.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── assets/ # 📁 Static Assets
│ │ │ ├── icons/
│ │ │ │ ├── critical-alert.svg
│ │ │ │ ├── urgent-alert.svg
│ │ │ │ ├── routine-alert.svg
│ │ │ │ └── index.ts
│ │ │ ├── images/
│ │ │ │ ├── logo.png
│ │ │ │ ├── placeholder.png
│ │ │ │ └── index.ts
│ │ │ ├── fonts/
│ │ │ │ ├── Roboto-Regular.ttf
│ │ │ │ ├── Roboto-Bold.ttf
│ │ │ │ └── index.ts
│ │ │ └── sounds/
│ │ │ ├── critical-alert.wav
│ │ │ ├── urgent-alert.wav
│ │ │ └── routine-alert.wav
│ │ │
│ │ ├── utils/ # 🛠️ Utility Functions
│ │ │ ├── dateTime/
│ │ │ │ ├── formatDate.ts
│ │ │ │ ├── timeAgo.ts
│ │ │ │ └── index.ts
│ │ │ ├── validation/
│ │ │ │ ├── validators.ts
│ │ │ │ ├── schemas.ts
│ │ │ │ └── index.ts
│ │ │ ├── helpers/
│ │ │ │ ├── debounce.ts
│ │ │ │ ├── throttle.ts
│ │ │ │ ├── formatters.ts
│ │ │ │ └── index.ts
│ │ │ ├── constants/
│ │ │ │ ├── regex.ts
│ │ │ │ ├── formats.ts
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── hooks/ # 🎣 Shared Custom Hooks
│ │ │ ├── useTheme.ts
│ │ │ ├── useDebounce.ts
│ │ │ ├── useKeyboard.ts
│ │ │ ├── useNetworkStatus.ts
│ │ │ ├── useOrientation.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── types/ # 📝 TypeScript Types
│ │ │ ├── common.ts
│ │ │ ├── api.ts
│ │ │ ├── navigation.ts
│ │ │ ├── theme.ts
│ │ │ └── index.ts
│ │ │
│ │ └── index.ts # 📦 Barrel Export
│ │
│ ├── package.json # 📦 Shared Package Config
│ └── tsconfig.json # ⚙️ TypeScript Config
├── __tests__/ # 🧪 Global Tests
│ ├── App.test.tsx
│ ├── Navigation.test.tsx
│ ├── Redux.test.tsx
│ └── Integration.test.tsx
├── assets/ # 🖼️ App-wide Assets
│ ├── images/
│ ├── videos/
│ └── audio/
├── android/ # 🤖 Android Native
├── ios/ # 🍎 iOS Native
├── index.js # 🎯 RN Entry Point
├── package.json # 📦 Root Dependencies
├── tsconfig.json # ⚙️ Base TypeScript Config
├── metro.config.js # 📦 Metro Bundler Config
├── babel.config.js # 🔄 Babel Config
└── .eslintrc.js # 📏 ESLint Config

View File

@ -0,0 +1,66 @@
---
description:
globs:
alwaysApply: true
---
Radiologist_Rule3
Theme and commonly used node_module packages
Theme
Radiologist App - "Clinical Blue Interface"
Primary: #5B7CE6 (Blue Purple - from the selected buttons)
Secondary: #7B94F0 (Light Blue Purple)
Tertiary: #E8EFFF (Very Light Blue)
Quaternary: #3B5998 (Deep Blue)
Text Primary: #1A1D29 (Almost Black)
Text Secondary: #4A5568 (Medium Gray)
Text Muted: #9CA3AF (Light Gray)
Background: #FFFFFF (White)
Background Alt: #F8FAFF (Very Light Blue Tint)
Background Accent: #F1F5FF (Soft Blue)
Card Background: #FFFFFF (White with shadow)
Success: #10B981 (Green - from the security icon)
Warning: #F59E0B (Amber)
Error: #EF4444 (Red)
Info: #3B82F6 (Blue)
Border: #E5E7EB (Light Gray)
Selected State: #5B7CE6 (Same as Primary)
Inactive State: #F3F4F6 (Light Gray)
Shadow: rgba(91, 124, 230, 0.1) (Blue Shadow)
Gradient Background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)
Packages Used
"@gorhom/bottom-sheet": "^5.1.2",
"@react-native-async-storage/async-storage": "^2.1.2",
"@react-native-community/masked-view": "^0.1.11",
"@react-native-community/netinfo": "^11.4.1",
"@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.9",
"@reduxjs/toolkit": "^2.6.1",
"@testing-library/react-native": "^13.2.0",
"apisauce": "^2.1.6",
"axios": "^1.8.4",
"react": "19.0.0",
"react-native": "0.79.0",
"react-native-chart-kit": "^6.12.0",
"react-native-config": "^1.5.5",
"react-native-device-info": "^14.0.4",
"react-native-gesture-handler": "^2.25.0",
"react-native-linear-gradient": "^2.8.3",
"react-native-paper": "^5.13.1",
"react-native-permissions": "^5.4.0",
"react-native-raw-bottom-sheet": "^3.0.0",
"react-native-reanimated": "^3.17.4",
"react-native-safe-area-context": "^5.3.0",
"react-native-screens": "^4.10.0",
"react-native-skeleton-content": "^1.0.28",
"react-native-sound": "^0.11.2",
"react-native-svg": "^15.11.2",
"react-native-tab-view": "^4.0.10",
"react-native-toast-message": "^2.2.1",
"react-native-vector-icons": "^10.2.0",
"react-redux": "^9.2.0",
"redux-persist": "^6.0.0"

4
.env
View File

@ -1,2 +1,2 @@
BASE_URL='https://neoscan-backend.tech4bizsolutions.com' # BASE_URL='https://neoscan-backend.tech4bizsolutions.com'
# BASE_URL='http://192.168.1.87:3000' BASE_URL='http://192.168.1.87:3000'

10
App.tsx
View File

@ -16,13 +16,14 @@ import {
View, View,
} from 'react-native'; } from 'react-native';
import { Provider } from 'react-redux'; // Redux Provider import { Provider } from 'react-redux'; // Redux Provider
import store from './app/redux/store'; // Redux store import {store,persistor} from './app/redux/store'; // Redux store
import { import {
Colors, Colors,
} from 'react-native/Libraries/NewAppScreen'; } from 'react-native/Libraries/NewAppScreen';
import TabNavigator from './app/navigation/TabNavigator'; import TabNavigator from './app/navigation/TabNavigator';
import AppNavigator from './app/navigation/AppNavigator'; import AppNavigator from './app/navigation/AppNavigator';
import Toast from 'react-native-toast-message'; // Import Toast import Toast from 'react-native-toast-message'; // Import Toast
import { PersistGate } from 'redux-persist/integration/react';
type SectionProps = PropsWithChildren<{ type SectionProps = PropsWithChildren<{
title: string; title: string;
@ -77,6 +78,7 @@ function App(): React.JSX.Element {
// Wrap the app with Redux Provider to enable global state management // Wrap the app with Redux Provider to enable global state management
return ( return (
<Provider store={store}> <Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<View style={styles.container}> <View style={styles.container}>
<StatusBar <StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'} barStyle={isDarkMode ? 'light-content' : 'dark-content'}
@ -84,7 +86,11 @@ function App(): React.JSX.Element {
/> />
<AppNavigator /> <AppNavigator />
</View> </View>
<Toast /> <Toast
position='bottom'
bottomOffset={20}
/>
</PersistGate>
</Provider> </Provider>
); );
} }

View File

@ -3,6 +3,8 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" /> <uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"

View File

@ -0,0 +1,53 @@
"use client"
/*
* File: BackButton.tsx
* Description: Reusable back button component with consistent styling.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import type React from "react"
import { TouchableOpacity, StyleSheet } from "react-native"
import Icon from "react-native-vector-icons/Feather"
import { Colors } from "../../../../shared/src/theme"
interface BackButtonProps {
onPress: () => void
style?: any
}
const BackButton: React.FC<BackButtonProps> = ({ onPress, style }) => {
return (
<TouchableOpacity style={[styles.backButton, style]} onPress={onPress} activeOpacity={0.7}>
<Icon name="arrow-left" size={20} color={Colors.textPrimary} />
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
backButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: Colors.backButtonBackground,
justifyContent: "center",
alignItems: "center",
shadowColor: Colors.backButtonShadow,
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
})
export default BackButton
/*
* End of File: BackButton.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,401 @@
"use client"
/*
* File: DocumentUploadScreen.tsx
* Description: Image upload screen for onboarding flow with react-native-image-picker.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import type React from "react"
import { useState } from "react"
import {
View,
Text,
StyleSheet,
TouchableWithoutFeedback,
Keyboard,
TouchableOpacity,
Alert,
PermissionsAndroid,
Platform,
Image
} from "react-native"
import {
launchImageLibrary,
launchCamera,
ImagePickerResponse,
MediaType,
ImagePickerOptions
} from 'react-native-image-picker'
import { Button } from "../../../../shared/src/components/Button"
import { Colors, Spacing, Typography } from "../../../../shared/src/theme"
import { showError, showSuccess } from "../../../../shared/src/utils/helpers/Toast"
import Icon from "react-native-vector-icons/Feather"
import OnboardingContainer from "./OnboardingContainer"
interface DocumentUploadScreenProps {
onContinue: (imageData: {
uri: string
name: string
type: string
size?: number
}) => void
onBack: () => void
}
interface ImageData {
uri: string
name: string
type: string
size?: number
}
const DocumentUploadScreen: React.FC<DocumentUploadScreenProps> = ({ onContinue, onBack }) => {
const [selectedImage, setSelectedImage] = useState<ImageData | null>(null)
const [loading, setLoading] = useState(false)
// Request camera permission for Android
const requestCameraPermission = async (): Promise<boolean> => {
if (Platform.OS === 'android') {
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.CAMERA,
{
title: 'Camera Permission',
message: 'This app needs camera permission to capture images.',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
}
)
return granted === PermissionsAndroid.RESULTS.GRANTED
} catch (err) {
console.warn(err)
return false
}
}
return true
}
const handleImagePicker = () => {
Alert.alert("Select Image", "Choose how you want to upload your image", [
{
text: "Camera",
onPress: () => handleCameraCapture(),
},
{
text: "Gallery",
onPress: () => handleGalleryPicker(),
},
{
text: "Cancel",
style: "cancel",
},
])
}
const handleCameraCapture = async () => {
const hasPermission = await requestCameraPermission()
if (!hasPermission) {
showError("Permission Error", "Camera permission is required to capture images.")
return
}
const options: ImagePickerOptions = {
mediaType: 'photo' as MediaType,
quality: 0.8,
maxWidth: 2000,
maxHeight: 2000,
includeBase64: false,
}
launchCamera(options, (response: ImagePickerResponse) => {
if (response.didCancel) {
return
}
if (response.errorMessage) {
showError("Camera Error", response.errorMessage)
return
}
if (response.assets && response.assets[0]) {
const asset = response.assets[0]
const imageData: ImageData = {
uri: asset.uri!,
name: asset.fileName || `image_${Date.now()}.jpg`,
type: asset.type || 'image/jpeg',
size: asset.fileSize,
}
setSelectedImage(imageData)
showSuccess("Success", "Image captured successfully!")
}
})
}
const handleGalleryPicker = () => {
const options: ImagePickerOptions = {
mediaType: 'photo' as MediaType,
quality: 0.8,
maxWidth: 2000,
maxHeight: 2000,
includeBase64: false,
}
launchImageLibrary(options, (response: ImagePickerResponse) => {
if (response.didCancel) {
return
}
if (response.errorMessage) {
showError("Gallery Error", response.errorMessage)
return
}
if (response.assets && response.assets[0]) {
const asset = response.assets[0]
const imageData: ImageData = {
uri: asset.uri!,
name: asset.fileName || `image_${Date.now()}.jpg`,
type: asset.type || 'image/jpeg',
size: asset.fileSize,
}
setSelectedImage(imageData)
showSuccess("Success", "Image selected from gallery!")
}
})
}
const formatFileSize = (bytes?: number): string => {
if (!bytes) return ''
const sizes = ['Bytes', 'KB', 'MB', 'GB']
if (bytes === 0) return '0 Bytes'
const i = Math.floor(Math.log(bytes) / Math.log(1024))
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]
}
const getFileTypeDisplay = (type: string): string => {
if (type.includes('jpeg') || type.includes('jpg')) return 'JPEG'
if (type.includes('png')) return 'PNG'
if (type.includes('gif')) return 'GIF'
if (type.includes('webp')) return 'WebP'
return 'Image'
}
const handleContinue = () => {
if (!selectedImage) {
showError("Validation Error", "Please upload an image to continue.")
return
}
setLoading(true)
setTimeout(() => {
setLoading(false)
onContinue(selectedImage)
}, 1000)
}
return (
<OnboardingContainer onBack={onBack}>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.container}>
<View style={styles.content}>
<View style={styles.iconContainer}>
<View style={styles.iconWrapper}>
<Icon name="upload" size={32} color={Colors.primary} />
</View>
</View>
<Text style={styles.title}>Upload Image</Text>
<Text style={styles.subtitle}>Please upload your profile picture or identification image.</Text>
<TouchableOpacity style={styles.uploadContainer} onPress={handleImagePicker}>
<View style={styles.uploadContent}>
{selectedImage ? (
<View style={styles.imagePreviewContainer}>
<Image source={{ uri: selectedImage.uri }} style={styles.imagePreview} />
<TouchableOpacity style={styles.imageOverlay} onPress={()=>setSelectedImage(null)}>
<Icon name="x" size={24} color={'#FFFFFF'} />
</TouchableOpacity>
<Text style={styles.uploadedText}>Image Uploaded</Text>
<Text style={styles.fileName}>{selectedImage.name}</Text>
<View style={styles.fileDetails}>
<Text style={styles.fileType}>{getFileTypeDisplay(selectedImage.type)}</Text>
{selectedImage.size && (
<Text style={styles.fileSize}> {formatFileSize(selectedImage.size)}</Text>
)}
</View>
</View>
) : (
<>
<Icon name="image" size={48} color={Colors.textSecondary} />
<Text style={styles.uploadText}>Tap to upload image</Text>
<Text style={styles.uploadSubtext}>JPG, PNG supported</Text>
</>
)}
</View>
</TouchableOpacity>
{selectedImage && (
<TouchableOpacity style={styles.changeButton} onPress={handleImagePicker}>
<Text style={styles.changeButtonText}>Change Image</Text>
</TouchableOpacity>
)}
<Button
title="Continue"
onPress={handleContinue}
style={[styles.button, !selectedImage && styles.buttonDisabled]}
loading={loading}
/>
</View>
</View>
</TouchableWithoutFeedback>
</OnboardingContainer>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
flex: 1,
justifyContent: "center",
},
iconContainer: {
alignItems: "center",
marginBottom: Spacing.xl,
},
iconWrapper: {
width: 80,
height: 80,
borderRadius: 20,
backgroundColor: Colors.backgroundAlt,
justifyContent: "center",
alignItems: "center",
borderWidth: 1,
borderColor: Colors.border,
},
title: {
fontFamily: Typography.fontFamily.bold,
fontSize: Typography.fontSize.title,
color: Colors.textPrimary,
textAlign: "center",
marginBottom: Spacing.sm,
},
subtitle: {
fontFamily: Typography.fontFamily.regular,
fontSize: Typography.fontSize.md,
color: Colors.textSecondary,
textAlign: "center",
marginBottom: Spacing.xl,
},
uploadContainer: {
backgroundColor: Colors.backgroundAlt,
borderWidth: 2,
borderColor: Colors.border,
borderStyle: "dashed",
borderRadius: 12,
padding: Spacing.xl,
marginBottom: Spacing.md,
minHeight: 200,
},
uploadContent: {
alignItems: "center",
},
imagePreviewContainer: {
alignItems: "center",
width: '100%',
},
imagePreview: {
width: '100%',
height: 150,
borderRadius: 12,
marginBottom: Spacing.sm,
resizeMode: 'contain',
},
imageOverlay: {
position: 'absolute',
top: 0,
right: 20,
marginRight: -20, // Half of image width minus half of overlay
backgroundColor: 'rgba(0, 0, 0, 0.4)',
borderRadius: 17,
width: 34,
height: 34,
justifyContent: 'center',
alignItems: 'center',
padding:2
},
uploadText: {
fontFamily: Typography.fontFamily.medium,
fontSize: Typography.fontSize.md,
color: Colors.textPrimary,
marginTop: Spacing.sm,
},
uploadSubtext: {
fontFamily: Typography.fontFamily.regular,
fontSize: Typography.fontSize.sm,
color: Colors.textSecondary,
marginTop: Spacing.xs,
},
uploadedText: {
fontFamily: Typography.fontFamily.medium,
fontSize: Typography.fontSize.md,
color: Colors.success,
marginTop: Spacing.sm,
},
fileName: {
fontFamily: Typography.fontFamily.regular,
fontSize: Typography.fontSize.sm,
color: Colors.textSecondary,
marginTop: Spacing.xs,
textAlign: "center",
maxWidth: '80%',
},
fileDetails: {
flexDirection: "row",
alignItems: "center",
marginTop: Spacing.xs,
},
fileType: {
fontFamily: Typography.fontFamily.regular,
fontSize: Typography.fontSize.xs,
color: Colors.textMuted,
},
fileSize: {
fontFamily: Typography.fontFamily.regular,
fontSize: Typography.fontSize.xs,
color: Colors.textMuted,
},
changeButton: {
alignSelf: "center",
marginBottom: Spacing.xl,
},
changeButtonText: {
fontFamily: Typography.fontFamily.medium,
fontSize: Typography.fontSize.md,
color: Colors.primary,
},
button: {
marginTop: Spacing.md,
},
buttonDisabled: {
opacity: 0.5,
},
})
export default DocumentUploadScreen
/*
* End of File: DocumentUploadScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,127 @@
"use client"
/*
* File: EmailScreen.tsx
* Description: Email input screen for onboarding flow with gradient background.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import type React from "react"
import { useState } from "react"
import { View, Text, StyleSheet, TextInput } from "react-native"
import { Button } from "../../../../shared/src/components/Button"
import OnboardingContainer from "./OnboardingContainer"
import IconContainer from "./IconContainer"
import { Colors, Spacing, Typography } from "../../../../shared/src/theme"
import { showError } from "../../../../shared/src/utils/helpers/Toast"
import { validateEmail } from "../../../../shared/src/utils/validation/validators"
import Icon from "react-native-vector-icons/Feather"
interface EmailScreenProps {
onContinue: (email: string) => void
onBack: () => void
}
const EmailScreen: React.FC<EmailScreenProps> = ({ onContinue, onBack }) => {
const [email, setEmail] = useState("")
const [loading, setLoading] = useState(false)
const handleContinue = () => {
if (!email.trim()) {
showError("Validation Error", "Email address is required.")
return
}
if (!validateEmail(email)) {
showError("Validation Error", "Please enter a valid email address.")
return
}
setLoading(true)
setTimeout(() => {
setLoading(false)
onContinue(email)
}, 1000)
}
return (
<OnboardingContainer onBack={onBack}>
<IconContainer iconName="mail" />
<Text style={styles.title}>What's your email?</Text>
<Text style={styles.subtitle}>Please enter your email address.</Text>
<View style={styles.inputContainer}>
<Icon name="mail" size={20} color={Colors.textSecondary} style={styles.inputIcon} />
<TextInput
placeholder="Email address"
value={email}
onChangeText={setEmail}
style={styles.inputField}
autoCapitalize="none"
keyboardType="email-address"
placeholderTextColor={Colors.textMuted}
/>
</View>
<Button title="Continue" onPress={handleContinue} style={styles.button} loading={loading} />
</OnboardingContainer>
)
}
const styles = StyleSheet.create({
title: {
fontFamily: Typography.fontFamily.bold,
fontSize: Typography.fontSize.title,
color: Colors.textPrimary,
textAlign: "center",
marginBottom: Spacing.sm,
},
subtitle: {
fontFamily: Typography.fontFamily.regular,
fontSize: Typography.fontSize.md,
color: Colors.textSecondary,
textAlign: "center",
marginBottom: Spacing.xl,
},
inputContainer: {
flexDirection: "row",
alignItems: "center",
backgroundColor: Colors.inputBackground,
borderWidth: 1,
borderColor: Colors.border,
borderRadius: 12,
marginBottom: Spacing.xl,
paddingHorizontal: Spacing.md,
paddingVertical: 2,
shadowColor: Colors.backButtonShadow,
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 1,
},
inputIcon: {
marginRight: Spacing.sm,
},
inputField: {
flex: 1,
paddingVertical: Spacing.md,
fontSize: Typography.fontSize.md,
color: Colors.textPrimary,
},
button: {
marginTop: Spacing.md,
paddingVertical:Spacing.lg,
},
})
export default EmailScreen
/*
* End of File: EmailScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,231 @@
"use client"
/*
* File: HospitalSelectionScreen.tsx
* Description: Hospital selection screen with dropdown using react-native-element-dropdown.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import type React from "react"
import { useState } from "react"
import { View, Text, StyleSheet, TouchableWithoutFeedback, Keyboard } from "react-native"
import { Dropdown } from "react-native-element-dropdown"
import { Button } from "../../../../shared/src/components/Button"
import { Colors, Spacing, Typography } from "../../../../shared/src/theme"
import { showError } from "../../../../shared/src/utils/helpers/Toast"
import Icon from "react-native-vector-icons/Feather"
import OnboardingContainer from "./OnboardingContainer"
import { selectHospitals } from "../redux"
import { useSelector } from "react-redux"
interface HospitalSelectionScreenProps {
onContinue: (hospitalId: string) => void
onBack: () => void
}
const hospitalData = [
{ label: "City General Hospital", value: "city_general" },
{ label: "St. Mary's Medical Center", value: "st_marys" },
{ label: "University Hospital", value: "university" },
{ label: "Regional Medical Center", value: "regional" },
{ label: "Metropolitan Hospital", value: "metropolitan" },
{ label: "Community Health Center", value: "community" },
{ label: "Children's Hospital", value: "childrens" },
{ label: "Veterans Medical Center", value: "veterans" },
]
const HospitalSelectionScreen: React.FC<HospitalSelectionScreenProps> = ({ onContinue, onBack }) => {
const [selectedHospital, setSelectedHospital] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const [isFocus, setIsFocus] = useState(false)
const hospitals :any=useSelector(selectHospitals)
const handleContinue = () => {
if (!selectedHospital) {
showError("Validation Error", "Please select a hospital to continue.")
return
}
setLoading(true)
setTimeout(() => {
setLoading(false)
onContinue(selectedHospital)
}, 1000)
}
const renderLabel = () => {
if (selectedHospital || isFocus) {
return <Text style={[styles.label, isFocus && { color: Colors.primary }]}>Select Hospital</Text>
}
return null
}
return (
<OnboardingContainer onBack={onBack}>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.container}>
{/* <View style={styles.header}>
<Icon name="arrow-left" size={24} color={Colors.textPrimary} onPress={onBack} />
</View> */}
<View style={styles.content}>
<View style={styles.iconContainer}>
<View style={styles.iconWrapper}>
<Icon name="map-pin" size={32} color={Colors.primary} />
</View>
</View>
<Text style={styles.title}>Select Hospital</Text>
<Text style={styles.subtitle}>Choose the hospital you're affiliated with.</Text>
<View style={styles.dropdownContainer}>
{renderLabel()}
<Dropdown
style={[styles.dropdown, isFocus && { borderColor: Colors.primary }]}
placeholderStyle={styles.placeholderStyle}
selectedTextStyle={styles.selectedTextStyle}
inputSearchStyle={styles.inputSearchStyle}
iconStyle={styles.iconStyle}
data={hospitals}
search
maxHeight={300}
labelField="hospital_name"
valueField="hospital_id"
placeholder={!isFocus ? "Select Hospital" : "..."}
searchPlaceholder="Search hospitals..."
value={selectedHospital}
onFocus={() => setIsFocus(true)}
onBlur={() => setIsFocus(false)}
onChange={(item) => {
console.log('item',item)
setSelectedHospital(item.hospital_id)
setIsFocus(false)
}}
renderLeftIcon={() => (
<Icon
style={styles.icon}
color={isFocus ? Colors.primary : Colors.textSecondary}
name="map-pin"
size={20}
/>
)}
/>
</View>
<Button
title="Create Account"
onPress={handleContinue}
style={[styles.button, !selectedHospital && styles.buttonDisabled]}
loading={loading}
/>
</View>
</View>
</TouchableWithoutFeedback>
</OnboardingContainer>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
// backgroundColor: Colors.background,
},
header: {
paddingTop: Spacing.xl,
paddingHorizontal: Spacing.lg,
paddingBottom: Spacing.md,
},
content: {
flex: 1,
// paddingHorizontal: Spacing.lg,
justifyContent: "center",
},
iconContainer: {
alignItems: "center",
marginBottom: Spacing.xl,
},
iconWrapper: {
width: 80,
height: 80,
borderRadius: 20,
backgroundColor: Colors.backgroundAlt,
justifyContent: "center",
alignItems: "center",
borderWidth: 1,
borderColor: Colors.border,
},
title: {
fontFamily: Typography.fontFamily.bold,
fontSize: Typography.fontSize.title,
color: Colors.textPrimary,
textAlign: "center",
marginBottom: Spacing.sm,
},
subtitle: {
fontFamily: Typography.fontFamily.regular,
fontSize: Typography.fontSize.md,
color: Colors.textSecondary,
textAlign: "center",
marginBottom: Spacing.xl,
},
dropdownContainer: {
marginBottom: Spacing.xl,
},
label: {
position: "absolute",
// backgroundColor: Colors.background,
left: 0,
top: -20,
zIndex: 999,
paddingHorizontal: 8,
fontSize: Typography.fontSize.sm,
color: Colors.textSecondary,
fontFamily: Typography.fontFamily.bold,
},
dropdown: {
height: 50,
borderColor: Colors.border,
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: Spacing.md,
backgroundColor: Colors.backgroundAlt,
},
icon: {
marginRight: Spacing.sm,
},
placeholderStyle: {
fontSize: Typography.fontSize.md,
color: Colors.textMuted,
fontFamily: Typography.fontFamily.regular,
},
selectedTextStyle: {
fontSize: Typography.fontSize.md,
color: Colors.textPrimary,
fontFamily: Typography.fontFamily.regular,
},
iconStyle: {
width: 20,
height: 20,
},
inputSearchStyle: {
height: 40,
fontSize: Typography.fontSize.md,
color: Colors.textPrimary,
fontFamily: Typography.fontFamily.regular,
},
button: {
marginTop: Spacing.md,
},
buttonDisabled: {
opacity: 0.5,
},
})
export default HospitalSelectionScreen
/*
* End of File: HospitalSelectionScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,76 @@
"use client"
/*
* File: IconContainer.tsx
* Description: Reusable icon container component for onboarding screens.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import type React from "react"
import { View, StyleSheet } from "react-native"
import Icon from "react-native-vector-icons/Feather"
import { Colors, Spacing } from "../../../../shared/src/theme"
interface IconContainerProps {
iconName: string
iconSize?: number
containerSize?: number
backgroundColor?: string
iconColor?: string
}
const IconContainer: React.FC<IconContainerProps> = ({
iconName,
iconSize = 32,
containerSize = 80,
backgroundColor = Colors.inputBackground,
iconColor = Colors.primary,
}) => {
return (
<View style={styles.container}>
<View
style={[
styles.iconWrapper,
{
width: containerSize,
height: containerSize,
borderRadius: containerSize * 0.25,
backgroundColor,
},
]}
>
<Icon name={iconName} size={iconSize} color={iconColor} />
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
alignItems: "center",
marginBottom: Spacing.xl,
},
iconWrapper: {
justifyContent: "center",
alignItems: "center",
borderWidth: 1,
borderColor: Colors.border,
shadowColor: Colors.backButtonShadow,
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
})
export default IconContainer
/*
* End of File: IconContainer.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,164 @@
"use client"
/*
* File: NameScreen.tsx
* Description: Name input screen with gradient background and shared components.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import type React from "react"
import { useState } from "react"
import { View, Text, StyleSheet, TextInput } from "react-native"
import { Button } from "../../../../shared/src/components/Button"
import OnboardingContainer from "./OnboardingContainer"
import IconContainer from "./IconContainer"
import { Colors, Spacing, Typography } from "../../../../shared/src/theme"
import { showError } from "../../../../shared/src/utils/helpers/Toast"
import Icon from "react-native-vector-icons/Feather"
interface NameScreenProps {
onContinue: (firstName: string, lastName: string, username: string) => void
onBack: () => void
}
const NameScreen: React.FC<NameScreenProps> = ({ onContinue, onBack }) => {
const [firstName, setFirstName] = useState("")
const [lastName, setLastName] = useState("")
const [username, setUsername] = useState("")
const [loading, setLoading] = useState(false)
const handleContinue = () => {
if (!firstName.trim() || !lastName.trim() || !username.trim()) {
showError("Validation Error", "First name, last name, and username are required.")
return
}
if (firstName.trim().length < 2 ) {
showError("Validation Error", "Names must be at least 2 characters long.")
return
}
if (username.trim().length < 3) {
showError("Validation Error", "Username must be at least 3 characters long.")
return
}
// Basic username validation (alphanumeric and underscores only)
const usernameRegex = /^[a-zA-Z0-9_]+$/
if (!usernameRegex.test(username.trim())) {
showError("Validation Error", "Username can only contain letters, numbers, and underscores.")
return
}
setLoading(true)
setTimeout(() => {
setLoading(false)
onContinue(firstName.trim(), lastName.trim(), username.trim())
}, 1000)
}
return (
<OnboardingContainer onBack={onBack}>
<IconContainer iconName="user" />
<Text style={styles.title}>What's your name?</Text>
<Text style={styles.subtitle}>Please enter your details to get started.</Text>
<View style={styles.inputContainer}>
<Icon name="user" size={20} color={Colors.textSecondary} style={styles.inputIcon} />
<TextInput
placeholder="First Name"
value={firstName}
onChangeText={setFirstName}
style={styles.inputField}
autoCapitalize="words"
placeholderTextColor={Colors.textMuted}
/>
</View>
<View style={styles.inputContainer}>
<Icon name="user" size={20} color={Colors.textSecondary} style={styles.inputIcon} />
<TextInput
placeholder="Last Name"
value={lastName}
onChangeText={setLastName}
style={styles.inputField}
autoCapitalize="words"
placeholderTextColor={Colors.textMuted}
/>
</View>
<View style={styles.inputContainer}>
<Icon name="at-sign" size={20} color={Colors.textSecondary} style={styles.inputIcon} />
<TextInput
placeholder="Username"
value={username}
onChangeText={setUsername}
style={styles.inputField}
autoCapitalize="none"
autoCorrect={false}
placeholderTextColor={Colors.textMuted}
/>
</View>
<Button title="Continue" onPress={handleContinue} style={styles.button} loading={loading} />
</OnboardingContainer>
)
}
const styles = StyleSheet.create({
title: {
fontFamily: Typography.fontFamily.bold,
fontSize: Typography.fontSize.title,
color: Colors.textPrimary,
textAlign: "center",
marginBottom: Spacing.sm,
},
subtitle: {
fontFamily: Typography.fontFamily.regular,
fontSize: Typography.fontSize.md,
color: Colors.textSecondary,
textAlign: "center",
marginBottom: Spacing.xl,
},
inputContainer: {
flexDirection: "row",
alignItems: "center",
backgroundColor: Colors.inputBackground,
borderWidth: 1,
borderColor: Colors.border,
borderRadius: 12,
marginBottom: Spacing.md,
paddingHorizontal: Spacing.md,
paddingVertical: 2,
shadowColor: Colors.backButtonShadow,
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 1,
},
inputIcon: {
marginRight: Spacing.sm,
},
inputField: {
flex: 1,
paddingVertical: Spacing.md,
fontSize: Typography.fontSize.md,
color: Colors.textPrimary,
},
button: {
marginTop: Spacing.xl,
},
})
export default NameScreen
/*
* End of File: NameScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,64 @@
"use client"
/*
* File: OnboardingContainer.tsx
* Description: Shared container component for onboarding screens with gradient background.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import type React from "react"
import { View, StyleSheet, TouchableWithoutFeedback, Keyboard } from "react-native"
import LinearGradient from "react-native-linear-gradient"
import { Colors, Spacing } from "../../../../shared/src/theme"
import BackButton from "./BackButton"
interface OnboardingContainerProps {
children: React.ReactNode
onBack: () => void
showBackButton?: boolean
}
const OnboardingContainer: React.FC<OnboardingContainerProps> = ({ children, onBack, showBackButton = true }) => {
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<LinearGradient
colors={Colors.backgroundGradient}
style={styles.container}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
{showBackButton && (
<View style={styles.header}>
<BackButton onPress={onBack} />
</View>
)}
<View style={styles.content}>{children}</View>
</LinearGradient>
</TouchableWithoutFeedback>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
paddingTop: Spacing.md,
paddingHorizontal: Spacing.lg,
paddingBottom: Spacing.md,
},
content: {
flex: 1,
paddingHorizontal: Spacing.lg,
justifyContent: "center",
},
})
export default OnboardingContainer
/*
* End of File: OnboardingContainer.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,158 @@
"use client"
/*
* File: PasswordScreen.tsx
* Description: Password creation screen with gradient background and shared components.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import type React from "react"
import { useState } from "react"
import { View, Text, StyleSheet, TextInput, TouchableOpacity } from "react-native"
import { Button } from "../../../../shared/src/components/Button"
import OnboardingContainer from "./OnboardingContainer"
import IconContainer from "./IconContainer"
import { Colors, Spacing, Typography } from "../../../../shared/src/theme"
import { showError } from "../../../../shared/src/utils/helpers/Toast"
import Icon from "react-native-vector-icons/Feather"
interface PasswordScreenProps {
onContinue: (password: string) => void
onBack: () => void
}
const PasswordScreen: React.FC<PasswordScreenProps> = ({ onContinue, onBack }) => {
const [password, setPassword] = useState("")
const [confirmPassword, setConfirmPassword] = useState("")
const [isPasswordVisible, setPasswordVisible] = useState(false)
const [isConfirmPasswordVisible, setConfirmPasswordVisible] = useState(false)
const [loading, setLoading] = useState(false)
const validatePassword = (pwd: string): boolean => {
return pwd.length >= 8
}
const handleContinue = () => {
if (!password.trim() || !confirmPassword.trim()) {
showError("Validation Error", "Both password fields are required.")
return
}
if (!validatePassword(password)) {
showError("Validation Error", "Password must be at least 8 characters long.")
return
}
if (password !== confirmPassword) {
showError("Validation Error", "Passwords do not match.")
return
}
setLoading(true)
setTimeout(() => {
setLoading(false)
onContinue(password)
}, 1000)
}
return (
<OnboardingContainer onBack={onBack}>
<IconContainer iconName="lock" />
<Text style={styles.title}>Create a password</Text>
<Text style={styles.subtitle}>Password must be at least 8 characters</Text>
<View style={styles.inputContainer}>
<Icon name="lock" size={20} color={Colors.textSecondary} style={styles.inputIcon} />
<TextInput
placeholder="Password"
value={password}
onChangeText={setPassword}
style={styles.inputField}
secureTextEntry={!isPasswordVisible}
placeholderTextColor={Colors.textMuted}
/>
<TouchableOpacity onPress={() => setPasswordVisible(!isPasswordVisible)} style={styles.eyeIcon}>
<Icon name={isPasswordVisible ? "eye-off" : "eye"} size={22} color={Colors.textSecondary} />
</TouchableOpacity>
</View>
<View style={styles.inputContainer}>
<Icon name="lock" size={20} color={Colors.textSecondary} style={styles.inputIcon} />
<TextInput
placeholder="Confirm Password"
value={confirmPassword}
onChangeText={setConfirmPassword}
style={styles.inputField}
secureTextEntry={!isConfirmPasswordVisible}
placeholderTextColor={Colors.textMuted}
/>
<TouchableOpacity onPress={() => setConfirmPasswordVisible(!isConfirmPasswordVisible)} style={styles.eyeIcon}>
<Icon name={isConfirmPasswordVisible ? "eye-off" : "eye"} size={22} color={Colors.textSecondary} />
</TouchableOpacity>
</View>
<Button title="Continue" onPress={handleContinue} style={styles.button} loading={loading} />
</OnboardingContainer>
)
}
const styles = StyleSheet.create({
title: {
fontFamily: Typography.fontFamily.bold,
fontSize: Typography.fontSize.title,
color: Colors.textPrimary,
textAlign: "center",
marginBottom: Spacing.sm,
},
subtitle: {
fontFamily: Typography.fontFamily.regular,
fontSize: Typography.fontSize.md,
color: Colors.textSecondary,
textAlign: "center",
marginBottom: Spacing.xl,
},
inputContainer: {
flexDirection: "row",
alignItems: "center",
backgroundColor: Colors.inputBackground,
borderWidth: 1,
borderColor: Colors.border,
borderRadius: 12,
marginBottom: Spacing.md,
paddingHorizontal: Spacing.md,
paddingVertical: 2,
shadowColor: Colors.backButtonShadow,
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 1,
},
inputIcon: {
marginRight: Spacing.sm,
},
inputField: {
flex: 1,
paddingVertical: Spacing.md,
fontSize: Typography.fontSize.md,
color: Colors.textPrimary,
},
eyeIcon: {
paddingLeft: Spacing.sm,
},
button: {
marginTop: Spacing.xl,
},
})
export default PasswordScreen
/*
* End of File: PasswordScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -9,10 +9,12 @@ import React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { LoginScreen, SetupBiometricScreen } from '../screens'; import { LoginScreen, SetupBiometricScreen } from '../screens';
import { Colors } from '../../../../shared/src/theme'; import { Colors } from '../../../../shared/src/theme';
import SignUpScreen from '../screens/SignUpScreen';
export type AuthStackParamList = { export type AuthStackParamList = {
Login: undefined; Login: undefined;
SetupBiometric: undefined; SetupBiometric: undefined;
SignUpScreen:undefined;
}; };
const Stack = createNativeStackNavigator<AuthStackParamList>(); const Stack = createNativeStackNavigator<AuthStackParamList>();
@ -37,7 +39,12 @@ const AuthNavigator: React.FC = () => (
<Stack.Screen <Stack.Screen
name="SetupBiometric" name="SetupBiometric"
component={SetupBiometricScreen} component={SetupBiometricScreen}
options={{ title: 'Setup Biometric' }} options={{ title: 'Setup Biometric' ,headerShown:false}}
/>
<Stack.Screen
name="SignUpScreen"
component={SignUpScreen}
options={{ title: 'Setup Biometric' ,headerShown:false}}
/> />
</Stack.Navigator> </Stack.Navigator>
); );

View File

@ -8,6 +8,7 @@
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import { loginStart, loginSuccess, loginFailure } from './authSlice'; import { loginStart, loginSuccess, loginFailure } from './authSlice';
import { authAPI } from '../services/authAPI'; import { authAPI } from '../services/authAPI';
import { showError, showSuccess } from '../../../../shared/src/utils/helpers/Toast';
/** /**
* Thunk to login user * Thunk to login user
@ -19,6 +20,12 @@ export const login = createAsyncThunk(
dispatch(loginStart()); dispatch(loginStart());
const response:any = await authAPI.login(credentials.email, credentials.password,'web'); const response:any = await authAPI.login(credentials.email, credentials.password,'web');
console.log('user response',response) console.log('user response',response)
if(response.data.message&& !response.data.success){
showError(response.data.message)
}
if(response.data.message&& response.data.success){
showSuccess(response.data.message)
}
if (response.ok && response.data &&response.data.data) { if (response.ok && response.data &&response.data.data) {
//@ts-ignore //@ts-ignore
dispatch(loginSuccess({...response.data.data.user,access_token:response.data.data.access_token})); dispatch(loginSuccess({...response.data.data.user,access_token:response.data.data.access_token}));
@ -33,6 +40,7 @@ export const login = createAsyncThunk(
} }
); );
/* /*
* End of File: authActions.ts * End of File: authActions.ts
* Design & Developed by Tech4Biz Solutions * Design & Developed by Tech4Biz Solutions

View File

@ -11,6 +11,7 @@ export const selectUser = (state: RootState) => state.auth.user;
export const selectAuthLoading = (state: RootState) => state.auth.loading; export const selectAuthLoading = (state: RootState) => state.auth.loading;
export const selectAuthError = (state: RootState) => state.auth.error; export const selectAuthError = (state: RootState) => state.auth.error;
export const selectIsAuthenticated = (state: RootState) => state.auth.isAuthenticated; export const selectIsAuthenticated = (state: RootState) => state.auth.isAuthenticated;
export const selectHospitals = (state: RootState) => state.hospital.hospitals;
/* /*
* End of File: authSelectors.ts * End of File: authSelectors.ts

View File

@ -0,0 +1,55 @@
/*
* File: authSlice.ts
* Description: Redux slice for Auth state management
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface Hospital {
hospital_id: string | null;
hospital_name: string | null;
}
// Auth state type
interface HospitalState {
hospitals: Hospital[] | null;
loading: boolean;
error: string | null;
}
const initialState: HospitalState = {
hospitals: null,
loading: false,
error: null,
};
/**
* Auth slice for managing authentication state
*/
const hospitalSlice = createSlice({
name: 'hospital',
initialState,
reducers: {
setHospitals(state, action: PayloadAction<Hospital[]>) {
state.hospitals = action.payload;
state.loading = false;
},
},
});
export const {
setHospitals
} = hospitalSlice.actions;
export default hospitalSlice.reducer;
/*
* End of File: authSlice.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -22,6 +22,7 @@ import { login } from '../redux/authActions';
import { showError } from '../../../../shared/src/utils/helpers/Toast'; import { showError } from '../../../../shared/src/utils/helpers/Toast';
import { validateEmail } from '../../../../shared/src/utils/validation/validators'; import { validateEmail } from '../../../../shared/src/utils/validation/validators';
import Icon from 'react-native-vector-icons/Feather'; import Icon from 'react-native-vector-icons/Feather';
import { useNavigation } from '@react-navigation/native';
const LoginScreen: React.FC = () => { const LoginScreen: React.FC = () => {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
@ -29,6 +30,7 @@ const LoginScreen: React.FC = () => {
const [isPasswordVisible, setPasswordVisible] = useState(false); const [isPasswordVisible, setPasswordVisible] = useState(false);
const [loading, setLoading] = useState(false); // Add loading state const [loading, setLoading] = useState(false); // Add loading state
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigation=useNavigation();
const handleLogin = () => { const handleLogin = () => {
if (!email.trim() || !password.trim()) { if (!email.trim() || !password.trim()) {
@ -52,7 +54,7 @@ const LoginScreen: React.FC = () => {
return ( return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}> <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.title}>Radiologist Portal</Text> <Text style={styles.title}>Radiologist</Text>
{/* Email Input */} {/* Email Input */}
<View style={styles.inputContainer}> <View style={styles.inputContainer}>
@ -92,6 +94,14 @@ const LoginScreen: React.FC = () => {
style={styles.button} style={styles.button}
loading={loading} // Pass loading state to button loading={loading} // Pass loading state to button
/> />
<Text style={{alignSelf:'center',marginTop:15}}>-----OR------</Text>
<Button
title="Sign Up"
onPress={()=>navigation.navigate('SignUpScreen')}
style={styles.button}
/>
</View> </View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
); );

View File

@ -0,0 +1,192 @@
"use client"
/*
* File: SignUpScreen.tsx
* Description: Main signup screen that manages the onboarding flow through multiple steps.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import type React from "react"
import { useEffect, useState } from "react"
import { View, StyleSheet, StatusBar, Platform } from "react-native"
import { Colors } from "../../../../shared/src/theme"
import { useAppDispatch } from "../../../redux/hooks"
import { showError, showSuccess } from "../../../../shared/src/utils/helpers/Toast"
// Import all onboarding screens
import EmailScreen from "../components/EmailScreen"
import PasswordScreen from "../components/PasswordScreen"
import NameScreen from "../components/NameScreen"
import DocumentUploadScreen from "../components/DocumentUploadScreen"
import HospitalSelectionScreen from "../components/HospitalSelectionScreen"
import { authAPI } from "../services/authAPI"
import { setHospitals } from "../redux/hospitalSlice"
interface SignUpData {
email: string
password: string
first_name: string
last_name: string
username:string
id_photo_url: {}
hospital_id: string
}
interface SignUpScreenProps {
navigation: any
}
const SignUpScreen: React.FC<SignUpScreenProps> = ({ navigation }) => {
const [currentStep, setCurrentStep] = useState(0)
const [signUpData, setSignUpData] = useState<Partial<SignUpData>>({})
const dispatch = useAppDispatch()
const steps = ["email", "password", "name", "document", "hospital"]
const handleEmailContinue = (email: string) => {
setSignUpData((prev) => ({ ...prev, email }))
setCurrentStep(1)
}
useEffect(()=>{
fetchHospitals();
},[])
console.log('signup called')
const fetchHospitals= async ()=>{
try {
const response:any = await authAPI.gethospitals();
console.log('hospital response',response)
if(response.ok && response.data &&response.data.data) {
//@ts-ignore
dispatch(setHospitals(response.data.data))
} else {
showError('error while fetching hospital list')
}
} catch (error: any) {
console.log('error',error)
}
}
const onSignUpComplete=async (payload:SignUpData)=>{
console.log('final payload',payload);
try {
const formData = new FormData();
let role='radiologist'
formData.append('email',payload.email);
formData.append('password', payload.password);
formData.append('first_name', payload.first_name);
formData.append('last_name', payload.last_name);
formData.append('username', payload.username);
formData.append('dashboard_role',role);
formData.append('hospital_id', payload.hospital_id);
// Attach file (Node.js or React Native: use File or Blob API accordingly)
const filePath = payload.id_photo_url.uri;
const file = {
uri: Platform.OS === 'android' ? filePath! : filePath!.replace('file://', ''), // for React Native
name: 'id_photo',
type: 'image/jpg',
};
formData.append('id_photo_url', file);
console.log('payload prepared',formData);
const response:any = await authAPI.signup(formData);
console.log('signup response',response)
if(response.ok && response.data && response.data.success ) {
//@ts-ignore
showSuccess('Sign Up Successfully')
navigation.navigate('Login');
// dispatch(setHospitals(response.data.data))
} else {
showError('error while signup')
}
} catch (error: any) {
console.log('error',error)
}
}
const onBack=()=>{
navigation.goBack()
}
const handlePasswordContinue = (password: string) => {
setSignUpData((prev) => ({ ...prev, password }))
setCurrentStep(2)
}
const handleNameContinue = (firstName: string, lastName: string,userName:string) => {
setSignUpData((prev) => ({ ...prev, first_name : firstName, last_name : lastName ,username: userName}))
setCurrentStep(3)
}
const handleDocumentContinue = (documentUri: string) => {
setSignUpData((prev) => ({ ...prev, id_photo_url:documentUri }))
setCurrentStep(4)
}
const handleHospitalContinue = (hospitalId: string) => {
const finalData: SignUpData = {
...signUpData,
hospital_id:hospitalId,
} as SignUpData
setSignUpData(finalData)
// Show success message
// Call completion handler
if (onSignUpComplete) {
onSignUpComplete(finalData)
}
// Here you would typically dispatch a signup action
// dispatch(signUp(finalData))
}
const handleBack = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1)
} else if (onBack) {
onBack()
}
}
const renderCurrentStep = () => {
switch (currentStep) {
case 0:
return <EmailScreen onContinue={handleEmailContinue} onBack={handleBack} />
case 1:
return <PasswordScreen onContinue={handlePasswordContinue} onBack={handleBack} />
case 2:
return <NameScreen onContinue={handleNameContinue} onBack={handleBack} />
case 3:
return <DocumentUploadScreen onContinue={handleDocumentContinue} onBack={handleBack} />
case 4:
return <HospitalSelectionScreen onContinue={handleHospitalContinue} onBack={handleBack} />
default:
return <EmailScreen onContinue={handleEmailContinue} onBack={handleBack} />
}
}
return (
<View style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor={Colors.background} />
{renderCurrentStep()}
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: Colors.background,
},
})
export default SignUpScreen
/*
* End of File: SignUpScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -11,7 +11,6 @@ import { buildHeaders } from '../../../../shared/src/utils/helpers/Api';
const api = create({ const api = create({
baseURL: BASE_URL, // TODO: Replace with actual endpoint baseURL: BASE_URL, // TODO: Replace with actual endpoint
timeout: 3000,
}); });
/** /**
@ -19,6 +18,10 @@ const api = create({
*/ */
export const authAPI = { export const authAPI = {
login: (email: string, password: string,platform:string) => api.post('/api/auth/auth/login', { email, password,platform },buildHeaders()), login: (email: string, password: string,platform:string) => api.post('/api/auth/auth/login', { email, password,platform },buildHeaders()),
//fetch hospital list
gethospitals: () => api.get('/api/hospitals/hospitals/app_user/hospitals', {},buildHeaders()),
//user signup
signup: (formData:any) => api.post('/api/auth/auth/admin/create-user-fromapp', formData,buildHeaders({ contentType: 'multipart/form-data' })),
// Add more endpoints as needed // Add more endpoints as needed
}; };

View File

@ -37,7 +37,7 @@ export const fetchPatients = createAsyncThunk(
try { try {
dispatch(fetchCasesStart()); dispatch(fetchCasesStart());
const response :any = await caseAPI.getPatients(payload); const response :any = await caseAPI.getPatients(payload);
console.log('response i got',response) // console.log('response i got',response)
if (response.ok && response.data && response.data.data) { if (response.ok && response.data && response.data.data) {
dispatch(fetchPatientsSuccess(response.data.data)); dispatch(fetchPatientsSuccess(response.data.data));
} }

View File

@ -364,7 +364,7 @@ const styles = StyleSheet.create({
backgroundColor: Colors.background, backgroundColor: Colors.background,
}, },
content: { content: {
padding: Spacing.lg, padding: Spacing.md,
}, },
summaryCard: { summaryCard: {
marginBottom: Spacing.lg, marginBottom: Spacing.lg,

View File

@ -101,7 +101,7 @@ const DashboardScreen: React.FC<{ navigation: any }> = ({ navigation }) => {
filteredCases.forEach(case_ => { filteredCases.forEach(case_ => {
const type = case_.type; const type = case_.type;
const modality = case_.patientdetails.Modality; const modality = case_.patientdetails?.Modality;
stats[type].total += 1; stats[type].total += 1;
stats[type].modalityCounts[modality?.substring(1)] += 1; stats[type].modalityCounts[modality?.substring(1)] += 1;
@ -151,7 +151,6 @@ const DashboardScreen: React.FC<{ navigation: any }> = ({ navigation }) => {
const color = getCaseColor(type); const color = getCaseColor(type);
if (stats.total === 0) return null; // Don't render card if no cases of this type if (stats.total === 0) return null; // Don't render card if no cases of this type
console.log('counts data',stats.modalityCounts)
return ( return (
<Card key={type} style={styles.caseCard}> <Card key={type} style={styles.caseCard}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
@ -187,7 +186,6 @@ console.log('counts data',stats.modalityCounts)
); );
}; };
console.log('patients data', patientData);
return ( return (
<View style={styles.container}> <View style={styles.container}>

View File

@ -23,7 +23,6 @@ export const caseAPI = {
getCases: (token:string) => api.get('/api/dicom/medpacks-sync/get-synced-medpacks-data',{},buildHeaders({token})), getCases: (token:string) => api.get('/api/dicom/medpacks-sync/get-synced-medpacks-data',{},buildHeaders({token})),
// get patients data // get patients data
getPatients: (token:string) =>{ getPatients: (token:string) =>{
console.log('token',token)
return api.get('/api/dicom/medpacks-sync/get-synced-medpacks-data',{},buildHeaders({token})) return api.get('/api/dicom/medpacks-sync/get-synced-medpacks-data',{},buildHeaders({token}))
} }
// Add more endpoints as needed // Add more endpoints as needed

View File

@ -12,6 +12,7 @@ import settingsReducer from '../modules/Profile/redux/settingsSlice';
// Add other slices as needed, e.g.: // Add other slices as needed, e.g.:
import dashboardReducer from '../modules/Dashboard/redux/dashboardSlice'; import dashboardReducer from '../modules/Dashboard/redux/dashboardSlice';
import authReducer from '../modules/Auth/redux/authSlice'; import authReducer from '../modules/Auth/redux/authSlice';
import hospitalReducer from '../modules/Auth/redux/hospitalSlice';
// RootState type for use throughout the app // RootState type for use throughout the app
export type RootState = ReturnType<typeof rootReducer>; export type RootState = ReturnType<typeof rootReducer>;
@ -21,7 +22,8 @@ const rootReducer = combineReducers({
profile: profileReducer, profile: profileReducer,
settings: settingsReducer, settings: settingsReducer,
dashboard:dashboardReducer, dashboard:dashboardReducer,
auth:authReducer auth:authReducer,
hospital:hospitalReducer
// Add other reducers here // Add other reducers here
}); });

View File

@ -7,16 +7,34 @@
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer'; import rootReducer from './rootReducer';
import { persistStore, persistReducer, PersistConfig } from 'redux-persist';
import storage from '@react-native-async-storage/async-storage';
const persistConfig: PersistConfig<any> = {
key: 'root',
storage,
version: 1,
whitelist: ['auth']
};
// Configure the Redux store // Configure the Redux store
const store = configureStore({ const persistedReducer = persistReducer(persistConfig, rootReducer);
reducer: rootReducer,
// You can add middleware, devTools, and other options here if needed export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['persist/PERSIST'], // Ignore persist actions to avoid the non-serializable warning
},
}),
}); });
// Export store and types for use throughout the app // Export store and types for use throughout the app
export type AppDispatch = typeof store.dispatch; export type AppDispatch = typeof store.dispatch;
export default store; export const persistor = persistStore(store);
/* /*
* End of File: store.ts * End of File: store.ts

50
package-lock.json generated
View File

@ -24,7 +24,9 @@
"react-native": "0.79.0", "react-native": "0.79.0",
"react-native-biometrics": "^3.0.1", "react-native-biometrics": "^3.0.1",
"react-native-blob-util": "^0.22.2", "react-native-blob-util": "^0.22.2",
"react-native-chart-kit": "^6.12.0",
"react-native-config": "^1.5.5", "react-native-config": "^1.5.5",
"react-native-element-dropdown": "^2.12.4",
"react-native-gesture-handler": "^2.22.1", "react-native-gesture-handler": "^2.22.1",
"react-native-image-picker": "^7.2.3", "react-native-image-picker": "^7.2.3",
"react-native-keychain": "^10.0.0", "react-native-keychain": "^10.0.0",
@ -9781,7 +9783,6 @@
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.debounce": { "node_modules/lodash.debounce": {
@ -11154,6 +11155,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/paths-js": {
"version": "0.4.11",
"resolved": "https://registry.npmjs.org/paths-js/-/paths-js-0.4.11.tgz",
"integrity": "sha512-3mqcLomDBXOo7Fo+UlaenG6f71bk1ZezPQy2JCmYHy2W2k5VKpP+Jbin9H0bjXynelTbglCqdFhSEkeIkKTYUA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.11.0"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -11250,6 +11260,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/point-in-polygon": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.1.0.tgz",
"integrity": "sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==",
"license": "MIT"
},
"node_modules/possible-typed-array-names": { "node_modules/possible-typed-array-names": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@ -11664,6 +11680,22 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/react-native-chart-kit": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/react-native-chart-kit/-/react-native-chart-kit-6.12.0.tgz",
"integrity": "sha512-nZLGyCFzZ7zmX0KjYeeSV1HKuPhl1wOMlTAqa0JhlyW62qV/1ZPXHgT8o9s8mkFaGxdqbspOeuaa6I9jUQDgnA==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.13",
"paths-js": "^0.4.10",
"point-in-polygon": "^1.0.1"
},
"peerDependencies": {
"react": "> 16.7.0",
"react-native": ">= 0.50.0",
"react-native-svg": "> 6.4.1"
}
},
"node_modules/react-native-config": { "node_modules/react-native-config": {
"version": "1.5.5", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/react-native-config/-/react-native-config-1.5.5.tgz", "resolved": "https://registry.npmjs.org/react-native-config/-/react-native-config-1.5.5.tgz",
@ -11678,6 +11710,22 @@
} }
} }
}, },
"node_modules/react-native-element-dropdown": {
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/react-native-element-dropdown/-/react-native-element-dropdown-2.12.4.tgz",
"integrity": "sha512-abZc5SVji9FIt7fjojRYrbuvp03CoeZJrgvezQoDoSOrpiTqkX69ix5m+j06W2AVncA0VWvbT+vCMam8SoVadw==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.21"
},
"engines": {
"node": ">= 16.0.0"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-gesture-handler": { "node_modules/react-native-gesture-handler": {
"version": "2.27.1", "version": "2.27.1",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.27.1.tgz", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.27.1.tgz",

View File

@ -26,7 +26,9 @@
"react-native": "0.79.0", "react-native": "0.79.0",
"react-native-biometrics": "^3.0.1", "react-native-biometrics": "^3.0.1",
"react-native-blob-util": "^0.22.2", "react-native-blob-util": "^0.22.2",
"react-native-chart-kit": "^6.12.0",
"react-native-config": "^1.5.5", "react-native-config": "^1.5.5",
"react-native-element-dropdown": "^2.12.4",
"react-native-gesture-handler": "^2.22.1", "react-native-gesture-handler": "^2.22.1",
"react-native-image-picker": "^7.2.3", "react-native-image-picker": "^7.2.3",
"react-native-keychain": "^10.0.0", "react-native-keychain": "^10.0.0",

View File

@ -43,9 +43,9 @@ const Button: React.FC<ButtonProps> = ({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
backgroundColor: Colors.primary, backgroundColor: Colors.primary,
paddingVertical: Spacing.md, paddingVertical: Spacing.lg,
paddingHorizontal: Spacing.lg, paddingHorizontal: Spacing.lg,
borderRadius: BorderRadius.md, borderRadius: BorderRadius.xl,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },

View File

@ -27,6 +27,33 @@ export const Colors = {
inactiveState: '#F3F4F6', // Light Gray inactiveState: '#F3F4F6', // Light Gray
shadow: 'rgba(91, 124, 230, 0.1)', // Blue Shadow shadow: 'rgba(91, 124, 230, 0.1)', // Blue Shadow
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', // Gradient Background gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', // Gradient Background
// Primary colors
primaryDark: "#7C3AED",
primaryLight: "#A78BFA",
// Gradient colors
gradientStart: "#E0E7FF",
gradientMiddle: "#DDD6FE",
gradientEnd: "#E9D5FF",
// Background colors
backgroundGradient: ["#E0E7FF", "#DDD6FE", "#E9D5FF"],
// Text colors
textOnPrimary: "#FFFFFF",
// Border and input colors
borderFocus: "#8B5CF6",
inputBackground: "#FFFFFF",
// Back button
backButtonBackground: "#FFFFFF",
backButtonShadow: "rgba(0, 0, 0, 0.1)",
}; };
// Usage: import { Colors } from './colors'; // Usage: import { Colors } from './colors';