1111
This commit is contained in:
84
lib/presentation/screens/about_screen.dart
Normal file
84
lib/presentation/screens/about_screen.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
|
||||
class AboutScreen extends StatelessWidget {
|
||||
const AboutScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: AppBackground(
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
/// -------------------- SETTINGS BAR --------------------
|
||||
SettingsBar(
|
||||
selectedIndex: 0,
|
||||
onTap: (_) {},
|
||||
showBackButton: true,
|
||||
onBackTap: () => Navigator.pop(context),
|
||||
iconPaths: const [],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
/// -------------------- CENTER CONTENT --------------------
|
||||
///
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24.0,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"عن الشركة",
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Container(
|
||||
width: 200,
|
||||
height: 1,
|
||||
color: const Color(0x8732C599),
|
||||
),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
const Text(
|
||||
"شركة كودا ذ.م.م هي شركة عراقية رائدة تقدم حلولاً متميزة في البرمجيات المخصصة والتطبيقات وإدارة الخوادم والمواقع، ومنظومات الاتصال وخدمات الأمن السيبراني. تأسست عام 2008 ولديها عقود واتفاقيات عديدة مع مؤسسات حكومية عراقية والقطاع الخاص.\n\nوسعت الشركة شراكاتها مع وكالات عالمية مثل CODACS الأمريكية و Laipac الكندية و Teltonika اللتوانية وWolf Team الصينية وLibelium الإسبانية وOdoo العالمية وغيرها. مما أتاح لها تقديم أحدث التقنيات والمنتجات المبتكرة لعملائها.\n\nتوفر كودا حلولاً برمجية مخصصة للمؤسسات الكبيرة والدوائر الحكومية، مما يسهم في تبسيط الإجراءات وتقديم الخدمات للمواطنين بشكل إلكتروني وفعال.\n\nكما تقدم كودا مجموعة واسعة من المنتجات والخدمات الحديثة، بما في ذلك الشاشات الإعلانية، والبوابات الذكية، والمواقف الذكية، والأقفال، والمباني الذكية، وأجهزة الكشك (الخدمة الإلكترونية الذاتية)، وأجهزة محمولة تخصصية، وحساسات، وأجهزة نداء واستغاثة.\n\nبفضل هذه المنتجات والخدمات المبتكرة، تعزز كودا مكانتها الرائدة في السوق المحلي والإقليمي، وتساهم في تطوير التكنولوجيا وتعزيز جودة الحياة في العراق .",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.white70,
|
||||
height:
|
||||
1.5, // Added line height for better readability
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20), // Bottom padding
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
269
lib/presentation/screens/attendence_screen.dart
Normal file
269
lib/presentation/screens/attendence_screen.dart
Normal file
@@ -0,0 +1,269 @@
|
||||
import 'package:coda_project/presentation/screens/face_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
|
||||
class AttendanceScreen extends StatelessWidget {
|
||||
const AttendanceScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||
final screenHeight = MediaQuery.sizeOf(context).height;
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(height: MediaQuery.of(context).size.height),
|
||||
|
||||
/// ------------------------------
|
||||
/// SETTINGS BAR (STATIC)
|
||||
/// ------------------------------
|
||||
SafeArea(
|
||||
child: SettingsBar(
|
||||
selectedIndex: 0,
|
||||
showBackButton: false,
|
||||
iconPaths: ['assets/images/user.svg', 'assets/images/ball.svg'],
|
||||
onTap: (index) {
|
||||
if (index == 0) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => UserSettingsScreen(),
|
||||
),
|
||||
);
|
||||
} else if (index == 1) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NotificationsScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
/// ------------------------------
|
||||
/// GREETING TEXT
|
||||
/// ------------------------------
|
||||
Positioned(
|
||||
top:
|
||||
screenHeight *
|
||||
0.14, // moved down because settings bar now exists
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
"صباح الخير, محمد",
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
shadows: [Shadow(color: Color(0x42000000), blurRadius: 6)],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// ------------------------------
|
||||
/// MAIN CARD AREA
|
||||
/// ------------------------------
|
||||
Positioned(
|
||||
top:
|
||||
screenHeight *
|
||||
0.2, // pushed down because of settings bar + greeting
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: screenHeight * 0.05),
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: screenHeight * 0.5,
|
||||
width: screenWidth * 0.7,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Color(0x1F2B2B2B),
|
||||
blurRadius: 5,
|
||||
offset: Offset(10, -10),
|
||||
),
|
||||
BoxShadow(
|
||||
color: Color(0xABCECECE),
|
||||
blurRadius: 5,
|
||||
offset: Offset(-2, 5),
|
||||
),
|
||||
BoxShadow(
|
||||
color: Color.fromARGB(148, 2, 70, 35),
|
||||
blurRadius: 80,
|
||||
offset: Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: screenHeight * 0.5,
|
||||
width: screenWidth * 0.7,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0x92757575),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// ------------------------------
|
||||
/// LOGIN BUTTON
|
||||
/// ------------------------------
|
||||
Positioned(
|
||||
top: screenHeight * 0.21,
|
||||
left: screenWidth * 0.05,
|
||||
child: _ShadowedCard(
|
||||
shadow: [
|
||||
BoxShadow(
|
||||
color: Color(0x62000000),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 5,
|
||||
offset: Offset(5, 5),
|
||||
),
|
||||
],
|
||||
child: _FingerButton(
|
||||
icon: "assets/images/faceLogin.svg",
|
||||
label: "تسجيل الدخول",
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => OvalCameraCapturePage(isLogin: true),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// ------------------------------
|
||||
/// LOGOUT BUTTON
|
||||
/// ------------------------------
|
||||
Positioned(
|
||||
bottom: screenHeight * 0.2,
|
||||
right: screenWidth * 0.1,
|
||||
child: _ShadowedCard(
|
||||
shadow: [
|
||||
BoxShadow(
|
||||
color: Color(0xABCECECE),
|
||||
blurRadius: 5,
|
||||
spreadRadius: 3,
|
||||
offset: Offset(-6, -6),
|
||||
),
|
||||
BoxShadow(
|
||||
color: Color(0x92014221),
|
||||
blurRadius: 10,
|
||||
offset: Offset(-5, -5),
|
||||
),
|
||||
BoxShadow(
|
||||
color: Color(0x7D1A1A1A),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 3,
|
||||
offset: Offset(5, 5),
|
||||
),
|
||||
],
|
||||
child: _FingerButton(
|
||||
icon: "assets/images/faceLogout.svg",
|
||||
label: "تسجيل خروج",
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => OvalCameraCapturePage(isLogin: false),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ---------------------------------------------
|
||||
/// SHADOW WRAPPER
|
||||
/// ---------------------------------------------
|
||||
|
||||
class _ShadowedCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final List<BoxShadow> shadow;
|
||||
|
||||
const _ShadowedCard({required this.child, required this.shadow});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 160,
|
||||
width: 160,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
boxShadow: shadow,
|
||||
),
|
||||
),
|
||||
child,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ---------------------------------------------
|
||||
/// BUTTON WIDGET
|
||||
/// ---------------------------------------------
|
||||
|
||||
class _FingerButton extends StatelessWidget {
|
||||
final String icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _FingerButton({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 160,
|
||||
width: 160,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFEAFBF3),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(icon, width: 75, height: 75),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
34
lib/presentation/screens/auth_screen.dart
Normal file
34
lib/presentation/screens/auth_screen.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/auth_form.dart';
|
||||
import '../../core/di/injection_container.dart';
|
||||
import '../blocs/login/login_bloc.dart';
|
||||
|
||||
class AuthScreen extends StatelessWidget {
|
||||
const AuthScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => sl<LoginBloc>(),
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: AppBackground(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
// Logo
|
||||
Center(child: Image.asset("assets/images/logo2.png", width: 200)),
|
||||
// const SizedBox(height: 15),
|
||||
// Form - taking remaining space and centered
|
||||
Expanded(child: Center(child: const AuthForm())),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
351
lib/presentation/screens/face_screen.dart
Normal file
351
lib/presentation/screens/face_screen.dart
Normal file
@@ -0,0 +1,351 @@
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'dart:async';
|
||||
|
||||
class OvalCameraCapturePage extends StatefulWidget {
|
||||
final bool isLogin;
|
||||
const OvalCameraCapturePage({super.key, this.isLogin = true});
|
||||
|
||||
@override
|
||||
State<OvalCameraCapturePage> createState() => _OvalCameraCapturePageState();
|
||||
}
|
||||
|
||||
class _OvalCameraCapturePageState extends State<OvalCameraCapturePage> {
|
||||
CameraController? _cameraController;
|
||||
bool _isCameraInitialized = false;
|
||||
String? _errorMessage;
|
||||
bool _isSuccess = false;
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeCamera();
|
||||
}
|
||||
|
||||
Future<void> _initializeCamera() async {
|
||||
try {
|
||||
// Dispose existing controller if any
|
||||
await _cameraController?.dispose();
|
||||
_cameraController = null;
|
||||
|
||||
// Get available cameras
|
||||
final cameras = await availableCameras();
|
||||
|
||||
// Check if cameras list is available
|
||||
if (cameras.isEmpty) {
|
||||
setState(() {
|
||||
_errorMessage = "لا توجد كاميرات متاحة";
|
||||
_isCameraInitialized = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to find front camera, fallback to first available camera
|
||||
CameraDescription? selectedCamera;
|
||||
try {
|
||||
selectedCamera = cameras.firstWhere(
|
||||
(cam) => cam.lensDirection == CameraLensDirection.front,
|
||||
);
|
||||
} catch (e) {
|
||||
// If no front camera found, use the first available camera
|
||||
if (cameras.isNotEmpty) {
|
||||
selectedCamera = cameras.first;
|
||||
} else {
|
||||
setState(() {
|
||||
_errorMessage = "لا توجد كاميرات متاحة";
|
||||
_isCameraInitialized = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_cameraController = CameraController(
|
||||
selectedCamera,
|
||||
ResolutionPreset.medium,
|
||||
enableAudio: false,
|
||||
imageFormatGroup: ImageFormatGroup.jpeg,
|
||||
);
|
||||
|
||||
await _cameraController!.initialize();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isCameraInitialized = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
_timer = Timer(const Duration(seconds: 3), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSuccess = true;
|
||||
});
|
||||
|
||||
// Auto-close after 2 seconds
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_errorMessage = "خطأ في تهيئة الكاميرا: $e";
|
||||
_isCameraInitialized = false;
|
||||
});
|
||||
print("Error initializing camera: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cameraController?.dispose();
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xff000000),
|
||||
|
||||
body:
|
||||
_errorMessage != null
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
size: 64,
|
||||
color: Colors.white70,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: _initializeCamera,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xffE8001A),
|
||||
foregroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
child: Text("إعادة المحاولة"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: _isCameraInitialized && _cameraController != null
|
||||
? Stack(
|
||||
children: [
|
||||
SizedBox(height: MediaQuery.of(context).size.height),
|
||||
|
||||
// Camera Preview
|
||||
Positioned(
|
||||
child: Center(child: CameraPreview(_cameraController!)),
|
||||
),
|
||||
// Oval overlay with dimmed background
|
||||
Positioned.fill(
|
||||
child: CustomPaint(painter: _OvalOverlayPainter()),
|
||||
),
|
||||
|
||||
// Top Text
|
||||
Positioned(
|
||||
top: 100,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
widget.isLogin ? "تسجيل الدخول" : "تسجيل خروج",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily:
|
||||
'Cairo', // Assuming Cairo font based on Arabic text
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom Text and Logo
|
||||
Positioned(
|
||||
bottom: 80,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_isSuccess
|
||||
? (widget.isLogin
|
||||
? "تم تسجيل دخولك بنجاح"
|
||||
: "تم تسجيل خروجك بنجاح")
|
||||
: (widget.isLogin
|
||||
? "يتم تسجيل الدخول ..."
|
||||
: "يتم تسجيل الخروج ..."),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Logo
|
||||
SizedBox(
|
||||
height: _isSuccess ? 80 : 60,
|
||||
width: _isSuccess ? 80 : 60,
|
||||
child: SvgPicture.asset(
|
||||
_isSuccess
|
||||
? 'assets/images/logSuccess.svg'
|
||||
: 'assets/images/logLoading.svg',
|
||||
// ignore: deprecated_member_use
|
||||
color: Colors.white,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// // Capture button
|
||||
// Positioned(
|
||||
// bottom: 60,
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// child: Center(
|
||||
// child: GestureDetector(
|
||||
// onTap: (){},
|
||||
// child: Container(
|
||||
// width: 72,
|
||||
// height: 72,
|
||||
// decoration: BoxDecoration(
|
||||
// shape: BoxShape.circle,
|
||||
// color: Colors.white,
|
||||
// boxShadow: [
|
||||
// BoxShadow(
|
||||
// color: Colors.black26,
|
||||
// blurRadius: 8,
|
||||
// offset: Offset(0, 4),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// child: Icon(Icons.camera_alt, color: Color(0xffE8001A), size: 36),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
)
|
||||
: Center(
|
||||
child: CircularProgressIndicator(color: Color(0xffE8001A)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OvalOverlayPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final width = size.width * 0.75;
|
||||
final height = size.height * 0.4;
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final ovalRect = Rect.fromCenter(
|
||||
center: center,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
|
||||
// Create a path for the whole screen
|
||||
final screenPath =
|
||||
Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
|
||||
// Create a path for the oval
|
||||
final ovalPath = Path()..addOval(ovalRect);
|
||||
// Subtract the oval from the screen path
|
||||
final overlayPath = Path.combine(
|
||||
PathOperation.difference,
|
||||
screenPath,
|
||||
ovalPath,
|
||||
);
|
||||
|
||||
// Draw the dimmed area outside the oval with gradient
|
||||
final gradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color.fromARGB(255, 41, 41, 41), // top dark gray
|
||||
Color.fromARGB(255, 0, 20, 15), // bottom deep green
|
||||
],
|
||||
);
|
||||
final paint =
|
||||
Paint()
|
||||
..shader = gradient.createShader(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||
)
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawPath(overlayPath, paint);
|
||||
|
||||
// Draw glowing circles effect (like AppBackground) - drawn after overlay
|
||||
// Top circle - positioned similar to AppBackground (top: -250, left: 100, right: -200)
|
||||
final topCircleCenter = Offset(size.width * 0.3, -250);
|
||||
final topCircleRadius = 150.0;
|
||||
// Draw multiple circles with different opacities for spread effect (spreadRadius: 160)
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final spreadPaint =
|
||||
Paint()
|
||||
..color = Color.fromARGB(69 ~/ (i + 1), 62, 254, 190)
|
||||
..maskFilter = MaskFilter.blur(BlurStyle.normal, 140 - (i * 20));
|
||||
canvas.drawCircle(
|
||||
topCircleCenter,
|
||||
topCircleRadius + (i * 30),
|
||||
spreadPaint,
|
||||
);
|
||||
}
|
||||
|
||||
// Bottom circle - positioned similar to AppBackground (bottom: 100, left: -140, right: -120)
|
||||
final bottomCircleCenter = Offset(size.width * 0.2, size.height + 100);
|
||||
final bottomCircleRadius = 160.0;
|
||||
// Draw multiple circles with different opacities for spread effect (spreadRadius: 60)
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final spreadPaint =
|
||||
Paint()
|
||||
..color = Color.fromARGB(83 ~/ (i + 1), 62, 254, 190)
|
||||
..maskFilter = MaskFilter.blur(BlurStyle.normal, 180 - (i * 25));
|
||||
canvas.drawCircle(
|
||||
bottomCircleCenter,
|
||||
bottomCircleRadius + (i * 40),
|
||||
spreadPaint,
|
||||
);
|
||||
}
|
||||
|
||||
// Draw oval border
|
||||
final borderPaint =
|
||||
Paint()
|
||||
..color = Colors.greenAccent
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 4;
|
||||
canvas.drawOval(ovalRect, borderPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
105
lib/presentation/screens/finance_screen.dart
Normal file
105
lib/presentation/screens/finance_screen.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/finance_summary_card.dart';
|
||||
import '../widgets/work_day_card.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
|
||||
class FinanceScreen extends StatefulWidget {
|
||||
final void Function(bool isScrollingDown)? onScrollEvent;
|
||||
|
||||
const FinanceScreen({super.key, this.onScrollEvent});
|
||||
|
||||
@override
|
||||
State<FinanceScreen> createState() => _FinanceScreenState();
|
||||
}
|
||||
|
||||
class _FinanceScreenState extends State<FinanceScreen> {
|
||||
String dropdownValue = "الكل";
|
||||
late ScrollController scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollController = ScrollController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: SafeArea(
|
||||
child: CustomScrollView(
|
||||
|
||||
controller: scrollController,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsBar(
|
||||
selectedIndex: 0,
|
||||
showBackButton: false,
|
||||
iconPaths: [
|
||||
'assets/images/user.svg',
|
||||
'assets/images/ball.svg',
|
||||
],
|
||||
onTap: (index) {
|
||||
if (index == 0) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => UserSettingsScreen(),
|
||||
),
|
||||
);
|
||||
} else if (index == 1) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NotificationsScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 5)),
|
||||
|
||||
/// SUMMARY CARD
|
||||
SliverToBoxAdapter(
|
||||
child: FinanceSummaryCard(
|
||||
totalAmount: "333,000",
|
||||
dropdownValue: dropdownValue,
|
||||
onCalendarTap: () => showDatePicker(
|
||||
context: context,
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime(2030),
|
||||
),
|
||||
onDropdownChanged: (value) {
|
||||
setState(() => dropdownValue = value!);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
/// WORK DAY CARDS
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return const WorkDayCard();
|
||||
},
|
||||
childCount: 3,
|
||||
),
|
||||
),
|
||||
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 120)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
536
lib/presentation/screens/holiday_screen.dart
Normal file
536
lib/presentation/screens/holiday_screen.dart
Normal file
@@ -0,0 +1,536 @@
|
||||
import 'package:coda_project/presentation/screens/notifications_screen.dart';
|
||||
import 'package:coda_project/presentation/screens/user_settings_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
import '../widgets/settings_bar.dart';
|
||||
import 'request_leave_screen.dart';
|
||||
import 'request_advance_scrren.dart';
|
||||
import '../../models/leave_request.dart';
|
||||
import '../../models/advance_request.dart';
|
||||
import '../../core/services/request_service.dart';
|
||||
|
||||
class HolidayScreen extends StatefulWidget {
|
||||
final void Function(bool isScrollingDown)? onScrollEvent;
|
||||
|
||||
const HolidayScreen({super.key, this.onScrollEvent});
|
||||
|
||||
@override
|
||||
State<HolidayScreen> createState() => _HolidayScreenState();
|
||||
}
|
||||
|
||||
class _HolidayScreenState extends State<HolidayScreen> {
|
||||
int activeTab = 0;
|
||||
|
||||
final RequestService _requestService = RequestService();
|
||||
List<LeaveRequest> _leaveRequests = [];
|
||||
List<AdvanceRequest> _advanceRequests = [];
|
||||
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
bool _showActions = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeData();
|
||||
|
||||
_scrollController.addListener(() {
|
||||
final direction = _scrollController.position.userScrollDirection;
|
||||
|
||||
if (direction == ScrollDirection.reverse) {
|
||||
if (_showActions) setState(() => _showActions = false);
|
||||
widget.onScrollEvent?.call(true);
|
||||
} else if (direction == ScrollDirection.forward) {
|
||||
if (!_showActions) setState(() => _showActions = true);
|
||||
widget.onScrollEvent?.call(false);
|
||||
}
|
||||
|
||||
if (_scrollController.position.pixels <= 5) {
|
||||
setState(() {
|
||||
_showActions = true;
|
||||
});
|
||||
widget.onScrollEvent?.call(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _initializeData() async {
|
||||
_leaveRequests = await _requestService.getLeaveRequests();
|
||||
_advanceRequests = await _requestService.getAdvanceRequests();
|
||||
|
||||
_requestService.leaveRequestsStream.listen((requests) {
|
||||
if (mounted) setState(() => _leaveRequests = requests);
|
||||
});
|
||||
|
||||
_requestService.advanceRequestsStream.listen((requests) {
|
||||
if (mounted) setState(() => _advanceRequests = requests);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
// ---------------------------------------------------------
|
||||
// ⭐ MAIN CONTENT - CUSTOM SCROLL VIEW
|
||||
// ---------------------------------------------------------
|
||||
SafeArea(
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
// SETTINGS BAR
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
// First, make sure you have your screens defined
|
||||
|
||||
// Then in your main widget:
|
||||
child: SettingsBar(
|
||||
selectedIndex: 0,
|
||||
showBackButton: false,
|
||||
iconPaths: [
|
||||
'assets/images/user.svg',
|
||||
'assets/images/ball.svg',
|
||||
],
|
||||
onTap: (index) {
|
||||
if (index == 0) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => UserSettingsScreen(),
|
||||
),
|
||||
);
|
||||
} else if (index == 1) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NotificationsScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 5)),
|
||||
|
||||
// TABS SECTION
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 55,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// الأجازات
|
||||
GestureDetector(
|
||||
onTap: () => setState(() => activeTab = 1),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
"الأجازات",
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
color:
|
||||
activeTab == 1
|
||||
? const Color(0xFF8EFDC2)
|
||||
: const Color(0x9EFFFFFF),
|
||||
),
|
||||
),
|
||||
if (activeTab == 1)
|
||||
Container(
|
||||
width: 60,
|
||||
height: 2,
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
color: const Color(0xFF8EFDC2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 70),
|
||||
|
||||
// السلف
|
||||
GestureDetector(
|
||||
onTap: () => setState(() => activeTab = 0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
"السلف",
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
color:
|
||||
activeTab == 0
|
||||
? const Color(0xFF8EFDC2)
|
||||
: const Color(0x9EFFFFFF),
|
||||
),
|
||||
),
|
||||
if (activeTab == 0)
|
||||
Container(
|
||||
width: 60,
|
||||
height: 2,
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
color: const Color(0xFF8EFDC2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 20)),
|
||||
|
||||
// CONTENT LISTS
|
||||
activeTab == 1
|
||||
? _buildLeaveRequestsSliver()
|
||||
: _buildAdvanceRequestsSliver(),
|
||||
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 120)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// ⭐ FLOATING ACTION BUTTONS
|
||||
// ---------------------------------------------------------
|
||||
Positioned(
|
||||
bottom: 150,
|
||||
right: 20,
|
||||
child: AnimatedSlide(
|
||||
offset: _showActions ? Offset.zero : const Offset(0, 1.3),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: AnimatedOpacity(
|
||||
opacity: _showActions ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: Column(
|
||||
children: [
|
||||
_HolidayActionButton(
|
||||
label: "طلب سلفة",
|
||||
svgPath: "assets/images/money2.svg",
|
||||
iconWidth: 28,
|
||||
iconHeight: 20,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const RequestAdvanceScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
_HolidayActionButton(
|
||||
label: "طلب إجازة",
|
||||
svgPath: "assets/images/plus.svg",
|
||||
iconWidth: 28,
|
||||
iconHeight: 20,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const RequestLeaveScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// SLIVERS FOR LISTS
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
Widget _buildLeaveRequestsSliver() {
|
||||
if (_leaveRequests.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"لا توجد طلبات أجازة",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: _buildLeaveRequestCard(_leaveRequests[index]),
|
||||
);
|
||||
}, childCount: _leaveRequests.length),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAdvanceRequestsSliver() {
|
||||
if (_advanceRequests.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"لا توجد طلبات سلف",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: _buildAdvanceRequestCard(_advanceRequests[index]),
|
||||
);
|
||||
}, childCount: _advanceRequests.length),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// CARD BUILDERS (unchanged)
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
Widget _buildLeaveRequestCard(LeaveRequest request) {
|
||||
const bgColor = Color(0xFFEAF8F3);
|
||||
const lineColor = Color(0xFF22A685);
|
||||
|
||||
final bool isWaiting = request.status == "waiting";
|
||||
final bool isApproved = request.status == "approved";
|
||||
final bool isDenied = request.status == "denied";
|
||||
|
||||
final String dateText =
|
||||
"${request.fromDate.year}.${request.fromDate.month}.${request.fromDate.day}";
|
||||
|
||||
return Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
// STATUS ROW
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_statusCircle(
|
||||
isWaiting,
|
||||
"قيد الانتظار",
|
||||
isWaiting ? "assets/images/waiting.svg" : null,
|
||||
const Color(0xFFA58A1B),
|
||||
const Color(0xFFFFDC69),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_statusCircle(
|
||||
isApproved,
|
||||
"موافقة",
|
||||
isApproved ? "assets/images/yes.svg" : null,
|
||||
const Color(0xFF0A8A60),
|
||||
const Color(0xFF00D7A3),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_statusCircle(
|
||||
isDenied,
|
||||
"تم الرفض",
|
||||
isDenied ? "assets/images/no.svg" : null,
|
||||
const Color(0xFFE63946),
|
||||
const Color(0xFFE63946),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// TITLE + DATE
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
"assets/images/holiday.svg",
|
||||
width: 32,
|
||||
height: 32,
|
||||
color: const Color(0xFF11663C),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
request.leaveType,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(dateText),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
Container(height: 1.4, width: double.infinity, color: lineColor),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// REASON ROW
|
||||
Row(
|
||||
textDirection: TextDirection.ltr, // Add this
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(request.reason, textAlign: TextAlign.right),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
"السبب:",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _statusCircle(
|
||||
bool active,
|
||||
String label,
|
||||
String? svg,
|
||||
Color color,
|
||||
Color glow,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: active ? color : const Color(0xFFD6D6D6),
|
||||
boxShadow:
|
||||
active
|
||||
? [BoxShadow(color: glow.withOpacity(0.6), blurRadius: 12)]
|
||||
: [],
|
||||
),
|
||||
child:
|
||||
svg != null
|
||||
? Center(child: SvgPicture.asset(svg, width: 18, height: 18))
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(label, style: const TextStyle(fontSize: 10)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAdvanceRequestCard(AdvanceRequest request) {
|
||||
// Same card logic as before (unchanged)
|
||||
return _buildLeaveRequestCard(
|
||||
LeaveRequest(
|
||||
id: request.id,
|
||||
leaveType: "سلفة ${request.amount}",
|
||||
isTimedLeave: false,
|
||||
fromDate: DateTime.now(),
|
||||
toDate: DateTime.now(),
|
||||
fromTime: TimeOfDay.now(),
|
||||
toTime: TimeOfDay.now(),
|
||||
reason: request.reason,
|
||||
requestDate: DateTime.now(),
|
||||
status: request.status,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// FLOATING BUTTON WIDGET (unchanged)
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
class _HolidayActionButton extends StatelessWidget {
|
||||
final String label;
|
||||
final String svgPath;
|
||||
final VoidCallback onTap;
|
||||
final double iconWidth;
|
||||
final double iconHeight;
|
||||
|
||||
const _HolidayActionButton({
|
||||
required this.label,
|
||||
required this.svgPath,
|
||||
required this.onTap,
|
||||
required this.iconWidth,
|
||||
required this.iconHeight,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x4B00C68B),
|
||||
blurRadius: 20,
|
||||
offset: Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: iconWidth,
|
||||
height: iconHeight,
|
||||
child: SvgPicture.asset(svgPath, fit: BoxFit.contain),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
75
lib/presentation/screens/main_screen.dart
Normal file
75
lib/presentation/screens/main_screen.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../../widgets/floatingnavbar.dart';
|
||||
import 'attendence_screen.dart';
|
||||
import 'finance_screen.dart';
|
||||
import 'holiday_screen.dart';
|
||||
|
||||
class MainPage extends StatefulWidget {
|
||||
const MainPage({super.key});
|
||||
|
||||
@override
|
||||
State<MainPage> createState() => _MainPageState();
|
||||
}
|
||||
|
||||
class _MainPageState extends State<MainPage> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenHeight = MediaQuery.sizeOf(context).height;
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
|
||||
/// BACKGROUND
|
||||
const AppBackground(child: SizedBox()),
|
||||
|
||||
/// ACTIVE SCREEN (fills entire screen - content will extend behind navbar)
|
||||
///
|
||||
Positioned.fill(
|
||||
child: IndexedStack(
|
||||
index: _currentIndex,
|
||||
children: [
|
||||
const AttendanceScreen(),
|
||||
const FinanceScreen(),
|
||||
const HolidayScreen(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
/// FLOATING NAVBAR (positioned above content)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Floatingnavbar(
|
||||
items: [
|
||||
NavBarItem(
|
||||
iconPath: 'assets/images/attendance.svg',
|
||||
label: 'الحضور',
|
||||
),
|
||||
NavBarItem(
|
||||
iconPath: 'assets/images/finance.svg',
|
||||
label: 'المالية',
|
||||
),
|
||||
NavBarItem(
|
||||
iconPath: 'assets/images/holiday.svg',
|
||||
label: 'الإجازة',
|
||||
),
|
||||
],
|
||||
selectedIndex: _currentIndex,
|
||||
onTap: (index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
139
lib/presentation/screens/notifications_screen.dart
Normal file
139
lib/presentation/screens/notifications_screen.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
|
||||
class NotificationsScreen extends StatelessWidget {
|
||||
const NotificationsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
body: AppBackground(
|
||||
child: Column(
|
||||
children: [
|
||||
/// -------------------- SETTINGS BAR --------------------
|
||||
SettingsBar(
|
||||
selectedIndex: 0,
|
||||
onTap: (_) {},
|
||||
showBackButton: true,
|
||||
onBackTap: () => Navigator.pop(context),
|
||||
iconPaths: const [],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
/// -------------------- CONTENT --------------------
|
||||
Expanded(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Column(
|
||||
children: [
|
||||
/// Title
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 40),
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: const Text(
|
||||
"الأشعارات",
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
/// -------------------- NOTIFICATION CARD --------------------
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEFFFFA),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 12,
|
||||
offset: Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
/// ✅ DATE — TOP LEFT
|
||||
const Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: Text(
|
||||
"2025.12.1",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0x9E000000),
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// ✅ MAIN CONTENT — RTL
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"عنوان الاشعار",
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
|
||||
// const SizedBox(height: 6),
|
||||
|
||||
/// ✅ DETAILS WITH GREEN LINE
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Green line
|
||||
Container(
|
||||
width: 20,
|
||||
height: 2,
|
||||
color: Color(0xFF025A42),
|
||||
),
|
||||
|
||||
SizedBox(width: 8),
|
||||
|
||||
Text(
|
||||
"تفاصيل الاشعار",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
154
lib/presentation/screens/onboarding_screen.dart
Normal file
154
lib/presentation/screens/onboarding_screen.dart
Normal file
@@ -0,0 +1,154 @@
|
||||
import 'dart:async';
|
||||
import 'package:coda_project/presentation/screens/auth_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/onboarding_page.dart';
|
||||
import '../widgets/onboarding_button.dart';
|
||||
|
||||
class OnboardingScreen extends StatefulWidget {
|
||||
const OnboardingScreen({super.key});
|
||||
|
||||
@override
|
||||
State<OnboardingScreen> createState() => _OnboardingScreenState();
|
||||
}
|
||||
|
||||
class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
final PageController controller = PageController();
|
||||
int index = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// auto slide
|
||||
Timer.periodic(const Duration(seconds: 4), (timer) {
|
||||
if (!mounted) return;
|
||||
int next = index == 1 ? 0 : index + 1;
|
||||
|
||||
controller.animateToPage(
|
||||
next,
|
||||
duration: const Duration(milliseconds: 600),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
/// BACKGROUND GRADIENT (base layer)
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
stops: [0.0, 0.45, 0.75, 1.0],
|
||||
colors: [
|
||||
Color(0xFF2E2E2E),
|
||||
Color(0xFF00271D),
|
||||
Color(0xFF005841),
|
||||
Color.fromARGB(176, 62, 254, 203),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// BLURRED CIRCLE (subtle rounded glow)
|
||||
Positioned(
|
||||
bottom: -120,
|
||||
left: -60,
|
||||
right: -60,
|
||||
child: Container(
|
||||
height: 300,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Color.fromARGB(
|
||||
0,
|
||||
62,
|
||||
254,
|
||||
203,
|
||||
), // same green with opacity
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Color.fromARGB(
|
||||
69,
|
||||
62,
|
||||
254,
|
||||
142,
|
||||
), // stronger outer glow
|
||||
blurRadius: 60,
|
||||
spreadRadius: 100,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 70),
|
||||
Image.asset("assets/images/logo2.png", width: 200),
|
||||
|
||||
/// PAGEVIEW (SVG + TEXT ONLY)
|
||||
Expanded(
|
||||
child: PageView(
|
||||
physics: BouncingScrollPhysics(),
|
||||
controller: controller,
|
||||
onPageChanged: (i) => setState(() => index = i),
|
||||
children: const [
|
||||
OnboardingPage(
|
||||
imagePath: "assets/images/Onboarding1.svg",
|
||||
text:
|
||||
"سجل دخولك وخروجك بسهولة وتابع دوامك\nيومياً بدون تعقيد",
|
||||
),
|
||||
OnboardingPage(
|
||||
imagePath: "assets/images/Onboarding2.svg",
|
||||
text:
|
||||
"اعرف تفاصيل راتبك وقدّم طلب الإجازة\nوتابع حالته بكل شفافية",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 40),
|
||||
child: OnboardingButton(
|
||||
text: "تسجيل دخول",
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AuthScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
/// DOTS INDICATOR
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(2, (i) {
|
||||
bool active = i == index;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: active ? Colors.white : Colors.white60,
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
258
lib/presentation/screens/request_advance_scrren.dart
Normal file
258
lib/presentation/screens/request_advance_scrren.dart
Normal file
@@ -0,0 +1,258 @@
|
||||
import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
import '../widgets/onboarding_button.dart';
|
||||
import '../../models/advance_request.dart';
|
||||
import '../../core/services/request_service.dart';
|
||||
|
||||
class RequestAdvanceScreen extends StatefulWidget {
|
||||
const RequestAdvanceScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RequestAdvanceScreen> createState() => _RequestAdvanceScreenState();
|
||||
}
|
||||
|
||||
class _RequestAdvanceScreenState extends State<RequestAdvanceScreen> {
|
||||
|
||||
// Text controller for amount
|
||||
final TextEditingController amountController = TextEditingController();
|
||||
// Text controller for reason
|
||||
final TextEditingController reasonController = TextEditingController();
|
||||
|
||||
// Use the singleton instance
|
||||
final RequestService _requestService = RequestService();
|
||||
|
||||
// Method to save the advance request
|
||||
Future<void> _saveAdvanceRequest() async {
|
||||
if (amountController.text.isEmpty || reasonController.text.isEmpty) {
|
||||
// Show an error message if fields are empty
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('الرجاء إدخال جميع الحقول'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the amount to double
|
||||
final amount = double.tryParse(amountController.text);
|
||||
if (amount == null || amount <= 0) {
|
||||
// Show an error message if amount is invalid
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('الرجاء إدخال مبلغ صحيح'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new advance request with default status "waiting"
|
||||
final advanceRequest = AdvanceRequest(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
amount: amount,
|
||||
reason: reasonController.text,
|
||||
status: "waiting", // Default status
|
||||
);
|
||||
|
||||
try {
|
||||
// Save the advance request
|
||||
await _requestService.addAdvanceRequest(advanceRequest);
|
||||
|
||||
// Show a success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('تم إرسال طلب السلفة بنجاح'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
|
||||
// Navigate back to the previous screen
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
// Show an error message if something went wrong
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('حدث خطأ: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: AppBackground(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Settings bar
|
||||
SettingsBar(
|
||||
selectedIndex: -1,
|
||||
onTap: (_) {},
|
||||
showBackButton: true,
|
||||
onBackTap: () => Navigator.pop(context),
|
||||
iconPaths: const [
|
||||
"assets/images/user.svg",
|
||||
"assets/images/bell.svg",
|
||||
],
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title for advance request
|
||||
const Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Text(
|
||||
"طلب سلفة",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
//=============================
|
||||
// AMOUNT INPUT
|
||||
//=============================
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: const Text(
|
||||
"المبلغ المطلوب",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 6),
|
||||
|
||||
Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
height: 58,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x22000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: amountController,
|
||||
textAlign: TextAlign.right,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: "دع .000 ",
|
||||
hintStyle: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// =============================
|
||||
// REASON TEXTFIELD
|
||||
// =============================
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: const Text(
|
||||
"السبب",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// OUTER BORDER CONTAINER
|
||||
Container(
|
||||
padding: const EdgeInsets.all(
|
||||
15,
|
||||
), // border thickness space
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Color(0xFF00FFAA), // green border
|
||||
width: 0.5,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
|
||||
// INNER TEXTFIELD CONTAINER
|
||||
child: Container(
|
||||
height: 100,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEAEAEA),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
// Added Directionality to fix text direction
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: TextField(
|
||||
controller: reasonController,
|
||||
maxLines: 5,
|
||||
textAlign:
|
||||
TextAlign
|
||||
.right, // Added this to align text to the right
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText:
|
||||
"اكتب السبب", // Added placeholder text
|
||||
hintStyle: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 70),
|
||||
|
||||
// CONFIRM BUTTON
|
||||
Center(
|
||||
child: OnboardingButton(
|
||||
text: "تأكيد الطلب",
|
||||
backgroundColor: const Color(0xFFD1FEF0),
|
||||
textColor: Colors.black,
|
||||
onPressed: _saveAdvanceRequest, // Call the save method
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
642
lib/presentation/screens/request_leave_screen.dart
Normal file
642
lib/presentation/screens/request_leave_screen.dart
Normal file
@@ -0,0 +1,642 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
import '../widgets/onboarding_button.dart';
|
||||
import '../../models/leave_request.dart';
|
||||
import '../../core/services/request_service.dart';
|
||||
|
||||
class RequestLeaveScreen extends StatefulWidget {
|
||||
const RequestLeaveScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RequestLeaveScreen> createState() => _RequestLeaveScreenState();
|
||||
}
|
||||
|
||||
class _RequestLeaveScreenState extends State<RequestLeaveScreen> {
|
||||
// Dropdown value - initialize with default
|
||||
String leaveType = "إجازة مرضية ";
|
||||
|
||||
// Toggle switch
|
||||
bool isTimedLeave = false;
|
||||
|
||||
// Date & time selectors
|
||||
DateTime? fromDate = DateTime.now();
|
||||
DateTime? toDate = DateTime.now();
|
||||
|
||||
TimeOfDay? fromTime = const TimeOfDay(hour: 12, minute: 00);
|
||||
TimeOfDay? toTime = const TimeOfDay(hour: 12, minute: 00);
|
||||
|
||||
// Text controller for reason
|
||||
final TextEditingController reasonController = TextEditingController();
|
||||
|
||||
// Use the singleton instance
|
||||
final RequestService _requestService = RequestService();
|
||||
|
||||
/// PICK DATE
|
||||
Future<void> pickDate(bool isFrom) async {
|
||||
DateTime initial = isFrom ? fromDate! : toDate!;
|
||||
DateTime? newDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: initial,
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime(2035),
|
||||
builder: (context, child) {
|
||||
return Theme(data: ThemeData.dark(), child: child!);
|
||||
},
|
||||
);
|
||||
|
||||
if (newDate != null) {
|
||||
setState(() {
|
||||
if (isFrom) {
|
||||
fromDate = newDate;
|
||||
} else {
|
||||
toDate = newDate;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// PICK TIME
|
||||
Future<void> pickTime(bool isFrom) async {
|
||||
TimeOfDay initial = isFrom ? fromTime! : toTime!;
|
||||
TimeOfDay? newTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: initial,
|
||||
builder: (context, child) {
|
||||
return Theme(data: ThemeData.dark(), child: child!);
|
||||
},
|
||||
);
|
||||
|
||||
if (newTime != null) {
|
||||
setState(() {
|
||||
if (isFrom) {
|
||||
fromTime = newTime;
|
||||
} else {
|
||||
toTime = newTime;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Method to save the leave request
|
||||
Future<void> _saveLeaveRequest() async {
|
||||
if (reasonController.text.isEmpty) {
|
||||
// Show an error message if reason is empty
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('الرجاء إدخال السبب'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new leave request with default status "waiting"
|
||||
final leaveRequest = LeaveRequest(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
leaveType: leaveType, // Use the current leaveType value
|
||||
isTimedLeave: isTimedLeave,
|
||||
fromDate: fromDate!,
|
||||
toDate: toDate!,
|
||||
fromTime: fromTime!,
|
||||
toTime: toTime!,
|
||||
reason: reasonController.text,
|
||||
requestDate: DateTime.now(),
|
||||
status: "waiting", // Default status
|
||||
);
|
||||
|
||||
try {
|
||||
// Save the leave request
|
||||
await _requestService.addLeaveRequest(leaveRequest);
|
||||
|
||||
// Show a success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('تم إرسال طلب الأجازة بنجاح'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
|
||||
// Navigate back to the previous screen
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
// Show an error message if something went wrong
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('حدث خطأ: $e'), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: true, // ✅ IMPORTANT
|
||||
body: AppBackground(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// In your RequestLeaveScreen's build method, replace the SettingsBar with this:
|
||||
SettingsBar(
|
||||
selectedIndex: -1,
|
||||
onTap: (_) {},
|
||||
showBackButton: true,
|
||||
onBackTap: () => Navigator.pop(context),
|
||||
iconPaths: const [
|
||||
"assets/images/user.svg",
|
||||
"assets/images/bell.svg",
|
||||
],
|
||||
),
|
||||
|
||||
// Title
|
||||
// const SizedBox(height: 30),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Fixed alignment for "طلب أجازة"
|
||||
const Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Text(
|
||||
"طلب أجازة ",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
//=============================
|
||||
// DROPDOWN: نوع الإجازة
|
||||
//=============================
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: const Text(
|
||||
"نوع الإجازة",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 5),
|
||||
|
||||
// Modified dropdown with disabled state
|
||||
Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Opacity(
|
||||
opacity: isTimedLeave ? 0.45 : 1.0,
|
||||
child: IgnorePointer(
|
||||
ignoring: isTimedLeave,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
),
|
||||
height: 58,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isTimedLeave
|
||||
? Colors.grey[300]
|
||||
: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x22000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: leaveType,
|
||||
icon: const Icon(
|
||||
Icons.keyboard_arrow_down_rounded,
|
||||
color: Colors.black,
|
||||
size: 28,
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 17,
|
||||
),
|
||||
isExpanded: true,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
leaveType = value!;
|
||||
// Set toggle based on selected value
|
||||
isTimedLeave = value == "أجازة زمنية";
|
||||
});
|
||||
},
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: "إجازة مرضية ",
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text("إجازة مرضية "),
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: "إجازة مدفوعة",
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text("إجازة مدفوعة"),
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: "إجازة غير مدفوعة",
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text("إجازة غير مدفوعة"),
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: "أجازة زمنية",
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text("أجازة زمنية"),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 25),
|
||||
|
||||
//=============================
|
||||
// PERFECT CUSTOM TOGGLE (NEW)
|
||||
//=============================
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
isTimedLeave = !isTimedLeave;
|
||||
// Set leave type to "أجازة زمنية" when toggle is ON
|
||||
if (isTimedLeave) {
|
||||
leaveType = "أجازة زمنية";
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
// ---------- TOGGLE ----------
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
width: 75,
|
||||
height: 30,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isTimedLeave
|
||||
? const Color(
|
||||
0xFF0A6B4A,
|
||||
) // ON green track
|
||||
: const Color(
|
||||
0xFF9E9E9E,
|
||||
), // OFF grey track
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
child: Align(
|
||||
alignment:
|
||||
isTimedLeave
|
||||
? Alignment.centerRight
|
||||
: Alignment.centerLeft,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isTimedLeave
|
||||
? const Color(0xFF12BE85) // ON knob
|
||||
: Colors.white, // OFF knob
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0x2D000000),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 14),
|
||||
|
||||
// ---------- Dot (ALWAYS visible) ----------
|
||||
Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF32C59A),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 6),
|
||||
|
||||
// ---------- Line (ALWAYS visible) ----------
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 2,
|
||||
color: const Color(0xFF32C59A),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// ---------- Label ----------
|
||||
const Text(
|
||||
"إجازة زمنية",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 19,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// =============================
|
||||
// DATE PICKER BOX
|
||||
// =============================
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF4F4F4),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x33000000),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.end, // ALIGN RIGHT
|
||||
children: [
|
||||
// TEXT aligned right
|
||||
const Text(
|
||||
"فترة الإجازة",
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// 🟢 YOUR CUSTOM SVG ICON
|
||||
SvgPicture.asset(
|
||||
"assets/images/calendar.svg", // <-- replace with your icon filename
|
||||
width: 24,
|
||||
height: 24,
|
||||
color: Color(
|
||||
0xFF007C46,
|
||||
), // Optional: match your green
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// From date row
|
||||
_dateRow(
|
||||
label: "من",
|
||||
date: fromDate!,
|
||||
time: fromTime!,
|
||||
onDateTap:
|
||||
isTimedLeave ? null : () => pickDate(true),
|
||||
onTimeTap: () => pickTime(true),
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// To date row
|
||||
_dateRow(
|
||||
label: "الى",
|
||||
date: toDate!,
|
||||
time: toTime!,
|
||||
onDateTap:
|
||||
isTimedLeave ? null : () => pickDate(false),
|
||||
onTimeTap: () => pickTime(false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// =============================
|
||||
// REASON TEXTFIELD (Two Containers)
|
||||
// =============================
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: const Text(
|
||||
"السبب",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// OUTER BORDER CONTAINER
|
||||
Container(
|
||||
padding: const EdgeInsets.all(
|
||||
12,
|
||||
), // border thickness space
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Color(0xFF00FFAA), // green border
|
||||
width: 0.5,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
|
||||
// INNER TEXTFIELD CONTAINER
|
||||
child: Container(
|
||||
height: 70,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEAEAEA),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
// Added Directionality to fix text direction
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: TextField(
|
||||
controller: reasonController,
|
||||
maxLines: 5,
|
||||
textAlign:
|
||||
TextAlign
|
||||
.right, // Added this to align text to the right
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText:
|
||||
"اكتب السبب", // Added placeholder text
|
||||
hintStyle: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// CONFIRM BUTTON
|
||||
Center(
|
||||
child: OnboardingButton(
|
||||
text: "تأكيد الطلب",
|
||||
backgroundColor: const Color(0xFFD1FEF0),
|
||||
textColor: Colors.black, // Changed to black
|
||||
onPressed: _saveLeaveRequest, // Call the save method
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// CUSTOM DATE ROW WIDGET
|
||||
// ===============================================================
|
||||
Widget _dateRow({
|
||||
required String label,
|
||||
required DateTime date,
|
||||
required TimeOfDay time,
|
||||
required VoidCallback? onDateTap,
|
||||
required VoidCallback onTimeTap,
|
||||
}) {
|
||||
bool dateDisabled = onDateTap == null;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
height: 55,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// -----------------------
|
||||
// DATE PART (can be disabled)
|
||||
// -----------------------
|
||||
Opacity(
|
||||
opacity: dateDisabled ? 0.45 : 1,
|
||||
child: IgnorePointer(
|
||||
ignoring: dateDisabled,
|
||||
child: GestureDetector(
|
||||
onTap: onDateTap,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
"${date.year}-${date.month}-${date.day}",
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
_weekday(date.weekday),
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// -----------------------
|
||||
// TIME PART (always active)
|
||||
// -----------------------
|
||||
GestureDetector(
|
||||
onTap: onTimeTap,
|
||||
child: Text(
|
||||
"${time.hour}:${time.minute.toString().padLeft(2, '0')}",
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 10),
|
||||
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(color: Colors.black, fontSize: 15),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _weekday(int day) {
|
||||
switch (day) {
|
||||
case 1:
|
||||
return "الإثنين";
|
||||
case 2:
|
||||
return "الثلاثاء";
|
||||
case 3:
|
||||
return "الأربعاء";
|
||||
case 4:
|
||||
return "الخميس";
|
||||
case 5:
|
||||
return "الجمعة";
|
||||
case 6:
|
||||
return "السبت";
|
||||
case 7:
|
||||
return "الأحد";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
40
lib/presentation/screens/splash_screen.dart
Normal file
40
lib/presentation/screens/splash_screen.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'onboarding_screen.dart';
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SplashScreen> createState() => _SplashScreenState();
|
||||
}
|
||||
|
||||
class _SplashScreenState extends State<SplashScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
FlutterNativeSplash.remove();
|
||||
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => OnboardingScreen()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage("assets/images/splash.png"),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
child: Center(child: Image.asset("assets/images/logo.png", width: 200)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
317
lib/presentation/screens/user_settings_screen.dart
Normal file
317
lib/presentation/screens/user_settings_screen.dart
Normal file
@@ -0,0 +1,317 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../widgets/app_background.dart';
|
||||
import '../widgets/settings_bar.dart';
|
||||
import 'about_screen.dart';
|
||||
import 'auth_screen.dart';
|
||||
import '../widgets/change_password_modal.dart';
|
||||
|
||||
|
||||
class UserSettingsScreen extends StatefulWidget {
|
||||
const UserSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<UserSettingsScreen> createState() => _UserSettingsScreenState();
|
||||
}
|
||||
|
||||
class _UserSettingsScreenState extends State<UserSettingsScreen> {
|
||||
bool _notificationsOn = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
body: AppBackground(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
/// -------------------- SETTINGS BAR --------------------
|
||||
SettingsBar(
|
||||
selectedIndex: 0,
|
||||
onTap: (_) {},
|
||||
showBackButton: true,
|
||||
onBackTap: () => Navigator.pop(context),
|
||||
iconPaths: const [],
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
/// -------------------- PAGE TITLE --------------------
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 40),
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: const Text(
|
||||
"الإعدادات",
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// -------------------- FORM CONTAINER --------------------
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 40),
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.85,
|
||||
height: MediaQuery.of(context).size.height * 0.62,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xF3EEFFFA),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 30,
|
||||
offset: Offset(0, 20),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 22,
|
||||
vertical: 55,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
|
||||
/// -------------------- USER NAME --------------------
|
||||
const Text(
|
||||
"اسم الموظف",
|
||||
style: TextStyle(
|
||||
fontSize: 25,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 6),
|
||||
|
||||
/// ✅ CLICKABLE + UNDERLINED
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder:
|
||||
(_) => const ChangePasswordModal(),
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
"تغيير كلمة المرور",
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Colors.black54,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationThickness: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
SvgPicture.asset(
|
||||
"assets/images/edit.svg",
|
||||
width: 22,
|
||||
height: 22,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
/// -------------------- NOTIFICATION SWITCH --------------------
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_notificationsOn = !_notificationsOn;
|
||||
});
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(
|
||||
milliseconds: 250,
|
||||
),
|
||||
width: 75,
|
||||
height: 30,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
_notificationsOn
|
||||
? const Color(0xFF0A6B4A)
|
||||
: const Color(0xFFB5B5B5),
|
||||
borderRadius: BorderRadius.circular(
|
||||
40,
|
||||
),
|
||||
),
|
||||
child: Align(
|
||||
alignment:
|
||||
_notificationsOn
|
||||
? Alignment.centerRight
|
||||
: Alignment.centerLeft,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(
|
||||
milliseconds: 250,
|
||||
),
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
_notificationsOn
|
||||
? const Color(0xFF12BE85)
|
||||
: const Color(0xFF0F0F0F),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x2D000000),
|
||||
blurRadius: 6,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
"الإشعارات",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SvgPicture.asset(
|
||||
"assets/images/ball2.svg",
|
||||
width: 22,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
_divider(),
|
||||
|
||||
_settingsRow(
|
||||
label: "عن الشركة",
|
||||
icon: "assets/images/info.svg",
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const AboutScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
_divider(),
|
||||
|
||||
_settingsRow(
|
||||
label: "تسجيل خروج",
|
||||
icon: "assets/images/logout2.svg",
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const AuthScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// -------------------- AVATAR --------------------
|
||||
Positioned(
|
||||
top: -55,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: const Color(0xFFE1E1E1),
|
||||
border: Border.all(
|
||||
color: const Color(0xFF2D5E50),
|
||||
width: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _divider() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 1,
|
||||
color: const Color(0xFF008864),
|
||||
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _settingsRow({
|
||||
required String label,
|
||||
required String icon,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const SizedBox(width: 24),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
SvgPicture.asset(icon, width: 23),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
128
lib/presentation/widgets/FloatingNavBar.dart
Normal file
128
lib/presentation/widgets/FloatingNavBar.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class NavBarItem {
|
||||
final String iconPath;
|
||||
final String label;
|
||||
|
||||
NavBarItem({required this.iconPath, required this.label});
|
||||
}
|
||||
|
||||
class Floatingnavbar extends StatelessWidget {
|
||||
final List<NavBarItem> items;
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onTap;
|
||||
|
||||
const Floatingnavbar({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.selectedIndex,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(45),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
|
||||
child: Container(
|
||||
height: 70,
|
||||
margin: EdgeInsets.only(
|
||||
bottom: 10 + bottomPadding,
|
||||
left: 28,
|
||||
right: 28,
|
||||
),
|
||||
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white, // ⭐ frosted-glass effect
|
||||
borderRadius: BorderRadius.circular(45),
|
||||
border: Border.all(
|
||||
color: const Color(0x4DFFFFFF), // subtle glass border
|
||||
width: 1.2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, -5),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: items.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final item = entry.value;
|
||||
final isSelected = selectedIndex == index;
|
||||
|
||||
return _NavBarItemTile(
|
||||
item: item,
|
||||
isSelected: isSelected,
|
||||
onTap: () => onTap(index),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class _NavBarItemTile extends StatelessWidget {
|
||||
final NavBarItem item;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _NavBarItemTile({
|
||||
Key? key,
|
||||
required this.item,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(45),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
item.iconPath,
|
||||
width: 30,
|
||||
height: 30,
|
||||
colorFilter: ColorFilter.mode(
|
||||
isSelected ? const Color(0xFF177046) : Colors.black,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 6),
|
||||
|
||||
Text(
|
||||
item.label,
|
||||
style: TextStyle(
|
||||
color: isSelected ? const Color(0xFF177046) : Colors.black,
|
||||
fontSize: 15,
|
||||
fontFamily: 'AbdEriady',
|
||||
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
70
lib/presentation/widgets/app_background.dart
Normal file
70
lib/presentation/widgets/app_background.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppBackground extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const AppBackground({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color.fromARGB(255, 41, 41, 41), // top dark gray
|
||||
Color.fromARGB(255, 0, 20, 15), // bottom deep green
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: -250,
|
||||
left: 100,
|
||||
right: -200,
|
||||
child: Container(
|
||||
height: 300,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
// very soft inner fill
|
||||
color: Color.fromARGB(0, 62, 254, 203),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
// wide soft bloom
|
||||
color: Color.fromARGB(69, 62, 254, 190),
|
||||
blurRadius: 140,
|
||||
spreadRadius: 160,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
bottom: 100,
|
||||
left: -140,
|
||||
right: -120,
|
||||
child: Container(
|
||||
height: 320,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Color.fromARGB(0, 62, 254, 203),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Color.fromARGB(83, 62, 254, 190),
|
||||
blurRadius: 180,
|
||||
spreadRadius: 60,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
310
lib/presentation/widgets/auth_form.dart
Normal file
310
lib/presentation/widgets/auth_form.dart
Normal file
@@ -0,0 +1,310 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../screens/main_screen.dart';
|
||||
import '../../domain/models/login_request.dart';
|
||||
import '../blocs/login/login_bloc.dart';
|
||||
import '../blocs/login/login_event.dart';
|
||||
import '../blocs/login/login_state.dart';
|
||||
import 'onboarding_button.dart';
|
||||
|
||||
class AuthForm extends StatefulWidget {
|
||||
final VoidCallback? onSubmit;
|
||||
|
||||
const AuthForm({super.key, this.onSubmit});
|
||||
|
||||
@override
|
||||
State<AuthForm> createState() => _AuthFormState();
|
||||
}
|
||||
|
||||
class _AuthFormState extends State<AuthForm> {
|
||||
bool _obscure = true;
|
||||
|
||||
// Text controllers
|
||||
final TextEditingController _phoneNumberController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
|
||||
// Focus nodes for text fields
|
||||
late FocusNode _phoneNumberFocusNode;
|
||||
late FocusNode _passwordFocusNode;
|
||||
|
||||
void _handleLogin() {
|
||||
// Validate inputs
|
||||
if (_phoneNumberController.text.trim().isEmpty) {
|
||||
_showError('الرجاء إدخال رقم الهاتف');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_passwordController.text.trim().isEmpty) {
|
||||
_showError('الرجاء إدخال كلمة المرور');
|
||||
return;
|
||||
}
|
||||
|
||||
// Unfocus any focused text field
|
||||
_phoneNumberFocusNode.unfocus();
|
||||
_passwordFocusNode.unfocus();
|
||||
|
||||
// Dispatch login event
|
||||
final request = LoginRequest(
|
||||
phoneNumber: _phoneNumberController.text.trim(),
|
||||
password: _passwordController.text.trim(),
|
||||
);
|
||||
|
||||
context.read<LoginBloc>().add(LoginSubmitted(request));
|
||||
}
|
||||
|
||||
void _showError(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.red,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize focus nodes
|
||||
_phoneNumberFocusNode = FocusNode();
|
||||
_passwordFocusNode = FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Clean up controllers and focus nodes
|
||||
_phoneNumberController.dispose();
|
||||
_passwordController.dispose();
|
||||
_phoneNumberFocusNode.dispose();
|
||||
_passwordFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get screen dimensions
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final screenWidth = screenSize.width;
|
||||
final screenHeight = screenSize.height;
|
||||
|
||||
// Calculate responsive dimensions
|
||||
final formWidth = screenWidth > 600 ? screenWidth * 0.5 : screenWidth * 0.9;
|
||||
final formHeight =
|
||||
screenHeight > 800 ? screenHeight * 0.6 : screenHeight * 0.8;
|
||||
final borderWidth = formWidth + 20;
|
||||
final titleFontSize = screenWidth > 600 ? 28.0 : 24.0;
|
||||
final labelFontSize = screenWidth > 600 ? 18.0 : 16.0;
|
||||
final fieldFontSize = screenWidth > 600 ? 18.0 : 16.0;
|
||||
final verticalSpacing = screenHeight > 800 ? 34.0 : 24.0;
|
||||
final fieldSpacing = screenHeight > 800 ? 30.0 : 20.0;
|
||||
final buttonSpacing = screenHeight > 800 ? 100.0 : 60.0;
|
||||
final bottomSpacing = screenHeight > 800 ? 40.0 : 20.0;
|
||||
final horizontalPadding = screenWidth > 600 ? 30.0 : 25.0;
|
||||
final verticalPadding = screenHeight > 800 ? 38.0 : 28.0;
|
||||
|
||||
return BlocListener<LoginBloc, LoginState>(
|
||||
listener: (context, state) {
|
||||
if (state is LoginSuccess) {
|
||||
// Call the onSubmit callback if provided
|
||||
if (widget.onSubmit != null) {
|
||||
widget.onSubmit!();
|
||||
}
|
||||
|
||||
// Navigate to the MainPage
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const MainPage()),
|
||||
);
|
||||
} else if (state is LoginError) {
|
||||
_showError(state.message);
|
||||
}
|
||||
},
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: FocusScope(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Border container - decorative element behind the form
|
||||
Container(
|
||||
width: borderWidth,
|
||||
constraints: BoxConstraints(
|
||||
minHeight: formHeight + 40,
|
||||
maxHeight:
|
||||
formHeight + 80, // Allows shrinking when keyboard opens
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
border: Border.all(color: const Color(0xDD00C28E), width: 1),
|
||||
),
|
||||
),
|
||||
|
||||
// Main form container
|
||||
Container(
|
||||
width: formWidth,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: horizontalPadding,
|
||||
vertical: verticalPadding,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEEFFFA),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
/// Title
|
||||
Center(
|
||||
child: Text(
|
||||
"تسجيل دخول",
|
||||
style: TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: verticalSpacing),
|
||||
|
||||
/// Phone Number Label
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
"رقم الهاتف",
|
||||
style: TextStyle(
|
||||
fontSize: labelFontSize,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildField(
|
||||
controller: _phoneNumberController,
|
||||
hint: "رقم الهاتف",
|
||||
obscure: false,
|
||||
keyboardType: TextInputType.phone,
|
||||
focusNode: _phoneNumberFocusNode,
|
||||
textInputAction: TextInputAction.next,
|
||||
onSubmitted: (_) {
|
||||
// Move focus to password field when next is pressed
|
||||
FocusScope.of(context).requestFocus(_passwordFocusNode);
|
||||
},
|
||||
fontSize: fieldFontSize,
|
||||
),
|
||||
|
||||
SizedBox(height: fieldSpacing),
|
||||
|
||||
/// Password Label
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
"كلمة المرور",
|
||||
style: TextStyle(
|
||||
fontSize: labelFontSize,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildField(
|
||||
controller: _passwordController,
|
||||
hint: "كلمة المرور",
|
||||
obscure: _obscure,
|
||||
hasEye: true,
|
||||
focusNode: _passwordFocusNode,
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => _handleLogin(),
|
||||
fontSize: fieldFontSize,
|
||||
),
|
||||
|
||||
SizedBox(height: buttonSpacing), // Responsive spacing
|
||||
|
||||
BlocBuilder<LoginBloc, LoginState>(
|
||||
builder: (context, state) {
|
||||
final isLoading = state is LoginLoading;
|
||||
return Center(
|
||||
child: OnboardingButton(
|
||||
text: isLoading ? "جاري تسجيل الدخول..." : "تسجيل دخول",
|
||||
backgroundColor: const Color.fromARGB(239, 35, 87, 74),
|
||||
onPressed: isLoading ? null : _handleLogin,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
SizedBox(height: bottomSpacing),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildField({
|
||||
TextEditingController? controller,
|
||||
required String hint,
|
||||
required bool obscure,
|
||||
bool hasEye = false,
|
||||
TextInputType? keyboardType,
|
||||
FocusNode? focusNode,
|
||||
TextInputAction? textInputAction,
|
||||
Function(String)? onSubmitted,
|
||||
required double fontSize,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xDEDEDEDE),
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 2)),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
obscureText: obscure,
|
||||
keyboardType: keyboardType,
|
||||
textAlign: TextAlign.right,
|
||||
textInputAction: textInputAction,
|
||||
onSubmitted: onSubmitted,
|
||||
style: TextStyle(fontSize: fontSize),
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(color: Colors.black54),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
suffixIcon:
|
||||
hasEye
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
obscure ? Icons.visibility_off : Icons.visibility,
|
||||
color: Colors.black54,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => _obscure = !obscure);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
170
lib/presentation/widgets/change_password_modal.dart
Normal file
170
lib/presentation/widgets/change_password_modal.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'onboarding_button.dart';
|
||||
|
||||
class ChangePasswordModal extends StatefulWidget {
|
||||
const ChangePasswordModal({super.key});
|
||||
|
||||
@override
|
||||
State<ChangePasswordModal> createState() => _ChangePasswordModalState();
|
||||
}
|
||||
|
||||
class _ChangePasswordModalState extends State<ChangePasswordModal> {
|
||||
bool _oldObscure = true;
|
||||
// bool _newObscure = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final screenWidth = screenSize.width;
|
||||
final screenHeight = screenSize.height;
|
||||
|
||||
final formWidth =
|
||||
screenWidth > 600 ? screenWidth * 0.5 : screenWidth * 0.88;
|
||||
final labelFontSize = screenWidth > 600 ? 18.0 : 16.0;
|
||||
final fieldFontSize = screenWidth > 600 ? 18.0 : 16.0;
|
||||
final verticalPadding = screenHeight > 800 ? 38.0 : 28.0;
|
||||
final fieldSpacing = screenHeight > 800 ? 30.0 : 20.0;
|
||||
final buttonSpacing = screenHeight > 800 ? 80.0 : 60.0;
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
children: [
|
||||
/// ✅ DARK TRANSPARENT OVERLAY (NO BLUR)
|
||||
Positioned.fill(child: Container(color: const Color(0x80000000))),
|
||||
|
||||
/// ---------- MODAL ----------
|
||||
Center(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Container(
|
||||
width: formWidth,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 25,
|
||||
vertical: verticalPadding,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEEFFFA),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
/// ---------- OLD PASSWORD ----------
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
"كلمة المرور السابقة",
|
||||
style: TextStyle(
|
||||
fontSize: labelFontSize,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildField(
|
||||
hint: "أدخل كلمة المرور",
|
||||
obscure: _oldObscure,
|
||||
fontSize: fieldFontSize,
|
||||
hasEye: true, // ✅ eye here
|
||||
onEyeTap:
|
||||
() => setState(() => _oldObscure = !_oldObscure),
|
||||
),
|
||||
|
||||
SizedBox(height: fieldSpacing),
|
||||
|
||||
/// ---------- NEW PASSWORD ----------
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
"كلمة المرور الجديدة",
|
||||
style: TextStyle(
|
||||
fontSize: labelFontSize,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildField(
|
||||
hint: "كلمة المرور",
|
||||
obscure: false,
|
||||
fontSize: fieldFontSize,
|
||||
hasEye: false,
|
||||
onEyeTap: () {}, // unused
|
||||
),
|
||||
|
||||
SizedBox(height: buttonSpacing),
|
||||
|
||||
Center(
|
||||
child: OnboardingButton(
|
||||
text: "حفظ التغيير",
|
||||
backgroundColor: const Color(0xEE23574A),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildField({
|
||||
required String hint,
|
||||
required bool obscure,
|
||||
required double fontSize,
|
||||
required bool hasEye,
|
||||
required VoidCallback onEyeTap,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xDEDEDEDE),
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 2)),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
obscureText: obscure,
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(fontSize: fontSize),
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(color: Colors.black54),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
suffixIcon:
|
||||
hasEye
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
obscure ? Icons.visibility_off : Icons.visibility,
|
||||
color: Colors.black54,
|
||||
),
|
||||
onPressed: onEyeTap,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
177
lib/presentation/widgets/finance_summary_card.dart
Normal file
177
lib/presentation/widgets/finance_summary_card.dart
Normal file
@@ -0,0 +1,177 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class FinanceSummaryCard extends StatelessWidget {
|
||||
final String totalAmount;
|
||||
final String dropdownValue;
|
||||
final VoidCallback onCalendarTap;
|
||||
final ValueChanged<String?> onDropdownChanged;
|
||||
|
||||
const FinanceSummaryCard({
|
||||
super.key,
|
||||
required this.totalAmount,
|
||||
required this.dropdownValue,
|
||||
required this.onCalendarTap,
|
||||
required this.onDropdownChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
/// ==========================
|
||||
/// TOP AMOUNT + ICON
|
||||
/// ==========================
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"د.ع $totalAmount",
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFBA4404),
|
||||
),
|
||||
),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
"المبلغ الكلي",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
SvgPicture.asset("assets/images/money2.svg", width: 50),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
/// ==========================
|
||||
/// MONTH FILTER BOX
|
||||
/// ==========================
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFF0A8F6B), width: 1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// CALENDAR BUTTON
|
||||
GestureDetector(
|
||||
onTap: onCalendarTap,
|
||||
child: Container(
|
||||
width: 90,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEAEAEA),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
"assets/images/calendar.svg",
|
||||
width: 28,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text("الشهر", style: TextStyle(fontSize: 15)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 14),
|
||||
|
||||
// DROPDOWN MENU
|
||||
Expanded(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEAEAEA),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: dropdownValue,
|
||||
icon: const Icon(
|
||||
Icons.arrow_drop_down,
|
||||
size: 35,
|
||||
color: Color(0xFF0A8F6B),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
color: Color.from(
|
||||
alpha: 1,
|
||||
red: 0,
|
||||
green: 0,
|
||||
blue: 0,
|
||||
),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
onChanged: onDropdownChanged,
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: "الكل",
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Text("الكل"),
|
||||
),
|
||||
),
|
||||
|
||||
DropdownMenuItem(
|
||||
value: "ساعات أضافية",
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Text("ساعات أضافية"),
|
||||
),
|
||||
),
|
||||
|
||||
DropdownMenuItem(
|
||||
value: "مكافئة",
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Text("مكافئة"),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: " عقوبة",
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Text(" عقوبة"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
17
lib/presentation/widgets/gradient_line.dart
Normal file
17
lib/presentation/widgets/gradient_line.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GradientLine extends StatelessWidget {
|
||||
final Color start;
|
||||
final Color end;
|
||||
|
||||
const GradientLine({super.key, required this.start, required this.end});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 2,
|
||||
|
||||
decoration: BoxDecoration(gradient: LinearGradient(colors: [start, end])),
|
||||
);
|
||||
}
|
||||
}
|
||||
486
lib/presentation/widgets/login_animation.dart
Normal file
486
lib/presentation/widgets/login_animation.dart
Normal file
@@ -0,0 +1,486 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'app_background.dart';
|
||||
|
||||
class LoginAnimationScreen extends StatefulWidget {
|
||||
final bool isLogin;
|
||||
final bool isSuccess;
|
||||
|
||||
const LoginAnimationScreen({
|
||||
super.key,
|
||||
required this.isLogin,
|
||||
required this.isSuccess,
|
||||
});
|
||||
|
||||
@override
|
||||
State<LoginAnimationScreen> createState() => _LoginAnimationScreenState();
|
||||
}
|
||||
|
||||
class _LoginAnimationScreenState extends State<LoginAnimationScreen>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController ringController;
|
||||
late AnimationController progressController;
|
||||
late Animation<double> ringAnimation;
|
||||
late Animation<double> progressAnimation;
|
||||
|
||||
// ERROR pulse animation
|
||||
late AnimationController errorPulseController;
|
||||
late Animation<double> errorPulseAnimation;
|
||||
|
||||
// SUCCESS one-time pulse
|
||||
late AnimationController successPulseController;
|
||||
late Animation<double> successPulseAnimation;
|
||||
|
||||
bool showResult = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// MAIN loading ripple animations
|
||||
ringController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 1),
|
||||
);
|
||||
progressController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
|
||||
ringAnimation = Tween<double>(
|
||||
begin: 0,
|
||||
end: 1,
|
||||
).animate(CurvedAnimation(parent: ringController, curve: Curves.easeOut));
|
||||
|
||||
progressAnimation = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(parent: progressController, curve: Curves.linear),
|
||||
);
|
||||
|
||||
// ERROR pulse animation
|
||||
errorPulseController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 1),
|
||||
);
|
||||
errorPulseAnimation = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(parent: errorPulseController, curve: Curves.easeOut),
|
||||
);
|
||||
|
||||
// SUCCESS one-time pulse
|
||||
successPulseController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 1),
|
||||
);
|
||||
successPulseAnimation = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(parent: successPulseController, curve: Curves.easeOut),
|
||||
);
|
||||
|
||||
startAnimations();
|
||||
|
||||
// MAIN DELAY (same as before)
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) {
|
||||
setState(() => showResult = true);
|
||||
|
||||
ringController.stop();
|
||||
progressController.stop();
|
||||
|
||||
if (!widget.isSuccess) {
|
||||
startErrorPulse();
|
||||
} else {
|
||||
successPulseController.forward(); // ONE-TIME BEAT
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void startAnimations() {
|
||||
progressController.repeat();
|
||||
startPulseAnimation();
|
||||
}
|
||||
|
||||
void startPulseAnimation() {
|
||||
ringController.forward().then((_) {
|
||||
ringController.reset();
|
||||
if (!showResult) startPulseAnimation();
|
||||
});
|
||||
}
|
||||
|
||||
void startErrorPulse() {
|
||||
errorPulseController.forward().then((_) {
|
||||
errorPulseController.reset();
|
||||
if (showResult && !widget.isSuccess) startErrorPulse();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ringController.dispose();
|
||||
progressController.dispose();
|
||||
errorPulseController.dispose();
|
||||
successPulseController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildIcon() {
|
||||
if (!showResult) {
|
||||
return SvgPicture.asset(
|
||||
"assets/images/finger_print.svg",
|
||||
key: const ValueKey("fingerprint"),
|
||||
width: 70,
|
||||
height: 70,
|
||||
);
|
||||
} else if (widget.isSuccess) {
|
||||
return SizedBox(
|
||||
width: 120,
|
||||
height: 120,
|
||||
child: Image.asset(
|
||||
"assets/images/tick.png",
|
||||
key: const ValueKey("success"),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
width: 120,
|
||||
height: 120,
|
||||
child: Image.asset(
|
||||
"assets/images/error.png",
|
||||
key: const ValueKey('error'),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: AppBackground(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.isLogin ? "تسجيل الدخول" : "تسجيل خروج",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 100),
|
||||
|
||||
Container(
|
||||
width: 280,
|
||||
height: 400,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEEFFFA),
|
||||
borderRadius: BorderRadius.circular(38),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0x34000000),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 160,
|
||||
height: 160,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// GREY CIRCLE (base)
|
||||
Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFE5E5E5),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
|
||||
// MAIN ICON SWITCHER
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child: _buildIcon(),
|
||||
),
|
||||
|
||||
// LOADING MODE
|
||||
if (!showResult)
|
||||
Positioned.fill(
|
||||
child: AnimatedBuilder(
|
||||
animation: progressAnimation,
|
||||
builder:
|
||||
(_, __) => CustomPaint(
|
||||
painter: _ProgressRingPainter(
|
||||
progress: progressAnimation.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!showResult)
|
||||
Positioned.fill(
|
||||
child: AnimatedBuilder(
|
||||
animation: ringAnimation,
|
||||
builder:
|
||||
(_, __) => CustomPaint(
|
||||
painter: _PulseRingsPainter(
|
||||
progress: ringAnimation.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ERROR ANIMATION
|
||||
if (showResult && !widget.isSuccess)
|
||||
Positioned.fill(
|
||||
child: AnimatedBuilder(
|
||||
animation: errorPulseAnimation,
|
||||
builder:
|
||||
(_, __) => CustomPaint(
|
||||
painter: _ErrorPulsePainter(
|
||||
progress: errorPulseAnimation.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// SUCCESS ONE-TIME PULSE
|
||||
if (showResult && widget.isSuccess)
|
||||
Positioned.fill(
|
||||
child: AnimatedBuilder(
|
||||
animation: successPulseAnimation,
|
||||
builder:
|
||||
(_, __) => CustomPaint(
|
||||
painter: _SuccessPulsePainter(
|
||||
progress: successPulseAnimation.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child:
|
||||
showResult
|
||||
? Text(
|
||||
widget.isSuccess
|
||||
? (widget.isLogin
|
||||
? "تم تسجيل دخولك بنجاح"
|
||||
: "تم تسجيل خروجك بنجاح")
|
||||
: "تم رفض تسجيل الدخول",
|
||||
key: ValueKey("text_${widget.isSuccess}"),
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
widget.isLogin
|
||||
? "يتم تسجيل الدخول..."
|
||||
: "يتم تسجيل الخروج...",
|
||||
key: const ValueKey("loading_text"),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// RETRY BUTTON
|
||||
if (showResult && !widget.isSuccess) ...[
|
||||
const SizedBox(height: 20),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() => showResult = false);
|
||||
|
||||
errorPulseController.stop();
|
||||
successPulseController.reset();
|
||||
startAnimations();
|
||||
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() => showResult = true);
|
||||
|
||||
ringController.stop();
|
||||
progressController.stop();
|
||||
|
||||
if (!widget.isSuccess) {
|
||||
startErrorPulse();
|
||||
} else {
|
||||
successPulseController.forward();
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
child: const Text(
|
||||
"أعد المحاولة",
|
||||
style: TextStyle(
|
||||
color: Color(0xFFB00020),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------- PAINTERS -------------------------- //
|
||||
|
||||
class _ProgressRingPainter extends CustomPainter {
|
||||
final double progress;
|
||||
_ProgressRingPainter({required this.progress});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = size.center(Offset.zero);
|
||||
final radius = 50.0;
|
||||
|
||||
final bgPaint =
|
||||
Paint()
|
||||
..color = const Color(0x0032C599)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 3;
|
||||
|
||||
canvas.drawCircle(center, radius, bgPaint);
|
||||
|
||||
final fgPaint =
|
||||
Paint()
|
||||
..color = const Color(0xC40A4433)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 3
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
final sweep = 2 * 3.1415926 * progress;
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
-1.5708,
|
||||
sweep,
|
||||
false,
|
||||
fgPaint,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_) => true;
|
||||
}
|
||||
|
||||
class _PulseRingsPainter extends CustomPainter {
|
||||
final double progress;
|
||||
_PulseRingsPainter({required this.progress});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = size.center(Offset.zero);
|
||||
const baseRadius = 60.0;
|
||||
const maxRadius = 130.0;
|
||||
|
||||
for (final phase in [0.0, 0.25, 0.5]) {
|
||||
final rp = (progress - phase).clamp(0.0, 1.0);
|
||||
if (rp > 0) {
|
||||
final radius = baseRadius + (maxRadius - baseRadius) * rp;
|
||||
final opacity = (1 - rp) * 0.45;
|
||||
|
||||
final paint =
|
||||
Paint()
|
||||
..color = const Color(0xFF32C59A).withOpacity(opacity)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2;
|
||||
|
||||
canvas.drawCircle(center, radius, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_) => true;
|
||||
}
|
||||
|
||||
// ERROR PAINTER
|
||||
class _ErrorPulsePainter extends CustomPainter {
|
||||
final double progress;
|
||||
_ErrorPulsePainter({required this.progress});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = size.center(Offset.zero);
|
||||
|
||||
// static ring
|
||||
final staticPaint =
|
||||
Paint()
|
||||
..color = const Color(0xFFB00020).withOpacity(0.20)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 3;
|
||||
|
||||
canvas.drawCircle(center, 70, staticPaint);
|
||||
|
||||
// pulse ring
|
||||
final radius = 70 + (20 * progress);
|
||||
final opacity = (1 - progress) * 0.45;
|
||||
|
||||
final pulsePaint =
|
||||
Paint()
|
||||
..color = const Color(0xFFB00020).withOpacity(opacity)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 3;
|
||||
|
||||
canvas.drawCircle(center, radius, pulsePaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_) => true;
|
||||
}
|
||||
|
||||
// SUCCESS one-time pulse
|
||||
class _SuccessPulsePainter extends CustomPainter {
|
||||
final double progress;
|
||||
_SuccessPulsePainter({required this.progress});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = size.center(Offset.zero);
|
||||
|
||||
final radius = 70 + (20 * progress);
|
||||
final opacity = (1 - progress) * 0.40;
|
||||
|
||||
final paint =
|
||||
Paint()
|
||||
..color = const Color(0xFF32C59A).withOpacity(opacity)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 3;
|
||||
|
||||
canvas.drawCircle(center, radius, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_) => true;
|
||||
}
|
||||
55
lib/presentation/widgets/onboarding_button.dart
Normal file
55
lib/presentation/widgets/onboarding_button.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OnboardingButton extends StatelessWidget {
|
||||
final String text;
|
||||
final VoidCallback? onPressed;
|
||||
final Color backgroundColor;
|
||||
final Color textColor;
|
||||
|
||||
const OnboardingButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.onPressed,
|
||||
this.backgroundColor = const Color(0xFF2D2D2D),
|
||||
this.textColor = Colors.white,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color.fromARGB(59, 59, 59, 59),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 14,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: textColor,
|
||||
disabledForegroundColor: textColor,
|
||||
disabledBackgroundColor: backgroundColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 80, vertical: 10),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 8,
|
||||
shadowColor: const Color(0x47000000), // More defined shadow color
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: textColor, // Use the textColor parameter here
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
40
lib/presentation/widgets/onboarding_page.dart
Normal file
40
lib/presentation/widgets/onboarding_page.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class OnboardingPage extends StatelessWidget {
|
||||
final String imagePath;
|
||||
final String text;
|
||||
|
||||
const OnboardingPage({
|
||||
super.key,
|
||||
required this.imagePath,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(imagePath, height: 280),
|
||||
|
||||
const SizedBox(height: 25),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30),
|
||||
child: Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'AbdElRady',
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
105
lib/presentation/widgets/settings_bar.dart
Normal file
105
lib/presentation/widgets/settings_bar.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class SettingsBar extends StatelessWidget {
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onTap;
|
||||
final bool showBackButton;
|
||||
final VoidCallback? onBackTap;
|
||||
final List<String> iconPaths;
|
||||
|
||||
const SettingsBar({
|
||||
super.key,
|
||||
required this.selectedIndex,
|
||||
required this.onTap,
|
||||
this.showBackButton = false, // to switch between back button and settings icons
|
||||
this.onBackTap,
|
||||
required this.iconPaths,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 20.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Image.asset('assets/images/logo2.png', width: 150, height: 40),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (showBackButton)
|
||||
GestureDetector(
|
||||
onTap: onBackTap,
|
||||
child: Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0x10000000),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
// Always use Flutter's built-in back icon pointing to the right
|
||||
child: const Icon(
|
||||
Icons.arrow_forward, // Changed to arrow_forward for RTL
|
||||
size: 26,
|
||||
color: Colors.black, // Adjust color as needed
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// When back button is OFF → show user + settings icons
|
||||
if (!showBackButton)
|
||||
...iconPaths.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final iconPath = entry.value;
|
||||
// final isSelected = selectedIndex == index;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: GestureDetector(
|
||||
onTap: () => onTap(index),
|
||||
child: Container(
|
||||
width: 43,
|
||||
height: 43,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0x10000000),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
SvgPicture.asset(iconPath, width: 30, height: 30),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
28
lib/presentation/widgets/status_circle.dart
Normal file
28
lib/presentation/widgets/status_circle.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StatusCircle extends StatelessWidget {
|
||||
final Color color;
|
||||
final Widget icon;
|
||||
final double size;
|
||||
|
||||
const StatusCircle({
|
||||
super.key,
|
||||
required this.color,
|
||||
required this.icon,
|
||||
this.size = 42, // ✅ smaller = card height reduced
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: color, width: 2),
|
||||
color: Colors.white,
|
||||
),
|
||||
child: Center(child: icon),
|
||||
);
|
||||
}
|
||||
}
|
||||
137
lib/presentation/widgets/work_day_card.dart
Normal file
137
lib/presentation/widgets/work_day_card.dart
Normal file
@@ -0,0 +1,137 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'gradient_line.dart';
|
||||
import 'status_circle.dart';
|
||||
|
||||
class WorkDayCard extends StatelessWidget {
|
||||
const WorkDayCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 18, vertical: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 12,
|
||||
offset: Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
"يوم عمل",
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
/// 🔥 FIXED: CENTERED LINES BETWEEN CIRCLES
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_StatusItem(
|
||||
color: const Color(0xFFD16400),
|
||||
icon: SvgPicture.asset('assets/images/money3.svg', width: 20),
|
||||
label: "سعر كلي\n18,250 د.ع",
|
||||
),
|
||||
|
||||
/// LINE CENTERED VERTICALLY
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: GradientLine(
|
||||
start: const Color(0xFFD16400),
|
||||
end: const Color(0xFF1266A8),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
_StatusItem(
|
||||
color: const Color(0xFF1266A8),
|
||||
icon: SvgPicture.asset('assets/images/watch.svg', width: 20),
|
||||
label: "عدد ساعات\n5.50",
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: GradientLine(
|
||||
start: const Color(0xFF1266A8),
|
||||
end: const Color(0xFFB00000),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
_StatusItem(
|
||||
color: const Color(0xFFB00000),
|
||||
icon: SvgPicture.asset('assets/images/out.svg', width: 20),
|
||||
label: "خروج\n1:14pm",
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: GradientLine(
|
||||
start: const Color(0xFFB00000),
|
||||
end: const Color(0xFF0A8F6B),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
_StatusItem(
|
||||
color: const Color(0xFF0A8F6B),
|
||||
icon: SvgPicture.asset('assets/images/in.svg', width: 20),
|
||||
label: "دخول\n1:14pm",
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
const Divider(color: Colors.black38),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: const [
|
||||
Text("2025.12.1", style: TextStyle(fontSize: 12)),
|
||||
Text("ملاحظات ان وجدت", style: TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusItem extends StatelessWidget {
|
||||
final Color color;
|
||||
final Widget icon;
|
||||
final String label;
|
||||
|
||||
const _StatusItem({
|
||||
required this.color,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
StatusCircle(color: color, icon: icon),
|
||||
// const SizedBox(height: 3),
|
||||
Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user