scottohara/loot

View on GitHub
src/schedules/controllers/index.ts

Summary

Maintainability
B
5 hrs
Test Coverage
A
100%
import "~/transactions/css/index.css";
import type {
    OgTableActionHandlers,
    OgTableActions,
} from "~/og-components/og-table-navigable/types";
import type {
    SplitTransaction,
    SplitTransactionChild,
} from "~/transactions/types";
import { isEqual, startOfDay } from "date-fns";
import type OgModalErrorService from "~/og-components/og-modal-error/services/og-modal-error";
import type OgTableNavigableService from "~/og-components/og-table-navigable/services/og-table-navigable";
import ScheduleDeleteView from "~/schedules/views/delete.html";
import ScheduleEditView from "~/schedules/views/edit.html";
import type { ScheduledTransaction } from "~/schedules/types";
import type TransactionModel from "~/transactions/models/transaction";
import angular from "angular";

export default class ScheduleIndexController {
    public readonly tableActions: OgTableActions;

    // Today's date (for checking if a schedule is overdue)
    public readonly today: Date = startOfDay(new Date());

    private readonly showError: (message?: unknown) => void;

    public constructor(
        $scope: angular.IScope,
        $transitions: angular.ui.IStateParamsService,
        private readonly $uibModal: angular.ui.bootstrap.IModalService,
        private readonly $timeout: angular.ITimeoutService,
        private readonly $state: angular.ui.IStateService,
        private readonly transactionModel: TransactionModel,
        private readonly ogTableNavigableService: OgTableNavigableService,
        ogModalErrorService: OgModalErrorService,
        public readonly schedules: ScheduledTransaction[],
    ) {
        const self: this = this;

        this.tableActions = {
            selectAction(index: number): void {
                self.editSchedule(index);
            },
            editAction(index: number): void {
                self.editSchedule(index);
            },
            insertAction(): void {
                self.editSchedule();
            },
            deleteAction(index: number): void {
                self.deleteSchedule(index);
            },
            focusAction(index: number): void {
                $state
                    .go(`${$state.includes("**.schedule") ? "^" : ""}.schedule`, {
                        id: self.schedules[index].id,
                    })
                    .catch(self.showError);
            },
        };

        this.showError = ogModalErrorService.showError.bind(ogModalErrorService);

        // If we have a schedule id, focus the specified row
        if (!isNaN(Number($state.params.id))) {
            this.focusSchedule(Number($state.params.id));
        }

        // When the id state parameter changes, focus the specified row
        $scope.$on(
            "$destroy",
            $transitions.onSuccess(
                { to: "root.schedules.schedule" },
                (transition: angular.ui.IState): number =>
                    this.focusSchedule(Number(transition.params("to").id)),
            ) as () => void,
        );
    }

    // Shows/hides subtransactions
    public toggleSubtransactions(schedule: SplitTransaction): void {
        // Toggle the show flag
        schedule.showSubtransactions = !schedule.showSubtransactions;

        // If we're showing
        if (schedule.showSubtransactions) {
            // Show the loading indicator
            schedule.loadingSubtransactions = true;

            // Clear the array?
            schedule.subtransactions = [];

            // Resolve the subtransactions
            this.transactionModel
                .findSubtransactions(Number(schedule.id))
                .then((subtransactions: SplitTransactionChild[]): void => {
                    schedule.subtransactions = subtransactions;

                    // Hide the loading indicator
                    schedule.loadingSubtransactions = false;
                })
                .catch(this.showError);
        }
    }

