Refactor billing data parsing and filtering, update invoice queries, and remove the dedicated timesheets page.
This commit is contained in:
@@ -6,16 +6,21 @@ import '../../domain/repositories/billing_connector_repository.dart';
|
||||
|
||||
/// Implementation of [BillingConnectorRepository].
|
||||
class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
|
||||
BillingConnectorRepositoryImpl({
|
||||
dc.DataConnectService? service,
|
||||
}) : _service = service ?? dc.DataConnectService.instance;
|
||||
BillingConnectorRepositoryImpl({dc.DataConnectService? service})
|
||||
: _service = service ?? dc.DataConnectService.instance;
|
||||
|
||||
final dc.DataConnectService _service;
|
||||
|
||||
@override
|
||||
Future<List<BusinessBankAccount>> getBankAccounts({required String businessId}) async {
|
||||
Future<List<BusinessBankAccount>> getBankAccounts({
|
||||
required String businessId,
|
||||
}) async {
|
||||
return _service.run(() async {
|
||||
final QueryResult<dc.GetAccountsByOwnerIdData, dc.GetAccountsByOwnerIdVariables> result = await _service.connector
|
||||
final QueryResult<
|
||||
dc.GetAccountsByOwnerIdData,
|
||||
dc.GetAccountsByOwnerIdVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.getAccountsByOwnerId(ownerId: businessId)
|
||||
.execute();
|
||||
|
||||
@@ -26,21 +31,32 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
|
||||
@override
|
||||
Future<double> getCurrentBillAmount({required String businessId}) async {
|
||||
return _service.run(() async {
|
||||
final QueryResult<dc.ListInvoicesByBusinessIdData, dc.ListInvoicesByBusinessIdVariables> result = await _service.connector
|
||||
final QueryResult<
|
||||
dc.ListInvoicesByBusinessIdData,
|
||||
dc.ListInvoicesByBusinessIdVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listInvoicesByBusinessId(businessId: businessId)
|
||||
.execute();
|
||||
|
||||
return result.data.invoices
|
||||
.map(_mapInvoice)
|
||||
.where((Invoice i) => i.status == InvoiceStatus.open)
|
||||
.fold<double>(0.0, (double sum, Invoice item) => sum + item.totalAmount);
|
||||
.fold<double>(
|
||||
0.0,
|
||||
(double sum, Invoice item) => sum + item.totalAmount,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Invoice>> getInvoiceHistory({required String businessId}) async {
|
||||
return _service.run(() async {
|
||||
final QueryResult<dc.ListInvoicesByBusinessIdData, dc.ListInvoicesByBusinessIdVariables> result = await _service.connector
|
||||
final QueryResult<
|
||||
dc.ListInvoicesByBusinessIdData,
|
||||
dc.ListInvoicesByBusinessIdVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listInvoicesByBusinessId(businessId: businessId)
|
||||
.limit(20)
|
||||
.execute();
|
||||
@@ -55,14 +71,22 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
|
||||
@override
|
||||
Future<List<Invoice>> getPendingInvoices({required String businessId}) async {
|
||||
return _service.run(() async {
|
||||
final QueryResult<dc.ListInvoicesByBusinessIdData, dc.ListInvoicesByBusinessIdVariables> result = await _service.connector
|
||||
final QueryResult<
|
||||
dc.ListInvoicesByBusinessIdData,
|
||||
dc.ListInvoicesByBusinessIdVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listInvoicesByBusinessId(businessId: businessId)
|
||||
.execute();
|
||||
|
||||
return result.data.invoices
|
||||
.map(_mapInvoice)
|
||||
.where((Invoice i) =>
|
||||
i.status != InvoiceStatus.paid)
|
||||
.where(
|
||||
(Invoice i) =>
|
||||
i.status != InvoiceStatus.paid &&
|
||||
i.status != InvoiceStatus.disputed &&
|
||||
i.status != InvoiceStatus.open,
|
||||
)
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
@@ -79,16 +103,25 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
|
||||
|
||||
if (period == BillingPeriod.week) {
|
||||
final int daysFromMonday = now.weekday - DateTime.monday;
|
||||
final DateTime monday = DateTime(now.year, now.month, now.day)
|
||||
.subtract(Duration(days: daysFromMonday));
|
||||
final DateTime monday = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
).subtract(Duration(days: daysFromMonday));
|
||||
start = monday;
|
||||
end = monday.add(const Duration(days: 6, hours: 23, minutes: 59, seconds: 59));
|
||||
end = monday.add(
|
||||
const Duration(days: 6, hours: 23, minutes: 59, seconds: 59),
|
||||
);
|
||||
} else {
|
||||
start = DateTime(now.year, now.month, 1);
|
||||
end = DateTime(now.year, now.month + 1, 0, 23, 59, 59);
|
||||
}
|
||||
|
||||
final QueryResult<dc.ListShiftRolesByBusinessAndDatesSummaryData, dc.ListShiftRolesByBusinessAndDatesSummaryVariables> result = await _service.connector
|
||||
final QueryResult<
|
||||
dc.ListShiftRolesByBusinessAndDatesSummaryData,
|
||||
dc.ListShiftRolesByBusinessAndDatesSummaryVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listShiftRolesByBusinessAndDatesSummary(
|
||||
businessId: businessId,
|
||||
start: _service.toTimestamp(start),
|
||||
@@ -96,11 +129,13 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
|
||||
)
|
||||
.execute();
|
||||
|
||||
final List<dc.ListShiftRolesByBusinessAndDatesSummaryShiftRoles> shiftRoles = result.data.shiftRoles;
|
||||
final List<dc.ListShiftRolesByBusinessAndDatesSummaryShiftRoles>
|
||||
shiftRoles = result.data.shiftRoles;
|
||||
if (shiftRoles.isEmpty) return <InvoiceItem>[];
|
||||
|
||||
final Map<String, _RoleSummary> summary = <String, _RoleSummary>{};
|
||||
for (final dc.ListShiftRolesByBusinessAndDatesSummaryShiftRoles role in shiftRoles) {
|
||||
for (final dc.ListShiftRolesByBusinessAndDatesSummaryShiftRoles role
|
||||
in shiftRoles) {
|
||||
final String roleId = role.roleId;
|
||||
final String roleName = role.role.name;
|
||||
final double hours = role.hours ?? 0.0;
|
||||
@@ -123,14 +158,16 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
|
||||
}
|
||||
|
||||
return summary.values
|
||||
.map((_RoleSummary item) => InvoiceItem(
|
||||
.map(
|
||||
(_RoleSummary item) => InvoiceItem(
|
||||
id: item.roleId,
|
||||
invoiceId: item.roleId,
|
||||
staffId: item.roleName,
|
||||
workHours: item.totalHours,
|
||||
rate: item.totalHours > 0 ? item.totalValue / item.totalHours : 0,
|
||||
amount: item.totalValue,
|
||||
))
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
@@ -146,7 +183,10 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disputeInvoice({required String id, required String reason}) async {
|
||||
Future<void> disputeInvoice({
|
||||
required String id,
|
||||
required String reason,
|
||||
}) async {
|
||||
return _service.run(() async {
|
||||
await _service.connector
|
||||
.updateInvoice(id: id)
|
||||
@@ -159,23 +199,39 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
|
||||
// --- MAPPERS ---
|
||||
|
||||
Invoice _mapInvoice(dynamic invoice) {
|
||||
final List<dynamic> rolesData = invoice.roles is List ? invoice.roles : [];
|
||||
final List<InvoiceWorker> workers = rolesData.map((dynamic r) {
|
||||
List<InvoiceWorker> workers = <InvoiceWorker>[];
|
||||
|
||||
// Try to get workers from denormalized 'roles' field first
|
||||
final List<dynamic> rolesData = invoice.roles is List
|
||||
? invoice.roles
|
||||
: <dynamic>[];
|
||||
if (rolesData.isNotEmpty) {
|
||||
workers = rolesData.map((dynamic r) {
|
||||
final Map<String, dynamic> role = r as Map<String, dynamic>;
|
||||
|
||||
// Handle various possible key naming conventions in the JSON data
|
||||
final String name = role['name'] ?? role['staffName'] ?? role['fullName'] ?? 'Unknown';
|
||||
final String roleTitle = role['role'] ?? role['roleName'] ?? role['title'] ?? 'Staff';
|
||||
final double amount = (role['amount'] as num?)?.toDouble() ??
|
||||
(role['totalValue'] as num?)?.toDouble() ?? 0.0;
|
||||
final double hours = (role['hours'] as num?)?.toDouble() ??
|
||||
final String name =
|
||||
role['name'] ?? role['staffName'] ?? role['fullName'] ?? 'Unknown';
|
||||
final String roleTitle =
|
||||
role['role'] ?? role['roleName'] ?? role['title'] ?? 'Staff';
|
||||
final double amount =
|
||||
(role['amount'] as num?)?.toDouble() ??
|
||||
(role['totalValue'] as num?)?.toDouble() ??
|
||||
0.0;
|
||||
final double hours =
|
||||
(role['hours'] as num?)?.toDouble() ??
|
||||
(role['workHours'] as num?)?.toDouble() ??
|
||||
(role['totalHours'] as num?)?.toDouble() ?? 0.0;
|
||||
final double rate = (role['rate'] as num?)?.toDouble() ??
|
||||
(role['hourlyRate'] as num?)?.toDouble() ?? 0.0;
|
||||
(role['totalHours'] as num?)?.toDouble() ??
|
||||
0.0;
|
||||
final double rate =
|
||||
(role['rate'] as num?)?.toDouble() ??
|
||||
(role['hourlyRate'] as num?)?.toDouble() ??
|
||||
0.0;
|
||||
|
||||
final dynamic checkInVal = role['checkInTime'] ?? role['startTime'] ?? role['check_in_time'];
|
||||
final dynamic checkOutVal = role['checkOutTime'] ?? role['endTime'] ?? role['check_out_time'];
|
||||
final dynamic checkInVal =
|
||||
role['checkInTime'] ?? role['startTime'] ?? role['check_in_time'];
|
||||
final dynamic checkOutVal =
|
||||
role['checkOutTime'] ?? role['endTime'] ?? role['check_out_time'];
|
||||
|
||||
return InvoiceWorker(
|
||||
name: name,
|
||||
@@ -186,9 +242,57 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
|
||||
checkIn: _service.toDateTime(checkInVal),
|
||||
checkOut: _service.toDateTime(checkOutVal),
|
||||
breakMinutes: role['breakMinutes'] ?? role['break_minutes'] ?? 0,
|
||||
avatarUrl: role['avatarUrl'] ?? role['photoUrl'] ?? role['staffPhoto'],
|
||||
avatarUrl:
|
||||
role['avatarUrl'] ?? role['photoUrl'] ?? role['staffPhoto'],
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
// Fallback: If roles is empty, try to get workers from shift applications
|
||||
else if (invoice.shift != null &&
|
||||
invoice.shift.applications_on_shift != null) {
|
||||
final List<dynamic> apps = invoice.shift.applications_on_shift;
|
||||
workers = apps.map((dynamic app) {
|
||||
final String name = app.staff?.fullName ?? 'Unknown';
|
||||
final String roleTitle = app.shiftRole?.role?.name ?? 'Staff';
|
||||
final double amount =
|
||||
(app.shiftRole?.totalValue as num?)?.toDouble() ?? 0.0;
|
||||
final double hours = (app.shiftRole?.hours as num?)?.toDouble() ?? 0.0;
|
||||
|
||||
// Calculate rate if not explicitly provided
|
||||
double rate = 0.0;
|
||||
if (hours > 0) {
|
||||
rate = amount / hours;
|
||||
}
|
||||
|
||||
// Map break type to minutes
|
||||
int breakMin = 0;
|
||||
final String? breakType = app.shiftRole?.breakType?.toString();
|
||||
if (breakType != null) {
|
||||
if (breakType.contains('10'))
|
||||
breakMin = 10;
|
||||
else if (breakType.contains('15'))
|
||||
breakMin = 15;
|
||||
else if (breakType.contains('30'))
|
||||
breakMin = 30;
|
||||
else if (breakType.contains('45'))
|
||||
breakMin = 45;
|
||||
else if (breakType.contains('60'))
|
||||
breakMin = 60;
|
||||
}
|
||||
|
||||
return InvoiceWorker(
|
||||
name: name,
|
||||
role: roleTitle,
|
||||
amount: amount,
|
||||
hours: hours,
|
||||
rate: rate,
|
||||
checkIn: _service.toDateTime(app.checkInTime),
|
||||
checkOut: _service.toDateTime(app.checkOutTime),
|
||||
breakMinutes: breakMin,
|
||||
avatarUrl: app.staff?.photoUrl,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return Invoice(
|
||||
id: invoice.id,
|
||||
@@ -202,8 +306,10 @@ class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
|
||||
issueDate: _service.toDateTime(invoice.issueDate)!,
|
||||
title: invoice.order?.eventName,
|
||||
clientName: invoice.business?.businessName,
|
||||
locationAddress: invoice.order?.teamHub?.hubName ?? invoice.order?.teamHub?.address,
|
||||
staffCount: invoice.staffCount ?? (workers.isNotEmpty ? workers.length : 0),
|
||||
locationAddress:
|
||||
invoice.order?.teamHub?.hubName ?? invoice.order?.teamHub?.address,
|
||||
staffCount:
|
||||
invoice.staffCount ?? (workers.isNotEmpty ? workers.length : 0),
|
||||
totalHours: _calculateTotalHours(rolesData),
|
||||
workers: workers,
|
||||
);
|
||||
@@ -256,10 +362,7 @@ class _RoleSummary {
|
||||
final double totalHours;
|
||||
final double totalValue;
|
||||
|
||||
_RoleSummary copyWith({
|
||||
double? totalHours,
|
||||
double? totalValue,
|
||||
}) {
|
||||
_RoleSummary copyWith({double? totalHours, double? totalValue}) {
|
||||
return _RoleSummary(
|
||||
roleId: roleId,
|
||||
roleName: roleName,
|
||||
@@ -268,4 +371,3 @@ class _RoleSummary {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,10 +78,12 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
|
||||
results[5] as List<BusinessBankAccount>;
|
||||
|
||||
// Map Domain Entities to Presentation Models
|
||||
final List<BillingInvoice> uiPendingInvoices =
|
||||
pendingInvoices.map(_mapInvoiceToUiModel).toList();
|
||||
final List<BillingInvoice> uiInvoiceHistory =
|
||||
invoiceHistory.map(_mapInvoiceToUiModel).toList();
|
||||
final List<BillingInvoice> uiPendingInvoices = pendingInvoices
|
||||
.map(_mapInvoiceToUiModel)
|
||||
.toList();
|
||||
final List<BillingInvoice> uiInvoiceHistory = invoiceHistory
|
||||
.map(_mapInvoiceToUiModel)
|
||||
.toList();
|
||||
final List<SpendingBreakdownItem> uiSpendingBreakdown =
|
||||
_mapSpendingItemsToUiModel(spendingItems);
|
||||
final double periodTotal = uiSpendingBreakdown.fold(
|
||||
@@ -101,10 +103,8 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
|
||||
),
|
||||
);
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
status: BillingStatus.failure,
|
||||
errorMessage: errorKey,
|
||||
),
|
||||
onError: (String errorKey) =>
|
||||
state.copyWith(status: BillingStatus.failure, errorMessage: errorKey),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,8 +115,8 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final List<InvoiceItem> spendingItems =
|
||||
await _getSpendingBreakdown.call(event.period);
|
||||
final List<InvoiceItem> spendingItems = await _getSpendingBreakdown
|
||||
.call(event.period);
|
||||
final List<SpendingBreakdownItem> uiSpendingBreakdown =
|
||||
_mapSpendingItemsToUiModel(spendingItems);
|
||||
final double periodTotal = uiSpendingBreakdown.fold(
|
||||
@@ -131,10 +131,8 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
|
||||
),
|
||||
);
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
status: BillingStatus.failure,
|
||||
errorMessage: errorKey,
|
||||
),
|
||||
onError: (String errorKey) =>
|
||||
state.copyWith(status: BillingStatus.failure, errorMessage: errorKey),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,10 +146,8 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
|
||||
await _approveInvoice.call(event.invoiceId);
|
||||
add(const BillingLoadStarted());
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
status: BillingStatus.failure,
|
||||
errorMessage: errorKey,
|
||||
),
|
||||
onError: (String errorKey) =>
|
||||
state.copyWith(status: BillingStatus.failure, errorMessage: errorKey),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,10 +163,8 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
|
||||
);
|
||||
add(const BillingLoadStarted());
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
status: BillingStatus.failure,
|
||||
errorMessage: errorKey,
|
||||
),
|
||||
onError: (String errorKey) =>
|
||||
state.copyWith(status: BillingStatus.failure, errorMessage: errorKey),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -180,15 +174,18 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
|
||||
? 'N/A'
|
||||
: formatter.format(invoice.issueDate!);
|
||||
|
||||
final List<BillingWorkerRecord> workers = invoice.workers.map((InvoiceWorker w) {
|
||||
final List<BillingWorkerRecord> workers = invoice.workers.map((
|
||||
InvoiceWorker w,
|
||||
) {
|
||||
final DateFormat timeFormat = DateFormat('h:mm a');
|
||||
return BillingWorkerRecord(
|
||||
workerName: w.name,
|
||||
roleName: w.role,
|
||||
totalAmount: w.amount,
|
||||
hours: w.hours,
|
||||
rate: w.rate,
|
||||
startTime: w.checkIn != null ? '${w.checkIn!.hour.toString().padLeft(2, '0')}:${w.checkIn!.minute.toString().padLeft(2, '0')}' : '--:--',
|
||||
endTime: w.checkOut != null ? '${w.checkOut!.hour.toString().padLeft(2, '0')}:${w.checkOut!.minute.toString().padLeft(2, '0')}' : '--:--',
|
||||
startTime: w.checkIn != null ? timeFormat.format(w.checkIn!) : '--:--',
|
||||
endTime: w.checkOut != null ? timeFormat.format(w.checkOut!) : '--:--',
|
||||
breakMinutes: w.breakMinutes,
|
||||
workerAvatarUrl: w.avatarUrl,
|
||||
);
|
||||
@@ -197,32 +194,34 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
|
||||
String? overallStart;
|
||||
String? overallEnd;
|
||||
|
||||
// Find valid times from workers instead of just taking the first one
|
||||
final validStartTimes = workers
|
||||
.where((w) => w.startTime != '--:--')
|
||||
.map((w) => w.startTime)
|
||||
// Find valid times from actual DateTime checks to ensure chronological sorting
|
||||
final List<DateTime> validCheckIns = invoice.workers
|
||||
.where((InvoiceWorker w) => w.checkIn != null)
|
||||
.map((InvoiceWorker w) => w.checkIn!)
|
||||
.toList();
|
||||
final validEndTimes = workers
|
||||
.where((w) => w.endTime != '--:--')
|
||||
.map((w) => w.endTime)
|
||||
final List<DateTime> validCheckOuts = invoice.workers
|
||||
.where((InvoiceWorker w) => w.checkOut != null)
|
||||
.map((InvoiceWorker w) => w.checkOut!)
|
||||
.toList();
|
||||
|
||||
if (validStartTimes.isNotEmpty) {
|
||||
validStartTimes.sort();
|
||||
overallStart = validStartTimes.first;
|
||||
final DateFormat timeFormat = DateFormat('h:mm a');
|
||||
|
||||
if (validCheckIns.isNotEmpty) {
|
||||
validCheckIns.sort();
|
||||
overallStart = timeFormat.format(validCheckIns.first);
|
||||
} else if (workers.isNotEmpty) {
|
||||
overallStart = workers.first.startTime;
|
||||
}
|
||||
|
||||
if (validEndTimes.isNotEmpty) {
|
||||
validEndTimes.sort();
|
||||
overallEnd = validEndTimes.last;
|
||||
if (validCheckOuts.isNotEmpty) {
|
||||
validCheckOuts.sort();
|
||||
overallEnd = timeFormat.format(validCheckOuts.last);
|
||||
} else if (workers.isNotEmpty) {
|
||||
overallEnd = workers.first.endTime;
|
||||
}
|
||||
|
||||
return BillingInvoice(
|
||||
id: invoice.invoiceNumber ?? invoice.id,
|
||||
id: invoice.id,
|
||||
title: invoice.title ?? 'N/A',
|
||||
locationAddress: invoice.locationAddress ?? 'Remote',
|
||||
clientName: invoice.clientName ?? 'N/A',
|
||||
|
||||
@@ -96,7 +96,7 @@ class _BillingViewState extends State<BillingView> {
|
||||
leading: Center(
|
||||
child: UiIconButton(
|
||||
icon: UiIcons.arrowLeft,
|
||||
backgroundColor: UiColors.white.withOpacity(0.15),
|
||||
backgroundColor: UiColors.white.withValues(alpha: 0.15),
|
||||
iconColor: UiColors.white,
|
||||
useBlur: true,
|
||||
size: 40,
|
||||
@@ -119,7 +119,7 @@ class _BillingViewState extends State<BillingView> {
|
||||
Text(
|
||||
t.client_billing.current_period,
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.white.withOpacity(0.7),
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
@@ -232,77 +232,4 @@ class _BillingViewState extends State<BillingView> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const SizedBox(height: UiConstants.space12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: const Icon(
|
||||
UiIcons.file,
|
||||
size: 48,
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
t.client_billing.no_invoices_period,
|
||||
style: UiTypography.body1m.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InvoicesReadyBanner extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () => Modular.to.toInvoiceReady(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.success.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.success.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(UiIcons.file, color: UiColors.success),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
t.client_billing.invoices_ready_title,
|
||||
style: UiTypography.body1b.copyWith(
|
||||
color: UiColors.success,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
t.client_billing.invoices_ready_subtitle,
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.success,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(UiIcons.chevronRight, color: UiColors.success),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
class ClientTimesheetsPage extends StatelessWidget {
|
||||
const ClientTimesheetsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.t.client_billing.timesheets.title),
|
||||
elevation: 0,
|
||||
backgroundColor: UiColors.white,
|
||||
foregroundColor: UiColors.primary,
|
||||
),
|
||||
body: ListView.separated(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
itemCount: 3,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 16),
|
||||
itemBuilder: (context, index) {
|
||||
final workers = ['Sarah Miller', 'David Chen', 'Mike Ross'];
|
||||
final roles = ['Cashier', 'Stocker', 'Event Support'];
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.separatorPrimary),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(workers[index], style: UiTypography.body2b.textPrimary),
|
||||
Text('\$84.00', style: UiTypography.body2b.primary),
|
||||
],
|
||||
),
|
||||
Text(roles[index], style: UiTypography.footnote2r.textSecondary),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(UiIcons.clock, size: 14, color: UiColors.iconSecondary),
|
||||
const SizedBox(width: 6),
|
||||
Text('09:00 AM - 05:00 PM (8h)', style: UiTypography.footnote2r.textSecondary),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UiButton.secondary(
|
||||
text: context.t.client_billing.timesheets.decline_button,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: UiColors.destructive,
|
||||
side: const BorderSide(color: UiColors.destructive),
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UiButton.primary(
|
||||
text: context.t.client_billing.timesheets.approve_button,
|
||||
onPressed: () {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: context.t.client_billing.timesheets.approved_message,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import '../../blocs/billing_bloc.dart';
|
||||
import '../../blocs/billing_event.dart';
|
||||
@@ -72,8 +73,7 @@ class CompletionReviewActions extends StatelessWidget {
|
||||
Modular.get<BillingBloc>().add(
|
||||
BillingInvoiceDisputed(invoiceId, controller.text),
|
||||
);
|
||||
Navigator.pop(dialogContext);
|
||||
Modular.to.pop();
|
||||
Modular.to.toClientBilling();
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: t.client_billing.flagged_success,
|
||||
|
||||
@@ -25,7 +25,7 @@ class InvoiceHistorySection extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border.withOpacity(0.5)),
|
||||
border: Border.all(color: UiColors.border.withValues(alpha: 0.5)),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withValues(alpha: 0.04),
|
||||
@@ -77,7 +77,7 @@ class _InvoiceItem extends StatelessWidget {
|
||||
),
|
||||
child: Icon(
|
||||
UiIcons.file,
|
||||
color: UiColors.iconSecondary.withOpacity(0.6),
|
||||
color: UiColors.iconSecondary.withValues(alpha: 0.6),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
@@ -86,10 +86,7 @@ class _InvoiceItem extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
invoice.id,
|
||||
style: UiTypography.body1b.textPrimary.copyWith(fontSize: 15),
|
||||
),
|
||||
Text(invoice.title, style: UiTypography.body1r.textPrimary),
|
||||
Text(
|
||||
invoice.date,
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
|
||||
@@ -25,7 +25,7 @@ class PendingInvoicesSection extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border.withOpacity(0.5)),
|
||||
border: Border.all(color: UiColors.border.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
@@ -41,9 +41,9 @@ class PendingInvoicesSection extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.client_billing.awaiting_approval,
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
@@ -79,7 +79,7 @@ class PendingInvoicesSection extends StatelessWidget {
|
||||
Icon(
|
||||
UiIcons.chevronRight,
|
||||
size: 20,
|
||||
color: UiColors.iconSecondary.withOpacity(0.5),
|
||||
color: UiColors.iconSecondary.withValues(alpha: 0.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -180,7 +180,7 @@ class PendingInvoiceCard extends StatelessWidget {
|
||||
Container(
|
||||
width: 1,
|
||||
height: 32,
|
||||
color: UiColors.border.withOpacity(0.3),
|
||||
color: UiColors.border.withValues(alpha: 0.3),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildStatItem(
|
||||
@@ -192,7 +192,7 @@ class PendingInvoiceCard extends StatelessWidget {
|
||||
Container(
|
||||
width: 1,
|
||||
height: 32,
|
||||
color: UiColors.border.withOpacity(0.3),
|
||||
color: UiColors.border.withValues(alpha: 0.3),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildStatItem(
|
||||
@@ -225,7 +225,11 @@ class PendingInvoiceCard extends StatelessWidget {
|
||||
Widget _buildStatItem(IconData icon, String value, String label) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Icon(icon, size: 20, color: UiColors.iconSecondary.withOpacity(0.8)),
|
||||
Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: UiColors.iconSecondary.withValues(alpha: 0.8),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
value,
|
||||
|
||||
@@ -61,6 +61,29 @@ query listInvoices(
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
shift {
|
||||
id
|
||||
title
|
||||
applications_on_shift {
|
||||
id
|
||||
status
|
||||
checkInTime
|
||||
checkOutTime
|
||||
staff {
|
||||
fullName
|
||||
photoUrl
|
||||
}
|
||||
shiftRole {
|
||||
role {
|
||||
name
|
||||
}
|
||||
totalValue
|
||||
hours
|
||||
breakType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +146,29 @@ query getInvoiceById($id: UUID!) @auth(level: USER) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
shift {
|
||||
id
|
||||
title
|
||||
applications_on_shift {
|
||||
id
|
||||
status
|
||||
checkInTime
|
||||
checkOutTime
|
||||
staff {
|
||||
fullName
|
||||
photoUrl
|
||||
}
|
||||
shiftRole {
|
||||
role {
|
||||
name
|
||||
}
|
||||
totalValue
|
||||
hours
|
||||
breakType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +240,29 @@ query listInvoicesByVendorId(
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
shift {
|
||||
id
|
||||
title
|
||||
applications_on_shift {
|
||||
id
|
||||
status
|
||||
checkInTime
|
||||
checkOutTime
|
||||
staff {
|
||||
fullName
|
||||
photoUrl
|
||||
}
|
||||
shiftRole {
|
||||
role {
|
||||
name
|
||||
}
|
||||
totalValue
|
||||
hours
|
||||
breakType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,6 +334,29 @@ query listInvoicesByBusinessId(
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
shift {
|
||||
id
|
||||
title
|
||||
applications_on_shift {
|
||||
id
|
||||
status
|
||||
checkInTime
|
||||
checkOutTime
|
||||
staff {
|
||||
fullName
|
||||
photoUrl
|
||||
}
|
||||
shiftRole {
|
||||
role {
|
||||
name
|
||||
}
|
||||
totalValue
|
||||
hours
|
||||
breakType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,6 +428,29 @@ query listInvoicesByOrderId(
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
shift {
|
||||
id
|
||||
title
|
||||
applications_on_shift {
|
||||
id
|
||||
status
|
||||
checkInTime
|
||||
checkOutTime
|
||||
staff {
|
||||
fullName
|
||||
photoUrl
|
||||
}
|
||||
shiftRole {
|
||||
role {
|
||||
name
|
||||
}
|
||||
totalValue
|
||||
hours
|
||||
breakType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,6 +522,29 @@ query listInvoicesByStatus(
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
shift {
|
||||
id
|
||||
title
|
||||
applications_on_shift {
|
||||
id
|
||||
status
|
||||
checkInTime
|
||||
checkOutTime
|
||||
staff {
|
||||
fullName
|
||||
photoUrl
|
||||
}
|
||||
shiftRole {
|
||||
role {
|
||||
name
|
||||
}
|
||||
totalValue
|
||||
hours
|
||||
breakType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,6 +639,29 @@ query filterInvoices(
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
shift {
|
||||
id
|
||||
title
|
||||
applications_on_shift {
|
||||
id
|
||||
status
|
||||
checkInTime
|
||||
checkOutTime
|
||||
staff {
|
||||
fullName
|
||||
photoUrl
|
||||
}
|
||||
shiftRole {
|
||||
role {
|
||||
name
|
||||
}
|
||||
totalValue
|
||||
hours
|
||||
breakType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,5 +737,28 @@ query listOverdueInvoices(
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
shift {
|
||||
id
|
||||
title
|
||||
applications_on_shift {
|
||||
id
|
||||
status
|
||||
checkInTime
|
||||
checkOutTime
|
||||
staff {
|
||||
fullName
|
||||
photoUrl
|
||||
}
|
||||
shiftRole {
|
||||
role {
|
||||
name
|
||||
}
|
||||
totalValue
|
||||
hours
|
||||
breakType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user