fix: make client delivery selection explicit
This commit is contained in:
parent
3b4b6648ff
commit
31388f267d
|
|
@ -3,6 +3,7 @@ import { Badge } from "../UI/Badge";
|
|||
import { Button } from "../UI/Button";
|
||||
import { Panel } from "../UI/Panel";
|
||||
import { DeliveryStateNotice } from "./DeliveryStateNotice";
|
||||
import { formatDeliverySlotLabel } from "./deliveryDateFormatting";
|
||||
|
||||
const ACTIVE_STATES = new Set(["awaiting_choice", "opened", "reminder_sent"]);
|
||||
|
||||
|
|
@ -16,8 +17,6 @@ const STATE_LABELS = {
|
|||
agreed: "Доставка согласована",
|
||||
};
|
||||
|
||||
const DEFAULT_SLOTS = ["Первая половина дня", "Вторая половина дня"];
|
||||
|
||||
const splitOrderItem = (item) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
|
|
@ -50,20 +49,41 @@ const splitOrderItem = (item) => {
|
|||
|
||||
export const DeliveryChoiceFlow = ({
|
||||
invitation = {},
|
||||
selectedSlot = null,
|
||||
onConfirmChoice = () => {},
|
||||
onRequestNewLink = () => {},
|
||||
}) => {
|
||||
const state = invitation.state || "awaiting_choice";
|
||||
const isActive = ACTIVE_STATES.has(state);
|
||||
const slots = invitation.availableSlots?.length ? invitation.availableSlots : DEFAULT_SLOTS;
|
||||
const orderNumber = invitation.orderNumber || "—";
|
||||
const customerName = invitation.customerName || "Клиент";
|
||||
const orderItems = (invitation.orderItems || invitation.items || [])
|
||||
.map(splitOrderItem)
|
||||
.filter(Boolean);
|
||||
const slotSummary = selectedSlot ? formatDeliverySlotLabel(selectedSlot) : "";
|
||||
|
||||
const selectionCard = (
|
||||
<div className="space-y-3 rounded-[22px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4">
|
||||
<p className="text-sm uppercase tracking-[0.18em] text-[var(--color-text-muted)]">Выбранный слот</p>
|
||||
{slotSummary ? (
|
||||
<p className="text-sm leading-6">
|
||||
<span className="font-medium">Выбрано:</span> {slotSummary}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
|
||||
Выберите дату и половину дня выше, затем нажмите «Сохранить».
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!isActive) {
|
||||
return <DeliveryStateNotice state={state} />;
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{slotSummary ? selectionCard : null}
|
||||
<DeliveryStateNotice state={state} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -96,15 +116,16 @@ export const DeliveryChoiceFlow = ({
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{slots.map((slot) => (
|
||||
<Button key={slot} className="w-full" onClick={() => onConfirmChoice(slot)}>
|
||||
{slot}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{selectionCard}
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
disabled={!slotSummary}
|
||||
onClick={() => onConfirmChoice(selectedSlot)}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
<Button variant="secondary" className="w-full sm:w-auto" onClick={onRequestNewLink}>
|
||||
Запросить новую ссылку
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ describe("DeliveryChoiceFlow", () => {
|
|||
state: "awaiting_choice",
|
||||
orderNumber: "CD-240031",
|
||||
customerName: "Мария Волкова",
|
||||
availableSlots: ["Первая половина дня", "Вторая половина дня"],
|
||||
}}
|
||||
selectedSlot={{
|
||||
date: "2026-04-14",
|
||||
time: "До обеда",
|
||||
}}
|
||||
onConfirmChoice={() => {}}
|
||||
onRequestNewLink={() => {}}
|
||||
|
|
@ -19,11 +22,30 @@ describe("DeliveryChoiceFlow", () => {
|
|||
);
|
||||
|
||||
expect(markup).toContain("Выберите время доставки");
|
||||
expect(markup).toContain("Первая половина дня");
|
||||
expect(markup).toContain("Вторая половина дня");
|
||||
expect(markup).toContain("Выбрано");
|
||||
expect(markup).toContain("14.04.2026");
|
||||
expect(markup).toContain("До обеда");
|
||||
expect(markup).toContain("Сохранить");
|
||||
expect(markup).toContain("Ожидает ответа клиента");
|
||||
});
|
||||
|
||||
it("renders a disabled save action when nothing is selected", () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<DeliveryChoiceFlow
|
||||
invitation={{
|
||||
state: "awaiting_choice",
|
||||
orderNumber: "CD-240031",
|
||||
customerName: "Мария Волкова",
|
||||
}}
|
||||
onConfirmChoice={() => {}}
|
||||
onRequestNewLink={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain("Выберите дату и половину дня");
|
||||
expect(markup).toContain("disabled");
|
||||
});
|
||||
|
||||
it("renders order items with quantities when they are provided", () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<DeliveryChoiceFlow
|
||||
|
|
|
|||
|
|
@ -1,15 +1,7 @@
|
|||
import React from "react";
|
||||
import { Button } from "../UI/Button";
|
||||
import { Panel } from "../UI/Panel";
|
||||
|
||||
const formatSlotDate = (dateStr) => {
|
||||
const date = new Date(`${dateStr}T12:00:00`);
|
||||
return date.toLocaleDateString("ru-RU", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
weekday: "short",
|
||||
});
|
||||
};
|
||||
import { formatDeliverySlotGroupLabel } from "./deliveryDateFormatting";
|
||||
|
||||
const groupSlotsByDate = (slots) => {
|
||||
const groups = new Map();
|
||||
|
|
@ -25,7 +17,14 @@ const groupSlotsByDate = (slots) => {
|
|||
return Array.from(groups.entries()).sort(([a], [b]) => a.localeCompare(b));
|
||||
};
|
||||
|
||||
export const DeliverySlotsPicker = ({ slots, onSelectSlot, selectedSlotId }) => {
|
||||
export { formatDeliverySlotGroupLabel } from "./deliveryDateFormatting";
|
||||
|
||||
export const DeliverySlotsPicker = ({
|
||||
slots,
|
||||
onSelectSlot,
|
||||
selectedSlotId,
|
||||
referenceDate = new Date(),
|
||||
}) => {
|
||||
if (!slots || !slots.length) {
|
||||
return (
|
||||
<Panel className="p-5 sm:p-6">
|
||||
|
|
@ -39,32 +38,44 @@ export const DeliverySlotsPicker = ({ slots, onSelectSlot, selectedSlotId }) =>
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<Panel className="p-5 sm:p-6">
|
||||
<h3 className="text-lg font-semibold">Выберите дату и время доставки</h3>
|
||||
<h3 className="text-lg font-semibold">Выберите день и половину дня доставки</h3>
|
||||
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
|
||||
Нажмите на подходящий слот, чтобы подтвердить выбор.
|
||||
Раскройте нужный день, выберите подходящую половину и затем сохраните выбор ниже.
|
||||
</p>
|
||||
</Panel>
|
||||
|
||||
{grouped.map(([date, dateSlots]) => (
|
||||
<Panel key={date} className="space-y-3 p-5 sm:p-6">
|
||||
<h4 className="font-medium capitalize">{formatSlotDate(date)}</h4>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{dateSlots.map((slot) => {
|
||||
const isSelected = selectedSlotId === slot.id;
|
||||
<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">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<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="hidden text-sm text-[var(--color-text-muted)] group-open:inline">Свернуть</span>
|
||||
</div>
|
||||
</summary>
|
||||
<div className="px-5 pb-5 sm:px-6 sm:pb-6">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{dateSlots.map((slot) => {
|
||||
const isSelected = selectedSlotId === slot.id;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={slot.id}
|
||||
variant={isSelected ? "primary" : "secondary"}
|
||||
onClick={() => onSelectSlot(slot)}
|
||||
>
|
||||
{slot.time}
|
||||
{isSelected ? " \u2014 Выбрано" : ""}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<Button
|
||||
key={slot.id}
|
||||
variant={isSelected ? "primary" : "secondary"}
|
||||
aria-pressed={isSelected}
|
||||
onClick={() => onSelectSlot(slot)}
|
||||
>
|
||||
{slot.time}
|
||||
{isSelected ? " — Выбрано" : ""}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { DeliverySlotsPicker } from "./DeliverySlotsPicker";
|
||||
import {
|
||||
DeliverySlotsPicker,
|
||||
formatDeliverySlotGroupLabel,
|
||||
} from "./DeliverySlotsPicker";
|
||||
|
||||
const mockSlots = [
|
||||
{ date: "2026-04-14", time: "Первая половина дня", id: "slot-1" },
|
||||
|
|
@ -11,17 +14,27 @@ const mockSlots = [
|
|||
];
|
||||
|
||||
describe("DeliverySlotsPicker", () => {
|
||||
it("formats tomorrow and the day after labels with dd.mm.yyyy dates", () => {
|
||||
expect(
|
||||
formatDeliverySlotGroupLabel("2026-04-14", new Date("2026-04-13T09:00:00Z")),
|
||||
).toBe("Завтра · 14.04.2026");
|
||||
expect(
|
||||
formatDeliverySlotGroupLabel("2026-04-15", new Date("2026-04-13T09:00:00Z")),
|
||||
).toBe("Послезавтра · 15.04.2026");
|
||||
});
|
||||
|
||||
it("renders slots grouped by date with half-day choices", () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<DeliverySlotsPicker
|
||||
slots={mockSlots}
|
||||
onSelectSlot={() => {}}
|
||||
selectedSlotId={null}
|
||||
referenceDate={new Date("2026-04-13T09:00:00Z")}
|
||||
/>,
|
||||
).toLowerCase();
|
||||
|
||||
expect(markup).toContain("14 апреля");
|
||||
expect(markup).toContain("15 апреля");
|
||||
expect(markup).toContain("завтра · 14.04.2026");
|
||||
expect(markup).toContain("послезавтра · 15.04.2026");
|
||||
expect(markup).toContain("первая половина дня");
|
||||
expect(markup).toContain("вторая половина дня");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
const DAY_IN_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
const parseIsoDate = (dateStr) => {
|
||||
const [year, month, day] = dateStr.split("-").map(Number);
|
||||
if (!year || !month || !day) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Date(Date.UTC(year, month - 1, day));
|
||||
};
|
||||
|
||||
export const formatDeliveryDate = (dateStr) => {
|
||||
const parsed = parseIsoDate(dateStr);
|
||||
if (!parsed) {
|
||||
return dateStr;
|
||||
}
|
||||
|
||||
const day = String(parsed.getUTCDate()).padStart(2, "0");
|
||||
const month = String(parsed.getUTCMonth() + 1).padStart(2, "0");
|
||||
const year = String(parsed.getUTCFullYear());
|
||||
|
||||
return `${day}.${month}.${year}`;
|
||||
};
|
||||
|
||||
export const getDeliveryRelativeDayLabel = (dateStr, referenceDate = new Date()) => {
|
||||
const target = parseIsoDate(dateStr);
|
||||
if (!target) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const reference = Date.UTC(
|
||||
referenceDate.getUTCFullYear(),
|
||||
referenceDate.getUTCMonth(),
|
||||
referenceDate.getUTCDate(),
|
||||
);
|
||||
const diff = Math.round((target.getTime() - reference) / DAY_IN_MS);
|
||||
|
||||
if (diff === 1) {
|
||||
return "Завтра";
|
||||
}
|
||||
|
||||
if (diff === 2) {
|
||||
return "Послезавтра";
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
export const formatDeliverySlotGroupLabel = (dateStr, referenceDate = new Date()) => {
|
||||
const formattedDate = formatDeliveryDate(dateStr);
|
||||
const relativeLabel = getDeliveryRelativeDayLabel(dateStr, referenceDate);
|
||||
|
||||
return relativeLabel ? `${relativeLabel} · ${formattedDate}` : formattedDate;
|
||||
};
|
||||
|
||||
export const formatDeliverySlotLabel = ({ date, time } = {}) => {
|
||||
const formattedDate = date ? formatDeliveryDate(date) : "";
|
||||
|
||||
if (!formattedDate && !time) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!formattedDate) {
|
||||
return time || "";
|
||||
}
|
||||
|
||||
if (!time) {
|
||||
return formattedDate;
|
||||
}
|
||||
|
||||
return `${formattedDate}, ${time}`;
|
||||
};
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import React from "react";
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { DeliveryChoiceFlow } from "../components/client/DeliveryChoiceFlow";
|
||||
import { DeliverySlotsPicker } from "../components/client/DeliverySlotsPicker";
|
||||
import { DeliveryStateNotice } from "../components/client/DeliveryStateNotice";
|
||||
import { Panel } from "../components/UI/Panel";
|
||||
import { formatDeliveryDate } from "../components/client/deliveryDateFormatting";
|
||||
import {
|
||||
confirmDeliveryChoice,
|
||||
fetchDeliveryInvitation,
|
||||
|
|
@ -43,7 +44,7 @@ export const groupSlotsFromInvitation = (invitation) => {
|
|||
|
||||
return {
|
||||
id: `slot-${index}-${raw}`,
|
||||
date: deliveryDate || parsedDate,
|
||||
date: parsedDate || deliveryDate || "",
|
||||
time: timePart || deliveryTime || raw,
|
||||
};
|
||||
});
|
||||
|
|
@ -58,14 +59,32 @@ export const buildDeliveryConfirmationPayload = ({
|
|||
deliveryTime: slot?.time || invitation?.deliveryTime || undefined,
|
||||
});
|
||||
|
||||
export const buildSelectedSlotFromInvitation = (invitation, slots = []) => {
|
||||
if (!invitation?.deliveryDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const matchingSlot = slots.find(
|
||||
(slot) =>
|
||||
slot.date === invitation.deliveryDate &&
|
||||
(!invitation.deliveryTime || slot.time === invitation.deliveryTime),
|
||||
);
|
||||
|
||||
return matchingSlot || {
|
||||
id: `slot-${invitation.deliveryDate}-${invitation.deliveryTime || "default"}`,
|
||||
date: invitation.deliveryDate,
|
||||
time: invitation.deliveryTime || "Половина дня",
|
||||
};
|
||||
};
|
||||
|
||||
export const ClientDeliveryPage = () => {
|
||||
const { token } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [invitation, setInvitation] = React.useState(null);
|
||||
const [loading, setLoading] = React.useState(Boolean(token));
|
||||
const [error, setError] = React.useState("");
|
||||
const [actionMessage, setActionMessage] = React.useState("");
|
||||
const [selectedSlotId, setSelectedSlotId] = React.useState(null);
|
||||
const [selectedSlot, setSelectedSlot] = React.useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
|
@ -110,44 +129,57 @@ export const ClientDeliveryPage = () => {
|
|||
);
|
||||
|
||||
const invitationState = invitation?.state || "awaiting_choice";
|
||||
const isActiveState = ["awaiting_choice", "opened", "reminder_sent"].includes(invitationState);
|
||||
|
||||
const handleConfirmChoice = React.useCallback(
|
||||
async ({ deliveryDate, deliveryTime }) => {
|
||||
const invitationSelectedSlot = React.useMemo(
|
||||
() => (isActiveState ? null : buildSelectedSlotFromInvitation(invitation, slots)),
|
||||
[invitation, slots, isActiveState],
|
||||
);
|
||||
|
||||
const effectiveSelectedSlot = selectedSlot || invitationSelectedSlot;
|
||||
|
||||
const handleSaveChoice = React.useCallback(
|
||||
async () => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!effectiveSelectedSlot) {
|
||||
setError("Сначала выберите дату и половину дня.");
|
||||
return;
|
||||
}
|
||||
|
||||
setActionMessage("Сохраняем выбор...");
|
||||
setError("");
|
||||
|
||||
try {
|
||||
await confirmDeliveryChoice({
|
||||
token,
|
||||
deliveryTime,
|
||||
deliveryDate,
|
||||
deliveryTime: effectiveSelectedSlot.time,
|
||||
deliveryDate: effectiveSelectedSlot.date,
|
||||
});
|
||||
const loadedInvitation = await fetchDeliveryInvitation(token);
|
||||
setInvitation(loadedInvitation);
|
||||
setSelectedSlot(buildSelectedSlotFromInvitation(loadedInvitation, groupSlotsFromInvitation(loadedInvitation)) || effectiveSelectedSlot);
|
||||
setActionMessage("Выбор сохранен, спасибо.");
|
||||
} catch (confirmError) {
|
||||
setActionMessage("");
|
||||
setError(confirmError instanceof Error ? confirmError.message : "Не удалось сохранить выбор");
|
||||
}
|
||||
},
|
||||
[token, invitation],
|
||||
[effectiveSelectedSlot, token],
|
||||
);
|
||||
|
||||
const handleSlotSelect = React.useCallback(
|
||||
(slot) => {
|
||||
setSelectedSlotId(slot.id);
|
||||
handleConfirmChoice(
|
||||
buildDeliveryConfirmationPayload({
|
||||
slot,
|
||||
invitation,
|
||||
searchDate: searchParams.get("date"),
|
||||
}),
|
||||
setSelectedSlot(slot);
|
||||
setActionMessage(
|
||||
`Выбрано: ${slot.date ? `${formatDeliveryDate(slot.date)} / ${slot.time}` : slot.time}`,
|
||||
);
|
||||
setError("");
|
||||
},
|
||||
[handleConfirmChoice, invitation, searchParams],
|
||||
[],
|
||||
);
|
||||
|
||||
const handleRequestNewLink = React.useCallback(() => {
|
||||
|
|
@ -182,8 +214,6 @@ export const ClientDeliveryPage = () => {
|
|||
);
|
||||
}
|
||||
|
||||
const isActiveState = ["awaiting_choice", "opened", "reminder_sent"].includes(invitationState);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[var(--color-bg)] px-3 py-4 sm:px-6 sm:py-8">
|
||||
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4">
|
||||
|
|
@ -208,7 +238,8 @@ export const ClientDeliveryPage = () => {
|
|||
{isActiveState ? (
|
||||
<DeliveryChoiceFlow
|
||||
invitation={invitation}
|
||||
onConfirmChoice={handleConfirmChoice}
|
||||
selectedSlot={effectiveSelectedSlot}
|
||||
onConfirmChoice={handleSaveChoice}
|
||||
onRequestNewLink={handleRequestNewLink}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildDeliveryConfirmationPayload,
|
||||
buildSelectedSlotFromInvitation,
|
||||
groupSlotsFromInvitation,
|
||||
} from "./ClientDeliveryPage";
|
||||
|
||||
|
|
@ -45,4 +46,62 @@ describe("ClientDeliveryPage helpers", () => {
|
|||
deliveryTime: "После обеда",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the explicit slot dates when invitation already has a delivery date", () => {
|
||||
expect(
|
||||
groupSlotsFromInvitation({
|
||||
deliveryDate: "2026-04-14",
|
||||
deliveryTime: "До обеда",
|
||||
availableSlots: [
|
||||
"2026-04-14, До обеда",
|
||||
"2026-04-14, После обеда",
|
||||
"2026-04-15, До обеда",
|
||||
"2026-04-15, После обеда",
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
id: "slot-0-2026-04-14, До обеда",
|
||||
date: "2026-04-14",
|
||||
time: "До обеда",
|
||||
},
|
||||
{
|
||||
id: "slot-1-2026-04-14, После обеда",
|
||||
date: "2026-04-14",
|
||||
time: "После обеда",
|
||||
},
|
||||
{
|
||||
id: "slot-2-2026-04-15, До обеда",
|
||||
date: "2026-04-15",
|
||||
time: "До обеда",
|
||||
},
|
||||
{
|
||||
id: "slot-3-2026-04-15, После обеда",
|
||||
date: "2026-04-15",
|
||||
time: "После обеда",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("builds a selected slot from invitation data", () => {
|
||||
expect(
|
||||
buildSelectedSlotFromInvitation(
|
||||
{
|
||||
deliveryDate: "2026-04-15",
|
||||
deliveryTime: "После обеда",
|
||||
},
|
||||
[
|
||||
{
|
||||
id: "slot-1",
|
||||
date: "2026-04-15",
|
||||
time: "После обеда",
|
||||
},
|
||||
],
|
||||
),
|
||||
).toEqual({
|
||||
id: "slot-1",
|
||||
date: "2026-04-15",
|
||||
time: "После обеда",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue