Compare commits
No commits in common. "f35a84084b723d4b24f87c90f9706f3999899d62" and "f5a06188a2773cfef4e4ca2876723cf96dd0da31" have entirely different histories.
f35a84084b
...
f5a06188a2
|
|
@ -1,8 +1,8 @@
|
|||
const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1";
|
||||
|
||||
if (!isLocalhost) {
|
||||
const STATIC_CACHE = "construction-delivery-static-v116";
|
||||
const RUNTIME_CACHE = "construction-delivery-runtime-v116";
|
||||
const STATIC_CACHE = "construction-delivery-static-v115";
|
||||
const RUNTIME_CACHE = "construction-delivery-runtime-v115";
|
||||
const APP_SHELL_URLS = ["/", "/client", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"];
|
||||
|
||||
// Only these are treated as cacheable static assets. Everything else
|
||||
|
|
|
|||
|
|
@ -1,149 +0,0 @@
|
|||
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 }) => (
|
||||
<div className="min-w-0 flex-1">
|
||||
<label className="mb-1 block text-xs font-medium text-[var(--color-text-muted)]">{label}</label>
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={isOpen}
|
||||
className={[
|
||||
"flex h-[46px] w-full items-center justify-between rounded-2xl border px-4 text-left text-sm transition",
|
||||
isOpen
|
||||
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-text)]"
|
||||
: "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text)] hover:border-[var(--color-accent)]",
|
||||
].join(" ")}
|
||||
onClick={() => setIsOpen((c) => !c)}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">{valueLabel || "Выбрать"}</span>
|
||||
<span aria-hidden="true" className="ml-3 text-[var(--color-text-muted)]">▾</span>
|
||||
</button>
|
||||
{isOpen ? (
|
||||
<div
|
||||
role="listbox"
|
||||
className={[
|
||||
"absolute left-0 top-full z-20 mt-2 overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-dropdown-surface)] shadow-soft",
|
||||
wide ? "w-max min-w-full" : "w-full",
|
||||
].join(" ")}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const OptionRow = ({ value, selectedValue, label, onPick }) => (
|
||||
<button
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={selectedValue === value}
|
||||
className={[
|
||||
"flex w-full items-center justify-between px-4 py-2.5 text-left text-sm transition",
|
||||
selectedValue === value
|
||||
? "bg-[var(--color-accent-soft)] text-[var(--color-text)]"
|
||||
: "text-[var(--color-text)] hover:bg-[var(--color-surface-strong)]",
|
||||
].join(" ")}
|
||||
onClick={() => onPick(value)}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">{label}</span>
|
||||
{selectedValue === value ? <span className="ml-3 text-[var(--color-accent)]">✓</span> : null}
|
||||
</button>
|
||||
);
|
||||
|
||||
// ─── 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 (
|
||||
<SelectShell label="Дата звонка" valueLabel={formatRuDate(value)} isOpen={isOpen} setIsOpen={setIsOpen} menuRef={menuRef} wide>
|
||||
<div className="max-h-[280px] overflow-y-auto">
|
||||
{options.map((o) => (
|
||||
<OptionRow key={o.value} value={o.value} selectedValue={value} label={o.label} onPick={(v) => { onChange(v); setIsOpen(false); }} />
|
||||
))}
|
||||
</div>
|
||||
</SelectShell>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── 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 (
|
||||
<SelectShell label="Время звонка" valueLabel={value || "Выбрать"} isOpen={isOpen} setIsOpen={setIsOpen} menuRef={menuRef}>
|
||||
<div className="max-h-[280px] overflow-y-auto">
|
||||
{options.map((o) => (
|
||||
<OptionRow key={o.value} value={o.value} selectedValue={value} label={o.label} onPick={(v) => { onChange(v); setIsOpen(false); }} />
|
||||
))}
|
||||
</div>
|
||||
</SelectShell>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,54 +1,29 @@
|
|||
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?.calledComment || order?.called_comment || "");
|
||||
const [callDate, setCallDate] = useState("");
|
||||
const [callTime, setCallTime] = useState("");
|
||||
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 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 "";
|
||||
}
|
||||
};
|
||||
|
||||
// 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);
|
||||
return new Date(ts).toLocaleString("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
|
||||
} catch { return ""; }
|
||||
};
|
||||
|
||||
const save = async (newCalled) => {
|
||||
|
|
@ -65,11 +40,7 @@ const CalledPanel = ({ order, onUpdate }) => {
|
|||
}
|
||||
const { error } = await supabase
|
||||
.from("order_groups")
|
||||
.update({
|
||||
called: newCalled,
|
||||
called_at: calledAt,
|
||||
called_comment: newCalled ? (comment.trim() || null) : null,
|
||||
})
|
||||
.update({ called: newCalled, called_at: calledAt, called_comment: newCalled ? comment.trim() : null })
|
||||
.eq("id", order.id);
|
||||
if (error) {
|
||||
console.warn("[CalledPanel] update error", error);
|
||||
|
|
@ -82,78 +53,70 @@ const CalledPanel = ({ order, onUpdate }) => {
|
|||
}
|
||||
};
|
||||
|
||||
// Editing form: app-styled date/time pickers + comment + save/reset/cancel.
|
||||
// Form mode: date + time + comment
|
||||
if (editing) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<CallDateSelect value={callDate} onChange={setCallDate} />
|
||||
<CallTimeSelect value={callTime} onChange={setCallTime} />
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-[var(--color-text-muted)] mb-1">Дата звонка</label>
|
||||
<input type="date" value={callDate} onChange={(e) => 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" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-[var(--color-text-muted)] mb-1">Время звонка</label>
|
||||
<input type="time" value={callTime} onChange={(e) => 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" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-[var(--color-text-muted)]">Комментарий</label>
|
||||
<textarea
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
placeholder="Результат звонка..."
|
||||
rows={2}
|
||||
className="w-full rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 py-3 text-sm text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)] focus:outline-none resize-none"
|
||||
/>
|
||||
<label className="block text-xs font-medium text-[var(--color-text-muted)] mb-1">Комментарий</label>
|
||||
<textarea value={comment} onChange={(e) => setComment(e.target.value)} placeholder="Результат звонка..." rows={2}
|
||||
className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-sm !text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)] focus:outline-none resize-none" />
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="primary" size="sm" onClick={() => save(true)} disabled={busy}>
|
||||
{busy ? "Сохраняем..." : "Сохранить"}
|
||||
</Button>
|
||||
{isCalled && (
|
||||
<Button variant="ghost" size="sm" onClick={() => save(false)} disabled={busy}>
|
||||
Сбросить
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => save(false)} disabled={busy}>Сбросить</Button>
|
||||
)}
|
||||
<Button variant="secondary" size="sm" onClick={() => setEditing(false)} disabled={busy}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => setEditing(false)} disabled={busy}>Отмена</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default mode: buttons like status delivery
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{isCalled ? (
|
||||
<>
|
||||
<Button variant="primary" size="sm" disabled>
|
||||
<Button variant="primary" size="sm" onClick={() => setEditing(true)} disabled={busy}>
|
||||
📞 Звонили
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => save(false)} disabled={busy}>
|
||||
Не звонили
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={startEdit} disabled={busy} title="Изменить дату или комментарий">
|
||||
✎ Изменить
|
||||
</Button>
|
||||
{order?.calledAt || order?.called_at ? (
|
||||
<span className="text-xs text-[var(--color-text-muted)]">{formatCalledAt(order.calledAt || order.called_at)}</span>
|
||||
) : null}
|
||||
{order?.calledComment || order?.called_comment ? (
|
||||
<span className="text-xs text-[var(--color-text-muted)] italic">«{order.calledComment || order.called_comment}»</span>
|
||||
) : null}
|
||||
{order?.called_at && (
|
||||
<span className="text-xs text-[var(--color-text-muted)]">{formatCalledAt(order.called_at)}</span>
|
||||
)}
|
||||
{order?.called_comment && (
|
||||
<span className="text-xs text-[var(--color-text-muted)] italic">«{order.called_comment}»</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button variant="primary" size="sm" onClick={() => save(true)} disabled={busy}>
|
||||
{busy ? "Сохраняем..." : "📞 Звонили"}
|
||||
<Button variant="ghost" size="sm" onClick={() => setEditing(true)} disabled={busy}>
|
||||
📞 Звонили
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={startAdd} disabled={busy} title="Записать дату и время звонка">
|
||||
✎ Дата/комментарий
|
||||
<Button variant="primary" size="sm" disabled>
|
||||
Не звонили
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isCalled && (
|
||||
<p className="text-xs text-[var(--color-text-muted)]">
|
||||
Нажмите «Звонили», чтобы отметить звонок прямо сейчас.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -663,7 +663,6 @@ export const OrderDetailPanel = ({
|
|||
onAssignDriver,
|
||||
onChangeDeliveryStatus,
|
||||
onSaveShipmentData,
|
||||
onRefreshOrder,
|
||||
userRole = "driver",
|
||||
}) => {
|
||||
const [problemReason, setProblemReason] = React.useState(null);
|
||||
|
|
@ -1091,7 +1090,7 @@ export const OrderDetailPanel = ({
|
|||
userRole={userRole}
|
||||
canManageDelivery={canManageDelivery}
|
||||
isSavingStatusChange={isSavingStatusChange}
|
||||
onRefreshOrder={onRefreshOrder}
|
||||
onRefreshOrder={() => {}}
|
||||
onConfirmStatus={(action) => {
|
||||
if (action.type === "hint") {
|
||||
setFormMessage(action.hint);
|
||||
|
|
@ -1667,7 +1666,6 @@ export const OrderDetailPanel = ({
|
|||
userRole={userRole}
|
||||
canManageDelivery={canManageDelivery}
|
||||
isSavingStatusChange={isSavingStatusChange}
|
||||
onRefreshOrder={onRefreshOrder}
|
||||
onConfirmStatus={(action) => {
|
||||
if (action.type === "hint") {
|
||||
setFormMessage(action.hint);
|
||||
|
|
|
|||
|
|
@ -119,7 +119,6 @@ export const GroupDetailPage = () => {
|
|||
onAssignDriver={assignDriver}
|
||||
onChangeDeliveryStatus={changeDeliveryStatus}
|
||||
onSaveShipmentData={saveShipmentData}
|
||||
onRefreshOrder={() => refetch(false)}
|
||||
userRole={userRole}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
|
|
|
|||
|
|
@ -245,11 +245,6 @@ 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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue