feat: legacy mobile apps created

This commit is contained in:
Achintha Isuru
2025-12-02 23:51:04 -05:00
parent 850441ca64
commit 8e7753b324
1519 changed files with 0 additions and 16 deletions

View File

@@ -0,0 +1,80 @@
import 'dart:io';
import 'package:http/http.dart';
import 'package:http_parser/http_parser.dart';
import 'package:injectable/injectable.dart';
import 'package:krow/core/application/clients/api/api_client.dart';
import 'package:krow/core/data/models/pagination_wrapper/pagination_wrapper.dart';
import 'package:krow/features/profile/certificates/data/models/certificate_model.dart';
import 'package:krow/features/profile/certificates/data/models/staff_certificate.dart';
import 'certificates_gql.dart';
@injectable
class CertificatesApiProvider {
final ApiClient _apiClient;
CertificatesApiProvider(this._apiClient);
Future<List<CertificateModel>> fetchCertificates() async {
var result = await _apiClient.query(schema: getCertificatesQuery);
if (result.hasException) {
throw Exception(result.exception.toString());
}
return result.data!['certificates'].map<CertificateModel>((e) {
return CertificateModel.fromJson(e);
}).toList();
}
Future<PaginationWrapper<StaffCertificate>> fetchStaffCertificates() async {
var result = await _apiClient.query(schema: getStaffCertificatesQuery);
if (result.hasException) {
throw Exception(result.exception.toString());
}
return PaginationWrapper.fromJson(result.data!['staff_certificates'],
(json) => StaffCertificate.fromJson(json));
}
Future<StaffCertificate> putStaffCertificate(
String certificateId, String imagePath, String certificateDate) async {
var byteData = File(imagePath).readAsBytesSync();
var multipartFile = MultipartFile.fromBytes(
'file',
byteData,
filename: '${DateTime.now().millisecondsSinceEpoch}.jpg',
contentType: MediaType('image', 'jpg'),
);
final Map<String, dynamic> variables = {
'certificate_id': certificateId,
'expiration_date': certificateDate,
'file': multipartFile,
};
var result = await _apiClient.mutate(
schema: putStaffCertificateMutation, body: {'input': variables});
if (result.hasException) {
throw Exception(result.exception.toString());
} else {
return StaffCertificate.fromJson(
result.data!['upload_staff_certificate']);
}
}
Future<void> deleteStaffCertificate(String certificateId) async {
final Map<String, dynamic> variables = {
'id': certificateId,
};
var result = await _apiClient.mutate(
schema: deleteStaffCertificateMutation, body: variables);
if (result.hasException) {
throw Exception(result.exception.toString());
}
}
}

View File

@@ -0,0 +1,56 @@
const String getCertificatesQuery = '''
{
certificates {
id
name
}
}
''';
const String getStaffCertificatesQuery = '''
query fetchStaffCertificates () {
staff_certificates(first: 10) {
pageInfo {
hasNextPage
startCursor
endCursor
}
edges {
cursor
node {
id
expiration_date
status
file
certificate {
id
name
}
}
}
}
}
''';
const String putStaffCertificateMutation = '''
mutation UploadStaffCertificate(\$input: UploadStaffCertificateInput!) {
upload_staff_certificate(input: \$input) {
id
expiration_date
status
file
certificate {
id
name
}
}
}
''';
const String deleteStaffCertificateMutation = '''
mutation DeleteStaffCertificate(\$id: ID!) {
delete_staff_certificate(id: \$id) {
id
}
}
''';

View File

