R95 A5 合同台账权限请求降级
这个提交包含在:
@@ -19,6 +19,8 @@ import { fetchEmployees } from "../api/employees";
|
|||||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||||
import { fetchProjects, type ConfidentialityLevel } from "../api/projects";
|
import { fetchProjects, type ConfidentialityLevel } from "../api/projects";
|
||||||
import { isApiError } from "../api/types";
|
import { isApiError } from "../api/types";
|
||||||
|
import { useAuth } from "../auth/AuthContext";
|
||||||
|
import { hasAnyPermission } from "../auth/permissions";
|
||||||
import { PageHeader } from "../components/common/PageHeader";
|
import { PageHeader } from "../components/common/PageHeader";
|
||||||
import { StatusBadge } from "../components/common/StatusBadge";
|
import { StatusBadge } from "../components/common/StatusBadge";
|
||||||
import { Button } from "../components/ui/button";
|
import { Button } from "../components/ui/button";
|
||||||
@@ -124,7 +126,9 @@ function buildFiltersPayload(filters: FiltersState): ContractRegistryFilters {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSavePayload(form: EditorState) {
|
function buildSavePayload(form: EditorState, options: { includeOwnerEmployeeId?: boolean } = {}) {
|
||||||
|
const includeOwnerEmployeeId = options.includeOwnerEmployeeId ?? true;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contractNo: form.contractNo.trim(),
|
contractNo: form.contractNo.trim(),
|
||||||
projectId: form.projectId || null,
|
projectId: form.projectId || null,
|
||||||
@@ -139,7 +143,7 @@ function buildSavePayload(form: EditorState) {
|
|||||||
confidentialityLevel: form.confidentialityLevel,
|
confidentialityLevel: form.confidentialityLevel,
|
||||||
archiveLocation: form.archiveLocation.trim(),
|
archiveLocation: form.archiveLocation.trim(),
|
||||||
offlineArchiveNo: form.offlineArchiveNo.trim() || null,
|
offlineArchiveNo: form.offlineArchiveNo.trim() || null,
|
||||||
ownerEmployeeId: form.ownerEmployeeId || undefined,
|
...(includeOwnerEmployeeId && form.ownerEmployeeId ? { ownerEmployeeId: form.ownerEmployeeId } : {}),
|
||||||
status: form.status,
|
status: form.status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -170,8 +174,10 @@ function nextPersistedStatus(item: ContractRegistryItem): ContractRegistryPersis
|
|||||||
|
|
||||||
export function ContractsRegistryPage() {
|
export function ContractsRegistryPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { permissions } = useAuth();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const initialProjectId = searchParams.get("projectId") ?? "";
|
const initialProjectId = searchParams.get("projectId") ?? "";
|
||||||
|
const canBrowseEmployees = hasAnyPermission(permissions, ["system.users", "report.dashboard.view"]);
|
||||||
|
|
||||||
const [filters, setFilters] = useState<FiltersState>({
|
const [filters, setFilters] = useState<FiltersState>({
|
||||||
projectId: initialProjectId,
|
projectId: initialProjectId,
|
||||||
@@ -199,15 +205,21 @@ export function ContractsRegistryPage() {
|
|||||||
queryFn: () => fetchProjects({}),
|
queryFn: () => fetchProjects({}),
|
||||||
});
|
});
|
||||||
const employeesQuery = useQuery({
|
const employeesQuery = useQuery({
|
||||||
|
enabled: canBrowseEmployees,
|
||||||
queryKey: ["employees", "contracts-page"],
|
queryKey: ["employees", "contracts-page"],
|
||||||
queryFn: () => fetchEmployees({ page: 1, pageSize: 100, status: "active" }),
|
queryFn: () => fetchEmployees({ page: 1, pageSize: 100, status: "active" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectOptions = projectsQuery.data?.items ?? [];
|
const projectOptions = projectsQuery.data?.items ?? [];
|
||||||
const employeeOptions = employeesQuery.data?.items ?? [];
|
const employeeOptions = canBrowseEmployees ? employeesQuery.data?.items ?? [] : [];
|
||||||
const contracts = contractsQuery.data?.items ?? [];
|
const contracts = contractsQuery.data?.items ?? [];
|
||||||
const expiringItems = expiringQuery.data?.items ?? [];
|
const expiringItems = expiringQuery.data?.items ?? [];
|
||||||
const editingItem = editorId ? contracts.find((item) => item.id === editorId) ?? null : null;
|
const editingItem = editorId ? contracts.find((item) => item.id === editorId) ?? null : null;
|
||||||
|
const readonlyOwnerLabel = editingItem?.owner
|
||||||
|
? `${editingItem.owner.name} · ${editingItem.owner.employeeNo}`
|
||||||
|
: editor.ownerEmployeeId
|
||||||
|
? "保留现有责任人"
|
||||||
|
: "默认当前用户";
|
||||||
const amountInvalid = Boolean(editor.amount.trim()) && !AMOUNT_PATTERN.test(editor.amount.trim());
|
const amountInvalid = Boolean(editor.amount.trim()) && !AMOUNT_PATTERN.test(editor.amount.trim());
|
||||||
|
|
||||||
const refreshAll = async () => {
|
const refreshAll = async () => {
|
||||||
@@ -224,10 +236,11 @@ export function ContractsRegistryPage() {
|
|||||||
|
|
||||||
const saveMutation = useMutation({
|
const saveMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
|
const payload = buildSavePayload(editor, { includeOwnerEmployeeId: canBrowseEmployees });
|
||||||
if (editorId) {
|
if (editorId) {
|
||||||
return patchContractRegistry(editorId, buildSavePayload(editor));
|
return patchContractRegistry(editorId, payload);
|
||||||
}
|
}
|
||||||
return createContractRegistry(buildSavePayload(editor));
|
return createContractRegistry(payload);
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await refreshAll();
|
await refreshAll();
|
||||||
@@ -537,18 +550,27 @@ export function ContractsRegistryPage() {
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>责任人</Label>
|
<Label>责任人</Label>
|
||||||
<select
|
{canBrowseEmployees ? (
|
||||||
className="select-std"
|
<select
|
||||||
value={editor.ownerEmployeeId}
|
className="select-std"
|
||||||
onChange={(event) => setEditor((current) => ({ ...current, ownerEmployeeId: event.target.value }))}
|
value={editor.ownerEmployeeId}
|
||||||
>
|
onChange={(event) => setEditor((current) => ({ ...current, ownerEmployeeId: event.target.value }))}
|
||||||
<option value="">默认当前用户</option>
|
>
|
||||||
{employeeOptions.map((item) => (
|
<option value="">默认当前用户</option>
|
||||||
<option key={item.id} value={item.id}>
|
{employeeOptions.map((item) => (
|
||||||
{item.name} · {item.employeeNo}
|
<option key={item.id} value={item.id}>
|
||||||
</option>
|
{item.name} · {item.employeeNo}
|
||||||
))}
|
</option>
|
||||||
</select>
|
))}
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm">
|
||||||
|
<div className="font-medium text-zinc-900">{readonlyOwnerLabel}</div>
|
||||||
|
<div className="mt-1 text-xs text-zinc-500">
|
||||||
|
当前账号无人员目录权限,新建时使用当前用户,编辑时保留原责任人。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户