feat: Enhance attire verification status system with more granular states and update related UI and data handling.

This commit is contained in:
Achintha Isuru
2026-02-25 19:05:03 -05:00
parent 6eafba311b
commit 4515d42cd3
9 changed files with 334 additions and 157 deletions

View File

@@ -1,3 +1,43 @@
/// Represents the possible statuses of a verification job.
enum VerificationStatus {
/// Job is created and waiting to be processed.
pending('PENDING'),
/// Job is currently being processed by machine or human.
processing('PROCESSING'),
/// Machine verification passed automatically.
autoPass('AUTO_PASS'),
/// Machine verification failed automatically.
autoFail('AUTO_FAIL'),
/// Machine results are inconclusive and require human review.
needsReview('NEEDS_REVIEW'),
/// Human reviewer approved the verification.
approved('APPROVED'),
/// Human reviewer rejected the verification.
rejected('REJECTED'),
/// An error occurred during processing.
error('ERROR');
const VerificationStatus(this.value);
/// The string value expected by the Core API.
final String value;
/// Creates a [VerificationStatus] from a string.
static VerificationStatus fromString(String value) {
return VerificationStatus.values.firstWhere(
(VerificationStatus e) => e.value == value,
orElse: () => VerificationStatus.error,
);
}
}
/// Response model for verification operations. /// Response model for verification operations.
class VerificationResponse { class VerificationResponse {
/// Creates a [VerificationResponse]. /// Creates a [VerificationResponse].
@@ -13,7 +53,7 @@ class VerificationResponse {
factory VerificationResponse.fromJson(Map<String, dynamic> json) { factory VerificationResponse.fromJson(Map<String, dynamic> json) {
return VerificationResponse( return VerificationResponse(
verificationId: json['verificationId'] as String, verificationId: json['verificationId'] as String,
status: json['status'] as String, status: VerificationStatus.fromString(json['status'] as String),
type: json['type'] as String?, type: json['type'] as String?,
review: json['review'] != null review: json['review'] != null
? json['review'] as Map<String, dynamic> ? json['review'] as Map<String, dynamic>
@@ -25,8 +65,8 @@ class VerificationResponse {
/// The unique ID of the verification job. /// The unique ID of the verification job.
final String verificationId; final String verificationId;
/// Current status (e.g., PENDING, PROCESSING, SUCCESS, FAILED, NEEDS_REVIEW). /// Current status of the verification.
final String status; final VerificationStatus status;
/// The type of verification (e.g., attire, government_id). /// The type of verification (e.g., attire, government_id).
final String? type; final String? type;
@@ -41,7 +81,7 @@ class VerificationResponse {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return <String, dynamic>{ return <String, dynamic>{
'verificationId': verificationId, 'verificationId': verificationId,
'status': status, 'status': status.value,
'type': type, 'type': type,
'review': review, 'review': review,
'requestId': requestId, 'requestId': requestId,

View File

@@ -1,8 +1,7 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_data_connect/krow_data_connect.dart' import 'package:krow_domain/krow_domain.dart' as domain;
hide AttireVerificationStatus; import '../../domain/repositories/staff_connector_repository.dart';
import 'package:krow_domain/krow_domain.dart';
/// Implementation of [StaffConnectorRepository]. /// Implementation of [StaffConnectorRepository].
/// ///
@@ -12,10 +11,10 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
/// Creates a new [StaffConnectorRepositoryImpl]. /// Creates a new [StaffConnectorRepositoryImpl].
/// ///
/// Requires a [DataConnectService] instance for backend communication. /// Requires a [DataConnectService] instance for backend communication.
StaffConnectorRepositoryImpl({DataConnectService? service}) StaffConnectorRepositoryImpl({dc.DataConnectService? service})
: _service = service ?? DataConnectService.instance; : _service = service ?? dc.DataConnectService.instance;
final DataConnectService _service; final dc.DataConnectService _service;
@override @override
Future<bool> getProfileCompletion() async { Future<bool> getProfileCompletion() async {
@@ -23,17 +22,17 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
final String staffId = await _service.getStaffId(); final String staffId = await _service.getStaffId();
final QueryResult< final QueryResult<
GetStaffProfileCompletionData, dc.GetStaffProfileCompletionData,
GetStaffProfileCompletionVariables dc.GetStaffProfileCompletionVariables
> >
response = await _service.connector response = await _service.connector
.getStaffProfileCompletion(id: staffId) .getStaffProfileCompletion(id: staffId)
.execute(); .execute();
final GetStaffProfileCompletionStaff? staff = response.data.staff; final dc.GetStaffProfileCompletionStaff? staff = response.data.staff;
final List<GetStaffProfileCompletionEmergencyContacts> emergencyContacts = final List<dc.GetStaffProfileCompletionEmergencyContacts>
response.data.emergencyContacts; emergencyContacts = response.data.emergencyContacts;
final List<GetStaffProfileCompletionTaxForms> taxForms = final List<dc.GetStaffProfileCompletionTaxForms> taxForms =
response.data.taxForms; response.data.taxForms;
return _isProfileComplete(staff, emergencyContacts, taxForms); return _isProfileComplete(staff, emergencyContacts, taxForms);
@@ -46,15 +45,14 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
final String staffId = await _service.getStaffId(); final String staffId = await _service.getStaffId();
final QueryResult< final QueryResult<
GetStaffPersonalInfoCompletionData, dc.GetStaffPersonalInfoCompletionData,
GetStaffPersonalInfoCompletionVariables dc.GetStaffPersonalInfoCompletionVariables
> >
response = await _service.connector response = await _service.connector
.getStaffPersonalInfoCompletion(id: staffId) .getStaffPersonalInfoCompletion(id: staffId)
.execute(); .execute();
final GetStaffPersonalInfoCompletionStaff? staff = response.data.staff; final dc.GetStaffPersonalInfoCompletionStaff? staff = response.data.staff;
return _isPersonalInfoComplete(staff); return _isPersonalInfoComplete(staff);
}); });
} }
@@ -65,8 +63,8 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
final String staffId = await _service.getStaffId(); final String staffId = await _service.getStaffId();
final QueryResult< final QueryResult<
GetStaffEmergencyProfileCompletionData, dc.GetStaffEmergencyProfileCompletionData,
GetStaffEmergencyProfileCompletionVariables dc.GetStaffEmergencyProfileCompletionVariables
> >
response = await _service.connector response = await _service.connector
.getStaffEmergencyProfileCompletion(id: staffId) .getStaffEmergencyProfileCompletion(id: staffId)
@@ -82,16 +80,15 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
final String staffId = await _service.getStaffId(); final String staffId = await _service.getStaffId();
final QueryResult< final QueryResult<
GetStaffExperienceProfileCompletionData, dc.GetStaffExperienceProfileCompletionData,
GetStaffExperienceProfileCompletionVariables dc.GetStaffExperienceProfileCompletionVariables
> >
response = await _service.connector response = await _service.connector
.getStaffExperienceProfileCompletion(id: staffId) .getStaffExperienceProfileCompletion(id: staffId)
.execute(); .execute();
final GetStaffExperienceProfileCompletionStaff? staff = final dc.GetStaffExperienceProfileCompletionStaff? staff =
response.data.staff; response.data.staff;
return _hasExperience(staff); return _hasExperience(staff);
}); });
} }
@@ -102,8 +99,8 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
final String staffId = await _service.getStaffId(); final String staffId = await _service.getStaffId();
final QueryResult< final QueryResult<
GetStaffTaxFormsProfileCompletionData, dc.GetStaffTaxFormsProfileCompletionData,
GetStaffTaxFormsProfileCompletionVariables dc.GetStaffTaxFormsProfileCompletionVariables
> >
response = await _service.connector response = await _service.connector
.getStaffTaxFormsProfileCompletion(id: staffId) .getStaffTaxFormsProfileCompletion(id: staffId)
@@ -114,150 +111,162 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
} }
/// Checks if personal info is complete. /// Checks if personal info is complete.
bool _isPersonalInfoComplete(GetStaffPersonalInfoCompletionStaff? staff) { bool _isPersonalInfoComplete(dc.GetStaffPersonalInfoCompletionStaff? staff) {
if (staff == null) return false; if (staff == null) return false;
final String fullName = staff.fullName; final String fullName = staff.fullName;
final String? email = staff.email; final String? email = staff.email;
final String? phone = staff.phone; final String? phone = staff.phone;
return (fullName.trim().isNotEmpty ?? false) && return fullName.trim().isNotEmpty &&
(email?.trim().isNotEmpty ?? false) && (email?.trim().isNotEmpty ?? false) &&
(phone?.trim().isNotEmpty ?? false); (phone?.trim().isNotEmpty ?? false);
} }
/// Checks if staff has experience data (skills or industries). /// Checks if staff has experience data (skills or industries).
bool _hasExperience(GetStaffExperienceProfileCompletionStaff? staff) { bool _hasExperience(dc.GetStaffExperienceProfileCompletionStaff? staff) {
if (staff == null) return false; if (staff == null) return false;
final dynamic skills = staff.skills; final List<String>? skills = staff.skills;
final dynamic industries = staff.industries; final List<String>? industries = staff.industries;
return (skills is List && skills.isNotEmpty) || return (skills?.isNotEmpty ?? false) || (industries?.isNotEmpty ?? false);
(industries is List && industries.isNotEmpty);
} }
/// Determines if the profile is complete based on all sections. /// Determines if the profile is complete based on all sections.
bool _isProfileComplete( bool _isProfileComplete(
GetStaffProfileCompletionStaff? staff, dc.GetStaffProfileCompletionStaff? staff,
List<GetStaffProfileCompletionEmergencyContacts> emergencyContacts, List<dc.GetStaffProfileCompletionEmergencyContacts> emergencyContacts,
List<GetStaffProfileCompletionTaxForms> taxForms, List<dc.GetStaffProfileCompletionTaxForms> taxForms,
) { ) {
if (staff == null) return false; if (staff == null) return false;
final dynamic skills = staff.skills;
final dynamic industries = staff.industries; final List<String>? skills = staff.skills;
final List<String>? industries = staff.industries;
final bool hasExperience = final bool hasExperience =
(skills is List && skills.isNotEmpty) || (skills?.isNotEmpty ?? false) || (industries?.isNotEmpty ?? false);
(industries is List && industries.isNotEmpty);
return emergencyContacts.isNotEmpty && taxForms.isNotEmpty && hasExperience; return (staff.fullName.trim().isNotEmpty) &&
(staff.email?.trim().isNotEmpty ?? false) &&
emergencyContacts.isNotEmpty &&
taxForms.isNotEmpty &&
hasExperience;
} }
@override @override
Future<Staff> getStaffProfile() async { Future<domain.Staff> getStaffProfile() async {
return _service.run(() async { return _service.run(() async {
final String staffId = await _service.getStaffId(); final String staffId = await _service.getStaffId();
final QueryResult<GetStaffByIdData, GetStaffByIdVariables> response = final QueryResult<dc.GetStaffByIdData, dc.GetStaffByIdVariables>
await _service.connector.getStaffById(id: staffId).execute(); response = await _service.connector.getStaffById(id: staffId).execute();
if (response.data.staff == null) { final dc.GetStaffByIdStaff? staff = response.data.staff;
throw const ServerException(technicalMessage: 'Staff not found');
if (staff == null) {
throw Exception('Staff not found');
} }
final GetStaffByIdStaff rawStaff = response.data.staff!; return domain.Staff(
id: staff.id,
// Map the raw data connect object to the Domain Entity authProviderId: staff.userId,
return Staff( name: staff.fullName,
id: rawStaff.id, email: staff.email ?? '',
authProviderId: rawStaff.userId, phone: staff.phone,
name: rawStaff.fullName, avatar: staff.photoUrl,
email: rawStaff.email ?? '', status: domain.StaffStatus.active,
phone: rawStaff.phone, address: staff.addres,
avatar: rawStaff.photoUrl, totalShifts: staff.totalShifts,
status: StaffStatus.active, averageRating: staff.averageRating,
address: rawStaff.addres, onTimeRate: staff.onTimeRate,
totalShifts: rawStaff.totalShifts, noShowCount: staff.noShowCount,
averageRating: rawStaff.averageRating, cancellationCount: staff.cancellationCount,
onTimeRate: rawStaff.onTimeRate, reliabilityScore: staff.reliabilityScore,
noShowCount: rawStaff.noShowCount,
cancellationCount: rawStaff.cancellationCount,
reliabilityScore: rawStaff.reliabilityScore,
); );
}); });
} }
@override @override
Future<List<Benefit>> getBenefits() async { Future<List<domain.Benefit>> getBenefits() async {
return _service.run(() async { return _service.run(() async {
final String staffId = await _service.getStaffId(); final String staffId = await _service.getStaffId();
final QueryResult< final QueryResult<
ListBenefitsDataByStaffIdData, dc.ListBenefitsDataByStaffIdData,
ListBenefitsDataByStaffIdVariables dc.ListBenefitsDataByStaffIdVariables
> >
response = await _service.connector response = await _service.connector
.listBenefitsDataByStaffId(staffId: staffId) .listBenefitsDataByStaffId(staffId: staffId)
.execute(); .execute();
return response.data.benefitsDatas.map((data) { return response.data.benefitsDatas
final plan = data.vendorBenefitPlan; .map(
return Benefit( (dc.ListBenefitsDataByStaffIdBenefitsDatas e) => domain.Benefit(
title: plan.title, title: e.vendorBenefitPlan.title,
entitlementHours: plan.total?.toDouble() ?? 0.0, entitlementHours: e.vendorBenefitPlan.total?.toDouble() ?? 0,
usedHours: data.current.toDouble(), usedHours: e.current.toDouble(),
); ),
}).toList(); )
.toList();
}); });
} }
@override @override
Future<List<AttireItem>> getAttireOptions() async { Future<List<domain.AttireItem>> getAttireOptions() async {
return _service.run(() async { return _service.run(() async {
final String staffId = await _service.getStaffId(); final String staffId = await _service.getStaffId();
// Fetch all options final List<QueryResult<Object, Object?>> results =
final QueryResult<ListAttireOptionsData, void> optionsResponse = await Future.wait<QueryResult<Object, Object?>>(
await _service.connector.listAttireOptions().execute(); <Future<QueryResult<Object, Object?>>>[
_service.connector.listAttireOptions().execute(),
_service.connector.getStaffAttire(staffId: staffId).execute(),
],
);
// Fetch user's attire status final QueryResult<dc.ListAttireOptionsData, void> optionsRes =
final QueryResult<GetStaffAttireData, GetStaffAttireVariables> results[0] as QueryResult<dc.ListAttireOptionsData, void>;
attiresResponse = await _service.connector final QueryResult<dc.GetStaffAttireData, dc.GetStaffAttireVariables>
.getStaffAttire(staffId: staffId) staffAttireRes =
.execute(); results[1]
as QueryResult<dc.GetStaffAttireData, dc.GetStaffAttireVariables>;
final Map<String, GetStaffAttireStaffAttires> attireMap = { final List<dc.GetStaffAttireStaffAttires> staffAttire =
for (final item in attiresResponse.data.staffAttires) staffAttireRes.data.staffAttires;
item.attireOptionId: item,
};
return optionsResponse.data.attireOptions.map((e) { return optionsRes.data.attireOptions.map((
final GetStaffAttireStaffAttires? userAttire = attireMap[e.id]; dc.ListAttireOptionsAttireOptions opt,
return AttireItem( ) {
id: e.id, final dc.GetStaffAttireStaffAttires currentAttire = staffAttire
code: e.itemId, .firstWhere(
label: e.label, (dc.GetStaffAttireStaffAttires a) => a.attireOptionId == opt.id,
description: e.description, orElse: () => dc.GetStaffAttireStaffAttires(
imageUrl: e.imageUrl, attireOptionId: opt.id,
isMandatory: e.isMandatory ?? false, verificationPhotoUrl: null,
verificationStatus: _mapAttireStatus( verificationId: null,
userAttire?.verificationStatus?.stringValue, verificationStatus: null,
), ),
photoUrl: userAttire?.verificationPhotoUrl, );
verificationId: userAttire?.verificationId,
return domain.AttireItem(
id: opt.id,
code: opt.itemId,
label: opt.label,
description: opt.description,
imageUrl: opt.imageUrl,
isMandatory: opt.isMandatory ?? false,
photoUrl: currentAttire.verificationPhotoUrl,
verificationId: currentAttire.verificationId,
verificationStatus: currentAttire.verificationStatus != null
? _mapFromDCStatus(currentAttire.verificationStatus!)
: null,
); );
}).toList(); }).toList();
}); });
} }
AttireVerificationStatus? _mapAttireStatus(String? status) {
if (status == null) return null;
return AttireVerificationStatus.values.firstWhere(
(e) => e.name.toUpperCase() == status.toUpperCase(),
orElse: () => AttireVerificationStatus.pending,
);
}
@override @override
Future<void> upsertStaffAttire({ Future<void> upsertStaffAttire({
required String attireOptionId, required String attireOptionId,
required String photoUrl, required String photoUrl,
String? verificationId, String? verificationId,
domain.AttireVerificationStatus? verificationStatus,
}) async { }) async {
await _service.run(() async { await _service.run(() async {
final String staffId = await _service.getStaffId(); final String staffId = await _service.getStaffId();
@@ -266,6 +275,67 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
.upsertStaffAttire(staffId: staffId, attireOptionId: attireOptionId) .upsertStaffAttire(staffId: staffId, attireOptionId: attireOptionId)
.verificationPhotoUrl(photoUrl) .verificationPhotoUrl(photoUrl)
.verificationId(verificationId) .verificationId(verificationId)
.verificationStatus(
verificationStatus != null
? dc.AttireVerificationStatus.values.firstWhere(
(dc.AttireVerificationStatus e) =>
e.name == verificationStatus.value.toUpperCase(),
orElse: () => dc.AttireVerificationStatus.PENDING,
)
: null,
)
.execute();
});
}
domain.AttireVerificationStatus _mapFromDCStatus(
dc.EnumValue<dc.AttireVerificationStatus> status,
) {
if (status is dc.Unknown) {
return domain.AttireVerificationStatus.error;
}
final String name =
(status as dc.Known<dc.AttireVerificationStatus>).value.name;
switch (name) {
case 'PENDING':
return domain.AttireVerificationStatus.pending;
case 'PROCESSING':
return domain.AttireVerificationStatus.processing;
case 'AUTO_PASS':
return domain.AttireVerificationStatus.autoPass;
case 'AUTO_FAIL':
return domain.AttireVerificationStatus.autoFail;
case 'NEEDS_REVIEW':
return domain.AttireVerificationStatus.needsReview;
case 'APPROVED':
return domain.AttireVerificationStatus.approved;
case 'REJECTED':
return domain.AttireVerificationStatus.rejected;
case 'ERROR':
return domain.AttireVerificationStatus.error;
default:
return domain.AttireVerificationStatus.error;
}
}
@override
Future<void> saveStaffProfile({
String? firstName,
String? lastName,
String? bio,
String? profilePictureUrl,
}) async {
await _service.run(() async {
final String staffId = await _service.getStaffId();
final String? fullName = (firstName != null || lastName != null)
? '${firstName ?? ''} ${lastName ?? ''}'.trim()
: null;
await _service.connector
.updateStaff(id: staffId)
.fullName(fullName)
.bio(bio)
.photoUrl(profilePictureUrl)
.execute(); .execute();
}); });
} }