@@ -0,0 +1,24 @@
class CertificateModel {
final String id;
final String name;
CertificateModel({
required this.id,
required this.name,
});
factory CertificateModel.fromJson(Map<String, dynamic> json) {
return CertificateModel(
id: json['id'],
name: json['name'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
};
}
}

View File

@@ -0,0 +1,34 @@
import 'package:injectable/injectable.dart';
import 'package:krow/features/profile/certificates/data/certificates_api_provider.dart';
import 'package:krow/features/profile/certificates/domain/certificates_repository.dart';
import 'package:krow/features/profile/certificates/data/models/certificate_model.dart';
import 'package:krow/features/profile/certificates/data/models/staff_certificate.dart';
@Injectable(as: CertificatesRepository)
class CertificatesRepositoryImpl extends CertificatesRepository {
final CertificatesApiProvider _certificatesApiProvider;
CertificatesRepositoryImpl(this._certificatesApiProvider);
@override
Future<List<CertificateModel>> getCertificates() async {
return _certificatesApiProvider.fetchCertificates();
}
@override
Future<StaffCertificate> putStaffCertificate(
String certificateId, String imagePath, String certificateDate) {
return _certificatesApiProvider.putStaffCertificate(
certificateId, imagePath, certificateDate);
}
@override
Future<List<StaffCertificate>> getStaffCertificates() async{
return (await _certificatesApiProvider.fetchStaffCertificates()).edges.map((e) => e.node).toList();
}
@override
Future<void> deleteStaffCertificate(String certificateId) {
return _certificatesApiProvider.deleteStaffCertificate(certificateId);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:krow/features/profile/certificates/data/models/certificate_model.dart';
part 'staff_certificate.g.dart';
enum CertificateStatus {
verified,
pending,
declined,
}
@JsonSerializable(fieldRename: FieldRename.snake)
class StaffCertificate {
final String id;
CertificateModel certificate;
final String expirationDate;
final CertificateStatus status;
final String file;
StaffCertificate(
{required this.id,
required this.certificate,
required this.expirationDate,
required this.status,
required this.file});
factory StaffCertificate.fromJson(Map<String, dynamic> json) {
return _$StaffCertificateFromJson(json);
}
Map<String, dynamic> toJson() => _$StaffCertificateToJson(this);
}

View File

@@ -0,0 +1,91 @@
import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/features/profile/certificates/domain/certificates_repository.dart';
import 'package:krow/features/profile/certificates/data/models/staff_certificate.dart';
import 'package:krow/features/profile/certificates/domain/bloc/certificates_event.dart';
import 'package:krow/features/profile/certificates/domain/bloc/certificates_state.dart';
class CertificatesBloc extends Bloc<CertificatesEvent, CertificatesState> {
CertificatesBloc() : super(CertificatesState()) {
on<CertificatesEventFetch>(_onFetch);
on<CertificatesEventSubmit>(_onSubmit);
on<CertificatesEventUpload>(_onUploadPhoto);
on<CertificatesEventDelete>(_onDeleteCertificate);
}
void _onFetch(
CertificatesEventFetch event, Emitter<CertificatesState> emit) async {
emit(state.copyWith(loading: true));
var certificates = await getIt<CertificatesRepository>().getCertificates();
List<StaffCertificate> staffCertificates =
await getIt<CertificatesRepository>().getStaffCertificates();
var items = certificates.map((certificate) {
var staffCertificate = staffCertificates.firstWhereOrNull(
(e) => certificate.id == e.certificate.id,
);
return CertificatesViewModel(
id: staffCertificate?.id,
certificateId: certificate.id,
title: certificate.name,
imageUrl: staffCertificate?.file,
expirationDate: staffCertificate?.expirationDate,
status: staffCertificate?.status,
);
}).toList();
emit(state.copyWith(
loading: false,
certificatesItems: items,
));
}
void _onUploadPhoto(
CertificatesEventUpload event, Emitter<CertificatesState> emit) async {
event.item.uploading = true;
emit(state.copyWith());
try {
var split = event.expirationDate.split('.').reversed;
split = [split.elementAt(0), split.elementAt(2), split.elementAt(1)];
var formattedDate = split.join('-');
var newCertificate = await getIt<CertificatesRepository>()
.putStaffCertificate(
event.item.certificateId, event.path, formattedDate);
event.item.applyNewStaffCertificate(newCertificate);
} finally {
event.item.uploading = false;
emit(state.copyWith());
}
}
void _onSubmit(
CertificatesEventSubmit event, Emitter<CertificatesState> emit) async {
final allCertUploaded = state.certificatesItems.every((element) {
return element.certificateId == '3' ||
element.status == CertificateStatus.pending ||
element.status == CertificateStatus.verified;
});
if (allCertUploaded) {
emit(state.copyWith(success: true));
} else {
emit(state.copyWith(showError: true));
}
}
void _onDeleteCertificate(
CertificatesEventDelete event, Emitter<CertificatesState> emit) async {
emit(state.copyWith(loading: true));
try {
await getIt<CertificatesRepository>()
.deleteStaffCertificate(event.item.id ?? '0');
state.certificatesItems
.firstWhere((element) => element.id == event.item.id)
.clear();
} finally {
emit(state.copyWith(loading: false));
}
}
}

View File

@@ -0,0 +1,22 @@
import 'package:krow/features/profile/certificates/domain/bloc/certificates_state.dart';
sealed class CertificatesEvent {}
class CertificatesEventFetch extends CertificatesEvent {}
class CertificatesEventSubmit extends CertificatesEvent {}
class CertificatesEventUpload extends CertificatesEvent {
final String path;
final String expirationDate;
final CertificatesViewModel item;
CertificatesEventUpload(
{required this.path, required this.expirationDate, required this.item});
}
class CertificatesEventDelete extends CertificatesEvent {
final CertificatesViewModel item;
CertificatesEventDelete(this.item);
}

View File

@@ -0,0 +1,93 @@
import 'package:krow/features/profile/certificates/data/models/staff_certificate.dart';
class CertificatesState {
final bool showError;
final bool loading;
final bool success;
List<CertificatesViewModel> certificatesItems = [];
CertificatesState(
{this.showError = false,
this.certificatesItems = const [],
this.loading = false,
this.success = false});
copyWith(
{bool? showError,
List<CertificatesViewModel>? certificatesItems,
bool? loading,
bool? success}) {
return CertificatesState(
showError: showError ?? this.showError,
certificatesItems: certificatesItems ?? this.certificatesItems,
loading: loading ?? false,
success: success ?? false,
);
}
}
class CertificatesViewModel {
String? id;
String certificateId;
final String title;
bool uploading;
String? imageUrl;
String? expirationDate;
CertificateStatus? status;
CertificatesViewModel({
this.id,
required this.certificateId,
required this.title,
this.expirationDate,
this.imageUrl,
this.uploading = false,
this.status,
});
void clear() {
id = null;
imageUrl = null;
expirationDate = null;
status = null;
}
void applyNewStaffCertificate(StaffCertificate newCertificate) {
id = newCertificate.id;
imageUrl = newCertificate.file;
expirationDate = newCertificate.expirationDate;
status = newCertificate.status;
}
bool get isExpired {
if (expirationDate == null) return false;
DateTime expiration = DateTime.parse(expirationDate!);
DateTime now = DateTime.now();
return now.isAfter(expiration);
}
String getExpirationInfo() {
if (expirationDate == null) return '';
DateTime expiration = DateTime.parse(expirationDate!);
DateTime now = DateTime.now();
Duration difference = expiration.difference(now);
String formatted =
'${expiration.month.toString().padLeft(2, '0')}.${expiration.day.toString().padLeft(2, '0')}.${expiration.year}';
if (difference.inDays <= 0) {
return '$formatted (expired)';
} else if (difference.inDays < 14) {
return '$formatted (expires in ${difference.inDays} days)';
} else {
return formatted;
}
}
bool expireSoon() {
if (expirationDate == null) return false;
DateTime expiration = DateTime.parse(expirationDate!);
DateTime now = DateTime.now();
Duration difference = expiration.difference(now);
return difference.inDays <= 14;
}
}

View File

@@ -0,0 +1,13 @@
import 'package:krow/features/profile/certificates/data/models/certificate_model.dart';
import 'package:krow/features/profile/certificates/data/models/staff_certificate.dart';
abstract class CertificatesRepository {
Future<List<CertificateModel>> getCertificates();
Future<List<StaffCertificate>> getStaffCertificates();
Future<StaffCertificate> putStaffCertificate(
String certificateId, String imagePath, String certificateDate);
Future<void> deleteStaffCertificate(String certificateId);
}

View File

@@ -0,0 +1,219 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:image_picker/image_picker.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/scroll_layout_helper.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/uploud_image_card.dart';
import 'package:krow/features/profile/certificates/data/models/staff_certificate.dart';
import 'package:krow/features/profile/certificates/domain/bloc/certificates_bloc.dart';
import 'package:krow/features/profile/certificates/domain/bloc/certificates_event.dart';
import 'package:krow/features/profile/certificates/domain/bloc/certificates_state.dart';
import 'package:krow/features/profile/certificates/presentation/screen/certificates_upload_dialog.dart';
import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
@RoutePage()
class CertificatesScreen extends StatelessWidget implements AutoRouteWrapper {
const CertificatesScreen({super.key});
@override
Widget wrappedRoute(BuildContext context) {
return BlocProvider(
create: (_) => CertificatesBloc()..add(CertificatesEventFetch()),
child: this,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: KwAppBar(
titleText: 'certificates'.tr(),
),
body: BlocConsumer<CertificatesBloc, CertificatesState>(
listener: (context, state) {
if (state.success) {
context.router.maybePop();
}
},
builder: (context, state) {
return ModalProgressHUD(
inAsyncCall: state.loading,
child: ScrollLayoutHelper(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
upperWidget: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'please_indicate_certificates'.tr(),
style: AppTextStyles.bodyTinyMed
.copyWith(color: AppColors.blackGray),
),
if (state.showError) _buildErrorMessage(),
const Gap(24),
_buildUploadProofItems(
context,
state.certificatesItems,
state.showError,
),
],
),
lowerWidget: KwButton.primary(
label: 'confirm'.tr(),
onPressed: () {
BlocProvider.of<CertificatesBloc>(context)
.add(CertificatesEventSubmit());
}),
),
);
},
),
);
}
_buildUploadProofItems(
context,
List<CertificatesViewModel> items,
bool showError,
) {
return Column(
children: items.map(
(item) {
Color? statusColor = _getStatusColor(showError, item);
return UploadImageCard(
title: item.title,
onSelectImage: () {
ImagePicker()
.pickImage(source: ImageSource.gallery)
.then((value) {
if (value != null) {
_showSelectCertificateDialog(context, item, value.path);
}
});
},
onDeleteTap: () {
BlocProvider.of<CertificatesBloc>(context)
.add(CertificatesEventDelete(item));
},
onTap: () {},
imageUrl: item.imageUrl,
inUploading: item.uploading,
statusColor: statusColor,
message: showError && item.imageUrl == null
? 'availability_requires_confirmation'.tr()
: item.status == null
? 'supported_format'.tr()
: (item.status!.name[0].toUpperCase() +
item.status!.name.substring(1).toLowerCase()),
hasError: showError,
padding: const EdgeInsets.only(bottom: 8),
child: _buildExpirationRow(item),
);
},
).toList(),
);
}
Color? _getStatusColor(bool showError, CertificatesViewModel item) {
var statusColor = (showError && item.status == null) ||
item.status == CertificateStatus.declined
? AppColors.statusError
: item.status == CertificateStatus.verified
? AppColors.statusSuccess
: item.status == CertificateStatus.pending
? AppColors.primaryBlue
: null;
return statusColor;
}
Container _buildErrorMessage() {
return Container(
margin: const EdgeInsets.only(top: 12),
padding: const EdgeInsets.all(8),
decoration: KwBoxDecorations.primaryLight8,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 28,
width: 28,
decoration: const BoxDecoration(
shape: BoxShape.circle, color: AppColors.tintRed),
child: Center(
child: Assets.images.icons.alertCircle.svg(),
),
),
const Gap(8),
Expanded(
child: Text(
'listed_certificates_mandatory'.tr(),
style: AppTextStyles.bodyTinyMed
.copyWith(color: AppColors.statusError),
),
),
],
),
);
}
Widget _buildExpirationRow(CertificatesViewModel item) {
var bgColor = item.isExpired
? AppColors.tintRed
: item.expireSoon()
? AppColors.tintYellow
: AppColors.tintGray;
var textColor = item.isExpired
? AppColors.statusError
: item.expireSoon()
? AppColors.statusWarningBody
: null;
return Container(
margin: const EdgeInsets.only(top: 6, right: 4),
decoration: BoxDecoration(
color: AppColors.grayWhite,
borderRadius: BorderRadius.circular(4),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding:
const EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 5),
decoration: BoxDecoration(
color: bgColor, borderRadius: BorderRadius.circular(4)),
child: Text(
'${'expiration_date'.tr()} ',
style: AppTextStyles.bodyTinyReg.copyWith(color: textColor),
)),
const Gap(7),
if (item.expirationDate != null)
Text(
item.getExpirationInfo(),
style: AppTextStyles.bodyTinyMed
.copyWith(color: textColor ?? AppColors.blackGray),
),
],
),
);
}
void _showSelectCertificateDialog(
context, CertificatesViewModel item, imagePath) {
CertificatesUploadDialog.show(context, imagePath).then((expireDate) {
if (expireDate != null) {
BlocProvider.of<CertificatesBloc>(context).add(CertificatesEventUpload(
item: item, path: imagePath, expirationDate: expireDate));
}
});
}
}

