OAFRONT4 统一PC日期和状态展示
这个提交包含在:
@@ -0,0 +1,17 @@
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
export function DateTextInput({
|
||||
placeholder = "例如 2026-05-25",
|
||||
...props
|
||||
}: Omit<ComponentPropsWithoutRef<typeof Input>, "type">) {
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="\\d{4}-\\d{2}-\\d{2}"
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission, hasPermission } from "../auth/permissions";
|
||||
import { AiSuggestionCard } from "../components/ai/AiSuggestionCard";
|
||||
import { DateTextInput } from "../components/common/DateTextInput";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import { FilterBar, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
@@ -37,6 +38,7 @@ import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
import { formatDisplayDateTime } from "../lib/displayFormat";
|
||||
|
||||
function resolveError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
@@ -1200,19 +1202,19 @@ export function AssetRegistryPage() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-zinc-700">最近盘点/恢复验证日期</label>
|
||||
<Input type="date" value={assetForm.lastVerifiedAt} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, lastVerifiedAt: event.target.value }))} />
|
||||
<DateTextInput value={assetForm.lastVerifiedAt} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, lastVerifiedAt: event.target.value }))} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-zinc-700">到期/续费日期</label>
|
||||
<Input type="date" value={assetForm.expiresAt} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, expiresAt: event.target.value }))} />
|
||||
<DateTextInput value={assetForm.expiresAt} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, expiresAt: event.target.value }))} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-zinc-700">启用日期</label>
|
||||
<Input type="date" value={assetForm.activatedAt} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, activatedAt: event.target.value }))} />
|
||||
<DateTextInput value={assetForm.activatedAt} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, activatedAt: event.target.value }))} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-zinc-700">退役日期</label>
|
||||
<Input type="date" value={assetForm.retiredAt} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, retiredAt: event.target.value }))} />
|
||||
<DateTextInput value={assetForm.retiredAt} disabled={!canEditCurrent} onChange={(event) => setAssetForm((current) => ({ ...current, retiredAt: event.target.value }))} />
|
||||
</div>
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<label className="text-sm font-medium text-zinc-700">保密/回收要求</label>
|
||||
@@ -1324,7 +1326,7 @@ export function AssetRegistryPage() {
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="space-y-1 text-xs text-zinc-500">
|
||||
<div>
|
||||
最近更新时间:{resolveAssetErpTimestamp(assetErpStage) ? new Date(resolveAssetErpTimestamp(assetErpStage) as string).toLocaleString() : "尚无同步记录"}
|
||||
最近更新时间:{resolveAssetErpTimestamp(assetErpStage) ? formatDisplayDateTime(resolveAssetErpTimestamp(assetErpStage)) : "尚无同步记录"}
|
||||
</div>
|
||||
<div>变更来源:{assetErpStage.preview.sourceEventName ?? "-"}</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission } from "../auth/permissions";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { DateTextInput } from "../components/common/DateTextInput";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Label } from "../components/ui/label";
|
||||
@@ -499,24 +500,21 @@ export function ContractsRegistryPage() {
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<Label>签署日期</Label>
|
||||
<Input
|
||||
type="date"
|
||||
<DateTextInput
|
||||
value={editor.signedDate}
|
||||
onChange={(event) => setEditor((current) => ({ ...current, signedDate: event.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>生效日期</Label>
|
||||
<Input
|
||||
type="date"
|
||||
<DateTextInput
|
||||
value={editor.effectiveDate}
|
||||
onChange={(event) => setEditor((current) => ({ ...current, effectiveDate: event.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>到期日期</Label>
|
||||
<Input
|
||||
type="date"
|
||||
<DateTextInput
|
||||
value={editor.expiryDate}
|
||||
onChange={(event) => setEditor((current) => ({ ...current, expiryDate: event.target.value }))}
|
||||
/>
|
||||
@@ -685,8 +683,7 @@ export function ContractsRegistryPage() {
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>到期日开始</Label>
|
||||
<Input
|
||||
type="date"
|
||||
<DateTextInput
|
||||
value={filters.expiryDateFrom}
|
||||
onChange={(event) => setFilters((current) => ({ ...current, expiryDateFrom: event.target.value }))}
|
||||
/>
|
||||
@@ -694,8 +691,7 @@ export function ContractsRegistryPage() {
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>到期日结束</Label>
|
||||
<Input
|
||||
type="date"
|
||||
<DateTextInput
|
||||
value={filters.expiryDateTo}
|
||||
onChange={(event) => setFilters((current) => ({ ...current, expiryDateTo: event.target.value }))}
|
||||
/>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { Input } from "../components/ui/input";
|
||||
import { Label } from "../components/ui/label";
|
||||
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { DateTextInput } from "../components/common/DateTextInput";
|
||||
import { downloadBlob } from "../lib/download";
|
||||
import { formatDisplayDate, formatDisplayDateTime } from "../lib/displayFormat";
|
||||
import { toast } from "sonner";
|
||||
@@ -371,8 +372,7 @@ export function PaymentBatchesPage() {
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>默认付款日期</Label>
|
||||
<Input
|
||||
type="date"
|
||||
<DateTextInput
|
||||
value={defaultPaymentDate}
|
||||
disabled={!canManageBatch}
|
||||
onChange={(event) => setDefaultPaymentDate(event.target.value)}
|
||||
@@ -615,8 +615,7 @@ export function PaymentBatchesPage() {
|
||||
disabled={!canEditSelectedBatch || !["pending", "failed"].includes(item.status)}
|
||||
onBlur={(event) => updateItemMutation.mutate({ batchId: selectedBatch.id, itemId: item.id, paymentVoucherNoOverride: event.target.value || null })}
|
||||
/>
|
||||
<Input
|
||||
type="date"
|
||||
<DateTextInput
|
||||
defaultValue={item.paymentDateOverride ?? ""}
|
||||
disabled={!canEditSelectedBatch || !["pending", "failed"].includes(item.status)}
|
||||
onBlur={(event) => updateItemMutation.mutate({ batchId: selectedBatch.id, itemId: item.id, paymentDateOverride: event.target.value || null })}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { MetricCard } from "../components/patterns/MetricCard";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { isApiError } from "../api/types";
|
||||
import { DateTextInput } from "../components/common/DateTextInput";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../components/ui/dialog";
|
||||
@@ -18,6 +19,7 @@ import { Input } from "../components/ui/input";
|
||||
import { Label } from "../components/ui/label";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
import { Textarea } from "../components/ui/textarea";
|
||||
import { formatDisplayDateTime } from "../lib/displayFormat";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const PROJECT_STATUS_OPTIONS: ProjectStatus[] = ["draft", "active", "paused", "completed", "archived"];
|
||||
@@ -33,6 +35,58 @@ const CLOSURE_CHECKLIST_STATUS_OPTIONS: ProjectClosureChecklistStatus[] = ["pend
|
||||
const CONFIDENTIALITY_OPTIONS: ConfidentialityLevel[] = ["public", "internal", "confidential", "restricted"];
|
||||
const STORAGE_TYPE_OPTIONS: DocumentStorageType[] = ["oa_link", "nas", "internal_share", "offline_archive"];
|
||||
|
||||
function projectStatusLabel(value: ProjectStatus) {
|
||||
if (value === "draft") return "草稿";
|
||||
if (value === "active") return "进行中";
|
||||
if (value === "paused") return "已暂停";
|
||||
if (value === "completed") return "已完成";
|
||||
return "已归档";
|
||||
}
|
||||
|
||||
function memberRoleLabel(value: (typeof MEMBER_ROLE_OPTIONS)[number]) {
|
||||
if (value === "owner") return "负责人";
|
||||
if (value === "manager") return "项目经理";
|
||||
if (value === "observer") return "观察人";
|
||||
return "成员";
|
||||
}
|
||||
|
||||
function milestoneStatusLabel(value: ProjectMilestoneStatus) {
|
||||
if (value === "pending") return "待处理";
|
||||
if (value === "in_progress") return "进行中";
|
||||
if (value === "done") return "已完成";
|
||||
if (value === "delayed") return "已延期";
|
||||
return "已取消";
|
||||
}
|
||||
|
||||
function taskStatusLabel(value: ProjectTaskStatus) {
|
||||
if (value === "todo") return "待处理";
|
||||
if (value === "doing") return "进行中";
|
||||
if (value === "blocked") return "受阻";
|
||||
if (value === "done") return "已完成";
|
||||
return "已取消";
|
||||
}
|
||||
|
||||
function taskPriorityLabel(value: ProjectTaskPriority) {
|
||||
if (value === "low") return "低";
|
||||
if (value === "high") return "高";
|
||||
if (value === "urgent") return "紧急";
|
||||
return "普通";
|
||||
}
|
||||
|
||||
function riskLevelLabel(value: ProjectRiskLevel) {
|
||||
if (value === "low") return "低";
|
||||
if (value === "high") return "高";
|
||||
return "中";
|
||||
}
|
||||
|
||||
function riskStatusLabel(value: string) {
|
||||
if (value === "open") return "待处理";
|
||||
if (value === "monitoring") return "观察中";
|
||||
if (value === "mitigating") return "处理中";
|
||||
if (value === "closed") return "已关闭";
|
||||
return value || "-";
|
||||
}
|
||||
|
||||
function resolveError(error: unknown, fallback = "操作失败") {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
@@ -79,8 +133,7 @@ function closureChecklistStatusLabel(value: ProjectClosureChecklistStatus) {
|
||||
}
|
||||
|
||||
function formatDateTime(value: string | null) {
|
||||
if (!value) return "-";
|
||||
return value.slice(0, 16).replace("T", " ");
|
||||
return formatDisplayDateTime(value);
|
||||
}
|
||||
|
||||
export function ProjectDetailPage() {
|
||||
@@ -569,7 +622,7 @@ export function ProjectDetailPage() {
|
||||
) : null}
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-[160px,160px,minmax(0,1fr)]">
|
||||
<Input type="date" value={communicationForm.happenedAt} onChange={(event) => setCommunicationForm((prev) => ({ ...prev, happenedAt: event.target.value }))} />
|
||||
<DateTextInput value={communicationForm.happenedAt} onChange={(event) => setCommunicationForm((prev) => ({ ...prev, happenedAt: event.target.value }))} />
|
||||
<select className="select-std" value={communicationForm.channel} onChange={(event) => setCommunicationForm((prev) => ({ ...prev, channel: event.target.value as ProjectCommunicationChannel }))}>
|
||||
{COMMUNICATION_CHANNEL_OPTIONS.map((item) => <option key={item} value={item}>{communicationChannelLabel(item)}</option>)}
|
||||
</select>
|
||||
@@ -577,7 +630,7 @@ export function ProjectDetailPage() {
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-[minmax(0,1fr),180px]">
|
||||
<Input value={communicationForm.externalOrgName} onChange={(event) => setCommunicationForm((prev) => ({ ...prev, externalOrgName: event.target.value }))} placeholder="外部单位(可选)" />
|
||||
<Input type="date" value={communicationForm.nextFollowUpDate} onChange={(event) => setCommunicationForm((prev) => ({ ...prev, nextFollowUpDate: event.target.value }))} />
|
||||
<DateTextInput value={communicationForm.nextFollowUpDate} onChange={(event) => setCommunicationForm((prev) => ({ ...prev, nextFollowUpDate: event.target.value }))} />
|
||||
</div>
|
||||
<Textarea value={communicationForm.summary} onChange={(event) => setCommunicationForm((prev) => ({ ...prev, summary: event.target.value }))} placeholder="沟通结果摘要(1000字内)" data-testid="project-communication-summary-input" />
|
||||
<Textarea value={communicationForm.nextAction} onChange={(event) => setCommunicationForm((prev) => ({ ...prev, nextAction: event.target.value }))} placeholder="下一步动作(可选)" />
|
||||
@@ -687,20 +740,20 @@ export function ProjectDetailPage() {
|
||||
<div className="space-y-2">
|
||||
<Label>状态</Label>
|
||||
<select className="select-std" value={projectForm.status} onChange={(event) => setProjectForm((prev) => ({ ...prev, status: event.target.value as ProjectStatus }))}>
|
||||
{PROJECT_STATUS_OPTIONS.map((item) => <option key={item} value={item}>{item}</option>)}
|
||||
{PROJECT_STATUS_OPTIONS.map((item) => <option key={item} value={item}>{projectStatusLabel(item)}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>开始日期</Label>
|
||||
<Input type="date" value={projectForm.startsAt} onChange={(event) => setProjectForm((prev) => ({ ...prev, startsAt: event.target.value }))} />
|
||||
<DateTextInput value={projectForm.startsAt} onChange={(event) => setProjectForm((prev) => ({ ...prev, startsAt: event.target.value }))} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>目标完成</Label>
|
||||
<Input type="date" value={projectForm.targetEndAt} onChange={(event) => setProjectForm((prev) => ({ ...prev, targetEndAt: event.target.value }))} />
|
||||
<DateTextInput value={projectForm.targetEndAt} onChange={(event) => setProjectForm((prev) => ({ ...prev, targetEndAt: event.target.value }))} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>实际完成</Label>
|
||||
<Input type="date" value={projectForm.actualEndAt} onChange={(event) => setProjectForm((prev) => ({ ...prev, actualEndAt: event.target.value }))} />
|
||||
<DateTextInput value={projectForm.actualEndAt} onChange={(event) => setProjectForm((prev) => ({ ...prev, actualEndAt: event.target.value }))} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>项目经理</Label>
|
||||
@@ -735,7 +788,7 @@ export function ProjectDetailPage() {
|
||||
{employeeOptions.map((item) => <option key={item.id} value={item.id}>{item.name} · {item.employeeNo}</option>)}
|
||||
</select>
|
||||
<select className="select-std" value={memberRole} onChange={(event) => setMemberRole(event.target.value as (typeof MEMBER_ROLE_OPTIONS)[number])}>
|
||||
{MEMBER_ROLE_OPTIONS.map((item) => <option key={item} value={item}>{item}</option>)}
|
||||
{MEMBER_ROLE_OPTIONS.map((item) => <option key={item} value={item}>{memberRoleLabel(item)}</option>)}
|
||||
</select>
|
||||
<Button type="button" disabled={addMemberMutation.isPending || !memberEmployeeId} onClick={() => addMemberMutation.mutate()}>
|
||||
添加成员
|
||||
@@ -749,7 +802,7 @@ export function ProjectDetailPage() {
|
||||
<TableCell>{item.employee?.name ?? item.employeeId}</TableCell>
|
||||
<TableCell>
|
||||
<select className="select-std h-9" value={item.role} onChange={(event) => patchMemberMutation.mutate({ memberId: item.id, role: event.target.value as (typeof MEMBER_ROLE_OPTIONS)[number] })}>
|
||||
{MEMBER_ROLE_OPTIONS.map((role) => <option key={role} value={role}>{role}</option>)}
|
||||
{MEMBER_ROLE_OPTIONS.map((role) => <option key={role} value={role}>{memberRoleLabel(role)}</option>)}
|
||||
</select>
|
||||
</TableCell>
|
||||
<TableCell>{item.joinedAt ? item.joinedAt.slice(0, 10) : "-"}</TableCell>
|
||||
@@ -769,9 +822,9 @@ export function ProjectDetailPage() {
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-[minmax(0,1fr),160px,160px,120px]">
|
||||
<Input value={milestoneTitle} onChange={(event) => setMilestoneTitle(event.target.value)} placeholder="里程碑标题" />
|
||||
<Input type="date" value={milestoneDueDate} onChange={(event) => setMilestoneDueDate(event.target.value)} />
|
||||
<DateTextInput value={milestoneDueDate} onChange={(event) => setMilestoneDueDate(event.target.value)} />
|
||||
<select className="select-std" value={milestoneStatus} onChange={(event) => setMilestoneStatus(event.target.value as ProjectMilestoneStatus)}>
|
||||
{MILESTONE_STATUS_OPTIONS.map((item) => <option key={item} value={item}>{item}</option>)}
|
||||
{MILESTONE_STATUS_OPTIONS.map((item) => <option key={item} value={item}>{milestoneStatusLabel(item)}</option>)}
|
||||
</select>
|
||||
<Button type="button" disabled={addMilestoneMutation.isPending || !milestoneTitle.trim()} onClick={() => addMilestoneMutation.mutate()}>新增</Button>
|
||||
</div>
|
||||
@@ -784,7 +837,7 @@ export function ProjectDetailPage() {
|
||||
<TableCell>{item.dueDate ?? "-"}</TableCell>
|
||||
<TableCell>
|
||||
<select className="select-std h-9" value={item.status} onChange={(event) => patchMilestoneMutation.mutate({ milestoneId: item.id, status: event.target.value as ProjectMilestoneStatus })}>
|
||||
{MILESTONE_STATUS_OPTIONS.map((option) => <option key={option} value={option}>{option}</option>)}
|
||||
{MILESTONE_STATUS_OPTIONS.map((option) => <option key={option} value={option}>{milestoneStatusLabel(option)}</option>)}
|
||||
</select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -805,9 +858,9 @@ export function ProjectDetailPage() {
|
||||
{employeeOptions.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
<select className="select-std" value={taskPriority} onChange={(event) => setTaskPriority(event.target.value as ProjectTaskPriority)}>
|
||||
{TASK_PRIORITY_OPTIONS.map((item) => <option key={item} value={item}>{item}</option>)}
|
||||
{TASK_PRIORITY_OPTIONS.map((item) => <option key={item} value={item}>{taskPriorityLabel(item)}</option>)}
|
||||
</select>
|
||||
<Input type="date" value={taskDueDate} onChange={(event) => setTaskDueDate(event.target.value)} />
|
||||
<DateTextInput value={taskDueDate} onChange={(event) => setTaskDueDate(event.target.value)} />
|
||||
<Button type="button" disabled={addTaskMutation.isPending || !taskTitle.trim()} onClick={() => addTaskMutation.mutate()}>新增</Button>
|
||||
</div>
|
||||
<Table>
|
||||
@@ -827,12 +880,12 @@ export function ProjectDetailPage() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<select className="select-std h-9" value={item.priority} onChange={(event) => patchTaskMutation.mutate({ taskId: item.id, payload: { priority: event.target.value as ProjectTaskPriority } })}>
|
||||
{TASK_PRIORITY_OPTIONS.map((option) => <option key={option} value={option}>{option}</option>)}
|
||||
{TASK_PRIORITY_OPTIONS.map((option) => <option key={option} value={option}>{taskPriorityLabel(option)}</option>)}
|
||||
</select>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<select className="select-std h-9" value={item.status} onChange={(event) => patchTaskMutation.mutate({ taskId: item.id, payload: { status: event.target.value as ProjectTaskStatus } })}>
|
||||
{TASK_STATUS_OPTIONS.map((option) => <option key={option} value={option}>{option}</option>)}
|
||||
{TASK_STATUS_OPTIONS.map((option) => <option key={option} value={option}>{taskStatusLabel(option)}</option>)}
|
||||
</select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -849,7 +902,7 @@ export function ProjectDetailPage() {
|
||||
<Textarea value={riskDetail} onChange={(event) => setRiskDetail(event.target.value)} placeholder="风险详情、影响与当前处理动作" />
|
||||
<div className="grid gap-3 md:grid-cols-[160px,160px,minmax(0,1fr),120px]">
|
||||
<select className="select-std" value={riskLevel} onChange={(event) => setRiskLevel(event.target.value as ProjectRiskLevel)}>
|
||||
{RISK_LEVEL_OPTIONS.map((item) => <option key={item} value={item}>{item}</option>)}
|
||||
{RISK_LEVEL_OPTIONS.map((item) => <option key={item} value={item}>{riskLevelLabel(item)}</option>)}
|
||||
</select>
|
||||
<select className="select-std" value={riskOwnerId} onChange={(event) => setRiskOwnerId(event.target.value)}>
|
||||
<option value="">未指定责任人</option>
|
||||
@@ -868,11 +921,11 @@ export function ProjectDetailPage() {
|
||||
<div className="font-medium text-zinc-900">{item.title}</div>
|
||||
<div className="text-xs text-zinc-500">{item.detail}</div>
|
||||
</TableCell>
|
||||
<TableCell>{item.level}</TableCell>
|
||||
<TableCell>{riskLevelLabel(item.level)}</TableCell>
|
||||
<TableCell>{item.owner?.name ?? "-"}</TableCell>
|
||||
<TableCell>
|
||||
<select className="select-std h-9" value={item.status} onChange={(event) => patchRiskMutation.mutate({ riskId: item.id, status: event.target.value })}>
|
||||
{RISK_STATUS_OPTIONS.map((option) => <option key={option} value={option}>{option}</option>)}
|
||||
{RISK_STATUS_OPTIONS.map((option) => <option key={option} value={option}>{riskStatusLabel(option)}</option>)}
|
||||
</select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -893,7 +946,7 @@ export function ProjectDetailPage() {
|
||||
<select className="select-std" value={deliveryType} onChange={(event) => setDeliveryType(event.target.value as ProjectDeliveryRecordType)}>
|
||||
{DELIVERY_TYPE_OPTIONS.map((item) => <option key={item} value={item}>{recordTypeLabel(item)}</option>)}
|
||||
</select>
|
||||
<Input type="date" value={deliveryOccurredAt} onChange={(event) => setDeliveryOccurredAt(event.target.value)} />
|
||||
<DateTextInput value={deliveryOccurredAt} onChange={(event) => setDeliveryOccurredAt(event.target.value)} />
|
||||
<Button type="button" disabled={addDeliveryMutation.isPending || !deliveryTitle.trim() || !deliverySummary.trim()} onClick={() => addDeliveryMutation.mutate()}>新增</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission } from "../auth/permissions";
|
||||
import { DateTextInput } from "../components/common/DateTextInput";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { FilterBar, ListWorkspace, StatStrip, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { MetricCard } from "../components/patterns/MetricCard";
|
||||
@@ -20,6 +21,7 @@ import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Label } from "../components/ui/label";
|
||||
import { Textarea } from "../components/ui/textarea";
|
||||
import { formatDisplayDate } from "../lib/displayFormat";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const STATUS_OPTIONS: Array<{ value: "all" | ProjectStatus; label: string }> = [
|
||||
@@ -219,7 +221,7 @@ export function ProjectsPage() {
|
||||
<div className="text-sm text-zinc-500">
|
||||
<div>负责人:{item.owner?.name ?? "-"}</div>
|
||||
<div>项目经理:{item.manager?.name ?? "-"}</div>
|
||||
<div>目标完成:{item.targetEndAt ?? "-"}</div>
|
||||
<div>目标完成:{formatDisplayDate(item.targetEndAt)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-zinc-600">{item.summary || "暂无项目摘要"}</div>
|
||||
@@ -267,11 +269,11 @@ export function ProjectsPage() {
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>开始日期</Label>
|
||||
<Input type="date" value={startsAt} onChange={(event) => setStartsAt(event.target.value)} />
|
||||
<DateTextInput value={startsAt} onChange={(event) => setStartsAt(event.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>目标完成</Label>
|
||||
<Input type="date" value={targetEndAt} onChange={(event) => setTargetEndAt(event.target.value)} />
|
||||
<DateTextInput value={targetEndAt} onChange={(event) => setTargetEndAt(event.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -39,6 +39,7 @@ import { hasPermission } from "../auth/permissions";
|
||||
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
|
||||
import { AiSuggestionCard } from "../components/ai/AiSuggestionCard";
|
||||
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
|
||||
import { DateTextInput } from "../components/common/DateTextInput";
|
||||
import { Pagination } from "../components/common/Pagination";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
@@ -48,6 +49,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from ".
|
||||
import { Textarea } from "../components/ui/textarea";
|
||||
import { FilterBar, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { formatDisplayDateTime } from "../lib/displayFormat";
|
||||
import { buildPrecheckDialogMessage } from "../lib/precheck";
|
||||
import {
|
||||
formatApprovalHistoryTime,
|
||||
@@ -1229,7 +1231,7 @@ export function PurchaseRequestsPage() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-zinc-700">期望到货日期</label>
|
||||
<Input data-testid="purchase-expected-delivery-date" type="date" value={expectedDeliveryDate} disabled={!canEditDraft} onChange={(event) => setExpectedDeliveryDate(event.target.value)} />
|
||||
<DateTextInput data-testid="purchase-expected-delivery-date" value={expectedDeliveryDate} disabled={!canEditDraft} onChange={(event) => setExpectedDeliveryDate(event.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1535,7 +1537,7 @@ export function PurchaseRequestsPage() {
|
||||
<div>
|
||||
<div className="text-zinc-400">时间</div>
|
||||
<div className="mt-0.5 text-zinc-700">
|
||||
{attachment.createdAt ? new Date(attachment.createdAt).toLocaleString() : "-"}
|
||||
{formatDisplayDateTime(attachment.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1578,7 +1580,7 @@ export function PurchaseRequestsPage() {
|
||||
<div className="text-xs text-zinc-500">{attachment.mimeType} / {attachment.fileSize} bytes</div>
|
||||
</TableCell>
|
||||
<TableCell>{attachment.uploader?.name ?? "-"}</TableCell>
|
||||
<TableCell>{attachment.createdAt ? new Date(attachment.createdAt).toLocaleString() : "-"}</TableCell>
|
||||
<TableCell>{formatDisplayDateTime(attachment.createdAt)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button type="button" size="sm" variant="secondary" onClick={() => void handleDownloadAttachment(attachment.id)}>
|
||||
@@ -1618,7 +1620,7 @@ export function PurchaseRequestsPage() {
|
||||
</div>
|
||||
{archiveOrder ? (
|
||||
<div className="text-right text-xs text-zinc-500">
|
||||
<div>归档时间:{archiveOrder.archivedAt ? new Date(archiveOrder.archivedAt).toLocaleString() : "-"}</div>
|
||||
<div>归档时间:{formatDisplayDateTime(archiveOrder.archivedAt)}</div>
|
||||
<div>归档人:{archiveOrder.archivedByEmployee?.name ?? "-"}</div>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -1702,7 +1704,7 @@ export function PurchaseRequestsPage() {
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-zinc-900">{meta.title}</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
{stageTime ? `最近更新时间:${new Date(stageTime).toLocaleString()}` : "尚无同步记录"}
|
||||
{stageTime ? `最近更新时间:${formatDisplayDateTime(stageTime)}` : "尚无同步记录"}
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge status={stage.status} />
|
||||
|
||||
在新工单中引用
屏蔽一个用户