diff --git a/src/components/orders/CallDateTime.jsx b/src/components/orders/CallDateTime.jsx new file mode 100644 index 0000000..5cea8d0 --- /dev/null +++ b/src/components/orders/CallDateTime.jsx @@ -0,0 +1,149 @@ +import React from "react"; + +// ─── helpers ──────────────────────────────────────────────────────────────── + +const pad = (n) => String(n).padStart(2, "0"); + +export const toDateInputValue = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; +export const toTimeInputValue = (d) => `${pad(d.getHours())}:${pad(d.getMinutes())}`; + +const formatRuDate = (dateStr) => { + if (!dateStr) return ""; + const [y, m, d] = dateStr.split("-").map(Number); + if (!y || !m || !d) return dateStr; + const dt = new Date(y, m - 1, d); + const weekday = dt.toLocaleDateString("ru-RU", { weekday: "short" }); + return `${pad(d)}.${pad(m)}.${y} (${weekday[0]})`; +}; + +const useOutsideClose = (isOpen, setIsOpen) => { + const menuRef = React.useRef(null); + React.useEffect(() => { + if (!isOpen) return undefined; + const onPointerDown = (e) => { + if (menuRef.current && !menuRef.current.contains(e.target)) setIsOpen(false); + }; + const onKeyDown = (e) => { + if (e.key === "Escape") setIsOpen(false); + }; + document.addEventListener("pointerdown", onPointerDown); + document.addEventListener("keydown", onKeyDown); + return () => { + document.removeEventListener("pointerdown", onPointerDown); + document.removeEventListener("keydown", onKeyDown); + }; + }, [isOpen, setIsOpen]); + return menuRef; +}; + +const SelectShell = ({ label, valueLabel, children, isOpen, setIsOpen, menuRef, wide }) => ( +
+ +
+ + {isOpen ? ( +
+ {children} +
+ ) : null} +
+
+); + +const OptionRow = ({ value, selectedValue, label, onPick }) => ( + +); + +// ─── Date ───────────────────────────────────────────────────────────────── + +const buildDateOptions = (currentValue) => { + const today = new Date(); + const options = []; + for (let offset = -15; offset <= 15; offset += 1) { + const d = new Date(today.getFullYear(), today.getMonth(), today.getDate() + offset); + options.push({ value: toDateInputValue(d), label: formatRuDate(toDateInputValue(d)) }); + } + if (currentValue && !options.some((o) => o.value === currentValue)) { + options.unshift({ value: currentValue, label: formatRuDate(currentValue) }); + } + return options; +}; + +export const CallDateSelect = ({ value, onChange }) => { + const [isOpen, setIsOpen] = React.useState(false); + const menuRef = useOutsideClose(isOpen, setIsOpen); + const options = React.useMemo(() => buildDateOptions(value), [value]); + return ( + +
+ {options.map((o) => ( + { onChange(v); setIsOpen(false); }} /> + ))} +
+
+ ); +}; + +// ─── Time ────────────────────────────────────────────────────────────────── + +const buildTimeOptions = (currentValue) => { + const options = []; + for (let h = 0; h < 24; h += 1) { + for (const m of ["00", "30"]) { + options.push({ value: `${pad(h)}:${m}`, label: `${pad(h)}:${m}` }); + } + } + if (currentValue && !options.some((o) => o.value === currentValue)) { + options.unshift({ value: currentValue, label: currentValue }); + } + return options; +}; + +export const CallTimeSelect = ({ value, onChange }) => { + const [isOpen, setIsOpen] = React.useState(false); + const menuRef = useOutsideClose(isOpen, setIsOpen); + const options = React.useMemo(() => buildTimeOptions(value), [value]); + return ( + +
+ {options.map((o) => ( + { onChange(v); setIsOpen(false); }} /> + ))} +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/orders/CalledPanel.jsx b/src/components/orders/CalledPanel.jsx index 3e8971f..834e1dd 100644 --- a/src/components/orders/CalledPanel.jsx +++ b/src/components/orders/CalledPanel.jsx @@ -1,29 +1,54 @@ import React, { useState } from "react"; import { supabase } from "../../supabaseClient"; import { Button } from "../UI/Button"; +import { CallDateSelect, CallTimeSelect, toDateInputValue, toTimeInputValue } from "./CallDateTime"; const CalledPanel = ({ order, onUpdate }) => { const [busy, setBusy] = useState(false); const [editing, setEditing] = useState(false); - const [comment, setComment] = useState(order?.called_comment || ""); - const [callDate, setCallDate] = useState(() => { - if (!order?.called_at) return ""; - const d = new Date(order.called_at); - return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`; - }); - const [callTime, setCallTime] = useState(() => { - if (!order?.called_at) return ""; - const d = new Date(order.called_at); - return `${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}`; - }); + const [comment, setComment] = useState(order?.calledComment || order?.called_comment || ""); + const [callDate, setCallDate] = useState(""); + const [callTime, setCallTime] = useState(""); const isCalled = !!order?.called; const formatCalledAt = (ts) => { if (!ts) return ""; try { - return new Date(ts).toLocaleString("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" }); - } catch { return ""; } + return new Date(ts).toLocaleString("ru-RU", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + } catch { + return ""; + } + }; + + // Open add form with date+time defaulted to NOW for fast entry. + const startAdd = () => { + const now = new Date(); + setComment(""); + setCallDate(toDateInputValue(now)); + setCallTime(toTimeInputValue(now)); + setEditing(true); + }; + + // Open edit form for an existing call, pre-filled from stored values (or now). + const startEdit = () => { + setComment(order?.calledComment || order?.called_comment || ""); + if (order?.calledAt || order?.called_at) { + const d = new Date(order.calledAt || order.called_at); + setCallDate(toDateInputValue(d)); + setCallTime(toTimeInputValue(d)); + } else { + const now = new Date(); + setCallDate(toDateInputValue(now)); + setCallTime(toTimeInputValue(now)); + } + setEditing(true); }; const save = async (newCalled) => { @@ -40,7 +65,11 @@ const CalledPanel = ({ order, onUpdate }) => { } const { error } = await supabase .from("order_groups") - .update({ called: newCalled, called_at: calledAt, called_comment: newCalled ? comment.trim() : null }) + .update({ + called: newCalled, + called_at: calledAt, + called_comment: newCalled ? (comment.trim() || null) : null, + }) .eq("id", order.id); if (error) { console.warn("[CalledPanel] update error", error); @@ -53,70 +82,78 @@ const CalledPanel = ({ order, onUpdate }) => { } }; - // Form mode: date + time + comment + // Editing form: app-styled date/time pickers + comment + save/reset/cancel. if (editing) { return ( -
+
-
- - setCallDate(e.target.value)} - className="rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-sm !text-[var(--color-text)] focus:border-[var(--color-accent)] focus:outline-none" /> -
-
- - setCallTime(e.target.value)} - className="rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-sm !text-[var(--color-text)] focus:border-[var(--color-accent)] focus:outline-none" /> -
+ +
- -