va-tracker/src/ui/hooks/useActiveAppTracker.tsx

250 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useRef, useCallback } from "react";
import { useDispatch } from "react-redux";
import { setActiveWindow, WindowInfo } from "../store/windowSlice";
interface DailyHours {
todayTotal: string;
dailyLimit: string;
remaining: string;
}
export const useActiveAppTracker = (
isTracking: boolean,
activeProjectLogId: string | null,
projectId: string | null,
projectName: string | null,
onLimitExceeded?: () => void // Callback when limit is reached
) => {
const dispatch = useDispatch();
const lastActiveRef = useRef<WindowInfo | null>(null);
const lastLogIdRef = useRef<string | null>(null);
const lastTitleRef = useRef<string | null>(null);
const intervalIdRef = useRef<number | null>(null);
const finalizeTrackingRef = useRef<(() => Promise<void>) | null>(null);
const limitCheckedRef = useRef<boolean>(false);
const canSend = !!(activeProjectLogId && projectId);
// ✅ Parse time string to minutes
const parseTimeToMinutes = (timeStr: string): number => {
const hourMatch = timeStr.match(/(\d+)h/);
const minMatch = timeStr.match(/(\d+)m/);
const hours = hourMatch ? parseInt(hourMatch[1]) : 0;
const minutes = minMatch ? parseInt(minMatch[1]) : 0;
return hours * 60 + minutes;
};
// ✅ Check if daily limit is exceeded
const checkDailyLimit = (dailyHours?: DailyHours): boolean => {
if (!dailyHours) return false;
const todayTotalMins = parseTimeToMinutes(dailyHours.todayTotal);
const dailyLimitMins = parseTimeToMinutes(dailyHours.dailyLimit);
const remainingMins = parseTimeToMinutes(dailyHours.remaining);
console.log("⏰ Daily Limit Check:", {
todayTotal: `${todayTotalMins} mins`,
dailyLimit: `${dailyLimitMins} mins`,
remaining: `${remainingMins} mins`,
exceeded: remainingMins <= 0
});
// If remaining time is 0 or negative, limit is exceeded
if (remainingMins <= 0) {
return true;
}
return false;
};
const sendActivityLog = async (
projectLogId: string,
projectId: string,
title: string,
appName: string,
isNew: boolean
) => {
if (!canSend) {
console.log("🚫 Skipping API — missing activeProjectLogId or projectId");
return null;
}
try {
const payload: {
id?: string;
logId: string;
projectId: string;
title: string;
appName: string;
} = {
logId: projectLogId,
projectId,
title,
appName,
};
if (!isNew && lastLogIdRef.current) {
payload.id = lastLogIdRef.current;
}
console.log(isNew ? "➡️ POST new log" : "✏️ PATCH update log", payload);
const res = await window.electron.trackingTabsOpeningClosing(payload);
console.log("📊 Activity log response:", res);
// Store the log ID for future updates
if (isNew && res?.data?._id) {
console.log("res?.data?._id", res?.data?._id)
lastLogIdRef.current = res.data._id;
}
// ✅ Check daily limit after every API call
if (res?.dailyHours) {
console.log("res?.data?.dailyHours", res?.dailyHours)
const limitExceeded = checkDailyLimit(res?.dailyHours);
if (limitExceeded && !limitCheckedRef.current) {
limitCheckedRef.current = true;
console.warn("🚨 Daily limit exceeded! Stopping tracking...");
// Trigger stop tracking callback
if (onLimitExceeded) {
setTimeout(() => {
onLimitExceeded();
}, 500);
}
}
}
return res;
} catch (error) {
console.error("❌ Activity log error:", error);
throw error;
}
};
// ✅ Final PATCH before stopping
const sendFinalPatch = useCallback(async () => {
if (!canSend) {
console.log("🚫 Skipping final PATCH — no active project log ID");
return;
}
console.log("=".repeat(60));
console.log("🛑 FINAL PATCH INITIATED - Recording last activity");
console.log("=".repeat(60));
if (lastLogIdRef.current && lastTitleRef.current && lastActiveRef.current) {
try {
console.log("📊 Final Activity Details:");
console.log(` • Window Title: ${lastTitleRef.current}`);
console.log(` • App Name: ${lastActiveRef.current.owner.name}`);
console.log(` • Log ID: ${lastLogIdRef.current}`);
console.log(` • Project ID: ${projectId}`);
const result = await sendActivityLog(
activeProjectLogId!,
projectId!,
lastTitleRef.current,
lastActiveRef.current.owner.name || "Unknown",
false // This is a PATCH (update), not a POST (new)
);
console.log("✅ Final PATCH sent successfully!");
if (result?.dailyHours) {
console.log("📈 Final Daily Hours Summary:");
console.log(` • Today Total: ${result.dailyHours.todayTotal}`);
console.log(` • Daily Limit: ${result.dailyHours.dailyLimit}`);
console.log(` • Remaining: ${result.dailyHours.remaining}`);
}
} catch (error) {
console.error("❌ Final PATCH failed:", error);
throw error; // Re-throw to let caller handle
}
} else {
console.log("ℹ️ No active window to finalize");
console.log(" • Last Log ID:", lastLogIdRef.current || "None");
console.log(" • Last Title:", lastTitleRef.current || "None");
console.log(" • Last Active Window:", lastActiveRef.current ? "Yes" : "No");
}
// Clean up all refs
console.log("🧹 Cleaning up tracking references...");
lastActiveRef.current = null;
lastTitleRef.current = null;
lastLogIdRef.current = null;
limitCheckedRef.current = false;
console.log("✅ Cleanup complete");
console.log("=".repeat(60));
}, [activeProjectLogId, projectId, canSend]);
useEffect(() => {
finalizeTrackingRef.current = sendFinalPatch;
}, [sendFinalPatch]);
useEffect(() => {
// Reset limit check when tracking starts
if (isTracking) {
limitCheckedRef.current = false;
}
if (!isTracking || !canSend) {
console.log("🟡 Tracking inactive or missing IDs — skipping tracker setup");
return;
}
console.log(`🟢 Starting active tracker for: ${projectName}`);
const fetchActiveApp = async () => {
try {
const details = (await window.electron.getActiveAppDetails()) as WindowInfo;
if (!details || !details.id) return;
const title = details.title;
const appName = details.owner.name;
const prevTitle = lastTitleRef.current;
const hasChanged = !prevTitle || prevTitle !== title;
if (hasChanged) {
if (lastLogIdRef.current && prevTitle) {
console.log("🔄 Patching previous log:", prevTitle);
await sendActivityLog(activeProjectLogId!, projectId!, prevTitle, appName, false);
}
console.log("🆕 New title detected → creating POST:", title);
await sendActivityLog(activeProjectLogId!, projectId!, title, appName, true);
lastActiveRef.current = details;
lastTitleRef.current = title;
dispatch(setActiveWindow({ window: details, projectId, projectName }));
} else {
console.log("⏸ Same window, skipping log");
}
} catch (error) {
console.error("Failed to fetch active app:", error);
}
};
fetchActiveApp();
intervalIdRef.current = setInterval(fetchActiveApp, 5000);
return () => {
console.log("🧹 Cleaning up tracker interval");
if (intervalIdRef.current) {
clearInterval(intervalIdRef.current);
intervalIdRef.current = null;
}
};
}, [isTracking, activeProjectLogId]);
return {
finalizeTracking: finalizeTrackingRef.current,
};
};