Files
dashboard/utils/dateFilter.ts
T
2026-07-29 15:02:58 +07:00

225 lines
6.1 KiB
TypeScript

/**
* Frontend Date Filtering Utilities for D-1 Logic
* Mirrors backend dateUtils for client-side awareness
*/
const CUTOFF_HOUR = 17;
const WIB_TIME_ZONE = 'Asia/Jakarta';
type WibDateParts = {
year: number;
month: number;
day: number;
hour: number;
minute: number;
second: number;
};
function getWibDateParts(referenceDate?: Date): WibDateParts {
const now = referenceDate || new Date();
const formatter = new Intl.DateTimeFormat('en-CA', {
timeZone: WIB_TIME_ZONE,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
const parts = formatter.formatToParts(now);
const lookup = Object.fromEntries(
parts.filter((part) => part.type !== 'literal').map((part) => [part.type, part.value])
) as Record<string, string>;
return {
year: Number(lookup.year),
month: Number(lookup.month),
day: Number(lookup.day),
hour: Number(lookup.hour),
minute: Number(lookup.minute),
second: Number(lookup.second),
};
}
function createLocalDateFromWibParts(parts: WibDateParts): Date {
const date = new Date(
parts.year,
parts.month - 1,
parts.day,
parts.hour,
parts.minute,
parts.second
);
date.setMilliseconds(0);
return date;
}
/**
* Get current date/time in WIB (Asia/Jakarta, UTC+7) timezone
* @param referenceDate - Optional reference date for testing
* @returns Current date in WIB
*/
export function getCurrentWibDate(referenceDate?: Date): Date {
return createLocalDateFromWibParts(getWibDateParts(referenceDate));
}
/**
* Check if current WIB time is before 17:00
* @param referenceDate - Optional reference date for testing
* @returns True if before cutoff, false otherwise
*/
export function shouldUsePreviousDay(referenceDate?: Date): boolean {
return getWibDateParts(referenceDate).hour < CUTOFF_HOUR;
}
/**
* Get effective date for data queries (D-1 or today)
* @param referenceDate - Optional reference date for testing
* @returns Effective date for queries
*/
export function getEffectiveDate(referenceDate?: Date): Date {
const wibParts = getWibDateParts(referenceDate);
const wibToday = createLocalDateFromWibParts(wibParts);
if (shouldUsePreviousDay(referenceDate)) {
// Return D-1 (previous day)
const yesterday = new Date(wibToday);
yesterday.setDate(yesterday.getDate() - 1);
return yesterday;
}
return wibToday;
}
/**
* Calculate effective cycle day with D-1 adjustment
* @param cycleStartDate - Cycle start date
* @param referenceDate - Optional reference date for testing
* @returns Effective cycle day (0-indexed)
*/
export function getEffectiveCycleDay(cycleStartDate: Date | string, referenceDate?: Date): number {
const effectiveDate = getEffectiveDate(referenceDate);
const startDate = new Date(cycleStartDate);
// Reset hours to compare dates only
effectiveDate.setHours(0, 0, 0, 0);
startDate.setHours(0, 0, 0, 0);
const diffMs = effectiveDate.getTime() - startDate.getTime();
// Return -1 if effective date is before cycle start
if (diffMs < 0) {
return -1;
}
const dayOfCycle = Math.floor(diffMs / (1000 * 60 * 60 * 24));
return dayOfCycle;
}
/**
* Format date as YYYY-MM-DD
* @param date - Date object to format
* @returns Date in YYYY-MM-DD format
*/
export function formatDateForDb(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* Format effective date label with disclaimer if D-1 active
* @param date - Date to format
* @param referenceDate - Optional reference date for testing
* @returns Formatted date string with optional disclaimer
*/
export function formatEffectiveDateLabel(date: Date, referenceDate?: Date): string {
const monthNames = [
'Januari',
'Februari',
'Maret',
'April',
'Mei',
'Juni',
'Juli',
'Agustus',
'September',
'Oktober',
'November',
'Desember',
];
const day = date.getDate();
const month = monthNames[date.getMonth()];
const year = date.getFullYear();
const baseLabel = `${day} ${month} ${year}`;
if (shouldUsePreviousDay(referenceDate)) {
return `${baseLabel} (Data Hari Kemarin)`;
}
return baseLabel;
}
/**
* Get adjusted date range for API queries (applies D-1 filter if needed)
* @param startDate - Start date string (YYYY-MM-DD)
* @param endDate - End date string (YYYY-MM-DD)
* @param referenceDate - Optional reference date for testing
* @returns Adjusted start and end dates
*/
export function getAdjustedDateRange(
startDate: string,
endDate: string,
referenceDate?: Date
): { start: string; end: string } {
// Only adjust if querying a single day and before cutoff
if (startDate === endDate && shouldUsePreviousDay(referenceDate)) {
const adjusted = getEffectiveDate(referenceDate);
const adjustedStr = formatDateForDb(adjusted);
return { start: adjustedStr, end: adjustedStr };
}
return { start: startDate, end: endDate };
}
type CycleEstimasiPanenInput = {
startDate?: Date | string | null;
endDate?: Date | string | null;
totalDays?: number | null;
};
/**
* Estimasi Panen calendar date: endDate, or startDate + (totalDays - 1).
*/
export function getEstimasiPanenDate(cycle: CycleEstimasiPanenInput): Date | null {
if (cycle.endDate) {
const end = new Date(cycle.endDate);
if (!Number.isFinite(end.getTime())) return null;
end.setHours(0, 0, 0, 0);
return end;
}
if (!cycle.startDate) return null;
const start = new Date(cycle.startDate);
if (!Number.isFinite(start.getTime())) return null;
start.setHours(0, 0, 0, 0);
start.setDate(start.getDate() + Math.max(0, (cycle.totalDays || 1) - 1));
return start;
}
/**
* Inclusive end-of-day timestamp (local) for Estimasi Panen.
* IoT readings with updatedAt after this should not be shown.
*/
export function getEstimasiPanenEndMs(cycle: CycleEstimasiPanenInput): number | null {
const panenDate = getEstimasiPanenDate(cycle);
if (!panenDate) return null;
panenDate.setHours(23, 59, 59, 999);
return panenDate.getTime();
}