155 lines
4.8 KiB
TypeScript
155 lines
4.8 KiB
TypeScript
import { router } from "expo-router";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { Alert, Platform } from "react-native";
|
|
|
|
import { useAccounts } from "@/contexts/AccountsContext";
|
|
import { useAppLock } from "@/contexts/AppLockContext";
|
|
import {
|
|
clearPendingShortcut,
|
|
peekPendingShortcut,
|
|
subscribeShortcutQueue,
|
|
} from "@/lib/shortcut-queue";
|
|
import {
|
|
DEFAULT_CLOCK_DESCRIPTION,
|
|
resolveClockDescription,
|
|
resolveEffectiveHourlyRate,
|
|
} from "@/lib/time-clock";
|
|
import { getLastTimeClockClientId } from "@/lib/time-clock-prefs";
|
|
import {
|
|
endTimeClockLiveActivity,
|
|
syncTimeClockLiveActivity,
|
|
} from "@/lib/time-clock-live-activity";
|
|
import type { ParsedShortcut } from "@/lib/shortcuts";
|
|
import { api } from "@/lib/trpc";
|
|
|
|
/**
|
|
* Executes queued shortcut actions once the user is signed in, unlocked, and data is ready.
|
|
*/
|
|
export function ShortcutHandler() {
|
|
const { activeAccountId } = useAccounts();
|
|
const { isLocked } = useAppLock();
|
|
const utils = api.useUtils();
|
|
const clientsQuery = api.clients.getAll.useQuery();
|
|
const runningQuery = api.timeEntries.getRunning.useQuery();
|
|
const [pending, setPending] = useState<ParsedShortcut | null>(null);
|
|
const processingRef = useRef(false);
|
|
|
|
const clockIn = api.timeEntries.clockIn.useMutation();
|
|
const clockOut = api.timeEntries.clockOut.useMutation();
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
async function refresh() {
|
|
const next = await peekPendingShortcut();
|
|
if (!cancelled) setPending(next);
|
|
}
|
|
|
|
void refresh();
|
|
return subscribeShortcutQueue(() => {
|
|
void refresh();
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!pending || !activeAccountId || isLocked) return;
|
|
if (clientsQuery.isLoading || runningQuery.isLoading) return;
|
|
if (processingRef.current) return;
|
|
|
|
processingRef.current = true;
|
|
|
|
void (async () => {
|
|
try {
|
|
if (pending.action === "open-timer") {
|
|
await clearPendingShortcut();
|
|
setPending(null);
|
|
router.push("/(app)/timer");
|
|
return;
|
|
}
|
|
|
|
if (pending.action === "clock-out") {
|
|
if (!runningQuery.data) {
|
|
await clearPendingShortcut();
|
|
setPending(null);
|
|
router.push("/(app)/timer");
|
|
if (Platform.OS === "ios") {
|
|
Alert.alert("No timer running", "There is nothing to clock out.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
await clockOut.mutateAsync({});
|
|
await endTimeClockLiveActivity();
|
|
await Promise.all([
|
|
utils.timeEntries.getRunning.invalidate(),
|
|
utils.timeEntries.getAll.invalidate(),
|
|
utils.invoices.getAll.invalidate(),
|
|
utils.dashboard.getStats.invalidate(),
|
|
]);
|
|
await clearPendingShortcut();
|
|
setPending(null);
|
|
router.push("/(app)/timer");
|
|
return;
|
|
}
|
|
|
|
if (pending.action === "clock-in") {
|
|
if (runningQuery.data) {
|
|
await clearPendingShortcut();
|
|
setPending(null);
|
|
router.push("/(app)/timer");
|
|
if (Platform.OS === "ios") {
|
|
Alert.alert("Timer already running", "Stop the current timer before clocking in again.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
const clientId =
|
|
pending.clientId || (await getLastTimeClockClientId(activeAccountId)) || "";
|
|
const client = (clientsQuery.data ?? []).find((entry) => entry.id === clientId);
|
|
const rate = resolveEffectiveHourlyRate("", client?.defaultHourlyRate);
|
|
|
|
await clockIn.mutateAsync({
|
|
clientId: clientId || "",
|
|
description: resolveClockDescription(pending.title || DEFAULT_CLOCK_DESCRIPTION),
|
|
rate: rate ?? undefined,
|
|
});
|
|
await utils.timeEntries.getRunning.invalidate();
|
|
const running = await utils.timeEntries.getRunning.fetch();
|
|
if (running) {
|
|
const seconds = Math.floor(
|
|
(Date.now() - new Date(running.startedAt).getTime()) / 1000,
|
|
);
|
|
await syncTimeClockLiveActivity(running, seconds);
|
|
}
|
|
await clearPendingShortcut();
|
|
setPending(null);
|
|
router.push("/(app)/timer");
|
|
}
|
|
} catch (err) {
|
|
await clearPendingShortcut();
|
|
setPending(null);
|
|
Alert.alert(
|
|
pending.action === "clock-out" ? "Clock out failed" : "Clock in failed",
|
|
err instanceof Error ? err.message : "Something went wrong.",
|
|
);
|
|
router.push("/(app)/timer");
|
|
} finally {
|
|
processingRef.current = false;
|
|
}
|
|
})();
|
|
}, [
|
|
activeAccountId,
|
|
clockIn,
|
|
clockOut,
|
|
clientsQuery.data,
|
|
clientsQuery.isLoading,
|
|
isLocked,
|
|
pending,
|
|
runningQuery.data,
|
|
runningQuery.isLoading,
|
|
utils,
|
|
]);
|
|
|
|
return null;
|
|
}
|