feat: Enhance officer and profile models with place of birth and date of birth fields; update step indicators for improved layout and styling

This commit is contained in:
vergiLgood1 2025-05-19 21:20:14 +07:00
parent ce7d448b2f
commit ce7d5f5cf4
6 changed files with 334 additions and 346 deletions

View File

@ -12,6 +12,8 @@ class OfficerModel {
final String? phone;
final String? email;
final String? avatar;
final String? placeOfBirth;
final DateTime? dateOfBirth;
final DateTime? validUntil;
final String? qrCode;
final DateTime? createdAt;
@ -30,6 +32,8 @@ class OfficerModel {
this.phone,
this.email,
this.avatar,
this.placeOfBirth,
this.dateOfBirth,
this.validUntil,
this.qrCode,
this.createdAt,
@ -51,6 +55,11 @@ class OfficerModel {
phone: json['phone'] as String?,
email: json['email'] as String?,
avatar: json['avatar'] as String?,
placeOfBirth: json['place_of_birth'] as String?,
dateOfBirth:
json['date_of_birth'] != null
? DateTime.parse(json['date_of_birth'] as String)
: null,
validUntil:
json['valid_until'] != null
? DateTime.parse(json['valid_until'] as String)
@ -85,6 +94,8 @@ class OfficerModel {
'phone': phone,
'email': email,
'avatar': avatar,
'place_of_birth': placeOfBirth,
'date_of_birth': dateOfBirth?.toIso8601String(),
'valid_until': validUntil?.toIso8601String(),
'qr_code': qrCode,
'created_at': createdAt?.toIso8601String(),
@ -106,6 +117,8 @@ class OfficerModel {
String? phone,
String? email,
String? avatar,
String? placeOfBirth,
DateTime? dateOfBirth,
DateTime? validUntil,
String? qrCode,
DateTime? createdAt,
@ -124,6 +137,8 @@ class OfficerModel {
phone: phone ?? this.phone,
email: email ?? this.email,
avatar: avatar ?? this.avatar,
placeOfBirth: placeOfBirth ?? this.placeOfBirth,
dateOfBirth: dateOfBirth ?? this.dateOfBirth,
validUntil: validUntil ?? this.validUntil,
qrCode: qrCode ?? this.qrCode,
createdAt: createdAt ?? this.createdAt,
@ -149,6 +164,17 @@ class OfficerModel {
position: officerData['position'],
phone: officerData['phone'],
email: metadata['email'],
avatar: officerData['avatar'],
placeOfBirth: officerData['place_of_birth'],
dateOfBirth:
officerData['date_of_birth'] != null
? DateTime.parse(officerData['date_of_birth'])
: null,
validUntil:
officerData['valid_until'] != null
? DateTime.parse(officerData['valid_until'])
: null,
qrCode: officerData['qr_code'],
);
}

View File

@ -108,7 +108,7 @@ class OnboardingController extends GetxController
TFullScreenLoader.stopLoading();
if (!isLocationValid) {
if (isLocationValid) {
// If location is valid, proceed to role selection
Get.offAllNamed(AppRoutes.roleSelection);

View File

@ -8,7 +8,9 @@ class ProfileModel {
final String? lastName;
final String? bio;
final Map<String, dynamic>? address;
final String? placeOfBirth;
final DateTime? birthDate;
ProfileModel({
required this.id,
@ -20,6 +22,7 @@ class ProfileModel {
this.lastName,
this.bio,
this.address,
this.placeOfBirth,
this.birthDate,
});
@ -38,6 +41,7 @@ class ProfileModel {
json['address'] != null
? Map<String, dynamic>.from(json['address'] as Map)
: null,
placeOfBirth: json['place_of_birth'] as String?,
birthDate:
json['birth_date'] != null
? DateTime.parse(json['birth_date'] as String)
@ -57,6 +61,7 @@ class ProfileModel {
'last_name': lastName,
'bio': bio,
'address': address,
'place_of_birth': placeOfBirth,
'birth_date': birthDate?.toIso8601String(),
};
}
@ -72,6 +77,7 @@ class ProfileModel {
String? lastName,
String? bio,
Map<String, dynamic>? address,
String? placeOfBirth,
DateTime? birthDate,
}) {
return ProfileModel(
@ -84,6 +90,7 @@ class ProfileModel {
lastName: lastName ?? this.lastName,
bio: bio ?? this.bio,
address: address ?? this.address,
placeOfBirth: placeOfBirth ?? this.placeOfBirth,
birthDate: birthDate ?? this.birthDate,
);
}

View File

@ -32,14 +32,17 @@ class NumberedStepIndicator extends StatelessWidget {
}
Widget _buildTwoStepIndicator(BuildContext context) {
const boxSize = TSizes.xl + TSizes.xs;
final double padding = TSizes.xs;
return Column(
children: [
// Step indicators with connector line
Padding(
padding: const EdgeInsets.symmetric(horizontal: TSizes.md / 2),
padding: EdgeInsets.symmetric(horizontal: padding),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// First step
// First step box
_buildStepBox(0),
// Connector line
@ -61,110 +64,99 @@ class NumberedStepIndicator extends StatelessWidget {
),
),
// Second step
// Second step box
_buildStepBox(1),
],
),
),
SizedBox(height: TSizes.sm),
// Step titles
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(totalSteps, (index) {
return Text(
stepTitles[index],
style: theme.textTheme.labelMedium?.copyWith(
fontWeight:
index == currentStep ? FontWeight.bold : FontWeight.normal,
color:
index == currentStep
? theme.primaryColor
: isDark
? Colors.white70
: TColors.textSecondary,
const SizedBox(height: TSizes.sm),
// Step titles with fixed widths aligned with boxes
Padding(
padding: EdgeInsets.symmetric(horizontal: padding),
child: Row(
children: [
// First step title
SizedBox(
width: boxSize,
child: Text(
stepTitles[0],
textAlign: TextAlign.center,
style: _getTitleStyle(0),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
);
}),
// Empty space matching the connector
Expanded(child: Container()),
// Second step title
SizedBox(
width: boxSize,
child: Text(
stepTitles[1],
textAlign: TextAlign.center,
style: _getTitleStyle(1),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
],
),
),
],
);
}
Widget _buildMultiStepIndicator(BuildContext context) {
const boxSize = TSizes.xl + TSizes.xs;
final availableWidth =
MediaQuery.of(context).size.width - (TSizes.defaultSpace * 2);
final double spacing =
(availableWidth - (boxSize * totalSteps)) / (totalSteps - 1);
return Column(
children: [
// Step indicators with connector lines
Row(
children: List.generate(totalSteps, (index) {
final isLast = index == totalSteps - 1;
return Expanded(
child: Row(
children: [
// Step number
_buildStepBox(index),
// Connector line
if (!isLast)
Expanded(
child: Stack(
alignment: Alignment.center,
children: [
Container(
height: TSizes.dividerHeight,
color:
isDark
? TColors.darkerGrey
: TColors.borderPrimary,
),
Container(
height: TSizes.dividerHeight,
width:
index < currentStep
? MediaQuery.of(context).size.width
: 0,
color: theme.primaryColor,
),
],
),
),
],
),
);
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(2 * totalSteps - 1, (index) {
// Even indexes are step boxes, odd indexes are connectors
if (index.isEven) {
final stepIndex = index ~/ 2;
return _buildStepBox(stepIndex);
} else {
final prevStepIndex = index ~/ 2;
return Container(
width: spacing,
height: TSizes.dividerHeight,
color:
prevStepIndex < currentStep
? theme.primaryColor
: isDark
? TColors.darkerGrey
: TColors.borderPrimary,
);
}
}),
),
SizedBox(height: TSizes.sm),
// Step titles
const SizedBox(height: TSizes.md),
// Step titles with fixed positions matching boxes
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(totalSteps, (index) {
return Expanded(
child: Padding(
padding: EdgeInsets.only(
left: index == 0 ? 0 : TSizes.xs,
right: index == totalSteps - 1 ? 0 : TSizes.xs,
),
child: Text(
stepTitles[index],
textAlign:
index == 0
? TextAlign.start
: index == totalSteps - 1
? TextAlign.end
: TextAlign.center,
style: theme.textTheme.labelMedium?.copyWith(
fontWeight:
index == currentStep
? FontWeight.bold
: FontWeight.normal,
color:
index == currentStep
? theme.primaryColor
: isDark
? Colors.white70
: TColors.textSecondary,
),
),
return SizedBox(
width: boxSize,
child: Text(
stepTitles[index],
textAlign: TextAlign.center,
style: _getTitleStyle(index),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
);
}),
@ -176,13 +168,14 @@ class NumberedStepIndicator extends StatelessWidget {
Widget _buildStepBox(int index) {
final isActive = index <= currentStep;
final isCompleted = index < currentStep;
const boxSize = TSizes.xl + TSizes.xs;
return GestureDetector(
onTap: () => onStepTapped(index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: TSizes.xl + TSizes.xs,
height: TSizes.xl + TSizes.xs,
width: boxSize,
height: boxSize,
decoration: BoxDecoration(
color:
isCompleted
@ -218,4 +211,16 @@ class NumberedStepIndicator extends StatelessWidget {
),
);
}
TextStyle? _getTitleStyle(int index) {
return theme.textTheme.labelMedium?.copyWith(
fontWeight: index == currentStep ? FontWeight.bold : FontWeight.normal,
color:
index == currentStep
? theme.primaryColor
: isDark
? Colors.white70
: TColors.textSecondary,
);
}
}

View File

@ -32,12 +32,14 @@ class RoundedStepIndicator extends StatelessWidget {
}
Widget _buildTwoStepIndicator(BuildContext context) {
final circleWidth = TSizes.xl;
final double padding = TSizes.xs;
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: TSizes.md / 2),
padding: EdgeInsets.symmetric(horizontal: padding),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// First step
_buildStepCircle(0),
@ -66,106 +68,96 @@ class RoundedStepIndicator extends StatelessWidget {
],
),
),
SizedBox(height: TSizes.sm),
const SizedBox(height: TSizes.sm),
// Step titles
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(totalSteps, (index) {
return SizedBox(
width: TSizes.xl,
child: Text(
stepTitles[index],
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.labelMedium?.copyWith(
fontWeight:
index == currentStep
? FontWeight.bold
: FontWeight.normal,
color:
index == currentStep
? theme.primaryColor
: isDark
? Colors.white70
: TColors.textSecondary,
// Step titles with fixed widths aligned with circles
Padding(
padding: EdgeInsets.symmetric(horizontal: padding),
child: Row(
children: [
// First step title
SizedBox(
width: circleWidth,
child: Text(
stepTitles[0],
textAlign: TextAlign.center,
style: _getTitleStyle(0),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
);
}),
// Empty space matching the connector
Expanded(child: Container()),
// Second step title
SizedBox(
width: circleWidth,
child: Text(
stepTitles[1],
textAlign: TextAlign.center,
style: _getTitleStyle(1),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
],
),
),
],
);
}
Widget _buildMultiStepIndicator(BuildContext context) {
final circleWidth = TSizes.xl;
final availableWidth =
MediaQuery.of(context).size.width - (TSizes.defaultSpace * 2);
final double spacing =
(availableWidth - (circleWidth * totalSteps)) / (totalSteps - 1);
return Column(
children: [
SizedBox(
height: TSizes.xl + TSizes.xs,
child: Stack(
children: [
// Connector lines first (in the background)
Positioned.fill(
top: TSizes.xl / 2 - TSizes.dividerHeight / 2,
child: Row(
children: List.generate(totalSteps - 1, (index) {
final isActive = index < currentStep;
return Expanded(
child: Container(
height: TSizes.dividerHeight,
margin: EdgeInsets.symmetric(horizontal: TSizes.xs),
decoration: BoxDecoration(
color:
isActive
? theme.primaryColor
: isDark
? TColors.darkerGrey
: TColors.borderPrimary,
borderRadius: BorderRadius.circular(
TSizes.dividerHeight / 2,
),
),
),
);
}),
// Step indicators with connector lines
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(2 * totalSteps - 1, (index) {
// Even indexes are step circles, odd indexes are connectors
if (index.isEven) {
final stepIndex = index ~/ 2;
return _buildStepCircle(stepIndex);
} else {
return Container(
width: spacing,
height: TSizes.dividerHeight,
decoration: BoxDecoration(
color:
(index ~/ 2) < currentStep
? theme.primaryColor
: isDark
? TColors.darkerGrey
: TColors.borderPrimary,
borderRadius: BorderRadius.circular(TSizes.dividerHeight / 2),
),
),
// Step circles on top
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(totalSteps, (index) {
return _buildStepCircle(index);
}),
),
],
),
);
}
}),
),
SizedBox(height: TSizes.sm),
const SizedBox(height: TSizes.sm),
// Step titles
// Step titles with fixed positions matching circles
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(totalSteps, (index) {
return SizedBox(
width: TSizes.xl,
width: circleWidth,
child: Text(
stepTitles[index],
textAlign: TextAlign.center,
style: _getTitleStyle(index),
overflow: TextOverflow.ellipsis,
style: theme.textTheme.labelMedium?.copyWith(
fontWeight:
index == currentStep
? FontWeight.bold
: FontWeight.normal,
color:
index == currentStep
? theme.primaryColor
: isDark
? Colors.white70
: TColors.textSecondary,
),
maxLines: 2,
),
);
}),
@ -227,4 +219,16 @@ class RoundedStepIndicator extends StatelessWidget {
),
);
}
TextStyle? _getTitleStyle(int index) {
return theme.textTheme.labelMedium?.copyWith(
fontWeight: index == currentStep ? FontWeight.bold : FontWeight.normal,
color:
index == currentStep
? theme.primaryColor
: isDark
? Colors.white70
: TColors.textSecondary,
);
}
}

View File

@ -33,44 +33,17 @@ class StandardStepIndicator extends StatelessWidget {
Widget _buildTwoStepIndicator(BuildContext context) {
final circleWidth = TSizes.xl;
final double padding = TSizes.xs;
return Column(
children: [
// Step indicators with connector line
Padding(
padding: const EdgeInsets.symmetric(horizontal: TSizes.md / 2),
padding: EdgeInsets.symmetric(horizontal: padding),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// First step
Container(
width: circleWidth,
height: circleWidth,
decoration: BoxDecoration(
color: theme.primaryColor,
shape: BoxShape.circle,
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => onStepTapped(0),
borderRadius: BorderRadius.circular(circleWidth),
child: Center(
child:
0 < currentStep
? Icon(
Icons.check,
color: Colors.white,
size: TSizes.iconSm,
)
: Icon(
Icons.circle,
color: Colors.white,
size: TSizes.iconXs,
),
),
),
),
),
_buildStepCircle(0, circleWidth),
// Connector line that spans the entire space between circles
Expanded(
@ -86,173 +59,97 @@ class StandardStepIndicator extends StatelessWidget {
),
// Second step
Container(
_buildStepCircle(1, circleWidth),
],
),
),
const SizedBox(height: TSizes.sm),
// Step titles with fixed widths aligned with circles
Padding(
padding: EdgeInsets.symmetric(horizontal: padding),
child: Row(
children: [
SizedBox(
width: circleWidth,
height: circleWidth,
decoration: BoxDecoration(
color:
currentStep >= 1
? theme.primaryColor
: isDark
? TColors.darkerGrey
: TColors.secondary,
shape: BoxShape.circle,
child: Text(
stepTitles[0],
textAlign: TextAlign.center,
style: _getTitleStyle(0),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => onStepTapped(1),
borderRadius: BorderRadius.circular(circleWidth),
child: Center(
child:
currentStep >= 1
? Icon(
currentStep > 1 ? Icons.check : Icons.circle,
color: Colors.white,
size:
currentStep > 1
? TSizes.iconSm
: TSizes.iconXs,
)
: Text(
'2',
style: theme.textTheme.bodySmall?.copyWith(
color:
isDark
? Colors.white
: TColors.textSecondary,
fontWeight: FontWeight.bold,
),
),
),
),
),
// Empty space matching the connector
Expanded(child: Container()),
// Second step title
SizedBox(
width: circleWidth,
child: Text(
stepTitles[1],
textAlign: TextAlign.center,
style: _getTitleStyle(1),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
],
),
),
SizedBox(height: TSizes.sm),
// Step titles
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(totalSteps, (index) {
return Text(
stepTitles[index],
style: theme.textTheme.labelMedium?.copyWith(
fontWeight:
index == currentStep ? FontWeight.bold : FontWeight.normal,
color:
index == currentStep
? theme.primaryColor
: isDark
? Colors.white70
: TColors.textSecondary,
),
);
}),
),
],
);
}
Widget _buildMultiStepIndicator(BuildContext context) {
const circleWidth = TSizes.xl;
final availableWidth =
MediaQuery.of(context).size.width - (TSizes.defaultSpace * 2);
final double spacing =
(availableWidth - (circleWidth * totalSteps)) / (totalSteps - 1);
return Column(
children: [
// Step indicators with connector lines
Row(
children: List.generate(totalSteps, (index) {
final isActive = index <= currentStep;
final isLast = index == totalSteps - 1;
return Expanded(
child: Row(
children: [
// Step circle
GestureDetector(
onTap: () => onStepTapped(index),
child: Container(
width: TSizes.xl,
height: TSizes.xl,
decoration: BoxDecoration(
color:
isActive
? theme.primaryColor
: isDark
? TColors.darkerGrey
: TColors.secondary,
shape: BoxShape.circle,
),
child: Center(
child:
isActive
? Icon(
index < currentStep
? Icons.check
: Icons.circle,
color: Colors.white,
size:
index < currentStep
? TSizes.iconSm
: TSizes.iconXs,
)
: Text(
'${index + 1}',
style: theme.textTheme.bodySmall?.copyWith(
color:
isDark
? Colors.white
: TColors.textSecondary,
fontWeight: FontWeight.bold,
),
),
),
),
),
// Connector line
if (!isLast)
Expanded(
child: Container(
height: TSizes.dividerHeight,
color:
index < currentStep
? theme.primaryColor
: isDark
? TColors.darkerGrey
: TColors.borderPrimary,
),
),
],
),
);
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(2 * totalSteps - 1, (index) {
// Even indexes (0, 2, 4...) are step circles, odd indexes (1, 3, 5...) are connectors
if (index.isEven) {
final stepIndex = index ~/ 2;
return _buildStepCircle(stepIndex, circleWidth);
} else {
final prevStepIndex = index ~/ 2;
return Container(
width: spacing,
height: TSizes.dividerHeight,
color:
prevStepIndex < currentStep
? theme.primaryColor
: isDark
? TColors.darkerGrey
: TColors.borderPrimary,
);
}
}),
),
SizedBox(height: TSizes.sm),
const SizedBox(height: TSizes.sm),
// Step titles
// Step titles with fixed positions matching circles
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(totalSteps, (index) {
return Expanded(
return SizedBox(
width: circleWidth,
child: Text(
stepTitles[index],
textAlign:
index == 0
? TextAlign.start
: index == totalSteps - 1
? TextAlign.end
: TextAlign.center,
style: theme.textTheme.labelMedium?.copyWith(
fontWeight:
index == currentStep
? FontWeight.bold
: FontWeight.normal,
color:
index == currentStep
? theme.primaryColor
: isDark
? Colors.white70
: TColors.textSecondary,
),
textAlign: TextAlign.center,
style: _getTitleStyle(index),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
);
}),
@ -260,4 +157,53 @@ class StandardStepIndicator extends StatelessWidget {
],
);
}
Widget _buildStepCircle(int index, double circleWidth) {
final isActive = index <= currentStep;
return GestureDetector(
onTap: () => onStepTapped(index),
child: Container(
width: circleWidth,
height: circleWidth,
decoration: BoxDecoration(
color:
isActive
? theme.primaryColor
: isDark
? TColors.darkerGrey
: TColors.secondary,
shape: BoxShape.circle,
),
child: Center(
child:
isActive
? Icon(
index < currentStep ? Icons.check : Icons.circle,
color: Colors.white,
size: index < currentStep ? TSizes.iconSm : TSizes.iconXs,
)
: Text(
'${index + 1}',
style: theme.textTheme.bodySmall?.copyWith(
color: isDark ? Colors.white : TColors.textSecondary,
fontWeight: FontWeight.bold,
),
),
),
),
);
}
TextStyle? _getTitleStyle(int index) {
return theme.textTheme.labelMedium?.copyWith(
fontWeight: index == currentStep ? FontWeight.bold : FontWeight.normal,
color:
index == currentStep
? theme.primaryColor
: isDark
? Colors.white70
: TColors.textSecondary,
);
}
}