commit a09073a99ddc31a3d1d22a6689c6aad0b96e7c78 Author: yashwin-foxy Date: Wed Aug 20 20:42:33 2025 +0530 first commit diff --git a/.bundle/config b/.bundle/config new file mode 100644 index 0000000..848943b --- /dev/null +++ b/.bundle/config @@ -0,0 +1,2 @@ +BUNDLE_PATH: "vendor/bundle" +BUNDLE_FORCE_RUBY_PLATFORM: 1 diff --git a/.cursor/rules/appflow.mdc b/.cursor/rules/appflow.mdc new file mode 100644 index 0000000..4747c45 --- /dev/null +++ b/.cursor/rules/appflow.mdc @@ -0,0 +1,351 @@ +--- +alwaysApply: true +--- +# Physician App - Application Flow Rules + +## ๐Ÿš€ App Launch & Authentication Flow + +### 1. Initial App Launch +``` +App Launch โ†’ Splash Screen โ†’ Authentication Check + โ†“ +[Authenticated] โ†’ ER Dashboard +[Not Authenticated] โ†’ Login Screen +``` + +### 2. Authentication Flow Rules +``` +Login Screen Options: +โ”œโ”€โ”€ Hospital SSO Login (Primary) +โ”œโ”€โ”€ Credential Login (Fallback) +โ”œโ”€โ”€ Emergency Access (Quick) +โ””โ”€โ”€ Temporary Login (Limited) +``` + +### 3. SSO Integration Rules +- **Primary**: Hospital SSO integration +- **Fallback**: Username/password authentication +- **Emergency**: Quick access codes for urgent situations +- **Remember Device**: 30-day device authentication +- **Session Timeout**: 8 hours for security + +## ๐Ÿ“Š ER Dashboard Flow + +### 4. Dashboard Entry Points +``` +ER Dashboard โ†’ Load Patient List โ†’ Real-time Updates + โ†“ +โ”œโ”€โ”€ Critical Alerts (Priority 1) +โ”œโ”€โ”€ Pending Scans (Priority 2) +โ”œโ”€โ”€ Recent Reports (Priority 3) +โ””โ”€โ”€ All Patients (Complete View) +``` + +### 5. Real-time Data Flow +``` +WebSocket Connection โ†’ Live Updates + โ†“ +โ”œโ”€โ”€ Patient Status Changes +โ”œโ”€โ”€ New Scan Results +โ”œโ”€โ”€ Critical Findings +โ”œโ”€โ”€ Bed Assignments +โ””โ”€โ”€ Shift Changes +``` + +### 6. Alert Priority System +- **๐Ÿ”ด Critical**: Immediate action required (0-2 minutes) +- **๐ŸŸก Warning**: Attention needed (2-10 minutes) +- **๐ŸŸข Info**: Routine updates (10+ minutes) +- **๐Ÿ”ต Status**: General information + +## ๐Ÿšจ Critical Finding Response Workflow + +### 7. Critical Alert Reception +``` +Push Notification โ†’ Alert Screen โ†’ Patient Details + โ†“ +AI Summary โ†’ Image Review โ†’ Treatment Protocol + โ†“ +Consultation โ†’ Action โ†’ Documentation +``` + +### 8. Alert Response Timeline +- **0-30 seconds**: Alert received & acknowledged +- **30-60 seconds**: Patient details reviewed +- **1-2 minutes**: AI findings assessed +- **2-5 minutes**: Treatment decision made +- **5+ minutes**: Action initiated & documented + +### 9. Critical Alert Components +``` +Alert Screen Elements: +โ”œโ”€โ”€ Patient Identification +โ”œโ”€โ”€ Bed Location +โ”œโ”€โ”€ Critical Finding Type +โ”œโ”€โ”€ AI-Generated Summary +โ”œโ”€โ”€ Urgency Level +โ”œโ”€โ”€ Time Stamp +โ””โ”€โ”€ Action Buttons +``` + +## ๐Ÿฅ Patient Care Flow + +### 10. Patient Selection Flow +``` +Patient List โ†’ Patient Card โ†’ Patient Details + โ†“ +โ”œโ”€โ”€ Vital Signs +โ”œโ”€โ”€ Medical History +โ”œโ”€โ”€ Current Medications +โ”œโ”€โ”€ Allergy Information +โ”œโ”€โ”€ Bed Information +โ””โ”€โ”€ Admission Details +``` + +### 11. Patient Detail Navigation +``` +Patient Details Screen: +โ”œโ”€โ”€ Demographics (Top) +โ”œโ”€โ”€ Vital Signs (Real-time) +โ”œโ”€โ”€ Medical History (Expandable) +โ”œโ”€โ”€ Current Medications (List) +โ”œโ”€โ”€ Allergy Information (Alert) +โ”œโ”€โ”€ Bed Information (Status) +โ””โ”€โ”€ Action Buttons (Bottom) +``` + +### 12. Medical Record Integration +- **EMR Sync**: Real-time patient data +- **Vital Signs**: Live monitoring integration +- **Medication History**: Pharmacy system sync +- **Lab Results**: Laboratory system integration +- **Imaging**: PACS system connection + +## ๐Ÿ“ฑ Screen Navigation Patterns + +### 13. Primary Navigation Structure +``` +Bottom Tab Navigation: +โ”œโ”€โ”€ Dashboard (Home) +โ”œโ”€โ”€ Patients +โ”œโ”€โ”€ Alerts +โ”œโ”€โ”€ Reports +โ””โ”€โ”€ Settings +``` + +### 14. Secondary Navigation +``` +Stack Navigation per Tab: +โ”œโ”€โ”€ List View โ†’ Detail View +โ”œโ”€โ”€ Detail View โ†’ Action View +โ”œโ”€โ”€ Action View โ†’ Confirmation +โ””โ”€โ”€ Confirmation โ†’ Return to List +``` + +### 15. Modal Navigation Rules +- **Quick Actions**: Modal overlays +- **Critical Actions**: Full-screen modals +- **Confirmation**: Alert modals +- **Settings**: Sheet modals + +## ๐Ÿ”„ State Management Flow + +### 16. Redux State Structure +``` +Root State: +โ”œโ”€โ”€ Auth (Authentication state) +โ”œโ”€โ”€ Dashboard (ER dashboard data) +โ”œโ”€โ”€ PatientCare (Patient information) +โ”œโ”€โ”€ Alerts (Notification system) +โ”œโ”€โ”€ Settings (User preferences) +โ””โ”€โ”€ UI (Interface state) +``` + +### 17. Data Flow Patterns +``` +API Call โ†’ Redux Action โ†’ State Update โ†’ UI Re-render + โ†“ +WebSocket โ†’ Real-time Update โ†’ Immediate UI Change + โ†“ +User Action โ†’ Local State โ†’ API Call โ†’ Server Sync +``` + +### 18. Caching Strategy +- **Patient Data**: 15-minute cache +- **Critical Alerts**: No cache (real-time) +- **User Settings**: Persistent storage +- **Medical Records**: 5-minute cache + +## ๐Ÿ“‹ Workflow Rules + +### 19. Critical Finding Workflow +``` +Step 1: Alert Reception +โ”œโ”€โ”€ Push notification received +โ”œโ”€โ”€ Alert screen displayed +โ”œโ”€โ”€ Patient context loaded +โ””โ”€โ”€ AI summary generated + +Step 2: Assessment +โ”œโ”€โ”€ Patient details reviewed +โ”œโ”€โ”€ Medical history checked +โ”œโ”€โ”€ Current status assessed +โ””โ”€โ”€ Urgency level determined + +Step 3: Action Planning +โ”œโ”€โ”€ Treatment protocol loaded +โ”œโ”€โ”€ Specialist consultation initiated +โ”œโ”€โ”€ Emergency procedures prepared +โ””โ”€โ”€ Documentation started + +Step 4: Implementation +โ”œโ”€โ”€ Actions executed +โ”œโ”€โ”€ Status updated +โ”œโ”€โ”€ Team notified +โ””โ”€โ”€ Record documented +``` + +### 20. Routine Scan Processing +``` +Step 1: Report Notification +โ”œโ”€โ”€ Scan completion notification +โ”œโ”€โ”€ Report status update +โ”œโ”€โ”€ AI findings summary +โ””โ”€โ”€ Priority assignment + +Step 2: Review Process +โ”œโ”€โ”€ Report details loaded +โ”œโ”€โ”€ Images reviewed +โ”œโ”€โ”€ Findings assessed +โ””โ”€โ”€ Action plan created + +Step 3: Documentation +โ”œโ”€โ”€ Patient record updated +โ”œโ”€โ”€ Treatment plan documented +โ”œโ”€โ”€ Follow-up scheduled +โ””โ”€โ”€ Discharge planning initiated +``` + +## ๐Ÿ” Security & Access Control + +### 21. Authentication Rules +- **Session Management**: 8-hour timeout +- **Auto-logout**: Inactivity after 30 minutes +- **Device Remembering**: 30-day trusted devices +- **Emergency Access**: Limited functionality +- **Audit Trail**: All actions logged + +### 22. Permission Levels +``` +User Roles: +โ”œโ”€โ”€ ER Physician (Full Access) +โ”œโ”€โ”€ Resident (Limited Access) +โ”œโ”€โ”€ Medical Student (Read-only) +โ”œโ”€โ”€ Emergency Access (Critical Only) +โ””โ”€โ”€ Temporary Access (Time-limited) +``` + +### 23. Data Access Rules +- **Patient Data**: Role-based access +- **Critical Alerts**: All ER staff +- **Medical Records**: Authorized personnel only +- **Settings**: User-specific +- **Audit Logs**: Admin only + +## ๐Ÿ“Š Performance & Optimization + +### 24. Loading States +``` +Loading Hierarchy: +โ”œโ”€โ”€ Critical Alerts (Immediate) +โ”œโ”€โ”€ Patient List (Fast) +โ”œโ”€โ”€ Patient Details (Medium) +โ”œโ”€โ”€ Medical History (Medium) +โ””โ”€โ”€ Full Reports (Slow) +``` + +### 25. Offline Capabilities +- **Critical Alerts**: Always available +- **Patient List**: Cached data +- **Recent Reports**: Offline access +- **Settings**: Local storage +- **Sync**: Automatic when online + +### 26. Error Handling +``` +Error Recovery: +โ”œโ”€โ”€ Network Errors โ†’ Retry with backoff +โ”œโ”€โ”€ Authentication Errors โ†’ Re-login +โ”œโ”€โ”€ Data Errors โ†’ Fallback to cache +โ”œโ”€โ”€ Critical Errors โ†’ Emergency mode +โ””โ”€โ”€ UI Errors โ†’ Graceful degradation +``` + +## ๐ŸŽฏ User Experience Rules + +### 27. Interaction Patterns +- **Critical Actions**: Confirmation required +- **Quick Actions**: One-tap execution +- **Navigation**: Intuitive flow +- **Feedback**: Immediate response +- **Accessibility**: WCAG 2.1 compliance + +### 28. Visual Hierarchy +``` +Priority Order: +โ”œโ”€โ”€ Critical Alerts (Red, Large) +โ”œโ”€โ”€ Active Patients (Blue, Medium) +โ”œโ”€โ”€ Pending Items (Yellow, Medium) +โ”œโ”€โ”€ Completed Items (Green, Small) +โ””โ”€โ”€ Background Info (Gray, Small) +``` + +### 29. Responsive Design +- **Mobile First**: Optimized for phones +- **Tablet Support**: Enhanced layouts +- **Landscape Mode**: Alternative views +- **Accessibility**: Voice commands support + +## ๐Ÿ”„ Data Synchronization + +### 30. Real-time Updates +``` +Update Types: +โ”œโ”€โ”€ Patient Status (Immediate) +โ”œโ”€โ”€ Vital Signs (30-second intervals) +โ”œโ”€โ”€ Alert Status (Real-time) +โ”œโ”€โ”€ Bed Assignments (Real-time) +โ””โ”€โ”€ Report Status (5-minute intervals) +``` + +### 31. Conflict Resolution +- **Server Priority**: Server data overrides local +- **Timestamp Comparison**: Latest data wins +- **User Confirmation**: Manual resolution for conflicts +- **Audit Trail**: All changes tracked + +## ๐Ÿ“ฑ Device Integration + +### 32. Hardware Integration +- **Camera**: Document scanning +- **Microphone**: Voice notes +- **Biometrics**: Secure access +- **NFC**: Patient identification +- **Bluetooth**: Medical device connection + +### 33. Platform-Specific Features +``` +iOS Features: +โ”œโ”€โ”€ Face ID authentication +โ”œโ”€โ”€ Apple Health integration +โ”œโ”€โ”€ Siri shortcuts +โ””โ”€โ”€ iOS notifications + +Android Features: +โ”œโ”€โ”€ Fingerprint authentication +โ”œโ”€โ”€ Google Fit integration +โ”œโ”€โ”€ Android Auto +โ””โ”€โ”€ Android notifications +``` + +This comprehensive flow ensures efficient, secure, and user-friendly operation of the Physician App in emergency medical scenarios. \ No newline at end of file diff --git a/.cursor/rules/projectstructurerule.mdc b/.cursor/rules/projectstructurerule.mdc new file mode 100644 index 0000000..259f26a --- /dev/null +++ b/.cursor/rules/projectstructurerule.mdc @@ -0,0 +1,308 @@ +--- +alwaysApply: true +--- +# Physician App - Project Structure & File Naming Rules + +## ๐Ÿ“ Directory Structure Rules + +### 1. Root Level Organization +``` +NeoScan_Physician/ +โ”œโ”€โ”€ app/ # Main application code +โ”œโ”€โ”€ docs/ # Documentation +โ”œโ”€โ”€ android/ # Android native code +โ”œโ”€โ”€ ios/ # iOS native code +โ”œโ”€โ”€ index.js # React Native entry point +โ”œโ”€โ”€ package.json # Dependencies +โ”œโ”€โ”€ tsconfig.json # TypeScript config +โ”œโ”€โ”€ metro.config.js # Metro bundler config +โ”œโ”€โ”€ babel.config.js # Babel config +โ””โ”€โ”€ .eslintrc.js # ESLint config +``` + +### 2. App Directory Structure +``` +app/ +โ”œโ”€โ”€ modules/ # Feature-based modules +โ”œโ”€โ”€ shared/ # Shared utilities & components +โ”œโ”€โ”€ store/ # Redux store configuration +โ”œโ”€โ”€ navigation/ # Navigation setup +โ”œโ”€โ”€ theme/ # Styling & theming +โ”œโ”€โ”€ config/ # Configuration files +โ”œโ”€โ”€ assets/ # Static assets +โ”œโ”€โ”€ localization/ # i18n +โ”œโ”€โ”€ App.tsx # Root component +โ””โ”€โ”€ index.tsx # App entry point +``` + +## ๐Ÿ—๏ธ Module Architecture Rules + +### 3. Module Structure (Feature-based) +Each module MUST follow this structure: +``` +modules/ModuleName/ +โ”œโ”€โ”€ components/ # Reusable UI components +โ”œโ”€โ”€ screens/ # Screen components +โ”œโ”€โ”€ hooks/ # Custom hooks +โ”œโ”€โ”€ redux/ # State management +โ”œโ”€โ”€ services/ # API & external services +โ”œโ”€โ”€ __tests__/ # Test files +โ””โ”€โ”€ index.ts # Module exports +``` + +### 4. Required Modules +- **Auth/** - Authentication & SSO +- **Dashboard/** - ER Dashboard & patient tracking +- **PatientCare/** - Patient details & medical records +- **Settings/** - User preferences & app settings + +## ๐Ÿ“ File Naming Conventions + +### 5. Component Files +- **PascalCase** for all component files +- **Suffix with type**: `.tsx` for components, `.ts` for utilities +- **Examples**: + - `LoginScreen.tsx` + - `PatientCard.tsx` + - `CriticalAlerts.tsx` + - `HospitalSSO.tsx` + +### 6. Hook Files +- **camelCase** with `use` prefix +- **Examples**: + - `useAuth.ts` + - `usePatientList.ts` + - `useRealTimeAlerts.ts` + - `useCriticalAlerts.ts` + +### 7. Service Files +- **camelCase** with descriptive names +- **Suffix with type**: `API.ts`, `Service.ts` +- **Examples**: + - `authAPI.ts` + - `patientCareAPI.ts` + - `notificationService.ts` + - `emrIntegration.ts` + +### 8. Redux Files +- **camelCase** with descriptive suffixes +- **Examples**: + - `authSlice.ts` + - `erDashboardSlice.ts` + - `patientCareActions.ts` + - `dashboardSelectors.ts` + +### 9. Test Files +- **Same name as source file** + `.test.ts` or `.test.tsx` +- **Examples**: + - `LoginScreen.test.tsx` + - `useAuth.test.ts` + - `authSlice.test.ts` + - `PatientCard.test.tsx` + +## ๐Ÿ”ง Shared Components Rules + +### 10. UI Components Structure +``` +shared/components/ +โ”œโ”€โ”€ UI/ # Basic UI components +โ”‚ โ”œโ”€โ”€ Button.tsx +โ”‚ โ”œโ”€โ”€ Input.tsx +โ”‚ โ”œโ”€โ”€ Card.tsx +โ”‚ โ”œโ”€โ”€ Modal.tsx +โ”‚ โ”œโ”€โ”€ Badge.tsx +โ”‚ โ”œโ”€โ”€ Spinner.tsx +โ”‚ โ”œโ”€โ”€ Alert.tsx +โ”‚ โ”œโ”€โ”€ Dropdown.tsx +โ”‚ โ”œโ”€โ”€ Tabs.tsx +โ”‚ โ”œโ”€โ”€ ProgressBar.tsx +โ”‚ โ””โ”€โ”€ index.ts +โ”œโ”€โ”€ Forms/ # Form-related components +โ”‚ โ”œโ”€โ”€ FormField.tsx +โ”‚ โ”œโ”€โ”€ ValidationMessage.tsx +โ”‚ โ”œโ”€โ”€ FormContainer.tsx +โ”‚ โ””โ”€โ”€ index.ts +โ”œโ”€โ”€ Icons/ # Icon components +โ”‚ โ”œโ”€โ”€ MedicalIcons.tsx +โ”‚ โ”œโ”€โ”€ StatusIcons.tsx +โ”‚ โ”œโ”€โ”€ NavigationIcons.tsx +โ”‚ โ””โ”€โ”€ index.ts +โ””โ”€โ”€ index.ts +``` + +### 11. Utility Files +``` +shared/utils/ +โ”œโ”€โ”€ api.ts # API utilities +โ”œโ”€โ”€ constants.ts # App constants +โ”œโ”€โ”€ helpers.ts # Helper functions +โ”œโ”€โ”€ validators.ts # Validation functions +โ”œโ”€โ”€ formatters.ts # Data formatting +โ”œโ”€โ”€ dateUtils.ts # Date utilities +โ”œโ”€โ”€ medicalUtils.ts # Medical-specific utilities +โ”œโ”€โ”€ imageUtils.ts # Image processing +โ”œโ”€โ”€ stringUtils.ts # String manipulation +โ””โ”€โ”€ index.ts +``` + +## ๐ŸŽจ Assets Organization + +### 12. Image Assets +``` +assets/images/ +โ”œโ”€โ”€ logos/ # Hospital & app logos +โ”œโ”€โ”€ icons/ # UI icons +โ”‚ โ”œโ”€โ”€ medical/ # Medical-specific icons +โ”‚ โ”œโ”€โ”€ ui/ # General UI icons +โ”‚ โ””โ”€โ”€ status/ # Status indicators +โ”œโ”€โ”€ backgrounds/ # Background images +โ””โ”€โ”€ placeholders/ # Placeholder images +``` + +### 13. Asset Naming +- **kebab-case** for all asset files +- **Examples**: + - `hospital-logo.png` + - `critical-alert.mp3` + - `ct-scan-placeholder.png` + - `emergency-bg.jpg` + +## ๐Ÿ“ฑ Navigation Structure + +### 14. Navigation Files +``` +navigation/ +โ”œโ”€โ”€ AppNavigator.tsx # Root navigator +โ”œโ”€โ”€ AuthNavigator.tsx # Authentication flow +โ”œโ”€โ”€ MainNavigator.tsx # Main app flow +โ”œโ”€โ”€ TabNavigator.tsx # Tab navigation +โ”œโ”€โ”€ navigationTypes.ts # Type definitions +โ”œโ”€โ”€ navigationUtils.ts # Navigation utilities +โ””โ”€โ”€ __tests__/ +``` + +## ๐Ÿ” Configuration Rules + +### 15. Environment & Config +``` +config/ +โ”œโ”€โ”€ env.ts # Environment variables +โ”œโ”€โ”€ api.ts # API configuration +โ”œโ”€โ”€ websocket.ts # WebSocket config +โ”œโ”€โ”€ notifications.ts # Notification config +โ”œโ”€โ”€ security.ts # Security settings +โ””โ”€โ”€ index.ts +``` + +## ๐Ÿ“š Documentation Rules + +### 16. Documentation Structure +``` +docs/ +โ”œโ”€โ”€ README.md # Project overview +โ”œโ”€โ”€ ARCHITECTURE.md # Architecture documentation +โ”œโ”€โ”€ API.md # API documentation +โ”œโ”€โ”€ DEPLOYMENT.md # Deployment guide +โ”œโ”€โ”€ TESTING.md # Testing guidelines +โ”œโ”€โ”€ SECURITY.md # Security guidelines +โ””โ”€โ”€ wireframes/ # UI wireframes +``` + +## ๐Ÿงช Testing Rules + +### 17. Test Organization +- **Unit tests** alongside source files in `__tests__/` folders +- **Integration tests** in module-level `__tests__/` folders +- **E2E tests** in root-level `__tests__/` folder +- **Test utilities** in `shared/__tests__/` + +## ๐Ÿ“ฆ Package Management + +### 18. Dependencies Organization +- **Core dependencies** in root `package.json` +- **Platform-specific** dependencies in respective folders +- **Dev dependencies** clearly separated +- **Peer dependencies** explicitly declared + +## ๐Ÿ”„ Import/Export Rules + +### 19. Import Conventions +- **Absolute imports** for shared utilities +- **Relative imports** for module-internal files +- **Index files** for clean imports +- **Barrel exports** for module APIs + +### 20. Export Patterns +```typescript +// Module index.ts +export { default as ComponentName } from './components/ComponentName'; +export { useHookName } from './hooks/useHookName'; +export { actionName } from './redux/actions'; +export type { TypeName } from './types'; +``` + +## ๐Ÿšซ Naming Restrictions + +### 21. Forbidden Patterns +- โŒ No spaces in file names +- โŒ No special characters except `-` and `_` +- โŒ No uppercase in utility/service files +- โŒ No generic names like `utils.ts` or `helpers.ts` +- โŒ No abbreviations unless universally understood + +### 22. Required Patterns +- โœ… Descriptive, self-documenting names +- โœ… Consistent casing within categories +- โœ… Clear separation of concerns +- โœ… Meaningful directory structure +- โœ… Proper TypeScript extensions + +## ๐Ÿ“‹ File Size Guidelines + +### 23. Component Limits +- **Single component files**: Max 300 lines +- **Complex components**: Split into smaller components +- **Utility files**: Max 200 lines +- **Service files**: Max 150 lines per service + +### 24. Module Limits +- **Module components**: Max 10 files per category +- **Module services**: Max 5 files +- **Module hooks**: Max 8 files +- **Module redux**: Max 6 files + +## ๐Ÿ” Code Organization Principles + +### 25. Separation of Concerns +- **UI Logic** in components +- **Business Logic** in hooks/services +- **State Management** in Redux +- **Data Fetching** in services +- **Utilities** in shared/utils + +### 26. Reusability +- **Shared components** in shared/components +- **Common utilities** in shared/utils +- **Type definitions** in shared/types +- **Constants** in shared/constants + +This structure ensures maintainability, scalability, and consistency across the Physician App codebase. + + +and it should add proper comments in the each 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. \ No newline at end of file diff --git a/.cursor/rules/themeflow.mdc b/.cursor/rules/themeflow.mdc new file mode 100644 index 0000000..ca7c425 --- /dev/null +++ b/.cursor/rules/themeflow.mdc @@ -0,0 +1,718 @@ +--- +alwaysApply: true +--- +# Physician App - Theme & UI Design Rules + +## ๐ŸŽจ Color Palette - "Modern Healthcare Blue" + +### 1. Primary Color Scheme +```typescript +// Primary Colors +Primary: '#2196F3' // Material Blue - Main brand color +Secondary: '#1976D2' // Darker Blue - Secondary actions +Tertiary: '#E3F2FD' // Very Light Blue - Backgrounds +Quaternary: '#0D47A1' // Deep Blue - Accents + +// Text Colors +TextPrimary: '#212121' // Dark Gray - Main text +TextSecondary: '#757575' // Medium Gray - Secondary text +TextMuted: '#9E9E9E' // Light Gray - Muted text + +// Background Colors +Background: '#FFFFFF' // White - Primary background +BackgroundAlt: '#FAFAFA' // Light Gray - Alternative background +BackgroundAccent: '#F5F5F5' // Soft Gray - Accent backgrounds +``` + +### 2. Status & Feedback Colors +```typescript +// Status Colors +Success: '#4CAF50' // Material Green - Success states +Warning: '#FF9800' // Material Orange - Warning states +Error: '#F44336' // Material Red - Error states +Info: '#2196F3' // Material Blue - Information states + +// UI Elements +Border: '#E0E0E0' // Light Gray Border +CardBackground: '#FFFFFF' // White - Card backgrounds +Shadow: 'rgba(0, 0, 0, 0.1)' // Subtle Gray Shadow +``` + +## ๐Ÿ—๏ธ Typography System + +### 3. Font Hierarchy +```typescript +// Font Families +PrimaryFont: 'Roboto' // Main font family +SecondaryFont: 'Medical-Icons' // Icon font + +// Font Weights +Light: 300 +Regular: 400 +Medium: 500 +Bold: 700 + +// Font Sizes +DisplayLarge: 32px // Main headings +DisplayMedium: 24px // Section headings +DisplaySmall: 20px // Subsection headings +BodyLarge: 16px // Body text +BodyMedium: 14px // Secondary text +BodySmall: 12px // Captions +Caption: 10px // Small labels +``` + +### 4. Line Heights & Spacing +```typescript +// Line Heights +Tight: 1.2 // Headings +Normal: 1.4 // Body text +Relaxed: 1.6 // Long text + +// Letter Spacing +Tight: -0.5px // Headings +Normal: 0px // Body text +Wide: 0.5px // Labels +``` + +## ๐Ÿ“ Spacing & Layout + +### 5. Spacing Scale +```typescript +// Base Spacing Unit: 4px +Spacing: { + xs: 4, // 4px + sm: 8, // 8px + md: 16, // 16px + lg: 24, // 24px + xl: 32, // 32px + xxl: 48, // 48px + xxxl: 64 // 64px +} +``` + +### 6. Layout Rules +```typescript +// Container Max Widths +Mobile: 375 +Tablet: 768 +Desktop: 1024 + +// Border Radius +Small: 4 +Medium: 8 +Large: 12 +XLarge: 16 +Round: 50 + +// Shadows (React Native StyleSheet) +Small: { + shadowColor: '#000000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2 +} + +Medium: { + shadowColor: '#000000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 4 +} + +Large: { + shadowColor: '#000000', + shadowOffset: { width: 0, height: 8 }, + shadowOpacity: 0.2, + shadowRadius: 16, + elevation: 8 +} +``` + +## ๐ŸŽฏ Component Design Rules + +### 7. Button Design System +```typescript +// Button Variants +Primary: { + backgroundColor: '#2196F3', + borderColor: '#2196F3', + borderRadius: 8, + shadowColor: '#2196F3', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 3 +} + +Secondary: { + backgroundColor: 'transparent', + borderColor: '#2196F3', + borderRadius: 8 +} + +Success: { + backgroundColor: '#4CAF50', + borderColor: '#4CAF50', + borderRadius: 8, + shadowColor: '#4CAF50', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 3 +} + +Critical: { + backgroundColor: '#F44336', + borderColor: '#F44336', + borderRadius: 8, + shadowColor: '#F44336', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 3 +} + +// Button Sizes +Small: { paddingHorizontal: 16, paddingVertical: 8, fontSize: 14, borderRadius: 6 } +Medium: { paddingHorizontal: 24, paddingVertical: 12, fontSize: 16, borderRadius: 8 } +Large: { paddingHorizontal: 32, paddingVertical: 16, fontSize: 18, borderRadius: 10 } +``` + +### 8. Card Design Rules +```typescript +// Card Variants +Default: { + backgroundColor: '#FFFFFF', + borderColor: '#E0E0E0', + borderRadius: 12, + padding: 16, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2 +} + +Elevated: { + backgroundColor: '#FFFFFF', + borderColor: '#E0E0E0', + borderRadius: 12, + padding: 16, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 4 +} + +Critical: { + backgroundColor: '#FFEBEE', + borderColor: '#F44336', + borderRadius: 12, + padding: 16, + shadowColor: '#F44336', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2 +} + +PatientCard: { + backgroundColor: '#FFFFFF', + borderColor: '#E0E0E0', + borderRadius: 16, + padding: 20, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.08, + shadowRadius: 4, + elevation: 2 +} +``` + +### 9. Input Field Design +```typescript +// Input States +Default: { + borderColor: '#E0E0E0', + backgroundColor: '#FFFFFF', + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 12 +} + +Focused: { + borderColor: '#2196F3', + backgroundColor: '#FFFFFF', + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 12 +} + +Error: { + borderColor: '#F44336', + backgroundColor: '#FFEBEE', + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 12 +} + +Disabled: { + borderColor: '#E0E0E0', + backgroundColor: '#F5F5F5', + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 12 +} +``` + +## ๐Ÿšจ Alert & Status Design (Following Screenshot UI) + +### 10. Alert Priority System +```typescript +// Critical Alerts +Critical: { + backgroundColor: '#FFEBEE', + borderColor: '#F44336', + borderRadius: 8, + padding: 16 +} + +// Warning Alerts +Warning: { + backgroundColor: '#FFF3E0', + borderColor: '#FF9800', + borderRadius: 8, + padding: 16 +} + +// Success Alerts +Success: { + backgroundColor: '#E8F5E8', + borderColor: '#4CAF50', + borderRadius: 8, + padding: 16 +} + +// Info Alerts +Info: { + backgroundColor: '#E3F2FD', + borderColor: '#2196F3', + borderRadius: 8, + padding: 16 +} + +// Patient Status +Active: { + backgroundColor: '#E3F2FD', + borderColor: '#2196F3', + borderRadius: 12, + paddingHorizontal: 8, + paddingVertical: 4 +} + +Pending: { + backgroundColor: '#FFF3E0', + borderColor: '#FF9800', + borderRadius: 12, + paddingHorizontal: 8, + paddingVertical: 4 +} + +Completed: { + backgroundColor: '#E8F5E8', + borderColor: '#4CAF50', + borderRadius: 12, + paddingHorizontal: 8, + paddingVertical: 4 +} +``` + +### 11. Badge & Tag Design (Following Screenshot UI) +```typescript +// Status Badges +StatusBadge: { + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + fontSize: 12, + fontWeight: '500' +} + +// Priority Badges +PriorityBadge: { + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 16, + fontSize: 12, + fontWeight: '600' +} + +// Medical Badges +MedicalBadge: { + paddingHorizontal: 10, + paddingVertical: 5, + borderRadius: 14, + fontSize: 11, + fontWeight: '500' +} +``` + +## ๐Ÿ“ฑ Screen-Specific Design Rules + +### 12. ER Dashboard Screen Design (Following Screenshot UI) +```typescript +// Header Section +Header: { + backgroundColor: '#FFFFFF', + paddingHorizontal: 16, + paddingVertical: 12, + borderBottomColor: '#E0E0E0', + borderBottomWidth: 1 +} + +// Critical Alerts Section (Like promotional banner but for emergencies) +CriticalAlerts: { + backgroundColor: '#FFEBEE', + borderColor: '#F44336', + borderRadius: 16, + padding: 20, + marginHorizontal: 16, + marginVertical: 12, + shadowColor: '#F44336', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 4 +} + +// Patient List Section (Like specialist filter) +PatientList: { + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: '#FFFFFF' +} + +// Quick Actions (Like service icons but for emergency actions) +QuickActions: { + width: 60, + height: 60, + borderRadius: 30, + backgroundColor: '#F5F5F5', + justifyContent: 'center', + alignItems: 'center', + marginHorizontal: 8 +} + +// Patient Cards (Like doctor cards but for patients) +PatientCard: { + backgroundColor: '#FFFFFF', + borderRadius: 16, + padding: 16, + marginHorizontal: 8, + marginVertical: 4, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.08, + shadowRadius: 4, + elevation: 2, + minWidth: 200 +} +``` + +### 13. Patient Details Screen Design (Following Screenshot UI) +```typescript +// Patient Header +PatientHeader: { + backgroundColor: '#FFFFFF', + padding: 20, + borderBottomColor: '#E0E0E0', + borderBottomWidth: 1 +} + +// Patient Info Card +PatientInfoCard: { + backgroundColor: '#FFFFFF', + borderRadius: 16, + padding: 20, + margin: 16, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 4 +} + +// Vital Signs Section (Like contact options but for medical data) +VitalSigns: { + flexDirection: 'row', + justifyContent: 'space-around', + paddingVertical: 16, + borderBottomColor: '#E0E0E0', + borderBottomWidth: 1 +} + +// Medical History Section (Like calendar section but for medical records) +MedicalHistory: { + backgroundColor: '#FFFFFF', + borderRadius: 12, + padding: 16, + margin: 16, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.08, + shadowRadius: 4, + elevation: 2 +} + +// Action Buttons (Like booking button but for medical actions) +ActionButton: { + backgroundColor: '#2196F3', + borderRadius: 12, + padding: 16, + margin: 16, + alignItems: 'center', + shadowColor: '#2196F3', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 6 +} +``` + +### 14. Login Screen Design (Following Screenshot UI) +```typescript +// Login Container +LoginContainer: { + backgroundColor: '#F1FDFF', + padding: 24, + borderRadius: 12, + shadowColor: '#2196F3', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 4 +} + +// Hospital Logo +Logo: { + width: 120, + height: 120, + marginBottom: 32 +} + +// Form Fields +FormField: { + marginBottom: 16, + backgroundColor: '#FFFFFF', + borderColor: '#E0E0E0', + borderRadius: 8 +} + +// Login Button +LoginButton: { + backgroundColor: '#2196F3', + borderRadius: 8, + padding: 16, + fontSize: 16, + fontWeight: '600' +} +``` + +## ๐ŸŽจ Icon Design System + +### 15. Icon Specifications +```typescript +// Icon Sizes +IconSizes: { + xs: 12, // Extra small + sm: 16, // Small + md: 24, // Medium (default) + lg: 32, // Large + xl: 48 // Extra large +} + +// Icon Colors +IconColors: { + Primary: '#2196F3', + Secondary: '#1976D2', + Success: '#4CAF50', + Warning: '#FF9800', + Error: '#F44336', + Info: '#2196F3', + Muted: '#9E9E9E' +} + +// Medical Icons +MedicalIcons: { + Stethoscope: 'medical-stethoscope', + Heart: 'medical-heart', + Brain: 'medical-brain', + Emergency: 'medical-emergency', + Patient: 'medical-patient', + Bed: 'medical-bed', + Alert: 'medical-alert', + VitalSigns: 'medical-vital-signs', + Medication: 'medical-medication', + Lab: 'medical-lab', + Imaging: 'medical-imaging', + Surgery: 'medical-surgery' +} +``` + +## ๐Ÿ“Š Data Visualization + +### 16. Chart Colors +```typescript +// Chart Color Palette +ChartColors: { + Primary: '#2196F3', + Secondary: '#1976D2', + Tertiary: '#E3F2FD', + Quaternary: '#0D47A1', + Success: '#4CAF50', + Warning: '#FF9800', + Error: '#F44336' +} + +// Vital Signs Charts +VitalSignsChart: { + backgroundColor: '#FFFFFF', + gridColor: '#E0E0E0', + lineColor: '#2196F3', + pointColor: '#1976D2' +} + +// Patient Status Charts +PatientStatusChart: { + backgroundColor: '#FFFFFF', + gridColor: '#E0E0E0', + lineColor: '#4CAF50', + pointColor: '#2E7D32' +} +``` + +## ๐ŸŒ™ Dark Mode Support + +### 17. Dark Mode Colors +```typescript +// Dark Mode Palette +DarkMode: { + Background: '#121212', + BackgroundAlt: '#1E1E1E', + BackgroundAccent: '#2D2D2D', + TextPrimary: '#FFFFFF', + TextSecondary: '#B0B0B0', + TextMuted: '#808080', + Border: '#404040', + CardBackground: '#1E1E1E', + Primary: '#2196F3', + Secondary: '#1976D2' +} +``` + +## ๐Ÿ“ฑ Responsive Design + +### 18. Breakpoint System +```typescript +// Breakpoints +Breakpoints: { + Mobile: 375, + Tablet: 768, + Desktop: 1024, + LargeDesktop: 1440 +} + +// Responsive Spacing +ResponsiveSpacing: { + Mobile: { padding: 16, margin: 8 }, + Tablet: { padding: 24, margin: 16 }, + Desktop: { padding: 32, margin: 24 } +} +``` + +## ๐ŸŽฏ Accessibility Rules + +### 19. Accessibility Standards +```typescript +// Color Contrast +ContrastRatios: { + Normal: 4.5, // Minimum for normal text + Large: 3.0, // Minimum for large text + UI: 3.0 // Minimum for UI elements +} + +// Touch Targets +TouchTargets: { + Minimum: 44, // Minimum touch target size + Preferred: 48 // Preferred touch target size +} + +// Focus Indicators +FocusIndicator: { + Color: '#2196F3', + Width: 2, + Style: 'solid' +} +``` + +## ๐Ÿ”„ Animation & Transitions + +### 20. Animation Rules +```typescript +// Animation Durations +Durations: { + Fast: 150, // Quick interactions + Normal: 300, // Standard transitions + Slow: 500 // Complex animations +} + +// Easing Functions +Easing: { + Standard: 'cubic-bezier(0.4, 0.0, 0.2, 1)', + Deceleration: 'cubic-bezier(0.0, 0.0, 0.2, 1)', + Acceleration: 'cubic-bezier(0.4, 0.0, 1, 1)' +} + +// Transition Types +Transitions: { + Fade: { opacity: [0, 1], duration: 300 }, + Slide: { transform: [{ translateY: [20, 0] }], duration: 300 }, + Scale: { transform: [{ scale: [0.95, 1] }], duration: 200 } +} +``` + +## ๐ŸŽจ Design Tokens + +### 21. Design Token Structure +```typescript +// Theme Object Structure +Theme: { + colors: ColorPalette, + typography: TypographySystem, + spacing: SpacingScale, + borderRadius: BorderRadiusScale, + shadows: ShadowSystem, + breakpoints: BreakpointSystem, + animations: AnimationSystem +} + +// Usage Example +const styles = StyleSheet.create({ + container: { + backgroundColor: theme.colors.background, + padding: theme.spacing.md, + borderRadius: theme.borderRadius.medium, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2 + } +}); +``` + +This comprehensive theme system ensures consistency, accessibility, and modern healthcare aesthetics across the Physician App, following the clean and professional design patterns shown in the reference screenshots while maintaining the ER workflow functionality. \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..aa3551d --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +BASE_URL='https://neoscan-backend.tech4bizsolutions.com' +# BASE_URL='http://192.168.1.87:3000' \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..187894b --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: '@react-native', +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de99955 --- /dev/null +++ b/.gitignore @@ -0,0 +1,75 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +**/.xcode.env.local + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof +.cxx/ +*.keystore +!debug.keystore +.kotlin/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +**/fastlane/report.xml +**/fastlane/Preview.html +**/fastlane/screenshots +**/fastlane/test_output + +# Bundle artifact +*.jsbundle + +# Ruby / CocoaPods +**/Pods/ +/vendor/bundle/ + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* + +# testing +/coverage + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..2b54074 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,7 @@ +module.exports = { + arrowParens: 'avoid', + bracketSameLine: true, + bracketSpacing: false, + singleQuote: true, + trailingComma: 'all', +}; diff --git a/.watchmanconfig b/.watchmanconfig new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..7d8838a --- /dev/null +++ b/App.tsx @@ -0,0 +1,131 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + * + * @format + */ + +import React from 'react'; +import type {PropsWithChildren} from 'react'; +import { + ScrollView, + StatusBar, + StyleSheet, + Text, + useColorScheme, + View, +} from 'react-native'; + +import { + Colors, + DebugInstructions, + Header, + LearnMoreLinks, + ReloadInstructions, +} from 'react-native/Libraries/NewAppScreen'; + +type SectionProps = PropsWithChildren<{ + title: string; +}>; + +function Section({children, title}: SectionProps): React.JSX.Element { + const isDarkMode = useColorScheme() === 'dark'; + return ( + + + {title} + + + {children} + + + ); +} + +function App(): React.JSX.Element { + const isDarkMode = useColorScheme() === 'dark'; + + const backgroundStyle = { + backgroundColor: isDarkMode ? Colors.darker : Colors.lighter, + }; + + /* + * To keep the template simple and small we're adding padding to prevent view + * from rendering under the System UI. + * For bigger apps the recommendation is to use `react-native-safe-area-context`: + * https://github.com/AppAndFlow/react-native-safe-area-context + * + * You can read more about it here: + * https://github.com/react-native-community/discussions-and-proposals/discussions/827 + */ + const safePadding = '5%'; + + return ( + + + + +
+ + +
+ Edit App.tsx to change this + screen and then come back to see your edits. +
+
+ +
+
+ +
+
+ Read the docs to discover what to do next: +
+ +
+ + + ); +} + +const styles = StyleSheet.create({ + sectionContainer: { + marginTop: 32, + paddingHorizontal: 24, + }, + sectionTitle: { + fontSize: 24, + fontWeight: '600', + }, + sectionDescription: { + marginTop: 8, + fontSize: 18, + fontWeight: '400', + }, + highlight: { + fontWeight: '700', + }, +}); + +export default App; diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..6a4c5f1 --- /dev/null +++ b/Gemfile @@ -0,0 +1,16 @@ +source 'https://rubygems.org' + +# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version +ruby ">= 2.6.10" + +# Exclude problematic versions of cocoapods and activesupport that causes build failures. +gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' +gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' +gem 'xcodeproj', '< 1.26.0' +gem 'concurrent-ruby', '< 1.3.4' + +# Ruby 3.4.0 has removed some libraries from the standard library. +gem 'bigdecimal' +gem 'logger' +gem 'benchmark' +gem 'mutex_m' diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..7d7deca --- /dev/null +++ b/PROJECT_STRUCTURE.md @@ -0,0 +1,355 @@ +/* + * File: PROJECT_STRUCTURE.md + * Description: Complete project structure and architecture documentation + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +# NeoScan Physician App - Project Structure + +## ๐Ÿ“ Complete Directory Structure + +``` +NeoScan_Physician/ +โ”œโ”€โ”€ app/ # Main application code +โ”‚ โ”œโ”€โ”€ modules/ # Feature-based modules +โ”‚ โ”‚ โ”œโ”€โ”€ Auth/ # Authentication module +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ components/ # Auth-specific components +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ screens/ # Auth screens +โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ LoginScreen.tsx # Main login screen +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ hooks/ # Auth custom hooks +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ redux/ # Auth state management +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ services/ # Auth API services +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ index.ts # Auth module exports +โ”‚ โ”‚ โ”œโ”€โ”€ Dashboard/ # ER Dashboard module +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ components/ # Dashboard components +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ PatientCard.tsx # Patient information card +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ CriticalAlerts.tsx # Critical alerts display +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ DashboardHeader.tsx # Dashboard statistics header +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ QuickActions.tsx # Emergency quick actions +โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ DepartmentStats.tsx # Department statistics +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ screens/ # Dashboard screens +โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ DashboardScreen.tsx # Main ER dashboard +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ hooks/ # Dashboard custom hooks +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ redux/ # Dashboard state management +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ services/ # Dashboard API services +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ index.ts # Dashboard module exports +โ”‚ โ”‚ โ”œโ”€โ”€ PatientCare/ # Patient management module +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ components/ # Patient care components +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ screens/ # Patient care screens +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ hooks/ # Patient care hooks +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ redux/ # Patient care state +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ services/ # Patient care services +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ index.ts # Patient care exports +โ”‚ โ”‚ โ””โ”€โ”€ Settings/ # App settings module +โ”‚ โ”‚ โ”œโ”€โ”€ components/ # Settings components +โ”‚ โ”‚ โ”œโ”€โ”€ screens/ # Settings screens +โ”‚ โ”‚ โ”œโ”€โ”€ hooks/ # Settings hooks +โ”‚ โ”‚ โ”œโ”€โ”€ redux/ # Settings state +โ”‚ โ”‚ โ”œโ”€โ”€ services/ # Settings services +โ”‚ โ”‚ โ””โ”€โ”€ index.ts # Settings exports +โ”‚ โ”œโ”€โ”€ shared/ # Shared utilities & components +โ”‚ โ”‚ โ”œโ”€โ”€ components/ # Reusable UI components +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ UI/ # Basic UI components +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Button.tsx # Button component +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Input.tsx # Input field component +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Card.tsx # Card component +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Modal.tsx # Modal component +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Badge.tsx # Badge component +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Spinner.tsx # Loading spinner +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Alert.tsx # Alert component +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Dropdown.tsx # Dropdown component +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Tabs.tsx # Tab component +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ProgressBar.tsx # Progress bar +โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ index.ts # UI components export +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Forms/ # Form-related components +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ FormField.tsx # Form field component +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ValidationMessage.tsx # Validation message +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ FormContainer.tsx # Form container +โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ index.ts # Form components export +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Icons/ # Icon components +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ MedicalIcons.tsx # Medical-specific icons +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ StatusIcons.tsx # Status indicators +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ NavigationIcons.tsx # Navigation icons +โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ index.ts # Icon components export +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ index.ts # Shared components export +โ”‚ โ”‚ โ”œโ”€โ”€ utils/ # Utility functions +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ api.ts # API utilities +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ constants.ts # App constants +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ helpers.ts # Helper functions +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ validators.ts # Validation functions +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ formatters.ts # Data formatting +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ dateUtils.ts # Date utilities +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ medicalUtils.ts # Medical-specific utilities +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ imageUtils.ts # Image processing +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ stringUtils.ts # String manipulation +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ index.ts # Utils export +โ”‚ โ”‚ โ”œโ”€โ”€ types/ # TypeScript type definitions +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ auth.ts # Authentication types +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ patient.ts # Patient-related types +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ dashboard.ts # Dashboard types +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ alerts.ts # Alert types +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ common.ts # Common types +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ index.ts # Types export +โ”‚ โ”‚ โ””โ”€โ”€ index.ts # Shared module export +โ”‚ โ”œโ”€โ”€ theme/ # Design system & theming +โ”‚ โ”‚ โ”œโ”€โ”€ colors.ts # Color palette +โ”‚ โ”‚ โ”œโ”€โ”€ typography.ts # Typography system +โ”‚ โ”‚ โ”œโ”€โ”€ spacing.ts # Spacing & layout +โ”‚ โ”‚ โ”œโ”€โ”€ shadows.ts # Shadow system +โ”‚ โ”‚ โ”œโ”€โ”€ animations.ts # Animation system +โ”‚ โ”‚ โ”œโ”€โ”€ theme.ts # Main theme object +โ”‚ โ”‚ โ””โ”€โ”€ index.ts # Theme export +โ”‚ โ”œโ”€โ”€ navigation/ # Navigation setup +โ”‚ โ”‚ โ”œโ”€โ”€ AppNavigator.tsx # Root navigator +โ”‚ โ”‚ โ”œโ”€โ”€ AuthNavigator.tsx # Authentication flow +โ”‚ โ”‚ โ”œโ”€โ”€ MainNavigator.tsx # Main app flow +โ”‚ โ”‚ โ”œโ”€โ”€ TabNavigator.tsx # Tab navigation +โ”‚ โ”‚ โ”œโ”€โ”€ navigationTypes.ts # Navigation types +โ”‚ โ”‚ โ”œโ”€โ”€ navigationUtils.ts # Navigation utilities +โ”‚ โ”‚ โ””โ”€โ”€ __tests__/ # Navigation tests +โ”‚ โ”œโ”€โ”€ store/ # Redux store configuration +โ”‚ โ”‚ โ”œโ”€โ”€ index.ts # Store configuration +โ”‚ โ”‚ โ”œโ”€โ”€ rootReducer.ts # Root reducer +โ”‚ โ”‚ โ”œโ”€โ”€ middleware.ts # Custom middleware +โ”‚ โ”‚ โ””โ”€โ”€ __tests__/ # Store tests +โ”‚ โ”œโ”€โ”€ config/ # Configuration files +โ”‚ โ”‚ โ”œโ”€โ”€ env.ts # Environment variables +โ”‚ โ”‚ โ”œโ”€โ”€ api.ts # API configuration +โ”‚ โ”‚ โ”œโ”€โ”€ websocket.ts # WebSocket config +โ”‚ โ”‚ โ”œโ”€โ”€ notifications.ts # Notification config +โ”‚ โ”‚ โ”œโ”€โ”€ security.ts # Security settings +โ”‚ โ”‚ โ””โ”€โ”€ index.ts # Config export +โ”‚ โ”œโ”€โ”€ assets/ # Static assets +โ”‚ โ”‚ โ”œโ”€โ”€ images/ # Image assets +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ logos/ # Hospital & app logos +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ icons/ # UI icons +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ medical/ # Medical-specific icons +โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ui/ # General UI icons +โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ status/ # Status indicators +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ backgrounds/ # Background images +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ placeholders/ # Placeholder images +โ”‚ โ”‚ โ”œโ”€โ”€ fonts/ # Font files +โ”‚ โ”‚ โ””โ”€โ”€ sounds/ # Audio assets +โ”‚ โ”œโ”€โ”€ localization/ # Internationalization +โ”‚ โ”‚ โ”œโ”€โ”€ en/ # English translations +โ”‚ โ”‚ โ”œโ”€โ”€ es/ # Spanish translations +โ”‚ โ”‚ โ”œโ”€โ”€ fr/ # French translations +โ”‚ โ”‚ โ””โ”€โ”€ index.ts # i18n configuration +โ”‚ โ”œโ”€โ”€ App.tsx # Root component +โ”‚ โ””โ”€โ”€ index.tsx # App entry point +โ”œโ”€โ”€ android/ # Android native code +โ”‚ โ”œโ”€โ”€ app/ # Android app module +โ”‚ โ”‚ โ”œโ”€โ”€ build.gradle # App build configuration +โ”‚ โ”‚ โ”œโ”€โ”€ debug.keystore # Debug keystore +โ”‚ โ”‚ โ”œโ”€โ”€ proguard-rules.pro # ProGuard rules +โ”‚ โ”‚ โ””โ”€โ”€ src/ # Source code +โ”‚ โ”‚ โ”œโ”€โ”€ debug/ # Debug configuration +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ AndroidManifest.xml # Debug manifest +โ”‚ โ”‚ โ””โ”€โ”€ main/ # Main source +โ”‚ โ”‚ โ”œโ”€โ”€ AndroidManifest.xml # Main manifest +โ”‚ โ”‚ โ”œโ”€โ”€ java/ # Java source +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ com/ # Package structure +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ neoscan_physician/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ MainActivity.kt # Main activity +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ MainApplication.kt # Application class +โ”‚ โ”‚ โ””โ”€โ”€ res/ # Resources +โ”‚ โ”‚ โ”œโ”€โ”€ drawable/ # Drawable resources +โ”‚ โ”‚ โ”œโ”€โ”€ mipmap-*/ # App icons +โ”‚ โ”‚ โ””โ”€โ”€ values/ # Values +โ”‚ โ”‚ โ”œโ”€โ”€ strings.xml # String resources +โ”‚ โ”‚ โ””โ”€โ”€ styles.xml # Styles +โ”‚ โ”œโ”€โ”€ build.gradle # Project build configuration +โ”‚ โ”œโ”€โ”€ gradle/ # Gradle wrapper +โ”‚ โ”œโ”€โ”€ gradle.properties # Gradle properties +โ”‚ โ”œโ”€โ”€ gradlew # Gradle wrapper script +โ”‚ โ”œโ”€โ”€ gradlew.bat # Windows gradle wrapper +โ”‚ โ””โ”€โ”€ settings.gradle # Gradle settings +โ”œโ”€โ”€ ios/ # iOS native code +โ”‚ โ”œโ”€โ”€ NeoScan_Physician/ # iOS app +โ”‚ โ”‚ โ”œโ”€โ”€ AppDelegate.swift # App delegate +โ”‚ โ”‚ โ”œโ”€โ”€ Images.xcassets/ # Image assets +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ AppIcon.appiconset/ # App icons +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ Contents.json # Asset catalog +โ”‚ โ”‚ โ”œโ”€โ”€ Info.plist # App info +โ”‚ โ”‚ โ”œโ”€โ”€ LaunchScreen.storyboard # Launch screen +โ”‚ โ”‚ โ””โ”€โ”€ PrivacyInfo.xcprivacy # Privacy info +โ”‚ โ”œโ”€โ”€ NeoScan_Physician.xcodeproj/ # Xcode project +โ”‚ โ”‚ โ”œโ”€โ”€ project.pbxproj # Project file +โ”‚ โ”‚ โ””โ”€โ”€ xcshareddata/ # Shared data +โ”‚ โ”‚ โ””โ”€โ”€ xcschemes/ # Build schemes +โ”‚ โ”‚ โ””โ”€โ”€ NeoScan_Physician.xcscheme +โ”‚ โ””โ”€โ”€ Podfile # CocoaPods configuration +โ”œโ”€โ”€ __tests__/ # Test files +โ”‚ โ”œโ”€โ”€ App.test.tsx # App component tests +โ”‚ โ”œโ”€โ”€ components/ # Component tests +โ”‚ โ”œโ”€โ”€ utils/ # Utility tests +โ”‚ โ””โ”€โ”€ integration/ # Integration tests +โ”œโ”€โ”€ docs/ # Documentation +โ”‚ โ”œโ”€โ”€ README.md # Project overview +โ”‚ โ”œโ”€โ”€ ARCHITECTURE.md # Architecture documentation +โ”‚ โ”œโ”€โ”€ API.md # API documentation +โ”‚ โ”œโ”€โ”€ DEPLOYMENT.md # Deployment guide +โ”‚ โ”œโ”€โ”€ TESTING.md # Testing guidelines +โ”‚ โ”œโ”€โ”€ SECURITY.md # Security guidelines +โ”‚ โ””โ”€โ”€ wireframes/ # UI wireframes +โ”œโ”€โ”€ .gitignore # Git ignore rules +โ”œโ”€โ”€ .eslintrc.js # ESLint configuration +โ”œโ”€โ”€ .prettierrc.js # Prettier configuration +โ”œโ”€โ”€ .watchmanconfig # Watchman configuration +โ”œโ”€โ”€ app.json # App configuration +โ”œโ”€โ”€ babel.config.js # Babel configuration +โ”œโ”€โ”€ jest.config.js # Jest configuration +โ”œโ”€โ”€ metro.config.js # Metro bundler configuration +โ”œโ”€โ”€ package.json # Dependencies and scripts +โ”œโ”€โ”€ package-lock.json # Locked dependencies +โ”œโ”€โ”€ tsconfig.json # TypeScript configuration +โ”œโ”€โ”€ index.js # React Native entry point +โ”œโ”€โ”€ setup.sh # Unix setup script +โ”œโ”€โ”€ setup.bat # Windows setup script +โ”œโ”€โ”€ README.md # Project README +โ”œโ”€โ”€ PROJECT_STRUCTURE.md # This file +โ””โ”€โ”€ Gemfile # Ruby dependencies (iOS) +``` + +## ๐Ÿ—๏ธ Module Architecture + +### Auth Module +**Purpose**: Handles all authentication and authorization functionality +- **LoginScreen**: Hospital SSO, credential login, emergency access +- **Components**: Login forms, authentication modals +- **Services**: Authentication API, token management +- **Redux**: Auth state, user session management + +### Dashboard Module +**Purpose**: Main ER dashboard with patient monitoring and alerts +- **DashboardScreen**: Main dashboard with patient list and statistics +- **PatientCard**: Individual patient information display +- **CriticalAlerts**: High-priority alert notifications +- **QuickActions**: Emergency procedure shortcuts +- **DepartmentStats**: Real-time department overview + +### PatientCare Module +**Purpose**: Comprehensive patient management and medical records +- **PatientDetailsScreen**: Complete patient information +- **VitalSigns**: Real-time vital signs monitoring +- **MedicalHistory**: Patient medical records +- **Medications**: Current medication management +- **Allergies**: Allergy information and alerts + +### Settings Module +**Purpose**: App configuration and user preferences +- **SettingsScreen**: User preferences and app settings +- **NotificationSettings**: Alert and notification configuration +- **SecuritySettings**: Authentication and security options +- **AboutScreen**: App information and version details + +## ๐Ÿ“ File Naming Conventions + +### Components +- **PascalCase** for all component files +- **Suffix with type**: `.tsx` for components, `.ts` for utilities +- **Examples**: `LoginScreen.tsx`, `PatientCard.tsx`, `CriticalAlerts.tsx` + +### Hooks +- **camelCase** with `use` prefix +- **Examples**: `useAuth.ts`, `usePatientList.ts`, `useRealTimeAlerts.ts` + +### Services +- **camelCase** with descriptive names +- **Suffix with type**: `API.ts`, `Service.ts` +- **Examples**: `authAPI.ts`, `patientCareAPI.ts`, `notificationService.ts` + +### Redux +- **camelCase** with descriptive suffixes +- **Examples**: `authSlice.ts`, `erDashboardSlice.ts`, `patientCareActions.ts` + +### Tests +- **Same name as source file** + `.test.ts` or `.test.tsx` +- **Examples**: `LoginScreen.test.tsx`, `useAuth.test.ts`, `authSlice.test.ts` + +## ๐Ÿ”ง Configuration Files + +### Theme System +- **colors.ts**: Complete color palette with healthcare blue theme +- **typography.ts**: Font families, weights, sizes, and spacing +- **spacing.ts**: Spacing scale, border radius, breakpoints +- **shadows.ts**: Shadow system for elevation +- **animations.ts**: Animation durations and easing functions +- **theme.ts**: Main theme object combining all design tokens + +### Type Definitions +- **auth.ts**: Authentication types and interfaces +- **patient.ts**: Patient data and medical record types +- **dashboard.ts**: Dashboard and ER management types +- **alerts.ts**: Alert and notification types +- **common.ts**: Common utility types and interfaces + +### Utilities +- **constants.ts**: App constants, API configs, timeouts +- **helpers.ts**: Common utility functions and helpers +- **validators.ts**: Form validation and data validation +- **formatters.ts**: Data formatting and display utilities +- **medicalUtils.ts**: Medical-specific utility functions + +## ๐ŸŽจ Design System + +### Color Palette +- **Primary**: #2196F3 (Material Blue) +- **Secondary**: #1976D2 (Darker Blue) +- **Critical**: #F44336 (Material Red) +- **Warning**: #FF9800 (Material Orange) +- **Success**: #4CAF50 (Material Green) + +### Component Library +- **UI Components**: Basic building blocks (Button, Input, Card, etc.) +- **Form Components**: Form-specific components with validation +- **Icon Components**: Medical and UI icons +- **Layout Components**: Container and layout components + +### Responsive Design +- **Mobile First**: Optimized for mobile devices +- **Tablet Support**: Enhanced layouts for tablets +- **Breakpoints**: Mobile (375px), Tablet (768px), Desktop (1024px) + +## ๐Ÿš€ Getting Started + +1. **Clone the repository** +2. **Run setup script**: `./setup.sh` (Unix) or `setup.bat` (Windows) +3. **Start development server**: `npm start` +4. **Run on device**: `npm run android` or `npm run ios` + +## ๐Ÿ“ฑ Key Features Implemented + +### โœ… Completed +- **Theme System**: Complete design system with healthcare blue theme +- **Authentication**: Login screen with SSO and credential options +- **ER Dashboard**: Main dashboard with patient cards and statistics +- **Patient Cards**: Comprehensive patient information display +- **Critical Alerts**: High-priority alert system +- **Quick Actions**: Emergency procedure shortcuts +- **Department Stats**: Real-time department overview +- **Navigation**: Tab-based navigation structure +- **Type Safety**: Complete TypeScript implementation + +### ๐Ÿ”„ In Progress +- **Redux Store**: State management implementation +- **API Integration**: Backend service integration +- **Real-time Updates**: WebSocket implementation +- **Push Notifications**: Critical alert notifications + +### ๐Ÿ“‹ Planned +- **Patient Details**: Comprehensive patient management screens +- **Medical Records**: Complete medical history management +- **Settings**: App configuration and preferences +- **Offline Support**: Offline data access and sync +- **Testing**: Comprehensive test suite +- **Documentation**: Complete API and user documentation + +This structure provides a solid foundation for a comprehensive healthcare application with proper separation of concerns, type safety, and modern React Native best practices. + +/* + * End of File: PROJECT_STRUCTURE.md + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5389453 --- /dev/null +++ b/README.md @@ -0,0 +1,317 @@ +/* + * File: README.md + * Description: Project documentation and setup instructions + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +# NeoScan Physician App + +A comprehensive React Native application designed for emergency department physicians to manage patient care, critical alerts, and medical workflows in real-time. + +## ๐Ÿš€ Features + +### Authentication & Security +- **Hospital SSO Integration** - Seamless login with hospital credentials +- **Multi-factor Authentication** - Enhanced security for medical data +- **Emergency Access** - Quick access codes for urgent situations +- **Session Management** - 8-hour sessions with 30-minute inactivity timeout +- **Device Remembering** - 30-day trusted device authentication + +### ER Dashboard +- **Real-time Patient Monitoring** - Live updates of patient status and vital signs +- **Critical Alert System** - Immediate notifications for life-threatening conditions +- **Department Statistics** - Overview of emergency, trauma, cardiac, neurology, pediatrics, and ICU +- **Quick Actions** - Emergency procedures, scan orders, medication, lab work, consultations +- **Patient Filtering** - Filter by status (All, Critical, Active) + +### Patient Management +- **Comprehensive Patient Cards** - Vital signs, allergies, medications, diagnosis +- **Medical History** - Complete patient medical records +- **Real-time Vital Signs** - Blood pressure, heart rate, temperature, respiratory rate, oxygen saturation +- **Allergy Alerts** - Prominent display of patient allergies +- **Medication Tracking** - Current medications with dosages and schedules + +### Critical Finding Response +- **AI-Powered Detection** - Automated critical finding identification +- **Immediate Alert System** - Push notifications for urgent cases +- **Response Timeline** - 0-30 seconds acknowledgment, 2-5 minutes action +- **Treatment Protocols** - Quick access to emergency procedures + +## ๐Ÿ—๏ธ Architecture + +### Project Structure +``` +NeoScan_Physician/ +โ”œโ”€โ”€ app/ # Main application code +โ”‚ โ”œโ”€โ”€ modules/ # Feature-based modules +โ”‚ โ”‚ โ”œโ”€โ”€ Auth/ # Authentication module +โ”‚ โ”‚ โ”œโ”€โ”€ Dashboard/ # ER Dashboard module +โ”‚ โ”‚ โ”œโ”€โ”€ PatientCare/ # Patient management module +โ”‚ โ”‚ โ””โ”€โ”€ Settings/ # App settings module +โ”‚ โ”œโ”€โ”€ shared/ # Shared utilities & components +โ”‚ โ”‚ โ”œโ”€โ”€ components/ # Reusable UI components +โ”‚ โ”‚ โ”œโ”€โ”€ utils/ # Utility functions +โ”‚ โ”‚ โ””โ”€โ”€ types/ # TypeScript type definitions +โ”‚ โ”œโ”€โ”€ theme/ # Design system & theming +โ”‚ โ”œโ”€โ”€ navigation/ # Navigation setup +โ”‚ โ”œโ”€โ”€ store/ # Redux state management +โ”‚ โ”œโ”€โ”€ config/ # Configuration files +โ”‚ โ””โ”€โ”€ assets/ # Static assets +โ”œโ”€โ”€ android/ # Android native code +โ”œโ”€โ”€ ios/ # iOS native code +โ””โ”€โ”€ docs/ # Documentation +``` + +### Technology Stack +- **React Native 0.79.0** - Cross-platform mobile development +- **TypeScript** - Type-safe development +- **React Navigation 6** - Navigation management +- **Redux Toolkit** - State management +- **React Native Vector Icons** - Icon library +- **React Native Push Notification** - Real-time notifications +- **React Native Keychain** - Secure credential storage + +## ๐ŸŽจ Design System + +### Color Palette - "Modern Healthcare Blue" +- **Primary**: #2196F3 (Material Blue) +- **Secondary**: #1976D2 (Darker Blue) +- **Critical**: #F44336 (Material Red) +- **Warning**: #FF9800 (Material Orange) +- **Success**: #4CAF50 (Material Green) + +### Typography +- **Primary Font**: Roboto +- **Font Weights**: Light (300), Regular (400), Medium (500), Bold (700) +- **Font Sizes**: Display (32px, 24px, 20px), Body (16px, 14px, 12px), Caption (10px) + +### Components +- **Patient Cards** - Comprehensive patient information display +- **Critical Alerts** - High-priority notification system +- **Quick Actions** - Emergency procedure shortcuts +- **Department Stats** - Real-time department overview +- **Dashboard Header** - ER statistics and shift information + +## ๐Ÿ“ฑ Screens + +### Authentication Flow +1. **Splash Screen** - App initialization and authentication check +2. **Login Screen** - Hospital SSO, credential login, emergency access +3. **Main Dashboard** - ER overview with patient list and alerts + +### Main Application +1. **ER Dashboard** - Real-time patient monitoring and critical alerts +2. **Patient Details** - Comprehensive patient information and medical history +3. **Alerts Center** - Critical finding notifications and response +4. **Reports** - Medical reports and scan results +5. **Settings** - User preferences and app configuration + +## ๐Ÿ”ง Setup & Installation + +### Prerequisites +- Node.js >= 18 +- React Native CLI +- Android Studio (for Android development) +- Xcode (for iOS development) + +### Installation Steps + +1. **Clone the repository** + ```bash + git clone + cd NeoScan_Physician + ``` + +2. **Install dependencies** + ```bash + npm install + ``` + +3. **iOS Setup** (macOS only) + ```bash + cd ios + pod install + cd .. + ``` + +4. **Start the development server** + ```bash + npm start + ``` + +5. **Run on device/simulator** + ```bash + # Android + npm run android + + # iOS + npm run ios + ``` + +## ๐Ÿšจ Critical Alert Workflow + +### Alert Reception (0-30 seconds) +- Push notification received +- Alert screen displayed +- Patient context loaded +- AI summary generated + +### Assessment (30-60 seconds) +- Patient details reviewed +- Medical history checked +- Current status assessed +- Urgency level determined + +### Action Planning (1-2 minutes) +- Treatment protocol loaded +- Specialist consultation initiated +- Emergency procedures prepared +- Documentation started + +### Implementation (2-5 minutes) +- Actions executed +- Status updated +- Team notified +- Record documented + +## ๐Ÿ” Security Features + +### Authentication +- **Session Management**: 8-hour timeout +- **Auto-logout**: Inactivity after 30 minutes +- **Device Remembering**: 30-day trusted devices +- **Emergency Access**: Limited functionality +- **Audit Trail**: All actions logged + +### Data Protection +- **Encryption**: End-to-end data encryption +- **HIPAA Compliance**: Healthcare data protection +- **Secure Storage**: Encrypted local storage +- **Network Security**: HTTPS/TLS communication + +## ๐Ÿ“Š Performance Optimization + +### Loading States +- **Critical Alerts**: Immediate loading +- **Patient List**: Fast loading with caching +- **Patient Details**: Medium loading +- **Medical History**: Optimized loading +- **Full Reports**: Background loading + +### Offline Capabilities +- **Critical Alerts**: Always available +- **Patient List**: Cached data access +- **Recent Reports**: Offline viewing +- **Settings**: Local storage +- **Sync**: Automatic when online + +## ๐Ÿงช Testing + +### Test Structure +- **Unit Tests**: Component and utility testing +- **Integration Tests**: Module interaction testing +- **E2E Tests**: Complete workflow testing +- **Performance Tests**: Load and stress testing + +### Running Tests +```bash +# Unit tests +npm test + +# E2E tests +npm run test:e2e + +# Performance tests +npm run test:performance +``` + +## ๐Ÿ“ฑ Platform Support + +### iOS Features +- Face ID authentication +- Apple Health integration +- Siri shortcuts +- iOS notifications + +### Android Features +- Fingerprint authentication +- Google Fit integration +- Android Auto +- Android notifications + +## ๐Ÿ”„ Real-time Updates + +### WebSocket Integration +- **Patient Status**: Real-time updates +- **Vital Signs**: 30-second intervals +- **Alert Status**: Immediate updates +- **Bed Assignments**: Real-time changes +- **Report Status**: 5-minute intervals + +### Data Synchronization +- **Server Priority**: Server data overrides local +- **Timestamp Comparison**: Latest data wins +- **User Confirmation**: Manual resolution for conflicts +- **Audit Trail**: All changes tracked + +## ๐Ÿ“‹ Development Guidelines + +### Code Style +- **TypeScript**: Strict type checking +- **ESLint**: Code quality enforcement +- **Prettier**: Code formatting +- **Conventional Commits**: Git commit messages + +### Component Guidelines +- **Single Responsibility**: One component, one purpose +- **Reusability**: Shared components in shared/ +- **Type Safety**: Full TypeScript coverage +- **Accessibility**: WCAG 2.1 compliance + +### Performance Guidelines +- **Lazy Loading**: Components loaded on demand +- **Memoization**: React.memo for expensive components +- **Image Optimization**: Compressed and cached images +- **Bundle Size**: Minimal dependencies + +## ๐Ÿš€ Deployment + +### Build Configuration +- **Environment Variables**: Separate configs for dev/staging/prod +- **Code Signing**: Proper certificate management +- **Bundle Optimization**: Minified and optimized builds +- **Asset Management**: Optimized images and fonts + +### Release Process +1. **Development**: Feature development and testing +2. **Staging**: Integration testing and QA +3. **Production**: Final testing and deployment +4. **Monitoring**: Performance and error tracking + +## ๐Ÿ“ž Support + +### Documentation +- **API Documentation**: Complete API reference +- **User Guide**: End-user documentation +- **Developer Guide**: Technical documentation +- **Troubleshooting**: Common issues and solutions + +### Contact +- **Technical Support**: dev-support@neoscan.com +- **Emergency Support**: emergency-support@neoscan.com +- **Feature Requests**: features@neoscan.com + +## ๐Ÿ“„ License + +This project is proprietary software developed for healthcare institutions. All rights reserved. + +--- + +**NeoScan Physician App** - Empowering emergency care with real-time intelligence and seamless workflows. + +/* + * End of File: README.md + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/THEME_FLOW.md b/THEME_FLOW.md new file mode 100644 index 0000000..39812e1 --- /dev/null +++ b/THEME_FLOW.md @@ -0,0 +1,618 @@ +# Physician App - Theme System & Design Flow + +## ๐ŸŽจ Design System Overview + +### **Project Theme Structure** +``` +app/theme/ +โ”œโ”€โ”€ theme.ts # Main theme configuration +โ”œโ”€โ”€ colors.ts # Color palette definitions +โ”œโ”€โ”€ typography.ts # Font families, sizes, weights +โ”œโ”€โ”€ spacing.ts # Spacing scale and breakpoints +โ”œโ”€โ”€ shadows.ts # Shadow and elevation system +โ””โ”€โ”€ animations.ts # Animation durations and easing +``` + +--- + +## ๐ŸŽฏ Color Palette - "Modern Healthcare Blue" + +### **Primary Colors** +```typescript +Primary: '#2196F3' // Material Blue - Main brand color +Secondary: '#1976D2' // Darker Blue - Secondary actions +Tertiary: '#E3F2FD' // Very Light Blue - Backgrounds +Quaternary: '#0D47A1' // Deep Blue - Accents +``` + +### **Text Colors** +```typescript +TextPrimary: '#212121' // Dark Gray - Main text +TextSecondary: '#757575' // Medium Gray - Secondary text +TextMuted: '#9E9E9E' // Light Gray - Muted text +``` + +### **Background Colors** +```typescript +Background: '#FFFFFF' // White - Primary background +BackgroundAlt: '#FAFAFA' // Light Gray - Alternative background +BackgroundAccent: '#F5F5F5' // Soft Gray - Accent backgrounds +``` + +### **Status & Feedback Colors** +```typescript +Success: '#4CAF50' // Material Green - Success states +Warning: '#FF9800' // Material Orange - Warning states +Error: '#F44336' // Material Red - Error states +Info: '#2196F3' // Material Blue - Information states +Critical: '#F44336' // Critical alerts and emergencies +``` + +### **UI Elements** +```typescript +Border: '#E0E0E0' // Light Gray Border +CardBackground: '#FFFFFF' // White - Card backgrounds +Shadow: 'rgba(0, 0, 0, 0.1)' // Subtle Gray Shadow +``` + +--- + +## ๐Ÿ“ Typography System + +### **Font Families (Roboto)** +```typescript +// Available Font Families +fontFamily: { + bold: 'Roboto-Bold', + medium: 'Roboto-Medium', + regular: 'Roboto-Regular', + light: 'Roboto-Light', + semibold: 'Roboto-SemiBold', + extrabold: 'Roboto-ExtraBold', +} +``` + +### **Font Weights** +```typescript +fontWeight: { + light: '300', + regular: '400', + medium: '500', + bold: '700', +} +``` + +### **Font Sizes** +```typescript +fontSize: { + displayLarge: 32, // Main headings + displayMedium: 24, // Section headings + displaySmall: 20, // Subsection headings + bodyLarge: 16, // Body text + bodyMedium: 14, // Secondary text + bodySmall: 12, // Captions + caption: 10, // Small labels +} +``` + +### **Line Heights & Spacing** +```typescript +lineHeight: { + tight: 1.2, // Headings + normal: 1.4, // Body text + relaxed: 1.6, // Long text +} + +letterSpacing: { + tight: -0.5, // Headings + normal: 0, // Body text + wide: 0.5, // Labels +} +``` + +--- + +## ๐Ÿ“ Spacing & Layout + +### **Spacing Scale (Base: 4px)** +```typescript +spacing: { + xs: 4, // 4px + sm: 8, // 8px + md: 16, // 16px + lg: 24, // 24px + xl: 32, // 32px + xxl: 48, // 48px + xxxl: 64 // 64px +} +``` + +### **Border Radius** +```typescript +borderRadius: { + small: 4, + medium: 8, + large: 12, + xlarge: 16, + round: 50, +} +``` + +### **Breakpoints** +```typescript +breakpoints: { + mobile: 375, + tablet: 768, + desktop: 1024, + largeDesktop: 1440, +} +``` + +--- + +## ๐ŸŽจ Icon System + +### **React Native Vector Icons** + +#### **Available Icon Sets** +```typescript +// Primary Icon Sets +import Icon from 'react-native-vector-icons/Feather'; // Clean, minimal icons +import Icon from 'react-native-vector-icons/MaterialIcons'; // Material Design icons +import Icon from 'react-native-vector-icons/Ionicons'; // iOS-style icons +``` + +#### **Icon Usage Guidelines** +```typescript +// Standard Icon Implementation + + +// Icon Sizes +IconSizes: { + xs: 12, // Extra small + sm: 16, // Small + md: 24, // Medium (default) + lg: 32, // Large + xl: 48 // Extra large +} +``` + +#### **Common Icon Names** +```typescript +// Navigation Icons +'home', 'menu', 'arrow-left', 'arrow-right', 'chevron-down', 'chevron-up' + +// Action Icons +'plus', 'minus', 'edit', 'delete', 'save', 'cancel', 'check', 'close' + +// Medical Icons +'heart', 'user', 'settings', 'bell', 'search', 'filter', 'calendar' + +// Status Icons +'check-circle', 'alert-circle', 'info', 'warning', 'error' +``` + +--- + +## ๐Ÿ—๏ธ Component Design Rules + +### **Button Design System** +```typescript +// Button Variants +Primary: { + backgroundColor: theme.colors.primary, + borderColor: theme.colors.primary, + borderRadius: theme.borderRadius.medium, + shadowColor: theme.colors.primary, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 3 +} + +Secondary: { + backgroundColor: 'transparent', + borderColor: theme.colors.primary, + borderRadius: theme.borderRadius.medium +} + +Success: { + backgroundColor: theme.colors.success, + borderColor: theme.colors.success, + borderRadius: theme.borderRadius.medium, + shadowColor: theme.colors.success, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 3 +} + +Critical: { + backgroundColor: theme.colors.critical, + borderColor: theme.colors.critical, + borderRadius: theme.borderRadius.medium, + shadowColor: theme.colors.critical, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 3 +} +``` + +### **Input Field Design** +```typescript +// Input States +Default: { + borderColor: theme.colors.border, + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.md +} + +Focused: { + borderColor: theme.colors.primary, + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.md +} + +Error: { + borderColor: theme.colors.critical, + backgroundColor: theme.colors.criticalBackground, + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.md +} +``` + +### **Card Design Rules** +```typescript +// Card Variants +Default: { + backgroundColor: theme.colors.cardBackground, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.large, + padding: theme.spacing.lg, + shadowColor: theme.colors.shadow, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2 +} + +Elevated: { + backgroundColor: theme.colors.cardBackground, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.large, + padding: theme.spacing.lg, + shadowColor: theme.colors.shadow, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 4 +} +``` + +--- + +## ๐Ÿšจ Alert & Status Design + +### **Alert Priority System** +```typescript +// Critical Alerts +Critical: { + backgroundColor: theme.colors.criticalBackground, + borderColor: theme.colors.critical, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.md +} + +// Warning Alerts +Warning: { + backgroundColor: theme.colors.warningBackground, + borderColor: theme.colors.warning, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.md +} + +// Success Alerts +Success: { + backgroundColor: theme.colors.successBackground, + borderColor: theme.colors.success, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.md +} + +// Info Alerts +Info: { + backgroundColor: theme.colors.infoBackground, + borderColor: theme.colors.info, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.md +} +``` + +### **Status Badges** +```typescript +// Status Badges +StatusBadge: { + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + borderRadius: theme.borderRadius.round, + fontSize: theme.typography.fontSize.bodySmall, + fontWeight: theme.typography.fontWeight.medium +} + +// Priority Badges +PriorityBadge: { + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + borderRadius: theme.borderRadius.round, + fontSize: theme.typography.fontSize.bodySmall, + fontWeight: theme.typography.fontWeight.bold +} +``` + +--- + +## ๐Ÿ“ฑ Screen-Specific Design Rules + +### **Login Screen Design** +```typescript +// Login Container +LoginContainer: { + backgroundColor: theme.colors.background, + padding: theme.spacing.lg, + borderRadius: theme.borderRadius.large, + shadowColor: theme.colors.shadow, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 4 +} + +// Input Container with Icons +InputContainer: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.backgroundAlt, + borderColor: theme.colors.border, + borderWidth: 1, + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + paddingVertical: 2, + marginBottom: theme.spacing.md +} + +// Input Icon +InputIcon: { + marginRight: theme.spacing.sm, + color: theme.colors.textSecondary +} +``` + +### **Dashboard Screen Design** +```typescript +// Header Section +Header: { + backgroundColor: theme.colors.background, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + borderBottomColor: theme.colors.border, + borderBottomWidth: 1 +} + +// Critical Alerts Section +CriticalAlerts: { + backgroundColor: theme.colors.criticalBackground, + borderColor: theme.colors.critical, + borderRadius: theme.borderRadius.large, + padding: theme.spacing.lg, + marginHorizontal: theme.spacing.md, + marginVertical: theme.spacing.md, + shadowColor: theme.colors.critical, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 4 +} +``` + +--- + +## ๐Ÿ”„ Animation & Transitions + +### **Animation Rules** +```typescript +// Animation Durations +Durations: { + Fast: 150, // Quick interactions + Normal: 300, // Standard transitions + Slow: 500 // Complex animations +} + +// Easing Functions +Easing: { + Standard: 'cubic-bezier(0.4, 0.0, 0.2, 1)', + Deceleration: 'cubic-bezier(0.0, 0.0, 0.2, 1)', + Acceleration: 'cubic-bezier(0.4, 0.0, 1, 1)' +} + +// Transition Types +Transitions: { + Fade: { opacity: [0, 1], duration: 300 }, + Slide: { transform: [{ translateY: [20, 0] }], duration: 300 }, + Scale: { transform: [{ scale: [0.95, 1] }], duration: 200 } +} +``` + +--- + +## ๐ŸŽฏ Usage Guidelines + +### **Theme Import** +```typescript +// Import theme in components +import { theme } from '../theme/theme'; + +// Usage in StyleSheet +const styles = StyleSheet.create({ + container: { + backgroundColor: theme.colors.background, + padding: theme.spacing.md, + borderRadius: theme.borderRadius.medium, + shadowColor: theme.colors.shadow, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2 + }, + title: { + fontSize: theme.typography.fontSize.displayMedium, + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold + } +}); +``` + +### **Icon Implementation** +```typescript +// Import icon +import Icon from 'react-native-vector-icons/Feather'; + +// Usage in components + +``` + +### **Font Usage** +```typescript +// Typography with font families +const styles = StyleSheet.create({ + heading: { + fontSize: theme.typography.fontSize.displayMedium, + fontWeight: theme.typography.fontWeight.bold, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + lineHeight: theme.typography.lineHeight.tight + }, + body: { + fontSize: theme.typography.fontSize.bodyLarge, + fontWeight: theme.typography.fontWeight.regular, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + lineHeight: theme.typography.lineHeight.normal + } +}); +``` + +--- + +## ๐Ÿ”ง Configuration Files + +### **TypeScript Declaration** +```typescript +// app/types/react-native-vector-icons.d.ts +declare module 'react-native-vector-icons/Feather' { + import { Component } from 'react'; + import { TextProps } from 'react-native'; + + interface IconProps extends TextProps { + name: string; + size?: number; + color?: string; + } + + export default class Icon extends Component {} +} +``` + +### **Font Configuration** +```json +// react-native.config.js +module.exports = { + assets: ['./app/assets/fonts/'], +}; +``` + +--- + +## ๐Ÿ“‹ Best Practices + +### **Design Consistency** +- โœ… Always use theme colors instead of hardcoded values +- โœ… Use consistent spacing from the spacing scale +- โœ… Apply proper typography hierarchy +- โœ… Use appropriate icon sizes and colors +- โœ… Maintain consistent border radius values + +### **Performance** +- โœ… Use StyleSheet.create for all styles +- โœ… Minimize inline styles +- โœ… Use theme constants for repeated values +- โœ… Optimize icon usage with proper sizing + +### **Accessibility** +- โœ… Maintain proper color contrast ratios +- โœ… Use semantic color names +- โœ… Provide adequate touch targets (44px minimum) +- โœ… Support dynamic text sizing + +### **Maintainability** +- โœ… Keep theme centralized and well-documented +- โœ… Use semantic naming for colors and spacing +- โœ… Document any custom theme extensions +- โœ… Version control theme changes + +--- + +## ๐Ÿš€ Quick Reference + +### **Common Theme Values** +```typescript +// Colors +theme.colors.primary // #2196F3 +theme.colors.textPrimary // #212121 +theme.colors.background // #FFFFFF +theme.colors.critical // #F44336 + +// Spacing +theme.spacing.md // 16px +theme.spacing.lg // 24px +theme.spacing.xl // 32px + +// Typography +theme.typography.fontSize.bodyLarge // 16px +theme.typography.fontWeight.bold // 700 +theme.typography.fontFamily.bold // 'Roboto-Bold' + +// Border Radius +theme.borderRadius.medium // 8px +theme.borderRadius.large // 12px +theme.borderRadius.round // 50px +``` + +### **Icon Quick Reference** +```typescript +// Common Icons + + + + + + +``` + +This comprehensive theme system ensures consistency, accessibility, and maintainability across the Physician App while providing a modern healthcare-focused design experience. \ No newline at end of file diff --git a/__tests__/App.test.tsx b/__tests__/App.test.tsx new file mode 100644 index 0000000..e532f70 --- /dev/null +++ b/__tests__/App.test.tsx @@ -0,0 +1,13 @@ +/** + * @format + */ + +import React from 'react'; +import ReactTestRenderer from 'react-test-renderer'; +import App from '../App'; + +test('renders correctly', async () => { + await ReactTestRenderer.act(() => { + ReactTestRenderer.create(); + }); +}); diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..e937186 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,130 @@ +apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" +apply plugin: "com.facebook.react" +apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle" + +/** + * This is the configuration block to customize your React Native Android app. + * By default you don't need to apply any configuration, just uncomment the lines you need. + */ +react { + /* Folders */ + // The root of your project, i.e. where "package.json" lives. Default is '../..' + // root = file("../../") + // The folder where the react-native NPM package is. Default is ../../node_modules/react-native + // reactNativeDir = file("../../node_modules/react-native") + // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen + // codegenDir = file("../../node_modules/@react-native/codegen") + // The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js + // cliFile = file("../../node_modules/react-native/cli.js") + + /* Variants */ + // The list of variants to that are debuggable. For those we're going to + // skip the bundling of the JS bundle and the assets. By default is just 'debug'. + // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. + // debuggableVariants = ["liteDebug", "prodDebug"] + + /* Bundling */ + // A list containing the node command and its flags. Default is just 'node'. + // nodeExecutableAndArgs = ["node"] + // + // The command to run when bundling. By default is 'bundle' + // bundleCommand = "ram-bundle" + // + // The path to the CLI configuration file. Default is empty. + // bundleConfig = file(../rn-cli.config.js) + // + // The name of the generated asset file containing your JS bundle + // bundleAssetName = "MyApplication.android.bundle" + // + // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' + // entryFile = file("../js/MyApplication.android.js") + // + // A list of extra flags to pass to the 'bundle' commands. + // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle + // extraPackagerArgs = [] + + /* Hermes Commands */ + // The hermes compiler command to run. By default it is 'hermesc' + // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" + // + // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" + // hermesFlags = ["-O", "-output-source-map"] + + /* Autolinking */ + autolinkLibrariesWithApp() +} + +/** + * Set this to true to Run Proguard on Release builds to minify the Java bytecode. + */ +def enableProguardInReleaseBuilds = false + +/** + * The preferred build flavor of JavaScriptCore (JSC) + * + * For example, to use the international variant, you can use: + * `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+` + * + * The international variant includes ICU i18n library and necessary data + * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that + * give correct results when using with locales other than en-US. Note that + * this variant is about 6MiB larger per architecture than default. + */ +def enableSeparateBuildPerCPUArchitecture = true +def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+' + +android { + ndkVersion rootProject.ext.ndkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + compileSdk rootProject.ext.compileSdkVersion + + namespace "com.neoscan_physician" + splits { + abi { + enable true + include 'armeabi-v7a', 'arm64-v8a', 'x86' + universalApk false + } + } + defaultConfig { + applicationId "com.neoscan_physician" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + } + signingConfigs { + debug { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } + buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { + // Caution! In production, you need to generate your own keystore file. + // see https://reactnative.dev/docs/signed-apk-android. + signingConfig signingConfigs.debug + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + } + } +} + +dependencies { + // The version of react-native is set by the React Native Gradle Plugin + implementation("com.facebook.react:react-android") + + if (hermesEnabled.toBoolean()) { + implementation("com.facebook.react:hermes-android") + } else { + implementation jscFlavor + } +} + +apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle") diff --git a/android/app/debug.keystore b/android/app/debug.keystore new file mode 100644 index 0000000..364e105 Binary files /dev/null and b/android/app/debug.keystore differ diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..11b0257 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,10 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..eb98c01 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bd883f7 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + diff --git a/android/app/src/main/assets/fonts/WorkSans-Bold.ttf b/android/app/src/main/assets/fonts/WorkSans-Bold.ttf new file mode 100644 index 0000000..9542d67 Binary files /dev/null and b/android/app/src/main/assets/fonts/WorkSans-Bold.ttf differ diff --git a/android/app/src/main/assets/fonts/WorkSans-ExtraBold.ttf b/android/app/src/main/assets/fonts/WorkSans-ExtraBold.ttf new file mode 100644 index 0000000..6dc6fca Binary files /dev/null and b/android/app/src/main/assets/fonts/WorkSans-ExtraBold.ttf differ diff --git a/android/app/src/main/assets/fonts/WorkSans-ExtraLight.ttf b/android/app/src/main/assets/fonts/WorkSans-ExtraLight.ttf new file mode 100644 index 0000000..a6d4617 Binary files /dev/null and b/android/app/src/main/assets/fonts/WorkSans-ExtraLight.ttf differ diff --git a/android/app/src/main/assets/fonts/WorkSans-Light.ttf b/android/app/src/main/assets/fonts/WorkSans-Light.ttf new file mode 100644 index 0000000..1fe1fd1 Binary files /dev/null and b/android/app/src/main/assets/fonts/WorkSans-Light.ttf differ diff --git a/android/app/src/main/assets/fonts/WorkSans-Medium.ttf b/android/app/src/main/assets/fonts/WorkSans-Medium.ttf new file mode 100644 index 0000000..2168073 Binary files /dev/null and b/android/app/src/main/assets/fonts/WorkSans-Medium.ttf differ diff --git a/android/app/src/main/assets/fonts/WorkSans-Regular.ttf b/android/app/src/main/assets/fonts/WorkSans-Regular.ttf new file mode 100644 index 0000000..d24586c Binary files /dev/null and b/android/app/src/main/assets/fonts/WorkSans-Regular.ttf differ diff --git a/android/app/src/main/assets/fonts/WorkSans-SemiBold.ttf b/android/app/src/main/assets/fonts/WorkSans-SemiBold.ttf new file mode 100644 index 0000000..a75721c Binary files /dev/null and b/android/app/src/main/assets/fonts/WorkSans-SemiBold.ttf differ diff --git a/android/app/src/main/assets/fonts/WorkSans-Thin.ttf b/android/app/src/main/assets/fonts/WorkSans-Thin.ttf new file mode 100644 index 0000000..a097d7c Binary files /dev/null and b/android/app/src/main/assets/fonts/WorkSans-Thin.ttf differ diff --git a/android/app/src/main/java/com/neoscan_physician/MainActivity.kt b/android/app/src/main/java/com/neoscan_physician/MainActivity.kt new file mode 100644 index 0000000..dfc252c --- /dev/null +++ b/android/app/src/main/java/com/neoscan_physician/MainActivity.kt @@ -0,0 +1,22 @@ +package com.neoscan_physician + +import com.facebook.react.ReactActivity +import com.facebook.react.ReactActivityDelegate +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled +import com.facebook.react.defaults.DefaultReactActivityDelegate + +class MainActivity : ReactActivity() { + + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + override fun getMainComponentName(): String = "NeoScan_Physician" + + /** + * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] + * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] + */ + override fun createReactActivityDelegate(): ReactActivityDelegate = + DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) +} diff --git a/android/app/src/main/java/com/neoscan_physician/MainApplication.kt b/android/app/src/main/java/com/neoscan_physician/MainApplication.kt new file mode 100644 index 0000000..eb7dbec --- /dev/null +++ b/android/app/src/main/java/com/neoscan_physician/MainApplication.kt @@ -0,0 +1,44 @@ +package com.neoscan_physician + +import android.app.Application +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactHost +import com.facebook.react.ReactNativeHost +import com.facebook.react.ReactPackage +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost +import com.facebook.react.defaults.DefaultReactNativeHost +import com.facebook.react.soloader.OpenSourceMergedSoMapping +import com.facebook.soloader.SoLoader + +class MainApplication : Application(), ReactApplication { + + override val reactNativeHost: ReactNativeHost = + object : DefaultReactNativeHost(this) { + override fun getPackages(): List = + PackageList(this).packages.apply { + // Packages that cannot be autolinked yet can be added manually here, for example: + // add(MyReactNativePackage()) + } + + override fun getJSMainModuleName(): String = "index" + + override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG + + override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED + } + + override val reactHost: ReactHost + get() = getDefaultReactHost(applicationContext, reactNativeHost) + + override fun onCreate() { + super.onCreate() + SoLoader.init(this, OpenSourceMergedSoMapping) + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + // If you opted-in for the New Architecture, we load the native entry point for this app. + load() + } + } +} diff --git a/android/app/src/main/res/drawable/rn_edit_text_material.xml b/android/app/src/main/res/drawable/rn_edit_text_material.xml new file mode 100644 index 0000000..5c25e72 --- /dev/null +++ b/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..1d5d02a Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..1d5d02a Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..fed7c12 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..fed7c12 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..0d78f7e Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..0d78f7e Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..8ef759a Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..8ef759a Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..b4c03d7 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b4c03d7 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/playstore.png b/android/app/src/main/res/playstore.png new file mode 100644 index 0000000..bbab458 Binary files /dev/null and b/android/app/src/main/res/playstore.png differ diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..c4c180d --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Radiologist + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..7ba83a2 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..9766946 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,21 @@ +buildscript { + ext { + buildToolsVersion = "35.0.0" + minSdkVersion = 24 + compileSdkVersion = 35 + targetSdkVersion = 35 + ndkVersion = "27.1.12297006" + kotlinVersion = "2.0.21" + } + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle") + classpath("com.facebook.react:react-native-gradle-plugin") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") + } +} + +apply plugin: "com.facebook.react.rootproject" diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..9fb1566 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,39 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# Use this property to specify which architecture you want to build. +# You can also override it from the CLI using +# ./gradlew -PreactNativeArchitectures=x86_64 +reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 + +# Use this property to enable support to the new architecture. +# This will allow you to use TurboModules and the Fabric render in +# your application. You should enable this flag either if you want +# to write custom TurboModules/Fabric components OR use libraries that +# are providing them. +newArchEnabled=false + +# Use this property to enable or disable the Hermes JS engine. +# If set to false, you will be using JSC instead. +hermesEnabled=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..9bbc975 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..df97d72 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 0000000..faf9300 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright ยฉ 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions ยซ$varยป, ยซ${var}ยป, ยซ${var:-default}ยป, ยซ${var+SET}ยป, +# ยซ${var#prefix}ยป, ยซ${var%suffix}ยป, and ยซ$( cmd )ยป; +# * compound commands having a testable exit status, especially ยซcaseยป; +# * various built-in commands including ยซcommandยป, ยซsetยป, and ยซulimitยป. +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/link-assets-manifest.json b/android/link-assets-manifest.json new file mode 100644 index 0000000..5d7736d --- /dev/null +++ b/android/link-assets-manifest.json @@ -0,0 +1,37 @@ +{ + "migIndex": 1, + "data": [ + { + "path": "app/assets/fonts/WorkSans-Bold.ttf", + "sha1": "ec84061651ead3c3c5cbb61c2d338aca0bacdc1e" + }, + { + "path": "app/assets/fonts/WorkSans-ExtraBold.ttf", + "sha1": "0b371d1dbfbdd15db880bbd129b239530c71accb" + }, + { + "path": "app/assets/fonts/WorkSans-ExtraLight.ttf", + "sha1": "74596e55487e2961b6c43993698d658e2ceee77b" + }, + { + "path": "app/assets/fonts/WorkSans-Light.ttf", + "sha1": "293e11dae7e8b930bf5eea0b06ca979531f22189" + }, + { + "path": "app/assets/fonts/WorkSans-Medium.ttf", + "sha1": "c281f8454dd193c2260e43ae2de171c5dd4086e4" + }, + { + "path": "app/assets/fonts/WorkSans-Regular.ttf", + "sha1": "5e0183b29b57c54595c62ac6bc223b21f1434226" + }, + { + "path": "app/assets/fonts/WorkSans-SemiBold.ttf", + "sha1": "64b8fe156fafce221a0f66504255257053fc6062" + }, + { + "path": "app/assets/fonts/WorkSans-Thin.ttf", + "sha1": "a62251331038fdd079c47bc413a350efbf702db8" + } + ] +} diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..043a1d7 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,6 @@ +pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } +plugins { id("com.facebook.react.settings") } +extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } +rootProject.name = 'NeoScan_Physician' +include ':app' +includeBuild('../node_modules/@react-native/gradle-plugin') diff --git a/app.json b/app.json new file mode 100644 index 0000000..84d4398 --- /dev/null +++ b/app.json @@ -0,0 +1,4 @@ +{ + "name": "NeoScan_Physician", + "displayName": "NeoScan_Physician" +} diff --git a/app/App.tsx b/app/App.tsx new file mode 100644 index 0000000..a8d5695 --- /dev/null +++ b/app/App.tsx @@ -0,0 +1,224 @@ +/* + * File: App.tsx + * Description: Main application component with navigation setup + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useRef } from 'react'; +import { StatusBar } from 'react-native'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native'; +import { theme } from './theme/theme'; +import { RootStackNavigator, setNavigationRef } from './navigation'; +import { StoreProvider } from './store/Provider'; +import Toast from 'react-native-toast-message'; +import { useAppSelector } from './store/hooks'; +import { selectIsAuthenticated } from './modules/Auth/redux/authSelectors'; + +// ============================================================================ +// MOCK DATA SECTION - For demonstration and development purposes +// ============================================================================ + +// Mock dashboard data representing the current state of the ER +const mockDashboard = { + totalPatients: 24, // Total number of patients in ER + criticalPatients: 3, // Number of patients requiring immediate attention + pendingScans: 8, // Number of scans waiting for review + recentReports: 12, // Number of reports generated recently + bedOccupancy: 85, // Percentage of beds currently occupied + departmentStats: { + emergency: 8, // Patients in emergency department + trauma: 4, // Patients in trauma department + cardiac: 3, // Patients in cardiac department + neurology: 2, // Patients in neurology department + pediatrics: 5, // Patients in pediatrics department + icu: 2, // Patients in ICU + }, + shiftInfo: { + currentShift: 'DAY' as const, // Current shift (DAY/NIGHT) + startTime: new Date(), // Shift start time + endTime: new Date(), // Shift end time + attendingPhysician: 'Dr. Smith', // Lead physician on duty + residents: ['Dr. Johnson', 'Dr. Williams'], // Resident physicians + nurses: ['Nurse Brown', 'Nurse Davis'], // Nursing staff + }, + lastUpdated: new Date(), // Last time dashboard was updated +}; + +// Mock patient data representing real patients in the ER +const mockPatients = [ + { + id: '1', // Unique patient identifier + mrn: 'MRN001', // Medical Record Number + firstName: 'John', + lastName: 'Doe', + dateOfBirth: new Date('1985-03-15'), + gender: 'MALE' as const, + age: 38, + bedNumber: 'A1', // Assigned bed number + roomNumber: '101', // Room number + admissionDate: new Date('2024-01-15'), + status: 'ACTIVE' as const, // Current patient status + priority: 'CRITICAL' as const, // Priority level for treatment + department: 'Emergency', + attendingPhysician: 'Dr. Smith', + allergies: [ + { + id: '1', + name: 'Penicillin', + severity: 'SEVERE' as const, + reaction: 'Anaphylaxis' + }, + ], + medications: [ + { + id: '1', + name: 'Morphine', + dosage: '2mg', + frequency: 'Every 4 hours', + route: 'IV', // Administration route + startDate: new Date(), + status: 'ACTIVE' as const, + prescribedBy: 'Dr. Smith', + }, + ], + vitalSigns: { + bloodPressure: { systolic: 140, diastolic: 90, timestamp: new Date() }, + heartRate: { value: 95, timestamp: new Date() }, + temperature: { value: 37.2, timestamp: new Date() }, + respiratoryRate: { value: 18, timestamp: new Date() }, + oxygenSaturation: { value: 98, timestamp: new Date() }, + }, + medicalHistory: [], + currentDiagnosis: 'Chest pain, rule out MI', // Current medical diagnosis + lastUpdated: new Date(), + }, + { + id: '2', + mrn: 'MRN002', + firstName: 'Jane', + lastName: 'Smith', + dateOfBirth: new Date('1990-07-22'), + gender: 'FEMALE' as const, + age: 33, + bedNumber: 'B2', + roomNumber: '102', + admissionDate: new Date('2024-01-15'), + status: 'ACTIVE' as const, + priority: 'HIGH' as const, + department: 'Trauma', + attendingPhysician: 'Dr. Johnson', + allergies: [], + medications: [], + vitalSigns: { + bloodPressure: { systolic: 120, diastolic: 80, timestamp: new Date() }, + heartRate: { value: 88, timestamp: new Date() }, + temperature: { value: 36.8, timestamp: new Date() }, + respiratoryRate: { value: 16, timestamp: new Date() }, + oxygenSaturation: { value: 99, timestamp: new Date() }, + }, + medicalHistory: [], + currentDiagnosis: 'Multiple trauma from MVA', // MVA = Motor Vehicle Accident + lastUpdated: new Date(), + }, +]; + +// Mock alerts representing critical notifications that require immediate attention +const mockAlerts = [ + { + id: '1', + type: 'CRITICAL_FINDING' as const, // Type of alert + priority: 'CRITICAL' as const, // Priority level + title: 'Critical Finding Detected', + message: 'AI has detected a potential brain bleed in CT scan. Immediate review required.', + patientId: '1', // Associated patient + patientName: 'John Doe', + bedNumber: 'A1', + timestamp: new Date(), // When alert was generated + isRead: false, // Whether alert has been read + isAcknowledged: false, // Whether alert has been acknowledged + actionRequired: true, // Whether action is required + }, +]; + +/** + * AppContent Component (Inner Component) + * + * Purpose: Inner component that uses Redux hooks for authentication state + * + * Features: + * 1. Connect to Redux store for authentication state + * 2. Set up navigation container with global reference + * 3. Configure status bar appearance + * 4. Render the main app navigator based on authentication state + * + * Navigation Flow: + * 1. App starts โ†’ Check Redux authentication status + * 2. If not authenticated โ†’ Show LoginScreen + * 3. If authenticated โ†’ Show MainTabNavigator (dashboard) + */ +function AppContent() { + // ============================================================================ + // REDUX STATE + // ============================================================================ + + // Get authentication state from Redux + const isAuthenticated = useAppSelector(selectIsAuthenticated); + + // Navigation reference for programmatic navigation + const navigationRef = useRef | null>(null); + + // ============================================================================ + // EFFECTS + // ============================================================================ + + // Set up navigation reference for global access + React.useEffect(() => { + setNavigationRef(navigationRef); + }, []); + + // ============================================================================ + // RENDER SECTION + // ============================================================================ + + return ( + + + + + + + + ); +} + +/** + * App Component (Root Component) + * + * Purpose: Root component that wraps the entire application with Redux Provider + * + * Features: + * 1. Provide Redux store context + * 2. Wrap the main app content + * 3. Enable Redux state management throughout the app + */ +export default function App() { + return ( + + + + ); +} + +/* + * End of File: App.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/assets/dicom/dicom-viewer.html b/app/assets/dicom/dicom-viewer.html new file mode 100644 index 0000000..4e128a1 --- /dev/null +++ b/app/assets/dicom/dicom-viewer.html @@ -0,0 +1,726 @@ + + + + + +DICOM Viewer - Mobile Friendly + + + + +
+

DICOM Viewer

+
+ Ready to load DICOM files + Loading... +
+
+ +
+
+ + +
+ +
+ + + +
+ +
+
+ +
No images
+ +
+
+
+ +
+
+
๐Ÿฉป
+
DICOM Viewer
+
No image loaded
DICOM files will be loaded from parent component
+
+
+ + + + + + + + + + diff --git a/app/assets/dicom/test-dicom-viewer.html b/app/assets/dicom/test-dicom-viewer.html new file mode 100644 index 0000000..0f7c363 --- /dev/null +++ b/app/assets/dicom/test-dicom-viewer.html @@ -0,0 +1,443 @@ + + + + + + DICOM Viewer Test + + + +
+

DICOM Viewer Test

+

Test the DICOM viewer functionality in your browser before using it in React Native.

+ +
+

Sample DICOM URLs

+
+
+ Sample 1:
+ LIDC-IDRI-0001 +
+
+ Sample 2:
+ LIDC-IDRI-0001 +
+
+ Sample 3:
+ LIDC-IDRI-0001 +
+
+
+ +
+

Custom DICOM URL

+ + +
+ +
+
Ready to load DICOM image
+
+
Click a sample URL above or enter a custom URL to load a DICOM image
+
+
+ + + +
+
+ + + + diff --git a/app/assets/fonts/WorkSans-Bold.ttf b/app/assets/fonts/WorkSans-Bold.ttf new file mode 100644 index 0000000..9542d67 Binary files /dev/null and b/app/assets/fonts/WorkSans-Bold.ttf differ diff --git a/app/assets/fonts/WorkSans-ExtraBold.ttf b/app/assets/fonts/WorkSans-ExtraBold.ttf new file mode 100644 index 0000000..6dc6fca Binary files /dev/null and b/app/assets/fonts/WorkSans-ExtraBold.ttf differ diff --git a/app/assets/fonts/WorkSans-ExtraLight.ttf b/app/assets/fonts/WorkSans-ExtraLight.ttf new file mode 100644 index 0000000..a6d4617 Binary files /dev/null and b/app/assets/fonts/WorkSans-ExtraLight.ttf differ diff --git a/app/assets/fonts/WorkSans-Light.ttf b/app/assets/fonts/WorkSans-Light.ttf new file mode 100644 index 0000000..1fe1fd1 Binary files /dev/null and b/app/assets/fonts/WorkSans-Light.ttf differ diff --git a/app/assets/fonts/WorkSans-Medium.ttf b/app/assets/fonts/WorkSans-Medium.ttf new file mode 100644 index 0000000..2168073 Binary files /dev/null and b/app/assets/fonts/WorkSans-Medium.ttf differ diff --git a/app/assets/fonts/WorkSans-Regular.ttf b/app/assets/fonts/WorkSans-Regular.ttf new file mode 100644 index 0000000..d24586c Binary files /dev/null and b/app/assets/fonts/WorkSans-Regular.ttf differ diff --git a/app/assets/fonts/WorkSans-SemiBold.ttf b/app/assets/fonts/WorkSans-SemiBold.ttf new file mode 100644 index 0000000..a75721c Binary files /dev/null and b/app/assets/fonts/WorkSans-SemiBold.ttf differ diff --git a/app/assets/fonts/WorkSans-Thin.ttf b/app/assets/fonts/WorkSans-Thin.ttf new file mode 100644 index 0000000..a097d7c Binary files /dev/null and b/app/assets/fonts/WorkSans-Thin.ttf differ diff --git a/app/assets/images/coming-soon.jpg b/app/assets/images/coming-soon.jpg new file mode 100644 index 0000000..7e042c6 Binary files /dev/null and b/app/assets/images/coming-soon.jpg differ diff --git a/app/assets/images/default-avatar.png b/app/assets/images/default-avatar.png new file mode 100644 index 0000000..1c927a4 Binary files /dev/null and b/app/assets/images/default-avatar.png differ diff --git a/app/assets/images/hospital-logo.png b/app/assets/images/hospital-logo.png new file mode 100644 index 0000000..76c4ca4 Binary files /dev/null and b/app/assets/images/hospital-logo.png differ diff --git a/app/modules/AIPrediction/__tests__/AIPredictionCard.test.tsx b/app/modules/AIPrediction/__tests__/AIPredictionCard.test.tsx new file mode 100644 index 0000000..4c120e4 --- /dev/null +++ b/app/modules/AIPrediction/__tests__/AIPredictionCard.test.tsx @@ -0,0 +1,249 @@ +/* + * File: AIPredictionCard.test.tsx + * Description: Unit tests for AI Prediction Card component + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { render, fireEvent } from '@testing-library/react-native'; +import AIPredictionCard from '../components/AIPredictionCard'; +import type { AIPredictionCase } from '../types'; + +// ============================================================================ +// MOCK DATA +// ============================================================================ + +const mockPredictionCase: AIPredictionCase = { + patid: 'test-patient-001', + hospital_id: 'hospital-123', + prediction: { + label: 'midline shift', + finding_type: 'pathology', + clinical_urgency: 'urgent', + confidence_score: 0.96, + finding_category: 'abnormal', + primary_severity: 'high', + anatomical_location: 'brain', + }, + created_at: '2024-01-15T10:30:00Z', + updated_at: '2024-01-15T10:30:00Z', + review_status: 'pending', + priority: 'critical', +}; + +const mockProps = { + predictionCase: mockPredictionCase, + onPress: jest.fn(), + onReview: jest.fn(), + onToggleSelect: jest.fn(), + isSelected: false, + showReviewButton: true, +}; + +// ============================================================================ +// UNIT TESTS +// ============================================================================ + +describe('AIPredictionCard', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + // ============================================================================ + // RENDERING TESTS + // ============================================================================ + + describe('Rendering', () => { + it('should render correctly with required props', () => { + const { getByText } = render( + + ); + + expect(getByText('test-patient-001')).toBeTruthy(); + expect(getByText('Midline Shift')).toBeTruthy(); + expect(getByText('96%')).toBeTruthy(); + expect(getByText('Urgent')).toBeTruthy(); + }); + + it('should render review button when showReviewButton is true', () => { + const { getByText } = render(); + expect(getByText('Review')).toBeTruthy(); + }); + + it('should not render review button when showReviewButton is false', () => { + const { queryByText } = render( + + ); + expect(queryByText('Review')).toBeNull(); + }); + + it('should not render review button when status is not pending', () => { + const reviewedCase = { + ...mockPredictionCase, + review_status: 'reviewed' as const, + }; + + const { queryByText } = render( + + ); + expect(queryByText('Review')).toBeNull(); + }); + + it('should render selection checkbox when onToggleSelect is provided', () => { + const { getByRole } = render(); + expect(getByRole('checkbox')).toBeTruthy(); + }); + + it('should show selected state correctly', () => { + const { getByRole } = render( + + ); + + const checkbox = getByRole('checkbox'); + expect(checkbox.props.accessibilityState.checked).toBe(true); + }); + }); + + // ============================================================================ + // INTERACTION TESTS + // ============================================================================ + + describe('Interactions', () => { + it('should call onPress when card is pressed', () => { + const { getByRole } = render(); + + fireEvent.press(getByRole('button')); + expect(mockProps.onPress).toHaveBeenCalledWith(mockPredictionCase); + }); + + it('should call onReview when review button is pressed', () => { + const { getByText } = render(); + + fireEvent.press(getByText('Review')); + expect(mockProps.onReview).toHaveBeenCalledWith('test-patient-001'); + }); + + it('should call onToggleSelect when checkbox is pressed', () => { + const { getByRole } = render(); + + fireEvent.press(getByRole('checkbox')); + expect(mockProps.onToggleSelect).toHaveBeenCalledWith('test-patient-001'); + }); + }); + + // ============================================================================ + // DATA FORMATTING TESTS + // ============================================================================ + + describe('Data formatting', () => { + it('should format confidence score as percentage', () => { + const { getByText } = render(); + expect(getByText('96%')).toBeTruthy(); + }); + + it('should capitalize text correctly', () => { + const { getByText } = render(); + expect(getByText('Midline Shift')).toBeTruthy(); + expect(getByText('Pathology')).toBeTruthy(); + expect(getByText('Abnormal')).toBeTruthy(); + }); + + it('should handle missing anatomical location', () => { + const caseWithoutLocation = { + ...mockPredictionCase, + prediction: { + ...mockPredictionCase.prediction, + anatomical_location: 'not_applicable', + }, + }; + + const { queryByText } = render( + + ); + + // Should not render location when it's 'not_applicable' + expect(queryByText('Not Applicable')).toBeNull(); + }); + }); + + // ============================================================================ + // ACCESSIBILITY TESTS + // ============================================================================ + + describe('Accessibility', () => { + it('should have proper accessibility labels', () => { + const { getByLabelText } = render(); + + expect( + getByLabelText('AI Prediction case for patient test-patient-001') + ).toBeTruthy(); + }); + + it('should have proper accessibility hints', () => { + const { getByRole } = render(); + + const cardButton = getByRole('button'); + expect(cardButton.props.accessibilityHint).toBe( + 'Tap to view detailed prediction information' + ); + }); + }); + + // ============================================================================ + // EDGE CASES TESTS + // ============================================================================ + + describe('Edge cases', () => { + it('should handle missing dates gracefully', () => { + const caseWithoutDates = { + ...mockPredictionCase, + created_at: undefined, + updated_at: undefined, + }; + + const { getByText } = render( + + ); + + expect(getByText('N/A')).toBeTruthy(); + }); + + it('should handle emergency urgency with special styling', () => { + const emergencyCase = { + ...mockPredictionCase, + prediction: { + ...mockPredictionCase.prediction, + clinical_urgency: 'emergency' as const, + }, + }; + + const { getByText } = render( + + ); + + expect(getByText('Emergency')).toBeTruthy(); + }); + }); +}); + +/* + * End of File: AIPredictionCard.test.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/__tests__/aiPredictionAPI.test.ts b/app/modules/AIPrediction/__tests__/aiPredictionAPI.test.ts new file mode 100644 index 0000000..fb85b00 --- /dev/null +++ b/app/modules/AIPrediction/__tests__/aiPredictionAPI.test.ts @@ -0,0 +1,361 @@ +/* + * File: aiPredictionAPI.test.ts + * Description: Unit tests for AI Prediction API service + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { aiPredictionAPI } from '../services/aiPredictionAPI'; + +// Mock apisauce +jest.mock('apisauce', () => ({ + create: jest.fn(() => ({ + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + delete: jest.fn(), + })), +})); + +// Mock API utilities +jest.mock('../../../shared/utils', () => ({ + API_CONFIG: { + BASE_URL: 'https://test-api.com', + }, + buildHeaders: jest.fn((options = {}) => ({ + headers: { + 'Content-Type': 'application/json', + ...(options.token && { Authorization: `Bearer ${options.token}` }), + }, + })), +})); + +// ============================================================================ +// MOCK DATA +// ============================================================================ + +const mockToken = 'test-token-123'; +const mockResponse = { + ok: true, + data: { + success: true, + data: [ + { + patid: 'test-001', + hospital_id: 'hospital-001', + prediction: { + label: 'test finding', + finding_type: 'pathology', + clinical_urgency: 'urgent', + confidence_score: 0.95, + finding_category: 'abnormal', + primary_severity: 'high', + anatomical_location: 'brain', + }, + }, + ], + }, +}; + +// ============================================================================ +// UNIT TESTS +// ============================================================================ + +describe('AI Prediction API', () => { + let mockApi: any; + + beforeEach(() => { + // Reset mocks + jest.clearAllMocks(); + + // Get the mocked API instance + const { create } = require('apisauce'); + mockApi = create(); + }); + + // ============================================================================ + // GET ALL PREDICTIONS TESTS + // ============================================================================ + + describe('getAllPredictions', () => { + it('should call GET endpoint with correct parameters', async () => { + mockApi.get.mockResolvedValue(mockResponse); + + const params = { + page: 1, + limit: 20, + urgency: 'urgent', + search: 'test', + }; + + await aiPredictionAPI.getAllPredictions(mockToken, params); + + expect(mockApi.get).toHaveBeenCalledWith( + '/api/ai-cases/all-prediction-results', + params, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${mockToken}`, + }), + }) + ); + }); + + it('should call GET endpoint without parameters', async () => { + mockApi.get.mockResolvedValue(mockResponse); + + await aiPredictionAPI.getAllPredictions(mockToken); + + expect(mockApi.get).toHaveBeenCalledWith( + '/api/ai-cases/all-prediction-results', + {}, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${mockToken}`, + }), + }) + ); + }); + }); + + // ============================================================================ + // GET CASE DETAILS TESTS + // ============================================================================ + + describe('getCaseDetails', () => { + it('should call GET endpoint with correct case ID', async () => { + const caseId = 'test-case-001'; + mockApi.get.mockResolvedValue(mockResponse); + + await aiPredictionAPI.getCaseDetails(caseId, mockToken); + + expect(mockApi.get).toHaveBeenCalledWith( + `/api/ai-cases/prediction-details/${caseId}`, + {}, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${mockToken}`, + }), + }) + ); + }); + }); + + // ============================================================================ + // UPDATE CASE REVIEW TESTS + // ============================================================================ + + describe('updateCaseReview', () => { + it('should call PUT endpoint with correct data', async () => { + const caseId = 'test-case-001'; + const reviewData = { + review_status: 'reviewed' as const, + reviewed_by: 'Dr. Test', + review_notes: 'Test notes', + priority: 'high' as const, + }; + + mockApi.put.mockResolvedValue(mockResponse); + + await aiPredictionAPI.updateCaseReview(caseId, reviewData, mockToken); + + expect(mockApi.put).toHaveBeenCalledWith( + `/api/ai-cases/review/${caseId}`, + reviewData, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${mockToken}`, + }), + }) + ); + }); + }); + + // ============================================================================ + // GET STATISTICS TESTS + // ============================================================================ + + describe('getPredictionStats', () => { + it('should call GET endpoint with time range parameter', async () => { + mockApi.get.mockResolvedValue(mockResponse); + + await aiPredictionAPI.getPredictionStats(mockToken, 'week'); + + expect(mockApi.get).toHaveBeenCalledWith( + '/api/ai-cases/statistics', + { timeRange: 'week' }, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${mockToken}`, + }), + }) + ); + }); + + it('should call GET endpoint without time range parameter', async () => { + mockApi.get.mockResolvedValue(mockResponse); + + await aiPredictionAPI.getPredictionStats(mockToken); + + expect(mockApi.get).toHaveBeenCalledWith( + '/api/ai-cases/statistics', + {}, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${mockToken}`, + }), + }) + ); + }); + }); + + // ============================================================================ + // SEARCH PREDICTIONS TESTS + // ============================================================================ + + describe('searchPredictions', () => { + it('should call GET endpoint with search query and filters', async () => { + const query = 'test search'; + const filters = { + urgency: ['urgent', 'emergency'], + severity: ['high'], + dateRange: { start: '2024-01-01', end: '2024-01-31' }, + }; + + mockApi.get.mockResolvedValue(mockResponse); + + await aiPredictionAPI.searchPredictions(query, mockToken, filters); + + expect(mockApi.get).toHaveBeenCalledWith( + '/api/ai-cases/search', + { + q: query, + filters: JSON.stringify(filters), + }, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${mockToken}`, + }), + }) + ); + }); + + it('should call GET endpoint with only search query', async () => { + const query = 'test search'; + mockApi.get.mockResolvedValue(mockResponse); + + await aiPredictionAPI.searchPredictions(query, mockToken); + + expect(mockApi.get).toHaveBeenCalledWith( + '/api/ai-cases/search', + { q: query }, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${mockToken}`, + }), + }) + ); + }); + }); + + // ============================================================================ + // BULK OPERATIONS TESTS + // ============================================================================ + + describe('bulkUpdateReviews', () => { + it('should call PUT endpoint with case IDs and review data', async () => { + const caseIds = ['case-001', 'case-002', 'case-003']; + const reviewData = { + review_status: 'reviewed' as const, + reviewed_by: 'Dr. Test', + review_notes: 'Bulk review', + }; + + mockApi.put.mockResolvedValue(mockResponse); + + await aiPredictionAPI.bulkUpdateReviews(caseIds, reviewData, mockToken); + + expect(mockApi.put).toHaveBeenCalledWith( + '/api/ai-cases/bulk-review', + { caseIds, reviewData }, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${mockToken}`, + }), + }) + ); + }); + }); + + // ============================================================================ + // SUBMIT FEEDBACK TESTS + // ============================================================================ + + describe('submitPredictionFeedback', () => { + it('should call POST endpoint with feedback data', async () => { + const caseId = 'test-case-001'; + const feedbackData = { + accuracy_rating: 4 as const, + is_accurate: true, + physician_diagnosis: 'Confirmed midline shift', + feedback_notes: 'Accurate prediction', + improvement_suggestions: 'None', + }; + + mockApi.post.mockResolvedValue(mockResponse); + + await aiPredictionAPI.submitPredictionFeedback(caseId, feedbackData, mockToken); + + expect(mockApi.post).toHaveBeenCalledWith( + `/api/ai-cases/feedback/${caseId}`, + feedbackData, + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Bearer ${mockToken}`, + }), + }) + ); + }); + }); + + // ============================================================================ + // ERROR HANDLING TESTS + // ============================================================================ + + describe('Error handling', () => { + it('should handle API errors gracefully', async () => { + const errorResponse = { + ok: false, + problem: 'NETWORK_ERROR', + data: null, + }; + + mockApi.get.mockResolvedValue(errorResponse); + + const result = await aiPredictionAPI.getAllPredictions(mockToken); + expect(result).toEqual(errorResponse); + }); + + it('should handle missing token', async () => { + mockApi.get.mockResolvedValue(mockResponse); + + await aiPredictionAPI.getAllPredictions(''); + + expect(mockApi.get).toHaveBeenCalledWith( + '/api/ai-cases/all-prediction-results', + {}, + expect.objectContaining({ + headers: expect.objectContaining({ + 'Content-Type': 'application/json', + }), + }) + ); + }); + }); +}); + +/* + * End of File: aiPredictionAPI.test.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/__tests__/aiPredictionSlice.test.ts b/app/modules/AIPrediction/__tests__/aiPredictionSlice.test.ts new file mode 100644 index 0000000..7975d6c --- /dev/null +++ b/app/modules/AIPrediction/__tests__/aiPredictionSlice.test.ts @@ -0,0 +1,231 @@ +/* + * File: aiPredictionSlice.test.ts + * Description: Unit tests for AI Prediction Redux slice + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import aiPredictionReducer, { + setSearchQuery, + setUrgencyFilter, + setSeverityFilter, + setCategoryFilter, + clearAllFilters, + toggleShowFilters, + clearError, +} from '../redux/aiPredictionSlice'; +import type { AIPredictionState } from '../types'; + +// ============================================================================ +// MOCK DATA +// ============================================================================ + +const initialState: AIPredictionState = { + predictionCases: [], + currentCase: null, + isLoading: false, + isRefreshing: false, + isLoadingCaseDetails: false, + error: null, + searchQuery: '', + selectedUrgencyFilter: 'all', + selectedSeverityFilter: 'all', + selectedCategoryFilter: 'all', + sortBy: 'date', + sortOrder: 'desc', + currentPage: 1, + itemsPerPage: 20, + totalItems: 0, + lastUpdated: null, + cacheExpiry: null, + showFilters: false, + selectedCaseIds: [], +}; + +// ============================================================================ +// UNIT TESTS +// ============================================================================ + +describe('AI Prediction Slice', () => { + // ============================================================================ + // INITIAL STATE TESTS + // ============================================================================ + + it('should return the initial state', () => { + const result = aiPredictionReducer(undefined, { type: 'unknown' }); + expect(result).toEqual(initialState); + }); + + // ============================================================================ + // SEARCH TESTS + // ============================================================================ + + describe('Search functionality', () => { + it('should handle setSearchQuery', () => { + const searchQuery = 'test search'; + const action = setSearchQuery(searchQuery); + const result = aiPredictionReducer(initialState, action); + + expect(result.searchQuery).toBe(searchQuery); + expect(result.currentPage).toBe(1); // Should reset to first page + }); + + it('should handle empty search query', () => { + const state = { ...initialState, searchQuery: 'existing search' }; + const action = setSearchQuery(''); + const result = aiPredictionReducer(state, action); + + expect(result.searchQuery).toBe(''); + expect(result.currentPage).toBe(1); + }); + }); + + // ============================================================================ + // FILTER TESTS + // ============================================================================ + + describe('Filter functionality', () => { + it('should handle setUrgencyFilter', () => { + const filter = 'urgent'; + const action = setUrgencyFilter(filter); + const result = aiPredictionReducer(initialState, action); + + expect(result.selectedUrgencyFilter).toBe(filter); + expect(result.currentPage).toBe(1); // Should reset to first page + }); + + it('should handle setSeverityFilter', () => { + const filter = 'high'; + const action = setSeverityFilter(filter); + const result = aiPredictionReducer(initialState, action); + + expect(result.selectedSeverityFilter).toBe(filter); + expect(result.currentPage).toBe(1); + }); + + it('should handle setCategoryFilter', () => { + const filter = 'critical'; + const action = setCategoryFilter(filter); + const result = aiPredictionReducer(initialState, action); + + expect(result.selectedCategoryFilter).toBe(filter); + expect(result.currentPage).toBe(1); + }); + + it('should handle clearAllFilters', () => { + const state: AIPredictionState = { + ...initialState, + searchQuery: 'test', + selectedUrgencyFilter: 'urgent', + selectedSeverityFilter: 'high', + selectedCategoryFilter: 'critical', + currentPage: 3, + }; + + const action = clearAllFilters(); + const result = aiPredictionReducer(state, action); + + expect(result.searchQuery).toBe(''); + expect(result.selectedUrgencyFilter).toBe('all'); + expect(result.selectedSeverityFilter).toBe('all'); + expect(result.selectedCategoryFilter).toBe('all'); + expect(result.currentPage).toBe(1); + }); + }); + + // ============================================================================ + // UI STATE TESTS + // ============================================================================ + + describe('UI state functionality', () => { + it('should handle toggleShowFilters', () => { + const action = toggleShowFilters(); + + // Toggle from false to true + const result1 = aiPredictionReducer(initialState, action); + expect(result1.showFilters).toBe(true); + + // Toggle from true to false + const result2 = aiPredictionReducer(result1, action); + expect(result2.showFilters).toBe(false); + }); + + it('should handle clearError', () => { + const state = { ...initialState, error: 'Test error' }; + const action = clearError(); + const result = aiPredictionReducer(state, action); + + expect(result.error).toBe(null); + }); + }); + + // ============================================================================ + // ASYNC ACTION TESTS + // ============================================================================ + + describe('Async actions', () => { + it('should handle fetchAIPredictions.pending', () => { + const action = { type: 'aiPrediction/fetchAIPredictions/pending' }; + const result = aiPredictionReducer(initialState, action); + + expect(result.isLoading).toBe(true); + expect(result.error).toBe(null); + }); + + it('should handle fetchAIPredictions.fulfilled', () => { + const mockCases = [ + { + patid: 'test-001', + hospital_id: 'hospital-001', + prediction: { + label: 'test finding', + finding_type: 'pathology' as const, + clinical_urgency: 'urgent' as const, + confidence_score: 0.95, + finding_category: 'abnormal' as const, + primary_severity: 'high' as const, + anatomical_location: 'brain', + }, + }, + ]; + + const action = { + type: 'aiPrediction/fetchAIPredictions/fulfilled', + payload: { + cases: mockCases, + total: 1, + page: 1, + limit: 20, + }, + }; + + const result = aiPredictionReducer(initialState, action); + + expect(result.isLoading).toBe(false); + expect(result.predictionCases).toEqual(mockCases); + expect(result.totalItems).toBe(1); + expect(result.error).toBe(null); + expect(result.lastUpdated).toBeInstanceOf(Date); + expect(result.cacheExpiry).toBeInstanceOf(Date); + }); + + it('should handle fetchAIPredictions.rejected', () => { + const errorMessage = 'Failed to fetch predictions'; + const action = { + type: 'aiPrediction/fetchAIPredictions/rejected', + payload: errorMessage, + }; + + const result = aiPredictionReducer(initialState, action); + + expect(result.isLoading).toBe(false); + expect(result.error).toBe(errorMessage); + }); + }); +}); + +/* + * End of File: aiPredictionSlice.test.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/components/AIPredictionCard.tsx b/app/modules/AIPrediction/components/AIPredictionCard.tsx new file mode 100644 index 0000000..590d57a --- /dev/null +++ b/app/modules/AIPrediction/components/AIPredictionCard.tsx @@ -0,0 +1,522 @@ +/* + * File: AIPredictionCard.tsx + * Description: Card component for displaying AI prediction case information + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Dimensions, +} from 'react-native'; +import Icon from 'react-native-vector-icons/Feather'; +import { theme } from '../../../theme'; +import { AIPredictionCase, URGENCY_COLORS, SEVERITY_COLORS, CATEGORY_COLORS } from '../types'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface AIPredictionCardProps { + predictionCase: AIPredictionCase; + onPress: (predictionCase: AIPredictionCase) => void; + onReview?: (caseId: string) => void; + isSelected?: boolean; + onToggleSelect?: (caseId: string) => void; + showReviewButton?: boolean; +} + +// ============================================================================ +// CONSTANTS +// ============================================================================ + +const { width } = Dimensions.get('window'); +const CARD_WIDTH = width - 32; // Full width with margins + +// ============================================================================ +// AI PREDICTION CARD COMPONENT +// ============================================================================ + +/** + * AIPredictionCard Component + * + * Purpose: Display AI prediction case information in a card format + * + * Features: + * - Patient ID and hospital information + * - AI prediction results with confidence score + * - Urgency and severity indicators + * - Review status and actions + * - Selection support for bulk operations + * - Modern card design with proper spacing + * - Color-coded priority indicators + * - Accessibility support + */ +const AIPredictionCard: React.FC = ({ + predictionCase, + onPress, + onReview, + isSelected = false, + onToggleSelect, + showReviewButton = true, +}) => { + // ============================================================================ + // HELPER FUNCTIONS + // ============================================================================ + + /** + * Get Urgency Color + * + * Purpose: Get color based on clinical urgency + */ + const getUrgencyColor = (urgency: string): string => { + return URGENCY_COLORS[urgency as keyof typeof URGENCY_COLORS] || theme.colors.textMuted; + }; + + /** + * Get Severity Color + * + * Purpose: Get color based on primary severity + */ + const getSeverityColor = (severity: string): string => { + return SEVERITY_COLORS[severity as keyof typeof SEVERITY_COLORS] || theme.colors.textMuted; + }; + + /** + * Get Category Color + * + * Purpose: Get color based on finding category + */ + const getCategoryColor = (category: string): string => { + return CATEGORY_COLORS[category as keyof typeof CATEGORY_COLORS] || theme.colors.textMuted; + }; + + /** + * Get Review Status Color + * + * Purpose: Get color based on review status + */ + const getReviewStatusColor = (status: string): string => { + switch (status) { + case 'confirmed': + return theme.colors.success; + case 'reviewed': + return theme.colors.info; + case 'disputed': + return theme.colors.warning; + case 'pending': + default: + return theme.colors.error; + } + }; + + /** + * Format Confidence Score + * + * Purpose: Format confidence score as percentage + */ + const formatConfidence = (score: number): string => { + return `${Math.round(score * 100)}%`; + }; + + /** + * Capitalize Text + * + * Purpose: Capitalize first letter of each word + */ + const capitalize = (text: string): string => { + return text.split('_').map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(' '); + }; + + /** + * Format Date + * + * Purpose: Format date for display + */ + const formatDate = (dateString?: string): string => { + if (!dateString) return 'N/A'; + try { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + } catch { + return 'N/A'; + } + }; + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Handle Card Press + * + * Purpose: Handle card tap to view details + */ + const handleCardPress = () => { + onPress(predictionCase); + }; + + /** + * Handle Review Press + * + * Purpose: Handle review button press + */ + const handleReviewPress = (event: any) => { + event.stopPropagation(); + if (onReview) { + onReview(predictionCase.patid); + } + }; + + /** + * Handle Selection Toggle + * + * Purpose: Handle case selection toggle + */ + const handleSelectionToggle = (event: any) => { + event.stopPropagation(); + if (onToggleSelect) { + onToggleSelect(predictionCase.patid); + } + }; + + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + + {/* Header Section */} + + + + {predictionCase.patid} + + + {formatDate(predictionCase.processed_at)} + + + + + {onToggleSelect && ( + + + + )} + + + + {capitalize(predictionCase.prediction.clinical_urgency)} + + + + + + {/* Prediction Information */} + + + + {capitalize(predictionCase.prediction.label)} + + + + + {formatConfidence(predictionCase.prediction.confidence_score)} + + + + + {/* Finding Details */} + + + Type: + + {capitalize(predictionCase.prediction.finding_type)} + + + + + Category: + + + {capitalize(predictionCase.prediction.finding_category)} + + + + + + {/* Severity and Location */} + + + + + {capitalize(predictionCase.prediction.primary_severity)} Severity + + + + {predictionCase.prediction.anatomical_location !== 'not_applicable' && ( + + + + {capitalize(predictionCase.prediction.anatomical_location)} + + + )} + + + + {/* Footer Section */} + + + + + {capitalize(predictionCase.review_status || 'pending')} + + + + {predictionCase.reviewed_by && ( + + by {predictionCase.reviewed_by} + + )} + + + {showReviewButton && predictionCase.review_status === 'pending' && ( + + + Review + + )} + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.large, + padding: theme.spacing.lg, + marginHorizontal: theme.spacing.md, + marginVertical: theme.spacing.sm, + width: CARD_WIDTH, + ...theme.shadows.medium, + borderWidth: 1, + borderColor: theme.colors.border, + }, + selectedContainer: { + borderColor: theme.colors.primary, + borderWidth: 2, + }, + emergencyContainer: { + borderLeftWidth: 4, + borderLeftColor: URGENCY_COLORS.emergency, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: theme.spacing.md, + }, + headerLeft: { + flex: 1, + }, + headerRight: { + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing.sm, + }, + patientId: { + fontSize: theme.typography.fontSize.bodyLarge, + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + date: { + fontSize: theme.typography.fontSize.bodySmall, + color: theme.colors.textSecondary, + }, + selectionButton: { + padding: theme.spacing.xs, + }, + priorityBadge: { + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + borderRadius: theme.borderRadius.small, + }, + priorityText: { + fontSize: theme.typography.fontSize.caption, + fontWeight: theme.typography.fontWeight.medium, + color: theme.colors.background, + }, + predictionSection: { + marginBottom: theme.spacing.md, + }, + predictionHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: theme.spacing.sm, + }, + predictionLabel: { + fontSize: theme.typography.fontSize.bodyLarge, + fontWeight: theme.typography.fontWeight.medium, + color: theme.colors.textPrimary, + flex: 1, + marginRight: theme.spacing.sm, + }, + confidenceContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing.xs, + }, + confidenceText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontWeight: theme.typography.fontWeight.medium, + color: theme.colors.primary, + }, + findingDetails: { + marginBottom: theme.spacing.sm, + }, + findingItem: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: theme.spacing.xs, + }, + findingLabel: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textSecondary, + fontWeight: theme.typography.fontWeight.medium, + }, + findingValue: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textPrimary, + }, + categoryBadge: { + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + borderRadius: theme.borderRadius.small, + }, + categoryText: { + fontSize: theme.typography.fontSize.caption, + fontWeight: theme.typography.fontWeight.medium, + color: theme.colors.background, + }, + detailsRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + detailItem: { + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing.xs, + flex: 1, + }, + detailText: { + fontSize: theme.typography.fontSize.bodySmall, + color: theme.colors.textSecondary, + }, + footer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingTop: theme.spacing.sm, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + footerLeft: { + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing.sm, + flex: 1, + }, + reviewStatusBadge: { + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + borderRadius: theme.borderRadius.small, + }, + reviewStatusText: { + fontSize: theme.typography.fontSize.caption, + fontWeight: theme.typography.fontWeight.medium, + color: theme.colors.background, + }, + reviewedBy: { + fontSize: theme.typography.fontSize.caption, + color: theme.colors.textMuted, + fontStyle: 'italic', + }, + reviewButton: { + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing.xs, + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + borderRadius: theme.borderRadius.small, + borderWidth: 1, + borderColor: theme.colors.primary, + }, + reviewButtonText: { + fontSize: theme.typography.fontSize.bodySmall, + color: theme.colors.primary, + fontWeight: theme.typography.fontWeight.medium, + }, +}); + +export default AIPredictionCard; + +/* + * End of File: AIPredictionCard.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/components/EmptyState.tsx b/app/modules/AIPrediction/components/EmptyState.tsx new file mode 100644 index 0000000..81b26d7 --- /dev/null +++ b/app/modules/AIPrediction/components/EmptyState.tsx @@ -0,0 +1,287 @@ +/* + * File: EmptyState.tsx + * Description: Empty state component for AI predictions + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Dimensions, +} from 'react-native'; +import Icon from 'react-native-vector-icons/Feather'; +import { theme } from '../../../theme'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface EmptyStateProps { + title?: string; + message?: string; + iconName?: string; + actionText?: string; + onAction?: () => void; + style?: any; + showRefreshButton?: boolean; + onRefresh?: () => void; +} + +// ============================================================================ +// CONSTANTS +// ============================================================================ + +const { width, height } = Dimensions.get('window'); + +// ============================================================================ +// EMPTY STATE COMPONENT +// ============================================================================ + +/** + * EmptyState Component + * + * Purpose: Display empty state for AI predictions + * + * Features: + * - Customizable title and message + * - Icon display with customizable icon + * - Optional action button + * - Refresh functionality + * - Responsive design + * - Modern empty state design + * - Accessibility support + */ +const EmptyState: React.FC = ({ + title = 'No AI Predictions Found', + message = 'There are no AI prediction cases available at the moment. Try adjusting your filters or refresh to see new predictions.', + iconName = 'brain', + actionText = 'Refresh', + onAction, + style, + showRefreshButton = true, + onRefresh, +}) => { + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Handle Action Press + * + * Purpose: Handle action button press + */ + const handleActionPress = () => { + if (onAction) { + onAction(); + } else if (onRefresh) { + onRefresh(); + } + }; + + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + + {/* Empty State Icon */} + + + + + {/* Empty State Title */} + + {title} + + + {/* Empty State Message */} + + {message} + + + {/* Action Buttons */} + + {/* Primary Action Button */} + {(onAction || onRefresh) && ( + + + + {actionText} + + + )} + + {/* Secondary Refresh Button */} + {showRefreshButton && onRefresh && !onAction && ( + + + + Refresh Data + + + )} + + + {/* Suggestions */} + + Try: + + + + Clearing search filters + + + + Adjusting filter criteria + + + + Refreshing the data + + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: theme.spacing.xl, + paddingVertical: theme.spacing.xxl, + minHeight: height * 0.4, + }, + iconContainer: { + width: 120, + height: 120, + borderRadius: 60, + backgroundColor: theme.colors.backgroundAlt, + justifyContent: 'center', + alignItems: 'center', + marginBottom: theme.spacing.xl, + ...theme.shadows.small, + }, + icon: { + opacity: 0.6, + }, + title: { + fontSize: theme.typography.fontSize.displaySmall, + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.textPrimary, + textAlign: 'center', + marginBottom: theme.spacing.md, + }, + message: { + fontSize: theme.typography.fontSize.bodyLarge, + color: theme.colors.textSecondary, + textAlign: 'center', + lineHeight: theme.typography.lineHeight.relaxed * theme.typography.fontSize.bodyLarge, + marginBottom: theme.spacing.xl, + maxWidth: width * 0.8, + }, + buttonsContainer: { + flexDirection: 'row', + gap: theme.spacing.md, + marginBottom: theme.spacing.xl, + }, + actionButton: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.primary, + paddingHorizontal: theme.spacing.lg, + paddingVertical: theme.spacing.md, + borderRadius: theme.borderRadius.medium, + gap: theme.spacing.sm, + ...theme.shadows.medium, + }, + actionButtonText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontWeight: theme.typography.fontWeight.medium, + color: theme.colors.background, + }, + secondaryButton: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: 'transparent', + borderWidth: 1, + borderColor: theme.colors.primary, + paddingHorizontal: theme.spacing.lg, + paddingVertical: theme.spacing.md, + borderRadius: theme.borderRadius.medium, + gap: theme.spacing.sm, + }, + secondaryButtonText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontWeight: theme.typography.fontWeight.medium, + color: theme.colors.primary, + }, + buttonIcon: { + // No additional styles needed + }, + suggestionsContainer: { + alignItems: 'center', + maxWidth: width * 0.8, + }, + suggestionsTitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontWeight: theme.typography.fontWeight.medium, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.sm, + }, + suggestionsList: { + gap: theme.spacing.sm, + }, + suggestionItem: { + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + }, + suggestionText: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textMuted, + }, +}); + +export default EmptyState; + +/* + * End of File: EmptyState.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/components/FilterTabs.tsx b/app/modules/AIPrediction/components/FilterTabs.tsx new file mode 100644 index 0000000..3c1c82d --- /dev/null +++ b/app/modules/AIPrediction/components/FilterTabs.tsx @@ -0,0 +1,368 @@ +/* + * File: FilterTabs.tsx + * Description: Filter tabs component for AI predictions + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + ScrollView, + Dimensions, +} from 'react-native'; +import Icon from 'react-native-vector-icons/Feather'; +import { theme } from '../../../theme'; +import type { AIPredictionState } from '../types'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface FilterTabsProps { + selectedUrgencyFilter: AIPredictionState['selectedUrgencyFilter']; + selectedSeverityFilter: AIPredictionState['selectedSeverityFilter']; + selectedCategoryFilter: AIPredictionState['selectedCategoryFilter']; + onUrgencyFilterChange: (filter: AIPredictionState['selectedUrgencyFilter']) => void; + onSeverityFilterChange: (filter: AIPredictionState['selectedSeverityFilter']) => void; + onCategoryFilterChange: (filter: AIPredictionState['selectedCategoryFilter']) => void; + onClearFilters: () => void; + filterCounts?: { + urgency: Record; + severity: Record; + category: Record; + }; + activeFiltersCount?: number; +} + +interface FilterOption { + label: string; + value: string; + count?: number; + color?: string; +} + +// ============================================================================ +// CONSTANTS +// ============================================================================ + +const { width } = Dimensions.get('window'); + +const URGENCY_FILTERS: FilterOption[] = [ + { label: 'All', value: 'all' }, + { label: 'Emergency', value: 'emergency', color: '#F44336' }, + { label: 'Urgent', value: 'urgent', color: '#FF5722' }, + { label: 'Moderate', value: 'moderate', color: '#FF9800' }, + { label: 'Low', value: 'low', color: '#FFC107' }, + { label: 'Routine', value: 'routine', color: '#4CAF50' }, +]; + +const SEVERITY_FILTERS: FilterOption[] = [ + { label: 'All', value: 'all' }, + { label: 'High', value: 'high', color: '#F44336' }, + { label: 'Medium', value: 'medium', color: '#FF9800' }, + { label: 'Low', value: 'low', color: '#FFC107' }, + { label: 'None', value: 'none', color: '#4CAF50' }, +]; + +const CATEGORY_FILTERS: FilterOption[] = [ + { label: 'All', value: 'all' }, + { label: 'Critical', value: 'critical', color: '#F44336' }, + { label: 'Abnormal', value: 'abnormal', color: '#FF9800' }, + { label: 'Warning', value: 'warning', color: '#FFC107' }, + { label: 'Normal', value: 'normal', color: '#4CAF50' }, + { label: 'Unknown', value: 'unknown', color: '#9E9E9E' }, +]; + +// ============================================================================ +// FILTER TABS COMPONENT +// ============================================================================ + +/** + * FilterTabs Component + * + * Purpose: Provide filtering functionality for AI predictions + * + * Features: + * - Multiple filter categories (urgency, severity, category) + * - Visual filter counts + * - Active filter indicators + * - Clear all filters functionality + * - Color-coded filter options + * - Horizontal scroll support + * - Responsive design + * - Accessibility support + */ +const FilterTabs: React.FC = ({ + selectedUrgencyFilter, + selectedSeverityFilter, + selectedCategoryFilter, + onUrgencyFilterChange, + onSeverityFilterChange, + onCategoryFilterChange, + onClearFilters, + filterCounts, + activeFiltersCount = 0, +}) => { + // ============================================================================ + // HELPER FUNCTIONS + // ============================================================================ + + /** + * Get Filter Count + * + * Purpose: Get count for specific filter value + */ + const getFilterCount = (category: 'urgency' | 'severity' | 'category', value: string): number => { + return filterCounts?.[category]?.[value] || 0; + }; + + /** + * Render Filter Tab + * + * Purpose: Render individual filter tab + */ + const renderFilterTab = ( + option: FilterOption, + isSelected: boolean, + onPress: () => void, + category: 'urgency' | 'severity' | 'category' + ) => { + const count = getFilterCount(category, option.value); + + return ( + 0 ? `, ${count} items` : ''}`} + > + {option.color && isSelected && ( + + )} + + + {option.label} + + + {count > 0 && ( + + + {count} + + + )} + + ); + }; + + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + + {/* Header with Clear Filters */} + + Filters + + {activeFiltersCount > 0 && ( + + + Clear All + + )} + + + {/* Urgency Filters */} + + Clinical Urgency + + {URGENCY_FILTERS.map((option) => + renderFilterTab( + { ...option, count: getFilterCount('urgency', option.value) }, + selectedUrgencyFilter === option.value, + () => onUrgencyFilterChange(option.value as AIPredictionState['selectedUrgencyFilter']), + 'urgency' + ) + )} + + + + {/* Severity Filters */} + + Primary Severity + + {SEVERITY_FILTERS.map((option) => + renderFilterTab( + { ...option, count: getFilterCount('severity', option.value) }, + selectedSeverityFilter === option.value, + () => onSeverityFilterChange(option.value as AIPredictionState['selectedSeverityFilter']), + 'severity' + ) + )} + + + + {/* Category Filters */} + + Finding Category + + {CATEGORY_FILTERS.map((option) => + renderFilterTab( + { ...option, count: getFilterCount('category', option.value) }, + selectedCategoryFilter === option.value, + () => onCategoryFilterChange(option.value as AIPredictionState['selectedCategoryFilter']), + 'category' + ) + )} + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + backgroundColor: theme.colors.background, + paddingVertical: theme.spacing.md, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + marginBottom: theme.spacing.md, + }, + headerTitle: { + fontSize: theme.typography.fontSize.bodyLarge, + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.textPrimary, + }, + clearButton: { + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing.xs, + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + borderRadius: theme.borderRadius.small, + borderWidth: 1, + borderColor: theme.colors.primary, + }, + clearButtonText: { + fontSize: theme.typography.fontSize.bodySmall, + color: theme.colors.primary, + fontWeight: theme.typography.fontWeight.medium, + }, + filterSection: { + marginBottom: theme.spacing.lg, + }, + sectionTitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontWeight: theme.typography.fontWeight.medium, + color: theme.colors.textSecondary, + paddingHorizontal: theme.spacing.md, + marginBottom: theme.spacing.sm, + }, + filterRow: { + paddingHorizontal: theme.spacing.md, + gap: theme.spacing.sm, + }, + filterTab: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + borderRadius: theme.borderRadius.medium, + borderWidth: 1, + borderColor: theme.colors.border, + backgroundColor: theme.colors.background, + gap: theme.spacing.xs, + }, + selectedFilterTab: { + borderColor: theme.colors.primary, + backgroundColor: theme.colors.backgroundAccent, + }, + colorIndicator: { + width: 8, + height: 8, + borderRadius: 4, + }, + filterTabText: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textSecondary, + fontWeight: theme.typography.fontWeight.medium, + }, + selectedFilterTabText: { + color: theme.colors.primary, + fontWeight: theme.typography.fontWeight.bold, + }, + countBadge: { + backgroundColor: theme.colors.textMuted, + borderRadius: theme.borderRadius.small, + paddingHorizontal: theme.spacing.xs, + paddingVertical: 2, + minWidth: 20, + alignItems: 'center', + }, + selectedCountBadge: { + backgroundColor: theme.colors.primary, + }, + countText: { + fontSize: theme.typography.fontSize.caption, + color: theme.colors.background, + fontWeight: theme.typography.fontWeight.bold, + }, + selectedCountText: { + color: theme.colors.background, + }, +}); + +export default FilterTabs; + +/* + * End of File: FilterTabs.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/components/LoadingState.tsx b/app/modules/AIPrediction/components/LoadingState.tsx new file mode 100644 index 0000000..4331d7c --- /dev/null +++ b/app/modules/AIPrediction/components/LoadingState.tsx @@ -0,0 +1,139 @@ +/* + * File: LoadingState.tsx + * Description: Loading state component for AI predictions + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + StyleSheet, + ActivityIndicator, + Dimensions, +} from 'react-native'; +import { theme } from '../../../theme'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface LoadingStateProps { + message?: string; + showSpinner?: boolean; + size?: 'small' | 'large'; + style?: any; +} + +// ============================================================================ +// CONSTANTS +// ============================================================================ + +const { width, height } = Dimensions.get('window'); + +// ============================================================================ +// LOADING STATE COMPONENT +// ============================================================================ + +/** + * LoadingState Component + * + * Purpose: Display loading state for AI predictions + * + * Features: + * - Customizable loading message + * - Optional spinner display + * - Different spinner sizes + * - Custom styling support + * - Centered layout + * - Accessibility support + */ +const LoadingState: React.FC = ({ + message = 'Loading AI predictions...', + showSpinner = true, + size = 'large', + style, +}) => { + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + + {/* Loading Spinner */} + {showSpinner && ( + + )} + + {/* Loading Message */} + + {message} + + + {/* Loading Animation Dots */} + + + + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: theme.spacing.xl, + paddingVertical: theme.spacing.xxl, + minHeight: height * 0.3, + }, + spinner: { + marginBottom: theme.spacing.lg, + }, + message: { + fontSize: theme.typography.fontSize.bodyLarge, + color: theme.colors.textSecondary, + textAlign: 'center', + fontWeight: theme.typography.fontWeight.medium, + marginBottom: theme.spacing.xl, + }, + dotsContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing.sm, + }, + dot: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: theme.colors.primary, + }, + dot1: { + opacity: 0.3, + }, + dot2: { + opacity: 0.6, + }, + dot3: { + opacity: 1, + }, +}); + +export default LoadingState; + +/* + * End of File: LoadingState.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/components/SearchBar.tsx b/app/modules/AIPrediction/components/SearchBar.tsx new file mode 100644 index 0000000..46af82a --- /dev/null +++ b/app/modules/AIPrediction/components/SearchBar.tsx @@ -0,0 +1,226 @@ +/* + * File: SearchBar.tsx + * Description: Search bar component for filtering AI predictions + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState, useCallback } from 'react'; +import { + View, + TextInput, + StyleSheet, + TouchableOpacity, + Dimensions, +} from 'react-native'; +import Icon from 'react-native-vector-icons/Feather'; +import { theme } from '../../../theme'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface SearchBarProps { + value: string; + onChangeText: (text: string) => void; + onClear?: () => void; + placeholder?: string; + autoFocus?: boolean; + disabled?: boolean; +} + +// ============================================================================ +// CONSTANTS +// ============================================================================ + +const { width } = Dimensions.get('window'); + +// ============================================================================ +// SEARCH BAR COMPONENT +// ============================================================================ + +/** + * SearchBar Component + * + * Purpose: Provide search functionality for AI predictions + * + * Features: + * - Real-time search input + * - Clear button functionality + * - Customizable placeholder text + * - Auto-focus support + * - Disabled state support + * - Modern design with icons + * - Responsive width + * - Accessibility support + */ +const SearchBar: React.FC = ({ + value, + onChangeText, + onClear, + placeholder = 'Search predictions...', + autoFocus = false, + disabled = false, +}) => { + // ============================================================================ + // STATE + // ============================================================================ + + const [isFocused, setIsFocused] = useState(false); + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Handle Focus + * + * Purpose: Handle input focus state + */ + const handleFocus = useCallback(() => { + setIsFocused(true); + }, []); + + /** + * Handle Blur + * + * Purpose: Handle input blur state + */ + const handleBlur = useCallback(() => { + setIsFocused(false); + }, []); + + /** + * Handle Clear + * + * Purpose: Clear search input + */ + const handleClear = useCallback(() => { + onChangeText(''); + if (onClear) { + onClear(); + } + }, [onChangeText, onClear]); + + /** + * Handle Text Change + * + * Purpose: Handle search text input + */ + const handleTextChange = useCallback((text: string) => { + onChangeText(text); + }, [onChangeText]); + + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + + {/* Search Icon */} + + + {/* Text Input */} + + + {/* Clear Button */} + {value.length > 0 && !disabled && ( + + + + )} + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.background, + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + marginHorizontal: theme.spacing.md, + marginVertical: theme.spacing.sm, + ...theme.shadows.small, + }, + focusedContainer: { + borderColor: theme.colors.primary, + backgroundColor: theme.colors.background, + }, + disabledContainer: { + backgroundColor: theme.colors.backgroundAlt, + opacity: 0.6, + }, + searchIcon: { + marginRight: theme.spacing.sm, + }, + input: { + flex: 1, + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textPrimary, + paddingVertical: 0, // Remove default padding to maintain consistent height + fontFamily: theme.typography.fontFamily.regular, + }, + disabledInput: { + color: theme.colors.textMuted, + }, + clearButton: { + marginLeft: theme.spacing.sm, + padding: theme.spacing.xs, + }, +}); + +export default SearchBar; + +/* + * End of File: SearchBar.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/components/StatsOverview.tsx b/app/modules/AIPrediction/components/StatsOverview.tsx new file mode 100644 index 0000000..17a4e03 --- /dev/null +++ b/app/modules/AIPrediction/components/StatsOverview.tsx @@ -0,0 +1,454 @@ +/* + * File: StatsOverview.tsx + * Description: Statistics overview component for AI predictions dashboard + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Dimensions, +} from 'react-native'; +import Icon from 'react-native-vector-icons/Feather'; +import { theme } from '../../../theme'; +import type { AIPredictionStats } from '../types'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface StatsOverviewProps { + stats: AIPredictionStats; + onStatsPress?: (statType: string) => void; + isLoading?: boolean; + style?: any; +} + +interface StatCardProps { + title: string; + value: string | number; + subtitle?: string; + iconName: string; + color: string; + onPress?: () => void; + trend?: number; + isPercentage?: boolean; +} + +// ============================================================================ +// CONSTANTS +// ============================================================================ + +const { width } = Dimensions.get('window'); +const CARD_WIDTH = (width - 48) / 2; // Two cards per row with margins + +// ============================================================================ +// STAT CARD COMPONENT +// ============================================================================ + +/** + * StatCard Component + * + * Purpose: Individual statistics card + */ +const StatCard: React.FC = ({ + title, + value, + subtitle, + iconName, + color, + onPress, + trend, + isPercentage = false, +}) => { + const displayValue = typeof value === 'number' + ? isPercentage + ? `${Math.round(value * 100)}%` + : value.toLocaleString() + : value; + + return ( + + {/* Card Header */} + + + + + + {displayValue} + + + + {trend !== undefined && ( + + = 0 ? 'trending-up' : 'trending-down'} + size={14} + color={trend >= 0 ? theme.colors.success : theme.colors.error} + /> + = 0 ? theme.colors.success : theme.colors.error } + ]}> + {Math.abs(trend).toFixed(1)}% + + + )} + + + {/* Card Content */} + + + {title} + {subtitle && ( + {subtitle} + )} + + + ); +}; + +// ============================================================================ +// STATS OVERVIEW COMPONENT +// ============================================================================ + +/** + * StatsOverview Component + * + * Purpose: Display comprehensive AI predictions statistics + * + * Features: + * - Total cases overview + * - Critical and urgent case counts + * - Review progress tracking + * - Average confidence metrics + * - Trend indicators + * - Interactive stat cards + * - Responsive grid layout + * - Modern card design + * - Accessibility support + */ +const StatsOverview: React.FC = ({ + stats, + onStatsPress, + isLoading = false, + style, +}) => { + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Handle Stat Press + * + * Purpose: Handle statistics card press + */ + const handleStatPress = (statType: string) => { + if (onStatsPress) { + onStatsPress(statType); + } + }; + + // ============================================================================ + // RENDER + // ============================================================================ + + if (isLoading) { + return ( + + + AI Predictions Overview + + + Loading statistics... + + + ); + } + + return ( + + {/* Section Header */} + + AI Predictions Overview + handleStatPress('all')} + accessibilityRole="button" + accessibilityLabel="View all statistics" + > + View All + + + + + {/* Statistics Grid */} + + {/* Total Cases */} + handleStatPress('total')} + /> + + {/* Critical Cases */} + handleStatPress('critical')} + /> + + {/* Urgent Cases */} + handleStatPress('urgent')} + /> + + {/* Reviewed Cases */} + handleStatPress('reviewed')} + /> + + {/* Pending Cases */} + handleStatPress('pending')} + /> + + {/* Average Confidence */} + handleStatPress('confidence')} + isPercentage={true} + /> + + {/* Today's Cases */} + handleStatPress('today')} + /> + + {/* Weekly Trend */} + = 0 ? '+' : ''}${stats.weeklyTrend.toFixed(1)}%`} + subtitle="vs last week" + iconName={stats.weeklyTrend >= 0 ? 'trending-up' : 'trending-down'} + color={stats.weeklyTrend >= 0 ? theme.colors.success : theme.colors.error} + onPress={() => handleStatPress('trend')} + trend={stats.weeklyTrend} + /> + + + {/* Summary Section */} + + + + + Quick Insights + + + + + Review Progress: + + {Math.round((stats.reviewedCases / stats.totalCases) * 100)}% + + + + + Critical Rate: + + {Math.round((stats.criticalCases / stats.totalCases) * 100)}% + + + + + Daily Average: + + {Math.round(stats.totalCases / 7)} cases + + + + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + backgroundColor: theme.colors.background, + paddingVertical: theme.spacing.lg, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + marginBottom: theme.spacing.lg, + }, + sectionTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.textPrimary, + }, + viewAllButton: { + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing.xs, + }, + viewAllText: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.primary, + fontWeight: theme.typography.fontWeight.medium, + }, + loadingContainer: { + paddingVertical: theme.spacing.xxl, + alignItems: 'center', + }, + loadingText: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textMuted, + }, + statsGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + paddingHorizontal: theme.spacing.md, + gap: theme.spacing.md, + }, + statCard: { + width: CARD_WIDTH, + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.medium, + borderLeftWidth: 4, + padding: theme.spacing.md, + ...theme.shadows.medium, + }, + cardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + iconContainer: { + width: 36, + height: 36, + borderRadius: 18, + justifyContent: 'center', + alignItems: 'center', + }, + trendContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing.xs, + }, + trendText: { + fontSize: theme.typography.fontSize.caption, + fontWeight: theme.typography.fontWeight.medium, + }, + cardContent: { + gap: theme.spacing.xs, + }, + statValue: { + fontSize: theme.typography.fontSize.displayMedium, + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.textPrimary, + }, + statTitle: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textSecondary, + fontWeight: theme.typography.fontWeight.medium, + }, + statSubtitle: { + fontSize: theme.typography.fontSize.bodySmall, + color: theme.colors.textMuted, + }, + summarySection: { + paddingHorizontal: theme.spacing.md, + marginTop: theme.spacing.lg, + }, + summaryCard: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.lg, + ...theme.shadows.small, + }, + summaryHeader: { + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing.sm, + marginBottom: theme.spacing.md, + }, + summaryTitle: { + fontSize: theme.typography.fontSize.bodyLarge, + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.textPrimary, + }, + summaryContent: { + gap: theme.spacing.sm, + }, + summaryItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + summaryLabel: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textSecondary, + }, + summaryValue: { + fontSize: theme.typography.fontSize.bodyMedium, + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.textPrimary, + }, +}); + +export default StatsOverview; + +/* + * End of File: StatsOverview.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/components/index.ts b/app/modules/AIPrediction/components/index.ts new file mode 100644 index 0000000..dbc9ef2 --- /dev/null +++ b/app/modules/AIPrediction/components/index.ts @@ -0,0 +1,19 @@ +/* + * File: index.ts + * Description: Components exports for AI Prediction module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export { default as AIPredictionCard } from './AIPredictionCard'; +export { default as SearchBar } from './SearchBar'; +export { default as FilterTabs } from './FilterTabs'; +export { default as LoadingState } from './LoadingState'; +export { default as EmptyState } from './EmptyState'; +export { default as StatsOverview } from './StatsOverview'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/hooks/index.ts b/app/modules/AIPrediction/hooks/index.ts new file mode 100644 index 0000000..592bfb7 --- /dev/null +++ b/app/modules/AIPrediction/hooks/index.ts @@ -0,0 +1,14 @@ +/* + * File: index.ts + * Description: Hooks exports for AI Prediction module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export * from './useAIPredictions'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/hooks/useAIPredictions.ts b/app/modules/AIPrediction/hooks/useAIPredictions.ts new file mode 100644 index 0000000..613b0b4 --- /dev/null +++ b/app/modules/AIPrediction/hooks/useAIPredictions.ts @@ -0,0 +1,383 @@ +/* + * File: useAIPredictions.ts + * Description: Custom hook for AI Predictions functionality + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { useCallback, useEffect, useMemo } from 'react'; +import { useAppDispatch, useAppSelector } from '../../../store/hooks'; + +// Import Redux actions and selectors +import { + fetchAIPredictions, + setSearchQuery, + setUrgencyFilter, + setSeverityFilter, + setCategoryFilter, + clearAllFilters, + updateCaseReview, +} from '../redux'; + +import { + selectPaginatedCases, + selectIsLoading, + selectError, + selectSearchQuery, + selectUrgencyFilter, + selectSeverityFilter, + selectCategoryFilter, + selectCasesStatistics, + selectActiveFiltersCount, + selectCurrentPage, + selectTotalPages, +} from '../redux'; + +// Import auth selector +import { selectUser } from '../../Auth/redux/authSelectors'; + +// Import types +import type { AIPredictionState } from '../types'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface UseAIPredictionsOptions { + autoLoad?: boolean; + refreshInterval?: number; +} + +interface UseAIPredictionsReturn { + // Data + cases: ReturnType; + statistics: ReturnType; + + // Loading states + isLoading: boolean; + error: string | null; + + // Filters + searchQuery: string; + urgencyFilter: AIPredictionState['selectedUrgencyFilter']; + severityFilter: AIPredictionState['selectedSeverityFilter']; + categoryFilter: AIPredictionState['selectedCategoryFilter']; + activeFiltersCount: number; + + // Pagination + currentPage: number; + totalPages: number; + + // Actions + loadPredictions: () => Promise; + refreshPredictions: () => Promise; + setSearch: (query: string) => void; + setUrgency: (filter: AIPredictionState['selectedUrgencyFilter']) => void; + setSeverity: (filter: AIPredictionState['selectedSeverityFilter']) => void; + setCategory: (filter: AIPredictionState['selectedCategoryFilter']) => void; + clearFilters: () => void; + reviewCase: (caseId: string, reviewData?: Partial<{ + review_status: 'pending' | 'reviewed' | 'confirmed' | 'disputed'; + reviewed_by: string; + review_notes: string; + priority: 'critical' | 'high' | 'medium' | 'low'; + }>) => Promise; + + // Computed properties + hasFilters: boolean; + isEmpty: boolean; + hasError: boolean; +} + +// ============================================================================ +// USE AI PREDICTIONS HOOK +// ============================================================================ + +/** + * useAIPredictions Hook + * + * Purpose: Custom hook for managing AI predictions state and actions + * + * Features: + * - Automatic data loading on mount + * - Search and filtering functionality + * - Case review management + * - Error handling + * - Loading states + * - Computed properties for UI state + * - Auto-refresh capability + * - Type-safe actions and selectors + */ +export const useAIPredictions = (options: UseAIPredictionsOptions = {}): UseAIPredictionsReturn => { + const { + autoLoad = true, + refreshInterval, + } = options; + + // ============================================================================ + // REDUX STATE + // ============================================================================ + + const dispatch = useAppDispatch(); + + // Auth state + const user = useAppSelector(selectUser); + + // AI Predictions state + const cases = useAppSelector(selectPaginatedCases); + const statistics = useAppSelector(selectCasesStatistics); + const isLoading = useAppSelector(selectIsLoading); + const error = useAppSelector(selectError); + const searchQuery = useAppSelector(selectSearchQuery); + const urgencyFilter = useAppSelector(selectUrgencyFilter); + const severityFilter = useAppSelector(selectSeverityFilter); + const categoryFilter = useAppSelector(selectCategoryFilter); + const activeFiltersCount = useAppSelector(selectActiveFiltersCount); + const currentPage = useAppSelector(selectCurrentPage); + const totalPages = useAppSelector(selectTotalPages); + + // ============================================================================ + // MEMOIZED VALUES + // ============================================================================ + + /** + * Has Filters + * + * Purpose: Check if any filters are active + */ + const hasFilters = useMemo(() => activeFiltersCount > 0, [activeFiltersCount]); + + /** + * Is Empty + * + * Purpose: Check if the cases list is empty + */ + const isEmpty = useMemo(() => cases.length === 0, [cases.length]); + + /** + * Has Error + * + * Purpose: Check if there's an error + */ + const hasError = useMemo(() => error !== null, [error]); + + // ============================================================================ + // ACTIONS + // ============================================================================ + + /** + * Load Predictions + * + * Purpose: Load AI predictions from API + */ + const loadPredictions = useCallback(async () => { + + if (!user?.access_token) { + throw new Error('User not authenticated'); + } + + try { + const params = { + page: currentPage, + limit: 20, + ...(urgencyFilter !== 'all' && { urgency: urgencyFilter }), + ...(severityFilter !== 'all' && { severity: severityFilter }), + ...(categoryFilter !== 'all' && { category: categoryFilter }), + ...(searchQuery.trim() && { search: searchQuery.trim() }), + }; + + await dispatch(fetchAIPredictions({ + token: user.access_token, + params, + })).unwrap(); + } catch (error) { + console.error('Failed to load AI predictions:', error); + throw error; + } + }, [ + dispatch, + user?.access_token, + currentPage, + urgencyFilter, + severityFilter, + categoryFilter, + searchQuery, + ]); + + /** + * Refresh Predictions + * + * Purpose: Refresh AI predictions data + */ + const refreshPredictions = useCallback(async () => { + await loadPredictions(); + }, [loadPredictions]); + + /** + * Set Search + * + * Purpose: Set search query + */ + const setSearch = useCallback((query: string) => { + dispatch(setSearchQuery(query)); + }, [dispatch]); + + /** + * Set Urgency Filter + * + * Purpose: Set urgency filter + */ + const setUrgency = useCallback((filter: AIPredictionState['selectedUrgencyFilter']) => { + dispatch(setUrgencyFilter(filter)); + }, [dispatch]); + + /** + * Set Severity Filter + * + * Purpose: Set severity filter + */ + const setSeverity = useCallback((filter: AIPredictionState['selectedSeverityFilter']) => { + dispatch(setSeverityFilter(filter)); + }, [dispatch]); + + /** + * Set Category Filter + * + * Purpose: Set category filter + */ + const setCategory = useCallback((filter: AIPredictionState['selectedCategoryFilter']) => { + dispatch(setCategoryFilter(filter)); + }, [dispatch]); + + /** + * Clear Filters + * + * Purpose: Clear all active filters + */ + const clearFilters = useCallback(() => { + dispatch(clearAllFilters()); + }, [dispatch]); + + /** + * Review Case + * + * Purpose: Update case review status + */ + const reviewCase = useCallback(async ( + caseId: string, + reviewData: Partial<{ + review_status: 'pending' | 'reviewed' | 'confirmed' | 'disputed'; + reviewed_by: string; + review_notes: string; + priority: 'critical' | 'high' | 'medium' | 'low'; + }> = {} + ) => { + if (!user?.access_token) { + throw new Error('User not authenticated'); + } + + try { + const defaultReviewData = { + review_status: 'reviewed' as const, + reviewed_by: user.display_name || user.email || 'Current User', + ...reviewData, + }; + + await dispatch(updateCaseReview({ + caseId, + reviewData: defaultReviewData, + token: user.access_token, + })).unwrap(); + } catch (error) { + console.error('Failed to review case:', error); + throw error; + } + }, [dispatch, user]); + + // ============================================================================ + // EFFECTS + // ============================================================================ + + /** + * Auto-load Effect + * + * Purpose: Automatically load predictions on mount if enabled + */ + useEffect(() => { + if (autoLoad && user?.access_token) { + loadPredictions().catch(console.error); + } + }, [autoLoad, user?.access_token, loadPredictions]); + + /** + * Auto-refresh Effect + * + * Purpose: Set up auto-refresh interval if specified + */ + useEffect(() => { + if (!refreshInterval || !user?.access_token) return; + + const interval = setInterval(() => { + loadPredictions().catch(console.error); + }, refreshInterval); + + return () => clearInterval(interval); + }, [refreshInterval, user?.access_token, loadPredictions]); + + /** + * Filter Change Effect + * + * Purpose: Reload data when filters change + */ + useEffect(() => { + if (user?.access_token) { + loadPredictions().catch(console.error); + } + }, [urgencyFilter, severityFilter, categoryFilter, searchQuery, currentPage]); + + // ============================================================================ + // RETURN + // ============================================================================ + + return { + // Data + cases, + statistics, + + // Loading states + isLoading, + error, + + // Filters + searchQuery, + urgencyFilter, + severityFilter, + categoryFilter, + activeFiltersCount, + + // Pagination + currentPage, + totalPages, + + // Actions + loadPredictions, + refreshPredictions, + setSearch, + setUrgency, + setSeverity, + setCategory, + clearFilters, + reviewCase, + + // Computed properties + hasFilters, + isEmpty, + hasError, + }; +}; + +/* + * End of File: useAIPredictions.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/index.ts b/app/modules/AIPrediction/index.ts new file mode 100644 index 0000000..920e1d5 --- /dev/null +++ b/app/modules/AIPrediction/index.ts @@ -0,0 +1,54 @@ +/* + * File: index.ts + * Description: Main exports for AI Prediction module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// ============================================================================ +// COMPONENT EXPORTS +// ============================================================================ + +export * from './components'; + +// ============================================================================ +// SCREEN EXPORTS +// ============================================================================ + +export * from './screens'; + +// ============================================================================ +// NAVIGATION EXPORTS +// ============================================================================ + +export * from './navigation'; + +// ============================================================================ +// REDUX EXPORTS +// ============================================================================ + +export * from './redux'; + +// ============================================================================ +// SERVICE EXPORTS +// ============================================================================ + +export * from './services'; + +// ============================================================================ +// TYPE EXPORTS +// ============================================================================ + +export * from './types'; + +// ============================================================================ +// HOOK EXPORTS +// ============================================================================ + +export * from './hooks'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/navigation/AIPredictionStackNavigator.tsx b/app/modules/AIPrediction/navigation/AIPredictionStackNavigator.tsx new file mode 100644 index 0000000..c5c070f --- /dev/null +++ b/app/modules/AIPrediction/navigation/AIPredictionStackNavigator.tsx @@ -0,0 +1,249 @@ +/* + * File: AIPredictionStackNavigator.tsx + * Description: Stack navigator for AI Prediction module screens + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; +import { TouchableOpacity, Text, StyleSheet } from 'react-native'; +import Icon from 'react-native-vector-icons/Feather'; +import { theme } from '../../../theme'; + +// Import screens +import { AIPredictionsScreen, AIPredictionDetailScreen } from '../screens'; +import { ComingSoonScreen, DicomViewer } from '../../../shared/components'; + +// Import types +import type { AIPredictionStackParamList } from './navigationTypes'; + +// ============================================================================ +// STACK NAVIGATOR SETUP +// ============================================================================ + +const Stack = createStackNavigator(); + +// ============================================================================ +// HEADER COMPONENTS +// ============================================================================ + +/** + * Header Back Button + * + * Purpose: Custom back button for navigation header + */ +const HeaderBackButton: React.FC<{ onPress: () => void }> = ({ onPress }) => ( + + + +); + +/** + * Header Action Button + * + * Purpose: Custom action button for navigation header + */ +const HeaderActionButton: React.FC<{ + iconName: string; + onPress: () => void; + accessibilityLabel?: string; +}> = ({ iconName, onPress, accessibilityLabel }) => ( + + + +); + +// ============================================================================ +// SCREEN OPTIONS +// ============================================================================ + +/** + * Default Screen Options + * + * Purpose: Common screen options for all AI prediction screens + */ +const defaultScreenOptions = { + headerStyle: { + backgroundColor: theme.colors.background, + elevation: 2, + shadowOpacity: 0.1, + shadowRadius: 4, + shadowOffset: { width: 0, height: 2 }, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + headerTitleStyle: { + fontSize: theme.typography.fontSize.bodyLarge, + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.textPrimary, + }, + headerTintColor: theme.colors.textPrimary, + headerBackTitleVisible: false, + gestureEnabled: true, + cardStyleInterpolator: ({ current, layouts }: any) => { + return { + cardStyle: { + transform: [ + { + translateX: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [layouts.screen.width, 0], + }), + }, + ], + }, + }; + }, +}; + +// ============================================================================ +// AI PREDICTION STACK NAVIGATOR COMPONENT +// ============================================================================ + +/** + * AIPredictionStackNavigator Component + * + * Purpose: Stack navigator for AI prediction module + * + * Features: + * - AI Prediction List screen (main screen) + * - AI Prediction Details screen (case details) + * - AI Prediction Filters screen (advanced filtering) + * - AI Prediction Stats screen (detailed statistics) + * - Custom header styling and buttons + * - Smooth navigation transitions + * - Accessibility support + * - Coming soon screens for unimplemented features + */ +const AIPredictionStackNavigator: React.FC = () => { + return ( + + {/* AI Prediction List Screen */} + ({ + title: 'AI Predictions', + headerLeft: () => null, // No back button on main screen + headerRight: () => ( + { + // Open options menu + // For now, just navigate to stats + // @ts-ignore + navigation.navigate('AIPredictionStats'); + }} + accessibilityLabel="More options" + /> + ), + })} + /> + + {/* AI Prediction Details Screen */} + console.log('DICOM Error:', error)} + onLoad={() => console.log('DICOM Viewer loaded successfully')} + />} + options={({ navigation, route }) => ({ + title: 'Create Suggestion', + headerLeft: () => ( + navigation.goBack()} /> + ), + headerRight: () => ( + { + // Show help for suggestion form + console.log('Show help for case:', route.params?.caseId); + }} + accessibilityLabel="Help" + /> + ), + })} + /> + + {/* AI Prediction Filters Screen */} + ({ + title: 'Advanced Filters', + headerLeft: () => ( + navigation.goBack()} /> + ), + headerRight: () => ( + { + // Reset filters + console.log('Reset filters'); + }} + accessibilityLabel="Reset filters" + /> + ), + })} + /> + + {/* AI Prediction Stats Screen */} + ({ + title: 'Statistics', + headerLeft: () => ( + navigation.goBack()} /> + ), + headerRight: () => ( + { + // Export statistics + console.log('Export stats:', route.params?.timeRange); + }} + accessibilityLabel="Export statistics" + /> + ), + })} + /> + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + headerButton: { + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + marginHorizontal: theme.spacing.xs, + }, + headerButtonText: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.primary, + fontWeight: theme.typography.fontWeight.medium, + }, +}); + +export default AIPredictionStackNavigator; + +/* + * End of File: AIPredictionStackNavigator.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/navigation/index.ts b/app/modules/AIPrediction/navigation/index.ts new file mode 100644 index 0000000..5094ddf --- /dev/null +++ b/app/modules/AIPrediction/navigation/index.ts @@ -0,0 +1,16 @@ +/* + * File: index.ts + * Description: Navigation exports for AI Prediction module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export { default as AIPredictionStackNavigator } from './AIPredictionStackNavigator'; +export * from './navigationTypes'; +export * from './navigationUtils'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/navigation/navigationTypes.ts b/app/modules/AIPrediction/navigation/navigationTypes.ts new file mode 100644 index 0000000..0f8cf6a --- /dev/null +++ b/app/modules/AIPrediction/navigation/navigationTypes.ts @@ -0,0 +1,169 @@ +/* + * File: navigationTypes.ts + * Description: Navigation type definitions for AI Prediction module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import type { StackNavigationProp } from '@react-navigation/stack'; +import type { RouteProp } from '@react-navigation/native'; + +// ============================================================================ +// AI PREDICTION STACK PARAM LIST +// ============================================================================ + +/** + * AI Prediction Stack Param List + * + * Purpose: Define navigation parameters for AI prediction screens + * + * Screens: + * - AIPredictionList: Main list of AI predictions + * - AIPredictionDetails: Detailed view of a specific prediction with suggestion form + * - AIPredictionFilters: Advanced filtering options + * - AIPredictionStats: Detailed statistics view + */ +export type AIPredictionStackParamList = { + AIPredictionList: undefined; + AIPredictionDetails: { caseId: string }; + AIPredictionFilters: undefined; + AIPredictionStats: { timeRange?: 'today' | 'week' | 'month' }; +}; + +// ============================================================================ +// NAVIGATION PROP TYPES +// ============================================================================ + +/** + * AI Prediction List Navigation Prop + * + * Purpose: Navigation prop type for AI prediction list screen + */ +export type AIPredictionListNavigationProp = StackNavigationProp< + AIPredictionStackParamList, + 'AIPredictionList' +>; + +/** + * AI Prediction Details Navigation Prop + * + * Purpose: Navigation prop type for AI prediction details screen + */ +export type AIPredictionDetailsNavigationProp = StackNavigationProp< + AIPredictionStackParamList, + 'AIPredictionDetails' +>; + +/** + * AI Prediction Filters Navigation Prop + * + * Purpose: Navigation prop type for AI prediction filters screen + */ +export type AIPredictionFiltersNavigationProp = StackNavigationProp< + AIPredictionStackParamList, + 'AIPredictionFilters' +>; + +/** + * AI Prediction Stats Navigation Prop + * + * Purpose: Navigation prop type for AI prediction statistics screen + */ +export type AIPredictionStatsNavigationProp = StackNavigationProp< + AIPredictionStackParamList, + 'AIPredictionStats' +>; + +// ============================================================================ +// ROUTE PROP TYPES +// ============================================================================ + +/** + * AI Prediction List Route Prop + * + * Purpose: Route prop type for AI prediction list screen + */ +export type AIPredictionListRouteProp = RouteProp< + AIPredictionStackParamList, + 'AIPredictionList' +>; + +/** + * AI Prediction Details Route Prop + * + * Purpose: Route prop type for AI prediction details screen + */ +export type AIPredictionDetailsRouteProp = RouteProp< + AIPredictionStackParamList, + 'AIPredictionDetails' +>; + +/** + * AI Prediction Filters Route Prop + * + * Purpose: Route prop type for AI prediction filters screen + */ +export type AIPredictionFiltersRouteProp = RouteProp< + AIPredictionStackParamList, + 'AIPredictionFilters' +>; + +/** + * AI Prediction Stats Route Prop + * + * Purpose: Route prop type for AI prediction statistics screen + */ +export type AIPredictionStatsRouteProp = RouteProp< + AIPredictionStackParamList, + 'AIPredictionStats' +>; + +// ============================================================================ +// COMBINED PROP TYPES +// ============================================================================ + +/** + * AI Prediction List Screen Props + * + * Purpose: Combined props for AI prediction list screen + */ +export interface AIPredictionListScreenProps { + navigation: AIPredictionListNavigationProp; + route: AIPredictionListRouteProp; +} + +/** + * AI Prediction Details Screen Props + * + * Purpose: Combined props for AI prediction details screen + */ +export interface AIPredictionDetailsScreenProps { + navigation: AIPredictionDetailsNavigationProp; + route: AIPredictionDetailsRouteProp; +} + +/** + * AI Prediction Filters Screen Props + * + * Purpose: Combined props for AI prediction filters screen + */ +export interface AIPredictionFiltersScreenProps { + navigation: AIPredictionFiltersNavigationProp; + route: AIPredictionFiltersRouteProp; +} + +/** + * AI Prediction Stats Screen Props + * + * Purpose: Combined props for AI prediction statistics screen + */ +export interface AIPredictionStatsScreenProps { + navigation: AIPredictionStatsNavigationProp; + route: AIPredictionStatsRouteProp; +} + +/* + * End of File: navigationTypes.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/navigation/navigationUtils.ts b/app/modules/AIPrediction/navigation/navigationUtils.ts new file mode 100644 index 0000000..7dc12a3 --- /dev/null +++ b/app/modules/AIPrediction/navigation/navigationUtils.ts @@ -0,0 +1,251 @@ +/* + * File: navigationUtils.ts + * Description: Navigation utility functions for AI Prediction module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { CommonActions } from '@react-navigation/native'; +import type { AIPredictionStackParamList } from './navigationTypes'; + +// ============================================================================ +// NAVIGATION UTILITY FUNCTIONS +// ============================================================================ + +/** + * Navigate to AI Prediction Details + * + * Purpose: Navigate to AI prediction case details screen + * + * @param navigation - Navigation object + * @param caseId - AI prediction case ID + */ +export const navigateToAIPredictionDetails = ( + navigation: any, + caseId: string +) => { + navigation.navigate('AIPredictionDetails', { caseId }); +}; + +/** + * Navigate to AI Prediction Filters + * + * Purpose: Navigate to advanced filters screen + * + * @param navigation - Navigation object + */ +export const navigateToAIPredictionFilters = (navigation: any) => { + navigation.navigate('AIPredictionFilters'); +}; + +/** + * Navigate to AI Prediction Statistics + * + * Purpose: Navigate to detailed statistics screen + * + * @param navigation - Navigation object + * @param timeRange - Optional time range filter + */ +export const navigateToAIPredictionStats = ( + navigation: any, + timeRange?: 'today' | 'week' | 'month' +) => { + navigation.navigate('AIPredictionStats', { timeRange }); +}; + +/** + * Go Back to AI Prediction List + * + * Purpose: Navigate back to AI prediction list screen + * + * @param navigation - Navigation object + */ +export const goBackToAIPredictionList = (navigation: any) => { + navigation.navigate('AIPredictionList'); +}; + +/** + * Reset to AI Prediction List + * + * Purpose: Reset navigation stack to AI prediction list + * + * @param navigation - Navigation object + */ +export const resetToAIPredictionList = (navigation: any) => { + navigation.dispatch( + CommonActions.reset({ + index: 0, + routes: [{ name: 'AIPredictionList' }], + }) + ); +}; + +/** + * Can Go Back + * + * Purpose: Check if navigation can go back + * + * @param navigation - Navigation object + * @returns Boolean indicating if can go back + */ +export const canGoBack = (navigation: any): boolean => { + return navigation.canGoBack(); +}; + +/** + * Get Current Route Name + * + * Purpose: Get the current route name + * + * @param navigation - Navigation object + * @returns Current route name or undefined + */ +export const getCurrentRouteName = (navigation: any): string | undefined => { + return navigation.getCurrentRoute()?.name; +}; + +/** + * Get Current Route Params + * + * Purpose: Get the current route parameters + * + * @param navigation - Navigation object + * @returns Current route params or undefined + */ +export const getCurrentRouteParams = (navigation: any): any => { + return navigation.getCurrentRoute()?.params; +}; + +/** + * Navigate with Replace + * + * Purpose: Navigate to a screen by replacing the current one + * + * @param navigation - Navigation object + * @param routeName - Route name to navigate to + * @param params - Optional route parameters + */ +export const navigateWithReplace = ( + navigation: any, + routeName: keyof AIPredictionStackParamList, + params?: any +) => { + navigation.replace(routeName, params); +}; + +/** + * Navigate with Push + * + * Purpose: Navigate to a screen by pushing it onto the stack + * + * @param navigation - Navigation object + * @param routeName - Route name to navigate to + * @param params - Optional route parameters + */ +export const navigateWithPush = ( + navigation: any, + routeName: keyof AIPredictionStackParamList, + params?: any +) => { + navigation.push(routeName, params); +}; + +/** + * Pop Navigation Stack + * + * Purpose: Pop the specified number of screens from the stack + * + * @param navigation - Navigation object + * @param count - Number of screens to pop (default: 1) + */ +export const popNavigationStack = (navigation: any, count: number = 1) => { + navigation.pop(count); +}; + +/** + * Pop to Top + * + * Purpose: Pop to the top of the navigation stack + * + * @param navigation - Navigation object + */ +export const popToTop = (navigation: any) => { + navigation.popToTop(); +}; + +/** + * Set Navigation Params + * + * Purpose: Set parameters for the current screen + * + * @param navigation - Navigation object + * @param params - Parameters to set + */ +export const setNavigationParams = (navigation: any, params: any) => { + navigation.setParams(params); +}; + +/** + * Add Navigation Listener + * + * Purpose: Add a navigation event listener + * + * @param navigation - Navigation object + * @param eventName - Event name to listen for + * @param callback - Callback function + * @returns Unsubscribe function + */ +export const addNavigationListener = ( + navigation: any, + eventName: string, + callback: (e: any) => void +) => { + return navigation.addListener(eventName, callback); +}; + +/** + * Remove Navigation Listener + * + * Purpose: Remove a navigation event listener + * + * @param navigation - Navigation object + * @param eventName - Event name + * @param callback - Callback function + */ +export const removeNavigationListener = ( + navigation: any, + eventName: string, + callback: (e: any) => void +) => { + navigation.removeListener(eventName, callback); +}; + +/** + * Check if Screen is Focused + * + * Purpose: Check if the current screen is focused + * + * @param navigation - Navigation object + * @returns Boolean indicating if screen is focused + */ +export const isScreenFocused = (navigation: any): boolean => { + return navigation.isFocused(); +}; + +/** + * Get Navigation State + * + * Purpose: Get the current navigation state + * + * @param navigation - Navigation object + * @returns Navigation state + */ +export const getNavigationState = (navigation: any) => { + return navigation.getState(); +}; + +/* + * End of File: navigationUtils.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/redux/aiPredictionSelectors.ts b/app/modules/AIPrediction/redux/aiPredictionSelectors.ts new file mode 100644 index 0000000..aa7f222 --- /dev/null +++ b/app/modules/AIPrediction/redux/aiPredictionSelectors.ts @@ -0,0 +1,410 @@ +/* + * File: aiPredictionSelectors.ts + * Description: Redux selectors for AI Prediction state + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { createSelector } from '@reduxjs/toolkit'; +import type { RootState } from '../../../store'; +import { AIPredictionCase } from '../types'; + +// ============================================================================ +// BASE SELECTORS +// ============================================================================ + +/** + * Select AI Prediction State + * + * Purpose: Get the entire AI prediction state + */ +export const selectAIPredictionState = (state: RootState) => state.aiPrediction; + +/** + * Select Prediction Cases + * + * Purpose: Get all AI prediction cases + */ +export const selectPredictionCases = (state: RootState) => state.aiPrediction.predictionCases; + +/** + * Select Current Case + * + * Purpose: Get the currently selected AI prediction case + */ +export const selectCurrentCase = (state: RootState) => state.aiPrediction.currentCase; + +/** + * Select Loading State + * + * Purpose: Get the loading state for AI predictions + */ +export const selectIsLoading = (state: RootState) => state.aiPrediction.isLoading; + +/** + * Select Loading Case Details State + * + * Purpose: Get the loading state for case details + */ +export const selectIsLoadingCaseDetails = (state: RootState) => state.aiPrediction.isLoadingCaseDetails; + +/** + * Select Error + * + * Purpose: Get the current error message + */ +export const selectError = (state: RootState) => state.aiPrediction.error; + +/** + * Select Search Query + * + * Purpose: Get the current search query + */ +export const selectSearchQuery = (state: RootState) => state.aiPrediction.searchQuery; + +/** + * Select Filter States + * + * Purpose: Get all filter states + */ +export const selectUrgencyFilter = (state: RootState) => state.aiPrediction.selectedUrgencyFilter; +export const selectSeverityFilter = (state: RootState) => state.aiPrediction.selectedSeverityFilter; +export const selectCategoryFilter = (state: RootState) => state.aiPrediction.selectedCategoryFilter; + +/** + * Select Sort Options + * + * Purpose: Get current sort configuration + */ +export const selectSortBy = (state: RootState) => state.aiPrediction.sortBy; +export const selectSortOrder = (state: RootState) => state.aiPrediction.sortOrder; + +/** + * Select Pagination + * + * Purpose: Get pagination configuration + */ +export const selectCurrentPage = (state: RootState) => state.aiPrediction.currentPage; +export const selectItemsPerPage = (state: RootState) => state.aiPrediction.itemsPerPage; +export const selectTotalItems = (state: RootState) => state.aiPrediction.totalItems; + +/** + * Select UI State + * + * Purpose: Get UI state flags + */ +export const selectShowFilters = (state: RootState) => state.aiPrediction.showFilters; +export const selectSelectedCaseIds = (state: RootState) => state.aiPrediction.selectedCaseIds; + +// ============================================================================ +// COMPUTED SELECTORS +// ============================================================================ + +/** + * Select Filtered and Sorted Cases + * + * Purpose: Get AI prediction cases filtered and sorted based on current settings + */ +export const selectFilteredAndSortedCases = createSelector( + [ + selectPredictionCases, + selectSearchQuery, + selectUrgencyFilter, + selectSeverityFilter, + selectCategoryFilter, + selectSortBy, + selectSortOrder, + ], + (cases, searchQuery, urgencyFilter, severityFilter, categoryFilter, sortBy, sortOrder) => { + let filteredCases = [...cases]; + + // Apply search filter + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + filteredCases = filteredCases.filter(case_ => + case_.patid.toLowerCase().includes(query) || + case_.prediction.label.toLowerCase().includes(query) || + case_.prediction.anatomical_location.toLowerCase().includes(query) + ); + } + + // Apply urgency filter + if (urgencyFilter !== 'all') { + filteredCases = filteredCases.filter(case_ => + case_.prediction.clinical_urgency === urgencyFilter + ); + } + + // Apply severity filter + if (severityFilter !== 'all') { + filteredCases = filteredCases.filter(case_ => + case_.prediction.primary_severity === severityFilter + ); + } + + // Apply category filter + if (categoryFilter !== 'all') { + filteredCases = filteredCases.filter(case_ => + case_.prediction.finding_category === categoryFilter + ); + } + + // Apply sorting + filteredCases.sort((a, b) => { + let comparison = 0; + + switch (sortBy) { + case 'date': + comparison = new Date(a.created_at || '').getTime() - new Date(b.created_at || '').getTime(); + break; + case 'urgency': + const urgencyOrder = { emergency: 5, urgent: 4, moderate: 3, low: 2, routine: 1 }; + comparison = (urgencyOrder[a.prediction.clinical_urgency as keyof typeof urgencyOrder] || 0) - + (urgencyOrder[b.prediction.clinical_urgency as keyof typeof urgencyOrder] || 0); + break; + case 'confidence': + comparison = a.prediction.confidence_score - b.prediction.confidence_score; + break; + case 'severity': + const severityOrder = { high: 4, medium: 3, low: 2, none: 1 }; + comparison = (severityOrder[a.prediction.primary_severity as keyof typeof severityOrder] || 0) - + (severityOrder[b.prediction.primary_severity as keyof typeof severityOrder] || 0); + break; + default: + break; + } + + return sortOrder === 'desc' ? -comparison : comparison; + }); + + return filteredCases; + } +); + +/** + * Select Paginated Cases + * + * Purpose: Get the current page of filtered and sorted cases + */ +export const selectPaginatedCases = createSelector( + [selectFilteredAndSortedCases, selectCurrentPage, selectItemsPerPage], + (filteredCases, currentPage, itemsPerPage) => { + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + return filteredCases.slice(startIndex, endIndex); + } +); + +/** + * Select Critical Cases + * + * Purpose: Get cases marked as critical or emergency + */ +export const selectCriticalCases = createSelector( + [selectPredictionCases], + (cases) => cases.filter(case_ => + case_.prediction.clinical_urgency === 'emergency' || + case_.prediction.clinical_urgency === 'urgent' || + case_.prediction.primary_severity === 'high' || + case_.priority === 'critical' + ) +); + +/** + * Select Pending Cases + * + * Purpose: Get cases pending review + */ +export const selectPendingCases = createSelector( + [selectPredictionCases], + (cases) => cases.filter(case_ => case_.review_status === 'pending') +); + +/** + * Select Reviewed Cases + * + * Purpose: Get cases that have been reviewed + */ +export const selectReviewedCases = createSelector( + [selectPredictionCases], + (cases) => cases.filter(case_ => + case_.review_status === 'reviewed' || + case_.review_status === 'confirmed' || + case_.review_status === 'disputed' + ) +); + +/** + * Select Cases by Urgency + * + * Purpose: Group cases by urgency level + */ +export const selectCasesByUrgency = createSelector( + [selectPredictionCases], + (cases) => { + const grouped = { + emergency: [] as AIPredictionCase[], + urgent: [] as AIPredictionCase[], + moderate: [] as AIPredictionCase[], + low: [] as AIPredictionCase[], + routine: [] as AIPredictionCase[], + }; + + cases.forEach(case_ => { + const urgency = case_.prediction.clinical_urgency as keyof typeof grouped; + if (grouped[urgency]) { + grouped[urgency].push(case_); + } + }); + + return grouped; + } +); + +/** + * Select Cases Statistics + * + * Purpose: Get statistical overview of cases + */ +export const selectCasesStatistics = createSelector( + [selectPredictionCases], + (cases) => { + const total = cases.length; + const critical = cases.filter(c => + c.prediction.clinical_urgency === 'emergency' || + c.prediction.clinical_urgency === 'urgent' + ).length; + const pending = cases.filter(c => c.review_status === 'pending').length; + const reviewed = cases.filter(c => + c.review_status === 'reviewed' || + c.review_status === 'confirmed' + ).length; + const averageConfidence = total > 0 + ? cases.reduce((sum, c) => sum + c.prediction.confidence_score, 0) / total + : 0; + + return { + total, + critical, + pending, + reviewed, + averageConfidence: Math.round(averageConfidence * 1000) / 1000, // Round to 3 decimal places + reviewProgress: total > 0 ? Math.round((reviewed / total) * 100) : 0, + }; + } +); + +/** + * Select Filter Counts + * + * Purpose: Get counts for each filter option + */ +export const selectFilterCounts = createSelector( + [selectPredictionCases], + (cases) => { + const urgencyCounts = { + all: cases.length, + emergency: 0, + urgent: 0, + moderate: 0, + low: 0, + routine: 0, + }; + + const severityCounts = { + all: cases.length, + high: 0, + medium: 0, + low: 0, + none: 0, + }; + + const categoryCounts = { + all: cases.length, + normal: 0, + abnormal: 0, + critical: 0, + warning: 0, + unknown: 0, + }; + + cases.forEach(case_ => { + // Count urgency + const urgency = case_.prediction.clinical_urgency as keyof typeof urgencyCounts; + if (urgencyCounts[urgency] !== undefined) { + urgencyCounts[urgency]++; + } + + // Count severity + const severity = case_.prediction.primary_severity as keyof typeof severityCounts; + if (severityCounts[severity] !== undefined) { + severityCounts[severity]++; + } + + // Count category + const category = case_.prediction.finding_category as keyof typeof categoryCounts; + if (categoryCounts[category] !== undefined) { + categoryCounts[category]++; + } + }); + + return { + urgency: urgencyCounts, + severity: severityCounts, + category: categoryCounts, + }; + } +); + +/** + * Select Total Pages + * + * Purpose: Calculate total number of pages based on filtered results + */ +export const selectTotalPages = createSelector( + [selectFilteredAndSortedCases, selectItemsPerPage], + (filteredCases, itemsPerPage) => Math.ceil(filteredCases.length / itemsPerPage) +); + +/** + * Select Has Previous Page + * + * Purpose: Check if there's a previous page available + */ +export const selectHasPreviousPage = createSelector( + [selectCurrentPage], + (currentPage) => currentPage > 1 +); + +/** + * Select Has Next Page + * + * Purpose: Check if there's a next page available + */ +export const selectHasNextPage = createSelector( + [selectCurrentPage, selectTotalPages], + (currentPage, totalPages) => currentPage < totalPages +); + +/** + * Select Active Filters Count + * + * Purpose: Count how many filters are currently active + */ +export const selectActiveFiltersCount = createSelector( + [selectSearchQuery, selectUrgencyFilter, selectSeverityFilter, selectCategoryFilter], + (searchQuery, urgencyFilter, severityFilter, categoryFilter) => { + let count = 0; + if (searchQuery.trim()) count++; + if (urgencyFilter !== 'all') count++; + if (severityFilter !== 'all') count++; + if (categoryFilter !== 'all') count++; + return count; + } +); + +/* + * End of File: aiPredictionSelectors.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/redux/aiPredictionSlice.ts b/app/modules/AIPrediction/redux/aiPredictionSlice.ts new file mode 100644 index 0000000..005e484 --- /dev/null +++ b/app/modules/AIPrediction/redux/aiPredictionSlice.ts @@ -0,0 +1,621 @@ +/* + * File: aiPredictionSlice.ts + * Description: Redux slice for AI Prediction state management + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import { + AIPredictionCase, + AIPredictionState, + AIPredictionStats, + AIPredictionAPIResponse +} from '../types'; +import { aiPredictionAPI } from '../services'; + +// ============================================================================ +// ASYNC THUNKS +// ============================================================================ + +/** + * Fetch AI Predictions Async Thunk + * + * Purpose: Fetch AI prediction results from API + * + * @param token - Authentication token + * @param params - Optional query parameters for filtering + * @returns Promise with AI prediction data or error + */ +export const fetchAIPredictions = createAsyncThunk( + 'aiPrediction/fetchAIPredictions', + async (payload: { + token: string; + params?: { + page?: number; + limit?: number; + urgency?: string; + severity?: string; + category?: string; + search?: string; + } + }, { rejectWithValue }) => { + try { + const response: any = await aiPredictionAPI.getAllPredictions(payload.token, payload.params); + console.log('AI predictions response:', response); + + if (response.ok && response.data && response.data.success) { + // Add additional metadata to each case for UI purposes + const enhancedCases = response.data.data.map((aiCase: AIPredictionCase) => ({ + ...aiCase, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + review_status: 'pending' as const, + priority: getPriorityFromPrediction(aiCase.prediction) + })); + + console.log('Enhanced AI prediction cases:', enhancedCases); + return { + cases: enhancedCases as AIPredictionCase[], + total: response.data.total || enhancedCases.length, + page: response.data.page || 1, + limit: response.data.limit || 20 + }; + } else { + // Fallback to mock data for development + const mockData = generateMockAIPredictions(); + return { + cases: mockData, + total: mockData.length, + page: 1, + limit: 20 + }; + } + } catch (error: any) { + console.error('Fetch AI predictions error:', error); + return rejectWithValue(error.message || 'Failed to fetch AI predictions.'); + } + } +); + +/** + * Fetch AI Prediction Case Details Async Thunk + * + * Purpose: Fetch detailed information for a specific AI prediction case + * + * @param caseId - AI prediction case ID + * @param token - Authentication token + * @returns Promise with case details or error + */ +export const fetchAIPredictionDetails = createAsyncThunk( + 'aiPrediction/fetchAIPredictionDetails', + async (payload: { caseId: string; token: string }, { rejectWithValue }) => { + try { + const response: any = await aiPredictionAPI.getCaseDetails(payload.caseId, payload.token); + + if (response.ok && response.data) { + return response.data as AIPredictionCase; + } else { + // Fallback to mock data + const mockCase = generateMockAIPredictions().find(c => c.patid === payload.caseId); + if (mockCase) { + return mockCase; + } + throw new Error('Case not found'); + } + } catch (error: any) { + console.error('Fetch AI prediction details error:', error); + return rejectWithValue(error.message || 'Failed to fetch case details.'); + } + } +); + +/** + * Update Case Review Async Thunk + * + * Purpose: Update review status of an AI prediction case + * + * @param caseId - Case ID to update + * @param reviewData - Review data + * @param token - Authentication token + * @returns Promise with updated case or error + */ +export const updateCaseReview = createAsyncThunk( + 'aiPrediction/updateCaseReview', + async (payload: { + caseId: string; + reviewData: { + review_status: 'pending' | 'reviewed' | 'confirmed' | 'disputed'; + reviewed_by?: string; + review_notes?: string; + priority?: 'critical' | 'high' | 'medium' | 'low'; + }; + token: string; + }, { rejectWithValue }) => { + try { + const response: any = await aiPredictionAPI.updateCaseReview( + payload.caseId, + payload.reviewData, + payload.token + ); + + if (response.ok && response.data) { + return { + caseId: payload.caseId, + ...payload.reviewData, + updated_at: new Date().toISOString() + }; + } else { + throw new Error('Failed to update case review'); + } + } catch (error: any) { + console.error('Update case review error:', error); + return rejectWithValue(error.message || 'Failed to update case review.'); + } + } +); + +/** + * Fetch AI Prediction Statistics Async Thunk + * + * Purpose: Fetch statistics for AI predictions dashboard + * + * @param token - Authentication token + * @param timeRange - Time range filter + * @returns Promise with statistics data or error + */ +export const fetchAIPredictionStats = createAsyncThunk( + 'aiPrediction/fetchAIPredictionStats', + async (payload: { token: string; timeRange?: 'today' | 'week' | 'month' }, { rejectWithValue }) => { + try { + const response: any = await aiPredictionAPI.getPredictionStats(payload.token, payload.timeRange); + + if (response.ok && response.data) { + return response.data as AIPredictionStats; + } else { + // Fallback to mock stats + return generateMockStats(); + } + } catch (error: any) { + console.error('Fetch AI prediction stats error:', error); + return rejectWithValue(error.message || 'Failed to fetch statistics.'); + } + } +); + +// ============================================================================ +// HELPER FUNCTIONS +// ============================================================================ + +/** + * Get Priority from AI Prediction + * + * Purpose: Determine case priority based on AI prediction results + */ +function getPriorityFromPrediction(prediction: any): 'critical' | 'high' | 'medium' | 'low' { + if (prediction.clinical_urgency === 'emergency' || prediction.primary_severity === 'high') { + return 'critical'; + } + if (prediction.clinical_urgency === 'urgent' || prediction.primary_severity === 'medium') { + return 'high'; + } + if (prediction.clinical_urgency === 'moderate' || prediction.primary_severity === 'low') { + return 'medium'; + } + return 'low'; +} + +/** + * Generate Mock AI Predictions + * + * Purpose: Generate mock data for development and testing + */ +function generateMockAIPredictions(): AIPredictionCase[] { + return [ + { + patid: "demogw05-08-2017", + hospital_id: "eec24855-d8ae-4fad-8e54-af0480343dc2", + prediction: { + label: "midline shift", + finding_type: "pathology", + clinical_urgency: "urgent", + confidence_score: 0.996, + finding_category: "abnormal", + primary_severity: "high", + anatomical_location: "brain" + }, + created_at: "2024-01-15T10:30:00Z", + updated_at: "2024-01-15T10:30:00Z", + review_status: "pending", + priority: "critical" + }, + { + patid: "demo-patient-002", + hospital_id: "eec24855-d8ae-4fad-8e54-af0480343dc2", + prediction: { + label: "normal brain", + finding_type: "no_pathology", + clinical_urgency: "routine", + confidence_score: 0.892, + finding_category: "normal", + primary_severity: "none", + anatomical_location: "not_applicable" + }, + created_at: "2024-01-15T09:15:00Z", + updated_at: "2024-01-15T09:15:00Z", + review_status: "reviewed", + priority: "low" + }, + { + patid: "demo-patient-003", + hospital_id: "eec24855-d8ae-4fad-8e54-af0480343dc2", + prediction: { + label: "hemorrhage", + finding_type: "pathology", + clinical_urgency: "emergency", + confidence_score: 0.945, + finding_category: "critical", + primary_severity: "high", + anatomical_location: "temporal lobe" + }, + created_at: "2024-01-15T11:45:00Z", + updated_at: "2024-01-15T11:45:00Z", + review_status: "confirmed", + priority: "critical" + } + ]; +} + +/** + * Generate Mock Statistics + * + * Purpose: Generate mock statistics for development + */ +function generateMockStats(): AIPredictionStats { + return { + totalCases: 156, + criticalCases: 23, + urgentCases: 45, + reviewedCases: 89, + pendingCases: 67, + averageConfidence: 0.887, + todaysCases: 12, + weeklyTrend: 15.4 + }; +} + +// ============================================================================ +// INITIAL STATE +// ============================================================================ + +/** + * Initial AI Prediction State + * + * Purpose: Define the initial state for AI predictions + * + * Features: + * - Prediction cases list and management + * - Current case details + * - Loading states for async operations + * - Error handling and messages + * - Search and filtering + * - Pagination support + * - Cache management + */ +const initialState: AIPredictionState = { + // Prediction data + predictionCases: [], + currentCase: null, + + // Loading states + isLoading: false, + isRefreshing: false, + isLoadingCaseDetails: false, + + // Error handling + error: null, + + // Search and filtering + searchQuery: '', + selectedUrgencyFilter: 'all', + selectedSeverityFilter: 'all', + selectedCategoryFilter: 'all', + sortBy: 'date', + sortOrder: 'desc', + + // Pagination + currentPage: 1, + itemsPerPage: 20, + totalItems: 0, + + // Cache management + lastUpdated: null, + cacheExpiry: null, + + // UI state + showFilters: false, + selectedCaseIds: [], +}; + +// ============================================================================ +// AI PREDICTION SLICE +// ============================================================================ + +/** + * AI Prediction Slice + * + * Purpose: Redux slice for AI prediction state management + * + * Features: + * - AI prediction data management + * - Search and filtering + * - Case review management + * - Pagination + * - Caching + * - Error handling + * - Loading states + */ +const aiPredictionSlice = createSlice({ + name: 'aiPrediction', + initialState, + reducers: { + /** + * Clear Error Action + * + * Purpose: Clear AI prediction errors + */ + clearError: (state) => { + state.error = null; + }, + + /** + * Set Search Query Action + * + * Purpose: Set search query for AI predictions + */ + setSearchQuery: (state, action: PayloadAction) => { + state.searchQuery = action.payload; + state.currentPage = 1; // Reset to first page when searching + }, + + /** + * Set Urgency Filter Action + * + * Purpose: Set urgency filter for AI predictions + */ + setUrgencyFilter: (state, action: PayloadAction) => { + state.selectedUrgencyFilter = action.payload; + state.currentPage = 1; // Reset to first page when filtering + }, + + /** + * Set Severity Filter Action + * + * Purpose: Set severity filter for AI predictions + */ + setSeverityFilter: (state, action: PayloadAction) => { + state.selectedSeverityFilter = action.payload; + state.currentPage = 1; // Reset to first page when filtering + }, + + /** + * Set Category Filter Action + * + * Purpose: Set category filter for AI predictions + */ + setCategoryFilter: (state, action: PayloadAction) => { + state.selectedCategoryFilter = action.payload; + state.currentPage = 1; // Reset to first page when filtering + }, + + /** + * Set Sort Action + * + * Purpose: Set sort options for AI predictions + */ + setSort: (state, action: PayloadAction<{ by: 'date' | 'urgency' | 'confidence' | 'severity'; order: 'asc' | 'desc' }>) => { + state.sortBy = action.payload.by; + state.sortOrder = action.payload.order; + }, + + /** + * Set Current Page Action + * + * Purpose: Set current page for pagination + */ + setCurrentPage: (state, action: PayloadAction) => { + state.currentPage = action.payload; + }, + + /** + * Set Items Per Page Action + * + * Purpose: Set items per page for pagination + */ + setItemsPerPage: (state, action: PayloadAction) => { + state.itemsPerPage = action.payload; + state.currentPage = 1; // Reset to first page when changing items per page + }, + + /** + * Set Current Case Action + * + * Purpose: Set the currently selected AI prediction case + */ + setCurrentCase: (state, action: PayloadAction) => { + state.currentCase = action.payload; + }, + + /** + * Update Case in List Action + * + * Purpose: Update an AI prediction case in the list + */ + updateCaseInList: (state, action: PayloadAction) => { + const index = state.predictionCases.findIndex(case_ => case_.patid === action.payload.patid); + if (index !== -1) { + state.predictionCases[index] = action.payload; + } + + // Update current case if it's the same case + if (state.currentCase && state.currentCase.patid === action.payload.patid) { + state.currentCase = action.payload; + } + }, + + /** + * Toggle Show Filters Action + * + * Purpose: Toggle the display of filter options + */ + toggleShowFilters: (state) => { + state.showFilters = !state.showFilters; + }, + + /** + * Clear All Filters Action + * + * Purpose: Reset all filters to default values + */ + clearAllFilters: (state) => { + state.searchQuery = ''; + state.selectedUrgencyFilter = 'all'; + state.selectedSeverityFilter = 'all'; + state.selectedCategoryFilter = 'all'; + state.currentPage = 1; + }, + + /** + * Select Case Action + * + * Purpose: Add/remove case from selected cases + */ + toggleCaseSelection: (state, action: PayloadAction) => { + const caseId = action.payload; + const index = state.selectedCaseIds.indexOf(caseId); + + if (index === -1) { + state.selectedCaseIds.push(caseId); + } else { + state.selectedCaseIds.splice(index, 1); + } + }, + + /** + * Clear Selected Cases Action + * + * Purpose: Clear all selected cases + */ + clearSelectedCases: (state) => { + state.selectedCaseIds = []; + }, + + /** + * Clear Cache Action + * + * Purpose: Clear AI prediction data cache + */ + clearCache: (state) => { + state.predictionCases = []; + state.currentCase = null; + state.lastUpdated = null; + state.cacheExpiry = null; + }, + }, + extraReducers: (builder) => { + // Fetch AI Predictions + builder + .addCase(fetchAIPredictions.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(fetchAIPredictions.fulfilled, (state, action) => { + state.isLoading = false; + state.predictionCases = action.payload.cases; + state.totalItems = action.payload.total; + state.lastUpdated = new Date().toLocaleString(); + state.cacheExpiry = new Date(Date.now() + 5 * 60 * 1000).toLocaleString(); // 5 minutes + state.error = null; + }) + .addCase(fetchAIPredictions.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload as string; + }); + + // Fetch AI Prediction Details + builder + .addCase(fetchAIPredictionDetails.pending, (state) => { + state.isLoadingCaseDetails = true; + state.error = null; + }) + .addCase(fetchAIPredictionDetails.fulfilled, (state, action) => { + state.isLoadingCaseDetails = false; + state.currentCase = action.payload; + state.error = null; + }) + .addCase(fetchAIPredictionDetails.rejected, (state, action) => { + state.isLoadingCaseDetails = false; + state.error = action.payload as string; + }); + + // Update Case Review + builder + .addCase(updateCaseReview.fulfilled, (state, action) => { + // Update case in list + const index = state.predictionCases.findIndex(case_ => case_.patid === action.payload.caseId); + if (index !== -1) { + state.predictionCases[index] = { + ...state.predictionCases[index], + review_status: action.payload.review_status, + reviewed_by: action.payload.reviewed_by, + priority: action.payload.priority, + updated_at: action.payload.updated_at + }; + } + + // Update current case if it's the same case + if (state.currentCase && state.currentCase.patid === action.payload.caseId) { + state.currentCase = { + ...state.currentCase, + review_status: action.payload.review_status, + reviewed_by: action.payload.reviewed_by, + priority: action.payload.priority, + updated_at: action.payload.updated_at + }; + } + }) + .addCase(updateCaseReview.rejected, (state, action) => { + state.error = action.payload as string; + }); + }, +}); + +// ============================================================================ +// EXPORTS +// ============================================================================ + +export const { + clearError, + setSearchQuery, + setUrgencyFilter, + setSeverityFilter, + setCategoryFilter, + setSort, + setCurrentPage, + setItemsPerPage, + setCurrentCase, + updateCaseInList, + toggleShowFilters, + clearAllFilters, + toggleCaseSelection, + clearSelectedCases, + clearCache, +} = aiPredictionSlice.actions; + +export default aiPredictionSlice.reducer; + +/* + * End of File: aiPredictionSlice.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/redux/index.ts b/app/modules/AIPrediction/redux/index.ts new file mode 100644 index 0000000..f93e701 --- /dev/null +++ b/app/modules/AIPrediction/redux/index.ts @@ -0,0 +1,15 @@ +/* + * File: index.ts + * Description: Redux exports for AI Prediction module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export * from './aiPredictionSlice'; +export * from './aiPredictionSelectors'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/screens/AIPredictionDetailScreen.tsx b/app/modules/AIPrediction/screens/AIPredictionDetailScreen.tsx new file mode 100644 index 0000000..633d785 --- /dev/null +++ b/app/modules/AIPrediction/screens/AIPredictionDetailScreen.tsx @@ -0,0 +1,1326 @@ +/* + * File: AIPredictionDetailScreen.tsx + * Description: AI Prediction detail screen with suggestion form for updating AI findings + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState, useCallback, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + TextInput, + TouchableOpacity, + TouchableWithoutFeedback, + Alert, + SafeAreaView, + StatusBar, + KeyboardAvoidingView, + Platform, + Dimensions, +} from 'react-native'; +import DateTimePicker from '@react-native-community/datetimepicker'; +import Icon from 'react-native-vector-icons/Feather'; +import { theme } from '../../../theme'; +import { useAppDispatch, useAppSelector } from '../../../store/hooks'; +import { CustomModal } from '../../../shared/components/CustomModal'; +import Toast from 'react-native-toast-message'; +import { aiPredictionAPI } from '../services/aiPredictionAPI'; +import { showError } from '../../../shared/utils/toast'; + +// Import Redux selectors +import { selectCurrentCase, selectIsLoadingCaseDetails, selectError } from '../redux'; +import { selectUser } from '../../Auth/redux/authSelectors'; + +// Import types +import type { AIPredictionDetailsScreenProps } from '../navigation/navigationTypes'; +import type { AIPredictionCase } from '../types'; + +// Get screen dimensions +const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); + +// ============================================================================ +// ENUMS & TYPES +// ============================================================================ + +/** + * Suggestion Type Enum + * + * Purpose: Define available suggestion types as specified by user + */ +export enum SuggestionType { + TREATMENT = 'Treatment', + FOLLOW_UP = 'Follow Up', + DIAGNOSIS = 'Diagnosis', + OTHER = 'Other' +} + +/** + * Priority Enum + * + * Purpose: Define priority levels as specified by user + */ +export enum Priority { + LOW = 'Low', + MEDIUM = 'Medium', + HIGH = 'High', + CRITICAL = 'Critical' +} + +/** + * Related Finding Interface + * + * Purpose: Structure for key-value pairs in related findings + */ +interface RelatedFinding { + key: string; + value: string; +} + +/** + * Suggestion Form Data Interface + * + * Purpose: Structure for suggestion form data + */ +interface SuggestionFormData { + patientId: string; + suggestionType: SuggestionType; + title: string; + suggestionText: string; + confidence: string; + priority: Priority; + category: string; + costEstimate: string; + timeEstimate: string; + expiresAt: Date | null; + aiModelVersion: string; + evidenceSources: string; + contraindications: string; + tags: string; + relatedFindings: RelatedFinding[]; +} + +/** + * API Response Interface + * + * Purpose: Structure for API response data + */ +interface APIResponse { + success: boolean; + message: string; + data: { + created_at: string; + updated_at: string; + status: string; + suggestion_id: string; + }; +} + +// ============================================================================ +// CONSTANTS +// ============================================================================ + +const SUGGESTION_TYPE_OPTIONS = [ + { label: 'Treatment', value: SuggestionType.TREATMENT, icon: 'activity', color: '#4CAF50' }, + { label: 'Follow Up', value: SuggestionType.FOLLOW_UP, icon: 'calendar', color: '#2196F3' }, + { label: 'Diagnosis', value: SuggestionType.DIAGNOSIS, icon: 'search', color: '#FF9800' }, + { label: 'Other', value: SuggestionType.OTHER, icon: 'more-horizontal', color: '#9C27B0' }, +]; + +const PRIORITY_OPTIONS = [ + { label: 'Low', value: Priority.LOW, color: '#4CAF50', bgColor: '#E8F5E8' }, + { label: 'Medium', value: Priority.MEDIUM, color: '#FF9800', bgColor: '#FFF3E0' }, + { label: 'High', value: Priority.HIGH, color: '#FF5722', bgColor: '#FFEBEE' }, + { label: 'Critical', value: Priority.CRITICAL, color: '#F44336', bgColor: '#FFEBEE' }, +]; + +// ============================================================================ +// AI PREDICTION DETAIL SCREEN COMPONENT +// ============================================================================ + +/** + * AIPredictionDetailScreen Component + * + * Purpose: Display AI prediction details and allow suggestion updates + * + * Features: + * - View complete AI prediction details + * - Create/update suggestions for AI findings + * - Form validation and submission + * - Dynamic related findings management + * - Date picker for expiration + * - Dropdown selections for types and priorities + * - Responsive design with keyboard handling + * - Enhanced visual design with modern mobile styling + */ +const AIPredictionDetailScreen: React.FC = ({ + navigation, + route +}) => { + // ============================================================================ + // REDUX STATE + // ============================================================================ + + const dispatch = useAppDispatch(); + const user = useAppSelector(selectUser); + const currentCase = useAppSelector(selectCurrentCase); + const isLoading = useAppSelector(selectIsLoadingCaseDetails); + const error = useAppSelector(selectError); + + // ============================================================================ + // LOCAL STATE + // ============================================================================ + + const [formData, setFormData] = useState({ + patientId: route.params.caseId, + suggestionType: SuggestionType.TREATMENT, + title: '', + suggestionText: '', + confidence: '0.99', + priority: Priority.HIGH, + category: 'Radiology', + costEstimate: '', + timeEstimate: '1-2 hours', + expiresAt: null, + aiModelVersion: 'v2.1.0', + evidenceSources: '', + contraindications: '', + tags: '', + relatedFindings: [ + { key: 'finding_label', value: 'midline shift' }, + { key: 'finding_type', value: 'no_pathology' }, + { key: 'clinical_urgency', value: 'urgent' }, + { key: 'primary_severity', value: 'high' }, + { key: 'modality', value: '/DX' }, + { key: 'institution', value: 'Brighton Radiology' }, + ], + }); + + const [showSuggestionTypeDropdown, setShowSuggestionTypeDropdown] = useState(false); + const [showPriorityDropdown, setShowPriorityDropdown] = useState(false); + const [showDatePicker, setShowDatePicker] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [showSuccessModal, setShowSuccessModal] = useState(false); + const [apiResponse, setApiResponse] = useState(null); + + + // ============================================================================ + // EFFECTS + // ============================================================================ + + /** + * Initialize form data from current case + */ + useEffect(() => { + if (currentCase) { + setFormData(prev => ({ + ...prev, + patientId: currentCase.patid, + title: `Recommended CT Scan`, + suggestionText: `Describe your suggestion with clinical reasoning...`, + confidence: currentCase.prediction.confidence_score.toFixed(4), + })); + } + }, [currentCase]); + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Handle form field changes + */ + const handleFieldChange = useCallback((field: keyof SuggestionFormData, value: any) => { + setFormData(prev => ({ + ...prev, + [field]: value, + })); + }, []); + + /** + * Handle dropdown selection + */ + const handleDropdownSelect = useCallback((field: keyof SuggestionFormData, value: any) => { + setFormData(prev => ({ + ...prev, + [field]: value, + })); + setShowSuggestionTypeDropdown(false); + setShowPriorityDropdown(false); + }, []); + + /** + * Close all dropdowns + */ + const closeAllDropdowns = useCallback(() => { + setShowSuggestionTypeDropdown(false); + setShowPriorityDropdown(false); + }, []); + + /** + * Handle date selection + */ + const handleDateChange = useCallback((event: any, selectedDate?: Date) => { + setShowDatePicker(false); + if (selectedDate) { + handleFieldChange('expiresAt', selectedDate); + } + }, [handleFieldChange]); + + /** + * Add new related finding + */ + const handleAddRelatedFinding = useCallback(() => { + setFormData(prev => ({ + ...prev, + relatedFindings: [...prev.relatedFindings, { key: '', value: '' }], + })); + }, []); + + /** + * Remove related finding + */ + const handleRemoveRelatedFinding = useCallback((index: number) => { + setFormData(prev => ({ + ...prev, + relatedFindings: prev.relatedFindings.filter((_, i) => i !== index), + })); + }, []); + + /** + * Update related finding + */ + const handleUpdateRelatedFinding = useCallback((index: number, field: 'key' | 'value', value: string) => { + setFormData(prev => ({ + ...prev, + relatedFindings: prev.relatedFindings.map((finding, i) => + i === index ? { ...finding, [field]: value } : finding + ), + })); + }, []); + + /** + * Validate form data + */ + const validateForm = useCallback((): boolean => { + if (!formData.title.trim()) { + showError('Validation Error', 'Title is required'); + return false; + } + if (!formData.suggestionText.trim()) { + showError('Validation Error', 'Suggestion text is required'); + return false; + } + if (!formData.patientId.trim()) { + showError('Validation Error', 'Patient ID is required'); + return false; + } + + // Validate expiry date - must be in the future + if (formData.expiresAt) { + const now = new Date(); + const expiryDate = new Date(formData.expiresAt); + + if (expiryDate <= now) { + showError('Validation Error', 'Expiry date must be in the future'); + return false; + } + } + + return true; + }, [formData]); + + /** + * Handle form submission + */ + const handleSubmitSuggestion = useCallback(async () => { + if (!validateForm()) return; + + setIsSubmitting(true); + try { + // Prepare API payload according to backend structure + const apiPayload = { + patid: formData.patientId, + suggestion_type: formData.suggestionType.toLowerCase().replace(' ', '_'), + suggestion_title: formData.title, + suggestion_text: formData.suggestionText, + confidence_score: parseFloat(formData.confidence), + priority_level: formData.priority.toLowerCase(), + category: formData.category, + related_findings: formData.relatedFindings.reduce((acc, finding) => { + if (finding.key && finding.value) { + acc[finding.key] = finding.value; + } + return acc; + }, {} as Record), + evidence_sources: formData.evidenceSources ? formData.evidenceSources.split(',').map(s => s.trim()) : [], + contraindications: formData.contraindications || 'none', + cost_estimate: formData.costEstimate ? parseFloat(formData.costEstimate) : 0, + time_estimate: formData.timeEstimate, + expires_at: formData.expiresAt ? formData.expiresAt.toISOString() : null, + tags: formData.tags ? formData.tags.split(',').map(s => s.trim()) : [], + ai_model_version: formData.aiModelVersion, + }; + + // Call centralized API service + const response = await aiPredictionAPI.submitAISuggestion(apiPayload, user?.access_token || ''); + console.log('feed back response',response) + if (response.ok && response.data) { + const responseData = response.data as APIResponse; + + if (responseData.success) { + // Show success toast + Toast.show({ + type: 'success', + text1: 'Success!', + text2: responseData.message || 'Suggestion submitted successfully', + position: 'top', + visibilityTime: 4000, + }); + + // Store API response data + setApiResponse(responseData.data); + // Show success modal with API response data + setShowSuccessModal(true); + } else { + throw new Error(responseData.message || 'Failed to submit suggestion'); + } + } else { + throw new Error(response.problem || 'HTTP request failed'); + } + } catch (error) { + console.error('Submission Error:', error); + + // Show error toast + Toast.show({ + type: 'error', + text1: 'Error', + text2: error instanceof Error ? error.message : 'Failed to submit suggestion. Please try again.', + position: 'top', + visibilityTime: 5000, + }); + } finally { + setIsSubmitting(false); + } + }, [formData, validateForm, user]); + + /** + * Handle success modal close + */ + const handleSuccessModalClose = useCallback(() => { + setShowSuccessModal(false); + navigation.goBack(); + }, [navigation]); + + /** + * Handle back navigation + */ + const handleGoBack = useCallback(() => { + navigation.goBack(); + }, [navigation]); + + + + // ============================================================================ + // RENDER FUNCTIONS + // ============================================================================ + + /** + * Render enhanced header + */ + const renderHeader = () => ( + + + + + + AI Prediction Details + Create Medical Suggestion + + + + ); + + /** + * Render enhanced dropdown options + */ + const renderDropdownOptions = useCallback(( + options: Array<{ label: string; value: any; color?: string; bgColor?: string; icon?: string }>, + currentValue: any, + onSelect: (value: any) => void + ) => ( + + {options.map((option) => ( + onSelect(option.value)} + > + + {option.icon && ( + + )} + + {option.label} + + + {currentValue === option.value && ( + + )} + + ))} + + ), []); + + /** + * Render enhanced related findings section + */ + const renderRelatedFindings = useCallback(() => ( + + + + Related Findings + + + {formData.relatedFindings.map((finding, index) => ( + + + handleUpdateRelatedFinding(index, 'key', value)} + placeholder="Finding Key" + placeholderTextColor={theme.colors.textMuted} + /> + handleUpdateRelatedFinding(index, 'value', value)} + placeholder="Finding Value" + placeholderTextColor={theme.colors.textMuted} + /> + + handleRemoveRelatedFinding(index)} + accessibilityRole="button" + accessibilityLabel="Remove related finding" + > + + + + ))} + + + Add Finding + + + + ), [formData.relatedFindings, handleUpdateRelatedFinding, handleRemoveRelatedFinding, handleAddRelatedFinding]); + + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + + + + + + {/* Enhanced Header */} + {renderHeader()} + + + + + {/* Patient ID Card */} + + + + Patient Information + + + Patient ID + + + + + {/* Suggestion Type Card */} + + + + Suggestion Type + + + Type + + setShowSuggestionTypeDropdown(!showSuggestionTypeDropdown)} + > + + opt.value === formData.suggestionType)?.icon as any || 'edit-3'} + size={18} + color={SUGGESTION_TYPE_OPTIONS.find(opt => opt.value === formData.suggestionType)?.color || theme.colors.primary} + /> + {formData.suggestionType} + + + + {showSuggestionTypeDropdown && renderDropdownOptions( + SUGGESTION_TYPE_OPTIONS, + formData.suggestionType, + (value) => handleDropdownSelect('suggestionType', value) + )} + + + + + {/* Main Suggestion Card */} + + + + Suggestion Details + + + {/* Title */} + + Title + handleFieldChange('title', value)} + placeholder="Recommended CT Scan" + placeholderTextColor={theme.colors.textMuted} + /> + + + {/* Suggestion Text */} + + Suggestion Text + handleFieldChange('suggestionText', value)} + placeholder="Describe your suggestion with clinical reasoning..." + placeholderTextColor={theme.colors.textMuted} + multiline + numberOfLines={4} + textAlignVertical="top" + /> + + + + {/* Confidence & Priority Card */} + + + + Assessment & Priority + + + {/* Row 1: Confidence and Priority */} + + + Confidence (0-1) + handleFieldChange('confidence', value)} + placeholder="0.9979" + placeholderTextColor={theme.colors.textMuted} + keyboardType="numeric" + /> + + + + Priority + + setShowPriorityDropdown(!showPriorityDropdown)} + > + + opt.value === formData.priority)?.bgColor || theme.colors.backgroundAlt } + ]} /> + {formData.priority} + + + + {showPriorityDropdown && renderDropdownOptions( + PRIORITY_OPTIONS, + formData.priority, + (value) => handleDropdownSelect('priority', value) + )} + + + + + + {/* Category & Cost Card */} + + + + Classification & Resources + + + {/* Row 2: Category and Cost Estimate */} + + + Category + handleFieldChange('category', value)} + placeholder="Radiology" + placeholderTextColor={theme.colors.textMuted} + /> + + + + Cost Estimate (USD) + handleFieldChange('costEstimate', value)} + placeholder="Enter cost" + placeholderTextColor={theme.colors.textMuted} + keyboardType="numeric" + /> + + + + + {/* Time & Expiry Card */} + + + + Timeline + + + {/* Row 3: Time Estimate and Expires At */} + + + Time Estimate + handleFieldChange('timeEstimate', value)} + placeholder="1-2 hours" + placeholderTextColor={theme.colors.textMuted} + /> + + + + Expires At + setShowDatePicker(true)} + > + + + {formData.expiresAt + ? formData.expiresAt.toLocaleDateString() + : 'Select date' + } + + + Date must be in the future + + + + + {/* AI Model & Evidence Card */} + + + + AI Model & Evidence + + + {/* AI Model Version */} + + AI Model Version + handleFieldChange('aiModelVersion', value)} + placeholder="v2.1.0" + placeholderTextColor={theme.colors.textMuted} + /> + + + {/* Evidence Sources */} + + Evidence Sources (comma-separated) + handleFieldChange('evidenceSources', value)} + placeholder="Evidence A, Protocol B" + placeholderTextColor={theme.colors.textMuted} + /> + + + + {/* Contraindications & Tags Card */} + + + + Safety & Organization + + + {/* Contraindications */} + + Contraindications + handleFieldChange('contraindications', value)} + placeholder="Any known contraindications..." + placeholderTextColor={theme.colors.textMuted} + multiline + numberOfLines={3} + textAlignVertical="top" + /> + + + {/* Tags */} + + Tags (comma-separated) + handleFieldChange('tags', value)} + placeholder="emergency, chest, pulmonary" + placeholderTextColor={theme.colors.textMuted} + /> + + + + {/* Related Findings */} + {renderRelatedFindings()} + + {/* Submit Button */} + + {isSubmitting ? ( + + + Submitting... + + ) : ( + + + Submit Suggestion + + )} + + + + + + + + + {/* Date Picker */} + {showDatePicker && ( + + )} + + {/* Success Modal */} + + + {/* Toast Messages */} + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#F8FAFC', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.lg, + backgroundColor: theme.colors.background, + borderBottomWidth: 1, + borderBottomColor: '#E2E8F0', + shadowColor: '#000000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.06, + shadowRadius: 2, + elevation: 2, + }, + backButton: { + padding: theme.spacing.sm, + backgroundColor: '#F1F5F9', + borderRadius: theme.borderRadius.medium, + }, + headerContent: { + flex: 1, + alignItems: 'center', + }, + headerTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: 2, + }, + headerSubtitle: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + }, + headerSpacer: { + width: 40, + }, + keyboardAvoidingView: { + flex: 1, + position: 'relative', + }, + scrollView: { + flex: 1, + }, + scrollContent: { + padding: theme.spacing.md, + paddingBottom: theme.spacing.xl, + }, + mainContent: { + flex: 1, + }, + + // Card Styles + patientCard: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.large, + padding: theme.spacing.lg, + marginBottom: theme.spacing.lg, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 4, + elevation: 2, + borderLeftWidth: 4, + borderLeftColor: theme.colors.primary, + }, + formCard: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.large, + padding: theme.spacing.lg, + marginBottom: theme.spacing.lg, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 4, + elevation: 2, + }, + cardHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.md, + paddingBottom: theme.spacing.sm, + borderBottomWidth: 1, + borderBottomColor: '#E2E8F0', + }, + cardTitle: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginLeft: theme.spacing.sm, + }, + patientCardHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.md, + }, + patientCardTitle: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginLeft: theme.spacing.sm, + }, + + // Form Styles + formGroup: { + marginBottom: theme.spacing.md, + }, + formGroupHalf: { + flex: 1, + }, + formRow: { + flexDirection: 'row', + gap: theme.spacing.md, + }, + label: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.sm, + }, + input: { + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textPrimary, + backgroundColor: theme.colors.background, + }, + enhancedInput: { + borderWidth: 1, + borderColor: '#E2E8F0', + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.md, + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textPrimary, + backgroundColor: '#FFFFFF', + shadowColor: '#000000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.03, + shadowRadius: 1, + elevation: 1, + }, + disabledInput: { + backgroundColor: '#F8FAFC', + color: theme.colors.textSecondary, + borderColor: '#E2E8F0', + }, + textArea: { + height: 100, + textAlignVertical: 'top', + }, + + // Dropdown Styles + dropdownContainer: { + position: 'relative', + }, + dropdown: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + backgroundColor: theme.colors.background, + }, + enhancedDropdown: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + borderWidth: 1, + borderColor: '#E2E8F0', + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.md, + backgroundColor: '#FFFFFF', + shadowColor: '#000000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.03, + shadowRadius: 1, + elevation: 1, + }, + dropdownContent: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + dropdownText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textPrimary, + marginLeft: theme.spacing.sm, + }, + + dropdownOptions: { + position: 'absolute', + top: '100%', + left: 0, + right: 0, + borderWidth: 1, + borderColor: '#E2E8F0', + borderRadius: theme.borderRadius.medium, + backgroundColor: '#FFFFFF', + marginTop: theme.spacing.xs, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + dropdownOption: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.md, + borderBottomWidth: 1, + borderBottomColor: '#F1F5F9', + }, + dropdownOptionContent: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + dropdownOptionIcon: { + marginRight: theme.spacing.sm, + }, + dropdownOptionSelected: { + backgroundColor: '#F8FAFC', + }, + dropdownOptionText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textPrimary, + }, + dropdownOptionTextSelected: { + color: theme.colors.primary, + fontFamily: theme.typography.fontFamily.medium, + }, + priorityIndicator: { + width: 12, + height: 12, + borderRadius: 6, + marginRight: theme.spacing.sm, + }, + + // Date Input Styles + dateInput: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + backgroundColor: theme.colors.background, + }, + enhancedDateInput: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + borderWidth: 1, + borderColor: '#E2E8F0', + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.md, + backgroundColor: '#FFFFFF', + shadowColor: '#000000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.03, + shadowRadius: 1, + elevation: 1, + }, + dateText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textPrimary, + flex: 1, + marginLeft: theme.spacing.sm, + }, + placeholderText: { + color: theme.colors.textMuted, + }, + helperText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + marginTop: theme.spacing.xs, + fontStyle: 'italic', + }, + + // Section Styles + sectionHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.md, + }, + sectionTitle: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginLeft: theme.spacing.sm, + }, + + // Related Findings Styles + relatedFindingsContainer: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.large, + padding: theme.spacing.lg, + marginBottom: theme.spacing.lg, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 4, + elevation: 2, + }, + relatedFindingsContent: { + marginTop: theme.spacing.sm, + }, + relatedFindingItem: { + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing.sm, + marginBottom: theme.spacing.sm, + }, + relatedFindingInputs: { + flex: 1, + flexDirection: 'row', + gap: theme.spacing.sm, + }, + relatedFindingKey: { + flex: 1, + backgroundColor: '#F8FAFC', + borderColor: '#E2E8F0', + }, + relatedFindingValue: { + flex: 1, + backgroundColor: '#F8FAFC', + borderColor: '#E2E8F0', + }, + addButton: { + backgroundColor: theme.colors.primary, + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + alignItems: 'center', + alignSelf: 'flex-start', + flexDirection: 'row', + gap: theme.spacing.sm, + shadowColor: theme.colors.primary, + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.2, + shadowRadius: 2, + elevation: 2, + }, + addButtonText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.background, + }, + removeButton: { + padding: theme.spacing.sm, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#FEF2F2', + borderRadius: theme.borderRadius.medium, + }, + + // Submit Button Styles + submitButton: { + backgroundColor: theme.colors.primary, + borderRadius: theme.borderRadius.large, + paddingVertical: theme.spacing.lg, + alignItems: 'center', + marginTop: theme.spacing.xl, + shadowColor: theme.colors.primary, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 4, + elevation: 4, + }, + submitButtonDisabled: { + backgroundColor: theme.colors.textMuted, + shadowOpacity: 0.1, + }, + submitButtonContent: { + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing.sm, + }, + submitButtonText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + }, + spinningIcon: { + // Add rotation animation if needed + }, +}); + +export default AIPredictionDetailScreen; + +/* + * End of File: AIPredictionDetailScreen.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/screens/AIPredictionsScreen.tsx b/app/modules/AIPrediction/screens/AIPredictionsScreen.tsx new file mode 100644 index 0000000..acff58c --- /dev/null +++ b/app/modules/AIPrediction/screens/AIPredictionsScreen.tsx @@ -0,0 +1,749 @@ +/* + * File: AIPredictionsScreen.tsx + * Description: Main AI Predictions screen with data rendering and management + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useEffect, useCallback, useState } from 'react'; +import { + View, + Text, + StyleSheet, + FlatList, + RefreshControl, + TouchableOpacity, + Alert, + StatusBar, + SafeAreaView, +} from 'react-native'; +import Icon from 'react-native-vector-icons/Feather'; +import { theme } from '../../../theme'; +import { useAppDispatch, useAppSelector } from '../../../store/hooks'; + +// Import Redux actions and selectors +import { + fetchAIPredictions, + setSearchQuery, + setUrgencyFilter, + setSeverityFilter, + setCategoryFilter, + setCurrentPage, + clearAllFilters, + toggleShowFilters, + toggleCaseSelection, + clearSelectedCases, + updateCaseReview, +} from '../redux'; + +import { + selectPaginatedCases, + selectIsLoading, + selectError, + selectSearchQuery, + selectUrgencyFilter, + selectSeverityFilter, + selectCategoryFilter, + selectShowFilters, + selectSelectedCaseIds, + selectCasesStatistics, + selectFilterCounts, + selectActiveFiltersCount, + selectCurrentPage, + selectTotalPages, + selectHasNextPage, + selectHasPreviousPage, +} from '../redux'; + +// Import components +import { + AIPredictionCard, + SearchBar, + FilterTabs, + LoadingState, + EmptyState, + StatsOverview, +} from '../components'; + +// Import types +import type { AIPredictionCase } from '../types'; + +// Import auth selector +import { selectUser } from '../../Auth/redux/authSelectors'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface AIPredictionsScreenProps { + navigation: any; + route?: any; +} + +// ============================================================================ +// AI PREDICTIONS SCREEN COMPONENT +// ============================================================================ + +/** + * AIPredictionsScreen Component + * + * Purpose: Main screen for displaying and managing AI prediction cases + * + * Features: + * - Comprehensive AI predictions list + * - Real-time search and filtering + * - Statistics overview dashboard + * - Bulk case selection and actions + * - Pull-to-refresh functionality + * - Pagination support + * - Review status management + * - Modern card-based design + * - Error handling and retry + * - Loading states and empty states + * - Accessibility support + */ +const AIPredictionsScreen: React.FC = ({ navigation }) => { + // ============================================================================ + // REDUX STATE + // ============================================================================ + + const dispatch = useAppDispatch(); + + // Auth state + const user :any = useAppSelector(selectUser); + + // AI Prediction state + const cases = useAppSelector(selectPaginatedCases); + const isLoading = useAppSelector(selectIsLoading); + const error = useAppSelector(selectError); + const searchQuery = useAppSelector(selectSearchQuery); + const urgencyFilter = useAppSelector(selectUrgencyFilter); + const severityFilter = useAppSelector(selectSeverityFilter); + const categoryFilter = useAppSelector(selectCategoryFilter); + const showFilters = useAppSelector(selectShowFilters); + const selectedCaseIds = useAppSelector(selectSelectedCaseIds); + const statistics = useAppSelector(selectCasesStatistics); + const filterCounts = useAppSelector(selectFilterCounts); + const activeFiltersCount = useAppSelector(selectActiveFiltersCount); + const currentPage = useAppSelector(selectCurrentPage); + const totalPages = useAppSelector(selectTotalPages); + const hasNextPage = useAppSelector(selectHasNextPage); + const hasPreviousPage = useAppSelector(selectHasPreviousPage); + + // ============================================================================ + // LOCAL STATE + // ============================================================================ + + const [refreshing, setRefreshing] = useState(false); + const [showStats, setShowStats] = useState(true); + + // ============================================================================ + // EFFECTS + // ============================================================================ + + /** + * Load AI Predictions on Mount + * + * Purpose: Fetch AI predictions when component mounts + */ + console.log('user ===>', user); + useEffect(() => { + if (user?.access_token) { + loadAIPredictions(); + } + }, [user?.access_token]); + + /** + * Load AI Predictions on Filter Change + * + * Purpose: Reload data when filters change + */ + useEffect(() => { + if (user?.access_token) { + loadAIPredictions(); + } + }, [urgencyFilter, severityFilter, categoryFilter, searchQuery, currentPage]); + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Load AI Predictions + * + * Purpose: Fetch AI predictions from API + */ + const loadAIPredictions = useCallback(async () => { + if (!user?.access_token) return; + + try { + const params = { + page: currentPage, + limit: 20, + ...(urgencyFilter !== 'all' && { urgency: urgencyFilter }), + ...(severityFilter !== 'all' && { severity: severityFilter }), + ...(categoryFilter !== 'all' && { category: categoryFilter }), + ...(searchQuery.trim() && { search: searchQuery.trim() }), + }; + + await dispatch(fetchAIPredictions({ + token: user.access_token, + params, + })).unwrap(); + } catch (error) { + console.error('Failed to load AI predictions:', error); + // Error is handled by Redux state + } + }, [dispatch, user?.access_token, currentPage, urgencyFilter, severityFilter, categoryFilter, searchQuery]); + + /** + * Handle Refresh + * + * Purpose: Handle pull-to-refresh + */ + const handleRefresh = useCallback(async () => { + setRefreshing(true); + await loadAIPredictions(); + setRefreshing(false); + }, [loadAIPredictions]); + + /** + * Handle Search + * + * Purpose: Handle search query change + */ + const handleSearch = useCallback((query: string) => { + dispatch(setSearchQuery(query)); + }, [dispatch]); + + /** + * Handle Filter Changes + * + * Purpose: Handle filter option changes + */ + const handleUrgencyFilterChange = useCallback((filter: typeof urgencyFilter) => { + dispatch(setUrgencyFilter(filter)); + }, [dispatch]); + + const handleSeverityFilterChange = useCallback((filter: typeof severityFilter) => { + dispatch(setSeverityFilter(filter)); + }, [dispatch]); + + const handleCategoryFilterChange = useCallback((filter: typeof categoryFilter) => { + dispatch(setCategoryFilter(filter)); + }, [dispatch]); + + /** + * Handle Clear Filters + * + * Purpose: Clear all active filters + */ + const handleClearFilters = useCallback(() => { + dispatch(clearAllFilters()); + }, [dispatch]); + + /** + * Handle Toggle Filters + * + * Purpose: Toggle filter visibility + */ + const handleToggleFilters = useCallback(() => { + dispatch(toggleShowFilters()); + }, [dispatch]); + + /** + * Handle Case Press + * + * Purpose: Navigate to case details + */ + const handleCasePress = useCallback((predictionCase: AIPredictionCase) => { + navigation.navigate('AIPredictionDetails', { caseId: predictionCase.patid }); + }, [navigation]); + + /** + * Handle Case Review + * + * Purpose: Handle case review action + */ + const handleCaseReview = useCallback(async (caseId: string) => { + if (!user?.access_token) return; + + try { + await dispatch(updateCaseReview({ + caseId, + reviewData: { + review_status: 'reviewed', + reviewed_by: user.name || user.email || 'Current User', + }, + token: user.access_token, + })).unwrap(); + + Alert.alert( + 'Review Updated', + 'Case has been marked as reviewed.', + [{ text: 'OK' }] + ); + } catch (error) { + Alert.alert( + 'Error', + 'Failed to update case review. Please try again.', + [{ text: 'OK' }] + ); + } + }, [dispatch, user]); + + /** + * Handle Case Selection + * + * Purpose: Handle case selection for bulk operations + */ + const handleCaseSelection = useCallback((caseId: string) => { + dispatch(toggleCaseSelection(caseId)); + }, [dispatch]); + + /** + * Handle Bulk Actions + * + * Purpose: Handle bulk actions on selected cases + */ + const handleBulkReview = useCallback(() => { + if (selectedCaseIds.length === 0) return; + + Alert.alert( + 'Bulk Review', + `Mark ${selectedCaseIds.length} cases as reviewed?`, + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Confirm', + onPress: async () => { + // Implement bulk review logic here + // For now, just clear selections + dispatch(clearSelectedCases()); + }, + }, + ] + ); + }, [selectedCaseIds, dispatch]); + + /** + * Handle Page Change + * + * Purpose: Handle pagination + */ + const handlePreviousPage = useCallback(() => { + if (hasPreviousPage) { + dispatch(setCurrentPage(currentPage - 1)); + } + }, [dispatch, currentPage, hasPreviousPage]); + + const handleNextPage = useCallback(() => { + if (hasNextPage) { + dispatch(setCurrentPage(currentPage + 1)); + } + }, [dispatch, currentPage, hasNextPage]); + + /** + * Handle Stats Press + * + * Purpose: Handle statistics card press + */ + const handleStatsPress = useCallback((statType: string) => { + // Navigate to detailed statistics or apply relevant filters + switch (statType) { + case 'critical': + dispatch(setUrgencyFilter('emergency')); + break; + case 'urgent': + dispatch(setUrgencyFilter('urgent')); + break; + case 'pending': + // Filter for pending reviews + break; + default: + break; + } + }, [dispatch]); + + /** + * Handle Retry + * + * Purpose: Handle retry after error + */ + const handleRetry = useCallback(() => { + loadAIPredictions(); + }, [loadAIPredictions]); + + // ============================================================================ + // RENDER FUNCTIONS + // ============================================================================ + + /** + * Render AI Prediction Case + * + * Purpose: Render individual AI prediction case card + */ + const renderPredictionCase = useCallback(({ item }: { item: AIPredictionCase }) => ( + + ), [handleCasePress, handleCaseReview, selectedCaseIds, handleCaseSelection]); + + /** + * Render List Header + * + * Purpose: Render search, filters, and statistics + */ + const renderListHeader = useCallback(() => ( + + {/* Statistics Overview */} + {showStats && ( + + )} + + {/* Search Bar */} + + + {/* Filter Controls */} + + + + + Filters + + {activeFiltersCount > 0 && ( + + {activeFiltersCount} + + )} + + + {selectedCaseIds.length > 0 && ( + + + + Review {selectedCaseIds.length} + + + )} + + + {/* Filter Tabs */} + {showFilters && ( + + )} + + {/* Results Summary */} + + + {statistics.total} predictions found + {activeFiltersCount > 0 && ` (${activeFiltersCount} filters applied)`} + + + + ), [ + showStats, + statistics, + handleStatsPress, + searchQuery, + handleSearch, + showFilters, + handleToggleFilters, + activeFiltersCount, + selectedCaseIds, + handleBulkReview, + urgencyFilter, + severityFilter, + categoryFilter, + handleUrgencyFilterChange, + handleSeverityFilterChange, + handleCategoryFilterChange, + handleClearFilters, + filterCounts, + ]); + + /** + * Render List Footer + * + * Purpose: Render pagination controls + */ + const renderListFooter = useCallback(() => { + if (totalPages <= 1) return null; + + return ( + + + + + Previous + + + + + Page {currentPage} of {totalPages} + + + + + Next + + + + + ); + }, [totalPages, currentPage, hasPreviousPage, hasNextPage, handlePreviousPage, handleNextPage]); + + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + + + + {/* Header */} + + AI Predictions + setShowStats(!showStats)} + accessibilityRole="button" + accessibilityLabel="Toggle statistics" + > + + + + + {/* Content */} + {error ? ( + + ) : isLoading && cases.length === 0 ? ( + + ) : cases.length === 0 ? ( + 0 ? handleClearFilters : handleRefresh} + /> + ) : ( + item.patid} + ListHeaderComponent={renderListHeader} + ListFooterComponent={renderListFooter} + refreshControl={ + + } + showsVerticalScrollIndicator={false} + contentContainerStyle={styles.listContent} + accessibilityRole="list" + /> + )} + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.md, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + backgroundColor: theme.colors.background, + }, + headerTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.textPrimary, + }, + headerButton: { + padding: theme.spacing.sm, + }, + filterControls: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + }, + filterToggle: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + borderRadius: theme.borderRadius.medium, + borderWidth: 1, + borderColor: theme.colors.primary, + gap: theme.spacing.sm, + }, + filterToggleActive: { + backgroundColor: theme.colors.primary, + }, + filterToggleText: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.primary, + fontWeight: theme.typography.fontWeight.medium, + }, + filterToggleActiveText: { + color: theme.colors.background, + }, + filterBadge: { + backgroundColor: theme.colors.error, + borderRadius: 10, + minWidth: 20, + height: 20, + justifyContent: 'center', + alignItems: 'center', + }, + filterBadgeText: { + fontSize: theme.typography.fontSize.caption, + color: theme.colors.background, + fontWeight: theme.typography.fontWeight.bold, + }, + bulkActionButton: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.primary, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + borderRadius: theme.borderRadius.medium, + gap: theme.spacing.sm, + }, + bulkActionText: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.background, + fontWeight: theme.typography.fontWeight.medium, + }, + resultsSummary: { + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + }, + resultsText: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textSecondary, + }, + listContent: { + paddingBottom: theme.spacing.xl, + }, + paginationContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.lg, + marginTop: theme.spacing.lg, + }, + paginationButton: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + borderRadius: theme.borderRadius.medium, + borderWidth: 1, + borderColor: theme.colors.primary, + gap: theme.spacing.xs, + }, + paginationButtonDisabled: { + borderColor: theme.colors.textMuted, + opacity: 0.5, + }, + paginationButtonText: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.primary, + fontWeight: theme.typography.fontWeight.medium, + }, + paginationButtonTextDisabled: { + color: theme.colors.textMuted, + }, + paginationInfo: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textSecondary, + fontWeight: theme.typography.fontWeight.medium, + }, +}); + +export default AIPredictionsScreen; + +/* + * End of File: AIPredictionsScreen.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/screens/index.ts b/app/modules/AIPrediction/screens/index.ts new file mode 100644 index 0000000..4c7627e --- /dev/null +++ b/app/modules/AIPrediction/screens/index.ts @@ -0,0 +1,15 @@ +/* + * File: index.ts + * Description: Screens exports for AI Prediction module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export { default as AIPredictionsScreen } from './AIPredictionsScreen'; +export { default as AIPredictionDetailScreen } from './AIPredictionDetailScreen'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/services/aiPredictionAPI.ts b/app/modules/AIPrediction/services/aiPredictionAPI.ts new file mode 100644 index 0000000..c74460e --- /dev/null +++ b/app/modules/AIPrediction/services/aiPredictionAPI.ts @@ -0,0 +1,337 @@ +/* + * File: aiPredictionAPI.ts + * Description: API service for AI prediction operations using apisauce + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { create } from 'apisauce'; +import { API_CONFIG, buildHeaders } from '../../../shared/utils'; + +const api = create({ + baseURL: API_CONFIG.BASE_URL +}); + +/** + * AI Prediction API Service + * + * Purpose: Handle all AI prediction-related API operations + * + * Features: + * - Get AI prediction results for all patients + * - Get individual case prediction details + * - Update case review status + * - Search and filter predictions + * - Get prediction statistics + */ +export const aiPredictionAPI = { + /** + * Get All AI Prediction Results + * + * Purpose: Fetch all AI prediction results from server + * + * @param token - Authentication token + * @param params - Optional query parameters for filtering + * @returns Promise with AI prediction cases data + */ + getAllPredictions: (token: string, params?: { + page?: number; + limit?: number; + urgency?: string; + severity?: string; + category?: string; + search?: string; + }) => { + const queryParams = params ? { ...params } : {}; + return api.get('/api/ai-cases/all-prediction-results', queryParams, buildHeaders({ token })); + }, + + /** + * Get AI Prediction Case Details + * + * Purpose: Fetch detailed information for a specific AI prediction case + * + * @param caseId - AI prediction case ID (patid) + * @param token - Authentication token + * @returns Promise with detailed case prediction data + */ + getCaseDetails: (caseId: string, token: string) => { + return api.get(`/api/ai-cases/prediction-details/${caseId}`, {}, buildHeaders({ token })); + }, + + /** + * Update Case Review Status + * + * Purpose: Update the review status of an AI prediction case + * + * @param caseId - AI prediction case ID + * @param reviewData - Review status and notes + * @param token - Authentication token + * @returns Promise with updated case data + */ + updateCaseReview: (caseId: string, reviewData: { + review_status: 'pending' | 'reviewed' | 'confirmed' | 'disputed'; + reviewed_by?: string; + review_notes?: string; + priority?: 'critical' | 'high' | 'medium' | 'low'; + }, token: string) => { + return api.put(`/api/ai-cases/review/${caseId}`, reviewData, buildHeaders({ token })); + }, + + /** + * Get Prediction Statistics + * + * Purpose: Fetch AI prediction statistics for dashboard + * + * @param token - Authentication token + * @param timeRange - Optional time range filter (today, week, month) + * @returns Promise with prediction statistics + */ + getPredictionStats: (token: string, timeRange?: 'today' | 'week' | 'month') => { + const params = timeRange ? { timeRange } : {}; + return api.get('/api/ai-cases/statistics', params, buildHeaders({ token })); + }, + + /** + * Search AI Prediction Cases + * + * Purpose: Search AI prediction cases by various criteria + * + * @param query - Search query (patient ID, hospital, findings) + * @param token - Authentication token + * @param filters - Additional search filters + * @returns Promise with search results + */ + searchPredictions: (query: string, token: string, filters?: { + urgency?: string[]; + severity?: string[]; + category?: string[]; + dateRange?: { start: string; end: string }; + }) => { + const params = { + q: query, + ...(filters && { filters: JSON.stringify(filters) }) + }; + return api.get('/api/ai-cases/search', params, buildHeaders({ token })); + }, + + /** + * Get Predictions by Hospital + * + * Purpose: Fetch AI predictions filtered by hospital + * + * @param hospitalId - Hospital UUID + * @param token - Authentication token + * @param params - Optional query parameters + * @returns Promise with hospital-specific predictions + */ + getPredictionsByHospital: (hospitalId: string, token: string, params?: { + page?: number; + limit?: number; + urgency?: string; + startDate?: string; + endDate?: string; + }) => { + const queryParams = params ? { ...params } : {}; + return api.get(`/api/ai-cases/hospital/${hospitalId}/predictions`, queryParams, buildHeaders({ token })); + }, + + /** + * Get Critical Predictions + * + * Purpose: Fetch only critical and urgent AI predictions + * + * @param token - Authentication token + * @returns Promise with critical predictions data + */ + getCriticalPredictions: (token: string) => { + return api.get('/api/ai-cases/critical-predictions', {}, buildHeaders({ token })); + }, + + /** + * Bulk Update Case Reviews + * + * Purpose: Update multiple case reviews at once + * + * @param caseIds - Array of case IDs to update + * @param reviewData - Review data to apply to all cases + * @param token - Authentication token + * @returns Promise with bulk update results + */ + bulkUpdateReviews: (caseIds: string[], reviewData: { + review_status: 'pending' | 'reviewed' | 'confirmed' | 'disputed'; + reviewed_by?: string; + review_notes?: string; + }, token: string) => { + return api.put('/api/ai-cases/bulk-review', { + caseIds, + reviewData + }, buildHeaders({ token })); + }, + + /** + * Export Predictions Data + * + * Purpose: Export AI predictions data for reporting + * + * @param token - Authentication token + * @param filters - Export filters + * @param format - Export format (csv, xlsx, pdf) + * @returns Promise with export file data + */ + exportPredictions: (token: string, filters?: { + urgency?: string[]; + severity?: string[]; + dateRange?: { start: string; end: string }; + hospitalId?: string; + }, format: 'csv' | 'xlsx' | 'pdf' = 'csv') => { + const params = { + format, + ...(filters && { filters: JSON.stringify(filters) }) + }; + return api.get('/api/ai-cases/export', params, buildHeaders({ token })); + }, + + /** + * Get Prediction Trends + * + * Purpose: Fetch prediction trends data for analytics + * + * @param token - Authentication token + * @param period - Time period for trends (daily, weekly, monthly) + * @returns Promise with trends data + */ + getPredictionTrends: (token: string, period: 'daily' | 'weekly' | 'monthly' = 'weekly') => { + return api.get('/api/ai-cases/trends', { period }, buildHeaders({ token })); + }, + + /** + * Submit Feedback on Prediction + * + * Purpose: Submit physician feedback on AI prediction accuracy + * + * @param caseId - AI prediction case ID + * @param feedbackData - Feedback data + * @param token - Authentication token + * @returns Promise with feedback submission result + */ + submitPredictionFeedback: (caseId: string, feedbackData: { + accuracy_rating: 1 | 2 | 3 | 4 | 5; + is_accurate: boolean; + physician_diagnosis?: string; + feedback_notes?: string; + improvement_suggestions?: string; + }, token: string) => { + return api.post(`/api/ai-cases/feedback/${caseId}`, feedbackData, buildHeaders({ token })); + }, + + /** + * Submit AI Suggestion + * + * Purpose: Submit physician suggestions for AI findings + * + * @param suggestionData - Suggestion data including patient ID, type, title, text, etc. + * @param token - Authentication token + * @returns Promise with suggestion submission result + */ + submitAISuggestion: (suggestionData: { + patid: string; + suggestion_type: string; + suggestion_title: string; + suggestion_text: string; + confidence_score: number; + priority_level: string; + category: string; + related_findings: Record; + evidence_sources: string[]; + contraindications: string; + cost_estimate: number; + time_estimate: string; + expires_at: string | null; + tags: string[]; + ai_model_version: string; + }, token: string) => { + return api.post('/api/ai-cases/suggestions', suggestionData, buildHeaders({ token })); + }, + + /** + * Get AI Suggestions + * + * Purpose: Fetch AI suggestions for a specific case or all suggestions + * + * @param token - Authentication token + * @param params - Optional query parameters + * @returns Promise with suggestions data + */ + getAISuggestions: (token: string, params?: { + caseId?: string; + patientId?: string; + suggestionType?: string; + priority?: string; + status?: string; + page?: number; + limit?: number; + }) => { + const queryParams = params ? { ...params } : {}; + return api.get('/api/ai-cases/suggestions', queryParams, buildHeaders({ token })); + }, + + /** + * Update AI Suggestion + * + * Purpose: Update an existing AI suggestion + * + * @param suggestionId - Suggestion ID to update + * @param updateData - Updated suggestion data + * @param token - Authentication token + * @returns Promise with updated suggestion data + */ + updateAISuggestion: (suggestionId: string, updateData: { + suggestion_title?: string; + suggestion_text?: string; + priority_level?: string; + category?: string; + related_findings?: Record; + evidence_sources?: string[]; + contraindications?: string; + cost_estimate?: number; + time_estimate?: string; + expires_at?: string | null; + tags?: string[]; + }, token: string) => { + return api.put(`/api/ai-cases/suggestions/${suggestionId}`, updateData, buildHeaders({ token })); + }, + + /** + * Delete AI Suggestion + * + * Purpose: Delete an AI suggestion + * + * @param suggestionId - Suggestion ID to delete + * @param token - Authentication token + * @returns Promise with deletion result + */ + deleteAISuggestion: (suggestionId: string, token: string) => { + return api.delete(`/api/ai-cases/suggestions/${suggestionId}`, {}, buildHeaders({ token })); + }, + + /** + * Get Suggestion Statistics + * + * Purpose: Fetch statistics about AI suggestions + * + * @param token - Authentication token + * @param timeRange - Optional time range filter + * @returns Promise with suggestion statistics + */ + getSuggestionStats: (token: string, timeRange?: 'today' | 'week' | 'month') => { + const params = timeRange ? { timeRange } : {}; + return api.get('/api/ai-cases/suggestions/statistics', params, buildHeaders({ token })); + } +}; + +/* + * End of File: aiPredictionAPI.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/services/index.ts b/app/modules/AIPrediction/services/index.ts new file mode 100644 index 0000000..e410e41 --- /dev/null +++ b/app/modules/AIPrediction/services/index.ts @@ -0,0 +1,14 @@ +/* + * File: index.ts + * Description: Services exports for AI Prediction module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export * from './aiPredictionAPI'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/types/aiPrediction.ts b/app/modules/AIPrediction/types/aiPrediction.ts new file mode 100644 index 0000000..acf9582 --- /dev/null +++ b/app/modules/AIPrediction/types/aiPrediction.ts @@ -0,0 +1,221 @@ +/* + * File: aiPrediction.ts + * Description: Type definitions for AI Prediction module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// ============================================================================ +// AI PREDICTION INTERFACES +// ============================================================================ + +/** + * AI Prediction Interface + * + * Purpose: Define the structure of AI prediction data from the API + * + * Based on API response structure: + * - label: Type of medical finding + * - finding_type: Category of the finding + * - clinical_urgency: Urgency level for medical response + * - confidence_score: AI confidence in the prediction (0-1) + * - finding_category: General category of the finding + * - primary_severity: Severity level of the condition + * - anatomical_location: Where the finding is located + */ +export interface AIPrediction { + label: string; + finding_type: 'no_pathology' | 'pathology' | 'abnormal' | 'normal' | 'unknown'; + clinical_urgency: 'urgent' | 'moderate' | 'low' | 'routine' | 'emergency'; + confidence_score: number; // 0.0 to 1.0 + finding_category: 'normal' | 'abnormal' | 'critical' | 'warning' | 'unknown'; + primary_severity: 'high' | 'medium' | 'low' | 'none'; + anatomical_location: string; // 'not_applicable' | specific location +} + +/** + * AI Prediction Case Interface + * + * Purpose: Complete AI prediction case data structure + * + * Features: + * - Patient identification + * - Hospital association + * - AI prediction results + * - Metadata for tracking and display + */ +export interface AIPredictionCase { + patid: string; // Patient ID + hospital_id: string; // Hospital UUID + prediction: AIPrediction; + + // Additional metadata (will be added for UI purposes) + created_at?: string; + updated_at?: string; + reviewed_by?: string; + review_status?: 'pending' | 'reviewed' | 'confirmed' | 'disputed'; + priority?: 'critical' | 'high' | 'medium' | 'low'; + processed_at?: string; +} + +/** + * AI Prediction API Response Interface + * + * Purpose: Define the structure of API response + */ +export interface AIPredictionAPIResponse { + success: boolean; + data: AIPredictionCase[]; + message?: string; + total?: number; + page?: number; + limit?: number; +} + +/** + * AI Prediction State Interface + * + * Purpose: Define Redux state structure for AI predictions + * + * Features: + * - Prediction cases management + * - Current selected case + * - Loading states for async operations + * - Error handling and messages + * - Search and filtering + * - Pagination support + * - Cache management + */ +export interface AIPredictionState { + // Prediction data + predictionCases: AIPredictionCase[]; + currentCase: AIPredictionCase | null; + + // Loading states + isLoading: boolean; + isRefreshing: boolean; + isLoadingCaseDetails: boolean; + + // Error handling + error: string | null; + + // Search and filtering + searchQuery: string; + selectedUrgencyFilter: 'all' | 'urgent' | 'moderate' | 'low' | 'routine' | 'emergency'; + selectedSeverityFilter: 'all' | 'high' | 'medium' | 'low' | 'none'; + selectedCategoryFilter: 'all' | 'normal' | 'abnormal' | 'critical' | 'warning' | 'unknown'; + sortBy: 'date' | 'urgency' | 'confidence' | 'severity'; + sortOrder: 'asc' | 'desc'; + + // Pagination + currentPage: number; + itemsPerPage: number; + totalItems: number; + + // Cache management + lastUpdated: String | null; + cacheExpiry: String | null; + + // UI state + showFilters: boolean; + selectedCaseIds: string[]; +} + +/** + * AI Prediction Filter Options + * + * Purpose: Define available filter options for the UI + */ +export interface AIPredictionFilters { + urgency: Array<{ + label: string; + value: AIPredictionState['selectedUrgencyFilter']; + count?: number; + }>; + severity: Array<{ + label: string; + value: AIPredictionState['selectedSeverityFilter']; + count?: number; + }>; + category: Array<{ + label: string; + value: AIPredictionState['selectedCategoryFilter']; + count?: number; + }>; +} + +/** + * AI Prediction Statistics Interface + * + * Purpose: Define statistics data for dashboard display + */ +export interface AIPredictionStats { + totalCases: number; + criticalCases: number; + urgentCases: number; + reviewedCases: number; + pendingCases: number; + averageConfidence: number; + todaysCases: number; + weeklyTrend: number; // percentage change from last week +} + +/** + * AI Prediction Navigation Props + * + * Purpose: Type safety for navigation between AI prediction screens + */ +export type AIPredictionNavigationProps = { + AIPredictionList: undefined; + AIPredictionDetails: { caseId: string }; + AIPredictionFilters: undefined; + AIPredictionStats: undefined; +}; + +// ============================================================================ +// UTILITY TYPES +// ============================================================================ + +/** + * Prediction Urgency Colors + * + * Purpose: Map urgency levels to UI colors + */ +export const URGENCY_COLORS = { + emergency: '#F44336', // Red + urgent: '#FF5722', // Deep Orange + moderate: '#FF9800', // Orange + low: '#FFC107', // Amber + routine: '#4CAF50', // Green +} as const; + +/** + * Prediction Severity Colors + * + * Purpose: Map severity levels to UI colors + */ +export const SEVERITY_COLORS = { + high: '#F44336', // Red + medium: '#FF9800', // Orange + low: '#FFC107', // Amber + none: '#4CAF50', // Green +} as const; + +/** + * Finding Category Colors + * + * Purpose: Map finding categories to UI colors + */ +export const CATEGORY_COLORS = { + critical: '#F44336', // Red + abnormal: '#FF9800', // Orange + warning: '#FFC107', // Amber + normal: '#4CAF50', // Green + unknown: '#9E9E9E', // Gray +} as const; + +/* + * End of File: aiPrediction.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/AIPrediction/types/index.ts b/app/modules/AIPrediction/types/index.ts new file mode 100644 index 0000000..ea9fa52 --- /dev/null +++ b/app/modules/AIPrediction/types/index.ts @@ -0,0 +1,14 @@ +/* + * File: index.ts + * Description: Type definitions exports for AI Prediction module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export * from './aiPrediction'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/Auth/components/signup/DocumentUploadStep.tsx b/app/modules/Auth/components/signup/DocumentUploadStep.tsx new file mode 100644 index 0000000..b06bb9b --- /dev/null +++ b/app/modules/Auth/components/signup/DocumentUploadStep.tsx @@ -0,0 +1,641 @@ +/* + * File: DocumentUploadStep.tsx + * Description: Document upload step component for signup flow with image picker + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState } from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + ScrollView, + Image, + Alert, + PermissionsAndroid, + Platform, + KeyboardAvoidingView, +} from 'react-native'; +import { + launchImageLibrary, + launchCamera, + ImagePickerResponse, + MediaType, +} from 'react-native-image-picker'; +import { theme } from '../../../../theme/theme'; +import { DocumentUploadStepProps } from '../../types/signup'; +import Icon from 'react-native-vector-icons/Feather'; +import { showError, showSuccess } from '../../../../shared/utils/toast'; +import { validateFileType, validateFileSize, formatFileSize } from '../../../../shared/utils/fileUpload'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +/** + * ImageData Interface + * + * Purpose: Defines the structure for image data + */ +interface ImageData { + uri: string; + name: string; + type: string; + size?: number; +} + +// ============================================================================ +// DOCUMENT UPLOAD STEP COMPONENT +// ============================================================================ + +/** + * DocumentUploadStep Component + * + * Purpose: Fourth step of signup flow - document upload with image picker + * + * Features: + * - Camera and gallery image selection + * - Image preview with file details + * - Real-time file size and type display + * - Permission handling for camera + * - Modern UI with proper header alignment + * - Continue button with loading state + * - Back navigation + */ +const DocumentUploadStep: React.FC = ({ + onContinue, + onBack, + data, + isLoading, +}) => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + const [selectedImage, setSelectedImage] = useState( + data.id_photo_url ? { + uri: data.id_photo_url, + name: 'uploaded_document.jpg', + type: 'image/jpeg', + } : null + ); + + // ============================================================================ + // PERMISSION HANDLERS + // ============================================================================ + + /** + * Request Camera Permission + * + * Purpose: Request camera permission for Android devices + * + * @returns Promise - Whether permission was granted + */ + const requestCameraPermission = async (): Promise => { + 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('Camera permission error:', err); + return false; + } + } + return true; + }; + + // ============================================================================ + // IMAGE PICKER HANDLERS + // ============================================================================ + + /** + * Handle Image Picker Selection + * + * Purpose: Show options for camera or gallery selection + */ + const handleImagePicker = () => { + Alert.alert( + 'Select Image', + 'Choose how you want to upload your document', + [ + { + text: 'Camera', + onPress: () => handleCameraCapture(), + }, + { + text: 'Gallery', + onPress: () => handleGalleryPicker(), + }, + { + text: 'Cancel', + style: 'cancel', + }, + ] + ); + }; + + /** + * Handle Camera Capture + * + * Purpose: Launch camera to capture image + */ + const handleCameraCapture = async () => { + const hasPermission = await requestCameraPermission(); + + if (!hasPermission) { + showError('Permission Error', 'Camera permission is required to capture images.'); + return; + } + + const options = { + mediaType: 'photo' as MediaType, + 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 || `document_${Date.now()}.jpg`, + type: asset.type || 'image/jpeg', + size: asset.fileSize, + }; + + // Validate file type and size + if (!validateFileType(imageData)) { + showError('Invalid File Type', 'Please select a JPEG, JPG, or PNG image.'); + return; + } + + if (!validateFileSize(imageData, 10)) { + showError('File Too Large', 'Please select an image under 10MB.'); + return; + } + + setSelectedImage(imageData); + showSuccess('Success', 'Document captured successfully!'); + } + }); + }; + + /** + * Handle Gallery Picker + * + * Purpose: Launch image library to select image + */ + const handleGalleryPicker = () => { + const options = { + mediaType: 'photo' as MediaType, + 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 || `document_${Date.now()}.jpg`, + type: asset.type || 'image/jpeg', + size: asset.fileSize, + }; + + // Validate file type and size + if (!validateFileType(imageData)) { + showError('Invalid File Type', 'Please select a JPEG, JPG, or PNG image.'); + return; + } + + if (!validateFileSize(imageData, 10)) { + showError('File Too Large', 'Please select an image under 10MB.'); + return; + } + + setSelectedImage(imageData); + showSuccess('Success', 'Document selected from gallery!'); + } + }); + }; + + // ============================================================================ + // UTILITY FUNCTIONS + // ============================================================================ + + + + /** + * Get File Type Display + * + * Purpose: Get display name for file type + * + * @param type - MIME type + * @returns Display name for file type + */ + 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'; + }; + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Handle Continue + * + * Purpose: Proceed to next step with selected image + */ + const handleContinue = () => { + if (!selectedImage) { + showError('Validation Error', 'Please upload a document to continue.'); + return; + } + + onContinue(selectedImage.uri); + }; + + /** + * Handle Remove Image + * + * Purpose: Remove selected image + */ + const handleRemoveImage = () => { + setSelectedImage(null); + }; + + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + + + {/* Header */} + + + + + + + Upload Document + Step 4 of 5 + + + + + + {/* Content */} + + Upload your ID document + + Please upload a clear photo of your hospital-issued ID for verification. + + + {/* Document Upload Area */} + + + {selectedImage ? ( + + + + + + Document Uploaded + {selectedImage.name} + + {getFileTypeDisplay(selectedImage.type)} + {selectedImage.size && ( + โ€ข {formatFileSize(selectedImage.size)} + )} + + + ) : ( + <> + + Tap to upload document + JPG, PNG supported + + )} + + + + {selectedImage && ( + + Change Document + + )} + + {/* Continue Button */} + + + {isLoading ? 'Processing...' : 'Continue'} + + + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Scroll view + scrollView: { + flex: 1, + }, + + // Scroll content + scrollContent: { + flexGrow: 1, + paddingHorizontal: theme.spacing.sm, + }, + + // Header section + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingTop: theme.spacing.xl, + paddingBottom: theme.spacing.lg, + marginBottom: theme.spacing.lg, + }, + + // Back button + backButton: { + padding: theme.spacing.sm, + borderRadius: theme.borderRadius.medium, + backgroundColor: theme.colors.backgroundAlt, + }, + + // Header content + headerContent: { + flex: 1, + alignItems: 'center', + }, + + // Header spacer + headerSpacer: { + width: 40, + }, + + // Title + title: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + // Subtitle + subtitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + }, + + // Content section + content: { + flex: 1, + justifyContent: 'center', + paddingBottom: theme.spacing.xl, + }, + + // Section title + sectionTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.sm, + }, + + // Description + description: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xl, + }, + + // Upload container + uploadContainer: { + backgroundColor: theme.colors.backgroundAlt, + borderWidth: 2, + borderColor: theme.colors.border, + borderStyle: 'dashed', + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.xl, + marginBottom: theme.spacing.md, + minHeight: 200, + }, + + // Upload content + uploadContent: { + alignItems: 'center', + }, + + // Image preview container + imagePreviewContainer: { + alignItems: 'center', + width: '100%', + }, + + // Image preview + imagePreview: { + width: '100%', + height: 150, + borderRadius: theme.borderRadius.medium, + marginBottom: theme.spacing.sm, + resizeMode: 'contain', + }, + + // Image overlay (remove button) + imageOverlay: { + position: 'absolute', + top: 0, + right: 20, + backgroundColor: 'rgba(0, 0, 0, 0.4)', + borderRadius: 17, + width: 34, + height: 34, + justifyContent: 'center', + alignItems: 'center', + }, + + // Upload text + uploadText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + marginTop: theme.spacing.sm, + }, + + // Upload subtext + uploadSubtext: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginTop: theme.spacing.xs, + }, + + // Uploaded text + uploadedText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.success, + marginTop: theme.spacing.sm, + }, + + // File name + fileName: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginTop: theme.spacing.xs, + textAlign: 'center', + maxWidth: '80%', + }, + + // File details + fileDetails: { + flexDirection: 'row', + alignItems: 'center', + marginTop: theme.spacing.xs, + }, + + // File type + fileType: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + }, + + // File size + fileSize: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + }, + + // Change button + changeButton: { + alignSelf: 'center', + marginBottom: theme.spacing.xl, + }, + + // Change button text + changeButtonText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.primary, + }, + + // Continue button + continueButton: { + backgroundColor: theme.colors.primary, + borderRadius: theme.borderRadius.medium, + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + alignItems: 'center', + marginBottom: theme.spacing.lg, + ...theme.shadows.primary, + }, + + // Continue button disabled + continueButtonDisabled: { + backgroundColor: theme.colors.border, + opacity: 0.6, + }, + + // Continue button text + continueButtonText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + }, + + // Continue button text disabled + continueButtonTextDisabled: { + color: theme.colors.textMuted, + }, +}); + +export default DocumentUploadStep; + +/* + * End of File: DocumentUploadStep.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/components/signup/EmailAlreadyRegisteredModal.tsx b/app/modules/Auth/components/signup/EmailAlreadyRegisteredModal.tsx new file mode 100644 index 0000000..5786f7d --- /dev/null +++ b/app/modules/Auth/components/signup/EmailAlreadyRegisteredModal.tsx @@ -0,0 +1,231 @@ +/* + * File: EmailAlreadyRegisteredModal.tsx + * Description: Modal for when email is already registered + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + Modal, + Alert, +} from 'react-native'; +import { theme } from '../../../../theme/theme'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface EmailAlreadyRegisteredModalProps { + visible: boolean; + onClose: () => void; + onGoToLogin: () => void; +} + +// ============================================================================ +// EMAIL ALREADY REGISTERED MODAL COMPONENT +// ============================================================================ + +/** + * EmailAlreadyRegisteredModal Component + * + * Purpose: Modal shown when user tries to register with an existing email + * + * Features: + * - Informative message about existing email + * - Option to go to login + * - Option to close and try different email + */ +const EmailAlreadyRegisteredModal: React.FC = ({ + visible, + onClose, + onGoToLogin, +}) => { + // ============================================================================ + // HANDLERS + // ============================================================================ + + /** + * Handle Go To Login + * + * Purpose: Navigate to login screen + */ + const handleGoToLogin = () => { + onClose(); + onGoToLogin(); + }; + + /** + * Handle Try Different Email + * + * Purpose: Close modal and allow user to try different email + */ + const handleTryDifferentEmail = () => { + onClose(); + }; + + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + + + + {/* Header */} + + Email Already Registered + + This email address is already associated with an account. + + + + {/* Content */} + + + It looks like you already have an account with us. You can either: + + + + + โ€ข Sign in to your existing account + + + โ€ข Try a different email address + + + + + {/* Actions */} + + + Try Different Email + + + + Go to Login + + + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: theme.spacing.lg, + }, + modalContainer: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.large, + padding: theme.spacing.xl, + width: '100%', + maxWidth: 400, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 8, + }, + header: { + alignItems: 'center', + marginBottom: theme.spacing.lg, + }, + title: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.sm, + textAlign: 'center', + }, + subtitle: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textSecondary, + textAlign: 'center', + }, + content: { + marginBottom: theme.spacing.xl, + }, + message: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.md, + fontFamily: theme.typography.fontFamily.regular, + }, + optionsContainer: { + marginLeft: theme.spacing.sm, + }, + optionText: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xs, + fontFamily: theme.typography.fontFamily.regular, + }, + actions: { + flexDirection: 'column', // Changed from 'row' to 'column' + gap: theme.spacing.md, + }, + secondaryButton: { + backgroundColor: theme.colors.background, + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.medium, + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + alignItems: 'center', + }, + secondaryButtonText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + }, + primaryButton: { + backgroundColor: theme.colors.primary, + borderRadius: theme.borderRadius.medium, + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + alignItems: 'center', + shadowColor: theme.colors.primary, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 3, + }, + primaryButtonText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + }, +}); + +export default EmailAlreadyRegisteredModal; + +/* + * End of File: EmailAlreadyRegisteredModal.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/components/signup/EmailStep.tsx b/app/modules/Auth/components/signup/EmailStep.tsx new file mode 100644 index 0000000..b54af3f --- /dev/null +++ b/app/modules/Auth/components/signup/EmailStep.tsx @@ -0,0 +1,370 @@ +/* + * File: EmailStep.tsx + * Description: Email step component for signup flow + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState } from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + StyleSheet, + Alert, + KeyboardAvoidingView, + Platform, + ScrollView, +} from 'react-native'; +import { theme } from '../../../../theme/theme'; +import { EmailStepProps } from '../../types/signup'; +import Icon from 'react-native-vector-icons/Feather'; + +// ============================================================================ +// EMAIL STEP COMPONENT +// ============================================================================ + +/** + * EmailStep Component + * + * Purpose: First step of signup flow - email validation + * + * Features: + * - Email input with validation + * - Real-time email format checking + * - Continue button with loading state + * - Back navigation + * - Modern header with proper alignment + */ +const EmailStep: React.FC = ({ + onContinue, + onBack, + data, + isLoading, +}) => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + const [email, setEmail] = useState(data.email || ''); + const [emailError, setEmailError] = useState(''); + + // ============================================================================ + // VALIDATION FUNCTIONS + // ============================================================================ + + /** + * Validate Email Format + * + * Purpose: Check if email format is valid + */ + const validateEmailFormat = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + /** + * Handle Email Change + * + * Purpose: Update email and clear errors + */ + const handleEmailChange = (text: string) => { + setEmail(text); + setEmailError(''); + }; + + /** + * Handle Continue + * + * Purpose: Validate email and proceed to next step + */ + const handleContinue = () => { + // Clear previous errors + setEmailError(''); + + // Validate email format + if (!email.trim()) { + setEmailError('Email is required'); + return; + } + + if (!validateEmailFormat(email.trim())) { + setEmailError('Please enter a valid email address'); + return; + } + + // Call parent handler + onContinue(email.trim()); + }; + + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + + + {/* Header */} + + + + + + + Create Account + Step 1 of 5 + + + + + + {/* Content */} + + Enter your email address + + We'll use this email to create your account and send you important updates. + + + {/* Email Input */} + + Email Address + + {emailError ? ( + {emailError} + ) : null} + + + {/* Continue Button */} + + + {isLoading ? 'Validating...' : 'Continue'} + + + + {/* Additional Info */} + + + By continuing, you agree to our Terms of Service and Privacy Policy. + + + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Scroll view + scrollView: { + flex: 1, + }, + + // Scroll content + scrollContent: { + flexGrow: 1, + paddingHorizontal: theme.spacing.sm, + }, + + // Header section + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingTop: theme.spacing.xl, + paddingBottom: theme.spacing.lg, + marginBottom: theme.spacing.lg, + }, + + // Back button + backButton: { + padding: theme.spacing.sm, + borderRadius: theme.borderRadius.medium, + backgroundColor: theme.colors.backgroundAlt, + }, + + // Header content + headerContent: { + flex: 1, + alignItems: 'center', + }, + + // Header spacer + headerSpacer: { + width: 40, + }, + + // Title + title: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + // Subtitle + subtitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + }, + + // Content section + content: { + flex: 1, + justifyContent: 'center', + paddingBottom: theme.spacing.xl, + }, + + // Section title + sectionTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.sm, + }, + + // Description + description: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xl, + }, + + // Input container + inputContainer: { + marginBottom: theme.spacing.xl, + }, + + // Input label + inputLabel: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.sm, + }, + + // Input field + input: { + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.md, + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textPrimary, + backgroundColor: theme.colors.background, + }, + + // Input error state + inputError: { + borderColor: theme.colors.error, + }, + + // Error text + errorText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.error, + marginTop: theme.spacing.xs, + }, + + // Continue button + continueButton: { + backgroundColor: theme.colors.primary, + borderRadius: theme.borderRadius.medium, + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + alignItems: 'center', + marginBottom: theme.spacing.lg, + ...theme.shadows.primary, + }, + + // Continue button disabled + continueButtonDisabled: { + backgroundColor: theme.colors.border, + opacity: 0.6, + }, + + // Continue button text + continueButtonText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + }, + + // Continue button text disabled + continueButtonTextDisabled: { + color: theme.colors.textMuted, + }, + + // Info container + infoContainer: { + alignItems: 'center', + }, + + // Info text + infoText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + textAlign: 'center', + }, +}); + +export default EmailStep; + +/* + * End of File: EmailStep.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/components/signup/HospitalSelectionStep.tsx b/app/modules/Auth/components/signup/HospitalSelectionStep.tsx new file mode 100644 index 0000000..5f40016 --- /dev/null +++ b/app/modules/Auth/components/signup/HospitalSelectionStep.tsx @@ -0,0 +1,592 @@ +/* + * File: HospitalSelectionStep.tsx + * Description: Hospital selection step component for signup flow + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState, useMemo } from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + ScrollView, + FlatList, + ActivityIndicator, + TextInput, + KeyboardAvoidingView, + Platform, +} from 'react-native'; +import { theme } from '../../../../theme/theme'; +import { HospitalSelectionStepProps, Hospital } from '../../types/signup'; +import Icon from 'react-native-vector-icons/Feather'; + +// ============================================================================ +// HOSPITAL SELECTION STEP COMPONENT +// ============================================================================ + +/** + * HospitalSelectionStep Component + * + * Purpose: Fifth step of signup flow - hospital selection + * + * Features: + * - Hospital list display from Redux state + * - Hospital selection with visual feedback + * - Search functionality for hospitals + * - Loading states and error handling + * - Continue button with validation (sticky bottom) + * - Back navigation with modern header + * - Scrollable hospital list + */ +const HospitalSelectionStep: React.FC = ({ + onContinue, + onBack, + data, + isLoading, + hospitals, + hospitalLoading, +}) => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + const [selectedHospitalId, setSelectedHospitalId] = useState(data.hospital_id || ''); + const [searchQuery, setSearchQuery] = useState(''); + + // ============================================================================ + // COMPUTED VALUES + // ============================================================================ + + /** + * Filtered Hospitals + * + * Purpose: Filter hospitals based on search query + */ + const filteredHospitals = useMemo(() => { + if (!hospitals) return []; + + if (!searchQuery.trim()) return hospitals; + + const query = searchQuery.toLowerCase(); + return hospitals.filter(hospital => + hospital.hospital_name?.toLowerCase().includes(query) + ); + }, [hospitals, searchQuery]); + + // ============================================================================ + // HANDLERS + // ============================================================================ + + /** + * Handle Hospital Selection + * + * Purpose: Select a hospital + */ + const handleHospitalSelect = (hospitalId: string) => { + setSelectedHospitalId(hospitalId); + }; + + /** + * Handle Continue + * + * Purpose: Validate selection and proceed to next step + */ + const handleContinue = () => { + if (!selectedHospitalId) { + // Show error or alert + return; + } + onContinue(selectedHospitalId); + }; + + /** + * Handle Search Input Change + * + * Purpose: Update search query + */ + const handleSearchChange = (text: string) => { + setSearchQuery(text); + }; + + /** + * Clear Search + * + * Purpose: Clear search query + */ + const handleClearSearch = () => { + setSearchQuery(''); + }; + + // ============================================================================ + // RENDER FUNCTIONS + // ============================================================================ + + /** + * Render Hospital Item + * + * Purpose: Render individual hospital item + */ + const renderHospitalItem = ({ item }: { item: Hospital }) => { + const isSelected = selectedHospitalId === item.hospital_id; + + return ( + handleHospitalSelect(item.hospital_id || '')} + disabled={isLoading} + > + + + + {item.hospital_name || 'Unknown Hospital'} + + + + {isSelected && ( + + + + )} + + + ); + }; + + /** + * Render Search Input + * + * Purpose: Render search input field + */ + const renderSearchInput = () => ( + + + + + {searchQuery.length > 0 && ( + + + + )} + + + ); + + /** + * Render Loading State + * + * Purpose: Show loading indicator while fetching hospitals + */ + const renderLoadingState = () => ( + + + Loading hospitals... + + ); + + /** + * Render Empty State + * + * Purpose: Show message when no hospitals are available + */ + const renderEmptyState = () => ( + + + + {searchQuery ? 'No Hospitals Found' : 'No Hospitals Available'} + + + {searchQuery + ? 'Try adjusting your search terms or browse all hospitals.' + : 'There are no hospitals available at the moment. Please try again later.' + } + + + ); + + /** + * Render Search Results Info + * + * Purpose: Show search results count + */ + const renderSearchResultsInfo = () => { + if (!searchQuery.trim()) return null; + + return ( + + + {filteredHospitals.length} hospital{filteredHospitals.length !== 1 ? 's' : ''} found + + + ); + }; + + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + + {/* Header */} + + + + + + + Create Account + Step 5 of 5 + + + + + + {/* Content */} + + Select Your Hospital + + Choose the hospital where you work or will be practicing. + + + {/* Search Input */} + {renderSearchInput()} + + {/* Search Results Info */} + {renderSearchResultsInfo()} + + {/* Hospital List */} + + {hospitalLoading ? ( + renderLoadingState() + ) : filteredHospitals.length > 0 ? ( + item.hospital_id || ''} + showsVerticalScrollIndicator={true} + contentContainerStyle={styles.hospitalList} + keyboardShouldPersistTaps="handled" + /> + ) : ( + renderEmptyState() + )} + + + + {/* Sticky Continue Button */} + + + + {isLoading ? 'Creating Account...' : 'Create Account'} + + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Header section + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingTop: theme.spacing.xl, + paddingBottom: theme.spacing.lg, + paddingHorizontal: theme.spacing.lg, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + + // Back button + backButton: { + padding: theme.spacing.sm, + borderRadius: theme.borderRadius.medium, + backgroundColor: theme.colors.backgroundAlt, + }, + + // Header content + headerContent: { + flex: 1, + alignItems: 'center', + }, + + // Header spacer + headerSpacer: { + width: 40, + }, + + // Title + title: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + // Subtitle + subtitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + }, + + // Content section + content: { + flex: 1, + paddingHorizontal: theme.spacing.lg, + paddingTop: theme.spacing.lg, + }, + + // Section title + sectionTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.sm, + }, + + // Description + description: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.lg, + }, + + // Search container + searchContainer: { + marginBottom: theme.spacing.lg, + }, + + // Search input wrapper + searchInputWrapper: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.backgroundAlt, + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + }, + + // Search icon + searchIcon: { + marginRight: theme.spacing.sm, + }, + + // Search input + searchInput: { + flex: 1, + paddingVertical: theme.spacing.md, + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textPrimary, + }, + + // Clear button + clearButton: { + padding: theme.spacing.xs, + marginLeft: theme.spacing.sm, + }, + + // Search results info + searchResultsInfo: { + marginBottom: theme.spacing.md, + }, + + // Search results text + searchResultsText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textSecondary, + }, + + // Hospital list container + hospitalListContainer: { + flex: 1, + }, + + // Hospital list + hospitalList: { + paddingBottom: theme.spacing.lg, + }, + + // Hospital item + hospitalItem: { + backgroundColor: theme.colors.background, + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.md, + marginBottom: theme.spacing.sm, + }, + + // Hospital item selected + hospitalItemSelected: { + borderColor: theme.colors.primary, + backgroundColor: theme.colors.tertiary, + }, + + // Hospital content + hospitalContent: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + + // Hospital info + hospitalInfo: { + flex: 1, + }, + + // Hospital name + hospitalName: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + }, + + // Hospital name selected + hospitalNameSelected: { + color: theme.colors.primary, + fontFamily: theme.typography.fontFamily.bold, + }, + + // Selected icon + selectedIcon: { + width: 24, + height: 24, + borderRadius: 12, + backgroundColor: theme.colors.primary, + alignItems: 'center', + justifyContent: 'center', + }, + + // Loading container + loadingContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: theme.spacing.xxl, + }, + + // Loading text + loadingText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginTop: theme.spacing.md, + }, + + // Empty container + emptyContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: theme.spacing.xxl, + }, + + // Empty title + emptyTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginTop: theme.spacing.md, + marginBottom: theme.spacing.sm, + }, + + // Empty text + emptyText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + textAlign: 'center', + paddingHorizontal: theme.spacing.lg, + }, + + // Sticky button container + stickyButtonContainer: { + paddingHorizontal: theme.spacing.lg, + paddingVertical: theme.spacing.lg, + backgroundColor: theme.colors.background, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + + // Continue button + continueButton: { + backgroundColor: theme.colors.primary, + borderRadius: theme.borderRadius.medium, + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + alignItems: 'center', + ...theme.shadows.primary, + }, + + // Continue button disabled + continueButtonDisabled: { + backgroundColor: theme.colors.border, + opacity: 0.6, + }, + + // Continue button text + continueButtonText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + }, + + // Continue button text disabled + continueButtonTextDisabled: { + color: theme.colors.textMuted, + }, +}); + +export default HospitalSelectionStep; + +/* + * End of File: HospitalSelectionStep.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/components/signup/NameStep.tsx b/app/modules/Auth/components/signup/NameStep.tsx new file mode 100644 index 0000000..11e74f4 --- /dev/null +++ b/app/modules/Auth/components/signup/NameStep.tsx @@ -0,0 +1,399 @@ +/* + * File: NameStep.tsx + * Description: Name step component for signup flow + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState } from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + StyleSheet, + KeyboardAvoidingView, + Platform, + ScrollView, +} from 'react-native'; +import { theme } from '../../../../theme/theme'; +import { NameStepProps } from '../../types/signup'; +import Icon from 'react-native-vector-icons/Feather'; + +// ============================================================================ +// NAME STEP COMPONENT +// ============================================================================ + +/** + * NameStep Component + * + * Purpose: Third step of signup flow - personal information + * + * Features: + * - First name, last name, and username inputs + * - Real-time validation with error handling + * - Continue button with loading state + * - Back navigation with modern header + */ +const NameStep: React.FC = ({ + onContinue, + onBack, + data, + isLoading, +}) => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + const [firstName, setFirstName] = useState(data.first_name || ''); + const [lastName, setLastName] = useState(data.last_name || ''); + const [username, setUsername] = useState(data.username || ''); + const [errors, setErrors] = useState({ + firstName: '', + lastName: '', + username: '', + }); + + // ============================================================================ + // VALIDATION FUNCTIONS + // ============================================================================ + + /** + * Validate Input Fields + * + * Purpose: Check if all fields are valid + */ + const validateFields = (): boolean => { + const newErrors = { + firstName: '', + lastName: '', + username: '', + }; + + if (!firstName.trim()) { + newErrors.firstName = 'First name is required'; + } + + if (!lastName.trim()) { + newErrors.lastName = 'Last name is required'; + } + + if (!username.trim()) { + newErrors.username = 'Username is required'; + } else if (username.length < 3) { + newErrors.username = 'Username must be at least 3 characters'; + } + + setErrors(newErrors); + return !Object.values(newErrors).some(error => error !== ''); + }; + + /** + * Handle Continue + * + * Purpose: Validate fields and proceed to next step + */ + const handleContinue = () => { + if (validateFields()) { + onContinue(firstName.trim(), lastName.trim(), username.trim()); + } + }; + + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + + + {/* Header */} + + + + + + + Create Account + Step 3 of 5 + + + + + + {/* Content */} + + Tell us about yourself + + Please provide your name and choose a username for your account. + + + {/* First Name Input */} + + First Name + { + setFirstName(text); + setErrors(prev => ({ ...prev, firstName: '' })); + }} + autoCapitalize="words" + autoCorrect={false} + editable={!isLoading} + /> + {errors.firstName ? ( + {errors.firstName} + ) : null} + + + {/* Last Name Input */} + + Last Name + { + setLastName(text); + setErrors(prev => ({ ...prev, lastName: '' })); + }} + autoCapitalize="words" + autoCorrect={false} + editable={!isLoading} + /> + {errors.lastName ? ( + {errors.lastName} + ) : null} + + + {/* Username Input */} + + Username + { + setUsername(text); + setErrors(prev => ({ ...prev, username: '' })); + }} + autoCapitalize="none" + autoCorrect={false} + editable={!isLoading} + /> + {errors.username ? ( + {errors.username} + ) : null} + + + {/* Continue Button */} + + + {isLoading ? 'Validating...' : 'Continue'} + + + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Scroll view + scrollView: { + flex: 1, + }, + + // Scroll content + scrollContent: { + flexGrow: 1, + paddingHorizontal: theme.spacing.sm, + }, + + // Header section + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingTop: theme.spacing.xl, + paddingBottom: theme.spacing.lg, + marginBottom: theme.spacing.lg, + }, + + // Back button + backButton: { + padding: theme.spacing.sm, + borderRadius: theme.borderRadius.medium, + backgroundColor: theme.colors.backgroundAlt, + }, + + // Header content + headerContent: { + flex: 1, + alignItems: 'center', + }, + + // Header spacer + headerSpacer: { + width: 40, + }, + + // Title + title: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + // Subtitle + subtitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + }, + + // Content section + content: { + flex: 1, + justifyContent: 'center', + paddingBottom: theme.spacing.xl, + }, + + // Section title + sectionTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.sm, + }, + + // Description + description: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xl, + }, + + // Input container + inputContainer: { + marginBottom: theme.spacing.xl, + }, + + // Input label + inputLabel: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.sm, + }, + + // Input field + input: { + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.md, + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textPrimary, + backgroundColor: theme.colors.background, + }, + + // Input error state + inputError: { + borderColor: theme.colors.error, + }, + + // Error text + errorText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.error, + marginTop: theme.spacing.xs, + }, + + // Continue button + continueButton: { + backgroundColor: theme.colors.primary, + borderRadius: theme.borderRadius.medium, + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + alignItems: 'center', + marginBottom: theme.spacing.lg, + ...theme.shadows.primary, + }, + + // Continue button disabled + continueButtonDisabled: { + backgroundColor: theme.colors.border, + opacity: 0.6, + }, + + // Continue button text + continueButtonText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + }, + + // Continue button text disabled + continueButtonTextDisabled: { + color: theme.colors.textMuted, + }, +}); + +export default NameStep; + +/* + * End of File: NameStep.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/components/signup/PasswordStep.tsx b/app/modules/Auth/components/signup/PasswordStep.tsx new file mode 100644 index 0000000..da77d96 --- /dev/null +++ b/app/modules/Auth/components/signup/PasswordStep.tsx @@ -0,0 +1,628 @@ +/* + * File: PasswordStep.tsx + * Description: Password step component for signup flow with comprehensive validation + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + StyleSheet, + KeyboardAvoidingView, + Platform, + ScrollView, +} from 'react-native'; +import { theme } from '../../../../theme/theme'; +import { PasswordStepProps } from '../../types/signup'; +import Icon from 'react-native-vector-icons/Feather'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +/** + * PasswordRule Interface + * + * Purpose: Defines the structure for password validation rules + */ +interface PasswordRule { + id: string; + label: string; + validator: (password: string) => boolean; + isValid: boolean; +} + +// ============================================================================ +// PASSWORD STEP COMPONENT +// ============================================================================ + +/** + * PasswordStep Component + * + * Purpose: Second step of signup flow - password creation with comprehensive validation + * + * Features: + * - Password input with visibility toggle + * - Comprehensive password validation rules + * - Real-time password strength checking + * - Visual feedback for each requirement + * - Continue button with loading state + * - Back navigation with modern header + */ +const PasswordStep: React.FC = ({ + onContinue, + onBack, + data, + isLoading, +}) => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + const [password, setPassword] = useState(data.password || ''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [passwordError, setPasswordError] = useState(''); + const [confirmPasswordError, setConfirmPasswordError] = useState(''); + const [isPasswordVisible, setPasswordVisible] = useState(false); + const [isConfirmPasswordVisible, setConfirmPasswordVisible] = useState(false); + + // Password validation rules + const [passwordRules, setPasswordRules] = useState([ + { + id: 'length', + label: 'At least 8 characters', + validator: (pwd: string) => pwd.length >= 8, + isValid: false, + }, + { + id: 'uppercase', + label: 'One uppercase letter', + validator: (pwd: string) => /[A-Z]/.test(pwd), + isValid: false, + }, + { + id: 'lowercase', + label: 'One lowercase letter', + validator: (pwd: string) => /[a-z]/.test(pwd), + isValid: false, + }, + { + id: 'number', + label: 'One number', + validator: (pwd: string) => /\d/.test(pwd), + isValid: false, + }, + { + id: 'special', + label: 'One special character', + validator: (pwd: string) => /[!@#$%^&*(),.?":{}|<>]/.test(pwd), + isValid: false, + }, + { + id: 'match', + label: 'Passwords match', + validator: (pwd: string) => pwd === confirmPassword && confirmPassword.length > 0, + isValid: false, + }, + ]); + + // ============================================================================ + // EFFECTS + // ============================================================================ + + /** + * useEffect for password validation + * + * Purpose: Update password rules when password or confirm password changes + */ + useEffect(() => { + updatePasswordRules(password); + }, [password, confirmPassword]); + + // ============================================================================ + // VALIDATION FUNCTIONS + // ============================================================================ + + /** + * Update Password Rules + * + * Purpose: Update password validation rules based on current password and confirm password + * + * @param pwd - Current password value + */ + const updatePasswordRules = (pwd: string) => { + setPasswordRules(prevRules => + prevRules.map(rule => { + if (rule.id === 'match') { + return { + ...rule, + isValid: pwd === confirmPassword && confirmPassword.length > 0, + }; + } + return { + ...rule, + isValid: rule.validator(pwd), + }; + }) + ); + }; + + /** + * Validate Password + * + * Purpose: Check if all password requirements are met + * + * @param pwd - Password to validate + * @returns boolean indicating if password meets all requirements + */ + const validatePassword = (pwd: string): boolean => { + return passwordRules.every(rule => rule.isValid); + }; + + /** + * Handle Password Change + * + * Purpose: Update password and clear errors + */ + const handlePasswordChange = (text: string) => { + setPassword(text); + setPasswordError(''); + // Clear confirm password error if passwords now match + if (confirmPassword && text === confirmPassword) { + setConfirmPasswordError(''); + } + // Update password rules to reflect the match status + updatePasswordRules(text); + }; + + /** + * Handle Confirm Password Change + * + * Purpose: Update confirm password and validate match + */ + const handleConfirmPasswordChange = (text: string) => { + setConfirmPassword(text); + setConfirmPasswordError(''); + + // Check if passwords match + if (text && text !== password) { + setConfirmPasswordError('Passwords do not match'); + } + + // Update password rules to reflect the match status + updatePasswordRules(password); + }; + + /** + * Handle Continue + * + * Purpose: Validate password and proceed to next step + */ + const handleContinue = () => { + // Clear previous errors + setPasswordError(''); + setConfirmPasswordError(''); + + // Validate password + if (!password.trim()) { + setPasswordError('Password is required'); + return; + } + + if (!validatePassword(password.trim())) { + setPasswordError('Please meet all password requirements'); + return; + } + + // Validate confirm password + if (!confirmPassword.trim()) { + setConfirmPasswordError('Please confirm your password'); + return; + } + + if (password.trim() !== confirmPassword.trim()) { + setConfirmPasswordError('Passwords do not match'); + return; + } + + // Call parent handler + onContinue(password.trim()); + }; + + /** + * Render Password Rule + * + * Purpose: Render individual password validation rule + * + * @param rule - Password rule to render + * @returns JSX element for the rule + */ + const renderPasswordRule = (rule: PasswordRule) => ( + + + {rule.isValid && ( + + )} + + + {rule.label} + + + ); + + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + + + {/* Header */} + + + + + + + Create Account + Step 2 of 5 + + + + + + {/* Content */} + + Create a strong password + + Choose a password that meets all the security requirements below. + + + {/* Password Input */} + + Password + + + setPasswordVisible(!isPasswordVisible)} + style={styles.eyeIcon} + disabled={isLoading} + > + + + + {passwordError ? ( + {passwordError} + ) : null} + + + {/* Confirm Password Input */} + + Confirm Password + + + setConfirmPasswordVisible(!isConfirmPasswordVisible)} + style={styles.eyeIcon} + disabled={isLoading} + > + + + + {confirmPasswordError ? ( + {confirmPasswordError} + ) : null} + + + {/* Password Requirements */} + + Password Requirements: + + {passwordRules.map(renderPasswordRule)} + + + + {/* Continue Button */} + + + {isLoading ? 'Processing...' : 'Continue'} + + + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Scroll view + scrollView: { + flex: 1, + }, + + // Scroll content + scrollContent: { + flexGrow: 1, + paddingHorizontal: theme.spacing.sm, + }, + + // Header section + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingTop: theme.spacing.xl, + paddingBottom: theme.spacing.lg, + marginBottom: theme.spacing.lg, + }, + + // Back button + backButton: { + padding: theme.spacing.sm, + borderRadius: theme.borderRadius.medium, + backgroundColor: theme.colors.backgroundAlt, + }, + + // Header content + headerContent: { + flex: 1, + alignItems: 'center', + }, + + // Header spacer + headerSpacer: { + width: 40, + }, + + // Title + title: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + // Subtitle + subtitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + }, + + // Content section + content: { + flex: 1, + justifyContent: 'center', + paddingBottom: theme.spacing.xl, + }, + + // Section title + sectionTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.sm, + }, + + // Description + description: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xl, + }, + + // Input container + inputContainer: { + marginBottom: theme.spacing.xl, + }, + + // Input label + inputLabel: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.sm, + }, + + // Input wrapper + inputWrapper: { + flexDirection: 'row', + alignItems: 'center', + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.medium, + backgroundColor: theme.colors.background, + }, + + // Input field + input: { + flex: 1, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.md, + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textPrimary, + }, + + // Eye icon + eyeIcon: { + padding: theme.spacing.sm, + }, + + // Input wrapper error state + inputWrapperError: { + borderColor: theme.colors.error, + }, + + // Error text + errorText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.error, + marginTop: theme.spacing.xs, + }, + + // Requirements container + requirementsContainer: { + marginBottom: theme.spacing.xl, + padding: theme.spacing.md, + backgroundColor: theme.colors.backgroundAlt, + borderRadius: theme.borderRadius.medium, + }, + + // Requirements title + requirementsTitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.sm, + }, + + // Rules grid + rulesGrid: { + gap: theme.spacing.xs, + }, + + // Rule container + ruleContainer: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.xs, + }, + + // Checkbox + checkbox: { + width: 18, + height: 18, + borderRadius: 4, + borderWidth: 2, + borderColor: theme.colors.border, + backgroundColor: theme.colors.backgroundAlt, + marginRight: theme.spacing.sm, + alignItems: 'center', + justifyContent: 'center', + }, + + // Checkbox checked + checkboxChecked: { + backgroundColor: theme.colors.primary, + borderColor: theme.colors.primary, + }, + + // Rule text + ruleText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + flex: 1, + }, + + // Rule text valid + ruleTextValid: { + color: theme.colors.success, + }, + + // Continue button + continueButton: { + backgroundColor: theme.colors.primary, + borderRadius: theme.borderRadius.medium, + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + alignItems: 'center', + marginBottom: theme.spacing.lg, + ...theme.shadows.primary, + }, + + // Continue button disabled + continueButtonDisabled: { + backgroundColor: theme.colors.border, + opacity: 0.6, + }, + + // Continue button text + continueButtonText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + }, + + // Continue button text disabled + continueButtonTextDisabled: { + color: theme.colors.textMuted, + }, +}); + +export default PasswordStep; + +/* + * End of File: PasswordStep.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/components/signup/index.ts b/app/modules/Auth/components/signup/index.ts new file mode 100644 index 0000000..124a8ce --- /dev/null +++ b/app/modules/Auth/components/signup/index.ts @@ -0,0 +1,20 @@ +/* + * File: index.ts + * Description: Barrel exports for signup components + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// Export all signup step components +export { default as EmailStep } from './EmailStep'; +export { default as PasswordStep } from './PasswordStep'; +export { default as NameStep } from './NameStep'; +export { default as DocumentUploadStep } from './DocumentUploadStep'; +export { default as HospitalSelectionStep } from './HospitalSelectionStep'; +export { default as EmailAlreadyRegisteredModal } from './EmailAlreadyRegisteredModal'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/index.ts b/app/modules/Auth/index.ts new file mode 100644 index 0000000..92afd0f --- /dev/null +++ b/app/modules/Auth/index.ts @@ -0,0 +1,79 @@ +/* + * File: index.ts + * Description: Main exports for Auth module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// Export screens +export { default as LoginScreen } from './screens/LoginScreen'; +export { default as SignUpScreen } from './screens/SignUpScreen'; + +// Export navigation +export { + AuthStackNavigator, + AuthStackParamList, + AuthNavigationProp, + AuthScreenProps, + LoginScreenProps, + SignUpScreenProps, + navigateToLogin, + navigateToSignUp, + goBack, + resetToLogin, + resetToSignUp, + replaceWithLogin, + replaceWithSignUp, + navigateToSignUpAndClearStack, + navigateToLoginAndClearStack, +} from './navigation'; + +// Export signup components +export { + EmailStep, + PasswordStep, + NameStep, + DocumentUploadStep, + HospitalSelectionStep, + EmailAlreadyRegisteredModal, +} from './components/signup'; + +// Export services +export { authAPI } from './services/signupAPI'; + +// Export types +export type { + SignUpData, + SignUpStep, + EmailStepProps, + PasswordStepProps, + NameStepProps, + DocumentUploadStepProps, + HospitalSelectionStepProps, + EmailValidationApiResponse, + UsernameValidationApiResponse, + HospitalListApiResponse, + SignUpApiResponse, + Hospital, +} from './types/signup'; + +// Export Redux +export { + loginUser, + ssoLogin, + emergencyAccess, + logoutUser, + clearError, + setBiometricEnabled, + setRememberDevice, + updateUserProfile, + setSessionToken, + clearSession, + setEmergencyAccess, +} from './redux/authSlice'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/navigation/AuthStackNavigator.tsx b/app/modules/Auth/navigation/AuthStackNavigator.tsx new file mode 100644 index 0000000..3ef65c3 --- /dev/null +++ b/app/modules/Auth/navigation/AuthStackNavigator.tsx @@ -0,0 +1,101 @@ +/* + * File: AuthStackNavigator.tsx + * Description: Stack navigator for authentication screens within the Auth module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; + +// Import authentication screens +import LoginScreen from '../screens/LoginScreen'; +import SignUpScreen from '../screens/SignUpScreen'; + +// Import navigation types +import { AuthStackParamList } from './navigationTypes'; +import { theme } from '../../../theme'; + +// Create stack navigator for Auth module +const Stack = createStackNavigator(); + +/** + * AuthStackNavigator - Manages navigation between authentication screens + * + * This navigator handles the flow between: + * - LoginScreen: Main authentication screen + * - SignUpScreen: Multi-step registration process + * + * Features: + * - Clean header styling + * - Smooth transitions between screens + * - Type-safe navigation parameters + */ +const AuthStackNavigator: React.FC = () => { + return ( + + {/* Login Screen - Main authentication entry point */} + + + {/* Sign Up Screen - Multi-step registration process */} + + + ); +}; + +export default AuthStackNavigator; + +/* + * End of File: AuthStackNavigator.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/navigation/index.ts b/app/modules/Auth/navigation/index.ts new file mode 100644 index 0000000..64844b0 --- /dev/null +++ b/app/modules/Auth/navigation/index.ts @@ -0,0 +1,37 @@ +/* + * File: index.ts + * Description: Barrel exports for Auth module navigation + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// Export main navigator +export { default as AuthStackNavigator } from './AuthStackNavigator'; + +// Export navigation types +export type { + AuthStackParamList, + AuthNavigationProp, + AuthScreenProps, + LoginScreenProps, + SignUpScreenProps, +} from './navigationTypes'; + +// Export navigation utilities +export { + navigateToLogin, + navigateToSignUp, + goBack, + resetToLogin, + resetToSignUp, + replaceWithLogin, + replaceWithSignUp, + navigateToSignUpAndClearStack, + navigateToLoginAndClearStack, +} from './navigationUtils'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/navigation/navigationTypes.ts b/app/modules/Auth/navigation/navigationTypes.ts new file mode 100644 index 0000000..66a4574 --- /dev/null +++ b/app/modules/Auth/navigation/navigationTypes.ts @@ -0,0 +1,63 @@ +/* + * File: navigationTypes.ts + * Description: TypeScript types for Auth module navigation + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { StackNavigationProp } from '@react-navigation/stack'; + +/** + * AuthStackParamList - Defines the parameter list for Auth stack navigator + * + * This interface defines all the screens available in the Auth module + * and their associated navigation parameters. + */ +export type AuthStackParamList = { + // Login screen - Main authentication entry point + Login: undefined; + + // Sign Up screen - Multi-step registration process + SignUp: undefined; +}; + +/** + * AuthNavigationProp - Type for navigation prop in Auth screens + * + * This type provides type-safe navigation methods for screens + * within the Auth module. + */ +export type AuthNavigationProp = StackNavigationProp; + +/** + * AuthScreenProps - Base props interface for Auth screens + * + * This interface provides the common props that all Auth screens + * will receive, including navigation and route. + */ +export interface AuthScreenProps { + navigation: AuthNavigationProp; + route: { + key: string; + name: T; + params: AuthStackParamList[T]; + }; +} + +/** + * LoginScreenProps - Props for LoginScreen component + */ +export type LoginScreenProps = AuthScreenProps<'Login'>; + +/** + * SignUpScreenProps - Props for SignUpScreen component + */ +export type SignUpScreenProps = AuthScreenProps<'SignUp'>; + + + +/* + * End of File: navigationTypes.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/navigation/navigationUtils.ts b/app/modules/Auth/navigation/navigationUtils.ts new file mode 100644 index 0000000..8d62466 --- /dev/null +++ b/app/modules/Auth/navigation/navigationUtils.ts @@ -0,0 +1,107 @@ +/* + * File: navigationUtils.ts + * Description: Navigation utilities for Auth module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { AuthNavigationProp } from './navigationTypes'; + +/** + * AuthNavigationUtils - Utility functions for Auth module navigation + * + * This module provides helper functions for common navigation patterns + * within the Auth module, ensuring consistent navigation behavior. + */ + +/** + * Navigate to Login screen + * @param navigation - Navigation prop from React Navigation + */ +export const navigateToLogin = (navigation: AuthNavigationProp): void => { + navigation.navigate('Login'); +}; + +/** + * Navigate to Sign Up screen + * @param navigation - Navigation prop from React Navigation + */ +export const navigateToSignUp = (navigation: AuthNavigationProp): void => { + navigation.navigate('SignUp'); +}; + +/** + * Go back to previous screen + * @param navigation - Navigation prop from React Navigation + */ +export const goBack = (navigation: AuthNavigationProp): void => { + if (navigation.canGoBack()) { + navigation.goBack(); + } +}; + +/** + * Reset navigation stack to Login screen + * @param navigation - Navigation prop from React Navigation + */ +export const resetToLogin = (navigation: AuthNavigationProp): void => { + navigation.reset({ + index: 0, + routes: [{ name: 'Login' }], + }); +}; + +/** + * Reset navigation stack to Sign Up screen + * @param navigation - Navigation prop from React Navigation + */ +export const resetToSignUp = (navigation: AuthNavigationProp): void => { + navigation.reset({ + index: 0, + routes: [{ name: 'SignUp' }], + }); +}; + +/** + * Replace current screen with Login screen + * @param navigation - Navigation prop from React Navigation + */ +export const replaceWithLogin = (navigation: AuthNavigationProp): void => { + navigation.replace('Login'); +}; + +/** + * Replace current screen with Sign Up screen + * @param navigation - Navigation prop from React Navigation + */ +export const replaceWithSignUp = (navigation: AuthNavigationProp): void => { + navigation.replace('SignUp'); +}; + +/** + * Navigate to Sign Up screen and clear back stack + * @param navigation - Navigation prop from React Navigation + */ +export const navigateToSignUpAndClearStack = (navigation: AuthNavigationProp): void => { + navigation.reset({ + index: 0, + routes: [{ name: 'SignUp' }], + }); +}; + +/** + * Navigate to Login screen and clear back stack + * @param navigation - Navigation prop from React Navigation + */ +export const navigateToLoginAndClearStack = (navigation: AuthNavigationProp): void => { + navigation.reset({ + index: 0, + routes: [{ name: 'Login' }], + }); +}; + +/* + * End of File: navigationUtils.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/redux/authActions.ts b/app/modules/Auth/redux/authActions.ts new file mode 100644 index 0000000..d603953 --- /dev/null +++ b/app/modules/Auth/redux/authActions.ts @@ -0,0 +1,150 @@ +/* + * File: authActions.ts + * Description: Async actions (thunks) for Auth state + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { logout, updateUserProfile } from './authSlice'; +import { authAPI } from '../services/authAPI'; +import { showError, showSuccess, showWarning } from '../../../shared/utils/toast'; + +/** + * Thunk to login user + */ +export const login = createAsyncThunk( + 'auth/login', + async (credentials: { email: string; password: string }, { rejectWithValue }) => { + try { + 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) + return rejectWithValue(response.data.message); + } + + if(response.data.message && response.data.success){ + showSuccess(response.data.message) + } + + if (response.ok && response.data && response.data.data) { + // Return the user data for the fulfilled case + if(response.data.data.user.dashboard_role !=='radiologist'){ + showWarning('You are not authorized to access this application') + return rejectWithValue('Not Authorized'); + } + return {...response.data.data.user,access_token:response.data.data.access_token}; + } else { + const errorMessage = response.data?.message || response.problem || 'Unknown error'; + return rejectWithValue(errorMessage); + } + } catch (error: any) { + return rejectWithValue(error.message); + } + } +); + +/** + * Thunk to update user profile + */ +export const updateUserProfileAsync = createAsyncThunk( + 'auth/updateUserProfile', + async (profileData: { first_name: string; last_name: string }, { getState, rejectWithValue, dispatch }) => { + try { + const state = getState() as any; + const user = state.auth.user; + const token = user?.access_token; + + if (!user?.user_id || !token) { + return rejectWithValue('User not authenticated'); + } + + const response: any = await authAPI.updateUserProfile(user.user_id, profileData, token); + + if (response.ok && response.data) { + // Update local state + dispatch(updateUserProfile({ + first_name: profileData.first_name, + last_name: profileData.last_name, + display_name: `${profileData.first_name} ${profileData.last_name}` + })); + + showSuccess('Profile updated successfully'); + return response.data; + } else { + const errorMessage = response.data?.message || response.problem || 'Failed to update profile'; + showError(errorMessage); + return rejectWithValue(errorMessage); + } + } catch (error: any) { + const errorMessage = error.message || 'Failed to update profile'; + showError(errorMessage); + return rejectWithValue(errorMessage); + } + } +); + +/** + * Thunk to change password + */ +export const changePasswordAsync = createAsyncThunk( + 'auth/changePassword', + async (passwordData: { currentPassword: string; newPassword: string }, { getState, rejectWithValue }) => { + try { + const state = getState() as any; + const user = state.auth.user; + const token = user?.access_token; + + if (!user?.user_id || !token) { + return rejectWithValue('User not authenticated'); + } + + const response: any = await authAPI.changePassword(user.user_id, { password: passwordData.newPassword }, token); + + if (response.ok && response.data) { + showSuccess('Password changed successfully'); + return response.data; + } else { + const errorMessage = response.data?.message || response.problem || 'Failed to change password'; + showError(errorMessage); + return rejectWithValue(errorMessage); + } + } catch (error: any) { + const errorMessage = error.message || 'Failed to change password'; + showError(errorMessage); + return rejectWithValue(errorMessage); + } + } +); + +/** + * Thunk to logout user + */ +export const logoutUser = createAsyncThunk( + 'auth/logout', + async (_, { dispatch, rejectWithValue }) => { + try { + // TODO: Add logout API call if needed + // const response = await authAPI.logout(); + + // For now, just dispatch the logout action + dispatch(logout()); + + // Show success message + showSuccess('Logged out successfully'); + + return true; + } catch (error: any) { + console.error('Logout error:', error); + return rejectWithValue(error.message); + } + } +); + +/* + * End of File: authActions.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/redux/authSelectors.ts b/app/modules/Auth/redux/authSelectors.ts new file mode 100644 index 0000000..3221753 --- /dev/null +++ b/app/modules/Auth/redux/authSelectors.ts @@ -0,0 +1,48 @@ +/* + * File: authSelectors.ts + * Description: Selectors for Auth redux state + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { RootState } from '../../../store'; + +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; + +// User profile selectors +export const selectUserProfile = (state: RootState) => state.auth.user; +export const selectUserDisplayName = (state: RootState) => state.auth.user?.display_name; +export const selectUserEmail = (state: RootState) => state.auth.user?.email; +export const selectUserFirstName = (state: RootState) => state.auth.user?.first_name; +export const selectUserLastName = (state: RootState) => state.auth.user?.last_name; +export const selectUserHospitalId = (state: RootState) => state.auth.user?.hospital_id; +export const selectUserProfilePhoto = (state: RootState) => state.auth.user?.profile_photo_url; +export const selectUserThemeColor = (state: RootState) => state.auth.user?.theme_color; +export const selectUserAccentColor = (state: RootState) => state.auth.user?.accent_color; + +// Onboarding selectors +export const selectIsOnboarded = (state: RootState) => state.auth.user?.onboarded; +export const selectOnboardingCompleted = (state: RootState) => state.auth.user?.onboarding_completed; +export const selectOnboardingStep = (state: RootState) => state.auth.user?.onboarding_step; +export const selectOnboardingMessage = (state: RootState) => state.auth.user?.onboarding_message; + +// Dashboard settings selectors +export const selectDashboardSettings = (state: RootState) => state.auth.user?.dashboard_settings; +export const selectDashboardTheme = (state: RootState) => state.auth.user?.dashboard_settings?.theme; +export const selectDashboardLanguage = (state: RootState) => state.auth.user?.dashboard_settings?.language; +export const selectDashboardTimezone = (state: RootState) => state.auth.user?.dashboard_settings?.timezone; + +// Notification preferences selectors +export const selectNotificationPreferences = (state: RootState) => state.auth.user?.notification_preferences; +export const selectCriticalAlertsPreferences = (state: RootState) => state.auth.user?.notification_preferences?.critical_alerts; +export const selectSystemNotificationsPreferences = (state: RootState) => state.auth.user?.notification_preferences?.system_notifications; + +/* + * End of File: authSelectors.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/redux/authSlice.ts b/app/modules/Auth/redux/authSlice.ts new file mode 100644 index 0000000..3c481d5 --- /dev/null +++ b/app/modules/Auth/redux/authSlice.ts @@ -0,0 +1,103 @@ +/* + * 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'; +import { User, NotificationPreferences, DashboardSettings } from '../../../shared/types/auth'; +import { login } from './authActions'; + +// Use User type from shared types as UserProfile +type UserProfile = User; + +// Auth state type +interface AuthState { + user: UserProfile | null; + loading: boolean; + error: string | null; + isAuthenticated: boolean; +} + +const initialState: AuthState = { + user: null, + loading: false, + error: null, + isAuthenticated: false, +}; + +/** + * Auth slice for managing authentication state + */ +const authSlice = createSlice({ + name: 'auth', + initialState, + reducers: { + logout(state) { + state.user = null; + state.isAuthenticated = false; + state.loading = false; + state.error = null; + }, + updateOnboarded(state, action: PayloadAction) { + if (state.user) { + state.user.onboarded = action.payload; + } + }, + updateUserProfile(state, action: PayloadAction>) { + if (state.user) { + state.user = { ...state.user, ...action.payload }; + } + }, + updateNotificationPreferences(state, action: PayloadAction) { + if (state.user) { + state.user.notification_preferences = action.payload; + } + }, + updateDashboardSettings(state, action: PayloadAction) { + if (state.user) { + state.user.dashboard_settings = action.payload; + } + }, + clearError(state) { + state.error = null; + } + }, + extraReducers: (builder) => { + // Login thunk cases + builder + .addCase(login.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(login.fulfilled, (state, action) => { + state.loading = false; + state.user = action.payload as UserProfile; + state.isAuthenticated = true; + state.error = null; + }) + .addCase(login.rejected, (state, action) => { + state.loading = false; + state.error = action.payload as string || 'Login failed'; + state.isAuthenticated = false; + }); + }, +}); + +export const { + logout, + updateOnboarded, + updateUserProfile, + updateNotificationPreferences, + updateDashboardSettings, + clearError +} = authSlice.actions; + +export default authSlice.reducer; + +/* + * End of File: authSlice.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/redux/hospitalSelectors.ts b/app/modules/Auth/redux/hospitalSelectors.ts new file mode 100644 index 0000000..3dadc7f --- /dev/null +++ b/app/modules/Auth/redux/hospitalSelectors.ts @@ -0,0 +1,129 @@ +/* + * File: hospitalSelectors.ts + * Description: Redux selectors for Hospital state + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from '../../../store'; + +// ============================================================================ +// BASE SELECTORS +// ============================================================================ + +/** + * Select Hospital State + * + * Purpose: Get the entire hospital state from Redux store + */ +export const selectHospitalState = (state: RootState) => state.hospital; + +/** + * Select Hospitals List + * + * Purpose: Get the list of hospitals + */ +export const selectHospitals = (state: RootState) => state.hospital.hospitals; + +/** + * Select Hospital Loading State + * + * Purpose: Get the loading state for hospital operations + */ +export const selectHospitalLoading = (state: RootState) => state.hospital.loading; + +/** + * Select Hospital Error + * + * Purpose: Get any error from hospital operations + */ +export const selectHospitalError = (state: RootState) => state.hospital.error; + +// ============================================================================ +// DERIVED SELECTORS +// ============================================================================ + +/** + * Select Hospitals Count + * + * Purpose: Get the total number of hospitals + */ +export const selectHospitalsCount = createSelector( + [selectHospitals], + (hospitals) => hospitals?.length || 0 +); + +/** + * Select Hospital by ID + * + * Purpose: Get a specific hospital by its ID + */ +export const selectHospitalById = createSelector( + [selectHospitals, (_state: RootState, hospitalId: string) => hospitalId], + (hospitals, hospitalId) => + hospitals?.find(hospital => hospital.hospital_id === hospitalId) || null +); + +/** + * Select Hospital Names + * + * Purpose: Get an array of hospital names for display + */ +export const selectHospitalNames = createSelector( + [selectHospitals], + (hospitals) => + hospitals?.map(hospital => hospital.hospital_name).filter(Boolean) || [] +); + +/** + * Select Hospitals for Dropdown + * + * Purpose: Get hospitals formatted for dropdown/select components + */ +export const selectHospitalsForDropdown = createSelector( + [selectHospitals], + (hospitals) => + hospitals?.map(hospital => ({ + label: hospital.hospital_name || 'Unknown Hospital', + value: hospital.hospital_id || '', + })).filter(item => item.value) || [] +); + +/** + * Select Filtered Hospitals + * + * Purpose: Get hospitals filtered by search term + */ +export const selectFilteredHospitals = createSelector( + [selectHospitals, (_state: RootState, searchTerm: string) => searchTerm], + (hospitals, searchTerm) => { + if (!searchTerm.trim()) return hospitals || []; + + const term = searchTerm.toLowerCase(); + return hospitals?.filter(hospital => + hospital.hospital_name?.toLowerCase().includes(term) + ) || []; + } +); + +/** + * Select Hospital Loading Status + * + * Purpose: Get comprehensive loading status for hospital operations + */ +export const selectHospitalStatus = createSelector( + [selectHospitalLoading, selectHospitalError, selectHospitalsCount], + (loading, error, count) => ({ + loading, + error, + hasData: count > 0, + isEmpty: count === 0, + }) +); + +/* + * End of File: hospitalSelectors.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/redux/hospitalSlice.ts b/app/modules/Auth/redux/hospitalSlice.ts new file mode 100644 index 0000000..7ea2c08 --- /dev/null +++ b/app/modules/Auth/redux/hospitalSlice.ts @@ -0,0 +1,170 @@ +/* + * File: hospitalSlice.ts + * Description: Redux slice for Hospital state management (non-persisted) + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import { authAPI } from '../services/authAPI'; +import { showError } from '../../../shared/utils/toast'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +/** + * Hospital Interface + * + * Purpose: Defines the structure for hospital data + */ +interface Hospital { + hospital_id: string | null; + hospital_name: string | null; +} + +/** + * Hospital State Interface + * + * Purpose: Defines the structure for hospital state + */ +interface HospitalState { + hospitals: Hospital[] | null; + loading: boolean; + error: string | null; +} + +// ============================================================================ +// ASYNC THUNKS +// ============================================================================ + +/** + * Fetch Hospitals Async Thunk + * + * Purpose: Fetch hospital list from API + * + * Features: + * - API integration with error handling + * - Loading state management + * - Toast notifications for errors + * - Automatic state updates + */ +export const fetchHospitals = createAsyncThunk( + 'hospital/fetchHospitals', + async (_, { rejectWithValue }) => { + try { + const response: any = await authAPI.gethospitals(); + console.log('hospital response', response); + + if (response.ok && response.data && response.data.data) { + return response.data.data; + } else { + showError('Error while fetching hospital list'); + return rejectWithValue('Failed to fetch hospitals'); + } + } catch (error: any) { + console.log('Hospital fetch error:', error); + showError('Error while fetching hospital list'); + return rejectWithValue(error.message || 'Network error'); + } + } +); + +// ============================================================================ +// INITIAL STATE +// ============================================================================ + +const initialState: HospitalState = { + hospitals: [], + loading: false, + error: null, +}; + +// ============================================================================ +// HOSPITAL SLICE +// ============================================================================ + +/** + * Hospital Slice + * + * Purpose: Manages hospital-related state + * + * Features: + * - Hospital list management + * - Loading states for API calls + * - Error handling and display + * - Non-persisted state (not stored in AsyncStorage) + */ +const hospitalSlice = createSlice({ + name: 'hospital', + initialState, + reducers: { + /** + * Set Hospitals Action + * + * Purpose: Manually set hospital list + */ + setHospitals(state, action: PayloadAction) { + state.hospitals = action.payload; + state.loading = false; + state.error = null; + }, + + /** + * Clear Hospitals Action + * + * Purpose: Clear hospital list and reset state + */ + clearHospitals(state) { + state.hospitals = []; + state.loading = false; + state.error = null; + }, + + /** + * Clear Error Action + * + * Purpose: Clear error state + */ + clearError(state) { + state.error = null; + }, + }, + extraReducers: (builder) => { + builder + // Fetch Hospitals - Pending + .addCase(fetchHospitals.pending, (state) => { + state.loading = true; + state.error = null; + }) + // Fetch Hospitals - Fulfilled + .addCase(fetchHospitals.fulfilled, (state, action) => { + state.loading = false; + state.hospitals = action.payload; + state.error = null; + }) + // Fetch Hospitals - Rejected + .addCase(fetchHospitals.rejected, (state, action) => { + state.loading = false; + state.error = action.payload as string || 'Failed to fetch hospitals'; + }); + }, +}); + +// ============================================================================ +// EXPORTS +// ============================================================================ + +export const { + setHospitals, + clearHospitals, + clearError, +} = hospitalSlice.actions; + +export default hospitalSlice.reducer; + +/* + * End of File: hospitalSlice.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/redux/index.ts b/app/modules/Auth/redux/index.ts new file mode 100644 index 0000000..37b349e --- /dev/null +++ b/app/modules/Auth/redux/index.ts @@ -0,0 +1,16 @@ +/* + * File: index.ts + * Description: Barrel export for Auth redux + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export { default as authReducer } from './authSlice'; +export * from './authActions'; +export * from './authSelectors'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/screens/LoginScreen.tsx b/app/modules/Auth/screens/LoginScreen.tsx new file mode 100644 index 0000000..259b9a5 --- /dev/null +++ b/app/modules/Auth/screens/LoginScreen.tsx @@ -0,0 +1,420 @@ +/* + * File: LoginScreen.tsx + * Description: Login screen with credential-based authentication + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableWithoutFeedback, + Keyboard, + TouchableOpacity, + TextInput, + ScrollView, + KeyboardAvoidingView, + Alert, + Platform, + Image, +} from 'react-native'; +import { useAppDispatch, useAppSelector } from '../../../store/hooks'; +import { login } from '../redux/authActions'; +import { selectAuthLoading } from '../redux/authSelectors'; +import { theme } from '../../../theme/theme'; +import { validateEmail } from '../../../shared/utils/validators'; +import Icon from 'react-native-vector-icons/Feather'; +import { AuthNavigationProp } from '../navigation'; + +/** + * LoginScreenProps Interface + * + * Purpose: Defines the props required by the LoginScreen component + * + * Props: + * - navigation: React Navigation object for screen navigation + */ +interface LoginScreenProps { + navigation: AuthNavigationProp; +} + +/** + * LoginScreen Component + * + * Purpose: Main authentication screen for credential-based login + * + * Authentication Flow: + * 1. Email/Password validation + * 2. Redux action dispatch for login + * 3. Loading state management + * 4. Error handling and user feedback + * + * Features: + * - Keyboard-aware layout for better UX + * - Form validation and error handling + * - Loading states during authentication + * - Password visibility toggle + * - Navigation to sign up screen + * - Responsive design for different screen sizes + */ +const LoginScreen: React.FC = ({ navigation }) => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + // Form input states + const [email, setEmail] = useState(''); // User's email address + const [password, setPassword] = useState(''); // User's password + const [isPasswordVisible, setPasswordVisible] = useState(false); // Password visibility toggle + + // Redux state + const dispatch = useAppDispatch(); + const loading = useAppSelector(selectAuthLoading); + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * handleLogin Function + * + * Purpose: Process credential-based login with email and password + * + * Flow: + * 1. Validate that both email and password are provided + * 2. Validate email format + * 3. Show error alert if validation fails + * 4. Dispatch Redux login action with credentials + */ + const handleLogin = () => { + // Validate required fields + if (!email.trim() || !password.trim()) { + Alert.alert('Validation Error', 'Email and password are required.'); + return; + } + + // Validate email format + if (!validateEmail(email)) { + Alert.alert('Validation Error', 'Please enter a valid email address.'); + return; + } + + // Dispatch login action + dispatch(login({ email, password })); + }; + + /** + * handleSignUp Function + * + * Purpose: Navigate to the SignUpScreen for new user registration + * + * Flow: Navigate to SignUp screen using React Navigation + */ + const handleSignUp = () => { + navigation.navigate('SignUp'); + }; + + /** + * togglePasswordVisibility Function + * + * Purpose: Toggle password field visibility for better UX + */ + const togglePasswordVisibility = () => { + setPasswordVisible(!isPasswordVisible); + }; + + // ============================================================================ + // RENDER SECTION + // ============================================================================ + + return ( + + + + + {/* ======================================================================== + * HEADER SECTION - App branding and title + * ======================================================================== */} + + Radiologist + {/* Emergency Department Access */} + + + + + + {/* ======================================================================== + * LOGIN FORM - Main authentication interface + * ======================================================================== */} + + {/* Email Input */} + + + + + + {/* Password Input */} + + + + + + + + + + + {/* Login Button */} + + {loading ? ( + + Logging in... + + ) : ( + Login + )} + + + {/* Divider */} + + + OR + + + + {/* Sign Up Button */} + + Sign Up + + + + {/* ======================================================================== + * FOOTER - Security and information message + * ======================================================================== */} + + + Secure access to patient information and critical alerts + + + + + + + ); +}; + +// ============================================================================ +// STYLES SECTION +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Content wrapper + content: { + flex: 1, + justifyContent: 'center', + padding: theme.spacing.lg, + }, + + // Header section + header: { + alignItems: 'center', + marginBottom: theme.spacing.xxl, + }, + + // App title + title: { + fontSize: theme.typography.fontSize.displayMedium, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.primary, + marginBottom: theme.spacing.sm, + textAlign: 'center', + }, + + // App subtitle + subtitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + textAlign: 'center', + }, + + // Form container + formContainer: { + marginBottom: theme.spacing.xl, + }, + + // Input container + inputContainer: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.backgroundAlt, + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.medium, + marginBottom: theme.spacing.md, + paddingHorizontal: theme.spacing.md, + paddingVertical: 2, + }, + + // Input icon + inputIcon: { + marginRight: theme.spacing.sm, + }, + + // Input field + inputField: { + flex: 1, + paddingVertical: theme.spacing.md, + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textPrimary, + }, + + // Eye icon for password visibility + eyeIcon: { + padding: theme.spacing.sm, + }, + + // Base button styling + button: { + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + borderRadius: theme.borderRadius.medium, + alignItems: 'center', + marginBottom: theme.spacing.md, + }, + + // Login button + loginButton: { + backgroundColor: theme.colors.primary, + ...theme.shadows.primary, + }, + + // Sign up button + signUpButton: { + backgroundColor: theme.colors.background, + borderWidth: 1, + borderColor: theme.colors.primary, + }, + + // Disabled button + buttonDisabled: { + opacity: 0.6, + }, + + // Button text + buttonText: { + color: theme.colors.background, + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.medium, + }, + + // Sign up button text + signUpButtonText: { + color: theme.colors.primary, + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.medium, + }, + + // Loading container + loadingContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + + // Divider + divider: { + flexDirection: 'row', + alignItems: 'center', + marginVertical: theme.spacing.lg, + }, + + // Divider line + dividerLine: { + flex: 1, + height: 1, + backgroundColor: theme.colors.border, + }, + + // Divider text + dividerText: { + marginHorizontal: theme.spacing.md, + color: theme.colors.textSecondary, + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + }, + + // Footer + footer: { + alignItems: 'center', + }, + + // Footer text + footerText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + textAlign: 'center', + }, + + // Image container + imageContainer: { + alignItems: 'center', + marginBottom: theme.spacing.xl, + }, + + // Image + image: { + width: '100%', + height: 150, + }, +}); + +export default LoginScreen; + +/* + * End of File: LoginScreen.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/screens/ResetPasswordScreen.tsx b/app/modules/Auth/screens/ResetPasswordScreen.tsx new file mode 100644 index 0000000..8e51741 --- /dev/null +++ b/app/modules/Auth/screens/ResetPasswordScreen.tsx @@ -0,0 +1,1251 @@ +/* + * File: ResetPasswordScreen.tsx + * Description: Password reset screen for onboarding flow with document upload support + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + TextInput, + TouchableOpacity, + ScrollView, + KeyboardAvoidingView, + Platform, + Alert, + Image, + PermissionsAndroid, +} from 'react-native'; +import { + launchImageLibrary, + launchCamera, + ImagePickerResponse, + MediaType, +} from 'react-native-image-picker'; +import { useAppDispatch, useAppSelector } from '../../../store/hooks'; +import { updateOnboarded, logout } from '../redux/authSlice'; +import { authAPI } from '../services/authAPI'; +import { showError, showSuccess } from '../../../shared/utils/toast'; +import { validateFileType, validateFileSize, prepareFileForUpload } from '../../../shared/utils/fileUpload'; +import { theme } from '../../../theme/theme'; +import Icon from 'react-native-vector-icons/Feather'; +import { AuthNavigationProp } from '../navigation/navigationTypes'; + +/** + * ResetPasswordScreenProps Interface + * + * Purpose: Defines the props required by the ResetPasswordScreen component + * + * Props: + * - navigation: React Navigation object for screen navigation (optional when used in root stack) + */ +interface ResetPasswordScreenProps { + navigation?: AuthNavigationProp; +} + +/** + * PasswordRule Interface + * + * Purpose: Defines the structure for password validation rules + */ +interface PasswordRule { + id: string; + label: string; + validator: (password: string) => boolean; + isValid: boolean; +} + +/** + * ImageData Interface + * + * Purpose: Defines the structure for image data + */ +interface ImageData { + uri: string; + name: string; + type: string; + size?: number; +} + +/** + * OnboardingStep Type + * + * Purpose: Defines the different onboarding steps + */ +type OnboardingStep = 'document' | 'password'; + +/** + * ResetPasswordScreen Component + * + * Purpose: Password reset screen for users who need to set their initial password + * + * Features: + * 1. Two flows based on platform: + * - platform = 'web': Document upload first, then password reset + * - platform = 'app': Password reset only + * 2. Document upload with camera/gallery selection + * 3. Password and confirm password input fields + * 4. Real-time password validation with visual feedback + * 5. Password visibility toggles + * 6. Password strength requirements display + * 7. Integration with Redux for onboarding status + * 8. API integration for document upload and password change + * + * Password Requirements: + * - At least 8 characters + * - One uppercase letter + * - One lowercase letter + * - One number + * - One special character + * - Passwords must match + */ +export const ResetPasswordScreen: React.FC = ({ + navigation, +}) => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + // Form input states + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [isPasswordVisible, setPasswordVisible] = useState(false); + const [isConfirmPasswordVisible, setConfirmPasswordVisible] = useState(false); + const [loading, setLoading] = useState(false); + + // Document upload states + const [selectedImage, setSelectedImage] = useState(null); + const [documentUploaded, setDocumentUploaded] = useState(false); + + // Current step state + const [currentStep, setCurrentStep] = useState('document'); + + // Redux state + const dispatch = useAppDispatch(); + const user = useAppSelector((state) => state.auth.user); + + // Password validation rules + const [passwordRules, setPasswordRules] = useState([ + { + id: 'length', + label: 'At least 8 characters', + validator: (pwd: string) => pwd.length >= 8, + isValid: false, + }, + { + id: 'uppercase', + label: 'One uppercase letter', + validator: (pwd: string) => /[A-Z]/.test(pwd), + isValid: false, + }, + { + id: 'lowercase', + label: 'One lowercase letter', + validator: (pwd: string) => /[a-z]/.test(pwd), + isValid: false, + }, + { + id: 'number', + label: 'One number', + validator: (pwd: string) => /\d/.test(pwd), + isValid: false, + }, + { + id: 'special', + label: 'One special character', + validator: (pwd: string) => /[!@#$%^&*(),.?":{}|<>]/.test(pwd), + isValid: false, + }, + { + id: 'match', + label: 'Passwords match', + validator: (pwd: string) => pwd.length > 0 && confirmPassword.length > 0 && pwd === confirmPassword, + isValid: false, + }, + ]); + + // ============================================================================ + // EFFECTS + // ============================================================================ + + /** + * useEffect for password validation + * + * Purpose: Update password rules when password or confirm password changes + */ + useEffect(() => { + updatePasswordRules(password); + }, [password, confirmPassword]); + + /** + * useEffect for determining initial step + * + * Purpose: Set the initial step based on user signup platform + */ + useEffect(() => { + if (user?.platform === 'app') { + setCurrentStep('password'); + } else { + setCurrentStep('document'); + } + }, [user?.platform]); + + // ============================================================================ + // HELPER FUNCTIONS + // ============================================================================ + + /** + * validatePassword Function + * + * Purpose: Check if all password requirements are met + * + * @param pwd - Password to validate + * @returns boolean indicating if password meets all requirements + */ + const validatePassword = (pwd: string): boolean => { + return passwordRules.every(rule => rule.isValid); + }; + + /** + * updatePasswordRules Function + * + * Purpose: Update password validation rules based on current password + * + * @param pwd - Current password value + */ + const updatePasswordRules = (pwd: string) => { + setPasswordRules(prevRules => + prevRules.map(rule => { + if (rule.id === 'match') { + return { + ...rule, + isValid: pwd.length > 0 && confirmPassword.length > 0 && pwd === confirmPassword, + }; + } + return { + ...rule, + isValid: rule.validator(pwd), + }; + }) + ); + }; + + /** + * Request Camera Permission + * + * Purpose: Request camera permission for Android devices + * + * @returns Promise - Whether permission was granted + */ + const requestCameraPermission = async (): Promise => { + 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('Camera permission error:', err); + return false; + } + } + return true; + }; + + /** + * Format File Size + * + * Purpose: Convert bytes to human readable format + * + * @param bytes - File size in bytes + * @returns Formatted file size string + */ + 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]; + }; + + /** + * Get File Type Display + * + * Purpose: Get display name for file type + * + * @param type - MIME type + * @returns Display name for file type + */ + 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'; + }; + + // ============================================================================ + // DOCUMENT UPLOAD HANDLERS + // ============================================================================ + + /** + * Handle Image Picker Selection + * + * Purpose: Show options for camera or gallery selection + */ + const handleImagePicker = () => { + Alert.alert( + 'Select Image', + 'Choose how you want to upload your document', + [ + { + text: 'Camera', + onPress: () => handleCameraCapture(), + }, + { + text: 'Gallery', + onPress: () => handleGalleryPicker(), + }, + { + text: 'Cancel', + style: 'cancel', + }, + ] + ); + }; + + /** + * Handle Camera Capture + * + * Purpose: Launch camera to capture image + */ + const handleCameraCapture = async () => { + const hasPermission = await requestCameraPermission(); + + if (!hasPermission) { + showError('Permission Error', 'Camera permission is required to capture images.'); + return; + } + + const options = { + mediaType: 'photo' as MediaType, + 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 || `document_${Date.now()}.jpg`, + type: asset.type || 'image/jpeg', + size: asset.fileSize, + }; + + // Validate file type and size + if (!validateFileType(imageData)) { + showError('Invalid File Type', 'Please select a JPEG, JPG, or PNG image.'); + return; + } + + if (!validateFileSize(imageData, 10)) { + showError('File Too Large', 'Please select an image under 10MB.'); + return; + } + + setSelectedImage(imageData); + showSuccess('Success', 'Document captured successfully!'); + } + }); + }; + + /** + * Handle Gallery Picker + * + * Purpose: Launch image library to select image + */ + const handleGalleryPicker = () => { + const options = { + mediaType: 'photo' as MediaType, + 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 || `document_${Date.now()}.jpg`, + type: asset.type || 'image/jpeg', + size: asset.fileSize, + }; + + // Validate file type and size + if (!validateFileType(imageData)) { + showError('Invalid File Type', 'Please select a JPEG, JPG, or PNG image.'); + return; + } + + if (!validateFileSize(imageData, 10)) { + showError('File Too Large', 'Please select an image under 10MB.'); + return; + } + + setSelectedImage(imageData); + showSuccess('Success', 'Document selected from gallery!'); + } + }); + }; + + /** + * Handle Remove Image + * + * Purpose: Remove selected image + */ + const handleRemoveImage = () => { + setSelectedImage(null); + setDocumentUploaded(false); + }; + + /** + * Handle Document Upload + * + * Purpose: Upload document to server + */ + const handleDocumentUpload = async () => { + if (!selectedImage) { + showError('Validation Error', 'Please upload a document to continue.'); + return; + } + + setLoading(true); + + try { + const formData = new FormData(); + + // Prepare file with proper structure using utility function + const preparedFile = prepareFileForUpload(selectedImage, 'id_photo'); + + formData.append('id_photo', preparedFile as any); + + const response: any = await authAPI.uploadDocument(formData, user?.access_token); + console.log('upload response',response) + + if (response.data && response.data.message) { + if (response.data.success) { + showSuccess(response.data.message); + setDocumentUploaded(true); + // Move to password step after successful upload + setCurrentStep('password'); + } else { + showError(response.data.message); + } + } else { + showError('Error while uploading document'); + } + } catch (error: any) { + console.error('Document upload error:', error); + showError('Failed to upload document. Please try again.'); + } finally { + setLoading(false); + } + }; + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * handlePasswordChange Function + * + * Purpose: Handle password input changes + * + * @param pwd - New password value + */ + const handlePasswordChange = (pwd: string) => { + setPassword(pwd); + }; + + /** + * handleConfirmPasswordChange Function + * + * Purpose: Handle confirm password input changes + * + * @param pwd - New confirm password value + */ + const handleConfirmPasswordChange = (pwd: string) => { + setConfirmPassword(pwd); + }; + + /** + * handleReset Function + * + * Purpose: Handle password reset submission + * + * Flow: + * 1. Validate required fields + * 2. Validate password requirements + * 3. Check password match + * 4. Call API to change password + * 5. Update onboarding status on success + */ + const handleReset = async () => { + // Validate required fields + if (!password.trim() || !confirmPassword.trim()) { + Alert.alert('Validation Error', 'Both password fields are required.'); + return; + } + + // Validate password requirements + if (!validatePassword(password)) { + Alert.alert('Validation Error', 'Please meet all password requirements.'); + return; + } + + // Check password match + if (password !== confirmPassword) { + Alert.alert('Validation Error', 'Passwords do not match.'); + return; + } + + setLoading(true); + + try { + // Call API to change password + const response: any = await authAPI.changepassword({ + password, + token: user?.access_token, + }); + + console.log('reset response', response); + + if (response.data && response.data.message) { + if (response.data.success) { + showSuccess(response.data.message); + + // Update onboarding status + dispatch(updateOnboarded(true)); + + // Navigate to main app + // The app will automatically navigate to MainTabNavigator due to Redux state change + } else { + showError(response.data.message); + } + } else { + showError('Error while changing password'); + } + } catch (error: any) { + console.error('Password reset error:', error); + showError('Failed to reset password. Please try again.'); + } finally { + setLoading(false); + } + }; + + /** + * handleBack Function + * + * Purpose: Handle back navigation - logs out user since they can't go back to login + */ + const handleBack = () => { + Alert.alert( + 'Sign Out', + 'Are you sure you want to sign out? You will need to log in again.', + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Sign Out', + style: 'destructive', + onPress: () => { + // Dispatch logout action + dispatch(logout()); + }, + }, + ] + ); + }; + + /** + * renderPasswordRule Function + * + * Purpose: Render individual password validation rule + * + * @param rule - Password rule to render + * @returns JSX element for the rule + */ + const renderPasswordRule = (rule: PasswordRule) => ( + + + {rule.isValid && ( + + )} + + + {rule.label} + + + ); + + /** + * renderDocumentUploadStep Function + * + * Purpose: Render document upload step + * + * @returns JSX element for document upload step + */ + const renderDocumentUploadStep = () => ( + <> + {/* Icon */} + + + + + {/* Title and Subtitle */} + Upload Your ID Document + + Please upload a clear photo of your hospital-issued ID for verification + + + {/* Document Upload Area */} + + + {selectedImage ? ( + + + + + + Document Selected + {selectedImage.name} + + {getFileTypeDisplay(selectedImage.type)} + {selectedImage.size && ( + โ€ข {formatFileSize(selectedImage.size)} + )} + + + ) : ( + <> + + Tap to upload document + JPG, PNG supported + + )} + + + + {selectedImage && ( + + Change Document + + )} + + {/* Upload Button */} + + {loading ? ( + + Uploading Document... + + ) : ( + Upload Document + )} + + + ); + + /** + * renderPasswordResetStep Function + * + * Purpose: Render password reset step + * + * @returns JSX element for password reset step + */ + const renderPasswordResetStep = () => ( + <> + {/* Icon */} + + + + + {/* Title and Subtitle */} + Set Your Password + + Create a strong password to complete your account setup + + + {/* Password Input */} + + + + setPasswordVisible(!isPasswordVisible)} + style={styles.eyeIcon} + disabled={loading} + > + + + + + {/* Confirm Password Input */} + + + + setConfirmPasswordVisible(!isConfirmPasswordVisible)} + style={styles.eyeIcon} + disabled={loading} + > + + + + + {/* Password Rules Section */} + + Password Requirements: + + {passwordRules.map(renderPasswordRule)} + + + + {/* Reset Button */} + + {loading ? ( + + Setting Password... + + ) : ( + Set Password + )} + + + ); + + // ============================================================================ + // RENDER SECTION + // ============================================================================ + + return ( + + + {/* Header */} + + + + + + {currentStep === 'document' ? 'Upload Document' : 'Set Password'} + + + + + {/* Step Indicator */} + {user?.platform === 'web' && ( + + + + 1 + + Document + + + + + 2 + + Password + + + )} + + {/* Content based on current step */} + {currentStep === 'document' ? renderDocumentUploadStep() : renderPasswordResetStep()} + + + ); +}; + +// ============================================================================ +// STYLES SECTION +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Scroll view + scrollView: { + flex: 1, + }, + + // Scroll content + scrollContent: { + flexGrow: 1, + padding: theme.spacing.lg, + }, + + // Header section + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: theme.spacing.xl, + }, + + // Back button + backButton: { + padding: theme.spacing.sm, + borderRadius: theme.borderRadius.medium, + backgroundColor: theme.colors.backgroundAlt, + }, + + // Header title + headerTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + }, + + // Header spacer + headerSpacer: { + width: 40, + }, + + // Step indicator + stepIndicator: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + marginBottom: theme.spacing.xl, + paddingHorizontal: theme.spacing.lg, + }, + + // Step container + stepContainer: { + alignItems: 'center', + }, + + // Step circle + stepCircle: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: theme.colors.backgroundAlt, + borderWidth: 2, + borderColor: theme.colors.border, + alignItems: 'center', + justifyContent: 'center', + marginBottom: theme.spacing.xs, + }, + + // Step circle active + stepCircleActive: { + backgroundColor: theme.colors.primary, + borderColor: theme.colors.primary, + }, + + // Step number + stepNumber: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textSecondary, + }, + + // Step number active + stepNumberActive: { + color: theme.colors.background, + }, + + // Step label + stepLabel: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + }, + + // Step label active + stepLabelActive: { + color: theme.colors.primary, + fontFamily: theme.typography.fontFamily.medium, + }, + + // Step line + stepLine: { + flex: 1, + height: 2, + backgroundColor: theme.colors.border, + marginHorizontal: theme.spacing.md, + }, + + // Icon container + iconContainer: { + alignItems: 'center', + marginBottom: theme.spacing.xl, + }, + + // Title + title: { + fontSize: theme.typography.fontSize.displayMedium, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + textAlign: 'center', + marginBottom: theme.spacing.sm, + }, + + // Subtitle + subtitle: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + textAlign: 'center', + marginBottom: theme.spacing.xl, + }, + + // Upload container + uploadContainer: { + backgroundColor: theme.colors.backgroundAlt, + borderWidth: 2, + borderColor: theme.colors.border, + borderStyle: 'dashed', + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.xl, + marginBottom: theme.spacing.md, + minHeight: 200, + }, + + // Upload content + uploadContent: { + alignItems: 'center', + }, + + // Image preview container + imagePreviewContainer: { + alignItems: 'center', + width: '100%', + }, + + // Image preview + imagePreview: { + width: '100%', + height: 150, + borderRadius: theme.borderRadius.medium, + marginBottom: theme.spacing.sm, + resizeMode: 'contain', + }, + + // Image overlay (remove button) + imageOverlay: { + position: 'absolute', + top: 0, + right: 20, + backgroundColor: 'rgba(0, 0, 0, 0.4)', + borderRadius: 17, + width: 34, + height: 34, + justifyContent: 'center', + alignItems: 'center', + }, + + // Upload text + uploadText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + marginTop: theme.spacing.sm, + }, + + // Upload subtext + uploadSubtext: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginTop: theme.spacing.xs, + }, + + // Uploaded text + uploadedText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.success, + marginTop: theme.spacing.sm, + }, + + // File name + fileName: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginTop: theme.spacing.xs, + textAlign: 'center', + maxWidth: '80%', + }, + + // File details + fileDetails: { + flexDirection: 'row', + alignItems: 'center', + marginTop: theme.spacing.xs, + }, + + // File type + fileType: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + }, + + // File size + fileSize: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + }, + + // Change button + changeButton: { + alignSelf: 'center', + marginBottom: theme.spacing.xl, + }, + + // Change button text + changeButtonText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.primary, + }, + + // Input container + inputContainer: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.backgroundAlt, + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.medium, + marginBottom: theme.spacing.md, + paddingHorizontal: theme.spacing.md, + paddingVertical: 2, + }, + + // Input icon + inputIcon: { + marginRight: theme.spacing.sm, + }, + + // Input field + inputField: { + flex: 1, + paddingVertical: theme.spacing.md, + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textPrimary, + }, + + // Eye icon + eyeIcon: { + padding: theme.spacing.sm, + }, + + // Rules container + rulesContainer: { + marginTop: theme.spacing.sm, + marginBottom: theme.spacing.xl, + paddingHorizontal: theme.spacing.sm, + }, + + // Rules title + rulesTitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.sm, + }, + + // Rules grid + rulesGrid: { + gap: theme.spacing.xs, + }, + + // Rule container + ruleContainer: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.xs, + }, + + // Checkbox + checkbox: { + width: 18, + height: 18, + borderRadius: 4, + borderWidth: 2, + borderColor: theme.colors.border, + backgroundColor: theme.colors.backgroundAlt, + marginRight: theme.spacing.sm, + alignItems: 'center', + justifyContent: 'center', + }, + + // Checkbox checked + checkboxChecked: { + backgroundColor: theme.colors.primary, + borderColor: theme.colors.primary, + }, + + // Rule text + ruleText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + flex: 1, + }, + + // Rule text valid + ruleTextValid: { + color: theme.colors.success, + }, + + // Base button + button: { + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + borderRadius: theme.borderRadius.medium, + alignItems: 'center', + marginBottom: theme.spacing.md, + }, + + // Upload button + uploadButton: { + backgroundColor: theme.colors.primary, + ...theme.shadows.primary, + }, + + // Reset button + resetButton: { + backgroundColor: theme.colors.primary, + ...theme.shadows.primary, + }, + + // Disabled button + buttonDisabled: { + opacity: 0.6, + }, + + // Button text + buttonText: { + color: theme.colors.background, + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.medium, + }, + + // Loading container + loadingContainer: { + flexDirection: 'row', + alignItems: 'center', + }, +}); + +export default ResetPasswordScreen; + +/* + * End of File: ResetPasswordScreen.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/screens/SignUpScreen.tsx b/app/modules/Auth/screens/SignUpScreen.tsx new file mode 100644 index 0000000..adfcd74 --- /dev/null +++ b/app/modules/Auth/screens/SignUpScreen.tsx @@ -0,0 +1,523 @@ +/* + * File: SignUpScreen.tsx + * Description: Multi-step signup screen with validation and Redux integration + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useEffect, useState } from 'react'; +import { + View, + StyleSheet, + StatusBar, + Alert, + Text, + TouchableOpacity, + ScrollView, + KeyboardAvoidingView, + Platform +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import { useAppDispatch, useAppSelector } from '../../../store/hooks'; +import Icon from 'react-native-vector-icons/Feather'; + +// Import signup step components +import EmailStep from '../components/signup/EmailStep'; +import PasswordStep from '../components/signup/PasswordStep'; +import NameStep from '../components/signup/NameStep'; +import DocumentUploadStep from '../components/signup/DocumentUploadStep'; +import HospitalSelectionStep from '../components/signup/HospitalSelectionStep'; +import EmailAlreadyRegisteredModal from '../components/signup/EmailAlreadyRegisteredModal'; + +// Import API service + + +// Import hospital Redux functionality +import { fetchHospitals } from '../redux/hospitalSlice'; +import { selectHospitalLoading, selectHospitals } from '../redux/hospitalSelectors'; + +// Import types +import { SignUpData, SignUpStep } from '../types/signup'; +import { authAPI } from '../services/authAPI'; +import { showError, showSuccess } from '../../../shared/utils/toast'; +import { createFormDataWithFile, validateFileType, validateFileSize } from '../../../shared/utils/fileUpload'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface SignUpScreenProps { + navigation: any; +} + +// ============================================================================ +// SIGNUP SCREEN COMPONENT +// ============================================================================ + +/** + * SignUpScreen Component + * + * Purpose: Multi-step signup flow with validation and Redux integration + * + * Features: + * - Step-by-step signup process (email โ†’ password โ†’ name โ†’ document โ†’ hospital) + * - Real-time validation with visual feedback + * - Email and username availability checks + * - Hospital selection with search + * - Document upload with preview + * - Progress tracking with visual progress bar + * - Modern UI with icons and proper typography + * - Keyboard-aware layout for better UX + * - Redux state management + * - Loading states and error handling + */ +const SignUpScreen: React.FC = ({ navigation }) => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + const [currentStep, setCurrentStep] = useState('email'); + const [showEmailRegisteredModal, setShowEmailRegisteredModal] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [signUpData, setSignUpData] = useState>({ + email: '', + password: '', + first_name: '', + last_name: '', + username: '', + id_photo_url: null, + hospital_id: '', + }); + + const dispatch = useAppDispatch(); + + // ============================================================================ + // REDUX STATE + // ============================================================================ + + const hospitals = useAppSelector(selectHospitals); + const hospitalLoading = useAppSelector(selectHospitalLoading); + + // ============================================================================ + // STEP CONFIGURATION + // ============================================================================ + + const steps: SignUpStep[] = ['email', 'password', 'name', 'document', 'hospital']; + const currentStepIndex = steps.indexOf(currentStep); + + // ============================================================================ + // EFFECTS + // ============================================================================ + + useEffect(() => { + // Fetch hospitals on component mount + dispatch(fetchHospitals()); + }, [dispatch]); + + + + // ============================================================================ + // STEP HANDLERS + // ============================================================================ + + /** + * Handle Email Step Continue + * + * Purpose: Validate email and proceed to next step + */ + const handleEmailContinue = async (email: string) => { + setIsLoading(true); + + try { + const response :any = await authAPI.validatemail({email}); + console.log('response', response); + + if(response.status==409&&response.data.message){ + // Show modal instead of toast for already registered email + setShowEmailRegisteredModal(true) + } + if(response.status==200&&response.data.message){ + setSignUpData(prev => ({ ...prev, email })); + setCurrentStep('password'); + } + + } catch (error) { + Alert.alert('Error', 'Failed to validate email. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + /** + * Handle Password Step Continue + * + * Purpose: Validate password and proceed to next step + */ + const handlePasswordContinue = (password: string) => { + setSignUpData(prev => ({ ...prev, password })); + setCurrentStep('name'); + }; + + /** + * Handle Name Step Continue + * + * Purpose: Validate name and username, then proceed to next step + */ + const handleNameContinue = async (firstName: string, lastName: string, username: string) => { + setIsLoading(true); + + try { + const response:any = await authAPI.validateusername(username); + console.log('response', response); + + if(response.status==409&&response.data.message){ + showError(response.data.message); + } + if(response.status==200&&response.data.message){ + setSignUpData(prev => ({ + ...prev, + first_name: firstName, + last_name: lastName, + username: username + })); + setCurrentStep('document'); + } + } catch (error) { + Alert.alert('Error', 'Failed to validate username. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + /** + * Handle Document Upload Step Continue + * + * Purpose: Save document and proceed to next step + */ + const handleDocumentContinue = (documentUri: string) => { + setSignUpData(prev => ({ ...prev, id_photo_url: documentUri })); + setCurrentStep('hospital'); + }; + + /** + * Handle Hospital Selection Step Continue + * + * Purpose: Complete signup process + */ + const handleHospitalContinue = async (hospitalId: string) => { + const finalData: SignUpData = { + ...signUpData, + hospital_id: hospitalId, + } as SignUpData; + + setSignUpData(finalData); + + // Call completion handler + await onSignUpComplete(finalData); + }; + + /** + * Complete Signup Process + * + * Purpose: Submit final signup data to API + */ + const onSignUpComplete = async (payload: SignUpData) => { + console.log('final payload', payload); + setIsLoading(true); + + try { + let role = 'radiologist'; + + // Prepare form data with proper file handling + const formFields = { + email: payload.email, + password: payload.password, + first_name: payload.first_name, + last_name: payload.last_name, + username: payload.username, + dashboard_role: role, + hospital_id: payload.hospital_id, + }; + + let formData: FormData; + + // Handle file upload with validation + if (payload.id_photo_url) { + const fileData = { + uri: payload.id_photo_url, + name: `id_photo_${Date.now()}.jpg`, + type: 'image/jpeg', + }; + + // Validate file type and size + if (!validateFileType(fileData)) { + showError('Invalid file type. Please select a JPEG, JPG, or PNG image.'); + return; + } + + if (!validateFileSize(fileData, 10)) { + showError('File size too large. Please select an image under 10MB.'); + return; + } + + // Create FormData with file + formData = createFormDataWithFile(formFields, fileData, 'id_photo_url'); + } else { + // Create FormData without file + formData = createFormDataWithFile(formFields); + } + + 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'); + if( response.data && response.data.message ) { + //@ts-ignore + showError(response.data.message) + } + + } + } catch (error: any) { + console.log('error', error); + Alert.alert('Error', 'Failed to create account. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + // ============================================================================ + // NAVIGATION HANDLERS + // ============================================================================ + + /** + * Handle Back Navigation + * + * Purpose: Navigate to previous step or go back to previous screen + */ + const handleBack = () => { + if (currentStepIndex > 0) { + const previousStep = steps[currentStepIndex - 1]; + setCurrentStep(previousStep); + } else { + navigation.goBack(); + } + }; + + /** + * Handle Modal Close + * + * Purpose: Close email already registered modal + */ + const handleCloseModal = () => { + setShowEmailRegisteredModal(false); + }; + + /** + * Handle Go To Login + * + * Purpose: Navigate to login screen + */ + const handleGoToLogin = () => { + setShowEmailRegisteredModal(false); + navigation.navigate('Login'); + }; + + // ============================================================================ + // RENDER FUNCTIONS + // ============================================================================ + + /** + * Render Current Step + * + * Purpose: Render the appropriate step component based on current step + */ + const renderCurrentStep = () => { + console.log('signupdate', signUpData); + + const commonProps = { + onBack: handleBack, + data: signUpData, + isLoading, + }; + + switch (currentStep) { + case 'email': + return ( + + ); + + case 'password': + return ( + + ); + + case 'name': + return ( + + ); + + case 'document': + return ( + + ); + + case 'hospital': + return ( + + ); + + default: + return ( + + ); + } + }; + + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + + + + {/* Conditional Content Rendering */} + {currentStep === 'hospital' ? ( + // For hospital step, render without ScrollView to avoid conflicts + + {renderCurrentStep()} + + ) : ( + // For other steps, use ScrollView for proper scrolling + + {renderCurrentStep()} + + )} + + {/* Progress Bar - Bottom */} + + + + + + {Math.round(((currentStepIndex + 1) / steps.length) * 100)}% Complete + + + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + + + // Progress container + progressContainer: { + paddingHorizontal: theme.spacing.lg, + paddingVertical: theme.spacing.md, + backgroundColor: theme.colors.background, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + + // Progress bar + progressBar: { + height: 4, + backgroundColor: theme.colors.border, + borderRadius: theme.borderRadius.round, + marginBottom: theme.spacing.sm, + overflow: 'hidden', + }, + + // Progress fill + progressFill: { + height: '100%', + backgroundColor: theme.colors.primary, + borderRadius: theme.borderRadius.round, + }, + + // Progress text + progressText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textSecondary, + textAlign: 'center', + }, + + // Content area + content: { + flex: 1, + }, + + // Content container + contentContainer: { + flexGrow: 1, + padding: theme.spacing.lg, + paddingBottom: theme.spacing.xl, + }, +}); + +export default SignUpScreen; + +/* + * End of File: SignUpScreen.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/services/authAPI.ts b/app/modules/Auth/services/authAPI.ts new file mode 100644 index 0000000..8f8757b --- /dev/null +++ b/app/modules/Auth/services/authAPI.ts @@ -0,0 +1,77 @@ +/* + * File: authAPI.ts + * Description: API service for authentication using apisauce + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { create } from 'apisauce'; +import { API_CONFIG } from '../../../shared/utils/constants'; +import { buildHeaders } from '../../../shared/utils/api'; + +const api = create({ + baseURL: API_CONFIG.BASE_URL, // TODO: Replace with actual endpoint +}); + +/** + * login - authenticates user with email and password + */ +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, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }), + //validate email + validatemail: (payload:{email:string}) => api.post('/api/auth/auth/check-email', payload,buildHeaders()), + //change password + changepassword: (payload:{password:string,token:string | undefined}) => api.post('/api/auth/onboarding/change-password', {password:payload.password},buildHeaders({token:payload.token})), + //validate username + validateusername: (username:string|undefined) => api.post('/api/auth/auth/check-username', {username},buildHeaders()), + //upload document for onboarding + uploadDocument: (formData:any, token:string | undefined) => api.post('/api/auth/onboarding/upload-id-photo', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + ...(token && { 'Authorization': `Bearer ${token}` }), + }, + }), + + //upload profile photo for onboarding + uploadProfilePhoto: (formData:any, token:string | undefined) => api.post('/api/auth/onboarding/upload-profile-photo', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + ...(token && { 'Authorization': `Bearer ${token}` }), + }, + }), + + // Update user profile + updateUserProfile: (userId: string, profileData: { + first_name: string; + last_name: string; + }, token: string) => api.put( + `/api/auth/auth/admin/users/self/${userId}`, + profileData, + buildHeaders({ token }) + ), + + // Change password (admin endpoint) + changePassword: (userId: string, passwordData: { + password: string; + }, token: string) => api.put( + `/api/auth/auth/admin/users/self/${userId}`, + passwordData, + buildHeaders({ token }) + ), + + // Add more endpoints as needed +}; + +/* + * End of File: authAPI.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/services/biometricService.ts b/app/modules/Auth/services/biometricService.ts new file mode 100644 index 0000000..746d76a --- /dev/null +++ b/app/modules/Auth/services/biometricService.ts @@ -0,0 +1,23 @@ +/* + * File: biometricService.ts + * Description: Service for biometric authentication (stub) + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export const biometricService = { + authenticate: async () => { + // TODO: Implement biometric authentication + return true; + }, + isAvailable: async () => { + // TODO: Check if biometric is available + return true; + }, +}; + +/* + * End of File: biometricService.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/services/index.ts b/app/modules/Auth/services/index.ts new file mode 100644 index 0000000..c4028b5 --- /dev/null +++ b/app/modules/Auth/services/index.ts @@ -0,0 +1,15 @@ +/* + * File: index.ts + * Description: Barrel export for Auth services + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export * from './'; +export * from './biometricService'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/services/signupAPI.ts b/app/modules/Auth/services/signupAPI.ts new file mode 100644 index 0000000..aba2b12 --- /dev/null +++ b/app/modules/Auth/services/signupAPI.ts @@ -0,0 +1,392 @@ +/* + * File: signupAPI.ts + * Description: Signup API service with validation and hospital endpoints + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { create } from 'apisauce'; +import { SignUpData, EmailValidationApiResponse, UsernameValidationApiResponse, HospitalListApiResponse, SignUpApiResponse } from '../types/signup'; + +// ============================================================================ +// API CONFIGURATION +// ============================================================================ + +/** + * API Base Configuration + * + * Purpose: Configure the base API client for signup operations + * + * Features: + * - Base URL configuration + * - Request/response interceptors + * - Error handling + * - Timeout settings + */ +const API_BASE_URL = 'https://api.neoscan-physician.com/v1'; // TODO: Replace with actual API URL + +const api = create({ + baseURL: API_BASE_URL, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + timeout: 30000, // 30 seconds +}); + +// ============================================================================ +// REQUEST INTERCEPTORS +// ============================================================================ + +/** + * Request Interceptor + * + * Purpose: Add authentication headers and logging + */ +api.addRequestTransform((request) => { + // Add any common headers here + console.log('API Request:', { + method: request.method, + url: request.url, + data: request.data, + }); +}); + +// ============================================================================ +// RESPONSE INTERCEPTORS +// ============================================================================ + +/** + * Response Interceptor + * + * Purpose: Handle common response patterns and errors + */ +api.addResponseTransform((response) => { + console.log('API Response:', { + status: response.status, + url: response.config?.url, + data: response.data, + }); + + // Handle common error patterns + if (response.status === 401) { + // Handle unauthorized access + console.error('Unauthorized access'); + } + + if (response.status === 500) { + // Handle server errors + console.error('Server error occurred'); + } +}); + +// ============================================================================ +// EMAIL VALIDATION API +// ============================================================================ + +/** + * Validate Email + * + * Purpose: Check if email is available for registration + * + * @param email - Email address to validate + * @returns Promise with validation result + */ +export const validatemail = async (email: string): Promise => { + try { + const response = await api.post('/auth/validate-email', { + email, + }); + + if (response.ok && response.data) { + return response.data; + } else { + throw new Error(response.problem || 'Failed to validate email'); + } + } catch (error) { + console.error('Email validation error:', error); + throw error; + } +}; + +// ============================================================================ +// USERNAME VALIDATION API +// ============================================================================ + +/** + * Validate Username + * + * Purpose: Check if username is available for registration + * + * @param username - Username to validate + * @returns Promise with validation result + */ +export const validateusername = async (username: string): Promise => { + try { + const response = await api.post('/auth/validate-username', { + username, + }); + + if (response.ok && response.data) { + return response.data; + } else { + throw new Error(response.problem || 'Failed to validate username'); + } + } catch (error) { + console.error('Username validation error:', error); + throw error; + } +}; + +// ============================================================================ +// HOSPITAL API +// ============================================================================ + +/** + * Get Hospitals List + * + * Purpose: Fetch list of available hospitals + * + * @param params - Query parameters for filtering + * @returns Promise with hospital list + */ +export const gethospitals = async (params?: { + page?: number; + limit?: number; + search?: string; + type?: string; + city?: string; + state?: string; +}): Promise => { + try { + const response = await api.get('/hospitals', params); + + if (response.ok && response.data) { + return response.data; + } else { + throw new Error(response.problem || 'Failed to fetch hospitals'); + } + } catch (error) { + console.error('Get hospitals error:', error); + throw error; + } +}; + +/** + * Get Hospital by ID + * + * Purpose: Fetch specific hospital details + * + * @param hospitalId - Hospital ID + * @returns Promise with hospital details + */ +export const getHospitalById = async (hospitalId: string) => { + try { + const response = await api.get(`/hospitals/${hospitalId}`); + + if (response.ok && response.data) { + return response.data; + } else { + throw new Error(response.problem || 'Failed to fetch hospital details'); + } + } catch (error) { + console.error('Get hospital by ID error:', error); + throw error; + } +}; + +// ============================================================================ +// SIGNUP API +// ============================================================================ + +/** + * Complete Signup + * + * Purpose: Submit complete signup data + * + * @param formData - FormData with signup information + * @returns Promise with signup result + */ +export const signup = async (formData: FormData): Promise => { + try { + const response = await api.post('/auth/signup', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); +console.log('actual response ',response) + if (response.ok && response.data) { + return response.data; + } else { + throw new Error(response.problem || 'Failed to complete signup'); + } + } catch (error) { + console.error('Complete signup error:', error); + throw error; + } +}; + +// ============================================================================ +// MOCK API FUNCTIONS (FOR DEVELOPMENT) +// ============================================================================ + +/** + * Mock Validate Email + * + * Purpose: Mock email validation for development + * + * @param email - Email address to validate + * @returns Promise with mock validation result + */ +export const mockValidatemail = async (email: string): Promise => { + // Simulate API delay + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Mock validation logic + const isAvailable = email !== 'existing@hospital.com'; + + return { + success: true, + isAvailable, + message: isAvailable ? 'Email is available' : 'Email is already registered', + suggestions: isAvailable ? undefined : ['Try a different email address'], + }; +}; + +/** + * Mock Validate Username + * + * Purpose: Mock username validation for development + * + * @param username - Username to validate + * @returns Promise with mock validation result + */ +export const mockValidateusername = async (username: string): Promise => { + // Simulate API delay + await new Promise(resolve => setTimeout(resolve, 800)); + + // Mock validation logic + const isAvailable = username !== 'existinguser'; + + return { + success: true, + isAvailable, + message: isAvailable ? 'Username is available' : 'Username is already taken', + suggestions: isAvailable ? undefined : ['Try adding numbers or special characters'], + }; +}; + +/** + * Mock Get Hospitals + * + * Purpose: Mock hospital list for development + * + * @returns Promise with mock hospital list + */ +export const mockGethospitals = async (): Promise => { + // Simulate API delay + await new Promise(resolve => setTimeout(resolve, 1500)); + + // Mock hospital data + const mockHospitals = [ + { + id: '1', + name: 'General Hospital', + address: '123 Main Street', + city: 'New York', + state: 'NY', + country: 'USA', + phoneNumber: '+1-555-0123', + email: 'info@generalhospital.com', + website: 'https://generalhospital.com', + type: 'GENERAL' as const, + specialties: ['Emergency Medicine', 'Cardiology', 'Neurology'], + isActive: true, + }, + { + id: '2', + name: 'University Medical Center', + address: '456 University Ave', + city: 'Boston', + state: 'MA', + country: 'USA', + phoneNumber: '+1-555-0456', + email: 'info@umc.edu', + website: 'https://umc.edu', + type: 'UNIVERSITY' as const, + specialties: ['Emergency Medicine', 'Trauma', 'Research'], + isActive: true, + }, + { + id: '3', + name: 'Specialty Medical Center', + address: '789 Specialty Blvd', + city: 'Los Angeles', + state: 'CA', + country: 'USA', + phoneNumber: '+1-555-0789', + email: 'info@specialtycenter.com', + website: 'https://specialtycenter.com', + type: 'SPECIALTY' as const, + specialties: ['Cardiology', 'Neurology', 'Oncology'], + isActive: true, + }, + ]; + + return { + success: true, + data: mockHospitals, + total: mockHospitals.length, + page: 1, + limit: 10, + }; +}; + +/** + * Mock Complete Signup + * + * Purpose: Mock signup completion for development + * + * @param signUpData - Complete signup data + * @returns Promise with mock signup result + */ +export const mockSignup = async (signUpData: SignUpData): Promise => { + // Simulate API delay + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Mock successful signup + return { + success: true, + message: 'Account created successfully', + data: { + userId: 'user_' + Date.now(), + email: signUpData.email, + token: 'mock_token_' + Date.now(), + }, + }; +}; + +// ============================================================================ +// EXPORTS +// ============================================================================ + +export const authAPI = { + // Real API functions + validatemail, + validateusername, + gethospitals, + getHospitalById, + signup, + + // Mock API functions (for development) + mockValidatemail, + mockValidateusername, + mockGethospitals, + mockSignup, +}; + +/* + * End of File: signupAPI.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Auth/types/signup.ts b/app/modules/Auth/types/signup.ts new file mode 100644 index 0000000..107d615 --- /dev/null +++ b/app/modules/Auth/types/signup.ts @@ -0,0 +1,256 @@ +/* + * File: signup.ts + * Description: Type definitions for signup flow + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// ============================================================================ +// SIGNUP STEP TYPES +// ============================================================================ + +/** + * SignUpStep Type + * + * Purpose: Define the different steps in the signup process + */ +export type SignUpStep = 'email' | 'password' | 'name' | 'document' | 'hospital'; + +// ============================================================================ +// SIGNUP DATA INTERFACES +// ============================================================================ + +/** + * SignUpData Interface + * + * Purpose: Complete signup data structure matching reference code + */ +export interface SignUpData { + // Email and Password + email: string; + password: string; + + // Personal Information + first_name: string; + last_name: string; + username: string; + + // Document + id_photo_url: string | null; + + // Hospital Information + hospital_id: string; +} + +// ============================================================================ +// STEP COMPONENT PROPS INTERFACES +// ============================================================================ + +/** + * Base Step Props Interface + * + * Purpose: Common props for all step components + */ +export interface BaseStepProps { + onBack: () => void; + data: Partial; + isLoading: boolean; +} + +/** + * Email Step Props Interface + * + * Purpose: Props for email step component + */ +export interface EmailStepProps extends BaseStepProps { + onContinue: (email: string) => void; +} + +/** + * Password Step Props Interface + * + * Purpose: Props for password step component + */ +export interface PasswordStepProps extends BaseStepProps { + onContinue: (password: string) => void; +} + +/** + * Name Step Props Interface + * + * Purpose: Props for name step component + */ +export interface NameStepProps extends BaseStepProps { + onContinue: (firstName: string, lastName: string, username: string) => void; +} + +/** + * Document Upload Step Props Interface + * + * Purpose: Props for document upload step component + */ +export interface DocumentUploadStepProps extends BaseStepProps { + onContinue: (documentUri: string) => void; +} + +/** + * Hospital Selection Step Props Interface + * + * Purpose: Props for hospital selection step component + */ +export interface HospitalSelectionStepProps extends BaseStepProps { + onContinue: (hospitalId: string) => void; + hospitals: Hospital[] | null; + hospitalLoading: boolean; +} + +// ============================================================================ +// VALIDATION INTERFACES +// ============================================================================ + +/** + * Validation Result Interface + * + * Purpose: Result of validation operations + */ +export interface ValidationResult { + isValid: boolean; + message?: string; +} + +/** + * Email Validation Result Interface + * + * Purpose: Result of email validation + */ +export interface EmailValidationResult extends ValidationResult { + isAvailable: boolean; + suggestions?: string[]; +} + +/** + * Username Validation Result Interface + * + * Purpose: Result of username validation + */ +export interface UsernameValidationResult extends ValidationResult { + isAvailable: boolean; + suggestions?: string[]; +} + +// ============================================================================ +// API RESPONSE INTERFACES +// ============================================================================ + +/** + * SignUp API Response Interface + * + * Purpose: Response from signup API + */ +export interface SignUpApiResponse { + success: boolean; + message: string; + data?: { + userId: string; + email: string; + token?: string; + }; + errors?: { + [key: string]: string[]; + }; +} + +/** + * Email Validation API Response Interface + * + * Purpose: Response from email validation API + */ +export interface EmailValidationApiResponse { + success: boolean; + isAvailable: boolean; + message: string; + suggestions?: string[]; +} + +/** + * Username Validation API Response Interface + * + * Purpose: Response from username validation API + */ +export interface UsernameValidationApiResponse { + success: boolean; + isAvailable: boolean; + message: string; + suggestions?: string[]; +} + +// ============================================================================ +// HOSPITAL INTERFACES +// ============================================================================ + +/** + * Hospital Interface + * + * Purpose: Hospital information for selection + */ +export interface Hospital { + hospital_id: string | null; + hospital_name: string | null; +} + +/** + * Hospital List API Response Interface + * + * Purpose: Response from hospital list API + */ +export interface HospitalListApiResponse { + success: boolean; + data: Hospital[]; + total: number; + page: number; + limit: number; +} + +// ============================================================================ +// FORM VALIDATION INTERFACES +// ============================================================================ + +/** + * Form Validation Rules Interface + * + * Purpose: Validation rules for form fields + */ +export interface FormValidationRules { + required?: boolean; + minLength?: number; + maxLength?: number; + pattern?: RegExp; + custom?: (value: any) => boolean; +} + +/** + * Form Field Validation Interface + * + * Purpose: Validation state for form fields + */ +export interface FormFieldValidation { + isValid: boolean; + message: string; + isDirty: boolean; + isTouched: boolean; +} + +/** + * Form Validation State Interface + * + * Purpose: Overall form validation state + */ +export interface FormValidationState { + [key: string]: FormFieldValidation; +} + +/* + * End of File: signup.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/components/BrainPredictionsOverview.tsx b/app/modules/Dashboard/components/BrainPredictionsOverview.tsx new file mode 100644 index 0000000..f124c79 --- /dev/null +++ b/app/modules/Dashboard/components/BrainPredictionsOverview.tsx @@ -0,0 +1,613 @@ +/* + * File: BrainPredictionsOverview.tsx + * Description: Component to display patient overview statistics based on different AI prediction scenarios for brain conditions + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + StyleSheet, + Dimensions, + ScrollView, +} from 'react-native'; +import { PieChart } from 'react-native-chart-kit'; +import { theme } from '../../../theme/theme'; +import { ERDashboard } from '../../../shared/types'; + +/** + * BrainPredictionsOverviewProps Interface + * + * Purpose: Defines the props required by the BrainPredictionsOverview component + * + * Props: + * - dashboard: ERDashboard object containing brain prediction statistics + */ +interface BrainPredictionsOverviewProps { + dashboard: ERDashboard; +} + +/** + * BrainPredictionsOverview Component + * + * Purpose: Display patient overview statistics based on different AI prediction scenarios + * + * Features: + * 1. Shows distribution of patients by brain condition predictions + * 2. Displays AI confidence levels for each prediction type + * 3. Shows critical vs non-critical brain conditions + * 4. Provides summary statistics for quick overview + * 5. Visualizes data using react-native-chart-kit pie chart + * + * Prediction Categories: + * - Hemorrhagic Conditions (IPH, IVH, SAH, SDH, EDH) + * - Ischemic Conditions (Stroke) + * - Structural Changes (Mass effect, Midline shift) + * - Normal Findings + */ +export const BrainPredictionsOverview: React.FC = ({ + dashboard, +}) => { + // ============================================================================ + // MOCK PREDICTION DATA + // ============================================================================ + + /** + * Mock prediction statistics based on brain conditions + * In a real app, this would come from the AI analysis API + */ + const predictionStats = { + // Hemorrhagic Conditions + intraparenchymal: { count: 2, confidence: 94, critical: true }, + intraventricular: { count: 1, confidence: 87, critical: true }, + subarachnoid: { count: 1, confidence: 87, critical: true }, + subdural: { count: 1, confidence: 89, critical: true }, + epidural: { count: 1, confidence: 96, critical: true }, + + // Ischemic Conditions + ischemic_stroke: { count: 1, confidence: 92, critical: true }, + + // Structural Changes + mass_effect: { count: 2, confidence: 91, critical: true }, + midline_shift: { count: 1, confidence: 96, critical: true }, + + // Normal Findings + normal_brain: { count: 3, confidence: 98, critical: false }, + + // Pending Analysis + pending_analysis: { count: 6, confidence: 0, critical: false }, + }; + + // ============================================================================ + // HELPER FUNCTIONS + // ============================================================================ + + /** + * getPredictionLabel Function + * + * Purpose: Get human-readable label for prediction type + * + * @param predictionType - Type of brain prediction + * @returns Human-readable label + */ + const getPredictionLabel = (predictionType: string) => { + if (!predictionType) { + return 'Unknown'; // Default label for undefined types + } + + const labels: { [key: string]: string } = { + intraparenchymal: 'IPH', + intraventricular: 'IVH', + subarachnoid: 'SAH', + subdural: 'SDH', + epidural: 'EDH', + ischemic_stroke: 'Stroke', + mass_effect: 'Mass Effect', + midline_shift: 'Midline Shift', + normal_brain: 'Normal', + pending_analysis: 'Pending', + }; + + return labels[predictionType] || predictionType; + }; + + /** + * getPredictionColor Function + * + * Purpose: Get color based on prediction type and criticality + * + * @param predictionType - Type of brain prediction + * @param isCritical - Whether the condition is critical + * @returns Color string for styling + */ + const getPredictionColor = (predictionType: string, isCritical: boolean) => { + if (!predictionType) { + return theme.colors.textSecondary; // Default color for undefined types + } + + // Define distinct colors for each prediction class + const predictionColors: { [key: string]: string } = { + intraparenchymal: '#E53E3E', // Red for IPH + intraventricular: '#C53030', // Dark Red for IVH + subarachnoid: '#F56565', // Light Red for SAH + subdural: '#FC8181', // Pink Red for SDH + epidural: '#E53E3E', // Red for EDH + ischemic_stroke: '#3182CE', // Blue for Stroke + mass_effect: '#805AD5', // Purple for Mass Effect + midline_shift: '#553C9A', // Dark Purple for Midline Shift + normal_brain: '#38A169', // Green for Normal + pending_analysis: '#D69E2E', // Yellow for Pending + }; + + return predictionColors[predictionType] || theme.colors.textSecondary; + }; + + // ============================================================================ + // COMPUTED VALUES + // ============================================================================ + + /** + * Calculate total patients + */ + const totalPatients = Object.values(predictionStats).reduce((sum, stats) => sum + stats.count, 0); + + /** + * Calculate total critical conditions + */ + const totalCritical = Object.values(predictionStats) + .filter(stats => stats.critical) + .reduce((sum, stats) => sum + stats.count, 0); + + /** + * Calculate total normal findings + */ + const totalNormal = predictionStats.normal_brain.count; + + /** + * Calculate total pending analysis + */ + const totalPending = predictionStats.pending_analysis.count; + + /** + * Calculate average confidence for critical conditions + */ + const criticalConditions = Object.values(predictionStats).filter(stats => stats.critical && stats.confidence > 0); + const averageConfidence = criticalConditions.length > 0 + ? Math.round(criticalConditions.reduce((sum, stats) => sum + stats.confidence, 0) / criticalConditions.length) + : 0; + + /** + * Get all prediction classes by count + */ + const allPredictions = Object.entries(predictionStats) + .filter(([_, stats]) => stats.count > 0) + .sort((a, b) => b[1].count - a[1].count); + + // ============================================================================ + // CHART DATA PREPARATION + // ============================================================================ + + /** + * Prepare chart data for react-native-chart-kit PieChart + */ + const chartData = Object.entries(predictionStats) + .filter(([_, stats]) => stats && stats.count > 0) // Filter out undefined or zero count entries + .map(([predictionType, stats]) => ({ + name: getPredictionLabel(predictionType), + population: stats.count, // react-native-chart-kit uses 'population' property + color: getPredictionColor(predictionType, stats.critical), + legendFontColor: theme.colors.textPrimary, + legendFontSize: 12, + confidence: stats.confidence, + isCritical: stats.critical, + })) + .filter(item => item.population > 0) // Additional filter to ensure no zero count items + .sort((a, b) => b.population - a.population); // Sort by count descending - removed limit to show all classes + + /** + * Chart configuration for react-native-chart-kit + */ + const chartConfig = { + backgroundColor: theme.colors.background, + backgroundGradientFrom: theme.colors.background, + backgroundGradientTo: theme.colors.background, + decimalPlaces: 0, + color: (opacity = 1) => `rgba(33, 150, 243, ${opacity})`, + labelColor: (opacity = 1) => `rgba(33, 33, 33, ${opacity})`, + style: { + borderRadius: 16, + }, + propsForDots: { + r: '6', + strokeWidth: '2', + stroke: theme.colors.primary, + }, + useShadowColorFromDataset: false, + }; + + // ============================================================================ + // RENDER FUNCTIONS + // ============================================================================ + + + + /** + * renderLegend Function + * + * Purpose: Render chart legend with confidence levels in grid layout + * + * @returns Legend component + */ + const renderLegend = () => ( + + Prediction Classes Breakdown + + {chartData.map((item, index) => ( + + + + {item.name} + + {item.population} patients + {item.confidence > 0 && ( + {item.confidence}% + )} + + ))} + + + ); + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + return ( + + {/* Section Header */} + + Brain AI Predictions Overview + + Patient distribution by AI-detected conditions + + + + {/* Pie Chart Section */} + + + + {chartData.length > 0 ? ( + + + {(() => { + try { + return ( + + ); + } catch (error) { + console.warn('PieChart error:', error); + return ( + + Chart temporarily unavailable + + ); + } + })()} + + {renderLegend()} + + ) : ( + + No data available for chart + + )} + + + + {/* Summary Statistics */} + + + Total Patients + {totalPatients} + + + Critical Cases + + {totalCritical} + + + + Pending Analysis + + {totalPending} + + + + Avg Confidence + + {averageConfidence}% + + + + + ); +}; + +// ============================================================================ +// STYLES SECTION +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container + container: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.large, + padding: theme.spacing.md, + marginVertical: theme.spacing.sm, + borderWidth: 1, + borderColor: theme.colors.border, + }, + + // Header section + header: { + // marginBottom: theme.spacing.lg, + }, + + // Main title + title: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + // Subtitle + subtitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + }, + + // Statistics grid + statsGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: theme.spacing.xs, + marginBottom: theme.spacing.lg, + }, + + // Individual stat card + statCard: { + flex: 1, + minWidth: '30%', + maxWidth: '32%', + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.sm, + borderWidth: 1, + borderColor: theme.colors.border, + borderLeftWidth: 3, + alignItems: 'center', + }, + + // Stat value + statValue: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + // Stat title + statTitle: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.semibold, + color: theme.colors.textPrimary, + textAlign: 'center', + marginBottom: theme.spacing.xs, + }, + + // Stat subtitle + statSubtitle: { + fontSize: theme.typography.fontSize.caption, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + textAlign: 'center', + }, + + // Chart section + chartSection: { + marginBottom: theme.spacing.lg, + }, + + // Chart title + chartTitle: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.semibold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + // Chart subtitle + chartSubtitle: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.md, + }, + + // Chart scroll view + chartScrollView: { + // Removed maxHeight to allow full content to be visible + }, + + // Chart content + chartContent: { + alignItems: 'center', + paddingHorizontal: theme.spacing.sm, + }, + + // Pie chart container + pieChartContainer: { + alignItems: 'center', + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.medium, + }, + + // Legend container + legendContainer: { + width: '100%', + paddingHorizontal: theme.spacing.sm, + backgroundColor: theme.colors.backgroundAlt, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.md, + }, + + // Legend title + legendTitle: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.semibold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.md, + textAlign: 'center', + }, + + // Legend grid container + legendGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: theme.spacing.sm, + }, + + // Legend grid item + legendGridItem: { + width: '48%', + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.small, + padding: theme.spacing.sm, + borderWidth: 1, + borderColor: theme.colors.border, + marginBottom: theme.spacing.xs, + }, + + // Legend item header + legendItemHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.xs, + }, + + // Legend color + legendColor: { + width: 12, + height: 12, + borderRadius: 6, + marginRight: theme.spacing.xs, + }, + + // Legend label + legendLabel: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.semibold, + color: theme.colors.textPrimary, + flex: 1, + }, + + // Legend count + legendCount: { + fontSize: theme.typography.fontSize.caption, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + // Legend confidence + legendConfidence: { + fontSize: theme.typography.fontSize.caption, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + }, + + // No data container + noDataContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: theme.spacing.md, + }, + + // No data text + noDataText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + }, + + // Summary container + summaryContainer: { + flexDirection: 'row', + justifyContent: 'space-around', + paddingTop: theme.spacing.md, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + + // Summary item + summaryItem: { + alignItems: 'center', + }, + + // Summary label + summaryLabel: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xs, + }, + + // Summary value + summaryValue: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + }, + + // Error container + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: theme.spacing.md, + backgroundColor: theme.colors.backgroundAlt, + borderRadius: theme.borderRadius.medium, + }, + + // Error text + errorText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + textAlign: 'center', + }, +}); + +/* + * End of File: BrainPredictionsOverview.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/components/CriticalAlerts.tsx b/app/modules/Dashboard/components/CriticalAlerts.tsx new file mode 100644 index 0000000..3a2a4d6 --- /dev/null +++ b/app/modules/Dashboard/components/CriticalAlerts.tsx @@ -0,0 +1,189 @@ +/* + * File: CriticalAlerts.tsx + * Description: Critical alerts component displaying emergency notifications + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + ScrollView, +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import { Alert } from '../../../shared/types/alerts'; + +interface CriticalAlertsProps { + alerts: Alert[]; + onAlertPress: (alert: Alert) => void; +} + +export const CriticalAlerts: React.FC = ({ + alerts, + onAlertPress, +}) => { + if (alerts.length === 0) { + return null; + } + + return ( + + + ๐Ÿšจ Critical Alerts + {alerts.length} + + + + {alerts.map((alert) => ( + onAlertPress(alert)} + activeOpacity={0.7} + > + + {alert.type.replace('_', ' ')} + + {new Date(alert.timestamp).toLocaleTimeString()} + + + + {alert.title} + + {alert.message} + + + {alert.patientName && ( + + {alert.patientName} + {alert.bedNumber && ( + Bed {alert.bedNumber} + )} + + )} + + {alert.actionRequired && ( + + Action Required + + )} + + ))} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + backgroundColor: theme.colors.criticalBackground, + borderColor: theme.colors.critical, + borderWidth: 1, + borderRadius: theme.borderRadius.large, + padding: theme.spacing.md, + marginBottom: theme.spacing.lg, + ...theme.shadows.critical, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: theme.spacing.md, + }, + title: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.critical, + }, + count: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.critical, + backgroundColor: theme.colors.background, + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + borderRadius: theme.borderRadius.small, + }, + scrollContainer: { + paddingRight: theme.spacing.md, + }, + alertCard: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.md, + marginRight: theme.spacing.md, + // minWidth: 280, + ...theme.shadows.small, + width:280 + }, + alertHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + alertType: { + fontSize: theme.typography.fontSize.bodySmall, + color: theme.colors.critical, + textTransform: 'uppercase', + }, + alertTime: { + fontSize: theme.typography.fontSize.caption, + color: theme.colors.textMuted, + }, + alertTitle: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + alertMessage: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.sm, + lineHeight: theme.typography.lineHeight.normal * theme.typography.fontSize.bodyMedium, + }, + patientInfo: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + patientName: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textPrimary, + }, + bedNumber: { + fontSize: theme.typography.fontSize.bodySmall, + color: theme.colors.textSecondary, + backgroundColor: theme.colors.backgroundAccent, + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + borderRadius: theme.borderRadius.small, + }, + actionRequired: { + backgroundColor: theme.colors.critical, + borderRadius: theme.borderRadius.small, + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + alignSelf: 'flex-start', + }, + actionText: { + fontSize: theme.typography.fontSize.caption, + color: theme.colors.background, + textTransform: 'uppercase', + }, +}); + +/* + * End of File: CriticalAlerts.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/components/DashboardHeader.tsx b/app/modules/Dashboard/components/DashboardHeader.tsx new file mode 100644 index 0000000..5606b63 --- /dev/null +++ b/app/modules/Dashboard/components/DashboardHeader.tsx @@ -0,0 +1,60 @@ +/* + * File: DashboardHeader.tsx + * Description: Dashboard header component displaying ER department overview and statistics + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + StyleSheet, +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import { ERDashboard } from '../../../shared/types/dashboard'; + +interface DashboardHeaderProps { + dashboard: ERDashboard; +} + +export const DashboardHeader: React.FC = ({ + dashboard, +}) => { + return ( + + + Emergency Department + + {dashboard.shiftInfo.currentShift} Shift โ€ข {dashboard.shiftInfo.attendingPhysician} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.large, + padding: theme.spacing.lg, + marginBottom: theme.spacing.lg, + ...theme.shadows.medium, + }, + title: { + fontSize: theme.typography.fontSize.displayMedium, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + subtitle: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textSecondary, + }, +}); + +/* + * End of File: DashboardHeader.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/components/DepartmentStats.tsx b/app/modules/Dashboard/components/DepartmentStats.tsx new file mode 100644 index 0000000..6e684da --- /dev/null +++ b/app/modules/Dashboard/components/DepartmentStats.tsx @@ -0,0 +1,102 @@ +/* + * File: DepartmentStats.tsx + * Description: Department statistics component displaying patient counts per department + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + StyleSheet, +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import { DepartmentStats as DepartmentStatsType } from '../../../shared/types/dashboard'; + +interface DepartmentStatsProps { + stats: DepartmentStatsType; +} + +export const DepartmentStats: React.FC = ({ + stats, +}) => { + const departments = [ + { key: 'emergency', label: 'Emergency', color: theme.colors.primary }, + { key: 'trauma', label: 'Trauma', color: theme.colors.critical }, + { key: 'cardiac', label: 'Cardiac', color: theme.colors.warning }, + { key: 'neurology', label: 'Neurology', color: theme.colors.info }, + { key: 'pediatrics', label: 'Pediatrics', color: theme.colors.success }, + { key: 'icu', label: 'ICU', color: theme.colors.secondary }, + ]; + + return ( + + Department Overview + + {departments.map((dept) => ( + + + + + {stats[dept.key as keyof DepartmentStatsType]} + + {dept.label} + + + ))} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.large, + padding: theme.spacing.lg, + marginBottom: theme.spacing.lg, + ...theme.shadows.medium, + }, + title: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.lg, + }, + statsGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + }, + statItem: { + flexDirection: 'row', + alignItems: 'center', + width: '48%', + marginBottom: theme.spacing.md, + }, + colorIndicator: { + width: 12, + height: 12, + borderRadius: 6, + marginRight: theme.spacing.sm, + }, + statContent: { + flex: 1, + }, + statValue: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + }, + statLabel: { + fontSize: theme.typography.fontSize.bodySmall, + color: theme.colors.textSecondary, + }, +}); + +/* + * End of File: DepartmentStats.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/components/FeedbackAnalysisPieChart.tsx b/app/modules/Dashboard/components/FeedbackAnalysisPieChart.tsx new file mode 100644 index 0000000..c20536a --- /dev/null +++ b/app/modules/Dashboard/components/FeedbackAnalysisPieChart.tsx @@ -0,0 +1,337 @@ +/* + * File: FeedbackAnalysisPieChart.tsx + * Description: Pie chart component for feedback analysis using react-native-chart-kit + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { View, Text, StyleSheet, Dimensions } from 'react-native'; +import { PieChart } from 'react-native-chart-kit'; +import { theme } from '../../../theme/theme'; + +// ============================================================================ +// TYPES +// ============================================================================ + +/** + * Feedback Analysis Data Interface + * + * Purpose: Defines the structure of feedback analysis data for pie chart + */ +interface FeedbackAnalysisData { + positive: number; + negative: number; + total: number; +} + +/** + * FeedbackAnalysisPieChart Props Interface + * + * Purpose: Defines the props required by the FeedbackAnalysisPieChart component + * + * Props: + * - data: Feedback analysis data containing positive, negative, and total counts + * - title: Optional title for the chart + * - width: Chart width (defaults to screen width - 32) + * - height: Chart height (defaults to 220) + */ +interface FeedbackAnalysisPieChartProps { + data: FeedbackAnalysisData; + title?: string; + width?: number; + height?: number; +} + +// ============================================================================ +// COMPONENT +// ============================================================================ + +/** + * FeedbackAnalysisPieChart Component + * + * Purpose: Renders a pie chart showing feedback analysis distribution + * + * Features: + * - Pie chart visualization of positive vs negative feedback + * - Custom colors for different feedback types + * - Responsive sizing + * - Legend with percentages + * - Empty state handling + */ +export const FeedbackAnalysisPieChart: React.FC = ({ + data, + title = 'Feedback Analysis Overview', + width = Dimensions.get('window').width - 32, + height = 220, +}) => { + // ============================================================================ + // DATA PROCESSING + // ============================================================================ + + /** + * Process data for pie chart + * + * Purpose: Convert feedback data into chart-kit format + */ + const chartData = React.useMemo(() => { + const { positive, negative } = data; + + // Only show data if there are actual feedbacks + if (positive === 0 && negative === 0) { + return []; + } + + const chartDataArray = []; + + // Add positive feedback data + if (positive > 0) { + chartDataArray.push({ + name: 'Positive', + population: positive, + color: theme.colors.success, + legendFontColor: theme.colors.textPrimary, + legendFontSize: 12, + }); + } + + // Add negative feedback data + if (negative > 0) { + chartDataArray.push({ + name: 'Negative', + population: negative, + color: theme.colors.error, + legendFontColor: theme.colors.textPrimary, + legendFontSize: 12, + }); + } + + return chartDataArray; + }, [data]); + + // ============================================================================ + // CHART CONFIGURATION + // ============================================================================ + + /** + * Chart configuration object + * + * Purpose: Configure pie chart appearance and behavior + */ + const chartConfig = { + backgroundColor: theme.colors.background, + backgroundGradientFrom: theme.colors.background, + backgroundGradientTo: theme.colors.background, + decimalPlaces: 0, + color: (opacity = 1) => theme.colors.primary, + labelColor: (opacity = 1) => theme.colors.textPrimary, + style: { + borderRadius: theme.borderRadius.medium, + }, + propsForDots: { + r: '6', + strokeWidth: '2', + stroke: theme.colors.primary, + }, + }; + + // ============================================================================ + // RENDER FUNCTIONS + // ============================================================================ + + /** + * Render empty state + * + * Purpose: Show message when no feedback data is available + */ + const renderEmptyState = () => ( + + No feedback data available + + Feedback will appear here once received + + + ); + + /** + * Render chart legend + * + * Purpose: Display custom legend with percentages + */ + const renderLegend = () => { + const { positive, negative, total } = data; + + if (total === 0) return null; + + const positivePercentage = ((positive / total) * 100).toFixed(1); + const negativePercentage = ((negative / total) * 100).toFixed(1); + + return ( + + + + + Positive: {positive} ({positivePercentage}%) + + + + + + Negative: {negative} ({negativePercentage}%) + + + + + Total Feedback: {total} + + + + ); + }; + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + {/* Chart Title */} + {/* {title && ( + {title} + )} */} + + {/* Chart Container */} + + {chartData.length > 0 ? ( + <> + {/* Pie Chart */} + + + {/* Custom Legend */} + {renderLegend()} + + ) : ( + renderEmptyState() + )} + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container + container: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + alignItems: 'center', + justifyContent: 'center', + minHeight: 250, + }, + + // Chart title styling + title: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.md, + textAlign: 'center', + }, + + // Chart container + chartContainer: { + alignItems: 'center', + justifyContent: 'center', + width: '100%', + flex: 1, + }, + + // Empty state styling + emptyState: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: theme.spacing.xl, + minHeight: 150, + }, + + // Empty state text styling + emptyStateText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xs, + }, + + // Empty state subtext styling + emptyStateSubtext: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + textAlign: 'center', + }, + + // Legend container styling + legendContainer: { + marginTop: theme.spacing.md, + alignItems: 'center', + }, + + // Legend item styling + legendItem: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + + // Legend color indicator styling + legendColor: { + width: 16, + height: 16, + borderRadius: 8, + marginRight: theme.spacing.sm, + }, + + // Legend text styling + legendText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + }, + + // Total container styling + totalContainer: { + marginTop: theme.spacing.sm, + paddingTop: theme.spacing.sm, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + alignItems: 'center', + }, + + // Total text styling + totalText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + }, +}); + +/* + * End of File: FeedbackAnalysisPieChart.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/Dashboard/components/PatientCard.tsx b/app/modules/Dashboard/components/PatientCard.tsx new file mode 100644 index 0000000..1a04ec6 --- /dev/null +++ b/app/modules/Dashboard/components/PatientCard.tsx @@ -0,0 +1,362 @@ +/* + * File: PatientCard.tsx + * Description: Patient card component displaying patient information and status + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import { Patient } from '../../../shared/types/patient'; +import { getPriorityColor, getStatusColor, calculateAge } from '../../../shared/utils/helpers'; + +/** + * PatientCardProps Interface + * + * Purpose: Defines the props required by the PatientCard component + * + * Props: + * - patient: Patient data object containing all patient information + * - onPress: Callback function triggered when the card is pressed + */ +interface PatientCardProps { + patient: Patient; + onPress: (patient: Patient) => void; +} + +/** + * PatientCard Component + * + * Purpose: Display comprehensive patient information in a compact, touchable card format + * + * Features: + * 1. Patient identification (name, age, gender, MRN) + * 2. Priority and status badges with color coding + * 3. Location information (bed, room, attending physician) + * 4. Real-time vital signs display + * 5. Allergy warnings (if any) + * 6. Current diagnosis and last update time + * + * Data Display: + * - Header: Patient name, demographics, and status badges + * - Location: Bed/room assignment and attending physician + * - Vital Signs: BP, HR, Temperature, Oxygen saturation + * - Allergies: Warning display for known allergies + * - Footer: Current diagnosis and timestamp + * + * Interaction: + * - Touchable card that navigates to detailed patient view + * - Visual feedback with activeOpacity for better UX + */ +export const PatientCard: React.FC = ({ + patient, + onPress, +}) => { + // ============================================================================ + // DATA PROCESSING + // ============================================================================ + + // Calculate patient age from date of birth + const age = calculateAge(patient.dateOfBirth); + + // Get color coding for priority and status badges + const priorityColor = getPriorityColor(patient.priority); // Color based on priority level + const statusColor = getStatusColor(patient.status); // Color based on patient status + + /** + * formatVitalSigns Function + * + * Purpose: Format vital signs data for display in the card + * + * Returns: + * - bp: Blood pressure in systolic/diastolic format + * - hr: Heart rate value + * - temp: Temperature in Celsius + * - o2: Oxygen saturation percentage + */ + const formatVitalSigns = () => { + const { vitalSigns } = patient; + return { + bp: `${vitalSigns.bloodPressure.systolic}/${vitalSigns.bloodPressure.diastolic}`, // Format: 120/80 + hr: vitalSigns.heartRate.value, // Heart rate: 75 + temp: vitalSigns.temperature.value, // Temperature: 37.2 + o2: vitalSigns.oxygenSaturation.value, // Oxygen: 98 + }; + }; + + // Format vital signs for display + const vitals = formatVitalSigns(); + + // ============================================================================ + // RENDER SECTION + // ============================================================================ + + return ( + onPress(patient)} // Navigate to patient details + activeOpacity={0.7} // Visual feedback on touch + > + {/* ======================================================================== + * HEADER SECTION - Patient identification and status badges + * ======================================================================== */} + + {/* Patient information section */} + + {/* Patient full name */} + + {patient.firstName} {patient.lastName} + + {/* Patient demographics and MRN */} + + {age} years โ€ข {patient.gender} โ€ข MRN: {patient.mrn} + + + + {/* Status badges section */} + + {/* Priority badge with color coding */} + + + {patient.priority} + + + {/* Status badge with color coding */} + + + {patient.status} + + + + + + {/* ======================================================================== + * LOCATION SECTION - Bed assignment and attending physician + * ======================================================================== */} + + {/* Bed and room location */} + + Bed {patient.bedNumber} โ€ข Room {patient.roomNumber} + + {/* Attending physician */} + + Dr. {patient.attendingPhysician} + + + + {/* ======================================================================== + * VITAL SIGNS SECTION - Real-time patient vitals + * ======================================================================== */} + + {/* Blood Pressure */} + + BP + {vitals.bp} + + {/* Heart Rate */} + + HR + {vitals.hr} + + {/* Temperature */} + + Temp + {vitals.temp}ยฐC + + {/* Oxygen Saturation */} + + Oโ‚‚ + {vitals.o2}% + + + + {/* ======================================================================== + * ALLERGIES SECTION - Warning display for known allergies + * ======================================================================== */} + {patient.allergies.length > 0 && ( + + Allergies: + + {patient.allergies.map(a => a.name).join(', ')} + + + )} + + {/* ======================================================================== + * FOOTER SECTION - Diagnosis and last update time + * ======================================================================== */} + + {/* Current diagnosis */} + + {patient.currentDiagnosis} + + {/* Last update timestamp */} + + Updated {new Date(patient.lastUpdated).toLocaleTimeString()} + + + + ); +}; + +// ============================================================================ +// STYLES SECTION +// ============================================================================ + +const styles = StyleSheet.create({ + // Main card container with shadow and rounded corners + container: { + backgroundColor: theme.colors.cardBackground, + borderRadius: theme.borderRadius.large, + padding: theme.spacing.md, + marginHorizontal: theme.spacing.md, + marginVertical: theme.spacing.sm, + ...theme.shadows.small, // Add subtle shadow for elevation + }, + + // Header section with patient info and badges + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: theme.spacing.sm, + }, + + // Patient information container + patientInfo: { + flex: 1, // Take available space + }, + + // Patient name styling + patientName: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + // Patient details styling (age, gender, MRN) + patientDetails: { + fontSize: theme.typography.fontSize.bodySmall, + color: theme.colors.textSecondary, + }, + + // Badges container for priority and status + badges: { + flexDirection: 'row', + gap: theme.spacing.xs, // Space between badges + }, + + // Individual badge styling + badge: { + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + borderRadius: theme.borderRadius.small, + }, + + // Badge text styling + badgeText: { + fontSize: theme.typography.fontSize.caption, + fontFamily: theme.typography.fontFamily.medium, + }, + + // Location information container + locationInfo: { + marginBottom: theme.spacing.sm, + }, + + // Bed and room location text + locationText: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.medium, + }, + + // Attending physician text + attendingText: { + fontSize: theme.typography.fontSize.bodySmall, + color: theme.colors.textSecondary, + }, + + // Vital signs container with background + vitalSigns: { + flexDirection: 'row', + justifyContent: 'space-between', + backgroundColor: theme.colors.backgroundAccent, // Light background for vitals + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.sm, + marginBottom: theme.spacing.sm, + }, + + // Individual vital sign item + vitalItem: { + alignItems: 'center', // Center align label and value + }, + + // Vital sign label (BP, HR, Temp, Oโ‚‚) + vitalLabel: { + fontSize: theme.typography.fontSize.caption, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + marginBottom: theme.spacing.xs, + }, + + // Vital sign value styling + vitalValue: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + + // Allergies container + allergiesContainer: { + marginBottom: theme.spacing.sm, + }, + + // Allergies label with warning color + allergiesLabel: { + fontSize: theme.typography.fontSize.bodySmall, + color: theme.colors.warning, // Warning color for allergies + fontFamily: theme.typography.fontFamily.medium, + marginBottom: theme.spacing.xs, + }, + + // Allergies text listing + allergiesText: { + fontSize: theme.typography.fontSize.bodySmall, + color: theme.colors.textSecondary, + }, + + // Footer container with diagnosis and timestamp + footer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + + // Current diagnosis text + diagnosisText: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.medium, + flex: 1, // Take available space + }, + + // Last update timestamp + timeText: { + fontSize: theme.typography.fontSize.caption, + color: theme.colors.textMuted, + }, +}); + +/* + * End of File: PatientCard.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/components/QuickActions.tsx b/app/modules/Dashboard/components/QuickActions.tsx new file mode 100644 index 0000000..5d029a1 --- /dev/null +++ b/app/modules/Dashboard/components/QuickActions.tsx @@ -0,0 +1,139 @@ +/* + * File: QuickActions.tsx + * Description: Quick actions component providing common emergency actions + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + ScrollView, +} from 'react-native'; +import { theme } from '../../../theme/theme'; + +interface QuickAction { + id: string; + title: string; + icon: string; + color: string; +} + +interface QuickActionsProps { + onQuickAction: (action: QuickAction) => void; +} + +const quickActions: QuickAction[] = [ + { + id: 'emergency', + title: 'Emergency', + icon: '๐Ÿšจ', + color: theme.colors.critical, + }, + { + id: 'scan', + title: 'Order Scan', + icon: '๐Ÿ“ท', + color: theme.colors.primary, + }, + { + id: 'medication', + title: 'Medication', + icon: '๐Ÿ’Š', + color: theme.colors.warning, + }, + { + id: 'lab', + title: 'Lab Work', + icon: '๐Ÿงช', + color: theme.colors.info, + }, + { + id: 'consult', + title: 'Consult', + icon: '๐Ÿ‘จโ€โš•๏ธ', + color: theme.colors.success, + }, + { + id: 'transfer', + title: 'Transfer', + icon: '๐Ÿš‘', + color: theme.colors.secondary, + }, +]; + +export const QuickActions: React.FC = ({ + onQuickAction, +}) => { + return ( + + Quick Actions + + {quickActions.map((action) => ( + onQuickAction(action)} + activeOpacity={0.7} + > + + {action.icon} + + {action.title} + + ))} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + marginBottom: theme.spacing.lg, + }, + title: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.md, + }, + scrollContainer: { + paddingRight: theme.spacing.md, + }, + actionButton: { + alignItems: 'center', + marginRight: theme.spacing.lg, + minWidth: 80, + }, + iconContainer: { + width: 60, + height: 60, + borderRadius: 30, + justifyContent: 'center', + alignItems: 'center', + marginBottom: theme.spacing.sm, + ...theme.shadows.small, + }, + icon: { + fontSize: 24, + }, + actionTitle: { + fontSize: theme.typography.fontSize.bodySmall, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.medium, + textAlign: 'center', + }, +}); + +/* + * End of File: QuickActions.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/components/index.ts b/app/modules/Dashboard/components/index.ts new file mode 100644 index 0000000..f893da3 --- /dev/null +++ b/app/modules/Dashboard/components/index.ts @@ -0,0 +1,7 @@ +export { PatientCard } from './PatientCard'; +export { CriticalAlerts } from './CriticalAlerts'; +export { DashboardHeader } from './DashboardHeader'; +export { QuickActions } from './QuickActions'; +export { DepartmentStats } from './DepartmentStats'; +export { BrainPredictionsOverview } from './BrainPredictionsOverview'; +export { FeedbackAnalysisPieChart } from './FeedbackAnalysisPieChart'; \ No newline at end of file diff --git a/app/modules/Dashboard/hooks/index.ts b/app/modules/Dashboard/hooks/index.ts new file mode 100644 index 0000000..e5736d3 --- /dev/null +++ b/app/modules/Dashboard/hooks/index.ts @@ -0,0 +1,14 @@ +/* + * File: index.ts + * Description: Dashboard hooks exports + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export * from './useAIDashboard'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/Dashboard/hooks/useAIDashboard.ts b/app/modules/Dashboard/hooks/useAIDashboard.ts new file mode 100644 index 0000000..4b1e70c --- /dev/null +++ b/app/modules/Dashboard/hooks/useAIDashboard.ts @@ -0,0 +1,101 @@ +/* + * File: useAIDashboard.ts + * Description: Custom hook for AI dashboard functionality + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { AppDispatch } from '../../../store'; +import { selectAIDashboardData, selectAIDashboardError, selectAIDashboardLoading, selectAIDashboardRefreshing, selectDashboardMessage } from '../redux/aiDashboardSelectors'; +import { fetchAIDashboardStatistics, refreshAIDashboardStatistics } from '../redux/aiDashboardSlice'; +import { selectUser } from '../../Auth/redux'; + +// import { +// fetchAIDashboardStatistics, +// refreshAIDashboardStatistics, +// selectAIDashboardData, +// selectAIDashboardLoading, +// selectAIDashboardRefreshing, +// selectAIDashboardError, +// selectDashboardMessage +// } from '../redux'; + +/** + * useAIDashboard Custom Hook + * + * Purpose: Custom hook for AI dashboard functionality + * + * Features: + * - Fetch dashboard statistics from API + * - Refresh dashboard data + * - Access dashboard state from Redux + * - Handle authentication token + * + * @returns Object containing dashboard state and actions + */ +export const useAIDashboard = () => { + const dispatch = useDispatch(); + + // Select dashboard data from Redux store + const dashboardData = useSelector(selectAIDashboardData); + const isLoading = useSelector(selectAIDashboardLoading); + const isRefreshing = useSelector(selectAIDashboardRefreshing); + const error = useSelector(selectAIDashboardError); + const dashboardMessage = useSelector(selectDashboardMessage); + + // TODO: Get actual authentication token from auth store + // For now, using a placeholder token + const authToken = useSelector(selectUser)?.access_token; + + /** + * Fetch Dashboard Statistics + * + * Purpose: Fetch dashboard statistics from API + */ + const fetchDashboardStatistics = () => { + dispatch(fetchAIDashboardStatistics(authToken)); + }; + + /** + * Refresh Dashboard Statistics + * + * Purpose: Refresh dashboard statistics from API + */ + const refreshDashboardStatistics = () => { + dispatch(refreshAIDashboardStatistics(authToken)); + }; + + /** + * useEffect for initial data loading + * + * Purpose: Load initial dashboard data from API when hook is used + */ + useEffect(() => { + // Fetch dashboard statistics from API + fetchDashboardStatistics(); + }, []); + + return { + // State + dashboardData, + isLoading, + isRefreshing, + error, + dashboardMessage, + + // Actions + fetchDashboardStatistics, + refreshDashboardStatistics, + + // Constants + authToken + }; +}; + +/* + * End of File: useAIDashboard.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/Dashboard/index.ts b/app/modules/Dashboard/index.ts new file mode 100644 index 0000000..0d0feb1 --- /dev/null +++ b/app/modules/Dashboard/index.ts @@ -0,0 +1,124 @@ +/* + * File: index.ts + * Description: Main exports for Dashboard module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// Export screens +export { default as DashboardScreen } from './screens/DashboardScreen'; + +// Export navigation +export { + DashboardStackNavigator, + DashboardStackParamList, + DashboardNavigationProp, + DashboardScreenProps, + DashboardScreenProps, + PatientDetailsScreenProps, + AlertDetailsScreenProps, + DepartmentStatsScreenProps, + QuickActionsScreenProps, + navigateToERDashboard, + navigateToPatientDetails, + navigateToAlertDetails, + navigateToDepartmentStats, + navigateToQuickActions, + goBack, + resetToERDashboard, + replaceWithERDashboard, + navigateToERDashboardAndClearStack, + navigateToPatientDetailsAndClearStack, + navigateToAlertDetailsAndClearStack, +} from './navigation'; + +// Export components +export { default as PatientCard } from './components/PatientCard'; +export { default as CriticalAlerts } from './components/CriticalAlerts'; +export { default as DashboardHeader } from './components/DashboardHeader'; +export { default as QuickActions } from './components/QuickActions'; +export { default as DepartmentStats } from './components/DepartmentStats'; + +// Export hooks +export * from './hooks'; + +// Export Redux +export { + fetchDashboardData, + refreshDashboardData, + clearError, + setFilter, + setSort, + updateConnectionStatus, + updateLastUpdated, + updateDashboardData, +} from './redux/dashboardSlice'; + +// Export AI Dashboard Redux +export { + fetchAIDashboardStatistics, + refreshAIDashboardStatistics, + clearError as clearAIDashboardError, + setTimeRange, + setHospital, + setDepartment, + updateDashboardData as updateAIDashboardData, +} from './redux/aiDashboardSlice'; + +// Export AI Dashboard Selectors +export { + selectAIDashboardData, + selectAIDashboardLoading, + selectAIDashboardRefreshing, + selectAIDashboardError, + selectDashboardMessage, + selectTotalPredictions, + selectTotalPatients, + selectTotalFeedbacks, + selectFeedbackRatePercentage, + selectAverageConfidenceScore, + selectCriticalCasePercentage, + selectConfidenceScores, + selectUrgencyLevels, + selectFeedbackAnalysis, + selectTimeAnalysis, +} from './redux/aiDashboardSelectors'; + +export { + fetchAlerts, + acknowledgeAlert, + markAlertAsRead, + clearError as clearAlertsError, + setFilter as setAlertsFilter, + setSort as setAlertsSort, + addAlert, + removeAlert, + updateAlert, + clearAllAlerts, + markAllAsRead, +} from './redux/alertsSlice'; + +export { + setLoading, + showModal, + hideModal, + showOverlay, + hideOverlay, + setCurrentScreen, + clearNavigationStack, + toggleDarkMode, + setFontSize, + toggleHighContrast, + setRefreshing, + setScrolling, + updateLastInteraction, + showError, + clearError as clearUIError, + resetUIState, +} from './redux/uiSlice'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/navigation/DashboardStackNavigator.tsx b/app/modules/Dashboard/navigation/DashboardStackNavigator.tsx new file mode 100644 index 0000000..ee262f1 --- /dev/null +++ b/app/modules/Dashboard/navigation/DashboardStackNavigator.tsx @@ -0,0 +1,91 @@ +/* + * File: DashboardStackNavigator.tsx + * Description: Stack navigator for dashboard screens within the Dashboard module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; + +// Import dashboard screens +import { DashboardScreen } from '../screens/DashboardScreen'; + +// Import navigation types +import { DashboardStackParamList } from './navigationTypes'; +import { theme } from '../../../theme'; + +// Create stack navigator for Dashboard module +const Stack = createStackNavigator(); + +/** + * DashboardStackNavigator - Manages navigation between dashboard screens + * + * This navigator handles the flow between: + * - DashboardScreen: Main ER dashboard with patient overview + * - Future screens: Patient details, alerts, reports, etc. + * + * Features: + * - Clean header styling + * - Smooth transitions between screens + * - Type-safe navigation parameters + * - Healthcare-focused design + */ +const DashboardStackNavigator: React.FC = () => { + return ( + + {/* ER Dashboard Screen - Main dashboard entry point */} + + + ); +}; + +export default DashboardStackNavigator; + +/* + * End of File: DashboardStackNavigator.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/navigation/index.ts b/app/modules/Dashboard/navigation/index.ts new file mode 100644 index 0000000..fd2f28d --- /dev/null +++ b/app/modules/Dashboard/navigation/index.ts @@ -0,0 +1,47 @@ +/* + * File: index.ts + * Description: Barrel exports for Dashboard module navigation + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// Export main navigator +export { default as DashboardStackNavigator } from './DashboardStackNavigator'; + +// Export navigation types +export type { + DashboardStackParamList, + DashboardNavigationProp, + DashboardScreenProps, + DashboardScreenProps, + PatientDetailsScreenProps, + AlertDetailsScreenProps, + DepartmentStatsScreenProps, + QuickActionsScreenProps, + DashboardScreenParams, + PatientDetailsScreenParams, + AlertDetailsScreenParams, + DepartmentStatsScreenParams, + QuickActionsScreenParams, +} from './navigationTypes'; + +// Export navigation utilities +export { + navigateToERDashboard, + navigateToPatientDetails, + navigateToAlertDetails, + navigateToDepartmentStats, + navigateToQuickActions, + goBack, + resetToERDashboard, + replaceWithERDashboard, + navigateToERDashboardAndClearStack, + navigateToPatientDetailsAndClearStack, + navigateToAlertDetailsAndClearStack, +} from './navigationUtils'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/navigation/navigationTypes.ts b/app/modules/Dashboard/navigation/navigationTypes.ts new file mode 100644 index 0000000..a0a8b5a --- /dev/null +++ b/app/modules/Dashboard/navigation/navigationTypes.ts @@ -0,0 +1,171 @@ +/* + * File: navigationTypes.ts + * Description: TypeScript types for Dashboard module navigation + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { StackNavigationProp } from '@react-navigation/stack'; +import { Patient, Alert as AlertType, ERDashboard } from '../../../shared/types'; + +/** + * DashboardStackParamList - Defines the parameter list for Dashboard stack navigator + * + * This interface defines all the screens available in the Dashboard module + * and their associated navigation parameters. + */ +export type DashboardStackParamList = { + // ER Dashboard screen - Main dashboard with patient overview + ERDashboard: DashboardScreenParams; + + // Patient Details screen - Detailed patient information + PatientDetails: PatientDetailsScreenParams; + + // Alert Details screen - Detailed alert information + AlertDetails: AlertDetailsScreenParams; + + // Department Stats screen - Department-specific statistics + DepartmentStats: DepartmentStatsScreenParams; + + // Quick Actions screen - Quick action menu + QuickActions: QuickActionsScreenParams; +}; + +/** + * DashboardNavigationProp - Type for navigation prop in Dashboard screens + * + * This type provides type-safe navigation methods for screens + * within the Dashboard module. + */ +export type DashboardNavigationProp = StackNavigationProp; + +/** + * DashboardScreenProps - Base props interface for Dashboard screens + * + * This interface provides the common props that all Dashboard screens + * will receive, including navigation and route. + */ +export interface DashboardScreenProps { + navigation: DashboardNavigationProp; + route: { + key: string; + name: T; + params: DashboardStackParamList[T]; + }; +} + +// ============================================================================ +// SCREEN PARAMETER TYPES +// ============================================================================ + +/** + * DashboardScreenParams + * + * Purpose: Parameters passed to the ER dashboard screen + * + * Parameters: + * - filter: Optional filter to apply to dashboard data + * - refresh: Optional flag to force refresh + */ +export interface DashboardScreenParams { + filter?: 'all' | 'critical' | 'active' | 'pending'; + refresh?: boolean; +} + +/** + * PatientDetailsScreenParams + * + * Purpose: Parameters for the patient details screen + * + * Parameters: + * - patientId: Required patient ID to display details + * - patient: Optional patient data to pre-populate + * - fromScreen: Optional source screen for back navigation + */ +export interface PatientDetailsScreenParams { + patientId: string; + patient?: Patient; + fromScreen?: keyof DashboardStackParamList; +} + +/** + * AlertDetailsScreenParams + * + * Purpose: Parameters for the alert details screen + * + * Parameters: + * - alertId: Required alert ID to display details + * - alert: Optional alert data to pre-populate + * - fromScreen: Optional source screen for back navigation + */ +export interface AlertDetailsScreenParams { + alertId: string; + alert?: AlertType; + fromScreen?: keyof DashboardStackParamList; +} + +/** + * DepartmentStatsScreenParams + * + * Purpose: Parameters for the department stats screen + * + * Parameters: + * - department: Required department name + * - dateRange: Optional date range for statistics + */ +export interface DepartmentStatsScreenParams { + department: string; + dateRange?: { + start: Date; + end: Date; + }; +} + +/** + * QuickActionsScreenParams + * + * Purpose: Parameters for the quick actions screen + * + * Parameters: + * - actionType: Optional action type to pre-select + * - patientId: Optional patient ID for patient-specific actions + */ +export interface QuickActionsScreenParams { + actionType?: 'emergency' | 'scan' | 'report' | 'consultation'; + patientId?: string; +} + +// ============================================================================ +// SCREEN-SPECIFIC PROPS TYPES +// ============================================================================ + +/** + * DashboardScreenProps - Props for DashboardScreen component + */ +export type DashboardScreenProps = DashboardScreenProps<'ERDashboard'>; + +/** + * PatientDetailsScreenProps - Props for PatientDetailsScreen component + */ +export type PatientDetailsScreenProps = DashboardScreenProps<'PatientDetails'>; + +/** + * AlertDetailsScreenProps - Props for AlertDetailsScreen component + */ +export type AlertDetailsScreenProps = DashboardScreenProps<'AlertDetails'>; + +/** + * DepartmentStatsScreenProps - Props for DepartmentStatsScreen component + */ +export type DepartmentStatsScreenProps = DashboardScreenProps<'DepartmentStats'>; + +/** + * QuickActionsScreenProps - Props for QuickActionsScreen component + */ +export type QuickActionsScreenProps = DashboardScreenProps<'QuickActions'>; + +/* + * End of File: navigationTypes.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/navigation/navigationUtils.ts b/app/modules/Dashboard/navigation/navigationUtils.ts new file mode 100644 index 0000000..0c9f0c9 --- /dev/null +++ b/app/modules/Dashboard/navigation/navigationUtils.ts @@ -0,0 +1,209 @@ +/* + * File: navigationUtils.ts + * Description: Navigation utilities for Dashboard module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { DashboardNavigationProp } from './navigationTypes'; +import { Patient, Alert as AlertType } from '../../../shared/types'; + +/** + * DashboardNavigationUtils - Utility functions for Dashboard module navigation + * + * This module provides helper functions for common navigation patterns + * within the Dashboard module, ensuring consistent navigation behavior. + */ + +/** + * Navigate to ER Dashboard screen + * @param navigation - Navigation prop from React Navigation + * @param params - Optional parameters for the dashboard + */ +export const navigateToERDashboard = ( + navigation: DashboardNavigationProp, + params?: { + filter?: 'all' | 'critical' | 'active' | 'pending'; + refresh?: boolean; + } +): void => { + navigation.navigate('ERDashboard', params); +}; + +/** + * Navigate to Patient Details screen + * @param navigation - Navigation prop from React Navigation + * @param patientId - Required patient ID + * @param patient - Optional patient data + * @param fromScreen - Optional source screen + */ +export const navigateToPatientDetails = ( + navigation: DashboardNavigationProp, + patientId: string, + patient?: Patient, + fromScreen?: keyof import('./navigationTypes').DashboardStackParamList +): void => { + navigation.navigate('PatientDetails', { + patientId, + patient, + fromScreen, + }); +}; + +/** + * Navigate to Alert Details screen + * @param navigation - Navigation prop from React Navigation + * @param alertId - Required alert ID + * @param alert - Optional alert data + * @param fromScreen - Optional source screen + */ +export const navigateToAlertDetails = ( + navigation: DashboardNavigationProp, + alertId: string, + alert?: AlertType, + fromScreen?: keyof import('./navigationTypes').DashboardStackParamList +): void => { + navigation.navigate('AlertDetails', { + alertId, + alert, + fromScreen, + }); +}; + +/** + * Navigate to Department Stats screen + * @param navigation - Navigation prop from React Navigation + * @param department - Required department name + * @param dateRange - Optional date range for statistics + */ +export const navigateToDepartmentStats = ( + navigation: DashboardNavigationProp, + department: string, + dateRange?: { + start: Date; + end: Date; + } +): void => { + navigation.navigate('DepartmentStats', { + department, + dateRange, + }); +}; + +/** + * Navigate to Quick Actions screen + * @param navigation - Navigation prop from React Navigation + * @param actionType - Optional action type to pre-select + * @param patientId - Optional patient ID for patient-specific actions + */ +export const navigateToQuickActions = ( + navigation: DashboardNavigationProp, + actionType?: 'emergency' | 'scan' | 'report' | 'consultation', + patientId?: string +): void => { + navigation.navigate('QuickActions', { + actionType, + patientId, + }); +}; + +/** + * Go back to previous screen + * @param navigation - Navigation prop from React Navigation + */ +export const goBack = (navigation: DashboardNavigationProp): void => { + if (navigation.canGoBack()) { + navigation.goBack(); + } +}; + +/** + * Reset navigation stack to ER Dashboard screen + * @param navigation - Navigation prop from React Navigation + * @param params - Optional parameters for the dashboard + */ +export const resetToERDashboard = ( + navigation: DashboardNavigationProp, + params?: { + filter?: 'all' | 'critical' | 'active' | 'pending'; + refresh?: boolean; + } +): void => { + navigation.reset({ + index: 0, + routes: [{ name: 'ERDashboard', params }], + }); +}; + +/** + * Replace current screen with ER Dashboard screen + * @param navigation - Navigation prop from React Navigation + * @param params - Optional parameters for the dashboard + */ +export const replaceWithERDashboard = ( + navigation: DashboardNavigationProp, + params?: { + filter?: 'all' | 'critical' | 'active' | 'pending'; + refresh?: boolean; + } +): void => { + navigation.replace('ERDashboard', params); +}; + +/** + * Navigate to ER Dashboard and clear back stack + * @param navigation - Navigation prop from React Navigation + * @param params - Optional parameters for the dashboard + */ +export const navigateToERDashboardAndClearStack = ( + navigation: DashboardNavigationProp, + params?: { + filter?: 'all' | 'critical' | 'active' | 'pending'; + refresh?: boolean; + } +): void => { + navigation.reset({ + index: 0, + routes: [{ name: 'ERDashboard', params }], + }); +}; + +/** + * Navigate to Patient Details and clear back stack + * @param navigation - Navigation prop from React Navigation + * @param patientId - Required patient ID + * @param patient - Optional patient data + */ +export const navigateToPatientDetailsAndClearStack = ( + navigation: DashboardNavigationProp, + patientId: string, + patient?: Patient +): void => { + navigation.reset({ + index: 0, + routes: [{ name: 'PatientDetails', params: { patientId, patient } }], + }); +}; + +/** + * Navigate to Alert Details and clear back stack + * @param navigation - Navigation prop from React Navigation + * @param alertId - Required alert ID + * @param alert - Optional alert data + */ +export const navigateToAlertDetailsAndClearStack = ( + navigation: DashboardNavigationProp, + alertId: string, + alert?: AlertType +): void => { + navigation.reset({ + index: 0, + routes: [{ name: 'AlertDetails', params: { alertId, alert } }], + }); +}; + +/* + * End of File: navigationUtils.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/redux/aiDashboardSelectors.ts b/app/modules/Dashboard/redux/aiDashboardSelectors.ts new file mode 100644 index 0000000..4db5899 --- /dev/null +++ b/app/modules/Dashboard/redux/aiDashboardSelectors.ts @@ -0,0 +1,426 @@ +/* + * File: aiDashboardSelectors.ts + * Description: Selectors for AI dashboard state management + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from '../../../store'; + +// ============================================================================ +// BASE SELECTORS +// ============================================================================ + +/** + * Select AI Dashboard State + * + * Purpose: Get the entire AI dashboard state from root state + */ +const selectAIDashboardState = (state: RootState) => state.aiDashboard; + +/** + * Select AI Dashboard Data + * + * Purpose: Get the AI dashboard data from state + */ +export const selectAIDashboardData = createSelector( + [selectAIDashboardState], + (aiDashboard) => aiDashboard.dashboardData +); + +/** + * Select AI Dashboard Statistics + * + * Purpose: Get the AI dashboard statistics data + */ +export const selectAIDashboardStats = createSelector( + [selectAIDashboardData], + (dashboardData) => dashboardData?.data +); + +/** + * Select AI Dashboard Summary + * + * Purpose: Get the AI dashboard summary data + */ +export const selectAIDashboardSummary = createSelector( + [selectAIDashboardData], + (dashboardData) => dashboardData?.summary +); + +// ============================================================================ +// LOADING STATE SELECTORS +// ============================================================================ + +/** + * Select AI Dashboard Loading State + * + * Purpose: Get the loading state for AI dashboard + */ +export const selectAIDashboardLoading = createSelector( + [selectAIDashboardState], + (aiDashboard) => aiDashboard.isLoading +); + +/** + * Select AI Dashboard Refreshing State + * + * Purpose: Get the refreshing state for AI dashboard + */ +export const selectAIDashboardRefreshing = createSelector( + [selectAIDashboardState], + (aiDashboard) => aiDashboard.isRefreshing +); + +// ============================================================================ +// ERROR STATE SELECTORS +// ============================================================================ + +/** + * Select AI Dashboard Error + * + * Purpose: Get the error state for AI dashboard + */ +export const selectAIDashboardError = createSelector( + [selectAIDashboardState], + (aiDashboard) => aiDashboard.error +); + +// ============================================================================ +// FILTER STATE SELECTORS +// ============================================================================ + +/** + * Select Selected Time Range + * + * Purpose: Get the currently selected time range filter + */ +export const selectSelectedTimeRange = createSelector( + [selectAIDashboardState], + (aiDashboard) => aiDashboard.selectedTimeRange +); + +/** + * Select Selected Hospital + * + * Purpose: Get the currently selected hospital filter + */ +export const selectSelectedHospital = createSelector( + [selectAIDashboardState], + (aiDashboard) => aiDashboard.selectedHospital +); + +/** + * Select Selected Department + * + * Purpose: Get the currently selected department filter + */ +export const selectSelectedDepartment = createSelector( + [selectAIDashboardState], + (aiDashboard) => aiDashboard.selectedDepartment +); + +// ============================================================================ +// DERIVED DATA SELECTORS +// ============================================================================ + +/** + * Select Total Predictions Count + * + * Purpose: Get the total number of AI predictions + */ +export const selectTotalPredictions = createSelector( + [selectAIDashboardStats], + (stats) => stats?.total_predictions || 0 +); + +/** + * Select Total Patients Count + * + * Purpose: Get the total number of unique patients + */ +export const selectTotalPatients = createSelector( + [selectAIDashboardStats], + (stats) => stats?.total_patients || 0 +); + +/** + * Select Total Feedbacks Count + * + * Purpose: Get the total number of feedbacks received + */ +export const selectTotalFeedbacks = createSelector( + [selectAIDashboardStats], + (stats) => stats?.total_feedbacks || 0 +); + +/** + * Select Feedback Rate Percentage + * + * Purpose: Get the feedback rate as a percentage + */ +export const selectFeedbackRatePercentage = createSelector( + [selectAIDashboardStats], + (stats) => stats?.feedback_rate_percentage || 0 +); + +/** + * Select Average Confidence Score + * + * Purpose: Get the average confidence score for AI predictions + */ +export const selectAverageConfidenceScore = createSelector( + [selectAIDashboardStats], + (stats) => stats?.average_confidence_score || 0 +); + +/** + * Select Critical Case Percentage + * + * Purpose: Get the percentage of critical cases + */ +export const selectCriticalCasePercentage = createSelector( + [selectAIDashboardStats], + (stats) => stats?.critical_case_percentage || 0 +); + +// ============================================================================ +// CONFIDENCE SCORE SELECTORS +// ============================================================================ + +/** + * Select Confidence Score Distribution + * + * Purpose: Get the distribution of confidence scores + */ +export const selectConfidenceScores = createSelector( + [selectAIDashboardStats], + (stats) => stats?.confidence_scores || { high: 0, medium: 0, low: 0 } +); + +/** + * Select High Confidence Count + * + * Purpose: Get the count of high confidence predictions + */ +export const selectHighConfidenceCount = createSelector( + [selectConfidenceScores], + (confidenceScores) => confidenceScores.high +); + +/** + * Select Medium Confidence Count + * + * Purpose: Get the count of medium confidence predictions + */ +export const selectMediumConfidenceCount = createSelector( + [selectConfidenceScores], + (confidenceScores) => confidenceScores.medium +); + +/** + * Select Low Confidence Count + * + * Purpose: Get the count of low confidence predictions + */ +export const selectLowConfidenceCount = createSelector( + [selectConfidenceScores], + (confidenceScores) => confidenceScores.low +); + +// ============================================================================ +// URGENCY LEVEL SELECTORS +// ============================================================================ + +/** + * Select Urgency Level Distribution + * + * Purpose: Get the distribution of urgency levels + */ +export const selectUrgencyLevels = createSelector( + [selectAIDashboardStats], + (stats) => stats?.urgency_levels || { critical: 0, urgent: 0, routine: 0 } +); + +/** + * Select Critical Urgency Count + * + * Purpose: Get the count of critical urgency cases + */ +export const selectCriticalUrgencyCount = createSelector( + [selectUrgencyLevels], + (urgencyLevels) => urgencyLevels.critical +); + +/** + * Select Urgent Urgency Count + * + * Purpose: Get the count of urgent cases + */ +export const selectUrgentUrgencyCount = createSelector( + [selectUrgencyLevels], + (urgencyLevels) => urgencyLevels.urgent +); + +/** + * Select Routine Urgency Count + * + * Purpose: Get the count of routine cases + */ +export const selectRoutineUrgencyCount = createSelector( + [selectUrgencyLevels], + (urgencyLevels) => urgencyLevels.routine +); + +// ============================================================================ +// FEEDBACK ANALYSIS SELECTORS +// ============================================================================ + +/** + * Select Feedback Analysis Data + * + * Purpose: Get the feedback analysis data + */ +export const selectFeedbackAnalysis = createSelector( + [selectAIDashboardStats], + (stats) => stats?.feedback_analysis || { positive: 0, negative: 0, total: 0 } +); + +/** + * Select Positive Feedback Count + * + * Purpose: Get the count of positive feedbacks + */ +export const selectPositiveFeedbackCount = createSelector( + [selectFeedbackAnalysis], + (feedbackAnalysis) => feedbackAnalysis.positive +); + +/** + * Select Negative Feedback Count + * + * Purpose: Get the count of negative feedbacks + */ +export const selectNegativeFeedbackCount = createSelector( + [selectFeedbackAnalysis], + (feedbackAnalysis) => feedbackAnalysis.negative +); + +/** + * Select Total Feedback Count + * + * Purpose: Get the total count of feedbacks + */ +export const selectTotalFeedbackCount = createSelector( + [selectFeedbackAnalysis], + (feedbackAnalysis) => feedbackAnalysis.total +); + +// ============================================================================ +// TIME ANALYSIS SELECTORS +// ============================================================================ + +/** + * Select Time Analysis Data + * + * Purpose: Get the time-based analysis data + */ +export const selectTimeAnalysis = createSelector( + [selectAIDashboardStats], + (stats) => stats?.time_analysis || { today: 0, this_week: 0, this_month: 0, this_year: 0 } +); + +/** + * Select Today's Count + * + * Purpose: Get the count for today + */ +export const selectTodayCount = createSelector( + [selectTimeAnalysis], + (timeAnalysis) => timeAnalysis.today +); + +/** + * Select This Week's Count + * + * Purpose: Get the count for this week + */ +export const selectThisWeekCount = createSelector( + [selectTimeAnalysis], + (timeAnalysis) => timeAnalysis.this_week +); + +/** + * Select This Month's Count + * + * Purpose: Get the count for this month + */ +export const selectThisMonthCount = createSelector( + [selectTimeAnalysis], + (timeAnalysis) => timeAnalysis.this_month +); + +/** + * Select This Year's Count + * + * Purpose: Get the count for this year + */ +export const selectThisYearCount = createSelector( + [selectTimeAnalysis], + (timeAnalysis) => timeAnalysis.this_year +); + +// ============================================================================ +// LAST UPDATED SELECTORS +// ============================================================================ + +/** + * Select Last Updated Timestamp + * + * Purpose: Get the last updated timestamp + */ +export const selectLastUpdated = createSelector( + [selectAIDashboardState], + (aiDashboard) => aiDashboard.lastUpdated +); + +// ============================================================================ +// COMPUTED SELECTORS +// ============================================================================ + +/** + * Select Dashboard Message + * + * Purpose: Get the dashboard message or default message + */ +export const selectDashboardMessage = createSelector( + [selectAIDashboardData], + (dashboardData) => dashboardData?.message || 'Loading statistics...' +); + +/** + * Select Is Dashboard Empty + * + * Purpose: Check if dashboard has no data + */ +export const selectIsDashboardEmpty = createSelector( + [selectAIDashboardData], + (dashboardData) => !dashboardData || !dashboardData.data +); + +/** + * Select Has Dashboard Data + * + * Purpose: Check if dashboard has data + */ +export const selectHasDashboardData = createSelector( + [selectIsDashboardEmpty], + (isEmpty) => !isEmpty +); + +/* + * End of File: aiDashboardSelectors.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/Dashboard/redux/aiDashboardSlice.ts b/app/modules/Dashboard/redux/aiDashboardSlice.ts new file mode 100644 index 0000000..9309a3d --- /dev/null +++ b/app/modules/Dashboard/redux/aiDashboardSlice.ts @@ -0,0 +1,368 @@ +/* + * File: aiDashboardSlice.ts + * Description: AI Analysis Dashboard state management slice + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import { dashboardAPI } from '../services/dashboardAPI'; + +// ============================================================================ +// TYPES +// ============================================================================ + +/** + * AI Dashboard Statistics Interface + * + * Purpose: Defines the structure of AI dashboard statistics data + */ +export interface AIDashboardStats { + total_predictions: number; + total_patients: number; + total_feedbacks: number; + prediction_breakdown: Record; + critical_findings: Record; + midline_shift_stats: Record; + hemorrhage_stats: Record; + mass_lesion_stats: Record; + edema_stats: Record; + fracture_stats: Record; + feedback_analysis: { + positive: number; + negative: number; + total: number; + }; + hospital_distribution: Record; + time_analysis: { + today: number; + this_week: number; + this_month: number; + this_year: number; + }; + urgency_levels: { + critical: number; + urgent: number; + routine: number; + }; + confidence_scores: { + high: number; + medium: number; + low: number; + }; + feedback_rate_percentage: number; + predictions_with_feedback: number; + predictions_without_feedback: number; + average_feedback_per_prediction: string; + critical_case_percentage: number; + average_confidence_score: number; +} + +/** + * AI Dashboard Summary Interface + * + * Purpose: Defines the structure of the AI dashboard summary data + */ +export interface AIDashboardSummary { + total_cases: number; + critical_cases: number; + routine_cases: number; + feedback_coverage: string; + critical_case_rate: string; + average_confidence: string; +} + +/** + * Complete AI Dashboard Data Interface + * + * Purpose: Defines the complete structure of the AI dashboard API response + */ +export interface AIDashboardData { + success: boolean; + data: AIDashboardStats; + summary: AIDashboardSummary; + message: string; +} + +/** + * AI Dashboard State Interface + * + * Purpose: Defines the state structure for AI dashboard + */ +export interface AIDashboardState { + // Dashboard data + dashboardData: AIDashboardData | null; + + // Loading states + isLoading: boolean; + isRefreshing: boolean; + + // Error handling + error: string | null; + + // Filters and preferences + selectedTimeRange: 'today' | 'week' | 'month' | 'year'; + selectedHospital: string | null; + selectedDepartment: string | null; + + // Last updated timestamp + lastUpdated: string | null; +} + +// ============================================================================ +// ASYNC THUNKS +// ============================================================================ + +/** + * Fetch AI Dashboard Statistics Async Thunk + * + * Purpose: Fetch AI analysis dashboard statistics from API + * + * @param token - Authentication token + * @returns Promise with AI dashboard statistics data or error + */ +export const fetchAIDashboardStatistics = createAsyncThunk( + 'aiDashboard/fetchStatistics', + async (token: string, { rejectWithValue }) => { + try { + const response :any = await dashboardAPI.getDashboardStatistics(token); + console.log('statistics response',response); + + if (response.ok && response.data ) { + return response.data as AIDashboardData; + } else { + return rejectWithValue(response.problem || 'Failed to fetch dashboard statistics'); + } + } catch (error) { + return rejectWithValue('Network error occurred while fetching dashboard statistics'); + } + } +); + +/** + * Refresh AI Dashboard Statistics Async Thunk + * + * Purpose: Refresh AI dashboard statistics data + * + * @param token - Authentication token + * @returns Promise with refreshed AI dashboard statistics or error + */ +export const refreshAIDashboardStatistics = createAsyncThunk( + 'aiDashboard/refreshStatistics', + async (token: string, { rejectWithValue }) => { + try { + const response = await dashboardAPI.getDashboardStatistics(token); + + if (response.ok && response.data) { + return response.data as AIDashboardData; + } else { + return rejectWithValue(response.problem || 'Failed to refresh dashboard statistics'); + } + } catch (error) { + return rejectWithValue('Network error occurred while refreshing dashboard statistics'); + } + } +); + +/** + * Fetch Time-based Analysis Async Thunk + * + * Purpose: Fetch time-based analysis data for specific time range + * + * @param params - Parameters including token and time range + * @returns Promise with time-based analysis data or error + */ +export const fetchTimeBasedAnalysis = createAsyncThunk( + 'aiDashboard/fetchTimeAnalysis', + async (params: { token: string; timeRange: 'today' | 'week' | 'month' | 'year' }, { rejectWithValue }) => { + try { + const response = await dashboardAPI.getTimeBasedAnalysis(params.token, params.timeRange); + + if (response.ok && response.data) { + return response.data as AIDashboardData; + } else { + return rejectWithValue(response.problem || 'Failed to fetch time-based analysis'); + } + } catch (error) { + return rejectWithValue('Network error occurred while fetching time-based analysis'); + } + } +); + +// ============================================================================ +// INITIAL STATE +// ============================================================================ + +/** + * Initial AI Dashboard State + * + * Purpose: Define the initial state for AI dashboard + * + * Features: + * - AI dashboard statistics data + * - Loading states for async operations + * - Error handling and messages + * - Filter preferences + * - Last updated tracking + */ +const initialState: AIDashboardState = { + // Dashboard data + dashboardData: null, + + // Loading states + isLoading: false, + isRefreshing: false, + + // Error handling + error: null, + + // Filters and preferences + selectedTimeRange: 'today', + selectedHospital: null, + selectedDepartment: null, + + // Last updated timestamp + lastUpdated: null, +}; + +// ============================================================================ +// AI DASHBOARD SLICE +// ============================================================================ + +/** + * AI Dashboard Slice + * + * Purpose: Redux slice for AI dashboard state management + * + * Features: + * - AI dashboard statistics management + * - Time-based filtering + * - Hospital and department filtering + * - Error handling + * - Loading states + */ +const aiDashboardSlice = createSlice({ + name: 'aiDashboard', + initialState, + reducers: { + /** + * Clear Error Action + * + * Purpose: Clear AI dashboard errors + */ + clearError: (state) => { + state.error = null; + }, + + /** + * Set Time Range Filter Action + * + * Purpose: Set time range filter for statistics + */ + setTimeRange: (state, action: PayloadAction<'today' | 'week' | 'month' | 'year'>) => { + state.selectedTimeRange = action.payload; + }, + + /** + * Set Hospital Filter Action + * + * Purpose: Set hospital filter for statistics + */ + setHospital: (state, action: PayloadAction) => { + state.selectedHospital = action.payload; + }, + + /** + * Set Department Filter Action + * + * Purpose: Set department filter for statistics + */ + setDepartment: (state, action: PayloadAction) => { + state.selectedDepartment = action.payload; + }, + + /** + * Update Dashboard Data Action + * + * Purpose: Update dashboard data manually + */ + updateDashboardData: (state, action: PayloadAction>) => { + if (state.dashboardData) { + state.dashboardData = { ...state.dashboardData, ...action.payload }; + state.lastUpdated = new Date().toLocaleDateString(); + } + }, + }, + extraReducers: (builder) => { + // Fetch AI Dashboard Statistics + builder + .addCase(fetchAIDashboardStatistics.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(fetchAIDashboardStatistics.fulfilled, (state, action) => { + state.isLoading = false; + state.dashboardData = action.payload; + state.lastUpdated = new Date().toLocaleDateString(); + state.error = null; + }) + .addCase(fetchAIDashboardStatistics.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload as string; + }); + + // Refresh AI Dashboard Statistics + builder + .addCase(refreshAIDashboardStatistics.pending, (state) => { + state.isRefreshing = true; + state.error = null; + }) + .addCase(refreshAIDashboardStatistics.fulfilled, (state, action) => { + state.isRefreshing = false; + state.dashboardData = action.payload; + state.lastUpdated = new Date().toLocaleDateString(); + state.error = null; + }) + .addCase(refreshAIDashboardStatistics.rejected, (state, action) => { + state.isRefreshing = false; + state.error = action.payload as string; + }); + + // Fetch Time-based Analysis + builder + .addCase(fetchTimeBasedAnalysis.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(fetchTimeBasedAnalysis.fulfilled, (state, action) => { + state.isLoading = false; + state.dashboardData = action.payload; + state.lastUpdated = new Date().toLocaleDateString(); + state.error = null; + }) + .addCase(fetchTimeBasedAnalysis.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload as string; + }); + }, +}); + +// ============================================================================ +// EXPORTS +// ============================================================================ + +export const { + clearError, + setTimeRange, + setHospital, + setDepartment, + updateDashboardData, +} = aiDashboardSlice.actions; + +export default aiDashboardSlice.reducer; + +/* + * End of File: aiDashboardSlice.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/Dashboard/redux/alertsSlice.ts b/app/modules/Dashboard/redux/alertsSlice.ts new file mode 100644 index 0000000..beaf0ee --- /dev/null +++ b/app/modules/Dashboard/redux/alertsSlice.ts @@ -0,0 +1,341 @@ +/* + * File: alertsSlice.ts + * Description: Alerts state management slice + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import { AlertType, AlertsState } from '../../../shared/types'; + +// ============================================================================ +// ASYNC THUNKS +// ============================================================================ + +/** + * Fetch Alerts Async Thunk + * + * Purpose: Fetch alerts from API + * + * @returns Promise with alerts data or error + */ +export const fetchAlerts = createAsyncThunk( + 'alerts/fetchAlerts', + async (_, { rejectWithValue }) => { + try { + // TODO: Replace with actual API call + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Mock alerts data + const mockAlerts: AlertType[] = [ + { + id: '1', + type: 'CRITICAL_FINDING', + priority: 'CRITICAL', + title: 'Critical Finding Detected', + message: 'AI has detected a potential brain bleed in CT scan. Immediate review required.', + patientId: '1', + patientName: 'John Doe', + bedNumber: 'A1', + timestamp: new Date(), + isRead: false, + isAcknowledged: false, + actionRequired: true, + }, + { + id: '2', + type: 'VITAL_SIGNS_ALERT', + priority: 'HIGH', + title: 'Vital Signs Alert', + message: 'Patient vitals showing concerning trends. Blood pressure elevated.', + patientId: '2', + patientName: 'Jane Smith', + bedNumber: 'B2', + timestamp: new Date(Date.now() - 300000), // 5 minutes ago + isRead: true, + isAcknowledged: true, + actionRequired: false, + }, + ]; + + return mockAlerts; + } catch (error) { + return rejectWithValue('Failed to fetch alerts.'); + } + } +); + +/** + * Acknowledge Alert Async Thunk + * + * Purpose: Acknowledge an alert + * + * @param alertId - ID of the alert to acknowledge + * @returns Promise with success or error + */ +export const acknowledgeAlert = createAsyncThunk( + 'alerts/acknowledgeAlert', + async (alertId: string, { rejectWithValue }) => { + try { + // TODO: Replace with actual API call + await new Promise(resolve => setTimeout(resolve, 500)); + return alertId; + } catch (error) { + return rejectWithValue('Failed to acknowledge alert.'); + } + } +); + +/** + * Mark Alert as Read Async Thunk + * + * Purpose: Mark an alert as read + * + * @param alertId - ID of the alert to mark as read + * @returns Promise with success or error + */ +export const markAlertAsRead = createAsyncThunk( + 'alerts/markAlertAsRead', + async (alertId: string, { rejectWithValue }) => { + try { + // TODO: Replace with actual API call + await new Promise(resolve => setTimeout(resolve, 300)); + return alertId; + } catch (error) { + return rejectWithValue('Failed to mark alert as read.'); + } + } +); + +// ============================================================================ +// INITIAL STATE +// ============================================================================ + +/** + * Initial Alerts State + * + * Purpose: Define the initial state for alerts + * + * Features: + * - Alerts list and management + * - Loading states for async operations + * - Error handling and messages + * - Real-time updates tracking + */ +const initialState: AlertsState = { + // Alerts data + alerts: [], + + // Loading states + isLoading: false, + isRefreshing: false, + + // Error handling + error: null, + + // Real-time updates + lastUpdated: null, + unreadCount: 0, + criticalCount: 0, + + // Filters and preferences + selectedFilter: 'all', + sortBy: 'timestamp', + sortOrder: 'desc', +}; + +// ============================================================================ +// ALERTS SLICE +// ============================================================================ + +/** + * Alerts Slice + * + * Purpose: Redux slice for alerts state management + * + * Features: + * - Alerts data management + * - Real-time updates + * - Filtering and sorting + * - Error handling + * - Loading states + */ +const alertsSlice = createSlice({ + name: 'alerts', + initialState, + reducers: { + /** + * Clear Error Action + * + * Purpose: Clear alerts errors + */ + clearError: (state) => { + state.error = null; + }, + + /** + * Set Filter Action + * + * Purpose: Set alerts filter + */ + setFilter: (state, action: PayloadAction<'all' | 'critical' | 'unread' | 'acknowledged'>) => { + state.selectedFilter = action.payload; + }, + + /** + * Set Sort Action + * + * Purpose: Set alerts sort options + */ + setSort: (state, action: PayloadAction<{ by: string; order: 'asc' | 'desc' }>) => { + state.sortBy = action.payload.by; + state.sortOrder = action.payload.order; + }, + + /** + * Add Alert Action + * + * Purpose: Add a new alert + */ + addAlert: (state, action: PayloadAction) => { + state.alerts.unshift(action.payload); + state.unreadCount += 1; + if (action.payload.priority === 'CRITICAL') { + state.criticalCount += 1; + } + state.lastUpdated = new Date(); + }, + + /** + * Remove Alert Action + * + * Purpose: Remove an alert + */ + removeAlert: (state, action: PayloadAction) => { + const alertIndex = state.alerts.findIndex(alert => alert.id === action.payload); + if (alertIndex !== -1) { + const alert = state.alerts[alertIndex]; + if (!alert.isRead) { + state.unreadCount -= 1; + } + if (alert.priority === 'CRITICAL') { + state.criticalCount -= 1; + } + state.alerts.splice(alertIndex, 1); + } + }, + + /** + * Update Alert Action + * + * Purpose: Update an existing alert + */ + updateAlert: (state, action: PayloadAction<{ id: string; updates: Partial }>) => { + const alertIndex = state.alerts.findIndex(alert => alert.id === action.payload.id); + if (alertIndex !== -1) { + const oldAlert = state.alerts[alertIndex]; + state.alerts[alertIndex] = { ...oldAlert, ...action.payload.updates }; + + // Update counters + if (action.payload.updates.isRead !== undefined) { + if (action.payload.updates.isRead && !oldAlert.isRead) { + state.unreadCount -= 1; + } else if (!action.payload.updates.isRead && oldAlert.isRead) { + state.unreadCount += 1; + } + } + } + }, + + /** + * Clear All Alerts Action + * + * Purpose: Clear all alerts + */ + clearAllAlerts: (state) => { + state.alerts = []; + state.unreadCount = 0; + state.criticalCount = 0; + }, + + /** + * Mark All as Read Action + * + * Purpose: Mark all alerts as read + */ + markAllAsRead: (state) => { + state.alerts.forEach(alert => { + alert.isRead = true; + }); + state.unreadCount = 0; + }, + }, + extraReducers: (builder) => { + // Fetch Alerts + builder + .addCase(fetchAlerts.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(fetchAlerts.fulfilled, (state, action) => { + state.isLoading = false; + state.alerts = action.payload; + state.unreadCount = action.payload.filter(alert => !alert.isRead).length; + state.criticalCount = action.payload.filter(alert => alert.priority === 'CRITICAL').length; + state.lastUpdated = new Date(); + state.error = null; + }) + .addCase(fetchAlerts.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload as string; + }); + + // Acknowledge Alert + builder + .addCase(acknowledgeAlert.fulfilled, (state, action) => { + const alertIndex = state.alerts.findIndex(alert => alert.id === action.payload); + if (alertIndex !== -1) { + state.alerts[alertIndex].isAcknowledged = true; + } + }) + .addCase(acknowledgeAlert.rejected, (state, action) => { + state.error = action.payload as string; + }); + + // Mark Alert as Read + builder + .addCase(markAlertAsRead.fulfilled, (state, action) => { + const alertIndex = state.alerts.findIndex(alert => alert.id === action.payload); + if (alertIndex !== -1 && !state.alerts[alertIndex].isRead) { + state.alerts[alertIndex].isRead = true; + state.unreadCount -= 1; + } + }) + .addCase(markAlertAsRead.rejected, (state, action) => { + state.error = action.payload as string; + }); + }, +}); + +// ============================================================================ +// EXPORTS +// ============================================================================ + +export const { + clearError, + setFilter, + setSort, + addAlert, + removeAlert, + updateAlert, + clearAllAlerts, + markAllAsRead, +} = alertsSlice.actions; + +export default alertsSlice.reducer; + +/* + * End of File: alertsSlice.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/redux/dashboardSlice.ts b/app/modules/Dashboard/redux/dashboardSlice.ts new file mode 100644 index 0000000..73d198a --- /dev/null +++ b/app/modules/Dashboard/redux/dashboardSlice.ts @@ -0,0 +1,280 @@ +/* + * File: dashboardSlice.ts + * Description: Dashboard state management slice + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import { ERDashboard, DashboardState } from '../../../shared/types'; + +// ============================================================================ +// ASYNC THUNKS +// ============================================================================ + +/** + * Fetch Dashboard Data Async Thunk + * + * Purpose: Fetch dashboard data from API + * + * @returns Promise with dashboard data or error + */ +export const fetchDashboardData = createAsyncThunk( + 'dashboard/fetchDashboardData', + async (_, { rejectWithValue }) => { + try { + // TODO: Replace with actual API call + // Simulate API call with timeout + await new Promise(resolve => setTimeout(resolve, 1500)); + + // Mock dashboard data + const mockDashboard: ERDashboard = { + totalPatients: 24, + criticalPatients: 3, + pendingScans: 8, + recentReports: 12, + bedOccupancy: 85, + departmentStats: { + emergency: 8, + trauma: 4, + cardiac: 3, + neurology: 2, + pediatrics: 5, + icu: 2, + }, + shiftInfo: { + currentShift: 'DAY', + startTime: new Date(), + endTime: new Date(), + attendingPhysician: 'Dr. Smith', + residents: ['Dr. Johnson', 'Dr. Williams'], + nurses: ['Nurse Brown', 'Nurse Davis'], + }, + lastUpdated: new Date(), + }; + + return mockDashboard; + } catch (error) { + return rejectWithValue('Failed to fetch dashboard data.'); + } + } +); + +/** + * Refresh Dashboard Data Async Thunk + * + * Purpose: Refresh dashboard data + * + * @returns Promise with updated dashboard data or error + */ +export const refreshDashboardData = createAsyncThunk( + 'dashboard/refreshDashboardData', + async (_, { rejectWithValue }) => { + try { + // TODO: Replace with actual API call + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Mock refreshed dashboard data + const mockDashboard: ERDashboard = { + totalPatients: 26, + criticalPatients: 2, + pendingScans: 6, + recentReports: 15, + bedOccupancy: 87, + departmentStats: { + emergency: 9, + trauma: 3, + cardiac: 4, + neurology: 2, + pediatrics: 6, + icu: 2, + }, + shiftInfo: { + currentShift: 'DAY', + startTime: new Date(), + endTime: new Date(), + attendingPhysician: 'Dr. Smith', + residents: ['Dr. Johnson', 'Dr. Williams'], + nurses: ['Nurse Brown', 'Nurse Davis'], + }, + lastUpdated: new Date(), + }; + + return mockDashboard; + } catch (error) { + return rejectWithValue('Failed to refresh dashboard data.'); + } + } +); + +// ============================================================================ +// INITIAL STATE +// ============================================================================ + +/** + * Initial Dashboard State + * + * Purpose: Define the initial state for dashboard + * + * Features: + * - Dashboard data and statistics + * - Loading states for async operations + * - Error handling and messages + * - Real-time updates tracking + */ +const initialState: DashboardState = { + // Dashboard data + dashboard: null, + + // Loading states + isLoading: false, + isRefreshing: false, + + // Error handling + error: null, + + // Real-time updates + lastUpdated: null, + isConnected: true, + + // Filters and preferences + selectedFilter: 'all', + sortBy: 'priority', + sortOrder: 'desc', +}; + +// ============================================================================ +// DASHBOARD SLICE +// ============================================================================ + +/** + * Dashboard Slice + * + * Purpose: Redux slice for dashboard state management + * + * Features: + * - Dashboard data management + * - Real-time updates + * - Filtering and sorting + * - Error handling + * - Loading states + */ +const dashboardSlice = createSlice({ + name: 'dashboard', + initialState, + reducers: { + /** + * Clear Error Action + * + * Purpose: Clear dashboard errors + */ + clearError: (state) => { + state.error = null; + }, + + /** + * Set Filter Action + * + * Purpose: Set dashboard filter + */ + setFilter: (state, action: PayloadAction<'all' | 'critical' | 'active' | 'pending'>) => { + state.selectedFilter = action.payload; + }, + + /** + * Set Sort Action + * + * Purpose: Set dashboard sort options + */ + setSort: (state, action: PayloadAction<{ by: string; order: 'asc' | 'desc' }>) => { + state.sortBy = action.payload.by; + state.sortOrder = action.payload.order; + }, + + /** + * Update Connection Status Action + * + * Purpose: Update real-time connection status + */ + updateConnectionStatus: (state, action: PayloadAction) => { + state.isConnected = action.payload; + }, + + /** + * Update Last Updated Action + * + * Purpose: Update last updated timestamp + */ + updateLastUpdated: (state, action: PayloadAction) => { + state.lastUpdated = action.payload; + }, + + /** + * Update Dashboard Data Action + * + * Purpose: Update dashboard data manually + */ + updateDashboardData: (state, action: PayloadAction>) => { + if (state.dashboard) { + state.dashboard = { ...state.dashboard, ...action.payload }; + state.lastUpdated = new Date(); + } + }, + }, + extraReducers: (builder) => { + // Fetch Dashboard Data + builder + .addCase(fetchDashboardData.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(fetchDashboardData.fulfilled, (state, action) => { + state.isLoading = false; + state.dashboard = action.payload; + state.lastUpdated = action.payload.lastUpdated; + state.error = null; + }) + .addCase(fetchDashboardData.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload as string; + }); + + // Refresh Dashboard Data + builder + .addCase(refreshDashboardData.pending, (state) => { + state.isRefreshing = true; + state.error = null; + }) + .addCase(refreshDashboardData.fulfilled, (state, action) => { + state.isRefreshing = false; + state.dashboard = action.payload; + state.lastUpdated = action.payload.lastUpdated; + state.error = null; + }) + .addCase(refreshDashboardData.rejected, (state, action) => { + state.isRefreshing = false; + state.error = action.payload as string; + }); + }, +}); + +// ============================================================================ +// EXPORTS +// ============================================================================ + +export const { + clearError, + setFilter, + setSort, + updateConnectionStatus, + updateLastUpdated, + updateDashboardData, +} = dashboardSlice.actions; + +export default dashboardSlice.reducer; + +/* + * End of File: dashboardSlice.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/redux/index.ts b/app/modules/Dashboard/redux/index.ts new file mode 100644 index 0000000..59a10e7 --- /dev/null +++ b/app/modules/Dashboard/redux/index.ts @@ -0,0 +1,31 @@ +/* + * File: index.ts + * Description: Dashboard Redux exports + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// Dashboard Slice +export { default as dashboardReducer } from './dashboardSlice'; +export * from './dashboardSlice'; + +// AI Dashboard Slice +// export { default as aiDashboardReducer } from './aiDashboardSlice'; +// export * from './aiDashboardSlice'; + +// // UI Slice +// export { default as uiReducer } from './uiSlice'; +// export * from './uiSlice'; + +// // Alerts Slice +// export { default as alertsReducer } from './alertsSlice'; +// export * from './alertsSlice'; + +// // AI Dashboard Selectors +// export * from './aiDashboardSelectors'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/Dashboard/redux/uiSlice.ts b/app/modules/Dashboard/redux/uiSlice.ts new file mode 100644 index 0000000..f8891d5 --- /dev/null +++ b/app/modules/Dashboard/redux/uiSlice.ts @@ -0,0 +1,330 @@ +/* + * File: uiSlice.ts + * Description: UI state management slice + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +// ============================================================================ +// UI STATE INTERFACE +// ============================================================================ + +/** + * UI State Interface + * + * Purpose: Define the structure of UI state + * + * Features: + * - Loading states for different UI components + * - Modal and overlay management + * - Navigation state + * - Theme and appearance settings + * - User interaction states + */ +interface UIState { + // Loading states + isLoading: boolean; + loadingMessage: string | null; + + // Modal states + isModalOpen: boolean; + modalType: string | null; + modalData: any; + + // Overlay states + isOverlayVisible: boolean; + overlayType: string | null; + + // Navigation states + currentScreen: string | null; + navigationStack: string[]; + + // Theme and appearance + isDarkMode: boolean; + fontSize: 'small' | 'medium' | 'large'; + highContrast: boolean; + + // User interaction states + isRefreshing: boolean; + isScrolling: boolean; + lastInteraction: Date | null; + + // Error states + hasError: boolean; + errorMessage: string | null; + errorType: 'warning' | 'error' | 'info' | null; +} + +// ============================================================================ +// INITIAL STATE +// ============================================================================ + +/** + * Initial UI State + * + * Purpose: Define the initial state for UI + * + * Features: + * - Default loading states + * - Default modal and overlay states + * - Default theme settings + * - Default interaction states + */ +const initialState: UIState = { + // Loading states + isLoading: false, + loadingMessage: null, + + // Modal states + isModalOpen: false, + modalType: null, + modalData: null, + + // Overlay states + isOverlayVisible: false, + overlayType: null, + + // Navigation states + currentScreen: null, + navigationStack: [], + + // Theme and appearance + isDarkMode: false, + fontSize: 'medium', + highContrast: false, + + // User interaction states + isRefreshing: false, + isScrolling: false, + lastInteraction: null, + + // Error states + hasError: false, + errorMessage: null, + errorType: null, +}; + +// ============================================================================ +// UI SLICE +// ============================================================================ + +/** + * UI Slice + * + * Purpose: Redux slice for UI state management + * + * Features: + * - Loading state management + * - Modal and overlay control + * - Navigation state tracking + * - Theme and appearance settings + * - User interaction tracking + * - Error state management + */ +const uiSlice = createSlice({ + name: 'ui', + initialState, + reducers: { + /** + * Set Loading Action + * + * Purpose: Set loading state with optional message + */ + setLoading: (state, action: PayloadAction<{ isLoading: boolean; message?: string }>) => { + state.isLoading = action.payload.isLoading; + state.loadingMessage = action.payload.message || null; + }, + + /** + * Show Modal Action + * + * Purpose: Show a modal with specific type and data + */ + showModal: (state, action: PayloadAction<{ type: string; data?: any }>) => { + state.isModalOpen = true; + state.modalType = action.payload.type; + state.modalData = action.payload.data || null; + }, + + /** + * Hide Modal Action + * + * Purpose: Hide the current modal + */ + hideModal: (state) => { + state.isModalOpen = false; + state.modalType = null; + state.modalData = null; + }, + + /** + * Show Overlay Action + * + * Purpose: Show an overlay with specific type + */ + showOverlay: (state, action: PayloadAction<{ type: string }>) => { + state.isOverlayVisible = true; + state.overlayType = action.payload.type; + }, + + /** + * Hide Overlay Action + * + * Purpose: Hide the current overlay + */ + hideOverlay: (state) => { + state.isOverlayVisible = false; + state.overlayType = null; + }, + + /** + * Set Current Screen Action + * + * Purpose: Set the current screen name + */ + setCurrentScreen: (state, action: PayloadAction) => { + state.currentScreen = action.payload; + if (!state.navigationStack.includes(action.payload)) { + state.navigationStack.push(action.payload); + } + }, + + /** + * Clear Navigation Stack Action + * + * Purpose: Clear the navigation stack + */ + clearNavigationStack: (state) => { + state.navigationStack = []; + }, + + /** + * Toggle Dark Mode Action + * + * Purpose: Toggle dark mode on/off + */ + toggleDarkMode: (state) => { + state.isDarkMode = !state.isDarkMode; + }, + + /** + * Set Font Size Action + * + * Purpose: Set the font size preference + */ + setFontSize: (state, action: PayloadAction<'small' | 'medium' | 'large'>) => { + state.fontSize = action.payload; + }, + + /** + * Toggle High Contrast Action + * + * Purpose: Toggle high contrast mode + */ + toggleHighContrast: (state) => { + state.highContrast = !state.highContrast; + }, + + /** + * Set Refreshing Action + * + * Purpose: Set refreshing state + */ + setRefreshing: (state, action: PayloadAction) => { + state.isRefreshing = action.payload; + }, + + /** + * Set Scrolling Action + * + * Purpose: Set scrolling state + */ + setScrolling: (state, action: PayloadAction) => { + state.isScrolling = action.payload; + }, + + /** + * Update Last Interaction Action + * + * Purpose: Update the last interaction timestamp + */ + updateLastInteraction: (state) => { + state.lastInteraction = new Date(); + }, + + /** + * Show Error Action + * + * Purpose: Show an error message + */ + showError: (state, action: PayloadAction<{ message: string; type?: 'warning' | 'error' | 'info' }>) => { + state.hasError = true; + state.errorMessage = action.payload.message; + state.errorType = action.payload.type || 'error'; + }, + + /** + * Clear Error Action + * + * Purpose: Clear the current error + */ + clearError: (state) => { + state.hasError = false; + state.errorMessage = null; + state.errorType = null; + }, + + /** + * Reset UI State Action + * + * Purpose: Reset UI state to initial values + */ + resetUIState: (state) => { + state.isLoading = false; + state.loadingMessage = null; + state.isModalOpen = false; + state.modalType = null; + state.modalData = null; + state.isOverlayVisible = false; + state.overlayType = null; + state.isRefreshing = false; + state.isScrolling = false; + state.hasError = false; + state.errorMessage = null; + state.errorType = null; + }, + }, +}); + +// ============================================================================ +// EXPORTS +// ============================================================================ + +export const { + setLoading, + showModal, + hideModal, + showOverlay, + hideOverlay, + setCurrentScreen, + clearNavigationStack, + toggleDarkMode, + setFontSize, + toggleHighContrast, + setRefreshing, + setScrolling, + updateLastInteraction, + showError, + clearError, + resetUIState, +} = uiSlice.actions; + +export default uiSlice.reducer; + +/* + * End of File: uiSlice.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/screens/DashboardScreen.tsx b/app/modules/Dashboard/screens/DashboardScreen.tsx new file mode 100644 index 0000000..07b6c44 --- /dev/null +++ b/app/modules/Dashboard/screens/DashboardScreen.tsx @@ -0,0 +1,1253 @@ +/* + * File: DashboardScreen.tsx + * Description: AI Analysis Dashboard - Main dashboard for AI predictions and analysis statistics + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + TouchableOpacity, + RefreshControl, + FlatList, + Dimensions, +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import { DashboardHeader } from '../components/DashboardHeader'; +import { BrainPredictionsOverview } from '../components/BrainPredictionsOverview'; +import { FeedbackAnalysisPieChart } from '../components/FeedbackAnalysisPieChart'; +import { useAIDashboard } from '../hooks/useAIDashboard'; + +/** + * DashboardScreenProps Interface + * + * Purpose: Defines the props required by the DashboardScreen component + * + * Props: + * - navigation: React Navigation object for screen navigation + */ +interface DashboardScreenProps { + navigation: any; +} + +/** + * Dashboard Stats Data Interface + * + * Purpose: Defines the structure of the dashboard statistics data + */ +interface DashboardStats { + total_predictions: number; + total_patients: number; + total_feedbacks: number; + prediction_breakdown: Record; + critical_findings: Record; + midline_shift_stats: Record; + hemorrhage_stats: Record; + mass_lesion_stats: Record; + edema_stats: Record; + fracture_stats: Record; + feedback_analysis: { + positive: number; + negative: number; + total: number; + }; + hospital_distribution: Record; + time_analysis: { + today: number; + this_week: number; + this_month: number; + this_year: number; + }; + urgency_levels: { + critical: number; + urgent: number; + routine: number; + }; + confidence_scores: { + high: number; + medium: number; + low: number; + }; + feedback_rate_percentage: number; + predictions_with_feedback: number; + predictions_without_feedback: number; + average_feedback_per_prediction: string; + critical_case_percentage: number; + average_confidence_score: number; +} + +/** + * Dashboard Summary Interface + * + * Purpose: Defines the structure of the dashboard summary data + */ +interface DashboardSummary { + total_cases: number; + critical_cases: number; + routine_cases: number; + feedback_coverage: string; + critical_case_rate: string; + average_confidence: string; +} + +/** + * Complete Dashboard Data Interface + * + * Purpose: Defines the complete structure of the dashboard API response + */ +interface DashboardData { + success: boolean; + data: DashboardStats; + summary: DashboardSummary; + message: string; +} + +/** + * DashboardScreen Component + * + * Purpose: AI Analysis Dashboard for physicians showing prediction statistics + * + * Dashboard Features: + * 1. AI prediction statistics and breakdown + * 2. Feedback analysis and coverage metrics + * 3. Confidence score distribution + * 4. Time-based analysis trends + * 5. Urgency level distribution + * 6. Pull-to-refresh functionality for live updates + */ +export const DashboardScreen: React.FC = ({ + navigation, +}) => { + // ============================================================================ + // CUSTOM HOOKS + // ============================================================================ + + // Use custom hook for AI dashboard functionality + const { + dashboardData, + isLoading, + isRefreshing, + error, + dashboardMessage, + refreshDashboardStatistics + } = useAIDashboard(); + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * handleRefresh Function + * + * Purpose: Handle pull-to-refresh functionality to update dashboard data + */ + const handleRefresh = async () => { + // Refresh dashboard statistics from API + refreshDashboardStatistics(); + }; + + // ============================================================================ + // RENDER FUNCTIONS + // ============================================================================ + + /** + * renderErrorState Function + * + * Purpose: Render error state when there's a critical error + */ + const renderErrorState = () => { + if (!error) return null; + + return ( + + Connection Error + + Unable to connect to the dashboard service + + + โ€ข Check your internet connection + โ€ข Verify server status + โ€ข Try again in a few moments + + + Retry Connection + + + ); + }; + + /** + * renderNetworkStatus Function + * + * Purpose: Render network status indicator + */ + const renderNetworkStatus = () => { + if (!error && dashboardData) return null; + + return ( + + + {error ? 'โš ๏ธ Connection Issue' : '๐Ÿ”„ Checking Connection...'} + + + {error ? 'Please check your internet connection' : 'Verifying dashboard service'} + + + ); + }; + + /** + * renderNoDataState Function + * + * Purpose: Render no data state when dashboard loads but has no meaningful data + */ + const renderNoDataState = () => { + return ( + + + Dashboard is Ready + + Your AI Analysis Dashboard is ready, but there's no data to display yet. + + + โ€ข AI predictions will appear here once scans are processed + โ€ข Check back after medical scans are uploaded + โ€ข The system will automatically populate data + + + Check for Updates + + + + ); + }; + + /** + * renderStatsCard Function + * + * Purpose: Render individual statistics card component + * + * @param title - Card title + * @param value - Main value to display + * @param subtitle - Optional subtitle + * @param color - Optional color theme + * @returns Statistics card component + */ + const renderStatsCard = (title: string, value: string | number, subtitle?: string, color?: string) => ( + + {title} + {value} + {subtitle && {subtitle}} + + ); + + /** + * renderConfidenceBreakdown Function + * + * Purpose: Render confidence score breakdown section + */ + const renderConfidenceBreakdown = () => { + // Check if dashboard data exists + if (!dashboardData) { + return ( + + Confidence Score Distribution + + Dashboard data not available + + Retry + + + + ); + } + + // Check if confidence scores data exists + if (!dashboardData.data?.confidence_scores) { + return ( + + Confidence Score Distribution + + Confidence data not available + AI confidence scores are not currently accessible + + โ€ข AI system may be initializing + โ€ข Check system status + โ€ข Refresh in a few minutes + + + + ); + } + + const { high, medium, low } = dashboardData.data.confidence_scores; + + // Check if the object is empty or if all values are undefined/null/zero + if (!high && !medium && !low) { + return ( + + Confidence Score Distribution + + No data found + + + ); + } + + // Check if all required fields exist and are numbers + if (typeof high !== 'number' || typeof medium !== 'number' || typeof low !== 'number') { + return ( + + Confidence Score Distribution + + No confidence data available + + + ); + } + + const total = high + medium + low; + + // If no predictions, show empty state + if (total === 0) { + return ( + + Confidence Score Distribution + + No predictions available yet + AI predictions will appear here once the system processes medical scans + + + ); + } + + // Calculate percentages for better visualization + const highPercent = Math.round((high / total) * 100); + const mediumPercent = Math.round((medium / total) * 100); + const lowPercent = Math.round((low / total) * 100); + + // Helper function to get bar opacity + const getBarOpacity = (count: number) => { + if (count === 0) return 0.3; // Dimmed for zero values + return 0.9; // Full opacity for non-zero values + }; + + return ( + + Confidence Score Distribution + + {/* High Confidence */} + + + + High Confidence + {highPercent}% + + + + + {high} predictions + + + {/* Medium Confidence */} + + + + Medium Confidence + {mediumPercent}% + + + + + {medium} predictions + + + {/* Low Confidence */} + + + + Low Confidence + {lowPercent}% + + + + + {low} predictions + + + + {/* Summary Stats */} + + + Total Predictions: {total} + + + High Confidence Rate: {highPercent}% + + + + ); + }; + + /** + * renderUrgencyBreakdown Function + * + * Purpose: Render urgency level breakdown section + */ + const renderUrgencyBreakdown = () => { + if (!dashboardData?.data.urgency_levels) return null; + + const { critical, urgent, routine } = dashboardData.data.urgency_levels; + + // Check if the object is empty or if all values are undefined/null/zero + if (!critical && !urgent && !routine) { + return ( + + Case Urgency Distribution + + No data found + + + ); + } + + // Check if all values are zero (no cases) + if (critical === 0 && urgent === 0 && routine === 0) { + return ( + + Case Urgency Distribution + + No cases recorded yet + + + ); + } + + return ( + + Case Urgency Distribution + + + + Critical + {critical} + + + + Urgent + {urgent} + + + + Routine + {routine} + + + + ); + }; + + /** + * renderFeedbackAnalysis Function + * + * Purpose: Render feedback analysis section with pie chart + */ + const renderFeedbackAnalysis = () => { + if (!dashboardData?.data.feedback_analysis) return null; + + const { positive, negative, total } = dashboardData.data.feedback_analysis; + + // Check if the object is empty or if all values are undefined/null/zero + if (!positive && !negative && !total) { + return ( + + Feedback Analysis + + No data found + + + ); + } + + // Check if all values are zero (no feedback) + if (positive === 0 && negative === 0 && total === 0) { + return ( + + Feedback Analysis + + No feedback recorded yet + Feedback analysis will appear once users provide feedback + + โ€ข No user feedback has been submitted yet + + + + + ); + } + + return ( + + Feedback Analysis + + {/* Pie Chart */} + + + {/* Additional Feedback Metrics */} + + + Feedback Coverage: {dashboardData.data.feedback_rate_percentage}% + + + Average Feedback per Prediction: {dashboardData.data.average_feedback_per_prediction} + + + + ); + }; + + /** + * renderTimeAnalysis Function + * + * Purpose: Render time-based analysis section + */ + const renderTimeAnalysis = () => { + if (!dashboardData?.data.time_analysis) return null; + + const { today, this_week, this_month, this_year } = dashboardData.data.time_analysis; + + // Check if the object is empty or if all values are undefined/null/zero + if (!today && !this_week && !this_month && !this_year) { + return ( + + Time-based Analysis + + No data found + + + ); + } + + // Check if all values are zero (no activity) + if (today === 0 && this_week === 0 && this_month === 0 && this_year === 0) { + return ( + + Time-based Analysis + + No activity recorded yet + Time-based statistics will appear once AI predictions are made + + + ); + } + + return ( + + Time-based Analysis + + + Today + {today} + + + This Week + {this_week} + + + This Month + {this_month} + + + This Year + {this_year} + + + + ); + }; + + /** + * renderHeader Function + * + * Purpose: Render the dashboard header section with key metrics + */ + const renderHeader = () => ( + + {/* Dashboard header with title and refresh button */} + + AI Analysis Dashboard + + {dashboardMessage} + + + + {/* Key statistics cards */} + + {renderStatsCard( + 'Total Predictions', + dashboardData?.data.total_predictions || 0, + 'AI analyses performed', + theme.colors.primary + )} + {renderStatsCard( + 'Total Patients', + dashboardData?.data.total_patients || 0, + 'Unique patients', + theme.colors.info + )} + {renderStatsCard( + 'Feedback Rate', + `${dashboardData?.data.feedback_rate_percentage || 0}%`, + 'User feedback coverage', + theme.colors.success + )} + {renderStatsCard( + 'Avg Confidence', + (dashboardData?.data.average_confidence_score || 0).toFixed(2), + 'AI prediction confidence', + theme.colors.warning + )} + + + ); + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + // Show error state if there's a critical error + if (error) { + return ( + + {renderErrorState()} + + ); + } + + // Show loading state while data is being fetched + if (isLoading) { + return ( + + Loading AI Analysis Dashboard... + + ); + } + + // Show no data state if dashboard loads but has no meaningful data + if (!dashboardData || !dashboardData.data) { + return renderNoDataState(); + } + + return ( + + {/* Scrollable dashboard content */} + + } + showsVerticalScrollIndicator={false} + > + {/* Dashboard header with key metrics */} + {renderHeader()} + + {/* Confidence score breakdown */} + {renderConfidenceBreakdown()} + + {/* Urgency level breakdown */} + {renderUrgencyBreakdown()} + + {/* Feedback analysis */} + {renderFeedbackAnalysis()} + + {/* Time-based analysis */} + {renderTimeAnalysis()} + + {/* Bottom spacing for tab bar */} + + + + ); +}; + +// ============================================================================ +// STYLES SECTION +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container for the dashboard screen + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Loading container for initial data loading + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: theme.colors.background, + }, + + // Loading text styling + loadingText: { + fontSize: theme.typography.fontSize.bodyLarge, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + }, + + // Scroll view styling + scrollView: { + flex: 1, + }, + + // Scroll content styling + scrollContent: { + paddingBottom: theme.spacing.lg, + }, + + // Header section containing dashboard components + header: { + paddingHorizontal: theme.spacing.md, + paddingTop: theme.spacing.md, + }, + + // Header top section with title + headerTop: { + marginBottom: theme.spacing.lg, + }, + + // Dashboard title styling + dashboardTitle: { + fontSize: theme.typography.fontSize.displayLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + // Dashboard subtitle styling + dashboardSubtitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + }, + + // Stats grid container + statsGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: theme.spacing.sm, + marginBottom: theme.spacing.lg, + }, + + // Individual stats card styling + statsCard: { + flex: 1, + minWidth: '45%', + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.md, + borderLeftWidth: 0, + borderLeftColor: 'transparent', + ...theme.shadows.primary, + }, + + // Stats card title styling + statsCardTitle: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xs, + }, + + // Stats card value styling + statsCardValue: { + fontSize: theme.typography.fontSize.displayMedium, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + // Stats card subtitle styling + statsCardSubtitle: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + }, + + // Section container styling + section: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.md, + marginHorizontal: theme.spacing.md, + marginBottom: theme.spacing.md, + ...theme.shadows.primary, + }, + + // Section title styling + sectionTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.lg, + }, + + // Confidence breakdown container + confidenceContainer: { + gap: theme.spacing.md, + }, + + // Confidence item styling + confidenceItem: { + backgroundColor: theme.colors.backgroundAlt, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.md, + marginBottom: theme.spacing.sm, + ...theme.shadows.small, + }, + + // Confidence bar styling + confidenceBar: { + height: 12, + borderRadius: theme.borderRadius.small, + backgroundColor: theme.colors.primary, + }, + + // Confidence label styling + confidenceLabel: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + flex: 1, + }, + + // Confidence value styling + confidenceValue: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + marginTop: theme.spacing.xs, + textAlign: 'center', + }, + + // Confidence header styling + confidenceHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + + // Confidence indicator styling + confidenceIndicator: { + width: 16, + height: 16, + borderRadius: 8, + marginRight: theme.spacing.xs, + }, + + // Confidence percentage styling + confidencePercentage: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginLeft: 'auto', + }, + + // Confidence bar container styling + confidenceBarContainer: { + width: '100%', + height: 12, + backgroundColor: theme.colors.border, + borderRadius: theme.borderRadius.small, + marginBottom: theme.spacing.sm, + overflow: 'hidden', + }, + + // Urgency container styling + urgencyContainer: { + flexDirection: 'row', + justifyContent: 'space-around', + }, + + // Urgency item styling + urgencyItem: { + alignItems: 'center', + flex: 1, + }, + + // Urgency indicator styling + urgencyIndicator: { + width: 16, + height: 16, + borderRadius: 8, + marginBottom: theme.spacing.xs, + }, + + // Urgency label styling + urgencyLabel: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xs, + }, + + // Urgency value styling + urgencyValue: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + }, + + // Feedback container styling + feedbackContainer: { + flexDirection: 'row', + justifyContent: 'space-around', + marginBottom: theme.spacing.md, + }, + + // Feedback item styling + feedbackItem: { + alignItems: 'center', + flex: 1, + }, + + // Feedback indicator styling + feedbackIndicator: { + width: 16, + height: 16, + borderRadius: 8, + marginBottom: theme.spacing.xs, + }, + + // Feedback label styling + feedbackLabel: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xs, + }, + + // Feedback value styling + feedbackValue: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + // Feedback percentage styling + feedbackPercentage: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + }, + + // Feedback summary styling + feedbackSummary: { + borderTopWidth: 1, + borderTopColor: theme.colors.border, + paddingTop: theme.spacing.md, + }, + + // Feedback summary text styling + feedbackSummaryText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + textAlign: 'center', + marginBottom: theme.spacing.xs, + }, + + // Feedback metrics container styling + feedbackMetrics: { + marginTop: theme.spacing.md, + paddingTop: theme.spacing.md, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + alignItems: 'center', + }, + + // Feedback metrics text styling + feedbackMetricsText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + textAlign: 'center', + marginBottom: theme.spacing.xs, + }, + + // Time container styling + timeContainer: { + flexDirection: 'row', + justifyContent: 'space-around', + }, + + // Time item styling + timeItem: { + alignItems: 'center', + flex: 1, + }, + + // Time label styling + timeLabel: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xs, + }, + + // Time value styling + timeValue: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + }, + + // Confidence summary styling + confidenceSummary: { + marginTop: theme.spacing.lg, + paddingTop: theme.spacing.md, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + alignItems: 'center', + backgroundColor: theme.colors.backgroundAlt, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.md, + marginHorizontal: -theme.spacing.md, + }, + + // Summary text styling + summaryText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.sm, + }, + + // Summary value styling + summaryValue: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + }, + + // Bottom spacing for tab bar + bottomSpacing: { + height: theme.spacing.xl, + }, + + // Empty state container styling + emptyStateContainer: { + alignItems: 'center', + paddingVertical: theme.spacing.lg, + }, + + // Empty state text styling + emptyStateText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xs, + }, + + // Empty state subtext styling + emptyStateSubtext: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + textAlign: 'center', + marginBottom: theme.spacing.sm, + }, + + // Empty state info styling + emptyStateInfo: { + marginTop: theme.spacing.sm, + paddingHorizontal: theme.spacing.md, + }, + + // Empty state info text styling + emptyStateInfoText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + marginBottom: theme.spacing.xs, + }, + + // Retry button styling + retryButton: { + marginTop: theme.spacing.md, + paddingVertical: theme.spacing.sm, + paddingHorizontal: theme.spacing.lg, + backgroundColor: theme.colors.primary, + borderRadius: theme.borderRadius.small, + borderWidth: 1, + borderColor: theme.colors.primary, + }, + + // Retry button text styling + retryButtonText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.background, + }, + + // Error container styling + errorContainer: { + alignItems: 'center', + paddingVertical: theme.spacing.lg, + backgroundColor: theme.colors.backgroundAlt, + borderRadius: theme.borderRadius.medium, + marginHorizontal: theme.spacing.md, + marginBottom: theme.spacing.md, + ...theme.shadows.primary, + }, + + // Error title styling + errorTitle: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xs, + }, + + // Error message styling + errorMessage: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + textAlign: 'center', + marginBottom: theme.spacing.sm, + }, + + // Error info styling + errorInfo: { + marginTop: theme.spacing.sm, + paddingHorizontal: theme.spacing.md, + }, + + // Error info text styling + errorInfoText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + marginBottom: theme.spacing.xs, + }, + + // No data container styling + noDataContainer: { + alignItems: 'center', + paddingVertical: theme.spacing.lg, + backgroundColor: theme.colors.backgroundAlt, + borderRadius: theme.borderRadius.medium, + marginHorizontal: theme.spacing.md, + marginBottom: theme.spacing.md, + ...theme.shadows.primary, + }, + + // No data title styling + noDataTitle: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xs, + }, + + // No data message styling + noDataMessage: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + textAlign: 'center', + marginBottom: theme.spacing.sm, + }, + + // No data info styling + noDataInfo: { + marginTop: theme.spacing.sm, + paddingHorizontal: theme.spacing.md, + }, + + // No data info text styling + noDataInfoText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + marginBottom: theme.spacing.xs, + }, + + // Network status container styling + networkStatusContainer: { + alignItems: 'center', + paddingVertical: theme.spacing.md, + backgroundColor: theme.colors.backgroundAlt, + borderRadius: theme.borderRadius.small, + marginHorizontal: theme.spacing.md, + marginBottom: theme.spacing.md, + ...theme.shadows.small, + }, + + // Network status text styling + networkStatusText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.warning, + marginBottom: theme.spacing.xs, + }, + + // Network status subtext styling + networkStatusSubtext: { + fontSize: theme.typography.fontSize.caption, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.warning, + textAlign: 'center', + }, +}); + +/* + * End of File: DashboardScreen.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/screens/ERDashboardScreen.tsx b/app/modules/Dashboard/screens/ERDashboardScreen.tsx new file mode 100644 index 0000000..3dd844c --- /dev/null +++ b/app/modules/Dashboard/screens/ERDashboardScreen.tsx @@ -0,0 +1,791 @@ +/* + * File: ERDashboardScreen.tsx + * Description: Brain AI Analysis Dashboard - Main dashboard for neurological AI predictions and brain imaging analysis + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + TouchableOpacity, + RefreshControl, + FlatList, + Alert, +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import { ERDashboard, Patient, Alert as AlertType } from '../../../shared/types'; +import { PatientCard } from '../components/PatientCard'; +import { CriticalAlerts } from '../components/CriticalAlerts'; +import { DashboardHeader } from '../components/DashboardHeader'; +import { BrainPredictionsOverview } from '../components/BrainPredictionsOverview'; + +/** + * ERDashboardScreenProps Interface + * + * Purpose: Defines the props required by the ERDashboardScreen component + * + * Props: + * - navigation: React Navigation object for screen navigation + */ +interface ERDashboardScreenProps { + navigation: any; +} + +/** + * Brain AI Prediction Types + * + * Purpose: Defines the different types of brain conditions that AI can predict + */ +type BrainCondition = + | 'Intraparenchymal hemorrhage (IPH)' + | 'Intraventricular hemorrhage (IVH)' + | 'Subarachnoid hemorrhage (SAH)' + | 'Subdural hematoma (SDH)' + | 'Epidural hematoma (EDH)' + | 'Mass effect' + | 'Midline shift' + | 'Intracranial hemorrhage (ICH)' + | 'Stroke (ischemic)' + | 'Normal brain'; + +/** + * ERDashboardScreen Component + * + * Purpose: Brain AI Analysis Dashboard for Emergency Department physicians + * + * Dashboard Features: + * 1. Real-time brain scan analysis with AI predictions + * 2. Critical neurological alerts for immediate attention + * 3. Quick action buttons for brain imaging tasks + * 4. Department statistics focused on neurological cases + * 5. Pull-to-refresh functionality for live updates + * 6. AI prediction confidence scores and analysis + * + * Brain Condition Filtering: + * - All: Shows all brain scan cases + * - Critical: Shows only cases with critical brain conditions + * - Pending: Shows cases awaiting AI analysis + * + * AI Prediction Classes: + * - Intraparenchymal hemorrhage (IPH) + * - Intraventricular hemorrhage (IVH) + * - Subarachnoid hemorrhage (SAH) + * - Subdural hematoma (SDH) + * - Epidural hematoma (EDH) + * - Mass effect + * - Midline shift + * - Intracranial hemorrhage (ICH) + * - Stroke (ischemic) + * - Normal brain + */ +export const ERDashboardScreen: React.FC = ({ + navigation, +}) => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + // Refresh state for pull-to-refresh functionality + const [refreshing, setRefreshing] = useState(false); + + // Patient filter state to control which brain cases are displayed + const [selectedFilter, setSelectedFilter] = useState<'all' | 'critical' | 'pending'>('all'); + + // Dashboard data state + const [dashboard, setDashboard] = useState(null); + const [patients, setPatients] = useState([]); + const [alerts, setAlerts] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + // ============================================================================ + // MOCK DATA GENERATION + // ============================================================================ + + /** + * generateMockDashboard Function + * + * Purpose: Generate mock dashboard data focused on brain AI analysis + * + * Returns: ERDashboard object with brain imaging statistics + */ + const generateMockDashboard = (): ERDashboard => ({ + totalPatients: 18, // Total number of brain scan cases + criticalPatients: 5, // Number of critical brain conditions + pendingScans: 6, // Number of scans awaiting AI analysis + recentReports: 12, // Number of AI analysis reports generated + bedOccupancy: 78, // Percentage of neurological beds occupied + departmentStats: { + emergency: 8, // Emergency brain cases + trauma: 4, // Traumatic brain injury cases + cardiac: 3, // Stroke cases (using cardiac as placeholder) + neurology: 2, // General neurology cases + pediatrics: 1, // Neurosurgery cases (using pediatrics as placeholder) + icu: 0, // ICU brain cases + }, + shiftInfo: { + currentShift: 'DAY' as const, // Current shift (DAY/NIGHT) + startTime: new Date(), // Shift start time + endTime: new Date(), // Shift end time + attendingPhysician: 'Dr. Smith', // Lead neurologist on duty + residents: ['Dr. Johnson', 'Dr. Williams'], // Neurology residents + nurses: ['Nurse Brown', 'Nurse Davis'], // Neurology nursing staff + }, + lastUpdated: new Date(), // Last time dashboard was updated + }); + + /** + * generateMockPatients Function + * + * Purpose: Generate mock patient data focused on brain conditions and AI predictions + * + * Returns: Array of Patient objects with brain-related medical data + */ + const generateMockPatients = (): Patient[] => [ + { + id: '1', + mrn: 'MRN001', + firstName: 'John', + lastName: 'Doe', + dateOfBirth: new Date('1985-03-15'), + gender: 'MALE' as const, + age: 38, + bedNumber: 'A1', + roomNumber: '101', + admissionDate: new Date('2024-01-15'), + status: 'ACTIVE' as const, + priority: 'CRITICAL' as const, + department: 'Emergency', + attendingPhysician: 'Dr. Smith', + allergies: [ + { + id: '1', + name: 'Contrast Dye', + severity: 'SEVERE' as const, + reaction: 'Anaphylaxis' + }, + ], + medications: [ + { + id: '1', + name: 'Mannitol', + dosage: '100ml', + frequency: 'Every 6 hours', + route: 'IV', + startDate: new Date(), + status: 'ACTIVE' as const, + prescribedBy: 'Dr. Smith', + }, + ], + vitalSigns: { + bloodPressure: { systolic: 180, diastolic: 110, timestamp: new Date() }, + heartRate: { value: 95, timestamp: new Date() }, + temperature: { value: 37.2, timestamp: new Date() }, + respiratoryRate: { value: 18, timestamp: new Date() }, + oxygenSaturation: { value: 98, timestamp: new Date() }, + }, + medicalHistory: [], + currentDiagnosis: 'AI Predicted: Intraparenchymal hemorrhage (IPH) - 94% confidence', + lastUpdated: new Date(), + }, + { + id: '2', + mrn: 'MRN002', + firstName: 'Jane', + lastName: 'Smith', + dateOfBirth: new Date('1990-07-22'), + gender: 'FEMALE' as const, + age: 33, + bedNumber: 'B2', + roomNumber: '102', + admissionDate: new Date('2024-01-15'), + status: 'ACTIVE' as const, + priority: 'CRITICAL' as const, + department: 'Trauma', + attendingPhysician: 'Dr. Johnson', + allergies: [], + medications: [ + { + id: '2', + name: 'Dexamethasone', + dosage: '4mg', + frequency: 'Every 6 hours', + route: 'IV', + startDate: new Date(), + status: 'ACTIVE' as const, + prescribedBy: 'Dr. Johnson', + }, + ], + vitalSigns: { + bloodPressure: { systolic: 160, diastolic: 90, timestamp: new Date() }, + heartRate: { value: 88, timestamp: new Date() }, + temperature: { value: 36.8, timestamp: new Date() }, + respiratoryRate: { value: 16, timestamp: new Date() }, + oxygenSaturation: { value: 99, timestamp: new Date() }, + }, + medicalHistory: [], + currentDiagnosis: 'AI Predicted: Subdural hematoma (SDH) with mass effect - 89% confidence', + lastUpdated: new Date(), + }, + { + id: '3', + mrn: 'MRN003', + firstName: 'Michael', + lastName: 'Brown', + dateOfBirth: new Date('1978-11-08'), + gender: 'MALE' as const, + age: 45, + bedNumber: 'C3', + roomNumber: '103', + admissionDate: new Date('2024-01-15'), + status: 'PENDING' as const, + priority: 'HIGH' as const, + department: 'Stroke', + attendingPhysician: 'Dr. Williams', + allergies: [ + { + id: '2', + name: 'Aspirin', + severity: 'MODERATE' as const, + reaction: 'Stomach upset' + }, + ], + medications: [ + { + id: '3', + name: 'tPA', + dosage: '0.9mg/kg', + frequency: 'Single dose', + route: 'IV', + startDate: new Date(), + status: 'ACTIVE' as const, + prescribedBy: 'Dr. Williams', + }, + ], + vitalSigns: { + bloodPressure: { systolic: 140, diastolic: 85, timestamp: new Date() }, + heartRate: { value: 110, timestamp: new Date() }, + temperature: { value: 36.9, timestamp: new Date() }, + respiratoryRate: { value: 20, timestamp: new Date() }, + oxygenSaturation: { value: 95, timestamp: new Date() }, + }, + medicalHistory: [], + currentDiagnosis: 'AI Predicted: Stroke (ischemic) - 92% confidence', + lastUpdated: new Date(), + }, + { + id: '4', + mrn: 'MRN004', + firstName: 'Sarah', + lastName: 'Wilson', + dateOfBirth: new Date('1995-04-12'), + gender: 'FEMALE' as const, + age: 28, + bedNumber: 'D4', + roomNumber: '104', + admissionDate: new Date('2024-01-15'), + status: 'ACTIVE' as const, + priority: 'CRITICAL' as const, + department: 'Neurosurgery', + attendingPhysician: 'Dr. Davis', + allergies: [], + medications: [ + { + id: '4', + name: 'Phenytoin', + dosage: '100mg', + frequency: 'Every 8 hours', + route: 'IV', + startDate: new Date(), + status: 'ACTIVE' as const, + prescribedBy: 'Dr. Davis', + }, + ], + vitalSigns: { + bloodPressure: { systolic: 190, diastolic: 120, timestamp: new Date() }, + heartRate: { value: 60, timestamp: new Date() }, + temperature: { value: 38.5, timestamp: new Date() }, + respiratoryRate: { value: 12, timestamp: new Date() }, + oxygenSaturation: { value: 92, timestamp: new Date() }, + }, + medicalHistory: [], + currentDiagnosis: 'AI Predicted: Epidural hematoma (EDH) with midline shift - 96% confidence', + lastUpdated: new Date(), + }, + { + id: '5', + mrn: 'MRN005', + firstName: 'David', + lastName: 'Miller', + dateOfBirth: new Date('1982-09-30'), + gender: 'MALE' as const, + age: 41, + bedNumber: 'E5', + roomNumber: '105', + admissionDate: new Date('2024-01-15'), + status: 'ACTIVE' as const, + priority: 'HIGH' as const, + department: 'Neurology', + attendingPhysician: 'Dr. Brown', + allergies: [ + { + id: '3', + name: 'Latex', + severity: 'SEVERE' as const, + reaction: 'Contact dermatitis' + }, + ], + medications: [], + vitalSigns: { + bloodPressure: { systolic: 130, diastolic: 80, timestamp: new Date() }, + heartRate: { value: 85, timestamp: new Date() }, + temperature: { value: 37.8, timestamp: new Date() }, + respiratoryRate: { value: 22, timestamp: new Date() }, + oxygenSaturation: { value: 97, timestamp: new Date() }, + }, + medicalHistory: [], + currentDiagnosis: 'AI Predicted: Subarachnoid hemorrhage (SAH) - 87% confidence', + lastUpdated: new Date(), + }, + { + id: '6', + mrn: 'MRN006', + firstName: 'Emily', + lastName: 'Johnson', + dateOfBirth: new Date('1988-12-05'), + gender: 'FEMALE' as const, + age: 35, + bedNumber: 'F6', + roomNumber: '106', + admissionDate: new Date('2024-01-15'), + status: 'PENDING' as const, + priority: 'MEDIUM' as const, + department: 'Emergency', + attendingPhysician: 'Dr. Wilson', + allergies: [], + medications: [], + vitalSigns: { + bloodPressure: { systolic: 120, diastolic: 75, timestamp: new Date() }, + heartRate: { value: 72, timestamp: new Date() }, + temperature: { value: 37.0, timestamp: new Date() }, + respiratoryRate: { value: 16, timestamp: new Date() }, + oxygenSaturation: { value: 99, timestamp: new Date() }, + }, + medicalHistory: [], + currentDiagnosis: 'AI Analysis Pending - CT scan uploaded', + lastUpdated: new Date(), + }, + ]; + + /** + * generateMockAlerts Function + * + * Purpose: Generate mock alert data focused on brain AI predictions + * + * Returns: Array of Alert objects with brain-related alerts + */ + const generateMockAlerts = (): AlertType[] => [ + { + id: '1', + type: 'CRITICAL_FINDING' as const, + priority: 'CRITICAL' as const, + title: 'Critical Brain Finding Detected', + message: 'AI has detected Intraparenchymal hemorrhage (IPH) with 94% confidence. Immediate neurosurgical consultation required.', + patientId: '1', + patientName: 'John Doe', + bedNumber: 'A1', + timestamp: new Date(), + isRead: false, + isAcknowledged: false, + actionRequired: true, + }, + { + id: '2', + type: 'VITAL_SIGNS_ALERT' as const, + priority: 'HIGH' as const, + title: 'Elevated ICP Alert', + message: 'Patient Sarah Wilson showing signs of elevated intracranial pressure. BP: 190/120, HR: 60. Immediate intervention needed.', + patientId: '4', + patientName: 'Sarah Wilson', + bedNumber: 'D4', + timestamp: new Date(Date.now() - 300000), // 5 minutes ago + isRead: false, + isAcknowledged: false, + actionRequired: true, + }, + { + id: '3', + type: 'MEDICATION_ALERT' as const, + priority: 'MEDIUM' as const, + title: 'AI Analysis Complete', + message: 'Brain CT analysis complete for Emily Johnson. Results: Normal brain - 98% confidence. No intervention required.', + patientId: '6', + patientName: 'Emily Johnson', + bedNumber: 'F6', + timestamp: new Date(Date.now() - 600000), // 10 minutes ago + isRead: true, + isAcknowledged: true, + actionRequired: false, + }, + { + id: '4', + type: 'CRITICAL_FINDING' as const, + priority: 'CRITICAL' as const, + title: 'Stroke Alert - Time Sensitive', + message: 'Michael Brown diagnosed with ischemic stroke. tPA window closing. Immediate thrombolytic therapy required.', + patientId: '3', + patientName: 'Michael Brown', + bedNumber: 'C3', + timestamp: new Date(Date.now() - 120000), // 2 minutes ago + isRead: false, + isAcknowledged: false, + actionRequired: true, + }, + ]; + + // ============================================================================ + // EFFECTS + // ============================================================================ + + /** + * useEffect for initial data loading + * + * Purpose: Load initial mock data when component mounts + */ + useEffect(() => { + const loadInitialData = async () => { + setIsLoading(true); + + // Simulate API call delay + setTimeout(() => {}, 1000); + + // Generate and set mock data + setDashboard(generateMockDashboard()); + setPatients(generateMockPatients()); + setAlerts(generateMockAlerts()); + + setIsLoading(false); + }; + + loadInitialData(); + }, []); + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * handleRefresh Function + * + * Purpose: Handle pull-to-refresh functionality to update dashboard data + */ + const handleRefresh = async () => { + setRefreshing(true); + + // Simulate API call with 1-second delay + setTimeout(() => {}, 1000); + + // Update data with fresh mock data + setDashboard(generateMockDashboard()); + setPatients(generateMockPatients()); + setAlerts(generateMockAlerts()); + + setRefreshing(false); + }; + + /** + * handlePatientPress Function + * + * Purpose: Handle patient card press navigation + * + * @param patient - Patient object that was pressed + */ + const handlePatientPress = (patient: Patient) => { + console.log('Patient pressed:', patient.firstName, patient.lastName); + Alert.alert('Brain Analysis Details', `Navigate to ${patient.firstName} ${patient.lastName}'s brain scan analysis`); + }; + + /** + * handleAlertPress Function + * + * Purpose: Handle alert press interaction + * + * @param alert - Alert object that was pressed + */ + const handleAlertPress = (alert: AlertType) => { + console.log('Alert pressed:', alert.title); + Alert.alert('Brain Alert Details', alert.message); + }; + + // ============================================================================ + // DATA PROCESSING + // ============================================================================ + + /** + * filteredPatients - Computed property + * + * Purpose: Filter patients based on selected filter criteria + * + * Filter Options: + * - 'all': Show all brain scan cases + * - 'critical': Show only cases with CRITICAL priority + * - 'pending': Show only cases with PENDING status (awaiting AI analysis) + */ + const filteredPatients = patients.filter(patient => { + switch (selectedFilter) { + case 'critical': + return patient.priority === 'CRITICAL'; + case 'pending': + return patient.status === 'PENDING'; + default: + return true; + } + }); + + /** + * criticalAlerts - Computed property + * + * Purpose: Extract critical alerts from all alerts for immediate display + */ + const criticalAlerts = alerts.filter(alert => alert.priority === 'CRITICAL'); + + /** + * pendingScans - Computed property + * + * Purpose: Identify patients with pending AI analysis + */ + const pendingScans = patients.filter(patient => patient.status === 'PENDING'); + + // ============================================================================ + // RENDER FUNCTIONS + // ============================================================================ + + /** + * renderPatientCard Function + * + * Purpose: Render individual patient card component + * + * @param item - Patient data object + * @returns PatientCard component with patient data and press handler + */ + const renderPatientCard = ({ item }: { item: Patient }) => ( + handlePatientPress(item)} + /> + ); + + /** + * renderHeader Function + * + * Purpose: Render the dashboard header section with all dashboard components + */ + const renderHeader = () => ( + + {/* Dashboard header with shift information and key metrics */} + {dashboard && } + + {/* Critical alerts section - only show if there are critical alerts */} + {criticalAlerts.length > 0 && ( + + )} + + {/* Department statistics showing brain case distribution */} + {dashboard && } + + {/* Brain case filter section with filter buttons */} + + Brain Scan Cases + + {/* All cases filter button */} + setSelectedFilter('all')} + > + + All ({patients.length}) + + + + {/* Critical cases filter button */} + setSelectedFilter('critical')} + > + + Critical ({patients.filter(p => p.priority === 'CRITICAL').length}) + + + + {/* Pending AI analysis filter button */} + setSelectedFilter('pending')} + > + + Pending AI ({patients.filter(p => p.status === 'PENDING').length}) + + + + + + ); + + // ============================================================================ + // LOADING STATE + // ============================================================================ + + /** + * Loading state render + * + * Purpose: Show loading indicator while data is being generated + */ + if (isLoading) { + return ( + + Loading Brain AI Dashboard... + + ); + } + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + {/* FlatList for efficient rendering of patient cards */} + item.id} + ListHeaderComponent={renderHeader} + contentContainerStyle={styles.listContainer} + refreshControl={ + + } + showsVerticalScrollIndicator={false} + /> + + ); +}; + +// ============================================================================ +// STYLES SECTION +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container for the dashboard screen + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Loading container for initial data loading + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: theme.colors.background, + }, + + // Loading text styling + loadingText: { + fontSize: theme.typography.fontSize.bodyLarge, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + }, + + // Container for the FlatList content + listContainer: { + paddingBottom: theme.spacing.lg, + }, + + // Header section containing dashboard components + header: { + paddingHorizontal: theme.spacing.md, + }, + + // Container for patient filter section + filterContainer: { + marginTop: theme.spacing.lg, + marginBottom: theme.spacing.md, + }, + + // Section title styling + sectionTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.md, + }, + + // Container for filter buttons + filterButtons: { + flexDirection: 'row', + gap: theme.spacing.sm, + }, + + // Individual filter button styling + filterButton: { + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + borderRadius: theme.borderRadius.medium, + borderWidth: 1, + borderColor: theme.colors.border, + backgroundColor: theme.colors.background, + }, + + // Active filter button styling + filterButtonActive: { + backgroundColor: theme.colors.primary, + borderColor: theme.colors.primary, + }, + + // Filter button text styling + filterButtonText: { + fontSize: theme.typography.fontSize.bodyMedium, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + }, + + // Active filter button text styling + filterButtonTextActive: { + color: theme.colors.background, + }, +}); + +/* + * End of File: ERDashboardScreen.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Dashboard/screens/patient.json b/app/modules/Dashboard/screens/patient.json new file mode 100644 index 0000000..2607766 --- /dev/null +++ b/app/modules/Dashboard/screens/patient.json @@ -0,0 +1,57 @@ +{ + "success": true, + "data": { + "total_predictions": 24, + "total_patients": 9, + "total_feedbacks": 6, + "prediction_breakdown": { + "Other": 24 + }, + "critical_findings": {}, + "midline_shift_stats": {}, + "hemorrhage_stats": {}, + "mass_lesion_stats": {}, + "edema_stats": {}, + "fracture_stats": {}, + "feedback_analysis": { + "positive": 6, + "negative": 0, + "total": 6 + }, + "hospital_distribution": { + "b491dfc2-521b-4eb1-8d88-02b0940ea1ff": 24 + }, + "time_analysis": { + "today": 24, + "this_week": 24, + "this_month": 24, + "this_year": 24 + }, + "urgency_levels": { + "critical": 0, + "urgent": 0, + "routine": 24 + }, + "confidence_scores": { + "high": 23, + "medium": 1, + "low": 0 + }, + "feedback_rate_percentage": 25, + "predictions_with_feedback": 2, + "predictions_without_feedback": 22, + "average_feedback_per_prediction": "0.25", + "critical_case_percentage": 0, + "average_confidence_score": 0.89 + }, + "summary": { + "total_cases": 24, + "critical_cases": 0, + "routine_cases": 24, + "feedback_coverage": "25.00%", + "critical_case_rate": "0.00%", + "average_confidence": "0.89" + } + }, + "message": "Statistics generated for 24 predictions" +} \ No newline at end of file diff --git a/app/modules/Dashboard/services/dashboardAPI.ts b/app/modules/Dashboard/services/dashboardAPI.ts new file mode 100644 index 0000000..1c808f4 --- /dev/null +++ b/app/modules/Dashboard/services/dashboardAPI.ts @@ -0,0 +1,165 @@ +/* + * File: dashboardAPI.ts + * Description: API service for dashboard operations using apisauce + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { create } from 'apisauce'; +import { API_CONFIG, buildHeaders } from '../../../shared/utils'; + +const api = create({ + baseURL: API_CONFIG.BASE_URL +}); + +/** + * Dashboard API Service + * + * Purpose: Handle all dashboard-related API operations + * + * Features: + * - Get AI analysis dashboard statistics + * - Get feedback statistics for AI cases + * - Get real-time dashboard metrics + * - Get time-based analysis data + */ +export const dashboardAPI = { + /** + * Get AI Analysis Dashboard Statistics + * + * Purpose: Fetch comprehensive dashboard statistics for AI analysis + * + * @param token - Authentication token + * @returns Promise with dashboard statistics data + */ + getDashboardStatistics: (token: string) => { + return api.get('/api/ai-cases/feedbacks/statistics', {}, buildHeaders({ token })); + }, + + /** + * Get Real-time Dashboard Metrics + * + * Purpose: Fetch real-time dashboard metrics for live updates + * + * @param token - Authentication token + * @returns Promise with real-time dashboard metrics + */ + getRealTimeMetrics: (token: string) => { + return api.get('/api/ai-cases/feedbacks/statistics/realtime', {}, buildHeaders({ token })); + }, + + /** + * Get Time-based Analysis Data + * + * Purpose: Fetch time-based analysis data for trend visualization + * + * @param token - Authentication token + * @param timeRange - Time range for analysis (today, week, month, year) + * @returns Promise with time-based analysis data + */ + getTimeBasedAnalysis: (token: string, timeRange: 'today' | 'week' | 'month' | 'year') => { + return api.get(`/api/ai-cases/feedbacks/statistics/time-analysis/${timeRange}`, {}, buildHeaders({ token })); + }, + + /** + * Get Hospital-specific Statistics + * + * Purpose: Fetch statistics for a specific hospital + * + * @param token - Authentication token + * @param hospitalId - Hospital identifier + * @returns Promise with hospital-specific statistics + */ + getHospitalStatistics: (token: string, hospitalId: string) => { + return api.get(`/api/ai-cases/feedbacks/statistics/hospital/${hospitalId}`, {}, buildHeaders({ token })); + }, + + /** + * Get Department Performance Metrics + * + * Purpose: Fetch performance metrics for specific departments + * + * @param token - Authentication token + * @param department - Department name + * @returns Promise with department performance data + */ + getDepartmentMetrics: (token: string, department: string) => { + return api.get(`/api/ai-cases/feedbacks/statistics/department/${department}`, {}, buildHeaders({ token })); + }, + + /** + * Get Confidence Score Distribution + * + * Purpose: Fetch confidence score distribution for AI predictions + * + * @param token - Authentication token + * @param timeRange - Optional time range filter + * @returns Promise with confidence score distribution data + */ + getConfidenceDistribution: (token: string, timeRange?: 'today' | 'week' | 'month' | 'year') => { + const params = timeRange ? { timeRange } : {}; + return api.get('/api/ai-cases/feedbacks/statistics/confidence-distribution', params, buildHeaders({ token })); + }, + + /** + * Get Urgency Level Distribution + * + * Purpose: Fetch urgency level distribution for AI cases + * + * @param token - Authentication token + * @param timeRange - Optional time range filter + * @returns Promise with urgency level distribution data + */ + getUrgencyDistribution: (token: string, timeRange?: 'today' | 'week' | 'month' | 'year') => { + const params = timeRange ? { timeRange } : {}; + return api.get('/api/ai-cases/feedbacks/statistics/urgency-distribution', params, buildHeaders({ token })); + }, + + /** + * Get Feedback Analysis Data + * + * Purpose: Fetch feedback analysis and coverage metrics + * + * @param token - Authentication token + * @param timeRange - Optional time range filter + * @returns Promise with feedback analysis data + */ + getFeedbackAnalysis: (token: string, timeRange?: 'today' | 'week' | 'month' | 'year') => { + const params = timeRange ? { timeRange } : {}; + return api.get('/api/ai-cases/feedbacks/statistics/feedback-analysis', params, buildHeaders({ token })); + }, + + /** + * Get Critical Findings Statistics + * + * Purpose: Fetch statistics for critical findings and cases + * + * @param token - Authentication token + * @param timeRange - Optional time range filter + * @returns Promise with critical findings statistics + */ + getCriticalFindingsStats: (token: string, timeRange?: 'today' | 'week' | 'month' | 'year') => { + const params = timeRange ? { timeRange } : {}; + return api.get('/api/ai-cases/feedbacks/statistics/critical-findings', params, buildHeaders({ token })); + }, + + /** + * Get Prediction Breakdown Statistics + * + * Purpose: Fetch breakdown of AI predictions by category + * + * @param token - Authentication token + * @param timeRange - Optional time range filter + * @returns Promise with prediction breakdown data + */ + getPredictionBreakdown: (token: string, timeRange?: 'today' | 'week' | 'month' | 'year') => { + const params = timeRange ? { timeRange } : {}; + return api.get('/api/ai-cases/feedbacks/statistics/prediction-breakdown', params, buildHeaders({ token })); + } +}; + +/* + * End of File: dashboardAPI.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/Dashboard/services/index.ts b/app/modules/Dashboard/services/index.ts new file mode 100644 index 0000000..6cf8f50 --- /dev/null +++ b/app/modules/Dashboard/services/index.ts @@ -0,0 +1,14 @@ +/* + * File: index.ts + * Description: Dashboard services exports + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export * from './dashboardAPI'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/PatientCare/components/EmptyState.tsx b/app/modules/PatientCare/components/EmptyState.tsx new file mode 100644 index 0000000..f30673d --- /dev/null +++ b/app/modules/PatientCare/components/EmptyState.tsx @@ -0,0 +1,159 @@ +/* + * File: EmptyState.tsx + * Description: Empty state component for when no patients are found + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import Icon from 'react-native-vector-icons/Feather'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface EmptyStateProps { + title: string; + subtitle: string; + iconName?: string; + onRetry?: () => void; + retryText?: string; +} + +// ============================================================================ +// EMPTY STATE COMPONENT +// ============================================================================ + +/** + * EmptyState Component + * + * Purpose: Display empty state when no patients are found + * + * Features: + * - Customizable title and subtitle + * - Icon display + * - Optional retry functionality + * - Centered layout with proper spacing + * - Medical-themed design + */ +const EmptyState: React.FC = ({ + title, + subtitle, + iconName = 'users', + onRetry, + retryText = 'Retry', +}) => { + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + {/* Icon */} + + + + + {/* Title */} + {title} + + {/* Subtitle */} + {subtitle} + + {/* Retry Button */} + {onRetry && ( + + + {retryText} + + )} + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: theme.spacing.xl, + paddingVertical: theme.spacing.xxl, + }, + iconContainer: { + width: 120, + height: 120, + borderRadius: 60, + backgroundColor: theme.colors.backgroundAlt, + justifyContent: 'center', + alignItems: 'center', + marginBottom: theme.spacing.lg, + }, + title: { + fontSize: 20, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + textAlign: 'center', + marginBottom: theme.spacing.sm, + }, + subtitle: { + fontSize: 16, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + textAlign: 'center', + lineHeight: 24, + marginBottom: theme.spacing.lg, + }, + retryButton: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.primary, + paddingHorizontal: theme.spacing.lg, + paddingVertical: theme.spacing.sm, + borderRadius: 12, + shadowColor: theme.colors.primary, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 3, + }, + retryIcon: { + marginRight: theme.spacing.xs, + }, + retryText: { + fontSize: 16, + color: theme.colors.background, + fontFamily: theme.typography.fontFamily.bold, + }, +}); + +export default EmptyState; + +/* + * End of File: EmptyState.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/components/FilterTabs.tsx b/app/modules/PatientCare/components/FilterTabs.tsx new file mode 100644 index 0000000..7fe0143 --- /dev/null +++ b/app/modules/PatientCare/components/FilterTabs.tsx @@ -0,0 +1,337 @@ +/* + * File: FilterTabs.tsx + * Description: Filter tabs component for patient status filtering + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + ScrollView, +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import Icon from 'react-native-vector-icons/Feather'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface FilterTabsProps { + selectedFilter: 'all' | 'processed' | 'pending' | 'error'; + onFilterChange: (filter: 'all' | 'processed' | 'pending' | 'error') => void; + patientCounts: { + all: number; + processed: number; + pending: number; + error: number; + }; +} + +interface FilterTab { + id: 'all' | 'processed' | 'pending' | 'error'; + label: string; + icon: string; + color: string; + activeColor: string; +} + +// ============================================================================ +// FILTER TABS COMPONENT +// ============================================================================ + +/** + * FilterTabs Component + * + * Purpose: Provide filtering options for patient list + * + * Features: + * - Multiple filter options (All, Processed, Pending, Error) + * - Patient count display for each filter + * - Visual indicators with icons and colors + * - Horizontal scrollable layout + * - Active state highlighting + * - ER-focused design with medical priority colors + */ +const FilterTabs: React.FC = ({ + selectedFilter, + onFilterChange, + patientCounts, +}) => { + // ============================================================================ + // TAB CONFIGURATION + // ============================================================================ + + const filterTabs: FilterTab[] = [ + { + id: 'all', + label: 'All Cases', + icon: 'users', + color: theme.colors.textSecondary, + activeColor: theme.colors.primary, + }, + { + id: 'processed', + label: 'Processed', + icon: 'check-circle', + color: theme.colors.success, + activeColor: theme.colors.success, + }, + { + id: 'pending', + label: 'Pending', + icon: 'clock', + color: theme.colors.warning, + activeColor: theme.colors.warning, + }, + { + id: 'error', + label: 'Error', + icon: 'alert-triangle', + color: theme.colors.error, + activeColor: theme.colors.error, + }, + ]; + + // ============================================================================ + // UTILITY FUNCTIONS + // ============================================================================ + + /** + * Get Patient Count for Filter + * + * Purpose: Get the count of patients for a specific filter + * + * @param filterId - Filter ID + * @returns Number of patients for the filter + */ + const getPatientCount = (filterId: string): number => { + switch (filterId) { + case 'all': + return patientCounts.all; + case 'processed': + return patientCounts.processed; + case 'pending': + return patientCounts.pending; + case 'error': + return patientCounts.error; + default: + return 0; + } + }; + + // ============================================================================ + // RENDER HELPERS + // ============================================================================ + + /** + * Render Filter Tab + * + * Purpose: Render individual filter tab + * + * @param tab - Filter tab configuration + */ + const renderFilterTab = (tab: FilterTab) => { + const isSelected = selectedFilter === tab.id; + const patientCount = getPatientCount(tab.id); + const tabColor = isSelected ? tab.activeColor : tab.color; + + return ( + onFilterChange(tab.id)} + activeOpacity={0.7} + > + {/* Tab Icon */} + + + {/* Tab Content */} + + + {tab.label} + + + {/* Patient Count Badge */} + + + {patientCount} + + + + + {/* Error Indicator */} + {tab.id === 'error' && patientCount > 0 && ( + + + + )} + + ); + }; + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + + {filterTabs.map(renderFilterTab)} + + + {/* Active Filter Indicator */} + + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + marginBottom: theme.spacing.sm, + }, + scrollContent: { + paddingHorizontal: theme.spacing.xs, + }, + + // Tab Styles + tab: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + marginHorizontal: theme.spacing.xs, + borderRadius: 12, + backgroundColor: theme.colors.backgroundAlt, + borderBottomWidth: 3, + borderBottomColor: 'transparent', + position: 'relative', + minWidth: 100, + }, + tabSelected: { + backgroundColor: theme.colors.background, + shadowColor: theme.colors.shadow, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + tabIcon: { + marginRight: theme.spacing.xs, + }, + tabContent: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + tabLabel: { + fontSize: 14, + fontFamily: theme.typography.fontFamily.regular, + flex: 1, + }, + tabLabelSelected: { + fontFamily: theme.typography.fontFamily.bold, + }, + + // Count Badge Styles + countBadge: { + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 12, + marginLeft: theme.spacing.xs, + minWidth: 24, + alignItems: 'center', + }, + countBadgeSelected: { + shadowColor: theme.colors.shadow, + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.2, + shadowRadius: 2, + elevation: 1, + }, + countText: { + fontSize: 12, + fontFamily: theme.typography.fontFamily.bold, + textAlign: 'center', + }, + countTextSelected: { + color: theme.colors.background, + }, + + // Error Indicator + errorIndicator: { + position: 'absolute', + top: 8, + right: 8, + width: 8, + height: 8, + }, + pulseDot: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: theme.colors.error, + // Note: In a real app, you'd add animation here + }, + + // Active Filter Indicator + activeIndicator: { + marginTop: theme.spacing.xs, + alignItems: 'center', + }, + indicatorLine: { + width: 40, + height: 2, + backgroundColor: theme.colors.primary, + borderRadius: 1, + }, +}); + +export default FilterTabs; + +/* + * End of File: FilterTabs.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/components/ImageViewer.tsx b/app/modules/PatientCare/components/ImageViewer.tsx new file mode 100644 index 0000000..3ea3669 --- /dev/null +++ b/app/modules/PatientCare/components/ImageViewer.tsx @@ -0,0 +1,501 @@ +/* + * File: ImageViewer.tsx + * Description: Full-screen DICOM image viewer with zoom, pan, and navigation + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState, useCallback } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Dimensions, + Image, + ScrollView, + StatusBar, + SafeAreaView, +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import Icon from 'react-native-vector-icons/Feather'; +import { API_CONFIG } from '../../../shared/utils'; + +// Get screen dimensions +const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface ImageViewerProps { + visible: boolean; + images: string[]; + initialIndex: number; + onClose: () => void; + patientName?: string; + seriesInfo?: string; +} + +// ============================================================================ +// IMAGE VIEWER COMPONENT +// ============================================================================ + +/** + * ImageViewer Component + * + * Purpose: Full-screen DICOM image viewer with advanced viewing capabilities + * + * Features: + * - Full-screen image display + * - Image navigation (previous/next) + * - Zoom and pan functionality + * - Patient information display + * - Series information + * - Touch gestures for navigation + * - Professional medical imaging interface + */ +const ImageViewer: React.FC = ({ + visible, + images, + initialIndex, + onClose, + patientName = 'Unknown Patient', + seriesInfo = 'DICOM Series', +}) => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + const [currentIndex, setCurrentIndex] = useState(initialIndex); + const [scale, setScale] = useState(1); + const [isZoomed, setIsZoomed] = useState(false); + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Handle Previous Image + * + * Purpose: Navigate to previous image in series + */ + const handlePrevious = useCallback(() => { + if (currentIndex > 0) { + setCurrentIndex(currentIndex - 1); + setScale(1); + setIsZoomed(false); + } + }, [currentIndex]); + + /** + * Handle Next Image + * + * Purpose: Navigate to next image in series + */ + const handleNext = useCallback(() => { + if (currentIndex < images.length - 1) { + setCurrentIndex(currentIndex + 1); + setScale(1); + setIsZoomed(false); + } + }, [currentIndex, images.length]); + + /** + * Handle Zoom In + * + * Purpose: Increase image zoom level + */ + const handleZoomIn = useCallback(() => { + const newScale = Math.min(scale * 1.5, 3); + setScale(newScale); + setIsZoomed(newScale > 1); + }, [scale]); + + /** + * Handle Zoom Out + * + * Purpose: Decrease image zoom level + */ + const handleZoomOut = useCallback(() => { + const newScale = Math.max(scale / 1.5, 0.5); + setScale(newScale); + setIsZoomed(newScale > 1); + }, [scale]); + + /** + * Handle Reset Zoom + * + * Purpose: Reset image to original size + */ + const handleResetZoom = useCallback(() => { + setScale(1); + setIsZoomed(false); + }, []); + + /** + * Handle Close + * + * Purpose: Close image viewer and return to previous screen + */ + const handleClose = useCallback(() => { + setScale(1); + setIsZoomed(false); + setCurrentIndex(initialIndex); + onClose(); + }, [initialIndex, onClose]); + + // ============================================================================ + // RENDER HELPERS + // ============================================================================ + + /** + * Render Header + * + * Purpose: Render image viewer header with patient info and controls + */ + const renderHeader = () => ( + + + + + + + {patientName} + {seriesInfo} + + + + + + {currentIndex + 1} of {images.length} + + + + ); + + /** + * Render Navigation Controls + * + * Purpose: Render image navigation controls + */ + const renderNavigationControls = () => ( + + + + + + + + + + ); + + /** + * Render Zoom Controls + * + * Purpose: Render zoom control buttons + */ + const renderZoomControls = () => ( + + + + + + + {Math.round(scale * 100)}% + + + + + + + ); + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + if (!visible || images.length === 0) { + return null; + } + + return ( + + + + {/* Header */} + {renderHeader()} + + {/* Main Image Area */} + + + + + + + {/* Navigation Controls */} + {renderNavigationControls()} + + {/* Zoom Controls */} + {renderZoomControls()} + + {/* Thumbnail Strip */} + + + {images.map((image, index) => ( + setCurrentIndex(index)} + > + + {index === currentIndex && ( + + + + )} + + ))} + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#000000', + }, + + // Header Styles + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + backgroundColor: 'rgba(0, 0, 0, 0.8)', + }, + headerLeft: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + closeButton: { + padding: theme.spacing.sm, + marginRight: theme.spacing.md, + }, + patientInfo: { + flex: 1, + }, + patientName: { + fontSize: 16, + color: theme.colors.background, + fontFamily: theme.typography.fontFamily.bold, + }, + seriesInfo: { + fontSize: 12, + color: theme.colors.background, + opacity: 0.8, + fontFamily: theme.typography.fontFamily.regular, + }, + headerRight: { + alignItems: 'flex-end', + }, + imageCounter: { + fontSize: 14, + color: theme.colors.background, + fontFamily: theme.typography.fontFamily.medium, + }, + + // Image Container Styles + imageContainer: { + flex: 1, + // backgroundColor: '#000000', + }, + scrollView: { + flex: 1, + }, + scrollContent: { + flexGrow: 1, + justifyContent: 'center', + alignItems: 'center', + }, + image: { + width: screenWidth, + height: screenHeight * 0.7, + }, + + // Navigation Controls Styles + navigationControls: { + position: 'absolute', + top: '50%', + left: 0, + right: 0, + flexDirection: 'row', + justifyContent: 'space-between', + paddingHorizontal: theme.spacing.md, + transform: [{ translateY: -20 }], + }, + navButton: { + width: 48, + height: 48, + borderRadius: 24, + backgroundColor: 'rgba(0, 0, 0, 0.6)', + justifyContent: 'center', + alignItems: 'center', + shadowColor: '#000000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 4, + }, + navButtonDisabled: { + backgroundColor: 'rgba(0, 0, 0, 0.3)', + }, + + // Zoom Controls Styles + zoomControls: { + position: 'absolute', + bottom: 100, + right: theme.spacing.md, + flexDirection: 'column', + alignItems: 'center', + }, + zoomButton: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: 'rgba(0, 0, 0, 0.6)', + justifyContent: 'center', + alignItems: 'center', + marginBottom: theme.spacing.sm, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 4, + }, + zoomText: { + color: theme.colors.background, + fontSize: 12, + fontFamily: theme.typography.fontFamily.bold, + }, + + // Thumbnail Strip Styles + thumbnailStrip: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + backgroundColor: 'rgba(0, 0, 0, 0.8)', + paddingVertical: theme.spacing.sm, + }, + thumbnailContent: { + paddingHorizontal: theme.spacing.md, + }, + thumbnail: { + width: 60, + height: 60, + borderRadius: 8, + marginRight: theme.spacing.sm, + position: 'relative', + borderWidth: 2, + borderColor: 'transparent', + }, + activeThumbnail: { + borderColor: theme.colors.primary, + }, + thumbnailImage: { + width: '100%', + height: '100%', + borderRadius: 6, + }, + activeIndicator: { + position: 'absolute', + top: -4, + right: -4, + width: 20, + height: 20, + borderRadius: 10, + backgroundColor: theme.colors.primary, + justifyContent: 'center', + alignItems: 'center', + }, +}); + +export default ImageViewer; + +/* + * End of File: ImageViewer.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/PatientCare/components/LoadingState.tsx b/app/modules/PatientCare/components/LoadingState.tsx new file mode 100644 index 0000000..5c5b3b9 --- /dev/null +++ b/app/modules/PatientCare/components/LoadingState.tsx @@ -0,0 +1,178 @@ +/* + * File: LoadingState.tsx + * Description: Loading state component for patient data fetching + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + ActivityIndicator, + StyleSheet, +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import Icon from 'react-native-vector-icons/Feather'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface LoadingStateProps { + title?: string; + subtitle?: string; + showIcon?: boolean; + iconName?: string; + size?: 'small' | 'large'; +} + +// ============================================================================ +// LOADING STATE COMPONENT +// ============================================================================ + +/** + * LoadingState Component + * + * Purpose: Display loading state during data fetching + * + * Features: + * - Customizable loading messages + * - Optional icon display + * - Different sizes (small/large) + * - Centered layout with spinner + * - Medical-themed design + */ +const LoadingState: React.FC = ({ + title = 'Loading...', + subtitle = 'Please wait while we fetch the data', + showIcon = true, + iconName = 'loader', + size = 'large', +}) => { + // ============================================================================ + // RENDER HELPERS + // ============================================================================ + + /** + * Render Large Loading State + * + * Purpose: Render full-screen loading state + */ + const renderLargeState = () => ( + + {/* Loading Animation */} + + {showIcon && ( + + + + )} + + + + {/* Loading Text */} + {title} + {subtitle} + + ); + + /** + * Render Small Loading State + * + * Purpose: Render compact loading state + */ + const renderSmallState = () => ( + + + {title} + + ); + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return size === 'large' ? renderLargeState() : renderSmallState(); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + // Large Loading State + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: theme.spacing.xl, + paddingVertical: theme.spacing.xxl, + backgroundColor: theme.colors.background, + }, + loadingContainer: { + position: 'relative', + marginBottom: theme.spacing.lg, + }, + iconContainer: { + position: 'absolute', + top: '50%', + left: '50%', + marginTop: -16, + marginLeft: -16, + zIndex: 1, + }, + spinner: { + transform: [{ scale: 1.5 }], + }, + title: { + fontSize: 20, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + textAlign: 'center', + marginBottom: theme.spacing.sm, + }, + subtitle: { + fontSize: 16, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + textAlign: 'center', + lineHeight: 24, + }, + + // Small Loading State + smallContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: theme.spacing.md, + }, + smallSpinner: { + marginRight: theme.spacing.sm, + }, + smallText: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, +}); + +export default LoadingState; + +/* + * End of File: LoadingState.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/components/PatientCard.tsx b/app/modules/PatientCare/components/PatientCard.tsx new file mode 100644 index 0000000..8431994 --- /dev/null +++ b/app/modules/PatientCare/components/PatientCard.tsx @@ -0,0 +1,496 @@ +/* + * File: PatientCard.tsx + * Description: Patient card component for displaying DICOM medical case information + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import Icon from 'react-native-vector-icons/Feather'; +import { PatientData } from '../redux/patientCareSlice'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface PatientCardProps { + patient: PatientData; + onPress: () => void; + onEmergencyPress?: () => void; +} + +// ============================================================================ +// PATIENT CARD COMPONENT +// ============================================================================ + +/** + * PatientCard Component + * + * Purpose: Display DICOM medical case information in a card format + * + * Features: + * - Patient basic information from DICOM data + * - Modality and institution information + * - Processing status with color coding + * - Series information + * - Time since processed + * - Emergency alert for critical cases + * - Modern ER-focused design + */ +const PatientCard: React.FC = ({ + patient, + onPress, + onEmergencyPress, +}) => { + // ============================================================================ + // UTILITY FUNCTIONS + // ============================================================================ + + /** + * Get Status Color Configuration + * + * Purpose: Get color and icon based on processing status + * + * @param status - Processing status + * @returns Color configuration object + */ + const getStatusConfig = (status: string) => { + switch (status.toLowerCase()) { + case 'processed': + return { + color: theme.colors.success, + icon: 'check-circle', + bgColor: '#F0FFF4' + }; + case 'pending': + return { + color: theme.colors.warning, + icon: 'clock', + bgColor: '#FFF8E1' + }; + case 'error': + return { + color: theme.colors.error, + icon: 'alert-triangle', + bgColor: '#FFF5F5' + }; + default: + return { + color: theme.colors.primary, + icon: 'info', + bgColor: theme.colors.background + }; + } + }; + + /** + * Get Modality Color + * + * Purpose: Get color based on imaging modality + * + * @param modality - Imaging modality + * @returns Color code + */ + const getModalityColor = (modality: string) => { + switch (modality.toUpperCase()) { + case 'CT': + return '#4A90E2'; + case 'MR': + return '#7B68EE'; + case 'DX': + return '#50C878'; + case 'DICOM': + return '#FF6B6B'; + default: + return theme.colors.textSecondary; + } + }; + + /** + * Format Date + * + * Purpose: Format date string to readable format + * + * @param dateString - ISO date string + * @returns Formatted date string + */ + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }; + + /** + * Get Time Since Processed + * + * Purpose: Get human-readable time since last processed + * + * @param dateString - ISO date string + * @returns Formatted time string + */ + const getTimeSinceProcessed = (dateString: string) => { + const now = new Date(); + const processed = new Date(dateString); + const diffInMinutes = Math.floor((now.getTime() - processed.getTime()) / (1000 * 60)); + + if (diffInMinutes < 1) return 'Just now'; + if (diffInMinutes < 60) return `${diffInMinutes}m ago`; + if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)}h ago`; + return `${Math.floor(diffInMinutes / 1440)}d ago`; + }; + + // ============================================================================ + // DATA EXTRACTION + // ============================================================================ + + const patientInfo = patient.patient_info; + const seriesCount = patient.series_summary.length; + const statusConfig = getStatusConfig(patientInfo.status); + const isCritical = patientInfo.report_status === 'Critical' || patientInfo.status === 'Error'; + + // ============================================================================ + // RENDER HELPERS + // ============================================================================ + + /** + * Render Status Badge + * + * Purpose: Render processing status indicator badge + */ + const renderStatusBadge = () => ( + + + {patientInfo.status} + + ); + + /** + * Render Emergency Button + * + * Purpose: Render emergency alert button for critical cases + */ + const renderEmergencyButton = () => { + if (!isCritical) { + return null; + } + + return ( + + + ALERT + + ); + }; + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + {/* Header Section */} + + + + {patientInfo.name || 'Unknown Patient'} + + + ID: {patient.patid} โ€ข {patientInfo.age || 'N/A'}y โ€ข {patientInfo.sex || 'N/A'} + + + + {renderStatusBadge()} + {renderEmergencyButton()} + + + + {/* Medical Information Section */} + + + + Modality + + {patientInfo.modality || 'N/A'} + + + + Files + + {patient.total_files_processed} + + + + Report + + {patientInfo.report_status || 'Pending'} + + + + + {/* Institution */} + + + + {patientInfo.institution || 'Unknown Institution'} + + + + + {/* Series Information */} + + + + Series Information + + + {seriesCount} Series Available โ€ข {patientInfo.frame_count} Total Frames + + + + {/* Footer */} + + + + {formatDate(patientInfo.date)} + + + {getTimeSinceProcessed(patient.last_processed_at)} + + + + Case #{patient.patid} + + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + backgroundColor: theme.colors.background, + borderRadius: 12, + padding: theme.spacing.md, + marginHorizontal: theme.spacing.md, + marginVertical: theme.spacing.xs, + shadowColor: theme.colors.shadow, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + borderWidth: 1, + borderColor: theme.colors.border, + borderLeftWidth: 4, + }, + containerCritical: { + borderColor: theme.colors.error, + borderWidth: 2, + backgroundColor: '#FFF5F5', + }, + + // Header Section + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: theme.spacing.sm, + }, + headerLeft: { + flex: 1, + marginRight: theme.spacing.sm, + }, + headerRight: { + flexDirection: 'row', + alignItems: 'center', + }, + patientName: { + fontSize: 18, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + patientInfo: { + fontSize: 14, + color: theme.colors.textSecondary, + marginTop: 2, + fontFamily: theme.typography.fontFamily.regular, + }, + + // Status Badge + statusBadge: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + marginRight: theme.spacing.xs, + borderWidth: 1, + }, + statusText: { + fontSize: 10, + fontFamily: theme.typography.fontFamily.bold, + marginLeft: 4, + textTransform: 'uppercase', + }, + + // Emergency Button + emergencyButton: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.error, + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + }, + emergencyButtonText: { + fontSize: 10, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + marginLeft: 4, + }, + + // Medical Section + medicalSection: { + marginBottom: theme.spacing.sm, + paddingBottom: theme.spacing.sm, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + infoRow: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: theme.spacing.sm, + }, + infoItem: { + flex: 1, + alignItems: 'center', + }, + infoLabel: { + fontSize: 10, + color: theme.colors.textMuted, + marginBottom: 2, + textTransform: 'uppercase', + fontFamily: theme.typography.fontFamily.regular, + }, + infoValue: { + fontSize: 14, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + textAlign: 'center', + }, + modalityText: { + fontFamily: theme.typography.fontFamily.bold, + }, + + // Institution Row + institutionRow: { + flexDirection: 'row', + alignItems: 'center', + }, + institutionText: { + fontSize: 14, + color: theme.colors.textSecondary, + marginLeft: 6, + flex: 1, + fontFamily: theme.typography.fontFamily.regular, + }, + + // Series Section + seriesSection: { + backgroundColor: theme.colors.backgroundAlt, + borderRadius: 8, + padding: theme.spacing.sm, + marginBottom: theme.spacing.sm, + }, + seriesHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 4, + }, + seriesLabel: { + fontSize: 12, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginLeft: 4, + }, + seriesText: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.regular, + }, + + // Footer Section + footer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + footerLeft: { + flex: 1, + }, + dateText: { + fontSize: 12, + color: theme.colors.textMuted, + fontFamily: theme.typography.fontFamily.regular, + }, + processedText: { + fontSize: 11, + color: theme.colors.textSecondary, + marginTop: 2, + fontFamily: theme.typography.fontFamily.regular, + }, + footerRight: { + flexDirection: 'row', + alignItems: 'center', + }, + caseId: { + fontSize: 12, + color: theme.colors.textSecondary, + marginRight: theme.spacing.xs, + fontFamily: theme.typography.fontFamily.regular, + }, +}); + +export default PatientCard; + +/* + * End of File: PatientCard.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/components/SearchBar.tsx b/app/modules/PatientCare/components/SearchBar.tsx new file mode 100644 index 0000000..e1d9889 --- /dev/null +++ b/app/modules/PatientCare/components/SearchBar.tsx @@ -0,0 +1,178 @@ +/* + * File: SearchBar.tsx + * Description: Search bar component for patient filtering + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + TextInput, + TouchableOpacity, + StyleSheet, +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import Icon from 'react-native-vector-icons/Feather'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface SearchBarProps { + value: string; + onChangeText: (text: string) => void; + placeholder?: string; + showFilter?: boolean; + onFilterPress?: () => void; +} + +// ============================================================================ +// SEARCH BAR COMPONENT +// ============================================================================ + +/** + * SearchBar Component + * + * Purpose: Provide search functionality for patient list + * + * Features: + * - Real-time search input + * - Clear button when text is present + * - Optional filter button + * - Modern design with icons + * - Optimized for medical data search + */ +const SearchBar: React.FC = ({ + value, + onChangeText, + placeholder = 'Search patients...', + showFilter = false, + onFilterPress, +}) => { + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Handle Clear Search + * + * Purpose: Clear the search input + */ + const handleClear = () => { + onChangeText(''); + }; + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + + {/* Search Icon */} + + + {/* Search Input */} + + + {/* Clear Button */} + {value.length > 0 && ( + + + + )} + + + {/* Filter Button */} + {showFilter && ( + + + + )} + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + searchContainer: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.backgroundAlt, + borderRadius: 12, + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + borderWidth: 1, + borderColor: theme.colors.border, + }, + searchIcon: { + marginRight: theme.spacing.xs, + }, + searchInput: { + flex: 1, + fontSize: 16, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.primary, + paddingVertical: theme.spacing.xs, + }, + clearButton: { + padding: theme.spacing.xs, + marginLeft: theme.spacing.xs, + }, + filterButton: { + backgroundColor: theme.colors.backgroundAlt, + borderRadius: 12, + padding: theme.spacing.sm, + marginLeft: theme.spacing.sm, + borderWidth: 1, + borderColor: theme.colors.primary, + }, +}); + +export default SearchBar; + +/* + * End of File: SearchBar.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/components/index.ts b/app/modules/PatientCare/components/index.ts new file mode 100644 index 0000000..c91bd56 --- /dev/null +++ b/app/modules/PatientCare/components/index.ts @@ -0,0 +1,19 @@ +/* + * File: index.ts + * Description: Barrel export for PatientCare components + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export { default as PatientCard } from './PatientCard'; +export { default as SearchBar } from './SearchBar'; +export { default as FilterTabs } from './FilterTabs'; +export { default as EmptyState } from './EmptyState'; +export { default as LoadingState } from './LoadingState'; +export { default as ImageViewer } from './ImageViewer'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/index.ts b/app/modules/PatientCare/index.ts new file mode 100644 index 0000000..8a53851 --- /dev/null +++ b/app/modules/PatientCare/index.ts @@ -0,0 +1,25 @@ +/* + * File: index.ts + * Description: Barrel export for PatientCare module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// Screens +export * from './screens'; + +// Components +export * from './components'; + +// Services +export * from './services'; + +// Redux +export * from './redux/patientCareSlice'; +export * from './redux/patientCareSelectors'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/navigation/PatientCareStackNavigator.tsx b/app/modules/PatientCare/navigation/PatientCareStackNavigator.tsx new file mode 100644 index 0000000..6d7a68b --- /dev/null +++ b/app/modules/PatientCare/navigation/PatientCareStackNavigator.tsx @@ -0,0 +1,88 @@ +/* + * File: PatientCareStackNavigator.tsx + * Description: Stack navigator for PatientCare module navigation + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; + +// Import screens +import { PatientsScreen, PatientDetailsScreen, SeriesDetailScreen } from '../screens'; + +// Import types +import { PatientCareStackParamList } from './navigationTypes'; + +// ============================================================================ +// STACK NAVIGATOR +// ============================================================================ + +const Stack = createStackNavigator(); + +/** + * PatientCareStackNavigator Component + * + * Purpose: Provides stack navigation for PatientCare module + * + * Screens: + * - PatientsScreen: Main patient list screen + * - PatientDetailsScreen: Detailed patient information and DICOM images + * - SeriesDetailScreen: Detailed series information with predictions and feedback + * + * Navigation Flow: + * PatientsScreen โ†’ PatientDetailsScreen (with patient data) โ†’ SeriesDetailScreen (with series data) + */ +const PatientCareStackNavigator: React.FC = () => { + return ( + + {/* Patients Screen - Main patient list */} + + + {/* Patient Details Screen - Comprehensive patient information */} + + + {/* Series Detail Screen - Detailed series information with predictions and feedback */} + + + ); +}; + +export default PatientCareStackNavigator; + +/* + * End of File: PatientCareStackNavigator.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/PatientCare/navigation/index.ts b/app/modules/PatientCare/navigation/index.ts new file mode 100644 index 0000000..43ffdd9 --- /dev/null +++ b/app/modules/PatientCare/navigation/index.ts @@ -0,0 +1,15 @@ +/* + * File: index.ts + * Description: Barrel export for PatientCare navigation components + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export { default as PatientCareStackNavigator } from './PatientCareStackNavigator'; +export * from './navigationTypes'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/PatientCare/navigation/navigationTypes.ts b/app/modules/PatientCare/navigation/navigationTypes.ts new file mode 100644 index 0000000..4a92bef --- /dev/null +++ b/app/modules/PatientCare/navigation/navigationTypes.ts @@ -0,0 +1,153 @@ +/* + * File: navigationTypes.ts + * Description: TypeScript types for PatientCare module navigation + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { StackNavigationProp } from '@react-navigation/stack'; +import { MedicalCase } from '../../../shared/types'; + +// ============================================================================ +// NAVIGATION PARAMETER LISTS +// ============================================================================ + +/** + * PatientCareStackParamList - Defines the parameter list for PatientCare stack navigator + * + * This interface defines all the screens available in the PatientCare module + * and their associated navigation parameters. + */ +export type PatientCareStackParamList = { + // Patients Screen - Main patient list with search and filtering + PatientsScreen: PatientsScreenParams; + + // Patient Details Screen - Comprehensive patient information and DICOM images + PatientDetails: PatientDetailsScreenParams; + + // Series Detail Screen - Detailed series information with predictions and feedback + SeriesDetail: SeriesDetailScreenParams; +}; + +// ============================================================================ +// SCREEN PARAMETER INTERFACES +// ============================================================================ + +/** + * PatientsScreenParams + * + * Purpose: Parameters for the patients list screen + * + * Parameters: + * - None required - this is the main entry point + */ +export interface PatientsScreenParams { + // No parameters required for main patients screen +} + +/** + * PatientDetailsScreenParams + * + * Purpose: Parameters for the patient details screen + * + * Parameters: + * - patientId: Required patient ID to display details + * - patientName: Optional patient name for display (will be fetched from API if not provided) + */ +export interface PatientDetailsScreenParams { + patientId: string; + patientName?: string; +} + +/** + * SeriesDetailScreenParams + * + * Purpose: Parameters for the series detail screen + * + * Parameters: + * - patientId: Required patient ID for the series + * - patientName: Required patient name for display + * - seriesNumber: Required series number to display + * - seriesData: Required series data object + * - patientData: Required patient data object for context + * - onFeedbackSubmitted: Optional callback to refresh parent screen data + */ +export interface SeriesDetailScreenParams { + patientId: string; + patientName: string; + seriesNumber: string; + seriesData: any; + patientData: any; + // Callback function to refresh parent screen data when feedback is submitted + // This ensures PatientDetailsScreen shows updated information when user navigates back + onFeedbackSubmitted?: () => void; +} + +// ============================================================================ +// NAVIGATION PROP TYPES +// ============================================================================ + +/** + * PatientCareNavigationProp - Navigation prop type for PatientCare screens + * + * Purpose: Provides type-safe navigation methods for PatientCare module screens + */ +export type PatientCareNavigationProp = StackNavigationProp; + +// ============================================================================ +// SCREEN PROP TYPES +// ============================================================================ + +/** + * PatientsScreenProps - Props for PatientsScreen component + */ +export interface PatientsScreenProps { + navigation: PatientCareNavigationProp; + route: { + params: PatientsScreenParams; + }; +} + +/** + * PatientDetailsScreenProps - Props for PatientDetailsScreen component + */ +export interface PatientDetailsScreenProps { + navigation: PatientCareNavigationProp; + route: { + params: PatientDetailsScreenParams; + }; +} + +/** + * SeriesDetailScreenProps - Props for SeriesDetailScreen component + */ +export interface SeriesDetailScreenProps { + navigation: PatientCareNavigationProp; + route: { + params: SeriesDetailScreenParams; + }; +} + +// ============================================================================ +// NAVIGATION UTILITY TYPES +// ============================================================================ + +/** + * NavigationHelper - Helper type for navigation functions + * + * Purpose: Provides type-safe navigation helper functions + */ +export type NavigationHelper = { + navigateToPatientDetails: ( + patientId: string, + patientName: string, + medicalCase: MedicalCase + ) => void; + navigateBack: () => void; +}; + +/* + * End of File: navigationTypes.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/PatientCare/redux/patientCareSelectors.ts b/app/modules/PatientCare/redux/patientCareSelectors.ts new file mode 100644 index 0000000..4ecc20b --- /dev/null +++ b/app/modules/PatientCare/redux/patientCareSelectors.ts @@ -0,0 +1,411 @@ +/* + * File: patientCareSelectors.ts + * Description: Redux selectors for patient care state + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from '../../../store'; +import { PatientData } from './patientCareSlice'; + +// ============================================================================ +// BASE SELECTORS +// ============================================================================ + +/** + * Select Patient Care State + * + * Purpose: Get the entire patient care state + */ +export const selectPatientCareState = (state: RootState) => state.patientCare; + +/** + * Select Patients + * + * Purpose: Get the patients array + */ +export const selectPatients = (state: RootState) => state.patientCare.patients; + +/** + * Select Current Patient + * + * Purpose: Get the currently selected patient + */ +export const selectCurrentPatient = (state: RootState) => state.patientCare.currentPatient; + +/** + * Select Patients Loading State + * + * Purpose: Get the loading state for patients + */ +export const selectPatientsLoading = (state: RootState) => state.patientCare.isLoading; + +/** + * Select Is Refreshing State + * + * Purpose: Get the refreshing state for pull-to-refresh + */ +export const selectIsRefreshing = (state: RootState) => state.patientCare.isRefreshing; + +/** + * Select Patient Details Loading State + * + * Purpose: Get the loading state for patient details + */ +export const selectPatientDetailsLoading = (state: RootState) => state.patientCare.isLoadingPatientDetails; + +/** + * Select Patients Error + * + * Purpose: Get the error state for patients + */ +export const selectPatientsError = (state: RootState) => state.patientCare.error; + +/** + * Select Search Query + * + * Purpose: Get the current search query + */ +export const selectSearchQuery = (state: RootState) => state.patientCare.searchQuery; + +/** + * Select Selected Filter + * + * Purpose: Get the currently selected filter + */ +export const selectSelectedFilter = (state: RootState) => state.patientCare.selectedFilter; + +/** + * Select Sort By + * + * Purpose: Get the current sort option + */ +export const selectSortBy = (state: RootState) => state.patientCare.sortBy; + +/** + * Select Sort Order + * + * Purpose: Get the current sort order + */ +export const selectSortOrder = (state: RootState) => state.patientCare.sortOrder; + +/** + * Select Pagination Info + * + * Purpose: Get pagination-related state + */ +export const selectPaginationInfo = (state: RootState) => ({ + currentPage: state.patientCare.currentPage, + itemsPerPage: state.patientCare.itemsPerPage, + totalItems: state.patientCare.totalItems, +}); + +/** + * Select Last Updated + * + * Purpose: Get the last updated timestamp + */ +export const selectLastUpdated = (state: RootState) => state.patientCare.lastUpdated; + +// ============================================================================ +// COMPUTED SELECTORS +// ============================================================================ + +/** + * Select Filtered Patients + * + * Purpose: Get patients filtered by search query and selected filter + */ +export const selectFilteredPatients = createSelector( + [selectPatients, selectSearchQuery, selectSelectedFilter, selectSortBy, selectSortOrder], + (patients, searchQuery, selectedFilter, sortBy, sortOrder) => { + // Ensure patients is always an array + if (!patients || !Array.isArray(patients)) { + return []; + } + + let filteredPatients = [...patients]; + + // Apply filter based on processing status + if (selectedFilter !== 'all') { + filteredPatients = filteredPatients.filter((patient: PatientData) => { + const status = patient.patient_info.status.toLowerCase(); + return status === selectedFilter; + }); + } + + // Apply search + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase().trim(); + filteredPatients = filteredPatients.filter((patient: PatientData) => { + const patientInfo = patient.patient_info; + + const name = (patientInfo.name || '').toLowerCase(); + const patId = (patient.patid || '').toLowerCase(); + const institution = (patientInfo.institution || '').toLowerCase(); + const modality = (patientInfo.modality || '').toLowerCase(); + + return ( + name.includes(query) || + patId.includes(query) || + institution.includes(query) || + modality.includes(query) + ); + }); + } + + // Apply sorting + filteredPatients.sort((a: PatientData, b: PatientData) => { + let aValue: any; + let bValue: any; + + switch (sortBy) { + case 'name': + aValue = (a.patient_info.name || '').toLowerCase(); + bValue = (b.patient_info.name || '').toLowerCase(); + break; + case 'processed': + aValue = new Date(a.last_processed_at).getTime(); + bValue = new Date(b.last_processed_at).getTime(); + break; + case 'date': + default: + aValue = new Date(a.patient_info.date).getTime(); + bValue = new Date(b.patient_info.date).getTime(); + break; + } + + if (aValue < bValue) { + return sortOrder === 'asc' ? -1 : 1; + } + if (aValue > bValue) { + return sortOrder === 'asc' ? 1 : -1; + } + return 0; + }); + + return filteredPatients; + } +); + +/** + * Select Processed Patients + * + * Purpose: Get patients with processed status + */ +export const selectProcessedPatients = createSelector( + [selectPatients], + (patients) => { + if (!patients || !Array.isArray(patients)) return []; + return patients.filter((patient: PatientData) => + patient.patient_info.status.toLowerCase() === 'processed' + ); + } +); + +/** + * Select Pending Patients + * + * Purpose: Get patients with pending status + */ +export const selectPendingPatients = createSelector( + [selectPatients], + (patients) => { + if (!patients || !Array.isArray(patients)) return []; + return patients.filter((patient: PatientData) => + patient.patient_info.status.toLowerCase() === 'pending' + ); + } +); + +/** + * Select Error Patients + * + * Purpose: Get patients with error status + */ +export const selectErrorPatients = createSelector( + [selectPatients], + (patients) => { + if (!patients || !Array.isArray(patients)) return []; + return patients.filter((patient: PatientData) => + patient.patient_info.status.toLowerCase() === 'error' + ); + } +); + +/** + * Select Patients by Modality + * + * Purpose: Get patients grouped by imaging modality + */ +export const selectPatientsByModality = createSelector( + [selectPatients], + (patients) => { + if (!patients || !Array.isArray(patients)) return {}; + + const grouped: { [key: string]: PatientData[] } = {}; + + patients.forEach((patient: PatientData) => { + const modality = patient.patient_info.modality || 'Unknown'; + if (!grouped[modality]) { + grouped[modality] = []; + } + grouped[modality].push(patient); + }); + + return grouped; + } +); + +/** + * Select Patient Statistics + * + * Purpose: Get statistics about patients + */ +export const selectPatientStats = createSelector( + [selectPatients], + (patients) => { + if (!patients || !Array.isArray(patients)) { + return { + total: 0, + processed: 0, + pending: 0, + error: 0, + averageAge: 0, + modalities: {}, + totalFiles: 0, + processedPercentage: 0, + pendingPercentage: 0, + errorPercentage: 0, + }; + } + + const total = patients.length; + const processed = patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'processed').length; + const pending = patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'pending').length; + const error = patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'error').length; + + // Calculate average age + const totalAge = patients.reduce((sum: number, patient: PatientData) => { + const age = parseInt(patient.patient_info.age) || 0; + return sum + age; + }, 0); + const averageAge = total > 0 ? Math.round(totalAge / total) : 0; + + // Modality distribution + const modalities: { [key: string]: number } = {}; + patients.forEach((patient: PatientData) => { + const modality = patient.patient_info.modality || 'Unknown'; + modalities[modality] = (modalities[modality] || 0) + 1; + }); + + // Total files processed + const totalFiles = patients.reduce((sum: number, patient: PatientData) => sum + (patient.total_files_processed || 0), 0); + + return { + total, + processed, + pending, + error, + averageAge, + modalities, + totalFiles, + processedPercentage: total > 0 ? Math.round((processed / total) * 100) : 0, + pendingPercentage: total > 0 ? Math.round((pending / total) * 100) : 0, + errorPercentage: total > 0 ? Math.round((error / total) * 100) : 0, + }; + } +); + +/** + * Select Patient by ID + * + * Purpose: Get a specific patient by ID + * + * @param patientId - The ID of the patient to find + */ +export const selectPatientById = (patientId: string) => + createSelector( + [selectPatients], + (patients) => { + if (!patients || !Array.isArray(patients)) return undefined; + return patients.find((patient: PatientData) => patient.patid === patientId); + } + ); + +/** + * Select Patients Need Attention + * + * Purpose: Get patients that need immediate attention + */ +export const selectPatientsNeedAttention = createSelector( + [selectPatients], + (patients) => { + if (!patients || !Array.isArray(patients)) return []; + + return patients.filter((patient: PatientData) => { + // Error patients always need attention + if (patient.patient_info.status.toLowerCase() === 'error') return true; + + // Patients with critical report status + if (patient.patient_info.report_status.toLowerCase() === 'critical') return true; + + // Patients with high frame count (complex cases) + if (patient.patient_info.frame_count > 100) return true; + + // Patients with multiple series (complex cases) + if (patient.series_summary.length > 5) return true; + + return false; + }); + } +); + +/** + * Select Has Data + * + * Purpose: Check if we have patient data + */ +export const selectHasPatientData = createSelector( + [selectPatients], + (patients) => patients && Array.isArray(patients) && patients.length > 0 +); + +/** + * Select Is Empty State + * + * Purpose: Check if we should show empty state + */ +export const selectIsEmptyState = createSelector( + [selectPatients, selectPatientsLoading, selectFilteredPatients], + (patients, isLoading, filteredPatients) => + !isLoading && patients && Array.isArray(patients) && patients.length > 0 && filteredPatients.length === 0 +); + +/** + * Select Patient Counts for Filters + * + * Purpose: Get patient counts for each filter category + */ +export const selectPatientCounts = createSelector( + [selectPatients], + (patients) => { + if (!patients || !Array.isArray(patients)) { + return { all: 0, processed: 0, pending: 0, error: 0 }; + } + + return { + all: patients.length, + processed: patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'processed').length, + pending: patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'pending').length, + error: patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'error').length, + }; + } +); + +/* + * End of File: patientCareSelectors.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/redux/patientCareSlice.ts b/app/modules/PatientCare/redux/patientCareSlice.ts new file mode 100644 index 0000000..8222b65 --- /dev/null +++ b/app/modules/PatientCare/redux/patientCareSlice.ts @@ -0,0 +1,521 @@ +/* + * File: patientCareSlice.ts + * Description: Patient care state management slice + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import { patientAPI } from '../services/patientAPI'; + +// ============================================================================ +// TYPES +// ============================================================================ + +/** + * New API Response Types + */ +export interface SeriesSummary { + series_num: string; + series_description: string; + total_images: number; + png_preview: string; + modality: string; +} + +export interface PatientInfo { + name: string; + age: string; + sex: string; + date: string; + institution: string; + modality: string; + status: string; + report_status: string; + file_name: string; + file_type: string; + frame_count: number; +} + +export interface PatientData { + patid: string; + hospital_id: string; + first_processed_at: string; + last_processed_at: string; + total_files_processed: number; + patient_info: PatientInfo; + series_summary: SeriesSummary[]; + processing_metadata: any; +} + +export interface PatientCareState { + // Patients data + patients: PatientData[]; + currentPatient: PatientData | null; + + // Loading states + isLoading: boolean; + isRefreshing: boolean; + isLoadingPatientDetails: boolean; + + // Error handling + error: string | null; + + // Search and filtering + searchQuery: string; + selectedFilter: 'all' | 'processed' | 'pending' | 'error'; + sortBy: 'date' | 'name' | 'processed'; + sortOrder: 'asc' | 'desc'; + + // Pagination + currentPage: number; + itemsPerPage: number; + totalItems: number; + + // Cache + lastUpdated: string | null; + cacheExpiry: string | null; +} + +// ============================================================================ +// ASYNC THUNKS +// ============================================================================ + +/** + * Fetch Patients Async Thunk + * + * Purpose: Fetch patients list from API + * + * @returns Promise with patients data or error + */ +export const fetchPatients = createAsyncThunk( + 'patientCare/fetchPatients', + async (token: string, { rejectWithValue }) => { + try { + const response: any = await patientAPI.getPatients(token); + console.log('response', response); + + if (response.ok && response.data&& response.data.data) { + // Return the patients data directly from the new API structure + return response.data.data as PatientData[]; + } else { + // Fallback to mock data for development + const mockPatients: PatientData[] = [ + { + patid: "demo001", + hospital_id: "demo-hospital-001", + first_processed_at: "2025-01-15T10:30:00Z", + last_processed_at: "2025-01-15T11:45:00Z", + total_files_processed: 3, + patient_info: { + name: "John Doe", + age: "38", + sex: "M", + date: "2025-01-15", + institution: "City General Hospital", + modality: "CT", + status: "Processed", + report_status: "Available", + file_name: "chest_ct_001.dcm", + file_type: "dcm", + frame_count: 50 + }, + series_summary: [ + { + series_num: "1", + series_description: "Chest CT", + total_images: 50, + png_preview: "/images/ct_chest_1.png", + modality: "CT" + } + ], + processing_metadata: {} + }, + { + patid: "demo002", + hospital_id: "demo-hospital-002", + first_processed_at: "2025-01-15T09:15:00Z", + last_processed_at: "2025-01-15T10:30:00Z", + total_files_processed: 2, + patient_info: { + name: "Jane Smith", + age: "33", + sex: "F", + date: "2025-01-15", + institution: "Memorial Medical Center", + modality: "MR", + status: "Processed", + report_status: "Available", + file_name: "brain_mri_001.dcm", + file_type: "dcm", + frame_count: 120 + }, + series_summary: [ + { + series_num: "1", + series_description: "Brain MRI", + total_images: 120, + png_preview: "/images/mri_brain_1.png", + modality: "MR" + } + ], + processing_metadata: {} + } + ]; + + return []; + } + } catch (error: any) { + console.error('Fetch patients error:', error); + return rejectWithValue(error.message || 'Failed to fetch patients.'); + } + } +); + +/** + * Fetch Patient Details Async Thunk + * + * Purpose: Fetch detailed patient information + * + * @param patientId - ID of the patient to fetch + * @returns Promise with patient details or error + */ +export const fetchPatientDetails = createAsyncThunk( + 'patientCare/fetchPatientDetails', + async (patientId: string, { rejectWithValue }) => { + try { + // TODO: Replace with actual API call + await new Promise((resolve) => setTimeout(resolve as any, 1000)); + + // Mock patient details for specific patient + const mockPatient: PatientData = { + patid: patientId, + hospital_id: `demo-hospital-${patientId}`, + first_processed_at: "2025-01-15T10:30:00Z", + last_processed_at: "2025-01-15T11:45:00Z", + total_files_processed: 3, + patient_info: { + name: `Patient ${patientId}`, + age: "38", + sex: "M", + date: "2025-01-15", + institution: "City General Hospital", + modality: "CT", + status: "Processed", + report_status: "Available", + file_name: `patient_${patientId}.dcm`, + file_type: "dcm", + frame_count: 50 + }, + series_summary: [ + { + series_num: "1", + series_description: "Chest CT", + total_images: 50, + png_preview: `/images/ct_chest_${patientId}.png`, + modality: "CT" + } + ], + processing_metadata: {} + }; + + return mockPatient; + } catch (error) { + return rejectWithValue('Failed to fetch patient details.'); + } + } +); + +/** + * Update Patient Async Thunk + * + * Purpose: Update patient information + * + * @param patientData - Updated patient data + * @returns Promise with updated patient or error + */ +export const updatePatient = createAsyncThunk( + 'patientCare/updatePatient', + async (patientData: Partial & { patid: string }, { rejectWithValue }) => { + try { + // TODO: Replace with actual API call + await new Promise((resolve) => setTimeout(resolve as any, 800)); + return patientData; + } catch (error) { + return rejectWithValue('Failed to update patient.'); + } + } +); + +// ============================================================================ +// INITIAL STATE +// ============================================================================ + +/** + * Initial Patient Care State + * + * Purpose: Define the initial state for patient care + * + * Features: + * - Patients list and management + * - Current patient details + * - Loading states for async operations + * - Error handling and messages + * - Search and filtering + * - Pagination and caching + */ +const initialState: PatientCareState = { + // Patients data + patients: [], + currentPatient: null, + + // Loading states + isLoading: false, + isRefreshing: false, + isLoadingPatientDetails: false, + + // Error handling + error: null, + + // Search and filtering + searchQuery: '', + selectedFilter: 'all', + sortBy: 'date', + sortOrder: 'desc', + + // Pagination + currentPage: 1, + itemsPerPage: 20, + totalItems: 0, + + // Cache + lastUpdated: null, + cacheExpiry: null, +}; + +// ============================================================================ +// PATIENT CARE SLICE +// ============================================================================ + +/** + * Patient Care Slice + * + * Purpose: Redux slice for patient care state management + * + * Features: + * - Patient data management + * - Search and filtering + * - Pagination + * - Caching + * - Error handling + * - Loading states + */ +const patientCareSlice = createSlice({ + name: 'patientCare', + initialState, + reducers: { + /** + * Clear Error Action + * + * Purpose: Clear patient care errors + */ + clearError: (state) => { + state.error = null; + }, + + /** + * Set Search Query Action + * + * Purpose: Set search query for patients + */ + setSearchQuery: (state, action: PayloadAction) => { + state.searchQuery = action.payload; + state.currentPage = 1; // Reset to first page when searching + }, + + /** + * Set Filter Action + * + * Purpose: Set patient filter + */ + setFilter: (state, action: PayloadAction<'all' | 'processed' | 'pending' | 'error'>) => { + state.selectedFilter = action.payload; + state.currentPage = 1; // Reset to first page when filtering + }, + + /** + * Set Sort Action + * + * Purpose: Set patient sort options + */ + setSort: (state, action: PayloadAction<{ by: 'date' | 'name' | 'processed'; order: 'asc' | 'desc' }>) => { + state.sortBy = action.payload.by; + state.sortOrder = action.payload.order; + }, + + /** + * Set Current Page Action + * + * Purpose: Set current page for pagination + */ + setCurrentPage: (state, action: PayloadAction) => { + state.currentPage = action.payload; + }, + + /** + * Set Items Per Page Action + * + * Purpose: Set items per page for pagination + */ + setItemsPerPage: (state, action: PayloadAction) => { + state.itemsPerPage = action.payload; + state.currentPage = 1; // Reset to first page when changing items per page + }, + + /** + * Set Current Patient Action + * + * Purpose: Set the currently selected patient + */ + setCurrentPatient: (state, action: PayloadAction) => { + state.currentPatient = action.payload; + }, + + /** + * Update Patient in List Action + * + * Purpose: Update a patient in the patients list + */ + updatePatientInList: (state, action: PayloadAction) => { + const index = state.patients.findIndex(patient => patient.patid === action.payload.patid); + if (index !== -1) { + state.patients[index] = action.payload; + } + + // Update current patient if it's the same patient + if (state.currentPatient && state.currentPatient.patid === action.payload.patid) { + state.currentPatient = action.payload; + } + }, + + /** + * Add Patient Action + * + * Purpose: Add a new patient to the list + */ + addPatient: (state, action: PayloadAction) => { + state.patients.unshift(action.payload); + state.totalItems += 1; + }, + + /** + * Remove Patient Action + * + * Purpose: Remove a patient from the list + */ + removePatient: (state, action: PayloadAction) => { + const index = state.patients.findIndex(patient => patient.patid === action.payload); + if (index !== -1) { + state.patients.splice(index, 1); + state.totalItems -= 1; + } + + // Clear current patient if it's the same patient + if (state.currentPatient && state.currentPatient.patid === action.payload) { + state.currentPatient = null; + } + }, + + /** + * Clear Cache Action + * + * Purpose: Clear patient data cache + */ + clearCache: (state) => { + state.patients = []; + state.currentPatient = null; + state.lastUpdated = null; + state.cacheExpiry = null; + }, + }, + extraReducers: (builder) => { + // Fetch Patients + builder + .addCase(fetchPatients.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(fetchPatients.fulfilled, (state, action) => { + state.isLoading = false; + state.patients = action.payload; + state.totalItems = action.payload.length; + state.lastUpdated = new Date().toLocaleDateString(); + state.cacheExpiry = new Date(Date.now() + 5 * 60 * 1000).toLocaleDateString(); // 5 minutes + state.error = null; + }) + .addCase(fetchPatients.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload as string; + }); + + // Fetch Patient Details + builder + .addCase(fetchPatientDetails.pending, (state) => { + state.isLoadingPatientDetails = true; + state.error = null; + }) + .addCase(fetchPatientDetails.fulfilled, (state, action) => { + state.isLoadingPatientDetails = false; + state.currentPatient = action.payload; + state.error = null; + }) + .addCase(fetchPatientDetails.rejected, (state, action) => { + state.isLoadingPatientDetails = false; + state.error = action.payload as string; + }); + + // Update Patient + builder + .addCase(updatePatient.fulfilled, (state, action) => { + // Update patient in list + const index = state.patients.findIndex(patient => patient.patid === action.payload.patid); + if (index !== -1) { + state.patients[index] = { ...state.patients[index], ...action.payload }; + } + + // Update current patient if it's the same patient + if (state.currentPatient && state.currentPatient.patid === action.payload.patid) { + state.currentPatient = { ...state.currentPatient, ...action.payload }; + } + }) + .addCase(updatePatient.rejected, (state, action) => { + state.error = action.payload as string; + }); + }, +}); + +// ============================================================================ +// EXPORTS +// ============================================================================ + +export const { + clearError, + setSearchQuery, + setFilter, + setSort, + setCurrentPage, + setItemsPerPage, + setCurrentPatient, + updatePatientInList, + addPatient, + removePatient, + clearCache, +} = patientCareSlice.actions; + +export default patientCareSlice.reducer; + +/* + * End of File: patientCareSlice.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/screens/PatientDetailsScreen.tsx b/app/modules/PatientCare/screens/PatientDetailsScreen.tsx new file mode 100644 index 0000000..bc937c1 --- /dev/null +++ b/app/modules/PatientCare/screens/PatientDetailsScreen.tsx @@ -0,0 +1,1587 @@ +/* + * File: PatientDetailsScreen.tsx + * Description: Comprehensive patient details screen with DICOM image viewer and AI analysis + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + * + * Features: + * - Patient demographics and medical information + * - Merged AI Analysis tab showing DICOM images alongside AI predictions + * - Processing history and timeline + * - Responsive design for different screen sizes + * - Emergency actions for critical cases + * - Clinical feedback system for AI predictions and DICOM images + */ + +import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + TouchableOpacity, + StatusBar, + Alert, + Dimensions, + Image, + FlatList, + RefreshControl, + TextInput, +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import { useAppDispatch, useAppSelector } from '../../../store/hooks'; +import Icon from 'react-native-vector-icons/Feather'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +// Import types and API +import { patientAPI } from '../services/patientAPI'; +import { selectUser } from '../../Auth/redux/authSelectors'; +import { API_CONFIG } from '../../../shared/utils'; +import { PatientDetailsScreenProps } from '../navigation/navigationTypes'; + +// Get screen dimensions +const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); + +// ============================================================================ +// INTERFACES +// ============================================================================ + + + +interface PatientInfo { + name: string; + age: string; + sex: string; + date: string; + institution: string; + modality: string; + status: string; + report_status: string; + file_name: string; + file_type: string; + frame_count: number; +} + +interface SeriesSummary { + series_num: string; + series_description: string; + total_images: number; + png_preview: string; + modality: string; +} + +interface Prediction { + id: number; + file_path: string; + prediction: { + label: string; + finding_type: string; + clinical_urgency: string; + confidence_score: number; + detailed_results: any; + finding_category: string; + primary_severity: string; + anatomical_location: string; + }; + processed_at: string; + preview: string; +} + +interface PatientData { + patid: string; + hospital_id: string; + patient_info: PatientInfo; + series_summary: SeriesSummary[]; + processing_metadata: any; + total_predictions: number; + first_processed_at: string; + last_processed_at: string; + predictions_by_series: { [key: string]: Prediction[] }; +} + +// ============================================================================ +// PATIENT DETAILS SCREEN COMPONENT +// ============================================================================ + +/** + * PatientDetailsScreen Component + * + * Purpose: Comprehensive patient details display with DICOM image viewer + * + * Features: + * - Full patient demographic information + * - Medical case details and status + * - DICOM series information + * - AI predictions and findings + * - Image gallery with thumbnail previews + * - Real-time data updates from API + * - Emergency actions for critical cases + * - Medical history and notes + * - Modern healthcare-focused UI design + * - Responsive layout for different screen sizes + */ +const PatientDetailsScreen: React.FC = ({ navigation, route }) => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + const dispatch = useAppDispatch(); + + // Route parameters + const { patientId, patientName } = route.params; + + // Redux state + const user = useAppSelector(selectUser); + + // Local state + const [patientData, setPatientData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isRefreshing, setIsRefreshing] = useState(false); + const [error, setError] = useState(null); + const [selectedImageIndex, setSelectedImageIndex] = useState(0); + const [showFullImage, setShowFullImage] = useState(false); + const [activeTab, setActiveTab] = useState<'overview' | 'aiAnalysis' | 'history'>('overview'); + + // Navigation state + const [selectedSeriesForDetail, setSelectedSeriesForDetail] = useState(null); + + // ============================================================================ + // DATA FETCHING + // ============================================================================ + + /** + * Fetch Patient Data + * + * Purpose: Fetch patient details from API + */ + const fetchPatientData = useCallback(async () => { + if (!user?.access_token) { + setError('Authentication token not available'); + setIsLoading(false); + return; + } + + try { + setIsLoading(true); + setError(null); + + const response: any = await patientAPI.getPatientDetailsById(patientId, user.access_token); + + if (response.ok && response.data && response.data.data ) { + setPatientData(response.data.data as PatientData); + } else { + setError(response.problem || 'Failed to fetch patient data'); + } + } catch (err: any) { + setError(err.message || 'An error occurred while fetching patient data'); + } finally { + setIsLoading(false); + } + }, [patientId, user?.access_token]); + + // ============================================================================ + // EFFECTS + // ============================================================================ + + /** + * Component Mount Effect + * + * Purpose: Initialize screen and fetch patient data + */ + useEffect(() => { + fetchPatientData(); + }, [fetchPatientData]); + + /** + * Navigation Title Effect + * + * Purpose: Set navigation title when patient data is loaded + */ + useEffect(() => { + if (patientData) { + navigation.setOptions({ + title: patientData.patient_info.name || 'Patient Details', + headerShown: false, + }); + } + }, [navigation, patientData]); + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Handle Refresh + * + * Purpose: Pull-to-refresh functionality + */ + const handleRefresh = useCallback(async () => { + setIsRefreshing(true); + await fetchPatientData(); + setIsRefreshing(false); + }, [fetchPatientData]); + + /** + * Handle Image Press + * + * Purpose: Open full-screen image viewer for selected series + * + * @param seriesIndex - Index of the series + */ + const handleImagePress = useCallback((seriesIndex: number) => { + setSelectedImageIndex(seriesIndex); + setShowFullImage(true); + }, []); + + /** + * Handle Close Image Viewer + * + * Purpose: Close full-screen image viewer + */ + const handleCloseImageViewer = useCallback(() => { + setShowFullImage(false); + }, []); + + /** + * Get All Images from Series + * + * Purpose: Extract image paths from DICOM series + */ + const getAllImages = useCallback(() => { + if (!patientData) return []; + + const images: string[] = []; + patientData.series_summary.forEach(series => { + if (series.png_preview) { + images.push(series.png_preview); + } + }); + return images; + }, [patientData]); + + /** + * Get Series Info for Image Index + * + * Purpose: Get series information for a given image index + */ + const getSeriesInfoForImage = useCallback((imageIndex: number) => { + if (!patientData || imageIndex < 0 || imageIndex >= patientData.series_summary.length) { + return { + seriesNum: '1', + seriesDesc: 'Unknown Series', + imageInSeries: 1, + totalInSeries: 1 + }; + } + + const series = patientData.series_summary[imageIndex]; + return { + seriesNum: series.series_num, + seriesDesc: series.series_description, + imageInSeries: 1, + totalInSeries: series.total_images + }; + }, [patientData]); + + /** + * Handle Emergency Action + * + * Purpose: Handle emergency actions for critical patients + */ + const handleEmergencyAction = useCallback(() => { + if (!patientData) return; + + Alert.alert( + 'Emergency Action Required', + `Patient ${patientData.patient_info.name} requires immediate attention`, + [ + { + text: 'Call Code Blue', + style: 'destructive', + onPress: () => { + // TODO: Implement emergency code calling + Alert.alert('Emergency', 'Code Blue activated'); + }, + }, + { + text: 'Alert Team', + onPress: () => { + // TODO: Implement team alerting + Alert.alert('Alert', 'Team notified'); + }, + }, + { + text: 'Cancel', + style: 'cancel', + }, + ] + ); + }, [patientData]); + + /** + * Handle Back Navigation + * + * Purpose: Navigate back to previous screen + */ + const handleBackPress = useCallback(() => { + navigation.goBack(); + }, [navigation]); + + // ============================================================================ + // NAVIGATION HANDLERS + // ============================================================================ + + /** + * Handle Navigate to Series Detail + * + * Purpose: Navigate to detailed series view + * + * @param series - Series data to view in detail + */ + const handleNavigateToSeriesDetail = useCallback((series: SeriesSummary) => { + if (!patientData) return; + + navigation.navigate('SeriesDetail', { + patientId: patientData.patid, + patientName: patientData.patient_info.name, + seriesNumber: series.series_num, + seriesData: series, + patientData: patientData, + // Pass the refresh function as callback so parent screen can update when feedback is submitted + onFeedbackSubmitted: fetchPatientData + }); + }, [navigation, patientData, fetchPatientData]); + + // ============================================================================ + // UTILITY FUNCTIONS + // ============================================================================ + + /** + * Get Status Color + * + * Purpose: Get appropriate color for patient status + * + * @param status - Patient status + */ + const getStatusColor = (status: string) => { + switch (status.toLowerCase()) { + case 'processed': + return theme.colors.success; + case 'pending': + return theme.colors.warning; + case 'error': + return theme.colors.error; + default: + return theme.colors.info; + } + }; + + /** + * Get Clinical Urgency Color + * + * Purpose: Get appropriate color for clinical urgency + * + * @param urgency - Clinical urgency level + */ + const getUrgencyColor = (urgency: string) => { + switch (urgency.toLowerCase()) { + case 'urgent': + return theme.colors.error; + case 'semi-urgent': + return theme.colors.warning; + case 'non-urgent': + return theme.colors.success; + default: + return theme.colors.info; + } + }; + + // ============================================================================ + // RENDER HELPERS + // ============================================================================ + + /** + * Render Loading State + * + * Purpose: Render loading state while fetching data + */ + const renderLoadingState = () => ( + + + Loading patient data... + + ); + + /** + * Render Error State + * + * Purpose: Render error state when API fails + */ + const renderErrorState = () => ( + + + Error Loading Patient Data + {error} + + Retry + + + ); + + /** + * Render Patient Header + * + * Purpose: Render patient identification and status section + */ + const renderPatientHeader = () => { + if (!patientData) return null; + + const isCritical = patientData.patient_info.status === 'Error' || + patientData.patient_info.report_status === 'Critical'; + + return ( + + + + + + + + {patientData.patient_info.name || 'Unknown Patient'} + + + ID: {patientData.patid} + + + + {patientData.patient_info.age || 'N/A'} โ€ข {patientData.patient_info.sex || 'N/A'} + + + {patientData.patient_info.status} + + + + + + {isCritical && ( + + + EMERGENCY + + )} + + ); + }; + + /** + * Render Tab Navigation + * + * Purpose: Render tab navigation for different sections + * + * Tab Structure: + * - Overview: Patient demographics and medical information + * - AI Analysis: Merged view of DICOM images and AI predictions (formerly separate Images and Predictions tabs) + * - History: Processing history and notes + */ + const renderTabNavigation = () => { + if (!patientData) return null; + + return ( + + {[ + { key: 'overview', label: 'Overview', icon: 'info' }, + { key: 'aiAnalysis', label: 'AI Analysis', icon: 'activity', count: patientData.series_summary.length }, + { key: 'history', label: 'History', icon: 'clock' }, + ].map((tab) => ( + setActiveTab(tab.key as any)} + > + + + {tab.label} + + {tab.count !== undefined && ( + + {tab.count} + + )} + + ))} + + ); + }; + + /** + * Render Overview Tab + * + * Purpose: Render patient overview information + */ + const renderOverviewTab = () => { + if (!patientData) return null; + + return ( + + {/* Patient Information */} + + Patient Information + + + Patient ID + {patientData.patid} + + + Hospital ID + {patientData.hospital_id.substring(0, 8)}... + + + Name + {patientData.patient_info.name} + + + Age + {patientData.patient_info.age} + + + Sex + {patientData.patient_info.sex} + + + Date + {patientData.patient_info.date} + + + + + {/* Medical Information */} + + Medical Information + + + Institution + {patientData.patient_info.institution} + + + Modality + {patientData.patient_info.modality} + + + Status + {patientData.patient_info.status} + + + Report Status + {patientData.patient_info.report_status} + + + File Name + {patientData.patient_info.file_name} + + + Frame Count + {patientData.patient_info.frame_count} + + + + + {/* Processing Information */} + + Processing Information + + + First Processed + + {new Date(patientData.first_processed_at).toLocaleDateString()} + + + + Last Processed + + {new Date(patientData.last_processed_at).toLocaleDateString()} + + + + Total Predictions + {patientData.total_predictions} + + + Series Count + {patientData.series_summary.length} + + + + + ); + }; + + /** + * Render AI Analysis Tab + * + * Purpose: Render AI predictions and findings alongside their related images + * + * Features: + * - Merged view of DICOM images and AI predictions + * - Side-by-side display on larger screens, stacked on mobile + * - Summary statistics at the top + * - Series-based organization with integrated image and prediction data + * - Responsive design for different screen sizes + * - Feedback system for each series allowing physicians to provide clinical insights + */ + const renderAIAnalysisTab = () => { + if (!patientData) return null; + + if (patientData.series_summary.length === 0) { + return ( + + + + No Images Available + + No DICOM images are currently available for this patient + + + + ); + } + + return ( + + + AI Analysis & DICOM Images + + + {/* Summary Statistics */} + + + + Total Series + {patientData.series_summary.length} + + + + AI Predictions + {patientData.total_predictions} + + + + Processing Status + {patientData.patient_info.status} + + + + {patientData.series_summary.map((series, seriesIndex) => { + // Get predictions for this series + const seriesPredictions = patientData.predictions_by_series[series.series_num] || []; + const hasPredictions = seriesPredictions.length > 0; + + return ( + + {/* Series Header */} + + + + Series {series.series_num}: {series.series_description} + + handleNavigateToSeriesDetail(series)} + activeOpacity={0.7} + > + + Series Details + + + + {series.total_images} images โ€ข {series.modality} modality + {hasPredictions && ` โ€ข ${seriesPredictions.length} AI predictions`} + + + + {/* Series Details */} + + + Series Number: + {series.series_num} + + + Description: + + {series.series_description} + + + + Total Images: + {series.total_images} + + + Modality: + {series.modality} + + + AI Predictions: + + {hasPredictions ? `${seriesPredictions.length} found` : 'None'} + + + + + {/* Image and Predictions Row */} + + {/* DICOM Image */} + + + DICOM Preview + + {seriesPredictions[0]?.preview ? ( + handleImagePress(seriesIndex)} + > + + + Series {series.series_num} + + + ) : ( + + + + No Preview Available + + )} + + + {/* AI Predictions */} + + + AI Analysis Results + + + {hasPredictions ? ( + seriesPredictions.map((prediction) => ( + + + {prediction.prediction.label} + + + {prediction.prediction.clinical_urgency} + + + + + + + Finding Type: + + {prediction.prediction.finding_type} + + + + Confidence: + + {(prediction.prediction.confidence_score * 100).toFixed(1)}% + + + + Category: + + {prediction.prediction.finding_category} + + + + Severity: + + {prediction.prediction.primary_severity} + + + + Location: + + {prediction.prediction.anatomical_location} + + + + + + Processed: {new Date(prediction.processed_at).toLocaleDateString()} + + + )) + ) : ( + + + No AI predictions for this series + + )} + + + + ); + })} + + ); + }; + + /** + * Render History Tab + * + * Purpose: Render patient medical history + */ + const renderHistoryTab = () => { + if (!patientData) return null; + + return ( + + + Processing History + + + + First processed on {new Date(patientData.first_processed_at).toLocaleDateString()} + + + + + + Last updated on {new Date(patientData.last_processed_at).toLocaleDateString()} + + + + + + Status: {patientData.patient_info.status} case + + + + + + Total AI predictions: {patientData.total_predictions} + + + + + + Notes + + Patient case processed with {patientData.series_summary.length} DICOM series. + AI analysis completed with {patientData.total_predictions} predictions. + + + + ); + }; + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + if (isLoading) { + return ( + + + {renderLoadingState()} + + ); + } + + if (error) { + return ( + + + {renderErrorState()} + + ); + } + + if (!patientData) { + return ( + + + + + Patient Not Found + The requested patient data could not be found. + + + ); + } + + return ( + + + + {/* Header */} + + + + + + Patient Details + Emergency Department + + + + + + + {/* Patient Header */} + {renderPatientHeader()} + + {/* Tab Navigation */} + {renderTabNavigation()} + + {/* Tab Content */} + + } + > + {/* Overview Tab: Patient demographics and medical information */} + {activeTab === 'overview' && renderOverviewTab()} + + {/* AI Analysis Tab: Merged view of DICOM images and AI predictions */} + {activeTab === 'aiAnalysis' && renderAIAnalysisTab()} + + {/* History Tab: Processing history and notes */} + {activeTab === 'history' && renderHistoryTab()} + + + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Header Styles + header: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + backgroundColor: theme.colors.background, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + backButton: { + padding: theme.spacing.sm, + marginRight: theme.spacing.sm, + }, + headerTitle: { + flex: 1, + alignItems: 'center', + }, + headerTitleText: { + fontSize: 18, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + headerSubtitleText: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, + refreshButton: { + padding: theme.spacing.sm, + marginLeft: theme.spacing.sm, + }, + + // Patient Header Styles + patientHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.lg, + backgroundColor: theme.colors.background, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + patientInfo: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + patientAvatar: { + width: 60, + height: 60, + borderRadius: 30, + backgroundColor: theme.colors.backgroundAlt, + justifyContent: 'center', + alignItems: 'center', + marginRight: theme.spacing.md, + }, + patientDetails: { + flex: 1, + }, + patientName: { + fontSize: 20, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: 4, + }, + patientId: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + marginBottom: 8, + }, + patientMeta: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + patientMetaText: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, + statusBadge: { + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 16, + }, + statusText: { + fontSize: 12, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + textTransform: 'uppercase', + }, + emergencyButton: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.error, + paddingHorizontal: 16, + paddingVertical: 12, + borderRadius: 8, + shadowColor: theme.colors.error, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 4, + }, + emergencyButtonText: { + color: theme.colors.background, + fontSize: 12, + fontFamily: theme.typography.fontFamily.bold, + marginLeft: 8, + textTransform: 'uppercase', + }, + + // Tab Navigation Styles + tabContainer: { + flexDirection: 'row', + backgroundColor: theme.colors.background, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + tabButton: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.sm, + position: 'relative', + }, + activeTabButton: { + borderBottomWidth: 2, + borderBottomColor: theme.colors.primary, + }, + tabLabel: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + marginLeft: 8, + }, + activeTabLabel: { + color: theme.colors.primary, + fontFamily: theme.typography.fontFamily.bold, + }, + tabCount: { + backgroundColor: theme.colors.primary, + borderRadius: 10, + paddingHorizontal: 6, + paddingVertical: 2, + marginLeft: 8, + }, + tabCountText: { + color: theme.colors.background, + fontSize: 10, + fontFamily: theme.typography.fontFamily.bold, + }, + + // Content Styles + content: { + flex: 1, + }, + tabContent: { + padding: theme.spacing.md, + }, + + // Section Styles + section: { + marginBottom: theme.spacing.lg, + }, + sectionTitle: { + fontSize: 18, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.md, + }, + + // Info Grid Styles + infoGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + }, + infoItem: { + width: '48%', + marginBottom: theme.spacing.md, + }, + infoLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + marginBottom: 4, + textTransform: 'uppercase', + }, + infoValue: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.regular, + }, + + // Series Info Styles + seriesInfo: { + backgroundColor: theme.colors.backgroundAlt, + padding: theme.spacing.md, + borderRadius: 8, + }, + seriesText: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + marginBottom: 4, + }, + + // Images Styles + imagesContainer: { + marginTop: theme.spacing.sm, + }, + seriesContainer: { + marginBottom: theme.spacing.lg, + }, + seriesHeader: { + marginBottom: theme.spacing.md, + }, + seriesHeaderTop: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 4, + }, + seriesTitle: { + fontSize: 16, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + seriesDetailButton: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.backgroundAlt, + paddingHorizontal: 10, + paddingVertical: 6, + borderRadius: 16, + borderWidth: 1, + borderColor: theme.colors.border, + }, + seriesDetailButtonText: { + fontSize: 12, + color: theme.colors.primary, + fontFamily: theme.typography.fontFamily.medium, + marginLeft: 4, + }, + seriesMeta: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, + imageList: { + paddingRight: theme.spacing.md, + }, + imageThumbnail: { + width: 120, + height: 120, + borderRadius: 8, + marginRight: theme.spacing.sm, + position: 'relative', + shadowColor: '#000000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + thumbnailImage: { + width: '100%', + height: '100%', + borderRadius: 8, + }, + imageOverlay: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + backgroundColor: 'rgba(0, 0, 0, 0.7)', + paddingVertical: 4, + paddingHorizontal: 8, + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, + }, + imageNumber: { + color: theme.colors.background, + fontSize: 10, + fontFamily: theme.typography.fontFamily.bold, + textAlign: 'center', + }, + + // History Styles + historyItem: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + historyText: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + marginLeft: theme.spacing.sm, + }, + notesText: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + lineHeight: 20, + }, + + // Empty State Styles + emptyState: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: theme.spacing.xl, + }, + emptyStateTitle: { + fontSize: 18, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginTop: theme.spacing.md, + marginBottom: theme.spacing.sm, + }, + emptyStateSubtitle: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + textAlign: 'center', + lineHeight: 20, + }, + + // No Image Placeholder Styles + noImagePlaceholder: { + width: 120, + height: 120, + borderRadius: 8, + marginRight: theme.spacing.sm, + backgroundColor: theme.colors.backgroundAlt, + justifyContent: 'center', + alignItems: 'center', + borderWidth: 1, + borderColor: theme.colors.border, + borderStyle: 'dashed', + }, + noImageText: { + fontSize: 10, + color: theme.colors.textMuted, + fontFamily: theme.typography.fontFamily.regular, + textAlign: 'center', + marginTop: 4, + }, + + // Series Details Styles + seriesDetails: { + backgroundColor: theme.colors.backgroundAlt, + padding: theme.spacing.sm, + borderRadius: 8, + marginBottom: theme.spacing.md, + }, + seriesDetailItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: theme.spacing.xs, + }, + seriesDetailLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + flex: 1, + }, + seriesDetailValue: { + fontSize: 12, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.regular, + flex: 2, + textAlign: 'right', + }, + + // Prediction Styles + predictionSeries: { + marginBottom: theme.spacing.lg, + }, + predictionSeriesTitle: { + fontSize: 16, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.sm, + }, + predictionCard: { + backgroundColor: theme.colors.backgroundAlt, + borderRadius: 8, + padding: theme.spacing.md, + marginBottom: theme.spacing.sm, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + predictionHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + predictionLabel: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + urgencyBadge: { + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + }, + urgencyText: { + fontSize: 10, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + textTransform: 'uppercase', + }, + predictionDetails: { + marginBottom: theme.spacing.sm, + }, + predictionDetailItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: theme.spacing.xs, + }, + predictionDetailLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + flex: 1, + }, + predictionDetailValue: { + fontSize: 12, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.regular, + flex: 2, + textAlign: 'right', + }, + predictionTimestamp: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + textAlign: 'right', + }, + + // Loading and Error States + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: theme.spacing.lg, + }, + loadingText: { + marginTop: theme.spacing.sm, + fontSize: 16, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: theme.spacing.lg, + }, + errorTitle: { + fontSize: 18, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginTop: theme.spacing.md, + marginBottom: theme.spacing.sm, + }, + errorMessage: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + textAlign: 'center', + marginBottom: theme.spacing.md, + }, + retryButton: { + backgroundColor: theme.colors.primary, + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + borderRadius: 8, + shadowColor: theme.colors.primary, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 4, + }, + retryButtonText: { + color: theme.colors.background, + fontSize: 16, + fontFamily: theme.typography.fontFamily.bold, + }, + + // New styles for AI Analysis Tab + imagePredictionsRow: { + flexDirection: screenWidth > 600 ? 'row' : 'column', + justifyContent: 'space-between', + marginTop: theme.spacing.md, + }, + imageSection: { + flex: 1, + marginRight: screenWidth > 600 ? theme.spacing.md : 0, + marginBottom: screenWidth > 600 ? 0 : theme.spacing.md, + }, + imageSectionTitle: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.sm, + }, + predictionsSection: { + flex: 1, + }, + predictionsSectionTitle: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.sm, + }, + noPredictionsPlaceholder: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: theme.spacing.md, + }, + noPredictionsText: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + marginTop: theme.spacing.sm, + textAlign: 'center', + }, + + // New styles for AI Analysis Tab + analysisSummary: { + flexDirection: 'row', + justifyContent: 'space-around', + backgroundColor: theme.colors.backgroundAlt, + borderRadius: 8, + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.sm, + marginBottom: theme.spacing.md, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + summaryItem: { + alignItems: 'center', + }, + summaryLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + marginTop: theme.spacing.xs, + }, + summaryValue: { + fontSize: 18, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginTop: theme.spacing.xs, + }, + + +}); + +export default PatientDetailsScreen; diff --git a/app/modules/PatientCare/screens/PatientsScreen.tsx b/app/modules/PatientCare/screens/PatientsScreen.tsx new file mode 100644 index 0000000..e58a606 --- /dev/null +++ b/app/modules/PatientCare/screens/PatientsScreen.tsx @@ -0,0 +1,506 @@ +/* + * File: PatientsScreen.tsx + * Description: Main patients screen with search, filtering, and patient list + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useEffect, useCallback } from 'react'; +import { + View, + Text, + TouchableOpacity, + FlatList, + RefreshControl, + SafeAreaView, + StatusBar, + StyleSheet, + Alert, +} from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { useAppDispatch, useAppSelector } from '../../../store/hooks'; +import { theme } from '../../../theme/theme'; + +// Components +import PatientCard from '../components/PatientCard'; +import SearchBar from '../components/SearchBar'; +import FilterTabs from '../components/FilterTabs'; +import LoadingState from '../components/LoadingState'; +import EmptyState from '../components/EmptyState'; + +// Redux +import { + fetchPatients, + setSearchQuery, + setFilter, +} from '../redux/patientCareSlice'; +import { + selectPatients, + selectFilteredPatients, + selectPatientsLoading, + selectIsRefreshing, + selectPatientsError, + selectSearchQuery, + selectSelectedFilter, + selectPatientCounts, +} from '../redux/patientCareSelectors'; + +// Types +import { PatientData } from '../redux/patientCareSlice'; +import { selectUser } from '../../Auth/redux/authSelectors'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +// ============================================================================ +// PATIENTS SCREEN COMPONENT +// ============================================================================ + +/** + * PatientsScreen Component + * + * Purpose: Main screen for displaying and managing patient list + * + * Features: + * - Real-time patient data fetching + * - Search functionality with real-time filtering + * - Filter tabs (All, Processed, Pending, Error) + * - Sort options (Date, Name, Processed) + * - Pull-to-refresh functionality + * - Patient cards with vital information + * - Navigation to patient details + * - Loading and error states + * - Empty state handling + * - Modern ER-focused UI design + */ +const PatientsScreen: React.FC = () => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + const dispatch = useAppDispatch(); + const navigation = useNavigation(); + + // Redux state + const patients = useAppSelector(selectPatients); + const filteredPatients = useAppSelector(selectFilteredPatients); + const isLoading = useAppSelector(selectPatientsLoading); + const isRefreshing = useAppSelector(selectIsRefreshing); + const error = useAppSelector(selectPatientsError); + const searchQuery = useAppSelector(selectSearchQuery); + const selectedFilter = useAppSelector(selectSelectedFilter); + const patientCounts = useAppSelector(selectPatientCounts); + + // Auth state + const user = useAppSelector(selectUser); + + // ============================================================================ + // EFFECTS + // ============================================================================ + + /** + * Fetch Patients on Mount + * + * Purpose: Load patients when component mounts + */ + useEffect(() => { + if (user?.access_token) { + dispatch(fetchPatients(user.access_token)); + } + }, [dispatch, user?.access_token]); + + /** + * Clear Error on Unmount + * + * Purpose: Clean up error state when component unmounts + */ + useEffect(() => { + return () => { + // No clearError action in this file, so this effect is removed. + }; + }, [dispatch]); + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Handle Refresh + * + * Purpose: Handle pull-to-refresh functionality + */ + const handleRefresh = useCallback(() => { + if (user?.access_token) { + dispatch(fetchPatients(user.access_token)); + } + }, [dispatch, user?.access_token]); + + /** + * Handle Search + * + * Purpose: Handle search query changes + * + * @param query - Search query string + */ + const handleSearch = useCallback((query: string) => { + dispatch(setSearchQuery(query)); + }, [dispatch]); + + /** + * Handle Filter Change + * + * Purpose: Update the selected filter and refresh the list + */ + const handleFilterChange = useCallback((filter: 'all' | 'processed' | 'pending' | 'error') => { + dispatch(setFilter(filter)); + }, [dispatch]); + + /** + * Handle Patient Press + * + * Purpose: Navigate to patient details when a patient card is pressed + */ + const handlePatientPress = useCallback((patient: PatientData) => { + (navigation as any).navigate('PatientDetails', { + patientId: patient.patid, + patientName: patient.patient_info.name, + }); + }, [navigation]); + + /** + * Handle Emergency Alert + * + * Purpose: Show emergency alert for critical patients + */ + const handleEmergencyAlert = useCallback((patient: PatientData) => { + Alert.alert( + 'Emergency Alert', + `Patient ${patient.patient_info.name} (ID: ${patient.patid}) requires immediate attention!\n\nStatus: ${patient.patient_info.report_status}\nPriority: ${patient.patient_info.status}`, + [ + { text: 'Cancel', style: 'cancel' }, + { text: 'View Details', onPress: () => handlePatientPress(patient) }, + ] + ); + }, [handlePatientPress]); + + // ============================================================================ + // RENDER HELPERS + // ============================================================================ + + /** + * Render Patient Card + * + * Purpose: Render individual patient card component + */ + const renderPatientCard = useCallback(({ item }: { item: PatientData }) => ( + handlePatientPress(item)} + onEmergencyPress={() => handleEmergencyAlert(item)} + /> + ), [handlePatientPress, handleEmergencyAlert]); + + /** + * Render Header + * + * Purpose: Render the screen header with title and action buttons + */ + const renderHeader = () => ( + + + Patients + + {filteredPatients.length} of {patients?.length || 0} patients + + + + {/* + { + // TODO: Implement sort modal + }} + > + Sort + + + { + // TODO: Implement filter modal + }} + > + Filter + + */} + + ); + + /** + * Render Empty State + * + * Purpose: Render empty state when no patients found + */ + const renderEmptyState = () => ( + + ); + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + if (error && !isLoading) { + return ( + + + + Error Loading Patients + {error} + + Retry + + + + ); + } + + return ( + + + + {/* Header */} + {renderHeader()} + + {/* Search and Filters */} + + + + + + + {/* Loading State */} + {isLoading && patients.length === 0 && ( + + + + )} + + {/* Error State */} + {error && patients.length === 0 && ( + + + + )} + + {/* Empty State */} + {!isLoading && !error && patients.length === 0 && ( + + + + )} + + {/* No Results State */} + {!isLoading && !error && patients.length > 0 && filteredPatients.length === 0 && ( + + { + handleSearch(''); + handleFilterChange('all'); + }} + /> + + )} + + {/* Patient List */} + {!isLoading && !error && filteredPatients.length > 0 && ( + item.patid} + contentContainerStyle={styles.listContainer} + showsVerticalScrollIndicator={false} + refreshControl={ + + } + /> + )} + + {/* TODO: Implement sort and filter modals for enhanced functionality */} + + {/* Note: Patient data will be loaded from API when fetchPatients is called */} + {/* Currently using mock data from Redux slice for development */} + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + // Container Styles + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Header Styles + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.md, + backgroundColor: theme.colors.background, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + marginBottom: theme.spacing.md, + }, + headerLeft: { + flex: 1, + }, + headerRight: { + flexDirection: 'row', + gap: theme.spacing.sm, + }, + headerTitle: { + fontSize: 24, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + headerSubtitle: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, + actionButton: { + backgroundColor: theme.colors.backgroundAlt, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + borderRadius: 8, + borderWidth: 1, + borderColor: theme.colors.border, + }, + actionButtonText: { + color: theme.colors.textSecondary, + fontSize: 14, + fontFamily: theme.typography.fontFamily.medium, + }, + + // Search and Filters + searchAndFilters: { + paddingHorizontal: theme.spacing.md, + paddingBottom: theme.spacing.sm, + backgroundColor: theme.colors.background, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + + // Center Container for States + centerContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: theme.spacing.md, + }, + + // List Styles + listContainer: { + paddingBottom: theme.spacing.lg, + }, + listFooter: { + paddingVertical: theme.spacing.md, + alignItems: 'center', + }, + footerText: { + fontSize: 14, + color: theme.colors.textMuted, + fontFamily: theme.typography.fontFamily.regular, + }, + + // Error State Styles + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: theme.spacing.xl, + }, + errorTitle: { + fontSize: 20, + color: theme.colors.error, + marginBottom: theme.spacing.sm, + fontFamily: theme.typography.fontFamily.bold, + textAlign: 'center', + }, + errorMessage: { + fontSize: 16, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.lg, + fontFamily: theme.typography.fontFamily.regular, + textAlign: 'center', + }, + retryButton: { + backgroundColor: theme.colors.primary, + paddingHorizontal: theme.spacing.lg, + paddingVertical: theme.spacing.md, + borderRadius: 8, + minWidth: 120, + alignItems: 'center', + }, + retryButtonText: { + color: theme.colors.background, + fontSize: 16, + fontFamily: theme.typography.fontFamily.medium, + }, +}); + +export default PatientsScreen; + +/* + * End of File: PatientsScreen.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/screens/SeriesDetailScreen.tsx b/app/modules/PatientCare/screens/SeriesDetailScreen.tsx new file mode 100644 index 0000000..40cfb39 --- /dev/null +++ b/app/modules/PatientCare/screens/SeriesDetailScreen.tsx @@ -0,0 +1,2865 @@ +/* + * File: SeriesDetailScreen.tsx + * Description: Detailed series information screen with predictions and feedback + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + * + * Features: + * - Complete series information and metadata + * - AI predictions and findings for the series + * - Feedback history from physicians + * - Floating feedback button for new feedback + * - Responsive design for different screen sizes + * - Integration with patient data and feedback system + * - Tabbed interface for better organization + */ + +import React, { useEffect, useState, useCallback } from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + TouchableOpacity, + StatusBar, + Alert, + Dimensions, + Image, + FlatList, + RefreshControl, + TextInput, +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import { useAppDispatch, useAppSelector } from '../../../store/hooks'; +import Icon from 'react-native-vector-icons/Feather'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +// Import types and API +import { patientAPI } from '../services/patientAPI'; +import { selectUser } from '../../Auth/redux/authSelectors'; +import { API_CONFIG } from '../../../shared/utils'; +import { SeriesDetailScreenProps } from '../navigation/navigationTypes'; + +// Get screen dimensions +const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface Feedback { + feedback_id: string; + user_id: string; + feedback_text: string; + is_positive: boolean; + email: string; + created_at: string; + prediction_id: number; + prediction_file_path: string; + series_number: string; + feedback_type: string; +} + +interface Prediction { + id: number; + file_path: string; + prediction: { + label: string; + finding_type: string; + clinical_urgency: string; + confidence_score: number; + detailed_results: any; + finding_category: string; + primary_severity: string; + anatomical_location: string; + // Hemorrhage type properties + epidural?: boolean; + subdural?: boolean; + intraparenchymal?: boolean; + subarachnoid?: boolean; + intraventricular?: boolean; + epidural_percentage?: number; + subdural_percentage?: number; + intraparenchymal_percentage?: number; + subarachnoid_percentage?: number; + intraventricular_percentage?: number; + midline_shift?: number; + }; + processed_at: string; + preview: string; +} + +// Tab types +type TabType = 'series' | 'ai' | 'feedback'; + +// ============================================================================ +// SERIES DETAIL SCREEN COMPONENT +// ============================================================================ + +/** + * SeriesDetailScreen Component + * + * Purpose: Detailed view of a specific DICOM series with predictions and feedback + * + * Features: + * - Complete series information and metadata + * - AI predictions and findings display + * - Feedback history from physicians + * - Floating feedback button for new feedback + * - Responsive design for different screen sizes + * - Integration with patient data and feedback system + * - Tabbed interface for better organization + */ +const SeriesDetailScreen: React.FC = ({ navigation, route }) => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + const dispatch = useAppDispatch(); + + // Route parameters + const { patientId, patientName, seriesNumber, seriesData, patientData, onFeedbackSubmitted } = route.params; + + // Redux state + const user = useAppSelector(selectUser); + + // Local state + const [isLoading, setIsLoading] = useState(false); + const [isRefreshing, setIsRefreshing] = useState(false); + const [error, setError] = useState(null); + + // Tab state + const [activeTab, setActiveTab] = useState('series'); + + // Local patient data state for real-time updates + const [localPatientData, setLocalPatientData] = useState(patientData); + + // Feedback state + const [showFeedbackModal, setShowFeedbackModal] = useState(false); + const [selectedPrediction, setSelectedPrediction] = useState(null); + const [feedbackText, setFeedbackText] = useState(''); + const [isPositive, setIsPositive] = useState(null); + const [isSubmittingFeedback, setIsSubmittingFeedback] = useState(false); + + // Feedback result modal state + const [showFeedbackResultModal, setShowFeedbackResultModal] = useState(false); + const [feedbackResult, setFeedbackResult] = useState<{ + type: 'success' | 'error'; + title: string; + message: string; + } | null>(null); + + // Track newly added feedback for visual indication + const [newFeedbackIds, setNewFeedbackIds] = useState>(new Set()); + + // ============================================================================ + // EFFECTS + // ============================================================================ + + /** + * Component Mount Effect + * + * Purpose: Set navigation title and initialize screen + */ + useEffect(() => { + navigation.setOptions({ + title: `Series ${seriesNumber}`, + headerShown: false, + }); + }, [navigation, seriesNumber]); + + /** + * Sync Local Patient Data Effect + * + * Purpose: Keep local patient data in sync with route params + */ + useEffect(() => { + setLocalPatientData(patientData); + }, [patientData]); + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Refresh Patient Data + * + * Purpose: Fetch updated patient data including new feedback + */ + const refreshPatientData = useCallback(async () => { + if (!user?.access_token) return; + + try { + const response: any = await patientAPI.getPatientDetailsById(patientId, user.access_token); + + if (response.ok && response.data && response.data.data) { + // Update the local patient data with fresh data from API + // This will include the newly submitted feedback + const updatedPatientData = response.data.data; + setLocalPatientData(updatedPatientData); + + // Also update the route params for consistency + route.params.patientData = updatedPatientData; + + setError(null); + } + } catch (err: any) { + console.log('Error refreshing patient data:', err.message); + // Don't show error to user for background refresh + } + }, [patientId, user?.access_token, route.params]); + + /** + * Handle Back Navigation + * + * Purpose: Navigate back to previous screen + */ + const handleBackPress = useCallback(() => { + navigation.goBack(); + }, [navigation]); + + /** + * Handle Refresh + * + * Purpose: Pull-to-refresh functionality + */ + const handleRefresh = useCallback(async () => { + setIsRefreshing(true); + // Refresh patient data to get latest information + await refreshPatientData(); + setIsRefreshing(false); + }, [refreshPatientData]); + + /** + * Handle Open Feedback Modal + * + * Purpose: Open feedback modal for a specific prediction + * + * @param prediction - Prediction data for feedback + */ + const handleOpenFeedback = useCallback((prediction: Prediction) => { + setSelectedPrediction(prediction); + setFeedbackText(''); + setIsPositive(null); + setShowFeedbackModal(true); + }, []); + + /** + * Handle Submit Feedback + * + * Purpose: Submit feedback to API + */ + const handleSubmitFeedback = useCallback(async () => { + if (!selectedPrediction || !feedbackText.trim() || isPositive === null) { + setFeedbackResult({ + type: 'error', + title: 'Validation Error', + message: 'Please provide all required feedback information' + }); + setShowFeedbackResultModal(true); + return; + } + + try { + setIsSubmittingFeedback(true); + + if (!patientId) { + throw new Error('Patient ID not available'); + } + + const feedbackPayload = { + patid: patientId, + prediction_id: selectedPrediction.id, + feedback_text: feedbackText.trim(), + is_positive: isPositive + }; + + console.log('Submitting feedback payload:', feedbackPayload); + + // Call the actual API + const response = await patientAPI.submitFeedback(feedbackPayload, user?.access_token); + console.log('Feedback response:', response); + + if (!response.ok) { + throw new Error(response.problem || 'Failed to submit feedback'); + } + + // Show success message + setFeedbackResult({ + type: 'success', + title: 'Feedback Submitted', + message: 'Your feedback has been recorded successfully.' + }); + setShowFeedbackResultModal(true); + + // Track this feedback as newly added for visual indication + const newFeedbackId = `new_${Date.now()}`; + setNewFeedbackIds(prev => new Set(prev).add(newFeedbackId)); + + // Refresh patient data to get updated feedback + await refreshPatientData(); + + // Notify parent screen to refresh its data as well + // This ensures PatientDetailsScreen shows updated information when user navigates back + if (onFeedbackSubmitted) { + onFeedbackSubmitted(); + } + } catch (error: any) { + setFeedbackResult({ + type: 'error', + title: 'Error', + message: error.message || 'Failed to submit feedback. Please try again.' + }); + setShowFeedbackResultModal(true); + } finally { + setIsSubmittingFeedback(false); + } + }, [selectedPrediction, feedbackText, isPositive, patientId, user?.access_token, refreshPatientData, onFeedbackSubmitted]); + + /** + * Handle Close Feedback Modal + * + * Purpose: Close feedback modal and reset state + */ + const handleCloseFeedback = useCallback(() => { + setShowFeedbackModal(false); + setSelectedPrediction(null); + setFeedbackText(''); + setIsPositive(null); + }, []); + + /** + * Handle Feedback Result Modal Close + * + * Purpose: Close feedback result modal and reset form if success + */ + const handleFeedbackResultClose = useCallback(() => { + setShowFeedbackResultModal(false); + setFeedbackResult(null); + + // If it was a success, also close the feedback modal and reset form + if (feedbackResult?.type === 'success') { + setShowFeedbackModal(false); + setSelectedPrediction(null); + setFeedbackText(''); + setIsPositive(null); + } + }, [feedbackResult?.type]); + + /** + * Auto-close Success Modal Effect + * + * Purpose: Automatically close success modal after 2 seconds + */ + useEffect(() => { + if (feedbackResult?.type === 'success') { + const timer = setTimeout(() => { + handleFeedbackResultClose(); + }, 2000); + + return () => clearTimeout(timer); + } + }, [feedbackResult?.type, handleFeedbackResultClose]); + + /** + * Clear New Feedback Badges Effect + * + * Purpose: Clear "New" badges after 30 seconds to avoid UI clutter + */ + useEffect(() => { + if (newFeedbackIds.size > 0) { + const timer = setTimeout(() => { + setNewFeedbackIds(new Set()); + }, 30000); // 30 seconds + + return () => clearTimeout(timer); + } + }, [newFeedbackIds.size]); + + // ============================================================================ + // UTILITY FUNCTIONS + // ============================================================================ + + /** + * Get Clinical Urgency Color + * + * Purpose: Get appropriate color for clinical urgency + * + * @param urgency - Clinical urgency level + */ + const getUrgencyColor = (urgency: string) => { + switch (urgency.toLowerCase()) { + case 'urgent': + return theme.colors.error; + case 'semi-urgent': + return theme.colors.warning; + case 'non-urgent': + return theme.colors.success; + default: + return theme.colors.info; + } + }; + + /** + * Get Feedback Type Color + * + * Purpose: Get appropriate color for feedback type + * + * @param feedbackType - Feedback type (positive/negative) + */ + const getFeedbackTypeColor = (feedbackType: string) => { + switch (feedbackType.toLowerCase()) { + case 'positive': + return theme.colors.success; + case 'negative': + return theme.colors.error; + default: + return theme.colors.info; + } + }; + + /** + * Get Percentage Color + * + * Purpose: Get appropriate color based on percentage value + * + * @param percentage - Percentage value (0-100) + */ + const getPercentageColor = (percentage: number) => { + if (percentage >= 70) { + return theme.colors.error; // High detection - Red + } else if (percentage >= 40) { + return theme.colors.warning; // Medium detection - Orange + } else if (percentage >= 10) { + return theme.colors.info; // Low detection - Blue + } else { + return theme.colors.success; // No detection - Green + } + }; + + /** + * Get Percentage Status + * + * Purpose: Get status text based on percentage value + * + * @param percentage - Percentage value (0-100) + */ + const getPercentageStatus = (percentage: number) => { + if (percentage >= 70) { + return 'HIGH'; + } else if (percentage >= 40) { + return 'MEDIUM'; + } else if (percentage >= 10) { + return 'LOW'; + } else { + return 'NONE'; + } + }; + + /** + * Get Percentage Value + * + * Purpose: Extract percentage value from prediction data with flexible field naming + * + * @param prediction - Prediction object + * @param type - Hemorrhage type (epidural, subdural, etc.) + */ + const getPercentageValue = (prediction: any, type: string): number => { + // First, try to get from detailed_results.hemorrhage_detection + if (prediction.detailed_results?.hemorrhage_detection) { + const hemorrhageData = prediction.detailed_results.hemorrhage_detection; + + // Map our types to the actual field names in the data + const fieldMapping: { [key: string]: string } = { + 'epidural': 'Epidural', + 'subdural': 'Subdural', + 'subarachnoid': 'Subarachnoid', + 'intraparenchymal': 'Intraparenchymal', + 'intraventricular': 'Intraventricular', + 'midline_shift': 'Midline shift' + }; + + const actualFieldName = fieldMapping[type]; + if (actualFieldName && hemorrhageData[actualFieldName] !== undefined) { + const value = hemorrhageData[actualFieldName]; + + // Convert decimal (0-1) to percentage (0-100) + if (typeof value === 'number' && value >= 0 && value <= 1) { + return value * 100; + } + + // If it's already a percentage, return as is + if (typeof value === 'number' && value >= 0 && value <= 100) { + return value; + } + + // Handle string values + if (typeof value === 'string') { + const numValue = parseFloat(value); + if (!isNaN(numValue)) { + if (numValue >= 0 && numValue <= 1) { + return numValue * 100; + } + if (numValue >= 0 && numValue <= 100) { + return numValue; + } + } + } + } + } + + // Fallback: try different possible field names for the percentage + const possibleFields = [ + `${type}_percentage`, + `${type}_score`, + `${type}_value`, + `${type}_detection`, + `${type}_probability`, + type + ]; + + for (const field of possibleFields) { + if (prediction[field] !== undefined && prediction[field] !== null) { + const value = prediction[field]; + + // Handle different data types + if (typeof value === 'number') { + // If it's already a percentage (0-100), return as is + if (value >= 0 && value <= 100) { + return value; + } + // If it's a decimal (0-1), convert to percentage + if (value >= 0 && value <= 1) { + return value * 100; + } + // If it's a large number, assume it's already a percentage + return value; + } + + // Handle string values + if (typeof value === 'string') { + const numValue = parseFloat(value); + if (!isNaN(numValue)) { + // If it's already a percentage (0-100), return as is + if (numValue >= 0 && numValue <= 100) { + return numValue; + } + // If it's a decimal (0-1), convert to percentage + if (numValue >= 0 && numValue <= 1) { + return numValue * 100; + } + // If it's a large number, assume it's already a percentage + return numValue; + } + } + + // Handle boolean values (convert to 0% or 100%) + if (typeof value === 'boolean') { + return value ? 100 : 0; + } + } + } + + // If no valid percentage found, return 0 + return 0; + }; + + /** + * Get Adjusted Percentage Value + * + * Purpose: Show 100% when there's only 1 image in the series to indicate full analysis + * + * @param prediction - Prediction object + * @param type - Hemorrhage type (epidural, subdural, etc.) + */ + const getAdjustedPercentageValue = (prediction: any, type: string): number => { + const basePercentage = getPercentageValue(prediction, type); + + // If there's only 1 image in the series, show 100% to indicate full analysis + if (seriesData && seriesData.total_images === 1) { + return 100; + } + + return basePercentage; + }; + + /** + * Get Series Predictions + * + * Purpose: Get predictions for the current series + */ + const getSeriesPredictions = () => { + if (!localPatientData?.predictions_by_series) return []; + return localPatientData.predictions_by_series[seriesNumber] || []; + }; + + /** + * Get Series Feedback + * + * Purpose: Get feedback for the current series + */ + const getSeriesFeedback = () => { + if (!localPatientData?.feedback_by_series) return []; + return localPatientData.feedback_by_series[seriesNumber] || []; + }; + + /** + * Check if Feedback is New + * + * Purpose: Check if feedback was recently added for visual indication + * + * @param feedbackId - Feedback ID to check + */ + const isFeedbackNew = (feedbackId: string) => { + return newFeedbackIds.has(feedbackId); + }; + + // ============================================================================ + // RENDER HELPERS + // ============================================================================ + + /** + * Render Tab Navigation + * + * Purpose: Render tab navigation for switching between different sections + */ + const renderTabNavigation = () => { + const tabs = [ + { id: 'series' as TabType, label: 'Series Info', icon: 'info' }, + { id: 'ai' as TabType, label: 'AI Analysis', icon: 'activity' }, + { id: 'feedback' as TabType, label: 'Feedback', icon: 'message-circle' } + ]; + + return ( + + {tabs.map((tab) => ( + setActiveTab(tab.id)} + activeOpacity={0.7} + > + + + {tab.label} + + {/* Active Tab Indicator */} + {activeTab === tab.id && ( + + )} + + ))} + + ); + }; + + /** + * Render Series Header + * + * Purpose: Render series identification and basic information + */ + const renderSeriesHeader = () => { + if (!seriesData) return null; + + return ( + + + + + Series {seriesData.series_num} + + + {seriesData.series_description || 'No description available'} + + + + + {seriesData.total_images} images + + + + {seriesData.modality} modality + + {seriesData.body_part && ( + + + {seriesData.body_part} + + )} + + + + + + + Processed + + {seriesData.study_date && ( + + {new Date(seriesData.study_date).toLocaleDateString()} + + )} + + + + ); + }; + + /** + * Render Series Details + * + * Purpose: Render detailed series information in a comprehensive format + */ + const renderSeriesDetails = () => { + if (!seriesData) return null; + + return ( + + Series Information + + {/* Series Summary Bar */} + + + Series Number + + {seriesData.series_num} + + + + Total Images + + {seriesData.total_images} + + + + Modality + + {seriesData.modality} + + + + + {/* Detailed Series Information Card */} + + + Series Details + Complete series metadata and information + + + + + + Series Number + {seriesData.series_num} + + + Series Description + + {seriesData.series_description || 'No description available'} + + + + + + + Total Images + {seriesData.total_images} + + + Modality + {seriesData.modality} + + + + + + Patient ID + {patientId} + + + Patient Name + {patientName} + + + + {seriesData.body_part && ( + + + Body Part + {seriesData.body_part} + + + Study Date + + {seriesData.study_date ? new Date(seriesData.study_date).toLocaleDateString() : 'Not specified'} + + + + )} + + {seriesData.institution_name && ( + + + Institution + {seriesData.institution_name} + + + Manufacturer + {seriesData.manufacturer || 'Not specified'} + + + )} + + + {/* Processing Information */} + + Processing Information + + + File Type + DICOM + + + Multiframe + + {seriesData.total_images > 1 ? 'Yes' : 'No'} + + + + Frames + {seriesData.total_images} + + + Status + + Processed + + + + + + + ); + }; + + /** + * Render AI Predictions + * + * Purpose: Render AI predictions and findings for the series + */ + const renderAIPredictions = () => { + const predictions = getSeriesPredictions(); + + // Debug: Log the prediction data structure + + if (predictions.length === 0) { + return ( + + AI Analysis Results + + + No AI Predictions + + No AI predictions are available for this series yet + + + + ); + } + + // Calculate summary metrics + const totalPredictions = predictions.length; + const highPriorityCount = predictions.filter((p: Prediction) => + p.prediction.clinical_urgency?.toLowerCase() === 'urgent' + ).length; + const avgConfidence = predictions.reduce((sum: number, p: Prediction) => + sum + (p.prediction.confidence_score || 0), 0 + ) / totalPredictions; + + return ( + + AI Analysis Results + + {/* AI Summary Bar */} + + + Total Predictions + + {totalPredictions} predictions found + + + + High Priority + + + {highPriorityCount > 0 ? `${highPriorityCount} urgent` : 'None'} + + + + + Avg Confidence + + + {(avgConfidence * 100).toFixed(1)}% + + + + + + {/* AI Predictions Card */} + + + AI Predictions Analysis + Medical scan analysis results + + + {predictions.map((prediction: Prediction) => ( + + + {prediction.prediction.label} + + + {prediction.prediction.clinical_urgency} + + + + + + + Finding Type: + + {prediction.prediction.finding_type} + + + + Confidence: + + {(prediction.prediction.confidence_score * 100).toFixed(1)}% + + + + Category: + + {prediction.prediction.finding_category} + + + + Severity: + + {prediction.prediction.primary_severity} + + + + Location: + + {prediction.prediction.anatomical_location} + + + + + + + {/* Show specific hemorrhage types if they exist */} + {prediction.prediction.epidural !== undefined && ( + + Epidural Hemorrhage: + + {prediction.prediction.epidural ? 'Detected' : 'Not Detected'} + + + )} + + {prediction.prediction.subdural !== undefined && ( + + Subdural Hemorrhage: + + {prediction.prediction.subdural ? 'Detected' : 'Not Detected'} + + + )} + + {prediction.prediction.intraparenchymal !== undefined && ( + + Intraparenchymal Hemorrhage: + + {prediction.prediction.intraparenchymal ? 'Detected' : 'Not Detected'} + + + )} + + {prediction.prediction.subarachnoid !== undefined && ( + + Subarachnoid Hemorrhage: + + {prediction.prediction.subarachnoid ? 'Detected' : 'Not Detected'} + + + )} + + {prediction.prediction.intraventricular !== undefined && ( + + Intraventricular Hemorrhage: + + {prediction.prediction.intraventricular ? 'Detected' : 'Not Detected'} + + + )} + + {/* Additional Findings from detailed_results */} + {prediction.prediction.detailed_results && ( + + Additional Analysis Results + + {/* Stroke Detection */} + {prediction.prediction.detailed_results.stroke_detection && ( + + Stroke Detection: + + + Normal: {((prediction.prediction.detailed_results.stroke_detection.Normal || 0) * 100).toFixed(1)}% + + + Stroke: {((prediction.prediction.detailed_results.stroke_detection.Stroke || 0) * 100).toFixed(1)}% + + + + )} + + {/* Binary Hemorrhage */} + {prediction.prediction.detailed_results.binary_hemorrhage && ( + + Hemorrhage Detection: + + + Normal: {((prediction.prediction.detailed_results.binary_hemorrhage.Normal || 0) * 100).toFixed(1)}% + + + Hemorrhage: {((prediction.prediction.detailed_results.binary_hemorrhage.Hemorrhage || 0) * 100).toFixed(1)}% + + + + )} + + )} + + {/* Visual Indicators Section - Show detailed findings with percentage indicators */} + + Detailed Findings Analysis + + {/* Compact Hemorrhage Type Percentage Indicators with Progress Bars */} + + {/* Epidural */} + + + + Epidural + + {getAdjustedPercentageValue(prediction.prediction, 'epidural').toFixed(1)}% + + + + + + + + {/* Subdural */} + + + + Subdural + + {getAdjustedPercentageValue(prediction.prediction, 'subdural').toFixed(1)}% + + + + + + + + {/* Intraparenchymal */} + + + + Intraparenchymal + + {getAdjustedPercentageValue(prediction.prediction, 'intraparenchymal').toFixed(1)}% + + + + + + + + {/* Subarachnoid */} + + + + Subarachnoid + + {getAdjustedPercentageValue(prediction.prediction, 'subarachnoid').toFixed(1)}% + + + + + + + + {/* Intraventricular */} + + + + Intraventricular + + {getAdjustedPercentageValue(prediction.prediction, 'intraventricular').toFixed(1)}% + + + + + + + + {/* Midline Shift */} + + + + Midline Shift + + {getAdjustedPercentageValue(prediction.prediction, 'midline_shift').toFixed(1)}% + + + + 50 ? theme.colors.error : theme.colors.warning + } + ]} + /> + + + + + {/* Summary Indicator with Overall Percentage */} + + Overall Assessment + + {/* Overall Percentage Calculation */} + {(() => { + const percentages = [ + getAdjustedPercentageValue(prediction.prediction, 'epidural'), + getAdjustedPercentageValue(prediction.prediction, 'subdural'), + getAdjustedPercentageValue(prediction.prediction, 'intraparenchymal'), + getAdjustedPercentageValue(prediction.prediction, 'subarachnoid'), + getAdjustedPercentageValue(prediction.prediction, 'intraventricular'), + getAdjustedPercentageValue(prediction.prediction, 'midline_shift') + ]; + const maxPercentage = Math.max(...percentages); + const hasHemorrhage = maxPercentage > 10; // Consider >10% as detected + + return ( + + Highest Detection: + {maxPercentage.toFixed(1)}% + + + + + {hasHemorrhage ? 'HEMORRHAGE DETECTED' : 'NO HEMORRHAGE'} + + + + ); + })()} + + + + + Processed: {new Date(prediction.processed_at).toLocaleDateString()} + + + ))} + + + ); + }; + + /** + * Render Series Images + * + * Purpose: Render DICOM image previews for the series + */ + const renderSeriesImages = () => { + const predictions = getSeriesPredictions(); + + if (predictions.length === 0) { + return ( + + DICOM Images + + + No Images Available + + No DICOM images are available for this series + + + + ); + } + + // Calculate image summary metrics + const totalImages = predictions.length; + const imagesWithPreview = predictions.filter((p: Prediction) => p.preview).length; + const imagesWithoutPreview = totalImages - imagesWithPreview; + + return ( + + DICOM Images + + {/* Images Summary Bar */} + + + Total Images + + {totalImages} + + + + With Preview + + {imagesWithPreview} + + + + Format + + DICOM + + + + + {/* Images Display Card */} + + + Image Gallery + Series image previews and metadata + + + + {predictions.map((prediction: Prediction, index: number) => ( + + {prediction.preview ? ( + + ) : ( + + + No Preview + + )} + Image {index + 1} + + {prediction.prediction.label} + + + ))} + + + + ); + }; + + /** + * Render Feedback History + * + * Purpose: Render feedback history from physicians + */ + const renderFeedbackHistory = () => { + const feedback = getSeriesFeedback(); + + if (isRefreshing) { + return ( + + Feedback History + + + Refreshing feedback... + + + ); + } + + if (feedback.length === 0) { + return ( + + Feedback History + + + No Feedback Yet + + Be the first to provide feedback on this series + + + + ); + } + + // Calculate feedback summary metrics + const totalFeedback = feedback.length; + const positiveFeedback = feedback.filter((f: Feedback) => f.is_positive).length; + const negativeFeedback = feedback.filter((f: Feedback) => !f.is_positive).length; + + return ( + + Feedback History + + {/* Feedback Summary Bar */} + + + Total Feedback + + {totalFeedback} entries + + + + Positive + + {positiveFeedback} + + + + Negative + + {negativeFeedback} + + + + + {/* Feedback History Card */} + + + Clinical Feedback + Radiologist insights and corrections + + + {feedback.map((feedbackItem: Feedback) => ( + + + + + + {feedbackItem.feedback_type} + + + {isFeedbackNew(feedbackItem.feedback_id) && ( + + NEW + + )} + + + {new Date(feedbackItem.created_at).toLocaleDateString()} + + + + {feedbackItem.feedback_text} + + + {feedbackItem.email} + + Prediction ID: {feedbackItem.prediction_id} + + + + ))} + + + ); + }; + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + + + {/* Header */} + + + + + + Series {seriesNumber} + {patientName} + + + + + + + {/* Tab Navigation */} + {renderTabNavigation()} + + {/* Content */} + + } + > + {/* Series Header */} + {renderSeriesHeader()} + + {/* Tab Content */} + {activeTab === 'series' && ( + <> + {/* Series Details */} + {renderSeriesDetails()} + + {/* Series Images */} + {renderSeriesImages()} + + )} + + {activeTab === 'ai' && ( + <> + {/* AI Predictions */} + {renderAIPredictions()} + + )} + + {activeTab === 'feedback' && ( + <> + {/* Feedback History */} + {renderFeedbackHistory()} + + )} + + + {/* Floating Feedback Button - Show on all tabs */} + { + const predictions = getSeriesPredictions(); + if (predictions.length > 0) { + handleOpenFeedback(predictions[0]); + } else { + Alert.alert('No Predictions', 'No AI predictions available for feedback'); + } + }} + activeOpacity={0.8} + > + + + + {/* Feedback Modal */} + {showFeedbackModal && selectedPrediction && ( + + + + Provide Feedback + + + + + + + {/* Prediction Info */} + + + AI Prediction: {selectedPrediction.prediction.label} + + + Confidence: {(selectedPrediction.prediction.confidence_score * 100).toFixed(1)}% โ€ข + Type: {selectedPrediction.prediction.finding_type} + + + + {/* Prediction Accuracy Selection */} + + Is this prediction accurate? + + {[ + { key: 'true', label: 'Yes (Positive)', color: theme.colors.success, icon: 'check-circle', value: true }, + { key: 'false', label: 'No (Negative)', color: theme.colors.error, icon: 'x-circle', value: false } + ].map((option) => ( + setIsPositive(option.value)} + style={[ + styles.predictionAccuracyButton, + isPositive === option.value && styles.predictionAccuracyButtonActive, + { borderColor: option.color } + ]} + > + + + {option.label} + + + ))} + + + + {/* Feedback Text Input */} + + Your Feedback + + + + + + + Cancel + + + {isSubmittingFeedback ? ( + + + Submitting... + + ) : ( + Submit Feedback + )} + + + + + )} + + {/* Feedback Result Modal */} + {showFeedbackResultModal && feedbackResult && ( + + + + + + {feedbackResult.title} + + + + + + {feedbackResult.message} + + + + + + OK + + + + + )} + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Header Styles + header: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + backgroundColor: theme.colors.background, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + backButton: { + padding: theme.spacing.sm, + marginRight: theme.spacing.sm, + }, + headerTitle: { + flex: 1, + alignItems: 'center', + }, + headerTitleText: { + fontSize: 18, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + headerSubtitleText: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, + refreshButton: { + padding: theme.spacing.sm, + marginLeft: theme.spacing.sm, + }, + + // Content Styles + content: { + flex: 1, + }, + + // Section Styles + section: { + marginBottom: theme.spacing.lg, + paddingHorizontal: theme.spacing.md, + }, + sectionTitle: { + fontSize: 18, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.md, + }, + + // Series Header Styles + seriesHeader: { + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.lg, + backgroundColor: theme.colors.background, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + seriesHeaderContent: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + seriesHeaderLeft: { + flex: 1, + }, + seriesHeaderRight: { + alignItems: 'flex-end', + }, + seriesTitle: { + fontSize: 24, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.xs, + }, + seriesDescription: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + marginBottom: theme.spacing.sm, + }, + seriesMetaRow: { + flexDirection: 'row', + flexWrap: 'wrap', + marginTop: theme.spacing.xs, + }, + metaItem: { + flexDirection: 'row', + alignItems: 'center', + marginRight: theme.spacing.md, + marginBottom: theme.spacing.xs, + }, + metaText: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + marginLeft: theme.spacing.sm, + }, + studyDate: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, + seriesStatusBadge: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.success, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.xs, + borderRadius: 12, + }, + seriesStatusText: { + fontSize: 12, + color: theme.colors.background, + fontFamily: theme.typography.fontFamily.bold, + marginLeft: theme.spacing.sm, + }, + + // Series Details Styles + seriesDetails: { + backgroundColor: theme.colors.backgroundAlt, + padding: theme.spacing.md, + borderRadius: 8, + }, + seriesDetailItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: theme.spacing.sm, + }, + seriesDetailLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + flex: 1, + }, + seriesDetailValue: { + fontSize: 12, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.regular, + flex: 2, + textAlign: 'right', + }, + + // Prediction Styles + predictionCard: { + backgroundColor: theme.colors.backgroundAlt, + borderRadius: 8, + padding: theme.spacing.sm, + marginBottom: theme.spacing.sm, + // shadowColor: '#000', + // shadowOffset: { width: 0, height: 2 }, + // shadowOpacity: 0.1, + // shadowRadius: 4, + // elevation: 2, + }, + predictionHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + predictionLabel: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + flex: 1, + }, + urgencyBadge: { + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + }, + urgencyText: { + fontSize: 10, + color: theme.colors.background, + textTransform: 'uppercase', + fontFamily: theme.typography.fontFamily.medium, + }, + predictionDetails: { + marginBottom: theme.spacing.sm, + }, + predictionDetailItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: theme.spacing.xs, + }, + predictionDetailLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + flex: 1, + }, + predictionDetailValue: { + fontSize: 12, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.regular, + flex: 2, + textAlign: 'right', + }, + predictionTimestamp: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + textAlign: 'right', + }, + + // Image Styles + imageList: { + paddingRight: theme.spacing.md, + }, + imageContainer: { + alignItems: 'center', + marginRight: theme.spacing.md, + }, + seriesImage: { + width: 120, + height: 120, + borderRadius: 8, + marginBottom: theme.spacing.xs, + }, + noImagePlaceholder: { + width: 120, + height: 120, + borderRadius: 8, + backgroundColor: theme.colors.backgroundAlt, + justifyContent: 'center', + alignItems: 'center', + borderWidth: 1, + borderColor: theme.colors.border, + borderStyle: 'dashed', + marginBottom: theme.spacing.xs, + }, + noImageText: { + fontSize: 10, + color: theme.colors.textMuted, + fontFamily: theme.typography.fontFamily.regular, + textAlign: 'center', + }, + imageLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, + imagePredictionLabel: { + fontSize: 10, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + marginTop: theme.spacing.xs, + }, + + // Feedback Styles + feedbackCard: { + backgroundColor: theme.colors.backgroundAlt, + borderRadius: 8, + padding: theme.spacing.md, + marginBottom: theme.spacing.sm, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + feedbackHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + feedbackHeaderLeft: { + flexDirection: 'row', + alignItems: 'center', + }, + feedbackTypeBadge: { + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + }, + feedbackTypeText: { + fontSize: 10, + color: theme.colors.background, + textTransform: 'uppercase', + fontFamily: theme.typography.fontFamily.medium, + }, + newFeedbackBadge: { + backgroundColor: theme.colors.primary, + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + marginLeft: theme.spacing.sm, + }, + newFeedbackBadgeText: { + fontSize: 10, + color: theme.colors.background, + textTransform: 'uppercase', + fontFamily: theme.typography.fontFamily.bold, + }, + feedbackDate: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, + feedbackText: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.regular, + lineHeight: 20, + marginBottom: theme.spacing.sm, + }, + feedbackFooter: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + feedbackEmail: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, + feedbackPredictionId: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, + + // Empty State Styles + emptyState: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: theme.spacing.xl, + }, + emptyStateTitle: { + fontSize: 18, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginTop: theme.spacing.md, + marginBottom: theme.spacing.sm, + }, + emptyStateSubtitle: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + textAlign: 'center', + lineHeight: 20, + }, + + // Floating Feedback Button + floatingFeedbackButton: { + position: 'absolute', + bottom: theme.spacing.lg, + right: theme.spacing.lg, + width: 56, + height: 56, + borderRadius: 28, + backgroundColor: theme.colors.primary, + justifyContent: 'center', + alignItems: 'center', + shadowColor: theme.colors.primary, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 8, + }, + + // Modal Styles + modalOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.7)', + justifyContent: 'center', + alignItems: 'center', + zIndex: 1000, + }, + feedbackModal: { + backgroundColor: theme.colors.background, + borderRadius: 12, + width: '90%', + maxWidth: 450, + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 10, + elevation: 10, + }, + modalHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: theme.spacing.md, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + modalTitle: { + fontSize: 20, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + closeButton: { + padding: theme.spacing.sm, + }, + modalContent: { + padding: theme.spacing.md, + }, + feedbackSection: { + marginBottom: theme.spacing.md, + }, + feedbackSectionTitle: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.sm, + }, + feedbackPredictionInfo: { + paddingTop: theme.spacing.md, + marginBottom: theme.spacing.sm, + }, + feedbackPredictionTitle: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.xs, + }, + feedbackPredictionMeta: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, + predictionAccuracyContainer: { + flexDirection: 'row', + justifyContent: 'space-around', + marginBottom: theme.spacing.sm, + }, + predictionAccuracyButton: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: theme.spacing.sm, + paddingHorizontal: theme.spacing.md, + borderRadius: 12, + borderWidth: 1, + borderColor: theme.colors.border, + minWidth: 120, + justifyContent: 'center', + }, + predictionAccuracyButtonActive: { + borderColor: theme.colors.primary, + backgroundColor: theme.colors.primary, + }, + predictionAccuracyButtonText: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + marginLeft: theme.spacing.sm, + }, + predictionAccuracyButtonTextActive: { + color: theme.colors.background, + fontFamily: theme.typography.fontFamily.bold, + }, + feedbackTextInput: { + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: 8, + padding: theme.spacing.md, + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.regular, + minHeight: 100, + textAlignVertical: 'top', + }, + modalFooter: { + flexDirection: 'row', + justifyContent: 'space-around', + padding: theme.spacing.md, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + cancelButton: { + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + borderRadius: 8, + borderWidth: 1, + borderColor: theme.colors.border, + }, + cancelButtonText: { + fontSize: 16, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + }, + submitButton: { + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + borderRadius: 8, + backgroundColor: theme.colors.primary, + shadowColor: theme.colors.primary, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 4, + }, + submitButtonDisabled: { + backgroundColor: theme.colors.textMuted, + opacity: 0.7, + }, + submitButtonText: { + color: theme.colors.background, + fontSize: 16, + fontFamily: theme.typography.fontFamily.bold, + }, + submitButtonLoading: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + + // Feedback Result Modal Styles + feedbackResultModal: { + backgroundColor: theme.colors.background, + borderRadius: 16, + padding: 0, + width: '90%', + maxWidth: 400, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 8, + }, + feedbackResultMessage: { + fontSize: 16, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.regular, + textAlign: 'center', + lineHeight: 24, + paddingHorizontal: theme.spacing.md, + }, + okButton: { + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.xl, + borderRadius: 8, + backgroundColor: theme.colors.primary, + shadowColor: theme.colors.primary, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 4, + minWidth: 100, + alignItems: 'center', + }, + okButtonText: { + color: theme.colors.background, + fontSize: 16, + fontFamily: theme.typography.fontFamily.bold, + }, + loadingState: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: theme.spacing.xl, + }, + loadingStateText: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + marginTop: theme.spacing.sm, + }, + // New styles for detailed series info + seriesSummaryBar: { + flexDirection: 'row', + justifyContent: 'space-around', + backgroundColor: theme.colors.backgroundAlt, + paddingVertical: theme.spacing.sm, + paddingHorizontal: theme.spacing.md, + borderRadius: 8, + marginBottom: theme.spacing.md, + }, + summaryItem: { + alignItems: 'center', + }, + summaryLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + marginBottom: theme.spacing.xs, + }, + summaryValueContainer: { + backgroundColor: theme.colors.background, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.xs, + borderRadius: 12, + }, + summaryValue: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + seriesDetailsCard: { + backgroundColor: theme.colors.backgroundAlt, + borderRadius: 12, + padding: theme.spacing.md, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + cardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + cardTitle: { + fontSize: 18, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + cardSubtitle: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, + detailsGrid: { + marginBottom: theme.spacing.md, + }, + detailRow: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: theme.spacing.sm, + }, + detailColumn: { + flex: 1, + marginRight: theme.spacing.sm, + }, + detailLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + marginBottom: theme.spacing.xs, + }, + detailValue: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.regular, + }, + processingInfo: { + marginTop: theme.spacing.md, + paddingTop: theme.spacing.md, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + processingTitle: { + fontSize: 16, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.sm, + }, + processingGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-around', + }, + processingItem: { + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + processingLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + }, + processingValue: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + statusBadge: { + backgroundColor: theme.colors.success, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.xs, + borderRadius: 12, + }, + statusText: { + fontSize: 12, + color: theme.colors.background, + fontFamily: theme.typography.fontFamily.bold, + }, + // New styles for AI summary bar + aiSummaryBar: { + flexDirection: 'row', + justifyContent: 'space-around', + backgroundColor: theme.colors.backgroundAlt, + paddingVertical: theme.spacing.sm, + paddingHorizontal: theme.spacing.md, + borderRadius: 8, + marginBottom: theme.spacing.md, + }, + aiSummaryItem: { + alignItems: 'center', + }, + aiSummaryLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + marginBottom: theme.spacing.xs, + }, + aiSummaryValueContainer: { + backgroundColor: theme.colors.background, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.xs, + borderRadius: 12, + }, + aiSummaryValue: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + // New styles for AI predictions card + aiPredictionsCard: { + backgroundColor: theme.colors.backgroundAlt, + borderRadius: 12, + padding: theme.spacing.md, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + // New styles for detailed results section + detailedResultsSection: { + marginTop: theme.spacing.md, + paddingTop: theme.spacing.md, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + detailedResultsTitle: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.sm, + }, + detailedResultsGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-around', + }, + detailedResultItem: { + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + detailedResultLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + }, + detailedResultValue: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.regular, + }, + // New styles for additional findings + additionalFindingItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginTop: theme.spacing.sm, + paddingTop: theme.spacing.sm, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + additionalFindingLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + }, + additionalFindingValue: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.regular, + }, + // New styles for additional findings section + additionalFindingsSection: { + marginTop: theme.spacing.md, + paddingTop: theme.spacing.md, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + additionalFindingsTitle: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.sm, + }, + additionalFindingValues: { + alignItems: 'flex-end', + }, + // New styles for images summary bar + imagesSummaryBar: { + flexDirection: 'row', + justifyContent: 'space-around', + backgroundColor: theme.colors.backgroundAlt, + paddingVertical: theme.spacing.sm, + paddingHorizontal: theme.spacing.md, + borderRadius: 8, + marginBottom: theme.spacing.md, + }, + imagesSummaryItem: { + alignItems: 'center', + }, + imagesSummaryLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + marginBottom: theme.spacing.xs, + }, + imagesSummaryValueContainer: { + backgroundColor: theme.colors.background, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.xs, + borderRadius: 12, + }, + imagesSummaryValue: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + // New styles for images display card + imagesDisplayCard: { + backgroundColor: theme.colors.backgroundAlt, + borderRadius: 12, + padding: theme.spacing.md, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + // New styles for feedback summary bar + feedbackSummaryBar: { + flexDirection: 'row', + justifyContent: 'space-around', + backgroundColor: theme.colors.backgroundAlt, + paddingVertical: theme.spacing.sm, + paddingHorizontal: theme.spacing.md, + borderRadius: 8, + marginBottom: theme.spacing.md, + }, + feedbackSummaryItem: { + alignItems: 'center', + }, + feedbackSummaryLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + marginBottom: theme.spacing.xs, + }, + feedbackSummaryValueContainer: { + backgroundColor: theme.colors.background, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.xs, + borderRadius: 12, + }, + feedbackSummaryValue: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + // New styles for feedback history card + feedbackHistoryCard: { + backgroundColor: theme.colors.backgroundAlt, + borderRadius: 12, + padding: theme.spacing.sm, + paddingBottom: theme.spacing.md, + // shadowColor: '#000', + // shadowOffset: { width: 0, height: 2 }, + // shadowOpacity: 0.1, + // shadowRadius: 4, + // elevation: 2, + }, + // New styles for visual indicators section + visualIndicatorsSection: { + marginTop: theme.spacing.md, + paddingTop: theme.spacing.md, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + visualIndicatorsTitle: { + fontSize: 16, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.md, + textAlign: 'center', + }, + // New styles for compact indicators + compactIndicatorsContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + marginBottom: theme.spacing.md, + // paddingHorizontal: theme.spacing.sm, + }, + compactIndicatorItem: { + width: '100%', + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.md, + backgroundColor: theme.colors.background, + borderRadius: 12, + marginBottom: theme.spacing.sm, + borderWidth: 1, + borderColor: theme.colors.border, + // shadowColor: '#000', + // shadowOffset: { width: 0, height: 1 }, + // shadowOpacity: 0.05, + // shadowRadius: 2, + // elevation: 1, + }, + indicatorHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: theme.spacing.sm, + }, + compactIndicatorTitle: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.medium, + flex: 1, + marginLeft: theme.spacing.sm, + }, + compactIndicatorPercentage: { + fontSize: 16, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginLeft: theme.spacing.sm, + }, + indicatorsGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-around', + marginBottom: theme.spacing.md, + }, + indicatorCard: { + backgroundColor: theme.colors.background, + borderRadius: 12, + padding: theme.spacing.md, + marginBottom: theme.spacing.sm, + minWidth: 140, + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + indicatorTitle: { + fontSize: 12, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginLeft: theme.spacing.xs, + textAlign: 'center', + }, + indicatorStatus: { + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + borderRadius: 8, + minWidth: 100, + alignItems: 'center', + }, + indicatorStatusText: { + fontSize: 10, + color: theme.colors.background, + fontFamily: theme.typography.fontFamily.bold, + textTransform: 'uppercase', + textAlign: 'center', + }, + summaryIndicator: { + alignItems: 'center', + marginTop: theme.spacing.md, + }, + summaryIndicatorTitle: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.sm, + }, + summaryIndicatorStatus: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + borderRadius: 12, + minWidth: 200, + justifyContent: 'center', + }, + summaryIndicatorText: { + fontSize: 12, + color: theme.colors.background, + fontFamily: theme.typography.fontFamily.bold, + textTransform: 'uppercase', + marginLeft: theme.spacing.sm, + textAlign: 'center', + }, + percentageContainer: { + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + percentageValue: { + fontSize: 16, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.xs, + }, + progressBarContainer: { + width: '100%', + height: 6, + backgroundColor: theme.colors.backgroundAlt, + borderRadius: 3, + marginTop: theme.spacing.xs, + overflow: 'hidden', + }, + progressBar: { + height: '100%', + borderRadius: 3, + minWidth: 4, + }, + progressBarBackground: { + width: '100%', + height: '100%', + backgroundColor: theme.colors.background, + borderRadius: 4, + }, + progressBarFill: { + height: '100%', + borderRadius: 4, + minWidth: 4, // Ensure minimum width for very small percentages + }, + overallPercentageContainer: { + alignItems: 'center', + marginBottom: theme.spacing.md, + }, + overallPercentageLabel: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + marginBottom: theme.spacing.xs, + }, + overallPercentageValue: { + fontSize: 24, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.md, + }, + debugSection: { + marginTop: theme.spacing.md, + paddingTop: theme.spacing.md, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + debugTitle: { + fontSize: 16, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.sm, + }, + debugText: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + textAlign: 'center', + paddingHorizontal: theme.spacing.md, + lineHeight: 20, + }, + tabContainer: { + flexDirection: 'row', + justifyContent: 'space-around', + paddingVertical: theme.spacing.sm, + paddingHorizontal: theme.spacing.md, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + backgroundColor: theme.colors.background, + }, + tabButton: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + borderRadius: 12, + borderWidth: 1, + borderColor: theme.colors.border, + minWidth: 110, + justifyContent: 'center', + position: 'relative', + backgroundColor: theme.colors.backgroundAlt, + }, + tabButtonActive: { + borderColor: theme.colors.primary, + backgroundColor: theme.colors.primary, + shadowColor: theme.colors.primary, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 4, + elevation: 3, + }, + tabButtonText: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + marginLeft: theme.spacing.xs, + }, + tabButtonTextActive: { + color: theme.colors.background, + fontFamily: theme.typography.fontFamily.bold, + }, + tabIndicator: { + position: 'absolute', + bottom: -theme.spacing.sm, + left: 0, + right: 0, + height: 3, + backgroundColor: theme.colors.primary, + borderRadius: 2, + }, +}); + +export default SeriesDetailScreen; diff --git a/app/modules/PatientCare/screens/index.ts b/app/modules/PatientCare/screens/index.ts new file mode 100644 index 0000000..c78055d --- /dev/null +++ b/app/modules/PatientCare/screens/index.ts @@ -0,0 +1,16 @@ +/* + * File: index.ts + * Description: Barrel export for PatientCare screens + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export { default as PatientsScreen } from './PatientsScreen'; +export { default as PatientDetailsScreen } from './PatientDetailsScreen'; +export { default as SeriesDetailScreen } from './SeriesDetailScreen'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/services/index.ts b/app/modules/PatientCare/services/index.ts new file mode 100644 index 0000000..520a274 --- /dev/null +++ b/app/modules/PatientCare/services/index.ts @@ -0,0 +1,14 @@ +/* + * File: index.ts + * Description: Barrel export for Dashboard services + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export * from './patientAPI'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/PatientCare/services/patientAPI.ts b/app/modules/PatientCare/services/patientAPI.ts new file mode 100644 index 0000000..ddd3017 --- /dev/null +++ b/app/modules/PatientCare/services/patientAPI.ts @@ -0,0 +1,196 @@ +/* + * File: patientAPI.ts + * Description: API service for patient care operations using apisauce + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { create } from 'apisauce'; +import { API_CONFIG, buildHeaders } from '../../../shared/utils'; + +const api = create({ + baseURL: API_CONFIG.BASE_URL +}); + +/** + * Patient API Service + * + * Purpose: Handle all patient-related API operations + * + * Features: + * - Get patient list with filtering + * - Get individual patient details + * - Update patient information + * - Get patient vital signs + * - Get patient medical history + */ +export const patientAPI = { + /** + * Get Patients + * + * Purpose: Fetch list of medical cases from server + * + * @param token - Authentication token + * @returns Promise with medical cases data + */ + getPatients: (token: string) => { + return api.get('/api/ai-cases/all-patients', {}, buildHeaders({ token })); + }, + + /** + * Get Patient Details by ID + * + * Purpose: Fetch detailed information for a specific patient by ID + * + * @param patientId - Patient ID + * @param token - Authentication token + * @returns Promise with patient details including predictions and series + */ + getPatientDetailsById: (patientId: string, token: string) => { + return api.get(`/api/ai-cases/patient/${patientId}/predictions`, {}, buildHeaders({ token })); + }, + + /** + * Get Patient Details (Legacy - kept for backward compatibility) + * + * Purpose: Fetch detailed information for a specific patient + * + * @param patientId - Patient ID + * @param token - Authentication token + * @returns Promise with patient details + */ + getPatientDetails: (patientId: string, token: string) => { + return api.get(`/api/ai-cases/patient/${patientId}/predictions`, {}, buildHeaders({ token })); + }, + + /** + * Update Patient + * + * Purpose: Update patient information + * + * @param patientId - Patient ID + * @param patientData - Updated patient data + * @param token - Authentication token + * @returns Promise with updated patient data + */ + updatePatient: (patientId: string, patientData: any, token: string) => { + return api.put(`/api/patients/${patientId}`, patientData, buildHeaders({ token })); + }, + + /** + * Get Patient Vital Signs + * + * Purpose: Fetch latest vital signs for a patient + * + * @param patientId - Patient ID + * @param token - Authentication token + * @returns Promise with vital signs data + */ + getPatientVitals: (patientId: string, token: string) => { + return api.get(`/api/patients/${patientId}/vitals`, {}, buildHeaders({ token })); + }, + + /** + * Update Patient Vital Signs + * + * Purpose: Add new vital signs reading for a patient + * + * @param patientId - Patient ID + * @param vitalSigns - Vital signs data + * @param token - Authentication token + * @returns Promise with updated vital signs + */ + updatePatientVitals: (patientId: string, vitalSigns: any, token: string) => { + return api.put(`/api/patients/${patientId}/vitals`, vitalSigns, buildHeaders({ token })); + }, + + /** + * Submit Feedback for AI Prediction + * + * Purpose: Submit physician feedback for AI predictions + * + * @param feedbackData - Feedback payload + * @returns API response + */ + submitFeedback: (feedbackData: { + patid: string; + prediction_id: number; + feedback_text: string; + is_positive: boolean; + },token) => api.post('/api/ai-cases/feedbacks', feedbackData, buildHeaders({ token })), + + /** + * Get Patient Medical History + * + * Purpose: Fetch medical history for a patient + * + * @param patientId - Patient ID + * @param token - Authentication token + * @returns Promise with medical history data + */ + getPatientHistory: (patientId: string, token: string) => { + return api.get(`/api/patients/${patientId}/history`, {}, buildHeaders({ token })); + }, + + /** + * Get Patient Medications + * + * Purpose: Fetch current medications for a patient + * + * @param patientId - Patient ID + * @param token - Authentication token + * @returns Promise with medications data + */ + getPatientMedications: (patientId: string, token: string) => { + return api.get(`/api/patients/${patientId}/medications`, {}, buildHeaders({ token })); + }, + + /** + * Add Patient Medication + * + * Purpose: Add new medication for a patient + * + * @param patientId - Patient ID + * @param medication - Medication data + * @param token - Authentication token + * @returns Promise with updated medications + */ + addPatientMedication: (patientId: string, medication: any, token: string) => { + return api.post(`/api/patients/${patientId}/medications`, medication, buildHeaders({ token })); + }, + + /** + * Get Patient Allergies + * + * Purpose: Fetch allergies for a patient + * + * @param patientId - Patient ID + * @param token - Authentication token + * @returns Promise with allergies data + */ + getPatientAllergies: (patientId: string, token: string) => { + return api.get(`/api/patients/${patientId}/allergies`, {}, buildHeaders({ token })); + }, + + /** + * Search Patients + * + * Purpose: Search patients by various criteria + * + * @param query - Search query + * @param token - Authentication token + * @returns Promise with search results + */ + searchPatients: (query: string, token: string) => { + return api.get('/api/patients/search', { q: query }, buildHeaders({ token })); + }, +}; + +// Legacy export for backward compatibility +export const caseAPI = patientAPI; + +/* + * End of File: patientAPI.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Settings/components/ProfileCard.tsx b/app/modules/Settings/components/ProfileCard.tsx new file mode 100644 index 0000000..1901986 --- /dev/null +++ b/app/modules/Settings/components/ProfileCard.tsx @@ -0,0 +1,310 @@ +/* + * File: ProfileCard.tsx + * Description: Profile card component displaying user information + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity, Image } from 'react-native'; +import { theme } from '../../../theme/theme'; +import { UserProfile } from '../../../shared/types'; + +/** + * ProfileCardProps Interface + * + * Purpose: Defines the props required by the ProfileCard component + * + * Props: + * - profile: User profile data to display + * - onPress: Callback function when card is pressed + */ +interface ProfileCardProps { + profile: UserProfile; + onPress: () => void; +} + +/** + * ProfileCard Component + * + * Purpose: Displays user profile information in a card format + * + * Features: + * - Profile picture display + * - User name and credentials + * - Department and role information + * - Contact information + * - Interactive card design + */ +export const ProfileCard: React.FC = ({ profile, onPress }) => { + /** + * formatCredentials Function + * + * Purpose: Format credentials array into a readable string + * + * @returns Formatted credentials string + */ + const formatCredentials = (): string => { + return profile.credentials.join(', '); + }; + + /** + * formatSpecialties Function + * + * Purpose: Format specialties array into a readable string + * + * @returns Formatted specialties string + */ + const formatSpecialties = (): string => { + return profile.specialties.slice(0, 2).join(', '); + }; + + + + return ( + + {/* Profile picture and basic info */} + + + {profile.profilePicture ? ( + { + console.log('Profile image loading error:', error.nativeEvent.error); + }} + /> + ) : ( + + + {profile.firstName.charAt(0)}{profile.lastName.charAt(0)} + + + )} + + + + + {profile.firstName} {profile.lastName} + + {formatCredentials()} + {formatSpecialties()} + + + + Edit + + + + {/* Department and role information */} + + + Department: + {profile.department} + + + + Role: + + {profile.role.replace('_', ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase())} + + + + + Experience: + {profile.yearsOfExperience} years + + + + {/* Contact information */} + + + Email: + + {profile.email} + + + + + Phone: + {profile.phoneNumber} + + + + ); +}; + +// ============================================================================ +// STYLES SECTION +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container for the profile card + container: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.large, + padding: theme.spacing.lg, + marginVertical: theme.spacing.md, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + borderColor: theme.colors.border, + borderWidth: 1, + }, + + // Header section with profile picture and basic info + header: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.md, + }, + + // Profile image container + imageContainer: { + marginRight: theme.spacing.md, + }, + + // Profile image styling + profileImage: { + width: 60, + height: 60, + borderRadius: 30, + backgroundColor: theme.colors.backgroundAlt, + borderWidth: 2, + borderColor: theme.colors.border, + }, + + // Basic information section + basicInfo: { + flex: 1, + }, + + // User name styling + name: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + // Credentials styling + credentials: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.primary, + marginBottom: theme.spacing.xs, + }, + + // Specialties styling + specialties: { + fontSize: theme.typography.fontSize.bodySmall, + color: theme.colors.textSecondary, + }, + + // Edit icon/text styling + editIcon: { + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + backgroundColor: theme.colors.primary, + borderRadius: theme.borderRadius.small, + }, + + // Edit text styling + editText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.background, + }, + + // Information section styling + infoSection: { + marginBottom: theme.spacing.md, + paddingTop: theme.spacing.md, + borderTopColor: theme.colors.border, + borderTopWidth: 1, + }, + + // Information row styling + infoRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + + // Information label styling + infoLabel: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textSecondary, + }, + + // Information value styling + infoValue: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + textAlign: 'right', + flex: 1, + }, + + // Contact section styling + contactSection: { + paddingTop: theme.spacing.md, + borderTopColor: theme.colors.border, + borderTopWidth: 1, + }, + + // Contact row styling + contactRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + + // Contact label styling + contactLabel: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textSecondary, + }, + + // Contact value styling + contactValue: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + textAlign: 'right', + flex: 1, + }, + + // Fallback avatar styling + fallbackAvatar: { + width: 60, + height: 60, + borderRadius: 30, + backgroundColor: theme.colors.primary, + justifyContent: 'center', + alignItems: 'center', + borderWidth: 2, + borderColor: theme.colors.border, + }, + + // Fallback text styling + fallbackText: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + textAlign: 'center', + }, +}); + +/* + * End of File: ProfileCard.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Settings/components/SettingsHeader.tsx b/app/modules/Settings/components/SettingsHeader.tsx new file mode 100644 index 0000000..3b6860a --- /dev/null +++ b/app/modules/Settings/components/SettingsHeader.tsx @@ -0,0 +1,96 @@ +/* + * File: SettingsHeader.tsx + * Description: Header component for the settings screen + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import Icon from 'react-native-vector-icons/Feather'; +import { theme } from '../../../theme/theme'; + +/** + * SettingsHeaderProps Interface + * + * Purpose: Defines the props required by the SettingsHeader component + * + * Props: + * - title: Title text to display in the header + * - showBackButton: Whether to show the back button (optional) + * - onBackPress: Function to call when back button is pressed (optional) + */ +interface SettingsHeaderProps { + title: string; + showBackButton?: boolean; + onBackPress?: () => void; +} + +/** + * SettingsHeader Component + * + * Purpose: Displays the header for the settings screen + * + * Features: + * - Clean, minimal header design + * - Consistent with app theme + * - Proper spacing and typography + */ +export const SettingsHeader: React.FC = ({ + title, + showBackButton = false, + onBackPress +}) => { + return ( + + {showBackButton && onBackPress && ( + + + + )} + {title} + + ); +}; + +// ============================================================================ +// STYLES SECTION +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container for the header + container: { + backgroundColor: theme.colors.background, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.md, + borderBottomColor: theme.colors.border, + borderBottomWidth: 1, + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.md, + }, + + // Back button styling + backButton: { + marginRight: theme.spacing.md, + padding: theme.spacing.xs, + }, + + // Title text styling + title: { + fontSize: theme.typography.fontSize.displayMedium, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + flex: 1, + }, +}); + +/* + * End of File: SettingsHeader.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Settings/components/SettingsItemComponent.tsx b/app/modules/Settings/components/SettingsItemComponent.tsx new file mode 100644 index 0000000..b3b4037 --- /dev/null +++ b/app/modules/Settings/components/SettingsItemComponent.tsx @@ -0,0 +1,299 @@ +/* + * File: SettingsItemComponent.tsx + * Description: Settings item component displaying individual settings options + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity, Switch } from 'react-native'; +import { theme } from '../../../theme/theme'; +import { SettingsItem } from '../../../shared/types'; +import Icon from 'react-native-vector-icons/Feather'; +/** + * SettingsItemComponentProps Interface + * + * Purpose: Defines the props required by the SettingsItemComponent + * + * Props: + * - item: Settings item data to display + * - isLast: Boolean indicating if this is the last item in the section + */ +interface SettingsItemComponentProps { + item: SettingsItem; + isLast: boolean; +} + +/** + * SettingsItemComponent Component + * + * Purpose: Displays individual settings items with different interaction types + * + * Features: + * - Navigation items with chevron + * - Toggle items with switch + * - Action items with custom styling + * - Icon support (placeholder) + * - Consistent styling and spacing + */ +export const SettingsItemComponent: React.FC = ({ item, isLast }) => { + /** + * renderIcon Function + * + * Purpose: Render icon for the settings item + * + * @returns Icon component or placeholder + */ + const renderIcon = () => { + // TODO: Implement actual icon rendering + return ( + + + + ); + }; + + /** + * renderValue Function + * + * Purpose: Render the value/action component based on item type + * + * @returns Value component (switch, chevron, or custom) + */ + const renderValue = () => { + switch (item.type) { + case 'TOGGLE': + return ( + + ); + + case 'NAVIGATION': + return ( + โ€บ + ); + + case 'ACTION': + return ( + Sign Out + ); + + default: + return null; + } + }; + + /** + * getItemStyle Function + * + * Purpose: Get appropriate styling based on item type and position + * + * @returns Style object for the item container + */ + const getItemStyle = () => { + const baseStyle = [styles.container]; + + // Add border bottom if not the last item + if (!isLast) { + baseStyle.push(styles.withBorder); + } + + // Add special styling for action items + if (item.type === 'ACTION') { + baseStyle.push(styles.actionItem); + } + + // Add disabled styling if item is disabled + if (item.disabled) { + baseStyle.push(styles.disabledItem); + } + + return baseStyle; + }; + + /** + * getTextStyle Function + * + * Purpose: Get appropriate text styling based on item state + * + * @returns Style object for text + */ + const getTextStyle = () => { + if (item.disabled) { + return [styles.title, styles.disabledText]; + } + if (item.type === 'ACTION') { + return [styles.title, styles.actionTitle]; + } + return styles.title; + }; + + /** + * getSubtitleStyle Function + * + * Purpose: Get appropriate subtitle styling based on item state + * + * @returns Style object for subtitle + */ + const getSubtitleStyle = () => { + if (item.disabled) { + return [styles.subtitle, styles.disabledText]; + } + return styles.subtitle; + }; + + return ( + + {/* Icon */} + {renderIcon()} + + {/* Content */} + + + {item.title} + {item.subtitle && ( + {item.subtitle} + )} + + + {/* Badge */} + {item.badge && ( + + {item.badge} + + )} + + + {/* Value/Action */} + {renderValue()} + + ); +}; + +// ============================================================================ +// STYLES SECTION +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container for the settings item + container: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.md, + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.medium, + }, + + // Container with bottom border + withBorder: { + borderBottomColor: theme.colors.border, + borderBottomWidth: 1, + }, + + // Action item styling + actionItem: { + backgroundColor: theme.colors.background, + }, + + // Disabled item styling + disabledItem: { + opacity: 0.5, + }, + + // Icon placeholder styling + iconPlaceholder: { + width: 24, + height: 24, + marginRight: theme.spacing.md, + justifyContent: 'center', + alignItems: 'center', + }, + + // Icon text styling + iconText: { + fontSize: 16, + }, + + // Content container + content: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + }, + + // Text container + textContainer: { + flex: 1, + }, + + // Title text styling + title: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + // Action title styling + actionTitle: { + color: theme.colors.error, + }, + + // Subtitle text styling + subtitle: { + fontSize: theme.typography.fontSize.bodySmall, + color: theme.colors.textSecondary, + }, + + // Disabled text styling + disabledText: { + color: theme.colors.textMuted, + }, + + // Badge styling + badge: { + backgroundColor: theme.colors.primary, + borderRadius: theme.borderRadius.small, + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + marginLeft: theme.spacing.sm, + }, + + // Badge text styling + badgeText: { + fontSize: theme.typography.fontSize.caption, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.background, + }, + + // Chevron styling for navigation items + chevron: { + fontSize: theme.typography.fontSize.bodyLarge, + color: theme.colors.textMuted, + fontFamily: theme.typography.fontFamily.bold, + }, + + // Action text styling + actionText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.error, + }, +}); + +/* + * End of File: SettingsItemComponent.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Settings/components/SettingsSectionComponent.tsx b/app/modules/Settings/components/SettingsSectionComponent.tsx new file mode 100644 index 0000000..2b5b56e --- /dev/null +++ b/app/modules/Settings/components/SettingsSectionComponent.tsx @@ -0,0 +1,90 @@ +/* + * File: SettingsSectionComponent.tsx + * Description: Settings section component displaying grouped settings items + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { theme } from '../../../theme/theme'; +import { SettingsSectionData } from '../../../shared/types'; +import { SettingsItemComponent } from './SettingsItemComponent'; + +/** + * SettingsSectionComponentProps Interface + * + * Purpose: Defines the props required by the SettingsSectionComponent + * + * Props: + * - section: Settings section data to display + */ +interface SettingsSectionComponentProps { + section: SettingsSectionData; +} + +/** + * SettingsSectionComponent Component + * + * Purpose: Displays a settings section with grouped items + * + * Features: + * - Section title display + * - Grouped settings items + * - Consistent styling and spacing + * - Clean visual separation between sections + */ +export const SettingsSectionComponent: React.FC = ({ section }) => { + return ( + + {/* Section title */} + {section.title} + + {/* Settings items */} + + {section.items.map((item, index) => ( + + ))} + + + ); +}; + +// ============================================================================ +// STYLES SECTION +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container for the settings section + container: { + marginBottom: theme.spacing.lg, + }, + + // Section title styling + sectionTitle: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.sm, + paddingHorizontal: theme.spacing.sm, + }, + + // Container for settings items + itemsContainer: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.medium, + borderColor: theme.colors.border, + borderWidth: 1, + ...theme.shadows.primary, + }, +}); + +/* + * End of File: SettingsSectionComponent.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Settings/index.ts b/app/modules/Settings/index.ts new file mode 100644 index 0000000..4b4bdc1 --- /dev/null +++ b/app/modules/Settings/index.ts @@ -0,0 +1,80 @@ +/* + * File: index.ts + * Description: Settings module exports + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// ============================================================================ +// SCREENS +// ============================================================================ + +export { default as SettingsScreen } from './screens/SettingsScreen'; + +// ============================================================================ +// NAVIGATION +// ============================================================================ + +export { + SettingsStackNavigator, + SettingsStackParamList, + SettingsNavigationProp, + SettingsScreenProps, + ProfileEditScreenProps, + SecuritySettingsScreenProps, + NotificationSettingsScreenProps, + ClinicalPreferencesScreenProps, + PrivacySettingsScreenProps, + AccessibilitySettingsScreenProps, + AboutScreenProps, + HelpSupportScreenProps, + navigateToSettings, + navigateToProfileEdit, + navigateToSecuritySettings, + navigateToNotificationSettings, + navigateToClinicalPreferences, + navigateToPrivacySettings, + navigateToAccessibilitySettings, + navigateToAbout, + navigateToHelpSupport, + goBack, + resetToSettings, + replaceWithSettings, + navigateToSettingsAndClearStack, + navigateToProfileEditAndClearStack, + navigateToSecuritySettingsAndClearStack, +} from './navigation'; + +// ============================================================================ +// COMPONENTS +// ============================================================================ + +export { default as SettingsHeader } from './components/SettingsHeader'; +export { default as ProfileCard } from './components/ProfileCard'; +export { default as SettingsSectionComponent } from './components/SettingsSectionComponent'; +export { default as SettingsItemComponent } from './components/SettingsItemComponent'; + +// ============================================================================ +// REDUX +// ============================================================================ + +export { + fetchUserProfile, + updateUserProfile, + fetchUserPreferences, + updateUserPreferences, + clearError, + setProfileValidation, + setPreferencesValidation, + updateProfileField, + updatePreferenceField, + resetProfile, + resetPreferences, + clearSettingsCache, +} from './redux/settingsSlice'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Settings/navigation/SettingsStackNavigator.tsx b/app/modules/Settings/navigation/SettingsStackNavigator.tsx new file mode 100644 index 0000000..d4999af --- /dev/null +++ b/app/modules/Settings/navigation/SettingsStackNavigator.tsx @@ -0,0 +1,124 @@ +/* + * File: SettingsStackNavigator.tsx + * Description: Stack navigator for settings screens within the Settings module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; + +// Import settings screens +import { SettingsScreen } from '../screens/SettingsScreen'; + +// Import navigation types +import { SettingsStackParamList } from './navigationTypes'; +import { theme } from '../../../theme'; +import { AppInfoScreen, ChangePasswordScreen, EditProfileScreen } from '../screens'; + +// Create stack navigator for Settings module +const Stack = createStackNavigator(); + +/** + * SettingsStackNavigator - Manages navigation between settings screens + * + * This navigator handles the flow between: + * - SettingsScreen: Main settings screen with profile and preferences + * - Future screens: Profile edit, security settings, notifications, etc. + * + * Features: + * - Clean header styling + * - Smooth transitions between screens + * - Type-safe navigation parameters + * - Settings-focused design + */ +const SettingsStackNavigator: React.FC = () => { + return ( + + {/* Settings Screen - Main settings entry point */} + + + + + {/* */} + + ); +}; + +export default SettingsStackNavigator; + +/* + * End of File: SettingsStackNavigator.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Settings/navigation/index.ts b/app/modules/Settings/navigation/index.ts new file mode 100644 index 0000000..b9f40db --- /dev/null +++ b/app/modules/Settings/navigation/index.ts @@ -0,0 +1,59 @@ +/* + * File: index.ts + * Description: Barrel exports for Settings module navigation + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// Export main navigator +export { default as SettingsStackNavigator } from './SettingsStackNavigator'; + +// Export navigation types +export type { + SettingsStackParamList, + SettingsNavigationProp, + SettingsScreenProps, + SettingsScreenProps as MainSettingsScreenProps, + ProfileEditScreenProps, + SecuritySettingsScreenProps, + NotificationSettingsScreenProps, + ClinicalPreferencesScreenProps, + PrivacySettingsScreenProps, + AccessibilitySettingsScreenProps, + AboutScreenProps, + HelpSupportScreenProps, + SettingsScreenParams, + ProfileEditScreenParams, + SecuritySettingsScreenParams, + NotificationSettingsScreenParams, + ClinicalPreferencesScreenParams, + PrivacySettingsScreenParams, + AccessibilitySettingsScreenParams, + AboutScreenParams, + HelpSupportScreenParams, +} from './navigationTypes'; + +// Export navigation utilities +export { + navigateToSettings, + navigateToProfileEdit, + navigateToSecuritySettings, + navigateToNotificationSettings, + navigateToClinicalPreferences, + navigateToPrivacySettings, + navigateToAccessibilitySettings, + navigateToAbout, + navigateToHelpSupport, + goBack, + resetToSettings, + replaceWithSettings, + navigateToSettingsAndClearStack, + navigateToProfileEditAndClearStack, + navigateToSecuritySettingsAndClearStack, +} from './navigationUtils'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Settings/navigation/navigationTypes.ts b/app/modules/Settings/navigation/navigationTypes.ts new file mode 100644 index 0000000..e484f63 --- /dev/null +++ b/app/modules/Settings/navigation/navigationTypes.ts @@ -0,0 +1,259 @@ +/* + * File: navigationTypes.ts + * Description: TypeScript types for Settings module navigation + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { StackNavigationProp } from '@react-navigation/stack'; +import { UserProfile, UserPreferences } from '../../../shared/types'; + +/** + * SettingsStackParamList - Defines the parameter list for Settings stack navigator + * + * This interface defines all the screens available in the Settings module + * and their associated navigation parameters. + */ +export type SettingsStackParamList = { + // Settings screen - Main settings with profile and preferences + SettingScreen: SettingsScreenParams; + + // Profile Edit screen - Edit user profile information + // ProfileEdit: ProfileEditScreenParams; + + // Security Settings screen - Security and privacy settings + + // About screen - App information and version + AppInfoScreen: AppInfoScreenParams; + + // Change Password screen - Change user password + ChangePasswordScreen: ChangePasswordScreenParams; + + // Edit Profile screen - Edit user profile information + EditProfileScreen: EditProfileScreenParams; + + // Help & Support screen - Help documentation and support + // HelpSupport: HelpSupportScreenParams; +}; + +/** + * SettingsNavigationProp - Type for navigation prop in Settings screens + * + * This type provides type-safe navigation methods for screens + * within the Settings module. + */ +export type SettingsNavigationProp = StackNavigationProp; + +/** + * SettingsScreenProps - Base props interface for Settings screens + * + * This interface provides the common props that all Settings screens + * will receive, including navigation and route. + */ +export interface SettingsScreenProps { + navigation: SettingsNavigationProp; + route: { + key: string; + name: T; + params: SettingsStackParamList[T]; + }; +} + +// ============================================================================ +// SCREEN PARAMETER TYPES +// ============================================================================ + +/** + * SettingsScreenParams + * + * Purpose: Parameters passed to the main settings screen + * + * Parameters: + * - section: Optional section to navigate to within settings + * - refresh: Optional flag to force refresh + */ +export interface SettingsScreenParams { + section?: 'profile' | 'security' | 'notifications' | 'clinical' | 'privacy' | 'accessibility' | 'about' | 'help'; + refresh?: boolean; +} + +/** + * ProfileEditScreenParams + * + * Purpose: Parameters for the profile edit screen + * + * Parameters: + * - profile: Optional profile data to pre-populate + * - fromScreen: Optional source screen for back navigation + */ +export interface ProfileEditScreenParams { + profile?: UserProfile; + fromScreen?: keyof SettingsStackParamList; +} + +/** + * SecuritySettingsScreenParams + * + * Purpose: Parameters for the security settings screen + * + * Parameters: + * - showBiometricSetup: Optional flag to show biometric setup + * - fromScreen: Optional source screen for back navigation + */ +export interface SecuritySettingsScreenParams { + showBiometricSetup?: boolean; + fromScreen?: keyof SettingsStackParamList; +} + +/** + * NotificationSettingsScreenParams + * + * Purpose: Parameters for the notification settings screen + * + * Parameters: + * - preferences: Optional notification preferences to pre-populate + * - fromScreen: Optional source screen for back navigation + */ +export interface NotificationSettingsScreenParams { + preferences?: UserPreferences['notificationPreferences']; + fromScreen?: keyof SettingsStackParamList; +} + +/** + * ClinicalPreferencesScreenParams + * + * Purpose: Parameters for the clinical preferences screen + * + * Parameters: + * - preferences: Optional clinical preferences to pre-populate + * - fromScreen: Optional source screen for back navigation + */ +export interface ClinicalPreferencesScreenParams { + preferences?: UserPreferences['clinicalPreferences']; + fromScreen?: keyof SettingsStackParamList; +} + +/** + * PrivacySettingsScreenParams + * + * Purpose: Parameters for the privacy settings screen + * + * Parameters: + * - preferences: Optional privacy preferences to pre-populate + * - fromScreen: Optional source screen for back navigation + */ +export interface PrivacySettingsScreenParams { + preferences?: UserPreferences['privacyPreferences']; + fromScreen?: keyof SettingsStackParamList; +} + +/** + * AccessibilitySettingsScreenParams + * + * Purpose: Parameters for the accessibility settings screen + * + * Parameters: + * - preferences: Optional accessibility preferences to pre-populate + * - fromScreen: Optional source screen for back navigation + */ +export interface AccessibilitySettingsScreenParams { + preferences?: UserPreferences['accessibilityPreferences']; + fromScreen?: keyof SettingsStackParamList; +} + +/** + * AboutScreenParams + * + * Purpose: Parameters for the about screen + * + * Parameters: + * - showChangelog: Optional flag to show changelog + * - showLicenses: Optional flag to show licenses + */ +export interface AboutScreenParams { + showChangelog?: boolean; + showLicenses?: boolean; +} + +/** + * HelpSupportScreenParams + * + * Purpose: Parameters for the help and support screen + * + * Parameters: + * - topic: Optional help topic to navigate to + * - showContact: Optional flag to show contact form + */ +export interface HelpSupportScreenParams { + topic?: 'faq' | 'troubleshooting' | 'contact' | 'feedback'; + showContact?: boolean; +} + +// ============================================================================ +// SCREEN-SPECIFIC PROPS TYPES +// ============================================================================ + +/** + * SettingsScreenProps - Props for SettingsScreen component + */ +export type SettingsScreenProps = SettingsScreenProps<'Settings'>; + +/** + * ProfileEditScreenProps - Props for ProfileEditScreen component + */ +export type ProfileEditScreenProps = SettingsScreenProps<'ProfileEdit'>; + +/** + * SecuritySettingsScreenProps - Props for SecuritySettingsScreen component + */ +export type SecuritySettingsScreenProps = SettingsScreenProps<'SecuritySettings'>; + +/** + * NotificationSettingsScreenProps - Props for NotificationSettingsScreen component + */ +export type NotificationSettingsScreenProps = SettingsScreenProps<'NotificationSettings'>; + +/** + * ClinicalPreferencesScreenProps - Props for ClinicalPreferencesScreen component + */ +export type ClinicalPreferencesScreenProps = SettingsScreenProps<'ClinicalPreferences'>; + +/** + * PrivacySettingsScreenProps - Props for PrivacySettingsScreen component + */ +export type PrivacySettingsScreenProps = SettingsScreenProps<'PrivacySettings'>; + +/** + * AccessibilitySettingsScreenProps - Props for AccessibilitySettingsScreen component + */ +export type AccessibilitySettingsScreenProps = SettingsScreenProps<'AccessibilitySettings'>; + +/** + * AboutScreenProps - Props for AboutScreen component + */ +export type AppInfoScreenParams = SettingsScreenProps<'AppInfoScreen'> + +/** + * ChangePasswordScreenProps - Props for ChangePasswordScreen component + */ +export type ChangePasswordScreenProps = SettingsScreenProps<'ChangePasswordScreen'>; + + +/** + * EditProfileScreenProps - Props for EditProfileScreen component + */ +export type EditProfileScreenProps = SettingsScreenProps<'EditProfileScreen'>; + + + + +/** + * HelpSupportScreenProps - Props for HelpSupportScreen component + */ +export type HelpSupportScreenProps = SettingsScreenProps<'HelpSupport'>; + +/* + * End of File: navigationTypes.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Settings/navigation/navigationUtils.ts b/app/modules/Settings/navigation/navigationUtils.ts new file mode 100644 index 0000000..6b68fdd --- /dev/null +++ b/app/modules/Settings/navigation/navigationUtils.ts @@ -0,0 +1,264 @@ +/* + * File: navigationUtils.ts + * Description: Navigation utilities for Settings module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { SettingsNavigationProp } from './navigationTypes'; +import { UserProfile, UserPreferences } from '../../../shared/types'; + +/** + * SettingsNavigationUtils - Utility functions for Settings module navigation + * + * This module provides helper functions for common navigation patterns + * within the Settings module, ensuring consistent navigation behavior. + */ + +/** + * Navigate to Settings screen + * @param navigation - Navigation prop from React Navigation + * @param params - Optional parameters for the settings screen + */ +export const navigateToSettings = ( + navigation: SettingsNavigationProp, + params?: { + section?: 'profile' | 'security' | 'notifications' | 'clinical' | 'privacy' | 'accessibility' | 'about' | 'help'; + refresh?: boolean; + } +): void => { + navigation.navigate('Settings', params); +}; + +/** + * Navigate to Profile Edit screen + * @param navigation - Navigation prop from React Navigation + * @param profile - Optional profile data to pre-populate + * @param fromScreen - Optional source screen for back navigation + */ +export const navigateToProfileEdit = ( + navigation: SettingsNavigationProp, + profile?: UserProfile, + fromScreen?: keyof import('./navigationTypes').SettingsStackParamList +): void => { + navigation.navigate('ProfileEdit', { + profile, + fromScreen, + }); +}; + +/** + * Navigate to Security Settings screen + * @param navigation - Navigation prop from React Navigation + * @param showBiometricSetup - Optional flag to show biometric setup + * @param fromScreen - Optional source screen for back navigation + */ +export const navigateToSecuritySettings = ( + navigation: SettingsNavigationProp, + showBiometricSetup?: boolean, + fromScreen?: keyof import('./navigationTypes').SettingsStackParamList +): void => { + navigation.navigate('SecuritySettings', { + showBiometricSetup, + fromScreen, + }); +}; + +/** + * Navigate to Notification Settings screen + * @param navigation - Navigation prop from React Navigation + * @param preferences - Optional notification preferences to pre-populate + * @param fromScreen - Optional source screen for back navigation + */ +export const navigateToNotificationSettings = ( + navigation: SettingsNavigationProp, + preferences?: UserPreferences['notificationPreferences'], + fromScreen?: keyof import('./navigationTypes').SettingsStackParamList +): void => { + navigation.navigate('NotificationSettings', { + preferences, + fromScreen, + }); +}; + +/** + * Navigate to Clinical Preferences screen + * @param navigation - Navigation prop from React Navigation + * @param preferences - Optional clinical preferences to pre-populate + * @param fromScreen - Optional source screen for back navigation + */ +export const navigateToClinicalPreferences = ( + navigation: SettingsNavigationProp, + preferences?: UserPreferences['clinicalPreferences'], + fromScreen?: keyof import('./navigationTypes').SettingsStackParamList +): void => { + navigation.navigate('ClinicalPreferences', { + preferences, + fromScreen, + }); +}; + +/** + * Navigate to Privacy Settings screen + * @param navigation - Navigation prop from React Navigation + * @param preferences - Optional privacy preferences to pre-populate + * @param fromScreen - Optional source screen for back navigation + */ +export const navigateToPrivacySettings = ( + navigation: SettingsNavigationProp, + preferences?: UserPreferences['privacyPreferences'], + fromScreen?: keyof import('./navigationTypes').SettingsStackParamList +): void => { + navigation.navigate('PrivacySettings', { + preferences, + fromScreen, + }); +}; + +/** + * Navigate to Accessibility Settings screen + * @param navigation - Navigation prop from React Navigation + * @param preferences - Optional accessibility preferences to pre-populate + * @param fromScreen - Optional source screen for back navigation + */ +export const navigateToAccessibilitySettings = ( + navigation: SettingsNavigationProp, + preferences?: UserPreferences['accessibilityPreferences'], + fromScreen?: keyof import('./navigationTypes').SettingsStackParamList +): void => { + navigation.navigate('AccessibilitySettings', { + preferences, + fromScreen, + }); +}; + +/** + * Navigate to About screen + * @param navigation - Navigation prop from React Navigation + * @param showChangelog - Optional flag to show changelog + * @param showLicenses - Optional flag to show licenses + */ +export const navigateToAbout = ( + navigation: SettingsNavigationProp, + showChangelog?: boolean, + showLicenses?: boolean +): void => { + navigation.navigate('About', { + showChangelog, + showLicenses, + }); +}; + +/** + * Navigate to Help & Support screen + * @param navigation - Navigation prop from React Navigation + * @param topic - Optional help topic to navigate to + * @param showContact - Optional flag to show contact form + */ +export const navigateToHelpSupport = ( + navigation: SettingsNavigationProp, + topic?: 'faq' | 'troubleshooting' | 'contact' | 'feedback', + showContact?: boolean +): void => { + navigation.navigate('HelpSupport', { + topic, + showContact, + }); +}; + +/** + * Go back to previous screen + * @param navigation - Navigation prop from React Navigation + */ +export const goBack = (navigation: SettingsNavigationProp): void => { + if (navigation.canGoBack()) { + navigation.goBack(); + } +}; + +/** + * Reset navigation stack to Settings screen + * @param navigation - Navigation prop from React Navigation + * @param params - Optional parameters for the settings screen + */ +export const resetToSettings = ( + navigation: SettingsNavigationProp, + params?: { + section?: 'profile' | 'security' | 'notifications' | 'clinical' | 'privacy' | 'accessibility' | 'about' | 'help'; + refresh?: boolean; + } +): void => { + navigation.reset({ + index: 0, + routes: [{ name: 'Settings', params }], + }); +}; + +/** + * Replace current screen with Settings screen + * @param navigation - Navigation prop from React Navigation + * @param params - Optional parameters for the settings screen + */ +export const replaceWithSettings = ( + navigation: SettingsNavigationProp, + params?: { + section?: 'profile' | 'security' | 'notifications' | 'clinical' | 'privacy' | 'accessibility' | 'about' | 'help'; + refresh?: boolean; + } +): void => { + navigation.replace('Settings', params); +}; + +/** + * Navigate to Settings and clear back stack + * @param navigation - Navigation prop from React Navigation + * @param params - Optional parameters for the settings screen + */ +export const navigateToSettingsAndClearStack = ( + navigation: SettingsNavigationProp, + params?: { + section?: 'profile' | 'security' | 'notifications' | 'clinical' | 'privacy' | 'accessibility' | 'about' | 'help'; + refresh?: boolean; + } +): void => { + navigation.reset({ + index: 0, + routes: [{ name: 'Settings', params }], + }); +}; + +/** + * Navigate to Profile Edit and clear back stack + * @param navigation - Navigation prop from React Navigation + * @param profile - Optional profile data to pre-populate + */ +export const navigateToProfileEditAndClearStack = ( + navigation: SettingsNavigationProp, + profile?: UserProfile +): void => { + navigation.reset({ + index: 0, + routes: [{ name: 'ProfileEdit', params: { profile } }], + }); +}; + +/** + * Navigate to Security Settings and clear back stack + * @param navigation - Navigation prop from React Navigation + * @param showBiometricSetup - Optional flag to show biometric setup + */ +export const navigateToSecuritySettingsAndClearStack = ( + navigation: SettingsNavigationProp, + showBiometricSetup?: boolean +): void => { + navigation.reset({ + index: 0, + routes: [{ name: 'SecuritySettings', params: { showBiometricSetup } }], + }); +}; + +/* + * End of File: navigationUtils.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Settings/redux/settingsSlice.ts b/app/modules/Settings/redux/settingsSlice.ts new file mode 100644 index 0000000..388c40a --- /dev/null +++ b/app/modules/Settings/redux/settingsSlice.ts @@ -0,0 +1,394 @@ +/* + * File: settingsSlice.ts + * Description: Settings state management slice + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import { UserProfile, UserPreferences, SettingsState } from '../../../shared/types'; + +// ============================================================================ +// ASYNC THUNKS +// ============================================================================ + +/** + * Fetch User Profile Async Thunk + * + * Purpose: Fetch user profile from API + * + * @returns Promise with user profile data or error + */ +export const fetchUserProfile = createAsyncThunk( + 'settings/fetchUserProfile', + async (_, { rejectWithValue }) => { + try { + // TODO: Replace with actual API call + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Mock user profile data + const mockProfile: UserProfile = { + id: '1', + email: 'john.smith@hospital.com', + firstName: 'Dr. John', + lastName: 'Smith', + role: 'ATTENDING_PHYSICIAN', + department: 'Emergency Medicine', + credentials: ['MD', 'FACEP'], + specialties: ['Emergency Medicine', 'Trauma'], + profilePicture: null, + phoneNumber: '+1-555-0123', + yearsOfExperience: 15, + isActive: true, + lastLogin: new Date().toISOString(), + permissions: ['READ_PATIENTS', 'WRITE_PATIENTS', 'VIEW_ALERTS'], + }; + + return mockProfile; + } catch (error) { + return rejectWithValue('Failed to fetch user profile.'); + } + } +); + +/** + * Update User Profile Async Thunk + * + * Purpose: Update user profile information + * + * @param profileData - Updated profile data + * @returns Promise with updated profile or error + */ +export const updateUserProfile = createAsyncThunk( + 'settings/updateUserProfile', + async (profileData: Partial & { id: string }, { rejectWithValue }) => { + try { + // TODO: Replace with actual API call + await new Promise(resolve => setTimeout(resolve, 800)); + return profileData; + } catch (error) { + return rejectWithValue('Failed to update user profile.'); + } + } +); + +/** + * Fetch User Preferences Async Thunk + * + * Purpose: Fetch user preferences from API + * + * @returns Promise with user preferences data or error + */ +export const fetchUserPreferences = createAsyncThunk( + 'settings/fetchUserPreferences', + async (_, { rejectWithValue }) => { + try { + // TODO: Replace with actual API call + await new Promise(resolve => setTimeout(resolve, 500)); + + // Mock user preferences data + const mockPreferences: UserPreferences = { + notifications: { + criticalAlerts: true, + patientUpdates: true, + shiftChanges: false, + systemMaintenance: false, + soundEnabled: true, + vibrationEnabled: true, + }, + clinical: { + autoRefreshInterval: 30, + showVitalSigns: true, + showAllergies: true, + showMedications: true, + defaultView: 'list', + }, + privacy: { + biometricEnabled: true, + sessionTimeout: 30, + dataRetention: 90, + auditLogging: true, + }, + accessibility: { + fontSize: 'medium', + highContrast: false, + screenReader: false, + reducedMotion: false, + }, + }; + + return mockPreferences; + } catch (error) { + return rejectWithValue('Failed to fetch user preferences.'); + } + } +); + +/** + * Update User Preferences Async Thunk + * + * Purpose: Update user preferences + * + * @param preferences - Updated preferences data + * @returns Promise with updated preferences or error + */ +export const updateUserPreferences = createAsyncThunk( + 'settings/updateUserPreferences', + async (preferences: Partial, { rejectWithValue }) => { + try { + // TODO: Replace with actual API call + await new Promise(resolve => setTimeout(resolve, 600)); + return preferences; + } catch (error) { + return rejectWithValue('Failed to update user preferences.'); + } + } +); + +// ============================================================================ +// INITIAL STATE +// ============================================================================ + +/** + * Initial Settings State + * + * Purpose: Define the initial state for settings + * + * Features: + * - User profile management + * - User preferences management + * - Loading states for async operations + * - Error handling and messages + * - Settings validation + */ +const initialState: SettingsState = { + // User profile + userProfile: null, + + // User preferences + userPreferences: null, + + // Loading states + isLoading: false, + isUpdatingProfile: false, + isUpdatingPreferences: false, + + // Error handling + error: null, + + // Settings validation + isProfileValid: true, + isPreferencesValid: true, + + // Last updated timestamps + profileLastUpdated: null, + preferencesLastUpdated: null, +}; + +// ============================================================================ +// SETTINGS SLICE +// ============================================================================ + +/** + * Settings Slice + * + * Purpose: Redux slice for settings state management + * + * Features: + * - User profile management + * - User preferences management + * - Settings validation + * - Error handling + * - Loading states + */ +const settingsSlice = createSlice({ + name: 'settings', + initialState, + reducers: { + /** + * Clear Error Action + * + * Purpose: Clear settings errors + */ + clearError: (state) => { + state.error = null; + }, + + /** + * Set Profile Validation Action + * + * Purpose: Set profile validation status + */ + setProfileValidation: (state, action: PayloadAction) => { + state.isProfileValid = action.payload; + }, + + /** + * Set Preferences Validation Action + * + * Purpose: Set preferences validation status + */ + setPreferencesValidation: (state, action: PayloadAction) => { + state.isPreferencesValid = action.payload; + }, + + /** + * Update Profile Field Action + * + * Purpose: Update a specific profile field + */ + updateProfileField: (state, action: PayloadAction<{ field: keyof UserProfile; value: any }>) => { + if (state.userProfile) { + (state.userProfile as any)[action.payload.field] = action.payload.value; + state.profileLastUpdated = new Date(); + } + }, + + /** + * Update Preference Field Action + * + * Purpose: Update a specific preference field + */ + updatePreferenceField: (state, action: PayloadAction<{ path: string; value: any }>) => { + if (state.userPreferences) { + const path = action.payload.path.split('.'); + let current: any = state.userPreferences; + + for (let i = 0; i < path.length - 1; i++) { + current = current[path[i]]; + } + + current[path[path.length - 1]] = action.payload.value; + state.preferencesLastUpdated = new Date(); + } + }, + + /** + * Reset Profile Action + * + * Purpose: Reset profile to last saved state + */ + resetProfile: (state) => { + // TODO: Implement reset logic + state.isProfileValid = true; + }, + + /** + * Reset Preferences Action + * + * Purpose: Reset preferences to last saved state + */ + resetPreferences: (state) => { + // TODO: Implement reset logic + state.isPreferencesValid = true; + }, + + /** + * Clear Settings Cache Action + * + * Purpose: Clear settings data cache + */ + clearSettingsCache: (state) => { + state.userProfile = null; + state.userPreferences = null; + state.profileLastUpdated = null; + state.preferencesLastUpdated = null; + }, + }, + extraReducers: (builder) => { + // Fetch User Profile + builder + .addCase(fetchUserProfile.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(fetchUserProfile.fulfilled, (state, action) => { + state.isLoading = false; + state.userProfile = action.payload; + state.profileLastUpdated = new Date(); + state.error = null; + }) + .addCase(fetchUserProfile.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload as string; + }); + + // Update User Profile + builder + .addCase(updateUserProfile.pending, (state) => { + state.isUpdatingProfile = true; + state.error = null; + }) + .addCase(updateUserProfile.fulfilled, (state, action) => { + state.isUpdatingProfile = false; + if (state.userProfile) { + state.userProfile = { ...state.userProfile, ...action.payload }; + state.profileLastUpdated = new Date(); + } + state.error = null; + }) + .addCase(updateUserProfile.rejected, (state, action) => { + state.isUpdatingProfile = false; + state.error = action.payload as string; + }); + + // Fetch User Preferences + builder + .addCase(fetchUserPreferences.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(fetchUserPreferences.fulfilled, (state, action) => { + state.isLoading = false; + state.userPreferences = action.payload; + state.preferencesLastUpdated = new Date(); + state.error = null; + }) + .addCase(fetchUserPreferences.rejected, (state, action) => { + state.isLoading = false; + state.error = action.payload as string; + }); + + // Update User Preferences + builder + .addCase(updateUserPreferences.pending, (state) => { + state.isUpdatingPreferences = true; + state.error = null; + }) + .addCase(updateUserPreferences.fulfilled, (state, action) => { + state.isUpdatingPreferences = false; + if (state.userPreferences) { + state.userPreferences = { ...state.userPreferences, ...action.payload }; + state.preferencesLastUpdated = new Date(); + } + state.error = null; + }) + .addCase(updateUserPreferences.rejected, (state, action) => { + state.isUpdatingPreferences = false; + state.error = action.payload as string; + }); + }, +}); + +// ============================================================================ +// EXPORTS +// ============================================================================ + +export const { + clearError, + setProfileValidation, + setPreferencesValidation, + updateProfileField, + updatePreferenceField, + resetProfile, + resetPreferences, + clearSettingsCache, +} = settingsSlice.actions; + +export default settingsSlice.reducer; + +/* + * End of File: settingsSlice.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Settings/screens/AppInfoScreen.tsx b/app/modules/Settings/screens/AppInfoScreen.tsx new file mode 100644 index 0000000..319abf1 --- /dev/null +++ b/app/modules/Settings/screens/AppInfoScreen.tsx @@ -0,0 +1,486 @@ +/* + * File: AppInfoScreen.tsx + * Description: App information screen displaying version, build, and app details + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + Image, + Linking, + TouchableOpacity, +} from 'react-native'; +import Icon from 'react-native-vector-icons/Feather'; +import { theme } from '../../../theme/theme'; +import { SettingsHeader } from '../components/SettingsHeader'; +import { CustomModal } from '../../../shared/components'; + +/** + * AppInfoScreenProps Interface + * + * Purpose: Defines the props required by the AppInfoScreen component + * + * Props: + * - navigation: React Navigation object for screen navigation + */ +interface AppInfoScreenProps { + navigation: any; +} + +/** + * AppInfoScreen Component + * + * Purpose: Display comprehensive app information including version, build details, + * and links to legal documents and support resources + * + * Features: + * 1. App logo and basic information + * 2. Version and build details + * 3. Legal and privacy information + * 4. Support and contact links + * 5. App description and features + */ +export const AppInfoScreen: React.FC = ({ + navigation, +}) => { + // ============================================================================ + // APP INFORMATION DATA + // ============================================================================ + + // App version and build information + const appInfo = { + name: 'NeoScan Radiologist', + version: '1.0.0', + buildNumber: '2025.08.001', + releaseDate: 'August 2025', + developer: 'Tech4Biz Solutions', + copyright: 'ยฉ 2024 Spurrin Innovations. All rights reserved.', + }; + + // App features and description + const appDescription = { + title: 'Emergency Radiology Physician App', + description: 'Advanced medical imaging and patient management platform designed specifically for emergency room physicians. Provides real-time access to critical patient scans, AI-powered diagnostic assistance, and streamlined clinical workflows.', + features: [ + 'Real-time patient monitoring', + 'AI-powered diagnostic assistance', + 'Emergency alert system', + 'Secure patient data management', + 'Mobile-optimized interface', + 'Hospital system integration', + ], + }; + + // Legal and support information + const legalInfo = { + privacyPolicy: 'https://neoscan.com/privacy', + termsOfService: 'https://neoscan.com/terms', + supportEmail: 'support@neoscan.com', + supportPhone: '+1-800-NEOSCAN', + website: 'https://neoscan.com', + }; + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * handleExternalLink Function + * + * Purpose: Handle opening external links in browser + * + * @param url - URL to open + */ + const handleExternalLink = async (url: string) => { + try { + const supported = await Linking.canOpenURL(url); + if (supported) { + await Linking.openURL(url); + } else { + console.log("Can't open URL:", url); + } + } catch (error) { + console.error('Error opening URL:', error); + } + }; + + /** + * handleEmailSupport Function + * + * Purpose: Handle opening email client for support + */ + const handleEmailSupport = () => { + const emailUrl = `mailto:${legalInfo.supportEmail}?subject=NeoScan Physician App Support`; + handleExternalLink(emailUrl); + }; + + /** + * handlePhoneSupport Function + * + * Purpose: Handle opening phone dialer for support + */ + const handlePhoneSupport = () => { + const phoneUrl = `tel:${legalInfo.supportPhone}`; + handleExternalLink(phoneUrl); + }; + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + {/* App info header with back button */} + navigation.goBack()} + /> + + {/* Scrollable app information content */} + + {/* App logo and basic info section */} + + + + NS + + + + {appInfo.name} + Version {appInfo.version} + Build {appInfo.buildNumber} + {appInfo.releaseDate} + + + {/* App description section */} + + {appDescription.title} + {appDescription.description} + + Key Features: + {appDescription.features.map((feature, index) => ( + + + {feature} + + ))} + + + {/* Version and build details section */} + + Technical Information + + + App Version: + {appInfo.version} + + + + Build Number: + {appInfo.buildNumber} + + + + Release Date: + {appInfo.releaseDate} + + + + Developer: + {appInfo.developer} + + + + {/* Support and contact section */} + + Support & Contact + + + Email Support: + {legalInfo.supportEmail} + + + + Phone Support: + {legalInfo.supportPhone} + + + handleExternalLink(legalInfo.website)} + activeOpacity={0.7} + > + Website: + {legalInfo.website} + + + + {/* Legal information section */} + + Legal Information + + handleExternalLink(legalInfo.privacyPolicy)} + activeOpacity={0.7} + > + Privacy Policy + + + + handleExternalLink(legalInfo.termsOfService)} + activeOpacity={0.7} + > + Terms of Service + + + + + {/* Copyright section */} + + {appInfo.copyright} + + + {/* Bottom spacing for tab bar */} + + + + ); +}; + +// ============================================================================ +// STYLES SECTION +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container for the app info screen + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Scroll view styling + scrollView: { + flex: 1, + }, + + // Scroll content styling + scrollContent: { + paddingHorizontal: theme.spacing.md, + }, + + // Bottom spacing for tab bar + bottomSpacing: { + height: theme.spacing.xl, + }, + + // App header section with logo and basic info + appHeaderSection: { + alignItems: 'center', + paddingVertical: theme.spacing.xl, + marginBottom: theme.spacing.md, + }, + + appLogoContainer: { + marginBottom: theme.spacing.md, + }, + + appLogo: { + width: 80, + height: 80, + borderRadius: 40, + backgroundColor: theme.colors.primary, + justifyContent: 'center', + alignItems: 'center', + ...theme.shadows.medium, + }, + + appLogoText: { + color: theme.colors.background, + fontSize: 32, + fontFamily: theme.typography.fontFamily.bold, + }, + + appName: { + fontSize: theme.typography.fontSize.displayMedium, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + textAlign: 'center', + }, + + appVersion: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.primary, + marginBottom: theme.spacing.xs, + }, + + appBuild: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xs, + }, + + appReleaseDate: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + }, + + // Information sections + infoSection: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.md, + marginBottom: theme.spacing.md, + ...theme.shadows.primary, + }, + + sectionTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.md, + }, + + appDescription: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + lineHeight: 22, + marginBottom: theme.spacing.md, + }, + + featuresTitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.sm, + }, + + featureItem: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.xs, + }, + + featureBullet: { + width: 6, + height: 6, + borderRadius: 3, + backgroundColor: theme.colors.primary, + marginRight: theme.spacing.sm, + }, + + featureText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + flex: 1, + }, + + // Information rows + infoRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: theme.spacing.xs, + borderBottomColor: theme.colors.border, + borderBottomWidth: 1, + }, + + infoLabel: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + }, + + infoValue: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + }, + + // Contact items + contactItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: theme.spacing.sm, + borderBottomColor: theme.colors.border, + borderBottomWidth: 1, + }, + + contactLabel: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + }, + + contactValue: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.primary, + }, + + // Legal items + legalItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: theme.spacing.sm, + borderBottomColor: theme.colors.border, + borderBottomWidth: 1, + }, + + legalText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.primary, + }, + + // Copyright section + copyrightSection: { + alignItems: 'center', + paddingVertical: theme.spacing.lg, + marginBottom: theme.spacing.md, + }, + + copyrightText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + textAlign: 'center', + lineHeight: 18, + }, + + +}); + +/* + * End of File: AppInfoScreen.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/Settings/screens/ChangePasswordScreen.tsx b/app/modules/Settings/screens/ChangePasswordScreen.tsx new file mode 100644 index 0000000..d349ba7 --- /dev/null +++ b/app/modules/Settings/screens/ChangePasswordScreen.tsx @@ -0,0 +1,826 @@ +/* + * File: ChangePasswordScreen.tsx + * Description: Change password screen with comprehensive password validation + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState } from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + TextInput, + TouchableOpacity, + Alert, + KeyboardAvoidingView, + Platform, +} from 'react-native'; +import Icon from 'react-native-vector-icons/Feather'; +import { theme } from '../../../theme/theme'; +import { SettingsHeader } from '../components/SettingsHeader'; +import { useAppDispatch } from '../../../store/hooks'; +import { changePasswordAsync } from '../../Auth/redux/authActions'; + +/** + * ChangePasswordScreenProps Interface + * + * Purpose: Defines the props required by the ChangePasswordScreen component + * + * Props: + * - navigation: React Navigation object for screen navigation + */ +interface ChangePasswordScreenProps { + navigation: any; +} + +/** + * FormData Interface + * + * Purpose: Defines the structure of the password change form data + */ +interface FormData { + currentPassword: string; + newPassword: string; + confirmPassword: string; +} + +/** + * FormErrors Interface + * + * Purpose: Defines the structure of form validation errors + */ +interface FormErrors { + currentPassword?: string; + newPassword?: string; + confirmPassword?: string; +} + +/** + * PasswordStrength Interface + * + * Purpose: Defines the structure of password strength information + */ +interface PasswordStrength { + score: number; + label: string; + color: string; + requirements: string[]; +} + +/** + * ChangePasswordScreen Component + * + * Purpose: Allows users to change their password with comprehensive validation + * including current password verification, new password strength requirements, + * and password confirmation + * + * Features: + * 1. Current password verification + * 2. New password strength validation + * 3. Password confirmation matching + * 4. Real-time password strength indicator + * 5. Comprehensive error handling + */ +export const ChangePasswordScreen: React.FC = ({ + navigation, +}) => { + // ============================================================================ + // REDUX STATE + // ============================================================================ + + const dispatch = useAppDispatch(); + + // ============================================================================ + // LOCAL STATE + // ============================================================================ + + const [formData, setFormData] = useState({ + currentPassword: '', + newPassword: '', + confirmPassword: '', + }); + + const [errors, setErrors] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [showCurrentPassword, setShowCurrentPassword] = useState(false); + const [showNewPassword, setShowNewPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [passwordStrength, setPasswordStrength] = useState({ + score: 0, + label: 'Very Weak', + color: theme.colors.error, + requirements: [], + }); + + // ============================================================================ + // PASSWORD STRENGTH VALIDATION + // ============================================================================ + + /** + * checkPasswordStrength Function + * + * Purpose: Analyze password strength and return strength information + * + * @param password - Password to analyze + * @returns PasswordStrength object with score, label, color, and requirements + */ + const checkPasswordStrength = (password: string): PasswordStrength => { + const requirements: string[] = []; + let score = 0; + + // Length requirement + if (password.length >= 8) { + score += 1; + requirements.push('โœ“ At least 8 characters'); + } else { + requirements.push('โœ— At least 8 characters'); + } + + // Uppercase requirement + if (/[A-Z]/.test(password)) { + score += 1; + requirements.push('โœ“ Contains uppercase letter'); + } else { + requirements.push('โœ— Contains uppercase letter'); + } + + // Lowercase requirement + if (/[a-z]/.test(password)) { + score += 1; + requirements.push('โœ“ Contains lowercase letter'); + } else { + requirements.push('โœ— Contains lowercase letter'); + } + + // Number requirement + if (/\d/.test(password)) { + score += 1; + requirements.push('โœ“ Contains number'); + } else { + requirements.push('โœ— Contains number'); + } + + // Special character requirement + if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + score += 1; + requirements.push('โœ“ Contains special character'); + } else { + requirements.push('โœ— Contains special character'); + } + + // Determine strength label and color + let label: string; + let color: string; + + if (score <= 1) { + label = 'Very Weak'; + color = theme.colors.error; + } else if (score <= 2) { + label = 'Weak'; + color = theme.colors.warning; + } else if (score <= 3) { + label = 'Fair'; + color = theme.colors.warning; + } else if (score <= 4) { + label = 'Good'; + color = theme.colors.info; + } else { + label = 'Strong'; + color = theme.colors.success; + } + + return { + score, + label, + color, + requirements, + }; + }; + + // ============================================================================ + // VALIDATION FUNCTIONS + // ============================================================================ + + /** + * validateField Function + * + * Purpose: Validate individual form fields + * + * @param field - Field name to validate + * @param value - Field value to validate + * @returns Validation error message or undefined + */ + const validateField = (field: keyof FormData, value: string): string | undefined => { + switch (field) { + case 'currentPassword': + if (!value.trim()) { + return 'Current password is required'; + } + if (value.trim().length < 6) { + return 'Current password must be at least 6 characters'; + } + break; + + case 'newPassword': + if (!value.trim()) { + return 'New password is required'; + } + if (value.trim().length < 8) { + return 'New password must be at least 8 characters'; + } + if (value === formData.currentPassword) { + return 'New password must be different from current password'; + } + break; + + case 'confirmPassword': + if (!value.trim()) { + return 'Please confirm your new password'; + } + if (value !== formData.newPassword) { + return 'Passwords do not match'; + } + break; + } + return undefined; + }; + + /** + * validateForm Function + * + * Purpose: Validate entire form and return validation errors + * + * @returns Object containing validation errors + */ + const validateForm = (): FormErrors => { + const newErrors: FormErrors = {}; + + Object.keys(formData).forEach((field) => { + const key = field as keyof FormData; + const error = validateField(key, formData[key]); + if (error) { + newErrors[key] = error; + } + }); + + return newErrors; + }; + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * handleInputChange Function + * + * Purpose: Handle input field changes and update password strength + * + * @param field - Field name that changed + * @param value - New field value + */ + const handleInputChange = (field: keyof FormData, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + + // Clear field-specific error when user starts typing + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: undefined })); + } + + // Update password strength for new password field + if (field === 'newPassword') { + const strength = checkPasswordStrength(value); + setPasswordStrength(strength); + } + }; + + /** + * handleInputBlur Function + * + * Purpose: Validate field when user leaves the input + * + * @param field - Field name to validate + */ + const handleInputBlur = (field: keyof FormData) => { + const error = validateField(field, formData[field]); + setErrors(prev => ({ ...prev, [field]: error })); + }; + + /** + * handleSubmit Function + * + * Purpose: Handle form submission with validation and API call + */ + const handleSubmit = async () => { + // Validate form + const validationErrors = validateForm(); + if (Object.keys(validationErrors).length > 0) { + setErrors(validationErrors); + return; + } + + // Check password strength + if (passwordStrength.score < 3) { + Alert.alert( + 'Weak Password', + 'Please choose a stronger password that meets the requirements.', + [{ text: 'OK' }] + ); + return; + } + + setIsSubmitting(true); + + try { + // Dispatch password change action + await dispatch(changePasswordAsync({ + currentPassword: formData.currentPassword, + newPassword: formData.newPassword, + })).unwrap(); + + // Navigate back after successful password change + navigation.goBack(); + } catch (error: any) { + // Handle error - toast notification is already shown in the thunk + console.error('Password change error:', error); + } finally { + setIsSubmitting(false); + } + }; + + /** + * handleCancel Function + * + * Purpose: Handle cancel action + */ + const handleCancel = () => { + navigation.goBack(); + }; + + /** + * togglePasswordVisibility Function + * + * Purpose: Toggle password visibility for specified field + * + * @param field - Field to toggle visibility for + */ + const togglePasswordVisibility = (field: 'current' | 'new' | 'confirm') => { + switch (field) { + case 'current': + setShowCurrentPassword(!showCurrentPassword); + break; + case 'new': + setShowNewPassword(!showNewPassword); + break; + case 'confirm': + setShowConfirmPassword(!showConfirmPassword); + break; + } + }; + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + {/* Header with back button */} + + + {/* Scrollable form content */} + + {/* Password requirements info */} + + Password Requirements + + Your new password must meet the following requirements to ensure security: + + + โ€ข At least 8 characters long + โ€ข Contains uppercase and lowercase letters + โ€ข Contains at least one number + โ€ข Contains at least one special character + + + + {/* Password change form */} + + Change Password + + {/* Current Password Input */} + + Current Password * + + handleInputChange('currentPassword', value)} + onBlur={() => handleInputBlur('currentPassword')} + placeholder="Enter your current password" + placeholderTextColor={theme.colors.textMuted} + secureTextEntry={!showCurrentPassword} + autoCapitalize="none" + autoCorrect={false} + /> + togglePasswordVisibility('current')} + activeOpacity={0.7} + > + + + + {errors.currentPassword && ( + {errors.currentPassword} + )} + + + {/* New Password Input */} + + New Password * + + handleInputChange('newPassword', value)} + onBlur={() => handleInputBlur('newPassword')} + placeholder="Enter your new password" + placeholderTextColor={theme.colors.textMuted} + secureTextEntry={!showNewPassword} + autoCapitalize="none" + autoCorrect={false} + /> + togglePasswordVisibility('new')} + activeOpacity={0.7} + > + + + + {errors.newPassword && ( + {errors.newPassword} + )} + + + {/* Password Strength Indicator */} + {formData.newPassword.length > 0 && ( + + Password Strength: + + + + + {passwordStrength.label} + + + )} + + {/* Password Requirements Check */} + {formData.newPassword.length > 0 && ( + + {passwordStrength.requirements.map((requirement, index) => ( + + {requirement} + + ))} + + )} + + {/* Confirm Password Input */} + + Confirm New Password * + + handleInputChange('confirmPassword', value)} + onBlur={() => handleInputBlur('confirmPassword')} + placeholder="Confirm your new password" + placeholderTextColor={theme.colors.textMuted} + secureTextEntry={!showConfirmPassword} + autoCapitalize="none" + autoCorrect={false} + /> + togglePasswordVisibility('confirm')} + activeOpacity={0.7} + > + + + + {errors.confirmPassword && ( + {errors.confirmPassword} + )} + + + + {/* Action buttons */} + + + + {isSubmitting ? 'Changing Password...' : 'Change Password'} + + + + + Cancel + + + + {/* Bottom spacing for tab bar */} + + + + ); +}; + +// ============================================================================ +// STYLES SECTION +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Scroll view styling + scrollView: { + flex: 1, + }, + + // Scroll content styling + scrollContent: { + paddingHorizontal: theme.spacing.md, + }, + + // Bottom spacing for tab bar + bottomSpacing: { + height: theme.spacing.xl, + }, + + // Information sections + infoSection: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.md, + marginBottom: theme.spacing.md, + ...theme.shadows.primary, + }, + + sectionTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.md, + }, + + infoText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + lineHeight: 22, + marginBottom: theme.spacing.md, + }, + + // Requirements list styling + requirementsList: { + marginTop: theme.spacing.sm, + }, + + requirementItem: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xs, + }, + + // Input container styling + inputContainer: { + marginBottom: theme.spacing.sm, + backgroundColor: theme.colors.background, + padding: theme.spacing.sm, + borderRadius: theme.borderRadius.small, + }, + + inputLabel: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.sm, + fontWeight: '600', + }, + + // Password input styling + passwordInputContainer: { + position: 'relative', + }, + + textInput: { + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.md, + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textPrimary, + backgroundColor: theme.colors.background, + minHeight: 48, + }, + + passwordInput: { + paddingRight: theme.spacing.xl + theme.spacing.md, + }, + + eyeIcon: { + position: 'absolute', + right: theme.spacing.md, + top: theme.spacing.md, + padding: theme.spacing.xs, + }, + + inputError: { + borderColor: theme.colors.error, + backgroundColor: theme.colors.error + '10', + }, + + errorText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.error, + marginTop: theme.spacing.sm, + marginLeft: theme.spacing.xs, + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + backgroundColor: theme.colors.error + '10', + borderRadius: theme.borderRadius.small, + alignSelf: 'flex-start', + }, + + // Password strength styling + strengthContainer: { + marginBottom: theme.spacing.md, + }, + + strengthLabel: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + strengthBar: { + height: 4, + backgroundColor: theme.colors.border, + borderRadius: 2, + marginBottom: theme.spacing.xs, + overflow: 'hidden', + }, + + strengthProgress: { + height: '100%', + borderRadius: 2, + }, + + strengthText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + textAlign: 'center', + }, + + // Requirements check styling + requirementsCheck: { + marginBottom: theme.spacing.md, + padding: theme.spacing.sm, + backgroundColor: theme.colors.backgroundAlt, + borderRadius: theme.borderRadius.small, + }, + + requirementCheckItem: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xs, + }, + + // Button container + buttonContainer: { + marginTop: theme.spacing.lg, + marginBottom: theme.spacing.md, + }, + + submitButton: { + backgroundColor: theme.colors.primary, + borderRadius: theme.borderRadius.medium, + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + alignItems: 'center', + marginBottom: theme.spacing.md, + ...theme.shadows.primary, + }, + + submitButtonDisabled: { + backgroundColor: theme.colors.border, + shadowColor: 'transparent', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, + }, + + submitButtonText: { + color: theme.colors.background, + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + }, + + submitButtonTextDisabled: { + color: theme.colors.textMuted, + }, + + cancelButton: { + backgroundColor: 'transparent', + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.medium, + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + alignItems: 'center', + }, + + cancelButtonText: { + color: theme.colors.textSecondary, + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.medium, + }, +}); + +/* + * End of File: ChangePasswordScreen.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/Settings/screens/EditProfileScreen.tsx b/app/modules/Settings/screens/EditProfileScreen.tsx new file mode 100644 index 0000000..ff3bda9 --- /dev/null +++ b/app/modules/Settings/screens/EditProfileScreen.tsx @@ -0,0 +1,637 @@ +/* + * File: EditProfileScreen.tsx + * Description: Edit profile screen for updating user information + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + TextInput, + TouchableOpacity, + Alert, + KeyboardAvoidingView, + Platform, +} from 'react-native'; +import Icon from 'react-native-vector-icons/Feather'; +import { theme } from '../../../theme/theme'; +import { SettingsHeader } from '../components/SettingsHeader'; +import { useAppSelector, useAppDispatch } from '../../../store/hooks'; +import { + selectUserFirstName, + selectUserLastName, + selectUserDisplayName, + selectUserEmail, + selectUser, +} from '../../Auth/redux/authSelectors'; +import { updateUserProfileAsync } from '../../Auth/redux/authActions'; + +/** + * EditProfileScreenProps Interface + * + * Purpose: Defines the props required by the EditProfileScreen component + * + * Props: + * - navigation: React Navigation object for screen navigation + */ +interface EditProfileScreenProps { + navigation: any; +} + +/** + * FormData Interface + * + * Purpose: Defines the structure of the profile form data + */ +interface FormData { + firstName: string; + lastName: string; + displayName: string; +} + +/** + * FormErrors Interface + * + * Purpose: Defines the structure of form validation errors + */ +interface FormErrors { + firstName?: string; + lastName?: string; + displayName?: string; +} + +/** + * EditProfileScreen Component + * + * Purpose: Allows users to edit their profile information including first name, + * last name, and display name with proper validation + * + * Features: + * 1. Pre-populated form with current user data + * 2. Real-time validation + * 3. Form submission with error handling + * 4. Clean and intuitive user interface + */ +export const EditProfileScreen: React.FC = ({ + navigation, +}) => { + // ============================================================================ + // REDUX STATE + // ============================================================================ + + const dispatch = useAppDispatch(); + const user = useAppSelector(selectUser); + const currentFirstName = useAppSelector(selectUserFirstName); + const currentLastName = useAppSelector(selectUserLastName); + const currentDisplayName = useAppSelector(selectUserDisplayName); + const currentEmail = useAppSelector(selectUserEmail); + + // ============================================================================ + // LOCAL STATE + // ============================================================================ + + const [formData, setFormData] = useState({ + firstName: '', + lastName: '', + displayName: '', + }); + + const [errors, setErrors] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + + // ============================================================================ + // EFFECTS + // ============================================================================ + + /** + * Initialize form data with current user information + */ + useEffect(() => { + setFormData({ + firstName: currentFirstName || '', + lastName: currentLastName || '', + displayName: currentDisplayName || '', + }); + }, [currentFirstName, currentLastName, currentDisplayName]); + + /** + * Check if form has unsaved changes + */ + useEffect(() => { + const hasUnsavedChanges = + formData.firstName !== currentFirstName || + formData.lastName !== currentLastName || + formData.displayName !== currentDisplayName; + + setHasChanges(hasUnsavedChanges); + }, [formData, currentFirstName, currentLastName, currentDisplayName]); + + // ============================================================================ + // VALIDATION FUNCTIONS + // ============================================================================ + + /** + * validateField Function + * + * Purpose: Validate individual form fields + * + * @param field - Field name to validate + * @param value - Field value to validate + * @returns Validation error message or undefined + */ + const validateField = (field: keyof FormData, value: string): string | undefined => { + switch (field) { + case 'firstName': + if (!value.trim()) { + return 'First name is required'; + } + if (value.trim().length < 2) { + return 'First name must be at least 2 characters'; + } + if (value.trim().length > 50) { + return 'First name must be less than 50 characters'; + } + if (!/^[a-zA-Z\s'-]+$/.test(value.trim())) { + return 'First name can only contain letters, spaces, hyphens, and apostrophes'; + } + break; + + case 'lastName': + if (!value.trim()) { + return 'Last name is required'; + } + if (value.trim().length < 1) { + return 'Last name must be at least 1 character'; + } + if (value.trim().length > 50) { + return 'Last name must be less than 50 characters'; + } + if (!/^[a-zA-Z\s'-]+$/.test(value.trim())) { + return 'Last name can only contain letters, spaces, hyphens, and apostrophes'; + } + break; + + case 'displayName': + if (!value.trim()) { + return 'Display name is required'; + } + if (value.trim().length < 2) { + return 'Display name must be at least 2 characters'; + } + if (value.trim().length > 30) { + return 'Display name must be less than 30 characters'; + } + break; + } + return undefined; + }; + + /** + * validateForm Function + * + * Purpose: Validate entire form and return validation errors + * + * @returns Object containing validation errors + */ + const validateForm = (): FormErrors => { + const newErrors: FormErrors = {}; + + Object.keys(formData).forEach((field) => { + const key = field as keyof FormData; + const error = validateField(key, formData[key]); + if (error) { + newErrors[key] = error; + } + }); + + return newErrors; + }; + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * handleInputChange Function + * + * Purpose: Handle input field changes and clear field-specific errors + * + * @param field - Field name that changed + * @param value - New field value + */ + const handleInputChange = (field: keyof FormData, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + + // Clear field-specific error when user starts typing + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: undefined })); + } + }; + + /** + * handleInputBlur Function + * + * Purpose: Validate field when user leaves the input + * + * @param field - Field name to validate + */ + const handleInputBlur = (field: keyof FormData) => { + const error = validateField(field, formData[field]); + setErrors(prev => ({ ...prev, [field]: error })); + }; + + /** + * handleSubmit Function + * + * Purpose: Handle form submission with validation and API call + */ + const handleSubmit = async () => { + // Validate form + const validationErrors = validateForm(); + if (Object.keys(validationErrors).length > 0) { + setErrors(validationErrors); + return; + } + + setIsSubmitting(true); + + try { + // Dispatch update action + await dispatch(updateUserProfileAsync({ + first_name: formData.firstName.trim(), + last_name: formData.lastName.trim(), + })).unwrap(); + + // Navigate back after successful profile update + navigation.goBack(); + } catch (error: any) { + // Handle error - toast notification is already shown in the thunk + console.error('Profile update error:', error); + } finally { + setIsSubmitting(false); + } + }; + + /** + * handleCancel Function + * + * Purpose: Handle cancel action with unsaved changes warning + */ + const handleCancel = () => { + if (hasChanges) { + Alert.alert( + 'Unsaved Changes', + 'You have unsaved changes. Are you sure you want to leave?', + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Leave', + style: 'destructive', + onPress: () => navigation.goBack(), + }, + ] + ); + } else { + navigation.goBack(); + } + }; + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + {/* Header with back button */} + + + {/* Scrollable form content */} + + {/* Current email display (read-only) */} + + Account Information + + Email Address + + {currentEmail} + + + + + Email address cannot be changed + + + + {/* Profile form section */} + + Personal Information + + {/* First Name Input */} + + First Name * + handleInputChange('firstName', value)} + onBlur={() => handleInputBlur('firstName')} + placeholder="Enter your first name" + placeholderTextColor={theme.colors.textMuted} + autoCapitalize="words" + autoCorrect={false} + maxLength={50} + /> + {errors.firstName && ( + {errors.firstName} + )} + + + {/* Last Name Input */} + + Last Name * + handleInputChange('lastName', value)} + onBlur={() => handleInputBlur('lastName')} + placeholder="Enter your last name" + placeholderTextColor={theme.colors.textMuted} + autoCapitalize="words" + autoCorrect={false} + maxLength={50} + /> + {errors.lastName && ( + {errors.lastName} + )} + + + {/* Display Name Input */} + + Display Name * + handleInputChange('displayName', value)} + onBlur={() => handleInputBlur('displayName')} + placeholder="Enter your display name" + placeholderTextColor={theme.colors.textMuted} + autoCapitalize="words" + autoCorrect={false} + maxLength={30} + /> + {errors.displayName && ( + {errors.displayName} + )} + + + + {/* Action buttons */} + + + + {isSubmitting ? 'Updating...' : 'Update Profile'} + + + + + Cancel + + + + {/* Bottom spacing for tab bar */} + + + + ); +}; + +// ============================================================================ +// STYLES SECTION +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Scroll view styling + scrollView: { + flex: 1, + }, + + // Scroll content styling + scrollContent: { + paddingHorizontal: theme.spacing.md, + }, + + // Bottom spacing for tab bar + bottomSpacing: { + height: theme.spacing.xl, + }, + + // Information sections + infoSection: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.md, + marginBottom: theme.spacing.md, + ...theme.shadows.primary, + }, + + sectionTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.md, + }, + + // Read-only field styling + readOnlyField: { + paddingVertical: theme.spacing.sm, + borderBottomColor: theme.colors.border, + borderBottomWidth: 1, + }, + + readOnlyLabel: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textMuted, + marginBottom: theme.spacing.xs, + }, + + emailContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: theme.spacing.xs, + }, + + readOnlyValue: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textPrimary, + flex: 1, + }, + + lockIcon: { + padding: theme.spacing.xs, + backgroundColor: theme.colors.backgroundAlt, + borderRadius: theme.borderRadius.small, + }, + + readOnlyHint: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + fontStyle: 'italic', + }, + + // Input container styling + inputContainer: { + marginBottom: theme.spacing.sm, + backgroundColor: theme.colors.background, + padding: theme.spacing.sm, + borderRadius: theme.borderRadius.small, + }, + + inputLabel: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.sm, + fontWeight: '600', + }, + + textInput: { + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.medium, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.md, + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textPrimary, + backgroundColor: theme.colors.background, + minHeight: 48, + }, + + inputError: { + borderColor: theme.colors.error, + backgroundColor: theme.colors.error + '10', + }, + + errorText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.error, + marginTop: theme.spacing.sm, + marginLeft: theme.spacing.xs, + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + backgroundColor: theme.colors.error + '10', + borderRadius: theme.borderRadius.small, + alignSelf: 'flex-start', + }, + + // Button container + buttonContainer: { + marginTop: theme.spacing.lg, + marginBottom: theme.spacing.md, + }, + + submitButton: { + backgroundColor: theme.colors.primary, + borderRadius: theme.borderRadius.medium, + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + alignItems: 'center', + marginBottom: theme.spacing.md, + ...theme.shadows.primary, + }, + + submitButtonDisabled: { + backgroundColor: theme.colors.border, + shadowColor: 'transparent', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, + }, + + submitButtonText: { + color: theme.colors.background, + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + }, + + submitButtonTextDisabled: { + color: theme.colors.textMuted, + }, + + cancelButton: { + backgroundColor: 'transparent', + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.medium, + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + alignItems: 'center', + }, + + cancelButtonText: { + color: theme.colors.textSecondary, + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.medium, + }, +}); + +/* + * End of File: EditProfileScreen.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/Settings/screens/SettingsScreen.tsx b/app/modules/Settings/screens/SettingsScreen.tsx new file mode 100644 index 0000000..27ee3e5 --- /dev/null +++ b/app/modules/Settings/screens/SettingsScreen.tsx @@ -0,0 +1,822 @@ +/* + * File: SettingsScreen.tsx + * Description: Main settings screen with profile management and app preferences + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + Alert, + RefreshControl, + Image, + TouchableOpacity, + ActivityIndicator, + ActionSheetIOS, + Platform, +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import { + SettingsSection, + SettingsSectionData, + SettingsItem +} from '../../../shared/types'; +import { SettingsHeader } from '../components/SettingsHeader'; +import { SettingsSectionComponent } from '../components/SettingsSectionComponent'; +import { ProfileCard } from '../components/ProfileCard'; +import { CustomModal } from '../../../shared/components'; +import { useAppDispatch, useAppSelector } from '../../../store/hooks'; +import { logoutUser } from '../../Auth/redux/authActions'; +import { + selectUser, + selectUserDisplayName, + selectUserEmail, + selectUserFirstName, + selectUserLastName, + selectUserProfilePhoto, + selectDashboardSettings +} from '../../Auth/redux/authSelectors'; +import { API_CONFIG } from '../../../shared/utils'; +import { authAPI } from '../../Auth/services/authAPI'; +import { + launchImageLibrary, + launchCamera, + ImagePickerResponse, + MediaType +} from 'react-native-image-picker'; +import Icon from 'react-native-vector-icons/Feather'; + +/** + * SettingsScreenProps Interface + * + * Purpose: Defines the props required by the SettingsScreen component + * + * Props: + * - navigation: React Navigation object for screen navigation + */ +interface SettingsScreenProps { + navigation: any; +} + +/** + * SettingsScreen Component + * + * Purpose: Main settings screen for user profile management and app preferences + * + * Features: + * 1. User profile overview and quick access + * 2. Comprehensive settings sections + * 3. Navigation to detailed settings screens + * 4. Pull-to-refresh functionality + * 5. Mock data generation for demonstration + * + * Settings Sections: + * - Profile: Personal and professional information + * - Notifications: Alert and notification preferences + * - Clinical: Clinical workflow preferences + * - Privacy: Security and privacy settings + * - Accessibility: Accessibility features + * - About: App information and help + * - Logout: Sign out functionality + */ +export const SettingsScreen: React.FC = ({ + navigation, +}) => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + // Settings sections state + const [settingsSections, setSettingsSections] = useState([]); + + // UI state + const [refreshing, setRefreshing] = useState(false); + + // Profile photo state + const [uploadingPhoto, setUploadingPhoto] = useState(false); + const [tempProfilePhoto, setTempProfilePhoto] = useState(null); + + // Modal state + const [modalVisible, setModalVisible] = useState(false); + const [modalConfig, setModalConfig] = useState({ + title: '', + message: '', + type: 'info' as 'success' | 'error' | 'warning' | 'info' | 'confirm', + onConfirm: () => {}, + showCancel: false, + icon: '', + }); + + // Redux dispatch and selectors + const dispatch = useAppDispatch(); + + // User data from Redux + const user = useAppSelector(selectUser); + const userDisplayName = useAppSelector(selectUserDisplayName); + const userEmail = useAppSelector(selectUserEmail); + const userFirstName = useAppSelector(selectUserFirstName); + const userLastName = useAppSelector(selectUserLastName); + const userProfilePhoto = useAppSelector(selectUserProfilePhoto); + const dashboardSettings = useAppSelector(selectDashboardSettings); + + + // ============================================================================ + // SETTINGS SECTIONS GENERATION + // ============================================================================ + + /** + * generateSettingsSections Function + * + * Purpose: Generate settings sections with items for the settings screen + * + * Returns: Array of SettingsSectionData with navigation and action items + */ + const generateSettingsSections = (): SettingsSectionData[] => [ + { + id: 'PROFILE', + title: 'Profile & Account', + items: [ + { + id: 'edit-profile', + title: 'Edit Profile', + subtitle: 'Update personal and professional information', + icon: 'user', + type: 'NAVIGATION', + onPress: () => handleNavigation('PROFILE'), + }, + { + id: 'change-password', + title: 'Change Password', + subtitle: 'Update your account password', + icon: 'lock', + type: 'NAVIGATION', + onPress: () => handleNavigation('CHANGE_PASSWORD'), + }, + + ], + }, + + + { + id: 'ABOUT', + title: 'About & Support', + items: [ + { + id: 'app-info', + title: 'App Information', + subtitle: 'Version, build number, and details', + icon: 'smartphone', + type: 'NAVIGATION', + onPress: () => handleNavigation('APP_INFO'), + }, + { + id: 'help-support', + title: 'Help & Support', + subtitle: 'Contact support and view documentation', + icon: 'help', + type: 'NAVIGATION', + onPress: () => handleNavigation('HELP'), + }, + + ], + }, + { + id: 'LOGOUT', + title: 'Account', + items: [ + { + id: 'logout', + title: 'Sign Out', + subtitle: 'Sign out of your account', + icon: 'log-out', + type: 'ACTION', + onPress: handleLogout, + }, + ], + }, + ]; + + // ============================================================================ + // EFFECTS + // ============================================================================ + + /** + * useEffect for initial settings sections generation + * + * Purpose: Generate settings sections when component mounts or user data changes + */ + useEffect(() => { + setSettingsSections(generateSettingsSections()); + }, [user, dashboardSettings]); + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * handleProfilePhotoUpdate Function + * + * Purpose: Show action sheet with camera and gallery options for profile photo update + * + * Flow: + * 1. Show action sheet with camera and gallery options + * 2. Handle user selection + * 3. Launch appropriate image picker + */ + const handleProfilePhotoUpdate = () => { + if (Platform.OS === 'ios') { + ActionSheetIOS.showActionSheetWithOptions( + { + options: ['Cancel', 'Take Photo', 'Choose from Gallery'], + cancelButtonIndex: 0, + userInterfaceStyle: 'light', + }, + (buttonIndex) => { + if (buttonIndex === 1) { + handleCameraCapture(); + } else if (buttonIndex === 2) { + handleGallerySelection(); + } + } + ); + } else { + // For Android, show custom action sheet or Alert + Alert.alert( + 'Update Profile Photo', + 'Choose an option', + [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Take Photo', onPress: handleCameraCapture }, + { text: 'Choose from Gallery', onPress: handleGallerySelection }, + ] + ); + } + }; + + /** + * handleCameraCapture Function + * + * Purpose: Launch camera to capture new profile photo + * + * Flow: + * 1. Launch camera + * 2. Validate captured image + * 3. Upload to server + * 4. Update local state + */ + const handleCameraCapture = async () => { + try { + // Launch camera + const result: ImagePickerResponse = await launchCamera({ + mediaType: 'photo' as MediaType, + quality: 0.8, + maxWidth: 800, + maxHeight: 800, + saveToPhotos: false, + includeBase64: false, + }); + + if (result.didCancel || !result.assets || result.assets.length === 0) { + return; + } + + const asset = result.assets[0]; + if (!asset.uri) { + throw new Error('No image captured'); + } + + // Validate file size (max 5MB) + if (asset.fileSize && asset.fileSize > 5 * 1024 * 1024) { + throw new Error('Image size must be less than 5MB'); + } + + // Set temporary photo for preview + setTempProfilePhoto(asset.uri); + + // Upload the captured photo + await uploadProfilePhoto(asset.uri); + + } catch (error) { + console.error('Camera capture error:', error); + setModalConfig({ + title: 'Error', + message: error instanceof Error ? error.message : 'Failed to capture photo', + type: 'error', + onConfirm: () => {}, + showCancel: false, + icon: 'alert-circle', + }); + setModalVisible(true); + } + }; + + /** + * handleGallerySelection Function + * + * Purpose: Launch gallery to select existing profile photo + * + * Flow: + * 1. Launch image picker + * 2. Validate selected image + * 3. Upload to server + * 4. Update local state + */ + const handleGallerySelection = async () => { + try { + // Launch image picker + const result: ImagePickerResponse = await launchImageLibrary({ + mediaType: 'photo' as MediaType, + quality: 0.8, + maxWidth: 800, + maxHeight: 800, + includeBase64: false, + }); + + if (result.didCancel || !result.assets || result.assets.length === 0) { + return; + } + + const asset = result.assets[0]; + if (!asset.uri) { + throw new Error('No image selected'); + } + + // Validate file size (max 5MB) + if (asset.fileSize && asset.fileSize > 5 * 1024 * 1024) { + throw new Error('Image size must be less than 5MB'); + } + + // Set temporary photo for preview + setTempProfilePhoto(asset.uri); + + // Upload the selected photo + await uploadProfilePhoto(asset.uri); + + } catch (error) { + console.error('Gallery selection error:', error); + setModalConfig({ + title: 'Error', + message: error instanceof Error ? error.message : 'Failed to select photo', + type: 'error', + onConfirm: () => {}, + showCancel: false, + icon: 'alert-circle', + }); + setModalVisible(true); + } + }; + + /** + * uploadProfilePhoto Function + * + * Purpose: Upload selected profile photo to server + * + * @param imageUri - URI of the selected image + */ + const uploadProfilePhoto = async (imageUri: string) => { + try { + setUploadingPhoto(true); + + // Create form data + const formData = new FormData(); + formData.append('profile_photo', { + uri: imageUri, + type: 'image/jpeg', + name: 'profile_photo.jpg', + } as any); + + // Get user token from Redux + const token = user?.access_token; + + if (!token) { + throw new Error('Authentication token not found'); + } + + // Upload using authAPI + const response = await authAPI.uploadProfilePhoto(formData, token); + + // Type the response properly + const responseData = response.data as { success: boolean; message?: string; data?: any }; + + if (responseData.success) { + // Update local state with new photo + setTempProfilePhoto(null); + + // Show success message + setModalConfig({ + title: 'Success', + message: 'Profile photo updated successfully!', + type: 'success', + icon: 'check-circle', + onConfirm: () => { + // Refresh user data or update Redux state + handleRefresh(); + }, + showCancel: false, + }); + setModalVisible(true); + } else { + throw new Error(responseData.message || 'Upload failed'); + } + + } catch (error) { + console.error('Error uploading photo:', error); + setModalConfig({ + title: 'Upload Failed', + message: error instanceof Error ? error.message : 'Failed to upload profile photo', + type: 'error', + icon: 'alert-circle', + onConfirm: () => {}, + showCancel: false, + }); + setModalVisible(true); + } finally { + setUploadingPhoto(false); + } + }; + + /** + * handleRefresh Function + * + * Purpose: Handle pull-to-refresh functionality to update settings data + * + * Flow: + * 1. Set refreshing state to true (show loading indicator) + * 2. Simulate API call with delay + * 3. Regenerate settings sections with current user data + * 4. Set refreshing state to false (hide loading indicator) + */ + const handleRefresh = async () => { + setRefreshing(true); + + // Simulate API call with 1-second delay + await new Promise(resolve => setTimeout(() => resolve(), 1000)); + + // Regenerate settings sections with current user data + setSettingsSections(generateSettingsSections()); + + setRefreshing(false); + }; + + /** + * handleNavigation Function + * + * Purpose: Handle navigation to different settings screens + * + * @param screen - Screen to navigate to + */ + const handleNavigation = (screen: string) => { + switch (screen) { + case 'APP_INFO': + navigation.navigate('AppInfoScreen'); + break; + case 'PROFILE': + navigation.navigate('EditProfileScreen'); + break; + case 'CHANGE_PASSWORD': + navigation.navigate('ChangePasswordScreen'); + break; + case 'HELP': + // TODO: Implement help and support + setModalConfig({ + title: 'Help & Support', + message: 'Help and support functionality coming soon!', + type: 'info', + onConfirm: () => {}, + showCancel: false, + icon: 'info', + }); + setModalVisible(true); + break; + default: + console.log('Navigate to:', screen); + setModalConfig({ + title: 'Navigation', + message: `Navigate to ${screen} screen`, + type: 'info', + onConfirm: () => {}, + showCancel: false, + icon: 'info', + }); + setModalVisible(true); + } + }; + + /** + * handleToggleSetting Function + * + * Purpose: Handle toggle settings changes + * + * @param setting - Setting to toggle + */ + const handleToggleSetting = (setting: string) => { + // TODO: Implement setting toggle logic + console.log('Toggle setting:', setting); + setModalConfig({ + title: 'Setting Toggle', + message: `Toggle ${setting} setting`, + type: 'info', + icon: 'info', + onConfirm: () => {}, + showCancel: false, + }); + setModalVisible(true); + }; + + /** + * handleLogout Function + * + * Purpose: Handle user logout with Redux integration + * + * Flow: + * 1. Show confirmation dialog + * 2. Dispatch logout action to Redux + * 3. Clear authentication state + * 4. Show success message + * 5. Automatically navigate to login screen via Redux state change + */ + const handleLogout = () => { + setModalConfig({ + title: 'Sign Out', + message: 'Are you sure you want to sign out?', + type: 'confirm', + icon: 'log-out', + onConfirm: async () => { + try { + // Dispatch logout thunk to Redux + await dispatch(logoutUser()); + + // Log the logout action + console.log('User logged out successfully'); + } catch (error) { + console.error('Logout error:', error); + setModalConfig({ + title: 'Error', + message: 'Failed to sign out. Please try again.', + type: 'error', + icon: 'info', + onConfirm: () => {}, + showCancel: false, + }); + setModalVisible(true); + } + }, + showCancel: true, + }); + setModalVisible(true); + }; + + +console.log('user', user) + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + {/* Settings header with title */} + + + {/* Scrollable settings content */} + + } + showsVerticalScrollIndicator={false} + > + {/* Profile card section */} + {user && ( + + + + {tempProfilePhoto ? ( + + ) : user.profile_photo_url ? ( + + ) : ( + + + {user.first_name.charAt(0)}{user.last_name.charAt(0)} + + + )} + + {/* Edit icon overlay */} + + + + + {/* Loading indicator */} + {uploadingPhoto && ( + + + + )} + + + + + {user.display_name || `${user.first_name} ${user.last_name}`} + + {user.email} + Radiologist + + + + )} + + {/* Settings sections */} + {settingsSections.map((section) => ( + + ))} + + {/* Bottom spacing for tab bar */} + + + + {/* Custom Modal */} + setModalVisible(false)} + /> + + ); +}; + +// ============================================================================ +// STYLES SECTION +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container for the settings screen + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Loading container for initial data loading + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: theme.colors.background, + }, + + // Loading text styling + loadingText: { + fontSize: theme.typography.fontSize.bodyLarge, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + }, + + // Scroll view styling + scrollView: { + flex: 1, + }, + + // Scroll content styling + scrollContent: { + paddingHorizontal: theme.spacing.md, + }, + + // Bottom spacing for tab bar + bottomSpacing: { + height: theme.spacing.xl, + }, + + // Profile card styles + profileCard: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.medium, + padding: theme.spacing.md, + marginBottom: theme.spacing.md, + ...theme.shadows.primary, + }, + + profileHeader: { + flexDirection: 'row', + alignItems: 'center', + }, + + profileImageContainer: { + marginRight: theme.spacing.md, + }, + + profileImage: { + width: 60, + height: 60, + borderRadius: 30, + backgroundColor:theme.colors.primary, + }, + + fallbackAvatar: { + width: 60, + height: 60, + borderRadius: 30, + backgroundColor: theme.colors.primary, + justifyContent: 'center', + alignItems: 'center', + }, + + fallbackText: { + color: theme.colors.background, + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + }, + + profileInfo: { + flex: 1, + }, + + profileName: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + profileEmail: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xs, + }, + + profileRole: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.primary, + }, + + // Edit icon overlay for profile photo update + editIconOverlay: { + position: 'absolute', + bottom: 0, + right: 0, + backgroundColor: theme.colors.primary, + borderRadius: 12, + width: 24, + height: 24, + justifyContent: 'center', + alignItems: 'center', + borderWidth: 2, + borderColor: theme.colors.background, + }, + + // Uploading overlay with loading indicator + uploadingOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0,0,0,0.5)', + borderRadius: 30, + }, + +}); + +/* + * End of File: SettingsScreen.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/modules/Settings/screens/index.ts b/app/modules/Settings/screens/index.ts new file mode 100644 index 0000000..70e21ff --- /dev/null +++ b/app/modules/Settings/screens/index.ts @@ -0,0 +1,17 @@ +/* + * File: index.ts + * Description: Export all screen components from Settings module + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export { SettingsScreen } from './SettingsScreen'; +export { AppInfoScreen } from './AppInfoScreen'; +export { EditProfileScreen } from './EditProfileScreen'; +export { ChangePasswordScreen } from './ChangePasswordScreen'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/navigation/MainTabNavigator.tsx b/app/navigation/MainTabNavigator.tsx new file mode 100644 index 0000000..acc9602 --- /dev/null +++ b/app/navigation/MainTabNavigator.tsx @@ -0,0 +1,151 @@ +/* + * File: MainTabNavigator.tsx + * Description: Bottom tab navigator for the main app interface + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { theme } from '../theme/theme'; +import { DashboardStackNavigator } from '../modules/Dashboard/navigation'; +import { SettingsStackNavigator } from '../modules/Settings/navigation'; +import { AIPredictionStackNavigator } from '../modules/AIPrediction/navigation'; +import { MainTabParamList } from './navigationTypes'; +import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; +import { ComingSoonScreen } from '../shared/components'; +import { PatientCareStackNavigator } from '../modules/PatientCare/navigation'; + +// Create the bottom tab navigator +const Tab = createBottomTabNavigator(); + +/** + * MainTabNavigator Component + * + * Purpose: Creates the bottom tab navigation for the main app interface + * + * Tab Structure: + * - Dashboard: Main ER dashboard with patient overview and statistics + * - Patients: Detailed patient list and management interface + * - AI Predictions: AI-powered medical case predictions and analysis + * - Reports: Medical reports and documentation access + * - Settings: User preferences and app configuration + * + * Features: + * - Consistent styling with app theme + * - Tab-specific icons and labels + * - Proper parameter passing to screens + * - Accessibility support + */ +export const MainTabNavigator: React.FC = () => { + return ( + + {/* Dashboard Tab - Main ER overview */} + ( + + ), + headerShown: false, + }} + /> + + {/* Patients Tab - Patient list and management */} + ( + + ), + headerShown: false, + }} + /> + + {/* AI Predictions Tab - AI-powered medical predictions */} + {/* ( + + ), + headerShown: false, + }} + /> */} + + {/* Reports Tab - Medical documentation */} + {/* ( + + ), + headerShown: false, + }} + /> */} + + {/* Settings Tab - User preferences */} + ( + + ), + headerShown: false, + }} + /> + + ); +}; + +/* + * End of File: MainTabNavigator.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/navigation/RootStackNavigator.tsx b/app/navigation/RootStackNavigator.tsx new file mode 100644 index 0000000..f63d0cd --- /dev/null +++ b/app/navigation/RootStackNavigator.tsx @@ -0,0 +1,80 @@ +/* + * File: RootStackNavigator.tsx + * Description: Root stack navigator managing authentication and main app flow + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; +import { AuthStackNavigator } from '../modules/Auth/navigation'; +import { MainTabNavigator } from './MainTabNavigator'; +import { RootStackParamList } from './navigationTypes'; +import { useAppSelector } from '../store/hooks'; +import { selectIsAuthenticated, selectIsOnboarded } from '../modules/Auth/redux/authSelectors'; +import ResetPasswordScreen from '../modules/Auth/screens/ResetPasswordScreen'; + +// Create the stack navigator +const Stack = createStackNavigator(); + +/** + * RootStackNavigatorProps Interface + * + * Purpose: Defines the props required by the RootStackNavigator component + * + * Props: + * - isAuthenticated: Boolean indicating if user is logged in + */ +interface RootStackNavigatorProps { + isAuthenticated: boolean; +} + +/** + * RootStackNavigator Component + * + * Purpose: Manages the main navigation flow between authentication, onboarding, and main app + * + * Navigation Flow: + * 1. App starts โ†’ Check authentication status + * 2. If not authenticated โ†’ Show AuthStackNavigator (LoginScreen) + * 3. If authenticated but not onboarded โ†’ Show ResetPasswordScreen + * 4. If authenticated and onboarded โ†’ Show MainTabNavigator (dashboard) + * + * Features: + * - Conditional rendering based on authentication and onboarding status + * - Seamless transition between auth, onboarding, and main app + * - Proper prop passing to child components + * - Hidden headers for custom styling + */ +export const RootStackNavigator: React.FC = ({ + isAuthenticated, +}) => { + // Get onboarding status from Redux + const isOnboarded = useAppSelector(selectIsOnboarded); + + return ( + + {/* Conditional rendering based on authentication and onboarding status */} + {!isAuthenticated ? ( + // Show auth stack if user is not authenticated + + ) : !isOnboarded ? ( + // Show reset password screen if user is authenticated but not onboarded + + ) : ( + // Show main app tabs if user is authenticated and onboarded + + )} + + ); +}; + +/* + * End of File: RootStackNavigator.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/navigation/index.ts b/app/navigation/index.ts new file mode 100644 index 0000000..6b39d90 --- /dev/null +++ b/app/navigation/index.ts @@ -0,0 +1,59 @@ +/* + * File: index.ts + * Description: Main navigation module exports + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// ============================================================================ +// NAVIGATION COMPONENTS +// ============================================================================ + +// Main navigation components +export { MainTabNavigator } from './MainTabNavigator'; +export { RootStackNavigator } from './RootStackNavigator'; + +// ============================================================================ +// NAVIGATION TYPES +// ============================================================================ + +// Type definitions for navigation +export type { + RootStackParamList, + MainTabParamList, + DashboardScreenParams, + PatientsScreenParams, + AlertsScreenParams, + ReportsScreenParams, + SettingsScreenParams, + NavigationProps, + TabNavigationProps, + NavigationRef, +} from './navigationTypes'; + +// ============================================================================ +// NAVIGATION UTILITIES +// ============================================================================ + +// Navigation utility functions +export { + navigationRef, + setNavigationRef, + navigateToScreen, + goBack, + resetNavigation, + navigateToDashboard, + navigateToPatientDetails, + navigateToAlerts, + navigateToReports, + navigateToSettings, + navigateToLogin, + navigateToMainApp, + handleNavigationError, +} from './navigationUtils'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/navigation/navigationTypes.ts b/app/navigation/navigationTypes.ts new file mode 100644 index 0000000..80c2cc6 --- /dev/null +++ b/app/navigation/navigationTypes.ts @@ -0,0 +1,190 @@ +/* + * File: navigationTypes.ts + * Description: TypeScript type definitions for navigation parameters and routes + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { NavigatorScreenParams } from '@react-navigation/native'; +import { ERDashboard, Patient, Alert as AlertType } from '../shared/types'; + +// ============================================================================ +// ROOT NAVIGATION TYPES +// ============================================================================ + +/** + * RootStackParamList + * + * Purpose: Defines the main navigation stack parameters for the entire app + * + * Routes: + * - Auth: Authentication stack navigator + * - Main: Main app with bottom tab navigation + */ +export type RootStackParamList = { + Auth: undefined; // Auth stack navigator handles its own screens + ResetPassword: undefined; // Reset password screen for onboarding + Main: NavigatorScreenParams; // Main app with tab navigation +}; + +// ============================================================================ +// MAIN TAB NAVIGATION TYPES +// ============================================================================ + +/** + * MainTabParamList + * + * Purpose: Defines the bottom tab navigation parameters + * + * Tabs: + * - Dashboard: ER dashboard with patient overview + * - Patients: Patient list and management + * - AIPredictions: AI-powered medical case predictions and analysis + * - Settings: User preferences and app configuration + */ +export type MainTabParamList = { + Dashboard: DashboardScreenParams; // Dashboard with initial data + Patients: PatientsScreenParams; // Patient list screen + AIPredictions: AIPredictionScreenParams; // AI predictions screen + Settings: SettingsScreenParams; // Settings screen +}; + +// ============================================================================ +// SCREEN PARAMETER TYPES +// ============================================================================ + +/** + * DashboardScreenParams + * + * Purpose: Parameters passed to the dashboard screen + * + * Parameters: + * - dashboard: ER dashboard data including statistics and shift info + * - patients: Array of patient data for display + * - alerts: Array of critical alerts and notifications + */ +export interface DashboardScreenParams { + dashboard: ERDashboard; + patients: Patient[]; + alerts: AlertType[]; +} + +/** + * PatientsScreenParams + * + * Purpose: Parameters for the patients screen + * + * Parameters: + * - filter: Optional filter to apply to patient list + * - searchQuery: Optional search term for patient search + */ +export interface PatientsScreenParams { + filter?: 'all' | 'critical' | 'active' | 'pending'; + searchQuery?: string; +} + +/** + * AIPredictionScreenParams + * + * Purpose: Parameters for the AI predictions screen + * + * Parameters: + * - filter: Optional filter for prediction types + * - urgency: Optional urgency level filter + * - searchQuery: Optional search term for predictions + */ +export interface AIPredictionScreenParams { + filter?: 'all' | 'critical' | 'urgent' | 'pending' | 'reviewed'; + urgency?: 'emergency' | 'urgent' | 'moderate' | 'low' | 'routine'; + searchQuery?: string; +} + +/** + * ReportsScreenParams + * + * Purpose: Parameters for the reports screen + * + * Parameters: + * - patientId: Optional patient ID to filter reports + * - dateRange: Optional date range for report filtering + */ +export interface ReportsScreenParams { + patientId?: string; + dateRange?: { + start: Date; + end: Date; + }; +} + +/** + * SettingsScreenParams + * + * Purpose: Parameters for the settings screen + * + * Parameters: + * - section: Optional section to navigate to within settings + */ +export interface SettingsScreenParams { + section?: 'profile' | 'notifications' | 'security' | 'about'; +} + +// ============================================================================ +// NAVIGATION PROPS TYPES +// ============================================================================ + +/** + * NavigationProps + * + * Purpose: Common navigation props used across components + * + * Properties: + * - navigation: Navigation object for screen navigation + * - route: Current route information and parameters + */ +export interface NavigationProps { + navigation: any; // Will be properly typed when navigation is set up + route: { + params: RootStackParamList[T]; + }; +} + +/** + * TabNavigationProps + * + * Purpose: Navigation props for tab screens + * + * Properties: + * - navigation: Tab navigation object + * - route: Current tab route information + */ +export interface TabNavigationProps { + navigation: any; // Will be properly typed when navigation is set up + route: { + params: MainTabParamList[T]; + }; +} + +// ============================================================================ +// NAVIGATION UTILITY TYPES +// ============================================================================ + +/** + * NavigationRef + * + * Purpose: Reference to the navigation object for programmatic navigation + * + * Usage: + * - Used for navigation from outside React components + * - Enables navigation from services, utilities, or event handlers + */ +export type NavigationRef = { + navigate: (name: keyof RootStackParamList, params?: any) => void; + goBack: () => void; + reset: (state: any) => void; +}; + +/* + * End of File: navigationTypes.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/navigation/navigationUtils.ts b/app/navigation/navigationUtils.ts new file mode 100644 index 0000000..b26ad02 --- /dev/null +++ b/app/navigation/navigationUtils.ts @@ -0,0 +1,263 @@ +/* + * File: navigationUtils.ts + * Description: Navigation utility functions for common navigation operations + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { NavigationRef } from './navigationTypes'; + +// ============================================================================ +// NAVIGATION REFERENCE +// ============================================================================ + +/** + * navigationRef + * + * Purpose: Global navigation reference for programmatic navigation + * + * Usage: + * - Set this reference in the main App component + * - Use for navigation from outside React components + * - Enables navigation from services, utilities, or event handlers + * + * Example: + * navigationRef.current?.navigate('Main'); + */ +export let navigationRef: React.RefObject | null = null; + +/** + * setNavigationRef + * + * Purpose: Set the global navigation reference + * + * @param ref - Navigation reference object + * + * Usage: + * Called from the main App component to set the navigation reference + */ +export const setNavigationRef = (ref: React.RefObject) => { + navigationRef = ref as React.RefObject; +}; + +// ============================================================================ +// NAVIGATION HELPER FUNCTIONS +// ============================================================================ + +/** + * navigateToScreen + * + * Purpose: Navigate to a specific screen with optional parameters + * + * @param screenName - Name of the screen to navigate to + * @param params - Optional parameters to pass to the screen + * + * Example: + * navigateToScreen('Main', { tab: 'Dashboard' }); + */ +export const navigateToScreen = (screenName: string, params?: any) => { + if (navigationRef?.current) { + navigationRef.current.navigate(screenName as any, params); + } +}; + +/** + * goBack + * + * Purpose: Navigate back to the previous screen + * + * Example: + * goBack(); + */ +export const goBack = () => { + if (navigationRef?.current) { + navigationRef.current.goBack(); + } +}; + +/** + * resetNavigation + * + * Purpose: Reset the navigation state to a specific screen + * + * @param screenName - Name of the screen to reset to + * @param params - Optional parameters for the screen + * + * Example: + * resetNavigation('Login'); // Reset to login screen + */ +export const resetNavigation = (screenName: string, params?: any) => { + if (navigationRef?.current) { + navigationRef.current.reset({ + index: 0, + routes: [{ name: screenName, params }], + }); + } +}; + +// ============================================================================ +// SCREEN-SPECIFIC NAVIGATION FUNCTIONS +// ============================================================================ + +/** + * navigateToDashboard + * + * Purpose: Navigate to the dashboard with specific data + * + * @param dashboard - Dashboard data to pass + * @param patients - Patient data to pass + * @param alerts - Alert data to pass + * + * Example: + * navigateToDashboard(dashboardData, patientList, alertList); + */ +export const navigateToDashboard = ( + dashboard: any, + patients: any[], + alerts: any[] +) => { + navigateToScreen('Main', { + screen: 'Dashboard', + params: { + dashboard, + patients, + alerts, + }, + }); +}; + +/** + * navigateToPatientDetails + * + * Purpose: Navigate to patient details screen + * + * @param patientId - ID of the patient to view + * + * Example: + * navigateToPatientDetails('patient123'); + */ +export const navigateToPatientDetails = (patientId: string) => { + navigateToScreen('Main', { + screen: 'Patients', + params: { + patientId, + }, + }); +}; + +/** + * navigateToAlerts + * + * Purpose: Navigate to alerts screen with optional filters + * + * @param priority - Optional priority filter + * @param unreadOnly - Optional flag for unread alerts only + * + * Example: + * navigateToAlerts('CRITICAL', true); + */ +export const navigateToAlerts = (priority?: string, unreadOnly?: boolean) => { + navigateToScreen('Main', { + screen: 'Alerts', + params: { + priority, + unreadOnly, + }, + }); +}; + +/** + * navigateToReports + * + * Purpose: Navigate to reports screen with optional filters + * + * @param patientId - Optional patient ID filter + * @param dateRange - Optional date range filter + * + * Example: + * navigateToReports('patient123', { start: new Date(), end: new Date() }); + */ +export const navigateToReports = (patientId?: string, dateRange?: any) => { + navigateToScreen('Main', { + screen: 'Reports', + params: { + patientId, + dateRange, + }, + }); +}; + +/** + * navigateToSettings + * + * Purpose: Navigate to settings screen with optional section + * + * @param section - Optional section to navigate to within settings + * + * Example: + * navigateToSettings('profile'); + */ +export const navigateToSettings = (section?: string) => { + navigateToScreen('Main', { + screen: 'Settings', + params: { + section, + }, + }); +}; + +// ============================================================================ +// AUTHENTICATION NAVIGATION FUNCTIONS +// ============================================================================ + +/** + * navigateToLogin + * + * Purpose: Navigate to login screen + * + * Example: + * navigateToLogin(); + */ +export const navigateToLogin = () => { + resetNavigation('Login'); +}; + +/** + * navigateToMainApp + * + * Purpose: Navigate to main app after successful authentication + * + * Example: + * navigateToMainApp(); + */ +export const navigateToMainApp = () => { + resetNavigation('Main'); +}; + +// ============================================================================ +// ERROR HANDLING +// ============================================================================ + +/** + * handleNavigationError + * + * Purpose: Handle navigation errors gracefully + * + * @param error - Navigation error object + * @param fallbackScreen - Fallback screen to navigate to + * + * Example: + * handleNavigationError(error, 'Login'); + */ +export const handleNavigationError = (error: any, fallbackScreen: string) => { + console.error('Navigation error:', error); + + // Navigate to fallback screen + resetNavigation(fallbackScreen); +}; + +/* + * End of File: navigationUtils.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/shared/components/ComingSoonScreen.tsx b/app/shared/components/ComingSoonScreen.tsx new file mode 100644 index 0000000..d3514d1 --- /dev/null +++ b/app/shared/components/ComingSoonScreen.tsx @@ -0,0 +1,378 @@ +/* + * File: ComingSoonScreen.tsx + * Description: Coming Soon screen component with SVG image placeholder + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + TouchableOpacity, + SafeAreaView, + StatusBar, + Image, +} from 'react-native'; +import { theme } from '../../theme/theme'; +import Icon from 'react-native-vector-icons/Feather'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +/** + * ComingSoonScreenProps Interface + * + * Purpose: Defines the props required by the ComingSoonScreen component + * + * Props: + * - title: Main title for the coming soon screen (optional) + * - subtitle: Subtitle or description text (optional) + * - onBack: Function to handle back navigation (optional) + * - showBackButton: Whether to show back button (default: true) + * - backgroundColor: Custom background color (optional) + * - textColor: Custom text color (optional) + * - imageComponent: Custom SVG/image component to render (optional) + */ +interface ComingSoonScreenProps { + title?: string; + subtitle?: string; + onBack?: () => void; + showBackButton?: boolean; + backgroundColor?: string; + textColor?: string; + imageComponent?: React.ReactNode; +} + +// ============================================================================ +// COMING SOON SCREEN COMPONENT +// ============================================================================ + +/** + * ComingSoonScreen Component + * + * Purpose: Displays a coming soon message with space for SVG image + * + * Features: + * 1. Customizable title and subtitle + * 2. Space allocated for SVG image + * 3. Optional back button with navigation + * 4. Customizable colors and styling + * 5. Responsive design for different screen sizes + * 6. Safe area handling + * 7. Status bar management + * 8. Scrollable content for smaller screens + * + * Usage: + * - For features under development + * - Placeholder screens during app development + * - Maintenance or update screens + * - Feature announcements + */ +export const ComingSoonScreen: React.FC = ({ + title = 'Coming Soon', + subtitle = 'We\'re working hard to bring you something amazing. Stay tuned for updates!', + onBack, + showBackButton = true, + backgroundColor, + textColor, + imageComponent, +}) => { + // ============================================================================ + // HELPER FUNCTIONS + // ============================================================================ + + /** + * handleBack Function + * + * Purpose: Handle back navigation if onBack function is provided + */ + const handleBack = () => { + if (onBack) { + onBack(); + } + }; + + // ============================================================================ + // RENDER SECTION + // ============================================================================ + + return ( + + + + + {/* Header with Back Button */} + {showBackButton && onBack && ( + + + + + + )} + + {/* Main Content */} + + {/* SVG Image Container */} + + {imageComponent ? ( + imageComponent + ) : ( + + + + )} + + + {/* Title */} + + {title} + + + {/* Subtitle */} + + {subtitle} + + + + {/* Action Buttons */} + + + + Get Notified + + + + + + Learn More + + + + + + + ); +}; + +// ============================================================================ +// STYLES SECTION +// ============================================================================ + +const styles = StyleSheet.create({ + // Main container + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Scroll view + scrollView: { + flex: 1, + }, + + // Scroll content + scrollContent: { + flexGrow: 1, + paddingHorizontal: theme.spacing.lg, + }, + + // Header section + header: { + flexDirection: 'row', + alignItems: 'center', + paddingTop: theme.spacing.lg, + paddingBottom: theme.spacing.md, + }, + + // Back button + backButton: { + padding: theme.spacing.sm, + borderRadius: theme.borderRadius.medium, + backgroundColor: theme.colors.backgroundAlt, + ...theme.shadows.small, + }, + + // Main content + content: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingVertical: theme.spacing.xxl, + }, + + // Image container + imageContainer: { + width: 280, + height: 280, + borderRadius: theme.borderRadius.large, + backgroundColor: theme.colors.backgroundAlt, + justifyContent: 'center', + alignItems: 'center', + marginBottom: theme.spacing.xl, + ...theme.shadows.medium, + }, + + // Image placeholder + imagePlaceholder: { + width: '100%', + height: '100%', + alignItems: 'center', + justifyContent: 'center', + }, + + // Placeholder text + placeholderText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginTop: theme.spacing.sm, + textAlign: 'center', + }, + + // Title + title: { + fontSize: theme.typography.fontSize.displayLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + textAlign: 'center', + marginBottom: theme.spacing.md, + lineHeight: theme.typography.fontSize.displayLarge * 1.2, + }, + + // Subtitle + subtitle: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + textAlign: 'center', + marginBottom: theme.spacing.xl, + lineHeight: theme.typography.fontSize.bodyLarge * 1.4, + paddingHorizontal: theme.spacing.md, + }, + + // Info container + infoContainer: { + marginBottom: theme.spacing.xl, + width: '100%', + }, + + // Info item + infoItem: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + marginBottom: theme.spacing.sm, + }, + + // Info text + infoText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textSecondary, + marginLeft: theme.spacing.sm, + }, + + // Button container + buttonContainer: { + width: '100%', + gap: theme.spacing.md, + }, + + // Base button + button: { + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + borderRadius: theme.borderRadius.medium, + alignItems: 'center', + justifyContent: 'center', + minHeight: 48, + ...theme.shadows.primary, + }, + + // Primary button + primaryButton: { + backgroundColor: theme.colors.primary, + }, + + // Secondary button + secondaryButton: { + backgroundColor: 'white', + borderWidth: 2, + borderColor: theme.colors.primary, + }, + + // Button text + buttonText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + }, + + // Secondary button text + secondaryButtonText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.primary, + }, +}); + +export default ComingSoonScreen; + +/* + * End of File: ComingSoonScreen.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/shared/components/CustomModal.tsx b/app/shared/components/CustomModal.tsx new file mode 100644 index 0000000..907af20 --- /dev/null +++ b/app/shared/components/CustomModal.tsx @@ -0,0 +1,348 @@ +/* + * File: CustomModal.tsx + * Description: Custom modal component with matching UI design + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Modal, + TouchableWithoutFeedback, + Dimensions, +} from 'react-native'; +import { theme } from '../../theme/theme'; +import Icon from 'react-native-vector-icons/Feather'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +/** + * CustomModalProps Interface + * + * Purpose: Defines the props required by the CustomModal component + * + * Props: + * - visible: Whether the modal is visible + * - title: Modal title + * - message: Modal message/content + * - type: Modal type (success, error, warning, info, confirm) + * - onConfirm: Callback for confirm action + * - onCancel: Callback for cancel action + * - confirmText: Text for confirm button + * - cancelText: Text for cancel button + * - onClose: Callback for closing modal + * - showCancel: Whether to show cancel button + * - icon: Custom icon name + */ +interface CustomModalProps { + visible: boolean; + title: string; + message: string; + type?: 'success' | 'error' | 'warning' | 'info' | 'confirm'; + onConfirm?: () => void; + onCancel?: () => void; + confirmText?: string; + cancelText?: string; + onClose?: () => void; + showCancel?: boolean; + icon?: string; +} + +// ============================================================================ +// CUSTOM MODAL COMPONENT +// ============================================================================ + +/** + * CustomModal Component + * + * Purpose: Displays a custom modal with consistent UI design + * + * Features: + * - Multiple modal types (success, error, warning, info, confirm) + * - Customizable buttons and text + * - Consistent theme styling + * - Backdrop tap to close + * - Icon support + * - Responsive design + */ +export const CustomModal: React.FC = ({ + visible, + title, + message, + type = 'info', + onConfirm, + onCancel, + confirmText = 'OK', + cancelText = 'Cancel', + onClose, + showCancel = false, + icon, +}) => { + // ============================================================================ + // MODAL CONFIGURATION + // ============================================================================ + + /** + * Get modal configuration based on type + */ + const getModalConfig = () => { + switch (type) { + case 'success': + return { + icon: icon || 'check-circle', + iconColor: theme.colors.success, + backgroundColor: theme.colors.background, + borderColor: theme.colors.success, + }; + case 'error': + return { + icon: icon || 'alert-circle', + iconColor: theme.colors.error, + backgroundColor: theme.colors.background, + borderColor: theme.colors.error, + }; + case 'warning': + return { + icon: icon || 'alert-triangle', + iconColor: theme.colors.warning, + backgroundColor: theme.colors.background, + borderColor: theme.colors.warning, + }; + case 'confirm': + return { + icon: icon || 'help-circle', + iconColor: theme.colors.primary, + backgroundColor: theme.colors.background, + borderColor: theme.colors.primary, + }; + default: + return { + icon: icon || 'info', + iconColor: theme.colors.info, + backgroundColor: theme.colors.background, + borderColor: theme.colors.info, + }; + } + }; + + const config = getModalConfig(); + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Handle confirm action + */ + const handleConfirm = () => { + if (onConfirm) { + onConfirm(); + } + if (onClose) { + onClose(); + } + }; + + /** + * Handle cancel action + */ + const handleCancel = () => { + if (onCancel) { + onCancel(); + } + if (onClose) { + onClose(); + } + }; + + /** + * Handle backdrop press + */ + const handleBackdropPress = () => { + if (onClose) { + onClose(); + } + }; + + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + + + + + + {/* Icon */} + + + + + {/* Title */} + {title} + + {/* Message */} + {message} + + {/* Buttons */} + + {showCancel && ( + + {cancelText} + + )} + + + {confirmText} + + + + + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const { width: screenWidth } = Dimensions.get('window'); + +const styles = StyleSheet.create({ + // Backdrop + backdrop: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: theme.spacing.lg, + }, + + // Modal container + modalContainer: { + width: screenWidth - theme.spacing.lg * 2, + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.large, + padding: theme.spacing.xl, + alignItems: 'center', + borderWidth: 2, + ...theme.shadows.large, + }, + + // Icon container + iconContainer: { + width: 80, + height: 80, + borderRadius: 40, + justifyContent: 'center', + alignItems: 'center', + marginBottom: theme.spacing.lg, + }, + + // Title + title: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + textAlign: 'center', + marginBottom: theme.spacing.md, + }, + + // Message + message: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + textAlign: 'center', + lineHeight: theme.typography.fontSize.bodyLarge * 1.4, + marginBottom: theme.spacing.xl, + }, + + // Button container + buttonContainer: { + flexDirection: 'row', + width: '100%', + gap: theme.spacing.md, + }, + + // Button base + button: { + flex: 1, + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + borderRadius: theme.borderRadius.medium, + alignItems: 'center', + justifyContent: 'center', + ...theme.shadows.primary, + }, + + // Cancel button + cancelButton: { + backgroundColor: theme.colors.backgroundAlt, + borderWidth: 1, + borderColor: theme.colors.border, + }, + + // Cancel button text + cancelButtonText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textSecondary, + }, + + // Confirm button + confirmButton: { + backgroundColor: theme.colors.primary, + }, + + // Confirm button with cancel + confirmButtonWithCancel: { + flex: 1, + }, + + // Confirm button text + confirmButtonText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + }, +}); + +/* + * End of File: CustomModal.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/shared/components/DICOM_VIEWER_README.md b/app/shared/components/DICOM_VIEWER_README.md new file mode 100644 index 0000000..7ca6e3a --- /dev/null +++ b/app/shared/components/DICOM_VIEWER_README.md @@ -0,0 +1,207 @@ +# DICOM Viewer Component + +## Overview +The DICOM Viewer component is a React Native component that uses WebView to display DICOM medical imaging files. It integrates with Cornerstone.js and Cornerstone WADO Image Loader for robust DICOM file handling. + +## Features +- โœ… WebView-based DICOM rendering +- โœ… Cornerstone.js integration for medical imaging +- โœ… Support for remote DICOM URLs +- โœ… Loading states and error handling +- โœ… Real-time communication with React Native +- โœ… Responsive design for mobile devices + +## Components + +### 1. DicomViewer +The main DICOM viewer component. + +**Props:** +```typescript +interface DicomViewerProps { + dicomUrl: string; // URL to the DICOM file + onError?: (error: string) => void; // Error callback + onLoad?: () => void; // Load success callback +} +``` + +**Usage:** +```typescript +import { DicomViewer } from '../shared/components'; + + console.error('DICOM Error:', error)} + onLoad={() => console.log('DICOM loaded successfully')} +/> +``` + +### 2. DicomViewerTest +A test component for testing different DICOM URLs and debugging issues. + +**Usage:** +```typescript +import { DicomViewerTest } from '../shared/components'; + + +``` + +## How It Works + +### 1. WebView Setup +- Loads a local HTML file (`dicom-viewer.html`) +- Enables JavaScript and DOM storage +- Allows file access and universal access from file URLs + +### 2. Library Loading +- Dynamically loads Cornerstone.js from CDN +- Loads Cornerstone WADO Image Loader +- Initializes the viewer when libraries are ready + +### 3. DICOM Processing +- Receives DICOM URL from React Native via postMessage +- Uses Cornerstone to load and display the DICOM image +- Handles errors and success states + +### 4. Communication +- Sends status messages back to React Native +- Reports loading, success, and error states +- Enables debugging and user feedback + +## Troubleshooting + +### Black Screen Issues + +#### 1. Check Console Logs +Open the React Native debugger and check for: +- WebView loading errors +- JavaScript execution errors +- Network request failures + +#### 2. Verify DICOM URL +- Ensure the URL is accessible from the device +- Check if the URL returns a valid DICOM file +- Verify CORS settings if loading from a different domain + +#### 3. Library Loading Issues +- Check internet connectivity (libraries load from CDN) +- Verify the HTML file path is correct +- Check WebView permissions and settings + +#### 4. Platform-Specific Issues + +**Android:** +- Ensure `allowFileAccess` is enabled +- Check if the HTML file is in the correct assets folder +- Verify WebView permissions in AndroidManifest.xml + +**iOS:** +- Check WebView configuration in Info.plist +- Ensure JavaScript is enabled +- Verify file access permissions + +### Common Error Messages + +#### "Failed to load DICOM viewer libraries" +- Check internet connectivity +- Verify CDN URLs are accessible +- Check WebView JavaScript settings + +#### "Failed to load DICOM image" +- Verify DICOM URL is accessible +- Check if the file is a valid DICOM format +- Ensure the server supports CORS + +#### "Invalid message received from app" +- Check the message format being sent +- Verify the postMessage implementation +- Check WebView message handling + +## Testing + +### 1. Use Sample URLs +The test component includes sample DICOM URLs that are known to work: +- Sample DICOM 1-3 from OHIF examples + +### 2. Test Custom URLs +- Enter your own DICOM URLs +- Test with different file formats +- Verify error handling + +### 3. Debug Mode +- Check console logs in React Native debugger +- Monitor WebView messages +- Use the test component for isolated testing + +## Performance Tips + +### 1. Image Optimization +- Use compressed DICOM files when possible +- Consider implementing progressive loading +- Cache frequently accessed images + +### 2. Memory Management +- Dispose of WebView when not in use +- Monitor memory usage with large DICOM files +- Implement proper cleanup in useEffect + +### 3. Network Optimization +- Use CDN for DICOM files when possible +- Implement retry logic for failed requests +- Consider offline caching for critical images + +## Security Considerations + +### 1. URL Validation +- Validate DICOM URLs before loading +- Implement URL whitelisting if needed +- Sanitize user input for custom URLs + +### 2. WebView Security +- Limit WebView permissions to minimum required +- Implement proper origin whitelisting +- Monitor for malicious content + +### 3. Data Privacy +- Ensure DICOM files don't contain PHI +- Implement proper data handling protocols +- Follow HIPAA compliance guidelines + +## Future Enhancements + +### 1. Offline Support +- Bundle Cornerstone libraries locally +- Implement offline DICOM caching +- Support for local DICOM files + +### 2. Advanced Features +- Multi-planar reconstruction (MPR) +- Measurement tools +- Annotation capabilities +- 3D rendering support + +### 3. Performance Improvements +- WebAssembly integration +- GPU acceleration +- Progressive image loading +- Background processing + +## Support + +For issues and questions: +1. Check this README for common solutions +2. Review console logs and error messages +3. Test with sample URLs first +4. Verify WebView configuration +5. Check platform-specific requirements + +## Dependencies + +- `react-native-webview`: WebView component +- `cornerstone-core`: Medical imaging library +- `cornerstone-wado-image-loader`: DICOM file loader + +## License + +Design & Developed by Tech4Biz Solutions +Copyright (c) Spurrin Innovations. All rights reserved. diff --git a/app/shared/components/DicomViewer.tsx b/app/shared/components/DicomViewer.tsx new file mode 100644 index 0000000..9d0580a --- /dev/null +++ b/app/shared/components/DicomViewer.tsx @@ -0,0 +1,320 @@ +/* + * File: DicomViewer.tsx + * Description: DICOM viewer component using WebView for medical imaging + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useRef, useEffect, useState } from 'react'; +import { WebView, WebViewMessageEvent } from 'react-native-webview'; +import { Platform, View, Text, StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native'; + +// Interface for component props +interface DicomViewerProps { + dicomUrl: string; + onError?: (error: string) => void; + onLoad?: () => void; + debugMode?: boolean; +} + +// Interface for WebView reference +interface WebViewRef { + postMessage: (message: string) => void; + reload: () => void; +} + +export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = false }: DicomViewerProps): React.ReactElement { + const webViewRef = useRef(null); + const [isLoading, setIsLoading] = useState(true); + const [hasError, setHasError] = useState(false); + const [debugInfo, setDebugInfo] = useState([]); + const [webViewReady, setWebViewReady] = useState(false); + + // Debug logging function + const debugLog = (message: string) => { + if (debugMode) { + const timestamp = new Date().toLocaleTimeString(); + const logMessage = `[${timestamp}] ${message}`; + console.log(logMessage); + setDebugInfo(prev => [...prev.slice(-9), logMessage]); // Keep last 10 messages + } + }; + + // Handle WebView load events + const handleLoadStart = () => { + debugLog('WebView load started'); + setIsLoading(true); + setHasError(false); + }; + + const handleLoadEnd = () => { + debugLog('WebView load ended'); + setIsLoading(false); + setWebViewReady(true); + onLoad?.(); + }; + + const handleError = (error: any) => { + debugLog(`WebView error: ${JSON.stringify(error)}`); + setIsLoading(false); + setHasError(true); + onError?.(error?.nativeEvent?.description || 'Failed to load DICOM viewer'); + }; + + const handleMessage = (event: WebViewMessageEvent) => { + try { + const message = event.nativeEvent.data; + debugLog(`Message from WebView: ${message}`); + + // Try to parse JSON message + if (typeof message === 'string') { + try { + const parsedMessage = JSON.parse(message); + debugLog(`Parsed message: ${JSON.stringify(parsedMessage)}`); + + if (parsedMessage.type === 'error') { + setHasError(true); + onError?.(parsedMessage.message); + } else if (parsedMessage.type === 'success') { + setHasError(false); + } + } catch (parseError) { + debugLog(`Failed to parse message as JSON: ${parseError}`); + } + } + } catch (error) { + debugLog(`Error handling WebView message: ${error}`); + } + }; + + // Send DICOM URL to WebView when component mounts or URL changes + useEffect(() => { + if (webViewRef.current && dicomUrl && webViewReady) { + debugLog(`Sending DICOM URL to WebView: ${dicomUrl}`); + + // Wait a bit for WebView to be ready + const timer = setTimeout(() => { + if (webViewRef.current) { + try { + // Send the URL directly as a string message + webViewRef.current.postMessage(dicomUrl); + debugLog('DICOM URL sent successfully'); + + // Also try sending as a structured message + setTimeout(() => { + if (webViewRef.current) { + const structuredMessage = JSON.stringify({ + type: 'loadDicom', + data: dicomUrl + }); + webViewRef.current.postMessage(structuredMessage); + debugLog('Structured DICOM message sent'); + } + }, 500); + + } catch (error) { + debugLog(`Failed to send DICOM URL: ${error}`); + } + } + }, 1000); + + return () => clearTimeout(timer); + } + }, [dicomUrl, webViewReady]); + + // Reload WebView if there's an error + const handleRetry = () => { + debugLog('Retrying WebView load'); + if (webViewRef.current) { + setHasError(false); + setIsLoading(true); + setWebViewReady(false); + webViewRef.current.reload(); + } + }; + + // Clear debug info + const clearDebugInfo = () => { + setDebugInfo([]); + }; + + return ( + + ( + + + Loading DICOM Viewer... + + )} + /> + + {hasError && ( + + Failed to load DICOM viewer + + URL: {dicomUrl} + + + Tap to retry + + + )} + + {debugMode && ( + + + Debug Info + + Clear + + + + {debugInfo.map((info, index) => ( + {info} + ))} + + + + WebView Ready: {webViewReady ? 'Yes' : 'No'} + + + Loading: {isLoading ? 'Yes' : 'No'} + + + Error: {hasError ? 'Yes' : 'No'} + + + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#000', + }, + webview: { + flex: 1, + backgroundColor: '#000', + }, + loadingContainer: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#000', + }, + loadingText: { + color: '#FFF', + marginTop: 16, + fontSize: 16, + }, + errorContainer: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#000', + padding: 20, + }, + errorText: { + color: '#F44336', + fontSize: 18, + textAlign: 'center', + marginBottom: 16, + fontWeight: '600', + }, + errorDetails: { + color: '#FF9800', + fontSize: 14, + textAlign: 'center', + marginBottom: 20, + fontFamily: 'monospace', + }, + retryButton: { + backgroundColor: '#2196F3', + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 8, + }, + retryButtonText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '600', + }, + debugContainer: { + position: 'absolute', + top: 10, + right: 10, + backgroundColor: 'rgba(0,0,0,0.9)', + borderRadius: 8, + padding: 10, + maxWidth: 300, + maxHeight: 400, + }, + debugHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, + }, + debugTitle: { + color: '#FFFFFF', + fontSize: 14, + fontWeight: '600', + }, + clearButton: { + color: '#2196F3', + fontSize: 12, + textDecorationLine: 'underline', + }, + debugContent: { + maxHeight: 200, + }, + debugText: { + color: '#FFFFFF', + fontSize: 10, + fontFamily: 'monospace', + marginBottom: 2, + }, + debugStatus: { + marginTop: 8, + paddingTop: 8, + borderTopColor: '#333', + borderTopWidth: 1, + }, + debugStatusText: { + color: '#CCC', + fontSize: 10, + marginBottom: 2, + }, +}); + +/* + * End of File: DicomViewer.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/shared/components/DicomViewerTest.tsx b/app/shared/components/DicomViewerTest.tsx new file mode 100644 index 0000000..c8c670d --- /dev/null +++ b/app/shared/components/DicomViewerTest.tsx @@ -0,0 +1,252 @@ +/* + * File: DicomViewerTest.tsx + * Description: Test component for DICOM viewer with sample URLs + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState } from 'react'; +import { View, Text, StyleSheet, TextInput, TouchableOpacity, ScrollView, Alert } from 'react-native'; +import DicomViewer from './DicomViewer'; + +// Sample DICOM URLs for testing +const SAMPLE_DICOM_URLS = [ + { + name: 'Sample DICOM 1', + url: 'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm' + }, + { + name: 'Sample DICOM 2', + url: 'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-002.dcm' + }, + { + name: 'Sample DICOM 3', + url: 'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-003.dcm' + } +]; + +export default function DicomViewerTest(): React.ReactElement { + const [dicomUrl, setDicomUrl] = useState(SAMPLE_DICOM_URLS[0].url); + const [customUrl, setCustomUrl] = useState(''); + + const handleUrlSelect = (url: string) => { + setDicomUrl(url); + setCustomUrl(url); + }; + + const handleCustomUrlSubmit = () => { + if (customUrl.trim()) { + setDicomUrl(customUrl.trim()); + } else { + Alert.alert('Error', 'Please enter a valid URL'); + } + }; + + const handleViewerError = (error: string) => { + console.error('DICOM Viewer Error:', error); + Alert.alert('DICOM Viewer Error', error); + }; + + const handleViewerLoad = () => { + console.log('DICOM Viewer loaded successfully'); + }; + + return ( + + + DICOM Viewer Test + Test different DICOM URLs + + + + Sample DICOM URLs: + {SAMPLE_DICOM_URLS.map((sample, index) => ( + handleUrlSelect(sample.url)} + > + + {sample.name} + + + {sample.url} + + + ))} + + + Custom DICOM URL: + + + + Load + + + + + + Current URL: + {dicomUrl} + + + + + DICOM Viewer: + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#F5F5F5', + }, + header: { + backgroundColor: '#2196F3', + padding: 20, + alignItems: 'center', + }, + title: { + fontSize: 24, + fontWeight: 'bold', + color: '#FFFFFF', + marginBottom: 8, + }, + subtitle: { + fontSize: 16, + color: '#E3F2FD', + }, + urlSection: { + flex: 1, + padding: 16, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '600', + color: '#212121', + marginBottom: 12, + marginTop: 16, + }, + urlButton: { + backgroundColor: '#FFFFFF', + padding: 16, + borderRadius: 8, + marginBottom: 8, + borderWidth: 1, + borderColor: '#E0E0E0', + }, + selectedUrlButton: { + backgroundColor: '#E3F2FD', + borderColor: '#2196F3', + }, + urlButtonText: { + fontSize: 16, + fontWeight: '600', + color: '#212121', + marginBottom: 4, + }, + selectedUrlButtonText: { + color: '#1976D2', + }, + urlText: { + fontSize: 12, + color: '#757575', + fontFamily: 'monospace', + }, + selectedUrlText: { + color: '#1976D2', + }, + customUrlSection: { + marginTop: 16, + }, + inputContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + textInput: { + flex: 1, + backgroundColor: '#FFFFFF', + borderWidth: 1, + borderColor: '#E0E0E0', + borderRadius: 8, + padding: 12, + fontSize: 14, + color: '#212121', + marginRight: 8, + }, + submitButton: { + backgroundColor: '#4CAF50', + paddingHorizontal: 20, + paddingVertical: 12, + borderRadius: 8, + }, + submitButtonText: { + color: '#FFFFFF', + fontSize: 14, + fontWeight: '600', + }, + currentUrlSection: { + marginTop: 16, + backgroundColor: '#FFFFFF', + padding: 16, + borderRadius: 8, + borderWidth: 1, + borderColor: '#E0E0E0', + }, + currentUrl: { + fontSize: 12, + color: '#757575', + fontFamily: 'monospace', + backgroundColor: '#F5F5F5', + padding: 8, + borderRadius: 4, + }, + viewerContainer: { + flex: 2, + backgroundColor: '#000000', + margin: 16, + borderRadius: 8, + overflow: 'hidden', + }, + viewerTitle: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '600', + padding: 12, + backgroundColor: '#333333', + }, +}); + +/* + * End of File: DicomViewerTest.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/shared/components/index.ts b/app/shared/components/index.ts new file mode 100644 index 0000000..c126ee9 --- /dev/null +++ b/app/shared/components/index.ts @@ -0,0 +1,35 @@ +/* + * File: index.ts + * Description: Shared components exports + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// ============================================================================ +// SHARED COMPONENTS EXPORTS +// ============================================================================ + +// Custom Modal Component +export { CustomModal } from './CustomModal'; + +// Coming Soon Screen Component +export { ComingSoonScreen } from './ComingSoonScreen'; + +// DICOM Viewer Component +export { default as DicomViewer } from './DicomViewer'; + +// DICOM Viewer Test Component +export { default as DicomViewerTest } from './DicomViewerTest'; + +// ============================================================================ +// TYPE EXPORTS +// ============================================================================ + +// Export types for components +// export type { CustomModalProps } from './CustomModal'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/shared/types/alerts.ts b/app/shared/types/alerts.ts new file mode 100644 index 0000000..b58ccdc --- /dev/null +++ b/app/shared/types/alerts.ts @@ -0,0 +1,115 @@ +/* + * File: alerts.ts + * Description: Alert and notification type definitions + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export interface Alert { + id: string; + type: AlertType; + priority: AlertPriority; + title: string; + message: string; + patientId?: string; + patientName?: string; + bedNumber?: string; + timestamp: Date; + isRead: boolean; + isAcknowledged: boolean; + actionRequired: boolean; + expiresAt?: Date; + metadata?: AlertMetadata; +} + +export type AlertType = + | 'CRITICAL_FINDING' + | 'VITAL_SIGNS_ALERT' + | 'SCAN_COMPLETED' + | 'MEDICATION_ALERT' + | 'BED_ASSIGNMENT' + | 'SHIFT_CHANGE' + | 'SYSTEM_ALERT' + | 'EMERGENCY_CODE'; + +export interface AlertMetadata { + scanId?: string; + vitalSigns?: { + type: string; + value: number; + normalRange: string; + }; + medication?: { + name: string; + dosage: string; + reason: string; + }; + location?: { + floor: string; + room: string; + bed: string; + }; + aiFindings?: { + confidence: number; + summary: string; + recommendations: string[]; + }; +} + +export interface CriticalFinding { + id: string; + patientId: string; + patientName: string; + bedNumber: string; + findingType: CriticalFindingType; + severity: 'LIFE_THREATENING' | 'URGENT' | 'SERIOUS'; + description: string; + aiSummary: string; + recommendations: string[]; + timestamp: Date; + acknowledgedBy?: string; + acknowledgedAt?: Date; + actionTaken?: string; + followUpRequired: boolean; +} + +export type CriticalFindingType = + | 'BRAIN_BLEED' + | 'PULMONARY_EMBOLISM' + | 'AORTIC_DISSECTION' + | 'APPENDICITIS' + | 'FRACTURE' + | 'INTERNAL_BLEEDING' + | 'CARDIAC_ARREST' + | 'STROKE' + | 'SEPSIS' + | 'OTHER'; + +export interface AlertSettings { + pushNotifications: boolean; + soundAlerts: boolean; + vibrationAlerts: boolean; + criticalAlertsOnly: boolean; + quietHours: { + enabled: boolean; + startTime: string; + endTime: string; + }; + alertTypes: { + [key in AlertType]: boolean; + }; +} + +export interface AlertResponse { + alertId: string; + response: 'ACKNOWLEDGED' | 'DISMISSED' | 'ESCALATED'; + notes?: string; + timestamp: Date; + userId: string; +} + +/* + * End of File: alerts.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/shared/types/auth.ts b/app/shared/types/auth.ts new file mode 100644 index 0000000..21db44d --- /dev/null +++ b/app/shared/types/auth.ts @@ -0,0 +1,94 @@ +/* + * File: auth.ts + * Description: Authentication and user-related type definitions + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export interface NotificationPreferences { + critical_alerts: { + email: boolean; + in_app: boolean; + push: boolean; + sms: boolean; + }; + system_notifications: { + push: boolean; + email: boolean; + in_app: boolean; + }; +} + +export interface DashboardSettings { + theme: string; + language: string; + timezone: string; +} + +export interface User { + user_id: string; + email: string; + first_name: string; + last_name: string; + display_name: string; + hospital_id: string; + dashboard_role: string; + profile_photo_url: string | null; + theme_color: string | null; + accent_color: string | null; + notification_preferences: NotificationPreferences; + dashboard_settings: DashboardSettings; + onboarded: boolean; + onboarding_completed: boolean; + onboarding_step: number; + onboarding_message: string; + access_token: string; + platform: 'app'|'web'; +} + +export type UserRole = + | 'ER_PHYSICIAN' + | 'RESIDENT' + | 'MEDICAL_STUDENT' + | 'EMERGENCY_ACCESS' + | 'TEMPORARY_ACCESS'; + +export interface AuthState { + user: User | null; + isAuthenticated: boolean; + isLoading: boolean; + error: string | null; + sessionExpiry: Date | null; + deviceId: string | null; +} + +export interface LoginCredentials { + email: string; + password: string; + rememberDevice?: boolean; +} + +export interface SSOLoginData { + token: string; + hospitalId: string; + redirectUrl?: string; +} + +export interface EmergencyAccess { + accessCode: string; + reason: string; + duration: number; // minutes +} + +export interface SessionData { + token: string; + refreshToken: string; + expiresAt: Date; + deviceId: string; +} + +/* + * End of File: auth.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/shared/types/common.ts b/app/shared/types/common.ts new file mode 100644 index 0000000..1964b87 --- /dev/null +++ b/app/shared/types/common.ts @@ -0,0 +1,92 @@ +/* + * File: common.ts + * Description: Common type definitions used across the application + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export interface ApiResponse { + success: boolean; + data?: T; + error?: string; + message?: string; + timestamp: Date; +} + +export interface PaginatedResponse { + data: T[]; + pagination: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +} + +export interface LoadingState { + isLoading: boolean; + error: string | null; +} + +export interface NavigationParams { + [key: string]: any; +} + +export interface ScreenProps { + navigation: any; + route: { + params: NavigationParams; + }; +} + +export interface FormField { + name: string; + label: string; + type: 'text' | 'email' | 'password' | 'number' | 'select' | 'date' | 'textarea'; + required: boolean; + validation?: (value: any) => string | null; + options?: { label: string; value: any }[]; + placeholder?: string; + defaultValue?: any; +} + +export interface ValidationError { + field: string; + message: string; +} + +export interface AppSettings { + theme: 'light' | 'dark' | 'auto'; + language: string; + notifications: boolean; + biometricAuth: boolean; + autoLogout: boolean; + offlineMode: boolean; +} + +export interface DeviceInfo { + id: string; + name: string; + platform: 'ios' | 'android'; + version: string; + model: string; + isTablet: boolean; +} + +export interface NetworkStatus { + isConnected: boolean; + type: 'wifi' | 'cellular' | 'none'; + strength?: number; +} + +export interface ErrorBoundaryState { + hasError: boolean; + error?: Error; + errorInfo?: any; +} + +/* + * End of File: common.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/shared/types/dashboard.ts b/app/shared/types/dashboard.ts new file mode 100644 index 0000000..66fab45 --- /dev/null +++ b/app/shared/types/dashboard.ts @@ -0,0 +1,83 @@ +/* + * File: dashboard.ts + * Description: Dashboard and ER-related type definitions + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { Patient, PatientStatus, AlertPriority } from './patient'; + +export interface ERDashboard { + totalPatients: number; + criticalPatients: number; + pendingScans: number; + recentReports: number; + bedOccupancy: number; + departmentStats: DepartmentStats; + shiftInfo: ShiftInfo; + lastUpdated: Date; +} + +export interface DepartmentStats { + emergency: number; + trauma: number; + cardiac: number; + neurology: number; + pediatrics: number; + icu: number; +} + +export interface ShiftInfo { + currentShift: 'DAY' | 'NIGHT' | 'EVENING'; + startTime: Date; + endTime: Date; + attendingPhysician: string; + residents: string[]; + nurses: string[]; +} + +export interface PatientList { + patients: Patient[]; + filters: PatientFilters; + sortBy: PatientSortBy; + isLoading: boolean; + error: string | null; +} + +export interface PatientFilters { + status: PatientStatus[]; + priority: AlertPriority[]; + department: string[]; + bedNumber?: string; + attendingPhysician?: string; +} + +export type PatientSortBy = + | 'PRIORITY' + | 'NAME' + | 'BED_NUMBER' + | 'ADMISSION_DATE' + | 'LAST_UPDATED'; + +export interface QuickAction { + id: string; + title: string; + icon: string; + action: () => void; + isEnabled: boolean; + requiresConfirmation?: boolean; +} + +export interface DashboardMetrics { + responseTime: number; // average response time in minutes + patientSatisfaction: number; // percentage + criticalAlertsResolved: number; + scansReviewed: number; + patientsDischarged: number; +} + +/* + * End of File: dashboard.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/shared/types/index.ts b/app/shared/types/index.ts new file mode 100644 index 0000000..5cc6930 --- /dev/null +++ b/app/shared/types/index.ts @@ -0,0 +1,37 @@ +/* + * File: index.ts + * Description: Shared type definitions exports for the Physician App + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export * from './auth'; +export * from './patient'; +export * from './dashboard'; +export * from './alerts'; +export * from './settings'; +export * from './common'; + +// AI Prediction types (re-export from module) +export type { + AIPrediction, + AIPredictionCase, + AIPredictionAPIResponse, + AIPredictionState, + AIPredictionStats, + AIPredictionFilters, + AIPredictionNavigationProps, +} from '../modules/AIPrediction/types'; + +// AI Prediction constants (re-export from module) +export { + URGENCY_COLORS, + SEVERITY_COLORS, + CATEGORY_COLORS, +} from '../modules/AIPrediction/types'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/shared/types/patient.ts b/app/shared/types/patient.ts new file mode 100644 index 0000000..7cef959 --- /dev/null +++ b/app/shared/types/patient.ts @@ -0,0 +1,161 @@ +/* + * File: patient.ts + * Description: Patient-related type definitions for medical data + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// DICOM Patient Details Interface +export interface PatientDetails { + Date: string; + Name: string; + PatID: string; + PatAge: string; + PatSex: string; + Status: string; + InstName: string; + Modality: 'DX' | 'CT' | 'MR'; + ReportStatus: string | null; +} + +// DICOM Series Interface +export interface Series { + Path: string[]; + SerDes: string; + ViePos: string | null; + pngpath: string; + SeriesNum: string; + ImgTotalinSeries: string; +} + +// Medical Case Interface (Main Patient Record) +export interface MedicalCase { + id: number; + patientdetails: PatientDetails | string; // Can be object or JSON string + series: Series[] | string; // Can be array or JSON string + created_at: string; + updated_at: string; + series_id: string | null; + type: 'Critical' | 'Routine' | 'Emergency'; +} + +// Legacy Patient interface for backward compatibility +export interface Patient extends MedicalCase {} + +export type PatientStatus = + | 'ACTIVE' + | 'PENDING' + | 'DISCHARGED' + | 'TRANSFERRED' + | 'CRITICAL'; + +export type AlertPriority = + | 'CRITICAL' + | 'HIGH' + | 'MEDIUM' + | 'LOW'; + +export interface VitalSigns { + bloodPressure: { + systolic: number; + diastolic: number; + timestamp: Date; + }; + heartRate: { + value: number; + timestamp: Date; + }; + temperature: { + value: number; + timestamp: Date; + }; + respiratoryRate: { + value: number; + timestamp: Date; + }; + oxygenSaturation: { + value: number; + timestamp: Date; + }; +} + +export interface Allergy { + id: string; + name: string; + severity: 'MILD' | 'MODERATE' | 'SEVERE'; + reaction: string; + notes?: string; +} + +export interface Medication { + id: string; + name: string; + dosage: string; + frequency: string; + route: string; + startDate: Date; + endDate?: Date; + status: 'ACTIVE' | 'DISCONTINUED' | 'COMPLETED'; + prescribedBy: string; +} + +export interface MedicalHistory { + id: string; + condition: string; + diagnosisDate: Date; + status: 'ACTIVE' | 'RESOLVED' | 'CHRONIC'; + notes?: string; + treatingPhysician: string; +} + +export interface ScanResult { + id: string; + patientId: string; + scanType: 'CT' | 'MRI' | 'XRAY' | 'ULTRASOUND'; + bodyPart: string; + status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'REVIEWED'; + orderedBy: string; + orderedDate: Date; + completedDate?: Date; + reviewedBy?: string; + reviewedDate?: Date; + findings: string; + aiSummary?: string; + images: string[]; + priority: AlertPriority; +} + +export interface PatientCareState { + // Patients data (Medical Cases) + patients: MedicalCase[]; + currentPatient: MedicalCase | null; + + // Loading states + isLoading: boolean; + isRefreshing: boolean; + isLoadingPatientDetails: boolean; + + // Error handling + error: string | null; + + // Search and filtering + searchQuery: string; + selectedFilter: 'all' | 'Critical' | 'Routine' | 'Emergency'; + sortBy: 'date' | 'name' | 'age'; + sortOrder: 'asc' | 'desc'; + + // Pagination + currentPage: number; + itemsPerPage: number; + totalItems: number; + + // Cache + lastUpdated: string | null; + cacheExpiry: string | null; +} + +/* + * End of File: patient.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/shared/types/settings.ts b/app/shared/types/settings.ts new file mode 100644 index 0000000..980762f --- /dev/null +++ b/app/shared/types/settings.ts @@ -0,0 +1,414 @@ +/* + * File: settings.ts + * Description: Settings and user profile type definitions + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// ============================================================================ +// USER PROFILE TYPES +// ============================================================================ + +/** + * UserProfile Interface + * + * Purpose: Defines the complete user profile structure + * + * Properties: + * - Personal information (name, contact, demographics) + * - Professional information (credentials, specialties) + * - Hospital information (department, role, permissions) + * - Preferences and settings + */ +export interface UserProfile { + id: string; + mrn: string; // Medical Record Number + employeeId: string; // Hospital Employee ID + + // Personal Information + firstName: string; + lastName: string; + middleName?: string; + email: string; + phoneNumber: string; + dateOfBirth: Date; + gender: 'MALE' | 'FEMALE' | 'OTHER'; + address: Address; + + // Professional Information + credentials: string[]; // MD, PhD, etc. + specialties: string[]; // Emergency Medicine, Trauma, etc. + boardCertifications: string[]; // Board certifications + licenseNumber: string; // Medical license number + licenseExpiryDate: Date; + yearsOfExperience: number; + + // Hospital Information + department: string; // Emergency Department, etc. + role: ProfileUserRole; // Attending, Resident, etc. + shiftPreference: 'DAY' | 'NIGHT' | 'FLEXIBLE'; + supervisor?: string; // Direct supervisor + permissions: UserPermissions; + + // Profile Settings + profilePicture?: string; // URL to profile picture + emergencyContact: EmergencyContact; + preferences: UserPreferences; + + // Metadata + createdAt: Date; + updatedAt: Date; + lastLoginAt: Date; + isActive: boolean; +} + +/** + * Address Interface + * + * Purpose: Defines address structure for user profile + */ +export interface Address { + street: string; + city: string; + state: string; + zipCode: string; + country: string; +} + +/** + * EmergencyContact Interface + * + * Purpose: Defines emergency contact information + */ +export interface EmergencyContact { + name: string; + relationship: string; + phoneNumber: string; + email?: string; + address?: Address; +} + +/** + * ProfileUserRole Type + * + * Purpose: Defines different user roles in the hospital system for profile settings + */ +export type ProfileUserRole = + | 'ATTENDING_PHYSICIAN' + | 'RESIDENT_PHYSICIAN' + | 'FELLOW' + | 'MEDICAL_STUDENT' + | 'NURSE_PRACTITIONER' + | 'PHYSICIAN_ASSISTANT' + | 'ADMINISTRATOR' + | 'RESEARCHER'; + +/** + * UserPermissions Interface + * + * Purpose: Defines user permissions and access levels + */ +export interface UserPermissions { + canViewPatients: boolean; + canEditPatients: boolean; + canViewReports: boolean; + canEditReports: boolean; + canViewAlerts: boolean; + canAcknowledgeAlerts: boolean; + canManageUsers: boolean; + canViewAnalytics: boolean; + canExportData: boolean; + canAccessAdminPanel: boolean; +} + +// ============================================================================ +// USER PREFERENCES TYPES +// ============================================================================ + +/** + * UserPreferences Interface + * + * Purpose: Defines user preferences and settings + */ +export interface UserPreferences { + // Display Preferences + theme: 'LIGHT' | 'DARK' | 'AUTO'; + fontSize: 'SMALL' | 'MEDIUM' | 'LARGE'; + language: string; // ISO language code + timezone: string; // IANA timezone + + // Notification Preferences + notifications: NotificationPreferences; + + // Clinical Preferences + clinical: ClinicalPreferences; + + // Privacy Preferences + privacy: PrivacyPreferences; + + // Accessibility Preferences + accessibility: AccessibilityPreferences; +} + +/** + * NotificationPreferences Interface + * + * Purpose: Defines notification settings + */ +export interface NotificationPreferences { + pushNotifications: boolean; + emailNotifications: boolean; + smsNotifications: boolean; + + // Alert Types + criticalAlerts: boolean; + patientUpdates: boolean; + scanResults: boolean; + medicationReminders: boolean; + shiftChanges: boolean; + systemMaintenance: boolean; + + // Timing + quietHours: { + enabled: boolean; + startTime: string; // HH:MM format + endTime: string; // HH:MM format + }; + + // Sound and Vibration + soundEnabled: boolean; + vibrationEnabled: boolean; + customSound?: string; // Custom notification sound +} + +/** + * ClinicalPreferences Interface + * + * Purpose: Defines clinical workflow preferences + */ +export interface ClinicalPreferences { + // Default Views + defaultDashboardView: 'LIST' | 'GRID' | 'TIMELINE'; + defaultPatientSort: 'PRIORITY' | 'NAME' | 'ADMISSION_TIME' | 'BED_NUMBER'; + showPatientPhotos: boolean; + + // Clinical Alerts + autoAcknowledgeNonCritical: boolean; + alertTimeoutMinutes: number; // Auto-dismiss after X minutes + + // Documentation + autoSaveInterval: number; // Seconds + defaultReportTemplate: string; + enableVoiceNotes: boolean; + + // Patient Care + showAllergiesProminently: boolean; + showMedicationInteractions: boolean; + enableClinicalDecisionSupport: boolean; +} + +/** + * PrivacyPreferences Interface + * + * Purpose: Defines privacy and security settings + */ +export interface PrivacyPreferences { + // Session Management + sessionTimeoutMinutes: number; + requireBiometricAuth: boolean; + autoLockOnBackground: boolean; + + // Data Sharing + allowAnalytics: boolean; + allowCrashReporting: boolean; + allowUsageStatistics: boolean; + + // Privacy Controls + showPatientPhotos: boolean; + enableScreenCapture: boolean; + enableScreenshot: boolean; +} + +/** + * AccessibilityPreferences Interface + * + * Purpose: Defines accessibility settings + */ +export interface AccessibilityPreferences { + // Visual + highContrastMode: boolean; + reduceMotion: boolean; + boldText: boolean; + + // Audio + screenReaderEnabled: boolean; + audioDescriptions: boolean; + + // Interaction + largerTouchTargets: boolean; + reduceTransparency: boolean; +} + +// ============================================================================ +// SETTINGS SCREEN TYPES +// ============================================================================ + +/** + * SettingsSection Type + * + * Purpose: Defines different sections in the settings screen + */ +export type SettingsSection = + | 'PROFILE' + | 'NOTIFICATIONS' + | 'CLINICAL' + | 'PRIVACY' + | 'ACCESSIBILITY' + | 'ABOUT' + | 'HELP' + | 'LOGOUT'; + +/** + * SettingsItem Interface + * + * Purpose: Defines a single settings item + */ +export interface SettingsItem { + id: string; + title: string; + subtitle?: string; + icon: string; + type: 'NAVIGATION' | 'TOGGLE' | 'SELECTION' | 'ACTION'; + value?: any; + onPress?: () => void; + disabled?: boolean; + badge?: string; +} + +/** + * SettingsSection Interface + * + * Purpose: Defines a settings section with items + */ +export interface SettingsSectionData { + id: SettingsSection; + title: string; + items: SettingsItem[]; +} + +// ============================================================================ +// PROFILE UPDATE TYPES +// ============================================================================ + +/** + * ProfileUpdateData Interface + * + * Purpose: Defines data structure for profile updates + */ +export interface ProfileUpdateData { + firstName?: string; + lastName?: string; + middleName?: string; + email?: string; + phoneNumber?: string; + address?: Partial
; + emergencyContact?: Partial; + specialties?: string[]; + boardCertifications?: string[]; + shiftPreference?: 'DAY' | 'NIGHT' | 'FLEXIBLE'; +} + +/** + * ProfileUpdateRequest Interface + * + * Purpose: Defines API request for profile updates + */ +export interface ProfileUpdateRequest { + userId: string; + updates: ProfileUpdateData; + reason?: string; // Reason for update +} + +/** + * ProfileUpdateResponse Interface + * + * Purpose: Defines API response for profile updates + */ +export interface ProfileUpdateResponse { + success: boolean; + message: string; + updatedProfile?: UserProfile; + errors?: string[]; +} + +// ============================================================================ +// PASSWORD AND SECURITY TYPES +// ============================================================================ + +/** + * PasswordChangeRequest Interface + * + * Purpose: Defines password change request + */ +export interface PasswordChangeRequest { + currentPassword: string; + newPassword: string; + confirmPassword: string; +} + +/** + * SecuritySettings Interface + * + * Purpose: Defines security-related settings + */ +export interface SecuritySettings { + twoFactorEnabled: boolean; + biometricEnabled: boolean; + passwordExpiryDays: number; + failedLoginAttempts: number; + lastPasswordChange: Date; + securityQuestions: SecurityQuestion[]; +} + +/** + * SecurityQuestion Interface + * + * Purpose: Defines security question structure + */ +export interface SecurityQuestion { + id: string; + question: string; + answer: string; // Hashed answer +} + +// ============================================================================ +// EXPORT TYPES +// ============================================================================ + +export type { + UserProfile, + Address, + EmergencyContact, + ProfileUserRole, + UserPermissions, + UserPreferences, + NotificationPreferences, + ClinicalPreferences, + PrivacyPreferences, + AccessibilityPreferences, + SettingsSection, + SettingsItem, + SettingsSectionData, + ProfileUpdateData, + ProfileUpdateRequest, + ProfileUpdateResponse, + PasswordChangeRequest, + SecuritySettings, + SecurityQuestion, +}; + +/* + * End of File: settings.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/shared/utils/api.ts b/app/shared/utils/api.ts new file mode 100644 index 0000000..29ec5d5 --- /dev/null +++ b/app/shared/utils/api.ts @@ -0,0 +1,13 @@ +interface BuildHeadersParams { + token?: string; + contentType?: string; +} + +export const buildHeaders = ({ token, contentType }: BuildHeadersParams = {}) => { + const headers: Record = {}; + + if (token) headers['Authorization'] = `Bearer ${token}`; + if (contentType) headers['Content-Type'] = contentType; + + return { headers }; +}; diff --git a/app/shared/utils/constants.ts b/app/shared/utils/constants.ts new file mode 100644 index 0000000..12c6f79 --- /dev/null +++ b/app/shared/utils/constants.ts @@ -0,0 +1,96 @@ +/* + * File: constants.ts + * Description: Application constants and configuration values + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ +import Config from 'react-native-config'; + +// API Configuration +export const API_CONFIG = { + BASE_URL:Config.BASE_URL, + DICOM_BASE_URL:'https://demo.medpacsystems.com', + TIMEOUT: 30000, + RETRY_ATTEMPTS: 3, + RETRY_DELAY: 1000, +} as const; + +// WebSocket Configuration +export const WEBSOCKET_CONFIG = { + URL: 'wss://ws.neoscan-physician.com', + RECONNECT_INTERVAL: 5000, + MAX_RECONNECT_ATTEMPTS: 10, + HEARTBEAT_INTERVAL: 30000, +} as const; + +// Session Configuration +export const SESSION_CONFIG = { + TIMEOUT: 8 * 60 * 60 * 1000, // 8 hours + INACTIVITY_TIMEOUT: 30 * 60 * 1000, // 30 minutes + DEVICE_REMEMBER_DURATION: 30 * 24 * 60 * 60 * 1000, // 30 days +} as const; + +// Alert Configuration +export const ALERT_CONFIG = { + CRITICAL_TIMEOUT: 2 * 60 * 1000, // 2 minutes + WARNING_TIMEOUT: 10 * 60 * 1000, // 10 minutes + INFO_TIMEOUT: 30 * 60 * 1000, // 30 minutes + MAX_ALERTS: 100, +} as const; + +// Cache Configuration +export const CACHE_CONFIG = { + PATIENT_DATA: 15 * 60 * 1000, // 15 minutes + MEDICAL_RECORDS: 5 * 60 * 1000, // 5 minutes + USER_SETTINGS: 24 * 60 * 60 * 1000, // 24 hours +} as const; + +// UI Configuration +export const UI_CONFIG = { + ANIMATION_DURATION: 300, + DEBOUNCE_DELAY: 300, + THROTTLE_DELAY: 100, + TOUCH_TARGET_SIZE: 44, +} as const; + +// Medical Constants +export const MEDICAL_CONSTANTS = { + NORMAL_VITAL_SIGNS: { + HEART_RATE: { min: 60, max: 100 }, + BLOOD_PRESSURE: { systolic: { min: 90, max: 140 }, diastolic: { min: 60, max: 90 } }, + TEMPERATURE: { min: 36.1, max: 37.2 }, + RESPIRATORY_RATE: { min: 12, max: 20 }, + OXYGEN_SATURATION: { min: 95, max: 100 }, + }, + CRITICAL_VALUES: { + HEART_RATE: { min: 40, max: 140 }, + BLOOD_PRESSURE: { systolic: { min: 70, max: 200 }, diastolic: { min: 40, max: 120 } }, + TEMPERATURE: { min: 35.0, max: 40.0 }, + RESPIRATORY_RATE: { min: 8, max: 30 }, + OXYGEN_SATURATION: { min: 90, max: 100 }, + }, +} as const; + +// Error Messages +export const ERROR_MESSAGES = { + NETWORK_ERROR: 'Network connection error. Please check your internet connection.', + AUTHENTICATION_ERROR: 'Authentication failed. Please log in again.', + SERVER_ERROR: 'Server error. Please try again later.', + VALIDATION_ERROR: 'Please check your input and try again.', + UNKNOWN_ERROR: 'An unexpected error occurred. Please try again.', +} as const; + +// Success Messages +export const SUCCESS_MESSAGES = { + LOGIN_SUCCESS: 'Successfully logged in.', + LOGOUT_SUCCESS: 'Successfully logged out.', + DATA_SAVED: 'Data saved successfully.', + ALERT_ACKNOWLEDGED: 'Alert acknowledged.', + SETTINGS_UPDATED: 'Settings updated successfully.', +} as const; + +/* + * End of File: constants.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/shared/utils/fileUpload.ts b/app/shared/utils/fileUpload.ts new file mode 100644 index 0000000..20e100d --- /dev/null +++ b/app/shared/utils/fileUpload.ts @@ -0,0 +1,228 @@ +/* + * File: fileUpload.ts + * Description: Utility functions for handling file uploads in React Native + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// ============================================================================ +// INTERFACES +// ============================================================================ + +/** + * FileData Interface + * + * Purpose: Defines the structure for file data from image picker + */ +export interface FileData { + uri: string; + name?: string; + type?: string; + size?: number; +} + +/** + * UploadFile Interface + * + * Purpose: Defines the structure for file object in FormData + */ +export interface UploadFile { + uri: string; + name: string; + type: string; +} + +// ============================================================================ +// FILE UPLOAD UTILITIES +// ============================================================================ + +/** + * Prepare File for Upload + * + * Purpose: Convert FileData to proper UploadFile structure for FormData + * + * @param fileData - File data from image picker + * @param fieldName - Form field name for the file + * @returns Properly formatted UploadFile object + */ +export const prepareFileForUpload = ( + fileData: FileData, + fieldName: string = 'file' +): UploadFile => { + // Extract file extension from URI + const uri = fileData.uri; + const fileName = fileData.name || `${fieldName}_${Date.now()}`; + + // Determine file extension and MIME type + let fileExtension = 'jpg'; + let mimeType = 'image/jpeg'; + + if (fileData.type) { + // Use provided type + mimeType = fileData.type; + fileExtension = mimeType.split('/')[1] || 'jpg'; + } else { + // Extract from URI + const uriParts = uri.split('.'); + if (uriParts.length > 1) { + fileExtension = uriParts[uriParts.length - 1].toLowerCase(); + + // Map extension to MIME type + switch (fileExtension) { + case 'png': + mimeType = 'image/png'; + break; + case 'jpeg': + case 'jpg': + mimeType = 'image/jpeg'; + break; + case 'gif': + mimeType = 'image/gif'; + break; + case 'webp': + mimeType = 'image/webp'; + break; + default: + mimeType = 'image/jpeg'; + fileExtension = 'jpg'; + } + } + } + + // Ensure filename has proper extension + const finalFileName = fileName.includes('.') + ? fileName + : `${fileName}.${fileExtension}`; + + return { + uri, + name: finalFileName, + type: mimeType, + }; +}; + +/** + * Create FormData with File + * + * Purpose: Create FormData object with proper file attachment + * + * @param data - Object containing form fields + * @param fileData - File data to upload + * @param fileFieldName - Form field name for the file + * @returns FormData object ready for upload + */ +export const createFormDataWithFile = ( + data: Record, + fileData?: FileData, + fileFieldName: string = 'file' +): FormData => { + const formData = new FormData(); + + // Add regular form fields + Object.keys(data).forEach(key => { + if (data[key] !== null && data[key] !== undefined) { + formData.append(key, String(data[key])); + } + }); + + // Add file if provided + if (fileData) { + const uploadFile = prepareFileForUpload(fileData, fileFieldName); + formData.append(fileFieldName, uploadFile as any); + } + + return formData; +}; + +/** + * Validate File Type + * + * Purpose: Check if file type is allowed + * + * @param fileData - File data to validate + * @param allowedTypes - Array of allowed MIME types + * @returns Boolean indicating if file type is allowed + */ +export const validateFileType = ( + fileData: FileData, + allowedTypes: string[] = ['image/jpeg', 'image/jpg', 'image/png'] +): boolean => { + if (!fileData.type) { + // If no type provided, try to determine from URI + const uri = fileData.uri.toLowerCase(); + if (uri.includes('.jpg') || uri.includes('.jpeg')) { + return allowedTypes.includes('image/jpeg') || allowedTypes.includes('image/jpg'); + } + if (uri.includes('.png')) { + return allowedTypes.includes('image/png'); + } + return false; + } + + return allowedTypes.includes(fileData.type.toLowerCase()); +}; + +/** + * Validate File Size + * + * Purpose: Check if file size is within limits + * + * @param fileData - File data to validate + * @param maxSizeMB - Maximum file size in MB + * @returns Boolean indicating if file size is acceptable + */ +export const validateFileSize = ( + fileData: FileData, + maxSizeMB: number = 10 +): boolean => { + if (!fileData.size) { + return true; // Skip validation if size not available + } + + const maxSizeBytes = maxSizeMB * 1024 * 1024; + return fileData.size <= maxSizeBytes; +}; + +/** + * Format File Size + * + * Purpose: Convert bytes to human readable format + * + * @param bytes - File size in bytes + * @returns Formatted file size string + */ +export 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]; +}; + +/** + * Get File Type Display Name + * + * Purpose: Get user-friendly display name for file type + * + * @param mimeType - MIME type string + * @returns Display name for file type + */ +export const getFileTypeDisplay = (mimeType: string): string => { + const typeMap: Record = { + 'image/jpeg': 'JPEG', + 'image/jpg': 'JPEG', + 'image/png': 'PNG', + 'image/gif': 'GIF', + 'image/webp': 'WebP', + }; + + return typeMap[mimeType.toLowerCase()] || 'Image'; +}; + +/* + * End of File: fileUpload.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/shared/utils/helpers.ts b/app/shared/utils/helpers.ts new file mode 100644 index 0000000..7003f61 --- /dev/null +++ b/app/shared/utils/helpers.ts @@ -0,0 +1,524 @@ +/* + * File: helpers.ts + * Description: General helper functions for common operations + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { AlertPriority, PatientStatus } from '../types'; + +// ============================================================================ +// ARRAY HELPERS - Functions for array manipulation and processing +// ============================================================================ + +/** + * chunk Function + * + * Purpose: Split an array into smaller chunks of specified size + * + * @param array - The array to be chunked + * @param size - The size of each chunk + * @returns Array of arrays, each containing up to 'size' elements + * + * Example: + * chunk([1, 2, 3, 4, 5, 6], 2) โ†’ [[1, 2], [3, 4], [5, 6]] + * + * Use case: Pagination, displaying data in groups + */ +export const chunk = (array: T[], size: number): T[][] => { + const chunks: T[][] = []; + for (let i = 0; i < array.length; i += size) { + chunks.push(array.slice(i, i + size)); + } + return chunks; +}; + +/** + * groupBy Function + * + * Purpose: Group array elements by a specified key function + * + * @param array - The array to be grouped + * @param key - Function that returns the grouping key for each element + * @returns Object with keys as group names and values as arrays of grouped items + * + * Example: + * groupBy(patients, patient => patient.department) + * โ†’ { 'Emergency': [patient1, patient2], 'ICU': [patient3] } + * + * Use case: Organizing patients by department, status, or priority + */ +export const groupBy = ( + array: T[], + key: (item: T) => K +): Record => { + return array.reduce((groups, item) => { + const group = key(item); + if (!groups[group]) { + groups[group] = []; + } + groups[group].push(item); + return groups; + }, {} as Record); +}; + +// ============================================================================ +// OBJECT HELPERS - Functions for object manipulation and transformation +// ============================================================================ + +/** + * pick Function + * + * Purpose: Create a new object with only specified properties from the original + * + * @param obj - The source object + * @param keys - Array of property names to include + * @returns New object containing only the specified properties + * + * Example: + * pick({ name: 'John', age: 30, id: 1 }, ['name', 'age']) + * โ†’ { name: 'John', age: 30 } + * + * Use case: Extracting specific patient data fields + */ +export const pick = (obj: T, keys: K[]): Pick => { + const result = {} as Pick; + keys.forEach(key => { + if (key in obj) { + result[key] = obj[key]; + } + }); + return result; +}; + +/** + * omit Function + * + * Purpose: Create a new object excluding specified properties from the original + * + * @param obj - The source object + * @param keys - Array of property names to exclude + * @returns New object without the specified properties + * + * Example: + * omit({ name: 'John', age: 30, id: 1 }, ['id']) + * โ†’ { name: 'John', age: 30 } + * + * Use case: Removing sensitive data before sending to API + */ +export const omit = (obj: T, keys: K[]): Omit => { + const result = { ...obj }; + keys.forEach(key => { + delete result[key]; + }); + return result; +}; + +// ============================================================================ +// STRING HELPERS - Functions for string manipulation and formatting +// ============================================================================ + +/** + * capitalize Function + * + * Purpose: Capitalize the first letter of a string and lowercase the rest + * + * @param str - The string to capitalize + * @returns Capitalized string + * + * Example: + * capitalize('john doe') โ†’ 'John doe' + * + * Use case: Formatting patient names, department names + */ +export const capitalize = (str: string): string => { + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); +}; + +/** + * truncate Function + * + * Purpose: Truncate a string to specified length and add ellipsis + * + * @param str - The string to truncate + * @param length - Maximum length before truncation + * @returns Truncated string with ellipsis if needed + * + * Example: + * truncate('Very long diagnosis text', 15) โ†’ 'Very long diagn...' + * + * Use case: Displaying long text in limited space (diagnoses, notes) + */ +export const truncate = (str: string, length: number): string => { + if (str.length <= length) return str; + return str.slice(0, length) + '...'; +}; + +// ============================================================================ +// NUMBER HELPERS - Functions for number manipulation and validation +// ============================================================================ + +/** + * clamp Function + * + * Purpose: Constrain a number between minimum and maximum values + * + * @param value - The number to clamp + * @param min - Minimum allowed value + * @param max - Maximum allowed value + * @returns Clamped value within the specified range + * + * Example: + * clamp(150, 0, 100) โ†’ 100 + * clamp(-10, 0, 100) โ†’ 0 + * + * Use case: Validating vital signs, age ranges, scores + */ +export const clamp = (value: number, min: number, max: number): number => { + return Math.min(Math.max(value, min), max); +}; + +/** + * roundToDecimal Function + * + * Purpose: Round a number to specified decimal places + * + * @param value - The number to round + * @param decimals - Number of decimal places + * @returns Rounded number + * + * Example: + * roundToDecimal(3.14159, 2) โ†’ 3.14 + * + * Use case: Formatting vital signs, medication dosages, scores + */ +export const roundToDecimal = (value: number, decimals: number): number => { + return Math.round(value * Math.pow(10, decimals)) / Math.pow(10, decimals); +}; + +// ============================================================================ +// BOOLEAN HELPERS - Functions for boolean operations and validation +// ============================================================================ + +/** + * isTruthy Function + * + * Purpose: Check if a value is truthy (converts to true) + * + * @param value - The value to check + * @returns Boolean indicating if value is truthy + * + * Example: + * isTruthy('hello') โ†’ true + * isTruthy(0) โ†’ false + * + * Use case: Validating form inputs, checking optional fields + */ +export const isTruthy = (value: any): boolean => { + return Boolean(value); +}; + +/** + * isFalsy Function + * + * Purpose: Check if a value is falsy (converts to false) + * + * @param value - The value to check + * @returns Boolean indicating if value is falsy + * + * Example: + * isFalsy('') โ†’ true + * isFalsy(null) โ†’ true + * + * Use case: Checking for empty or null values + */ +export const isFalsy = (value: any): boolean => { + return !Boolean(value); +}; + +// ============================================================================ +// FUNCTION HELPERS - Functions for function manipulation and optimization +// ============================================================================ + +/** + * debounce Function + * + * Purpose: Create a debounced version of a function that delays execution + * + * @param func - The function to debounce + * @param delay - Delay in milliseconds + * @returns Debounced function + * + * Example: + * const debouncedSearch = debounce(searchPatients, 300); + * // Only executes search after 300ms of no input + * + * Use case: Search inputs, API calls, form validation + */ +export const debounce = any>( + func: T, + delay: number +): ((...args: Parameters) => void) => { + let timeoutId: NodeJS.Timeout; + return (...args: Parameters) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => func(...args), delay); + }; +}; + +/** + * throttle Function + * + * Purpose: Create a throttled version of a function that limits execution frequency + * + * @param func - The function to throttle + * @param delay - Minimum time between executions in milliseconds + * @returns Throttled function + * + * Example: + * const throttledScroll = throttle(handleScroll, 100); + * // Only executes once every 100ms maximum + * + * Use case: Scroll events, resize events, real-time updates + */ +export const throttle = any>( + func: T, + delay: number +): ((...args: Parameters) => void) => { + let lastCall = 0; + return (...args: Parameters) => { + const now = Date.now(); + if (now - lastCall >= delay) { + lastCall = now; + func(...args); + } + }; +}; + +// ============================================================================ +// MEDICAL HELPERS - Healthcare-specific utility functions +// ============================================================================ + +/** + * getPriorityColor Function + * + * Purpose: Get the appropriate color for alert priority levels + * + * @param priority - The alert priority level + * @returns Hex color code for the priority + * + * Color Mapping: + * - CRITICAL: Red (#F44336) - Immediate attention required + * - HIGH: Orange (#FF9800) - High priority attention + * - MEDIUM: Blue (#2196F3) - Normal priority + * - LOW: Green (#4CAF50) - Low priority + * - Default: Gray (#9E9E9E) - Unknown priority + * + * Use case: Color-coding alerts, patient cards, status indicators + */ +export const getPriorityColor = (priority: AlertPriority): string => { + switch (priority) { + case 'CRITICAL': + return '#F44336'; // Red for critical alerts + case 'HIGH': + return '#FF9800'; // Orange for high priority + case 'MEDIUM': + return '#2196F3'; // Blue for medium priority + case 'LOW': + return '#4CAF50'; // Green for low priority + default: + return '#9E9E9E'; // Gray for unknown priority + } +}; + +/** + * getStatusColor Function + * + * Purpose: Get the appropriate color for patient status levels + * + * @param status - The patient status + * @returns Hex color code for the status + * + * Color Mapping: + * - CRITICAL: Red (#F44336) - Critical patient condition + * - ACTIVE: Blue (#2196F3) - Currently under care + * - PENDING: Orange (#FF9800) - Waiting for treatment + * - DISCHARGED: Green (#4CAF50) - Successfully discharged + * - TRANSFERRED: Gray (#9E9E9E) - Transferred to another facility + * + * Use case: Color-coding patient cards, status badges, dashboard indicators + */ +export const getStatusColor = (status: PatientStatus): string => { + switch (status) { + case 'CRITICAL': + return '#F44336'; // Red for critical patients + case 'ACTIVE': + return '#2196F3'; // Blue for active patients + case 'PENDING': + return '#FF9800'; // Orange for pending patients + case 'DISCHARGED': + return '#4CAF50'; // Green for discharged patients + case 'TRANSFERRED': + return '#9E9E9E'; // Gray for transferred patients + default: + return '#9E9E9E'; // Gray for unknown status + } +}; + +/** + * calculateAge Function + * + * Purpose: Calculate age from date of birth + * + * @param dateOfBirth - The patient's date of birth + * @returns Age in years + * + * Logic: + * - Calculates year difference + * - Adjusts for month and day to get accurate age + * - Handles leap years and month length variations + * + * Example: + * calculateAge(new Date('1990-05-15')) โ†’ 33 (if current year is 2023) + * + * Use case: Displaying patient age, age-based calculations, demographics + */ +export const calculateAge = (dateOfBirth: Date): number => { + const today = new Date(); + let age = today.getFullYear() - dateOfBirth.getFullYear(); + const monthDiff = today.getMonth() - dateOfBirth.getMonth(); + + // Adjust age if birthday hasn't occurred this year + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dateOfBirth.getDate())) { + age--; + } + + return age; +}; + +// ============================================================================ +// VALIDATION HELPERS - Functions for data validation +// ============================================================================ + +/** + * isValidEmail Function + * + * Purpose: Validate email address format + * + * @param email - The email address to validate + * @returns Boolean indicating if email is valid + * + * Validation Rules: + * - Must contain @ symbol + * - Must have text before and after @ + * - Must have domain extension + * + * Use case: Login forms, user registration, contact information + */ +export const isValidEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +}; + +/** + * isValidPhone Function + * + * Purpose: Validate phone number format + * + * @param phone - The phone number to validate + * @returns Boolean indicating if phone number is valid + * + * Validation Rules: + * - Minimum 10 digits + * - Allows +, spaces, hyphens, parentheses + * - International format supported + * + * Use case: Contact information, emergency contacts + */ +export const isValidPhone = (phone: string): boolean => { + const phoneRegex = /^\+?[\d\s\-\(\)]{10,}$/; + return phoneRegex.test(phone); +}; + +/** + * isValidMRN Function + * + * Purpose: Validate Medical Record Number format + * + * @param mrn - The MRN to validate + * @returns Boolean indicating if MRN is valid + * + * Validation Rules: + * - 6-12 characters long + * - Alphanumeric only (A-Z, 0-9) + * - No special characters or spaces + * + * Use case: Patient identification, medical record lookup + */ +export const isValidMRN = (mrn: string): boolean => { + const mrnRegex = /^[A-Z0-9]{6,12}$/; + return mrnRegex.test(mrn); +}; + +// ============================================================================ +// ASYNC HELPERS - Functions for asynchronous operations +// ============================================================================ + +/** + * sleep Function + * + * Purpose: Create a delay for specified milliseconds + * + * @param ms - Milliseconds to delay + * @returns Promise that resolves after the delay + * + * Example: + * await sleep(1000); // Wait for 1 second + * + * Use case: Rate limiting, loading states, animation delays + */ +export const sleep = (ms: number): Promise => { + return new Promise(resolve => setTimeout(resolve, ms)); +}; + +/** + * retry Function + * + * Purpose: Retry an async function with exponential backoff + * + * @param fn - The async function to retry + * @param retries - Number of retry attempts (default: 3) + * @param delay - Initial delay in milliseconds (default: 1000) + * @returns Promise that resolves with function result or rejects after all retries + * + * Retry Strategy: + * - Exponential backoff (delay doubles after each failure) + * - Stops after specified number of retries + * - Throws error if all retries fail + * + * Example: + * const result = await retry(fetchPatientData, 3, 1000); + * + * Use case: API calls, network requests, database operations + */ +export const retry = async ( + fn: () => Promise, + retries: number = 3, + delay: number = 1000 +): Promise => { + try { + return await fn(); + } catch (error) { + if (retries > 0) { + await sleep(delay); + return retry(fn, retries - 1, delay * 2); // Exponential backoff + } + throw error; + } +}; + +/* + * End of File: helpers.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/shared/utils/index.ts b/app/shared/utils/index.ts new file mode 100644 index 0000000..39303b7 --- /dev/null +++ b/app/shared/utils/index.ts @@ -0,0 +1,17 @@ +/* + * File: index.ts + * Description: Shared utility functions exports for the Physician App + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export * from './api'; +export * from './constants'; +export * from './helpers'; +export * from './validators'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/shared/utils/toast.ts b/app/shared/utils/toast.ts new file mode 100644 index 0000000..06b94c9 --- /dev/null +++ b/app/shared/utils/toast.ts @@ -0,0 +1,48 @@ +/* + * File: Toast.js + * Description: Utility for displaying toast notifications + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import Toast from 'react-native-toast-message'; +//shows success toast +const showSuccess=(text1='Successfull',text2='')=>{ + + Toast.show({ + type: 'success', + text1, + text2 + }); +} +//shows error text +const showError=(text1='something went wrong',text2='')=>{ + + Toast.show({ + type: 'error', + text1, + text2 + }); +} +//shows warning text +const showWarning=(text1='you have some warning',text2='')=>{ + + Toast.show({ + type: 'info', + text1, + text2 + }); +} + +const handleError=()=>{ + + +} + +export {showSuccess,showError,showWarning,handleError}; + +/* + * End of File: Toast.js + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/shared/utils/validators.ts b/app/shared/utils/validators.ts new file mode 100644 index 0000000..fb675be --- /dev/null +++ b/app/shared/utils/validators.ts @@ -0,0 +1,22 @@ +/* + * File: validators.ts + * Description: Common input validation functions. + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +/** + * Validates an email address against a standard regex pattern. + * @param email The email address string to validate. + * @returns `true` if the email is valid, `false` otherwise. + */ +export const validateEmail = (email: string): boolean => { + const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return regex.test(email); +}; + +/* + * End of File: validators.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/store/Provider.tsx b/app/store/Provider.tsx new file mode 100644 index 0000000..77bb4f5 --- /dev/null +++ b/app/store/Provider.tsx @@ -0,0 +1,98 @@ +/* + * File: Provider.tsx + * Description: Redux Provider wrapper with PersistGate + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { Provider as ReduxProvider } from 'react-redux'; +import { PersistGate } from 'redux-persist/integration/react'; +import { store ,persistor} from '.'; +import { Text, View } from 'react-native'; + + +// ============================================================================ +// PROVIDER COMPONENT +// ============================================================================ + +/** + * Store Provider Component + * + * Purpose: Wrap the app with Redux Provider and PersistGate + * + * Features: + * - Redux store provider + * - Redux Persist gate for state rehydration + * - Loading state during persistence + * - Error handling for persistence failures + * + * @param children - React children components + */ +interface StoreProviderProps { + children: React.ReactNode; +} + +/** + * Loading Component for PersistGate + * + * Purpose: Show loading state while Redux state is being rehydrated + * + * Features: + * - Simple loading indicator + * - Consistent with app theme + * - Minimal blocking UI + */ +const PersistLoadingComponent: React.FC = () => { + return ( + + Loading app data... + + ); +}; + +/** + * Store Provider Component + * + * Purpose: Main provider component that wraps the app + * + * Features: + * - Redux Provider for state management + * - PersistGate for state persistence + * - Loading state during rehydration + * - Error handling for persistence issues + */ +export const StoreProvider: React.FC = ({ children }) => { + return ( + + } + persistor={persistor} + onBeforeLift={() => { + // Called before the store is lifted (rehydrated) + console.log('Redux store is about to be rehydrated'); + }} + onAfterLift={() => { + // Called after the store is lifted (rehydrated) + console.log('Redux store has been rehydrated'); + }} + > + {children} + + + ); +}; + +/* + * End of File: Provider.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/store/hooks.ts b/app/store/hooks.ts new file mode 100644 index 0000000..4b6d1c6 --- /dev/null +++ b/app/store/hooks.ts @@ -0,0 +1,307 @@ +/* + * File: hooks.ts + * Description: Custom Redux hooks for easy store access + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'; +import { RootState, AppDispatch } from './index'; + +// ============================================================================ +// TYPED HOOKS +// ============================================================================ + +/** + * Typed Use Dispatch Hook + * + * Purpose: Provide typed dispatch function + * + * Features: + * - Type-safe dispatch function + * - Proper TypeScript support + * - Consistent with Redux Toolkit patterns + */ +export const useAppDispatch = () => useDispatch(); + +/** + * Typed Use Selector Hook + * + * Purpose: Provide typed selector function + * + * Features: + * - Type-safe selector function + * - Proper TypeScript support + * - Consistent with Redux Toolkit patterns + */ +export const useAppSelector: TypedUseSelectorHook = useSelector; + +// ============================================================================ +// AUTH HOOKS +// ============================================================================ + +/** + * Use Auth Hook + * + * Purpose: Get authentication state and actions + * + * @returns Authentication state and actions + */ +export const useAuth = () => { + const auth = useAppSelector((state) => state.auth); + const dispatch = useAppDispatch(); + + return { + // State + user: auth.user, + isAuthenticated: auth.isAuthenticated, + isLoading: auth.isLoading, + isLoggingIn: auth.isLoggingIn, + isLoggingOut: auth.isLoggingOut, + error: auth.error, + loginError: auth.loginError, + + // Actions + dispatch, + }; +}; + +/** + * Use User Hook + * + * Purpose: Get current user information + * + * @returns Current user data + */ +export const useUser = () => { + const user = useAppSelector((state) => state.auth.user); + + return { + user, + isLoggedIn: !!user, + hasPermission: (permission: string) => user?.permissions?.includes(permission) || false, + }; +}; + +// ============================================================================ +// DASHBOARD HOOKS +// ============================================================================ + +/** + * Use Dashboard Hook + * + * Purpose: Get dashboard state and actions + * + * @returns Dashboard state and actions + */ +export const useDashboard = () => { + const dashboard = useAppSelector((state) => state.dashboard); + const dispatch = useAppDispatch(); + + return { + // State + dashboard: dashboard.dashboard, + isLoading: dashboard.isLoading, + isRefreshing: dashboard.isRefreshing, + error: dashboard.error, + lastUpdated: dashboard.lastUpdated, + isConnected: dashboard.isConnected, + selectedFilter: dashboard.selectedFilter, + sortBy: dashboard.sortBy, + sortOrder: dashboard.sortOrder, + + // Actions + dispatch, + }; +}; + +// ============================================================================ +// ALERTS HOOKS +// ============================================================================ + +/** + * Use Alerts Hook + * + * Purpose: Get alerts state and actions + * + * @returns Alerts state and actions + */ +export const useAlerts = () => { + const alerts = useAppSelector((state) => state.alerts); + const dispatch = useAppDispatch(); + + return { + // State + alerts: alerts.alerts, + isLoading: alerts.isLoading, + isRefreshing: alerts.isRefreshing, + error: alerts.error, + lastUpdated: alerts.lastUpdated, + unreadCount: alerts.unreadCount, + criticalCount: alerts.criticalCount, + selectedFilter: alerts.selectedFilter, + sortBy: alerts.sortBy, + sortOrder: alerts.sortOrder, + + // Computed values + criticalAlerts: alerts.alerts.filter(alert => alert.priority === 'CRITICAL'), + unreadAlerts: alerts.alerts.filter(alert => !alert.isRead), + acknowledgedAlerts: alerts.alerts.filter(alert => alert.isAcknowledged), + + // Actions + dispatch, + }; +}; + +// ============================================================================ +// PATIENT CARE HOOKS +// ============================================================================ + +/** + * Use Patient Care Hook + * + * Purpose: Get patient care state and actions + * + * @returns Patient care state and actions + */ +export const usePatientCare = () => { + const patientCare = useAppSelector((state) => state.patientCare); + const dispatch = useAppDispatch(); + + return { + // State + patients: patientCare.patients, + currentPatient: patientCare.currentPatient, + isLoading: patientCare.isLoading, + isRefreshing: patientCare.isRefreshing, + isLoadingPatientDetails: patientCare.isLoadingPatientDetails, + error: patientCare.error, + searchQuery: patientCare.searchQuery, + selectedFilter: patientCare.selectedFilter, + sortBy: patientCare.sortBy, + sortOrder: patientCare.sortOrder, + currentPage: patientCare.currentPage, + itemsPerPage: patientCare.itemsPerPage, + totalItems: patientCare.totalItems, + lastUpdated: patientCare.lastUpdated, + + // Computed values + activePatients: patientCare.patients.filter(patient => patient.status === 'ACTIVE'), + criticalPatients: patientCare.patients.filter(patient => patient.priority === 'CRITICAL'), + dischargedPatients: patientCare.patients.filter(patient => patient.status === 'DISCHARGED'), + + // Actions + dispatch, + }; +}; + +// ============================================================================ +// SETTINGS HOOKS +// ============================================================================ + +/** + * Use Settings Hook + * + * Purpose: Get settings state and actions + * + * @returns Settings state and actions + */ +export const useSettings = () => { + const settings = useAppSelector((state) => state.settings); + const dispatch = useAppDispatch(); + + return { + // State + userProfile: settings.userProfile, + userPreferences: settings.userPreferences, + isLoading: settings.isLoading, + isUpdatingProfile: settings.isUpdatingProfile, + isUpdatingPreferences: settings.isUpdatingPreferences, + error: settings.error, + isProfileValid: settings.isProfileValid, + isPreferencesValid: settings.isPreferencesValid, + profileLastUpdated: settings.profileLastUpdated, + preferencesLastUpdated: settings.preferencesLastUpdated, + + // Actions + dispatch, + }; +}; + +// ============================================================================ +// UI HOOKS +// ============================================================================ + +/** + * Use UI Hook + * + * Purpose: Get UI state and actions + * + * @returns UI state and actions + */ +export const useUI = () => { + const ui = useAppSelector((state) => state.ui); + const dispatch = useAppDispatch(); + + return { + // State + isLoading: ui.isLoading, + loadingMessage: ui.loadingMessage, + isModalOpen: ui.isModalOpen, + modalType: ui.modalType, + modalData: ui.modalData, + isOverlayVisible: ui.isOverlayVisible, + overlayType: ui.overlayType, + currentScreen: ui.currentScreen, + navigationStack: ui.navigationStack, + isDarkMode: ui.isDarkMode, + fontSize: ui.fontSize, + highContrast: ui.highContrast, + isRefreshing: ui.isRefreshing, + isScrolling: ui.isScrolling, + lastInteraction: ui.lastInteraction, + hasError: ui.hasError, + errorMessage: ui.errorMessage, + errorType: ui.errorType, + + // Actions + dispatch, + }; +}; + +// ============================================================================ +// UTILITY HOOKS +// ============================================================================ + +/** + * Use Store Hook + * + * Purpose: Get access to the entire store state + * + * @returns Entire store state + */ +export const useStore = () => { + return useAppSelector((state) => state); +}; + +/** + * Use Persistence Status Hook + * + * Purpose: Get Redux Persist status + * + * @returns Persistence status + */ +export const usePersistenceStatus = () => { + // This would need to be implemented with a custom selector + // that accesses the persistor state + return { + isRehydrated: true, // Placeholder + isPersisting: false, // Placeholder + }; +}; + +/* + * End of File: hooks.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/store/index.ts b/app/store/index.ts new file mode 100644 index 0000000..14cf052 --- /dev/null +++ b/app/store/index.ts @@ -0,0 +1,250 @@ +/* + * File: index.ts + * Description: Main Redux store configuration with Redux Persist + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { configureStore, combineReducers } from '@reduxjs/toolkit'; +import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER } from 'redux-persist'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +// Import all slice reducers from their respective modules +import authReducer from '../modules/Auth/redux/authSlice'; +import dashboardReducer from '../modules/Dashboard/redux/dashboardSlice'; +import aiDashboardReducer from '../modules/Dashboard/redux/aiDashboardSlice'; +import patientCareReducer from '../modules/PatientCare/redux/patientCareSlice'; +import alertsReducer from '../modules/Dashboard/redux/alertsSlice'; +import settingsReducer from '../modules/Settings/redux/settingsSlice'; +import uiReducer from '../modules/Dashboard/redux/uiSlice'; +import hospitalReducer from '../modules/Auth/redux/hospitalSlice'; +import aiPredictionReducer from '../modules/AIPrediction/redux/aiPredictionSlice'; + +// ============================================================================ +// REDUX PERSIST CONFIGURATION +// ============================================================================ + +/** + * Redux Persist Configuration + * + * Purpose: Configure how Redux state is persisted to AsyncStorage + * + * Features: + * - AsyncStorage as storage engine + * - Selective persistence (not all state needs to be persisted) + * - Migration support for app updates + * - Debug mode for development + */ +const persistConfig = { + // Storage engine (AsyncStorage for React Native) + storage: AsyncStorage, + + // Key for the persisted state in AsyncStorage + key: 'neoscan_physician_store', + + // Version for migration support + version: 1, + + // Whitelist: Only persist these reducers + whitelist: [ + 'auth', // Authentication state (user login, tokens) + 'settings', // User preferences and settings + 'patientCare', // Patient data cache + 'aiPrediction', // AI prediction data cache + ], + + // Blacklist: Don't persist these reducers + blacklist: [ + 'ui', // UI state (loading, modals, etc.) + 'alerts', // Temporary alerts and notifications + 'dashboard', // Real-time dashboard data + 'aiDashboard', // AI dashboard statistics (fetched fresh each time) + 'hospital', // Hospital data (fetched fresh each time) + ], + + // Migration configuration for app updates + migrate: (state: any) => { + // Handle state migrations when app version changes + return Promise.resolve(state); + }, + + // Serialization options + serialize: true, + + // Timeout for storage operations + timeout: 10000, +}; + +// ============================================================================ +// ROOT REDUCER +// ============================================================================ + +/** + * Root Reducer + * + * Purpose: Combine all slice reducers into a single root reducer + * + * Structure: + * - auth: Authentication and user management + * - dashboard: ER dashboard data and statistics + * - aiDashboard: AI analysis dashboard statistics + * - patientCare: Patient information and medical records + * - aiPrediction: AI prediction cases and analysis + * - alerts: Critical alerts and notifications + * - settings: User preferences and app settings + * - ui: User interface state (loading, modals, etc.) + */ +const rootReducer = combineReducers({ + auth: authReducer, + dashboard: dashboardReducer, + aiDashboard: aiDashboardReducer, + patientCare: patientCareReducer, + aiPrediction: aiPredictionReducer, + alerts: alertsReducer, + settings: settingsReducer, + ui: uiReducer, + hospital: hospitalReducer, +}); + +// ============================================================================ +// PERSISTED REDUCER +// ============================================================================ + +/** + * Persisted Reducer + * + * Purpose: Wrap the root reducer with Redux Persist functionality + * + * Features: + * - Automatic state persistence to AsyncStorage + * - State rehydration on app startup + * - Selective persistence based on whitelist/blacklist + */ +const persistedReducer = persistReducer(persistConfig, rootReducer); + +// ============================================================================ +// STORE CONFIGURATION +// ============================================================================ + +/** + * Redux Store Configuration + * + * Purpose: Configure the Redux store with middleware and persistence + * + * Features: + * - Redux Toolkit for simplified Redux setup + * - Redux Persist for state persistence + * - Development tools integration + * - Error handling and logging + */ +export const store = configureStore({ + // Use the persisted reducer + reducer: persistedReducer, + + // Middleware configuration + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + // Configure serializable check for Redux Persist actions + serializableCheck: { + ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], + }, + + }), + + // Preloaded state (for SSR or initial state) + preloadedState: undefined, +}); + +// ============================================================================ +// PERSISTOR CONFIGURATION +// ============================================================================ + +/** + * Redux Persistor + * + * Purpose: Handle state persistence and rehydration + * + * Features: + * - Automatic state saving to AsyncStorage + * - State restoration on app startup + * - Migration handling for app updates + * - Error handling for storage operations + */ +export const persistor = persistStore(store); + +// ============================================================================ +// TYPE EXPORTS +// ============================================================================ + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; + +// ============================================================================ +// STORE UTILITIES +// ============================================================================ + +/** + * Get Store State + * + * Purpose: Get the current state from the store + * + * @returns Current Redux state + */ +export const getStoreState = (): RootState => store.getState(); + +/** + * Dispatch Action + * + * Purpose: Dispatch an action to the store + * + * @param action - Redux action to dispatch + * @returns Dispatched action result + */ +export const dispatchAction = (action: any) => store.dispatch(action); + +/** + * Subscribe to Store Changes + * + * Purpose: Subscribe to store state changes + * + * @param listener - Function to call when state changes + * @returns Unsubscribe function + */ +export const subscribeToStore = (listener: () => void) => store.subscribe(listener); + +/** + * Clear Persisted State + * + * Purpose: Clear all persisted state from AsyncStorage + * + * @returns Promise that resolves when state is cleared + */ +export const clearPersistedState = async (): Promise => { + try { + await persistor.purge(); + console.log('Persisted state cleared successfully'); + } catch (error) { + console.error('Failed to clear persisted state:', error); + throw error; + } +}; + +/** + * Get Persistence Status + * + * Purpose: Get the current status of Redux Persist + * + * @returns Persistence status object + */ +export const getPersistenceStatus = () => { + return { + isRehydrated: persistor.getState().bootstrapped, + }; +}; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/theme/animations.ts b/app/theme/animations.ts new file mode 100644 index 0000000..c86249d --- /dev/null +++ b/app/theme/animations.ts @@ -0,0 +1,32 @@ +/* + * File: animations.ts + * Description: Animation system definitions for the Physician App + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export const animations = { + durations: { + fast: 150, + normal: 300, + slow: 500, + }, + easing: { + standard: 'cubic-bezier(0.4, 0.0, 0.2, 1)', + deceleration: 'cubic-bezier(0.0, 0.0, 0.2, 1)', + acceleration: 'cubic-bezier(0.4, 0.0, 1, 1)', + }, + transitions: { + fade: { opacity: [0, 1], duration: 300 }, + slide: { transform: [{ translateY: [20, 0] }], duration: 300 }, + scale: { transform: [{ scale: [0.95, 1] }], duration: 200 }, + }, +} as const; + +export type AnimationKey = keyof typeof animations; + +/* + * End of File: animations.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/theme/colors.ts b/app/theme/colors.ts new file mode 100644 index 0000000..a38ded8 --- /dev/null +++ b/app/theme/colors.ts @@ -0,0 +1,58 @@ +/* + * File: colors.ts + * Description: Color palette definitions for the Physician App + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export const colors = { + // Primary Colors + primary: '#2196F3', + secondary: '#1976D2', + tertiary: '#E3F2FD', + quaternary: '#0D47A1', + + // Text Colors + textPrimary: '#212121', + textSecondary: '#757575', + textMuted: '#9E9E9E', + + // Background Colors + background: '#FFFFFF', + backgroundAlt: '#FAFAFA', + backgroundAccent: '#F5F5F5', + + // Status Colors + success: '#4CAF50', + warning: '#FF9800', + error: '#F44336', + info: '#2196F3', + + // UI Elements + border: '#E0E0E0', + cardBackground: '#FFFFFF', + shadow: 'rgba(0, 0, 0, 0.1)', + + // Alert Colors + critical: '#F44336', + criticalBackground: '#FFEBEE', + warningBackground: '#FFF3E0', + successBackground: '#E8F5E8', + infoBackground: '#E3F2FD', + + // Patient Status Colors + active: '#2196F3', + activeBackground: '#E3F2FD', + pending: '#FF9800', + pendingBackground: '#FFF3E0', + completed: '#4CAF50', + completedBackground: '#E8F5E8', +} as const; + +export type ColorKey = keyof typeof colors; + +/* + * End of File: colors.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/theme/index.ts b/app/theme/index.ts new file mode 100644 index 0000000..baa6efd --- /dev/null +++ b/app/theme/index.ts @@ -0,0 +1,19 @@ +/* + * File: index.ts + * Description: Theme module exports for the Physician App + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export * from './colors'; +export * from './typography'; +export * from './spacing'; +export * from './shadows'; +export * from './animations'; +export * from './theme'; + +/* + * End of File: index.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/theme/shadows.ts b/app/theme/shadows.ts new file mode 100644 index 0000000..53f3047 --- /dev/null +++ b/app/theme/shadows.ts @@ -0,0 +1,54 @@ +/* + * File: shadows.ts + * Description: Shadow system definitions for the Physician App + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { colors } from './colors'; + +export const shadows = { + small: { + shadowColor: colors.shadow, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + medium: { + shadowColor: colors.shadow, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 4, + }, + large: { + shadowColor: colors.shadow, + shadowOffset: { width: 0, height: 8 }, + shadowOpacity: 0.2, + shadowRadius: 16, + elevation: 8, + }, + critical: { + shadowColor: colors.critical, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 4, + }, + primary: { + shadowColor: colors.primary, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 6, + }, +} as const; + +export type ShadowKey = keyof typeof shadows; + +/* + * End of File: shadows.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/theme/spacing.ts b/app/theme/spacing.ts new file mode 100644 index 0000000..9a1d72e --- /dev/null +++ b/app/theme/spacing.ts @@ -0,0 +1,41 @@ +/* + * File: spacing.ts + * Description: Spacing and layout system definitions for the Physician App + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export const spacing = { + xs: 4, + sm: 8, + md: 16, + lg: 24, + xl: 32, + xxl: 48, + xxxl: 64, +} as const; + +export const borderRadius = { + small: 4, + medium: 8, + large: 12, + xlarge: 16, + round: 50, +} as const; + +export const breakpoints = { + mobile: 375, + tablet: 768, + desktop: 1024, + largeDesktop: 1440, +} as const; + +export type SpacingKey = keyof typeof spacing; +export type BorderRadiusKey = keyof typeof borderRadius; +export type BreakpointKey = keyof typeof breakpoints; + +/* + * End of File: spacing.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/theme/theme.ts b/app/theme/theme.ts new file mode 100644 index 0000000..426c0e9 --- /dev/null +++ b/app/theme/theme.ts @@ -0,0 +1,66 @@ +/* + * File: theme.ts + * Description: Main theme configuration combining all design system elements + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { colors } from './colors'; +import { typography } from './typography'; +import { spacing, borderRadius, breakpoints } from './spacing'; +import { shadows } from './shadows'; +import { animations } from './animations'; + +/** + * Main Theme Object + * + * Purpose: Centralized design system that provides consistent styling across the entire app + * + * Design System Components: + * 1. Colors: Healthcare-focused color palette with status indicators + * 2. Typography: Font families, sizes, weights, and line heights + * 3. Spacing: Consistent spacing scale for layouts and components + * 4. Border Radius: Standardized corner radius values + * 5. Breakpoints: Responsive design breakpoints for different screen sizes + * 6. Shadows: Elevation and depth system for UI components + * 7. Animations: Standardized animation durations and easing functions + * + * Usage: + * - Import this theme object in components: import { theme } from '../theme/theme' + * - Access design tokens: theme.colors.primary, theme.spacing.md, etc. + * - Ensures consistency and maintainability across the app + * + * Benefits: + * - Single source of truth for all design decisions + * - Easy to update and maintain design system + * - Consistent user experience across all screens + * - Supports dark mode and accessibility requirements + */ +export const theme = { + colors, // Color palette with primary, secondary, status, and background colors + typography, // Font system with sizes, weights, and families + spacing, // Spacing scale for margins, padding, and layouts + borderRadius, // Border radius values for rounded corners + breakpoints, // Responsive breakpoints for different screen sizes + shadows, // Shadow system for elevation and depth + animations, // Animation durations and easing functions +} as const; // Make theme immutable for type safety + +/** + * Theme Type + * + * Purpose: TypeScript type definition for the theme object + * + * Benefits: + * - Provides type safety when using theme properties + * - Enables autocomplete in IDEs + * - Prevents typos and invalid property access + * - Supports theme customization and extension + */ +export type Theme = typeof theme; + +/* + * End of File: theme.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/theme/typography.ts b/app/theme/typography.ts new file mode 100644 index 0000000..00591ef --- /dev/null +++ b/app/theme/typography.ts @@ -0,0 +1,59 @@ +/* + * File: typography.ts + * Description: Typography system definitions for the Physician App + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +export const typography = { + // Font Families + fontFamily: { + bold: 'WorkSans-Bold', + medium: 'WorkSans-Medium', + regular: 'WorkSans-Regular', + light: 'WorkSans-Light', + semibold: 'WorkSans-SemiBold', + extrabold: 'WorkSans-ExtraBold', + }, + + // Font Weights + fontWeight: { + light: '300', + regular: '400', + medium: '500', + bold: '700', + }, + + // Font Sizes + fontSize: { + displayLarge: 32, + displayMedium: 24, + displaySmall: 20, + bodyLarge: 16, + bodyMedium: 14, + bodySmall: 12, + caption: 10, + }, + + // Line Heights + lineHeight: { + tight: 1.2, + normal: 1.4, + relaxed: 1.6, + }, + + // Letter Spacing + letterSpacing: { + tight: -0.5, + normal: 0, + wide: 0.5, + }, +} as const; + +export type TypographyKey = keyof typeof typography; + +/* + * End of File: typography.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/app/types/react-native-vector-icons.d.ts b/app/types/react-native-vector-icons.d.ts new file mode 100644 index 0000000..fce721f --- /dev/null +++ b/app/types/react-native-vector-icons.d.ts @@ -0,0 +1,51 @@ +/* + * File: react-native-vector-icons.d.ts + * Description: Type declarations for react-native-vector-icons + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +declare module 'react-native-vector-icons/Feather' { + import { Component } from 'react'; + import { TextProps } from 'react-native'; + + interface IconProps extends TextProps { + name: string; + size?: number; + color?: string; + } + + export default class Icon extends Component {} +} + +declare module 'react-native-vector-icons/MaterialIcons' { + import { Component } from 'react'; + import { TextProps } from 'react-native'; + + interface IconProps extends TextProps { + name: string; + size?: number; + color?: string; + } + + export default class Icon extends Component {} +} + +declare module 'react-native-vector-icons/Ionicons' { + import { Component } from 'react'; + import { TextProps } from 'react-native'; + + interface IconProps extends TextProps { + name: string; + size?: number; + color?: string; + } + + export default class Icon extends Component {} +} + +/* + * End of File: react-native-vector-icons.d.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..02c7d13 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ['module:@react-native/babel-preset'], + plugins: ['react-native-reanimated/plugin'], +}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..7a02d48 --- /dev/null +++ b/index.js @@ -0,0 +1,9 @@ +/** + * @format + */ + +import {AppRegistry} from 'react-native'; +import App from './app/App'; +import {name as appName} from './app.json'; + +AppRegistry.registerComponent(appName, () => App); diff --git a/ios/.xcode.env b/ios/.xcode.env new file mode 100644 index 0000000..3d5782c --- /dev/null +++ b/ios/.xcode.env @@ -0,0 +1,11 @@ +# This `.xcode.env` file is versioned and is used to source the environment +# used when running script phases inside Xcode. +# To customize your local environment, you can create an `.xcode.env.local` +# file that is not versioned. + +# NODE_BINARY variable contains the PATH to the node executable. +# +# Customize the NODE_BINARY variable here. +# For example, to use nvm with brew, add the following line +# . "$(brew --prefix nvm)/nvm.sh" --no-use +export NODE_BINARY=$(command -v node) diff --git a/ios/NeoScan_Physician.xcodeproj/project.pbxproj b/ios/NeoScan_Physician.xcodeproj/project.pbxproj new file mode 100644 index 0000000..ac4b53a --- /dev/null +++ b/ios/NeoScan_Physician.xcodeproj/project.pbxproj @@ -0,0 +1,546 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 0C80B921A6F3F58F76C31292 /* libPods-NeoScan_Physician.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-NeoScan_Physician.a */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; + 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + 90EC1A731F2F480594C0F9D1 /* WorkSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6606F41B1382422DA695F61C /* WorkSans-Bold.ttf */; }; + 7E1975A765074059A27FA6F1 /* WorkSans-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 1569DC7537534ED39A79EE9E /* WorkSans-ExtraBold.ttf */; }; + E09CC0AFCD63425FABA5714F /* WorkSans-ExtraLight.ttf in Resources */ = {isa = PBXBuildFile; fileRef = FF48E43749B74116A2C4083C /* WorkSans-ExtraLight.ttf */; }; + 6A06861DDC314E49B482B4EB /* WorkSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B8C4C96FCA144E859DC2AFDA /* WorkSans-Light.ttf */; }; + 3BEFE3BEC3334662878A37D5 /* WorkSans-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55E647ABF16B47F6A83403C0 /* WorkSans-Medium.ttf */; }; + 6E9607F0F9DE4649802D618B /* WorkSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 589149FBA9F64E9F94AA50AF /* WorkSans-Regular.ttf */; }; + DCDB35B731E1422DBD1481D1 /* WorkSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A58733FB00504503BFBF4295 /* WorkSans-SemiBold.ttf */; }; + 25B52D1C5AB64F039BEF062F /* WorkSans-Thin.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C3614D19A4004E3E858E4CEC /* WorkSans-Thin.ttf */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13B07F861A680F5B00A75B9A; + remoteInfo = NeoScan_Physician; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 13B07F961A680F5B00A75B9A /* NeoScan_Physician.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NeoScan_Physician.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = NeoScan_Physician/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = NeoScan_Physician/Info.plist; sourceTree = ""; }; + 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = NeoScan_Physician/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 3B4392A12AC88292D35C810B /* Pods-NeoScan_Physician.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NeoScan_Physician.debug.xcconfig"; path = "Target Support Files/Pods-NeoScan_Physician/Pods-NeoScan_Physician.debug.xcconfig"; sourceTree = ""; }; + 5709B34CF0A7D63546082F79 /* Pods-NeoScan_Physician.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NeoScan_Physician.release.xcconfig"; path = "Target Support Files/Pods-NeoScan_Physician/Pods-NeoScan_Physician.release.xcconfig"; sourceTree = ""; }; + 5DCACB8F33CDC322A6C60F78 /* libPods-NeoScan_Physician.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NeoScan_Physician.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = NeoScan_Physician/AppDelegate.swift; sourceTree = ""; }; + 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = NeoScan_Physician/LaunchScreen.storyboard; sourceTree = ""; }; + ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + 6606F41B1382422DA695F61C /* WorkSans-Bold.ttf */ = {isa = PBXFileReference; name = "WorkSans-Bold.ttf"; path = "../app/assets/fonts/WorkSans-Bold.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + 1569DC7537534ED39A79EE9E /* WorkSans-ExtraBold.ttf */ = {isa = PBXFileReference; name = "WorkSans-ExtraBold.ttf"; path = "../app/assets/fonts/WorkSans-ExtraBold.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + FF48E43749B74116A2C4083C /* WorkSans-ExtraLight.ttf */ = {isa = PBXFileReference; name = "WorkSans-ExtraLight.ttf"; path = "../app/assets/fonts/WorkSans-ExtraLight.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + B8C4C96FCA144E859DC2AFDA /* WorkSans-Light.ttf */ = {isa = PBXFileReference; name = "WorkSans-Light.ttf"; path = "../app/assets/fonts/WorkSans-Light.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + 55E647ABF16B47F6A83403C0 /* WorkSans-Medium.ttf */ = {isa = PBXFileReference; name = "WorkSans-Medium.ttf"; path = "../app/assets/fonts/WorkSans-Medium.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + 589149FBA9F64E9F94AA50AF /* WorkSans-Regular.ttf */ = {isa = PBXFileReference; name = "WorkSans-Regular.ttf"; path = "../app/assets/fonts/WorkSans-Regular.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + A58733FB00504503BFBF4295 /* WorkSans-SemiBold.ttf */ = {isa = PBXFileReference; name = "WorkSans-SemiBold.ttf"; path = "../app/assets/fonts/WorkSans-SemiBold.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + C3614D19A4004E3E858E4CEC /* WorkSans-Thin.ttf */ = {isa = PBXFileReference; name = "WorkSans-Thin.ttf"; path = "../app/assets/fonts/WorkSans-Thin.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0C80B921A6F3F58F76C31292 /* libPods-NeoScan_Physician.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 00E356F01AD99517003FC87E /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 00E356F11AD99517003FC87E /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 13B07FAE1A68108700A75B9A /* NeoScan_Physician */ = { + isa = PBXGroup; + children = ( + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 761780EC2CA45674006654EE /* AppDelegate.swift */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, + 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */, + ); + name = NeoScan_Physician; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + 5DCACB8F33CDC322A6C60F78 /* libPods-NeoScan_Physician.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* NeoScan_Physician */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + BBD78D7AC51CEA395F1C20DB /* Pods */, + C38CA987921A4CA4AEDBB3E5 /* Resources */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* NeoScan_Physician.app */, + ); + name = Products; + sourceTree = ""; + }; + BBD78D7AC51CEA395F1C20DB /* Pods */ = { + isa = PBXGroup; + children = ( + 3B4392A12AC88292D35C810B /* Pods-NeoScan_Physician.debug.xcconfig */, + 5709B34CF0A7D63546082F79 /* Pods-NeoScan_Physician.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + C38CA987921A4CA4AEDBB3E5 /* Resources */ = { + isa = "PBXGroup"; + children = ( + 6606F41B1382422DA695F61C /* WorkSans-Bold.ttf */, + 1569DC7537534ED39A79EE9E /* WorkSans-ExtraBold.ttf */, + FF48E43749B74116A2C4083C /* WorkSans-ExtraLight.ttf */, + B8C4C96FCA144E859DC2AFDA /* WorkSans-Light.ttf */, + 55E647ABF16B47F6A83403C0 /* WorkSans-Medium.ttf */, + 589149FBA9F64E9F94AA50AF /* WorkSans-Regular.ttf */, + A58733FB00504503BFBF4295 /* WorkSans-SemiBold.ttf */, + C3614D19A4004E3E858E4CEC /* WorkSans-Thin.ttf */, + ); + name = Resources; + sourceTree = ""; + path = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 13B07F861A680F5B00A75B9A /* NeoScan_Physician */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "NeoScan_Physician" */; + buildPhases = ( + C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */, + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */, + E235C05ADACE081382539298 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = NeoScan_Physician; + productName = NeoScan_Physician; + productReference = 13B07F961A680F5B00A75B9A /* NeoScan_Physician.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1210; + TargetAttributes = { + 13B07F861A680F5B00A75B9A = { + LastSwiftMigration = 1120; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "NeoScan_Physician" */; + compatibilityVersion = "Xcode 12.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* NeoScan_Physician */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 00E356EC1AD99517003FC87E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 90EC1A731F2F480594C0F9D1 /* WorkSans-Bold.ttf in Resources */, + 7E1975A765074059A27FA6F1 /* WorkSans-ExtraBold.ttf in Resources */, + E09CC0AFCD63425FABA5714F /* WorkSans-ExtraLight.ttf in Resources */, + 6A06861DDC314E49B482B4EB /* WorkSans-Light.ttf in Resources */, + 3BEFE3BEC3334662878A37D5 /* WorkSans-Medium.ttf in Resources */, + 6E9607F0F9DE4649802D618B /* WorkSans-Regular.ttf in Resources */, + DCDB35B731E1422DBD1481D1 /* WorkSans-SemiBold.ttf in Resources */, + 25B52D1C5AB64F039BEF062F /* WorkSans-Thin.ttf in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(SRCROOT)/.xcode.env.local", + "$(SRCROOT)/.xcode.env", + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; + }; + 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NeoScan_Physician/Pods-NeoScan_Physician-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NeoScan_Physician/Pods-NeoScan_Physician-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NeoScan_Physician/Pods-NeoScan_Physician-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-NeoScan_Physician-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NeoScan_Physician/Pods-NeoScan_Physician-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NeoScan_Physician/Pods-NeoScan_Physician-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NeoScan_Physician/Pods-NeoScan_Physician-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 00E356F51AD99517003FC87E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 13B07F861A680F5B00A75B9A /* NeoScan_Physician */; + targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-NeoScan_Physician.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = NeoScan_Physician/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = NeoScan_Physician; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-NeoScan_Physician.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = NeoScan_Physician/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = NeoScan_Physician; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "\"$(SDKROOT)/usr/lib/swift\"", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + "-DFOLLY_HAVE_CLOCK_GETTIME=1", + ); + SDKROOT = iphoneos; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "\"$(SDKROOT)/usr/lib/swift\"", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + "-DFOLLY_HAVE_CLOCK_GETTIME=1", + ); + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "NeoScan_Physician" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "NeoScan_Physician" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/ios/NeoScan_Physician.xcodeproj/xcshareddata/xcschemes/NeoScan_Physician.xcscheme b/ios/NeoScan_Physician.xcodeproj/xcshareddata/xcschemes/NeoScan_Physician.xcscheme new file mode 100644 index 0000000..f0a736f --- /dev/null +++ b/ios/NeoScan_Physician.xcodeproj/xcshareddata/xcschemes/NeoScan_Physician.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/NeoScan_Physician/AppDelegate.swift b/ios/NeoScan_Physician/AppDelegate.swift new file mode 100644 index 0000000..cf58499 --- /dev/null +++ b/ios/NeoScan_Physician/AppDelegate.swift @@ -0,0 +1,48 @@ +import UIKit +import React +import React_RCTAppDelegate +import ReactAppDependencyProvider + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + var reactNativeDelegate: ReactNativeDelegate? + var reactNativeFactory: RCTReactNativeFactory? + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + let delegate = ReactNativeDelegate() + let factory = RCTReactNativeFactory(delegate: delegate) + delegate.dependencyProvider = RCTAppDependencyProvider() + + reactNativeDelegate = delegate + reactNativeFactory = factory + + window = UIWindow(frame: UIScreen.main.bounds) + + factory.startReactNative( + withModuleName: "NeoScan_Physician", + in: window, + launchOptions: launchOptions + ) + + return true + } +} + +class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { + override func sourceURL(for bridge: RCTBridge) -> URL? { + self.bundleURL() + } + + override func bundleURL() -> URL? { +#if DEBUG + RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") +#else + Bundle.main.url(forResource: "main", withExtension: "jsbundle") +#endif + } +} diff --git a/ios/NeoScan_Physician/Images.xcassets/AppIcon.appiconset/Contents.json b/ios/NeoScan_Physician/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..8121323 --- /dev/null +++ b/ios/NeoScan_Physician/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,53 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/NeoScan_Physician/Images.xcassets/Contents.json b/ios/NeoScan_Physician/Images.xcassets/Contents.json new file mode 100644 index 0000000..2d92bd5 --- /dev/null +++ b/ios/NeoScan_Physician/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/NeoScan_Physician/Info.plist b/ios/NeoScan_Physician/Info.plist new file mode 100644 index 0000000..245b969 --- /dev/null +++ b/ios/NeoScan_Physician/Info.plist @@ -0,0 +1,64 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + NeoScan_Physician + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + NSLocationWhenInUseUsageDescription + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + UIAppFonts + + WorkSans-Bold.ttf + WorkSans-ExtraBold.ttf + WorkSans-ExtraLight.ttf + WorkSans-Light.ttf + WorkSans-Medium.ttf + WorkSans-Regular.ttf + WorkSans-SemiBold.ttf + WorkSans-Thin.ttf + + NSCameraUsageDescription + This app needs access to the camera to take photos and videos. + + diff --git a/ios/NeoScan_Physician/LaunchScreen.storyboard b/ios/NeoScan_Physician/LaunchScreen.storyboard new file mode 100644 index 0000000..d2c963d --- /dev/null +++ b/ios/NeoScan_Physician/LaunchScreen.storyboard @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/NeoScan_Physician/PrivacyInfo.xcprivacy b/ios/NeoScan_Physician/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..41b8317 --- /dev/null +++ b/ios/NeoScan_Physician/PrivacyInfo.xcprivacy @@ -0,0 +1,37 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..5110ec5 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,35 @@ +# Resolve react_native_pods.rb with node to allow for hoisting +require Pod::Executable.execute_command('node', ['-p', + 'require.resolve( + "react-native/scripts/react_native_pods.rb", + {paths: [process.argv[1]]}, + )', __dir__]).strip + +platform :ios, min_ios_version_supported +prepare_react_native_project! + +linkage = ENV['USE_FRAMEWORKS'] +if linkage != nil + Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green + use_frameworks! :linkage => linkage.to_sym +end + +target 'NeoScan_Physician' do + config = use_native_modules! + + use_react_native!( + :path => config[:reactNativePath], + # An absolute path to your application root. + :app_path => "#{Pod::Config.instance.installation_root}/.." + ) + + post_install do |installer| + # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 + react_native_post_install( + installer, + config[:reactNativePath], + :mac_catalyst_enabled => false, + # :ccache_enabled => true + ) + end +end diff --git a/ios/link-assets-manifest.json b/ios/link-assets-manifest.json new file mode 100644 index 0000000..5d7736d --- /dev/null +++ b/ios/link-assets-manifest.json @@ -0,0 +1,37 @@ +{ + "migIndex": 1, + "data": [ + { + "path": "app/assets/fonts/WorkSans-Bold.ttf", + "sha1": "ec84061651ead3c3c5cbb61c2d338aca0bacdc1e" + }, + { + "path": "app/assets/fonts/WorkSans-ExtraBold.ttf", + "sha1": "0b371d1dbfbdd15db880bbd129b239530c71accb" + }, + { + "path": "app/assets/fonts/WorkSans-ExtraLight.ttf", + "sha1": "74596e55487e2961b6c43993698d658e2ceee77b" + }, + { + "path": "app/assets/fonts/WorkSans-Light.ttf", + "sha1": "293e11dae7e8b930bf5eea0b06ca979531f22189" + }, + { + "path": "app/assets/fonts/WorkSans-Medium.ttf", + "sha1": "c281f8454dd193c2260e43ae2de171c5dd4086e4" + }, + { + "path": "app/assets/fonts/WorkSans-Regular.ttf", + "sha1": "5e0183b29b57c54595c62ac6bc223b21f1434226" + }, + { + "path": "app/assets/fonts/WorkSans-SemiBold.ttf", + "sha1": "64b8fe156fafce221a0f66504255257053fc6062" + }, + { + "path": "app/assets/fonts/WorkSans-Thin.ttf", + "sha1": "a62251331038fdd079c47bc413a350efbf702db8" + } + ] +} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..8eb675e --- /dev/null +++ b/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: 'react-native', +}; diff --git a/metro.config.js b/metro.config.js new file mode 100644 index 0000000..ba95bf4 --- /dev/null +++ b/metro.config.js @@ -0,0 +1,11 @@ +const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); + +/** + * Metro configuration + * https://reactnative.dev/docs/metro + * + * @type {import('@react-native/metro-config').MetroConfig} + */ +const config = {}; + +module.exports = mergeConfig(getDefaultConfig(__dirname), config); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7668689 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,14179 @@ +{ + "name": "NeoScan_Physician", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "NeoScan_Physician", + "version": "0.0.1", + "dependencies": { + "@react-native-async-storage/async-storage": "^2.1.0", + "@react-native-clipboard/clipboard": "^1.16.1", + "@react-native-community/datetimepicker": "^8.4.4", + "@react-native-community/netinfo": "^11.4.1", + "@react-navigation/bottom-tabs": "^7.2.0", + "@react-navigation/native": "^7.0.14", + "@react-navigation/native-stack": "^7.2.0", + "@react-navigation/stack": "^7.1.1", + "@reduxjs/toolkit": "^2.5.1", + "@testing-library/react-native": "^13.2.0", + "apisauce": "^3.1.0", + "lottie-react-native": "^7.2.2", + "react": "19.0.0", + "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-device-info": "^10.11.0", + "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", + "react-native-linear-gradient": "^2.8.3", + "react-native-permissions": "^5.2.4", + "react-native-raw-bottom-sheet": "^3.0.0", + "react-native-reanimated": "^3.18.0", + "react-native-render-html": "^6.3.4", + "react-native-responsive-dimensions": "^3.1.1", + "react-native-safe-area-context": "^5.1.0", + "react-native-screens": "^4.5.0", + "react-native-share": "^12.0.9", + "react-native-svg": "^15.11.1", + "react-native-toast-message": "^2.2.1", + "react-native-tts": "^4.1.1", + "react-native-vector-icons": "^10.2.0", + "react-native-webview": "^13.15.0", + "react-redux": "^9.2.0", + "redux-persist": "^6.0.0" + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.25.3", + "@babel/runtime": "^7.25.0", + "@react-native-community/cli": "18.0.0", + "@react-native-community/cli-platform-android": "18.0.0", + "@react-native-community/cli-platform-ios": "18.0.0", + "@react-native/babel-preset": "0.79.0", + "@react-native/eslint-config": "0.79.0", + "@react-native/metro-config": "0.79.0", + "@react-native/typescript-config": "0.79.0", + "@types/jest": "^29.5.13", + "@types/react": "^19.0.0", + "@types/react-test-renderer": "^19.0.0", + "eslint": "^8.19.0", + "jest": "^29.6.3", + "prettier": "2.8.8", + "react-test-renderer": "19.0.0", + "typescript": "5.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/eslint-parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz", + "integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", + "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", + "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-export-default-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.27.1.tgz", + "integrity": "sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-default-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.27.1.tgz", + "integrity": "sha512-eBC/3KSekshx19+N40MzjWqJd7KTEdOoLesAfa4IDFI8eRz5a47i5Oszus6zG/cwIXN63YhgLOMSSNJx49sENg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz", + "integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", + "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", + "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz", + "integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", + "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", + "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-flow": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", + "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", + "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.0.tgz", + "integrity": "sha512-LOAozRVbqxEVjSKfhGnuLoE4Kz4Oc5UJzuvFUhSsQzdCdaAQu06mG8zDv2GFSerM62nImUZ7K92vxnQcLSDlCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.0.tgz", + "integrity": "sha512-dGopk9nZrtCs2+nfIem25UuHyt5moSJamArzIoh9/vezUQPmYDOzjaHDCkAzuGJibCIkPup8rMT2+wYB6S73cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", + "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.0.tgz", + "integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.0", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-classes": "^7.28.0", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.0", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse--for-generate-function-map": { + "name": "@babel/traverse", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", + "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "license": "MIT", + "dependencies": { + "@types/hammerjs": "^2.0.36" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "devOptional": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", + "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", + "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz", + "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsamr/counter-style": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@jsamr/counter-style/-/counter-style-2.0.2.tgz", + "integrity": "sha512-2mXudGVtSzVxWEA7B9jZLKjoXUeUFYDDtFrQoC0IFX9/Dszz4t1vZOmafi3JSw/FxD+udMQ+4TAFR8Qs0J3URQ==", + "license": "MIT" + }, + "node_modules/@jsamr/react-native-li": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@jsamr/react-native-li/-/react-native-li-2.3.1.tgz", + "integrity": "sha512-Qbo4NEj48SQ4k8FZJHFE2fgZDKTWaUGmVxcIQh3msg5JezLdTMMHuRRDYctfdHI6L0FZGObmEv3haWbIvmol8w==", + "license": "MIT", + "peerDependencies": { + "@jsamr/counter-style": "^1.0.0 || ^2.0.0", + "react": "*", + "react-native": "*" + } + }, + "node_modules/@native-html/css-processor": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@native-html/css-processor/-/css-processor-1.11.0.tgz", + "integrity": "sha512-NnhBEbJX5M2gBGltPKOetiLlKhNf3OHdRafc8//e2ZQxXN8JaSW/Hy8cm94pnIckQxwaMKxrtaNT3x4ZcffoNQ==", + "license": "MIT", + "dependencies": { + "css-to-react-native": "^3.0.0", + "csstype": "^3.0.8" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-native": "*" + } + }, + "node_modules/@native-html/transient-render-engine": { + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/@native-html/transient-render-engine/-/transient-render-engine-11.2.3.tgz", + "integrity": "sha512-zXwgA3gPUEmFs3I3syfnvDvS6WiUHXEE6jY09OBzK+trq7wkweOSFWIoyXiGkbXrozGYG0KY90YgPyr8Tg8Uyg==", + "license": "MIT", + "dependencies": { + "@native-html/css-processor": "1.11.0", + "@types/ramda": "^0.27.44", + "csstype": "^3.0.9", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "htmlparser2": "^7.1.2", + "ramda": "^0.27.2" + }, + "peerDependencies": { + "@types/react-native": "*", + "react-native": "^*" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.1.0.tgz", + "integrity": "sha512-eAGQGPTAuFNEoIQSB5j2Jh1zm5NPyBRTfjRMfCN0W1OakC5WIB5vsDyIQhUweKN9XOE2/V07lqTMGsL0dGXNkA==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, + "node_modules/@react-native-clipboard/clipboard": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.16.3.tgz", + "integrity": "sha512-cMIcvoZKIrShzJHEaHbTAp458R9WOv0fB6UyC7Ek4Qk561Ow/DrzmmJmH/rAZg21Z6ixJ4YSdFDC14crqIBmCQ==", + "license": "MIT", + "workspaces": [ + "example" + ], + "peerDependencies": { + "react": ">= 16.9.0", + "react-native": ">= 0.61.5", + "react-native-macos": ">= 0.61.0", + "react-native-windows": ">= 0.61.0" + }, + "peerDependenciesMeta": { + "react-native-macos": { + "optional": true + }, + "react-native-windows": { + "optional": true + } + } + }, + "node_modules/@react-native-community/cli": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-18.0.0.tgz", + "integrity": "sha512-DyKptlG78XPFo7tDod+we5a3R+U9qjyhaVFbOPvH4pFNu5Dehewtol/srl44K6Cszq0aEMlAJZ3juk0W4WnOJA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-clean": "18.0.0", + "@react-native-community/cli-config": "18.0.0", + "@react-native-community/cli-doctor": "18.0.0", + "@react-native-community/cli-server-api": "18.0.0", + "@react-native-community/cli-tools": "18.0.0", + "@react-native-community/cli-types": "18.0.0", + "chalk": "^4.1.2", + "commander": "^9.4.1", + "deepmerge": "^4.3.0", + "execa": "^5.0.0", + "find-up": "^5.0.0", + "fs-extra": "^8.1.0", + "graceful-fs": "^4.1.3", + "prompts": "^2.4.2", + "semver": "^7.5.2" + }, + "bin": { + "rnc-cli": "build/bin.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native-community/cli-clean": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-18.0.0.tgz", + "integrity": "sha512-+k64EnJaMI5U8iNDF9AftHBJW+pO/isAhncEXuKRc6IjRtIh6yoaUIIf5+C98fgjfux7CNRZAMQIkPbZodv2Gw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "18.0.0", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-glob": "^3.3.2" + } + }, + "node_modules/@react-native-community/cli-config": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-18.0.0.tgz", + "integrity": "sha512-GUGvyek06JRF4mfd9zXao9YQW4+H8ny69HznqNXVRtVSIIekFyjOpKQeSEzdcyqJEEa5gej22GOz1JCHMKBccg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "18.0.0", + "chalk": "^4.1.2", + "cosmiconfig": "^9.0.0", + "deepmerge": "^4.3.0", + "fast-glob": "^3.3.2", + "joi": "^17.2.1" + } + }, + "node_modules/@react-native-community/cli-config-android": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-config-android/-/cli-config-android-18.0.0.tgz", + "integrity": "sha512-pgnhEO2cmOeb+bBFEBZFYjeFjDTqWoV0JTorTiugj9bb4RQRCl8cr35baVlBGhxAuaio3722CsJ9GRF1oHjP8w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "18.0.0", + "chalk": "^4.1.2", + "fast-glob": "^3.3.2", + "fast-xml-parser": "^4.4.1" + } + }, + "node_modules/@react-native-community/cli-config-apple": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-config-apple/-/cli-config-apple-18.0.0.tgz", + "integrity": "sha512-6edjTt3mlNMFBuB/Xd6u0O7GEkhJiKvQgmcoBH18FsNy5cpiHDwQsG8EWM5cHeINp1gMK845qq9fFsTko6gqyQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "18.0.0", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-glob": "^3.3.2" + } + }, + "node_modules/@react-native-community/cli-doctor": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-18.0.0.tgz", + "integrity": "sha512-cD3LJfu2h2QSFmZai+fl7RrORKodd5XHSuB7Y9oF1zkebpRYN720vaUtK+GsepqBOElwKk5gl8uVolJ3j+xm8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-config": "18.0.0", + "@react-native-community/cli-platform-android": "18.0.0", + "@react-native-community/cli-platform-apple": "18.0.0", + "@react-native-community/cli-platform-ios": "18.0.0", + "@react-native-community/cli-tools": "18.0.0", + "chalk": "^4.1.2", + "command-exists": "^1.2.8", + "deepmerge": "^4.3.0", + "envinfo": "^7.13.0", + "execa": "^5.0.0", + "node-stream-zip": "^1.9.1", + "ora": "^5.4.1", + "semver": "^7.5.2", + "wcwidth": "^1.0.1", + "yaml": "^2.2.1" + } + }, + "node_modules/@react-native-community/cli-doctor/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@react-native-community/cli-platform-android": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-18.0.0.tgz", + "integrity": "sha512-3Y3RleN/des1C3oRS6s6fDvFYKN0KwsLrYFRpVx9vzdDnH1OGyFJOy4DbrruSPtdNiHUpvvHnOOxeLMj0+/tmw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-config-android": "18.0.0", + "@react-native-community/cli-tools": "18.0.0", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "logkitty": "^0.7.1" + } + }, + "node_modules/@react-native-community/cli-platform-apple": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-18.0.0.tgz", + "integrity": "sha512-zD18gdP5Wr8BSLJ79xtHuPYcg2Vi/nL+WsGsPm7TZjzR5ZU2WbY/tZ+qTGVTQYhQaah+92sU+Dam7gStMrF/fA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-config-apple": "18.0.0", + "@react-native-community/cli-tools": "18.0.0", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-xml-parser": "^4.4.1" + } + }, + "node_modules/@react-native-community/cli-platform-ios": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-18.0.0.tgz", + "integrity": "sha512-05Nvkkre/4Gao1TYqyP1wGet8td1dAH0CLJKqLNl9Te6ihnrQ8/8OhjIna5xw0iEFr9An8VdLfaPu1Dgv2qAnQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-platform-apple": "18.0.0" + } + }, + "node_modules/@react-native-community/cli-server-api": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-18.0.0.tgz", + "integrity": "sha512-tdmGV7ZntYmzrXWheZNpAy6dVI2yrsX4sQH+xAzU7lCfKHk6J8GucxedduXnB5qBB4JgSrrbzg2RLNxv5v0S/Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "18.0.0", + "body-parser": "^1.20.3", + "compression": "^1.7.1", + "connect": "^3.6.5", + "errorhandler": "^1.5.1", + "nocache": "^3.0.1", + "open": "^6.2.0", + "pretty-format": "^26.6.2", + "serve-static": "^1.13.1", + "ws": "^6.2.3" + } + }, + "node_modules/@react-native-community/cli-tools": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-18.0.0.tgz", + "integrity": "sha512-oR6FcDEcSDYos79vZy4+Tj8jgAE0Xf5HEiRXMJFGISYLRx7tvslSaK8SodUOW9TZe2bCZOb5QSvj8zeMpORmxg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@vscode/sudo-prompt": "^9.0.0", + "appdirsjs": "^1.2.4", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "find-up": "^5.0.0", + "launch-editor": "^2.9.1", + "mime": "^2.4.1", + "ora": "^5.4.1", + "prompts": "^2.4.2", + "semver": "^7.5.2" + } + }, + "node_modules/@react-native-community/cli-tools/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@react-native-community/cli-types": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-18.0.0.tgz", + "integrity": "sha512-J84+4IRXl8WlVdoe1maTD5skYZZO9CbQ6LNXEHx1kaZcFmvPZKfjsaxuyQ+8BsSqZnM2izOw8dEWnAp/Zuwb0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "joi": "^17.2.1" + } + }, + "node_modules/@react-native-community/cli/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@react-native-community/datetimepicker": { + "version": "8.4.4", + "resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-8.4.4.tgz", + "integrity": "sha512-bc4ZixEHxZC9/qf5gbdYvIJiLZ5CLmEsC3j+Yhe1D1KC/3QhaIfGDVdUcid0PdlSoGOSEq4VlB93AWyetEyBSQ==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, + "peerDependencies": { + "expo": ">=52.0.0", + "react": "*", + "react-native": "*", + "react-native-windows": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "react-native-windows": { + "optional": true + } + } + }, + "node_modules/@react-native-community/netinfo": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.4.1.tgz", + "integrity": "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg==", + "license": "MIT", + "peerDependencies": { + "react-native": ">=0.59" + } + }, + "node_modules/@react-native/assets-registry": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.0.tgz", + "integrity": "sha512-Rwvpu3A05lM1HVlX4klH4UR52JbQPDKc8gi2mst2REZL1KeVgJRJxPPw8d8euVlYcq/s8XI1Ol827JaRtSZBTA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/babel-plugin-codegen": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.79.0.tgz", + "integrity": "sha512-7IkObXF0dl5Dv1vGO5rBAB+yx26kqDntqrDvurO1ZjB11oeKiWOuDoWMnouaPZGhUbnswkYwMRLXCpYhDTG4bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@react-native/codegen": "0.79.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/babel-preset": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.79.0.tgz", + "integrity": "sha512-OcizKxBRxte1kZo932G4tpgDgKnDMErie0EkbVK83WaQAvnL0Dd1GWPoYjFmlKtJwh7PM2RZqTsrwqsksrmtRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/template": "^7.25.0", + "@react-native/babel-plugin-codegen": "0.79.0", + "babel-plugin-syntax-hermes-parser": "0.25.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/codegen": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.79.0.tgz", + "integrity": "sha512-D8bFlD0HH9SMUI00svdg64hEvLbu4ETeWQDlmEP8WmNbuILjwoLFqbnBmlGn69Tot0DM1PuBd1l1ooIzs8sU7w==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.1", + "hermes-parser": "0.25.1", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/community-cli-plugin": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.79.0.tgz", + "integrity": "sha512-pl+aSXxGj3ug80FpMDrArjxUbJWY2ibWiSP3MLKX+Xk7An2GUmFFjCzNVSbs0jzWv8814EG2oI60/GH2RXwE4g==", + "license": "MIT", + "dependencies": { + "@react-native/dev-middleware": "0.79.0", + "chalk": "^4.0.0", + "debug": "^2.2.0", + "invariant": "^2.2.4", + "metro": "^0.82.0", + "metro-config": "^0.82.0", + "metro-core": "^0.82.0", + "semver": "^7.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@react-native-community/cli": "*" + }, + "peerDependenciesMeta": { + "@react-native-community/cli": { + "optional": true + } + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/@react-native/community-cli-plugin/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@react-native/debugger-frontend": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.79.0.tgz", + "integrity": "sha512-chwKEWAmQMkOKZWwBra+utquuJ/2uFqh+ZgZbJfNX+U0YsBx6AQ3dVbfAaXW3bSLYEJyf9Wb3Opsal4fmcD9Ww==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/dev-middleware": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.79.0.tgz", + "integrity": "sha512-8Mh5L8zJXis2qhgkfXnWMbSmcvb07wrbxQe8KIgIO7C1rS97idg7BBtoPEtmARsaQgmbSGu/wdE7UWFkGYp0OQ==", + "license": "MIT", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.79.0", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^2.2.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "serve-static": "^1.16.2", + "ws": "^6.2.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/@react-native/dev-middleware/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native/eslint-config": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@react-native/eslint-config/-/eslint-config-0.79.0.tgz", + "integrity": "sha512-t0Em/GAuDESlWLvHf7Dr7L0OB2tWreLQeSKJZi2GKNEDmPV4NZFJFpD5tYxCiZsWuxUejuDMY4Uhvm46jgYb4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/eslint-parser": "^7.25.1", + "@react-native/eslint-plugin": "0.79.0", + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-ft-flow": "^2.0.1", + "eslint-plugin-jest": "^27.9.0", + "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-native": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": ">=8", + "prettier": ">=2" + } + }, + "node_modules/@react-native/eslint-plugin": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@react-native/eslint-plugin/-/eslint-plugin-0.79.0.tgz", + "integrity": "sha512-hbmVSkm3QE5u619CBQv9fVXE8AosuvU7a4Ju6UW3+CXtV/1pYVGsvPaQrKSH5nQAW29zTYKxwhkW7uo9kWrSFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.79.0.tgz", + "integrity": "sha512-c+/qKnmTx3kf8xZesp2BkZ9pAQVSnEPZziQUwviSJaq9jm8tKb/B8fyGG8yIuw/ZTKyGprD+ByzUSzJmCpC/Ow==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.79.0.tgz", + "integrity": "sha512-+8lk/zP90JC9xZBGhI8TPqqR1Y5dYXwXvfhXygr/LlHoo+H8TeQxcPrXWdT+PWOJl6Gf7dbCOGh9Std8J7CSQA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/metro-babel-transformer": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.79.0.tgz", + "integrity": "sha512-bv2y9WVmLXOZeEB6yi3Lol8nMjtYLTRY7Ws47x6MPNOG0XrXCsn/TmK704V2whq/GJsj22KMyvzkwG66DX6KSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@react-native/babel-preset": "0.79.0", + "hermes-parser": "0.25.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/metro-config": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.79.0.tgz", + "integrity": "sha512-MHevg80zRsyCsv8entCaa8W/dbf+ldnmrsQSllL4Qc8zJBhtXkqEHKDlY+W+qcKmsX48O6oI4PHER38sf3WO4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native/js-polyfills": "0.79.0", + "@react-native/metro-babel-transformer": "0.79.0", + "metro-config": "^0.82.0", + "metro-runtime": "^0.82.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.0.tgz", + "integrity": "sha512-RmM7Dgb69a4qwdguKR+8MhT0u1IAKa/s0uy8/7JP9b/fm8zjUV9HctMgRgIpZTOELsowEyQodyTnhHQf4HPX0A==", + "license": "MIT" + }, + "node_modules/@react-native/typescript-config": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@react-native/typescript-config/-/typescript-config-0.79.0.tgz", + "integrity": "sha512-Zt3TRh7MVuWNZgPbhYWPSCL14dS0CyXZymTi7KLI3Bq/41cCOfMj3JZxX6y76L8Hs0jG5fMIGJ+Hwt2gK5RCiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@react-native/virtualized-lists": { + "version": "0.72.8", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.72.8.tgz", + "integrity": "sha512-J3Q4Bkuo99k7mu+jPS9gSUSgq+lLRSI/+ahXNwV92XgJ/8UgOTxu2LPwhJnBk/sQKxq7E8WkZBnBiozukQMqrw==", + "license": "MIT", + "peer": true, + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "peerDependencies": { + "react-native": "*" + } + }, + "node_modules/@react-navigation/bottom-tabs": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.4.tgz", + "integrity": "sha512-/YEBu/cZUgYAaNoSfUnqoRjpbt8NOsb5YvDiKVyTcOOAF1GTbUw6kRi+AGW1Sm16CqzabO/TF2RvN1RmPS9VHg==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.6.1", + "color": "^4.2.3" + }, + "peerDependencies": { + "@react-navigation/native": "^7.1.16", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/core": { + "version": "7.12.3", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.12.3.tgz", + "integrity": "sha512-oEz5sL8KTYmCv8SQX1A4k75A7VzYadOCudp/ewOBqRXOmZdxDQA9JuN7baE9IVyaRW0QTVDy+N/Wnqx9F4aW6A==", + "license": "MIT", + "dependencies": { + "@react-navigation/routers": "^7.5.1", + "escape-string-regexp": "^4.0.0", + "nanoid": "^3.3.11", + "query-string": "^7.1.3", + "react-is": "^19.1.0", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "react": ">= 18.2.0" + } + }, + "node_modules/@react-navigation/core/node_modules/react-is": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", + "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", + "license": "MIT" + }, + "node_modules/@react-navigation/elements": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.6.1.tgz", + "integrity": "sha512-kVbIo+5FaqJv6MiYUR6nQHiw+10dmmH/P10C29wrH9S6fr7k69fImHGeiOI/h7SMDJ2vjWhftyEjqYO+c2LG/w==", + "license": "MIT", + "dependencies": { + "color": "^4.2.3", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@react-native-masked-view/masked-view": ">= 0.2.0", + "@react-navigation/native": "^7.1.16", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0" + }, + "peerDependenciesMeta": { + "@react-native-masked-view/masked-view": { + "optional": true + } + } + }, + "node_modules/@react-navigation/native": { + "version": "7.1.16", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.16.tgz", + "integrity": "sha512-JnnK81JYJ6PiMsuBEshPGHwfagRnH8W7SYdWNrPxQdNtakkHtG4u0O9FmrOnKiPl45DaftCcH1g+OVTFFgWa0Q==", + "license": "MIT", + "dependencies": { + "@react-navigation/core": "^7.12.3", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "use-latest-callback": "^0.2.4" + }, + "peerDependencies": { + "react": ">= 18.2.0", + "react-native": "*" + } + }, + "node_modules/@react-navigation/native-stack": { + "version": "7.3.23", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.3.23.tgz", + "integrity": "sha512-WQBBnPrlM0vXj5YAFnJTyrkiCyANl2KnBV8ZmUG61HkqXFwuBbnHij6eoggXH1VZkEVRxW8k0E3qqfPtEZfUjQ==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.6.1", + "warn-once": "^0.1.1" + }, + "peerDependencies": { + "@react-navigation/native": "^7.1.16", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/routers": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.1.tgz", + "integrity": "sha512-pxipMW/iEBSUrjxz2cDD7fNwkqR4xoi0E/PcfTQGCcdJwLoaxzab5kSadBLj1MTJyT0YRrOXL9umHpXtp+Dv4w==", + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11" + } + }, + "node_modules/@react-navigation/stack": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-7.4.4.tgz", + "integrity": "sha512-2TjeTRCjE6W8OgbZ3dt88FwESVqbZLOQkVijNdj0xSQ67awDwNmunyp5vsvNsIlXUNsc21w/iddr25euK0YwkA==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.6.1", + "color": "^4.2.3" + }, + "peerDependencies": { + "@react-navigation/native": "^7.1.16", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-gesture-handler": ">= 2.0.0", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", + "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "devOptional": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "devOptional": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@testing-library/react-native": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-13.2.1.tgz", + "integrity": "sha512-gryBmOI/PFvEJmMy6CNruulxXTW3zs7YyZHJEUlVYEt9vWo5v09FvV0RlYHurxs3yx3ZSc0vOsQOhld5a5SfPg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "jest-matcher-utils": "^30.0.2", + "pretty-format": "^30.0.2", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "jest": ">=29.0.0", + "react": ">=18.2.0", + "react-native": ">=0.71", + "react-test-renderer": ">=18.2.0" + }, + "peerDependenciesMeta": { + "jest": { + "optional": true + } + } + }, + "node_modules/@testing-library/react-native/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@testing-library/react-native/node_modules/@sinclair/typebox": { + "version": "0.34.38", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", + "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", + "license": "MIT" + }, + "node_modules/@testing-library/react-native/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/react-native/node_modules/jest-diff": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz", + "integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==", + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@testing-library/react-native/node_modules/jest-matcher-utils": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz", + "integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==", + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "jest-diff": "30.0.5", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@testing-library/react-native/node_modules/pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@testing-library/react-native/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.0.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.12.tgz", + "integrity": "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/ramda": { + "version": "0.27.66", + "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.27.66.tgz", + "integrity": "sha512-i2YW+E2U6NfMt3dp0RxNcejox+bxJUNDjB7BpYuRuoHIzv5juPHkJkNgcUOu+YSQEmaWu8cnAo/8r63C0NnuVA==", + "license": "MIT", + "dependencies": { + "ts-toolbelt": "^6.15.1" + } + }, + "node_modules/@types/react": { + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-native": { + "version": "0.72.8", + "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.72.8.tgz", + "integrity": "sha512-St6xA7+EoHN5mEYfdWnfYt0e8u6k2FR0P9s2arYgakQGFgU1f9FlPrIEcj0X24pLCF5c5i3WVuLCUdiCYHmOoA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@react-native/virtualized-lists": "^0.72.4", + "@types/react": "*" + } + }, + "node_modules/@types/react-test-renderer": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz", + "integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "license": "MIT" + }, + "node_modules/@types/urijs": { + "version": "1.19.25", + "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.25.tgz", + "integrity": "sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==", + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vscode/sudo-prompt": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.1.tgz", + "integrity": "sha512-9ORTwwS74VaTn38tNbQhsA5U44zkJfcb0BdTSyyG6frP4e8KMtHuTXYmwefe5dpL8XB1aGSIVTaLjD3BbWb5iA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/anser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", + "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "devOptional": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-fragments": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-fragments/-/ansi-fragments-0.2.1.tgz", + "integrity": "sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "colorette": "^1.0.7", + "slice-ansi": "^2.0.0", + "strip-ansi": "^5.0.0" + } + }, + "node_modules/ansi-fragments/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-fragments/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apisauce": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/apisauce/-/apisauce-3.2.0.tgz", + "integrity": "sha512-uEvNyBl86g9znFzb5DsBN0kaC9cs9Seo6Ztippf1lJgMEj/mcwE7YMsv4NZeKHjpxGOQ4AHDPItnDRiEgNIdDA==", + "license": "MIT", + "dependencies": { + "axios": "^1.10.0" + } + }, + "node_modules/appdirsjs": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/appdirsjs/-/appdirsjs-1.2.7.tgz", + "integrity": "sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "devOptional": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.25.1.tgz", + "integrity": "sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ==", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.25.1" + } + }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", + "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", + "license": "MIT", + "dependencies": { + "callsites": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-callsite/node_modules/callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", + "license": "MIT", + "dependencies": { + "caller-callsite": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/character-entities-html4": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz", + "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chrome-launcher/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chromium-edge-launcher": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", + "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "node_modules/chromium-edge-launcher/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", + "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.44.0.tgz", + "integrity": "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-select/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/css-select/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.181", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.181.tgz", + "integrity": "sha512-+ISMj8OIQ+0qEeDj14Rt8WwcTOiqHyAB+5bnK1K7xNNLjBJ4hRCQfUkw8RWtcLbfBzDwc15ZnKH0c7SNOfwiyA==", + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", + "devOptional": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/errorhandler": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.1.tgz", + "integrity": "sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.7", + "escape-html": "~1.0.3" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-eslint-comments": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz", + "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5", + "ignore": "^5.0.5" + }, + "engines": { + "node": ">=6.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-eslint-comments/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-plugin-ft-flow": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-ft-flow/-/eslint-plugin-ft-flow-2.0.3.tgz", + "integrity": "sha512-Vbsd/b+LYA99jUbsL6viEUWShFaYQt2YQs3QN3f+aeszOhh2sgdcU0mjzDyD4yyBvMc8qy2uwvBBWfMzEX06tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "string-natural-compare": "^3.0.1" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "@babel/eslint-parser": "^7.12.0", + "eslint": "^8.1.0" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "27.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz", + "integrity": "sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^5.10.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0 || ^7.0.0", + "eslint": "^7.0.0 || ^8.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-native": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-native/-/eslint-plugin-react-native-4.1.0.tgz", + "integrity": "sha512-QLo7rzTBOl43FvVqDdq5Ql9IoElIuTdjrz9SKAXCvULvBoRZ44JGSkx9z4999ZusCsb4rK3gjS8gOGyeYqZv2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-plugin-react-native-globals": "^0.1.1" + }, + "peerDependencies": { + "eslint": "^3.17.0 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-native-globals": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz", + "integrity": "sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "license": "Apache-2.0" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.1.1" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/flow-enums-runtime": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", + "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", + "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "entities": "^3.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsc-safe-url": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", + "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", + "license": "0BSD" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "devOptional": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/launch-editor": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz", + "integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/logkitty": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.7.1.tgz", + "integrity": "sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-fragments": "^0.2.1", + "dayjs": "^1.8.15", + "yargs": "^15.1.0" + }, + "bin": { + "logkitty": "bin/logkitty.js" + } + }, + "node_modules/logkitty/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/logkitty/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/logkitty/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/logkitty/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lottie-react-native": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/lottie-react-native/-/lottie-react-native-7.2.5.tgz", + "integrity": "sha512-S90gdsQ71PCG9r2OW01guA2mlHAiWDrYQ0acLa6mzf4q8y8RTlug3cL/eFHKxkmSgyy+unEBdPUcii+3YNWktA==", + "license": "Apache-2.0", + "peerDependencies": { + "@lottiefiles/dotlottie-react": "^0.6.5", + "react": "*", + "react-native": ">=0.46", + "react-native-windows": ">=0.63.x" + }, + "peerDependenciesMeta": { + "@lottiefiles/dotlottie-react": { + "optional": true + }, + "react-native-windows": { + "optional": true + } + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/metro": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.82.5.tgz", + "integrity": "sha512-8oAXxL7do8QckID/WZEKaIFuQJFUTLzfVcC48ghkHhNK2RGuQq8Xvf4AVd+TUA0SZtX0q8TGNXZ/eba1ckeGCg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "@babel/types": "^7.25.2", + "accepts": "^1.3.7", + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "error-stack-parser": "^2.0.6", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "hermes-parser": "0.29.1", + "image-size": "^1.0.2", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "jsc-safe-url": "^0.2.2", + "lodash.throttle": "^4.1.1", + "metro-babel-transformer": "0.82.5", + "metro-cache": "0.82.5", + "metro-cache-key": "0.82.5", + "metro-config": "0.82.5", + "metro-core": "0.82.5", + "metro-file-map": "0.82.5", + "metro-resolver": "0.82.5", + "metro-runtime": "0.82.5", + "metro-source-map": "0.82.5", + "metro-symbolicate": "0.82.5", + "metro-transform-plugins": "0.82.5", + "metro-transform-worker": "0.82.5", + "mime-types": "^2.1.27", + "nullthrows": "^1.1.1", + "serialize-error": "^2.1.0", + "source-map": "^0.5.6", + "throat": "^5.0.0", + "ws": "^7.5.10", + "yargs": "^17.6.2" + }, + "bin": { + "metro": "src/cli.js" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-babel-transformer": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.82.5.tgz", + "integrity": "sha512-W/scFDnwJXSccJYnOFdGiYr9srhbHPdxX9TvvACOFsIXdLilh3XuxQl/wXW6jEJfgIb0jTvoTlwwrqvuwymr6Q==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "hermes-parser": "0.29.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-babel-transformer/node_modules/hermes-estree": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz", + "integrity": "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==", + "license": "MIT" + }, + "node_modules/metro-babel-transformer/node_modules/hermes-parser": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.29.1.tgz", + "integrity": "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.29.1" + } + }, + "node_modules/metro-cache": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.82.5.tgz", + "integrity": "sha512-AwHV9607xZpedu1NQcjUkua8v7HfOTKfftl6Vc9OGr/jbpiJX6Gpy8E/V9jo/U9UuVYX2PqSUcVNZmu+LTm71Q==", + "license": "MIT", + "dependencies": { + "exponential-backoff": "^3.1.1", + "flow-enums-runtime": "^0.0.6", + "https-proxy-agent": "^7.0.5", + "metro-core": "0.82.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-cache-key": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.82.5.tgz", + "integrity": "sha512-qpVmPbDJuRLrT4kcGlUouyqLGssJnbTllVtvIgXfR7ZuzMKf0mGS+8WzcqzNK8+kCyakombQWR0uDd8qhWGJcA==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-config": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.82.5.tgz", + "integrity": "sha512-/r83VqE55l0WsBf8IhNmc/3z71y2zIPe5kRSuqA5tY/SL/ULzlHUJEMd1szztd0G45JozLwjvrhAzhDPJ/Qo/g==", + "license": "MIT", + "dependencies": { + "connect": "^3.6.5", + "cosmiconfig": "^5.0.5", + "flow-enums-runtime": "^0.0.6", + "jest-validate": "^29.7.0", + "metro": "0.82.5", + "metro-cache": "0.82.5", + "metro-core": "0.82.5", + "metro-runtime": "0.82.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/metro-config/node_modules/cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/metro-config/node_modules/import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "license": "MIT", + "dependencies": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/metro-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/metro-config/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/metro-config/node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/metro-core": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.82.5.tgz", + "integrity": "sha512-OJL18VbSw2RgtBm1f2P3J5kb892LCVJqMvslXxuxjAPex8OH7Eb8RBfgEo7VZSjgb/LOf4jhC4UFk5l5tAOHHA==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "lodash.throttle": "^4.1.1", + "metro-resolver": "0.82.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-file-map": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.82.5.tgz", + "integrity": "sha512-vpMDxkGIB+MTN8Af5hvSAanc6zXQipsAUO+XUx3PCQieKUfLwdoa8qaZ1WAQYRpaU+CJ8vhBcxtzzo3d9IsCIQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fb-watchman": "^2.0.0", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "nullthrows": "^1.1.1", + "walker": "^1.0.7" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-minify-terser": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.82.5.tgz", + "integrity": "sha512-v6Nx7A4We6PqPu/ta1oGTqJ4Usz0P7c+3XNeBxW9kp8zayS3lHUKR0sY0wsCHInxZlNAEICx791x+uXytFUuwg==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "terser": "^5.15.0" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-resolver": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.82.5.tgz", + "integrity": "sha512-kFowLnWACt3bEsuVsaRNgwplT8U7kETnaFHaZePlARz4Fg8tZtmRDUmjaD68CGAwc0rwdwNCkWizLYpnyVcs2g==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-runtime": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.82.5.tgz", + "integrity": "sha512-rQZDoCUf7k4Broyw3Ixxlq5ieIPiR1ULONdpcYpbJQ6yQ5GGEyYjtkztGD+OhHlw81LCR2SUAoPvtTus2WDK5g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-source-map": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.82.5.tgz", + "integrity": "sha512-wH+awTOQJVkbhn2SKyaw+0cd+RVSCZ3sHVgyqJFQXIee/yLs3dZqKjjeKKhhVeudgjXo7aE/vSu/zVfcQEcUfw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-symbolicate": "0.82.5", + "nullthrows": "^1.1.1", + "ob1": "0.82.5", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-source-map/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro-symbolicate": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.82.5.tgz", + "integrity": "sha512-1u+07gzrvYDJ/oNXuOG1EXSvXZka/0JSW1q2EYBWerVKMOhvv9JzDGyzmuV7hHbF2Hg3T3S2uiM36sLz1qKsiw==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-source-map": "0.82.5", + "nullthrows": "^1.1.1", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "bin": { + "metro-symbolicate": "src/index.js" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-symbolicate/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro-transform-plugins": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.82.5.tgz", + "integrity": "sha512-57Bqf3rgq9nPqLrT2d9kf/2WVieTFqsQ6qWHpEng5naIUtc/Iiw9+0bfLLWSAw0GH40iJ4yMjFcFJDtNSYynMA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "flow-enums-runtime": "^0.0.6", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-transform-worker": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.82.5.tgz", + "integrity": "sha512-mx0grhAX7xe+XUQH6qoHHlWedI8fhSpDGsfga7CpkO9Lk9W+aPitNtJWNGrW8PfjKEWbT9Uz9O50dkI8bJqigw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "metro": "0.82.5", + "metro-babel-transformer": "0.82.5", + "metro-cache": "0.82.5", + "metro-cache-key": "0.82.5", + "metro-minify-terser": "0.82.5", + "metro-source-map": "0.82.5", + "metro-transform-plugins": "0.82.5", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "license": "MIT" + }, + "node_modules/metro/node_modules/hermes-estree": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz", + "integrity": "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==", + "license": "MIT" + }, + "node_modules/metro/node_modules/hermes-parser": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.29.1.tgz", + "integrity": "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.29.1" + } + }, + "node_modules/metro/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "devOptional": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nocache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz", + "integrity": "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT" + }, + "node_modules/node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "license": "MIT" + }, + "node_modules/ob1": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.82.5.tgz", + "integrity": "sha512-QyQQ6e66f+Ut/qUVjEce0E/wux5nAGLXYZDn1jr15JWstHsCH3l6VVrg8NKDptW9NEiBXKOJeGF/ydxeSDF3IQ==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "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", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/pretty-format/node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/pretty-format/node_modules/@types/yargs": { + "version": "15.0.19", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", + "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "license": "MIT", + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/ramda": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.2.tgz", + "integrity": "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-devtools-core": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.5.tgz", + "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", + "license": "MIT", + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, + "node_modules/react-devtools-core/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/react-freeze": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz", + "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=17.0.0" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/react-native": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.79.0.tgz", + "integrity": "sha512-fLG/zl/YF30TWTmp2bbo3flHSFGe4WTyVkb7/wJnMEC39jjXVSCxfDtvSUVavhCc03fA/RTkWWvlmg7NEJk7Vg==", + "license": "MIT", + "dependencies": { + "@jest/create-cache-key-function": "^29.7.0", + "@react-native/assets-registry": "0.79.0", + "@react-native/codegen": "0.79.0", + "@react-native/community-cli-plugin": "0.79.0", + "@react-native/gradle-plugin": "0.79.0", + "@react-native/js-polyfills": "0.79.0", + "@react-native/normalize-colors": "0.79.0", + "@react-native/virtualized-lists": "0.79.0", + "abort-controller": "^3.0.0", + "anser": "^1.4.9", + "ansi-regex": "^5.0.0", + "babel-jest": "^29.7.0", + "babel-plugin-syntax-hermes-parser": "0.25.1", + "base64-js": "^1.5.1", + "chalk": "^4.0.0", + "commander": "^12.0.0", + "event-target-shim": "^5.0.1", + "flow-enums-runtime": "^0.0.6", + "glob": "^7.1.1", + "invariant": "^2.2.4", + "jest-environment-node": "^29.7.0", + "memoize-one": "^5.0.0", + "metro-runtime": "^0.82.0", + "metro-source-map": "^0.82.0", + "nullthrows": "^1.1.1", + "pretty-format": "^29.7.0", + "promise": "^8.3.0", + "react-devtools-core": "^6.1.1", + "react-refresh": "^0.14.0", + "regenerator-runtime": "^0.13.2", + "scheduler": "0.25.0", + "semver": "^7.1.3", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0", + "ws": "^6.2.3", + "yargs": "^17.6.2" + }, + "bin": { + "react-native": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^19.0.0", + "react": "^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-native-biometrics": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/react-native-biometrics/-/react-native-biometrics-3.0.1.tgz", + "integrity": "sha512-Ru80gXRa9KG04sl5AB9HyjLjVbduhqZVjA+AiOSGqr+fNqCDmCu9y5WEksnjbnniNLmq1yGcw+qcLXmR1ddLDQ==", + "license": "MIT", + "peerDependencies": { + "react-native": ">=0.60.0" + } + }, + "node_modules/react-native-blob-util": { + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/react-native-blob-util/-/react-native-blob-util-0.22.2.tgz", + "integrity": "sha512-Czx01QMg7aLsm/4F/7+eqoRAi1q/qjLY2Kao16g+n2SRnTH1+qkD8Qhx2q9okB+VNQvZKB1LbiXhktzYQV52xQ==", + "license": "MIT", + "dependencies": { + "base-64": "0.1.0", + "glob": "^10.3.10" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-blob-util/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "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", + "integrity": "sha512-dGdLnBU0cd5xL5bF0ROTmHYbsstZnQKOEPfglvZi1vStvAjpld14X25K6mY3KGPTMWAzx6TbjKeq5dR+ILuMMA==", + "license": "MIT", + "peerDependencies": { + "react-native-windows": ">=0.61" + }, + "peerDependenciesMeta": { + "react-native-windows": { + "optional": true + } + } + }, + "node_modules/react-native-device-info": { + "version": "10.14.0", + "resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-10.14.0.tgz", + "integrity": "sha512-9NnTGfhEU4UgQtz4p6COk2Gbqly0dpSWrJtp+dw5rNAi96KtYbaNnO5yoOHDlJ1SVIzh8+hFu3WxVbnWkFU9gA==", + "license": "MIT", + "peerDependencies": { + "react-native": "*" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.27.2.tgz", + "integrity": "sha512-+kNaY2m7uQu5+5ls8os6z92DTk9expsEAYsaPv30n08mrqX2r64G8iVGDwNWzZcId54+P7RlDnhyszTql0sQ0w==", + "license": "MIT", + "dependencies": { + "@egjs/hammerjs": "^2.0.17", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-image-picker": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-7.2.3.tgz", + "integrity": "sha512-zKIZUlQNU3EtqizsXSH92zPeve4vpUrsqHu2kkpCxWE9TZhJFZBb+irDsBOY8J21k0+Edgt06TMQGJ+iPUIXyA==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-is-edge-to-edge": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.7.tgz", + "integrity": "sha512-EH6i7E8epJGIcu7KpfXYXiV2JFIYITtq+rVS8uEb+92naMRBdxhTuS8Wn2Q7j9sqyO0B+Xbaaf9VdipIAmGW4w==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-keychain": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/react-native-keychain/-/react-native-keychain-10.0.0.tgz", + "integrity": "sha512-YzPKSAnSzGEJ12IK6CctNLU79T1W15WDrElRQ+1/FsOazGX9ucFPTQwgYe8Dy8jiSEDJKM4wkVa3g4lD2Z+Pnw==", + "license": "MIT", + "workspaces": [ + "KeychainExample", + "website" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/react-native-linear-gradient": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.3.tgz", + "integrity": "sha512-KflAXZcEg54PXkLyflaSZQ3PJp4uC4whM7nT/Uot9m0e/qxFV3p6uor1983D1YOBJbJN7rrWdqIjq0T42jOJyA==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-permissions": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-5.4.2.tgz", + "integrity": "sha512-XNMoG1fxrB9q73MLn/ZfTaP7pS8qPu0KWypbeFKVTvoR+JJ3O7uedMOTH/mts9bTG+GKhShOoZ+k0CR63q9jwA==", + "license": "MIT", + "peerDependencies": { + "react": ">=18.1.0", + "react-native": ">=0.70.0", + "react-native-windows": ">=0.70.0" + }, + "peerDependenciesMeta": { + "react-native-windows": { + "optional": true + } + } + }, + "node_modules/react-native-raw-bottom-sheet": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-native-raw-bottom-sheet/-/react-native-raw-bottom-sheet-3.0.0.tgz", + "integrity": "sha512-kHR7j2ExCLqf/AO3MECozMJXi48O1+YxUYSRgRo/5Ftm7mEcrxJEzvjqMmqUbVhhKlfk5hLCGFnEQ5Z9OHCUtg==", + "license": "MIT" + }, + "node_modules/react-native-reanimated": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.19.0.tgz", + "integrity": "sha512-FNfqLuPuVHsW9KcsZtnJqIPlMQvuySnSFJXgSt9fVDPqptbSUkiAF6MthUwd4Mxt05hCRcbV+T65CENgVS5iCg==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-arrow-functions": "^7.0.0-0", + "@babel/plugin-transform-class-properties": "^7.0.0-0", + "@babel/plugin-transform-classes": "^7.0.0-0", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", + "@babel/plugin-transform-optional-chaining": "^7.0.0-0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", + "@babel/plugin-transform-template-literals": "^7.0.0-0", + "@babel/plugin-transform-unicode-regex": "^7.0.0-0", + "@babel/preset-typescript": "^7.16.7", + "convert-source-map": "^2.0.0", + "invariant": "^2.2.4", + "react-native-is-edge-to-edge": "1.1.7" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0", + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-render-html": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/react-native-render-html/-/react-native-render-html-6.3.4.tgz", + "integrity": "sha512-H2jSMzZjidE+Wo3qCWPUMU1nm98Vs2SGCvQCz/i6xf0P3Y9uVtG/b0sDbG/cYFir2mSYBYCIlS1Dv0WC1LjYig==", + "license": "BSD-2-Clause", + "dependencies": { + "@jsamr/counter-style": "^2.0.1", + "@jsamr/react-native-li": "^2.3.0", + "@native-html/transient-render-engine": "11.2.3", + "@types/ramda": "^0.27.40", + "@types/urijs": "^1.19.15", + "prop-types": "^15.5.7", + "ramda": "^0.27.2", + "stringify-entities": "^3.1.0", + "urijs": "^1.19.6" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-responsive-dimensions": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/react-native-responsive-dimensions/-/react-native-responsive-dimensions-3.1.1.tgz", + "integrity": "sha512-Vo2OhWsphq0HgKsmeZOeyW+c+vsFn1ZaCFkGDgdeCEEiLriT76jGA1JlUjtrj27hvyo/xzeTlBZ+vBso1A84fw==", + "license": "MIT", + "peerDependencies": { + "react-native": ">=0.44.1" + } + }, + "node_modules/react-native-safe-area-context": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.5.2.tgz", + "integrity": "sha512-t4YVbHa9uAGf+pHMabGrb0uHrD5ogAusSu842oikJ3YKXcYp6iB4PTGl0EZNkUIR3pCnw/CXKn42OCfhsS0JIw==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-screens": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.13.1.tgz", + "integrity": "sha512-EESsMAtyzYcL3gpAI2NKKiIo+Ew0fnX4P4b3Zy/+MTc6SJIo3foJbZwdIWd/SUBswOf7IYCvWBppg+D8tbwnsw==", + "license": "MIT", + "dependencies": { + "react-freeze": "^1.0.0", + "react-native-is-edge-to-edge": "^1.2.1", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-screens/node_modules/react-native-is-edge-to-edge": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz", + "integrity": "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-share": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-12.1.1.tgz", + "integrity": "sha512-6nCdK2NYnvbsjMs9XEjnM0tCUF7AzoauxosL7mvBXSJWpjAfATZTJAMR7y5bjni0zDRJlI1bg2xt+0R1cF+sWA==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/react-native-svg": { + "version": "15.12.1", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz", + "integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "css-tree": "^1.1.3", + "warn-once": "0.1.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-toast-message": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/react-native-toast-message/-/react-native-toast-message-2.3.3.tgz", + "integrity": "sha512-4IIUHwUPvKHu4gjD0Vj2aGQzqPATiblL1ey8tOqsxOWRPGGu52iIbL8M/mCz4uyqecvPdIcMY38AfwRuUADfQQ==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-tts": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-native-tts/-/react-native-tts-4.1.1.tgz", + "integrity": "sha512-VL0TgCwkUWggbbFGIXAPKC3rM1baluAYtgOdgnaTm7UYsWf/y8n5VgmVB0J2Wa8qt1dldZ1cSsdQY9iz3evcAg==", + "license": "MIT" + }, + "node_modules/react-native-vector-icons": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz", + "integrity": "sha512-IFQ0RE57819hOUdFvgK4FowM5aMXg7C7XKsuGLevqXkkIJatc3QopN0wYrb2IrzUgmdpfP+QVIbI3S6h7M0btw==", + "deprecated": "react-native-vector-icons package has moved to a new model of per-icon-family packages. See the https://github.com/oblador/react-native-vector-icons/blob/master/MIGRATION.md on how to migrate", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2", + "yargs": "^16.1.1" + }, + "bin": { + "fa-upgrade.sh": "bin/fa-upgrade.sh", + "fa5-upgrade": "bin/fa5-upgrade.sh", + "fa6-upgrade": "bin/fa6-upgrade.sh", + "generate-icon": "bin/generate-icon.js" + } + }, + "node_modules/react-native-vector-icons/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/react-native-vector-icons/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native-vector-icons/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native-webview": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.15.0.tgz", + "integrity": "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "invariant": "2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native/node_modules/@react-native/virtualized-lists": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.0.tgz", + "integrity": "sha512-tCT1sHSI1O5KSclDwNfnkLTLe3cgiyYWjIlmNxWJHqhCCz017HGOS/oH0zs0ZgxYwN7xCzTkqY330XMDo+yj2g==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^19.0.0", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-native/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/react-native/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/react-native/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/react-native/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-native/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-test-renderer": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-19.0.0.tgz", + "integrity": "sha512-oX5u9rOQlHzqrE/64CNr0HB0uWxkCQmZNSfozlYvwE71TLVgeZxVf0IjouGEr1v7r1kcDifdAJBeOhdhxsG/DA==", + "license": "MIT", + "dependencies": { + "react-is": "^19.0.0", + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-test-renderer/node_modules/react-is": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", + "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-persist": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "license": "MIT", + "peerDependencies": { + "redux": ">4.0.0" + } + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "devOptional": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", + "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" + }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/stacktrace-parser/node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-natural-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", + "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-entities": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-3.1.0.tgz", + "integrity": "sha512-3FP+jGMmMV/ffZs86MoghGqAoqXAdxLrJP4GUdrDN1aIScYih5tuIO3eF4To5AJZ79KDZ8Fpdy7QJnK8SsL1Vg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/terser": { + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-toolbelt": { + "version": "6.15.5", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz", + "integrity": "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "license": "MIT" + }, + "node_modules/use-latest-callback": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.4.tgz", + "integrity": "sha512-LS2s2n1usUUnDq4oVh1ca6JFX9uSqUncTfAm44WMg0v6TxL7POUTk1B044NH8TeLkFbNajIsgDHcgNpNzZucdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vlq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", + "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", + "license": "MIT" + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/warn-once": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz", + "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", + "license": "MIT" + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "devOptional": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..322d181 --- /dev/null +++ b/package.json @@ -0,0 +1,76 @@ +{ + "name": "NeoScan_Physician", + "version": "0.0.1", + "private": true, + "scripts": { + "android": "react-native run-android", + "ios": "react-native run-ios", + "lint": "eslint .", + "start": "react-native start", + "test": "jest" + }, + "dependencies": { + "@react-native-async-storage/async-storage": "^2.1.0", + "@react-native-clipboard/clipboard": "^1.16.1", + "@react-native-community/datetimepicker": "^8.4.4", + "@react-native-community/netinfo": "^11.4.1", + "@react-navigation/bottom-tabs": "^7.2.0", + "@react-navigation/native": "^7.0.14", + "@react-navigation/native-stack": "^7.2.0", + "@react-navigation/stack": "^7.1.1", + "@reduxjs/toolkit": "^2.5.1", + "@testing-library/react-native": "^13.2.0", + "apisauce": "^3.1.0", + "lottie-react-native": "^7.2.2", + "react": "19.0.0", + "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-device-info": "^10.11.0", + "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", + "react-native-linear-gradient": "^2.8.3", + "react-native-permissions": "^5.2.4", + "react-native-raw-bottom-sheet": "^3.0.0", + "react-native-reanimated": "^3.18.0", + "react-native-render-html": "^6.3.4", + "react-native-responsive-dimensions": "^3.1.1", + "react-native-safe-area-context": "^5.1.0", + "react-native-screens": "^4.5.0", + "react-native-share": "^12.0.9", + "react-native-svg": "^15.11.1", + "react-native-toast-message": "^2.2.1", + "react-native-tts": "^4.1.1", + "react-native-vector-icons": "^10.2.0", + "react-native-webview": "^13.15.0", + "react-redux": "^9.2.0", + "redux-persist": "^6.0.0" + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.25.3", + "@babel/runtime": "^7.25.0", + "@react-native-community/cli": "18.0.0", + "@react-native-community/cli-platform-android": "18.0.0", + "@react-native-community/cli-platform-ios": "18.0.0", + "@react-native/babel-preset": "0.79.0", + "@react-native/eslint-config": "0.79.0", + "@react-native/metro-config": "0.79.0", + "@react-native/typescript-config": "0.79.0", + "@types/jest": "^29.5.13", + "@types/react": "^19.0.0", + "@types/react-test-renderer": "^19.0.0", + "eslint": "^8.19.0", + "jest": "^29.6.3", + "prettier": "2.8.8", + "react-test-renderer": "19.0.0", + "typescript": "5.0.4" + }, + "engines": { + "node": ">=18" + } +} diff --git a/react-native.config.js b/react-native.config.js new file mode 100644 index 0000000..a1ab960 --- /dev/null +++ b/react-native.config.js @@ -0,0 +1,7 @@ +module.exports = { + project: { + ios: {}, + android: {}, + }, + assets: ['./app/assets/fonts'], // adjust according to your path + }; \ No newline at end of file diff --git a/setup.bat b/setup.bat new file mode 100644 index 0000000..943e39f --- /dev/null +++ b/setup.bat @@ -0,0 +1,32 @@ +@echo off +echo ๐Ÿš€ Setting up NeoScan Physician App... + +REM Check if Node.js is installed +node --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo โŒ Node.js is not installed. Please install Node.js ^>= 18 first. + pause + exit /b 1 +) + +echo โœ… Node.js version: +node --version + +REM Install dependencies +echo ๐Ÿ“ฆ Installing dependencies... +npm install + +if %errorlevel% neq 0 ( + echo โŒ Failed to install dependencies. + pause + exit /b 1 +) + +echo โœ… Setup completed successfully! +echo. +echo ๐ŸŽฏ Next steps: +echo 1. Start the development server: npm start +echo 2. Run on Android: npm run android +echo. +echo ๐Ÿ“ฑ The app will open with a login screen. Use any credentials to proceed to the dashboard. +pause \ No newline at end of file diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..4b68cef --- /dev/null +++ b/setup.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +echo "๐Ÿš€ Setting up NeoScan Physician App..." + +# Check if Node.js is installed +if ! command -v node &> /dev/null; then + echo "โŒ Node.js is not installed. Please install Node.js >= 18 first." + exit 1 +fi + +# Check Node.js version +NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) +if [ "$NODE_VERSION" -lt 18 ]; then + echo "โŒ Node.js version 18 or higher is required. Current version: $(node -v)" + exit 1 +fi + +echo "โœ… Node.js version: $(node -v)" + +# Install dependencies +echo "๐Ÿ“ฆ Installing dependencies..." +npm install + +# iOS setup (macOS only) +if [[ "$OSTYPE" == "darwin"* ]]; then + echo "๐ŸŽ Setting up iOS dependencies..." + cd ios + pod install + cd .. + echo "โœ… iOS setup completed" +else + echo "โ„น๏ธ Skipping iOS setup (not on macOS)" +fi + +echo "โœ… Setup completed successfully!" +echo "" +echo "๐ŸŽฏ Next steps:" +echo "1. Start the development server: npm start" +echo "2. Run on Android: npm run android" +echo "3. Run on iOS (macOS only): npm run ios" +echo "" +echo "๐Ÿ“ฑ The app will open with a login screen. Use any credentials to proceed to the dashboard." \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ee46d0f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@react-native/typescript-config/tsconfig.json", + "compilerOptions": { + "typeRoots": ["./app/types", "./node_modules/@types"] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + "app/types/*.d.ts" + ] +}