View File

@@ -55,6 +55,7 @@ abstract interface class StaffConnectorRepository {
required String attireOptionId, required String attireOptionId,
required String photoUrl, required String photoUrl,
String? verificationId, String? verificationId,
AttireVerificationStatus? verificationStatus,
}); });
/// Signs out the current user. /// Signs out the current user.
@@ -63,4 +64,12 @@ abstract interface class StaffConnectorRepository {
/// ///
/// Throws an exception if the sign-out fails. /// Throws an exception if the sign-out fails.
Future<void> signOut(); Future<void> signOut();
/// Saves the staff profile information.
Future<void> saveStaffProfile({
String? firstName,
String? lastName,
String? bio,
String? profilePictureUrl,
});
} }

View File

@@ -1,11 +1,39 @@
/// Represents the verification status of an attire item photo. /// Represents the verification status of an attire item photo.
enum AttireVerificationStatus { enum AttireVerificationStatus {
/// The photo is waiting for review. /// Job is created and waiting to be processed.
pending, pending('PENDING'),
/// The photo was rejected. /// Job is currently being processed by machine or human.
failed, processing('PROCESSING'),
/// The photo was approved. /// Machine verification passed automatically.
success, autoPass('AUTO_PASS'),
/// Machine verification failed automatically.
autoFail('AUTO_FAIL'),
/// Machine results are inconclusive and require human review.
needsReview('NEEDS_REVIEW'),
/// Human reviewer approved the verification.
approved('APPROVED'),
/// Human reviewer rejected the verification.
rejected('REJECTED'),
/// An error occurred during processing.
error('ERROR');
const AttireVerificationStatus(this.value);
/// The string value expected by the Core API.
final String value;
/// Creates a [AttireVerificationStatus] from a string.
static AttireVerificationStatus fromString(String value) {
return AttireVerificationStatus.values.firstWhere(
(AttireVerificationStatus e) => e.value == value,
orElse: () => AttireVerificationStatus.error,
);
}
} }

