修复:员工端批量叫号按数量输入
问题:批量叫号被误实现为输入目标号码,人数输入框清空时会立即回填 1。 修复:恢复按号码数量叫号,并允许号码数量和目标人数先清空后重新输入;补充回归测试。
This commit is contained in:
@@ -239,16 +239,16 @@ describe("StaffPage scene-focused H5", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can call through a full target number beyond the 0001x range", async () => {
|
it("calls a requested number of tickets", async () => {
|
||||||
const callNext = vi.spyOn(api, "callNext").mockResolvedValueOnce({
|
const callNext = vi.spyOn(api, "callNext").mockResolvedValueOnce({
|
||||||
batch: {
|
batch: {
|
||||||
id: "batch-2",
|
id: "batch-2",
|
||||||
batch_number: 5,
|
batch_number: 5,
|
||||||
status: "CALLED",
|
status: "CALLED",
|
||||||
called_at: "2026-07-10T09:03:00Z",
|
called_at: "2026-07-10T09:03:00Z",
|
||||||
ticket_count: 13,
|
ticket_count: 10,
|
||||||
people_count: 28,
|
people_count: 20,
|
||||||
tickets: [{ id: "called-25", ticket_number: "00025", status: "CALLED", party_size: 2 }],
|
tickets: [{ id: "called-22", ticket_number: "00022", status: "CALLED", party_size: 2 }],
|
||||||
},
|
},
|
||||||
revision: 9,
|
revision: 9,
|
||||||
device_results: [{ channel: "SIMULATOR", status: "SUCCESS" }],
|
device_results: [{ channel: "SIMULATOR", status: "SUCCESS" }],
|
||||||
@@ -256,36 +256,29 @@ describe("StaffPage scene-focused H5", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
renderScene("/staff");
|
renderScene("/staff");
|
||||||
const targetInput = screen.getByRole("textbox", { name: "叫到号码" });
|
const countInput = screen.getByRole("spinbutton", { name: "按号码叫号数量" });
|
||||||
expect(targetInput).toHaveValue("00014");
|
expect(countInput).toHaveValue(2);
|
||||||
|
|
||||||
fireEvent.change(targetInput, { target: { value: "00025" } });
|
fireEvent.change(countInput, { target: { value: "" } });
|
||||||
|
expect(countInput).toHaveValue(null);
|
||||||
|
fireEvent.change(countInput, { target: { value: "10" } });
|
||||||
fireEvent.click(screen.getByRole("button", { name: "批量叫号" }));
|
fireEvent.click(screen.getByRole("button", { name: "批量叫号" }));
|
||||||
|
|
||||||
await waitFor(() => expect(callNext).toHaveBeenCalledWith("project-1", 8, "TICKET", 13, expect.any(String)));
|
await waitFor(() => expect(callNext).toHaveBeenCalledWith("project-1", 8, "TICKET", 10, expect.any(String)));
|
||||||
} finally {
|
} finally {
|
||||||
callNext.mockRestore();
|
callNext.mockRestore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects a target outside the waiting queue or beyond the configured batch limit", () => {
|
it("allows the people count to be cleared before entering a new value", () => {
|
||||||
const callNext = vi.spyOn(api, "callNext");
|
renderScene("/staff");
|
||||||
try {
|
const countInput = screen.getByRole("spinbutton", { name: "按人数叫号数量" });
|
||||||
renderScene("/staff");
|
expect(countInput).toHaveValue(5);
|
||||||
const targetInput = screen.getByRole("textbox", { name: "叫到号码" });
|
|
||||||
|
|
||||||
fireEvent.change(targetInput, { target: { value: "00040" } });
|
fireEvent.change(countInput, { target: { value: "" } });
|
||||||
fireEvent.click(screen.getByRole("button", { name: "批量叫号" }));
|
expect(countInput).toHaveValue(null);
|
||||||
expect(callNext).not.toHaveBeenCalled();
|
fireEvent.change(countInput, { target: { value: "10" } });
|
||||||
expect(screen.getByRole("alert")).toHaveTextContent("请输入当前待叫队列中的完整目标号码");
|
expect(countInput).toHaveValue(10);
|
||||||
|
|
||||||
fireEvent.change(targetInput, { target: { value: "00033" } });
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "批量叫号" }));
|
|
||||||
expect(callNext).not.toHaveBeenCalled();
|
|
||||||
expect(screen.getByRole("alert")).toHaveTextContent("从 00013 叫到 00033 共 21 个号码,超过单次上限 20 个");
|
|
||||||
} finally {
|
|
||||||
callNext.mockRestore();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates number indicators after creation without showing a success card", async () => {
|
it("updates number indicators after creation without showing a success card", async () => {
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ export function StaffPage() {
|
|||||||
const [formError, setFormError] = useState<string | null>(null);
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
const [notice, setNotice] = useState<{ scene: "call" | "tickets"; tone: "success" | "warning" | "danger"; title: string; detail?: string; url?: string } | null>(null);
|
const [notice, setNotice] = useState<{ scene: "call" | "tickets"; tone: "success" | "warning" | "danger"; title: string; detail?: string; url?: string } | null>(null);
|
||||||
const [busyAction, setBusyAction] = useState<string | null>(null);
|
const [busyAction, setBusyAction] = useState<string | null>(null);
|
||||||
const [ticketCallTarget, setTicketCallTarget] = useState("");
|
const [ticketCallCount, setTicketCallCount] = useState<number | "">(1);
|
||||||
const [peopleCallCount, setPeopleCallCount] = useState(1);
|
const [peopleCallCount, setPeopleCallCount] = useState<number | "">(1);
|
||||||
const [visibleWaitingTicketCount, setVisibleWaitingTicketCount] = useState(WAITING_TICKET_PAGE_SIZE);
|
const [visibleWaitingTicketCount, setVisibleWaitingTicketCount] = useState(WAITING_TICKET_PAGE_SIZE);
|
||||||
const [confirmDuplicatePhone, setConfirmDuplicatePhone] = useState(false);
|
const [confirmDuplicatePhone, setConfirmDuplicatePhone] = useState(false);
|
||||||
const [phoneError, setPhoneError] = useState<string | null>(null);
|
const [phoneError, setPhoneError] = useState<string | null>(null);
|
||||||
@@ -148,8 +148,6 @@ export function StaffPage() {
|
|||||||
const waitingTicketCount = Math.max(0, Number(queue?.metrics.waiting_ticket_count ?? queue?.metrics.waiting_count) || 0);
|
const waitingTicketCount = Math.max(0, Number(queue?.metrics.waiting_ticket_count ?? queue?.metrics.waiting_count) || 0);
|
||||||
const waitingPeopleCount = Math.max(0, Number(queue?.metrics.waiting_people_count) || 0);
|
const waitingPeopleCount = Math.max(0, Number(queue?.metrics.waiting_people_count) || 0);
|
||||||
const waitingTickets = queue?.waiting ?? [];
|
const waitingTickets = queue?.waiting ?? [];
|
||||||
const defaultTicketCallCount = selectedProject?.default_call_ticket_count ?? selectedProject?.call_batch_size ?? selectedProject?.batch_size ?? 1;
|
|
||||||
const defaultTicketCallTarget = waitingTickets[Math.min(Math.max(defaultTicketCallCount, 1), waitingTickets.length) - 1]?.ticket_number ?? "";
|
|
||||||
const visibleWaitingTickets = waitingTickets.slice(0, visibleWaitingTicketCount);
|
const visibleWaitingTickets = waitingTickets.slice(0, visibleWaitingTicketCount);
|
||||||
const canShowMoreWaitingTickets = visibleWaitingTickets.length < waitingTickets.length;
|
const canShowMoreWaitingTickets = visibleWaitingTickets.length < waitingTickets.length;
|
||||||
const canCollapseWaitingTickets = visibleWaitingTickets.length > WAITING_TICKET_PAGE_SIZE;
|
const canCollapseWaitingTickets = visibleWaitingTickets.length > WAITING_TICKET_PAGE_SIZE;
|
||||||
@@ -235,6 +233,7 @@ export function StaffPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedProject) return;
|
if (!selectedProject) return;
|
||||||
|
setTicketCallCount(selectedProject.default_call_ticket_count ?? selectedProject.call_batch_size ?? selectedProject.batch_size ?? 1);
|
||||||
setPeopleCallCount(selectedProject.default_call_people_count ?? 1);
|
setPeopleCallCount(selectedProject.default_call_people_count ?? 1);
|
||||||
const minPartySize = selectedProject.min_party_size ?? 1;
|
const minPartySize = selectedProject.min_party_size ?? 1;
|
||||||
const maxPartySize = selectedProject.max_party_size ?? minPartySize;
|
const maxPartySize = selectedProject.max_party_size ?? minPartySize;
|
||||||
@@ -246,14 +245,6 @@ export function StaffPage() {
|
|||||||
}));
|
}));
|
||||||
}, [selectedProject?.id]);
|
}, [selectedProject?.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTicketCallTarget((current) => (
|
|
||||||
waitingTickets.some((ticket) => ticket.ticket_number === current)
|
|
||||||
? current
|
|
||||||
: defaultTicketCallTarget
|
|
||||||
));
|
|
||||||
}, [defaultTicketCallTarget, queue?.revision, selectedProjectId]);
|
|
||||||
|
|
||||||
useGSAP(() => {
|
useGSAP(() => {
|
||||||
if (!showProjectSwitcher) return;
|
if (!showProjectSwitcher) return;
|
||||||
const mm = gsap.matchMedia();
|
const mm = gsap.matchMedia();
|
||||||
@@ -287,7 +278,6 @@ export function StaffPage() {
|
|||||||
setFormError(null);
|
setFormError(null);
|
||||||
setConfirmDuplicatePhone(false);
|
setConfirmDuplicatePhone(false);
|
||||||
setPhoneError(null);
|
setPhoneError(null);
|
||||||
setTicketCallTarget("");
|
|
||||||
createIntentRef.current = null;
|
createIntentRef.current = null;
|
||||||
callIntentRef.current = null;
|
callIntentRef.current = null;
|
||||||
};
|
};
|
||||||
@@ -347,12 +337,12 @@ export function StaffPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const callNext = async (mode: Exclude<CallMode, "BOTH">, count: number) => {
|
const callNext = async (mode: Exclude<CallMode, "BOTH">, count: number | "") => {
|
||||||
if (!queue || waitingTicketCount === 0) return;
|
if (!queue || waitingTicketCount === 0) return;
|
||||||
const limit = mode === "PEOPLE"
|
const limit = mode === "PEOPLE"
|
||||||
? selectedProject?.max_call_people_count ?? 100
|
? selectedProject?.max_call_people_count ?? 100
|
||||||
: selectedProject?.max_call_ticket_count ?? 100;
|
: selectedProject?.max_call_ticket_count ?? 100;
|
||||||
if (!Number.isInteger(count) || count < 1 || count > limit) {
|
if (typeof count !== "number" || !Number.isInteger(count) || count < 1 || count > limit) {
|
||||||
setNotice({
|
setNotice({
|
||||||
scene: "call",
|
scene: "call",
|
||||||
tone: "danger",
|
tone: "danger",
|
||||||
@@ -393,34 +383,6 @@ export function StaffPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const callThroughTicket = () => {
|
|
||||||
const target = ticketCallTarget.trim();
|
|
||||||
const targetIndex = waitingTickets.findIndex((ticket) => ticket.ticket_number === target);
|
|
||||||
if (targetIndex < 0) {
|
|
||||||
setNotice({
|
|
||||||
scene: "call",
|
|
||||||
tone: "danger",
|
|
||||||
title: "叫号未提交",
|
|
||||||
detail: "请输入当前待叫队列中的完整目标号码",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const count = targetIndex + 1;
|
|
||||||
const limit = selectedProject?.max_call_ticket_count ?? 100;
|
|
||||||
if (count > limit) {
|
|
||||||
setNotice({
|
|
||||||
scene: "call",
|
|
||||||
tone: "danger",
|
|
||||||
title: "叫号未提交",
|
|
||||||
detail: `从 ${waitingTickets[0].ticket_number} 叫到 ${target} 共 ${count} 个号码,超过单次上限 ${limit} 个`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void callNext("TICKET", count);
|
|
||||||
};
|
|
||||||
|
|
||||||
const noticeBanner = notice && notice.scene === scene ? (
|
const noticeBanner = notice && notice.scene === scene ? (
|
||||||
<div className={notice.scene === "call" ? "staff-call-notice" : undefined}>
|
<div className={notice.scene === "call" ? "staff-call-notice" : undefined}>
|
||||||
<FeedbackBanner
|
<FeedbackBanner
|
||||||
@@ -491,13 +453,13 @@ export function StaffPage() {
|
|||||||
{busyAction === "call-next" ? "正在叫号" : "快速叫下一个号"}
|
{busyAction === "call-next" ? "正在叫号" : "快速叫下一个号"}
|
||||||
</button>
|
</button>
|
||||||
<div className="staff-batch-action">
|
<div className="staff-batch-action">
|
||||||
<label className="field"><span>叫到号码</span><input aria-label="叫到号码" type="text" inputMode="numeric" autoComplete="off" value={ticketCallTarget} onChange={(event) => setTicketCallTarget(event.target.value.replace(/\D/g, ""))} /></label>
|
<label className="field"><span>号码数量</span><input aria-label="按号码叫号数量" type="number" min="1" max={selectedProject?.max_call_ticket_count ?? 100} value={ticketCallCount} onChange={(event) => setTicketCallCount(event.target.value === "" ? "" : Number(event.target.value))} /></label>
|
||||||
<button className="button button--secondary" onClick={callThroughTicket} disabled={busyAction === "call-next" || callBlocked || waitingTicketCount === 0}>批量叫号</button>
|
<button className="button button--secondary" onClick={() => void callNext("TICKET", ticketCallCount)} disabled={busyAction === "call-next" || callBlocked || waitingTicketCount === 0}>批量叫号</button>
|
||||||
</div>
|
</div>
|
||||||
</div> : null}
|
</div> : null}
|
||||||
{supportsPeopleCall ? <div className="staff-call-mode" aria-label="按人数叫号">
|
{supportsPeopleCall ? <div className="staff-call-mode" aria-label="按人数叫号">
|
||||||
<div className="staff-batch-action">
|
<div className="staff-batch-action">
|
||||||
<label className="field"><span>目标人数</span><input aria-label="按人数叫号数量" type="number" min="1" max={selectedProject?.max_call_people_count ?? 100} value={peopleCallCount} onChange={(event) => setPeopleCallCount(Number(event.target.value) || 1)} /></label>
|
<label className="field"><span>目标人数</span><input aria-label="按人数叫号数量" type="number" min="1" max={selectedProject?.max_call_people_count ?? 100} value={peopleCallCount} onChange={(event) => setPeopleCallCount(event.target.value === "" ? "" : Number(event.target.value))} /></label>
|
||||||
<button className="button button--secondary" onClick={() => void callNext("PEOPLE", peopleCallCount)} disabled={busyAction === "call-next" || callBlocked || waitingTicketCount === 0}>批量叫人</button>
|
<button className="button button--secondary" onClick={() => void callNext("PEOPLE", peopleCallCount)} disabled={busyAction === "call-next" || callBlocked || waitingTicketCount === 0}>批量叫人</button>
|
||||||
</div>
|
</div>
|
||||||
</div> : null}
|
</div> : null}
|
||||||
|
|||||||
Reference in New Issue
Block a user