Initial commit: Krow Worker App

This commit is contained in:
R-Bharathraj
2026-09-17 22:26:11 +05:30
commit cafffd27e0
113 changed files with 13067 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
enum DayBlock { morning, afternoon, evening }
extension DayBlockX on DayBlock {
String get label => switch (this) {
DayBlock.morning => 'Morning',
DayBlock.afternoon => 'Afternoon',
DayBlock.evening => 'Evening',
};
String get window => switch (this) {
DayBlock.morning => '04:00 - 12:00',
DayBlock.afternoon => '12:00 - 18:00',
DayBlock.evening => '18:00 - 24:00',
};
int get hours => switch (this) {
DayBlock.morning => 8,
DayBlock.afternoon => 6,
DayBlock.evening => 6,
};
}

View File

@@ -0,0 +1,82 @@
enum ComplianceState { verified, pending, missing }
extension ComplianceStateX on ComplianceState {
String get label => switch (this) {
ComplianceState.verified => 'Verified',
ComplianceState.pending => 'Pending',
ComplianceState.missing => 'Missing',
};
}
/// A single thing a business must see before you can work: a document, a
/// certificate, a tax form or a piece of attire.
class ComplianceItem {
ComplianceItem({
required this.id,
required this.title,
required this.subtitle,
required this.state,
this.statusLabel,
this.fileName,
this.fileMeta,
this.expires,
this.required_ = true,
});
final String id;
final String title;
final String subtitle;
final ComplianceState state;
final String? statusLabel;
final String? fileName;
final String? fileMeta;
final DateTime? expires;
final bool required_;
String get badge => statusLabel ?? state.label;
ComplianceItem copyWith({
ComplianceState? state,
String? statusLabel,
String? fileName,
String? fileMeta,
DateTime? expires,
bool clearFile = false,
}) => ComplianceItem(
id: id,
title: title,
subtitle: subtitle,
state: state ?? this.state,
statusLabel: statusLabel ?? this.statusLabel,
fileName: clearFile ? null : (fileName ?? this.fileName),
fileMeta: clearFile ? null : (fileMeta ?? this.fileMeta),
expires: expires ?? this.expires,
required_: required_,
);
}
class EmergencyContact {
EmergencyContact({
required this.name,
required this.phone,
required this.relationship,
});
final String name;
final String phone;
final String relationship;
}
class BankAccount {
BankAccount({
required this.bankName,
required this.routing,
required this.accountLast4,
required this.accountType,
});
final String bankName;
final String routing;
final String accountLast4;
final String accountType;
}

95
lib/models/shift.dart Normal file
View File

@@ -0,0 +1,95 @@
enum ShiftStatus { open, booked, active, completed, cancelled, noShow }
extension ShiftStatusX on ShiftStatus {
String get label => switch (this) {
ShiftStatus.open => 'Open',
ShiftStatus.booked => 'Confirmed',
ShiftStatus.active => 'In progress',
ShiftStatus.completed => 'Completed',
ShiftStatus.cancelled => 'Cancelled',
ShiftStatus.noShow => 'No show',
};
}
class Shift {
Shift({
required this.id,
required this.role,
required this.business,
required this.venue,
required this.address,
required this.start,
required this.end,
required this.rate,
required this.status,
this.breakMinutes = 30,
this.distanceMiles = 2.4,
this.manager,
this.clockIn,
this.clockOut,
this.requirements = const [],
});
final String id;
final String role;
final String business;
final String venue;
final String address;
final DateTime start;
final DateTime end;
final double rate;
final ShiftStatus status;
final int breakMinutes;
final double distanceMiles;
final String? manager;
final DateTime? clockIn;
final DateTime? clockOut;
final List<String> requirements;
/// Paid duration: the scheduled window less the unpaid break.
Duration get scheduledDuration =>
end.difference(start) - Duration(minutes: breakMinutes);
/// What was actually worked, once clocked out.
Duration? get workedDuration {
if (clockIn == null || clockOut == null) return null;
final d = clockOut!.difference(clockIn!) - Duration(minutes: breakMinutes);
return d.isNegative ? Duration.zero : d;
}
double get scheduledPay => rate * scheduledDuration.inMinutes / 60;
/// Gross, pre-tax. Falls back to the scheduled figure until clocked out.
double get pay {
if (status == ShiftStatus.cancelled || status == ShiftStatus.noShow) {
return 0;
}
final worked = workedDuration;
if (worked != null) return rate * worked.inMinutes / 60;
return scheduledPay;
}
String get logoLetter => business.isEmpty ? '?' : business[0].toUpperCase();
Shift copyWith({
ShiftStatus? status,
DateTime? clockIn,
DateTime? clockOut,
}) => Shift(
id: id,
role: role,
business: business,
venue: venue,
address: address,
start: start,
end: end,
rate: rate,
status: status ?? this.status,
breakMinutes: breakMinutes,
distanceMiles: distanceMiles,
manager: manager,
clockIn: clockIn ?? this.clockIn,
clockOut: clockOut ?? this.clockOut,
requirements: requirements,
);
}

