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" />
-
+
+
-
-
{isCalled && (
-
+
)}
-
+
);
}
- // Default mode: buttons like status delivery
return (
{isCalled ? (
<>
-
+ {!isCalled && (
+
+ Нажмите «Звонили», чтобы отметить звонок прямо сейчас.
+
+ )}
);
};
diff --git a/src/components/orders/OrderDetailPanel.jsx b/src/components/orders/OrderDetailPanel.jsx
index 36e43f2..9074fbd 100644
--- a/src/components/orders/OrderDetailPanel.jsx
+++ b/src/components/orders/OrderDetailPanel.jsx
@@ -663,6 +663,7 @@ export const OrderDetailPanel = ({
onAssignDriver,
onChangeDeliveryStatus,
onSaveShipmentData,
+ onRefreshOrder,
userRole = "driver",
}) => {
const [problemReason, setProblemReason] = React.useState(null);
@@ -1090,7 +1091,7 @@ export const OrderDetailPanel = ({
userRole={userRole}
canManageDelivery={canManageDelivery}
isSavingStatusChange={isSavingStatusChange}
- onRefreshOrder={() => {}}
+ onRefreshOrder={onRefreshOrder}
onConfirmStatus={(action) => {
if (action.type === "hint") {
setFormMessage(action.hint);
@@ -1666,6 +1667,7 @@ export const OrderDetailPanel = ({
userRole={userRole}
canManageDelivery={canManageDelivery}
isSavingStatusChange={isSavingStatusChange}
+ onRefreshOrder={onRefreshOrder}
onConfirmStatus={(action) => {
if (action.type === "hint") {
setFormMessage(action.hint);
diff --git a/src/pages/GroupDetailPage.jsx b/src/pages/GroupDetailPage.jsx
index a353d67..938f03e 100644
--- a/src/pages/GroupDetailPage.jsx
+++ b/src/pages/GroupDetailPage.jsx
@@ -119,6 +119,7 @@ export const GroupDetailPage = () => {
onAssignDriver={assignDriver}
onChangeDeliveryStatus={changeDeliveryStatus}
onSaveShipmentData={saveShipmentData}
+ onRefreshOrder={() => refetch(false)}
userRole={userRole}
/>
diff --git a/src/services/supabase/orderGroupRepository.js b/src/services/supabase/orderGroupRepository.js
index a72bb3a..821bc6e 100644
--- a/src/services/supabase/orderGroupRepository.js
+++ b/src/services/supabase/orderGroupRepository.js
@@ -245,6 +245,11 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
managerTel: normalizeText(row.manager_tel) || null,
payedShip: row.payed_ship || null,
isPayedShip: row.is_payed_ship || false,
+ called: !!row.called,
+ calledAt: row.called_at || null,
+ called_at: row.called_at || null,
+ calledComment: row.called_comment || null,
+ called_comment: row.called_comment || null,
invitationOpenedAt: row.delivery_invitation?.opened_at || null,
invitationAccessCount: row.delivery_invitation?.access_count || 0,
invitationLastAccessedAt: row.delivery_invitation?.last_accessed_at || null,