修复账号禁用及队列场次状态规则
问题与需求:后台禁用员工后旧会话仍可继续访问;实时队列跨天误读昨日场次;项目暂停后需禁止取号但允许叫号。 修复思路:账号权限变更时撤销会话并同步前端登录态;实时查询统一按项目时区当天场次过滤;拆分取号与叫号的状态校验,并补充前后端及 PostgreSQL 回归测试。
This commit is contained in:
@@ -2,6 +2,8 @@ import { act, fireEvent, render, screen, waitFor, within } from "@testing-librar
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
|
||||
const staffPageTestState = vi.hoisted(() => ({ projectStatus: "RUNNING" }));
|
||||
|
||||
vi.mock("../components/AppShell", () => ({
|
||||
AppShell: ({ children, variant }: { children: React.ReactNode; variant?: string }) => (
|
||||
<div data-testid="app-shell" data-variant={variant}>{children}</div>
|
||||
@@ -11,7 +13,7 @@ vi.mock("../hooks/usePollingResource", () => ({
|
||||
usePollingResource: (_load: unknown, options: { intervalMs: number }) => options.intervalMs === 60_000
|
||||
? {
|
||||
data: { projects: [
|
||||
{ id: "project-1", name: "东门观光车", status: "RUNNING", batch_size: 2, call_mode: "BOTH", default_call_ticket_count: 2, max_call_ticket_count: 20, default_call_people_count: 5, max_call_people_count: 30, min_party_size: 1, max_party_size: 8 },
|
||||
{ id: "project-1", name: "东门观光车", status: staffPageTestState.projectStatus, batch_size: 2, call_mode: "BOTH", default_call_ticket_count: 2, max_call_ticket_count: 20, default_call_people_count: 5, max_call_people_count: 30, min_party_size: 1, max_party_size: 8 },
|
||||
{ id: "project-2", name: "西门观光车", status: "RUNNING", batch_size: 2, call_mode: "BOTH", default_call_ticket_count: 2, max_call_ticket_count: 20, default_call_people_count: 5, max_call_people_count: 30, min_party_size: 1, max_party_size: 8 },
|
||||
] },
|
||||
loading: false,
|
||||
@@ -23,7 +25,7 @@ vi.mock("../hooks/usePollingResource", () => ({
|
||||
}
|
||||
: {
|
||||
data: {
|
||||
project: { id: "project-1", name: "东门观光车", status: "RUNNING", batch_size: 2, call_mode: "BOTH", default_call_ticket_count: 2, max_call_ticket_count: 20, default_call_people_count: 5, max_call_people_count: 30, min_party_size: 1, max_party_size: 8 },
|
||||
project: { id: "project-1", name: "东门观光车", status: staffPageTestState.projectStatus, batch_size: 2, call_mode: "BOTH", default_call_ticket_count: 2, max_call_ticket_count: 20, default_call_people_count: 5, max_call_people_count: 30, min_party_size: 1, max_party_size: 8 },
|
||||
revision: 8,
|
||||
waiting: Array.from({ length: 25 }, (_, index) => ({
|
||||
id: `waiting-${index + 1}`,
|
||||
@@ -82,7 +84,21 @@ function renderScene(path: string) {
|
||||
}
|
||||
|
||||
describe("StaffPage scene-focused H5", () => {
|
||||
beforeEach(() => sessionStorage.setItem("scenic-current-project", "project-1"));
|
||||
beforeEach(() => {
|
||||
staffPageTestState.projectStatus = "RUNNING";
|
||||
sessionStorage.setItem("scenic-current-project", "project-1");
|
||||
});
|
||||
|
||||
it("allows calling but blocks ticket creation while the project is paused", () => {
|
||||
staffPageTestState.projectStatus = "PAUSED";
|
||||
renderScene("/staff");
|
||||
|
||||
expect(screen.getByRole("button", { name: "快速叫下一个号" })).toBeEnabled();
|
||||
expect(screen.queryByText("当前项目未开放叫号")).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("link", { name: "取号" }));
|
||||
expect(screen.getByRole("button", { name: "创建排队号码" })).toBeDisabled();
|
||||
expect(screen.getByText("当前不可取号")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the call scene focused on the active batch and the primary call action", () => {
|
||||
renderScene("/staff");
|
||||
|
||||
@@ -142,7 +142,9 @@ export function StaffPage() {
|
||||
const maxPartySize = selectedProject?.max_party_size ?? minPartySize;
|
||||
const freshnessTime = queue?.last_success_at ?? queue?.server_time ?? queueResource.lastClientSuccessAt;
|
||||
const stale = Boolean(queue) && isTimestampStale(freshnessTime, 20_000);
|
||||
const writeBlocked = queueResource.offline || stale || !isProjectRunning(selectedProject?.status);
|
||||
const callBlocked = queueResource.offline || stale ||
|
||||
(selectedProject?.status !== "RUNNING" && selectedProject?.status !== "PAUSED");
|
||||
const takeTicketBlocked = queueResource.offline || stale || !isProjectRunning(selectedProject?.status);
|
||||
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 waitingTickets = queue?.waiting ?? [];
|
||||
@@ -153,11 +155,11 @@ export function StaffPage() {
|
||||
const latestCalledNumber = calledTickets[calledTickets.length - 1]?.ticket_number ?? "暂无";
|
||||
const nextWaitingNumber = queue?.waiting[0]?.ticket_number ?? "暂无";
|
||||
const nextWaitingPeopleCount = queue?.waiting[0]?.party_size;
|
||||
const writeBlockedReason = queueResource.offline
|
||||
const callBlockedReason = queueResource.offline
|
||||
? "网络连接中断,暂不可叫号"
|
||||
: stale
|
||||
? "队列数据更新延迟,请刷新后重试"
|
||||
: !isProjectRunning(selectedProject?.status)
|
||||
: callBlocked
|
||||
? "当前项目未开放叫号"
|
||||
: waitingTicketCount === 0
|
||||
? "当前没有等待号码"
|
||||
@@ -447,21 +449,21 @@ export function StaffPage() {
|
||||
<h2 id="staff-call-actions-title" className="staff-call-actions__title">按号数/人数叫号</h2>
|
||||
<div className="staff-call-controls">
|
||||
{supportsTicketCall ? <div className="staff-call-mode" aria-label="按号码叫号">
|
||||
<button className="button button--primary button--call" onClick={() => void callNext("TICKET", 1)} disabled={busyAction === "call-next" || writeBlocked || waitingTicketCount === 0}>
|
||||
<button className="button button--primary button--call" onClick={() => void callNext("TICKET", 1)} disabled={busyAction === "call-next" || callBlocked || waitingTicketCount === 0}>
|
||||
{busyAction === "call-next" ? "正在叫号" : "快速叫下一个号"}
|
||||
</button>
|
||||
<div className="staff-batch-action">
|
||||
<label className="field"><span>号码数量</span><input aria-label="按号码叫号数量" type="number" min="1" max={selectedProject?.max_call_ticket_count ?? 100} value={ticketCallCount} onChange={(event) => setTicketCallCount(Number(event.target.value) || 1)} /></label>
|
||||
<button className="button button--secondary" onClick={() => void callNext("TICKET", ticketCallCount)} disabled={busyAction === "call-next" || writeBlocked || waitingTicketCount === 0}>批量叫号</button>
|
||||
<button className="button button--secondary" onClick={() => void callNext("TICKET", ticketCallCount)} disabled={busyAction === "call-next" || callBlocked || waitingTicketCount === 0}>批量叫号</button>
|
||||
</div>
|
||||
</div> : null}
|
||||
{supportsPeopleCall ? <div className="staff-call-mode" aria-label="按人数叫号">
|
||||
<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>
|
||||
<button className="button button--secondary" onClick={() => void callNext("PEOPLE", peopleCallCount)} disabled={busyAction === "call-next" || writeBlocked || waitingTicketCount === 0}>批量叫人</button>
|
||||
<button className="button button--secondary" onClick={() => void callNext("PEOPLE", peopleCallCount)} disabled={busyAction === "call-next" || callBlocked || waitingTicketCount === 0}>批量叫人</button>
|
||||
</div>
|
||||
</div> : null}
|
||||
{writeBlockedReason ? <p className="staff-action-reason" role="status">{writeBlockedReason}</p> : null}
|
||||
{callBlockedReason ? <p className="staff-action-reason" role="status">{callBlockedReason}</p> : null}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -584,8 +586,8 @@ export function StaffPage() {
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
{writeBlocked ? <span className="sr-only" id="create-ticket-state">当前不可取号</span> : null}
|
||||
<button className="button button--primary button--wide" type="submit" aria-describedby={writeBlocked ? "create-ticket-state" : undefined} disabled={busyAction === "create-ticket" || writeBlocked}>
|
||||
{takeTicketBlocked ? <span className="sr-only" id="create-ticket-state">当前不可取号</span> : null}
|
||||
<button className="button button--primary button--wide" type="submit" aria-describedby={takeTicketBlocked ? "create-ticket-state" : undefined} disabled={busyAction === "create-ticket" || takeTicketBlocked}>
|
||||
{busyAction === "create-ticket"
|
||||
? "正在创建排队单"
|
||||
: confirmDuplicatePhone
|
||||
|
||||
Reference in New Issue
Block a user