Compare commits
5 Commits
8131e26c77
...
f5a06188a2
| Author | SHA1 | Date |
|---|---|---|
|
|
f5a06188a2 | |
|
|
ebff56c749 | |
|
|
46fac18e28 | |
|
|
0cdd9f52a4 | |
|
|
a3cd0838f0 |
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accessible iOS-style toggle switch.
|
||||||
|
* @param {object} props
|
||||||
|
* @param {boolean} props.checked
|
||||||
|
* @param {function} props.onChange Called with the next boolean value.
|
||||||
|
* @param {string} [props.label] Visible text next to the switch.
|
||||||
|
* @param {string} [props.title] Hover tooltip.
|
||||||
|
* @param {string} [props.className]
|
||||||
|
*/
|
||||||
|
export const Switch = ({ checked = false, onChange, label, title, className = "" }) => {
|
||||||
|
const id = React.useId();
|
||||||
|
return (
|
||||||
|
<label htmlFor={id} className={`inline-flex cursor-pointer select-none items-center gap-2.5 text-sm text-[var(--color-text)] ${className}`} title={title}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id={id}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={checked}
|
||||||
|
onClick={() => onChange && onChange(!checked)}
|
||||||
|
className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full border transition-colors ${
|
||||||
|
checked
|
||||||
|
? "border-[var(--color-accent)] bg-[var(--color-accent)]"
|
||||||
|
: "border-[var(--color-border)] bg-[var(--color-surface-strong)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform ${
|
||||||
|
checked ? "translate-x-[22px]" : "translate-x-0.5"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{label ? <span>{label}</span> : null}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -302,7 +302,7 @@ const isFilterActive = (filters) => {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusOptions = ORDER_GROUP_DISPLAY_STATUS_OPTIONS, isLoading = false }) => {
|
export const LogisticsReadinessBoard = ({ orderGroups = [], totalGroups: totalGroupsProp, hideCompleted = true, onToggleHideCompleted, onSelectSet, statusOptions = ORDER_GROUP_DISPLAY_STATUS_OPTIONS, isLoading = false }) => {
|
||||||
const FILTERS_KEY = 'logistics-board-filters';
|
const FILTERS_KEY = 'logistics-board-filters';
|
||||||
const [filters, setFilters] = React.useState(() => {
|
const [filters, setFilters] = React.useState(() => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -487,7 +487,7 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
||||||
Перетаскивайте секции за ручку слева, чтобы изменить порядок отображения.
|
Перетаскивайте секции за ручку слева, чтобы изменить порядок отображения.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge tone="neutral">{totalGroups} групп</Badge>
|
<Badge tone="neutral">{totalGroupsProp ?? totalGroups} групп</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OrderFilters
|
<OrderFilters
|
||||||
|
|
@ -495,6 +495,8 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
||||||
setFilters={setFilters}
|
setFilters={setFilters}
|
||||||
statusOptions={statusOptions}
|
statusOptions={statusOptions}
|
||||||
cities={cities}
|
cities={cities}
|
||||||
|
hideCompleted={hideCompleted}
|
||||||
|
onToggleHideCompleted={onToggleHideCompleted}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!hasActiveFilters && totalGroups > PAGE_SIZE && (
|
{!hasActiveFilters && totalGroups > PAGE_SIZE && (
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Button } from "../UI/Button";
|
||||||
import { Input } from "../UI/Input";
|
import { Input } from "../UI/Input";
|
||||||
import { Panel } from "../UI/Panel";
|
import { Panel } from "../UI/Panel";
|
||||||
import { DatePicker } from "../UI/DatePicker";
|
import { DatePicker } from "../UI/DatePicker";
|
||||||
|
import { Switch } from "../UI/Switch";
|
||||||
|
|
||||||
const DropdownSelect = ({ value, options, onChange, placeholder }) => {
|
const DropdownSelect = ({ value, options, onChange, placeholder }) => {
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
|
@ -46,7 +47,7 @@ const DropdownSelect = ({ value, options, onChange, placeholder }) => {
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<div
|
<div
|
||||||
role="listbox"
|
role="listbox"
|
||||||
className="absolute left-0 right-0 top-full z-20 mt-2 overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-dropdown-surface)] shadow-soft"
|
className="absolute left-0 top-full z-20 mt-2 w-max min-w-full whitespace-nowrap overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-dropdown-surface)] shadow-soft"
|
||||||
>
|
>
|
||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
const isSelected = option.value === value;
|
const isSelected = option.value === value;
|
||||||
|
|
@ -77,7 +78,7 @@ const DropdownSelect = ({ value, options, onChange, placeholder }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities = [] }) => {
|
export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities = [], hideCompleted = true, onToggleHideCompleted }) => {
|
||||||
const statusValue = filters.displayStatus || filters.status || "all";
|
const statusValue = filters.displayStatus || filters.status || "all";
|
||||||
const selectedStatusLabel = statusOptions.find((option) => option.value === statusValue)?.label || statusValue;
|
const selectedStatusLabel = statusOptions.find((option) => option.value === statusValue)?.label || statusValue;
|
||||||
const [isStatusOpen, setIsStatusOpen] = React.useState(false);
|
const [isStatusOpen, setIsStatusOpen] = React.useState(false);
|
||||||
|
|
@ -129,8 +130,8 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
|
||||||
return (
|
return (
|
||||||
<Panel className="p-4">
|
<Panel className="p-4">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{/* Row 1: Status + City + DeliveryType + Search */}
|
{/* Row 1: Status + City + DeliveryType + Search (search spans wider) */}
|
||||||
<div className="grid gap-3 md:grid-cols-[minmax(12rem,0.7fr)_minmax(0,0.5fr)_minmax(0,0.5fr)_minmax(0,1.4fr)] md:items-end">
|
<div className="grid gap-3 md:grid-cols-[minmax(12rem,0.7fr)_minmax(0,0.5fr)_minmax(0,0.5fr)_minmax(0,1.6fr)] md:items-end">
|
||||||
<div ref={statusMenuRef} className="relative flex min-w-0 flex-col gap-2">
|
<div ref={statusMenuRef} className="relative flex min-w-0 flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -196,13 +197,6 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
|
||||||
placeholder="Тип получения"
|
placeholder="Тип получения"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DropdownSelect
|
|
||||||
value={filters.smsStatus || "all"}
|
|
||||||
options={smsProblemOptions}
|
|
||||||
onChange={(v) => updateFilter("smsStatus", v)}
|
|
||||||
placeholder="Проблемы SMS"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
className="h-[46px] py-0"
|
className="h-[46px] py-0"
|
||||||
placeholder="Поиск по группе, клиенту или телефону"
|
placeholder="Поиск по группе, клиенту или телефону"
|
||||||
|
|
@ -211,8 +205,10 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Row 2: Date range */}
|
{/* Row 2: Date range + SMS problems (width matches its text) + hide-completed toggle.
|
||||||
<div className="flex items-end gap-3 flex-wrap">
|
Align by the bottom edge of the input fields, not by center — dates have a
|
||||||
|
label above the field, so items-center would lift SMS/toggle above them. */}
|
||||||
|
<div className="flex flex-wrap items-end gap-3">
|
||||||
<DatePicker
|
<DatePicker
|
||||||
value={filters.dateFrom || ""}
|
value={filters.dateFrom || ""}
|
||||||
onChange={(v) => updateFilter("dateFrom", v)}
|
onChange={(v) => updateFilter("dateFrom", v)}
|
||||||
|
|
@ -236,6 +232,22 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
|
||||||
✕ Сбросить
|
✕ Сбросить
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<DropdownSelect
|
||||||
|
value={filters.smsStatus || "all"}
|
||||||
|
options={smsProblemOptions}
|
||||||
|
onChange={(v) => updateFilter("smsStatus", v)}
|
||||||
|
placeholder="Проблемы SMS"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{typeof onToggleHideCompleted === "function" && (
|
||||||
|
<Switch
|
||||||
|
checked={hideCompleted}
|
||||||
|
onChange={onToggleHideCompleted}
|
||||||
|
label="Скрыть завершённые"
|
||||||
|
title="Доставленные и отгруженные заказы скрыты"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { assignDriverToOrderGroup, fetchOrderGroupById, fetchOrderGroups, saveShipmentData, updateDeliveryStatus, updateOrderGroupDeliveryChoice } from "../services/supabase/orderGroupRepository";
|
import { assignDriverToOrderGroup, fetchOrderGroupById, fetchOrderGroups, readOrderGroupsCache, saveShipmentData, updateDeliveryStatus, updateOrderGroupDeliveryChoice } from "../services/supabase/orderGroupRepository";
|
||||||
import {
|
import {
|
||||||
buildOrderGroupBuckets,
|
buildOrderGroupBuckets,
|
||||||
filterOrderGroups,
|
filterOrderGroups,
|
||||||
|
|
@ -9,8 +9,21 @@ import {
|
||||||
} from "../services/orderGroupViews";
|
} from "../services/orderGroupViews";
|
||||||
import { getErrorMessage } from "../utils/deliveryUtils";
|
import { getErrorMessage } from "../utils/deliveryUtils";
|
||||||
|
|
||||||
export const useOrderGroups = ({ enabled = true, details = false, groupId = null } = {}) => {
|
export const useOrderGroups = ({
|
||||||
const [orderGroups, setOrderGroups] = React.useState(() => []);
|
enabled = true,
|
||||||
|
details = false,
|
||||||
|
groupId = null,
|
||||||
|
hideCompleted = true,
|
||||||
|
} = {}) => {
|
||||||
|
const [orderGroups, setOrderGroups] = React.useState(() => {
|
||||||
|
// Instant paint from cache on return-to-list, then background refresh.
|
||||||
|
const cached = readOrderGroupsCache(details, hideCompleted);
|
||||||
|
return cached?.data || [];
|
||||||
|
});
|
||||||
|
const [totalGroups, setTotalGroups] = React.useState(() => {
|
||||||
|
const cached = readOrderGroupsCache(details, hideCompleted);
|
||||||
|
return cached?.total || 0;
|
||||||
|
});
|
||||||
const FILTERS_STORAGE_KEY = "supersam_order_filters";
|
const FILTERS_STORAGE_KEY = "supersam_order_filters";
|
||||||
const [filters, setFilters] = React.useState(() => {
|
const [filters, setFilters] = React.useState(() => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -33,7 +46,7 @@ export const useOrderGroups = ({ enabled = true, details = false, groupId = null
|
||||||
const [isSavingDriverAssignment, setIsSavingDriverAssignment] = React.useState(false);
|
const [isSavingDriverAssignment, setIsSavingDriverAssignment] = React.useState(false);
|
||||||
const [isSavingStatusChange, setIsSavingStatusChange] = React.useState(false);
|
const [isSavingStatusChange, setIsSavingStatusChange] = React.useState(false);
|
||||||
|
|
||||||
const loadLiveData = React.useCallback(async (showLoading = true) => {
|
const loadLiveData = React.useCallback(async (showLoading = true, opts = {}) => {
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +72,7 @@ export const useOrderGroups = ({ enabled = true, details = false, groupId = null
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupsResult = await fetchOrderGroups({ details });
|
const groupsResult = await fetchOrderGroups({ details, hideCompleted, forceRefresh: opts?.forceRefresh });
|
||||||
|
|
||||||
if (!groupsResult) {
|
if (!groupsResult) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -72,16 +85,29 @@ export const useOrderGroups = ({ enabled = true, details = false, groupId = null
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setOrderGroups(groupsResult.data || []);
|
const payload = groupsResult.data || {};
|
||||||
|
setOrderGroups(payload.data || []);
|
||||||
|
setTotalGroups(payload.total ?? (payload.data?.length || 0));
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}, [enabled, details, groupId]);
|
}, [enabled, details, groupId, hideCompleted]);
|
||||||
|
|
||||||
// Initial load
|
// Initial load
|
||||||
|
const hasLoadedRef = React.useRef(false);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const doLoad = async () => {
|
const doLoad = async () => {
|
||||||
setIsLoading(true);
|
const isFirst = !hasLoadedRef.current;
|
||||||
await loadLiveData(true);
|
if (isFirst) {
|
||||||
|
const hasCache = !!readOrderGroupsCache(details, hideCompleted);
|
||||||
|
// Full skeleton only on the very first entry; otherwise refresh silently
|
||||||
|
// so toggling the "hide completed" switch updates the list in place
|
||||||
|
// without flashing the whole page / jumping to top.
|
||||||
|
setIsLoading(!hasCache);
|
||||||
|
await loadLiveData(!hasCache, { forceRefresh: true });
|
||||||
|
hasLoadedRef.current = true;
|
||||||
|
} else {
|
||||||
|
await loadLiveData(false, { forceRefresh: true });
|
||||||
|
}
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
};
|
};
|
||||||
if (enabled) doLoad(); else { setOrderGroups([]); setIsLoading(false); }
|
if (enabled) doLoad(); else { setOrderGroups([]); setIsLoading(false); }
|
||||||
|
|
@ -262,6 +288,7 @@ export const useOrderGroups = ({ enabled = true, details = false, groupId = null
|
||||||
return {
|
return {
|
||||||
orderGroups,
|
orderGroups,
|
||||||
allOrderGroups: orderGroups,
|
allOrderGroups: orderGroups,
|
||||||
|
totalGroups,
|
||||||
filteredOrderGroups,
|
filteredOrderGroups,
|
||||||
visibleOrderGroups,
|
visibleOrderGroups,
|
||||||
selectedOrderGroup,
|
selectedOrderGroup,
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,14 @@ export const DashboardPage = () => {
|
||||||
const { isInstalled, isInstallAvailable, installApp: onInstallApp } = usePwaStatus();
|
const { isInstalled, isInstallAvailable, installApp: onInstallApp } = usePwaStatus();
|
||||||
|
|
||||||
// ── Order Groups ─────────────────────────────────────────────────────────
|
// ── Order Groups ─────────────────────────────────────────────────────────
|
||||||
|
const HIDE_COMPLETED_KEY = "supersam-hide-completed";
|
||||||
|
const [hideCompleted, setHideCompleted] = React.useState(() => {
|
||||||
|
try { return localStorage.getItem(HIDE_COMPLETED_KEY) !== "false"; } catch { return true; }
|
||||||
|
});
|
||||||
|
React.useEffect(() => {
|
||||||
|
try { localStorage.setItem(HIDE_COMPLETED_KEY, hideCompleted ? "true" : "false"); } catch {}
|
||||||
|
}, [hideCompleted]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
orderGroups,
|
orderGroups,
|
||||||
allOrderGroups,
|
allOrderGroups,
|
||||||
|
|
@ -102,7 +110,8 @@ export const DashboardPage = () => {
|
||||||
statusOptions,
|
statusOptions,
|
||||||
isLoading,
|
isLoading,
|
||||||
loadError,
|
loadError,
|
||||||
} = useOrderGroups({ enabled: ["orders", "logistics", "deliveries"].includes(activeSection), details: false });
|
totalGroups,
|
||||||
|
} = useOrderGroups({ enabled: ["orders", "logistics", "deliveries"].includes(activeSection), details: false, hideCompleted });
|
||||||
|
|
||||||
// ── Derived City List ─────────────────────────────────────────────────────
|
// ── Derived City List ─────────────────────────────────────────────────────
|
||||||
const cities = React.useMemo(() => {
|
const cities = React.useMemo(() => {
|
||||||
|
|
@ -200,7 +209,7 @@ const ALLOWED_DASHBOARD_ROLES = ["admin", "mega_admin", "manager", "logistician"
|
||||||
// All roles (logistician, admin, mega_admin, manager) — same grouped board
|
// All roles (logistician, admin, mega_admin, manager) — same grouped board
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 xl:space-y-8">
|
<div className="space-y-6 xl:space-y-8">
|
||||||
<LogisticsReadinessBoard orderGroups={allOrderGroups} onSelectSet={openGroupPage} statusOptions={statusOptions} isLoading={isLoading} />
|
<LogisticsReadinessBoard orderGroups={allOrderGroups} totalGroups={totalGroups} hideCompleted={hideCompleted} onToggleHideCompleted={setHideCompleted} onSelectSet={openGroupPage} statusOptions={statusOptions} isLoading={isLoading} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -289,7 +289,10 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const ORDER_GROUP_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, town, customer_name, customer_phone, customer_phone_normalized, customer_date, orders_total, orders_ready, orders_not_ready, source_orders, order_list, order_list_structured, delivery_invitation_id, delivery_link, delivery_link_code, notification_status, sms_attempts, first_sms_sent_at, second_sms_sent_at, last_sms_error, next_notification_check_at, delivery_date, delivery_time, delivery_address, customer_address, delivery_date_source, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, has_delivery_problem, delivery_problem_note, pickup_code, synced_to_1c_at, manager_name, manager_email, manager_tel, payed_ship, is_payed_ship, delivery_invitation:delivery_invitations!order_groups_delivery_invitation_id_fkey(opened_at, access_count, last_accessed_at), called, called_at, called_comment, order_history(id, action, old_status, new_status, user_id, metadata, created_at)`;
|
const ORDER_GROUP_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, town, customer_name, customer_phone, customer_phone_normalized, customer_date, orders_total, orders_ready, orders_not_ready, source_orders, order_list, order_list_structured, delivery_invitation_id, delivery_link, delivery_link_code, notification_status, sms_attempts, first_sms_sent_at, second_sms_sent_at, last_sms_error, next_notification_check_at, delivery_date, delivery_time, delivery_address, customer_address, delivery_date_source, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, has_delivery_problem, delivery_problem_note, pickup_code, synced_to_1c_at, manager_name, manager_email, manager_tel, payed_ship, is_payed_ship, delivery_invitation:delivery_invitations!order_groups_delivery_invitation_id_fkey(opened_at, access_count, last_accessed_at), called, called_at, called_comment, order_history(id, action, old_status, new_status, user_id, metadata, created_at)`;
|
||||||
const ORDER_GROUP_LIST_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, town, customer_name, customer_phone, customer_phone_normalized, customer_date, orders_total, orders_ready, orders_not_ready, notification_status, sms_attempts, last_sms_error, first_sms_sent_at, second_sms_sent_at, delivery_date, delivery_time, delivery_address, customer_address, delivery_date_source, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, has_delivery_problem, delivery_problem_note, pickup_code, synced_to_1c_at, manager_name, manager_email, manager_tel, payed_ship, is_payed_ship, called, called_at, called_comment`;
|
export const ORDER_GROUP_LIST_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, town, customer_name, customer_phone, customer_phone_normalized, customer_date, orders_total, orders_ready, orders_not_ready, notification_status, sms_attempts, last_sms_error, first_sms_sent_at, second_sms_sent_at, delivery_date, delivery_time, delivery_address, customer_address, delivery_date_source, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, has_delivery_problem, delivery_problem_note, pickup_code, synced_to_1c_at, manager_name, manager_email, manager_tel, payed_ship, is_payed_ship, called, called_at, called_comment`;
|
||||||
|
|
||||||
|
// Delivery statuses treated as "finished" — hidden by default in the board.
|
||||||
|
const COMPLETED_DELIVERY_STATUSES = ["delivered", "picked_up", "cancelled"];
|
||||||
|
|
||||||
export const updateOrderGroupDeliveryChoice = async ({
|
export const updateOrderGroupDeliveryChoice = async ({
|
||||||
orderGroupId,
|
orderGroupId,
|
||||||
|
|
@ -300,6 +303,7 @@ export const updateOrderGroupDeliveryChoice = async ({
|
||||||
pickupDate,
|
pickupDate,
|
||||||
pickupTimeSlot,
|
pickupTimeSlot,
|
||||||
}) => {
|
}) => {
|
||||||
|
orderGroupsCache.clear();
|
||||||
return safeSupabaseCall(async () => {
|
return safeSupabaseCall(async () => {
|
||||||
const client = requireSupabase();
|
const client = requireSupabase();
|
||||||
const effectiveDeliveryStatus = deliveryType === "pickup" ? "pickup" : "agreed";
|
const effectiveDeliveryStatus = deliveryType === "pickup" ? "pickup" : "agreed";
|
||||||
|
|
@ -354,6 +358,7 @@ export const assignDriverToOrderGroup = async ({
|
||||||
orderGroupId,
|
orderGroupId,
|
||||||
driverId,
|
driverId,
|
||||||
}) => {
|
}) => {
|
||||||
|
orderGroupsCache.clear();
|
||||||
return safeSupabaseCall(async () => {
|
return safeSupabaseCall(async () => {
|
||||||
const client = requireSupabase();
|
const client = requireSupabase();
|
||||||
|
|
||||||
|
|
@ -416,6 +421,7 @@ export const assignDriverToOrderGroup = async ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export const saveShipmentData = async ({ orderGroupId, shipmentData }) => {
|
export const saveShipmentData = async ({ orderGroupId, shipmentData }) => {
|
||||||
|
orderGroupsCache.clear();
|
||||||
return safeSupabaseCall(async () => {
|
return safeSupabaseCall(async () => {
|
||||||
const client = requireSupabase();
|
const client = requireSupabase();
|
||||||
const hasAnyShipped = shipmentData.some((i) => i.shipped);
|
const hasAnyShipped = shipmentData.some((i) => i.shipped);
|
||||||
|
|
@ -472,6 +478,7 @@ export const saveShipmentData = async ({ orderGroupId, shipmentData }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateDeliveryStatus = async ({ orderGroupId, status, details, shipmentData } = {}) => {
|
export const updateDeliveryStatus = async ({ orderGroupId, status, details, shipmentData } = {}) => {
|
||||||
|
orderGroupsCache.clear();
|
||||||
return safeSupabaseCall(async () => {
|
return safeSupabaseCall(async () => {
|
||||||
const client = requireSupabase();
|
const client = requireSupabase();
|
||||||
|
|
||||||
|
|
@ -566,14 +573,55 @@ export const updateDeliveryStatus = async ({ orderGroupId, status, details, ship
|
||||||
}, "Ошибка обновления статуса доставки");
|
}, "Ошибка обновления статуса доставки");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchOrderGroups = async ({ details = true } = {}) => {
|
// Module-level cache so returning to the list from a card doesn't refetch the
|
||||||
|
// whole board (the previous "return to list is slow" annoyance). Keyed by details
|
||||||
|
// + hideCompleted. The hook refetches silently in the background to stay fresh.
|
||||||
|
const orderGroupsCache = new Map();
|
||||||
|
|
||||||
|
const orderGroupsCacheKey = (details, hideCompleted) =>
|
||||||
|
`v1:${details ? "detail" : "list"}:${hideCompleted ? "hide" : "all"}`;
|
||||||
|
|
||||||
|
/** Synchronous read of the cached board list ({ data, total } | null). */
|
||||||
|
export const readOrderGroupsCache = (details = false, hideCompleted = true) =>
|
||||||
|
orderGroupsCache.get(orderGroupsCacheKey(details, hideCompleted)) || null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch order groups.
|
||||||
|
*
|
||||||
|
* Returns { data, total } where total is the full count matching the filter
|
||||||
|
* (requires `count` support from PostgREST — available on self-hosted).
|
||||||
|
*
|
||||||
|
* @param {object} options
|
||||||
|
* @param {boolean} [options.details=false] Load full detail fields (for detail pages)
|
||||||
|
* @param {boolean} [options.hideCompleted=true] Exclude delivered/picked_up/cancelled groups
|
||||||
|
* @param {boolean} [options.forceRefresh=false] Bypass the in-memory cache and refetch
|
||||||
|
*/
|
||||||
|
export const fetchOrderGroups = async ({
|
||||||
|
details = false,
|
||||||
|
hideCompleted = true,
|
||||||
|
forceRefresh = false,
|
||||||
|
} = {}) => {
|
||||||
|
const cacheKey = orderGroupsCacheKey(details, hideCompleted);
|
||||||
|
|
||||||
|
if (!forceRefresh) {
|
||||||
|
const cached = orderGroupsCache.get(cacheKey);
|
||||||
|
if (cached) return cached;
|
||||||
|
}
|
||||||
|
|
||||||
return safeSupabaseCall(async () => {
|
return safeSupabaseCall(async () => {
|
||||||
const client = requireSupabase();
|
const client = requireSupabase();
|
||||||
const { data, error } = await client
|
|
||||||
|
let query = client
|
||||||
.from("order_groups")
|
.from("order_groups")
|
||||||
.select(details ? ORDER_GROUP_SELECT_FIELDS : ORDER_GROUP_LIST_SELECT_FIELDS)
|
.select(details ? ORDER_GROUP_SELECT_FIELDS : ORDER_GROUP_LIST_SELECT_FIELDS, { count: "exact" })
|
||||||
.order("updated_at", { ascending: false });
|
.order("updated_at", { ascending: false });
|
||||||
|
|
||||||
|
if (hideCompleted) {
|
||||||
|
query = query.not("delivery_status", "in", `(${COMPLETED_DELIVERY_STATUSES.join(",")})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error, count } = await query;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -585,13 +633,17 @@ export const fetchOrderGroups = async ({ details = true } = {}) => {
|
||||||
drivers.forEach((d) => driverMap.set(d.id, d.name || d.email));
|
drivers.forEach((d) => driverMap.set(d.id, d.name || d.email));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (data || []).map((row) => {
|
const groups = (data || []).map((row) => {
|
||||||
const group = mapOrderGroupRowToDeliveryGroup(row);
|
const group = mapOrderGroupRowToDeliveryGroup(row);
|
||||||
if (group && group.assignedDriverId && !group.assignedDriverName) {
|
if (group && group.assignedDriverId && !group.assignedDriverName) {
|
||||||
group.assignedDriverName = driverMap.get(group.assignedDriverId) || "";
|
group.assignedDriverName = driverMap.get(group.assignedDriverId) || "";
|
||||||
}
|
}
|
||||||
return group;
|
return group;
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
const result = { data: groups, total: count ?? groups.length };
|
||||||
|
orderGroupsCache.set(cacheKey, result);
|
||||||
|
return result;
|
||||||
}, "Ошибка загрузки групп доставки");
|
}, "Ошибка загрузки групп доставки");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue