feat(08-05-interaction-pattern-unification): 统一前端交互与反馈模式
这个提交包含在:
@@ -1,3 +1,4 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
type FinanceLedgerPaymentStatus,
|
||||
type FinanceLedgerStatus,
|
||||
} from "../api/apOperations";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import {
|
||||
dismissRiskFlag,
|
||||
fetchRiskFlags,
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
fetchTravelReconciliationBatchDetail,
|
||||
type TravelReconciliationItem,
|
||||
} from "../api/travelReconciliation";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { ConfirmDialog } from "../components/common/ConfirmDialog";
|
||||
@@ -87,11 +86,7 @@ type FailedPaymentItemRow = PaymentBatchItem & {
|
||||
const BUDGET_RISK_CODES = new Set(["budget_exceeded", "budget_threshold_exceeded"]);
|
||||
|
||||
function resolveErrorMessage(error: unknown, fallback = "操作失败") {
|
||||
return isApiError(error)
|
||||
? `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
function formatDateTime(value: string | null) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
import {
|
||||
createApprovalRule,
|
||||
fetchApprovalRules,
|
||||
@@ -112,7 +111,7 @@ export function ApprovalRulesPage() {
|
||||
toast.success("规则已创建");
|
||||
},
|
||||
onError: (err) => {
|
||||
const msg = isApiError(err) ? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}` : err instanceof Error ? err.message : "创建审批规则失败";
|
||||
const msg = resolveUserErrorMessage(err, "创建审批规则失败");
|
||||
setError(msg);
|
||||
toast.error(msg);
|
||||
},
|
||||
@@ -126,7 +125,7 @@ export function ApprovalRulesPage() {
|
||||
toast.success("状态已更新");
|
||||
},
|
||||
onError: (err) => {
|
||||
const msg = isApiError(err) ? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}` : err instanceof Error ? err.message : "更新审批规则失败";
|
||||
const msg = resolveUserErrorMessage(err, "更新审批规则失败");
|
||||
setError(msg);
|
||||
toast.error(msg);
|
||||
},
|
||||
@@ -148,7 +147,7 @@ export function ApprovalRulesPage() {
|
||||
toast.success("规则已更新");
|
||||
},
|
||||
onError: (err) => {
|
||||
const msg = isApiError(err) ? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}` : err instanceof Error ? err.message : "更新审批规则失败";
|
||||
const msg = resolveUserErrorMessage(err, "更新审批规则失败");
|
||||
setError(msg);
|
||||
toast.error(msg);
|
||||
},
|
||||
@@ -382,13 +381,7 @@ export function ApprovalRulesPage() {
|
||||
|
||||
{listQuery.isError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{isApiError(listQuery.error)
|
||||
? `${listQuery.error.code}: ${mapApiErrorMessage(
|
||||
listQuery.error.code,
|
||||
listQuery.error.details,
|
||||
listQuery.error.message,
|
||||
)}`
|
||||
: "加载审批规则失败"}
|
||||
{resolveUserErrorMessage(listQuery.error, "加载审批规则失败")}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import {
|
||||
assignAsset,
|
||||
createAsset,
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
} from "../api/assets";
|
||||
import { fetchPurchaseRequests } from "../api/purchaseRequests";
|
||||
import { fetchCostCenters, fetchDocumentCategories, fetchLegalEntities } from "../api/masterData";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission, hasPermission } from "../auth/permissions";
|
||||
import { DateTextInput } from "../components/common/DateTextInput";
|
||||
@@ -51,13 +50,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from ".
|
||||
import { formatCompactIdentifier, formatDisplayDateTime, formatTextWithCompactLongNumbers, isCompactedIdentifier } from "../lib/displayFormat";
|
||||
|
||||
function resolveError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "请求失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function initialCategoryForm() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { type ReactNode, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
type AttendanceDailyStatus,
|
||||
type AttendanceMonthlyDay,
|
||||
} from "../api/attendance";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
@@ -90,13 +90,7 @@ function formatTime(value: string | null) {
|
||||
}
|
||||
|
||||
function resolveErrorMessage(error: unknown, fallback = "请求失败,请稍后重试") {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
function isUnifiedDisabledError(error: unknown) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { archiveBudget, createBudget, fetchBudgets, type BudgetListQuery, type BudgetPool, type BudgetScopeType } from "../api/budgets";
|
||||
import {
|
||||
archiveBudgetControlRule,
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
import { fetchDepartments } from "../api/departments";
|
||||
import { fetchCostCenters, fetchLegalEntities } from "../api/masterData";
|
||||
import { fetchExpenseTypes } from "../api/expenseTypes";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
@@ -59,13 +58,7 @@ function emptyRuleForm(): RuleFormState {
|
||||
}
|
||||
|
||||
function resolveError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "请求失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function describeScope(item: BudgetPool) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { AuthPanel } from "../components/auth/AuthPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -41,7 +41,7 @@ export function ChangePasswordPage() {
|
||||
navigate("/portal", { replace: true });
|
||||
} catch (err) {
|
||||
if (isApiError(err)) {
|
||||
setError(mapApiErrorMessage(err.code, err.details, err.message));
|
||||
setError(resolveUserErrorMessage(err));
|
||||
} else {
|
||||
setError("修改密码失败,请稍后重试");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -16,9 +17,7 @@ import {
|
||||
type ContractRegistryType,
|
||||
} from "../api/contractsRegistry";
|
||||
import { fetchEmployees } from "../api/employees";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { fetchProjects, type ConfidentialityLevel } from "../api/projects";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission } from "../auth/permissions";
|
||||
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
|
||||
@@ -92,10 +91,7 @@ type EditorState = {
|
||||
};
|
||||
|
||||
function resolveError(error: unknown, fallback = "操作失败") {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
function confidentialityLabel(value: ConfidentialityLevel) {
|
||||
|
||||
@@ -26,6 +26,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 { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs";
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
|
||||
const SCOPE_TYPE_OPTIONS: Array<{ value: CutoverScope["scopeType"]; label: string }> = [
|
||||
{ value: "department", label: "department" },
|
||||
@@ -44,10 +45,7 @@ type ScopeFormState = {
|
||||
};
|
||||
|
||||
function resolveErrorMessage(error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "请求失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function formatDateTime(value: string | null) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { exportDashboardReimbursements } from "../api/exports";
|
||||
import {
|
||||
fetchDashboardApprovalEfficiency,
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
type DashboardQuery,
|
||||
} from "../api/dashboard";
|
||||
import { fetchDepartments } from "../api/departments";
|
||||
import { isApiError } from "../api/types";
|
||||
import { toast } from "sonner";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
@@ -36,15 +35,7 @@ function toCurrency(value: string | number | null | undefined) {
|
||||
}
|
||||
|
||||
function resolveErrorMessage(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return "加载失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "加载失败,请稍后重试。");
|
||||
}
|
||||
|
||||
export function DashboardPage() {
|
||||
@@ -107,7 +98,7 @@ export function DashboardPage() {
|
||||
toast.success("导出完成");
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(err instanceof Error ? err.message : "导出失败");
|
||||
toast.error(resolveUserErrorMessage(err, "导出失败"));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
import { createDelegation, fetchMyDelegations, revokeDelegation } from "../api/delegations";
|
||||
import { fetchWorkflowApprovers } from "../api/workflow";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
@@ -50,7 +49,7 @@ export function DelegationsPage() {
|
||||
toast.success("委派已创建");
|
||||
},
|
||||
onError: (err) => {
|
||||
const msg = isApiError(err) ? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}` : err instanceof Error ? err.message : "创建委派失败";
|
||||
const msg = resolveUserErrorMessage(err, "创建委派失败");
|
||||
setError(msg);
|
||||
toast.error(msg);
|
||||
},
|
||||
@@ -64,7 +63,7 @@ export function DelegationsPage() {
|
||||
toast.success("已撤销");
|
||||
},
|
||||
onError: (err) => {
|
||||
const msg = isApiError(err) ? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}` : err instanceof Error ? err.message : "撤销委派失败";
|
||||
const msg = resolveUserErrorMessage(err, "撤销委派失败");
|
||||
setError(msg);
|
||||
toast.error(msg);
|
||||
},
|
||||
@@ -161,13 +160,7 @@ export function DelegationsPage() {
|
||||
{listQuery.isLoading ? <TableSkeleton columns={5} rows={3} /> : null}
|
||||
{listQuery.isError ? (
|
||||
<p className="text-sm text-red-600">
|
||||
{isApiError(listQuery.error)
|
||||
? `${listQuery.error.code}: ${mapApiErrorMessage(
|
||||
listQuery.error.code,
|
||||
listQuery.error.details,
|
||||
listQuery.error.message,
|
||||
)}`
|
||||
: "加载委派列表失败"}
|
||||
{resolveUserErrorMessage(listQuery.error, "加载委派列表失败")}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -259,4 +252,3 @@ export function DelegationsPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
import {
|
||||
archiveDepartment,
|
||||
@@ -31,34 +31,34 @@ import { ConfirmDialog } from "../components/common/ConfirmDialog";
|
||||
|
||||
function mapDepartmentErrorMessage(error: unknown, fallback: string) {
|
||||
if (!isApiError(error)) {
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
if (error.code === "VALID_001") {
|
||||
if (error.details?.parentId === "not_found") {
|
||||
return `${error.code}: 上级部门不存在。`;
|
||||
return "上级部门不存在。";
|
||||
}
|
||||
if (error.details?.parentId === "archived") {
|
||||
return `${error.code}: 上级部门已归档,无法继续使用。`;
|
||||
return "上级部门已归档,无法继续使用。";
|
||||
}
|
||||
if (error.details?.parentId === "self_not_allowed") {
|
||||
return `${error.code}: 上级部门不能选择自身。`;
|
||||
return "上级部门不能选择自身。";
|
||||
}
|
||||
if (error.details?.parentId === "cycle_not_allowed") {
|
||||
return `${error.code}: 上级部门不能形成循环层级。`;
|
||||
return "上级部门不能形成循环层级。";
|
||||
}
|
||||
if (error.details?.status === "archived") {
|
||||
return `${error.code}: 已归档部门不可编辑。`;
|
||||
return "已归档部门不可编辑。";
|
||||
}
|
||||
if (error.details?.department === "has_children") {
|
||||
return `${error.code}: 当前部门存在下级节点,无法归档。`;
|
||||
return "当前部门存在下级节点,无法归档。";
|
||||
}
|
||||
if (error.details?.department === "has_active_employees") {
|
||||
return `${error.code}: 当前部门仍有在职员工,无法归档。`;
|
||||
return "当前部门仍有在职员工,无法归档。";
|
||||
}
|
||||
}
|
||||
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
return resolveUserErrorMessage(error);
|
||||
}
|
||||
|
||||
function flattenDepartmentTree(nodes: DepartmentTreeNode[], depth = 0): Array<{ id: string; label: string }> {
|
||||
@@ -288,13 +288,7 @@ export function DepartmentsPage() {
|
||||
|
||||
{listQuery.isError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{isApiError(listQuery.error)
|
||||
? `${listQuery.error.code}: ${mapApiErrorMessage(
|
||||
listQuery.error.code,
|
||||
listQuery.error.details,
|
||||
listQuery.error.message,
|
||||
)}`
|
||||
: "加载部门列表失败"}
|
||||
{resolveUserErrorMessage(listQuery.error, "加载部门列表失败")}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link, useParams, useSearchParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
type EmployeeCertificate,
|
||||
type EmployeeContract,
|
||||
type EmployeeContractType,
|
||||
type EmployeeEmploymentStatus,
|
||||
type EmployeeTraining,
|
||||
type EmployeeTrainingStatus,
|
||||
type EmployeeTrainingType,
|
||||
@@ -50,12 +52,19 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from ".
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs";
|
||||
import { Textarea } from "../components/ui/textarea";
|
||||
import { toast } from "sonner";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
import { downloadBlob } from "../lib/download";
|
||||
|
||||
type ProfileTab = "profile" | "contracts" | "trainingCertificates" | "performance" | "salary";
|
||||
|
||||
const EMPLOYMENT_STATUS_LABELS: Record<EmployeeEmploymentStatus, string> = {
|
||||
onboarding: "待入职",
|
||||
probation: "试用期",
|
||||
active: "在职",
|
||||
offboarding: "离职办理中",
|
||||
inactive: "已离职",
|
||||
};
|
||||
|
||||
type ContractFormState = {
|
||||
contractNo: string;
|
||||
contractType: EmployeeContractType;
|
||||
@@ -90,10 +99,7 @@ const CONTRACT_TYPE_OPTIONS: Array<{ value: EmployeeContractType; label: string
|
||||
];
|
||||
|
||||
function errorMessage(error: unknown, fallback: string) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
function buildEmptyContractForm(): ContractFormState {
|
||||
@@ -808,7 +814,10 @@ export function EmployeeProfilePage() {
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<StatusBadge status={profile.employee.status} />
|
||||
<StatusBadge status={profile.employee.employmentStatus} />
|
||||
<StatusBadge
|
||||
status={profile.employee.employmentStatus}
|
||||
label={EMPLOYMENT_STATUS_LABELS[profile.employee.employmentStatus] ?? "未知状态"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -989,7 +998,7 @@ export function EmployeeProfilePage() {
|
||||
资产未归还:<span className="font-medium text-zinc-900">{profile.lifecycle.blockers.openAssetCount}</span>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 p-3 text-sm">
|
||||
待办审批任务:<span className="font-medium text-zinc-900">{profile.lifecycle.blockers.pendingWorkflowTaskCount}</span>
|
||||
待审批任务:<span className="font-medium text-zinc-900">{profile.lifecycle.blockers.pendingWorkflowTaskCount}</span>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 p-3 text-sm">
|
||||
可完成离职:<span className="font-medium text-zinc-900">{profile.lifecycle.blockers.canCompleteOffboarding ? "是" : "否"}</span>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { fetchDepartments } from "../api/departments";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { fetchExpenseTypes } from "../api/expenseTypes";
|
||||
import {
|
||||
createExpenseStandard,
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
type ExpenseStandardPerUnit,
|
||||
type ExpenseStandardScopeType,
|
||||
} from "../api/expenseStandards";
|
||||
import { isApiError } from "../api/types";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import { ConfirmDialog } from "../components/common/ConfirmDialog";
|
||||
@@ -77,13 +76,7 @@ function toForm(item: ExpenseStandard): FormState {
|
||||
}
|
||||
|
||||
function resolveErrorMessage(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "请求失败,请稍后重试";
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试");
|
||||
}
|
||||
|
||||
function toneClassForControlMode(mode: ExpenseStandardControlMode) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { fetchDepartments } from "../api/departments";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import {
|
||||
batchUpdateFeatureFlags,
|
||||
fetchFeatureFlagChangelog,
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
type FeatureFlagSection,
|
||||
type FeatureFlagSnapshot,
|
||||
} from "../api/featureFlags";
|
||||
import { isApiError } from "../api/types";
|
||||
import { Pagination } from "../components/common/Pagination";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
@@ -60,7 +59,7 @@ function toDisplayName(flagKey: string) {
|
||||
if (flagKey === "attendance_unified_v1") return "企微考勤统一汇总";
|
||||
if (flagKey === "weekly_report_v1") return "周报工作台";
|
||||
if (flagKey === "employee_lifecycle_v1") return "人员生命周期";
|
||||
if (flagKey === "todo_board_v1") return "待办工作台";
|
||||
if (flagKey === "todo_board_v1") return "个人事项工作台";
|
||||
if (flagKey === "wecom_entry_v1") return "企业微信入口";
|
||||
if (flagKey === "project_domain_v1") return "项目主域";
|
||||
if (flagKey === "project_linking_v1") return "项目联动";
|
||||
@@ -96,13 +95,7 @@ function dedupeIds(items: string[]) {
|
||||
}
|
||||
|
||||
function resolveErrorMessage(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "请求失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function summarizeSnapshot(snapshot: FeatureFlagSnapshot | null) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { MailCheck } from "lucide-react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { fetchAuthCapabilities, forgotPassword, resetPassword } from "../api/auth";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
import { AuthPanel } from "../components/auth/AuthPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -64,7 +64,7 @@ export function ForgotPasswordPage() {
|
||||
setMessage("如账号可用,验证码将发送到企业邮箱;10 分钟内有效,请同时查看免提醒邮件。");
|
||||
} catch (err) {
|
||||
if (isApiError(err)) {
|
||||
setError(mapApiErrorMessage(err.code, err.details, err.message));
|
||||
setError(resolveUserErrorMessage(err));
|
||||
} else {
|
||||
setError("请求验证码失败,请稍后重试或联系管理员。");
|
||||
}
|
||||
@@ -103,7 +103,7 @@ export function ForgotPasswordPage() {
|
||||
navigate("/login", { replace: true });
|
||||
} catch (err) {
|
||||
if (isApiError(err)) {
|
||||
setError(mapApiErrorMessage(err.code, err.details, err.message));
|
||||
setError(resolveUserErrorMessage(err));
|
||||
} else {
|
||||
setError("重置密码失败,请稍后重试或联系管理员。");
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { fetchAssets } from "../api/assets";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import {
|
||||
completeInventoryCheck,
|
||||
createInventoryCheck,
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
type InventoryCheckScopeType,
|
||||
} from "../api/inventoryChecks";
|
||||
import { fetchCostCenters, fetchLegalEntities } from "../api/masterData";
|
||||
import { isApiError } from "../api/types";
|
||||
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
@@ -24,13 +23,7 @@ import { Input } from "../components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
|
||||
function resolveError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "请求失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function scopeLabel(scopeType: InventoryCheckScopeType) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { fetchMyDelegations } from "../api/delegations";
|
||||
import { exportInvoicePool } from "../api/exports";
|
||||
import {
|
||||
@@ -228,9 +228,7 @@ export function InvoicePoolPage() {
|
||||
// --- Helper for error handling ---
|
||||
|
||||
const toUserError = (err: unknown, fallback: string) => {
|
||||
return isApiError(err)
|
||||
? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`
|
||||
: err instanceof Error ? err.message : fallback;
|
||||
return resolveUserErrorMessage(err, fallback);
|
||||
};
|
||||
|
||||
const handleError = (err: unknown, fallback: string) => {
|
||||
@@ -330,7 +328,7 @@ export function InvoicePoolPage() {
|
||||
},
|
||||
onError: (err) => {
|
||||
if (isApiError(err)) {
|
||||
setError(`${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`);
|
||||
setError(resolveUserErrorMessage(err));
|
||||
} else {
|
||||
setError("预览链接获取失败");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import {
|
||||
createLeaveRequest,
|
||||
fetchLeaveBalances,
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
type LeaveRequestStatus,
|
||||
type LeaveSession,
|
||||
} from "../api/leave";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { CardSkeleton } from "../components/common/CardSkeleton";
|
||||
@@ -149,13 +148,7 @@ function formatCalendarEntryLabel(item: LeaveRequestItem, dateKey: string) {
|
||||
}
|
||||
|
||||
function resolveError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "操作失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "操作失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function calculateRequestedDaysPreview(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Building2, MailCheck } from "lucide-react";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { fetchAuthCapabilities, fetchSsoProviders, fetchWecomConfig, requestEmailLoginCode } from "../api/auth";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { AuthPanel } from "../components/auth/AuthPanel";
|
||||
@@ -127,7 +127,7 @@ export function LoginPage() {
|
||||
}
|
||||
} catch (err) {
|
||||
if (isApiError(err)) {
|
||||
setError(mapApiErrorMessage(err.code, err.details, err.message));
|
||||
setError(resolveUserErrorMessage(err));
|
||||
} else if (err instanceof TypeError) {
|
||||
setError("无法连接后端服务,请稍后再试或联系管理员。");
|
||||
} else {
|
||||
@@ -179,7 +179,7 @@ export function LoginPage() {
|
||||
setEmailCodeMessage("如账号可用,验证码将发送到企业邮箱;10 分钟内有效,请同时查看免提醒邮件。");
|
||||
} catch (err) {
|
||||
if (isApiError(err)) {
|
||||
setEmailCodeError(mapApiErrorMessage(err.code, err.details, err.message));
|
||||
setEmailCodeError(resolveUserErrorMessage(err));
|
||||
} else if (err instanceof TypeError) {
|
||||
setEmailCodeError("无法连接后端服务,请稍后再试或联系管理员。");
|
||||
} else {
|
||||
@@ -215,7 +215,7 @@ export function LoginPage() {
|
||||
}
|
||||
} catch (err) {
|
||||
if (isApiError(err)) {
|
||||
setEmailCodeError(mapApiErrorMessage(err.code, err.details, err.message));
|
||||
setEmailCodeError(resolveUserErrorMessage(err));
|
||||
} else if (err instanceof TypeError) {
|
||||
setEmailCodeError("无法连接后端服务,请稍后再试或联系管理员。");
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import {
|
||||
archiveCostCenter,
|
||||
archiveDocumentCategory,
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
type DocumentCategoryEntityType,
|
||||
type DocumentResetPeriod,
|
||||
} from "../api/masterData";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission, hasPermission } from "../auth/permissions";
|
||||
import { ConfirmDialog } from "../components/common/ConfirmDialog";
|
||||
@@ -48,13 +47,7 @@ const RESET_PERIOD_OPTIONS: Array<{ value: DocumentResetPeriod; label: string }>
|
||||
];
|
||||
|
||||
function resolveError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "操作失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "操作失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function toEnabledFilter(value: "" | "true" | "false") {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { fetchLeaveRequests, type LeaveRequestItem } from "../api/leave";
|
||||
import { fetchPurchaseRequests, type PurchaseRequestSummary } from "../api/purchaseRequests";
|
||||
import { fetchReimbursements, type ReimbursementListItem } from "../api/reimbursements";
|
||||
@@ -25,8 +25,10 @@ import { PageHeader } from "../components/common/PageHeader";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { MobileDetailHeader } from "../components/mobile/MobileDetailHeader";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { MOBILE_STICKY_ACTION_PAGE_CLASS, MobileStickyActionBar } from "../components/patterns/MobileStickyActionBar";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Label } from "../components/ui/label";
|
||||
import { Spinner } from "../components/ui/spinner";
|
||||
import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs";
|
||||
import { Textarea } from "../components/ui/textarea";
|
||||
@@ -171,13 +173,7 @@ function publicReference(value: string | null | undefined) {
|
||||
}
|
||||
|
||||
function resolveErrorMessage(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "动作执行失败";
|
||||
return resolveUserErrorMessage(error, "动作执行失败");
|
||||
}
|
||||
|
||||
function isAlreadyProcessedTaskError(error: unknown) {
|
||||
@@ -643,10 +639,10 @@ export function MobileApprovalsPage() {
|
||||
throw new Error("请选择任务");
|
||||
}
|
||||
if (!selectedTask) {
|
||||
throw new Error("当前待办尚未缓存,请联网加载后再操作");
|
||||
throw new Error("当前审批任务尚未缓存,请联网加载后再操作");
|
||||
}
|
||||
if (!selectedTask.allowedActions.includes(action)) {
|
||||
throw new Error("当前待办不允许该动作");
|
||||
throw new Error("当前审批任务不允许该动作");
|
||||
}
|
||||
if (action === "reject" && !comment.trim()) {
|
||||
throw new Error("驳回必须填写意见");
|
||||
@@ -685,10 +681,10 @@ export function MobileApprovalsPage() {
|
||||
throw new Error("请选择任务");
|
||||
}
|
||||
if (!selectedTask) {
|
||||
throw new Error("当前待办尚未缓存,请联网加载后再操作");
|
||||
throw new Error("当前审批任务尚未缓存,请联网加载后再操作");
|
||||
}
|
||||
if (!selectedTask.allowedActions.includes(action)) {
|
||||
throw new Error("当前待办不允许该动作");
|
||||
throw new Error("当前审批任务不允许该动作");
|
||||
}
|
||||
|
||||
if (action === "approve") {
|
||||
@@ -744,6 +740,11 @@ export function MobileApprovalsPage() {
|
||||
});
|
||||
|
||||
const isActionSubmitting = actionMutation.isPending || offlineDraftSubmitting;
|
||||
const transferDisabledReason = !transferToApproverId
|
||||
? "请先选择目标审批人。"
|
||||
: transferReason.trim().length < 10
|
||||
? `请填写转审理由,至少 10 个字(还差 ${10 - transferReason.trim().length} 个字)。`
|
||||
: "转审理由已满足要求。";
|
||||
|
||||
const syncMutation = useMutation({
|
||||
mutationFn: async (items: WorkflowDraftQueueItem[]) => {
|
||||
@@ -831,7 +832,7 @@ export function MobileApprovalsPage() {
|
||||
}
|
||||
}, [canUseRemote, retryableQueueItems, syncMutation]);
|
||||
|
||||
const listTitle = taskTab === "pending" ? "待我处理" : "已处理";
|
||||
const listTitle = taskTab === "pending" ? "待我审批" : "已处理";
|
||||
const shouldShowDraftQueue = !isOnline || currentQueueItems.length > 0;
|
||||
const showMobileApprovalActions = Boolean(isPushDetail && selectedTask && selectedTaskAllowsActions);
|
||||
const approvalFocusText = taskTab === "pending"
|
||||
@@ -852,9 +853,7 @@ export function MobileApprovalsPage() {
|
||||
<PageScaffold
|
||||
className={cn(
|
||||
"space-y-4",
|
||||
showMobileApprovalActions
|
||||
? "mobile-fixed-action-page pb-[calc(var(--mobile-sticky-action-height,78px)+env(safe-area-inset-bottom,0px)+1.25rem)]"
|
||||
: "",
|
||||
showMobileApprovalActions ? MOBILE_STICKY_ACTION_PAGE_CLASS : "",
|
||||
)}
|
||||
>
|
||||
{isPushDetail ? (
|
||||
@@ -871,14 +870,14 @@ export function MobileApprovalsPage() {
|
||||
/>
|
||||
) : (
|
||||
<PageHeader
|
||||
title={canAccessRemoteApprovals ? "待我处理" : "进度"}
|
||||
subtitle={canAccessRemoteApprovals ? "处理待办,查看已处理。" : "查看我发起的申请进度。"}
|
||||
title={canAccessRemoteApprovals ? "待我审批" : "进度"}
|
||||
subtitle={canAccessRemoteApprovals ? "处理待审批任务,查看已处理记录。" : "查看我发起的申请进度。"}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isOnline && canAccessRemoteApprovals ? (
|
||||
<p className="rounded-lg border border-orange-200 bg-orange-50 px-3 py-2 text-sm text-orange-700">
|
||||
当前离线{offlineAccessMode ? ",已使用本地会话快照进入移动审批壳。" : "。"}仅支持查看已缓存待办,并保存通过 / 驳回草稿;联网同步成功后才会正式提交。
|
||||
当前离线{offlineAccessMode ? ",已使用本地会话快照进入移动审批壳。" : "。"}仅支持查看已缓存审批任务,并保存通过 / 驳回草稿;联网同步成功后才会正式提交。
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -985,7 +984,7 @@ export function MobileApprovalsPage() {
|
||||
<div className="mobile-command-surface">
|
||||
<Tabs value={taskTab} onValueChange={handleTaskTabChange}>
|
||||
<TabsList className="flex w-full justify-start gap-1">
|
||||
<TabsTrigger value="pending">待我处理</TabsTrigger>
|
||||
<TabsTrigger value="pending">待我审批</TabsTrigger>
|
||||
<TabsTrigger value="completed" disabled={!canUseRemote}>已处理</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
@@ -1041,7 +1040,7 @@ export function MobileApprovalsPage() {
|
||||
{taskTab === "completed"
|
||||
? "最近 30 天暂无符合筛选条件的已处理任务。"
|
||||
: !canAccessRemoteApprovals && isOnline
|
||||
? "暂无待我处理;收到企业微信审批消息后可从这里查看。"
|
||||
? "暂无待我审批;收到企业微信审批消息后可从这里查看。"
|
||||
: canUseRemote
|
||||
? "暂无符合筛选条件的审批任务。"
|
||||
: "暂无符合筛选条件的已缓存审批。"}
|
||||
@@ -1102,7 +1101,7 @@ export function MobileApprovalsPage() {
|
||||
|
||||
{isOfflineUncachedDeepLink ? (
|
||||
<p className="rounded-lg border border-orange-200 bg-orange-50 px-3 py-2 text-sm text-orange-700">
|
||||
离线未缓存该待办,请联网后重试。
|
||||
离线未缓存该审批任务,请联网后重试。
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -1223,7 +1222,14 @@ export function MobileApprovalsPage() {
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-xs leading-5 text-zinc-600" data-testid="mobile-workflow-action-consequence-help">
|
||||
<div><span className="font-medium text-zinc-800">退回:</span>原单回到申请人修改,保留单号和历史,修改后可重新提交。</div>
|
||||
<div><span className="font-medium text-zinc-800">驳回:</span>本次流程终止或进入驳回态,申请人通常需要重新发起。</div>
|
||||
</div>
|
||||
|
||||
<Label className="sr-only" htmlFor="mobile-approvals-comment">审批意见</Label>
|
||||
<Textarea
|
||||
id="mobile-approvals-comment"
|
||||
data-testid="mobile-approvals-comment"
|
||||
onChange={(event) => setComment(event.target.value)}
|
||||
placeholder="意见(驳回/退回必填)"
|
||||
@@ -1235,11 +1241,10 @@ export function MobileApprovalsPage() {
|
||||
<p className="text-xs text-zinc-500">离线仅保存通过 / 驳回草稿,联网同步成功后才会正式提交。</p>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className="mobile-sticky-action-surface fixed inset-x-0 bottom-0 z-50 border-t border-zinc-200 bg-white/95 shadow-[0_-10px_24px_rgba(15,23,42,0.10)] backdrop-blur"
|
||||
data-testid="mobile-approvals-sticky-actions"
|
||||
<MobileStickyActionBar
|
||||
contentClassName="mobile-approval-action-grid"
|
||||
testId="mobile-approvals-sticky-actions"
|
||||
>
|
||||
<div className="mobile-sticky-action-grid mobile-approval-action-grid">
|
||||
<Button
|
||||
className="min-h-12 w-full"
|
||||
data-testid="mobile-approvals-approve"
|
||||
@@ -1269,13 +1274,14 @@ export function MobileApprovalsPage() {
|
||||
>
|
||||
退回
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</MobileStickyActionBar>
|
||||
|
||||
{canTransfer ? (
|
||||
<div className="mobile-list-card space-y-3 text-sm">
|
||||
<p className="font-medium text-zinc-800">转审</p>
|
||||
<Label htmlFor="mobile-approvals-transfer-to">目标审批人</Label>
|
||||
<select
|
||||
id="mobile-approvals-transfer-to"
|
||||
className="h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm"
|
||||
data-testid="mobile-approvals-transfer-to"
|
||||
onChange={(event) => setTransferToApproverId(event.target.value)}
|
||||
@@ -1289,18 +1295,34 @@ export function MobileApprovalsPage() {
|
||||
))}
|
||||
</select>
|
||||
|
||||
<Label htmlFor="mobile-approvals-transfer-reason">转审理由</Label>
|
||||
<Textarea
|
||||
id="mobile-approvals-transfer-reason"
|
||||
aria-describedby="mobile-approvals-transfer-help"
|
||||
data-testid="mobile-approvals-transfer-reason"
|
||||
onChange={(event) => setTransferReason(event.target.value)}
|
||||
placeholder="转审理由(至少10字)"
|
||||
rows={2}
|
||||
value={transferReason}
|
||||
/>
|
||||
<p
|
||||
id="mobile-approvals-transfer-help"
|
||||
className={transferReason.trim().length >= 10 && transferToApproverId ? "text-xs text-emerald-700" : "text-xs text-amber-700"}
|
||||
data-testid="mobile-approvals-transfer-disabled-reason"
|
||||
>
|
||||
{transferDisabledReason}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
aria-describedby="mobile-approvals-transfer-help"
|
||||
className="min-h-12 w-full"
|
||||
data-testid="mobile-approvals-transfer"
|
||||
disabled={isActionSubmitting || !selectedTask.allowedActions.includes("transfer")}
|
||||
disabled={
|
||||
isActionSubmitting ||
|
||||
!selectedTask.allowedActions.includes("transfer") ||
|
||||
!transferToApproverId ||
|
||||
transferReason.trim().length < 10
|
||||
}
|
||||
onClick={() => actionMutation.mutate("transfer")}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { fetchAssets, type AssetItem } from "../api/assets";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import {
|
||||
fetchInventoryChecks,
|
||||
recordInventoryItems,
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
type InventoryCheck,
|
||||
type InventoryCheckResult,
|
||||
} from "../api/inventoryChecks";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
|
||||
@@ -29,13 +28,7 @@ import {
|
||||
} from "../mobile/inventoryScanQueue";
|
||||
|
||||
function resolveErrorMessage(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "请求失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { uploadInvoice } from "../api/invoicePool";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { MobileIcon } from "../components/mobile/MobileIcon";
|
||||
@@ -70,9 +70,7 @@ export function MobileCapturePage() {
|
||||
} else {
|
||||
result.failed.push({
|
||||
file,
|
||||
message: isApiError(err)
|
||||
? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`
|
||||
: err instanceof Error ? err.message : "上传失败",
|
||||
message: resolveUserErrorMessage(err, "上传失败"),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -99,14 +97,7 @@ export function MobileCapturePage() {
|
||||
toast.success(`上传完成:成功 ${result.succeeded},待重试 ${result.queued}`);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (isApiError(err)) {
|
||||
const msg = `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`;
|
||||
setError(msg);
|
||||
toast.error(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = err instanceof Error ? err.message : "上传失败";
|
||||
const msg = resolveUserErrorMessage(err, "上传失败");
|
||||
setError(msg);
|
||||
toast.error(msg);
|
||||
},
|
||||
@@ -138,7 +129,7 @@ export function MobileCapturePage() {
|
||||
toast.success(`同步完成:成功 ${result.succeeded},失败 ${result.failed}`);
|
||||
},
|
||||
onError: (err) => {
|
||||
const msg = err instanceof Error ? err.message : "队列同步失败";
|
||||
const msg = resolveUserErrorMessage(err, "队列同步失败");
|
||||
setError(msg);
|
||||
toast.error(msg);
|
||||
},
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { updateNotificationPreferences } from "../api/auth";
|
||||
import { fetchEmployeeProfile } from "../api/employees";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission, hasBusinessApproverRole } from "../auth/permissions";
|
||||
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
|
||||
@@ -16,13 +15,7 @@ import { Button } from "../components/ui/button";
|
||||
import { MOBILE_PURCHASE_PERMISSIONS } from "../features/mobile/navigation";
|
||||
|
||||
function resolveErrorMessage(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "请求失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function MobileActionLink({
|
||||
@@ -115,7 +108,7 @@ export function MobileMePage() {
|
||||
<MobileActionLink to="/mobile/leave" label="请假记录" icon="event_available" />
|
||||
<MobileActionLink to="/mobile/reimbursements" label="报销" icon="request_quote" />
|
||||
{canAccessPurchase ? <MobileActionLink to="/mobile/purchase-requests" label="采购" icon="shopping_cart" /> : null}
|
||||
<MobileActionLink to="/mobile/approvals" label={canAccessApprovals ? "待我处理" : "进度"} icon="fact_check" />
|
||||
<MobileActionLink to="/mobile/approvals" label={canAccessApprovals ? "待我审批" : "进度"} icon="fact_check" />
|
||||
{canAccessAssets ? <MobileActionLink to="/mobile/assets/scan" label="资产扫码" icon="inventory_2" /> : null}
|
||||
</div>
|
||||
</SectionPanel>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { fetchNotifications, markAllNotificationsRead, markNotificationRead, type NotificationItem } from "../api/notifications";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { Pagination } from "../components/common/Pagination";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
@@ -27,7 +27,7 @@ const NOTIFICATION_TYPE_OPTIONS = [
|
||||
|
||||
function resolveNotificationError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
return resolveUserErrorMessage(error);
|
||||
}
|
||||
return "加载通知失败";
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { fetchNotifications, markNotificationRead, type NotificationItem } from "../api/notifications";
|
||||
import { fetchPortalHome } from "../api/portal";
|
||||
import { isApiError } from "../api/types";
|
||||
import { fetchWorkflowTasks, fetchWorkflowTaskSummary } from "../api/workflow";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission, hasBusinessApproverRole, hasFinanceRole } from "../auth/permissions";
|
||||
@@ -34,13 +33,7 @@ type MobileAction = {
|
||||
};
|
||||
|
||||
function resolveErrorMessage(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "请求失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function formatDateTime(value: string | null) {
|
||||
@@ -114,13 +107,6 @@ export function MobilePortalPage() {
|
||||
const canAccessLeave = hasAnyPermission(permissions, [...MOBILE_LEAVE_PERMISSIONS]);
|
||||
const canAccessPurchase = hasAnyPermission(permissions, [...MOBILE_PURCHASE_PERMISSIONS]);
|
||||
const canAccessReimbursements = hasAnyPermission(permissions, [...MOBILE_REIMBURSEMENT_PERMISSIONS]);
|
||||
const canAccessWecomApprovalArchive = hasAnyPermission(permissions, [
|
||||
"leave.view_own",
|
||||
"attendance.view_own",
|
||||
"leave.view_all",
|
||||
"attendance.view_all",
|
||||
"*",
|
||||
]);
|
||||
const canCreateExpense = hasAnyPermission(permissions, ["expense.create"]);
|
||||
const currentMobileSource = `${location.pathname}${location.search}`;
|
||||
|
||||
@@ -259,7 +245,7 @@ export function MobilePortalPage() {
|
||||
return (
|
||||
<PageScaffold className="mobile-page-stack">
|
||||
<PageHeader
|
||||
title="今天待办"
|
||||
title="今日待处理"
|
||||
meta={<span className="badge-neutral">移动工作台</span>}
|
||||
/>
|
||||
|
||||
@@ -267,8 +253,8 @@ export function MobilePortalPage() {
|
||||
<section className="mobile-card mobile-focus-card mobile-page-stack" data-testid="mobile-pending-summary">
|
||||
<div className="mobile-section-header mobile-section-header--top">
|
||||
<div className="min-w-0">
|
||||
<div className="mobile-section-title">待处理</div>
|
||||
<div className="mobile-list-card__subtitle mt-1">待我处理</div>
|
||||
<div className="mobile-section-title">待我审批</div>
|
||||
<div className="mobile-list-card__subtitle mt-1">今日待处理</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-3xl font-semibold leading-none text-zinc-900" data-testid="mobile-pending-summary-count">
|
||||
@@ -307,7 +293,7 @@ export function MobilePortalPage() {
|
||||
</div>
|
||||
) : null}
|
||||
<Button asChild className="mt-3 w-full" size="sm" type="button" variant="secondary">
|
||||
<Link to="/mobile/approvals">查看全部待办</Link>
|
||||
<Link to="/mobile/approvals">查看全部审批</Link>
|
||||
</Button>
|
||||
</section>
|
||||
) : null}
|
||||
@@ -327,10 +313,6 @@ export function MobilePortalPage() {
|
||||
<div className="text-xs text-zinc-500">异常</div>
|
||||
<div className="mt-1 text-xl font-semibold text-zinc-900">{financeExceptionCount}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-zinc-50 px-3 py-2">
|
||||
<div className="text-xs text-zinc-500">处理</div>
|
||||
<div className="mt-1 text-xl font-semibold text-zinc-900">OA</div>
|
||||
</div>
|
||||
</div>
|
||||
{paymentBatchEnabled ? (
|
||||
<Button asChild className="mt-3 w-full" size="sm" type="button" variant="secondary">
|
||||
@@ -353,26 +335,6 @@ export function MobilePortalPage() {
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="mobile-card mobile-page-stack" data-testid="mobile-attendance-summary-card">
|
||||
<div className="mobile-section-header">
|
||||
<div className="mobile-section-title">考勤</div>
|
||||
<span className="badge-neutral">企业微信打卡</span>
|
||||
</div>
|
||||
<div className="text-sm leading-6 text-zinc-600">
|
||||
日常打卡、请假和补卡在企业微信完成;OA 保留已归档历史和考勤汇总。
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2">
|
||||
{canAccessWecomApprovalArchive ? (
|
||||
<Button asChild className="w-full" size="sm" type="button" variant="secondary">
|
||||
<Link to="/mobile/wecom-approval-archive">查看企微审批历史</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
<Button asChild className="w-full" size="sm" type="button" variant="secondary">
|
||||
<Link to="/mobile/attendance">查看考勤汇总</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mobile-card mobile-page-stack" data-testid="mobile-notifications">
|
||||
<div className="mobile-section-header">
|
||||
<div className="mobile-section-title">通知</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { fetchWecomConfig, startWecomBind, unbindWecom, updateNotificationPreferences } from "../api/auth";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
@@ -51,13 +51,7 @@ function roleLabel(roleCode: string) {
|
||||
}
|
||||
|
||||
function resolveProfileError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${error.message}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "请求失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function resolveBinding(
|
||||
|
||||
@@ -49,6 +49,7 @@ import { isTravelReimbursement } from "../lib/reimbursementTravel";
|
||||
import { ReimbursementOperationalHints } from "../components/reimbursements/ReimbursementOperationalHints";
|
||||
import { ProcessingProgress } from "../components/patterns/ProcessingProgress";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { MOBILE_STICKY_ACTION_PAGE_CLASS, MobileStickyActionBar } from "../components/patterns/MobileStickyActionBar";
|
||||
import { Button } from "../components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -881,10 +882,7 @@ export function MobileReimbursementDetailPage() {
|
||||
<PageScaffold
|
||||
className={cn(
|
||||
"space-y-4",
|
||||
showStickyActions ? "mobile-fixed-action-page" : "",
|
||||
showStickyActions
|
||||
? "pb-[calc(var(--mobile-sticky-action-height,78px)+env(safe-area-inset-bottom,0px)+1.25rem)]"
|
||||
: "pb-6",
|
||||
showStickyActions ? MOBILE_STICKY_ACTION_PAGE_CLASS : "pb-6",
|
||||
)}
|
||||
data-testid="mobile-reimbursement-detail"
|
||||
>
|
||||
@@ -1333,11 +1331,7 @@ export function MobileReimbursementDetailPage() {
|
||||
) : null}
|
||||
|
||||
{showStickyActions ? (
|
||||
<div
|
||||
className="mobile-sticky-action-surface fixed inset-x-0 bottom-0 z-50 border-t border-zinc-200 bg-white/95 shadow-[0_-10px_24px_rgba(15,23,42,0.10)] backdrop-blur"
|
||||
data-testid="mobile-reimbursement-sticky-actions"
|
||||
>
|
||||
<div className="mobile-sticky-action-grid">
|
||||
<MobileStickyActionBar testId="mobile-reimbursement-sticky-actions">
|
||||
{showSubmitAction ? (
|
||||
<Button
|
||||
className="min-h-11 min-w-0 px-2 text-sm whitespace-nowrap"
|
||||
@@ -1349,8 +1343,7 @@ export function MobileReimbursementDetailPage() {
|
||||
{submitMutation.isPending ? "提交中..." : "提交审批"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</MobileStickyActionBar>
|
||||
) : null}
|
||||
|
||||
<Dialog open={isInvoiceDialogOpen} onOpenChange={closeInvoiceDialog}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import {
|
||||
createNotificationChannel,
|
||||
fetchNotificationDeliveries,
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
type NotificationChannelTemplate,
|
||||
type NotificationChannelWriteConfig,
|
||||
} from "../api/notificationChannels";
|
||||
import { isApiError } from "../api/types";
|
||||
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -75,7 +74,7 @@ const ROLE_OPTIONS = [
|
||||
] as const;
|
||||
|
||||
const DOMAIN_SCOPE_OPTIONS = [
|
||||
{ value: "workflow", label: "待我处理" },
|
||||
{ value: "workflow", label: "待我审批" },
|
||||
{ value: "reimbursement", label: "报销" },
|
||||
{ value: "leave", label: "请假" },
|
||||
{ value: "procurement", label: "采购" },
|
||||
@@ -103,13 +102,7 @@ const NOTIFICATION_TYPE_OPTIONS = [
|
||||
] as const;
|
||||
|
||||
function resolveError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "请求失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function buildInitialForm(): ChannelFormState {
|
||||
|
||||
@@ -2,8 +2,6 @@ import { Link, useSearchParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { fetchNotifications, markAllNotificationsRead, markNotificationRead } from "../api/notifications";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import { Pagination } from "../components/common/Pagination";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -150,13 +148,7 @@ export function NotificationsPage() {
|
||||
|
||||
{listQuery.isError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{isApiError(listQuery.error)
|
||||
? `${listQuery.error.code}: ${mapApiErrorMessage(
|
||||
listQuery.error.code,
|
||||
listQuery.error.details,
|
||||
listQuery.error.message,
|
||||
)}`
|
||||
: "加载通知失败"}
|
||||
{resolveErrorMessage(listQuery.error, "加载通知失败")}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
type PaymentAccount,
|
||||
} from "../api/paymentAccounts";
|
||||
import { fetchLegalEntities } from "../api/masterData";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission, hasPermission } from "../auth/permissions";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
@@ -27,7 +27,7 @@ import { Label } from "../components/ui/label";
|
||||
import { Textarea } from "../components/ui/textarea";
|
||||
|
||||
function errorText(error: unknown) {
|
||||
return isApiError(error) ? `${error.code}: ${error.message}` : error instanceof Error ? error.message : "操作失败";
|
||||
return resolveUserErrorMessage(error, "操作失败");
|
||||
}
|
||||
|
||||
export function PaymentAccountsPage() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
@@ -20,8 +21,6 @@ import {
|
||||
type PaymentBatchReceiptAttachment,
|
||||
type PaymentChannel,
|
||||
} from "../api/paymentBatches";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { HeaderActionGroup, ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
@@ -109,11 +108,7 @@ function formatFileSize(value: number) {
|
||||
}
|
||||
|
||||
function resolveError(error: unknown) {
|
||||
return isApiError(error)
|
||||
? `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: "操作失败";
|
||||
return resolveUserErrorMessage(error, "操作失败");
|
||||
}
|
||||
|
||||
function paymentOrderBlockerLabel(blocker: { code: string; entityCode?: string }) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -14,8 +15,6 @@ import {
|
||||
type SalaryItemType,
|
||||
} from "../api/payroll";
|
||||
import { exportPayslips } from "../api/exports";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -58,10 +57,7 @@ function currentMonthString() {
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown, fallback: string) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
function moneyText(value: number) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -22,8 +23,6 @@ import {
|
||||
} from "../api/employees";
|
||||
import { exportEmployees } from "../api/exports";
|
||||
import { fetchDepartments } from "../api/departments";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
|
||||
@@ -55,11 +54,7 @@ const EMPLOYMENT_STATUS_OPTIONS: Array<{ value: EmployeeEmploymentStatus; label:
|
||||
];
|
||||
|
||||
function mapApiMessage(error: unknown, fallback: string) {
|
||||
return isApiError(error)
|
||||
? `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
function employmentStatusLabel(status: EmployeeEmploymentStatus | null) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -8,8 +9,6 @@ import {
|
||||
patchPerformanceCycle,
|
||||
type PerformanceReviewCycleType,
|
||||
} from "../api/performance";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -48,10 +47,7 @@ function buildEmptyCycleForm(): CycleFormState {
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown, fallback: string) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
function cycleTypeLabel(value: PerformanceReviewCycleType) {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { fetchPortalHome, type PortalHomePayload } from "../api/portal";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission, isManagerRole } from "../auth/permissions";
|
||||
import { ErrorBoundary } from "../components/common/ErrorBoundary";
|
||||
import { WelcomeDialog } from "../components/onboarding/WelcomeDialog";
|
||||
import { WelcomeDialog, type WelcomeLinkItem } from "../components/onboarding/WelcomeDialog";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -15,6 +13,7 @@ import {
|
||||
clearWelcomeDialogFlag,
|
||||
shouldShowWelcomeDialog,
|
||||
} from "../lib/onboardingGuide";
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
|
||||
type RoleCapabilities = NonNullable<PortalHomePayload["roleCapabilities"]>;
|
||||
|
||||
@@ -50,7 +49,7 @@ const LEAVE_PORTAL_PERMISSIONS = ["leave.view_own", "leave.view_all", "leave.cre
|
||||
const EXPENSE_PORTAL_PERMISSIONS = ["expense.view_own", "expense.view_all", "expense.create", "expense.proxy_manage"];
|
||||
const PROCUREMENT_PORTAL_PERMISSIONS = ["procurement.view_own", "procurement.view_all", "procurement.create", "procurement.submit"];
|
||||
const TRAVEL_PORTAL_PERMISSIONS = ["travel.view_own", "travel.view_all", "travel.create", "travel.submit"];
|
||||
const APPROVAL_PORTAL_PERMISSIONS = ["expense.approve", "leave.approve", "procurement.approve", "travel.approve"];
|
||||
const APPROVAL_PORTAL_PERMISSIONS = ["expense.approve", "leave.approve", "attendance.approve", "procurement.approve", "travel.approve"];
|
||||
const FINANCE_PORTAL_PERMISSIONS = ["payment.batch.manage", "risk.review.manage", "expense.finance.pay"];
|
||||
const SYSTEM_PORTAL_PERMISSIONS = ["system.users", "system.settings", "system.audit.view", "notification.channel.manage", "payment.account.read", "payment.account.manage"];
|
||||
const CONTRACT_PORTAL_PERMISSIONS = ["project.view_all", "project.manage", "system.settings"];
|
||||
@@ -112,9 +111,9 @@ function buildTodayActionItems(payload: PortalHomePayload, roleCapabilities: Rol
|
||||
if (payload.pendingTasks.count > 0) {
|
||||
items.push({
|
||||
key: "workflow-pending",
|
||||
title: `待我处理 ${payload.pendingTasks.count} 条`,
|
||||
title: `待我审批 ${payload.pendingTasks.count} 条`,
|
||||
reason: "审批、确认或流程处理正在等你。",
|
||||
meta: "流程待办",
|
||||
meta: "审批任务",
|
||||
href: payload.pendingTasks.href,
|
||||
actionLabel: "去处理",
|
||||
severity: "warning",
|
||||
@@ -129,13 +128,13 @@ function buildTodayActionItems(payload: PortalHomePayload, roleCapabilities: Rol
|
||||
const highPriorityCount = payload.todoSummary?.highPriorityCount ?? 0;
|
||||
items.push({
|
||||
key: "todo-open",
|
||||
title: `待办事项 ${openTodoCount} 条`,
|
||||
title: `我的事项 ${openTodoCount} 条`,
|
||||
reason: highPriorityCount > 0
|
||||
? `其中 ${highPriorityCount} 条是高优先级,需要先确认。`
|
||||
: "这些事项已分配给你,需要继续推进。",
|
||||
meta: highPriorityCount > 0 ? "高优先级" : "个人待办",
|
||||
meta: highPriorityCount > 0 ? "高优先级" : "个人事项",
|
||||
href: payload.todoSummary?.href ?? "/todos",
|
||||
actionLabel: "查看待办",
|
||||
actionLabel: "查看事项",
|
||||
severity: highPriorityCount > 0 ? "high" : "normal",
|
||||
icon: "check_circle",
|
||||
priority: 90,
|
||||
@@ -497,6 +496,39 @@ function buildQuickEntries(permissions: string[], roleCapabilities: RoleCapabili
|
||||
return entries.slice(0, 6);
|
||||
}
|
||||
|
||||
function buildWelcomeLinks(permissions: string[], roleCapabilities: RoleCapabilities): WelcomeLinkItem[] {
|
||||
const links: WelcomeLinkItem[] = [];
|
||||
|
||||
if (roleCapabilities.canApproveTasks && hasAnyPermission(permissions, APPROVAL_PORTAL_PERMISSIONS)) {
|
||||
links.push({
|
||||
icon: "fact_check",
|
||||
label: "待我审批",
|
||||
description: "查看今天需要审批的流程任务",
|
||||
to: "/workflow/tasks",
|
||||
});
|
||||
}
|
||||
|
||||
if (!roleCapabilities.isAdminOnly && hasAnyPermission(permissions, EXPENSE_PORTAL_PERMISSIONS)) {
|
||||
links.push({
|
||||
icon: "request_quote",
|
||||
label: "报销",
|
||||
description: "录入票据和报销说明,查看处理进度",
|
||||
to: "/reimbursements",
|
||||
});
|
||||
}
|
||||
|
||||
if (!roleCapabilities.isAdminOnly && hasAnyPermission(permissions, LEAVE_PORTAL_PERMISSIONS)) {
|
||||
links.push({
|
||||
icon: "event_available",
|
||||
label: "请假记录",
|
||||
description: "日常请假走企业微信,OA 查看记录和状态",
|
||||
to: "/leave-requests",
|
||||
});
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
function sumWorkItemCount(items: WorkItem[]) {
|
||||
return items.reduce((total, item) => total + Math.max(1, item.count), 0);
|
||||
}
|
||||
@@ -538,7 +570,10 @@ function WorkItemRow({ item }: { item: WorkItem }) {
|
||||
: "portal-task-row";
|
||||
|
||||
return (
|
||||
<Link to={item.href} className={severityClass}>
|
||||
<Link
|
||||
to={item.href}
|
||||
className={`${severityClass} focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2`}
|
||||
>
|
||||
<span className="portal-task-row__icon" aria-hidden="true">
|
||||
<span className="material-symbols-outlined text-[18px]">{item.icon}</span>
|
||||
</span>
|
||||
@@ -570,7 +605,11 @@ function QuickEntryPanel({ entries }: { entries: QuickEntry[] }) {
|
||||
<SectionPanel title="常用入口">
|
||||
<div className="portal-quick-entry-grid" data-testid="portal-quick-entries">
|
||||
{entries.map((entry) => (
|
||||
<Link className="portal-quick-entry" key={entry.key} to={entry.href}>
|
||||
<Link
|
||||
className="portal-quick-entry focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2"
|
||||
key={entry.key}
|
||||
to={entry.href}
|
||||
>
|
||||
<span className="portal-quick-entry__icon" aria-hidden="true">
|
||||
<span className="material-symbols-outlined text-[18px]">{entry.icon}</span>
|
||||
</span>
|
||||
@@ -653,10 +692,32 @@ export function PortalHomePage() {
|
||||
}
|
||||
|
||||
if (portalQuery.isError || !portalQuery.data) {
|
||||
const message = isApiError(portalQuery.error)
|
||||
? `${portalQuery.error.code}: ${mapApiErrorMessage(portalQuery.error.code, portalQuery.error.details, portalQuery.error.message)}`
|
||||
: "工作台加载失败";
|
||||
return <div className="card-std p-5 text-sm text-red-600">{message}</div>;
|
||||
const message = resolveUserErrorMessage(portalQuery.error, "工作台加载失败,请稍后重试");
|
||||
return (
|
||||
<PageScaffold>
|
||||
<section
|
||||
aria-busy={portalQuery.isFetching}
|
||||
aria-live="polite"
|
||||
className="card-std space-y-3 p-5"
|
||||
data-testid="portal-load-error"
|
||||
role="alert"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-red-700">{message}</p>
|
||||
<p className="mt-1 text-sm text-zinc-600">工作台内容暂时不可用,重新加载不会清除当前登录状态。</p>
|
||||
</div>
|
||||
<Button
|
||||
data-testid="portal-load-retry"
|
||||
disabled={portalQuery.isFetching}
|
||||
onClick={() => void portalQuery.refetch()}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
>
|
||||
{portalQuery.isFetching ? "重新加载中..." : "重新加载"}
|
||||
</Button>
|
||||
</section>
|
||||
</PageScaffold>
|
||||
);
|
||||
}
|
||||
|
||||
const payload = portalQuery.data;
|
||||
@@ -669,6 +730,7 @@ export function PortalHomePage() {
|
||||
isManagerRole(permissions);
|
||||
const todayActionItems = buildTodayActionItems(payload, roleCapabilities);
|
||||
const quickEntries = buildQuickEntries(permissions, roleCapabilities);
|
||||
const welcomeLinks = buildWelcomeLinks(permissions, roleCapabilities);
|
||||
const progressItems = buildProgressItems(payload);
|
||||
const teamBlockers = isManagerView ? buildTeamBlockers(payload, roleCapabilities) : [];
|
||||
const managementReminders = buildManagementReminders(payload, roleCapabilities, canSetupWorkspace);
|
||||
@@ -686,7 +748,11 @@ export function PortalHomePage() {
|
||||
return (
|
||||
<PageScaffold>
|
||||
<ErrorBoundary fallback={null}>
|
||||
<WelcomeDialog open={showWelcome} onClose={handleCloseWelcome} />
|
||||
<WelcomeDialog
|
||||
links={welcomeLinks}
|
||||
open={showWelcome && welcomeLinks.length > 0}
|
||||
onClose={handleCloseWelcome}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
|
||||
<SummaryStrip
|
||||
@@ -697,10 +763,10 @@ export function PortalHomePage() {
|
||||
reminderCount={reminderCount}
|
||||
/>
|
||||
|
||||
<QuickEntryPanel entries={quickEntries} />
|
||||
|
||||
<TodayActionPanel items={todayActionItems} emptyActions={emptyActions} />
|
||||
|
||||
<QuickEntryPanel entries={quickEntries} />
|
||||
|
||||
<div className="portal-task-secondary-grid">
|
||||
<ConditionalWorkPanel title="我的进度" items={progressItems} />
|
||||
<ConditionalWorkPanel title="团队卡点" items={teamBlockers} />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { fetchWecomConfig, startWecomBind, unbindWecom, updateNotificationPreferences } from "../api/auth";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
@@ -58,13 +58,7 @@ function roleLabel(roleCode: string) {
|
||||
}
|
||||
|
||||
function resolveProfileError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${error.message}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "请求失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试。");
|
||||
}
|
||||
|
||||
export function ProfilePage() {
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { fetchEmployees } from "../api/employees";
|
||||
import { archiveProject, createConfidentialDocumentRef, createProjectCommunicationLog, createProjectDelivery, createProjectMember, createProjectMilestone, createProjectRisk, createProjectTask, fetchProject, fetchProjectClosureChecklist, fetchProjectCommunicationLogs, fetchProjectDeliveries, fetchProjectDocumentRefs, fetchProjectExpensesSummary, fetchProjectMembers, fetchProjectMilestones, fetchProjectPurchaseSummary, fetchProjectRisks, fetchProjectTasks, fetchProjectTravelSummary, fetchProjectWeeklyReports, patchProject, patchProjectClosureChecklistItem, patchProjectCommunicationLog, patchProjectMember, patchProjectMilestone, patchProjectRisk, patchProjectTask, type ConfidentialityLevel, type DocumentStorageType, type ProjectCommunicationChannel, type ProjectClosureChecklistStatus, type ProjectDeliveryRecordType, type ProjectDetail, type ProjectMilestoneStatus, type ProjectRiskLevel, type ProjectStatus, type ProjectTaskPriority, type ProjectTaskStatus } from "../api/projects";
|
||||
import { fetchContractsRegistry } from "../api/contractsRegistry";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
|
||||
import { DetailSideMeta } from "../components/patterns/DetailSideMeta";
|
||||
import { EntityOverviewHero } from "../components/patterns/EntityOverviewHero";
|
||||
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";
|
||||
@@ -89,10 +88,7 @@ function riskStatusLabel(value: string) {
|
||||
}
|
||||
|
||||
function resolveError(error: unknown, fallback = "操作失败") {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
function confidentialityLabel(value: ConfidentialityLevel) {
|
||||
@@ -1094,7 +1090,7 @@ export function ProjectDetailPage() {
|
||||
<div>交付记录:{counts?.deliveries ?? 0}</div>
|
||||
<div>资料引用:{counts?.documentRefs ?? 0}</div>
|
||||
<div className="rounded-lg border border-zinc-200 p-3 text-zinc-600">
|
||||
项目相关的出差、报销、待办、周报都可以在当前项目下汇总查看;涉密正文与附件仍不进 OA。
|
||||
项目相关的出差、报销、事项、周报都可以在当前项目下汇总查看;涉密正文与附件仍不进 OA。
|
||||
</div>
|
||||
</DetailSideMeta>
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMemo, useState } from "react";
|
||||
import { fetchEmployees } from "../api/employees";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import {
|
||||
createProject,
|
||||
fetchProjects,
|
||||
type ProjectListItem,
|
||||
type ProjectStatus,
|
||||
} from "../api/projects";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission } from "../auth/permissions";
|
||||
import { DateTextInput } from "../components/common/DateTextInput";
|
||||
@@ -45,10 +44,7 @@ const STATUS_OPTIONS: Array<{ value: "all" | ProjectStatus; label: string }> = [
|
||||
];
|
||||
|
||||
function resolveError(error: unknown, fallback = "操作失败") {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
function summaryLine(item: ProjectListItem) {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import {
|
||||
archivePurchaseOrder,
|
||||
createPurchaseRequest,
|
||||
@@ -95,13 +94,7 @@ function purchaseDraftSignature(value: {
|
||||
}
|
||||
|
||||
function resolveError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return mapApiErrorMessage(error.code, error.details, error.message);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "请求失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function sanitizePurchaseAttachmentDownloadName(value?: string) {
|
||||
@@ -1430,7 +1423,7 @@ export function PurchaseRequestsPage() {
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-4 text-sm text-zinc-600">
|
||||
<div>预算池:{detail.budgetPool?.name ?? "未匹配"}</div>
|
||||
<div>关联项目:{detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联项目"}</div>
|
||||
<div>待办节点:{detail.pendingWorkflow?.task?.stepName ?? "-"}</div>
|
||||
<div>当前审批节点:{detail.pendingWorkflow?.task?.stepName ?? "-"}</div>
|
||||
<div>总金额:<span className="font-semibold text-zinc-900">{totalAmountPreview}</span></div>
|
||||
<div
|
||||
className={`mt-2 rounded-md border px-2 py-1 ${
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { fetchDepartments } from "../api/departments";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import {
|
||||
fetchReimbursementApprovalBottleneck,
|
||||
fetchReimbursementStatsSummary,
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
fetchReimbursementTopExpenseTypes,
|
||||
type ReimbursementStatsQuery,
|
||||
} from "../api/reimbursements";
|
||||
import { isApiError } from "../api/types";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -26,13 +25,7 @@ function defaultDateFromString() {
|
||||
}
|
||||
|
||||
function resolveErrorMessage(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "加载失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "加载失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function formatCurrency(value: string | number | null | undefined) {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ArrowLeft, ChevronRight } from "lucide-react";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { fetchExpenseTypes } from "../api/expenseTypes";
|
||||
import type { ExpenseStandardCheck, PrecheckResponse } from "../api/feeControl";
|
||||
import { fetchProjects } from "../api/projects";
|
||||
@@ -100,15 +99,7 @@ type ItemEditorState = { mode: "create" } | { mode: "edit"; itemId: string } | n
|
||||
type SelectedInvoiceMap = Record<string, string>;
|
||||
|
||||
function buildActionErrorMessage(error: unknown, fallbackMessage: string) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return fallbackMessage;
|
||||
return resolveUserErrorMessage(error, fallbackMessage);
|
||||
}
|
||||
|
||||
export function ReimbursementDetailPage() {
|
||||
@@ -247,21 +238,9 @@ export function ReimbursementDetailPage() {
|
||||
toast.success(actionMessages[params.action] ?? "操作成功");
|
||||
},
|
||||
onError: (error) => {
|
||||
if (isApiError(error)) {
|
||||
const msg = `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
setActionError(msg);
|
||||
toast.error(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
setActionError(error.message);
|
||||
toast.error(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
setActionError("操作失败,请稍后重试");
|
||||
toast.error("操作失败,请稍后重试");
|
||||
const message = buildActionErrorMessage(error, "操作失败,请稍后重试");
|
||||
setActionError(message);
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -315,21 +294,9 @@ export function ReimbursementDetailPage() {
|
||||
toast.success("明细已保存");
|
||||
},
|
||||
onError: (error) => {
|
||||
if (isApiError(error)) {
|
||||
const msg = `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
setItemError(msg);
|
||||
toast.error(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
setItemError(error.message);
|
||||
toast.error(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
setItemError("保存明细失败,请稍后重试");
|
||||
toast.error("保存明细失败,请稍后重试");
|
||||
const message = buildActionErrorMessage(error, "保存明细失败,请稍后重试");
|
||||
setItemError(message);
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -351,21 +318,9 @@ export function ReimbursementDetailPage() {
|
||||
toast.success("明细已删除");
|
||||
},
|
||||
onError: (error) => {
|
||||
if (isApiError(error)) {
|
||||
const msg = `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
setItemError(msg);
|
||||
toast.error(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
setItemError(error.message);
|
||||
toast.error(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
setItemError("删除明细失败,请稍后重试");
|
||||
toast.error("删除明细失败,请稍后重试");
|
||||
const message = buildActionErrorMessage(error, "删除明细失败,请稍后重试");
|
||||
setItemError(message);
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -401,11 +356,7 @@ export function ReimbursementDetailPage() {
|
||||
toast.success("已更新关联项目");
|
||||
},
|
||||
onError: (error) => {
|
||||
const message = isApiError(error)
|
||||
? `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: "关联项目失败";
|
||||
const message = buildActionErrorMessage(error, "关联项目失败");
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
@@ -756,13 +707,7 @@ export function ReimbursementDetailPage() {
|
||||
}
|
||||
|
||||
if (detailQuery.isError || !detailQuery.data) {
|
||||
const message = isApiError(detailQuery.error)
|
||||
? `${detailQuery.error.code}: ${mapApiErrorMessage(
|
||||
detailQuery.error.code,
|
||||
detailQuery.error.details,
|
||||
detailQuery.error.message,
|
||||
)}`
|
||||
: "加载报销详情失败";
|
||||
const message = resolveUserErrorMessage(detailQuery.error, "加载报销详情失败");
|
||||
|
||||
return (
|
||||
<div className="card-std space-y-3 p-5">
|
||||
@@ -893,21 +838,9 @@ export function ReimbursementDetailPage() {
|
||||
|
||||
mutation.mutate({ action: "submit" });
|
||||
} catch (error) {
|
||||
if (isApiError(error)) {
|
||||
const message = `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
setActionError(message);
|
||||
toast.error(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
setActionError(error.message);
|
||||
toast.error(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
setActionError("提交前校验失败,请稍后重试");
|
||||
toast.error("提交前校验失败,请稍后重试");
|
||||
const message = buildActionErrorMessage(error, "提交前校验失败,请稍后重试");
|
||||
setActionError(message);
|
||||
toast.error(message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { fetchReimbursementDetail } from "../api/reimbursements";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { CardSkeleton } from "../components/common/CardSkeleton";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -65,13 +64,7 @@ export function ReimbursementPrintPage() {
|
||||
}
|
||||
|
||||
if (detailQuery.isError || !detailQuery.data) {
|
||||
const message = isApiError(detailQuery.error)
|
||||
? `${detailQuery.error.code}: ${mapApiErrorMessage(
|
||||
detailQuery.error.code,
|
||||
detailQuery.error.details,
|
||||
detailQuery.error.message,
|
||||
)}`
|
||||
: "加载打印视图失败";
|
||||
const message = resolveUserErrorMessage(detailQuery.error, "加载打印视图失败");
|
||||
|
||||
return (
|
||||
<div className="card-std space-y-3 p-6">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { fetchExpenseTypes } from "../api/expenseTypes";
|
||||
import {
|
||||
createReimbursementTemplate,
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
type ReimbursementTemplate,
|
||||
type ReimbursementTemplateItem,
|
||||
} from "../api/reimbursementTemplates";
|
||||
import { isApiError } from "../api/types";
|
||||
import { ConfirmDialog } from "../components/common/ConfirmDialog";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -35,13 +34,7 @@ type FormState = {
|
||||
};
|
||||
|
||||
function resolveErrorMessage(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "请求失败,请稍后重试";
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试");
|
||||
}
|
||||
|
||||
function emptyForm(expenseTypeId = ""): FormState {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link, useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { fetchMyDelegations } from "../api/delegations";
|
||||
import { exportReimbursements } from "../api/exports";
|
||||
import { fetchReimbursementTemplates } from "../api/reimbursementTemplates";
|
||||
@@ -61,10 +60,7 @@ function formatCurrency(value: number) {
|
||||
}
|
||||
|
||||
function resolveErrorMessage(error: unknown, fallback: string) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
function resolveEmptyStateCopy(status: ReimbursementStatus) {
|
||||
@@ -344,14 +340,9 @@ export function ReimbursementsPage() {
|
||||
}
|
||||
/>
|
||||
|
||||
{createMutation.error && isApiError(createMutation.error) ? (
|
||||
{createMutation.error ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{createMutation.error.code}:
|
||||
{mapApiErrorMessage(
|
||||
createMutation.error.code,
|
||||
createMutation.error.details,
|
||||
createMutation.error.message,
|
||||
)}
|
||||
{resolveUserErrorMessage(createMutation.error, "创建报销单失败")}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -500,13 +491,7 @@ export function ReimbursementsPage() {
|
||||
|
||||
{listQuery.isError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{isApiError(listQuery.error)
|
||||
? `${listQuery.error.code}: ${mapApiErrorMessage(
|
||||
listQuery.error.code,
|
||||
listQuery.error.details,
|
||||
listQuery.error.message,
|
||||
)}`
|
||||
: "加载报销列表失败"}
|
||||
{resolveUserErrorMessage(listQuery.error, "加载报销列表失败")}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { fetchDepartments } from "../api/departments";
|
||||
@@ -15,8 +16,6 @@ import {
|
||||
type TeamPresenceHistoryItem,
|
||||
upsertMyPresence,
|
||||
} from "../api/presence";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
|
||||
@@ -79,15 +78,7 @@ function offsetDateString(offsetDays: number) {
|
||||
}
|
||||
|
||||
function resolveErrorMessage(error: unknown, fallback = "加载失败") {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
function resolveEditableStatus(item: TeamPresenceHistoryItem | null): EditablePresenceStatus {
|
||||
|
||||
+36
-29
@@ -1,3 +1,4 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { cancelTodo, completeTodo, createTodo, fetchTodos } from "../api/todos";
|
||||
@@ -8,19 +9,22 @@ import { MetricCard } from "../components/patterns/MetricCard";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Label } from "../components/ui/label";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
import { toast } from "sonner";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { TEST_IDS } from "../lib/testIds";
|
||||
|
||||
function errorMessage(error: unknown, fallback: string) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
const TODO_STATUS_LABELS = {
|
||||
todo: "待处理",
|
||||
doing: "进行中",
|
||||
done: "已完成",
|
||||
cancelled: "已取消",
|
||||
} as const;
|
||||
|
||||
export function TodosPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const [title, setTitle] = useState("");
|
||||
@@ -60,9 +64,9 @@ export function TodosPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ["todos"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["portal-home"] }),
|
||||
]);
|
||||
toast.success("待办已创建");
|
||||
toast.success("事项已创建");
|
||||
},
|
||||
onError: (error) => toast.error(errorMessage(error, "创建待办失败")),
|
||||
onError: (error) => toast.error(errorMessage(error, "创建事项失败")),
|
||||
});
|
||||
|
||||
const completeMutation = useMutation({
|
||||
@@ -72,9 +76,9 @@ export function TodosPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ["todos"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["portal-home"] }),
|
||||
]);
|
||||
toast.success("待办已完成");
|
||||
toast.success("事项已完成");
|
||||
},
|
||||
onError: (error) => toast.error(errorMessage(error, "完成待办失败")),
|
||||
onError: (error) => toast.error(errorMessage(error, "完成事项失败")),
|
||||
});
|
||||
|
||||
const cancelMutation = useMutation({
|
||||
@@ -84,17 +88,17 @@ export function TodosPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ["todos"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["portal-home"] }),
|
||||
]);
|
||||
toast.success("待办已取消");
|
||||
toast.success("事项已取消");
|
||||
},
|
||||
onError: (error) => toast.error(errorMessage(error, "取消待办失败")),
|
||||
onError: (error) => toast.error(errorMessage(error, "取消事项失败")),
|
||||
});
|
||||
|
||||
return (
|
||||
<ListWorkspace>
|
||||
<PageHeader title="待办" subtitle="员工维护个人待办,老板看高优先级和未完成项。支持直接挂到项目。" />
|
||||
<PageHeader title="我的事项" subtitle="维护个人事项,集中查看高优先级和未完成项,并可关联到项目。" />
|
||||
|
||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<MetricCard title="全部待办" value={todoStats.total} note="当前筛选结果" />
|
||||
<MetricCard title="全部事项" value={todoStats.total} note="当前筛选结果" />
|
||||
<MetricCard title="待处理" value={todoStats.todo} note="建议优先清空新增事项" tone={todoStats.todo > 0 ? "warning" : "neutral"} />
|
||||
<MetricCard title="进行中" value={todoStats.doing} note="重点跟进阻塞项" />
|
||||
<MetricCard title="已完成" value={todoStats.done} note="已完成项会持续留痕" />
|
||||
@@ -105,12 +109,12 @@ export function TodosPage() {
|
||||
<FilterBar className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="text-sm text-zinc-500">按状态筛选后,列表只保留当前关注的工作项。</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<select className="select-std w-[180px]" value={statusFilter} onChange={(event) => setStatusFilter(event.target.value as typeof statusFilter)}>
|
||||
<Label className="sr-only" htmlFor="todo-status-filter">事项状态</Label>
|
||||
<select id="todo-status-filter" className="select-std w-[180px]" value={statusFilter} onChange={(event) => setStatusFilter(event.target.value as typeof statusFilter)}>
|
||||
<option value="">全部状态</option>
|
||||
<option value="todo">todo</option>
|
||||
<option value="doing">doing</option>
|
||||
<option value="done">done</option>
|
||||
<option value="cancelled">cancelled</option>
|
||||
{Object.entries(TODO_STATUS_LABELS).map(([value, label]) => (
|
||||
<option key={value} value={value}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
<Button type="button" variant="secondary" onClick={() => queryClient.invalidateQueries({ queryKey: ["todos"] })}>
|
||||
刷新列表
|
||||
@@ -118,8 +122,8 @@ export function TodosPage() {
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
<SectionPanel title="待办列表" description={todoItems.length ? `当前共 ${todoItems.length} 条待办` : "暂无待办,建议从当前周计划或项目动作拆出任务。"}>
|
||||
{todosQuery.isError ? <div className="mb-4 text-sm text-red-600">{errorMessage(todosQuery.error, "加载待办失败")}</div> : null}
|
||||
<SectionPanel title="事项列表" description={todoItems.length ? `当前共 ${todoItems.length} 条事项` : "暂无事项,建议从当前周计划或项目动作拆出任务。"}>
|
||||
{todosQuery.isError ? <div className="mb-4 text-sm text-red-600">{errorMessage(todosQuery.error, "加载事项失败")}</div> : null}
|
||||
<PrimaryTable>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -136,7 +140,7 @@ export function TodosPage() {
|
||||
{todosQuery.isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell className="py-8 text-center text-sm text-zinc-500" colSpan={6}>
|
||||
待办加载中...
|
||||
事项加载中...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : todoItems.length ? (
|
||||
@@ -149,7 +153,7 @@ export function TodosPage() {
|
||||
<TableCell>{item.project ? `${item.project.projectNo} · ${item.project.name}` : "-"}</TableCell>
|
||||
<TableCell>{item.assignee?.name ?? "-"}</TableCell>
|
||||
<TableCell>{item.priority}</TableCell>
|
||||
<TableCell>{item.status}</TableCell>
|
||||
<TableCell>{TODO_STATUS_LABELS[item.status as keyof typeof TODO_STATUS_LABELS] ?? "未知状态"}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
@@ -171,7 +175,7 @@ export function TodosPage() {
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell className="py-8 text-center text-sm text-zinc-500" colSpan={6}>
|
||||
当前没有符合条件的待办。
|
||||
当前没有符合条件的事项。
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
@@ -181,16 +185,19 @@ export function TodosPage() {
|
||||
</SectionPanel>
|
||||
</div>
|
||||
|
||||
<SectionPanel title="新增待办" description="建议把周报计划、项目跟进或临时事项都拆成清晰动作。">
|
||||
<SectionPanel title="新增事项" description="建议把周报计划、项目跟进或临时事项都拆成清晰动作。">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Input data-testid={TEST_IDS.todo.createTitle} placeholder="待办标题" value={title} onChange={(event) => setTitle(event.target.value)} />
|
||||
<Label htmlFor="todo-title">事项标题</Label>
|
||||
<Input id="todo-title" data-testid={TEST_IDS.todo.createTitle} placeholder="请输入事项标题" value={title} onChange={(event) => setTitle(event.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Input placeholder="补充说明(可选)" value={description} onChange={(event) => setDescription(event.target.value)} />
|
||||
<Label htmlFor="todo-description">补充说明(可选)</Label>
|
||||
<Input id="todo-description" placeholder="补充背景或下一步" value={description} onChange={(event) => setDescription(event.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<select className="select-std" value={projectId} onChange={(event) => setProjectId(event.target.value)}>
|
||||
<Label htmlFor="todo-project">关联项目(可选)</Label>
|
||||
<select id="todo-project" className="select-std" value={projectId} onChange={(event) => setProjectId(event.target.value)}>
|
||||
<option value="">不关联项目</option>
|
||||
{(projectsQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
@@ -200,7 +207,7 @@ export function TodosPage() {
|
||||
</select>
|
||||
</div>
|
||||
<Button data-testid={TEST_IDS.todo.createSubmit} type="button" onClick={() => createMutation.mutate()} disabled={createMutation.isPending || !title.trim()}>
|
||||
{createMutation.isPending ? "新建中..." : "新建待办"}
|
||||
{createMutation.isPending ? "新建中..." : "新建事项"}
|
||||
</Button>
|
||||
</div>
|
||||
</SectionPanel>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useMemo, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { fetchProjects } from "../api/projects";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
import {
|
||||
createTravelRequest,
|
||||
fetchTravelRequestDetail,
|
||||
@@ -54,13 +53,7 @@ type DraftSegment = {
|
||||
};
|
||||
|
||||
function resolveError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "请求失败,请稍后重试。";
|
||||
return resolveUserErrorMessage(error, "请求失败,请稍后重试。");
|
||||
}
|
||||
|
||||
function emptySegment(): DraftSegment {
|
||||
@@ -306,19 +299,6 @@ export function TravelRequestsPage() {
|
||||
}
|
||||
}, [detailQuery.data, selectedId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== "requests" || selectedId || canCreate) {
|
||||
return;
|
||||
}
|
||||
const nextId = listQuery.data?.items?.[0]?.id ?? "";
|
||||
if (!nextId) {
|
||||
return;
|
||||
}
|
||||
const next = new URLSearchParams(searchParams);
|
||||
next.set("id", nextId);
|
||||
setSearchParams(next);
|
||||
}, [activeTab, canCreate, listQuery.data?.items, searchParams, selectedId, setSearchParams]);
|
||||
|
||||
const detail = detailQuery.data ?? null;
|
||||
const isDraftEditing = Boolean(detail && detail.status === "draft" && detail.permissions.canEditDraft);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
import { fetchDepartments } from "../api/departments";
|
||||
import { fetchEmployees } from "../api/employees";
|
||||
import {
|
||||
@@ -123,7 +122,7 @@ export function UserManagementPage() {
|
||||
toast.success("用户已创建");
|
||||
},
|
||||
onError: (err) => {
|
||||
const msg = isApiError(err) ? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}` : err instanceof Error ? err.message : "创建用户失败";
|
||||
const msg = resolveUserErrorMessage(err, "创建用户失败");
|
||||
setError(msg);
|
||||
toast.error(msg);
|
||||
},
|
||||
@@ -142,7 +141,7 @@ export function UserManagementPage() {
|
||||
toast.success("状态已更新");
|
||||
},
|
||||
onError: (err) => {
|
||||
const msg = isApiError(err) ? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}` : err instanceof Error ? err.message : "更新用户状态失败";
|
||||
const msg = resolveUserErrorMessage(err, "更新用户状态失败");
|
||||
setError(msg);
|
||||
toast.error(msg);
|
||||
},
|
||||
@@ -157,7 +156,7 @@ export function UserManagementPage() {
|
||||
toast.success("密码已重置");
|
||||
},
|
||||
onError: (err) => {
|
||||
const msg = isApiError(err) ? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}` : err instanceof Error ? err.message : "重置密码失败";
|
||||
const msg = resolveUserErrorMessage(err, "重置密码失败");
|
||||
setError(msg);
|
||||
toast.error(msg);
|
||||
},
|
||||
@@ -174,7 +173,7 @@ export function UserManagementPage() {
|
||||
toast.success("角色已更新");
|
||||
},
|
||||
onError: (err) => {
|
||||
const msg = isApiError(err) ? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}` : err instanceof Error ? err.message : "更新角色失败";
|
||||
const msg = resolveUserErrorMessage(err, "更新角色失败");
|
||||
setError(msg);
|
||||
toast.error(msg);
|
||||
},
|
||||
@@ -281,13 +280,7 @@ export function UserManagementPage() {
|
||||
{listQuery.isLoading ? <TableSkeleton columns={5} rows={5} /> : null}
|
||||
{listQuery.isError ? (
|
||||
<p className="text-sm text-red-600">
|
||||
{isApiError(listQuery.error)
|
||||
? `${listQuery.error.code}: ${mapApiErrorMessage(
|
||||
listQuery.error.code,
|
||||
listQuery.error.details,
|
||||
listQuery.error.message,
|
||||
)}`
|
||||
: "加载用户列表失败"}
|
||||
{resolveUserErrorMessage(listQuery.error, "加载用户列表失败")}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
@@ -16,8 +17,6 @@ import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Label } from "../components/ui/label";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { isApiError } from "../api/types";
|
||||
|
||||
const TYPE_OPTIONS: Array<{ value: "" | WecomApprovalArchiveType; label: string }> = [
|
||||
{ value: "", label: "全部类型" },
|
||||
@@ -43,10 +42,7 @@ const FINDING_OPTIONS: Array<{ value: "" | WecomApprovalArchiveFinding; label: s
|
||||
];
|
||||
|
||||
function resolveError(error: unknown, fallback = "读取企微审批历史失败") {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
function typeLabel(value: string) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { createTodo } from "../api/todos";
|
||||
@@ -13,15 +14,10 @@ import { Button } from "../components/ui/button";
|
||||
import { Textarea } from "../components/ui/textarea";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
import { toast } from "sonner";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { TEST_IDS } from "../lib/testIds";
|
||||
|
||||
function errorMessage(error: unknown, fallback: string) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
type DraftProjectLink = {
|
||||
@@ -122,9 +118,9 @@ export function WeeklyReportsPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ["todos"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["portal-home"] }),
|
||||
]);
|
||||
toast.success("已从周报生成待办");
|
||||
toast.success("已从周报生成事项");
|
||||
},
|
||||
onError: (error) => toast.error(errorMessage(error, "生成待办失败")),
|
||||
onError: (error) => toast.error(errorMessage(error, "生成事项失败")),
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -200,7 +196,7 @@ export function WeeklyReportsPage() {
|
||||
</div>
|
||||
))}
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<Button data-testid={TEST_IDS.weeklyReport.createTodo} type="button" variant="secondary" onClick={() => createTodoMutation.mutate()} disabled={createTodoMutation.isPending}>转成待办</Button>
|
||||
<Button data-testid={TEST_IDS.weeklyReport.createTodo} type="button" variant="secondary" onClick={() => createTodoMutation.mutate()} disabled={createTodoMutation.isPending}>转成事项</Button>
|
||||
<Button data-testid={TEST_IDS.weeklyReport.save} type="button" variant="secondary" onClick={() => saveMutation.mutate()} disabled={saveMutation.isPending}>保存草稿</Button>
|
||||
<Button data-testid={TEST_IDS.weeklyReport.submit} type="button" onClick={() => submitMutation.mutate()} disabled={submitMutation.isPending}>提交周报</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import type { ReactNode } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link, Navigate, useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import {
|
||||
approveWorkflowTask,
|
||||
fetchWorkflowApprovers,
|
||||
@@ -31,6 +31,7 @@ import { MetricCard } from "../components/patterns/MetricCard";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Label } from "../components/ui/label";
|
||||
import { Textarea } from "../components/ui/textarea";
|
||||
import { approvalRoleLabel, resolvePurchaseDisplayCode, workflowActionLabel } from "../lib/approvalDisplay";
|
||||
import { formatDisplayDateTime } from "../lib/displayFormat";
|
||||
@@ -177,14 +178,7 @@ export function WorkflowTaskDetailPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ["workflow", "task", taskId] }),
|
||||
]);
|
||||
}
|
||||
let msg: string;
|
||||
if (error instanceof Error && !isApiError(error)) {
|
||||
msg = error.message;
|
||||
} else if (isApiError(error)) {
|
||||
msg = `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
} else {
|
||||
msg = "动作执行失败,请稍后重试";
|
||||
}
|
||||
const msg = resolveUserErrorMessage(error, "动作执行失败,请稍后重试");
|
||||
setActionError(msg);
|
||||
toast.error(msg);
|
||||
},
|
||||
@@ -238,13 +232,7 @@ export function WorkflowTaskDetailPage() {
|
||||
}
|
||||
|
||||
if (detailQuery.isError || !detailQuery.data) {
|
||||
const message = isApiError(detailQuery.error)
|
||||
? `${detailQuery.error.code}: ${mapApiErrorMessage(
|
||||
detailQuery.error.code,
|
||||
detailQuery.error.details,
|
||||
detailQuery.error.message,
|
||||
)}`
|
||||
: "加载失败,请返回列表重试";
|
||||
const message = resolveUserErrorMessage(detailQuery.error, "加载失败,请返回列表重试");
|
||||
|
||||
return (
|
||||
<div className="card-std space-y-3 p-5">
|
||||
@@ -265,6 +253,11 @@ export function WorkflowTaskDetailPage() {
|
||||
const canReturn = allowedActions.has("return");
|
||||
const canReject = allowedActions.has("reject");
|
||||
const canTransfer = allowedActions.has("transfer");
|
||||
const transferDisabledReason = !selectedApprover
|
||||
? "请先选择目标审批人。"
|
||||
: transferReason.trim().length < 10
|
||||
? `请填写转审理由,至少 10 个字(还差 ${10 - transferReason.trim().length} 个字)。`
|
||||
: "转审理由已满足要求。";
|
||||
const allowedActionLabels = detail.allowedActions.map(workflowActionLabel).join(" / ");
|
||||
const businessStatusLabel = workflowInstanceStatusLabel(detail.workflowInstance?.currentStatus);
|
||||
|
||||
@@ -420,7 +413,11 @@ export function WorkflowTaskDetailPage() {
|
||||
当前 ¥{item.amount} / 标准 ¥{item.maxAmount}/{item.perUnit}
|
||||
</div>
|
||||
<div>超出 ¥{item.exceededAmount} · {item.standardName}</div>
|
||||
<div>管控模式:{item.controlMode === "block" ? "阻断" : "预警"}</div>
|
||||
<div>
|
||||
{item.controlMode === "block"
|
||||
? "处理影响:当前单据不应继续通过,请申请人调整金额或补充合规处理依据。"
|
||||
: "处理影响:可继续审批,但请核对超标原因并在审批意见中记录判断。"}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -510,6 +507,13 @@ export function WorkflowTaskDetailPage() {
|
||||
)}
|
||||
testId="workflow-task-action-panel"
|
||||
>
|
||||
{canReturn || canReject ? (
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-xs leading-5 text-zinc-600" data-testid="workflow-action-consequence-help">
|
||||
<div><span className="font-medium text-zinc-800">退回:</span>原单回到申请人修改,保留单号和历史,修改后可重新提交。</div>
|
||||
<div><span className="font-medium text-zinc-800">驳回:</span>本次流程终止或进入驳回态,申请人通常需要复制或重新发起。</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{actionError ? (
|
||||
<p
|
||||
className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600"
|
||||
@@ -521,8 +525,10 @@ export function WorkflowTaskDetailPage() {
|
||||
|
||||
{activeSecondaryAction === "reject" ? (
|
||||
<div className="detail-action-card space-y-2">
|
||||
<p className="text-sm font-medium text-zinc-800">驳回意见</p>
|
||||
<Label className="text-sm font-medium text-zinc-800" htmlFor="workflow-task-reject-comment">驳回意见</Label>
|
||||
<p className="text-xs leading-5 text-zinc-500">驳回会终止本次流程或进入驳回态,申请人通常需要重新发起。</p>
|
||||
<Textarea
|
||||
id="workflow-task-reject-comment"
|
||||
data-testid="workflow-task-reject-comment"
|
||||
onChange={(event) => setRejectComment(event.target.value)}
|
||||
rows={3}
|
||||
@@ -542,8 +548,10 @@ export function WorkflowTaskDetailPage() {
|
||||
|
||||
{activeSecondaryAction === "return" ? (
|
||||
<div className="detail-action-card space-y-2">
|
||||
<p className="text-sm font-medium text-zinc-800">退回意见</p>
|
||||
<Label className="text-sm font-medium text-zinc-800" htmlFor="workflow-task-return-comment">退回意见</Label>
|
||||
<p className="text-xs leading-5 text-zinc-500">退回后原单可修改并重新提交,单号和审批历史会保留。</p>
|
||||
<Textarea
|
||||
id="workflow-task-return-comment"
|
||||
data-testid="workflow-task-return-comment"
|
||||
onChange={(event) => setReturnComment(event.target.value)}
|
||||
rows={3}
|
||||
@@ -564,7 +572,9 @@ export function WorkflowTaskDetailPage() {
|
||||
{activeSecondaryAction === "transfer" && canTransfer ? (
|
||||
<div className="detail-action-card space-y-2">
|
||||
<p className="text-sm font-medium text-zinc-800">转审</p>
|
||||
<Label className="text-xs text-zinc-600" htmlFor="workflow-task-transfer-approver">目标审批人</Label>
|
||||
<select
|
||||
id="workflow-task-transfer-approver"
|
||||
className="select-std"
|
||||
data-testid="workflow-task-transfer-approver"
|
||||
onChange={(event) => setSelectedApprover(event.target.value)}
|
||||
@@ -577,13 +587,24 @@ export function WorkflowTaskDetailPage() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Label className="text-xs text-zinc-600" htmlFor="workflow-task-transfer-reason">转审理由</Label>
|
||||
<Textarea
|
||||
id="workflow-task-transfer-reason"
|
||||
aria-describedby="workflow-task-transfer-help"
|
||||
data-testid="workflow-task-transfer-reason"
|
||||
onChange={(event) => setTransferReason(event.target.value)}
|
||||
rows={3}
|
||||
value={transferReason}
|
||||
/>
|
||||
<p
|
||||
id="workflow-task-transfer-help"
|
||||
className={transferReason.trim().length >= 10 && selectedApprover ? "text-xs text-emerald-700" : "text-xs text-amber-700"}
|
||||
data-testid="workflow-task-transfer-disabled-reason"
|
||||
>
|
||||
{transferDisabledReason}
|
||||
</p>
|
||||
<Button
|
||||
aria-describedby="workflow-task-transfer-help"
|
||||
data-testid="workflow-task-transfer"
|
||||
variant="secondary"
|
||||
disabled={
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import {
|
||||
fetchWorkflowTasks,
|
||||
fetchWorkflowTaskSummary,
|
||||
@@ -257,7 +256,7 @@ export function WorkflowTasksPage() {
|
||||
|
||||
return (
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader title="待我处理" />
|
||||
<WorkspaceHeader title="待我审批" />
|
||||
|
||||
<FilterBar className="ops-command-bar !p-3 md:!p-3.5">
|
||||
<div className="ops-command-bar__row">
|
||||
@@ -339,13 +338,7 @@ export function WorkflowTasksPage() {
|
||||
|
||||
{query.isError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{isApiError(query.error)
|
||||
? `${query.error.code}: ${mapApiErrorMessage(
|
||||
query.error.code,
|
||||
query.error.details,
|
||||
query.error.message,
|
||||
)}`
|
||||
: "加载待办失败,请稍后重试"}
|
||||
{resolveUserErrorMessage(query.error, "加载审批任务失败,请稍后重试")}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { useMemo } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { fetchSystemNavigation, fetchWorkspaceSettings, updateWorkspaceSettings } from "../api/workspace";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
@@ -16,7 +15,7 @@ const MODULE_LABELS: Record<string, string> = {
|
||||
reimbursements: "报销",
|
||||
assets: "资产",
|
||||
weeklyReports: "周报",
|
||||
todos: "待办",
|
||||
todos: "我的事项",
|
||||
announcements: "公告",
|
||||
projects: "项目",
|
||||
workspace: "工作区设置",
|
||||
@@ -32,7 +31,7 @@ const MODULE_LABELS: Record<string, string> = {
|
||||
departments: "部门管理",
|
||||
masterData: "主数据",
|
||||
notificationChannels: "通知渠道",
|
||||
workflowTasks: "待我处理",
|
||||
workflowTasks: "待我审批",
|
||||
dashboard: "仪表盘",
|
||||
purchaseRequests: "采购",
|
||||
travelRequests: "差旅申请",
|
||||
@@ -71,11 +70,7 @@ export function WorkspaceSetupPage() {
|
||||
toast.success("工作区设置已保存");
|
||||
},
|
||||
onError: (error) => {
|
||||
const message = isApiError(error)
|
||||
? `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: "保存失败";
|
||||
const message = resolveUserErrorMessage(error, "保存失败");
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../../lib/resolveErrorMessage";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import {
|
||||
@@ -6,7 +7,6 @@ import {
|
||||
type UserImportApplyResult,
|
||||
type UserImportDryRunResult,
|
||||
} from "../../api/users";
|
||||
import { mapApiErrorMessage } from "../../api/errorMessages";
|
||||
import { isApiError } from "../../api/types";
|
||||
import { downloadBlob } from "../../lib/download";
|
||||
import { Button } from "../../components/ui/button";
|
||||
@@ -42,10 +42,7 @@ function toCsvCell(value: string) {
|
||||
}
|
||||
|
||||
function formatErrorMessage(error: unknown, fallback: string) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
}
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
return resolveUserErrorMessage(error, fallback);
|
||||
}
|
||||
|
||||
function parsePreviewFromError(error: unknown, sourceFilename: string): UserImportDryRunResult | null {
|
||||
|
||||
在新工单中引用
屏蔽一个用户