View File

@@ -1,7 +1,8 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart'
hide AttireVerificationStatus;
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/attire_repository.dart'; import '../../domain/repositories/attire_repository.dart';
@@ -72,6 +73,7 @@ class AttireRepositoryImpl implements AttireRepository {
rules: <String, dynamic>{'dressCode': dressCode}, rules: <String, dynamic>{'dressCode': dressCode},
); );
final String verificationId = verifyRes.verificationId; final String verificationId = verifyRes.verificationId;
VerificationStatus currentStatus = verifyRes.status;
// 4. Poll for status until it's finished or timeout (max 10 seconds) // 4. Poll for status until it's finished or timeout (max 10 seconds)
try { try {
@@ -81,8 +83,9 @@ class AttireRepositoryImpl implements AttireRepository {
await Future<void>.delayed(const Duration(seconds: 2)); await Future<void>.delayed(const Duration(seconds: 2));
final VerificationResponse statusRes = await verificationService final VerificationResponse statusRes = await verificationService
.getStatus(verificationId); .getStatus(verificationId);
final String status = statusRes.status; currentStatus = statusRes.status;
if (status != 'PENDING' && status != 'QUEUED') { if (currentStatus != VerificationStatus.pending &&
currentStatus != VerificationStatus.processing) {
isFinished = true; isFinished = true;
} }
attempts++; attempts++;
@@ -97,10 +100,32 @@ class AttireRepositoryImpl implements AttireRepository {
attireOptionId: itemId, attireOptionId: itemId,
photoUrl: photoUrl, photoUrl: photoUrl,
verificationId: verificationId, verificationId: verificationId,
verificationStatus: _mapToAttireStatus(currentStatus),
); );
// 6. Return updated AttireItem by re-fetching to get the PENDING/SUCCESS status // 6. Return updated AttireItem by re-fetching to get the PENDING/SUCCESS status
final List<AttireItem> finalOptions = await _connector.getAttireOptions(); final List<AttireItem> finalOptions = await _connector.getAttireOptions();
return finalOptions.firstWhere((AttireItem e) => e.id == itemId); return finalOptions.firstWhere((AttireItem e) => e.id == itemId);
} }
AttireVerificationStatus _mapToAttireStatus(VerificationStatus status) {
switch (status) {
case VerificationStatus.pending:
return AttireVerificationStatus.pending;
case VerificationStatus.processing:
return AttireVerificationStatus.processing;
case VerificationStatus.autoPass:
return AttireVerificationStatus.autoPass;
case VerificationStatus.autoFail:
return AttireVerificationStatus.autoFail;
case VerificationStatus.needsReview:
return AttireVerificationStatus.needsReview;
case VerificationStatus.approved:
return AttireVerificationStatus.approved;
case VerificationStatus.rejected:
return AttireVerificationStatus.rejected;
case VerificationStatus.error:
return AttireVerificationStatus.error;
}
}
} }

