169 lines
5.7 KiB
TypeScript
169 lines
5.7 KiB
TypeScript
import Link from "next/link";
|
||
import type { Route } from "next";
|
||
import { Bell, CalendarDays, ChevronRight, ClipboardList } from "lucide-react";
|
||
|
||
import { EmptyState } from "@/components/empty-state";
|
||
import { StatusPill } from "@/components/status-pill";
|
||
import { formatDateTime, roleNames } from "@/lib/format";
|
||
import { getBootstrapData } from "@/lib/mobile-data";
|
||
import type { TaskStatus } from "@/lib/types";
|
||
|
||
const taskStatusText: Record<TaskStatus, string> = {
|
||
pending: "待处理",
|
||
in_progress: "处理中",
|
||
completed: "已完成",
|
||
cancelled: "已取消"
|
||
};
|
||
|
||
const taskStatusTone: Record<TaskStatus, "default" | "success" | "warning" | "danger"> = {
|
||
pending: "default",
|
||
in_progress: "warning",
|
||
completed: "success",
|
||
cancelled: "danger"
|
||
};
|
||
|
||
export default async function DashboardPage() {
|
||
const data = await getBootstrapData();
|
||
const { user } = data;
|
||
|
||
return (
|
||
<div className="page-stack">
|
||
<section className="dashboard-hero">
|
||
<div className="dashboard-hero__content">
|
||
<p className="hero-kicker">{user.storeName || "员工端"}</p>
|
||
<h1>你好,{user.displayName}</h1>
|
||
<p>今日工作状态已同步,先处理最重要的事项。</p>
|
||
</div>
|
||
<div className="hero-stat-strip" aria-label="今日概览">
|
||
<span>
|
||
<strong>{data.pendingTaskCount}</strong>
|
||
待办
|
||
</span>
|
||
<span>
|
||
<strong>{data.todayShifts.length}</strong>
|
||
班次
|
||
</span>
|
||
<span>
|
||
<strong>{data.unreadAnnouncementCount}</strong>
|
||
未读
|
||
</span>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="identity-card">
|
||
<div className="identity-card__media">
|
||
<span className="avatar">{user.displayName.slice(0, 1)}</span>
|
||
</div>
|
||
<div className="identity-card__body">
|
||
<h2>{user.displayName}</h2>
|
||
<p>{user.storeName || "未绑定门店"}</p>
|
||
<div className="pill-row">
|
||
<StatusPill tone="success">账号正常</StatusPill>
|
||
<StatusPill>{roleNames(user.roles)}</StatusPill>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="metric-grid">
|
||
<Link className="metric-tile" href="/tasks">
|
||
<ClipboardList aria-hidden size={20} />
|
||
<span>{data.pendingTaskCount}</span>
|
||
<p>待办任务</p>
|
||
</Link>
|
||
<Link className="metric-tile" href="/tasks">
|
||
<Bell aria-hidden size={20} />
|
||
<span>{data.overdueTaskCount}</span>
|
||
<p>已逾期</p>
|
||
</Link>
|
||
<Link className="metric-tile" href="/announcements">
|
||
<Bell aria-hidden size={20} />
|
||
<span>{data.unreadAnnouncementCount}</span>
|
||
<p>未读公告</p>
|
||
</Link>
|
||
</section>
|
||
|
||
<section className="section-block">
|
||
<div className="section-title">
|
||
<h2>今日排班</h2>
|
||
<CalendarDays aria-hidden size={20} />
|
||
</div>
|
||
{data.todayShifts.length > 0 ? (
|
||
<div className="list-stack">
|
||
{data.todayShifts.map((shift) => (
|
||
<article className="list-card" key={shift.id}>
|
||
<div>
|
||
<h3>{shift.position}</h3>
|
||
<p>
|
||
{formatDateTime(shift.startAt)} - {formatDateTime(shift.endAt)}
|
||
</p>
|
||
</div>
|
||
<StatusPill tone="success">已排班</StatusPill>
|
||
</article>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<EmptyState title="今日暂无排班" description="今天没有安排班次,可在排班页查看未来排班。" />
|
||
)}
|
||
</section>
|
||
|
||
<section className="section-block">
|
||
<div className="section-title">
|
||
<h2>待办任务</h2>
|
||
<Link href="/tasks">
|
||
查看全部
|
||
<ChevronRight aria-hidden size={16} />
|
||
</Link>
|
||
</div>
|
||
{data.tasks.length > 0 ? (
|
||
<div className="list-stack">
|
||
{data.tasks.map((task) => {
|
||
const href = `/tasks/${task.id}` as Route;
|
||
|
||
return (
|
||
<Link className="list-card" href={href} key={task.id}>
|
||
<div>
|
||
<h3>{task.title}</h3>
|
||
<p>{task.description || `截止:${formatDateTime(task.dueAt)}`}</p>
|
||
</div>
|
||
<StatusPill tone={taskStatusTone[task.status]}>{taskStatusText[task.status]}</StatusPill>
|
||
</Link>
|
||
);
|
||
})}
|
||
</div>
|
||
) : (
|
||
<EmptyState title="暂无待办任务" description="待处理和处理中任务会显示在这里。" />
|
||
)}
|
||
</section>
|
||
|
||
<section className="section-block">
|
||
<div className="section-title">
|
||
<h2>最新公告</h2>
|
||
<Link href="/announcements">
|
||
查看全部
|
||
<ChevronRight aria-hidden size={16} />
|
||
</Link>
|
||
</div>
|
||
{data.latestAnnouncements.length > 0 ? (
|
||
<div className="list-stack">
|
||
{data.latestAnnouncements.map((item) => {
|
||
const href = `/announcements/${item.id}` as Route;
|
||
|
||
return (
|
||
<Link className="list-card" href={href} key={item.id}>
|
||
<div>
|
||
<h3>{item.title}</h3>
|
||
<p>{item.summary || formatDateTime(item.publishedAt)}</p>
|
||
</div>
|
||
<StatusPill tone={item.read ? "default" : "warning"}>{item.read ? "已读" : "未读"}</StatusPill>
|
||
</Link>
|
||
);
|
||
})}
|
||
</div>
|
||
) : (
|
||
<EmptyState title="暂无公告" description="当前没有面向你的门店公告。" />
|
||
)}
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|