    private editSchedule(index?: number): void {
        // Helper function to sort by next due date, then by transaction id
        function byNextDueDateAndId(
            a: ScheduledTransaction,
            b: ScheduledTransaction,
        ): number {
            let x: Date | number | string, y: Date | number | string;

            if (isEqual(a.next_due_date as Date, b.next_due_date as Date)) {
                x = Number(a.id);
                y = Number(b.id);
            } else {
                x = a.next_due_date;
                y = b.next_due_date;
            }

            return x < y ? -1 : x > y ? 1 : 0;
        }

        // Disable navigation on the table
        this.ogTableNavigableService.enabled = false;

        // Show the modal
        this.$uibModal
            .open({
                templateUrl: ScheduleEditView,
                controller: "ScheduleEditController",
                controllerAs: "vm",
                backdrop: "static",
                size: "lg",
                resolve: {
                    schedule: ():
                        | angular.IPromise<ScheduledTransaction>
                        | ScheduledTransaction
                        | undefined => {
                        // If we didn't get an index, we're adding a new schedule so just return null
                        if (isNaN(Number(index))) {
                            return undefined;
                        }

                        // If the selected schedule is a Split/Loan Repayment/Payslip; fetch the subtransactions first
                        switch (this.schedules[Number(index)].transaction_type) {
                            case "Split":
                            case "LoanRepayment":
                            case "Payslip":
                                (
                                    this.schedules[Number(index)] as SplitTransaction
                                ).subtransactions = [];

                                return this.transactionModel
                                    .findSubtransactions(Number(this.schedules[Number(index)].id))
                                    .then(
                                        (
                                            subtransactions: SplitTransactionChild[],
                                        ): ScheduledTransaction => {
                                            (
                                                this.schedules[Number(index)] as SplitTransaction
                                            ).subtransactions = subtransactions;

                                            return this.schedules[Number(index)];
                                        },
                                    );
                            default:
                                return this.schedules[Number(index)];
                        }
                    },
                },
            })
            .result.then(
                (schedule: { data: ScheduledTransaction; skipped: boolean }): void => {
                    if (isNaN(Number(index))) {
                        // Add new schedule to the end of the array
                        this.schedules.push(schedule.data);
                    } else {
                        // Update the existing schedule in the array
                        this.schedules[Number(index)] = schedule.data;
                    }

                    // Resort the array
                    this.schedules.sort(byNextDueDateAndId);

                    /*
                     * If we entered or skipped a transaction, refocus the schedule now at the original index,
                     * otherwise refocus the schedule that was edited
                     */
                    this.focusSchedule(
                        Number(
                            schedule.skipped
                                ? this.schedules[Number(index)].id
                                : schedule.data.id,
                        ),
                    );
                },
            )
            .finally((): true => (this.ogTableNavigableService.enabled = true))
            .catch(this.showError);
    }

    private deleteSchedule(index: number): void {
        // Disable navigation on the table
        this.ogTableNavigableService.enabled = false;

        // Show the modal
        this.$uibModal
            .open({
                templateUrl: ScheduleDeleteView,
                controller: "ScheduleDeleteController",
                controllerAs: "vm",
                backdrop: "static",
                resolve: {
                    schedule: (): ScheduledTransaction => this.schedules[index],
                },
            })
            .result.then((): void => {
                this.schedules.splice(index, 1);
                this.$state.go("root.schedules").catch(this.showError);
            })
            .finally((): true => (this.ogTableNavigableService.enabled = true))
            .catch(this.showError);
    }

    // Finds a specific schedule and focusses that row in the table
    private focusSchedule(scheduleIdToFocus: number): number {
        const delay = 50;
        let targetIndex = NaN;

        // Find the schedule by it's id
        angular.forEach(
            this.schedules,
            (schedule: ScheduledTransaction, index: number): void => {
                if (isNaN(targetIndex) && schedule.id === scheduleIdToFocus) {
                    targetIndex = index;
                }
            },
        );

        // If found, focus the row
        if (!isNaN(targetIndex)) {
            this.$timeout(
                (): void =>
                    (this.tableActions as OgTableActionHandlers).focusRow(targetIndex),
                delay,
            ).catch(this.showError);
        }

        return targetIndex;
    }
}

ScheduleIndexController.$inject = [
    "$scope",
    "$transitions",
    "$uibModal",
    "$timeout",
    "$state",
    "transactionModel",
    "ogTableNavigableService",
    "ogModalErrorService",
    "schedules",
];