feat: improve date handling by introducing utility functions for API datetime parsing and formatting
This commit is contained in:
@@ -32,6 +32,7 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import SearchableSelect from "@/components/ui/searchable-select";
|
import SearchableSelect from "@/components/ui/searchable-select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { localDateInputToUtcIso, toLocalDateInput } from "@/lib/datetime";
|
||||||
|
|
||||||
const slugify = (value: string) =>
|
const slugify = (value: string) =>
|
||||||
value
|
value
|
||||||
@@ -40,13 +41,6 @@ const slugify = (value: string) =>
|
|||||||
.replace(/[^a-z0-9]+/g, "-")
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
.replace(/(^-|-$)/g, "") || "board";
|
.replace(/(^-|-$)/g, "") || "board";
|
||||||
|
|
||||||
const toDateInput = (value?: string | null) => {
|
|
||||||
if (!value) return "";
|
|
||||||
const date = new Date(value);
|
|
||||||
if (Number.isNaN(date.getTime())) return "";
|
|
||||||
return date.toISOString().slice(0, 10);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function EditBoardPage() {
|
export default function EditBoardPage() {
|
||||||
const { isSignedIn } = useAuth();
|
const { isSignedIn } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -167,7 +161,7 @@ export default function EditBoardPage() {
|
|||||||
? JSON.stringify(baseBoard.success_metrics, null, 2)
|
? JSON.stringify(baseBoard.success_metrics, null, 2)
|
||||||
: "");
|
: "");
|
||||||
const resolvedTargetDate =
|
const resolvedTargetDate =
|
||||||
targetDate ?? toDateInput(baseBoard?.target_date);
|
targetDate ?? toLocalDateInput(baseBoard?.target_date);
|
||||||
|
|
||||||
const displayGatewayId = resolvedGatewayId || gateways[0]?.id || "";
|
const displayGatewayId = resolvedGatewayId || gateways[0]?.id || "";
|
||||||
|
|
||||||
@@ -193,7 +187,7 @@ export default function EditBoardPage() {
|
|||||||
setSuccessMetrics(
|
setSuccessMetrics(
|
||||||
updated.success_metrics ? JSON.stringify(updated.success_metrics, null, 2) : "",
|
updated.success_metrics ? JSON.stringify(updated.success_metrics, null, 2) : "",
|
||||||
);
|
);
|
||||||
setTargetDate(toDateInput(updated.target_date));
|
setTargetDate(toLocalDateInput(updated.target_date));
|
||||||
setIsOnboardingOpen(false);
|
setIsOnboardingOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -231,9 +225,7 @@ export default function EditBoardPage() {
|
|||||||
board_type: resolvedBoardType,
|
board_type: resolvedBoardType,
|
||||||
objective: resolvedObjective.trim() || null,
|
objective: resolvedObjective.trim() || null,
|
||||||
success_metrics: parsedMetrics,
|
success_metrics: parsedMetrics,
|
||||||
target_date: resolvedTargetDate
|
target_date: localDateInputToUtcIso(resolvedTargetDate),
|
||||||
? new Date(resolvedTargetDate).toISOString()
|
|
||||||
: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
updateBoardMutation.mutate({ boardId, data: payload });
|
updateBoardMutation.mutate({ boardId, data: payload });
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import type {
|
|||||||
TaskRead,
|
TaskRead,
|
||||||
} from "@/api/generated/model";
|
} from "@/api/generated/model";
|
||||||
import { createExponentialBackoff } from "@/lib/backoff";
|
import { createExponentialBackoff } from "@/lib/backoff";
|
||||||
|
import { apiDatetimeToMs, parseApiDatetime } from "@/lib/datetime";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type Board = BoardRead;
|
type Board = BoardRead;
|
||||||
@@ -250,8 +251,8 @@ const Markdown = memo(function Markdown({
|
|||||||
Markdown.displayName = "Markdown";
|
Markdown.displayName = "Markdown";
|
||||||
|
|
||||||
const formatShortTimestamp = (value: string) => {
|
const formatShortTimestamp = (value: string) => {
|
||||||
const date = new Date(value);
|
const date = parseApiDatetime(value);
|
||||||
if (Number.isNaN(date.getTime())) return "—";
|
if (!date) return "—";
|
||||||
return date.toLocaleString(undefined, {
|
return date.toLocaleString(undefined, {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
@@ -476,8 +477,8 @@ export default function BoardDetailPage() {
|
|||||||
items.forEach((task) => {
|
items.forEach((task) => {
|
||||||
const value = task.updated_at ?? task.created_at;
|
const value = task.updated_at ?? task.created_at;
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
const time = new Date(value).getTime();
|
const time = apiDatetimeToMs(value);
|
||||||
if (!Number.isNaN(time) && time > latestTime) {
|
if (time !== null && time > latestTime) {
|
||||||
latestTime = time;
|
latestTime = time;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -489,8 +490,8 @@ export default function BoardDetailPage() {
|
|||||||
items.forEach((approval) => {
|
items.forEach((approval) => {
|
||||||
const value = approval.resolved_at ?? approval.created_at;
|
const value = approval.resolved_at ?? approval.created_at;
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
const time = new Date(value).getTime();
|
const time = apiDatetimeToMs(value);
|
||||||
if (!Number.isNaN(time) && time > latestTime) {
|
if (time !== null && time > latestTime) {
|
||||||
latestTime = time;
|
latestTime = time;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -502,8 +503,8 @@ export default function BoardDetailPage() {
|
|||||||
items.forEach((agent) => {
|
items.forEach((agent) => {
|
||||||
const value = agent.updated_at ?? agent.last_seen_at;
|
const value = agent.updated_at ?? agent.last_seen_at;
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
const time = new Date(value).getTime();
|
const time = apiDatetimeToMs(value);
|
||||||
if (!Number.isNaN(time) && time > latestTime) {
|
if (time !== null && time > latestTime) {
|
||||||
latestTime = time;
|
latestTime = time;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -576,8 +577,8 @@ export default function BoardDetailPage() {
|
|||||||
const latestChatTimestamp = (items: BoardChatMessage[]) => {
|
const latestChatTimestamp = (items: BoardChatMessage[]) => {
|
||||||
if (!items.length) return undefined;
|
if (!items.length) return undefined;
|
||||||
const latest = items.reduce((max, item) => {
|
const latest = items.reduce((max, item) => {
|
||||||
const ts = new Date(item.created_at).getTime();
|
const ts = apiDatetimeToMs(item.created_at);
|
||||||
return Number.isNaN(ts) ? max : Math.max(max, ts);
|
return ts === null ? max : Math.max(max, ts);
|
||||||
}, 0);
|
}, 0);
|
||||||
if (!latest) return undefined;
|
if (!latest) return undefined;
|
||||||
return new Date(latest).toISOString();
|
return new Date(latest).toISOString();
|
||||||
@@ -647,14 +648,14 @@ export default function BoardDetailPage() {
|
|||||||
(item) => item.id === payload.memory?.id,
|
(item) => item.id === payload.memory?.id,
|
||||||
);
|
);
|
||||||
if (exists) return prev;
|
if (exists) return prev;
|
||||||
const next = [...prev, payload.memory as BoardChatMessage];
|
const next = [...prev, payload.memory as BoardChatMessage];
|
||||||
next.sort((a, b) => {
|
next.sort((a, b) => {
|
||||||
const aTime = new Date(a.created_at).getTime();
|
const aTime = apiDatetimeToMs(a.created_at) ?? 0;
|
||||||
const bTime = new Date(b.created_at).getTime();
|
const bTime = apiDatetimeToMs(b.created_at) ?? 0;
|
||||||
return aTime - bTime;
|
return aTime - bTime;
|
||||||
});
|
});
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore malformed
|
// ignore malformed
|
||||||
@@ -908,25 +909,24 @@ export default function BoardDetailPage() {
|
|||||||
const exists = prev.some((item) => item.id === payload.comment?.id);
|
const exists = prev.some((item) => item.id === payload.comment?.id);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
const createdAt = payload.comment?.created_at;
|
const createdMs = apiDatetimeToMs(payload.comment?.created_at);
|
||||||
const createdMs = createdAt ? new Date(createdAt).getTime() : NaN;
|
if (prev.length === 0 || createdMs === null) {
|
||||||
if (prev.length === 0 || Number.isNaN(createdMs)) {
|
return [...prev, payload.comment as TaskComment];
|
||||||
return [...prev, payload.comment as TaskComment];
|
}
|
||||||
}
|
const last = prev[prev.length - 1];
|
||||||
const last = prev[prev.length - 1];
|
const lastMs = apiDatetimeToMs(last?.created_at);
|
||||||
const lastMs = last?.created_at ? new Date(last.created_at).getTime() : NaN;
|
if (lastMs !== null && createdMs >= lastMs) {
|
||||||
if (!Number.isNaN(lastMs) && createdMs >= lastMs) {
|
return [...prev, payload.comment as TaskComment];
|
||||||
return [...prev, payload.comment as TaskComment];
|
}
|
||||||
}
|
const next = [...prev, payload.comment as TaskComment];
|
||||||
const next = [...prev, payload.comment as TaskComment];
|
next.sort((a, b) => {
|
||||||
next.sort((a, b) => {
|
const aTime = apiDatetimeToMs(a.created_at) ?? 0;
|
||||||
const aTime = new Date(a.created_at).getTime();
|
const bTime = apiDatetimeToMs(b.created_at) ?? 0;
|
||||||
const bTime = new Date(b.created_at).getTime();
|
return aTime - bTime;
|
||||||
return aTime - bTime;
|
});
|
||||||
});
|
return next;
|
||||||
return next;
|
});
|
||||||
});
|
|
||||||
} else if (payload.task) {
|
} else if (payload.task) {
|
||||||
setTasks((prev) => {
|
setTasks((prev) => {
|
||||||
const index = prev.findIndex((item) => item.id === payload.task?.id);
|
const index = prev.findIndex((item) => item.id === payload.task?.id);
|
||||||
@@ -1159,16 +1159,16 @@ export default function BoardDetailPage() {
|
|||||||
const created = result.data;
|
const created = result.data;
|
||||||
if (created.tags?.includes("chat")) {
|
if (created.tags?.includes("chat")) {
|
||||||
setChatMessages((prev) => {
|
setChatMessages((prev) => {
|
||||||
const exists = prev.some((item) => item.id === created.id);
|
const exists = prev.some((item) => item.id === created.id);
|
||||||
if (exists) return prev;
|
if (exists) return prev;
|
||||||
const next = [...prev, created];
|
const next = [...prev, created];
|
||||||
next.sort((a, b) => {
|
next.sort((a, b) => {
|
||||||
const aTime = new Date(a.created_at).getTime();
|
const aTime = apiDatetimeToMs(a.created_at) ?? 0;
|
||||||
const bTime = new Date(b.created_at).getTime();
|
const bTime = apiDatetimeToMs(b.created_at) ?? 0;
|
||||||
return aTime - bTime;
|
return aTime - bTime;
|
||||||
});
|
});
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1209,8 +1209,8 @@ export default function BoardDetailPage() {
|
|||||||
|
|
||||||
const orderedLiveFeed = useMemo(() => {
|
const orderedLiveFeed = useMemo(() => {
|
||||||
return [...liveFeed].sort((a, b) => {
|
return [...liveFeed].sort((a, b) => {
|
||||||
const aTime = new Date(a.created_at).getTime();
|
const aTime = apiDatetimeToMs(a.created_at) ?? 0;
|
||||||
const bTime = new Date(b.created_at).getTime();
|
const bTime = apiDatetimeToMs(b.created_at) ?? 0;
|
||||||
return bTime - aTime;
|
return bTime - aTime;
|
||||||
});
|
});
|
||||||
}, [liveFeed]);
|
}, [liveFeed]);
|
||||||
@@ -1320,8 +1320,8 @@ export default function BoardDetailPage() {
|
|||||||
if (result.status !== 200) throw new Error("Unable to load comments.");
|
if (result.status !== 200) throw new Error("Unable to load comments.");
|
||||||
const items = [...(result.data.items ?? [])];
|
const items = [...(result.data.items ?? [])];
|
||||||
items.sort((a, b) => {
|
items.sort((a, b) => {
|
||||||
const aTime = new Date(a.created_at).getTime();
|
const aTime = apiDatetimeToMs(a.created_at) ?? 0;
|
||||||
const bTime = new Date(b.created_at).getTime();
|
const bTime = apiDatetimeToMs(b.created_at) ?? 0;
|
||||||
return aTime - bTime;
|
return aTime - bTime;
|
||||||
});
|
});
|
||||||
setComments(items);
|
setComments(items);
|
||||||
@@ -1631,8 +1631,8 @@ export default function BoardDetailPage() {
|
|||||||
|
|
||||||
const formatTaskTimestamp = (value?: string | null) => {
|
const formatTaskTimestamp = (value?: string | null) => {
|
||||||
if (!value) return "—";
|
if (!value) return "—";
|
||||||
const date = new Date(value);
|
const date = parseApiDatetime(value);
|
||||||
if (Number.isNaN(date.getTime())) return "—";
|
if (!date) return "—";
|
||||||
return date.toLocaleString(undefined, {
|
return date.toLocaleString(undefined, {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
@@ -1669,8 +1669,8 @@ export default function BoardDetailPage() {
|
|||||||
|
|
||||||
const formatApprovalTimestamp = (value?: string | null) => {
|
const formatApprovalTimestamp = (value?: string | null) => {
|
||||||
if (!value) return "—";
|
if (!value) return "—";
|
||||||
const date = new Date(value);
|
const date = parseApiDatetime(value);
|
||||||
if (Number.isNaN(date.getTime())) return value;
|
if (!date) return value;
|
||||||
return date.toLocaleString(undefined, {
|
return date.toLocaleString(undefined, {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
type dashboardMetricsApiV1MetricsDashboardGetResponse,
|
type dashboardMetricsApiV1MetricsDashboardGetResponse,
|
||||||
useDashboardMetricsApiV1MetricsDashboardGet,
|
useDashboardMetricsApiV1MetricsDashboardGet,
|
||||||
} from "@/api/generated/metrics/metrics";
|
} from "@/api/generated/metrics/metrics";
|
||||||
|
import { parseApiDatetime } from "@/lib/datetime";
|
||||||
|
|
||||||
type RangeKey = "24h" | "7d";
|
type RangeKey = "24h" | "7d";
|
||||||
type BucketKey = "hour" | "day";
|
type BucketKey = "hour" | "day";
|
||||||
@@ -91,8 +92,8 @@ const updatedFormatter = new Intl.DateTimeFormat("en-US", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const formatPeriod = (value: string, bucket: BucketKey) => {
|
const formatPeriod = (value: string, bucket: BucketKey) => {
|
||||||
const date = new Date(value);
|
const date = parseApiDatetime(value);
|
||||||
if (Number.isNaN(date.getTime())) return "";
|
if (!date) return "";
|
||||||
return bucket === "hour" ? hourFormatter.format(date) : dayFormatter.format(date);
|
return bucket === "hour" ? hourFormatter.format(date) : dayFormatter.format(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -324,8 +325,8 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
const updatedAtLabel = useMemo(() => {
|
const updatedAtLabel = useMemo(() => {
|
||||||
if (!metrics?.generated_at) return null;
|
if (!metrics?.generated_at) return null;
|
||||||
const date = new Date(metrics.generated_at);
|
const date = parseApiDatetime(metrics.generated_at);
|
||||||
if (Number.isNaN(date.getTime())) return null;
|
if (!date) return null;
|
||||||
return updatedFormatter.format(date);
|
return updatedFormatter.format(date);
|
||||||
}, [metrics]);
|
}, [metrics]);
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
type ChartConfig,
|
type ChartConfig,
|
||||||
} from "@/components/charts/chart";
|
} from "@/components/charts/chart";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { apiDatetimeToMs, parseApiDatetime } from "@/lib/datetime";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type Approval = ApprovalRead & { status: string };
|
type Approval = ApprovalRead & { status: string };
|
||||||
@@ -42,8 +43,8 @@ type BoardApprovalsPanelProps = {
|
|||||||
|
|
||||||
const formatTimestamp = (value?: string | null) => {
|
const formatTimestamp = (value?: string | null) => {
|
||||||
if (!value) return "—";
|
if (!value) return "—";
|
||||||
const date = new Date(value);
|
const date = parseApiDatetime(value);
|
||||||
if (Number.isNaN(date.getTime())) return value;
|
if (!date) return value;
|
||||||
return date.toLocaleString(undefined, {
|
return date.toLocaleString(undefined, {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
@@ -241,7 +242,10 @@ export function BoardApprovalsPanel({
|
|||||||
const pendingNext = [...approvals]
|
const pendingNext = [...approvals]
|
||||||
.filter((item) => item.id !== approvalId)
|
.filter((item) => item.id !== approvalId)
|
||||||
.filter((item) => item.status === "pending")
|
.filter((item) => item.status === "pending")
|
||||||
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0]
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
(apiDatetimeToMs(b.created_at) ?? 0) - (apiDatetimeToMs(a.created_at) ?? 0),
|
||||||
|
)[0]
|
||||||
?.id;
|
?.id;
|
||||||
if (pendingNext) {
|
if (pendingNext) {
|
||||||
setSelectedId(pendingNext);
|
setSelectedId(pendingNext);
|
||||||
@@ -302,8 +306,8 @@ export function BoardApprovalsPanel({
|
|||||||
const sortedApprovals = useMemo(() => {
|
const sortedApprovals = useMemo(() => {
|
||||||
const sortByTime = (items: Approval[]) =>
|
const sortByTime = (items: Approval[]) =>
|
||||||
[...items].sort((a, b) => {
|
[...items].sort((a, b) => {
|
||||||
const aTime = new Date(a.created_at).getTime();
|
const aTime = apiDatetimeToMs(a.created_at) ?? 0;
|
||||||
const bTime = new Date(b.created_at).getTime();
|
const bTime = apiDatetimeToMs(b.created_at) ?? 0;
|
||||||
return bTime - aTime;
|
return bTime - aTime;
|
||||||
});
|
});
|
||||||
const pending = sortByTime(
|
const pending = sortByTime(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
import { parseApiDatetime } from "@/lib/datetime";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type BoardGoal = {
|
type BoardGoal = {
|
||||||
@@ -21,8 +22,8 @@ type BoardGoalPanelProps = {
|
|||||||
|
|
||||||
const formatTargetDate = (value?: string | null) => {
|
const formatTargetDate = (value?: string | null) => {
|
||||||
if (!value) return "—";
|
if (!value) return "—";
|
||||||
const date = new Date(value);
|
const date = parseApiDatetime(value);
|
||||||
if (Number.isNaN(date.getTime())) return value;
|
if (!date) return value;
|
||||||
return date.toLocaleDateString(undefined, {
|
return date.toLocaleDateString(undefined, {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { TaskCard } from "@/components/molecules/TaskCard";
|
import { TaskCard } from "@/components/molecules/TaskCard";
|
||||||
|
import { parseApiDatetime } from "@/lib/datetime";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type TaskStatus = "inbox" | "in_progress" | "review" | "done";
|
type TaskStatus = "inbox" | "in_progress" | "review" | "done";
|
||||||
@@ -74,8 +75,8 @@ const columns: Array<{
|
|||||||
|
|
||||||
const formatDueDate = (value?: string | null) => {
|
const formatDueDate = (value?: string | null) => {
|
||||||
if (!value) return undefined;
|
if (!value) return undefined;
|
||||||
const date = new Date(value);
|
const date = parseApiDatetime(value);
|
||||||
if (Number.isNaN(date.getTime())) return undefined;
|
if (!date) return undefined;
|
||||||
return date.toLocaleDateString(undefined, {
|
return date.toLocaleDateString(undefined, {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
|||||||
53
frontend/src/lib/datetime.ts
Normal file
53
frontend/src/lib/datetime.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
const HAS_TZ_RE = /[zZ]|[+-]\d\d:\d\d$/;
|
||||||
|
const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backend timestamps are emitted as ISO strings (often without a timezone).
|
||||||
|
* Treat missing timezone info as UTC, then format in the browser's local timezone.
|
||||||
|
*/
|
||||||
|
export function normalizeApiDatetime(value: string): string {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return trimmed;
|
||||||
|
if (DATE_ONLY_RE.test(trimmed)) {
|
||||||
|
// Convert date-only to a valid ISO timestamp.
|
||||||
|
return `${trimmed}T00:00:00Z`;
|
||||||
|
}
|
||||||
|
return HAS_TZ_RE.test(trimmed) ? trimmed : `${trimmed}Z`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseApiDatetime(value?: string | null): Date | null {
|
||||||
|
if (!value) return null;
|
||||||
|
const normalized = normalizeApiDatetime(value);
|
||||||
|
const date = new Date(normalized);
|
||||||
|
if (Number.isNaN(date.getTime())) return null;
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function apiDatetimeToMs(value?: string | null): number | null {
|
||||||
|
const date = parseApiDatetime(value);
|
||||||
|
return date ? date.getTime() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toLocalDateInput(value?: string | null): string {
|
||||||
|
const date = parseApiDatetime(value);
|
||||||
|
if (!date) return "";
|
||||||
|
const year = String(date.getFullYear());
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function localDateInputToUtcIso(value?: string | null): string | null {
|
||||||
|
if (!value) return null;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!match) return null;
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const monthIndex = Number(match[2]) - 1;
|
||||||
|
const day = Number(match[3]);
|
||||||
|
const date = new Date(year, monthIndex, day);
|
||||||
|
if (Number.isNaN(date.getTime())) return null;
|
||||||
|
return date.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user