import '../../about/external/data/Commitment.dart'; import '../../about/external/data/pages/request/HistoryRequest.dart'; import '../../about/external/data/pages/request/PageAndSort.dart'; import '../../about/external/data/pages/request/Pageable.dart'; import '../../about/external/data/pages/request/Sort.dart'; import '../../about/external/data/pages/response/CommitmentPage.dart'; import '../../about/external/initial/AbandonRequest.dart'; import '../../about/external/initial/AmnestyRequest.dart'; import '../../about/external/initial/CompletionRequest.dart'; import '../../about/external/initial/DeferralRequest.dart'; import '../../about/internal/application/CommitmentClass.dart'; import '../../about/internal/application/Standing.dart'; import '../../about/internal/application/UserDetails.dart'; import '../../utils/DebtEngine.dart'; import '../../utils/GuardrailEngine.dart'; import '../../utils/StandingEngine.dart'; import '../../utils/Thresholds.dart'; import '../../utils/ToneEngine.dart'; import '../parent/ParentViewModel.dart'; import 'ConnectOverdueQueue.dart'; class ViewOverdueQueue extends ParentViewModel { ConnectOverdueQueue connection; ViewOverdueQueue(super.context, this.connection); void loadQueue() async { if (!await hasNetwork(() => loadQueue())) return; showLoading("Loading what you owe"); try { final response = await getDataManager().getOverdueQueue(HistoryRequest( query: PageAndSort( sort: Sort('desc', 'dueEnd'), page: Pageable(0, 0, 50, 0), ), )); final CommitmentPage page = CommitmentPage.fromJson(response.data); final UserDetails details = await getDataManager().getUserDetails(); final double debt = DebtEngine.totalDebt(page.content); final Standing standing = StandingEngine.evaluate( debt, missedNonNegotiables: DebtEngine.missedNonNegotiables(page.content), sickMode: details.sickMode, ); await getDataManager().setCachedDebtScore(debt); closeLoading(); connection.onQueueLoaded(page.content, standing, debt); } catch (e) { handleError(e, () => loadQueue(), () => dismissError(), "Retry"); } } /// Completion outside the due window is recorded as a late complete, not a /// complete. The server decides which — the client never claims one. void complete(Commitment item, CompletionRequest request) async { if (!await hasNetwork(() => complete(item, request))) return; showLoading("Recording"); try { await getDataManager().completeCommitmentEntry(request); closeLoading(); connection.onItemCleared( item, DebtEngine.reliefFromClearing(item), item.wouldBeLate ? "Late complete" : "Complete", ); loadQueue(); } catch (e) { handleError( e, () => complete(item, request), () => dismissError(), "Retry"); } } /// The deferral gate. Non-negotiables never defer; everything else runs out /// of deferrals, after which the only routes left are completing or /// abandoning. void defer(Commitment item, String excuse) async { if (item.commitmentClass == CommitmentClass.NonNegotiable) { connection.onDeferralRefused(ToneEngine.nonNegotiableRefused()); return; } if (!item.deferrableUnder(Thresholds.maxDeferralsPerTask)) { connection.onDeferralRefused( ToneEngine.deferralRefused(Thresholds.maxDeferralsPerTask)); return; } if (excuse.trim().length < Thresholds.minExcuseLength) { connection .onDeferralRefused(ToneEngine.excuseTooShort(Thresholds.minExcuseLength)); return; } if (!await hasNetwork(() => defer(item, excuse))) return; showLoading("Recording the deferral"); try { // The new window opens tomorrow at the same time — a deferral moves the // window, it never removes it. final DateTime start = (item.dueStart ?? DateTime.now()).add(const Duration(days: 1)); final DateTime end = (item.dueEnd ?? DateTime.now()).add(const Duration(days: 1)); await getDataManager().deferCommitmentEntry(DeferralRequest( commitmentId: item.id ?? "", excuseText: excuse.trim(), newDueStart: start.toIso8601String(), newDueEnd: end.toIso8601String(), )); closeLoading(); connection.onItemCleared(item, 0, "Deferred"); loadQueue(); } catch (e) { handleError(e, () => defer(item, excuse), () => dismissError(), "Retry"); } } /// Abandoning costs the most debt of all, and resists decay for a month. void abandon(Commitment item, String reason) async { if (!await hasNetwork(() => abandon(item, reason))) return; showLoading("Recording"); try { await getDataManager().abandonCommitmentEntry(AbandonRequest( commitmentId: item.id ?? "", reason: reason.trim(), )); closeLoading(); connection.onItemCleared(item, 0, "Abandoned"); loadQueue(); } catch (e) { handleError( e, () => abandon(item, reason), () => dismissError(), "Retry"); } } /// Rationed, so a bad flu does not destroy three months of progress. void spendAmnesty(Commitment item) async { final int spent = await getDataManager().getAmnestySpent(); if (!GuardrailEngine.canSpendAmnesty( Thresholds.amnestyTokensPerMonth, spent)) { connection.onAmnestyRefused(); return; } if (!await hasNetwork(() => spendAmnesty(item))) return; showLoading("Applying amnesty"); try { await getDataManager() .spendAmnestyToken(AmnestyRequest(commitmentId: item.id ?? "")); await getDataManager().setAmnestySpent(spent + 1); closeLoading(); connection.onAmnestySpent(GuardrailEngine.tokensRemaining( Thresholds.amnestyTokensPerMonth, spent + 1)); loadQueue(); } catch (e) { handleError(e, () => spendAmnesty(item), () => dismissError(), "Retry"); } } }