evyos-frontend-development/frontend/components/ui/date-time-picker.tsx

220 lines
7.3 KiB
TypeScript

"use client";
import * as React from "react";
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { CalendarIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
interface Props {
value?: string; // "dd/mm/yyyy hh:mm:ss"
onChange?: (value: string) => void;
className?: string;
}
export function DateTimePicker({ value, onChange, className }: Props) {
// --- store each part as string ---
const [local, setLocal] = React.useState({
d: "",
m: "",
y: "",
h: "",
min: "",
s: "",
});
const [open, setOpen] = React.useState(false);
// --- parse incoming value string ---
React.useEffect(() => {
if (!value) return;
const dt = dateGetter(value);
if (!dt) return;
setLocal({
d: String(dt.getDate()),
m: String(dt.getMonth() + 1),
y: String(dt.getFullYear()),
h: String(dt.getHours()),
min: String(dt.getMinutes()),
s: String(dt.getSeconds()),
});
}, [value]);
// --- emit combined string ---
const emitChange = React.useCallback(() => {
const dt = new Date(
Number(local.y) || 0,
Number(local.m) - 1 || 0,
Number(local.d) || 1,
Number(local.h) || 0,
Number(local.min) || 0,
Number(local.s) || 0
);
if (!isNaN(dt.getTime())) {
onChange?.(dateSetter(dt));
}
}, [local, onChange]);
const handleInput = (field: keyof typeof local, val: string) => {
if (/^\d*$/.test(val)) {
setLocal((prev) => ({ ...prev, [field]: val }));
}
};
const handleBlur = (field: keyof typeof local) => {
// Validate ranges
let val = Number(local[field]);
if (isNaN(val)) val = 0;
switch (field) {
case "d":
val = Math.max(1, Math.min(31, val));
break;
case "m":
val = Math.max(1, Math.min(12, val));
break;
case "y":
val = Math.max(1900, Math.min(2100, val));
break;
case "h":
val = Math.max(0, Math.min(23, val));
break;
case "min":
case "s":
val = Math.max(0, Math.min(59, val));
break;
}
setLocal((prev) => ({ ...prev, [field]: String(val) }));
emitChange();
};
const currentDate = dateGetter(dateSetter(new Date(
Number(local.y) || 0,
Number(local.m) - 1 || 0,
Number(local.d) || 1,
Number(local.h) || 0,
Number(local.min) || 0,
Number(local.s) || 0
)));
const handleCalendarSelect = (d: Date | undefined) => {
if (!d) return;
const updated = new Date(
d.getFullYear(),
d.getMonth(),
d.getDate(),
Number(local.h) || 0,
Number(local.min) || 0,
Number(local.s) || 0
);
const str = dateSetter(updated);
setLocal({
d: String(updated.getDate()),
m: String(updated.getMonth() + 1),
y: String(updated.getFullYear()),
h: String(updated.getHours()),
min: String(updated.getMinutes()),
s: String(updated.getSeconds()),
});
onChange?.(str);
};
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline" className={cn("w-full justify-start", className)}>
<CalendarIcon className="mr-2 h-4 w-4" />
{value || "Select date and time"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-4 space-y-3" align="start">
{/* Calendar */}
<Calendar mode="single" selected={currentDate ?? undefined} onSelect={handleCalendarSelect} />
{/* Inputs */}
<div className="grid grid-cols-3 gap-2">
<Input
className="w-18"
min={1}
max={31}
placeholder="DD"
value={local.d}
onChange={(e) => handleInput("d", e.target.value)}
onBlur={() => handleBlur("d")}
/>
<Input
className="w-18"
min={1}
max={12}
placeholder="MM"
value={local.m}
onChange={(e) => handleInput("m", e.target.value)}
onBlur={() => handleBlur("m")}
/>
<Input
className="w-18"
min={1900}
max={2100}
placeholder="YYYY"
value={local.y}
onChange={(e) => handleInput("y", e.target.value)}
onBlur={() => handleBlur("y")}
/>
</div>
<div className="grid grid-cols-3 gap-2 mt-2">
<Input
className="w-18"
min={0}
max={23}
placeholder="HH"
value={local.h}
onChange={(e) => handleInput("h", e.target.value)}
onBlur={() => handleBlur("h")}
/>
<Input
className="w-18"
min={0}
max={59}
placeholder="MM"
value={local.min}
onChange={(e) => handleInput("min", e.target.value)}
onBlur={() => handleBlur("min")}
/>
<Input
className="w-18"
min={0}
max={59}
placeholder="SS"
value={local.s}
onChange={(e) => handleInput("s", e.target.value)}
onBlur={() => handleBlur("s")}
/>
</div>
</PopoverContent>
</Popover>
);
}
// --- helpers for external usage ---
export const dateGetter = (str: string | undefined): Date | null => {
if (!str) return null;
const [d, m, yAndTime] = str.split("/");
if (!d || !m || !yAndTime) return null;
const [y, time] = yAndTime.split(" ");
if (!time) return null;
const [h, min, s] = time.split(":").map(Number);
return new Date(Number(y), Number(m) - 1, Number(d), h || 0, min || 0, s || 0);
};
export const dateSetter = (date: Date | null): string => {
if (!date) return "";
const fmt = (n: number) => String(n).padStart(2, "0");
return `${fmt(date.getDate())}/${fmt(date.getMonth() + 1)}/${date.getFullYear()} ${fmt(
date.getHours()
)}:${fmt(date.getMinutes())}:${fmt(date.getSeconds())}`;
};