Download apps/dashboard/src/utils/tracker.ts from AUXteam/Midday: direct link, hf CLI and curl.
- Browser
- Download file 11.6 kB
-
https://huggingface.co/spaces/AUXteam/Midday/resolve/main/apps/dashboard/src/utils/tracker.ts
- Command line
-
hf download hf://spaces/AUXteam/Midday/apps/dashboard/src/utils/tracker.ts
-
curl -L -o tracker.ts https://huggingface.co/spaces/AUXteam/Midday/resolve/main/apps/dashboard/src/utils/tracker.ts
11.6 kB
| import type { RouterOutputs } from "@api/trpc/routers/_app"; | |
| import { tz } from "@date-fns/tz"; | |
| import { UTCDate, utc } from "@date-fns/utc"; | |
| import { | |
| addDays, | |
| addMinutes, | |
| addSeconds, | |
| differenceInSeconds, | |
| eachDayOfInterval, | |
| format, | |
| isValid, | |
| parse, | |
| parseISO, | |
| setHours, | |
| setMinutes, | |
| } from "date-fns"; | |
| import { parseDateAsUTC } from "./date"; | |
| export const NEW_EVENT_ID = "new-event"; | |
| // API Response type from the router | |
| type ApiTrackerRecord = | |
| RouterOutputs["trackerEntries"]["byDate"]["data"][number]; | |
| // Internal tracker record type with consistent Date handling | |
| export interface TrackerRecord { | |
| id: string; | |
| date: string | null; | |
| description: string | null; | |
| duration: number | null; | |
| start: Date; | |
| stop: Date; | |
| user: { | |
| id: string; | |
| fullName: string | null; | |
| avatarUrl: string | null; | |
| } | null; | |
| trackerProject: { | |
| id: string; | |
| name: string; | |
| currency: string | null; | |
| rate: number | null; | |
| customer: { | |
| id: string; | |
| name: string; | |
| } | null; | |
| } | null; | |
| } | |
| /** | |
| * Creates a safe Date using UTCDate for better UTC handling | |
| */ | |
| export const createSafeDate = ( | |
| dateInput: string | Date | null | undefined, | |
| fallback?: Date, | |
| ): Date => { | |
| if (!dateInput) return fallback || new UTCDate(); | |
| if (typeof dateInput === "string") { | |
| // Try parseISO first (handles ISO 8601 formats) | |
| const date = parseISO(dateInput); | |
| if (isValid(date)) { | |
| return date; | |
| } | |
| // Try UTCDate constructor as final fallback | |
| try { | |
| const utcDate = utc(dateInput); | |
| if (isValid(utcDate)) { | |
| return new Date(utcDate.getTime()); | |
| } | |
| } catch (error) { | |
| console.warn("Date parsing failed:", error); | |
| } | |
| return fallback || new UTCDate(); | |
| } | |
| return isValid(dateInput) ? dateInput : fallback || new UTCDate(); | |
| }; | |
| /** | |
| * Format time from date with optional timezone support | |
| */ | |
| export const formatTimeFromDate = ( | |
| date: Date | string | null, | |
| timezone?: string, | |
| ): string => { | |
| const safeDate = createSafeDate(date); | |
| if (timezone && timezone !== "UTC") { | |
| try { | |
| const createTZDate = tz(timezone); | |
| const tzDate = createTZDate(safeDate); | |
| return format(tzDate, "HH:mm"); | |
| } catch (error) { | |
| console.warn("Timezone formatting failed:", error); | |
| } | |
| } | |
| return format(safeDate, "HH:mm"); | |
| }; | |
| /** | |
| * Parse time with midnight crossing support using timezone-aware parsing | |
| */ | |
| export const parseTimeWithMidnightCrossing = ( | |
| startTime: string, | |
| stopTime: string, | |
| baseDate: Date, | |
| timezone?: string, | |
| ): { start: Date; stop: Date; duration: number } => { | |
| if (timezone && timezone !== "UTC") { | |
| try { | |
| const createTZDate = tz(timezone); | |
| // Create timezone-aware base date | |
| const tzBaseDate = createTZDate(baseDate); | |
| // Parse times in the timezone context | |
| const startDate = parse(startTime, "HH:mm", tzBaseDate); | |
| let stopDate = parse(stopTime, "HH:mm", tzBaseDate); | |
| // If stop time is before start time, assume it's on the next day | |
| if (stopDate < startDate) { | |
| stopDate = addDays(stopDate, 1); | |
| } | |
| const duration = differenceInSeconds(stopDate, startDate); | |
| return { | |
| start: new Date(startDate.getTime()), | |
| stop: new Date(stopDate.getTime()), | |
| duration, | |
| }; | |
| } catch (error) { | |
| console.warn("Timezone time parsing failed:", error); | |
| } | |
| } | |
| // Fallback to UTC parsing | |
| const startDate = parse(startTime, "HH:mm", baseDate); | |
| let stopDate = parse(stopTime, "HH:mm", baseDate); | |
| // If stop time is before start time, assume it's on the next day | |
| if (stopDate < startDate) { | |
| stopDate = addDays(stopDate, 1); | |
| } | |
| const duration = differenceInSeconds(stopDate, startDate); | |
| return { start: startDate, stop: stopDate, duration }; | |
| }; | |
| /** | |
| * Get slot from date with timezone support (already updated) | |
| */ | |
| export const getSlotFromDate = ( | |
| date: Date | string | null, | |
| timezone?: string, | |
| ): number => { | |
| const safeDate = createSafeDate(date); | |
| if (timezone && timezone !== "UTC") { | |
| try { | |
| // Use tz() function to create timezone-aware date | |
| const createTZDate = tz(timezone); | |
| const tzDate = createTZDate(safeDate); | |
| return tzDate.getHours() * 4 + Math.floor(tzDate.getMinutes() / 15); | |
| } catch (error) { | |
| console.warn("TZDate slot calculation failed:", error); | |
| // Fallback to browser timezone | |
| } | |
| } | |
| // Fallback to browser timezone (for backward compatibility) | |
| return safeDate.getHours() * 4 + Math.floor(safeDate.getMinutes() / 15); | |
| }; | |
| /** | |
| * Calculate duration between dates with timezone support | |
| */ | |
| export const calculateDuration = ( | |
| start: Date | string | null, | |
| stop: Date | string | null, | |
| ): number => { | |
| const startDate = createSafeDate(start); | |
| const stopDate = createSafeDate(stop); | |
| // If stop is before start, assume stop is on the next day | |
| if (stopDate < startDate) { | |
| const nextDayStop = addDays(stopDate, 1); | |
| return differenceInSeconds(nextDayStop, startDate); | |
| } | |
| return differenceInSeconds(stopDate, startDate); | |
| }; | |
| /** | |
| * Format hour with timezone support | |
| */ | |
| export const formatHour = ( | |
| hour: number, | |
| timeFormat?: number | null, | |
| _timezone?: string, | |
| ) => { | |
| // Create a simple date with the hour - no timezone conversion needed for labels | |
| const date = new Date(2024, 0, 1, hour, 0, 0, 0); // Use arbitrary date, just set the hour | |
| return format(date, timeFormat === 12 ? "hh:mm a" : "HH:mm"); | |
| }; | |
| /** | |
| * Create new event with timezone-aware time creation | |
| */ | |
| export const createNewEvent = ( | |
| slot: number, | |
| selectedProjectId: string | null, | |
| selectedDate?: string | null, | |
| timezone?: string, | |
| ): TrackerRecord => { | |
| // Parse as UTC calendar date to avoid timezone shift | |
| const baseDate = selectedDate ? parseDateAsUTC(selectedDate) : new UTCDate(); | |
| // Use the original date string directly if available | |
| const dateStr = selectedDate || format(baseDate, "yyyy-MM-dd"); | |
| if (timezone && timezone !== "UTC") { | |
| try { | |
| const createTZDate = tz(timezone); | |
| const tzBaseDate = createTZDate(baseDate); | |
| const startDate = setMinutes( | |
| setHours(tzBaseDate, Math.floor(slot / 4)), | |
| (slot % 4) * 15, | |
| ); | |
| const endDate = addMinutes(startDate, 15); | |
| // When selectedDate is null, compute date from tzBaseDate (user's local date) | |
| // to avoid UTC date mismatch (e.g., 11 PM local vs 4 AM UTC next day) | |
| const tzDateStr = selectedDate || format(tzBaseDate, "yyyy-MM-dd"); | |
| return { | |
| id: NEW_EVENT_ID, | |
| date: tzDateStr, | |
| description: null, | |
| duration: 15 * 60, // 15 minutes in seconds | |
| start: new Date(startDate.getTime()), | |
| stop: new Date(endDate.getTime()), | |
| user: null, | |
| trackerProject: selectedProjectId | |
| ? { | |
| id: selectedProjectId, | |
| name: "", | |
| currency: null, | |
| rate: null, | |
| customer: null, | |
| } | |
| : null, | |
| }; | |
| } catch (error) { | |
| console.warn("Timezone event creation failed:", error); | |
| } | |
| } | |
| // Fallback to UTC creation | |
| const startDate = setMinutes( | |
| setHours(baseDate, Math.floor(slot / 4)), | |
| (slot % 4) * 15, | |
| ); | |
| const endDate = addMinutes(startDate, 15); | |
| return { | |
| id: NEW_EVENT_ID, | |
| date: dateStr, | |
| description: null, | |
| duration: 15 * 60, // 15 minutes in seconds | |
| start: startDate, | |
| stop: endDate, | |
| user: null, | |
| trackerProject: selectedProjectId | |
| ? { | |
| id: selectedProjectId, | |
| name: "", | |
| currency: null, | |
| rate: null, | |
| customer: null, | |
| } | |
| : null, | |
| }; | |
| }; | |
| // Tracker record transformation | |
| export const transformApiRecord = ( | |
| apiRecord: ApiTrackerRecord, | |
| selectedDate: string | null, | |
| ): TrackerRecord => { | |
| const start = apiRecord.start | |
| ? parseISO(apiRecord.start) | |
| : parseISO(`${apiRecord.date || selectedDate}T09:00:00`); | |
| const stop = apiRecord.stop | |
| ? parseISO(apiRecord.stop) | |
| : addSeconds(start, apiRecord.duration || 0); | |
| return { | |
| id: apiRecord.id, | |
| date: apiRecord.date, | |
| description: apiRecord.description, | |
| duration: apiRecord.duration, | |
| start: isValid(start) ? start : new Date(), | |
| stop: isValid(stop) | |
| ? stop | |
| : addMinutes(isValid(start) ? start : new Date(), 15), | |
| user: apiRecord.user, | |
| trackerProject: apiRecord.trackerProject | |
| ? { | |
| id: apiRecord.trackerProject.id, | |
| name: apiRecord.trackerProject.name || "", | |
| currency: apiRecord.trackerProject.currency, | |
| rate: apiRecord.trackerProject.rate, | |
| customer: apiRecord.trackerProject.customer, | |
| } | |
| : null, | |
| }; | |
| }; | |
| export const updateEventTime = ( | |
| event: TrackerRecord, | |
| start: Date, | |
| stop: Date, | |
| ): TrackerRecord => { | |
| return { | |
| ...event, | |
| start: isValid(start) ? start : event.start, | |
| stop: isValid(stop) ? stop : event.stop, | |
| duration: calculateDuration(start, stop), | |
| }; | |
| }; | |
| // Date range utilities | |
| export function sortDates(dates: string[]) { | |
| return dates.sort((a, b) => parseISO(a).getTime() - parseISO(b).getTime()); | |
| } | |
| export function getTrackerDates( | |
| range: string[] | null, | |
| selectedDate: string | null, | |
| ): Date[] { | |
| if (range) { | |
| // Parse as UTC calendar dates to avoid timezone shift | |
| return sortDates(range).map((dateString) => parseDateAsUTC(dateString)); | |
| } | |
| if (selectedDate) { | |
| // Parse as UTC calendar date to avoid timezone shift | |
| return [parseDateAsUTC(selectedDate)]; | |
| } | |
| return [new Date()]; | |
| } | |
| export const getDates = ( | |
| selectedDate: string | null, | |
| sortedRange: string[] | null, | |
| ): string[] => { | |
| if (selectedDate) return [selectedDate]; | |
| if (sortedRange && sortedRange.length === 2) { | |
| const [start, end] = sortedRange; | |
| if (start && end) { | |
| return eachDayOfInterval({ | |
| start: parseISO(start), | |
| end: parseISO(end), | |
| }).map((date) => format(date, "yyyy-MM-dd")); | |
| } | |
| } | |
| return []; | |
| }; | |
| // Validation utilities | |
| export const isValidTimeSlot = (slot: number): boolean => { | |
| return slot >= 0 && slot < 96; // 24 hours * 4 slots per hour | |
| }; | |
| export const isValidDateString = (dateStr: string): boolean => { | |
| return isValid(parseISO(dateStr)); | |
| }; | |
| // Form data conversion utilities | |
| export const convertToFormData = (record: TrackerRecord) => { | |
| return { | |
| id: record.id === NEW_EVENT_ID ? undefined : record.id, | |
| start: formatTimeFromDate(record.start), | |
| stop: formatTimeFromDate(record.stop), | |
| projectId: record.trackerProject?.id || "", | |
| description: record.description || "", | |
| duration: calculateDuration(record.start, record.stop), | |
| }; | |
| }; | |
| export const convertFromFormData = ( | |
| formData: { | |
| id?: string; | |
| start: string; | |
| stop: string; | |
| projectId: string; | |
| assignedId?: string; | |
| description?: string; | |
| duration: number; | |
| }, | |
| baseDate: Date, | |
| dates: string[], | |
| timezone?: string, // Add timezone parameter | |
| ): { | |
| id?: string; | |
| start: string; | |
| stop: string; | |
| dates: string[]; | |
| assignedId: string | null; | |
| projectId: string; | |
| description: string | null; | |
| duration: number; | |
| } => { | |
| const { | |
| start: startDate, | |
| stop: stopDate, | |
| duration, | |
| } = parseTimeWithMidnightCrossing( | |
| formData.start, | |
| formData.stop, | |
| baseDate, | |
| timezone, | |
| ); | |
| return { | |
| id: formData.id === NEW_EVENT_ID ? undefined : formData.id, | |
| start: startDate.toISOString(), | |
| stop: stopDate.toISOString(), | |
| dates, | |
| assignedId: formData.assignedId || null, | |
| projectId: formData.projectId, | |
| description: formData.description || null, | |
| duration: duration, | |
| }; | |
| }; | |