70
lib/models/timecard.dart Normal file
View File

@@ -0,0 +1,70 @@
enum TimecardStatus { needsReview, approved, disputed }
extension TimecardStatusX on TimecardStatus {
String get label => switch (this) {
TimecardStatus.needsReview => 'Needs review',
TimecardStatus.approved => 'Approved',
TimecardStatus.disputed => 'Disputed',
};
}
class Timecard {
Timecard({
required this.id,
required this.shiftId,
required this.role,
required this.business,
required this.scheduledStart,
required this.scheduledEnd,
required this.clockIn,
required this.clockOut,
required this.breakMinutes,
required this.rate,
required this.status,
required this.submittedAt,
this.disputeNote,
});
final String id;
final String shiftId;
final String role;
final String business;
final DateTime scheduledStart;
final DateTime scheduledEnd;
final DateTime clockIn;
final DateTime clockOut;
final int breakMinutes;
final double rate;
final TimecardStatus status;
final DateTime submittedAt;
final String? disputeNote;
DateTime get date =>
DateTime(scheduledStart.year, scheduledStart.month, scheduledStart.day);
Duration get worked {
final d = clockOut.difference(clockIn) - Duration(minutes: breakMinutes);
return d.isNegative ? Duration.zero : d;
}
double get amount => rate * worked.inMinutes / 60;
/// Minutes late (positive) or early (negative) against the schedule.
int get varianceMinutes => clockIn.difference(scheduledStart).inMinutes;
Timecard copyWith({TimecardStatus? status, String? disputeNote}) => Timecard(
id: id,
shiftId: shiftId,
role: role,
business: business,
scheduledStart: scheduledStart,
scheduledEnd: scheduledEnd,
clockIn: clockIn,
clockOut: clockOut,
breakMinutes: breakMinutes,
rate: rate,
status: status ?? this.status,
submittedAt: submittedAt,
disputeNote: disputeNote ?? this.disputeNote,
);
}

57
lib/models/worker.dart Normal file
View File

@@ -0,0 +1,57 @@
class Worker {
const Worker({
required this.name,
required this.role,
required this.city,
required this.phone,
this.shiftsWorked = 0,
this.rating = 5.0,
this.onTimePercent = 100,
this.noShows = 0,
this.cancellations = 0,
});
final String name;
final String role;
final String city;
final String phone;
final int shiftsWorked;
final double rating;
final int onTimePercent;
final int noShows;
final int cancellations;
/// Stay above 45% to keep picking up shifts. On-time arrivals move it most.
int get trustIndex {
final base = 40 + onTimePercent ~/ 2 - noShows * 15 - cancellations * 5;
return base.clamp(0, 100);
}
String get initials => name
.split(' ')
.where((p) => p.isNotEmpty)
.map((p) => p[0].toUpperCase())
.take(2)
.join();
Worker copyWith({
String? name,
String? role,
String? city,
String? phone,
int? shiftsWorked,
int? onTimePercent,
int? noShows,
int? cancellations,
}) => Worker(
name: name ?? this.name,
role: role ?? this.role,
city: city ?? this.city,
phone: phone ?? this.phone,
shiftsWorked: shiftsWorked ?? this.shiftsWorked,
rating: rating,
onTimePercent: onTimePercent ?? this.onTimePercent,
noShows: noShows ?? this.noShows,
cancellations: cancellations ?? this.cancellations,
);
}