signup flow added for radiologist
This commit is contained in:
parent
df5616eaf5
commit
29940278e6
165
.cursor/rules/appflow.mdc
Normal file
165
.cursor/rules/appflow.mdc
Normal 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 |
|
||||
+------------------------+
|
||||
34
.cursor/rules/filestructure.mdc
Normal file
34
.cursor/rules/filestructure.mdc
Normal 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.
|
||||
@ -1,3 +0,0 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
397
.cursor/rules/projectstructure.mdc
Normal file
397
.cursor/rules/projectstructure.mdc
Normal 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
|
||||
66
.cursor/rules/themestructure.mdc
Normal file
66
.cursor/rules/themestructure.mdc
Normal 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
4
.env
@ -1,2 +1,2 @@
|
||||
BASE_URL='https://neoscan-backend.tech4bizsolutions.com'
|
||||
# BASE_URL='http://192.168.1.87:3000'
|
||||
# BASE_URL='https://neoscan-backend.tech4bizsolutions.com'
|
||||
BASE_URL='http://192.168.1.87:3000'
|
||||
10
App.tsx
10
App.tsx
@ -16,13 +16,14 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
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 {
|
||||
Colors,
|
||||
} from 'react-native/Libraries/NewAppScreen';
|
||||
import TabNavigator from './app/navigation/TabNavigator';
|
||||
import AppNavigator from './app/navigation/AppNavigator';
|
||||
import Toast from 'react-native-toast-message'; // Import Toast
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
|
||||
type SectionProps = PropsWithChildren<{
|
||||
title: string;
|
||||
@ -77,6 +78,7 @@ function App(): React.JSX.Element {
|
||||
// Wrap the app with Redux Provider to enable global state management
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<View style={styles.container}>
|
||||
<StatusBar
|
||||
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
|
||||
@ -84,7 +86,11 @@ function App(): React.JSX.Element {
|
||||
/>
|
||||
<AppNavigator />
|
||||
</View>
|
||||
<Toast />
|
||||
<Toast
|
||||
position='bottom'
|
||||
bottomOffset={20}
|
||||
/>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<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
|
||||
android:name=".MainApplication"
|
||||
|
||||
53
app/modules/Auth/components/BackButton.tsx
Normal file
53
app/modules/Auth/components/BackButton.tsx
Normal 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.
|
||||
*/
|
||||
401
app/modules/Auth/components/DocumentUploadScreen.tsx
Normal file
401
app/modules/Auth/components/DocumentUploadScreen.tsx
Normal 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.
|
||||
*/
|
||||
127
app/modules/Auth/components/EmailScreen.tsx
Normal file
127
app/modules/Auth/components/EmailScreen.tsx
Normal 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.
|
||||
*/
|
||||
231
app/modules/Auth/components/HospitalSelectionScreen.tsx
Normal file
231
app/modules/Auth/components/HospitalSelectionScreen.tsx
Normal 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.
|
||||
*/
|
||||
76
app/modules/Auth/components/IconContainer.tsx
Normal file
76
app/modules/Auth/components/IconContainer.tsx
Normal 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.
|
||||
*/
|
||||
164
app/modules/Auth/components/NameScreen.tsx
Normal file
164
app/modules/Auth/components/NameScreen.tsx
Normal 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.
|
||||
*/
|
||||
64
app/modules/Auth/components/OnboardingContainer.tsx
Normal file
64
app/modules/Auth/components/OnboardingContainer.tsx
Normal 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.
|
||||
*/
|
||||
158
app/modules/Auth/components/PasswordScreen.tsx
Normal file
158
app/modules/Auth/components/PasswordScreen.tsx
Normal 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.
|
||||
*/
|
||||
@ -9,10 +9,12 @@ import React from 'react';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import { LoginScreen, SetupBiometricScreen } from '../screens';
|
||||
import { Colors } from '../../../../shared/src/theme';
|
||||
import SignUpScreen from '../screens/SignUpScreen';
|
||||
|
||||
export type AuthStackParamList = {
|
||||
Login: undefined;
|
||||
SetupBiometric: undefined;
|
||||
SignUpScreen:undefined;
|
||||
};
|
||||
|
||||
const Stack = createNativeStackNavigator<AuthStackParamList>();
|
||||
@ -37,7 +39,12 @@ const AuthNavigator: React.FC = () => (
|
||||
<Stack.Screen
|
||||
name="SetupBiometric"
|
||||
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>
|
||||
);
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { loginStart, loginSuccess, loginFailure } from './authSlice';
|
||||
import { authAPI } from '../services/authAPI';
|
||||
import { showError, showSuccess } from '../../../../shared/src/utils/helpers/Toast';
|
||||
|
||||
/**
|
||||
* Thunk to login user
|
||||
@ -19,6 +20,12 @@ export const login = createAsyncThunk(
|
||||
dispatch(loginStart());
|
||||
const response:any = await authAPI.login(credentials.email, credentials.password,'web');
|
||||
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) {
|
||||
//@ts-ignore
|
||||
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
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
|
||||
@ -11,6 +11,7 @@ export const selectUser = (state: RootState) => state.auth.user;
|
||||
export const selectAuthLoading = (state: RootState) => state.auth.loading;
|
||||
export const selectAuthError = (state: RootState) => state.auth.error;
|
||||
export const selectIsAuthenticated = (state: RootState) => state.auth.isAuthenticated;
|
||||
export const selectHospitals = (state: RootState) => state.hospital.hospitals;
|
||||
|
||||
/*
|
||||
* End of File: authSelectors.ts
|
||||
|
||||
55
app/modules/Auth/redux/hospitalSlice.ts
Normal file
55
app/modules/Auth/redux/hospitalSlice.ts
Normal 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.
|
||||
*/
|
||||
@ -22,6 +22,7 @@ import { login } from '../redux/authActions';
|
||||
import { showError } from '../../../../shared/src/utils/helpers/Toast';
|
||||
import { validateEmail } from '../../../../shared/src/utils/validation/validators';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
const LoginScreen: React.FC = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
@ -29,6 +30,7 @@ const LoginScreen: React.FC = () => {
|
||||
const [isPasswordVisible, setPasswordVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false); // Add loading state
|
||||
const dispatch = useAppDispatch();
|
||||
const navigation=useNavigation();
|
||||
|
||||
const handleLogin = () => {
|
||||
if (!email.trim() || !password.trim()) {
|
||||
@ -52,7 +54,7 @@ const LoginScreen: React.FC = () => {
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Radiologist Portal</Text>
|
||||
<Text style={styles.title}>Radiologist</Text>
|
||||
|
||||
{/* Email Input */}
|
||||
<View style={styles.inputContainer}>
|
||||
@ -92,6 +94,14 @@ const LoginScreen: React.FC = () => {
|
||||
style={styles.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>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
|
||||
192
app/modules/Auth/screens/SignUpScreen.tsx
Normal file
192
app/modules/Auth/screens/SignUpScreen.tsx
Normal 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.
|
||||
*/
|
||||
@ -11,7 +11,6 @@ import { buildHeaders } from '../../../../shared/src/utils/helpers/Api';
|
||||
|
||||
const api = create({
|
||||
baseURL: BASE_URL, // TODO: Replace with actual endpoint
|
||||
timeout: 3000,
|
||||
});
|
||||
|
||||
/**
|
||||
@ -19,6 +18,10 @@ const api = create({
|
||||
*/
|
||||
export const authAPI = {
|
||||
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
|
||||
};
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ export const fetchPatients = createAsyncThunk(
|
||||
try {
|
||||
dispatch(fetchCasesStart());
|
||||
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) {
|
||||
dispatch(fetchPatientsSuccess(response.data.data));
|
||||
}
|
||||
|
||||
@ -364,7 +364,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: Colors.background,
|
||||
},
|
||||
content: {
|
||||
padding: Spacing.lg,
|
||||
padding: Spacing.md,
|
||||
},
|
||||
summaryCard: {
|
||||
marginBottom: Spacing.lg,
|
||||
|
||||
@ -101,7 +101,7 @@ const DashboardScreen: React.FC<{ navigation: any }> = ({ navigation }) => {
|
||||
|
||||
filteredCases.forEach(case_ => {
|
||||
const type = case_.type;
|
||||
const modality = case_.patientdetails.Modality;
|
||||
const modality = case_.patientdetails?.Modality;
|
||||
|
||||
stats[type].total += 1;
|
||||
stats[type].modalityCounts[modality?.substring(1)] += 1;
|
||||
@ -151,7 +151,6 @@ const DashboardScreen: React.FC<{ navigation: any }> = ({ navigation }) => {
|
||||
const color = getCaseColor(type);
|
||||
|
||||
if (stats.total === 0) return null; // Don't render card if no cases of this type
|
||||
console.log('counts data',stats.modalityCounts)
|
||||
return (
|
||||
<Card key={type} style={styles.caseCard}>
|
||||
<View style={styles.cardHeader}>
|
||||
@ -187,7 +186,6 @@ console.log('counts data',stats.modalityCounts)
|
||||
);
|
||||
};
|
||||
|
||||
console.log('patients data', patientData);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
||||
@ -23,7 +23,6 @@ export const caseAPI = {
|
||||
getCases: (token:string) => api.get('/api/dicom/medpacks-sync/get-synced-medpacks-data',{},buildHeaders({token})),
|
||||
// get patients data
|
||||
getPatients: (token:string) =>{
|
||||
console.log('token',token)
|
||||
return api.get('/api/dicom/medpacks-sync/get-synced-medpacks-data',{},buildHeaders({token}))
|
||||
}
|
||||
// Add more endpoints as needed
|
||||
|
||||
@ -12,6 +12,7 @@ import settingsReducer from '../modules/Profile/redux/settingsSlice';
|
||||
// Add other slices as needed, e.g.:
|
||||
import dashboardReducer from '../modules/Dashboard/redux/dashboardSlice';
|
||||
import authReducer from '../modules/Auth/redux/authSlice';
|
||||
import hospitalReducer from '../modules/Auth/redux/hospitalSlice';
|
||||
|
||||
// RootState type for use throughout the app
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
@ -21,7 +22,8 @@ const rootReducer = combineReducers({
|
||||
profile: profileReducer,
|
||||
settings: settingsReducer,
|
||||
dashboard:dashboardReducer,
|
||||
auth:authReducer
|
||||
auth:authReducer,
|
||||
hospital:hospitalReducer
|
||||
// Add other reducers here
|
||||
});
|
||||
|
||||
|
||||
@ -7,16 +7,34 @@
|
||||
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
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
|
||||
const store = configureStore({
|
||||
reducer: rootReducer,
|
||||
// You can add middleware, devTools, and other options here if needed
|
||||
const persistedReducer = persistReducer(persistConfig, rootReducer);
|
||||
|
||||
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 type AppDispatch = typeof store.dispatch;
|
||||
export default store;
|
||||
export const persistor = persistStore(store);
|
||||
|
||||
/*
|
||||
* End of File: store.ts
|
||||
|
||||
50
package-lock.json
generated
50
package-lock.json
generated
@ -24,7 +24,9 @@
|
||||
"react-native": "0.79.0",
|
||||
"react-native-biometrics": "^3.0.1",
|
||||
"react-native-blob-util": "^0.22.2",
|
||||
"react-native-chart-kit": "^6.12.0",
|
||||
"react-native-config": "^1.5.5",
|
||||
"react-native-element-dropdown": "^2.12.4",
|
||||
"react-native-gesture-handler": "^2.22.1",
|
||||
"react-native-image-picker": "^7.2.3",
|
||||
"react-native-keychain": "^10.0.0",
|
||||
@ -9781,7 +9783,6 @@
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
@ -11154,6 +11155,15 @@
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@ -11250,6 +11260,12 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "1.5.5",
|
||||
"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": {
|
||||
"version": "2.27.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.27.1.tgz",
|
||||
|
||||
@ -26,7 +26,9 @@
|
||||
"react-native": "0.79.0",
|
||||
"react-native-biometrics": "^3.0.1",
|
||||
"react-native-blob-util": "^0.22.2",
|
||||
"react-native-chart-kit": "^6.12.0",
|
||||
"react-native-config": "^1.5.5",
|
||||
"react-native-element-dropdown": "^2.12.4",
|
||||
"react-native-gesture-handler": "^2.22.1",
|
||||
"react-native-image-picker": "^7.2.3",
|
||||
"react-native-keychain": "^10.0.0",
|
||||
|
||||
@ -43,9 +43,9 @@ const Button: React.FC<ButtonProps> = ({
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
backgroundColor: Colors.primary,
|
||||
paddingVertical: Spacing.md,
|
||||
paddingVertical: Spacing.lg,
|
||||
paddingHorizontal: Spacing.lg,
|
||||
borderRadius: BorderRadius.md,
|
||||
borderRadius: BorderRadius.xl,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
@ -27,6 +27,33 @@ export const Colors = {
|
||||
inactiveState: '#F3F4F6', // Light Gray
|
||||
shadow: 'rgba(91, 124, 230, 0.1)', // Blue Shadow
|
||||
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';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user