fix: remove Группы tab from logistician, remove Справка, Телефон→Город, Заказы→Водитель, add city filter
This commit is contained in:
parent
bb439a4d93
commit
6f29948f8a
|
|
@ -12,9 +12,17 @@ import { OrderFilters } from "../orders/OrderFilters";
|
||||||
import { formatDateTime } from "../../utils/formatters";
|
import { formatDateTime } from "../../utils/formatters";
|
||||||
|
|
||||||
export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusOptions = ORDER_GROUP_DISPLAY_STATUS_OPTIONS }) => {
|
export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusOptions = ORDER_GROUP_DISPLAY_STATUS_OPTIONS }) => {
|
||||||
const [filters, setFilters] = React.useState({ query: "", displayStatus: "all" });
|
const [filters, setFilters] = React.useState({ query: "", displayStatus: "all", city: "" });
|
||||||
const [collapsedSections, setCollapsedSections] = React.useState(new Set());
|
const [collapsedSections, setCollapsedSections] = React.useState(new Set());
|
||||||
|
|
||||||
|
const cities = React.useMemo(() => {
|
||||||
|
const set = new Set();
|
||||||
|
for (const g of orderGroups) {
|
||||||
|
if (g.city) set.add(g.city);
|
||||||
|
}
|
||||||
|
return [...set].sort();
|
||||||
|
}, [orderGroups]);
|
||||||
|
|
||||||
const filteredGroups = React.useMemo(
|
const filteredGroups = React.useMemo(
|
||||||
() => filterOrderGroups(orderGroups, filters),
|
() => filterOrderGroups(orderGroups, filters),
|
||||||
[filters, orderGroups],
|
[filters, orderGroups],
|
||||||
|
|
@ -55,9 +63,9 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
||||||
<thead className="bg-[var(--color-surface-strong)] text-left text-xs uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
|
<thead className="bg-[var(--color-surface-strong)] text-left text-xs uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 font-medium">Клиент</th>
|
<th className="px-4 py-3 font-medium">Клиент</th>
|
||||||
<th className="px-4 py-3 font-medium hidden sm:table-cell">Телефон</th>
|
<th className="px-4 py-3 font-medium hidden sm:table-cell">Город</th>
|
||||||
<th className="px-4 py-3 font-medium hidden md:table-cell">Дата доставки</th>
|
<th className="px-4 py-3 font-medium hidden md:table-cell">Дата доставки</th>
|
||||||
<th className="px-4 py-3 font-medium hidden lg:table-cell">Заказы</th>
|
<th className="px-4 py-3 font-medium hidden lg:table-cell">Водитель</th>
|
||||||
<th className="px-4 py-3 font-medium">Статус</th>
|
<th className="px-4 py-3 font-medium">Статус</th>
|
||||||
<th className="px-4 py-3 font-medium hidden md:table-cell">Обновлён</th>
|
<th className="px-4 py-3 font-medium hidden md:table-cell">Обновлён</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -78,6 +86,7 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
||||||
filters={filters}
|
filters={filters}
|
||||||
setFilters={setFilters}
|
setFilters={setFilters}
|
||||||
statusOptions={statusOptions}
|
statusOptions={statusOptions}
|
||||||
|
cities={cities}
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
|
|
@ -147,7 +156,7 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
||||||
<div className="text-xs text-[var(--color-text-muted)] md:hidden">{group.deliveryDate || "—"}</div>
|
<div className="text-xs text-[var(--color-text-muted)] md:hidden">{group.deliveryDate || "—"}</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2.5 text-sm hidden sm:table-cell">
|
<td className="px-4 py-2.5 text-sm hidden sm:table-cell">
|
||||||
{group.customerPhone || "—"}
|
{group.city || group.customerAddress || "—"}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2.5 text-sm hidden md:table-cell">
|
<td className="px-4 py-2.5 text-sm hidden md:table-cell">
|
||||||
{group.deliveryDate
|
{group.deliveryDate
|
||||||
|
|
@ -156,7 +165,7 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2.5 text-sm hidden lg:table-cell">
|
<td className="px-4 py-2.5 text-sm hidden lg:table-cell">
|
||||||
{group.ordersCount || 0} {group.ordersCount === 1 ? "заказ" : group.ordersCount < 5 ? "заказа" : "заказов"}
|
{group.assignedDriverName || <span className="text-[var(--color-text-muted)]">—</span>}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2.5">
|
<td className="px-4 py-2.5">
|
||||||
<Badge tone={getOrderGroupStatusTone(group)}>{getOrderGroupDisplayStatusLabel(group)}</Badge>
|
<Badge tone={getOrderGroupStatusTone(group)}>{getOrderGroupDisplayStatusLabel(group)}</Badge>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ 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";
|
||||||
|
|
||||||
export const OrderFilters = ({ filters, setFilters, statusOptions = [] }) => {
|
export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities = [] }) => {
|
||||||
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);
|
||||||
|
|
@ -38,8 +38,8 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [] }) => {
|
||||||
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 + Search */}
|
{/* Row 1: Status + City + Search */}
|
||||||
<div className="grid gap-3 md:grid-cols-[minmax(12rem,0.7fr)_minmax(0,1.6fr)] md:items-end">
|
<div className="grid gap-3 md:grid-cols-[minmax(12rem,0.7fr)_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"
|
||||||
|
|
@ -93,6 +93,19 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [] }) => {
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{cities.length > 0 && (
|
||||||
|
<select
|
||||||
|
value={filters.city || ""}
|
||||||
|
onChange={(e) => updateFilter("city", e.target.value)}
|
||||||
|
className="h-[46px] rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 text-sm outline-none focus:border-[var(--color-accent)]"
|
||||||
|
>
|
||||||
|
<option value="">Все города</option>
|
||||||
|
{cities.map((c) => (
|
||||||
|
<option key={c} value={c}>{c}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
className="h-[46px] py-0"
|
className="h-[46px] py-0"
|
||||||
placeholder="Поиск по группе, клиенту или телефону"
|
placeholder="Поиск по группе, клиенту или телефону"
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import ErrorLogPanel from "../components/admin/ErrorLogPanel";
|
||||||
import { StopWordsPanel } from "../components/admin/StopWordsPanel";
|
import { StopWordsPanel } from "../components/admin/StopWordsPanel";
|
||||||
import { ActionLogPanel } from "../components/admin/ActionLogPanel";
|
import { ActionLogPanel } from "../components/admin/ActionLogPanel";
|
||||||
import { Panel } from "../components/UI/Panel";
|
import { Panel } from "../components/UI/Panel";
|
||||||
import { ProductGuidePanel } from "../components/dashboard/ProductGuidePanel";
|
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useNotifications } from "../hooks/useNotifications";
|
import { useNotifications } from "../hooks/useNotifications";
|
||||||
import { usePushNotifications } from "../hooks/usePushNotifications";
|
import { usePushNotifications } from "../hooks/usePushNotifications";
|
||||||
|
|
@ -102,22 +101,20 @@ export const DashboardPage = () => {
|
||||||
: userRole === "logistician"
|
: userRole === "logistician"
|
||||||
? [
|
? [
|
||||||
{ key: "logistics", label: "Логистика", description: "Группы доставки по готовности к уведомлению.", badge: String(allOrderGroups.length || orderGroups.length || 0) },
|
{ key: "logistics", label: "Логистика", description: "Группы доставки по готовности к уведомлению.", badge: String(allOrderGroups.length || orderGroups.length || 0) },
|
||||||
{ key: "orders", label: "Группы", description: "Реестр групп доставки.", badge: null },
|
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{ key: section.key, label: section.label, description: section.description, badge: String(allOrderGroups.length || orderGroups.length || 0) },
|
{ key: section.key, label: section.label, description: section.description, badge: String(allOrderGroups.length || orderGroups.length || 0) },
|
||||||
];
|
];
|
||||||
|
|
||||||
const guideSectionMeta = { key: "guide", label: "Справка", description: "Карта продукта, роли, сценарии и частые вопросы." };
|
const activeSectionMeta = navItems.find((n) => n.key === activeSection) || navItems[0];
|
||||||
const activeSectionMeta = activeSection === "guide" ? guideSectionMeta : navItems.find((n) => n.key === activeSection) || navItems[0];
|
const isGuideOpen = false;
|
||||||
const isGuideOpen = activeSection === "guide";
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return <Navigate to="/login" replace />;
|
return <Navigate to="/login" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderActiveSection = () => {
|
const renderActiveSection = () => {
|
||||||
if (activeSection === "guide") return <ProductGuidePanel />;
|
|
||||||
if (activeSection === "analytics") return <div className="space-y-6 xl:space-y-8"><AdminDashboard /></div>;
|
if (activeSection === "analytics") return <div className="space-y-6 xl:space-y-8"><AdminDashboard /></div>;
|
||||||
if (activeSection === "users") return <div className="space-y-6 xl:space-y-8"><UserManagementPanel /></div>;
|
if (activeSection === "users") return <div className="space-y-6 xl:space-y-8"><UserManagementPanel /></div>;
|
||||||
if (activeSection === "stop_words") return <div className="space-y-6 xl:space-y-8"><StopWordsPanel /></div>;
|
if (activeSection === "stop_words") return <div className="space-y-6 xl:space-y-8"><StopWordsPanel /></div>;
|
||||||
|
|
@ -159,8 +156,8 @@ export const DashboardPage = () => {
|
||||||
onInstallApp={onInstallApp}
|
onInstallApp={onInstallApp}
|
||||||
isInstalled={isInstalled}
|
isInstalled={isInstalled}
|
||||||
isInstallAvailable={isInstallAvailable}
|
isInstallAvailable={isInstallAvailable}
|
||||||
onOpenGuide={() => setActiveSection("guide")}
|
onOpenGuide={undefined}
|
||||||
isGuideOpen={isGuideOpen}
|
isGuideOpen={false}
|
||||||
navItems={navItems}
|
navItems={navItems}
|
||||||
activeSection={activeSection}
|
activeSection={activeSection}
|
||||||
onSectionChange={setActiveSection}
|
onSectionChange={setActiveSection}
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,8 @@ export const filterOrderGroups = (groups, filters = {}) => {
|
||||||
getOrderGroupStatusLabel(group.status),
|
getOrderGroupStatusLabel(group.status),
|
||||||
group.deliveryStatus,
|
group.deliveryStatus,
|
||||||
getOrderGroupDeliveryStatusLabel(group.deliveryStatus),
|
getOrderGroupDeliveryStatusLabel(group.deliveryStatus),
|
||||||
|
group.city,
|
||||||
|
group.customerAddress,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" "))
|
.join(" "))
|
||||||
|
|
@ -289,6 +291,14 @@ export const filterOrderGroups = (groups, filters = {}) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cityFilter = (filters.city || "").trim().toLowerCase();
|
||||||
|
if (cityFilter) {
|
||||||
|
const groupCity = (group.city || "").toLowerCase();
|
||||||
|
if (!groupCity.includes(cityFilter) && cityFilter !== groupCity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,15 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const deliveryAddress = normalizeText(row.delivery_address) || extractAddressFromSourceOrders(row.source_orders);
|
const deliveryAddress = normalizeText(row.delivery_address) || extractAddressFromSourceOrders(row.source_orders);
|
||||||
|
const customerAddress = normalizeText(row.customer_address) || "";
|
||||||
|
const extractCity = (addr) => {
|
||||||
|
if (!addr) return "";
|
||||||
|
const m = addr.match(/(?:г\.|гор\.?|пос\.|с\.|село|дер\.|пгт|город)\s*([А-ЯЁа-яёA-Za-z\-\s]+?)(?:[,\\s]|$)/i);
|
||||||
|
if (m) return m[1].trim();
|
||||||
|
const words = addr.split(/[,.]/)[0].trim();
|
||||||
|
return words;
|
||||||
|
};
|
||||||
|
const city = extractCity(customerAddress) || extractCity(deliveryAddress) || "";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
|
|
@ -107,6 +116,8 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
|
||||||
customerPhoneNormalized: parsedKey.phone || normalizePhone(customerPhone),
|
customerPhoneNormalized: parsedKey.phone || normalizePhone(customerPhone),
|
||||||
customerDate,
|
customerDate,
|
||||||
deliveryAddress,
|
deliveryAddress,
|
||||||
|
customerAddress,
|
||||||
|
city,
|
||||||
assignedDriverId: row.assigned_driver_id || null,
|
assignedDriverId: row.assigned_driver_id || null,
|
||||||
assignedDriverName: row.assigned_driver?.name || "",
|
assignedDriverName: row.assigned_driver?.name || "",
|
||||||
ordersCount,
|
ordersCount,
|
||||||
|
|
@ -154,6 +165,8 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
|
||||||
customerPhone,
|
customerPhone,
|
||||||
customerDate,
|
customerDate,
|
||||||
deliveryAddress,
|
deliveryAddress,
|
||||||
|
customerAddress,
|
||||||
|
city,
|
||||||
rawDeliveryHalfDay,
|
rawDeliveryHalfDay,
|
||||||
rawDeliveryTime,
|
rawDeliveryTime,
|
||||||
row.delivery_window,
|
row.delivery_window,
|
||||||
|
|
@ -203,7 +216,7 @@ export const updateOrderGroupDeliveryChoice = async ({
|
||||||
|
|
||||||
const { data, error } = await client
|
const { data, error } = await client
|
||||||
.from("order_groups")
|
.from("order_groups")
|
||||||
.select("id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, 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, 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, delivery_date_source, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name)")
|
.select("id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, 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, 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)")
|
||||||
.eq("id", orderGroupId)
|
.eq("id", orderGroupId)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
|
|
@ -357,7 +370,7 @@ export const fetchOrderGroups = async () => {
|
||||||
const client = requireSupabase();
|
const client = requireSupabase();
|
||||||
const { data, error } = await client
|
const { data, error } = await client
|
||||||
.from("order_groups")
|
.from("order_groups")
|
||||||
.select("id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, 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, 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, delivery_date_source, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name)")
|
.select("id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, 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, 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)")
|
||||||
.order("updated_at", { ascending: false });
|
.order("updated_at", { ascending: false });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue