Skip to content

Commit bae91dc

Browse files
authored
feat(statistics): completely implement user's statistics (#12)
* chore: implement RPC for statistics feature * implement statictics_service, statistics_viewmodel, change sample data to real data in DailyProgressCard * feat(statistics): debug and test with real data * update supabase migration * completely implement feature statistics * add supabase migrations
1 parent bad84bd commit bae91dc

19 files changed

Lines changed: 1182 additions & 152 deletions

src/lib/core/enum/TaskStatus.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
enum TaskStatus {
2+
toDo(0),
3+
completed(1),
4+
overDue(2);
5+
6+
final int value;
7+
const TaskStatus(this.value);
8+
9+
factory TaskStatus.fromInt(int dbValue){
10+
return TaskStatus.values.firstWhere(
11+
(status) => status.value == dbValue,
12+
orElse: () => TaskStatus.toDo,
13+
);
14+
}
15+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import 'package:flutter/material.dart';
2+
import '../../core/theme/app_colors.dart';
3+
import '../../core/theme/auth_layout_template.dart';
4+
import '../../core/theme/custom_text_field.dart';
5+
import '../../viewmodels/auth_viewmodels.dart';
6+
import 'otp_verification_view.dart';
7+
8+
class ForgotPasswordView extends StatefulWidget {
9+
const ForgotPasswordView({super.key});
10+
11+
@override
12+
State<ForgotPasswordView> createState() => _ForgotPasswordViewState();
13+
}
14+
15+
class _ForgotPasswordViewState extends State<ForgotPasswordView> {
16+
final _vm = ForgotPassViewModel();
17+
18+
@override
19+
Widget build(BuildContext context) {
20+
return AnimatedBuilder(
21+
animation: _vm,
22+
builder: (context, _) => AuthLayoutTemplate(
23+
title: 'Quên mật khẩu?',
24+
subtitle: 'Nhập email của bạn để nhận mã xác thực',
25+
submitText: 'Gửi mã',
26+
useCard: false,
27+
isLoading: _vm.isLoading,
28+
customHeaderIcon: Container(
29+
width: 120,
30+
height: 120,
31+
decoration: const BoxDecoration(
32+
color: AppColors.white,
33+
shape: BoxShape.circle,
34+
boxShadow: [BoxShadow(color: Color(0xFFD1D9E6), blurRadius: 16)],
35+
),
36+
child: const Icon(
37+
Icons.lock_reset,
38+
size: 64,
39+
color: AppColors.primary,
40+
),
41+
),
42+
onSubmit: () async {
43+
FocusScope.of(context).unfocus();
44+
final success = await _vm.sendCode();
45+
if (!context.mounted) return;
46+
47+
if (success) {
48+
// Jump to Step 2: OTP
49+
Navigator.push(
50+
context,
51+
MaterialPageRoute(builder: (_) => const OtpVerificationView()),
52+
);
53+
} else {
54+
ScaffoldMessenger.of(context).showSnackBar(
55+
const SnackBar(
56+
content: Text('Vui lòng nhập email hợp lệ!'),
57+
backgroundColor: AppColors.error,
58+
),
59+
);
60+
}
61+
},
62+
formContent: CustomTextField(
63+
label: 'Địa chỉ Email',
64+
65+
icon: Icons.mail,
66+
controller: _vm.emailCtrl,
67+
),
68+
footerContent: const Row(
69+
mainAxisAlignment: MainAxisAlignment.center,
70+
children: [
71+
Icon(Icons.help, size: 16, color: AppColors.primary),
72+
SizedBox(width: 4),
73+
Text(
74+
'Cần hỗ trợ? Liên hệ CSKH',
75+
style: TextStyle(
76+
color: AppColors.textSecondary,
77+
fontSize: 12,
78+
fontWeight: FontWeight.bold,
79+
),
80+
),
81+
],
82+
),
83+
),
84+
);
85+
}
86+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import 'package:flutter/material.dart';
2+
import '../../core/theme/app_colors.dart';
3+
import '../../core/theme/auth_layout_template.dart';
4+
import '../../core/theme/custom_text_field.dart';
5+
import '../../viewmodels/auth_viewmodels.dart';
6+
import 'register_view.dart';
7+
import 'forgot_password_view.dart';
8+
9+
class LoginView extends StatefulWidget {
10+
const LoginView({super.key});
11+
12+
@override
13+
State<LoginView> createState() => _LoginViewState();
14+
}
15+
16+
class _LoginViewState extends State<LoginView> {
17+
final _vm = LoginViewModel();
18+
19+
@override
20+
Widget build(BuildContext context) {
21+
return AnimatedBuilder(
22+
animation: _vm,
23+
builder: (context, _) =>
24+
AuthLayoutTemplate(
25+
title: 'Task Management',
26+
subtitle: 'Chào mừng trở lại!',
27+
submitText: 'Đăng nhập',
28+
isLoading: _vm.isLoading,
29+
showSocial: true,
30+
onSubmit: () async {
31+
FocusScope.of(context).unfocus(); // Hide keyboard
32+
final success = await _vm.login();
33+
if (!context.mounted) return;
34+
35+
if (success) {
36+
ScaffoldMessenger.of(context).showSnackBar(
37+
const SnackBar(
38+
content: Text('Đăng nhập thành công!'),
39+
backgroundColor: AppColors.success,
40+
),
41+
);
42+
// TODO: Navigator.pushReplacementNamed(context, '/home');
43+
} else {
44+
ScaffoldMessenger.of(context).showSnackBar(
45+
const SnackBar(
46+
content: Text('Vui lòng điền đầy đủ thông tin!'),
47+
backgroundColor: AppColors.error,
48+
),
49+
);
50+
}
51+
},
52+
formContent: Column(
53+
crossAxisAlignment: CrossAxisAlignment.end,
54+
children: [
55+
CustomTextField(
56+
label: 'Email',
57+
58+
icon: Icons.mail,
59+
controller: _vm.emailCtrl,
60+
),
61+
CustomTextField(
62+
label: 'Mật khẩu',
63+
hint: '••••••••',
64+
icon: Icons.lock,
65+
controller: _vm.passCtrl,
66+
isPassword: true,
67+
obscureText: _vm.obscurePass,
68+
onToggleVisibility: _vm.togglePass,
69+
),
70+
TextButton(
71+
onPressed: () =>
72+
Navigator.push(
73+
context,
74+
MaterialPageRoute(
75+
builder: (_) => const ForgotPasswordView()),
76+
),
77+
child: const Text(
78+
'Quên mật khẩu?',
79+
style: TextStyle(
80+
color: AppColors.primary,
81+
fontWeight: FontWeight.bold,
82+
),
83+
),
84+
),
85+
],
86+
),
87+
footerContent: _buildFooter(
88+
'Chưa có tài khoản? ', 'Đăng ký ngay', () {
89+
Navigator.push(
90+
context,
91+
MaterialPageRoute(builder: (_) => const RegisterView()),
92+
);
93+
}),
94+
),
95+
);
96+
}
97+
98+
Widget _buildFooter(String text, String action, VoidCallback onTap) {
99+
return Padding(
100+
// Position this block 24dp above the bottom of the screen
101+
padding: const EdgeInsets.only(bottom: 24.0),
102+
child: Row(
103+
mainAxisAlignment: MainAxisAlignment.center,
104+
children: [
105+
Text(text, style: const TextStyle(color: Colors.grey, fontSize: 16)),
106+
107+
TextButton(
108+
onPressed: onTap,
109+
style: TextButton.styleFrom(
110+
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
111+
minimumSize: const Size(50, 48),
112+
),
113+
child: Text(
114+
action,
115+
style: const TextStyle(color: Color(0xFF5A8DF3),
116+
fontWeight: FontWeight.bold,
117+
fontSize: 16),
118+
),
119+
),
120+
],
121+
),
122+
);
123+
}
124+
}

0 commit comments

Comments
 (0)