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(null); const lastLogIdRef = useRef(null); const lastTitleRef = useRef(null); const intervalIdRef = useRef(null); const finalizeTrackingRef = useRef<(() => Promise) | null>(null); const limitCheckedRef = useRef(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, }; };