View File

@@ -115,6 +115,22 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
} }
} }
String _getStatusText(bool hasUploadedPhoto) {
return switch (widget.item.verificationStatus) {
AttireVerificationStatus.approved => 'Approved',
AttireVerificationStatus.rejected => 'Rejected',
_ => hasUploadedPhoto ? 'Pending Verification' : 'Not Uploaded',
};
}
Color _getStatusColor(bool hasUploadedPhoto) {
return switch (widget.item.verificationStatus) {
AttireVerificationStatus.approved => UiColors.textSuccess,
AttireVerificationStatus.rejected => UiColors.textError,
_ => hasUploadedPhoto ? UiColors.textWarning : UiColors.textInactive,
};
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<AttireCaptureCubit>( return BlocProvider<AttireCaptureCubit>(
@@ -145,26 +161,9 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
state.photoUrl ?? widget.initialPhotoUrl; state.photoUrl ?? widget.initialPhotoUrl;
final bool hasUploadedPhoto = currentPhotoUrl != null; final bool hasUploadedPhoto = currentPhotoUrl != null;
final String statusText = switch (widget final String statusText = _getStatusText(hasUploadedPhoto);
.item
.verificationStatus) {
AttireVerificationStatus.success => 'Approved',
AttireVerificationStatus.failed => 'Rejected',
AttireVerificationStatus.pending => 'Pending Verification',
_ =>
hasUploadedPhoto ? 'Pending Verification' : 'Not Uploaded',
};
final Color statusColor = final Color statusColor = _getStatusColor(hasUploadedPhoto);
switch (widget.item.verificationStatus) {
AttireVerificationStatus.success => UiColors.textSuccess,
AttireVerificationStatus.failed => UiColors.textError,
AttireVerificationStatus.pending => UiColors.textWarning,
_ =>
hasUploadedPhoto
? UiColors.textWarning
: UiColors.textInactive,
};
return Column( return Column(
children: <Widget>[ children: <Widget>[
@@ -196,7 +195,7 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
widget.item.imageUrl ?? '', widget.item.imageUrl ?? '',
height: 120, height: 120,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (_, __, ___) => errorBuilder: (_, _, _) =>
const SizedBox.shrink(), const SizedBox.shrink(),
), ),
), ),
@@ -223,7 +222,7 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
widget.item.imageUrl ?? '', widget.item.imageUrl ?? '',
height: 120, height: 120,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (_, __, ___) => errorBuilder: (_, _, _) =>
const SizedBox.shrink(), const SizedBox.shrink(),
), ),
), ),

