feat: Enhance attire verification status system with more granular states and update related UI and data handling.
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user