fix: resolve IST/UTC timezone scheduling delta and enable syllabus tracker hot-reload on JEE+NEET switch
This commit is contained in:
@@ -4,8 +4,9 @@ from typing import Generator
|
||||
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://pragativeer_admin:local_secure_password_2026@postgres:5432/pragativeer_prod")
|
||||
|
||||
# Using the standard SQLModel engine config for PostgreSQL
|
||||
engine = create_engine(DATABASE_URL, echo=True)
|
||||
connect_args = {"options": "-c timezone=Asia/Kolkata"} if "postgresql" in DATABASE_URL else {}
|
||||
# Using the standard SQLModel engine config for PostgreSQL with IST timezone session
|
||||
engine = create_engine(DATABASE_URL, echo=True, connect_args=connect_args)
|
||||
|
||||
def init_db():
|
||||
# Automatically generates tables based on the classes imported from models.py
|
||||
|
||||
@@ -12,6 +12,7 @@ services:
|
||||
POSTGRES_USER: pragativeer_admin
|
||||
POSTGRES_PASSWORD: local_secure_password_2026
|
||||
POSTGRES_DB: pragativeer_prod
|
||||
TZ: Asia/Kolkata
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
@@ -79,6 +80,7 @@ services:
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- TZ=Asia/Kolkata
|
||||
- DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@pgbouncer:5432/pragativeer_prod
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- ENV=development
|
||||
@@ -108,6 +110,7 @@ services:
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
environment:
|
||||
- TZ=Asia/Kolkata
|
||||
- DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@pgbouncer:5432/pragativeer_prod
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- MINIO_ENDPOINT=minio:9000
|
||||
@@ -135,6 +138,7 @@ services:
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
environment:
|
||||
- TZ=Asia/Kolkata
|
||||
- DATABASE_URL=postgresql://pragativeer_admin:local_secure_password_2026@pgbouncer:5432/pragativeer_prod
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
env_file:
|
||||
@@ -160,6 +164,7 @@ services:
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- TZ=Asia/Kolkata
|
||||
- NEXT_PUBLIC_API_URL=http://backend:8000
|
||||
- NEXT_PUBLIC_PAYMENT_GATEWAY=razorpay
|
||||
depends_on:
|
||||
|
||||
@@ -18,6 +18,7 @@ import { z } from "zod"
|
||||
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
import { CalendarFilter, getLocalDateString } from "@/components/CalendarFilter"
|
||||
import { parseAsISTDate, formatISTDateTime, toISTISOString } from "@/lib/dateUtils"
|
||||
|
||||
// Permissive, auto-normalizing Zod schema for client-side question parsing
|
||||
const QuestionSchema = z.preprocess((val: any) => {
|
||||
@@ -188,8 +189,8 @@ export default function AdminTestsPage() {
|
||||
const openScheduleModal = (test: any) => {
|
||||
setEditingTest(test)
|
||||
setEditTitle(test.title || "")
|
||||
setEditScheduledStart(test.scheduled_date ? new Date(test.scheduled_date) : null)
|
||||
setEditScheduledEnd(test.end_date ? new Date(test.end_date) : null)
|
||||
setEditScheduledStart(parseAsISTDate(test.scheduled_date))
|
||||
setEditScheduledEnd(parseAsISTDate(test.end_date))
|
||||
setEditIsActive(test.is_active ?? true)
|
||||
}
|
||||
|
||||
@@ -202,8 +203,8 @@ export default function AdminTestsPage() {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
title: editTitle,
|
||||
scheduled_date: editScheduledStart ? editScheduledStart.toISOString() : null,
|
||||
end_date: editScheduledEnd ? editScheduledEnd.toISOString() : null,
|
||||
scheduled_date: toISTISOString(editScheduledStart),
|
||||
end_date: toISTISOString(editScheduledEnd),
|
||||
is_active: editIsActive
|
||||
})
|
||||
})
|
||||
@@ -342,8 +343,8 @@ export default function AdminTestsPage() {
|
||||
formData.append("exam_pattern", examPattern)
|
||||
formData.append("time_limit", timeLimit)
|
||||
formData.append("admin_id", effectiveAdminId)
|
||||
if (scheduledDate) formData.append("scheduled_date", scheduledDate.toISOString())
|
||||
if (endDate) formData.append("end_date", endDate.toISOString())
|
||||
if (scheduledDate) formData.append("scheduled_date", toISTISOString(scheduledDate)!)
|
||||
if (endDate) formData.append("end_date", toISTISOString(endDate)!)
|
||||
|
||||
const res = await fetchWithAuth("/api/admin/tests/upload", {
|
||||
method: "POST",
|
||||
@@ -382,8 +383,8 @@ export default function AdminTestsPage() {
|
||||
time_limit: parseInt(timeLimit),
|
||||
admin_id: effectiveAdminId,
|
||||
questions_json: validQuestions,
|
||||
scheduled_date: scheduledDate ? scheduledDate.toISOString() : null,
|
||||
end_date: endDate ? endDate.toISOString() : null,
|
||||
scheduled_date: toISTISOString(scheduledDate),
|
||||
end_date: toISTISOString(endDate),
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -436,10 +437,7 @@ export default function AdminTestsPage() {
|
||||
}, [tests, adminTypeFilter, adminDateFilter])
|
||||
|
||||
const formatScheduleDateTime = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleString("en-US", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short"
|
||||
})
|
||||
return formatISTDateTime(dateStr)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -681,8 +679,8 @@ export default function AdminTestsPage() {
|
||||
<div className="space-y-3.5">
|
||||
{filteredTests.map(test => {
|
||||
const now = new Date()
|
||||
const startTime = test.scheduled_date ? new Date(test.scheduled_date) : null
|
||||
const endTime = test.end_date ? new Date(test.end_date) : null
|
||||
const startTime = parseAsISTDate(test.scheduled_date)
|
||||
const endTime = parseAsISTDate(test.end_date)
|
||||
const isUpcoming = startTime ? startTime > now : false
|
||||
const isExpired = endTime ? now > endTime : false
|
||||
const isLive = !isUpcoming && !isExpired
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Scratchpad } from "@/components/CBT/Scratchpad"
|
||||
import { PeriodicTable } from "@/components/CBT/PeriodicTable"
|
||||
import { ExamResult } from "@/components/CBT/ExamResult"
|
||||
import { fetchWithAuth } from "@/lib/apiClient"
|
||||
import { parseAsISTDate, formatISTDateTime } from "@/lib/dateUtils"
|
||||
|
||||
// Dummy data for testing the CBT client engine
|
||||
const DUMMY_QUESTIONS = Array.from({ length: 20 }).map((_, i) => ({
|
||||
@@ -103,15 +104,17 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
if (!testRes.ok) throw new Error("Failed to fetch test structure");
|
||||
const testData = await testRes.json();
|
||||
|
||||
// Strict boundary checking
|
||||
// Strict boundary checking (IST aware)
|
||||
const now = new Date();
|
||||
if (testData.scheduled_date && new Date(testData.scheduled_date) > now) {
|
||||
setAccessError(`This exam is scheduled for ${new Date(testData.scheduled_date).toLocaleString()}. You cannot start it yet.`);
|
||||
const scheduledTime = parseAsISTDate(testData.scheduled_date);
|
||||
const endTime = parseAsISTDate(testData.end_date);
|
||||
if (scheduledTime && scheduledTime > now) {
|
||||
setAccessError(`This exam is scheduled for ${formatISTDateTime(testData.scheduled_date)}. You cannot start it yet.`);
|
||||
setMounted(true);
|
||||
return;
|
||||
}
|
||||
if (testData.end_date && new Date(testData.end_date) < now) {
|
||||
setAccessError(`This exam expired on ${new Date(testData.end_date).toLocaleString()} and is no longer available.`);
|
||||
if (endTime && endTime < now) {
|
||||
setAccessError(`This exam expired on ${formatISTDateTime(testData.end_date)} and is no longer available.`);
|
||||
setMounted(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useEffect, useState, useMemo } from "react"
|
||||
import Link from "next/link"
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
import { CalendarFilter, getLocalDateString } from "@/components/CalendarFilter"
|
||||
import { formatISTDateTime, formatISTTime, parseAsISTDate } from "@/lib/dateUtils"
|
||||
|
||||
export default function DPPPage() {
|
||||
const { user, userId, hasAccessTo } = useActiveUser()
|
||||
@@ -100,24 +101,8 @@ export default function DPPPage() {
|
||||
return true;
|
||||
});
|
||||
|
||||
const formatDateTime = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleString("en-US", {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
}
|
||||
|
||||
const formatTime = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleTimeString("en-US", {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
}
|
||||
const formatDateTime = (dateStr: string) => formatISTDateTime(dateStr);
|
||||
const formatTime = (dateStr: string) => formatISTTime(dateStr);
|
||||
|
||||
const getPdfUrl = (title: string) => {
|
||||
const t = title.toLowerCase();
|
||||
@@ -190,10 +175,10 @@ export default function DPPPage() {
|
||||
const attempt = attemptsByTest[dpp.id];
|
||||
const isAttempted = !!attempt;
|
||||
|
||||
// Date & Time logic
|
||||
// Date & Time logic (IST aware)
|
||||
const now = new Date();
|
||||
const scheduledTime = dpp.scheduled_date ? new Date(dpp.scheduled_date) : null;
|
||||
const endTime = dpp.end_date ? new Date(dpp.end_date) : null;
|
||||
const scheduledTime = parseAsISTDate(dpp.scheduled_date);
|
||||
const endTime = parseAsISTDate(dpp.end_date);
|
||||
const isUpcoming = scheduledTime ? scheduledTime > now : false;
|
||||
const isExpired = endTime ? now > endTime : false;
|
||||
const isLive = !isUpcoming && !isExpired;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { PremiumGate } from "@/components/PremiumGate";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { CalendarFilter, getLocalDateString } from "@/components/CalendarFilter";
|
||||
import { formatISTDateTime, formatISTDate, formatISTTime, parseAsISTDate } from "@/lib/dateUtils";
|
||||
|
||||
interface TestData {
|
||||
id: string;
|
||||
@@ -114,32 +115,9 @@ export default function CBTPage() {
|
||||
return true;
|
||||
});
|
||||
|
||||
const formatDateTime = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleString("en-US", {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
}
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleDateString("en-US", {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
const formatTime = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleTimeString("en-US", {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
}
|
||||
const formatDateTime = (dateStr: string) => formatISTDateTime(dateStr);
|
||||
const formatDate = (dateStr: string) => formatISTDate(dateStr);
|
||||
const formatTime = (dateStr: string) => formatISTTime(dateStr);
|
||||
|
||||
// Get the highest score for each test if attempted multiple times
|
||||
const attemptsByTest = attempts.reduce((acc, curr) => {
|
||||
@@ -234,8 +212,8 @@ export default function CBTPage() {
|
||||
|
||||
// Date logic
|
||||
const now = new Date();
|
||||
const scheduledTime = exam.scheduled_date ? new Date(exam.scheduled_date) : now;
|
||||
const endTime = exam.end_date ? new Date(exam.end_date) : null;
|
||||
const scheduledTime = parseAsISTDate(exam.scheduled_date) || now;
|
||||
const endTime = parseAsISTDate(exam.end_date);
|
||||
const isUpcoming = scheduledTime > now;
|
||||
const isExpired = endTime ? now > endTime : false;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useEffect, useState, useMemo } from "react"
|
||||
import Link from "next/link"
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
import { CalendarFilter, getLocalDateString } from "@/components/CalendarFilter"
|
||||
import { formatISTDateTime, formatISTTime, parseAsISTDate } from "@/lib/dateUtils"
|
||||
|
||||
export default function AIDailyPracticePage() {
|
||||
const { user, userId, hasAccessTo } = useActiveUser()
|
||||
@@ -102,24 +103,8 @@ export default function AIDailyPracticePage() {
|
||||
return true;
|
||||
});
|
||||
|
||||
const formatDateTime = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleString("en-US", {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
}
|
||||
|
||||
const formatTime = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleTimeString("en-US", {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
}
|
||||
const formatDateTime = (dateStr: string) => formatISTDateTime(dateStr);
|
||||
const formatTime = (dateStr: string) => formatISTTime(dateStr);
|
||||
|
||||
const getPdfUrl = (title: string) => {
|
||||
const t = title.toLowerCase();
|
||||
@@ -188,10 +173,10 @@ export default function AIDailyPracticePage() {
|
||||
const attempt = attemptsByTest[dpp.id];
|
||||
const isAttempted = !!attempt;
|
||||
|
||||
// Accurate Date & Time logic
|
||||
// Accurate Date & Time logic (IST aware)
|
||||
const now = new Date();
|
||||
const scheduledTime = dpp.scheduled_date ? new Date(dpp.scheduled_date) : null;
|
||||
const endTime = dpp.end_date ? new Date(dpp.end_date) : null;
|
||||
const scheduledTime = parseAsISTDate(dpp.scheduled_date);
|
||||
const endTime = parseAsISTDate(dpp.end_date);
|
||||
const isUpcoming = scheduledTime ? scheduledTime > now : false;
|
||||
const isExpired = endTime ? now > endTime : false;
|
||||
const isLive = !isUpcoming && !isExpired;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { BrainCircuit, Play, ChevronRight, Activity, Target, CheckCircle2, Alert
|
||||
import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
|
||||
import { AdaptiveStudyPlan } from "@/components/AI/AdaptiveStudyPlan"
|
||||
import { useActiveUser, isDemoUser, DEFAULT_USER_ID } from "@/hooks/useActiveUser"
|
||||
import { formatISTDate } from "@/lib/dateUtils"
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { user, userId, loading: userLoading, hasAccessTo, trialInfo } = useActiveUser()
|
||||
@@ -316,7 +317,7 @@ export default function DashboardPage() {
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Badge variant="outline" className="border-primary/20 bg-primary/5 text-primary">{cbt.exam_pattern}</Badge>
|
||||
<span className="text-xs text-muted-foreground font-semibold uppercase tracking-wider">{cbt.scheduled_date ? new Date(cbt.scheduled_date).toLocaleDateString() : 'Available Now'}</span>
|
||||
<span className="text-xs text-muted-foreground font-semibold uppercase tracking-wider">{cbt.scheduled_date ? formatISTDate(cbt.scheduled_date) : 'Available Now'}</span>
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg tracking-tight">{cbt.title}</h3>
|
||||
<p className="text-sm text-muted-foreground flex items-center gap-1.5 mt-1.5">
|
||||
|
||||
@@ -252,7 +252,27 @@ export default function ProgressTrackerPage() {
|
||||
|
||||
useEffect(() => {
|
||||
setSubjects(getInitialSubjects(isDemo, user?.target_exam || "JEE_Main"));
|
||||
}, [isDemo, user?.target_exam]);
|
||||
|
||||
const handleSwitched = (e: any) => {
|
||||
const switchedUser = e.detail?.user;
|
||||
const switchedId = e.detail?.userId;
|
||||
const exam = switchedUser?.target_exam || (switchedId === "33333333-3333-3333-3333-333333333333" ? "JEE_NEET" : "JEE_Main");
|
||||
const demo = isDemoUser(switchedId);
|
||||
setSubjects(getInitialSubjects(demo, exam));
|
||||
};
|
||||
|
||||
const handleUpdated = () => {
|
||||
setSubjects(getInitialSubjects(isDemoUser(userId), user?.target_exam || "JEE_Main"));
|
||||
};
|
||||
|
||||
window.addEventListener("pragativeer-user-switched", handleSwitched);
|
||||
window.addEventListener("pragativeer-user-updated", handleUpdated);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pragativeer-user-switched", handleSwitched);
|
||||
window.removeEventListener("pragativeer-user-updated", handleUpdated);
|
||||
};
|
||||
}, [isDemo, user?.target_exam, userId]);
|
||||
|
||||
if (!hasAccessTo("chapter-wise-progress-tracking") && !isDemo) {
|
||||
return (
|
||||
|
||||
@@ -13,15 +13,11 @@ export interface CalendarFilterProps {
|
||||
align?: "left" | "right"
|
||||
}
|
||||
|
||||
/** Helper to format any date safely in local time as "YYYY-MM-DD" */
|
||||
import { getISTDateString } from "@/lib/dateUtils"
|
||||
|
||||
/** Helper to format any date safely in IST (Asia/Kolkata) as "YYYY-MM-DD" */
|
||||
export function getLocalDateString(dateInput: string | Date | null | undefined): string {
|
||||
if (!dateInput) return ""
|
||||
const d = typeof dateInput === "string" ? new Date(dateInput) : dateInput
|
||||
if (isNaN(d.getTime())) return ""
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, "0")
|
||||
const day = String(d.getDate()).padStart(2, "0")
|
||||
return `${y}-${m}-${day}`
|
||||
return getISTDateString(dateInput)
|
||||
}
|
||||
|
||||
export function CalendarFilter({
|
||||
|
||||
@@ -361,7 +361,23 @@ export function useActiveUser() {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const handleUpdate = () => fetchUserProfile(activeUserIdRef.current);
|
||||
const handleSwitched = (e: any) => {
|
||||
const switchedId = e.detail?.userId;
|
||||
const switchedUser = e.detail?.user;
|
||||
if (switchedId) {
|
||||
activeUserIdRef.current = switchedId;
|
||||
setUserIdState(switchedId);
|
||||
if (switchedUser) {
|
||||
cachedUserProfile = switchedUser;
|
||||
setUser(switchedUser);
|
||||
} else {
|
||||
fetchUserProfile(switchedId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("pragativeer-user-updated", handleUpdate);
|
||||
window.addEventListener("pragativeer-user-switched", handleSwitched);
|
||||
|
||||
// Poll every 30s (less aggressive, kinder on backend)
|
||||
const interval = setInterval(() => {
|
||||
@@ -370,6 +386,7 @@ export function useActiveUser() {
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pragativeer-user-updated", handleUpdate);
|
||||
window.removeEventListener("pragativeer-user-switched", handleSwitched);
|
||||
clearInterval(interval);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
108
src/lib/dateUtils.ts
Normal file
108
src/lib/dateUtils.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Unified Date & Time utilities configured for Indian Standard Time (IST, Asia/Kolkata).
|
||||
* Pragativeer is an Indian EdTech platform where all test and DPP scheduling,
|
||||
* calendar bounds, and student-facing timestamps operate strictly in IST.
|
||||
*/
|
||||
|
||||
export const TIMEZONE_IST = "Asia/Kolkata";
|
||||
|
||||
/**
|
||||
* Safely parse any date input (string, Date, or ISO string) into a Date object.
|
||||
* If the string lacks an explicit timezone offset or Zulu indicator, it is
|
||||
* treated as an Indian Standard Time (IST) timestamp (+05:30) so that server
|
||||
* environments running in UTC do not shift the time by 5h 30m.
|
||||
*/
|
||||
export function parseAsISTDate(dateInput: string | Date | null | undefined): Date | null {
|
||||
if (!dateInput) return null;
|
||||
if (dateInput instanceof Date) {
|
||||
return isNaN(dateInput.getTime()) ? null : dateInput;
|
||||
}
|
||||
let str = String(dateInput).trim();
|
||||
if (!str) return null;
|
||||
|
||||
// If string has no timezone indicator (no 'Z' and no explicit +HH:MM or -HH:MM):
|
||||
// e.g. "2026-10-24T05:30:00" or "2026-10-24 05:30:00"
|
||||
if (!str.endsWith("Z") && !/[+-]\d{2}:?\d{2}$/.test(str)) {
|
||||
str = str.replace(" ", "T");
|
||||
str = `${str}+05:30`;
|
||||
}
|
||||
const d = new Date(str);
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a date in IST as "Oct 24, 2026, 5:30 AM"
|
||||
*/
|
||||
export function formatISTDateTime(dateInput: string | Date | null | undefined): string {
|
||||
const d = parseAsISTDate(dateInput);
|
||||
if (!d) return "";
|
||||
return d.toLocaleString("en-US", {
|
||||
timeZone: TIMEZONE_IST,
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a date in IST as "Oct 24, 2026"
|
||||
*/
|
||||
export function formatISTDate(dateInput: string | Date | null | undefined): string {
|
||||
const d = parseAsISTDate(dateInput);
|
||||
if (!d) return "";
|
||||
return d.toLocaleDateString("en-US", {
|
||||
timeZone: TIMEZONE_IST,
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a date in IST as "5:30 AM" or "5:30 PM"
|
||||
*/
|
||||
export function formatISTTime(dateInput: string | Date | null | undefined): string {
|
||||
const d = parseAsISTDate(dateInput);
|
||||
if (!d) return "";
|
||||
return d.toLocaleTimeString("en-US", {
|
||||
timeZone: TIMEZONE_IST,
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a "YYYY-MM-DD" date string evaluated in Indian Standard Time (Asia/Kolkata).
|
||||
* Useful for CalendarFilter matching and date grouping without UTC day boundary shifts.
|
||||
*/
|
||||
export function getISTDateString(dateInput: string | Date | null | undefined): string {
|
||||
const d = parseAsISTDate(dateInput);
|
||||
if (!d) return "";
|
||||
const fmt = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: TIMEZONE_IST,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
return fmt.format(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Date object picked in DatePicker into an ISO-8601 string
|
||||
* explicitly tagged with the IST offset (+05:30) so the backend and database
|
||||
* preserve the exact hour and minute selected by the admin.
|
||||
*/
|
||||
export function toISTISOString(date: Date | null | undefined): string | null {
|
||||
if (!date || isNaN(date.getTime())) return null;
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(date.getDate()).padStart(2, "0");
|
||||
const hh = String(date.getHours()).padStart(2, "0");
|
||||
const mm = String(date.getMinutes()).padStart(2, "0");
|
||||
const ss = String(date.getSeconds()).padStart(2, "0");
|
||||
return `${y}-${m}-${d}T${hh}:${mm}:${ss}+05:30`;
|
||||
}
|
||||
Reference in New Issue
Block a user