feat: Implement staff attire management including fetching options, user attire status, and upserting attire details.
This commit is contained in:
@@ -11,9 +11,8 @@ 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({
|
StaffConnectorRepositoryImpl({DataConnectService? service})
|
||||||
DataConnectService? service,
|
: _service = service ?? DataConnectService.instance;
|
||||||
}) : _service = service ?? DataConnectService.instance;
|
|
||||||
|
|
||||||
final DataConnectService _service;
|
final DataConnectService _service;
|
||||||
|
|
||||||
@@ -22,15 +21,17 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final String staffId = await _service.getStaffId();
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
final QueryResult<GetStaffProfileCompletionData,
|
final QueryResult<
|
||||||
GetStaffProfileCompletionVariables> response =
|
GetStaffProfileCompletionData,
|
||||||
await _service.connector
|
GetStaffProfileCompletionVariables
|
||||||
.getStaffProfileCompletion(id: staffId)
|
>
|
||||||
.execute();
|
response = await _service.connector
|
||||||
|
.getStaffProfileCompletion(id: staffId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
final GetStaffProfileCompletionStaff? staff = response.data.staff;
|
final GetStaffProfileCompletionStaff? staff = response.data.staff;
|
||||||
final List<GetStaffProfileCompletionEmergencyContacts>
|
final List<GetStaffProfileCompletionEmergencyContacts> emergencyContacts =
|
||||||
emergencyContacts = response.data.emergencyContacts;
|
response.data.emergencyContacts;
|
||||||
final List<GetStaffProfileCompletionTaxForms> taxForms =
|
final List<GetStaffProfileCompletionTaxForms> taxForms =
|
||||||
response.data.taxForms;
|
response.data.taxForms;
|
||||||
|
|
||||||
@@ -43,11 +44,13 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final String staffId = await _service.getStaffId();
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
final QueryResult<GetStaffPersonalInfoCompletionData,
|
final QueryResult<
|
||||||
GetStaffPersonalInfoCompletionVariables> response =
|
GetStaffPersonalInfoCompletionData,
|
||||||
await _service.connector
|
GetStaffPersonalInfoCompletionVariables
|
||||||
.getStaffPersonalInfoCompletion(id: staffId)
|
>
|
||||||
.execute();
|
response = await _service.connector
|
||||||
|
.getStaffPersonalInfoCompletion(id: staffId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
final GetStaffPersonalInfoCompletionStaff? staff = response.data.staff;
|
final GetStaffPersonalInfoCompletionStaff? staff = response.data.staff;
|
||||||
|
|
||||||
@@ -60,11 +63,13 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final String staffId = await _service.getStaffId();
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
final QueryResult<GetStaffEmergencyProfileCompletionData,
|
final QueryResult<
|
||||||
GetStaffEmergencyProfileCompletionVariables> response =
|
GetStaffEmergencyProfileCompletionData,
|
||||||
await _service.connector
|
GetStaffEmergencyProfileCompletionVariables
|
||||||
.getStaffEmergencyProfileCompletion(id: staffId)
|
>
|
||||||
.execute();
|
response = await _service.connector
|
||||||
|
.getStaffEmergencyProfileCompletion(id: staffId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
return response.data.emergencyContacts.isNotEmpty;
|
return response.data.emergencyContacts.isNotEmpty;
|
||||||
});
|
});
|
||||||
@@ -75,11 +80,13 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final String staffId = await _service.getStaffId();
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
final QueryResult<GetStaffExperienceProfileCompletionData,
|
final QueryResult<
|
||||||
GetStaffExperienceProfileCompletionVariables> response =
|
GetStaffExperienceProfileCompletionData,
|
||||||
await _service.connector
|
GetStaffExperienceProfileCompletionVariables
|
||||||
.getStaffExperienceProfileCompletion(id: staffId)
|
>
|
||||||
.execute();
|
response = await _service.connector
|
||||||
|
.getStaffExperienceProfileCompletion(id: staffId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
final GetStaffExperienceProfileCompletionStaff? staff =
|
final GetStaffExperienceProfileCompletionStaff? staff =
|
||||||
response.data.staff;
|
response.data.staff;
|
||||||
@@ -93,11 +100,13 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final String staffId = await _service.getStaffId();
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
final QueryResult<GetStaffTaxFormsProfileCompletionData,
|
final QueryResult<
|
||||||
GetStaffTaxFormsProfileCompletionVariables> response =
|
GetStaffTaxFormsProfileCompletionData,
|
||||||
await _service.connector
|
GetStaffTaxFormsProfileCompletionVariables
|
||||||
.getStaffTaxFormsProfileCompletion(id: staffId)
|
>
|
||||||
.execute();
|
response = await _service.connector
|
||||||
|
.getStaffTaxFormsProfileCompletion(id: staffId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
return response.data.taxForms.isNotEmpty;
|
return response.data.taxForms.isNotEmpty;
|
||||||
});
|
});
|
||||||
@@ -135,9 +144,7 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
final bool hasExperience =
|
final bool hasExperience =
|
||||||
(skills is List && skills.isNotEmpty) ||
|
(skills is List && skills.isNotEmpty) ||
|
||||||
(industries is List && industries.isNotEmpty);
|
(industries is List && industries.isNotEmpty);
|
||||||
return emergencyContacts.isNotEmpty &&
|
return emergencyContacts.isNotEmpty && taxForms.isNotEmpty && hasExperience;
|
||||||
taxForms.isNotEmpty &&
|
|
||||||
hasExperience;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -146,14 +153,10 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
final String staffId = await _service.getStaffId();
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
final QueryResult<GetStaffByIdData, GetStaffByIdVariables> response =
|
final QueryResult<GetStaffByIdData, GetStaffByIdVariables> response =
|
||||||
await _service.connector
|
await _service.connector.getStaffById(id: staffId).execute();
|
||||||
.getStaffById(id: staffId)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
if (response.data.staff == null) {
|
if (response.data.staff == null) {
|
||||||
throw const ServerException(
|
throw const ServerException(technicalMessage: 'Staff not found');
|
||||||
technicalMessage: 'Staff not found',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final GetStaffByIdStaff rawStaff = response.data.staff!;
|
final GetStaffByIdStaff rawStaff = response.data.staff!;
|
||||||
@@ -183,11 +186,13 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final String staffId = await _service.getStaffId();
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
final QueryResult<ListBenefitsDataByStaffIdData,
|
final QueryResult<
|
||||||
ListBenefitsDataByStaffIdVariables> response =
|
ListBenefitsDataByStaffIdData,
|
||||||
await _service.connector
|
ListBenefitsDataByStaffIdVariables
|
||||||
.listBenefitsDataByStaffId(staffId: staffId)
|
>
|
||||||
.execute();
|
response = await _service.connector
|
||||||
|
.listBenefitsDataByStaffId(staffId: staffId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
return response.data.benefitsDatas.map((data) {
|
return response.data.benefitsDatas.map((data) {
|
||||||
final plan = data.vendorBenefitPlan;
|
final plan = data.vendorBenefitPlan;
|
||||||
@@ -200,6 +205,56 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<AttireItem>> getAttireOptions() async {
|
||||||
|
return _service.run(() async {
|
||||||
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
|
// Fetch all options
|
||||||
|
final QueryResult<ListAttireOptionsData, void> optionsResponse =
|
||||||
|
await _service.connector.listAttireOptions().execute();
|
||||||
|
|
||||||
|
// Fetch user's attire status
|
||||||
|
final QueryResult<GetStaffAttireData, GetStaffAttireVariables>
|
||||||
|
attiresResponse = await _service.connector
|
||||||
|
.getStaffAttire(staffId: staffId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
final Map<String, GetStaffAttireStaffAttires> attireMap = {
|
||||||
|
for (final item in attiresResponse.data.staffAttires)
|
||||||
|
item.attireOptionId: item,
|
||||||
|
};
|
||||||
|
|
||||||
|
return optionsResponse.data.attireOptions.map((e) {
|
||||||
|
final GetStaffAttireStaffAttires? userAttire = attireMap[e.id];
|
||||||
|
return AttireItem(
|
||||||
|
id: e.itemId,
|
||||||
|
label: e.label,
|
||||||
|
description: e.description,
|
||||||
|
imageUrl: e.imageUrl,
|
||||||
|
isMandatory: e.isMandatory ?? false,
|
||||||
|
verificationStatus: userAttire?.verificationStatus?.stringValue,
|
||||||
|
photoUrl: userAttire?.verificationPhotoUrl,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> upsertStaffAttire({
|
||||||
|
required String attireOptionId,
|
||||||
|
required String photoUrl,
|
||||||
|
}) async {
|
||||||
|
await _service.run(() async {
|
||||||
|
final String staffId = await _service.getStaffId();
|
||||||
|
|
||||||
|
await _service.connector
|
||||||
|
.upsertStaffAttire(staffId: staffId, attireOptionId: attireOptionId)
|
||||||
|
.verificationPhotoUrl(photoUrl)
|
||||||
|
.execute();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> signOut() async {
|
Future<void> signOut() async {
|
||||||
try {
|
try {
|
||||||
@@ -210,4 +265,3 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,17 @@ abstract interface class StaffConnectorRepository {
|
|||||||
/// Returns a list of [Benefit] entities.
|
/// Returns a list of [Benefit] entities.
|
||||||
Future<List<Benefit>> getBenefits();
|
Future<List<Benefit>> getBenefits();
|
||||||
|
|
||||||
|
/// Fetches the attire options for the current authenticated user.
|
||||||
|
///
|
||||||
|
/// Returns a list of [AttireItem] entities.
|
||||||
|
Future<List<AttireItem>> getAttireOptions();
|
||||||
|
|
||||||
|
/// Upserts staff attire photo information.
|
||||||
|
Future<void> upsertStaffAttire({
|
||||||
|
required String attireOptionId,
|
||||||
|
required String photoUrl,
|
||||||
|
});
|
||||||
|
|
||||||
/// Signs out the current user.
|
/// Signs out the current user.
|
||||||
///
|
///
|
||||||
/// Clears the user's session and authentication state.
|
/// Clears the user's session and authentication state.
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ class AttireItem extends Equatable {
|
|||||||
this.description,
|
this.description,
|
||||||
this.imageUrl,
|
this.imageUrl,
|
||||||
this.isMandatory = false,
|
this.isMandatory = false,
|
||||||
|
this.verificationStatus,
|
||||||
|
this.photoUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Unique identifier of the attire item.
|
/// Unique identifier of the attire item.
|
||||||
@@ -28,6 +30,12 @@ class AttireItem extends Equatable {
|
|||||||
/// Whether this item is mandatory for onboarding.
|
/// Whether this item is mandatory for onboarding.
|
||||||
final bool isMandatory;
|
final bool isMandatory;
|
||||||
|
|
||||||
|
/// The current verification status of the uploaded photo.
|
||||||
|
final String? verificationStatus;
|
||||||
|
|
||||||
|
/// The URL of the photo uploaded by the staff member.
|
||||||
|
final String? photoUrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
id,
|
id,
|
||||||
@@ -35,5 +43,7 @@ class AttireItem extends Equatable {
|
|||||||
description,
|
description,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
isMandatory,
|
isMandatory,
|
||||||
|
verificationStatus,
|
||||||
|
photoUrl,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
@@ -6,34 +5,19 @@ import '../../domain/repositories/attire_repository.dart';
|
|||||||
|
|
||||||
/// Implementation of [AttireRepository].
|
/// Implementation of [AttireRepository].
|
||||||
///
|
///
|
||||||
/// Delegates data access to [DataConnectService].
|
/// Delegates data access to [StaffConnectorRepository].
|
||||||
class AttireRepositoryImpl implements AttireRepository {
|
class AttireRepositoryImpl implements AttireRepository {
|
||||||
/// Creates an [AttireRepositoryImpl].
|
/// Creates an [AttireRepositoryImpl].
|
||||||
AttireRepositoryImpl({DataConnectService? service})
|
AttireRepositoryImpl({StaffConnectorRepository? connector})
|
||||||
: _service = service ?? DataConnectService.instance;
|
: _connector =
|
||||||
|
connector ?? DataConnectService.instance.getStaffRepository();
|
||||||
|
|
||||||
/// The Data Connect service.
|
/// The Staff Connector repository.
|
||||||
final DataConnectService _service;
|
final StaffConnectorRepository _connector;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<AttireItem>> getAttireOptions() async {
|
Future<List<AttireItem>> getAttireOptions() async {
|
||||||
return _service.run(() async {
|
return _connector.getAttireOptions();
|
||||||
final QueryResult<ListAttireOptionsData, void> result = await _service
|
|
||||||
.connector
|
|
||||||
.listAttireOptions()
|
|
||||||
.execute();
|
|
||||||
return result.data.attireOptions
|
|
||||||
.map(
|
|
||||||
(ListAttireOptionsAttireOptions e) => AttireItem(
|
|
||||||
id: e.itemId,
|
|
||||||
label: e.label,
|
|
||||||
description: e.description,
|
|
||||||
imageUrl: e.imageUrl,
|
|
||||||
isMandatory: e.isMandatory ?? false,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -41,16 +25,22 @@ class AttireRepositoryImpl implements AttireRepository {
|
|||||||
required List<String> selectedItemIds,
|
required List<String> selectedItemIds,
|
||||||
required Map<String, String> photoUrls,
|
required Map<String, String> photoUrls,
|
||||||
}) async {
|
}) async {
|
||||||
// TODO: Connect to actual backend mutation when available.
|
// We already upsert photos in uploadPhoto (to follow the new flow).
|
||||||
// For now, simulate network delay as per prototype behavior.
|
// This could save selections if there was a separate "SelectedAttire" table.
|
||||||
await Future<void>.delayed(const Duration(seconds: 1));
|
// For now, it's a no-op as the source of truth is the StaffAttire table.
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> uploadPhoto(String itemId) async {
|
Future<String> uploadPhoto(String itemId) async {
|
||||||
// TODO: Connect to actual storage service/mutation when available.
|
// In a real app, this would upload to Firebase Storage first.
|
||||||
// For now, simulate upload delay and return mock URL.
|
// Since the prototype returns a mock URL, we'll use that to upsert our record.
|
||||||
await Future<void>.delayed(const Duration(seconds: 1));
|
final String mockUrl = 'mock_url_for_$itemId';
|
||||||
return 'mock_url_for_$itemId';
|
|
||||||
|
await _connector.upsertStaffAttire(
|
||||||
|
attireOptionId: itemId,
|
||||||
|
photoUrl: mockUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
return mockUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,18 +23,17 @@ class AttireCubit extends Cubit<AttireState>
|
|||||||
action: () async {
|
action: () async {
|
||||||
final List<AttireItem> options = await _getAttireOptionsUseCase();
|
final List<AttireItem> options = await _getAttireOptionsUseCase();
|
||||||
|
|
||||||
// Auto-select mandatory items initially as per prototype
|
// Extract photo URLs and selection status from backend data
|
||||||
final List<String> mandatoryIds = options
|
final Map<String, String> photoUrls = <String, String>{};
|
||||||
.where((AttireItem e) => e.isMandatory)
|
final List<String> selectedIds = <String>[];
|
||||||
.map((AttireItem e) => e.id)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final List<String> initialSelection = List<String>.from(
|
for (final AttireItem item in options) {
|
||||||
state.selectedIds,
|
if (item.photoUrl != null) {
|
||||||
);
|
photoUrls[item.id] = item.photoUrl!;
|
||||||
for (final String id in mandatoryIds) {
|
}
|
||||||
if (!initialSelection.contains(id)) {
|
// If mandatory or has photo, consider it selected initially
|
||||||
initialSelection.add(id);
|
if (item.isMandatory || item.photoUrl != null) {
|
||||||
|
selectedIds.add(item.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +41,8 @@ class AttireCubit extends Cubit<AttireState>
|
|||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: AttireStatus.success,
|
status: AttireStatus.success,
|
||||||
options: options,
|
options: options,
|
||||||
selectedIds: initialSelection,
|
selectedIds: selectedIds,
|
||||||
|
photoUrls: photoUrls,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -65,20 +65,8 @@ class AttireCubit extends Cubit<AttireState>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void syncCapturedPhoto(String itemId, String url) {
|
void syncCapturedPhoto(String itemId, String url) {
|
||||||
final Map<String, String> currentPhotos = Map<String, String>.from(
|
// When a photo is captured, we refresh the options to get the updated status from backend
|
||||||
state.photoUrls,
|
loadOptions();
|
||||||
);
|
|
||||||
currentPhotos[itemId] = url;
|
|
||||||
|
|
||||||
// Auto-select item on upload success if not selected
|
|
||||||
final List<String> currentSelection = List<String>.from(state.selectedIds);
|
|
||||||
if (!currentSelection.contains(itemId)) {
|
|
||||||
currentSelection.add(itemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(
|
|
||||||
state.copyWith(photoUrls: currentPhotos, selectedIds: currentSelection),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> save() async {
|
Future<void> save() async {
|
||||||
|
|||||||
@@ -70,14 +70,22 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
|||||||
builder: (BuildContext context, AttireCaptureState state) {
|
builder: (BuildContext context, AttireCaptureState state) {
|
||||||
final bool isUploading =
|
final bool isUploading =
|
||||||
state.status == AttireCaptureStatus.uploading;
|
state.status == AttireCaptureStatus.uploading;
|
||||||
final bool hasPhoto =
|
final String? currentPhotoUrl =
|
||||||
state.photoUrl != null || widget.initialPhotoUrl != null;
|
state.photoUrl ?? widget.initialPhotoUrl;
|
||||||
final String statusText = hasPhoto
|
final bool hasUploadedPhoto = currentPhotoUrl != null;
|
||||||
? 'Pending Verification'
|
|
||||||
: 'Not Uploaded';
|
final String statusText =
|
||||||
final Color statusColor = hasPhoto
|
widget.item.verificationStatus ??
|
||||||
? UiColors.textWarning
|
(hasUploadedPhoto
|
||||||
: UiColors.textInactive;
|
? 'Pending Verification'
|
||||||
|
: 'Not Uploaded');
|
||||||
|
|
||||||
|
final Color statusColor =
|
||||||
|
widget.item.verificationStatus == 'SUCCESS'
|
||||||
|
? UiColors.textPrimary
|
||||||
|
: (hasUploadedPhoto
|
||||||
|
? UiColors.textWarning
|
||||||
|
: UiColors.textInactive);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@@ -86,21 +94,54 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
|||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// Image Preview
|
// Image Preview (Toggle between example and uploaded)
|
||||||
AttireImagePreview(imageUrl: widget.item.imageUrl),
|
if (hasUploadedPhoto) ...<Widget>[
|
||||||
const SizedBox(height: UiConstants.space6),
|
Text(
|
||||||
|
'Your Uploaded Photo',
|
||||||
|
style: UiTypography.body1b.textPrimary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
AttireImagePreview(imageUrl: currentPhotoUrl),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
Text(
|
||||||
|
'Reference Example',
|
||||||
|
style: UiTypography.body2b.textSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
Center(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
UiConstants.radiusBase,
|
||||||
|
),
|
||||||
|
child: Image.network(
|
||||||
|
widget.item.imageUrl ?? '',
|
||||||
|
height: 120,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (_, __, ___) =>
|
||||||
|
const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else ...<Widget>[
|
||||||
|
AttireImagePreview(
|
||||||
|
imageUrl: widget.item.imageUrl,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
Text(
|
||||||
|
'Example of the item that you need to upload.',
|
||||||
|
style: UiTypography.body1b.textSecondary,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
Text(
|
|
||||||
'Example of the item that you need to upload.',
|
|
||||||
style: UiTypography.body1b.textSecondary,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
widget.item.description ?? '',
|
|
||||||
style: UiTypography.body1r.textSecondary,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
if (widget.item.description != null)
|
||||||
|
Text(
|
||||||
|
widget.item.description!,
|
||||||
|
style: UiTypography.body1r.textSecondary,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space8),
|
||||||
|
|
||||||
// Verification info
|
// Verification info
|
||||||
AttireVerificationStatusCard(
|
AttireVerificationStatusCard(
|
||||||
@@ -118,15 +159,19 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
|||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
|
||||||
if (isUploading)
|
if (isUploading)
|
||||||
const Center(child: CircularProgressIndicator())
|
const Center(
|
||||||
else if (!hasPhoto ||
|
child: Padding(
|
||||||
true) // Show options even if has photo (allows re-upload)
|
padding: EdgeInsets.all(UiConstants.space8),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
AttireUploadButtons(onUpload: _onUpload),
|
AttireUploadButtons(onUpload: _onUpload),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (hasPhoto)
|
if (hasUploadedPhoto)
|
||||||
SafeArea(
|
SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
@@ -135,7 +180,7 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
|||||||
child: UiButton.primary(
|
child: UiButton.primary(
|
||||||
text: 'Submit Image',
|
text: 'Submit Image',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Modular.to.pop(state.photoUrl);
|
Modular.to.pop(currentPhotoUrl);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -80,36 +80,59 @@ class _AttirePageState extends State<AttirePage> {
|
|||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
|
||||||
// Item List
|
// Item List
|
||||||
...filteredOptions.map((AttireItem item) {
|
if (filteredOptions.isEmpty)
|
||||||
return Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.symmetric(
|
||||||
bottom: UiConstants.space3,
|
vertical: UiConstants.space10,
|
||||||
),
|
),
|
||||||
child: AttireItemCard(
|
child: Center(
|
||||||
item: item,
|
child: Column(
|
||||||
isUploading: false,
|
children: <Widget>[
|
||||||
uploadedPhotoUrl: state.photoUrls[item.id],
|
const Icon(
|
||||||
onTap: () async {
|
UiIcons.shirt,
|
||||||
final String? resultUrl =
|
size: 48,
|
||||||
await Navigator.push<String?>(
|
color: UiColors.iconInactive,
|
||||||
context,
|
),
|
||||||
MaterialPageRoute<String?>(
|
const SizedBox(height: UiConstants.space4),
|
||||||
builder: (BuildContext ctx) =>
|
Text(
|
||||||
AttireCapturePage(
|
'No items found for this filter.',
|
||||||
item: item,
|
style: UiTypography.body1m.textSecondary,
|
||||||
initialPhotoUrl:
|
),
|
||||||
state.photoUrls[item.id],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
|
else
|
||||||
|
...filteredOptions.map((AttireItem item) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: UiConstants.space3,
|
||||||
|
),
|
||||||
|
child: AttireItemCard(
|
||||||
|
item: item,
|
||||||
|
isUploading: false,
|
||||||
|
uploadedPhotoUrl: state.photoUrls[item.id],
|
||||||
|
onTap: () async {
|
||||||
|
final String? resultUrl =
|
||||||
|
await Navigator.push<String?>(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute<String?>(
|
||||||
|
builder: (BuildContext ctx) =>
|
||||||
|
AttireCapturePage(
|
||||||
|
item: item,
|
||||||
|
initialPhotoUrl:
|
||||||
|
state.photoUrls[item.id],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (resultUrl != null && mounted) {
|
if (resultUrl != null && mounted) {
|
||||||
cubit.syncCapturedPhoto(item.id, resultUrl);
|
cubit.syncCapturedPhoto(item.id, resultUrl);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
const SizedBox(height: UiConstants.space20),
|
const SizedBox(height: UiConstants.space20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -18,9 +18,8 @@ class AttireItemCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final bool hasPhoto = uploadedPhotoUrl != null;
|
final bool hasPhoto = item.photoUrl != null;
|
||||||
|
final String statusText = item.verificationStatus ?? 'Not Uploaded';
|
||||||
final String statusText = hasPhoto ? 'Pending' : 'Not Uploaded';
|
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
@@ -85,7 +84,9 @@ class AttireItemCard extends StatelessWidget {
|
|||||||
UiChip(
|
UiChip(
|
||||||
label: statusText,
|
label: statusText,
|
||||||
size: UiChipSize.xSmall,
|
size: UiChipSize.xSmall,
|
||||||
variant: UiChipVariant.secondary,
|
variant: item.verificationStatus == 'SUCCESS'
|
||||||
|
? UiChipVariant.primary
|
||||||
|
: UiChipVariant.secondary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -105,9 +106,13 @@ class AttireItemCard extends StatelessWidget {
|
|||||||
size: 24,
|
size: 24,
|
||||||
)
|
)
|
||||||
else if (hasPhoto && !isUploading)
|
else if (hasPhoto && !isUploading)
|
||||||
const Icon(
|
Icon(
|
||||||
UiIcons.check,
|
item.verificationStatus == 'SUCCESS'
|
||||||
color: UiColors.textWarning,
|
? UiIcons.check
|
||||||
|
: UiIcons.clock,
|
||||||
|
color: item.verificationStatus == 'SUCCESS'
|
||||||
|
? UiColors.textPrimary
|
||||||
|
: UiColors.textWarning,
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
14
backend/dataconnect/connector/staffAttire/mutations.gql
Normal file
14
backend/dataconnect/connector/staffAttire/mutations.gql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
mutation upsertStaffAttire(
|
||||||
|
$staffId: UUID!
|
||||||
|
$attireOptionId: UUID!
|
||||||
|
$verificationPhotoUrl: String
|
||||||
|
) @auth(level: USER) {
|
||||||
|
staffAttire_upsert(
|
||||||
|
data: {
|
||||||
|
staffId: $staffId
|
||||||
|
attireOptionId: $attireOptionId
|
||||||
|
verificationPhotoUrl: $verificationPhotoUrl
|
||||||
|
verificationStatus: PENDING
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
7
backend/dataconnect/connector/staffAttire/queries.gql
Normal file
7
backend/dataconnect/connector/staffAttire/queries.gql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
query getStaffAttire($staffId: UUID!) @auth(level: USER) {
|
||||||
|
staffAttires(where: { staffId: { eq: $staffId } }) {
|
||||||
|
attireOptionId
|
||||||
|
verificationStatus
|
||||||
|
verificationPhotoUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1771,7 +1771,6 @@ mutation seedAll @transaction {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
mutation seedAttireOptions @transaction {
|
|
||||||
# Attire Options (Required)
|
# Attire Options (Required)
|
||||||
attire_1: attireOption_insert(
|
attire_1: attireOption_insert(
|
||||||
data: {
|
data: {
|
||||||
@@ -1930,5 +1929,4 @@ mutation seedAll @transaction {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#v.3
|
#v.3
|
||||||
@@ -12,7 +12,7 @@ type StaffAttire @table(name: "staff_attires", key: ["staffId", "attireOptionId"
|
|||||||
attireOption: AttireOption! @ref(fields: "attireOptionId", references: "id")
|
attireOption: AttireOption! @ref(fields: "attireOptionId", references: "id")
|
||||||
|
|
||||||
# Verification Metadata
|
# Verification Metadata
|
||||||
verificationStatus: AttireVerificationStatus @default(expr: "PENDING")
|
verificationStatus: AttireVerificationStatus @default(expr: "'PENDING'")
|
||||||
verifiedAt: Timestamp
|
verifiedAt: Timestamp
|
||||||
verificationPhotoUrl: String # Proof of ownership
|
verificationPhotoUrl: String # Proof of ownership
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user