完成五人团队正式内用最小收口(07-18-small-team-internal-readiness)
这个提交包含在:
@@ -22,6 +22,7 @@ import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission } from "../auth/permissions";
|
||||
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
|
||||
import { useUnsavedChangesGuard } from "../components/common/UnsavedChangesGuard";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { DateTextInput } from "../components/common/DateTextInput";
|
||||
@@ -203,6 +204,7 @@ export function ContractsRegistryPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const initialProjectId = searchParams.get("projectId") ?? "";
|
||||
const canBrowseEmployees = hasAnyPermission(permissions, ["system.users", "report.dashboard.view"]);
|
||||
const canManageContracts = hasAnyPermission(permissions, ["project.manage", "project.view_all", "system.settings"]);
|
||||
|
||||
const [filters, setFilters] = useState<FiltersState>({
|
||||
projectId: initialProjectId,
|
||||
@@ -214,6 +216,7 @@ export function ContractsRegistryPage() {
|
||||
});
|
||||
const [editorId, setEditorId] = useState<string | null>(null);
|
||||
const [editor, setEditor] = useState<EditorState>(() => buildEmptyEditor(initialProjectId));
|
||||
const [savedEditorSignature, setSavedEditorSignature] = useState(() => JSON.stringify(buildEmptyEditor(initialProjectId)));
|
||||
const [showExpiring, setShowExpiring] = useState(false);
|
||||
const [showTrialData, setShowTrialData] = useState(false);
|
||||
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
|
||||
@@ -262,6 +265,10 @@ export function ContractsRegistryPage() {
|
||||
: "默认当前用户";
|
||||
const amountInvalid = Boolean(editor.amount.trim()) && !AMOUNT_PATTERN.test(editor.amount.trim());
|
||||
const expiringContractCount = expiringQuery.data?.total ?? expiringItems.length;
|
||||
const runGuarded = useUnsavedChangesGuard(
|
||||
showEditor && JSON.stringify(editor) !== savedEditorSignature,
|
||||
"合同表单",
|
||||
);
|
||||
|
||||
const refreshAll = async () => {
|
||||
await Promise.all([
|
||||
@@ -271,14 +278,20 @@ export function ContractsRegistryPage() {
|
||||
};
|
||||
|
||||
const resetEditor = () => {
|
||||
const nextEditor = buildEmptyEditor(filters.projectId);
|
||||
setEditorId(null);
|
||||
setEditor(buildEmptyEditor(filters.projectId));
|
||||
setEditor(nextEditor);
|
||||
setSavedEditorSignature(JSON.stringify(nextEditor));
|
||||
};
|
||||
|
||||
const openEditor = (item: ContractRegistryItem) => {
|
||||
setEditorId(item.id);
|
||||
setEditor(loadEditorFromItem(item));
|
||||
setShowEditor(true);
|
||||
runGuarded(() => {
|
||||
const nextEditor = loadEditorFromItem(item);
|
||||
setEditorId(item.id);
|
||||
setEditor(nextEditor);
|
||||
setSavedEditorSignature(JSON.stringify(nextEditor));
|
||||
setShowEditor(true);
|
||||
});
|
||||
};
|
||||
|
||||
const saveMutation = useMutation({
|
||||
@@ -335,15 +348,19 @@ export function ContractsRegistryPage() {
|
||||
title="合同"
|
||||
actions={
|
||||
<HeaderActionGroup>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
resetEditor();
|
||||
setShowEditor(true);
|
||||
}}
|
||||
>
|
||||
新增合同
|
||||
</Button>
|
||||
{canManageContracts ? (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
runGuarded(() => {
|
||||
resetEditor();
|
||||
setShowEditor(true);
|
||||
});
|
||||
}}
|
||||
>
|
||||
新增合同
|
||||
</Button>
|
||||
) : null}
|
||||
</HeaderActionGroup>
|
||||
}
|
||||
/>
|
||||
@@ -359,9 +376,11 @@ export function ContractsRegistryPage() {
|
||||
<div className="truncate font-medium text-zinc-900">{item.contractNo} · {item.counterpartyName}</div>
|
||||
<div className="text-xs text-zinc-500">到期日 {formatDisplayDate(item.expiryDate)} · 责任人 {item.owner?.name ?? "-"}</div>
|
||||
</div>
|
||||
<Button type="button" size="sm" variant="secondary" onClick={() => openEditor(item)}>
|
||||
编辑
|
||||
</Button>
|
||||
{canManageContracts ? (
|
||||
<Button type="button" size="sm" variant="secondary" onClick={() => openEditor(item)}>
|
||||
编辑
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
{!expiringQuery.isLoading && !expiringItems.length ? (
|
||||
@@ -382,7 +401,7 @@ export function ContractsRegistryPage() {
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{editorId ? (
|
||||
<Button type="button" variant="secondary" size="sm" onClick={resetEditor}>
|
||||
<Button type="button" variant="secondary" size="sm" onClick={() => runGuarded(resetEditor)}>
|
||||
切换到新建
|
||||
</Button>
|
||||
) : null}
|
||||
@@ -391,8 +410,10 @@ export function ContractsRegistryPage() {
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShowEditor(false);
|
||||
resetEditor();
|
||||
runGuarded(() => {
|
||||
setShowEditor(false);
|
||||
resetEditor();
|
||||
});
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
@@ -705,17 +726,21 @@ export function ContractsRegistryPage() {
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setFilters({
|
||||
projectId: initialProjectId,
|
||||
status: "",
|
||||
contractType: "",
|
||||
counterpartyName: "",
|
||||
expiryDateFrom: "",
|
||||
expiryDateTo: "",
|
||||
runGuarded(() => {
|
||||
const nextEditor = buildEmptyEditor(initialProjectId);
|
||||
setFilters({
|
||||
projectId: initialProjectId,
|
||||
status: "",
|
||||
contractType: "",
|
||||
counterpartyName: "",
|
||||
expiryDateFrom: "",
|
||||
expiryDateTo: "",
|
||||
});
|
||||
setShowEditor(false);
|
||||
setEditorId(null);
|
||||
setEditor(nextEditor);
|
||||
setSavedEditorSignature(JSON.stringify(nextEditor));
|
||||
});
|
||||
setShowEditor(false);
|
||||
setEditorId(null);
|
||||
setEditor(buildEmptyEditor(initialProjectId));
|
||||
}}
|
||||
>
|
||||
重置筛选
|
||||
@@ -810,9 +835,11 @@ export function ContractsRegistryPage() {
|
||||
{item.amount ? ` · ${item.amount} ${item.currency}` : ""}
|
||||
{item.expiryDate ? ` · ${formatDisplayDate(item.expiryDate)}` : ""}
|
||||
</div>
|
||||
<Button className="mt-3" type="button" size="sm" variant="secondary" onClick={() => openEditor(item)}>
|
||||
编辑
|
||||
</Button>
|
||||
{canManageContracts ? (
|
||||
<Button className="mt-3" type="button" size="sm" variant="secondary" onClick={() => openEditor(item)}>
|
||||
编辑
|
||||
</Button>
|
||||
) : null}
|
||||
</article>
|
||||
))}
|
||||
{!contractsQuery.isLoading && !visibleContracts.length ? (
|
||||
@@ -860,9 +887,12 @@ export function ContractsRegistryPage() {
|
||||
</td>
|
||||
<td className="table-td">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button type="button" size="sm" variant="secondary" onClick={() => openEditor(item)}>
|
||||
编辑
|
||||
</Button>
|
||||
{canManageContracts ? (
|
||||
<Button type="button" size="sm" variant="secondary" onClick={() => openEditor(item)}>
|
||||
编辑
|
||||
</Button>
|
||||
) : null}
|
||||
{canManageContracts || item.project ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="button" size="sm" variant="secondary">
|
||||
@@ -870,25 +900,30 @@ export function ContractsRegistryPage() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
disabled={renewMutation.isPending || !["active", "expired"].includes(item.status)}
|
||||
onSelect={() => renewMutation.mutate(item)}
|
||||
>
|
||||
续约
|
||||
</DropdownMenuItem>
|
||||
{canManageContracts ? (
|
||||
<DropdownMenuItem
|
||||
disabled={renewMutation.isPending || !["active", "expired"].includes(item.status)}
|
||||
onSelect={() => renewMutation.mutate(item)}
|
||||
>
|
||||
续约
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{item.project ? (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to={`/projects/${item.project.id}`}>查看项目</Link>
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
<DropdownMenuItem
|
||||
disabled={toggleStatusMutation.isPending}
|
||||
onSelect={() => setConfirmStatusTarget(item)}
|
||||
>
|
||||
{item.persistedStatus === "archived" ? "恢复生效" : "归档"}
|
||||
</DropdownMenuItem>
|
||||
{canManageContracts ? (
|
||||
<DropdownMenuItem
|
||||
disabled={toggleStatusMutation.isPending}
|
||||
onSelect={() => setConfirmStatusTarget(item)}
|
||||
>
|
||||
{item.persistedStatus === "archived" ? "恢复生效" : "归档"}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
在新工单中引用
屏蔽一个用户