View File

@@ -3,11 +3,6 @@ import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
class AttireItemCard extends StatelessWidget { class AttireItemCard extends StatelessWidget {
final AttireItem item;
final String? uploadedPhotoUrl;
final bool isUploading;
final VoidCallback onTap;
const AttireItemCard({ const AttireItemCard({
super.key, super.key,
required this.item, required this.item,
@@ -16,12 +11,17 @@ class AttireItemCard extends StatelessWidget {
required this.onTap, required this.onTap,
}); });
final AttireItem item;
final String? uploadedPhotoUrl;
final bool isUploading;
final VoidCallback onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool hasPhoto = item.photoUrl != null; final bool hasPhoto = item.photoUrl != null;
final String statusText = switch (item.verificationStatus) { final String statusText = switch (item.verificationStatus) {
AttireVerificationStatus.success => 'Approved', AttireVerificationStatus.approved => 'Approved',
AttireVerificationStatus.failed => 'Rejected', AttireVerificationStatus.rejected => 'Rejected',
AttireVerificationStatus.pending => 'Pending', AttireVerificationStatus.pending => 'Pending',
_ => hasPhoto ? 'Pending' : 'To Do', _ => hasPhoto ? 'Pending' : 'To Do',
}; };
@@ -91,7 +91,7 @@ class AttireItemCard extends StatelessWidget {
size: UiChipSize.xSmall, size: UiChipSize.xSmall,
variant: variant:
item.verificationStatus == item.verificationStatus ==
AttireVerificationStatus.success AttireVerificationStatus.approved
? UiChipVariant.primary ? UiChipVariant.primary
: UiChipVariant.secondary, : UiChipVariant.secondary,
), ),
@@ -114,12 +114,12 @@ class AttireItemCard extends StatelessWidget {
) )
else if (hasPhoto && !isUploading) else if (hasPhoto && !isUploading)
Icon( Icon(
item.verificationStatus == AttireVerificationStatus.success item.verificationStatus == AttireVerificationStatus.approved
? UiIcons.check ? UiIcons.check
: UiIcons.clock, : UiIcons.clock,
color: color:
item.verificationStatus == item.verificationStatus ==
AttireVerificationStatus.success AttireVerificationStatus.approved
? UiColors.textPrimary ? UiColors.textPrimary
: UiColors.textWarning, : UiColors.textWarning,
size: 24, size: 24,

View File

@@ -3,6 +3,7 @@ mutation upsertStaffAttire(
$attireOptionId: UUID! $attireOptionId: UUID!
$verificationPhotoUrl: String $verificationPhotoUrl: String
$verificationId: String $verificationId: String
$verificationStatus: AttireVerificationStatus
) @auth(level: USER) { ) @auth(level: USER) {
staffAttire_upsert( staffAttire_upsert(
data: { data: {
@@ -10,7 +11,7 @@ mutation upsertStaffAttire(
attireOptionId: $attireOptionId attireOptionId: $attireOptionId
verificationPhotoUrl: $verificationPhotoUrl verificationPhotoUrl: $verificationPhotoUrl
verificationId: $verificationId verificationId: $verificationId
verificationStatus: PENDING verificationStatus: $verificationStatus
} }
) )
} }

View File

@@ -1,7 +1,12 @@
enum AttireVerificationStatus { enum AttireVerificationStatus {
PENDING PENDING
FAILED PROCESSING
SUCCESS AUTO_PASS
AUTO_FAIL
NEEDS_REVIEW
APPROVED
REJECTED
ERROR
} }
type StaffAttire @table(name: "staff_attires", key: ["staffId", "attireOptionId"]) { type StaffAttire @table(name: "staff_attires", key: ["staffId", "attireOptionId"]) {