250 lines
7.7 KiB
TypeScript
250 lines
7.7 KiB
TypeScript
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,
|
||
};
|
||
}; |