diff --git a/.agents/skills/hyb-oa-delivery/SKILL.md b/.agents/skills/hyb-oa-delivery/SKILL.md index e3242729..2a991657 100644 --- a/.agents/skills/hyb-oa-delivery/SKILL.md +++ b/.agents/skills/hyb-oa-delivery/SKILL.md @@ -31,7 +31,7 @@ Read only what is relevant: - OALAUNCH1 is the current final-enable gate after OAOPS1/ASSET1D/OSMOKE1: do not treat ASSET1D completion or `PASS_OA_REAL_USE_DAILY_READY` as final launch approval. Use `scripts/qa/run-oa-final-launch-readiness.sh` to aggregate evidence only; it must not create business data, execute payments, write WeCom, reset/restore the production DB, print secrets, or mark final launch ready without real business closeout, 7-day stabilization, permission matrix acceptance, backup/restore drill evidence, data-governance/training acceptance, and Day 7 Go/No-Go. Valid conclusions are `PASS_OA_LAUNCH_READY`, `PASS_WITH_FINDINGS`, `PAUSED_MANUAL_CONFIRMATION`, and `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`. Permission or backup/restore fixes require read-only reviewer review before launch acceptance. - OPOLICY1 is the current policy-system alignment lane after OMAT1/OALAUNCH1 review: external company policies under `/home/shunli/Projects/faculty-daily-assistant/公司/制度体系建设_20260524` are adjusted to a small-team V1口径 first, then hyb-oa facts are aligned. Treat policy parameters, data-retention boundaries, seal-use ledger, approval-exception SOP, policy signoff, and P2 freeze rules as OALAUNCH1 data-governance gates. Do not assume budget hard control, complex procurement/tendering, e-signature, full e-archive, ERP/bank deep integration, or AI rule writes are pre-launch requirements. For V1, contract registry summary/archive/offline archive fields and existing attachment audit can carry seal-use traceability; announcements/manual evidence can carry policy notices/signoff until a later Issue proves dedicated schema is required. Never copy sensitive policy text, thresholds, confidential contract bodies, credentials, or full local paths into repo docs. - OAFLOW1 is the current OA reimbursement approval hardening lane: `wangshunli/HYD000006` as `ceo + admin` must not be assigned a fake direct manager, must not self-approve, and `admin` must not be treated as business approval authority. Reimbursement `approval_rules` now support `ownerSelfFallbackRole`; OAFLOW1 V1 only allows `finance_mgr` or null, and existing enabled rules default to `finance_mgr`. For CEO owner self-applications with no direct manager, build and persist a new workflow snapshot by replacing the first `manager` node with `finance_mgr`, removing the owner's own `ceo` node and duplicate fallback role. Precheck, submit, and approval advancement must use the same snapshot; public deployment and low-risk owner reimbursement verification require separate approval and must not execute real payment. -- OAFRONT6 is the current frontend maturity small-step lane after OAFRONT5: use `issues/2026-05-25_12-29-03-oa-frontend-maturity-small-step-v1.csv` for scoped design follow-ups only. Route-title cleanup, asset registry sectioning, mobile visual-matrix stability, and contract registry form grouping are implemented; long-number display compaction and Portal observation remain later TODO/observation items. Do not treat OAFRONT6 as approval for a broad redesign, business logic changes, public writes, deployment, or final launch readiness; further implementation still needs `docs/产品设计规则.md`, focused Playwright/visual evidence, and OALAUNCH1 gates. +- OAFRONT6 is the current frontend maturity small-step lane after OAFRONT5: use `issues/2026-05-25_12-29-03-oa-frontend-maturity-small-step-v1.csv` for scoped design follow-ups only. Route-title cleanup, asset registry sectioning, mobile visual-matrix stability, and contract registry form grouping are implemented and deployed; long-number display compaction is implemented locally and still needs deployment plus public PC readonly visual rerun before closure; Portal observation remains later feedback-driven work. Do not treat OAFRONT6 as approval for a broad redesign, business logic changes, public writes, deployment, or final launch readiness; further implementation still needs `docs/产品设计规则.md`, focused Playwright/visual evidence, and OALAUNCH1 gates. - OMAT1 is the current maturity-gap review goal after OALAUNCH1: use `issues/2026-05-23_17-20-00-oa-maturity-gap-review-v1.csv` as the reviewable ledger for every problem or suggestion. It includes domestic benchmark sources, a domain capability comparison matrix, manual decision Q&A checklist, review decision packet, status audit, and MAGI round reviews in the manual trial-run report, the small-team rule that maturity means controlled operation before feature breadth, explicit adopt/defer/reject decision回填, and a need for a concise evidence view. Follow the MAGI cycle wording as audit structure only: review/evaluate first, record or solve bounded evidence gaps second, then prioritize direction. Do not implement OMAT1 suggestions until the user chooses adopt/defer/reject and a proper Issue CSV exists for the adopted work. - WEGOV1 changes the current WeCom route to "Enterprise WeChat owns mature HR/attendance flows; OA stores self-owned history, audit, entry, messages, and emergency fallback." Treat four domains explicitly: WeCom-owned domain is check-in, leave, missed punch, outing, overtime, leave balance, and HR assistant; OA-owned domain remains reimbursement, procurement, contracts, projects, payments, invoices, bank receipts, sensitive attachments, and operating audit; OA archive domain stores WeCom business history, full external keys, and necessary snapshots; OA emergency domain is manually enabled only when WeCom is unavailable. Do not assume a full isolated WeCom sandbox; use the controlled test domain `SMOKE002 -> SMOKE101`, `OA-POC` templates, low-risk approvals, synthetic tests, public live runners, and limited real-device confirmation. WARCH1 now supplies the minimum leave/missed-punch archive implementation, WARCH2 supplies the formal read-only history query surface, and WROAD2 supplies controlled ops daily checks. Do not start deep mirror until WROAD2-CLOSEOUT, WROAD2-BIZ1, WAPPR1, WRULE1, WAUTO1/WEXT1LD, WFULLSMOKE1, WTCLOSE2, WEXT1-FORMAL, WREAL1 question gates or explicit approval, and the mirror stage's own contract are complete. - For reimbursement, invoice, OCR, duplicate invoice, or attachment work, read `docs/报销发票附件规则.md` and `references/reimbursement.md`. diff --git a/client/src/lib/displayFormat.ts b/client/src/lib/displayFormat.ts index 2f485ab8..7018b15e 100644 --- a/client/src/lib/displayFormat.ts +++ b/client/src/lib/displayFormat.ts @@ -30,3 +30,35 @@ export function formatDisplayDateTime(value: string | null | undefined) { minute: "2-digit", }).format(timestamp); } + +export function formatCompactIdentifier( + value: string | number | null | undefined, + options: { head?: number; tail?: number; minLength?: number } = {}, +) { + const text = String(value ?? "").trim(); + if (!text) return "-"; + + const head = options.head ?? 10; + const tail = options.tail ?? 4; + const minLength = options.minLength ?? 18; + + if (text.length <= minLength || text.length <= head + tail + 1) { + return text; + } + + return `${text.slice(0, head)}…${text.slice(-tail)}`; +} + +export function isCompactedIdentifier(value: string | number | null | undefined, displayValue: string) { + const text = String(value ?? "").trim(); + return Boolean(text) && text !== displayValue; +} + +export function formatTextWithCompactLongNumbers(value: string | number | null | undefined) { + const text = String(value ?? "").trim(); + if (!text) return "-"; + + return text.replace(/\b20[0-9]{12,}\b/g, (match) => + formatCompactIdentifier(match, { head: 10, tail: 4, minLength: 14 }), + ); +} diff --git a/client/src/pages/AssetRegistryPage.tsx b/client/src/pages/AssetRegistryPage.tsx index 10a29187..20559125 100644 --- a/client/src/pages/AssetRegistryPage.tsx +++ b/client/src/pages/AssetRegistryPage.tsx @@ -38,7 +38,7 @@ import { SectionPanel } from "../components/patterns/SectionPanel"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; -import { formatDisplayDateTime } from "../lib/displayFormat"; +import { formatCompactIdentifier, formatDisplayDateTime, formatTextWithCompactLongNumbers, isCompactedIdentifier } from "../lib/displayFormat"; function resolveError(error: unknown) { if (isApiError(error)) { @@ -156,6 +156,41 @@ function typeHint(assetType: AssetType) { } } +function CompactAssetNo({ value, copyable = true }: { value: string; copyable?: boolean }) { + const displayValue = formatCompactIdentifier(value, { head: 14, tail: 6, minLength: 24 }); + const compacted = isCompactedIdentifier(value, displayValue); + + async function copyAssetNo() { + if (!compacted) return; + try { + await navigator.clipboard.writeText(value); + toast.success("已复制完整资产编号"); + } catch { + toast.error("复制失败,可在资产详情中查看完整编号"); + } + } + + return ( + + + {displayValue} + + {compacted && copyable ? ( + + ) : null} + + ); +} + function unsafeAssetTextReason(value: string) { if (!value.trim()) { return ""; @@ -796,7 +831,7 @@ export function AssetRegistryPage() { {(documentCategoriesQuery.data?.items ?? []).map((item) => ( ))} @@ -829,8 +864,12 @@ export function AssetRegistryPage() { >
{isCreatingAsset ? "创建新的资产台账卡片。" : currentAsset?.name}
++ {isCreatingAsset ? "创建新的资产台账卡片。" : formatTextWithCompactLongNumbers(currentAsset?.name)} +
申请人:{detail.applicant?.name ?? "-"}
{resolvePurchaseStatusHint(detail)} diff --git a/client/src/pages/invoice-pool/InvoiceDetailPanel.tsx b/client/src/pages/invoice-pool/InvoiceDetailPanel.tsx index 9b32b859..e7b6732a 100644 --- a/client/src/pages/invoice-pool/InvoiceDetailPanel.tsx +++ b/client/src/pages/invoice-pool/InvoiceDetailPanel.tsx @@ -7,8 +7,9 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D import { Input } from "../../components/ui/input"; import { Textarea } from "../../components/ui/textarea"; import { DateTextInput } from "../../components/common/DateTextInput"; -import { formatDisplayDate, formatDisplayDateTime } from "../../lib/displayFormat"; +import { formatCompactIdentifier, formatDisplayDate, formatDisplayDateTime, isCompactedIdentifier } from "../../lib/displayFormat"; import { formatInvoiceOcrErrorMessage } from "../../lib/invoiceOcr"; +import { toast } from "sonner"; export type InvoiceDetailPanelProps = { detail: InvoicePoolDetail | undefined; @@ -150,6 +151,8 @@ export function InvoiceDetailPanel({ const invoiceNo = detail ? [detail.invoiceCode, detail.invoiceNumber].filter(Boolean).join("-") || detail.originalName || "待识别" : ""; + const invoiceNoDisplay = formatCompactIdentifier(invoiceNo, { head: 12, tail: 4, minLength: 18 }); + const invoiceNoCompacted = isCompactedIdentifier(invoiceNo, invoiceNoDisplay); const bindingStatus = detail?.linkedReimbursementId ? "已绑定报销单" : detail?.linkedTravelOrderId @@ -168,6 +171,15 @@ export function InvoiceDetailPanel({ setDuplicateOverrideRefFormNumber(""); setDuplicateOverrideReason(""); }; + const copyInvoiceNo = async () => { + if (!invoiceNoCompacted) return; + try { + await navigator.clipboard.writeText(invoiceNo); + toast.success("已复制完整发票号码"); + } catch { + toast.error("复制失败,可在编辑表单中查看完整号码"); + } + }; return ( <> @@ -178,7 +190,16 @@ export function InvoiceDetailPanel({