feat: delivery page improvements and driver shipment workflow
- OrderCompositionPanel: flatten nested source_orders into product list, collapsed by default - DeliverySlotsPicker: single-line heading 'Доставка завтра · 22.05.2026' instead of two-line - OrderDetailPanel: add logistics status change panel, reorder sections (delivery date → driver → status → paid storage) - OrderDetailPanel: require delivery date before assigning driver - GroupDetailPage: dedicated route /dashboard/group/:groupId instead of modal - DashboardPage: navigate to group detail page instead of opening modal - DriverDeliveryPlanner: remove 'agreed' status, add city filter pills, group by status within dates - DriverDeliveryPlanner: show delivery address instead of order numbers/ready count/SMS - DriverShipmentPanel: new component with per-item checkboxes, ship-all, comments for unshipped items - OrderDetailPanel: driver sees shipment panel + status buttons; 'delivered' requires all items shipped or commented - Driver status: start from 'Назначено вам' (driver_assigned), not 'agreed' - Remove duplicate header from GroupDetailPage
This commit is contained in:
parent
9abfbff654
commit
43c5f75055
|
|
@ -1,9 +1,5 @@
|
||||||
-- Migration: add source_orders items to get_delivery_invitation_by_token
|
-- Migration: flatten ALL products from source_orders into simple orderItems list
|
||||||
-- This replaces ONLY the orderItems building section for the group path.
|
-- This replaces the previous nested orderItems building section.
|
||||||
-- Apply AFTER the base function is restored.
|
|
||||||
|
|
||||||
-- Step 1: First restore the original function (run restore-rpc-original.sql if needed)
|
|
||||||
-- Step 2: Then run this migration
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION public.get_delivery_invitation_by_token(p_token text)
|
CREATE OR REPLACE FUNCTION public.get_delivery_invitation_by_token(p_token text)
|
||||||
RETURNS jsonb
|
RETURNS jsonb
|
||||||
|
|
@ -21,7 +17,6 @@ DECLARE
|
||||||
v_customer_name text;
|
v_customer_name text;
|
||||||
v_customer_phone text;
|
v_customer_phone text;
|
||||||
v_order_items jsonb;
|
v_order_items jsonb;
|
||||||
v_order_numbers jsonb;
|
|
||||||
v_now timestamptz := timezone('utc', now());
|
v_now timestamptz := timezone('utc', now());
|
||||||
BEGIN
|
BEGIN
|
||||||
IF nullif(trim(coalesce(p_token, '')), '') IS NULL THEN
|
IF nullif(trim(coalesce(p_token, '')), '') IS NULL THEN
|
||||||
|
|
@ -90,20 +85,67 @@ BEGIN
|
||||||
NULLIF(v_invitation.customer_phone, '')
|
NULLIF(v_invitation.customer_phone, '')
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Build orderItems: use source_orders for real product lines if available,
|
-- Build orderItems: flatten ALL products from source_orders into a flat list.
|
||||||
-- otherwise fall back to invoice numbers from order_numbers.
|
-- Strategy 1: products inside orderList[].items[]
|
||||||
|
-- Strategy 2: products inside items[] directly on source_orders entry
|
||||||
|
-- Strategy 3: fallback to invoice names from order_numbers
|
||||||
v_order_items := CASE
|
v_order_items := CASE
|
||||||
WHEN v_group.source_orders IS NOT NULL
|
WHEN v_group.source_orders IS NOT NULL
|
||||||
AND jsonb_typeof(v_group.source_orders) = 'array'
|
AND jsonb_typeof(v_group.source_orders) = 'array'
|
||||||
AND jsonb_array_length(v_group.source_orders) > 0
|
AND jsonb_array_length(v_group.source_orders) > 0
|
||||||
THEN COALESCE(
|
THEN COALESCE(
|
||||||
|
-- Strategy 1: flatten products from orderList[].items[]
|
||||||
(SELECT jsonb_agg(
|
(SELECT jsonb_agg(
|
||||||
jsonb_build_object(
|
jsonb_build_object(
|
||||||
'name', COALESCE(src ->> 'nom', src ->> 'name', ''),
|
'name', p ->> 'product_name',
|
||||||
'quantity', '',
|
'quantity', COALESCE(NULLIF(p ->> 'product_quantity', ''), ''),
|
||||||
'items', COALESCE(src -> 'orderList', src -> 'items', '[]'::jsonb)
|
'unit', COALESCE(NULLIF(p ->> 'product_ed', ''), '')
|
||||||
)
|
)
|
||||||
) FROM jsonb_array_elements(v_group.source_orders) AS src),
|
)
|
||||||
|
FROM jsonb_array_elements(v_group.source_orders) AS src,
|
||||||
|
LATERAL jsonb_array_elements(
|
||||||
|
CASE
|
||||||
|
WHEN jsonb_typeof(src -> 'orderList') = 'array' THEN src -> 'orderList'
|
||||||
|
ELSE '[]'::jsonb
|
||||||
|
END
|
||||||
|
) AS so,
|
||||||
|
LATERAL jsonb_array_elements(
|
||||||
|
CASE
|
||||||
|
WHEN jsonb_typeof(so -> 'items') = 'array' THEN so -> 'items'
|
||||||
|
ELSE '[]'::jsonb
|
||||||
|
END
|
||||||
|
) AS p
|
||||||
|
WHERE p ->> 'product_name' IS NOT NULL
|
||||||
|
AND p ->> 'product_name' != ''),
|
||||||
|
|
||||||
|
-- Strategy 2: products inside items[] directly on source_orders entry
|
||||||
|
(SELECT jsonb_agg(
|
||||||
|
jsonb_build_object(
|
||||||
|
'name', p ->> 'product_name',
|
||||||
|
'quantity', COALESCE(NULLIF(p ->> 'product_quantity', ''), ''),
|
||||||
|
'unit', COALESCE(NULLIF(p ->> 'product_ed', ''), '')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
FROM jsonb_array_elements(v_group.source_orders) AS src,
|
||||||
|
LATERAL jsonb_array_elements(
|
||||||
|
CASE
|
||||||
|
WHEN jsonb_typeof(src -> 'items') = 'array' THEN src -> 'items'
|
||||||
|
ELSE '[]'::jsonb
|
||||||
|
END
|
||||||
|
) AS p
|
||||||
|
WHERE p ->> 'product_name' IS NOT NULL
|
||||||
|
AND p ->> 'product_name' != ''
|
||||||
|
AND NOT (p ? 'nom' AND p ? 'items')), -- exclude sub-order entries
|
||||||
|
|
||||||
|
-- Strategy 3: fallback to invoice names
|
||||||
|
(SELECT jsonb_agg(jsonb_build_object('name', on_num, 'quantity', ''))
|
||||||
|
FROM jsonb_array_elements_text(
|
||||||
|
CASE
|
||||||
|
WHEN jsonb_typeof(to_jsonb(v_group.order_numbers)) = 'array' THEN to_jsonb(v_group.order_numbers)
|
||||||
|
ELSE '[]'::jsonb
|
||||||
|
END
|
||||||
|
) AS on_num),
|
||||||
|
|
||||||
'[]'::jsonb
|
'[]'::jsonb
|
||||||
)
|
)
|
||||||
ELSE COALESCE(
|
ELSE COALESCE(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button } from "../UI/Button";
|
import { Button } from "../UI/Button";
|
||||||
import { Panel } from "../UI/Panel";
|
import { Panel } from "../UI/Panel";
|
||||||
import { formatDeliverySlotGroupLabel } from "./deliveryDateFormatting";
|
import { formatDeliveryDate, getDeliveryRelativeDayLabel } from "./deliveryDateFormatting";
|
||||||
|
|
||||||
const groupSlotsByDate = (slots) => {
|
const groupSlotsByDate = (slots) => {
|
||||||
const groups = new Map();
|
const groups = new Map();
|
||||||
|
|
@ -36,7 +36,19 @@ const groupSlotsByDate = (slots) => {
|
||||||
.sort(([a], [b]) => a.localeCompare(b));
|
.sort(([a], [b]) => a.localeCompare(b));
|
||||||
};
|
};
|
||||||
|
|
||||||
export { formatDeliverySlotGroupLabel } from "./deliveryDateFormatting";
|
const getDeliverySlotGroupHeading = (dateStr, referenceDate = new Date()) => {
|
||||||
|
const relative = getDeliveryRelativeDayLabel(dateStr, referenceDate);
|
||||||
|
const formatted = formatDeliveryDate(dateStr);
|
||||||
|
|
||||||
|
if (relative) {
|
||||||
|
return `Доставка ${relative.charAt(0).toLowerCase()}${relative.slice(1)} · ${formatted}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Доставка ${formatted}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { formatDeliveryDate, formatDeliverySlotGroupLabel } from "./deliveryDateFormatting";
|
||||||
|
export { getDeliverySlotGroupHeading };
|
||||||
|
|
||||||
export const DeliverySlotsPicker = ({
|
export const DeliverySlotsPicker = ({
|
||||||
slots,
|
slots,
|
||||||
|
|
@ -60,10 +72,7 @@ export const DeliverySlotsPicker = ({
|
||||||
<details key={date} className="group rounded-[28px] border border-[var(--color-border)] bg-[var(--color-surface)] shadow-soft backdrop-blur" open>
|
<details key={date} className="group rounded-[28px] border border-[var(--color-border)] bg-[var(--color-surface)] shadow-soft backdrop-blur" open>
|
||||||
<summary className="cursor-pointer list-none p-5 sm:p-6">
|
<summary className="cursor-pointer list-none p-5 sm:p-6">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="space-y-1">
|
<h4 className="font-medium">{getDeliverySlotGroupHeading(date, referenceDate)}</h4>
|
||||||
<p className="text-sm uppercase tracking-[0.18em] text-[var(--color-text-muted)]">Доставка на день</p>
|
|
||||||
<h4 className="font-medium">{formatDeliverySlotGroupLabel(date, referenceDate)}</h4>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-[var(--color-text-muted)] group-open:hidden">Раскрыть</span>
|
<span className="text-sm text-[var(--color-text-muted)] group-open:hidden">Раскрыть</span>
|
||||||
<span className="hidden text-sm text-[var(--color-text-muted)] group-open:inline">Свернуть</span>
|
<span className="hidden text-sm text-[var(--color-text-muted)] group-open:inline">Свернуть</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,44 +4,57 @@ import { Panel } from "../UI/Panel";
|
||||||
import { getInvitationReferenceLabel } from "./invitationReference";
|
import { getInvitationReferenceLabel } from "./invitationReference";
|
||||||
|
|
||||||
const flattenOrderProducts = (rawItems) => {
|
const flattenOrderProducts = (rawItems) => {
|
||||||
|
if (!Array.isArray(rawItems) || rawItems.length === 0) return [];
|
||||||
|
|
||||||
const products = [];
|
const products = [];
|
||||||
|
|
||||||
for (const item of rawItems) {
|
for (const item of rawItems) {
|
||||||
if (!item || typeof item !== "object") continue;
|
if (!item || typeof item !== "object") continue;
|
||||||
|
|
||||||
const subOrders = Array.isArray(item.items) ? item.items : [];
|
const subItems = Array.isArray(item.items) ? item.items : [];
|
||||||
|
|
||||||
if (subOrders.length === 0) {
|
if (subItems.length > 0) {
|
||||||
const name = String(item.product_name || item.name || item.nom || "").trim();
|
const hasSubOrders = subItems.some(
|
||||||
const qty = String(item.product_quantity || item.quantity || item.count || item.amount || "").trim();
|
(s) => typeof s === "object" && ("nom" in s || ("items" in s && Array.isArray(s.items))),
|
||||||
const unit = String(item.product_ed || item.unit || "").trim();
|
);
|
||||||
if (name) products.push({ name, quantity: qty, unit });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasSubOrders = subOrders.some((s) => typeof s === "object" && ("nom" in s || ("items" in s && Array.isArray(s.items))));
|
|
||||||
|
|
||||||
if (hasSubOrders) {
|
if (hasSubOrders) {
|
||||||
for (const sub of subOrders) {
|
for (const sub of subItems) {
|
||||||
if (!sub || typeof sub !== "object") continue;
|
if (!sub || typeof sub !== "object") continue;
|
||||||
const productsList = Array.isArray(sub.items) ? sub.items : [];
|
const productsList = Array.isArray(sub.items) ? sub.items : [];
|
||||||
for (const p of productsList) {
|
for (const p of productsList) {
|
||||||
if (!p || typeof p !== "object") continue;
|
if (!p || typeof p !== "object") continue;
|
||||||
const pName = String(p.product_name || p.name || "").trim();
|
const pName = String(p.product_name || p.name || "").trim();
|
||||||
const pQty = String(p.product_quantity || p.quantity || p.count || p.amount || "").trim();
|
if (!pName) continue;
|
||||||
const pUnit = String(p.product_ed || p.unit || "").trim();
|
products.push({
|
||||||
if (pName) products.push({ name: pName, quantity: pQty, unit: pUnit });
|
name: pName,
|
||||||
|
quantity: String(p.product_quantity || p.quantity || p.count || p.amount || "").trim(),
|
||||||
|
unit: String(p.product_ed || p.unit || "").trim(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const p of subOrders) {
|
for (const p of subItems) {
|
||||||
if (!p || typeof p !== "object") continue;
|
if (!p || typeof p !== "object") continue;
|
||||||
const pName = String(p.product_name || p.name || "").trim();
|
const pName = String(p.product_name || p.name || "").trim();
|
||||||
const pQty = String(p.product_quantity || p.quantity || p.count || p.amount || "").trim();
|
if (!pName) continue;
|
||||||
const pUnit = String(p.product_ed || p.unit || "").trim();
|
products.push({
|
||||||
if (pName) products.push({ name: pName, quantity: pQty, unit: pUnit });
|
name: pName,
|
||||||
|
quantity: String(p.product_quantity || p.quantity || p.count || p.amount || "").trim(),
|
||||||
|
unit: String(p.product_ed || p.unit || "").trim(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = String(item.product_name || item.name || item.nom || "").trim();
|
||||||
|
if (!name) continue;
|
||||||
|
products.push({
|
||||||
|
name,
|
||||||
|
quantity: String(item.product_quantity || item.quantity || item.count || item.amount || "").trim(),
|
||||||
|
unit: String(item.product_ed || item.unit || "").trim(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return products;
|
return products;
|
||||||
|
|
@ -64,10 +77,10 @@ export const OrderCompositionPanel = ({ invitation = {} }) => {
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
>
|
>
|
||||||
<p className="text-sm uppercase tracking-[0.18em] text-[var(--color-text-muted)]">
|
<p className="text-sm uppercase tracking-[0.18em] text-[var(--color-text-muted)]">
|
||||||
Состав заказа {reference !== "Счет —" ? reference : ""}
|
Состав заказа{reference && reference !== "Счет —" ? ` ${reference}` : ""}
|
||||||
</p>
|
</p>
|
||||||
<span className="flex items-center gap-2 text-sm text-[var(--color-text-muted)]">
|
<span className="flex items-center gap-2 text-sm text-[var(--color-text-muted)]">
|
||||||
{products.length > 0 ? `${products.length} поз.` : ""}
|
{products.length} поз.
|
||||||
<svg
|
<svg
|
||||||
className="h-4 w-4 transition-transform"
|
className="h-4 w-4 transition-transform"
|
||||||
style={{ transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)" }}
|
style={{ transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)" }}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,43 @@ import { Input } from "../UI/Input";
|
||||||
import { Select } from "../UI/Select";
|
import { Select } from "../UI/Select";
|
||||||
import { Panel } from "../UI/Panel";
|
import { Panel } from "../UI/Panel";
|
||||||
|
|
||||||
|
const extractCity = (address) => {
|
||||||
|
if (!address || typeof address !== "string") return null;
|
||||||
|
const trimmed = address.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
|
||||||
|
// Patterns: "г.Ялта", "г. Ялта", "г Ялта", "г Ялта ", "Ялта,", "г.Севастополь", etc.
|
||||||
|
const cityMatch = trimmed.match(/(?:г\.?\s*|г\s+)([А-ЯЁA-Z][а-яёa-zA-Z\s\-]+?)(?:\s*[,;.]|$)/i);
|
||||||
|
if (cityMatch) {
|
||||||
|
return cityMatch[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try common city names directly in the address
|
||||||
|
const knownCities = [
|
||||||
|
"Севастополь", "Ялта", "Симферополь", "Феодосия", "Евпатория",
|
||||||
|
"Керчь", "Алушта", "Бахчисарай", "Судак", "Инкерман",
|
||||||
|
"Джанкой", "Красногвардейское", "Раздольное", "Черноморское",
|
||||||
|
];
|
||||||
|
for (const city of knownCities) {
|
||||||
|
if (trimmed.toLowerCase().includes(city.toLowerCase())) {
|
||||||
|
return city;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: first comma-separated segment if it looks like a city
|
||||||
|
const firstSegment = trimmed.split(/[,;]/)[0].trim();
|
||||||
|
if (firstSegment.length > 2 && firstSegment.length < 30 && !/^\d/.test(firstSegment)) {
|
||||||
|
return firstSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeCity = (address) => {
|
||||||
|
const city = extractCity(address);
|
||||||
|
return city || "Севастополь";
|
||||||
|
};
|
||||||
|
|
||||||
const DRIVER_DELIVERY_STATUS_OPTIONS = [
|
const DRIVER_DELIVERY_STATUS_OPTIONS = [
|
||||||
{ value: "all", label: "Все статусы" },
|
{ value: "all", label: "Все статусы" },
|
||||||
...DRIVER_VISIBLE_DELIVERY_STATUSES.map((status) => ({
|
...DRIVER_VISIBLE_DELIVERY_STATUSES.map((status) => ({
|
||||||
|
|
@ -25,6 +62,7 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
||||||
const [filters, setFilters] = React.useState({
|
const [filters, setFilters] = React.useState({
|
||||||
selectedDate: "",
|
selectedDate: "",
|
||||||
deliveryStatus: "all",
|
deliveryStatus: "all",
|
||||||
|
selectedCity: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const driverOrderGroups = React.useMemo(
|
const driverOrderGroups = React.useMemo(
|
||||||
|
|
@ -36,7 +74,7 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
||||||
[orderGroups, currentUser],
|
[orderGroups, currentUser],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build map of date -> count for quick lookup
|
// Build map of date -> count
|
||||||
const dateDeliveryMap = React.useMemo(() => {
|
const dateDeliveryMap = React.useMemo(() => {
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
driverOrderGroups.forEach((group) => {
|
driverOrderGroups.forEach((group) => {
|
||||||
|
|
@ -52,6 +90,25 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
||||||
return Array.from(dateDeliveryMap.keys()).sort();
|
return Array.from(dateDeliveryMap.keys()).sort();
|
||||||
}, [dateDeliveryMap]);
|
}, [dateDeliveryMap]);
|
||||||
|
|
||||||
|
// Build map of city -> count
|
||||||
|
const cityDeliveryMap = React.useMemo(() => {
|
||||||
|
const map = new Map();
|
||||||
|
driverOrderGroups.forEach((group) => {
|
||||||
|
const city = normalizeCity(group.deliveryAddress || group.delivery_address);
|
||||||
|
map.set(city, (map.get(city) || 0) + 1);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [driverOrderGroups]);
|
||||||
|
|
||||||
|
const sortedCities = React.useMemo(() => {
|
||||||
|
return Array.from(cityDeliveryMap.keys()).sort((a, b) => {
|
||||||
|
// Севастополь first, then alphabetical
|
||||||
|
if (a === "Севастополь") return -1;
|
||||||
|
if (b === "Севастополь") return 1;
|
||||||
|
return a.localeCompare(b, "ru");
|
||||||
|
});
|
||||||
|
}, [cityDeliveryMap]);
|
||||||
|
|
||||||
const filteredOrderGroups = React.useMemo(() => {
|
const filteredOrderGroups = React.useMemo(() => {
|
||||||
let result = [...driverOrderGroups];
|
let result = [...driverOrderGroups];
|
||||||
if (filters.selectedDate) {
|
if (filters.selectedDate) {
|
||||||
|
|
@ -60,8 +117,14 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
||||||
if (filters.deliveryStatus !== "all") {
|
if (filters.deliveryStatus !== "all") {
|
||||||
result = result.filter((group) => (group.deliveryStatus || group.delivery_status) === filters.deliveryStatus);
|
result = result.filter((group) => (group.deliveryStatus || group.delivery_status) === filters.deliveryStatus);
|
||||||
}
|
}
|
||||||
|
if (filters.selectedCity) {
|
||||||
|
result = result.filter((group) => {
|
||||||
|
const city = normalizeCity(group.deliveryAddress || group.delivery_address);
|
||||||
|
return city === filters.selectedCity;
|
||||||
|
});
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}, [driverOrderGroups, filters.selectedDate, filters.deliveryStatus]);
|
}, [driverOrderGroups, filters.selectedDate, filters.deliveryStatus, filters.selectedCity]);
|
||||||
|
|
||||||
const groupedOrderGroups = React.useMemo(
|
const groupedOrderGroups = React.useMemo(
|
||||||
() => groupOrderGroupsByDate(filteredOrderGroups),
|
() => groupOrderGroupsByDate(filteredOrderGroups),
|
||||||
|
|
@ -85,7 +148,7 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
||||||
<Badge tone="neutral">{deliveryCountLabel}</Badge>
|
<Badge tone="neutral">{deliveryCountLabel}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
|
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
|
||||||
Показываем только согласованные к доставке группы. Выберите дату ниже.
|
Показываем только назначенные вам группы доставки. Выберите дату и город.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -120,7 +183,7 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Date pills showing days with deliveries */}
|
{/* Date pills */}
|
||||||
{sortedDeliveryDates.length > 0 && (
|
{sortedDeliveryDates.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2 pt-2">
|
<div className="flex flex-wrap gap-2 pt-2">
|
||||||
<button
|
<button
|
||||||
|
|
@ -161,11 +224,77 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* City pills */}
|
||||||
|
{sortedCities.length > 1 && (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFilters((current) => ({ ...current, selectedCity: "" }))}
|
||||||
|
className={[
|
||||||
|
"rounded-full border px-3 py-1.5 text-xs font-medium transition",
|
||||||
|
!filters.selectedCity
|
||||||
|
? "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-muted)] hover:border-[var(--color-accent)]",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
Все города
|
||||||
|
</button>
|
||||||
|
{sortedCities.map((city) => {
|
||||||
|
const count = cityDeliveryMap.get(city) || 0;
|
||||||
|
const selected = filters.selectedCity === city;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={city}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFilters((current) => ({ ...current, selectedCity: city }))}
|
||||||
|
className={[
|
||||||
|
"flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium transition",
|
||||||
|
selected
|
||||||
|
? "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-muted)] hover:border-[var(--color-accent)]",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
<span>{city}</span>
|
||||||
|
{count > 0 && (
|
||||||
|
<span className="rounded-full bg-[var(--color-accent)] px-1.5 py-0.5 text-[10px] font-bold text-white">
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
{groupedOrderGroups.length ? (
|
{groupedOrderGroups.length ? (
|
||||||
groupedOrderGroups.map((group) => (
|
groupedOrderGroups.map((group) => {
|
||||||
|
// Group items by delivery status within each date
|
||||||
|
const statusBuckets = new Map();
|
||||||
|
for (const item of group.items) {
|
||||||
|
const s = item.deliveryStatus || item.delivery_status || "unknown";
|
||||||
|
const label = s === "driver_assigned" ? "Назначено вам" : getOrderGroupDeliveryStatusLabel(s);
|
||||||
|
const tone = getOrderGroupDeliveryStatusTone(s);
|
||||||
|
if (!statusBuckets.has(s)) {
|
||||||
|
statusBuckets.set(s, { label, tone, items: [] });
|
||||||
|
}
|
||||||
|
statusBuckets.get(s).items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort status buckets in driver-relevant order
|
||||||
|
const statusOrder = ["driver_assigned", "loaded", "on_route", "delivered", "problem"];
|
||||||
|
const sortedBuckets = Array.from(statusBuckets.entries()).sort(([a], [b]) => {
|
||||||
|
const ia = statusOrder.indexOf(a);
|
||||||
|
const ib = statusOrder.indexOf(b);
|
||||||
|
if (ia === -1 && ib === -1) return a.localeCompare(b);
|
||||||
|
if (ia === -1) return 1;
|
||||||
|
if (ib === -1) return -1;
|
||||||
|
return ia - ib;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
<Panel key={group.date} className="space-y-4 p-5">
|
<Panel key={group.date} className="space-y-4 p-5">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -192,8 +321,14 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{sortedBuckets.map(([statusValue, { label, tone, items }]) => (
|
||||||
|
<div key={statusValue} className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge tone={tone}>{label}</Badge>
|
||||||
|
<span className="text-xs text-[var(--color-text-muted)]">{items.length} {items.length === 1 ? "группа" : "группы"}</span>
|
||||||
|
</div>
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
{group.items.map((item) => (
|
{items.map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -210,23 +345,19 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
||||||
{getOrderGroupDeliveryHalfDay(item) ? ` · ${getOrderGroupDeliveryHalfDay(item)}` : ""}
|
{getOrderGroupDeliveryHalfDay(item) ? ` · ${getOrderGroupDeliveryHalfDay(item)}` : ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge tone={getOrderGroupDeliveryStatusTone(item.deliveryStatus || item.delivery_status)}>
|
|
||||||
{(() => { const s = item.deliveryStatus || item.delivery_status; return s === "driver_assigned" ? "Назначено вам" : getOrderGroupDeliveryStatusLabel(s); })()}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 grid gap-2 text-sm text-[var(--color-text-muted)] md:grid-cols-3">
|
<div className="mt-3 text-sm text-[var(--color-text-muted)]">
|
||||||
<div>{item.orderNumbers?.[0] || "Номера не указаны"}</div>
|
{item.deliveryAddress || item.delivery_address || "Адрес не указан"}
|
||||||
<div>
|
|
||||||
{item.readyCount || 0}/{item.ordersCount || 0} готово
|
|
||||||
</div>
|
|
||||||
<div>{item.smsSentAt ? "SMS отправлено" : "SMS не отправлено"}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</Panel>
|
</Panel>
|
||||||
))
|
);
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<Panel className="p-6">
|
<Panel className="p-6">
|
||||||
<h4 className="text-lg font-semibold">Доставки не найдены</h4>
|
<h4 className="text-lg font-semibold">Доставки не найдены</h4>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,246 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Badge } from "../UI/Badge";
|
||||||
|
import { Button } from "../UI/Button";
|
||||||
|
import { Panel } from "../UI/Panel";
|
||||||
|
|
||||||
|
const parseOrderItems = (order) => {
|
||||||
|
if (!order) return [];
|
||||||
|
|
||||||
|
const sourceOrders = order.sourceOrders || order.source_orders;
|
||||||
|
if (Array.isArray(sourceOrders) && sourceOrders.length > 0) {
|
||||||
|
const products = [];
|
||||||
|
for (const src of sourceOrders) {
|
||||||
|
if (!src || typeof src !== "object") continue;
|
||||||
|
const orderList = Array.isArray(src.orderList) ? src.orderList : Array.isArray(src.items) ? src.items : [];
|
||||||
|
for (const sub of orderList) {
|
||||||
|
if (!sub || typeof sub !== "object") continue;
|
||||||
|
const subItems = Array.isArray(sub.items) ? sub.items : [];
|
||||||
|
const hasProducts = subItems.some(
|
||||||
|
(p) => typeof p === "object" && (p.product_name || p.name)
|
||||||
|
);
|
||||||
|
if (hasProducts) {
|
||||||
|
for (const p of subItems) {
|
||||||
|
if (!p || typeof p !== "object") continue;
|
||||||
|
const name = String(p.product_name || p.name || "").trim();
|
||||||
|
if (!name) continue;
|
||||||
|
products.push({
|
||||||
|
id: `${src.nom || ""}-${sub.nom || ""}-${name}`,
|
||||||
|
name,
|
||||||
|
quantity: String(p.product_quantity || p.quantity || p.count || p.amount || "").trim(),
|
||||||
|
unit: String(p.product_ed || p.unit || "").trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (sub.nom || sub.name) {
|
||||||
|
products.push({
|
||||||
|
id: `${src.nom || ""}-${sub.nom || sub.name || ""}`,
|
||||||
|
name: String(sub.nom || sub.name || "").trim(),
|
||||||
|
quantity: "",
|
||||||
|
unit: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orderList.length === 0) {
|
||||||
|
const directItems = Array.isArray(src.items) ? src.items : [];
|
||||||
|
const hasDirect = directItems.some(
|
||||||
|
(p) => typeof p === "object" && (p.product_name || p.name) && !p.nom
|
||||||
|
);
|
||||||
|
if (hasDirect) {
|
||||||
|
for (const p of directItems) {
|
||||||
|
if (!p || typeof p !== "object") continue;
|
||||||
|
const name = String(p.product_name || p.name || "").trim();
|
||||||
|
if (!name || ("nom" in p && "items" in p)) continue;
|
||||||
|
products.push({
|
||||||
|
id: `${src.nom || ""}-${name}`,
|
||||||
|
name,
|
||||||
|
quantity: String(p.product_quantity || p.quantity || p.count || p.amount || "").trim(),
|
||||||
|
unit: String(p.product_ed || p.unit || "").trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (products.length > 0) return products;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderList = order.orderList || order.order_list;
|
||||||
|
if (Array.isArray(orderList)) {
|
||||||
|
const products = [];
|
||||||
|
for (const sub of orderList) {
|
||||||
|
if (!sub || typeof sub !== "object") continue;
|
||||||
|
const items = Array.isArray(sub.items) ? sub.items : [];
|
||||||
|
for (const p of items) {
|
||||||
|
if (!p || typeof p !== "object") continue;
|
||||||
|
const name = String(p.product_name || p.name || "").trim();
|
||||||
|
if (!name) continue;
|
||||||
|
products.push({
|
||||||
|
id: `${sub.nom || sub.name || ""}-${name}`,
|
||||||
|
name,
|
||||||
|
quantity: String(p.product_quantity || p.quantity || p.count || p.amount || "").trim(),
|
||||||
|
unit: String(p.product_ed || p.unit || "").trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (products.length > 0) return products;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DriverShipmentPanel = ({ order, onShipmentChange }) => {
|
||||||
|
const items = React.useMemo(() => parseOrderItems(order), [order]);
|
||||||
|
const [shippedItems, setShippedItems] = React.useState(new Set());
|
||||||
|
const [comments, setComments] = React.useState({});
|
||||||
|
const [commentInput, setCommentInput] = React.useState("");
|
||||||
|
|
||||||
|
const toggleItem = (itemId) => {
|
||||||
|
setShippedItems((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(itemId)) {
|
||||||
|
next.delete(itemId);
|
||||||
|
} else {
|
||||||
|
next.add(itemId);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const shipAll = () => {
|
||||||
|
setShippedItems(new Set(items.map((i) => i.id)));
|
||||||
|
setComments({});
|
||||||
|
};
|
||||||
|
|
||||||
|
const unshipAll = () => {
|
||||||
|
setShippedItems(new Set());
|
||||||
|
setComments({});
|
||||||
|
};
|
||||||
|
|
||||||
|
const shippedCount = items.filter((i) => shippedItems.has(i.id)).length;
|
||||||
|
const unshippedCount = items.length - shippedCount;
|
||||||
|
const allShipped = items.length > 0 && shippedCount === items.length;
|
||||||
|
const unshippedWithComment = items.filter(
|
||||||
|
(i) => !shippedItems.has(i.id) && comments[i.id]?.trim(),
|
||||||
|
).length;
|
||||||
|
const unshippedWithoutComment = unshippedCount - unshippedWithComment;
|
||||||
|
|
||||||
|
// Notify parent of shipment state
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (onShipmentChange) {
|
||||||
|
onShipmentChange({
|
||||||
|
total: items.length,
|
||||||
|
shipped: shippedCount,
|
||||||
|
unshipped: unshippedCount,
|
||||||
|
unshippedWithoutComment,
|
||||||
|
allShipped,
|
||||||
|
canMarkDelivered: allShipped || unshippedWithoutComment === 0,
|
||||||
|
shipmentData: items.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
quantity: item.quantity,
|
||||||
|
unit: item.unit,
|
||||||
|
shipped: shippedItems.has(item.id),
|
||||||
|
comment: shippedItems.has(item.id) ? "" : (comments[item.id] || ""),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [items.length, shippedCount, unshippedCount, unshippedWithoutComment, allShipped, shippedItems, comments, onShipmentChange]);
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
return (
|
||||||
|
<Panel className="space-y-3 p-5">
|
||||||
|
<strong>Состав заказа</strong>
|
||||||
|
<p className="text-sm text-[var(--color-text-muted)]">Позиции не указаны</p>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel className="space-y-4 p-5">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<strong>Отгрузка</strong>
|
||||||
|
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
|
||||||
|
Отметьте позиции, которые отгружены. Для смены статуса на «Доставлено» все позиции должны быть отгружены.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Badge tone={allShipped ? "accent" : "neutral"}>
|
||||||
|
{shippedCount}/{items.length} отгружено
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="secondary" size="sm" onClick={shipAll} disabled={allShipped}>
|
||||||
|
Отгрузить всё
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" onClick={unshipAll} disabled={shippedCount === 0}>
|
||||||
|
Сбросить
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{items.map((item) => {
|
||||||
|
const isShipped = shippedItems.has(item.id);
|
||||||
|
const hasComment = !isShipped && comments[item.id]?.trim();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className={[
|
||||||
|
"rounded-[18px] border px-4 py-3 text-sm transition",
|
||||||
|
isShipped
|
||||||
|
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)]"
|
||||||
|
: hasComment
|
||||||
|
? "border-[var(--color-warning)] bg-[var(--color-warning-soft)]"
|
||||||
|
: "border-[var(--color-border)] bg-[var(--color-surface-strong)]",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
<label className="flex cursor-pointer items-start gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isShipped}
|
||||||
|
onChange={() => toggleItem(item.id)}
|
||||||
|
className="mt-0.5 h-4 w-4 flex-shrink-0 accent-[var(--color-accent)]"
|
||||||
|
/>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<span className={isShipped ? "text-[var(--color-text-muted)]" : "text-[var(--color-text)]"}>
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
{(item.quantity || item.unit) ? (
|
||||||
|
<Badge tone="neutral">{[item.quantity, item.unit].filter(Boolean).join(" ")}</Badge>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{!isShipped && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Причина неотгрузки (дефект, нет в наличии...)"
|
||||||
|
value={comments[item.id] || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setComments((prev) => ({ ...prev, [item.id]: e.target.value }))
|
||||||
|
}
|
||||||
|
className="mt-2 w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-xs text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)] focus:outline-none"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{unshippedCount > 0 && (
|
||||||
|
<div className="rounded-xl border border-[var(--color-warning)] bg-[var(--color-warning-soft)] p-3 text-sm">
|
||||||
|
<p className="font-medium text-[var(--color-text)]">
|
||||||
|
Не отгружено: {unshippedCount} {unshippedCount === 1 ? "позиция" : unshippedCount < 5 ? "позиции" : "позиций"}
|
||||||
|
</p>
|
||||||
|
{unshippedWithoutComment > 0 && (
|
||||||
|
<p className="mt-1 text-[var(--color-text-muted)]">
|
||||||
|
Укажите причину для каждой неотгруженной позиции, чтобы завершить доставку.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -4,6 +4,7 @@ import { Badge } from "../UI/Badge";
|
||||||
import { Button } from "../UI/Button";
|
import { Button } from "../UI/Button";
|
||||||
import { Select } from "../UI/Select";
|
import { Select } from "../UI/Select";
|
||||||
import { Panel } from "../UI/Panel";
|
import { Panel } from "../UI/Panel";
|
||||||
|
import { DriverShipmentPanel } from "../driver/DriverShipmentPanel";
|
||||||
import {
|
import {
|
||||||
getOrderGroupDeliveryStatusLabel,
|
getOrderGroupDeliveryStatusLabel,
|
||||||
getOrderGroupDisplayStatusLabel,
|
getOrderGroupDisplayStatusLabel,
|
||||||
|
|
@ -397,6 +398,7 @@ export const OrderDetailPanel = ({
|
||||||
const [deliveryDate, setDeliveryDate] = React.useState("");
|
const [deliveryDate, setDeliveryDate] = React.useState("");
|
||||||
const [deliveryTime, setDeliveryTime] = React.useState(DELIVERY_TIME_OPTIONS[0]);
|
const [deliveryTime, setDeliveryTime] = React.useState(DELIVERY_TIME_OPTIONS[0]);
|
||||||
const [formMessage, setFormMessage] = React.useState("");
|
const [formMessage, setFormMessage] = React.useState("");
|
||||||
|
const [shipmentState, setShipmentState] = React.useState(null);
|
||||||
const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
|
const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
|
||||||
const [driverMessage, setDriverMessage] = React.useState("");
|
const [driverMessage, setDriverMessage] = React.useState("");
|
||||||
const [selectedDriverId, setSelectedDriverId] = React.useState(order?.assignedDriverId || "");
|
const [selectedDriverId, setSelectedDriverId] = React.useState(order?.assignedDriverId || "");
|
||||||
|
|
@ -450,6 +452,10 @@ export const OrderDetailPanel = ({
|
||||||
order.deliveryTime || order.deliveryHalfDay,
|
order.deliveryTime || order.deliveryHalfDay,
|
||||||
].filter((value) => value && value !== "Нет данных").join(" · ");
|
].filter((value) => value && value !== "Нет данных").join(" · ");
|
||||||
|
|
||||||
|
const handleShipmentChange = React.useCallback((state) => {
|
||||||
|
setShipmentState(state);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleSaveDeliveryChoice = async () => {
|
const handleSaveDeliveryChoice = async () => {
|
||||||
if (!deliveryDate || !deliveryTime) {
|
if (!deliveryDate || !deliveryTime) {
|
||||||
setFormMessage("Укажите дату и половину дня доставки.");
|
setFormMessage("Укажите дату и половину дня доставки.");
|
||||||
|
|
@ -485,6 +491,11 @@ export const OrderDetailPanel = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!order.deliveryDate) {
|
||||||
|
setDriverMessage("Сначала укажите дату и время доставки.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setDriverMessage("");
|
setDriverMessage("");
|
||||||
const response = await onAssignDriver({
|
const response = await onAssignDriver({
|
||||||
orderGroupId: order.id,
|
orderGroupId: order.id,
|
||||||
|
|
@ -590,96 +601,6 @@ export const OrderDetailPanel = ({
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
{canManageDelivery && ["manager", "logistician", "admin"].includes(userRole) ? (
|
|
||||||
<Panel className="space-y-4 p-5">
|
|
||||||
<div>
|
|
||||||
<strong>Назначение водителя</strong>
|
|
||||||
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
|
|
||||||
{order.assignedDriverId
|
|
||||||
? `Назначен водитель: ${order.assignedDriverName || "Неизвестно"}. Вы можете изменить назначение.`
|
|
||||||
: "Выберите водителя для доставки."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-3 md:grid-cols-[minmax(16rem,24rem)_auto]">
|
|
||||||
<Select
|
|
||||||
className="h-[46px] py-0"
|
|
||||||
value={selectedDriverId}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSelectedDriverId(e.target.value);
|
|
||||||
setDriverMessage("");
|
|
||||||
}}
|
|
||||||
disabled={isSavingDeliveryChoice}
|
|
||||||
>
|
|
||||||
<option value="">{order.assignedDriverId ? "Сменить водителя..." : "Выберите водителя..."}</option>
|
|
||||||
{drivers.map((driver) => (
|
|
||||||
<option key={driver.id} value={driver.id}>{driver.name || driver.email}</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<Button
|
|
||||||
className="md:px-4 md:py-2 md:whitespace-nowrap md:self-start"
|
|
||||||
onClick={handleAssignDriver}
|
|
||||||
disabled={isSavingDeliveryChoice || !selectedDriverId}
|
|
||||||
>
|
|
||||||
{isSavingDeliveryChoice ? "Назначаем..." : "Назначить"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{driverMessage ? (
|
|
||||||
<p className="text-sm text-[var(--color-text-muted)]">{driverMessage}</p>
|
|
||||||
) : null}
|
|
||||||
</Panel>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{userRole === "driver" && order && onChangeDeliveryStatus ? (
|
|
||||||
<Panel className="space-y-4 p-5">
|
|
||||||
<div>
|
|
||||||
<strong>Статус доставки</strong>
|
|
||||||
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
|
|
||||||
Обновите статус по мере выполнения доставки.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{[
|
|
||||||
{ value: "loaded", label: "Загружено" },
|
|
||||||
{ value: "on_route", label: "В пути" },
|
|
||||||
{ value: "delivered", label: "Доставлено" },
|
|
||||||
{ value: "problem", label: "Проблема" },
|
|
||||||
{ value: "cancelled", label: "Отменено" },
|
|
||||||
].map((statusOption) => (
|
|
||||||
<Button
|
|
||||||
key={statusOption.value}
|
|
||||||
variant={
|
|
||||||
(order.deliveryStatus || order.delivery_status) === statusOption.value ? "primary" : "secondary"}
|
|
||||||
onClick={() => {
|
|
||||||
onChangeDeliveryStatus({
|
|
||||||
orderGroupId: order.id,
|
|
||||||
status: statusOption.value,
|
|
||||||
}).then((response) => {
|
|
||||||
if (!response.success) {
|
|
||||||
setFormMessage(response.error || "Не удалось обновить статус");
|
|
||||||
} else {
|
|
||||||
setFormMessage("");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
disabled={isSavingDeliveryChoice}
|
|
||||||
>
|
|
||||||
{statusOption.label}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Panel>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
|
|
||||||
{["manager", "logistician", "admin"].includes(userRole) && order && onChangeDeliveryStatus ? (
|
|
||||||
<PaidStoragePanel
|
|
||||||
order={order}
|
|
||||||
onChangeDeliveryStatus={onChangeDeliveryStatus}
|
|
||||||
isSavingDeliveryChoice={isSavingDeliveryChoice}
|
|
||||||
setFormMessage={setFormMessage}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{canManageDelivery ? (
|
{canManageDelivery ? (
|
||||||
<Panel className="space-y-4 p-5">
|
<Panel className="space-y-4 p-5">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -849,6 +770,152 @@ export const OrderDetailPanel = ({
|
||||||
</Panel>
|
</Panel>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
||||||
|
{canManageDelivery && ["manager", "logistician", "admin"].includes(userRole) ? (
|
||||||
|
<Panel className="space-y-4 p-5">
|
||||||
|
<div>
|
||||||
|
<strong>Назначение водителя</strong>
|
||||||
|
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
|
||||||
|
{order.assignedDriverId
|
||||||
|
? `Назначен водитель: ${order.assignedDriverName || "Неизвестно"}. Вы можете изменить назначение.`
|
||||||
|
: "Выберите водителя для доставки."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 md:grid-cols-[minmax(16rem,24rem)_auto]">
|
||||||
|
<Select
|
||||||
|
className="h-[46px] py-0"
|
||||||
|
value={selectedDriverId}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedDriverId(e.target.value);
|
||||||
|
setDriverMessage("");
|
||||||
|
}}
|
||||||
|
disabled={isSavingDeliveryChoice}
|
||||||
|
>
|
||||||
|
<option value="">{order.assignedDriverId ? "Сменить водителя..." : "Выберите водителя..."}</option>
|
||||||
|
{drivers.map((driver) => (
|
||||||
|
<option key={driver.id} value={driver.id}>{driver.name || driver.email}</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
className="md:px-4 md:py-2 md:whitespace-nowrap md:self-start"
|
||||||
|
onClick={handleAssignDriver}
|
||||||
|
disabled={isSavingDeliveryChoice || !selectedDriverId}
|
||||||
|
>
|
||||||
|
{isSavingDeliveryChoice ? "Назначаем..." : "Назначить"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{driverMessage ? (
|
||||||
|
<p className="text-sm text-[var(--color-text-muted)]">{driverMessage}</p>
|
||||||
|
) : null}
|
||||||
|
</Panel>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
|
||||||
|
{["manager", "logistician", "admin"].includes(userRole) && order && onChangeDeliveryStatus ? (
|
||||||
|
<Panel className="space-y-4 p-5">
|
||||||
|
<div>
|
||||||
|
<strong>Статус доставки</strong>
|
||||||
|
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
|
||||||
|
Измените статус, если водитель забыл обновить или нужна корректировка.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{[
|
||||||
|
{ value: "pending_confirmation", label: "Ожидает согласования" },
|
||||||
|
{ value: "agreed", label: "Согласовано" },
|
||||||
|
{ value: "driver_assigned", label: "Назначен водитель" },
|
||||||
|
{ value: "loaded", label: "Загружено" },
|
||||||
|
{ value: "on_route", label: "В пути" },
|
||||||
|
{ value: "delivered", label: "Доставлено" },
|
||||||
|
{ value: "problem", label: "Проблема" },
|
||||||
|
{ value: "cancelled", label: "Отменено" },
|
||||||
|
].map((statusOption) => (
|
||||||
|
<Button
|
||||||
|
key={statusOption.value}
|
||||||
|
variant={
|
||||||
|
(order.deliveryStatus || order.delivery_status) === statusOption.value ? "primary" : "secondary"}
|
||||||
|
onClick={() => {
|
||||||
|
onChangeDeliveryStatus({
|
||||||
|
orderGroupId: order.id,
|
||||||
|
status: statusOption.value,
|
||||||
|
}).then((response) => {
|
||||||
|
if (!response.success) {
|
||||||
|
setFormMessage(response.error || "Не удалось обновить статус");
|
||||||
|
} else {
|
||||||
|
setFormMessage("");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={isSavingDeliveryChoice}
|
||||||
|
>
|
||||||
|
{statusOption.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
|
||||||
|
{["manager", "logistician", "admin"].includes(userRole) && order && onChangeDeliveryStatus ? (
|
||||||
|
<PaidStoragePanel
|
||||||
|
order={order}
|
||||||
|
onChangeDeliveryStatus={onChangeDeliveryStatus}
|
||||||
|
isSavingDeliveryChoice={isSavingDeliveryChoice}
|
||||||
|
setFormMessage={setFormMessage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{userRole === "driver" && order ? (
|
||||||
|
<DriverShipmentPanel order={order} onShipmentChange={handleShipmentChange} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{userRole === "driver" && order && onChangeDeliveryStatus ? (
|
||||||
|
<Panel className="space-y-4 p-5">
|
||||||
|
<div>
|
||||||
|
<strong>Статус доставки</strong>
|
||||||
|
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
|
||||||
|
Обновите статус по мере выполнения доставки.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{[
|
||||||
|
{ value: "loaded", label: "Загружено" },
|
||||||
|
{ value: "on_route", label: "В пути" },
|
||||||
|
{ value: "delivered", label: "Доставлено" },
|
||||||
|
{ value: "problem", label: "Проблема" },
|
||||||
|
].map((statusOption) => (
|
||||||
|
<Button
|
||||||
|
key={statusOption.value}
|
||||||
|
variant={
|
||||||
|
(order.deliveryStatus || order.delivery_status) === statusOption.value ? "primary" : "secondary"}
|
||||||
|
onClick={() => {
|
||||||
|
if (statusOption.value === "delivered" && shipmentState && !shipmentState.canMarkDelivered) {
|
||||||
|
setFormMessage("Укажите причину для каждой неотгруженной позиции перед завершением доставки.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onChangeDeliveryStatus({
|
||||||
|
orderGroupId: order.id,
|
||||||
|
status: statusOption.value,
|
||||||
|
}).then((response) => {
|
||||||
|
if (!response.success) {
|
||||||
|
setFormMessage(response.error || "Не удалось обновить статус");
|
||||||
|
} else {
|
||||||
|
setFormMessage("");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={isSavingDeliveryChoice}
|
||||||
|
>
|
||||||
|
{statusOption.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{formMessage ? (
|
||||||
|
<p className="text-sm text-[var(--color-warning)]">{formMessage}</p>
|
||||||
|
) : null}
|
||||||
|
</Panel>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Panel className="space-y-4 p-5">
|
<Panel className="space-y-4 p-5">
|
||||||
<strong>Номера заказов</strong>
|
<strong>Номера заказов</strong>
|
||||||
{renderList(order.orderNumbers)}
|
{renderList(order.orderNumbers)}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Navigate } from "react-router-dom";
|
import { Navigate, useNavigate } from "react-router-dom";
|
||||||
import { DriverDeliveryPlanner } from "../components/driver/DriverDeliveryPlanner";
|
import { DriverDeliveryPlanner } from "../components/driver/DriverDeliveryPlanner";
|
||||||
import { LogisticsReadinessBoard } from "../components/logistics/LogisticsReadinessBoard";
|
import { LogisticsReadinessBoard } from "../components/logistics/LogisticsReadinessBoard";
|
||||||
import { OrderDetailPanel } from "../components/orders/OrderDetailPanel";
|
|
||||||
import { OrdersTable } from "../components/orders/OrdersTable";
|
import { OrdersTable } from "../components/orders/OrdersTable";
|
||||||
import { Button } from "../components/UI/Button";
|
|
||||||
import { Modal } from "../components/UI/Modal";
|
|
||||||
import { Panel } from "../components/UI/Panel";
|
import { Panel } from "../components/UI/Panel";
|
||||||
import { ProductGuidePanel } from "../components/dashboard/ProductGuidePanel";
|
import { ProductGuidePanel } from "../components/dashboard/ProductGuidePanel";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { fetchDrivers } from "../services/supabase/userRepository";
|
|
||||||
import { useOrderGroups } from "../hooks/useOrderGroups";
|
import { useOrderGroups } from "../hooks/useOrderGroups";
|
||||||
import { AppShell } from "../layouts/AppShell";
|
import { AppShell } from "../layouts/AppShell";
|
||||||
|
|
||||||
|
|
@ -33,17 +29,15 @@ const ROLE_SECTION = {
|
||||||
|
|
||||||
export const DashboardPage = () => {
|
export const DashboardPage = () => {
|
||||||
const { user, signOut } = useAuth();
|
const { user, signOut } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
const userRole = user?.role;
|
const userRole = user?.role;
|
||||||
const section = ROLE_SECTION[userRole] || ROLE_SECTION.manager;
|
const section = ROLE_SECTION[userRole] || ROLE_SECTION.manager;
|
||||||
const [activeSection, setActiveSection] = React.useState(section.key);
|
const [activeSection, setActiveSection] = React.useState(section.key);
|
||||||
const [isGroupModalOpen, setIsGroupModalOpen] = React.useState(false);
|
|
||||||
const [drivers, setDrivers] = React.useState([]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
orderGroups,
|
orderGroups,
|
||||||
allOrderGroups,
|
allOrderGroups,
|
||||||
filteredOrderGroups,
|
filteredOrderGroups,
|
||||||
selectedOrderGroup,
|
|
||||||
selectedOrderGroupId,
|
selectedOrderGroupId,
|
||||||
setSelectedOrderGroupId,
|
setSelectedOrderGroupId,
|
||||||
filters,
|
filters,
|
||||||
|
|
@ -51,33 +45,15 @@ export const DashboardPage = () => {
|
||||||
statusOptions,
|
statusOptions,
|
||||||
isLoading,
|
isLoading,
|
||||||
loadError,
|
loadError,
|
||||||
saveManualDeliveryChoice,
|
|
||||||
isSavingDeliveryChoice,
|
|
||||||
assignDriver,
|
|
||||||
changeDeliveryStatus,
|
|
||||||
} = useOrderGroups();
|
} = useOrderGroups();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setActiveSection(section.key);
|
setActiveSection(section.key);
|
||||||
}, [section.key]);
|
}, [section.key]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
const openGroupPage = React.useCallback((groupId) => {
|
||||||
let cancelled = false;
|
navigate(`/dashboard/group/${groupId}`);
|
||||||
const loadDrivers = async () => {
|
}, [navigate]);
|
||||||
const result = await fetchDrivers();
|
|
||||||
if (cancelled) return;
|
|
||||||
if (result.data) {
|
|
||||||
setDrivers(result.data.filter((u) => u.role === "driver"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadDrivers();
|
|
||||||
return () => { cancelled = true; };
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const openGroupModal = React.useCallback((groupId) => {
|
|
||||||
setSelectedOrderGroupId(groupId);
|
|
||||||
setIsGroupModalOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{
|
{
|
||||||
|
|
@ -104,7 +80,7 @@ export const DashboardPage = () => {
|
||||||
<OrdersTable
|
<OrdersTable
|
||||||
orderGroups={filteredOrderGroups}
|
orderGroups={filteredOrderGroups}
|
||||||
selectedOrderGroupId={selectedOrderGroupId}
|
selectedOrderGroupId={selectedOrderGroupId}
|
||||||
onOpenOrder={openGroupModal}
|
onOpenOrder={openGroupPage}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
setFilters={setFilters}
|
setFilters={setFilters}
|
||||||
statusOptions={statusOptions}
|
statusOptions={statusOptions}
|
||||||
|
|
@ -114,7 +90,7 @@ export const DashboardPage = () => {
|
||||||
|
|
||||||
const renderLogisticsWorkspace = () => (
|
const renderLogisticsWorkspace = () => (
|
||||||
<div className="space-y-6 xl:space-y-8">
|
<div className="space-y-6 xl:space-y-8">
|
||||||
<LogisticsReadinessBoard orderGroups={allOrderGroups} onSelectSet={openGroupModal} statusOptions={statusOptions} />
|
<LogisticsReadinessBoard orderGroups={allOrderGroups} onSelectSet={openGroupPage} statusOptions={statusOptions} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -122,7 +98,7 @@ export const DashboardPage = () => {
|
||||||
<div className="space-y-6 xl:space-y-8">
|
<div className="space-y-6 xl:space-y-8">
|
||||||
<DriverDeliveryPlanner
|
<DriverDeliveryPlanner
|
||||||
orderGroups={allOrderGroups}
|
orderGroups={allOrderGroups}
|
||||||
onOpenOrder={openGroupModal}
|
onOpenOrder={openGroupPage}
|
||||||
currentUser={user}
|
currentUser={user}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -167,34 +143,6 @@ export const DashboardPage = () => {
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{renderActiveSection()}
|
{renderActiveSection()}
|
||||||
|
|
||||||
<Modal isOpen={isGroupModalOpen} onClose={() => setIsGroupModalOpen(false)} className="md:max-w-[800px]">
|
|
||||||
<div className="space-y-5">
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-semibold">Карточка группы доставки</h3>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
setIsGroupModalOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Закрыть
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<OrderDetailPanel
|
|
||||||
order={selectedOrderGroup}
|
|
||||||
canManageDelivery={["manager", "logistician", "admin"].includes(userRole)}
|
|
||||||
onSaveManualDeliveryChoice={saveManualDeliveryChoice}
|
|
||||||
isSavingDeliveryChoice={isSavingDeliveryChoice}
|
|
||||||
drivers={drivers}
|
|
||||||
onAssignDriver={assignDriver}
|
|
||||||
onChangeDeliveryStatus={changeDeliveryStatus}
|
|
||||||
userRole={userRole}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { OrderDetailPanel } from "../components/orders/OrderDetailPanel";
|
||||||
|
import { Button } from "../components/UI/Button";
|
||||||
|
import { Panel } from "../components/UI/Panel";
|
||||||
|
import { useAuth } from "../context/AuthContext";
|
||||||
|
import { fetchDrivers } from "../services/supabase/userRepository";
|
||||||
|
import { useOrderGroups } from "../hooks/useOrderGroups";
|
||||||
|
|
||||||
|
export const GroupDetailPage = () => {
|
||||||
|
const { groupId } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const userRole = user?.role;
|
||||||
|
|
||||||
|
const {
|
||||||
|
allOrderGroups,
|
||||||
|
selectedOrderGroupId,
|
||||||
|
setSelectedOrderGroupId,
|
||||||
|
saveManualDeliveryChoice,
|
||||||
|
isSavingDeliveryChoice,
|
||||||
|
assignDriver,
|
||||||
|
changeDeliveryStatus,
|
||||||
|
} = useOrderGroups();
|
||||||
|
|
||||||
|
const [drivers, setDrivers] = React.useState([]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (groupId) {
|
||||||
|
setSelectedOrderGroupId(groupId);
|
||||||
|
}
|
||||||
|
}, [groupId, setSelectedOrderGroupId]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
const load = async () => {
|
||||||
|
const result = await fetchDrivers();
|
||||||
|
if (cancelled) return;
|
||||||
|
if (result.data) {
|
||||||
|
setDrivers(result.data.filter((u) => u.role === "driver"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
load();
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const order = allOrderGroups.find((g) => g.id === groupId) ||
|
||||||
|
allOrderGroups.find((g) => g.id === selectedOrderGroupId) ||
|
||||||
|
null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-3xl space-y-5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button variant="ghost" onClick={() => navigate("/dashboard")} className="text-sm">
|
||||||
|
← Назад к списку
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{order ? (
|
||||||
|
<OrderDetailPanel
|
||||||
|
order={order}
|
||||||
|
canManageDelivery={["manager", "logistician", "admin"].includes(userRole)}
|
||||||
|
onSaveManualDeliveryChoice={saveManualDeliveryChoice}
|
||||||
|
isSavingDeliveryChoice={isSavingDeliveryChoice}
|
||||||
|
drivers={drivers}
|
||||||
|
onAssignDriver={assignDriver}
|
||||||
|
onChangeDeliveryStatus={changeDeliveryStatus}
|
||||||
|
userRole={userRole}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Panel className="p-6 text-sm text-[var(--color-text-muted)]">
|
||||||
|
Группа доставки не найдена.
|
||||||
|
</Panel>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -3,6 +3,7 @@ import { Navigate, createBrowserRouter } from "react-router-dom";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import { ClientDeliveryPage } from "./pages/ClientDeliveryPage";
|
import { ClientDeliveryPage } from "./pages/ClientDeliveryPage";
|
||||||
import { DashboardPage } from "./pages/DashboardPage";
|
import { DashboardPage } from "./pages/DashboardPage";
|
||||||
|
import { GroupDetailPage } from "./pages/GroupDetailPage";
|
||||||
import { LoginPage } from "./pages/LoginPage";
|
import { LoginPage } from "./pages/LoginPage";
|
||||||
import { NotFoundPage } from "./pages/NotFoundPage";
|
import { NotFoundPage } from "./pages/NotFoundPage";
|
||||||
|
|
||||||
|
|
@ -27,6 +28,10 @@ export const router = createBrowserRouter([
|
||||||
path: "dashboard",
|
path: "dashboard",
|
||||||
element: <DashboardPage />,
|
element: <DashboardPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "dashboard/group/:groupId",
|
||||||
|
element: <GroupDetailPage />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "*",
|
path: "*",
|
||||||
element: <NotFoundPage />,
|
element: <NotFoundPage />,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ export const NOTIFICATION_STATUS_LABELS = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DRIVER_VISIBLE_DELIVERY_STATUSES = [
|
export const DRIVER_VISIBLE_DELIVERY_STATUSES = [
|
||||||
"agreed",
|
|
||||||
"driver_assigned",
|
"driver_assigned",
|
||||||
"loaded",
|
"loaded",
|
||||||
"on_route",
|
"on_route",
|
||||||
|
|
@ -34,7 +33,7 @@ export const DRIVER_VISIBLE_DELIVERY_STATUSES = [
|
||||||
"delivered",
|
"delivered",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DRIVER_ACTIVE_DELIVERY_STATUSES = ["agreed", "driver_assigned", "loaded", "on_route", "problem"];
|
export const DRIVER_ACTIVE_DELIVERY_STATUSES = ["driver_assigned", "loaded", "on_route", "problem"];
|
||||||
|
|
||||||
const HALF_DAY_LABELS = {
|
const HALF_DAY_LABELS = {
|
||||||
morning: "Первая половина дня",
|
morning: "Первая половина дня",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue