Compare commits
No commits in common. "54386e97106edfe1dae16b6da70c73d7d0f4734c" and "ae1e0a2e0e32010e306d8f517112599488cfb663" have entirely different histories.
54386e9710
...
ae1e0a2e0e
|
|
@ -1,8 +0,0 @@
|
|||
-- Allow drivers to update order_groups where they are assigned
|
||||
-- This enables saving shipment data and updating delivery status
|
||||
CREATE POLICY "order groups driver update shipment data"
|
||||
ON public.order_groups
|
||||
FOR UPDATE
|
||||
TO public
|
||||
USING (current_role_name() = 'driver' AND assigned_driver_id = auth.uid())
|
||||
WITH CHECK (current_role_name() = 'driver' AND assigned_driver_id = auth.uid());
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import { Panel } from "../UI/Panel";
|
||||
import { Badge } from "../UI/Badge";
|
||||
import { Button } from "../UI/Button";
|
||||
import { Skeleton } from "../UI/Loading";
|
||||
import { fetchActionLogs, getActionLabel } from "../../services/supabase/actionLogService";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
|
@ -198,13 +197,13 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
|
|||
Кто, что и когда делал с доставками
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
<button
|
||||
onClick={loadLogs}
|
||||
disabled={loading}
|
||||
className="rounded-[14px] bg-[var(--color-accent)] px-3 py-1.5 text-sm font-medium text-white hover:opacity-90 disabled:opacity-50"
|
||||
>
|
||||
{loading ? "Загрузка..." : "Обновить"}
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ export const AdminDashboard = () => {
|
|||
const econ = economics || {};
|
||||
|
||||
const statusPieData = (statusDist || []).map(s => ({
|
||||
name: STATUS_LABELS[s.delivery_status] || (s.delivery_status ? `Неизвестно (${s.delivery_status})` : 'Неизвестно'),
|
||||
name: STATUS_LABELS[s.delivery_status] || s.delivery_status,
|
||||
value: s.count,
|
||||
status: s.delivery_status,
|
||||
})).filter(d => d.value > 0);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Panel } from '../UI/Panel';
|
||||
import { Badge } from '../UI/Badge';
|
||||
import { Button } from '../UI/Button';
|
||||
import { Select } from '../UI/Select';
|
||||
import { Skeleton } from '../UI/Loading';
|
||||
import { supabase } from '../../supabaseClient';
|
||||
|
|
@ -236,29 +235,29 @@ export default function ErrorLogPanel() {
|
|||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<Button onClick={fetchErrors} style={btnBase} title="Обновить">↻</Button>
|
||||
<Button onClick={handleCopyAll} disabled={errors.length === 0} style={{ ...btnAccent, opacity: errors.length === 0 ? 0.4 : 1 }}>
|
||||
<button onClick={fetchErrors} style={btnBase} title="Обновить">↻</button>
|
||||
<button onClick={handleCopyAll} disabled={errors.length === 0} style={{ ...btnAccent, opacity: errors.length === 0 ? 0.4 : 1 }}>
|
||||
📋 Копировать всё
|
||||
</Button>
|
||||
</button>
|
||||
{selected.size > 0 && (
|
||||
<Button onClick={handleCopySelected} style={btnAccent}>
|
||||
<button onClick={handleCopySelected} style={btnAccent}>
|
||||
📋 Копировать выбр.
|
||||
</Button>
|
||||
</button>
|
||||
)}
|
||||
<Button onClick={downloadJSON} disabled={errors.length === 0} style={{ ...btnBase, opacity: errors.length === 0 ? 0.4 : 1 }}>
|
||||
<button onClick={downloadJSON} disabled={errors.length === 0} style={{ ...btnBase, opacity: errors.length === 0 ? 0.4 : 1 }}>
|
||||
⬇ JSON
|
||||
</Button>
|
||||
<Button onClick={downloadCSV} disabled={errors.length === 0} style={{ ...btnBase, opacity: errors.length === 0 ? 0.4 : 1 }}>
|
||||
</button>
|
||||
<button onClick={downloadCSV} disabled={errors.length === 0} style={{ ...btnBase, opacity: errors.length === 0 ? 0.4 : 1 }}>
|
||||
⬇ CSV
|
||||
</Button>
|
||||
</button>
|
||||
{selected.size > 0 && (
|
||||
<Button onClick={handleDeleteSelected} disabled={deleting} style={{ ...btnDanger, opacity: deleting ? 0.5 : 1 }}>
|
||||
<button onClick={handleDeleteSelected} disabled={deleting} style={{ ...btnDanger, opacity: deleting ? 0.5 : 1 }}>
|
||||
🗑 Удалить выбр.
|
||||
</Button>
|
||||
</button>
|
||||
)}
|
||||
<Button onClick={handleDeleteAll} disabled={deleting || errors.length === 0} style={{ ...btnDanger, opacity: deleting || errors.length === 0 ? 0.4 : 1 }}>
|
||||
<button onClick={handleDeleteAll} disabled={deleting || errors.length === 0} style={{ ...btnDanger, opacity: deleting || errors.length === 0 ? 0.4 : 1 }}>
|
||||
🗑 Удалить все
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -383,9 +382,9 @@ export default function ErrorLogPanel() {
|
|||
{err.user_id && <div style={{ gridColumn: '1 / -1' }}><strong>User ID:</strong> {err.user_id}</div>}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button onClick={() => handleDeleteOne(err.id)} disabled={deleting} style={{ ...btnDanger, fontSize: '0.8rem' }}>
|
||||
<button onClick={() => handleDeleteOne(err.id)} disabled={deleting} style={{ ...btnDanger, fontSize: '0.8rem' }}>
|
||||
Удалить запись
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ export const StopWordsPanel = () => {
|
|||
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4 space-y-3">
|
||||
<p className="text-sm font-medium text-[var(--color-text)]">Где применять стоп-слова:</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSavingScope}
|
||||
onClick={() => handleScopeChange("everywhere")}
|
||||
|
|
@ -120,8 +120,8 @@ export const StopWordsPanel = () => {
|
|||
].join(" ")}
|
||||
>
|
||||
Везде
|
||||
</Button>
|
||||
<Button
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSavingScope}
|
||||
onClick={() => handleScopeChange("client_only")}
|
||||
|
|
@ -134,7 +134,7 @@ export const StopWordsPanel = () => {
|
|||
].join(" ")}
|
||||
>
|
||||
Только карточка клиента
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--color-text-muted)]">
|
||||
{scope === "everywhere"
|
||||
|
|
@ -178,7 +178,7 @@ export const StopWordsPanel = () => {
|
|||
className="inline-flex items-center gap-1.5 rounded-full border border-[var(--color-border)] bg-[var(--color-surface-strong)] px-3 py-1.5 text-sm !text-[var(--color-text)]"
|
||||
>
|
||||
{w.word}
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
disabled={savingId === w.id}
|
||||
onClick={() => handleDelete(w.id)}
|
||||
|
|
@ -186,7 +186,7 @@ export const StopWordsPanel = () => {
|
|||
aria-label={`Удалить ${w.word}`}
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React from "react";
|
||||
import { Panel } from "../UI/Panel";
|
||||
import { Button } from "../UI/Button";
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
|
||||
const CATEGORY_OPTIONS = [
|
||||
|
|
@ -104,7 +103,7 @@ export const SuggestionsPanel = () => {
|
|||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{CATEGORY_OPTIONS.map((cat) => (
|
||||
<Button
|
||||
<button
|
||||
key={cat.value}
|
||||
type="button"
|
||||
onClick={() => setNewCategory(cat.value)}
|
||||
|
|
@ -116,7 +115,7 @@ export const SuggestionsPanel = () => {
|
|||
].join(" ")}
|
||||
>
|
||||
{cat.icon} {cat.label}
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<textarea
|
||||
|
|
@ -127,14 +126,14 @@ export const SuggestionsPanel = () => {
|
|||
className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] px-4 py-3 text-sm text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)] focus:outline-none resize-none"
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
disabled={!newText.trim() || submitting}
|
||||
onClick={handleSubmit}
|
||||
className="rounded-xl bg-[var(--color-accent)] px-5 py-2.5 text-sm font-semibold text-white transition hover:opacity-90 disabled:opacity-40"
|
||||
>
|
||||
{submitting ? "Отправка..." : "Отправить"}
|
||||
</Button>
|
||||
</button>
|
||||
{message && <span className="text-sm text-[var(--color-text-muted)]">{message}</span>}
|
||||
</div>
|
||||
</Panel>
|
||||
|
|
@ -210,7 +209,7 @@ const SuggestionCard = ({ suggestion: s, category, status, isAdmin, onStatusChan
|
|||
<div className="border-t border-[var(--color-border)] pt-2 mt-2 space-y-2">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{Object.entries(STATUS_MAP).map(([key, val]) => (
|
||||
<Button
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
onClick={() => onStatusChange(s.id, key)}
|
||||
|
|
@ -224,7 +223,7 @@ const SuggestionCard = ({ suggestion: s, category, status, isAdmin, onStatusChan
|
|||
}}
|
||||
>
|
||||
{val.label}
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{editing ? (
|
||||
|
|
@ -236,22 +235,22 @@ const SuggestionCard = ({ suggestion: s, category, status, isAdmin, onStatusChan
|
|||
placeholder="Комментарий админа..."
|
||||
className="flex-1 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] px-3 py-1.5 text-xs text-[var(--color-text)]"
|
||||
/>
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { onStatusChange(s.id, s.status, comment); setEditing(false); }}
|
||||
className="rounded-lg bg-[var(--color-accent)] px-3 py-1.5 text-xs font-semibold text-white"
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditing(true)}
|
||||
className="text-xs text-[var(--color-accent)] hover:underline"
|
||||
>
|
||||
✏️ Комментарий
|
||||
</Button>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Panel } from '../UI/Panel';
|
||||
import { Badge } from '../UI/Badge';
|
||||
import { Button } from '../UI/Button';
|
||||
import { Input } from '../UI/Input';
|
||||
import { Skeleton } from '../UI/Loading';
|
||||
|
||||
|
|
@ -69,7 +68,7 @@ function RoleDropdown({ value, onChange }) {
|
|||
|
||||
return (
|
||||
<div ref={ref} className="relative w-full">
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className={[
|
||||
|
|
@ -83,13 +82,13 @@ function RoleDropdown({ value, onChange }) {
|
|||
<span className="text-[var(--color-text-muted)]">Выберите роль</span>
|
||||
)}
|
||||
<span className="text-[var(--color-text-muted)] text-xs">▾</span>
|
||||
</Button>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="absolute left-0 right-0 top-full z-50 mt-1.5 overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-dropdown-surface)] shadow-soft">
|
||||
{ROLES.map((r) => {
|
||||
const sel = r === value;
|
||||
return (
|
||||
<Button
|
||||
<button
|
||||
key={r}
|
||||
type="button"
|
||||
onClick={() => pick(r)}
|
||||
|
|
@ -100,7 +99,7 @@ function RoleDropdown({ value, onChange }) {
|
|||
>
|
||||
<span>{ROLE_LABELS[r]}</span>
|
||||
{sel && <span className="text-[var(--color-accent)]">✓</span>}
|
||||
</Button>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -162,20 +161,20 @@ function AddUserModal({ onSubmit, onClose, submitting, error }) {
|
|||
<div className="rounded-xl bg-[rgba(201,61,61,0.12)] px-4 py-2.5 text-sm text-[var(--color-danger)]">{error}</div>
|
||||
)}
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 rounded-full border border-[var(--color-border)] px-4 py-2.5 text-sm font-semibold text-[var(--color-text-muted)] hover:bg-[var(--color-surface)] transition"
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="flex-1 rounded-full bg-[var(--color-accent)] px-4 py-2.5 text-sm font-semibold text-white hover:opacity-90 transition disabled:opacity-50"
|
||||
>
|
||||
{submitting ? 'Добавление…' : 'Добавить'}
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -295,12 +294,12 @@ export default function UserManagementPanel() {
|
|||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between gap-2 flex-wrap mb-4">
|
||||
<span className="text-sm text-[var(--color-text-muted)]">{users.length} пользователей</span>
|
||||
<Button
|
||||
<button
|
||||
onClick={() => { setShowAddForm(true); setAddError(null); }}
|
||||
className="rounded-full bg-[var(--color-accent)] px-4 py-2 text-sm font-semibold text-white hover:opacity-90 transition"
|
||||
>
|
||||
+ Добавить
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Add user modal */}
|
||||
|
|
@ -355,15 +354,15 @@ export default function UserManagementPanel() {
|
|||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
<button
|
||||
onClick={cancelEdit}
|
||||
className="rounded-lg border border-[var(--color-border)] px-3 py-1.5 text-sm text-[var(--color-text-muted)] hover:bg-[var(--color-surface)] transition"
|
||||
>Отмена</Button>
|
||||
<Button
|
||||
>Отмена</button>
|
||||
<button
|
||||
onClick={saveEdit}
|
||||
disabled={saving}
|
||||
className="rounded-full bg-[var(--color-accent)] px-4 py-1.5 text-sm font-semibold text-white hover:opacity-90 transition disabled:opacity-50"
|
||||
>{saving ? 'Сохранение…' : 'Сохранить'}</Button>
|
||||
>{saving ? 'Сохранение…' : 'Сохранить'}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -380,20 +379,20 @@ export default function UserManagementPanel() {
|
|||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge tone={ROLE_TONES[rn] || 'neutral'}>{ROLE_LABELS[rn] || rn}</Badge>
|
||||
<Button
|
||||
<button
|
||||
onClick={() => startEdit(user)}
|
||||
className="rounded-lg border border-[var(--color-accent)] px-2.5 py-1 text-xs font-semibold text-[var(--color-accent)] hover:bg-[var(--color-accent-soft)] transition"
|
||||
>Изменить</Button>
|
||||
>Изменить</button>
|
||||
{deleteConfirmId === user.id ? (
|
||||
<div className="flex gap-1">
|
||||
<Button onClick={() => handleDeleteUser(user.id)} className="rounded-lg bg-[var(--color-danger)] px-2.5 py-1 text-xs font-semibold text-white transition">Да</Button>
|
||||
<Button onClick={() => setDeleteConfirmId(null)} className="rounded-lg border border-[var(--color-border)] px-2.5 py-1 text-xs text-[var(--color-text-muted)] transition">Нет</Button>
|
||||
<button onClick={() => handleDeleteUser(user.id)} className="rounded-lg bg-[var(--color-danger)] px-2.5 py-1 text-xs font-semibold text-white transition">Да</button>
|
||||
<button onClick={() => setDeleteConfirmId(null)} className="rounded-lg border border-[var(--color-border)] px-2.5 py-1 text-xs text-[var(--color-text-muted)] transition">Нет</button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
<button
|
||||
onClick={() => setDeleteConfirmId(user.id)}
|
||||
className="rounded-lg border border-[var(--color-danger)] px-2.5 py-1 text-xs font-semibold text-[var(--color-danger)] hover:bg-[rgba(201,61,61,0.08)] transition"
|
||||
>Удалить</Button>
|
||||
>Удалить</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React from "react";
|
||||
import { Badge } from "../UI/Badge";
|
||||
import { Button } from "../UI/Button";
|
||||
import { Panel } from "../UI/Panel";
|
||||
import { getInvitationReferenceLabel } from "./invitationReference";
|
||||
import { supabase } from "../../supabaseClient";
|
||||
|
|
@ -101,8 +100,8 @@ export const OrderCompositionPanel = ({ invitation = {} }) => {
|
|||
|
||||
return (
|
||||
<Panel className="space-y-3 border shadow-soft p-5 sm:p-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between text-left"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
|
|
@ -122,7 +121,7 @@ export const OrderCompositionPanel = ({ invitation = {} }) => {
|
|||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</span>
|
||||
</Button>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="space-y-2">
|
||||
{products.map((product, idx) => (
|
||||
|
|
|
|||
|
|
@ -19,15 +19,15 @@ const ProblemReasonModal = ({ onSelect, onCancel }) => (
|
|||
<p className="text-sm text-[var(--color-text-muted)]">Укажите причину возникшей проблемы с доставкой.</p>
|
||||
<div className="space-y-2">
|
||||
{PROBLEM_REASONS.map((reason) => (
|
||||
<Button
|
||||
<button
|
||||
key={reason.value}
|
||||
variant="secondary"
|
||||
className="w-full rounded-[16px] p-3 text-left"
|
||||
type="button"
|
||||
className="w-full rounded-[16px] border border-[var(--color-border)] bg-[var(--color-surface)] p-3 text-left transition hover:border-[var(--color-accent)] hover:bg-[var(--color-accent-soft)]"
|
||||
onClick={() => onSelect(reason.value, reason.label)}
|
||||
>
|
||||
<span className="font-medium">{reason.label}</span>
|
||||
<p className="mt-0.5 text-xs text-[var(--color-text-muted)]">{reason.description}</p>
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {
|
|||
parseGroupDate,
|
||||
} from "../../services/orderGroupViews";
|
||||
import { Badge } from "../UI/Badge";
|
||||
import { Button } from "../UI/Button";
|
||||
import { Input } from "../UI/Input";
|
||||
import { Select } from "../UI/Select";
|
||||
import { Panel } from "../UI/Panel";
|
||||
|
|
@ -248,35 +247,31 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
|||
{/* Date pills */}
|
||||
{sortedDeliveryDates.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 pt-2">
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setFilters((current) => ({ ...current, selectedDate: "" }))}
|
||||
className={[
|
||||
"rounded-full px-3 py-1.5 text-xs font-medium",
|
||||
"rounded-full border px-3 py-1.5 text-xs font-medium transition",
|
||||
!filters.selectedDate
|
||||
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-text)]"
|
||||
: "text-[var(--color-text-muted)]",
|
||||
: "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)]",
|
||||
].join(" ")}
|
||||
>
|
||||
Все даты
|
||||
</Button>
|
||||
</button>
|
||||
{sortedDeliveryDates.map((date) => {
|
||||
const count = dateDeliveryMap.get(date) || 0;
|
||||
const selected = isDateSelected(date);
|
||||
return (
|
||||
<Button
|
||||
<button
|
||||
key={date}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setFilters((current) => ({ ...current, selectedDate: date }))}
|
||||
className={[
|
||||
"flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium",
|
||||
"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)]"
|
||||
: "text-[var(--color-text-muted)]",
|
||||
: "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)]",
|
||||
].join(" ")}
|
||||
>
|
||||
<span>{parseGroupDate(date)?.toLocaleDateString("ru-RU", { day: "numeric", month: "short" }) || "—"}</span>
|
||||
|
|
@ -285,7 +280,7 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
|||
{count}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -294,35 +289,31 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
|||
{/* City pills */}
|
||||
{sortedCities.length > 1 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setFilters((current) => ({ ...current, selectedCity: "" }))}
|
||||
className={[
|
||||
"rounded-full px-3 py-1.5 text-xs font-medium",
|
||||
"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)]"
|
||||
: "text-[var(--color-text-muted)]",
|
||||
: "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)]",
|
||||
].join(" ")}
|
||||
>
|
||||
Все города
|
||||
</Button>
|
||||
</button>
|
||||
{sortedCities.map((city) => {
|
||||
const count = cityDeliveryMap.get(city) || 0;
|
||||
const selected = filters.selectedCity === city;
|
||||
return (
|
||||
<Button
|
||||
<button
|
||||
key={city}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setFilters((current) => ({ ...current, selectedCity: city }))}
|
||||
className={[
|
||||
"flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium",
|
||||
"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)]"
|
||||
: "text-[var(--color-text-muted)]",
|
||||
: "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)]",
|
||||
].join(" ")}
|
||||
>
|
||||
<span>{city}</span>
|
||||
|
|
@ -331,7 +322,7 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
|||
{count}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -368,9 +359,8 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
|||
|
||||
return (
|
||||
<Panel key={group.date} className="space-y-4 p-5">
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="flex w-full items-center gap-3 text-left"
|
||||
onClick={() => toggleDate(group.date)}
|
||||
>
|
||||
|
|
@ -391,7 +381,7 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
|||
<Badge key={status} tone={tone}>{count} {label}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</Button>
|
||||
</button>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className="space-y-4 pt-1">
|
||||
|
|
@ -403,10 +393,10 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
|||
</div>
|
||||
<div className="grid gap-3">
|
||||
{items.map((item) => (
|
||||
<Button
|
||||
<button
|
||||
key={item.id}
|
||||
variant="secondary"
|
||||
className="rounded-[24px] p-4 text-left"
|
||||
type="button"
|
||||
className="rounded-[24px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4 text-left transition hover:bg-[var(--color-accent-soft)]"
|
||||
onClick={() => onOpenOrder?.(item.id)}
|
||||
>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
|
|
@ -424,7 +414,7 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
|
|||
<div className="mt-3 text-sm text-[var(--color-text-muted)]">
|
||||
{item.deliveryAddress || item.delivery_address || "Адрес не указан"}
|
||||
</div>
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -92,8 +92,6 @@ const parseOrderItems = (order) => {
|
|||
export const DriverShipmentPanel = ({ order, onShipmentChange, onSaveShipment, isSavingShipment }) => {
|
||||
const [stopWords, setStopWords] = React.useState([]);
|
||||
const [scopeActive, setScopeActive] = React.useState(true);
|
||||
const [savedShipment, setSavedShipment] = React.useState([]);
|
||||
const [justSaved, setJustSaved] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!supabase) return;
|
||||
|
|
@ -111,46 +109,10 @@ export const DriverShipmentPanel = ({ order, onShipmentChange, onSaveShipment, i
|
|||
if (!stopWords.length || !scopeActive) return allItems;
|
||||
return allItems.filter((item) => !matchesStopWord(item.name, stopWords));
|
||||
}, [allItems, stopWords, scopeActive]);
|
||||
|
||||
// Restore previously saved shipment data from order
|
||||
const initialShippedIds = React.useMemo(() => {
|
||||
const saved = order?.driverShipmentData;
|
||||
if (!Array.isArray(saved) || saved.length === 0) return new Set();
|
||||
const ids = new Set();
|
||||
for (const item of saved) {
|
||||
if (item.shipped && item.id) ids.add(item.id);
|
||||
}
|
||||
return ids;
|
||||
}, [order?.driverShipmentData]);
|
||||
|
||||
const initialComments = React.useMemo(() => {
|
||||
const saved = order?.driverShipmentData;
|
||||
if (!Array.isArray(saved) || saved.length === 0) return {};
|
||||
const map = {};
|
||||
for (const item of saved) {
|
||||
if (!item.shipped && item.id && item.comment) map[item.id] = item.comment;
|
||||
}
|
||||
return map;
|
||||
}, [order?.driverShipmentData]);
|
||||
|
||||
const [shippedItems, setShippedItems] = React.useState(initialShippedIds);
|
||||
const [comments, setComments] = React.useState(initialComments);
|
||||
const [shippedItems, setShippedItems] = React.useState(new Set());
|
||||
const [comments, setComments] = React.useState({});
|
||||
const [commentInput, setCommentInput] = React.useState("");
|
||||
|
||||
// Sync state when order data changes (e.g. after save)
|
||||
React.useEffect(() => {
|
||||
setShippedItems(initialShippedIds);
|
||||
setComments(initialComments);
|
||||
}, [initialShippedIds, initialComments]);
|
||||
|
||||
// Track last saved shipment data for visual display
|
||||
React.useEffect(() => {
|
||||
const saved = order?.driverShipmentData;
|
||||
if (Array.isArray(saved) && saved.length > 0) {
|
||||
setSavedShipment(saved);
|
||||
}
|
||||
}, [order?.driverShipmentData]);
|
||||
|
||||
const toggleItem = (itemId) => {
|
||||
setShippedItems((prev) => {
|
||||
const next = new Set(prev);
|
||||
|
|
@ -303,22 +265,17 @@ export const DriverShipmentPanel = ({ order, onShipmentChange, onSaveShipment, i
|
|||
{onSaveShipment && items.length > 0 && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
onClick={() => {
|
||||
const data = items.map((item) => ({
|
||||
onClick={() => onSaveShipment(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] || ""),
|
||||
}));
|
||||
setSavedShipment(data);
|
||||
setJustSaved(true);
|
||||
onSaveShipment(data);
|
||||
}}
|
||||
})))}
|
||||
disabled={isSavingShipment || shippedCount === 0}
|
||||
>
|
||||
{isSavingShipment ? "Сохраняем..." : justSaved ? "Сохранить изменения" : "Сохранить"}
|
||||
{isSavingShipment ? "Сохраняем..." : "Сохранить отгрузку"}
|
||||
</Button>
|
||||
{shippedCount > 0 && shippedCount < items.length && (
|
||||
<span className="text-xs text-[var(--color-text-muted)]">
|
||||
|
|
@ -327,66 +284,6 @@ export const DriverShipmentPanel = ({ order, onShipmentChange, onSaveShipment, i
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Saved shipment summary — show prominently after save */}
|
||||
{justSaved && savedShipment.length > 0 && (
|
||||
<div className="rounded-xl border-2 border-[var(--color-accent)] bg-[var(--color-accent-soft)] p-4 text-sm">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">✅</span>
|
||||
<strong className="text-[var(--color-accent)]">
|
||||
Отгрузка сохранена: {savedShipment.filter((i) => i.shipped).length} из {savedShipment.length}
|
||||
</strong>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => setJustSaved(false)}>
|
||||
Изменить
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{savedShipment.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={[
|
||||
"rounded-lg px-3 py-2",
|
||||
item.shipped
|
||||
? "bg-[var(--color-surface-strong)] border border-[var(--color-accent)]"
|
||||
: "bg-[var(--color-warning-soft)] border border-[var(--color-warning)]",
|
||||
].join(" ")}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={item.shipped ? "text-[var(--color-accent)]" : "text-[var(--color-warning)]"}>
|
||||
{item.shipped ? "✓" : "✗"}
|
||||
</span>
|
||||
<span className={item.shipped ? "text-[var(--color-text-muted)] line-through" : "text-[var(--color-text)] font-medium"}>
|
||||
{item.name}{item.quantity ? ` — ${item.quantity}${item.unit ? ` ${item.unit}` : ""}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
{!item.shipped && item.comment && (
|
||||
<div className="mt-1 ml-6 text-xs text-[var(--color-warning)]">
|
||||
Причина: {item.comment}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Restore saved state on load (when not justSaved) */}
|
||||
{!justSaved && savedShipment.length > 0 && (
|
||||
<div className="rounded-xl border border-[var(--color-accent)] bg-[var(--color-accent-soft)] px-3 py-2 text-xs text-[var(--color-accent)]">
|
||||
✅ Отгрузка ранее сохранена: {savedShipment.filter((i) => i.shipped).length}/{savedShipment.length} позиций
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="underline text-[var(--color-text-muted)]"
|
||||
onClick={() => setJustSaved(true)}
|
||||
>
|
||||
Показать
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
ORDER_GROUP_DISPLAY_STATUS_OPTIONS,
|
||||
} from "../../services/orderGroupViews";
|
||||
import { Badge } from "../UI/Badge";
|
||||
import { Button } from "../UI/Button";
|
||||
import { Panel } from "../UI/Panel";
|
||||
import { SkeletonPage } from "../UI/Loading";
|
||||
import { OrderFilters } from "../orders/OrderFilters";
|
||||
|
|
@ -114,9 +113,9 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
|||
|
||||
return (
|
||||
<Panel key={statusValue} className="overflow-hidden p-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex w-full items-center justify-between px-5 py-3 text-left"
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between px-5 py-3 text-left transition hover:bg-[var(--color-accent-soft)]"
|
||||
onClick={() => {
|
||||
setCollapsedSections((prev) => {
|
||||
const next = new Set(prev);
|
||||
|
|
@ -143,16 +142,16 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
|||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</Button>
|
||||
</button>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className="overflow-x-auto">
|
||||
<TableHeader />
|
||||
{groups.map((group) => (
|
||||
<Button
|
||||
<button
|
||||
key={group.id}
|
||||
variant="ghost"
|
||||
className={`grid ${COLS} gap-0 w-full border-t border-[var(--color-border)] text-left`}
|
||||
type="button"
|
||||
className={`grid ${COLS} gap-0 w-full border-t border-[var(--color-border)] text-left transition hover:bg-[var(--color-accent-soft)]`}
|
||||
onClick={() => { if (onSelectSet) onSelectSet(group.id); }}
|
||||
>
|
||||
<div className="px-4 py-2.5">
|
||||
|
|
@ -177,7 +176,7 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
|
|||
<div className="px-4 py-2.5 text-sm text-[var(--color-text-muted)]">
|
||||
{formatDateTime(group.updatedAt)}
|
||||
</div>
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export function NotificationBell({ notifications, unreadCount, onMarkAsRead, onM
|
|||
|
||||
return (
|
||||
<div className="relative" ref={bellRef}>
|
||||
<Button
|
||||
<button
|
||||
className="relative flex h-9 w-9 items-center justify-center rounded-full transition hover:bg-[var(--color-surface-strong)]"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
aria-label={`Уведомления${unreadCount > 0 ? ` (${unreadCount})` : ""}`}
|
||||
|
|
@ -58,7 +58,7 @@ export function NotificationBell({ notifications, unreadCount, onMarkAsRead, onM
|
|||
{unreadCount > 99 ? "99+" : unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 top-full z-50 mt-2 w-80 sm:w-96">
|
||||
|
|
@ -67,29 +67,29 @@ export function NotificationBell({ notifications, unreadCount, onMarkAsRead, onM
|
|||
<h3 className="text-sm font-semibold">Уведомления</h3>
|
||||
<div className="flex items-center gap-1">
|
||||
{unreadCount > 0 && (
|
||||
<Button
|
||||
<button
|
||||
className="rounded p-1 text-xs text-[var(--color-accent)] hover:bg-[var(--color-surface-strong)]"
|
||||
onClick={onMarkAllAsRead}
|
||||
title="Прочитать все"
|
||||
>
|
||||
<CheckCheck className="h-4 w-4" />
|
||||
</Button>
|
||||
</button>
|
||||
)}
|
||||
{onOpenSettings && (
|
||||
<Button
|
||||
<button
|
||||
className="rounded p-1 text-xs text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)]"
|
||||
onClick={() => { onOpenSettings(); setIsOpen(false); }}
|
||||
title="Настройки"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</button>
|
||||
)}
|
||||
<Button
|
||||
<button
|
||||
className="rounded p-1 text-xs text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)]"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React from "react";
|
|||
import { useNotificationPreferences } from "../../hooks/useNotifications";
|
||||
import { usePushNotifications } from "../../hooks/usePushNotifications";
|
||||
import { Panel } from "../UI/Panel";
|
||||
import { Button } from "../UI/Button";
|
||||
import { Bell, Settings } from "../UI/Icons";
|
||||
import { Skeleton } from "../UI/Loading";
|
||||
|
||||
|
|
@ -28,12 +27,12 @@ export function NotificationSettings({ userId, userRole, onBack }) {
|
|||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{onBack && (
|
||||
<Button
|
||||
<button
|
||||
className="rounded p-1 text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)]"
|
||||
onClick={onBack}
|
||||
>
|
||||
← Назад
|
||||
</Button>
|
||||
</button>
|
||||
)}
|
||||
<h2 className="text-lg font-semibold">Настройки уведомлений</h2>
|
||||
</div>
|
||||
|
|
@ -68,12 +67,12 @@ export function NotificationSettings({ userId, userRole, onBack }) {
|
|||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{onBack && (
|
||||
<Button
|
||||
<button
|
||||
className="rounded p-1 text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)]"
|
||||
onClick={onBack}
|
||||
>
|
||||
← Назад
|
||||
</Button>
|
||||
</button>
|
||||
)}
|
||||
<h2 className="text-lg font-semibold">Настройки уведомлений</h2>
|
||||
</div>
|
||||
|
|
@ -95,7 +94,7 @@ export function NotificationSettings({ userId, userRole, onBack }) {
|
|||
</div>
|
||||
</div>
|
||||
{isSupported && (
|
||||
<Button
|
||||
<button
|
||||
className={`relative h-6 w-11 rounded-full transition-colors ${
|
||||
isSubscribed ? "bg-[var(--color-accent)]" : "bg-[var(--color-border)]"
|
||||
}`}
|
||||
|
|
@ -107,7 +106,7 @@ export function NotificationSettings({ userId, userRole, onBack }) {
|
|||
isSubscribed ? "translate-x-5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</Button>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
|
|
@ -125,7 +124,7 @@ export function NotificationSettings({ userId, userRole, onBack }) {
|
|||
<p className="text-sm font-medium">{label}</p>
|
||||
<p className="text-xs text-[var(--color-text-muted)]">{description}</p>
|
||||
</div>
|
||||
<Button
|
||||
<button
|
||||
className={`relative mt-0.5 h-6 w-11 shrink-0 rounded-full transition-colors ${
|
||||
prefs[key] ? "bg-[var(--color-accent)]" : "bg-[var(--color-border)]"
|
||||
}`}
|
||||
|
|
@ -137,7 +136,7 @@ export function NotificationSettings({ userId, userRole, onBack }) {
|
|||
prefs[key] ? "translate-x-5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</Button>
|
||||
</button>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React from "react";
|
||||
import { Button } from "../UI/Button";
|
||||
|
||||
const WEEK_DAY_LABELS = ["ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ", "ВС"];
|
||||
|
||||
|
|
@ -59,8 +58,8 @@ const CalendarWidget = ({
|
|||
return (
|
||||
<div className={layoutClassName}>
|
||||
<div className={calendarClassName}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
<button
|
||||
type="button"
|
||||
aria-label={label}
|
||||
aria-expanded={isCalendarOpen}
|
||||
className="flex min-h-[54px] w-full items-center justify-between rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 text-left text-sm font-medium !text-[var(--color-text)] transition hover:border-[var(--color-accent)] focus:border-[var(--color-accent)] focus:outline-none"
|
||||
|
|
@ -68,7 +67,7 @@ const CalendarWidget = ({
|
|||
>
|
||||
<span>{selectedDate ? formatDateForDisplay(selectedDate) : "Выберите дату"}</span>
|
||||
<span aria-hidden="true" className="text-[var(--color-text-muted)]">▾</span>
|
||||
</Button>
|
||||
</button>
|
||||
{isCalendarOpen ? (
|
||||
<div className="rounded-[20px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-3 shadow-lg absolute left-0 top-full z-50 mt-2 w-[300px]">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
|
|
@ -84,23 +83,23 @@ const CalendarWidget = ({
|
|||
</h4>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canGoBack}
|
||||
aria-label="Предыдущий месяц"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full border border-[var(--color-border)] text-sm text-[var(--color-text-muted)] transition hover:border-[var(--color-accent)] hover:!text-[var(--color-text)] disabled:cursor-not-allowed disabled:opacity-40"
|
||||
onClick={() => setCurrentMonth((month) => new Date(month.getFullYear(), month.getMonth() - 1, 1))}
|
||||
>
|
||||
‹
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Следующий месяц"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full border border-[var(--color-border)] text-sm text-[var(--color-text-muted)] transition hover:border-[var(--color-accent)] hover:!text-[var(--color-text)]"
|
||||
onClick={() => setCurrentMonth((month) => new Date(month.getFullYear(), month.getMonth() + 1, 1))}
|
||||
>
|
||||
›
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-7 gap-1 text-center text-[10px] font-semibold uppercase text-[var(--color-text-muted)]">
|
||||
|
|
@ -124,9 +123,9 @@ const CalendarWidget = ({
|
|||
const dayNumber = String(day.getDate()).padStart(2, "0");
|
||||
|
||||
return (
|
||||
<Button
|
||||
<button
|
||||
key={dateKey}
|
||||
variant="ghost"
|
||||
type="button"
|
||||
disabled={isDisabled}
|
||||
title={isWeekend ? "Выходной, доставки нет" : isSelectable ? "Можно выбрать" : "Недоступно"}
|
||||
className={[
|
||||
|
|
@ -151,7 +150,7 @@ const CalendarWidget = ({
|
|||
className="absolute inset-x-2 top-1/2 h-px -rotate-12 bg-[var(--color-text-muted)] opacity-70"
|
||||
/>
|
||||
) : null}
|
||||
</Button>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -163,9 +162,9 @@ const CalendarWidget = ({
|
|||
</div>
|
||||
<div className={timeClassName}>
|
||||
{timeOptions.map((option) => (
|
||||
<Button
|
||||
<button
|
||||
key={option}
|
||||
variant="ghost"
|
||||
type="button"
|
||||
aria-pressed={selectedTime === option}
|
||||
className={[
|
||||
"min-h-[54px] rounded-2xl border px-4 text-left text-sm font-medium transition",
|
||||
|
|
@ -176,7 +175,7 @@ const CalendarWidget = ({
|
|||
onClick={() => onTimeChange(option)}
|
||||
>
|
||||
{option}
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@ const ConfirmModal = ({ open, title, message, onConfirm, onCancel }) => {
|
|||
{title && <h3 className="text-lg font-semibold">{title}</h3>}
|
||||
{message && <p className="mt-2 text-sm text-[var(--color-text-muted)]">{message}</p>}
|
||||
<div className="mt-5 flex justify-end gap-3">
|
||||
<Button variant="secondary" onClick={onCancel}>Отмена</Button>
|
||||
<Button variant="primary" onClick={onConfirm}>Подтвердить</Button>
|
||||
<button type="button" className="rounded-xl border border-[var(--color-border)] px-4 py-2 text-sm font-medium text-[var(--color-text-muted)] hover:bg-[var(--color-surface)]" onClick={onCancel}>Отмена</button>
|
||||
<button type="button" className="rounded-xl bg-[var(--color-accent)] px-4 py-2 text-sm font-medium text-white hover:opacity-90" onClick={onConfirm}>Подтвердить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -84,14 +84,13 @@ const CollapsibleChips = ({ label, items }) => {
|
|||
if (!Array.isArray(items) || items.length === 0) return null;
|
||||
return (
|
||||
<span className="inline">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer rounded-full bg-[var(--color-surface)] px-2 py-0.5 text-xs font-medium text-[var(--color-text-muted)] transition hover:bg-[var(--color-accent-soft)] hover:text-[var(--color-accent)]"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{label} {open ? "▲" : "▼"}
|
||||
</Button>
|
||||
</button>
|
||||
{open && (
|
||||
<span className="ml-1 inline-flex flex-wrap gap-1">
|
||||
{items.map((item, idx) => (
|
||||
|
|
@ -343,8 +342,8 @@ const CollapsibleOrderComposition = ({ order }) => {
|
|||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between text-left"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
|
|
@ -366,7 +365,7 @@ const CollapsibleOrderComposition = ({ order }) => {
|
|||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</span>
|
||||
</Button>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="space-y-3">
|
||||
{!orders.length ? (
|
||||
|
|
@ -517,15 +516,15 @@ const ProblemReasonModal = ({ onSelect, onCancel }) => (
|
|||
<p className="text-sm text-[var(--color-text-muted)]">Укажите причину возникшей проблемы с доставкой.</p>
|
||||
<div className="space-y-2">
|
||||
{PROBLEM_REASONS.map((reason) => (
|
||||
<Button
|
||||
<button
|
||||
key={reason.value}
|
||||
variant="secondary"
|
||||
className="w-full rounded-[16px] p-3 text-left"
|
||||
type="button"
|
||||
className="w-full rounded-[16px] border border-[var(--color-border)] bg-[var(--color-surface)] p-3 text-left transition hover:border-[var(--color-accent)] hover:bg-[var(--color-accent-soft)]"
|
||||
onClick={() => onSelect(reason.value, reason.label)}
|
||||
>
|
||||
<span className="font-medium">{reason.label}</span>
|
||||
<p className="mt-0.5 text-xs text-[var(--color-text-muted)]">{reason.description}</p>
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
|
|
@ -636,7 +635,7 @@ export const OrderDetailPanel = ({
|
|||
);
|
||||
}
|
||||
|
||||
const isDeliveryAgreed = ["agreed", "driver_assigned", "loaded", "on_route", "delivered", "picked_up", "pickup"].includes(order.deliveryStatus || order.delivery_status);
|
||||
const isDeliveryAgreed = ["agreed", "driver_assigned", "loaded", "on_route", "delivered", "picked_up"].includes(order.deliveryStatus || order.delivery_status);
|
||||
const isPickupOrder = order.deliveryType === "pickup" || order.deliveryStatus === "pickup" || order.delivery_status === "pickup";
|
||||
// Show "agreed" banner only when selected tab matches the already-agreed type
|
||||
const agreedTypeMatchesTab = isDeliveryAgreed && !isEditingDate && (
|
||||
|
|
@ -864,12 +863,6 @@ export const OrderDetailPanel = ({
|
|||
<p className="text-xs text-[var(--color-text-muted)]">{isPickupOrder ? "Статус самовывоза" : "Статус доставки"}</p>
|
||||
<p className="font-medium !text-[var(--color-text)]">{getOrderGroupDeliveryStatusLabel(order.deliveryStatus || order.delivery_status)}</p>
|
||||
</div>
|
||||
{(order.pickupCode || order.pickup_code) && (order.deliveryType === "pickup" || order.delivery_type === "pickup") ? (
|
||||
<div>
|
||||
<p className="text-xs text-[var(--color-text-muted)]">Код выдачи</p>
|
||||
<p className="text-2xl font-bold tracking-[0.3em] text-[var(--color-accent)]">{order.pickupCode || order.pickup_code}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
|
|
@ -885,20 +878,28 @@ export const OrderDetailPanel = ({
|
|||
</div>
|
||||
{/* Delivery type tabs */}
|
||||
<div className="flex gap-2 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-1">
|
||||
<Button
|
||||
variant={deliveryType === "delivery" ? "primary" : "ghost"}
|
||||
className="flex-1 rounded-xl px-3 py-2 text-sm font-semibold"
|
||||
<button
|
||||
type="button"
|
||||
className={`flex-1 rounded-xl px-3 py-2 text-sm font-semibold transition ${
|
||||
deliveryType === "delivery"
|
||||
? "bg-[var(--color-accent)] text-white"
|
||||
: "text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
|
||||
}`}
|
||||
onClick={() => { setDeliveryType("delivery"); setFormMessage(""); }}
|
||||
>
|
||||
🚚 Доставка
|
||||
</Button>
|
||||
<Button
|
||||
variant={deliveryType === "pickup" ? "primary" : "ghost"}
|
||||
className="flex-1 rounded-xl px-3 py-2 text-sm font-semibold"
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex-1 rounded-xl px-3 py-2 text-sm font-semibold transition ${
|
||||
deliveryType === "pickup"
|
||||
? "bg-[var(--color-accent)] text-white"
|
||||
: "text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
|
||||
}`}
|
||||
onClick={() => { setDeliveryType("pickup"); setFormMessage(""); }}
|
||||
>
|
||||
🏪 Самовывоз
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
{deliveryType === "pickup" && (
|
||||
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-3 text-sm text-[var(--color-text-muted)]">
|
||||
|
|
@ -1086,11 +1087,14 @@ export const OrderDetailPanel = ({
|
|||
|
||||
return statusOptions.map((statusOption) => {
|
||||
const isSelected = pendingStatus?.value === statusOption.value;
|
||||
const isDeliveredBtn = statusOption.value === "delivered";
|
||||
const deliveryBlocked = isDeliveredBtn && shipmentState && !shipmentState.canMarkDelivered;
|
||||
return (
|
||||
<Button
|
||||
key={statusOption.value}
|
||||
variant={isSelected ? "primary" : "secondary"}
|
||||
disabled={isSavingStatusChange}
|
||||
disabled={deliveryBlocked}
|
||||
title={deliveryBlocked ? "Сначала отметьте все позиции как отгруженные" : undefined}
|
||||
onClick={() => {
|
||||
if (statusOption.value === "problem") {
|
||||
setProblemReason("selecting");
|
||||
|
|
@ -1100,6 +1104,11 @@ export const OrderDetailPanel = ({
|
|||
}}
|
||||
>
|
||||
{statusOption.label}
|
||||
{isDeliveredBtn && shipmentState && !shipmentState.canMarkDelivered ? (
|
||||
<span className="ml-1.5 text-xs text-[var(--color-text-muted)]">
|
||||
({shipmentState.shipped}/{shipmentState.total})
|
||||
</span>
|
||||
) : null}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
|
@ -1111,6 +1120,7 @@ export const OrderDetailPanel = ({
|
|||
variant="primary"
|
||||
disabled={isSavingStatusChange}
|
||||
onClick={() => {
|
||||
if (pendingStatus.value === "delivered" && shipmentState && !shipmentState.canMarkDelivered) return;
|
||||
onChangeDeliveryStatus({
|
||||
orderGroupId: order.id,
|
||||
status: pendingStatus.value,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React from "react";
|
||||
import { Button } from "../UI/Button";
|
||||
import { Input } from "../UI/Input";
|
||||
import { Panel } from "../UI/Panel";
|
||||
import { DatePicker } from "../UI/DatePicker";
|
||||
|
|
@ -42,11 +41,12 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
|
|||
{/* Row 1: Status + City + Search */}
|
||||
<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">
|
||||
<Button
|
||||
variant="ghost"
|
||||
<button
|
||||
type="button"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={isStatusOpen}
|
||||
className={["flex h-[46px] w-full items-center justify-between rounded-2xl border px-4 text-left text-sm transition",
|
||||
className={[
|
||||
"flex h-[46px] w-full items-center justify-between rounded-2xl border px-4 text-left text-sm transition",
|
||||
isStatusOpen
|
||||
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-text)]"
|
||||
: "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text)] hover:border-[var(--color-accent)]",
|
||||
|
|
@ -55,7 +55,7 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
|
|||
>
|
||||
<span className="min-w-0 flex-1 truncate">{selectedStatusLabel}</span>
|
||||
<span aria-hidden="true" className="ml-3 text-[var(--color-text-muted)]">▾</span>
|
||||
</Button>
|
||||
</button>
|
||||
|
||||
{isStatusOpen ? (
|
||||
<div
|
||||
|
|
@ -65,12 +65,13 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
|
|||
{statusOptions.map((option) => {
|
||||
const isSelected = option.value === statusValue;
|
||||
return (
|
||||
<Button
|
||||
<button
|
||||
key={option.value}
|
||||
variant="ghost"
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
className={["flex w-full items-center justify-between px-4 py-3 text-left text-sm transition",
|
||||
className={[
|
||||
"flex w-full items-center justify-between px-4 py-3 text-left text-sm transition",
|
||||
isSelected
|
||||
? "bg-[var(--color-accent-soft)] text-[var(--color-text)]"
|
||||
: "text-[var(--color-text)] hover:bg-[var(--color-surface-strong)]",
|
||||
|
|
@ -85,7 +86,7 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
|
|||
{option.count || 0}
|
||||
</span>
|
||||
{isSelected ? <span className="ml-3 text-[var(--color-accent)]">✓</span> : null}
|
||||
</Button>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -105,16 +106,6 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
|
|||
</select>
|
||||
)}
|
||||
|
||||
<select
|
||||
value={filters.deliveryType || ""}
|
||||
onChange={(e) => updateFilter("deliveryType", 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>
|
||||
<option value="delivery">Доставка</option>
|
||||
<option value="pickup">Самовывоз</option>
|
||||
</select>
|
||||
|
||||
<Input
|
||||
className="h-[46px] py-0"
|
||||
placeholder="Поиск по группе, клиенту или телефону"
|
||||
|
|
@ -140,13 +131,13 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
|
|||
className="min-w-[140px] flex-1 md:max-w-[200px]"
|
||||
/>
|
||||
{hasDateFilter && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearDateFilter}
|
||||
className="mb-0.5 h-[46px] gap-1.5 rounded-2xl border border-[var(--color-danger)] px-4 text-sm font-semibold text-[var(--color-danger)] hover:bg-[rgba(201,61,61,0.08)]"
|
||||
className="mb-0.5 flex h-[46px] items-center gap-1.5 rounded-2xl border border-[var(--color-danger)] px-4 text-sm font-semibold text-[var(--color-danger)] hover:bg-[rgba(201,61,61,0.08)] transition"
|
||||
>
|
||||
✕ Сбросить
|
||||
</Button>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -107,15 +107,15 @@ export const OrdersCalendarView = ({ orders, onOpenOrder }) => {
|
|||
</div>
|
||||
<div className="space-y-2">
|
||||
{dayOrders.map((order) => (
|
||||
<Button
|
||||
<button
|
||||
key={order.id}
|
||||
variant="ghost"
|
||||
className="w-full rounded-[16px] px-3 py-3 text-left text-sm"
|
||||
type="button"
|
||||
onClick={() => onOpenOrder(order.id)}
|
||||
className="w-full rounded-[16px] bg-[var(--color-surface)] px-3 py-3 text-left text-sm hover:bg-[var(--color-accent-soft)]"
|
||||
>
|
||||
<div className="font-medium">{order.orderNumber}</div>
|
||||
<div className="mt-1 text-xs text-[var(--color-text-muted)]">{order.customer.name}</div>
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -157,17 +157,17 @@ export const OrdersCalendarView = ({ orders, onOpenOrder }) => {
|
|||
|
||||
<div className="space-y-2">
|
||||
{dayOrders.slice(0, 2).map((order) => (
|
||||
<Button
|
||||
<button
|
||||
key={order.id}
|
||||
variant="ghost"
|
||||
className="w-full rounded-[16px] px-3 py-2 text-left text-sm"
|
||||
type="button"
|
||||
onClick={() => onOpenOrder(order.id)}
|
||||
className="w-full rounded-[16px] bg-[var(--color-surface)] px-3 py-2 text-left text-sm hover:bg-[var(--color-accent-soft)]"
|
||||
>
|
||||
<div className="font-medium">{order.orderNumber}</div>
|
||||
<div className="mt-1 text-xs text-[var(--color-text-muted)]">
|
||||
{order.customer.name}
|
||||
</div>
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
{dayOrders.length > 2 ? (
|
||||
<div className="px-1 text-xs text-[var(--color-text-muted)]">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { formatDateTime } from "../../utils/formatters";
|
||||
import { Badge } from "../UI/Badge";
|
||||
import { Button } from "../UI/Button";
|
||||
import { Panel } from "../UI/Panel";
|
||||
import { SkeletonTable } from "../UI/Loading";
|
||||
import { OrderFilters } from "./OrderFilters";
|
||||
|
|
@ -18,14 +17,6 @@ const fmtDate = (d) => {
|
|||
return `${day}.${m}.${y}`;
|
||||
};
|
||||
|
||||
const getShipmentIssues = (group) => {
|
||||
const data = group?.driverShipmentData;
|
||||
if (!Array.isArray(data) || data.length === 0) return null;
|
||||
const unshipped = data.filter((i) => !i.shipped);
|
||||
if (unshipped.length === 0) return null;
|
||||
return unshipped;
|
||||
};
|
||||
|
||||
const buildGroupSummary = (group) => {
|
||||
const orderCountLabel = `${group.ordersCount || 0} ${group.ordersCount === 1 ? "заказ" : group.ordersCount < 5 ? "заказа" : "заказов"}`;
|
||||
const parts = [orderCountLabel];
|
||||
|
|
@ -54,12 +45,6 @@ const renderOrderNumbers = (group) => {
|
|||
return `${visible.join(", ")} +${remaining}`;
|
||||
};
|
||||
|
||||
const getDeliveryTypeLabel = (deliveryType) => {
|
||||
if (deliveryType === "pickup") return "Самовывоз";
|
||||
if (deliveryType === "delivery") return "Доставка";
|
||||
return "—";
|
||||
};
|
||||
|
||||
const renderMobileOrderNumbers = (group) => {
|
||||
const numbers = group.allBillNumbers || group.orderNumbers;
|
||||
if (!Array.isArray(numbers) || !numbers.length) {
|
||||
|
|
@ -117,21 +102,15 @@ export const OrdersTable = ({
|
|||
Группы не найдены. Попробуйте изменить поиск или статус.
|
||||
</div>
|
||||
) : null}
|
||||
{orderGroups.map((group) => {
|
||||
const hasProblem = group.hasDeliveryProblem;
|
||||
const baseClass = "w-full rounded-[22px] border text-left transition";
|
||||
const selectedClass = selectedOrderGroupId === group.id
|
||||
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)]"
|
||||
: hasProblem
|
||||
? "border-[var(--color-danger)] bg-[rgba(201,61,61,0.1)]"
|
||||
: "border-[var(--color-border)] bg-[var(--color-surface-strong)]";
|
||||
|
||||
return (
|
||||
<Button
|
||||
{orderGroups.map((group) => (
|
||||
<button
|
||||
key={group.id}
|
||||
variant="ghost"
|
||||
type="button"
|
||||
onClick={() => onOpenOrder(group.id)}
|
||||
className={[baseClass, selectedClass].join(" ")}
|
||||
className={[
|
||||
"w-full rounded-[22px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4 text-left transition",
|
||||
selectedOrderGroupId === group.id ? "bg-[var(--color-accent-soft)]" : "",
|
||||
].join(" ")}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3">
|
||||
|
|
@ -149,24 +128,11 @@ export const OrdersTable = ({
|
|||
|
||||
<div className="mt-3 text-sm text-[var(--color-text-muted)]">{buildGroupSummary(group)}</div>
|
||||
<div className="mt-2 text-sm text-[var(--color-text-muted)]">{renderMobileOrderNumbers(group)}</div>
|
||||
<div className="mt-2 text-sm text-[var(--color-text-muted)]">Тип: {getDeliveryTypeLabel(group.deliveryType)}</div>
|
||||
{group.hasDeliveryProblem && (
|
||||
<div className="mt-2 rounded-lg border border-[var(--color-warning)] bg-[var(--color-warning-soft)] px-2 py-1 text-xs">
|
||||
<span className="font-medium text-[var(--color-warning)]">⚠ Проблема с доставкой</span>
|
||||
{group.deliveryProblemNote && (
|
||||
<div className="mt-0.5 text-[var(--color-text-muted)]">{group.deliveryProblemNote}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!group.hasDeliveryProblem && Array.isArray(group?.driverShipmentData) && group.driverShipmentData.length > 0 && (
|
||||
<div className="mt-2 text-xs text-[var(--color-accent)]">✓ Доставка без проблем</div>
|
||||
)}
|
||||
<div className="mt-3 text-xs text-[var(--color-text-muted)]">
|
||||
{formatDateTime(group.updatedAt)}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hidden overflow-x-auto md:block">
|
||||
|
|
@ -176,28 +142,19 @@ export const OrdersTable = ({
|
|||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="grid grid-cols-[minmax(180px,2.5fr)_minmax(100px,1.2fr)_minmax(100px,1fr)_minmax(80px,1fr)_minmax(120px,1.2fr)_minmax(80px,0.8fr)_minmax(130px,1.2fr)] gap-0 border-b border-[var(--color-border)] bg-[var(--color-surface-strong)] text-xs uppercase tracking-[0.16em] text-[var(--color-text-muted)]">
|
||||
<div className="grid grid-cols-[minmax(180px,2.5fr)_minmax(100px,1.2fr)_minmax(100px,1fr)_minmax(80px,1fr)_minmax(120px,1.2fr)_minmax(130px,1.2fr)] gap-0 border-b border-[var(--color-border)] bg-[var(--color-surface-strong)] text-xs uppercase tracking-[0.16em] text-[var(--color-text-muted)]">
|
||||
<div className="px-5 py-4 font-medium">Группа / Клиент</div>
|
||||
<div className="px-5 py-4 font-medium">Счёта</div>
|
||||
<div className="px-5 py-4 font-medium">Статус</div>
|
||||
<div className="px-5 py-4 font-medium">Водитель</div>
|
||||
<div className="px-5 py-4 font-medium">Дата доставки</div>
|
||||
<div className="px-5 py-4 font-medium">Тип</div>
|
||||
<div className="px-5 py-4 font-medium">Обновлён</div>
|
||||
</div>
|
||||
{orderGroups.map((group) => {
|
||||
const hasProblem = group.hasDeliveryProblem;
|
||||
const rowClassName = `grid grid-cols-[minmax(180px,2.5fr)_minmax(100px,1.2fr)_minmax(100px,1fr)_minmax(80px,1fr)_minmax(120px,1.2fr)_minmax(80px,0.8fr)_minmax(130px,1.2fr)] gap-0 w-full border-t border-[var(--color-border)] text-left transition ${
|
||||
hasProblem
|
||||
? "bg-[rgba(201,61,61,0.1)] hover:bg-[rgba(201,61,61,0.15)]"
|
||||
: "hover:bg-[var(--color-accent-soft)]"
|
||||
} ${selectedOrderGroupId === group.id ? "bg-[var(--color-accent-soft)]" : ""}`;
|
||||
|
||||
return (
|
||||
<Button
|
||||
{orderGroups.map((group) => (
|
||||
<button
|
||||
key={group.id}
|
||||
variant="ghost"
|
||||
className={rowClassName}
|
||||
type="button"
|
||||
className={`grid grid-cols-[minmax(180px,2.5fr)_minmax(100px,1.2fr)_minmax(100px,1fr)_minmax(80px,1fr)_minmax(120px,1.2fr)_minmax(130px,1.2fr)] gap-0 w-full border-t border-[var(--color-border)] text-left transition hover:bg-[var(--color-accent-soft)] ${selectedOrderGroupId === group.id ? "bg-[var(--color-accent-soft)]" : ""}`}
|
||||
onClick={() => onOpenOrder(group.id)}
|
||||
>
|
||||
<div className="px-5 py-4">
|
||||
|
|
@ -223,15 +180,11 @@ export const OrdersTable = ({
|
|||
<span className="text-[var(--color-text-muted)]">—</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-5 py-4 text-sm">
|
||||
{getDeliveryTypeLabel(group.deliveryType)}
|
||||
</div>
|
||||
<div className="px-5 py-4 text-sm text-[var(--color-text-muted)]">
|
||||
{formatDateTime(group.updatedAt)}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ export const useOrderGroups = () => {
|
|||
const [filters, setFilters] = React.useState({
|
||||
query: "",
|
||||
displayStatus: "all",
|
||||
deliveryType: "",
|
||||
});
|
||||
const [selectedOrderGroupId, setSelectedOrderGroupId] = React.useState(null);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
|
|
@ -23,15 +22,18 @@ export const useOrderGroups = () => {
|
|||
const [isSavingDriverAssignment, setIsSavingDriverAssignment] = React.useState(false);
|
||||
const [isSavingStatusChange, setIsSavingStatusChange] = React.useState(false);
|
||||
|
||||
const loadLiveData = React.useCallback(async (showLoading = true) => {
|
||||
if (showLoading) {
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const loadLiveData = async () => {
|
||||
/* Demo mode removed — always use Supabase */
|
||||
|
||||
setIsLoading(true);
|
||||
}
|
||||
setLoadError("");
|
||||
|
||||
const groupsResult = await fetchOrderGroups();
|
||||
|
||||
if (!groupsResult) {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -44,29 +46,15 @@ export const useOrderGroups = () => {
|
|||
|
||||
setOrderGroups(groupsResult.data || []);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
loadLiveData();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Initial load
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
const doLoad = async () => {
|
||||
setIsLoading(true);
|
||||
await loadLiveData(true);
|
||||
if (cancelled) return;
|
||||
};
|
||||
doLoad();
|
||||
return () => { cancelled = true; };
|
||||
}, [loadLiveData]);
|
||||
|
||||
// Refetch on window focus to keep data fresh across tabs/pages
|
||||
React.useEffect(() => {
|
||||
const handleFocus = () => {
|
||||
loadLiveData(false); // silent refetch, no loading spinner
|
||||
};
|
||||
window.addEventListener("focus", handleFocus);
|
||||
return () => window.removeEventListener("focus", handleFocus);
|
||||
}, [loadLiveData]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!orderGroups.length) {
|
||||
setSelectedOrderGroupId(null);
|
||||
|
|
@ -250,6 +238,5 @@ export const useOrderGroups = () => {
|
|||
isSavingStatusChange,
|
||||
isLoading,
|
||||
loadError,
|
||||
refetch: loadLiveData,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export const AppShell = ({
|
|||
|
||||
<div className="space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<Button
|
||||
<button
|
||||
key={item.key}
|
||||
className={[
|
||||
"flex w-full items-center justify-between rounded-[18px] px-3 py-3 text-left text-sm transition",
|
||||
|
|
@ -70,7 +70,7 @@ export const AppShell = ({
|
|||
>
|
||||
<span className="font-medium">{item.label}</span>
|
||||
{item.badge ? <Badge tone="accent">{item.badge}</Badge> : null}
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
|
@ -129,7 +129,7 @@ export const AppShell = ({
|
|||
<div className="sticky inset-x-0 top-0 z-40 -mx-3 -mt-4 border-b border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 backdrop-blur xl:hidden sm:-mx-4 md:-mx-6">
|
||||
<div className="flex gap-1 overflow-x-auto" style={{ WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none' }}>
|
||||
{navItems.map((item) => (
|
||||
<Button
|
||||
<button
|
||||
key={item.key}
|
||||
className={[
|
||||
"flex flex-shrink-0 items-center gap-1.5 rounded-[14px] px-3 py-2 text-sm transition",
|
||||
|
|
@ -144,7 +144,7 @@ export const AppShell = ({
|
|||
{item.badge ? (
|
||||
<Badge tone={activeSection === item.key ? "neutral" : "accent"}>{item.badge}</Badge>
|
||||
) : null}
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { OrderCompositionPanel } from "../components/client/OrderCompositionPane
|
|||
import { getInvitationReferenceLabel } from "../components/client/invitationReference";
|
||||
import { DeliveryStateNotice } from "../components/client/DeliveryStateNotice";
|
||||
import { Panel } from "../components/UI/Panel";
|
||||
import { Button } from "../components/UI/Button";
|
||||
import { Skeleton } from "../components/UI/Loading";
|
||||
import { formatDeliveryDate } from "../components/client/deliveryDateFormatting";
|
||||
import {
|
||||
|
|
@ -379,13 +378,6 @@ export const ClientDeliveryPage = () => {
|
|||
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
|
||||
Статус: {invitation?.deliveryType === "pickup" ? "самовывоз" : "доставка"} уже согласован. При повторном открытии этой ссылки будет показан тот же выбор.
|
||||
</p>
|
||||
{(invitation?.pickupCode || invitation?.pickup_code) ? (
|
||||
<div className="mt-3 rounded-2xl border-2 border-[var(--color-accent)] bg-[var(--color-accent-soft)] p-4 text-center">
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-[var(--color-text-muted)]">Код выдачи</p>
|
||||
<p className="mt-1 text-3xl font-bold tracking-[0.4em] text-[var(--color-accent)]">{invitation.pickupCode || invitation.pickup_code}</p>
|
||||
<p className="mt-2 text-xs text-[var(--color-text-muted)]">Покажите этот код при получении заказа</p>
|
||||
</div>
|
||||
) : null}
|
||||
</Panel>
|
||||
) : null}
|
||||
|
||||
|
|
@ -393,7 +385,7 @@ export const ClientDeliveryPage = () => {
|
|||
<>
|
||||
{/* Tab switcher */}
|
||||
<div className="flex gap-2 rounded-[28px] border border-[var(--color-border)] bg-[var(--color-surface)] p-1">
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
className={`flex-1 rounded-[24px] px-4 py-2.5 text-sm font-semibold transition ${
|
||||
activeTab === TAB_DELIVERY
|
||||
|
|
@ -408,8 +400,8 @@ export const ClientDeliveryPage = () => {
|
|||
}}
|
||||
>
|
||||
🚚 Доставка
|
||||
</Button>
|
||||
<Button
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex-1 rounded-[24px] px-4 py-2.5 text-sm font-semibold transition ${
|
||||
activeTab === TAB_PICKUP
|
||||
|
|
@ -424,7 +416,7 @@ export const ClientDeliveryPage = () => {
|
|||
}}
|
||||
>
|
||||
🏪 Самовывоз
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeTab === TAB_DELIVERY && !invitation?.deliveryAddress && !invitation?.customerAddress && (
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ export const GroupDetailPage = () => {
|
|||
changeDeliveryStatus,
|
||||
saveShipmentData,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useOrderGroups();
|
||||
|
||||
// ── Drivers ────────────────────────────────────────────────────────────────
|
||||
|
|
@ -49,11 +48,6 @@ export const GroupDetailPage = () => {
|
|||
}
|
||||
}, [groupId, setSelectedOrderGroupId]);
|
||||
|
||||
// Refetch data when navigating to this page to ensure fresh status
|
||||
React.useEffect(() => {
|
||||
refetch(false); // silent refetch, no loading spinner
|
||||
}, [refetch]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
const load = async () => {
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ export const isOrderGroupAgreedForDelivery = (group) => {
|
|||
};
|
||||
|
||||
export const getOrderGroupDeliveryStatusLabel = (status) =>
|
||||
DELIVERY_GROUP_STATUS_LABELS[status] || (status ? `Неизвестно (${status})` : "Неизвестно");
|
||||
DELIVERY_GROUP_STATUS_LABELS[status] || status || "Неизвестно";
|
||||
|
||||
export const getOrderGroupDisplayStatusLabel = (group) => {
|
||||
const deliveryStatus = group?.deliveryStatus || group?.delivery_status;
|
||||
|
|
@ -305,11 +305,6 @@ export const filterOrderGroups = (groups, filters = {}) => {
|
|||
}
|
||||
}
|
||||
|
||||
const deliveryTypeFilter = (filters.deliveryType || "").trim().toLowerCase();
|
||||
if (deliveryTypeFilter && group.deliveryType !== deliveryTypeFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!query) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -338,11 +333,13 @@ export const ORDER_GROUP_DISPLAY_STATUS_OPTIONS = [
|
|||
{ value: "delivery:paid_storage", label: DELIVERY_GROUP_STATUS_LABELS.paid_storage },
|
||||
{ value: "delivery:cancelled", label: DELIVERY_GROUP_STATUS_LABELS.cancelled },
|
||||
{ value: "status:manual_required", label: "Требует ручной обработки" },
|
||||
{ value: "status:second_sms_sent", label: "Повторное SMS" },
|
||||
{ value: "status:sms_sent", label: "SMS отправлены" },
|
||||
{ value: "status:ready_for_notification", label: "Готово к уведомлению" },
|
||||
];
|
||||
|
||||
export const getOrderGroupStatusLabel = (status) =>
|
||||
ORDER_GROUP_STATUS_LABELS[status] || (status ? `Неизвестно (${status})` : "Неизвестно");
|
||||
ORDER_GROUP_STATUS_LABELS[status] || status || "Неизвестно";
|
||||
|
||||
export const getOrderGroupDeliveryStatusTone = (status) => {
|
||||
switch (status) {
|
||||
|
|
@ -464,9 +461,6 @@ export const buildOrderGroupBuckets = (groups) => {
|
|||
export const getOrderGroupStatusTone = (group) => {
|
||||
const deliveryStatus = group?.deliveryStatus || group?.delivery_status;
|
||||
|
||||
// Highlight groups with delivery problems
|
||||
if (group?.hasDeliveryProblem) return "warning";
|
||||
|
||||
if (deliveryStatus && deliveryStatus !== "pending_confirmation") {
|
||||
return getOrderGroupDeliveryStatusTone(deliveryStatus);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,9 +221,6 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
|
|||
pickupDate: row.pickup_date || null,
|
||||
pickupTimeSlot: row.pickup_time_slot || null,
|
||||
driverShipmentData: row.driver_shipment_data || null,
|
||||
hasDeliveryProblem: row.has_delivery_problem || false,
|
||||
deliveryProblemNote: row.delivery_problem_note || null,
|
||||
pickupCode: row.pickup_code || null,
|
||||
syncedTo1cAt: row.synced_to_1c_at || null,
|
||||
deliveryHalfDay: getOrderGroupDeliveryHalfDay({
|
||||
deliveryHalfDay: rawDeliveryHalfDay,
|
||||
|
|
@ -265,7 +262,7 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
|
|||
};
|
||||
};
|
||||
|
||||
const ORDER_GROUP_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, 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), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, has_delivery_problem, delivery_problem_note, pickup_code, synced_to_1c_at`;
|
||||
const ORDER_GROUP_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, 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), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, synced_to_1c_at`;
|
||||
|
||||
export const updateOrderGroupDeliveryChoice = async ({
|
||||
orderGroupId,
|
||||
|
|
@ -353,7 +350,7 @@ export const assignDriverToOrderGroup = async ({
|
|||
const ADVANCED_STATUSES = ["loaded", "on_route", "delivered", "picked_up"];
|
||||
const newStatus = driverId
|
||||
? (ADVANCED_STATUSES.includes(currentStatus) ? undefined : "driver_assigned")
|
||||
: "agreed"; // removing driver → revert to agreed (NOT null — would violate NOT NULL constraint)
|
||||
: null; // removing driver → reset to pending
|
||||
const updates = {
|
||||
assigned_driver_id: driverId || null,
|
||||
...(newStatus !== undefined && { delivery_status: newStatus }),
|
||||
|
|
@ -394,28 +391,13 @@ export const assignDriverToOrderGroup = async ({
|
|||
export const saveShipmentData = async ({ orderGroupId, shipmentData }) => {
|
||||
return safeSupabaseCall(async () => {
|
||||
const client = requireSupabase();
|
||||
const hasAnyShipped = shipmentData.some((i) => i.shipped);
|
||||
const hasProblem = shipmentData.some((i) => !i.shipped);
|
||||
const problemNote = hasProblem
|
||||
? shipmentData.filter((i) => !i.shipped).map((i) => `${i.name}${i.quantity ? ` (${i.quantity}${i.unit ? ` ${i.unit}` : ""})` : ""}${i.comment ? ` — ${i.comment}` : ""}`).join("; ")
|
||||
: null;
|
||||
|
||||
// Any shipment data saved = delivered status. Problems go into has_delivery_problem/delivery_problem_note columns.
|
||||
const newDeliveryStatus = hasAnyShipped ? "delivered" : undefined;
|
||||
|
||||
const updatePayload = {
|
||||
driver_shipment_data: shipmentData,
|
||||
has_delivery_problem: hasProblem,
|
||||
delivery_problem_note: problemNote,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
if (newDeliveryStatus) {
|
||||
updatePayload.delivery_status = newDeliveryStatus;
|
||||
}
|
||||
|
||||
const { error: updateError } = await client
|
||||
.from("order_groups")
|
||||
.update(updatePayload)
|
||||
.update({
|
||||
driver_shipment_data: shipmentData,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("id", orderGroupId);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
|
|
|
|||
|
|
@ -138,17 +138,6 @@ Deno.serve(async (request) => {
|
|||
|
||||
const deliveryType = body.deliveryType || "delivery";
|
||||
|
||||
if (invitation.order_group_id) {
|
||||
const { data: currentGroup, error: groupError } = await supabase
|
||||
.from("order_groups")
|
||||
.select("id, delivery_status, delivery_address, customer_address")
|
||||
.eq("id", invitation.order_group_id)
|
||||
.single();
|
||||
|
||||
if (groupError) {
|
||||
throw groupError;
|
||||
}
|
||||
|
||||
// When user switches from pickup to delivery but has no address → requires_address
|
||||
const hasAddress = invitation.delivery_address?.trim() || currentGroup?.delivery_address?.trim() || currentGroup?.customer_address?.trim();
|
||||
const effectiveDeliveryStatus = deliveryType === "pickup"
|
||||
|
|
@ -157,6 +146,17 @@ Deno.serve(async (request) => {
|
|||
? "agreed"
|
||||
: "requires_address";
|
||||
|
||||
if (invitation.order_group_id) {
|
||||
const { data: currentGroup, error: groupError } = await supabase
|
||||
.from("order_groups")
|
||||
.select("id, delivery_status")
|
||||
.eq("id", invitation.order_group_id)
|
||||
.single();
|
||||
|
||||
if (groupError) {
|
||||
throw groupError;
|
||||
}
|
||||
|
||||
if (!isActiveInvitationState(invitation.state) || currentGroup.delivery_status !== "pending_confirmation") {
|
||||
return jsonResponse(
|
||||
{
|
||||
|
|
@ -208,8 +208,6 @@ Deno.serve(async (request) => {
|
|||
if (deliveryType === "pickup") {
|
||||
groupUpdateData.pickup_date = body.pickupDate || requestedSlot.deliveryDate || null;
|
||||
groupUpdateData.pickup_time_slot = body.pickupTimeSlot || requestedSlot.deliveryTime || null;
|
||||
// Pickup orders don't need a driver — clear assignment
|
||||
groupUpdateData.assigned_driver_id = null;
|
||||
}
|
||||
|
||||
const { error: groupUpdateError } = await supabase
|
||||
|
|
@ -258,7 +256,6 @@ Deno.serve(async (request) => {
|
|||
ok: true,
|
||||
orderGroupId: invitation.order_group_id,
|
||||
deliveryStatus: effectiveDeliveryStatus,
|
||||
pickupCode: groupUpdateData.pickup_code || null,
|
||||
},
|
||||
200,
|
||||
corsHeaders,
|
||||
|
|
|
|||
|
|
@ -23,9 +23,6 @@ type UpdateDeliveryChoiceBody = {
|
|||
orderGroupId?: string;
|
||||
deliveryDate?: string;
|
||||
deliveryTime?: string;
|
||||
deliveryType?: string;
|
||||
pickupDate?: string;
|
||||
pickupTimeSlot?: string;
|
||||
};
|
||||
|
||||
const isValidDate = (value: string) => /^\d{4}-\d{2}-\d{2}$/.test(value);
|
||||
|
|
@ -114,14 +111,12 @@ Deno.serve(async (request) => {
|
|||
const orderGroupId = String(body.orderGroupId || "").trim();
|
||||
const deliveryDate = String(body.deliveryDate || "").trim();
|
||||
const deliveryTime = normalizeDeliveryTime(String(body.deliveryTime || "").trim());
|
||||
const deliveryType = String(body.deliveryType || "delivery").trim();
|
||||
|
||||
if (!orderGroupId) {
|
||||
return jsonResponse({ ok: false, error: "orderGroupId is required" }, 400, corsHeaders);
|
||||
}
|
||||
|
||||
// Pickup orders don't require a delivery date — allow skipping date validation
|
||||
if (deliveryType !== "pickup" && !isAllowedDeliveryDate(deliveryDate)) {
|
||||
if (!isAllowedDeliveryDate(deliveryDate)) {
|
||||
return jsonResponse({ ok: false, error: "Выберите будущий будний день доставки" }, 400, corsHeaders);
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +147,7 @@ Deno.serve(async (request) => {
|
|||
|
||||
const { data: currentGroup, error: currentGroupError } = await supabase
|
||||
.from("order_groups")
|
||||
.select("id, delivery_status, delivery_invitation_id, pickup_code")
|
||||
.select("id, delivery_status, delivery_invitation_id")
|
||||
.eq("id", orderGroupId)
|
||||
.single();
|
||||
|
||||
|
|
@ -160,29 +155,15 @@ Deno.serve(async (request) => {
|
|||
throw currentGroupError;
|
||||
}
|
||||
|
||||
const effectiveDeliveryStatus = deliveryType === "pickup" ? "pickup" : "agreed";
|
||||
|
||||
const updateData: Record<string, unknown> = {
|
||||
delivery_status: effectiveDeliveryStatus,
|
||||
delivery_date: deliveryDate || null,
|
||||
delivery_time: deliveryTime || null,
|
||||
delivery_type: deliveryType,
|
||||
notification_status: "confirmed",
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (deliveryType === "pickup") {
|
||||
updateData.pickup_date = body.pickupDate || deliveryDate || null;
|
||||
updateData.pickup_time_slot = body.pickupTimeSlot || deliveryTime || null;
|
||||
updateData.assigned_driver_id = null;
|
||||
} else {
|
||||
updateData.pickup_date = null;
|
||||
updateData.pickup_time_slot = null;
|
||||
}
|
||||
|
||||
const { data: group, error: groupUpdateError } = await supabase
|
||||
.from("order_groups")
|
||||
.update(updateData)
|
||||
.update({
|
||||
delivery_status: "agreed",
|
||||
delivery_date: deliveryDate,
|
||||
delivery_time: deliveryTime,
|
||||
notification_status: "confirmed",
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("id", orderGroupId)
|
||||
.select("*")
|
||||
.single();
|
||||
|
|
@ -217,8 +198,7 @@ Deno.serve(async (request) => {
|
|||
actor_user_id: actor.userId,
|
||||
actor_role: actor.role,
|
||||
old_delivery_status: currentGroup.delivery_status || null,
|
||||
new_delivery_status: effectiveDeliveryStatus,
|
||||
delivery_type: deliveryType,
|
||||
new_delivery_status: "agreed",
|
||||
delivery_date: deliveryDate,
|
||||
delivery_time: deliveryTime,
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue