feat: Implement staff attire management including fetching options, user attire status, and upserting attire details.

This commit is contained in:
Achintha Isuru
2026-02-24 17:16:52 -05:00
parent cb180af7cf
commit 616f23fec9
12 changed files with 310 additions and 165 deletions

View File

@@ -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 {
} }
} }
} }

View File

@@ -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.

View File

@@ -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,
]; ];
} }

View File

@@ -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;
} }
} }

View File

@@ -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 {

View File

@@ -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);
}, },
), ),
), ),

View File

@@ -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),
], ],
), ),

View File

@@ -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,
), ),
], ],

View 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
}
)
}

View File

@@ -0,0 +1,7 @@
query getStaffAttire($staffId: UUID!) @auth(level: USER) {
staffAttires(where: { staffId: { eq: $staffId } }) {
attireOptionId
verificationStatus
verificationPhotoUrl
}
}

View File

@@ -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

View File

@@ -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