Merge branch 'dev' of https://github.com/Oloodi/krow-workforce into feature/session-persistence-new
This commit is contained in:
@@ -59,7 +59,7 @@ class CoreBlocObserver extends BlocObserver {
|
||||
super.onChange(bloc, change);
|
||||
if (logStateChanges) {
|
||||
developer.log(
|
||||
'State: ${change.currentState.runtimeType} → ${change.nextState.runtimeType}',
|
||||
'State: ${change.currentState.runtimeType}’ ${change.nextState.runtimeType}',
|
||||
name: bloc.runtimeType.toString(),
|
||||
);
|
||||
}
|
||||
@@ -109,7 +109,7 @@ class CoreBlocObserver extends BlocObserver {
|
||||
super.onTransition(bloc, transition);
|
||||
if (logStateChanges) {
|
||||
developer.log(
|
||||
'Transition: ${transition.event.runtimeType} → ${transition.nextState.runtimeType}',
|
||||
'Transition: ${transition.event.runtimeType}’ ${transition.nextState.runtimeType}',
|
||||
name: bloc.runtimeType.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ export 'src/connectors/staff/domain/usecases/get_personal_info_completion_usecas
|
||||
export 'src/connectors/staff/domain/usecases/get_emergency_contacts_completion_usecase.dart';
|
||||
export 'src/connectors/staff/domain/usecases/get_experience_completion_usecase.dart';
|
||||
export 'src/connectors/staff/domain/usecases/get_tax_forms_completion_usecase.dart';
|
||||
export 'src/connectors/staff/domain/usecases/get_attire_options_completion_usecase.dart';
|
||||
export 'src/connectors/staff/domain/usecases/get_staff_documents_completion_usecase.dart';
|
||||
export 'src/connectors/staff/domain/usecases/get_staff_certificates_completion_usecase.dart';
|
||||
export 'src/connectors/staff/domain/usecases/get_staff_profile_usecase.dart';
|
||||
export 'src/connectors/staff/domain/usecases/sign_out_staff_usecase.dart';
|
||||
export 'src/connectors/staff/data/repositories/staff_connector_repository_impl.dart';
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
|
||||
import '../../domain/repositories/staff_connector_repository.dart';
|
||||
|
||||
/// Implementation of [StaffConnectorRepository].
|
||||
@@ -33,10 +34,7 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
||||
final dc.GetStaffProfileCompletionStaff? staff = response.data.staff;
|
||||
final List<dc.GetStaffProfileCompletionEmergencyContacts>
|
||||
emergencyContacts = response.data.emergencyContacts;
|
||||
final List<dc.GetStaffProfileCompletionTaxForms> taxForms =
|
||||
response.data.taxForms;
|
||||
|
||||
return _isProfileComplete(staff, emergencyContacts, taxForms);
|
||||
return _isProfileComplete(staff, emergencyContacts);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,7 +105,140 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
||||
.getStaffTaxFormsProfileCompletion(id: staffId)
|
||||
.execute();
|
||||
|
||||
return response.data.taxForms.isNotEmpty;
|
||||
final List<dc.GetStaffTaxFormsProfileCompletionTaxForms> taxForms =
|
||||
response.data.taxForms;
|
||||
|
||||
// Return false if no tax forms exist
|
||||
if (taxForms.isEmpty) return false;
|
||||
|
||||
// Return true only if all tax forms have status == "SUBMITTED"
|
||||
return taxForms.every(
|
||||
(dc.GetStaffTaxFormsProfileCompletionTaxForms form) {
|
||||
if (form.status is dc.Unknown) return false;
|
||||
final dc.TaxFormStatus status =
|
||||
(form.status as dc.Known<dc.TaxFormStatus>).value;
|
||||
return status == dc.TaxFormStatus.SUBMITTED;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool?> getAttireOptionsCompletion() async {
|
||||
return _service.run(() async {
|
||||
final String staffId = await _service.getStaffId();
|
||||
|
||||
final List<QueryResult<Object, Object?>> results =
|
||||
await Future.wait<QueryResult<Object, Object?>>(
|
||||
<Future<QueryResult<Object, Object?>>>[
|
||||
_service.connector.listAttireOptions().execute(),
|
||||
_service.connector.getStaffAttire(staffId: staffId).execute(),
|
||||
],
|
||||
);
|
||||
|
||||
final QueryResult<dc.ListAttireOptionsData, void> optionsRes =
|
||||
results[0] as QueryResult<dc.ListAttireOptionsData, void>;
|
||||
final QueryResult<dc.GetStaffAttireData, dc.GetStaffAttireVariables>
|
||||
staffAttireRes =
|
||||
results[1]
|
||||
as QueryResult<dc.GetStaffAttireData, dc.GetStaffAttireVariables>;
|
||||
|
||||
final List<dc.ListAttireOptionsAttireOptions> attireOptions =
|
||||
optionsRes.data.attireOptions;
|
||||
final List<dc.GetStaffAttireStaffAttires> staffAttire =
|
||||
staffAttireRes.data.staffAttires;
|
||||
|
||||
// Get only mandatory attire options
|
||||
final List<dc.ListAttireOptionsAttireOptions> mandatoryOptions =
|
||||
attireOptions
|
||||
.where((dc.ListAttireOptionsAttireOptions opt) =>
|
||||
opt.isMandatory ?? false)
|
||||
.toList();
|
||||
|
||||
// Return null if no mandatory attire options
|
||||
if (mandatoryOptions.isEmpty) return null;
|
||||
|
||||
// Return true only if all mandatory attire items are verified
|
||||
return mandatoryOptions.every(
|
||||
(dc.ListAttireOptionsAttireOptions mandatoryOpt) {
|
||||
final dc.GetStaffAttireStaffAttires? currentAttire = staffAttire
|
||||
.where(
|
||||
(dc.GetStaffAttireStaffAttires a) =>
|
||||
a.attireOptionId == mandatoryOpt.id,
|
||||
)
|
||||
.firstOrNull;
|
||||
|
||||
if (currentAttire == null) return false; // Not uploaded
|
||||
if (currentAttire.verificationStatus is dc.Unknown) return false;
|
||||
final dc.AttireVerificationStatus status =
|
||||
(currentAttire.verificationStatus
|
||||
as dc.Known<dc.AttireVerificationStatus>)
|
||||
.value;
|
||||
return status == dc.AttireVerificationStatus.APPROVED;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool?> getStaffDocumentsCompletion() async {
|
||||
return _service.run(() async {
|
||||
final String staffId = await _service.getStaffId();
|
||||
|
||||
final QueryResult<
|
||||
dc.ListStaffDocumentsByStaffIdData,
|
||||
dc.ListStaffDocumentsByStaffIdVariables
|
||||
>
|
||||
response = await _service.connector
|
||||
.listStaffDocumentsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
|
||||
final List<dc.ListStaffDocumentsByStaffIdStaffDocuments> staffDocs =
|
||||
response.data.staffDocuments;
|
||||
|
||||
// Return null if no documents
|
||||
if (staffDocs.isEmpty) return null;
|
||||
|
||||
// Return true only if all documents are verified
|
||||
return staffDocs.every(
|
||||
(dc.ListStaffDocumentsByStaffIdStaffDocuments doc) {
|
||||
if (doc.status is dc.Unknown) return false;
|
||||
final dc.DocumentStatus status =
|
||||
(doc.status as dc.Known<dc.DocumentStatus>).value;
|
||||
return status == dc.DocumentStatus.VERIFIED;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool?> getStaffCertificatesCompletion() async {
|
||||
return _service.run(() async {
|
||||
final String staffId = await _service.getStaffId();
|
||||
|
||||
final QueryResult<
|
||||
dc.ListCertificatesByStaffIdData,
|
||||
dc.ListCertificatesByStaffIdVariables
|
||||
>
|
||||
response = await _service.connector
|
||||
.listCertificatesByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
|
||||
final List<dc.ListCertificatesByStaffIdCertificates> certificates =
|
||||
response.data.certificates;
|
||||
|
||||
// Return false if no certificates
|
||||
if (certificates.isEmpty) return null;
|
||||
|
||||
// Return true only if all certificates are fully validated
|
||||
return certificates.every(
|
||||
(dc.ListCertificatesByStaffIdCertificates cert) {
|
||||
if (cert.validationStatus is dc.Unknown) return false;
|
||||
final dc.ValidationStatus status =
|
||||
(cert.validationStatus as dc.Known<dc.ValidationStatus>).value;
|
||||
return status == dc.ValidationStatus.APPROVED;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -134,7 +265,6 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
||||
bool _isProfileComplete(
|
||||
dc.GetStaffProfileCompletionStaff? staff,
|
||||
List<dc.GetStaffProfileCompletionEmergencyContacts> emergencyContacts,
|
||||
List<dc.GetStaffProfileCompletionTaxForms> taxForms,
|
||||
) {
|
||||
if (staff == null) return false;
|
||||
|
||||
@@ -146,7 +276,6 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
||||
return (staff.fullName.trim().isNotEmpty) &&
|
||||
(staff.email?.trim().isNotEmpty ?? false) &&
|
||||
emergencyContacts.isNotEmpty &&
|
||||
taxForms.isNotEmpty &&
|
||||
hasExperience;
|
||||
}
|
||||
|
||||
@@ -199,8 +328,8 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
||||
return response.data.benefitsDatas.map((
|
||||
dc.ListBenefitsDataByStaffIdBenefitsDatas e,
|
||||
) {
|
||||
final total = e.vendorBenefitPlan.total?.toDouble() ?? 0.0;
|
||||
final remaining = e.current.toDouble();
|
||||
final double total = e.vendorBenefitPlan.total?.toDouble() ?? 0.0;
|
||||
final double remaining = e.current.toDouble();
|
||||
return domain.Benefit(
|
||||
title: e.vendorBenefitPlan.title,
|
||||
entitlementHours: total,
|
||||
@@ -346,8 +475,7 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
||||
@override
|
||||
Future<void> signOut() async {
|
||||
try {
|
||||
await _service.auth.signOut();
|
||||
_service.clearCache();
|
||||
await _service.signOut();
|
||||
} catch (e) {
|
||||
throw Exception('Error signing out: ${e.toString()}');
|
||||
}
|
||||
|
||||
@@ -33,6 +33,21 @@ abstract interface class StaffConnectorRepository {
|
||||
/// Returns true if at least one tax form exists.
|
||||
Future<bool> getTaxFormsCompletion();
|
||||
|
||||
/// Fetches attire options completion status.
|
||||
///
|
||||
/// Returns true if all mandatory attire options are verified.
|
||||
Future<bool?> getAttireOptionsCompletion();
|
||||
|
||||
/// Fetches documents completion status.
|
||||
///
|
||||
/// Returns true if all mandatory documents are verified.
|
||||
Future<bool?> getStaffDocumentsCompletion();
|
||||
|
||||
/// Fetches certificates completion status.
|
||||
///
|
||||
/// Returns true if all certificates are validated.
|
||||
Future<bool?> getStaffCertificatesCompletion();
|
||||
|
||||
/// Fetches the full staff profile for the current authenticated user.
|
||||
///
|
||||
/// Returns a [Staff] entity containing all profile information.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import '../repositories/staff_connector_repository.dart';
|
||||
|
||||
/// Use case for retrieving attire options completion status.
|
||||
///
|
||||
/// This use case encapsulates the business logic for determining whether
|
||||
/// a staff member has fully uploaded and verified all mandatory attire options.
|
||||
/// It delegates to the repository for data access.
|
||||
class GetAttireOptionsCompletionUseCase extends NoInputUseCase<bool?> {
|
||||
/// Creates a [GetAttireOptionsCompletionUseCase].
|
||||
///
|
||||
/// Requires a [StaffConnectorRepository] for data access.
|
||||
GetAttireOptionsCompletionUseCase({
|
||||
required StaffConnectorRepository repository,
|
||||
}) : _repository = repository;
|
||||
|
||||
final StaffConnectorRepository _repository;
|
||||
|
||||
/// Executes the use case to get attire options completion status.
|
||||
///
|
||||
/// Returns true if all mandatory attire options are verified, false otherwise.
|
||||
///
|
||||
/// Throws an exception if the operation fails.
|
||||
@override
|
||||
Future<bool?> call() => _repository.getAttireOptionsCompletion();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import '../repositories/staff_connector_repository.dart';
|
||||
|
||||
/// Use case for retrieving certificates completion status.
|
||||
///
|
||||
/// This use case encapsulates the business logic for determining whether
|
||||
/// a staff member has fully validated all certificates.
|
||||
/// It delegates to the repository for data access.
|
||||
class GetStaffCertificatesCompletionUseCase extends NoInputUseCase<bool?> {
|
||||
/// Creates a [GetStaffCertificatesCompletionUseCase].
|
||||
///
|
||||
/// Requires a [StaffConnectorRepository] for data access.
|
||||
GetStaffCertificatesCompletionUseCase({
|
||||
required StaffConnectorRepository repository,
|
||||
}) : _repository = repository;
|
||||
|
||||
final StaffConnectorRepository _repository;
|
||||
|
||||
/// Executes the use case to get certificates completion status.
|
||||
///
|
||||
/// Returns true if all certificates are validated, false otherwise.
|
||||
///
|
||||
/// Throws an exception if the operation fails.
|
||||
@override
|
||||
Future<bool?> call() => _repository.getStaffCertificatesCompletion();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import '../repositories/staff_connector_repository.dart';
|
||||
|
||||
/// Use case for retrieving documents completion status.
|
||||
///
|
||||
/// This use case encapsulates the business logic for determining whether
|
||||
/// a staff member has fully uploaded and verified all mandatory documents.
|
||||
/// It delegates to the repository for data access.
|
||||
class GetStaffDocumentsCompletionUseCase extends NoInputUseCase<bool?> {
|
||||
/// Creates a [GetStaffDocumentsCompletionUseCase].
|
||||
///
|
||||
/// Requires a [StaffConnectorRepository] for data access.
|
||||
GetStaffDocumentsCompletionUseCase({
|
||||
required StaffConnectorRepository repository,
|
||||
}) : _repository = repository;
|
||||
|
||||
final StaffConnectorRepository _repository;
|
||||
|
||||
/// Executes the use case to get documents completion status.
|
||||
///
|
||||
/// Returns true if all mandatory documents are verified, false otherwise.
|
||||
///
|
||||
/// Throws an exception if the operation fails.
|
||||
@override
|
||||
Future<bool?> call() => _repository.getStaffDocumentsCompletion();
|
||||
}
|
||||
@@ -224,8 +224,19 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Signs out the current user from Firebase Auth and clears all session data.
|
||||
Future<void> signOut() async {
|
||||
try {
|
||||
await auth.signOut();
|
||||
_clearCache();
|
||||
} catch (e) {
|
||||
debugPrint('DataConnectService: Error signing out: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears Cached Repositories and Session data.
|
||||
void clearCache() {
|
||||
void _clearCache() {
|
||||
_reportsRepository = null;
|
||||
_shiftsRepository = null;
|
||||
_hubsRepository = null;
|
||||
|
||||
@@ -338,8 +338,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
@override
|
||||
Future<void> signOut() async {
|
||||
try {
|
||||
await _service.auth.signOut();
|
||||
_service.clearCache();
|
||||
await _service.signOut();
|
||||
} catch (e) {
|
||||
throw Exception('Error signing out: ${e.toString()}');
|
||||
}
|
||||
@@ -371,9 +370,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
if (requireBusinessRole &&
|
||||
user.userRole != 'BUSINESS' &&
|
||||
user.userRole != 'BOTH') {
|
||||
await _service.auth.signOut();
|
||||
dc.ClientSessionStore.instance.clear();
|
||||
_service.clearCache();
|
||||
await _service.signOut();
|
||||
throw UnauthorizedAppException(
|
||||
technicalMessage:
|
||||
'User role is ${user.userRole}, expected BUSINESS or BOTH',
|
||||
|
||||
@@ -44,7 +44,6 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
// ── Header ──────────────────────────────────────────
|
||||
Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 60,
|
||||
@@ -151,7 +150,6 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// ── Content ─────────────────────────────────────────
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -16),
|
||||
child: Padding(
|
||||
@@ -241,7 +239,7 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Summary chip (top 3 stats) ───────────────────────────────────────────────
|
||||
// Summary chip (top 3 stats)
|
||||
class _SummaryChip extends StatelessWidget {
|
||||
|
||||
const _SummaryChip({
|
||||
@@ -305,7 +303,7 @@ class _SummaryChip extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Worker card with risk badge + latest incident ────────────────────────────
|
||||
// ” Worker card with risk badge + latest incident ””””””””””””””
|
||||
class _WorkerCard extends StatelessWidget {
|
||||
|
||||
const _WorkerCard({required this.worker});
|
||||
@@ -448,5 +446,5 @@ class _WorkerCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Insight line ─────────────────────────────────────────────────────────────
|
||||
// Insight line
|
||||
|
||||
|
||||
@@ -40,11 +40,11 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
||||
if (state is PerformanceLoaded) {
|
||||
final PerformanceReport report = state.report;
|
||||
|
||||
// Compute overall score (0–100) from the 4 KPIs
|
||||
// Compute overall score (0 - 100) from the 4 KPIs
|
||||
final double overallScore = ((report.fillRate * 0.3) +
|
||||
(report.completionRate * 0.3) +
|
||||
(report.onTimeRate * 0.25) +
|
||||
// avg fill time: 3h target → invert to score
|
||||
// avg fill time: 3h target invert to score
|
||||
((report.avgFillTimeHours <= 3
|
||||
? 100
|
||||
: (3 / report.avgFillTimeHours) * 100) *
|
||||
@@ -107,7 +107,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
||||
iconColor: const Color(0xFFF39C12),
|
||||
label: context.t.client_reports.performance_report.kpis.avg_fill_time,
|
||||
target: context.t.client_reports.performance_report.kpis.target_hours(hours: '3'),
|
||||
// invert: lower is better — show as % of target met
|
||||
// invert: lower is better show as % of target met
|
||||
value: report.avgFillTimeHours == 0
|
||||
? 100
|
||||
: (3 / report.avgFillTimeHours * 100).clamp(0, 100),
|
||||
@@ -122,7 +122,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
// ── Header ───────────────────────────────────────────
|
||||
// Header
|
||||
Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 60,
|
||||
@@ -225,14 +225,14 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// ── Content ──────────────────────────────────────────
|
||||
// ” Content ”””””””””””””””””””””
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
// ── Overall Score Hero Card ───────────────────
|
||||
// Overall Score Hero Card
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -299,7 +299,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── KPI List ─────────────────────────────────
|
||||
// KPI List
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
@@ -349,7 +349,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── KPI data model ────────────────────────────────────────────────────────────
|
||||
// ” KPI data model ””””””””””””””””””””””””””””””
|
||||
class _KpiData {
|
||||
|
||||
const _KpiData({
|
||||
@@ -367,14 +367,14 @@ class _KpiData {
|
||||
final Color iconColor;
|
||||
final String label;
|
||||
final String target;
|
||||
final double value; // 0–100 for bar
|
||||
final double value; // 0-100 for bar
|
||||
final String displayValue;
|
||||
final Color barColor;
|
||||
final bool met;
|
||||
final bool close;
|
||||
}
|
||||
|
||||
// ── KPI row widget ────────────────────────────────────────────────────────────
|
||||
// ” KPI row widget ””””””””””””””””””””””””””””””
|
||||
class _KpiRow extends StatelessWidget {
|
||||
|
||||
const _KpiRow({required this.kpi});
|
||||
|
||||
@@ -15,7 +15,7 @@ class SettingsRepositoryImpl implements SettingsRepositoryInterface {
|
||||
@override
|
||||
Future<void> signOut() async {
|
||||
return _service.run(() async {
|
||||
await _service.auth.signOut();
|
||||
await _service.signOut();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,10 +96,8 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
|
||||
/// Signs out the current user.
|
||||
@override
|
||||
Future<void> signOut() {
|
||||
StaffSessionStore.instance.clear();
|
||||
_service.clearCache();
|
||||
return _service.auth.signOut();
|
||||
Future<void> signOut() async {
|
||||
return await _service.signOut();
|
||||
}
|
||||
|
||||
/// Verifies an OTP code and returns the authenticated user.
|
||||
@@ -163,7 +161,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
|
||||
if (staffResponse.data.staffs.isNotEmpty) {
|
||||
// If profile exists, they should use Login mode.
|
||||
await _service.auth.signOut();
|
||||
await _service.signOut();
|
||||
throw const domain.AccountExistsException(
|
||||
technicalMessage:
|
||||
'This user already has a staff profile. Please log in.',
|
||||
@@ -185,14 +183,14 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
}
|
||||
} else {
|
||||
if (user == null) {
|
||||
await _service.auth.signOut();
|
||||
await _service.signOut();
|
||||
throw const domain.UserNotFoundException(
|
||||
technicalMessage: 'Authenticated user profile not found in database.',
|
||||
);
|
||||
}
|
||||
// Allow STAFF or BOTH roles to log in to the Staff App
|
||||
if (user.userRole != 'STAFF' && user.userRole != 'BOTH') {
|
||||
await _service.auth.signOut();
|
||||
await _service.signOut();
|
||||
throw const domain.UnauthorizedAppException(
|
||||
technicalMessage: 'User is not authorized for this app.',
|
||||
);
|
||||
@@ -206,7 +204,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
requiresAuthentication: false,
|
||||
);
|
||||
if (staffResponse.data.staffs.isEmpty) {
|
||||
await _service.auth.signOut();
|
||||
await _service.signOut();
|
||||
throw const domain.UserNotFoundException(
|
||||
technicalMessage:
|
||||
'Your account is not registered yet. Please register first.',
|
||||
|
||||
@@ -180,7 +180,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
style: UiTypography.body2b,
|
||||
),
|
||||
Text(
|
||||
"${shift.clientName} • ${shift.location}",
|
||||
"${shift.clientName} ${shift.location}",
|
||||
style: UiTypography
|
||||
.body3r
|
||||
.textSecondary,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:staff_home/src/presentation/blocs/home_cubit.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
/// Page displaying a detailed overview of the worker's benefits.
|
||||
class BenefitsOverviewPage extends StatelessWidget {
|
||||
|
||||
@@ -19,6 +19,9 @@ class ProfileCubit extends Cubit<ProfileState>
|
||||
this._getEmergencyContactsCompletionUseCase,
|
||||
this._getExperienceCompletionUseCase,
|
||||
this._getTaxFormsCompletionUseCase,
|
||||
this._getAttireOptionsCompletionUseCase,
|
||||
this._getStaffDocumentsCompletionUseCase,
|
||||
this._getStaffCertificatesCompletionUseCase,
|
||||
) : super(const ProfileState());
|
||||
final GetStaffProfileUseCase _getProfileUseCase;
|
||||
final SignOutStaffUseCase _signOutUseCase;
|
||||
@@ -26,6 +29,9 @@ class ProfileCubit extends Cubit<ProfileState>
|
||||
final GetEmergencyContactsCompletionUseCase _getEmergencyContactsCompletionUseCase;
|
||||
final GetExperienceCompletionUseCase _getExperienceCompletionUseCase;
|
||||
final GetTaxFormsCompletionUseCase _getTaxFormsCompletionUseCase;
|
||||
final GetAttireOptionsCompletionUseCase _getAttireOptionsCompletionUseCase;
|
||||
final GetStaffDocumentsCompletionUseCase _getStaffDocumentsCompletionUseCase;
|
||||
final GetStaffCertificatesCompletionUseCase _getStaffCertificatesCompletionUseCase;
|
||||
|
||||
/// Loads the staff member's profile.
|
||||
///
|
||||
@@ -130,5 +136,47 @@ class ProfileCubit extends Cubit<ProfileState>
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads attire options completion status.
|
||||
Future<void> loadAttireCompletion() async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final bool? isComplete = await _getAttireOptionsCompletionUseCase();
|
||||
emit(state.copyWith(attireComplete: isComplete));
|
||||
},
|
||||
onError: (String _) {
|
||||
return state.copyWith(attireComplete: false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads documents completion status.
|
||||
Future<void> loadDocumentsCompletion() async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final bool? isComplete = await _getStaffDocumentsCompletionUseCase();
|
||||
emit(state.copyWith(documentsComplete: isComplete));
|
||||
},
|
||||
onError: (String _) {
|
||||
return state.copyWith(documentsComplete: false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads certificates completion status.
|
||||
Future<void> loadCertificatesCompletion() async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final bool? isComplete = await _getStaffCertificatesCompletionUseCase();
|
||||
emit(state.copyWith(certificatesComplete: isComplete));
|
||||
},
|
||||
onError: (String _) {
|
||||
return state.copyWith(certificatesComplete: false);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ class ProfileState extends Equatable {
|
||||
this.emergencyContactsComplete,
|
||||
this.experienceComplete,
|
||||
this.taxFormsComplete,
|
||||
this.attireComplete,
|
||||
this.documentsComplete,
|
||||
this.certificatesComplete,
|
||||
});
|
||||
/// Current status of the profile feature
|
||||
final ProfileStatus status;
|
||||
@@ -54,6 +57,15 @@ class ProfileState extends Equatable {
|
||||
|
||||
/// Whether tax forms are complete
|
||||
final bool? taxFormsComplete;
|
||||
|
||||
/// Whether attire options are complete
|
||||
final bool? attireComplete;
|
||||
|
||||
/// Whether documents are complete
|
||||
final bool? documentsComplete;
|
||||
|
||||
/// Whether certificates are complete
|
||||
final bool? certificatesComplete;
|
||||
|
||||
/// Creates a copy of this state with updated values.
|
||||
ProfileState copyWith({
|
||||
@@ -64,6 +76,9 @@ class ProfileState extends Equatable {
|
||||
bool? emergencyContactsComplete,
|
||||
bool? experienceComplete,
|
||||
bool? taxFormsComplete,
|
||||
bool? attireComplete,
|
||||
bool? documentsComplete,
|
||||
bool? certificatesComplete,
|
||||
}) {
|
||||
return ProfileState(
|
||||
status: status ?? this.status,
|
||||
@@ -73,6 +88,9 @@ class ProfileState extends Equatable {
|
||||
emergencyContactsComplete: emergencyContactsComplete ?? this.emergencyContactsComplete,
|
||||
experienceComplete: experienceComplete ?? this.experienceComplete,
|
||||
taxFormsComplete: taxFormsComplete ?? this.taxFormsComplete,
|
||||
attireComplete: attireComplete ?? this.attireComplete,
|
||||
documentsComplete: documentsComplete ?? this.documentsComplete,
|
||||
certificatesComplete: certificatesComplete ?? this.certificatesComplete,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,5 +103,8 @@ class ProfileState extends Equatable {
|
||||
emergencyContactsComplete,
|
||||
experienceComplete,
|
||||
taxFormsComplete,
|
||||
attireComplete,
|
||||
documentsComplete,
|
||||
certificatesComplete,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ class StaffProfilePage extends StatelessWidget {
|
||||
cubit.loadEmergencyContactsCompletion();
|
||||
cubit.loadExperienceCompletion();
|
||||
cubit.loadTaxFormsCompletion();
|
||||
cubit.loadAttireCompletion();
|
||||
cubit.loadDocumentsCompletion();
|
||||
cubit.loadCertificatesCompletion();
|
||||
}
|
||||
|
||||
if (state.status == ProfileStatus.signedOut) {
|
||||
|
||||
@@ -43,11 +43,13 @@ class ComplianceSection extends StatelessWidget {
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.file,
|
||||
label: i18n.menu_items.documents,
|
||||
completed: state.documentsComplete,
|
||||
onTap: () => Modular.to.toDocuments(),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.certificate,
|
||||
label: i18n.menu_items.certificates,
|
||||
completed: state.certificatesComplete,
|
||||
onTap: () => Modular.to.toCertificates(),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -54,6 +54,7 @@ class OnboardingSection extends StatelessWidget {
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.shirt,
|
||||
label: i18n.menu_items.attire,
|
||||
completed: state.attireComplete,
|
||||
onTap: () => Modular.to.toAttire(),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -50,6 +50,21 @@ class StaffProfileModule extends Module {
|
||||
repository: i.get<StaffConnectorRepository>(),
|
||||
),
|
||||
);
|
||||
i.addLazySingleton<GetAttireOptionsCompletionUseCase>(
|
||||
() => GetAttireOptionsCompletionUseCase(
|
||||
repository: i.get<StaffConnectorRepository>(),
|
||||
),
|
||||
);
|
||||
i.addLazySingleton<GetStaffDocumentsCompletionUseCase>(
|
||||
() => GetStaffDocumentsCompletionUseCase(
|
||||
repository: i.get<StaffConnectorRepository>(),
|
||||
),
|
||||
);
|
||||
i.addLazySingleton<GetStaffCertificatesCompletionUseCase>(
|
||||
() => GetStaffCertificatesCompletionUseCase(
|
||||
repository: i.get<StaffConnectorRepository>(),
|
||||
),
|
||||
);
|
||||
|
||||
// Presentation layer - Cubit as singleton to avoid recreation
|
||||
// BlocProvider will use this same instance, preventing state emission after close
|
||||
@@ -61,6 +76,9 @@ class StaffProfileModule extends Module {
|
||||
i.get<GetEmergencyContactsCompletionUseCase>(),
|
||||
i.get<GetExperienceCompletionUseCase>(),
|
||||
i.get<GetTaxFormsCompletionUseCase>(),
|
||||
i.get<GetAttireOptionsCompletionUseCase>(),
|
||||
i.get<GetStaffDocumentsCompletionUseCase>(),
|
||||
i.get<GetStaffCertificatesCompletionUseCase>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,14 @@ class TaxFormMapper {
|
||||
String subtitle = '';
|
||||
String description = '';
|
||||
|
||||
if (form.formType == dc.TaxFormType.I9) {
|
||||
final dc.TaxFormType formType;
|
||||
if (form.formType is dc.Known<dc.TaxFormType>) {
|
||||
formType = (form.formType as dc.Known<dc.TaxFormType>).value;
|
||||
} else {
|
||||
formType = dc.TaxFormType.W4;
|
||||
}
|
||||
|
||||
if (formType == dc.TaxFormType.I9) {
|
||||
title = 'Form I-9';
|
||||
subtitle = 'Employment Eligibility Verification';
|
||||
description = 'Required for all new hires to verify identity.';
|
||||
|
||||
@@ -7,8 +7,7 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/tax_forms_repository.dart';
|
||||
import '../mappers/tax_form_mapper.dart';
|
||||
|
||||
class TaxFormsRepositoryImpl
|
||||
implements TaxFormsRepository {
|
||||
class TaxFormsRepositoryImpl implements TaxFormsRepository {
|
||||
TaxFormsRepositoryImpl() : _service = dc.DataConnectService.instance;
|
||||
|
||||
final dc.DataConnectService _service;
|
||||
@@ -17,16 +16,22 @@ class TaxFormsRepositoryImpl
|
||||
Future<List<TaxForm>> getTaxForms() async {
|
||||
return _service.run(() async {
|
||||
final String staffId = await _service.getStaffId();
|
||||
final QueryResult<dc.GetTaxFormsByStaffIdData, dc.GetTaxFormsByStaffIdVariables> response = await _service.connector
|
||||
.getTaxFormsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
final QueryResult<
|
||||
dc.GetTaxFormsByStaffIdData,
|
||||
dc.GetTaxFormsByStaffIdVariables
|
||||
>
|
||||
response = await _service.connector
|
||||
.getTaxFormsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
|
||||
final List<TaxForm> forms =
|
||||
response.data.taxForms.map(TaxFormMapper.fromDataConnect).toList();
|
||||
final List<TaxForm> forms = response.data.taxForms
|
||||
.map(TaxFormMapper.fromDataConnect)
|
||||
.toList();
|
||||
|
||||
// Check if required forms exist, create if not.
|
||||
final Set<TaxFormType> typesPresent =
|
||||
forms.map((TaxForm f) => f.type).toSet();
|
||||
final Set<TaxFormType> typesPresent = forms
|
||||
.map((TaxForm f) => f.type)
|
||||
.toSet();
|
||||
bool createdNew = false;
|
||||
|
||||
if (!typesPresent.contains(TaxFormType.i9)) {
|
||||
@@ -39,8 +44,13 @@ class TaxFormsRepositoryImpl
|
||||
}
|
||||
|
||||
if (createdNew) {
|
||||
final QueryResult<dc.GetTaxFormsByStaffIdData, dc.GetTaxFormsByStaffIdVariables> response2 =
|
||||
await _service.connector.getTaxFormsByStaffId(staffId: staffId).execute();
|
||||
final QueryResult<
|
||||
dc.GetTaxFormsByStaffIdData,
|
||||
dc.GetTaxFormsByStaffIdVariables
|
||||
>
|
||||
response2 = await _service.connector
|
||||
.getTaxFormsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
return response2.data.taxForms
|
||||
.map(TaxFormMapper.fromDataConnect)
|
||||
.toList();
|
||||
@@ -54,8 +64,9 @@ class TaxFormsRepositoryImpl
|
||||
await _service.connector
|
||||
.createTaxForm(
|
||||
staffId: staffId,
|
||||
formType:
|
||||
dc.TaxFormType.values.byName(TaxFormAdapter.typeToString(type)),
|
||||
formType: dc.TaxFormType.values.byName(
|
||||
TaxFormAdapter.typeToString(type),
|
||||
),
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
socialSN: 0,
|
||||
@@ -69,8 +80,8 @@ class TaxFormsRepositoryImpl
|
||||
Future<void> updateI9Form(I9TaxForm form) async {
|
||||
return _service.run(() async {
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
final dc.UpdateTaxFormVariablesBuilder builder =
|
||||
_service.connector.updateTaxForm(id: form.id);
|
||||
final dc.UpdateTaxFormVariablesBuilder builder = _service.connector
|
||||
.updateTaxForm(id: form.id);
|
||||
_mapCommonFields(builder, data);
|
||||
_mapI9Fields(builder, data);
|
||||
await builder.execute();
|
||||
@@ -81,8 +92,8 @@ class TaxFormsRepositoryImpl
|
||||
Future<void> submitI9Form(I9TaxForm form) async {
|
||||
return _service.run(() async {
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
final dc.UpdateTaxFormVariablesBuilder builder =
|
||||
_service.connector.updateTaxForm(id: form.id);
|
||||
final dc.UpdateTaxFormVariablesBuilder builder = _service.connector
|
||||
.updateTaxForm(id: form.id);
|
||||
_mapCommonFields(builder, data);
|
||||
_mapI9Fields(builder, data);
|
||||
await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
|
||||
@@ -93,8 +104,8 @@ class TaxFormsRepositoryImpl
|
||||
Future<void> updateW4Form(W4TaxForm form) async {
|
||||
return _service.run(() async {
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
final dc.UpdateTaxFormVariablesBuilder builder =
|
||||
_service.connector.updateTaxForm(id: form.id);
|
||||
final dc.UpdateTaxFormVariablesBuilder builder = _service.connector
|
||||
.updateTaxForm(id: form.id);
|
||||
_mapCommonFields(builder, data);
|
||||
_mapW4Fields(builder, data);
|
||||
await builder.execute();
|
||||
@@ -105,8 +116,8 @@ class TaxFormsRepositoryImpl
|
||||
Future<void> submitW4Form(W4TaxForm form) async {
|
||||
return _service.run(() async {
|
||||
final Map<String, dynamic> data = form.formData;
|
||||
final dc.UpdateTaxFormVariablesBuilder builder =
|
||||
_service.connector.updateTaxForm(id: form.id);
|
||||
final dc.UpdateTaxFormVariablesBuilder builder = _service.connector
|
||||
.updateTaxForm(id: form.id);
|
||||
_mapCommonFields(builder, data);
|
||||
_mapW4Fields(builder, data);
|
||||
await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
|
||||
@@ -114,7 +125,9 @@ class TaxFormsRepositoryImpl
|
||||
}
|
||||
|
||||
void _mapCommonFields(
|
||||
dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
|
||||
dc.UpdateTaxFormVariablesBuilder builder,
|
||||
Map<String, dynamic> data,
|
||||
) {
|
||||
if (data.containsKey('firstName')) {
|
||||
builder.firstName(data['firstName'] as String?);
|
||||
}
|
||||
@@ -154,8 +167,8 @@ class TaxFormsRepositoryImpl
|
||||
}
|
||||
if (data.containsKey('ssn') && data['ssn']?.toString().isNotEmpty == true) {
|
||||
builder.socialSN(
|
||||
int.tryParse(data['ssn'].toString().replaceAll(RegExp(r'\D'), '')) ??
|
||||
0);
|
||||
int.tryParse(data['ssn'].toString().replaceAll(RegExp(r'\D'), '')) ?? 0,
|
||||
);
|
||||
}
|
||||
if (data.containsKey('email')) builder.email(data['email'] as String?);
|
||||
if (data.containsKey('phone')) builder.phone(data['phone'] as String?);
|
||||
@@ -173,14 +186,17 @@ class TaxFormsRepositoryImpl
|
||||
}
|
||||
|
||||
void _mapI9Fields(
|
||||
dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
|
||||
dc.UpdateTaxFormVariablesBuilder builder,
|
||||
Map<String, dynamic> data,
|
||||
) {
|
||||
if (data.containsKey('citizenshipStatus')) {
|
||||
final String status = data['citizenshipStatus'] as String;
|
||||
// Map string to enum if possible, or handle otherwise.
|
||||
// Generated enum: CITIZEN, NONCITIZEN_NATIONAL, PERMANENT_RESIDENT, ALIEN_AUTHORIZED
|
||||
try {
|
||||
builder.citizen(
|
||||
dc.CitizenshipStatus.values.byName(status.toUpperCase()));
|
||||
dc.CitizenshipStatus.values.byName(status.toUpperCase()),
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
if (data.containsKey('uscisNumber')) {
|
||||
@@ -202,7 +218,9 @@ class TaxFormsRepositoryImpl
|
||||
}
|
||||
|
||||
void _mapW4Fields(
|
||||
dc.UpdateTaxFormVariablesBuilder builder, Map<String, dynamic> data) {
|
||||
dc.UpdateTaxFormVariablesBuilder builder,
|
||||
Map<String, dynamic> data,
|
||||
) {
|
||||
if (data.containsKey('cityStateZip')) {
|
||||
final String csz = data['cityStateZip'] as String;
|
||||
// Extremely basic split: City, State Zip
|
||||
@@ -222,10 +240,11 @@ class TaxFormsRepositoryImpl
|
||||
// Simple mapping assumptions:
|
||||
if (status.contains('single')) {
|
||||
builder.marital(dc.MaritalStatus.SINGLE);
|
||||
} else if (status.contains('married'))
|
||||
} else if (status.contains('married')) {
|
||||
builder.marital(dc.MaritalStatus.MARRIED);
|
||||
else if (status.contains('head'))
|
||||
} else if (status.contains('head')) {
|
||||
builder.marital(dc.MaritalStatus.HEAD);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
if (data.containsKey('multipleJobs')) {
|
||||
@@ -245,11 +264,11 @@ class TaxFormsRepositoryImpl
|
||||
}
|
||||
if (data.containsKey('extraWithholding')) {
|
||||
builder.extraWithholding(
|
||||
double.tryParse(data['extraWithholding'].toString()));
|
||||
double.tryParse(data['extraWithholding'].toString()),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('signature')) {
|
||||
builder.signature(data['signature'] as String?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../blocs/tax_forms/tax_forms_cubit.dart';
|
||||
import '../blocs/tax_forms/tax_forms_state.dart';
|
||||
import '../widgets/tax_forms_page/index.dart';
|
||||
|
||||
class TaxFormsPage extends StatelessWidget {
|
||||
const TaxFormsPage({super.key});
|
||||
@@ -57,11 +58,16 @@ class TaxFormsPage extends StatelessWidget {
|
||||
child: Column(
|
||||
spacing: UiConstants.space4,
|
||||
children: <Widget>[
|
||||
_buildProgressOverview(state.forms),
|
||||
...state.forms.map(
|
||||
(TaxForm form) => _buildFormCard(context, form),
|
||||
const TaxFormsInfoCard(),
|
||||
TaxFormsProgressOverview(forms: state.forms),
|
||||
Column(
|
||||
spacing: UiConstants.space2,
|
||||
children: <Widget>[
|
||||
...state.forms.map(
|
||||
(TaxForm form) => _buildFormCard(context, form),
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildInfoCard(),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -71,56 +77,9 @@ class TaxFormsPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressOverview(List<TaxForm> forms) {
|
||||
final int completedCount = forms
|
||||
.where(
|
||||
(TaxForm f) =>
|
||||
f.status == TaxFormStatus.submitted ||
|
||||
f.status == TaxFormStatus.approved,
|
||||
)
|
||||
.length;
|
||||
final int totalCount = forms.length;
|
||||
final double progress = totalCount > 0 ? completedCount / totalCount : 0.0;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text('Document Progress', style: UiTypography.body2m.textPrimary),
|
||||
Text(
|
||||
'$completedCount/$totalCount',
|
||||
style: UiTypography.body2m.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
ClipRRect(
|
||||
borderRadius: UiConstants.radiusSm,
|
||||
child: LinearProgressIndicator(
|
||||
value: progress,
|
||||
minHeight: 8,
|
||||
backgroundColor: UiColors.background,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(UiColors.primary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormCard(BuildContext context, TaxForm form) {
|
||||
// Helper to get icon based on type (could be in entity or a mapper)
|
||||
final String icon = form is I9TaxForm ? '🛂' : '📋';
|
||||
|
||||
return GestureDetector(
|
||||
return TaxFormCard(
|
||||
form: form,
|
||||
onTap: () async {
|
||||
if (form is I9TaxForm) {
|
||||
final Object? result = await Modular.to.pushNamed(
|
||||
@@ -140,161 +99,6 @@ class TaxFormsPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Center(child: Text(icon, style: UiTypography.headline1m)),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
form.title,
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
_buildStatusBadge(form.status),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
form.subtitle ?? '',
|
||||
style: UiTypography.body2m.textSecondary.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
form.description ?? '',
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
const Icon(
|
||||
UiIcons.chevronRight,
|
||||
color: UiColors.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusBadge(TaxFormStatus status) {
|
||||
switch (status) {
|
||||
case TaxFormStatus.submitted:
|
||||
case TaxFormStatus.approved:
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space2,
|
||||
vertical: UiConstants.space1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagSuccess,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.success,
|
||||
size: 12,
|
||||
color: UiColors.textSuccess,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Text('Completed', style: UiTypography.footnote2b.textSuccess),
|
||||
],
|
||||
),
|
||||
);
|
||||
case TaxFormStatus.inProgress:
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space2,
|
||||
vertical: UiConstants.space1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagPending,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.clock, size: 12, color: UiColors.textWarning),
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Text('In Progress', style: UiTypography.footnote2b.textWarning),
|
||||
],
|
||||
),
|
||||
);
|
||||
default:
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space2,
|
||||
vertical: UiConstants.space1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagValue,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Text(
|
||||
'Not Started',
|
||||
style: UiTypography.footnote2b.textSecondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildInfoCard() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgSecondary,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.file, color: UiColors.primary, size: 20),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Why are these needed?',
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
'I-9 and W-4 forms are required by federal law to verify your employment eligibility and set up correct tax withholding.',
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export 'progress_overview.dart';
|
||||
export 'tax_form_card.dart';
|
||||
export 'tax_form_status_badge.dart';
|
||||
export 'tax_forms_info_card.dart';
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Widget displaying the overall progress of tax form completion.
|
||||
class TaxFormsProgressOverview extends StatelessWidget {
|
||||
const TaxFormsProgressOverview({required this.forms});
|
||||
|
||||
final List<TaxForm> forms;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final int completedCount = forms
|
||||
.where(
|
||||
(TaxForm f) =>
|
||||
f.status == TaxFormStatus.submitted ||
|
||||
f.status == TaxFormStatus.approved,
|
||||
)
|
||||
.length;
|
||||
final int totalCount = forms.length;
|
||||
final double progress = totalCount > 0 ? completedCount / totalCount : 0.0;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text('Document Progress', style: UiTypography.body2m.textPrimary),
|
||||
Text(
|
||||
'$completedCount/$totalCount',
|
||||
style: UiTypography.body2m.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
ClipRRect(
|
||||
borderRadius: UiConstants.radiusSm,
|
||||
child: LinearProgressIndicator(
|
||||
value: progress,
|
||||
minHeight: 8,
|
||||
backgroundColor: UiColors.background,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(UiColors.primary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'tax_form_status_badge.dart';
|
||||
|
||||
/// Widget displaying a single tax form card with information and navigation.
|
||||
class TaxFormCard extends StatelessWidget {
|
||||
const TaxFormCard({super.key, required this.form, required this.onTap});
|
||||
|
||||
final TaxForm form;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Helper to get icon based on type
|
||||
final String icon = form is I9TaxForm ? '🛂' : '📋';
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border, width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Center(child: Text(icon, style: UiTypography.headline1m)),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
form.title,
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
TaxFormStatusBadge(status: form.status),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
form.subtitle ?? '',
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
Text(
|
||||
form.description ?? '',
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
const Icon(
|
||||
UiIcons.chevronRight,
|
||||
color: UiColors.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Widget displaying status badge for a tax form.
|
||||
class TaxFormStatusBadge extends StatelessWidget {
|
||||
const TaxFormStatusBadge({required this.status});
|
||||
|
||||
final TaxFormStatus status;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch (status) {
|
||||
case TaxFormStatus.submitted:
|
||||
case TaxFormStatus.approved:
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space2,
|
||||
vertical: UiConstants.space1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagSuccess,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.success,
|
||||
size: 12,
|
||||
color: UiColors.textSuccess,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Text('Completed', style: UiTypography.footnote2b.textSuccess),
|
||||
],
|
||||
),
|
||||
);
|
||||
case TaxFormStatus.inProgress:
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space2,
|
||||
vertical: UiConstants.space1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagPending,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.clock, size: 12, color: UiColors.textWarning),
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Text('In Progress', style: UiTypography.footnote2b.textWarning),
|
||||
],
|
||||
),
|
||||
);
|
||||
default:
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space2,
|
||||
vertical: UiConstants.space1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagValue,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Text(
|
||||
'Not Started',
|
||||
style: UiTypography.footnote2b.textSecondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Information card explaining why tax forms are required.
|
||||
class TaxFormsInfoCard extends StatelessWidget {
|
||||
const TaxFormsInfoCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const UiNoticeBanner(
|
||||
title: 'Why are these needed?',
|
||||
description:
|
||||
'I-9 and W-4 forms are required by federal law to verify your employment eligibility and set up correct tax withholding.',
|
||||
icon: UiIcons.file,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -298,17 +298,11 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
currentPhotoUrl: currentPhotoUrl,
|
||||
referenceImageUrl: widget.item.imageUrl,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
InfoSection(
|
||||
description: widget.item.description,
|
||||
statusText: _getStatusText(hasUploadedPhoto),
|
||||
statusColor: _getStatusColor(hasUploadedPhoto),
|
||||
isPending: _isPending,
|
||||
showCheckbox: !_hasVerificationStatus,
|
||||
isAttested: state.isAttested,
|
||||
onAttestationChanged: (bool? val) {
|
||||
cubit.toggleAttestation(val ?? false);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -321,6 +315,11 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
hasVerificationStatus: _hasVerificationStatus,
|
||||
hasUploadedPhoto: hasUploadedPhoto,
|
||||
updatedItem: state.updatedItem,
|
||||
showCheckbox: !_hasVerificationStatus,
|
||||
isAttested: state.isAttested,
|
||||
onAttestationChanged: (bool? val) {
|
||||
cubit.toggleAttestation(val ?? false);
|
||||
},
|
||||
onGallery: () => _onGallery(context),
|
||||
onCamera: () => _onCamera(context),
|
||||
onSubmit: () => _onSubmit(context),
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
class AttestationCheckbox extends StatelessWidget {
|
||||
|
||||
const AttestationCheckbox({
|
||||
super.key,
|
||||
required this.isChecked,
|
||||
@@ -14,37 +13,22 @@ class AttestationCheckbox extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: Checkbox(
|
||||
value: isChecked,
|
||||
onChanged: onChanged,
|
||||
activeColor: UiColors.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: UiConstants.space4,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: Checkbox(value: isChecked, onChanged: onChanged),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
t.staff_profile_attire.attestation,
|
||||
style: UiTypography.body2r,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Text(
|
||||
t.staff_profile_attire.attestation,
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textPrimary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import '../attestation_checkbox.dart';
|
||||
import 'attire_upload_buttons.dart';
|
||||
|
||||
/// Handles the primary actions at the bottom of the page.
|
||||
@@ -16,6 +17,9 @@ class FooterSection extends StatelessWidget {
|
||||
required this.hasVerificationStatus,
|
||||
required this.hasUploadedPhoto,
|
||||
this.updatedItem,
|
||||
required this.showCheckbox,
|
||||
required this.isAttested,
|
||||
required this.onAttestationChanged,
|
||||
required this.onGallery,
|
||||
required this.onCamera,
|
||||
required this.onSubmit,
|
||||
@@ -37,6 +41,15 @@ class FooterSection extends StatelessWidget {
|
||||
/// The updated attire item, if any.
|
||||
final AttireItem? updatedItem;
|
||||
|
||||
/// Whether to show the attestation checkbox.
|
||||
final bool showCheckbox;
|
||||
|
||||
/// Whether the user has attested to owning the item.
|
||||
final bool isAttested;
|
||||
|
||||
/// Callback when the attestation status changes.
|
||||
final ValueChanged<bool?> onAttestationChanged;
|
||||
|
||||
/// Callback to open the gallery.
|
||||
final VoidCallback onGallery;
|
||||
|
||||
@@ -57,6 +70,13 @@ class FooterSection extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (showCheckbox) ...<Widget>[
|
||||
AttestationCheckbox(
|
||||
isChecked: isAttested,
|
||||
onChanged: onAttestationChanged,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
],
|
||||
if (isUploading)
|
||||
const Center(
|
||||
child: Padding(
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../attestation_checkbox.dart';
|
||||
import 'attire_verification_status_card.dart';
|
||||
|
||||
/// Displays the item details, verification status, and attestation checkbox.
|
||||
/// Displays the item details and verification status.
|
||||
class InfoSection extends StatelessWidget {
|
||||
/// Creates an [InfoSection].
|
||||
const InfoSection({
|
||||
@@ -13,9 +12,6 @@ class InfoSection extends StatelessWidget {
|
||||
required this.statusText,
|
||||
required this.statusColor,
|
||||
required this.isPending,
|
||||
required this.showCheckbox,
|
||||
required this.isAttested,
|
||||
required this.onAttestationChanged,
|
||||
});
|
||||
|
||||
/// The description of the attire item.
|
||||
@@ -30,15 +26,6 @@ class InfoSection extends StatelessWidget {
|
||||
/// Whether the item is currently pending verification.
|
||||
final bool isPending;
|
||||
|
||||
/// Whether to show the attestation checkbox.
|
||||
final bool showCheckbox;
|
||||
|
||||
/// Whether the user has attested to owning the item.
|
||||
final bool isAttested;
|
||||
|
||||
/// Callback when the attestation status changes.
|
||||
final ValueChanged<bool?> onAttestationChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@@ -74,15 +61,6 @@ class InfoSection extends StatelessWidget {
|
||||
statusText: statusText,
|
||||
statusColor: statusColor,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
if (showCheckbox) ...<Widget>[
|
||||
AttestationCheckbox(
|
||||
isChecked: isAttested,
|
||||
onChanged: onAttestationChanged,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
|
||||
PersonalInfoAddressSelected event,
|
||||
Emitter<PersonalInfoState> emit,
|
||||
) {
|
||||
// Legacy address selected – no-op; use PersonalInfoLocationAdded instead.
|
||||
// Legacy address selected no-op; use PersonalInfoLocationAdded instead.
|
||||
}
|
||||
|
||||
/// Adds a location to the preferredLocations list (max 5, no duplicates).
|
||||
|
||||
@@ -109,7 +109,7 @@ class _PreferredLocationsPageState extends State<PreferredLocationsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// ── Description
|
||||
// ” Description
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
@@ -123,7 +123,7 @@ class _PreferredLocationsPageState extends State<PreferredLocationsPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// ── Search autocomplete field
|
||||
// ” Search autocomplete field
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
@@ -137,7 +137,7 @@ class _PreferredLocationsPageState extends State<PreferredLocationsPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// ── "Max reached" banner
|
||||
// ” "Max reached" banner
|
||||
if (atMax)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
@@ -164,7 +164,7 @@ class _PreferredLocationsPageState extends State<PreferredLocationsPage> {
|
||||
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
|
||||
// ── Section label
|
||||
// ” Section label
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
|
||||
@@ -28,9 +28,9 @@ class LegalSectionWidget extends StatelessWidget {
|
||||
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border, width: 0.5),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
|
||||
@@ -40,10 +40,11 @@ class PrivacySectionWidget extends StatelessWidget {
|
||||
const SizedBox(height: 12.0),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(
|
||||
color: UiColors.border,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
|
||||
Reference in New Issue
Block a user