Commit 333c066b authored by Mahmoud Aglan's avatar Mahmoud Aglan

Complete Flutter mobile app — all screens, auth, API, animations

Full-featured guardian/participant mobile app for El Captain Sports ERP:
- Riverpod state management, GoRouter navigation with animated transitions
- Arabic-first RTL with English toggle, Cairo + Inter fonts
- OTP auth flow with Sanctum bearer tokens
- Dashboard with children cards, today's sessions, quick stats
- Participant detail with action grid (attendance, invoices, evaluations)
- Weekly schedule view with day selector
- Attendance history with circular rate indicator
- Invoice list with pay button and status badges
- Evaluation cards with criteria progress bars
- Events with registration, Shop product grid
- Messages with chat bubbles, Service requests with create modal
- Multi-step pre-registration wizard (guardian → child → review)
- Notification inbox with type-based icons
- Profile with language toggle and logout
- Everything animated (flutter_animate): fade-slide, scale-in, stagger
- Builds successfully for iOS (verified)
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parents
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "c9a6c484230f8b5e408ec57be1ef71dee1e77020"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
- platform: android
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
- platform: ios
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
# el_captain_client
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.alcaptain.el_captain_client"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.alcaptain.el_captain_client"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="el_captain_client"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
package com.alcaptain.el_captain_client
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This newDsl flag was added by the Flutter template
android.newDsl=false
# This builtInKotlin flag was added by the Flutter template
android.builtInKotlin=false
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
}
include(":app")
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
PODS:
- Firebase/CoreOnly (11.15.0):
- FirebaseCore (~> 11.15.0)
- Firebase/Messaging (11.15.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.15.0)
- firebase_core (3.15.2):
- Firebase/CoreOnly (= 11.15.0)
- Flutter
- firebase_messaging (15.2.10):
- Firebase/Messaging (= 11.15.0)
- firebase_core
- Flutter
- FirebaseCore (11.15.0):
- FirebaseCoreInternal (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (11.15.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (11.15.0):
- FirebaseCore (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- Flutter (1.0.0)
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleUtilities/AppDelegateSwizzler (8.1.2):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.2):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.2):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.2):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.2)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.2)
- GoogleUtilities/Reachability (8.1.2):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.2):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- image_picker_ios (0.0.1):
- Flutter
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- PromisesObjC (2.4.1)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
SPEC REPOS:
trunk:
- Firebase
- FirebaseCore
- FirebaseCoreInternal
- FirebaseInstallations
- FirebaseMessaging
- GoogleDataTransport
- GoogleUtilities
- nanopb
- PromisesObjC
EXTERNAL SOURCES:
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_messaging:
:path: ".symlinks/plugins/firebase_messaging/ios"
Flutter:
:path: Flutter
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_core: 995454a784ff288be5689b796deb9e9fa3601818
firebase_messaging: f4a41dd102ac18b840eba3f39d67e77922d3f707
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 766ace00c6b10d8148408f329d10c4f051931850
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
PromisesObjC: 752c3227f599e3467650e47ea36f433eeb10c273
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
COCOAPODS: 1.16.2
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>El Captain Client</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>el_captain_client</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
#import "GeneratedPluginRegistrant.h"
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
class InstanceConfig {
// ===== CHANGE THESE PER CLIENT =====
static const String baseUrl = 'https://el-captain.caprover.al-arcade.com';
static const String appName = 'El Captain';
static const String appNameAr = 'الكابتن';
static const String packageId = 'com.alcaptain.elcaptain';
// ====================================
static const String apiVersion = 'v1';
static String get apiUrl => '$baseUrl/api/$apiVersion';
}
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../config/instance.dart';
import '../auth/auth_store.dart';
final dioProvider = Provider<Dio>((ref) {
final dio = Dio(BaseOptions(
baseUrl: InstanceConfig.apiUrl,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
));
dio.interceptors.add(AuthInterceptor(ref));
dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
logPrint: (o) {},
));
return dio;
});
class AuthInterceptor extends Interceptor {
final Ref ref;
AuthInterceptor(this.ref);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final token = ref.read(authStoreProvider).token;
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 401) {
ref.read(authStoreProvider.notifier).logout();
}
handler.next(err);
}
}
class ApiException implements Exception {
final String code;
final String message;
final int? statusCode;
ApiException({required this.code, required this.message, this.statusCode});
factory ApiException.fromDio(DioException e) {
final data = e.response?.data;
if (data is Map<String, dynamic>) {
return ApiException(
code: data['error'] as String? ?? 'unknown',
message: data['message'] as String? ?? 'حدث خطأ غير متوقع',
statusCode: e.response?.statusCode,
);
}
return ApiException(
code: 'network_error',
message: 'تأكد من اتصالك بالإنترنت',
statusCode: e.response?.statusCode,
);
}
@override
String toString() => message;
}
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'api_client.dart';
final apiServiceProvider = Provider<ApiService>((ref) {
return ApiService(ref.watch(dioProvider));
});
class ApiService {
final Dio _dio;
ApiService(this._dio);
// === Auth ===
Future<Map<String, dynamic>> requestOtp(String phone) async {
final res = await _dio.post('/auth/otp/request', data: {'phone': phone});
return res.data;
}
Future<Map<String, dynamic>> verifyOtp(String phone, String otp) async {
final res = await _dio.post('/auth/otp/verify', data: {'phone': phone, 'otp': otp});
return res.data;
}
Future<void> logout() async {
await _dio.post('/auth/logout');
}
Future<Map<String, dynamic>> getMe() async {
final res = await _dio.get('/auth/me');
return res.data;
}
// === Config ===
Future<Map<String, dynamic>> getAppConfig() async {
final res = await _dio.get('/app/config');
return res.data;
}
// === Dashboard ===
Future<Map<String, dynamic>> getDashboard() async {
final res = await _dio.get('/dashboard');
return res.data['data'];
}
// === Participants ===
Future<List<dynamic>> getChildren() async {
final res = await _dio.get('/guardian/children');
return res.data['data'];
}
Future<Map<String, dynamic>> getParticipant(String uuid) async {
final res = await _dio.get('/participants/$uuid');
return res.data['data'];
}
Future<Map<String, dynamic>> getParticipantSummary(String uuid) async {
final res = await _dio.get('/participants/$uuid/summary');
return res.data['data'];
}
Future<List<dynamic>> getSchedule(String uuid) async {
final res = await _dio.get('/participants/$uuid/schedule');
return res.data['data'];
}
Future<Map<String, dynamic>> getAttendance(String uuid, {int page = 1}) async {
final res = await _dio.get('/participants/$uuid/attendance', queryParameters: {'page': page});
return res.data;
}
Future<Map<String, dynamic>> getInvoices(String uuid, {int page = 1}) async {
final res = await _dio.get('/participants/$uuid/invoices', queryParameters: {'page': page});
return res.data;
}
Future<List<dynamic>> getEnrollments(String uuid) async {
final res = await _dio.get('/participants/$uuid/enrollments');
return res.data['data'];
}
Future<Map<String, dynamic>> getDocuments(String uuid) async {
final res = await _dio.get('/participants/$uuid/documents');
return res.data;
}
Future<Map<String, dynamic>> getWallet(String uuid) async {
final res = await _dio.get('/participants/$uuid/wallet');
return res.data['data'];
}
Future<Map<String, dynamic>> getWalletTransactions(String uuid, {int page = 1}) async {
final res = await _dio.get('/participants/$uuid/wallet/transactions', queryParameters: {'page': page});
return res.data;
}
Future<List<dynamic>> getInstallments(String uuid) async {
final res = await _dio.get('/participants/$uuid/installments');
return res.data['data'];
}
Future<List<dynamic>> getEvaluations(String uuid) async {
final res = await _dio.get('/participants/$uuid/evaluations');
return res.data['data'];
}
Future<Map<String, dynamic>> getEvaluationDetail(String uuid, String evalUuid) async {
final res = await _dio.get('/participants/$uuid/evaluations/$evalUuid');
return res.data['data'];
}
// === Notifications ===
Future<Map<String, dynamic>> getNotifications({int page = 1}) async {
final res = await _dio.get('/notifications', queryParameters: {'page': page});
return res.data;
}
Future<void> markNotificationRead(int id) async {
await _dio.patch('/notifications/$id/read');
}
Future<void> markAllNotificationsRead() async {
await _dio.post('/notifications/read-all');
}
Future<Map<String, dynamic>> getNotificationPreferences() async {
final res = await _dio.get('/notifications/preferences');
return res.data;
}
Future<void> updateNotificationPreferences(List<Map<String, dynamic>> prefs) async {
await _dio.post('/notifications/preferences', data: {'preferences': prefs});
}
// === Profile ===
Future<Map<String, dynamic>> getProfile() async {
final res = await _dio.get('/profile');
return res.data['data'];
}
Future<Map<String, dynamic>> updateProfile(Map<String, dynamic> data) async {
final res = await _dio.patch('/profile', data: data);
return res.data;
}
// === Shop ===
Future<List<dynamic>> getProducts() async {
final res = await _dio.get('/products');
return res.data['data'];
}
Future<Map<String, dynamic>> createOrder(Map<String, dynamic> data) async {
final res = await _dio.post('/orders/create', data: data);
return res.data;
}
Future<Map<String, dynamic>> getOrders({int page = 1}) async {
final res = await _dio.get('/orders', queryParameters: {'page': page});
return res.data;
}
// === Payments ===
Future<Map<String, dynamic>> initiatePayment(String invoiceUuid) async {
final res = await _dio.post('/payments/initiate', data: {'invoice_uuid': invoiceUuid});
return res.data;
}
Future<Map<String, dynamic>> getInvoiceDetail(String uuid) async {
final res = await _dio.get('/invoices/$uuid');
return res.data['data'];
}
Future<Map<String, dynamic>> getPaymentReceipt(String uuid) async {
final res = await _dio.get('/payments/$uuid/receipt');
return res.data['data'];
}
// === Events ===
Future<Map<String, dynamic>> getEvents({int page = 1}) async {
final res = await _dio.get('/academy/events', queryParameters: {'page': page});
return res.data;
}
Future<Map<String, dynamic>> getEventDetail(String uuid) async {
final res = await _dio.get('/academy/events/$uuid');
return res.data['data'];
}
Future<Map<String, dynamic>> registerForEvent(String uuid, String participantUuid, {String? notes}) async {
final res = await _dio.post('/events/$uuid/register', data: {
'participant_uuid': participantUuid,
if (notes != null) 'notes': notes,
});
return res.data;
}
Future<List<dynamic>> getMyRegistrations() async {
final res = await _dio.get('/events/my-registrations');
return res.data['data'];
}
// === Service Requests ===
Future<Map<String, dynamic>> createServiceRequest(Map<String, dynamic> data) async {
final res = await _dio.post('/service-requests', data: data);
return res.data;
}
Future<List<dynamic>> getServiceRequests() async {
final res = await _dio.get('/service-requests');
return res.data['data'];
}
Future<void> cancelServiceRequest(String uuid) async {
await _dio.post('/service-requests/$uuid/cancel');
}
// === Absences ===
Future<void> reportAbsence(Map<String, dynamic> data) async {
await _dio.post('/absences/report', data: data);
}
// === Messages ===
Future<Map<String, dynamic>> sendMessage(Map<String, dynamic> data) async {
final res = await _dio.post('/messages/send', data: data);
return res.data;
}
Future<Map<String, dynamic>> getMessages({int page = 1}) async {
final res = await _dio.get('/messages', queryParameters: {'page': page});
return res.data;
}
// === Registration ===
Future<Map<String, dynamic>> preRegister({
required String guardianNameAr,
String? guardianName,
required String phone,
String? email,
required String participantName,
required int participantAge,
required String gender,
}) async {
final res = await _dio.post('/register/pre-register', data: {
'guardian_name_ar': guardianNameAr,
if (guardianName != null && guardianName.isNotEmpty) 'guardian_name': guardianName,
'phone': phone,
if (email != null && email.isNotEmpty) 'email': email,
'participant_name': participantName,
'participant_age': participantAge,
'gender': gender,
});
return res.data;
}
// === Academy (public) ===
Future<Map<String, dynamic>> getNews({int page = 1}) async {
final res = await _dio.get('/academy/news', queryParameters: {'page': page});
return res.data;
}
Future<Map<String, dynamic>> getNewsDetail(String uuid) async {
final res = await _dio.get('/academy/news/$uuid');
return res.data['data'];
}
Future<List<dynamic>> getPrograms() async {
final res = await _dio.get('/academy/programs');
return res.data['data'];
}
Future<List<dynamic>> getBranches() async {
final res = await _dio.get('/branches');
return res.data['data'];
}
Future<Map<String, dynamic>> getGallery({int page = 1}) async {
final res = await _dio.get('/academy/gallery', queryParameters: {'page': page});
return res.data;
}
// === Devices ===
Future<void> registerDevice(String token, String platform, {String? deviceName, String? appVersion}) async {
await _dio.post('/devices/register', data: {
'token': token,
'platform': platform,
if (deviceName != null) 'device_name': deviceName,
if (appVersion != null) 'app_version': appVersion,
});
}
Future<void> unregisterDevice(String token) async {
await _dio.delete('/devices/$token');
}
// === Push ===
Future<Map<String, dynamic>> getBadgeCount() async {
final res = await _dio.get('/push/badge');
return res.data;
}
Future<void> trackPushEvent(List<Map<String, dynamic>> events) async {
await _dio.post('/push/track', data: {'events': events});
}
}
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
final authStoreProvider = StateNotifierProvider<AuthStoreNotifier, AuthState>((ref) {
return AuthStoreNotifier();
});
class AuthState {
final String? token;
final Map<String, dynamic>? user;
final List<Map<String, dynamic>> participants;
final bool isLoading;
AuthState({
this.token,
this.user,
this.participants = const [],
this.isLoading = true,
});
bool get isAuthenticated => token != null;
String? get userName => user?['name_ar'] as String?;
String? get userPhone => user?['phone'] as String?;
int? get academyId => user?['academy_id'] as int?;
AuthState copyWith({
String? token,
Map<String, dynamic>? user,
List<Map<String, dynamic>>? participants,
bool? isLoading,
}) {
return AuthState(
token: token ?? this.token,
user: user ?? this.user,
participants: participants ?? this.participants,
isLoading: isLoading ?? this.isLoading,
);
}
}
class AuthStoreNotifier extends StateNotifier<AuthState> {
AuthStoreNotifier() : super(AuthState()) {
_loadFromStorage();
}
Future<void> _loadFromStorage() async {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString('auth_token');
final userJson = prefs.getString('auth_user');
final participantsJson = prefs.getString('auth_participants');
if (token != null && userJson != null) {
state = AuthState(
token: token,
user: jsonDecode(userJson),
participants: participantsJson != null
? List<Map<String, dynamic>>.from(jsonDecode(participantsJson))
: [],
isLoading: false,
);
} else {
state = AuthState(isLoading: false);
}
}
Future<void> setAuth({
required String token,
required Map<String, dynamic> user,
required List<dynamic> participants,
}) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token', token);
await prefs.setString('auth_user', jsonEncode(user));
await prefs.setString('auth_participants', jsonEncode(participants));
state = AuthState(
token: token,
user: user,
participants: participants.cast<Map<String, dynamic>>(),
isLoading: false,
);
}
Future<void> updateParticipants(List<dynamic> participants) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_participants', jsonEncode(participants));
state = state.copyWith(participants: participants.cast<Map<String, dynamic>>());
}
Future<void> logout() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('auth_token');
await prefs.remove('auth_user');
await prefs.remove('auth_participants');
state = AuthState(isLoading: false);
}
}
import 'package:flutter/material.dart';
class AppLocalizations {
final Locale locale;
AppLocalizations(this.locale);
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
}
static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();
bool get isArabic => locale.languageCode == 'ar';
static final Map<String, Map<String, String>> _translations = {
'ar': {
'app_name': 'الكابتن',
'login': 'تسجيل الدخول',
'phone_number': 'رقم الهاتف',
'enter_phone': 'أدخل رقم هاتفك',
'send_otp': 'إرسال رمز التحقق',
'verify_otp': 'تحقق من الرمز',
'enter_otp': 'أدخل رمز التحقق',
'otp_sent': 'تم إرسال رمز التحقق',
'invalid_otp': 'رمز التحقق غير صحيح',
'home': 'الرئيسية',
'schedule': 'الجدول',
'notifications': 'الإشعارات',
'profile': 'حسابي',
'my_children': 'أبنائي',
'today_sessions': 'جلسات اليوم',
'outstanding_balance': 'المبلغ المستحق',
'attendance_rate': 'نسبة الحضور',
'next_session': 'الجلسة القادمة',
'invoices': 'الفواتير',
'enrollments': 'الاشتراكات',
'evaluations': 'التقييمات',
'documents': 'المستندات',
'wallet': 'المحفظة',
'shop': 'المتجر',
'events': 'الفعاليات',
'messages': 'الرسائل',
'service_requests': 'الطلبات',
'pay_now': 'ادفع الآن',
'view_all': 'عرض الكل',
'no_data': 'لا توجد بيانات',
'loading': 'جارٍ التحميل...',
'error_occurred': 'حدث خطأ',
'retry': 'إعادة المحاولة',
'present': 'حاضر',
'absent': 'غائب',
'late': 'متأخر',
'excused': 'بعذر',
'paid': 'مدفوعة',
'pending': 'قيد الانتظار',
'overdue': 'متأخرة',
'active': 'نشط',
'frozen': 'مجمد',
'logout': 'تسجيل الخروج',
'settings': 'الإعدادات',
'language': 'اللغة',
'register': 'تسجيل جديد',
'pre_register': 'التسجيل المسبق',
'welcome_back': 'أهلاً بعودتك',
'good_morning': 'صباح الخير',
'good_evening': 'مساء الخير',
'branches': 'الفروع',
'programs': 'البرامج',
'news': 'الأخبار',
'gallery': 'المعرض',
'report_absence': 'تبليغ غياب',
'send_message': 'إرسال رسالة',
'freeze_request': 'طلب تجميد',
'transfer_request': 'طلب نقل',
'cancel_request': 'طلب إلغاء',
'total_balance': 'إجمالي المستحق',
'installments': 'الأقساط',
'payment_plans': 'خطط الدفع',
'explore': 'استكشف',
'more': 'المزيد',
'mark_all_read': 'قراءة الكل',
'no_notifications': 'لا توجد إشعارات',
'no_sessions': 'لا توجد جلسات',
'no_attendance': 'لا توجد سجلات حضور',
'no_invoices': 'لا توجد فواتير',
'no_evaluations': 'لا توجد تقييمات',
'no_events': 'لا توجد فعاليات',
'no_products': 'لا توجد منتجات',
'no_messages': 'لا توجد رسائل',
'no_requests': 'لا توجد طلبات',
'sessions': 'جلسات',
'edit_profile': 'تعديل الحساب',
'notification_settings': 'إعدادات الإشعارات',
'about': 'عن التطبيق',
'guardian_info': 'بيانات ولي الأمر',
'child_info': 'بيانات المشترك',
'review': 'مراجعة',
'submit': 'إرسال',
'next': 'التالي',
'type_message': 'اكتب رسالة...',
'attendance': 'الحضور',
},
'en': {
'app_name': 'El Captain',
'login': 'Login',
'phone_number': 'Phone Number',
'enter_phone': 'Enter your phone number',
'send_otp': 'Send OTP',
'verify_otp': 'Verify OTP',
'enter_otp': 'Enter verification code',
'otp_sent': 'OTP code sent',
'invalid_otp': 'Invalid verification code',
'home': 'Home',
'schedule': 'Schedule',
'notifications': 'Notifications',
'profile': 'Profile',
'my_children': 'My Children',
'today_sessions': 'Today\'s Sessions',
'outstanding_balance': 'Outstanding Balance',
'attendance_rate': 'Attendance Rate',
'next_session': 'Next Session',
'invoices': 'Invoices',
'enrollments': 'Enrollments',
'evaluations': 'Evaluations',
'documents': 'Documents',
'wallet': 'Wallet',
'shop': 'Shop',
'events': 'Events',
'messages': 'Messages',
'service_requests': 'Requests',
'pay_now': 'Pay Now',
'view_all': 'View All',
'no_data': 'No data available',
'loading': 'Loading...',
'error_occurred': 'An error occurred',
'retry': 'Retry',
'present': 'Present',
'absent': 'Absent',
'late': 'Late',
'excused': 'Excused',
'paid': 'Paid',
'pending': 'Pending',
'overdue': 'Overdue',
'active': 'Active',
'frozen': 'Frozen',
'logout': 'Logout',
'settings': 'Settings',
'language': 'Language',
'register': 'Register',
'pre_register': 'Pre-Register',
'welcome_back': 'Welcome Back',
'good_morning': 'Good Morning',
'good_evening': 'Good Evening',
'branches': 'Branches',
'programs': 'Programs',
'news': 'News',
'gallery': 'Gallery',
'report_absence': 'Report Absence',
'send_message': 'Send Message',
'freeze_request': 'Freeze Request',
'transfer_request': 'Transfer Request',
'cancel_request': 'Cancel Request',
'total_balance': 'Total Balance',
'installments': 'Installments',
'payment_plans': 'Payment Plans',
'explore': 'Explore',
'more': 'More',
'mark_all_read': 'Mark All Read',
'no_notifications': 'No notifications',
'no_sessions': 'No sessions',
'no_attendance': 'No attendance records',
'no_invoices': 'No invoices',
'no_evaluations': 'No evaluations',
'no_events': 'No events',
'no_products': 'No products',
'no_messages': 'No messages',
'no_requests': 'No requests',
'sessions': 'sessions',
'edit_profile': 'Edit Profile',
'notification_settings': 'Notification Settings',
'about': 'About',
'guardian_info': 'Guardian Info',
'child_info': 'Child Info',
'review': 'Review',
'submit': 'Submit',
'next': 'Next',
'type_message': 'Type a message...',
'attendance': 'Attendance',
},
};
String t(String key) => _translations[locale.languageCode]?[key] ?? key;
}
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
bool isSupported(Locale locale) => ['ar', 'en'].contains(locale.languageCode);
@override
Future<AppLocalizations> load(Locale locale) async => AppLocalizations(locale);
@override
bool shouldReload(covariant LocalizationsDelegate<AppLocalizations> old) => false;
}
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
final localeProvider = StateNotifierProvider<LocaleNotifier, Locale>((ref) {
return LocaleNotifier();
});
class LocaleNotifier extends StateNotifier<Locale> {
LocaleNotifier() : super(const Locale('ar')) {
_load();
}
Future<void> _load() async {
final prefs = await SharedPreferences.getInstance();
final code = prefs.getString('app_locale') ?? 'ar';
state = Locale(code);
}
Future<void> setLocale(Locale locale) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('app_locale', locale.languageCode);
state = locale;
}
void toggle() {
final next = state.languageCode == 'ar' ? const Locale('en') : const Locale('ar');
setLocale(next);
}
}
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../auth/auth_store.dart';
import '../theme/app_animations.dart';
import '../../features/splash/splash_screen.dart';
import '../../features/auth/login_screen.dart';
import '../../features/auth/otp_screen.dart';
import '../../features/home/home_shell.dart';
import '../../features/home/dashboard_screen.dart';
import '../../features/schedule/schedule_screen.dart';
import '../../features/notifications/notifications_screen.dart';
import '../../features/profile/profile_screen.dart';
import '../../features/participants/participant_detail_screen.dart';
import '../../features/attendance/attendance_screen.dart';
import '../../features/invoices/invoices_screen.dart';
import '../../features/evaluations/evaluations_screen.dart';
import '../../features/events/events_screen.dart';
import '../../features/shop/shop_screen.dart';
import '../../features/messages/messages_screen.dart';
import '../../features/service_requests/service_requests_screen.dart';
import '../../features/registration/registration_screen.dart';
final routerProvider = Provider<GoRouter>((ref) {
final authState = ref.watch(authStoreProvider);
return GoRouter(
initialLocation: '/splash',
redirect: (context, state) {
if (authState.isLoading) return '/splash';
final isAuth = authState.isAuthenticated;
final isAuthRoute = state.matchedLocation.startsWith('/auth');
final isSplash = state.matchedLocation == '/splash';
final isPublic = state.matchedLocation.startsWith('/register');
if (isSplash && !authState.isLoading) {
return isAuth ? '/home' : '/auth/login';
}
if (!isAuth && !isAuthRoute && !isSplash && !isPublic) {
return '/auth/login';
}
if (isAuth && isAuthRoute) {
return '/home';
}
return null;
},
routes: [
GoRoute(
path: '/splash',
pageBuilder: (context, state) =>
PageTransitions.fade(const SplashScreen(), key: 'splash'),
),
GoRoute(
path: '/auth/login',
pageBuilder: (context, state) =>
PageTransitions.fade(const LoginScreen(), key: 'login'),
),
GoRoute(
path: '/auth/otp',
pageBuilder: (context, state) {
final phone = state.extra as String? ?? '';
return PageTransitions.slide(OtpScreen(phone: phone), key: 'otp');
},
),
GoRoute(
path: '/register',
pageBuilder: (context, state) =>
PageTransitions.slide(const RegistrationScreen(), key: 'register'),
),
ShellRoute(
builder: (context, state, child) => HomeShell(child: child),
routes: [
GoRoute(
path: '/home',
pageBuilder: (context, state) =>
PageTransitions.fade(const DashboardScreen(), key: 'home'),
),
GoRoute(
path: '/schedule',
pageBuilder: (context, state) =>
PageTransitions.fade(const ScheduleScreen(), key: 'schedule'),
),
GoRoute(
path: '/notifications',
pageBuilder: (context, state) =>
PageTransitions.fade(const NotificationsScreen(), key: 'notifications'),
),
GoRoute(
path: '/profile',
pageBuilder: (context, state) =>
PageTransitions.fade(const ProfileScreen(), key: 'profile'),
),
],
),
GoRoute(
path: '/participants/:uuid',
pageBuilder: (context, state) {
final uuid = state.pathParameters['uuid']!;
return PageTransitions.slide(
ParticipantDetailScreen(uuid: uuid),
key: 'participant-$uuid',
);
},
),
GoRoute(
path: '/participants/:uuid/attendance',
pageBuilder: (context, state) {
final uuid = state.pathParameters['uuid']!;
return PageTransitions.slide(
AttendanceScreen(participantUuid: uuid),
key: 'attendance-$uuid',
);
},
),
GoRoute(
path: '/participants/:uuid/invoices',
pageBuilder: (context, state) {
final uuid = state.pathParameters['uuid']!;
return PageTransitions.slide(
InvoicesScreen(participantUuid: uuid),
key: 'invoices-$uuid',
);
},
),
GoRoute(
path: '/participants/:uuid/evaluations',
pageBuilder: (context, state) {
final uuid = state.pathParameters['uuid']!;
return PageTransitions.slide(
EvaluationsScreen(participantUuid: uuid),
key: 'evaluations-$uuid',
);
},
),
GoRoute(
path: '/events',
pageBuilder: (context, state) =>
PageTransitions.slide(const EventsScreen(), key: 'events'),
),
GoRoute(
path: '/shop',
pageBuilder: (context, state) =>
PageTransitions.slide(const ShopScreen(), key: 'shop'),
),
GoRoute(
path: '/messages',
pageBuilder: (context, state) =>
PageTransitions.slide(const MessagesScreen(), key: 'messages'),
),
GoRoute(
path: '/service-requests',
pageBuilder: (context, state) =>
PageTransitions.slide(const ServiceRequestsScreen(), key: 'requests'),
),
],
);
});
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:go_router/go_router.dart';
extension AnimateWidgetExtensions on Widget {
Widget fadeSlideUp({int delay = 0, int duration = 500}) {
return animate(delay: Duration(milliseconds: delay))
.fadeIn(duration: Duration(milliseconds: duration), curve: Curves.easeOutCubic)
.slideY(begin: 0.08, end: 0, duration: Duration(milliseconds: duration), curve: Curves.easeOutCubic);
}
Widget fadeSlideRight({int delay = 0, int duration = 400}) {
return animate(delay: Duration(milliseconds: delay))
.fadeIn(duration: Duration(milliseconds: duration), curve: Curves.easeOutCubic)
.slideX(begin: -0.05, end: 0, duration: Duration(milliseconds: duration), curve: Curves.easeOutCubic);
}
Widget scaleIn({int delay = 0, int duration = 400}) {
return animate(delay: Duration(milliseconds: delay))
.fadeIn(duration: Duration(milliseconds: duration))
.scale(begin: const Offset(0.9, 0.9), end: const Offset(1, 1), duration: Duration(milliseconds: duration), curve: Curves.easeOutBack);
}
Widget shimmerEffect() {
return animate(onPlay: (c) => c.repeat())
.shimmer(duration: 1500.ms, color: Colors.white24);
}
}
class StaggeredColumn extends StatelessWidget {
final List<Widget> children;
final int baseDelay;
final int staggerDelay;
const StaggeredColumn({
super.key,
required this.children,
this.baseDelay = 100,
this.staggerDelay = 80,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (int i = 0; i < children.length; i++)
children[i]
.animate(delay: Duration(milliseconds: baseDelay + (i * staggerDelay)))
.fadeIn(duration: 450.ms, curve: Curves.easeOutCubic)
.slideY(begin: 0.06, end: 0, duration: 450.ms, curve: Curves.easeOutCubic),
],
);
}
}
class AnimatedCounter extends StatelessWidget {
final int value;
final TextStyle? style;
final String? suffix;
const AnimatedCounter({super.key, required this.value, this.style, this.suffix});
@override
Widget build(BuildContext context) {
return TweenAnimationBuilder<int>(
tween: IntTween(begin: 0, end: value),
duration: const Duration(milliseconds: 800),
curve: Curves.easeOutCubic,
builder: (context, val, _) => Text(
'$val${suffix ?? ''}',
style: style,
),
);
}
}
class PageTransitions {
static CustomTransitionPage slide(Widget child, {required String key}) {
return CustomTransitionPage(
key: ValueKey(key),
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.15, 0),
end: Offset.zero,
).animate(CurvedAnimation(parent: animation, curve: Curves.easeOutCubic)),
child: FadeTransition(opacity: animation, child: child),
);
},
transitionDuration: const Duration(milliseconds: 350),
);
}
static CustomTransitionPage fade(Widget child, {required String key}) {
return CustomTransitionPage(
key: ValueKey(key),
child: child,
transitionsBuilder: (context, animation, _, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 300),
);
}
}
import 'package:flutter/material.dart';
class AppTheme {
static Color primaryColor = const Color(0xFF1e40af);
static Color accentColor = const Color(0xFFf59e0b);
static void updateFromConfig(String? primary, String? accent) {
if (primary != null && primary.startsWith('#')) {
primaryColor = Color(int.parse('FF${primary.substring(1)}', radix: 16));
}
if (accent != null && accent.startsWith('#')) {
accentColor = Color(int.parse('FF${accent.substring(1)}', radix: 16));
}
}
static ThemeData get light {
final colorScheme = ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.light,
primary: primaryColor,
secondary: accentColor,
surface: Colors.white,
onSurface: const Color(0xFF1a1a2e),
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
fontFamily: 'Cairo',
scaffoldBackgroundColor: const Color(0xFFF8FAFC),
appBarTheme: AppBarTheme(
backgroundColor: Colors.white,
foregroundColor: colorScheme.onSurface,
elevation: 0,
scrolledUnderElevation: 0.5,
centerTitle: true,
titleTextStyle: TextStyle(
fontFamily: 'Cairo',
fontSize: 18,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
cardTheme: CardThemeData(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.grey.shade100),
),
color: Colors.white,
margin: EdgeInsets.zero,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
textStyle: const TextStyle(
fontFamily: 'Cairo',
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: primaryColor,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
side: BorderSide(color: primaryColor.withValues(alpha: 0.3)),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: const Color(0xFFF1F5F9),
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: primaryColor, width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Colors.red, width: 1.5),
),
hintStyle: TextStyle(
color: Colors.grey.shade400,
fontFamily: 'Cairo',
),
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: Colors.white,
selectedItemColor: primaryColor,
unselectedItemColor: Colors.grey.shade400,
type: BottomNavigationBarType.fixed,
elevation: 8,
selectedLabelStyle: const TextStyle(
fontFamily: 'Cairo',
fontSize: 11,
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: const TextStyle(
fontFamily: 'Cairo',
fontSize: 11,
),
),
dividerTheme: DividerThemeData(
color: Colors.grey.shade100,
thickness: 1,
),
chipTheme: ChipThemeData(
backgroundColor: const Color(0xFFF1F5F9),
selectedColor: primaryColor.withValues(alpha: 0.1),
labelStyle: const TextStyle(
fontFamily: 'Cairo',
fontSize: 13,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
side: BorderSide.none,
),
);
}
}
import 'package:intl/intl.dart';
String formatMoney(dynamic piasters, {bool showCurrency = true}) {
final amount = (piasters is int ? piasters : int.tryParse(piasters.toString()) ?? 0) / 100;
final formatted = NumberFormat('#,##0.00', 'ar').format(amount);
return showCurrency ? '$formatted ج.م' : formatted;
}
String formatMoneyEn(dynamic piasters, {bool showCurrency = true}) {
final amount = (piasters is int ? piasters : int.tryParse(piasters.toString()) ?? 0) / 100;
final formatted = NumberFormat('#,##0.00', 'en').format(amount);
return showCurrency ? 'EGP $formatted' : formatted;
}
import 'package:flutter/material.dart';
class AnimatedCard extends StatefulWidget {
final Widget child;
final VoidCallback? onTap;
final EdgeInsets? padding;
final Color? color;
final BorderRadius? borderRadius;
const AnimatedCard({
super.key,
required this.child,
this.onTap,
this.padding,
this.color,
this.borderRadius,
});
@override
State<AnimatedCard> createState() => _AnimatedCardState();
}
class _AnimatedCardState extends State<AnimatedCard> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 150),
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.97).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => _controller.forward(),
onTapUp: (_) {
_controller.reverse();
widget.onTap?.call();
},
onTapCancel: () => _controller.reverse(),
child: AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) => Transform.scale(
scale: _scaleAnimation.value,
child: child,
),
child: Container(
padding: widget.padding ?? const EdgeInsets.all(16),
decoration: BoxDecoration(
color: widget.color ?? Colors.white,
borderRadius: widget.borderRadius ?? BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade100),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.03),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: widget.child,
),
),
);
}
}
class ShimmerCard extends StatelessWidget {
final double height;
final double? width;
const ShimmerCard({super.key, this.height = 100, this.width});
@override
Widget build(BuildContext context) {
return Container(
height: height,
width: width,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(16),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
class LoadingOverlay extends StatelessWidget {
final bool isLoading;
final Widget child;
final String? message;
const LoadingOverlay({
super.key,
required this.isLoading,
required this.child,
this.message,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
child,
if (isLoading)
Positioned.fill(
child: Container(
color: Colors.white.withValues(alpha: 0.8),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 20,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
strokeWidth: 3,
color: Theme.of(context).colorScheme.primary,
),
),
if (message != null) ...[
const SizedBox(height: 16),
Text(
message!,
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
),
],
],
),
).animate().scale(
begin: const Offset(0.8, 0.8),
end: const Offset(1, 1),
duration: 300.ms,
curve: Curves.easeOutBack,
),
],
),
),
),
),
],
);
}
}
class EmptyState extends StatelessWidget {
final String message;
final IconData icon;
final VoidCallback? onAction;
final String? actionLabel;
const EmptyState({
super.key,
required this.message,
this.icon = Icons.inbox_outlined,
this.onAction,
this.actionLabel,
});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(40),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.grey.shade50,
shape: BoxShape.circle,
),
child: Icon(icon, size: 48, color: Colors.grey.shade300),
),
const SizedBox(height: 20),
Text(
message,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade500,
fontWeight: FontWeight.w500,
),
),
if (onAction != null && actionLabel != null) ...[
const SizedBox(height: 20),
TextButton(
onPressed: onAction,
child: Text(actionLabel!),
),
],
],
),
),
).animate().fadeIn(duration: 400.ms).slideY(begin: 0.05, end: 0);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/api/api_service.dart';
import '../../core/l10n/app_localizations.dart';
import '../../core/widgets/loading_overlay.dart';
final attendanceProvider =
FutureProvider.autoDispose.family<Map<String, dynamic>, String>((ref, uuid) async {
return ref.watch(apiServiceProvider).getAttendance(uuid);
});
class AttendanceScreen extends ConsumerWidget {
final String participantUuid;
const AttendanceScreen({super.key, required this.participantUuid});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l = AppLocalizations.of(context);
final attendance = ref.watch(attendanceProvider(participantUuid));
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC),
appBar: AppBar(
title: Text(l.t('attendance')),
centerTitle: true,
backgroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, size: 18),
onPressed: () => context.pop(),
),
),
body: attendance.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text(l.t('error_occurred'))),
data: (data) {
final records = (data['data'] as List?) ?? [];
final rate = data['rate'] ?? 0;
return Column(
children: [
// Rate header
Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 10),
],
),
child: Row(
children: [
SizedBox(
width: 70,
height: 70,
child: Stack(
alignment: Alignment.center,
children: [
CircularProgressIndicator(
value: (rate as num).toDouble() / 100,
strokeWidth: 6,
backgroundColor: Colors.grey.shade200,
color: _rateColor(rate),
),
Text(
'$rate%',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: _rateColor(rate),
),
),
],
),
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l.t('attendance_rate'),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
Text(
'${records.length} ${l.t('sessions')}',
style: TextStyle(fontSize: 13, color: Colors.grey.shade500),
),
],
),
),
],
),
).animate().fadeIn(duration: 500.ms).slideY(begin: 0.05, end: 0),
// Records
Expanded(
child: records.isEmpty
? EmptyState(icon: Icons.event_busy, message: l.t('no_attendance'))
: ListView.builder(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: records.length,
itemBuilder: (ctx, i) {
final r = records[i] as Map<String, dynamic>;
return _AttendanceTile(record: r, index: i);
},
),
),
],
);
},
),
);
}
Color _rateColor(dynamic rate) {
final r = (rate as num).toDouble();
if (r >= 80) return Colors.green;
if (r >= 60) return Colors.orange;
return Colors.red;
}
}
class _AttendanceTile extends StatelessWidget {
final Map<String, dynamic> record;
final int index;
const _AttendanceTile({required this.record, required this.index});
@override
Widget build(BuildContext context) {
final status = record['status'] ?? 'expected';
final (color, icon, label) = _statusInfo(status);
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade100),
),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color, size: 18),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
record['session_date'] ?? '',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
if (record['group_name'] != null) ...[
const SizedBox(height: 2),
Text(
record['group_name'],
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
),
],
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(label, style: TextStyle(fontSize: 11, color: color, fontWeight: FontWeight.w600)),
),
],
),
)
.animate(delay: Duration(milliseconds: 40 * index))
.fadeIn(duration: 350.ms)
.slideX(begin: 0.03, end: 0);
}
(Color, IconData, String) _statusInfo(String status) => switch (status) {
'present' => (Colors.green, Icons.check_circle, 'حاضر'),
'late' => (Colors.orange, Icons.schedule, 'متأخر'),
'absent' => (Colors.red, Icons.cancel, 'غائب'),
'excused' => (Colors.blue, Icons.info, 'معذور'),
'no_show' => (Colors.red.shade800, Icons.close, 'لم يحضر'),
_ => (Colors.grey, Icons.circle_outlined, status),
};
}
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/l10n/app_localizations.dart';
import '../../core/theme/app_theme.dart';
class LoginScreen extends ConsumerStatefulWidget {
const LoginScreen({super.key});
@override
ConsumerState<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends ConsumerState<LoginScreen> {
final _phoneController = TextEditingController();
final _formKey = GlobalKey<FormState>();
@override
void dispose() {
_phoneController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context);
final size = MediaQuery.sizeOf(context);
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: SizedBox(
height: size.height - MediaQuery.paddingOf(context).top - MediaQuery.paddingOf(context).bottom,
child: Form(
key: _formKey,
child: Column(
children: [
const Spacer(flex: 2),
// Logo
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppTheme.primaryColor,
AppTheme.primaryColor.withValues(alpha: 0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: AppTheme.primaryColor.withValues(alpha: 0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: const Icon(Icons.sports_soccer, size: 48, color: Colors.white),
)
.animate()
.scale(begin: const Offset(0.6, 0.6), end: const Offset(1, 1), duration: 600.ms, curve: Curves.easeOutBack)
.fadeIn(duration: 400.ms),
const SizedBox(height: 32),
// Title
Text(
l.t('welcome_back'),
style: const TextStyle(fontSize: 26, fontWeight: FontWeight.bold),
).animate(delay: 200.ms).fadeIn(duration: 500.ms).slideY(begin: 0.2, end: 0),
const SizedBox(height: 8),
Text(
l.t('enter_phone'),
style: TextStyle(fontSize: 15, color: Colors.grey.shade500),
).animate(delay: 300.ms).fadeIn(duration: 500.ms),
const SizedBox(height: 48),
// Phone input
Directionality(
textDirection: TextDirection.ltr,
child: TextFormField(
controller: _phoneController,
keyboardType: TextInputType.phone,
textDirection: TextDirection.ltr,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9+]')),
LengthLimitingTextInputFormatter(15),
],
style: const TextStyle(fontSize: 18, letterSpacing: 1),
decoration: InputDecoration(
hintText: '+201XXXXXXXXX',
prefixIcon: Container(
padding: const EdgeInsets.all(12),
child: const Text('🇪🇬', style: TextStyle(fontSize: 24)),
),
),
validator: (v) {
if (v == null || v.isEmpty) return '';
if (v.length < 10) return '';
return null;
},
),
).animate(delay: 400.ms).fadeIn(duration: 500.ms).slideY(begin: 0.1, end: 0),
const SizedBox(height: 24),
// Submit button
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: _submit,
child: Text(l.t('send_otp'), style: const TextStyle(fontSize: 17)),
),
).animate(delay: 500.ms).fadeIn(duration: 500.ms).slideY(begin: 0.1, end: 0),
const SizedBox(height: 20),
// Pre-register link
TextButton(
onPressed: () => context.push('/register'),
child: Text(
l.t('pre_register'),
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
).animate(delay: 600.ms).fadeIn(duration: 400.ms),
const Spacer(flex: 3),
],
),
),
),
),
),
);
}
void _submit() {
if (!_formKey.currentState!.validate()) return;
final phone = _phoneController.text.trim();
context.push('/auth/otp', extra: phone);
}
}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/api/api_client.dart';
import '../../core/api/api_service.dart';
import '../../core/auth/auth_store.dart';
import '../../core/l10n/app_localizations.dart';
import '../../core/theme/app_theme.dart';
class OtpScreen extends ConsumerStatefulWidget {
final String phone;
const OtpScreen({super.key, required this.phone});
@override
ConsumerState<OtpScreen> createState() => _OtpScreenState();
}
class _OtpScreenState extends ConsumerState<OtpScreen> {
final List<TextEditingController> _controllers = List.generate(6, (_) => TextEditingController());
final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
bool _isLoading = false;
bool _otpSent = false;
String? _error;
int _countdown = 60;
Timer? _timer;
@override
void initState() {
super.initState();
_requestOtp();
}
@override
void dispose() {
_timer?.cancel();
for (var c in _controllers) {
c.dispose();
}
for (var n in _focusNodes) {
n.dispose();
}
super.dispose();
}
Future<void> _requestOtp() async {
try {
await ref.read(apiServiceProvider).requestOtp(widget.phone);
setState(() {
_otpSent = true;
_countdown = 60;
});
_startTimer();
} catch (e) {
if (e is ApiException) {
setState(() => _error = e.message);
}
}
}
void _startTimer() {
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 1), (t) {
if (_countdown > 0) {
setState(() => _countdown--);
} else {
t.cancel();
}
});
}
Future<void> _verify() async {
final otp = _controllers.map((c) => c.text).join();
if (otp.length != 6) return;
setState(() {
_isLoading = true;
_error = null;
});
try {
final result = await ref.read(apiServiceProvider).verifyOtp(widget.phone, otp);
await ref.read(authStoreProvider.notifier).setAuth(
token: result['token'],
user: result['user'],
participants: result['participants'] ?? [],
);
if (mounted) context.go('/home');
} catch (e) {
setState(() {
_isLoading = false;
_error = e is ApiException ? e.message : 'حدث خطأ';
});
// Shake animation on error
for (var c in _controllers) {
c.clear();
}
_focusNodes[0].requestFocus();
}
}
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, size: 20),
onPressed: () => context.pop(),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 20),
Text(
l.t('verify_otp'),
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
).animate().fadeIn(duration: 400.ms).slideY(begin: 0.1, end: 0),
const SizedBox(height: 12),
Text(
'${l.t('otp_sent')} ${widget.phone}',
style: TextStyle(fontSize: 14, color: Colors.grey.shade500),
).animate(delay: 100.ms).fadeIn(duration: 400.ms),
const SizedBox(height: 48),
// OTP Input boxes
Directionality(
textDirection: TextDirection.ltr,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(6, (i) => _buildOtpBox(i)),
),
).animate(delay: 200.ms).fadeIn(duration: 500.ms).slideY(begin: 0.1, end: 0),
if (_error != null) ...[
const SizedBox(height: 20),
Text(
_error!,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red, fontSize: 14),
).animate().shake(hz: 3, duration: 400.ms),
],
const SizedBox(height: 32),
SizedBox(
height: 56,
child: ElevatedButton(
onPressed: _isLoading ? null : _verify,
child: _isLoading
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5),
)
: Text(l.t('verify_otp'), style: const TextStyle(fontSize: 17)),
),
).animate(delay: 300.ms).fadeIn(duration: 400.ms),
const SizedBox(height: 24),
Center(
child: _countdown > 0
? Text(
'إعادة الإرسال خلال $_countdown ثانية',
style: TextStyle(color: Colors.grey.shade500, fontSize: 14),
)
: TextButton(
onPressed: _requestOtp,
child: Text(
'إعادة إرسال الرمز',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
).animate(delay: 400.ms).fadeIn(duration: 400.ms),
],
),
),
),
);
}
Widget _buildOtpBox(int index) {
return SizedBox(
width: 48,
height: 56,
child: TextField(
controller: _controllers[index],
focusNode: _focusNodes[index],
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
maxLength: 1,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration(
counterText: '',
contentPadding: EdgeInsets.zero,
filled: true,
fillColor: _controllers[index].text.isNotEmpty
? AppTheme.primaryColor.withValues(alpha: 0.05)
: const Color(0xFFF1F5F9),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: AppTheme.primaryColor, width: 2),
),
),
onChanged: (v) {
if (v.isNotEmpty && index < 5) {
_focusNodes[index + 1].requestFocus();
}
if (v.isEmpty && index > 0) {
_focusNodes[index - 1].requestFocus();
}
setState(() {});
if (index == 5 && v.isNotEmpty) {
_verify();
}
},
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/api/api_service.dart';
import '../../core/l10n/app_localizations.dart';
import '../../core/widgets/loading_overlay.dart';
final evaluationsProvider =
FutureProvider.autoDispose.family<List<dynamic>, String>((ref, uuid) async {
return ref.watch(apiServiceProvider).getEvaluations(uuid);
});
class EvaluationsScreen extends ConsumerWidget {
final String participantUuid;
const EvaluationsScreen({super.key, required this.participantUuid});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l = AppLocalizations.of(context);
final evaluations = ref.watch(evaluationsProvider(participantUuid));
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC),
appBar: AppBar(
title: Text(l.t('evaluations')),
centerTitle: true,
backgroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, size: 18),
onPressed: () => context.pop(),
),
),
body: evaluations.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text(l.t('error_occurred'))),
data: (items) {
if (items.isEmpty) {
return EmptyState(icon: Icons.assessment, message: l.t('no_evaluations'));
}
return ListView.builder(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16),
itemCount: items.length,
itemBuilder: (ctx, i) {
final eval = items[i] as Map<String, dynamic>;
return _EvaluationCard(evaluation: eval, index: i);
},
);
},
),
);
}
}
class _EvaluationCard extends StatelessWidget {
final Map<String, dynamic> evaluation;
final int index;
const _EvaluationCard({required this.evaluation, required this.index});
@override
Widget build(BuildContext context) {
final criteria = (evaluation['criteria'] as List?) ?? [];
final overallScore = evaluation['overall_score'] ?? 0;
final date = evaluation['evaluation_date'] ?? '';
final trainerName = evaluation['trainer_name'] ?? '';
return Container(
margin: const EdgeInsets.only(bottom: 14),
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 12, offset: const Offset(0, 3)),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: _scoreColor(overallScore).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
'$overallScore',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: _scoreColor(overallScore),
),
),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
evaluation['period_name'] ?? 'تقييم',
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
),
const SizedBox(height: 2),
Text(
'$trainerName$date',
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
),
],
),
),
],
),
if (criteria.isNotEmpty) ...[
const SizedBox(height: 16),
...criteria.map((c) {
final crit = c as Map<String, dynamic>;
final score = (crit['score'] ?? 0) as num;
final maxScore = (crit['max_score'] ?? 10) as num;
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
crit['name'] ?? '',
style: const TextStyle(fontSize: 13),
),
),
Text(
'$score/$maxScore',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600, fontWeight: FontWeight.w600),
),
],
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: maxScore > 0 ? score / maxScore : 0,
backgroundColor: Colors.grey.shade100,
color: _scoreColor((score / maxScore * 10).round()),
minHeight: 6,
),
),
],
),
);
}),
],
if (evaluation['notes'] != null && (evaluation['notes'] as String).isNotEmpty) ...[
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(10),
),
child: Text(
evaluation['notes'],
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
),
),
],
],
),
)
.animate(delay: Duration(milliseconds: 80 * index))
.fadeIn(duration: 450.ms)
.slideY(begin: 0.04, end: 0);
}
Color _scoreColor(dynamic score) {
final s = (score as num).toDouble();
if (s >= 8) return Colors.green;
if (s >= 6) return Colors.orange;
if (s >= 4) return Colors.amber;
return Colors.red;
}
}
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/api/api_service.dart';
import '../../core/l10n/app_localizations.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/loading_overlay.dart';
final eventsProvider = FutureProvider.autoDispose<Map<String, dynamic>>((ref) async {
return ref.watch(apiServiceProvider).getEvents();
});
class EventsScreen extends ConsumerWidget {
const EventsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l = AppLocalizations.of(context);
final events = ref.watch(eventsProvider);
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC),
appBar: AppBar(
title: Text(l.t('events')),
centerTitle: true,
backgroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
),
body: RefreshIndicator(
onRefresh: () async => ref.invalidate(eventsProvider),
child: events.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text(l.t('error_occurred'))),
data: (data) {
final items = (data['data'] as List?) ?? [];
if (items.isEmpty) {
return EmptyState(icon: Icons.event, message: l.t('no_events'));
}
return ListView.builder(
physics: const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()),
padding: const EdgeInsets.all(16),
itemCount: items.length,
itemBuilder: (ctx, i) {
final event = items[i] as Map<String, dynamic>;
return _EventCard(event: event, index: i, ref: ref);
},
);
},
),
),
);
}
}
class _EventCard extends StatelessWidget {
final Map<String, dynamic> event;
final int index;
final WidgetRef ref;
const _EventCard({required this.event, required this.index, required this.ref});
@override
Widget build(BuildContext context) {
final isRegistered = event['is_registered'] == true;
return Container(
margin: const EdgeInsets.only(bottom: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 12, offset: const Offset(0, 3)),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image header
Container(
height: 140,
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.08),
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
),
child: event['image_url'] != null
? ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
child: Image.network(
event['image_url'],
width: double.infinity,
fit: BoxFit.cover,
),
)
: Center(
child: Icon(Icons.event, size: 48, color: AppTheme.primaryColor.withValues(alpha: 0.3)),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
event['title'] ?? '',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Row(
children: [
Icon(Icons.calendar_today, size: 14, color: Colors.grey.shade500),
const SizedBox(width: 6),
Text(
event['date'] ?? '',
style: TextStyle(fontSize: 13, color: Colors.grey.shade500),
),
const SizedBox(width: 16),
Icon(Icons.access_time, size: 14, color: Colors.grey.shade500),
const SizedBox(width: 6),
Text(
event['time'] ?? '',
style: TextStyle(fontSize: 13, color: Colors.grey.shade500),
),
],
),
if (event['location'] != null) ...[
const SizedBox(height: 6),
Row(
children: [
Icon(Icons.location_on, size: 14, color: Colors.grey.shade500),
const SizedBox(width: 6),
Text(
event['location'],
style: TextStyle(fontSize: 13, color: Colors.grey.shade500),
),
],
),
],
const SizedBox(height: 14),
SizedBox(
width: double.infinity,
height: 42,
child: ElevatedButton(
onPressed: isRegistered
? null
: () async {
await ref.read(apiServiceProvider).registerForEvent(event['uuid'], '');
ref.invalidate(eventsProvider);
},
style: ElevatedButton.styleFrom(
backgroundColor: isRegistered ? Colors.grey.shade200 : AppTheme.primaryColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: Text(
isRegistered ? 'مسجل ✓' : 'سجل الآن',
style: TextStyle(
fontSize: 14,
color: isRegistered ? Colors.grey.shade600 : Colors.white,
),
),
),
),
],
),
),
],
),
)
.animate(delay: Duration(milliseconds: 100 * index))
.fadeIn(duration: 500.ms)
.slideY(begin: 0.05, end: 0, curve: Curves.easeOutCubic);
}
}
This diff is collapsed.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/l10n/app_localizations.dart';
import '../../core/theme/app_theme.dart';
class HomeShell extends ConsumerStatefulWidget {
final Widget child;
const HomeShell({super.key, required this.child});
@override
ConsumerState<HomeShell> createState() => _HomeShellState();
}
class _HomeShellState extends ConsumerState<HomeShell> {
int _currentIndex = 0;
static const _routes = ['/home', '/schedule', '/notifications', '/profile'];
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context);
final location = GoRouterState.of(context).matchedLocation;
_currentIndex = _routes.indexOf(location).clamp(0, 3);
return Scaffold(
body: widget.child,
bottomNavigationBar: Container(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 20,
offset: const Offset(0, -5),
),
],
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_NavItem(
icon: Icons.home_rounded,
label: l.t('home'),
isActive: _currentIndex == 0,
onTap: () => _navigate(0),
),
_NavItem(
icon: Icons.calendar_month_rounded,
label: l.t('schedule'),
isActive: _currentIndex == 1,
onTap: () => _navigate(1),
),
_NavItem(
icon: Icons.notifications_rounded,
label: l.t('notifications'),
isActive: _currentIndex == 2,
onTap: () => _navigate(2),
badge: 0, // TODO: wire to unread count
),
_NavItem(
icon: Icons.person_rounded,
label: l.t('profile'),
isActive: _currentIndex == 3,
onTap: () => _navigate(3),
),
],
),
),
),
),
);
}
void _navigate(int index) {
if (_currentIndex == index) return;
context.go(_routes[index]);
}
}
class _NavItem extends StatelessWidget {
final IconData icon;
final String label;
final bool isActive;
final VoidCallback onTap;
final int? badge;
const _NavItem({
required this.icon,
required this.label,
required this.isActive,
required this.onTap,
this.badge,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
padding: EdgeInsets.symmetric(
horizontal: isActive ? 16 : 12,
vertical: 8,
),
decoration: BoxDecoration(
color: isActive ? AppTheme.primaryColor.withValues(alpha: 0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Stack(
clipBehavior: Clip.none,
children: [
AnimatedScale(
scale: isActive ? 1.1 : 1.0,
duration: const Duration(milliseconds: 200),
child: Icon(
icon,
size: 24,
color: isActive ? AppTheme.primaryColor : Colors.grey.shade400,
),
),
if (badge != null && badge! > 0)
Positioned(
right: -6,
top: -4,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: Text(
'$badge',
style: const TextStyle(color: Colors.white, fontSize: 9, fontWeight: FontWeight.bold),
),
),
),
],
),
const SizedBox(height: 4),
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 200),
style: TextStyle(
fontSize: 11,
fontFamily: 'Cairo',
fontWeight: isActive ? FontWeight.w600 : FontWeight.w400,
color: isActive ? AppTheme.primaryColor : Colors.grey.shade400,
),
child: Text(label),
),
],
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/api/api_service.dart';
import '../../core/l10n/app_localizations.dart';
import '../../core/theme/app_theme.dart';
import '../../core/utils/money.dart';
import '../../core/widgets/loading_overlay.dart';
final invoicesProvider =
FutureProvider.autoDispose.family<Map<String, dynamic>, String>((ref, uuid) async {
return ref.watch(apiServiceProvider).getInvoices(uuid);
});
class InvoicesScreen extends ConsumerWidget {
final String participantUuid;
const InvoicesScreen({super.key, required this.participantUuid});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l = AppLocalizations.of(context);
final invoices = ref.watch(invoicesProvider(participantUuid));
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC),
appBar: AppBar(
title: Text(l.t('invoices')),
centerTitle: true,
backgroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, size: 18),
onPressed: () => context.pop(),
),
),
body: invoices.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text(l.t('error_occurred'))),
data: (data) {
final items = (data['data'] as List?) ?? [];
if (items.isEmpty) {
return EmptyState(icon: Icons.receipt_long, message: l.t('no_invoices'));
}
return ListView.builder(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16),
itemCount: items.length,
itemBuilder: (ctx, i) {
final inv = items[i] as Map<String, dynamic>;
return _InvoiceTile(invoice: inv, index: i);
},
);
},
),
);
}
}
class _InvoiceTile extends StatelessWidget {
final Map<String, dynamic> invoice;
final int index;
const _InvoiceTile({required this.invoice, required this.index});
@override
Widget build(BuildContext context) {
final status = invoice['status'] ?? 'draft';
final (statusColor, statusLabel) = _statusInfo(status);
final total = invoice['total_amount'] ?? 0;
final balance = invoice['balance_due'] ?? 0;
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2)),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
statusLabel,
style: TextStyle(fontSize: 11, color: statusColor, fontWeight: FontWeight.w600),
),
),
const Spacer(),
Text(
invoice['invoice_number'] ?? '',
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
invoice['description'] ?? invoice['items_summary'] ?? '',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
invoice['issued_date'] ?? '',
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
formatMoney(total),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
if (balance > 0)
Text(
'متبقي: ${formatMoney(balance)}',
style: TextStyle(fontSize: 12, color: Colors.red.shade600),
),
],
),
],
),
if (balance > 0 && status != 'cancelled') ...[
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
height: 40,
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
child: const Text('ادفع الآن', style: TextStyle(fontSize: 13, color: Colors.white)),
),
),
],
],
),
)
.animate(delay: Duration(milliseconds: 60 * index))
.fadeIn(duration: 400.ms)
.slideY(begin: 0.04, end: 0, curve: Curves.easeOut);
}
(Color, String) _statusInfo(String status) => switch (status) {
'paid' => (Colors.green, 'مدفوعة'),
'partially_paid' => (Colors.orange, 'مدفوعة جزئياً'),
'sent' => (Colors.blue, 'مرسلة'),
'overdue' => (Colors.red, 'متأخرة'),
'cancelled' => (Colors.grey, 'ملغاة'),
'draft' => (Colors.grey.shade600, 'مسودة'),
_ => (Colors.grey, status),
};
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment