fix: remove Группы tab from logistician, remove Справка, Телефон→Город, Заказы→Водитель, add city filter

This commit is contained in:
root 2026-05-28 10:29:13 +00:00
parent bb439a4d93
commit 6f29948f8a
5 changed files with 60 additions and 18 deletions

View File

@ -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>

View File

@ -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="Поиск по группе, клиенту или телефону"

View File

@ -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}

View File

@ -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;
} }

View File

@ -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) {