View File

@@ -0,0 +1,143 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/application/common/text_formatters/expiration_date_formatter.dart';
import 'package:krow/core/application/common/validators/certificate_date_validator.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
class CertificatesUploadDialog extends StatefulWidget {
final String path;
const CertificatesUploadDialog({
super.key,
required this.path,
});
@override
State<CertificatesUploadDialog> createState() =>
_CertificatesUploadDialogState();
static Future<String?> show(BuildContext context, String path) {
return showDialog<String?>(
context: context,
builder: (BuildContext context) {
return CertificatesUploadDialog(
path: path,
);
},
);
}
}
class _CertificatesUploadDialogState extends State<CertificatesUploadDialog> {
final TextEditingController expiryDateController = TextEditingController();
String? inputError;
@override
void initState() {
super.initState();
expiryDateController.addListener(() {
if(expiryDateController.text.length == 10) {
inputError =
CertificateDateValidator.validate(expiryDateController.text);
}else{
inputError = null;
}
setState(() {});
});
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Center(
child: Container(
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.9),
decoration: BoxDecoration(
color: AppColors.grayWhite,
borderRadius: BorderRadius.circular(16),
),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
..._buildDialogTitle(context),
Material(
type: MaterialType.transparency,
child: KwTextInput(
title: 'expiry_date_1'.tr(),
hintText: 'enter_certificate_expiry_date'.tr(),
controller: expiryDateController,
maxLength: 10,
inputFormatters: [DateTextFormatter()],
keyboardType: TextInputType.datetime,
helperText: inputError,
showError: inputError != null,
),
),
const Gap(12),
_buildImage(context),
const Gap(24),
KwButton.primary(
label: 'save_certificate'.tr(),
disabled: expiryDateController.text.length != 10 || inputError != null,
onPressed: () {
Navigator.of(context).pop(expiryDateController.text);
},
),
],
),
),
),
),
),
);
}
ClipRRect _buildImage(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.file(
File(widget.path),
width: MediaQuery.of(context).size.width - 80,
fit: BoxFit.contain,
),
);
}
List<Widget> _buildDialogTitle(BuildContext context) {
return [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'add_certificate_expiry_date'.tr(),
style: AppTextStyles.bodyLargeMed,
),
GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: Assets.images.icons.x.svg(),
),
],
),
const Gap(8),
Text(
'please_enter_expiry_date'.tr(),
style: AppTextStyles.bodySmallReg.copyWith(color: AppColors.blackGray),
),
const SizedBox(height: 24),
];
}
}