| 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"; |
|
|
| |
| type ApiTrackerRecord = |
| RouterOutputs["trackerEntries"]["byDate"]["data"][number]; |
|
|
| |
| 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; |
| } |
|
|
| |
| |
| |
| export const createSafeDate = ( |
| dateInput: string | Date | null | undefined, |
| fallback?: Date, |
| ): Date => { |
| if (!dateInput) return fallback || new UTCDate(); |
|
|
| if (typeof dateInput === "string") { |
| |
| const date = parseISO(dateInput); |
| if (isValid(date)) { |
| return date; |
| } |
|
|
| |
| 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(); |
| }; |
|
|
| |
| |
| |
| 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"); |
| }; |
|
|
| |
| |
| |
| 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); |
|
|
| |
| const tzBaseDate = createTZDate(baseDate); |
|
|
| |
| const startDate = parse(startTime, "HH:mm", tzBaseDate); |
| let stopDate = parse(stopTime, "HH:mm", tzBaseDate); |
|
|
| |
| 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); |
| } |
| } |
|
|
| |
| const startDate = parse(startTime, "HH:mm", baseDate); |
| let stopDate = parse(stopTime, "HH:mm", baseDate); |
|
|
| |
| if (stopDate < startDate) { |
| stopDate = addDays(stopDate, 1); |
| } |
|
|
| const duration = differenceInSeconds(stopDate, startDate); |
|
|
| return { start: startDate, stop: stopDate, duration }; |
| }; |
|
|
| |
| |
| |
| export const getSlotFromDate = ( |
| date: Date | string | null, |
| timezone?: string, |
| ): number => { |
| const safeDate = createSafeDate(date); |
|
|
| if (timezone && timezone !== "UTC") { |
| try { |
| |
| 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); |
| |
| } |
| } |
|
|
| |
| return safeDate.getHours() * 4 + Math.floor(safeDate.getMinutes() / 15); |
| }; |
|
|
| |
| |
| |
| export const calculateDuration = ( |
| start: Date | string | null, |
| stop: Date | string | null, |
| ): number => { |
| const startDate = createSafeDate(start); |
| const stopDate = createSafeDate(stop); |
|
|
| |
| if (stopDate < startDate) { |
| const nextDayStop = addDays(stopDate, 1); |
| return differenceInSeconds(nextDayStop, startDate); |
| } |
|
|
| return differenceInSeconds(stopDate, startDate); |
| }; |
|
|
| |
| |
| |
| export const formatHour = ( |
| hour: number, |
| timeFormat?: number | null, |
| _timezone?: string, |
| ) => { |
| |
| const date = new Date(2024, 0, 1, hour, 0, 0, 0); |
| return format(date, timeFormat === 12 ? "hh:mm a" : "HH:mm"); |
| }; |
|
|
| |
| |
| |
| export const createNewEvent = ( |
| slot: number, |
| selectedProjectId: string | null, |
| selectedDate?: string | null, |
| timezone?: string, |
| ): TrackerRecord => { |
| |
| const baseDate = selectedDate ? parseDateAsUTC(selectedDate) : new UTCDate(); |
| |
| 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); |
|
|
| |
| |
| const tzDateStr = selectedDate || format(tzBaseDate, "yyyy-MM-dd"); |
|
|
| return { |
| id: NEW_EVENT_ID, |
| date: tzDateStr, |
| description: null, |
| duration: 15 * 60, |
| 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); |
| } |
| } |
|
|
| |
| 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, |
| start: startDate, |
| stop: endDate, |
| user: null, |
| trackerProject: selectedProjectId |
| ? { |
| id: selectedProjectId, |
| name: "", |
| currency: null, |
| rate: null, |
| customer: null, |
| } |
| : null, |
| }; |
| }; |
|
|
| |
| 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), |
| }; |
| }; |
|
|
| |
| 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) { |
| |
| return sortDates(range).map((dateString) => parseDateAsUTC(dateString)); |
| } |
|
|
| if (selectedDate) { |
| |
| 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 []; |
| }; |
|
|
| |
| export const isValidTimeSlot = (slot: number): boolean => { |
| return slot >= 0 && slot < 96; |
| }; |
|
|
| export const isValidDateString = (dateStr: string): boolean => { |
| return isValid(parseISO(dateStr)); |
| }; |
|
|
| |
| 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, |
| ): { |
| 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, |
| }; |
| }; |
|
|