From 669d22e71fa357bec2706b44c34f09ca2e8c9e6d Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Sun, 21 Jun 2026 23:24:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=B7=AE=E6=97=85=E9=A2=86?= =?UTF-8?q?=E5=AF=BC=E6=84=8F=E8=A7=81=E4=BA=8B=E4=BB=B6=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E5=8C=96=E4=B8=8E=E7=94=B3=E8=AF=B7=E5=AE=A1=E6=89=B9=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - applicationApproval 新增按日期/时间/审批角色拆分格式化,buildLeaderApprovalEvents 补充 dateLabel/timeLabel/roleLabel 字段 - TravelRequestDetailView 领导意见事件改为日期+时间+审批人结构化展示,travel-request-detail-view.css 重构对应样式 - travelReimbursementAttachmentModel 微调附件标识,同步更新 application-approval-info、travel-request-detail-leader-approval、attachment-association-confirmation 测试 - 更新公司通信费报销规则表 --- .../finance-rules/公司通信费报销规则.xlsx | Bin 5933 -> 5933 bytes .../views/travel-request-detail-view.css | 344 ++++++++++-------- web/src/utils/applicationApproval.js | 41 +++ web/src/views/TravelRequestDetailView.vue | 38 +- .../travelReimbursementAttachmentModel.js | 2 +- web/tests/application-approval-info.test.mjs | 3 + ...tachment-association-confirmation.test.mjs | 41 +++ ...el-request-detail-leader-approval.test.mjs | 46 ++- 8 files changed, 329 insertions(+), 186 deletions(-) diff --git a/server/rules/finance-rules/公司通信费报销规则.xlsx b/server/rules/finance-rules/公司通信费报销规则.xlsx index cb89ac04d7e0663d8ff274e80a3a2eaf9b40a9cb..6a95d07c6e89051af2c10353764b043d67ad9246 100644 GIT binary patch delta 423 zcmZ3hw^olgz?+#xgn@y9gQ2YQ>O|fH96&0@VUw@(#H;f42mKBk@U+FJzS>)QxmRwH zj<|sOgb6cP9*J4r3%j@_FzL#>x1Oi3H9k0I_|Lw6n`moXlKczJFY}npMZyGEX-6-fd{Dt*httU+`9B zg5U~OcUgCV)&}j?K?v9C0C(b_0 zE&BUnKUZx2?Ap)#k6jmt{`qD8mlGuvVrIt7Q5IlhVA%YL(UgrD7$BRqIG(Y98N$4V z%pk^OcY$YM0RzD%MzFwEp+{hWS;F2Bffpjy@W7m`Eou%{l`3inrk9C2gXuS-7GPRk U%nVE?h*^Q@xnizh`l*-)04D>rc>n+a delta 423 zcmZ3hw^olgz?+#xgn@y9gW+27m5ICuIDk}4%tVfr6R*nG`}!X?;A#7R>Pq$NFV40u zk^0Va9GsGbI-;LcM7vIQ>kQmo?;pL5kHx(2*Ml;#I@h9OkX6t@U)*pMa$Yh7sU-uVc zjgAZYbS(QOq+8yJSXQ6#cs{T9rb)>;;aL)GIU3@5w=~+XA8&u2`g2R^#I4R=QmFw+ z3TJCGo^}7Jd?Qk$z3TLv^-MF*nmB&CdD(2In*P&wdG+&6S~XsUT**}YR;lW(xcXIY z+20rYIdk)GIsfK=9C|_H$JhL`>L{TQGc#t6vH%+c!{$$nrfkf>0NJd?@r(t`5au;x z1~DeP3p@i07zj2of(5n;Jpv2N6844&yb!U52j*mLQFE}WR8c!Hy-d^@OurGe0MqJX TW?(u&%nD4;6>|mCPsKa{m`%VW diff --git a/web/src/assets/styles/views/travel-request-detail-view.css b/web/src/assets/styles/views/travel-request-detail-view.css index 9795c3a..e8476a0 100644 --- a/web/src/assets/styles/views/travel-request-detail-view.css +++ b/web/src/assets/styles/views/travel-request-detail-view.css @@ -718,11 +718,10 @@ .application-leader-opinion { display: grid; gap: 12px; - margin-top: 14px; - padding: 14px; - border: 1px solid rgba(var(--theme-primary-rgb, 58, 124, 165), .16); - background: linear-gradient(180deg, rgba(248, 251, 255, .98) 0%, #ffffff 100%); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .86); + margin-top: 12px; + padding: 14px 0 4px; + border-top: 1px solid #e5edf5; + background: #ffffff; } .application-leader-opinion-head { @@ -730,8 +729,6 @@ align-items: center; justify-content: space-between; gap: 12px; - padding-bottom: 10px; - border-bottom: 1px solid #e2e8f0; color: #334155; font-size: 14px; line-height: 1.5; @@ -742,209 +739,238 @@ align-items: center; gap: 8px; color: #0f172a; - font-weight: 850; - font-size: 16px; + font-weight: 700; + font-size: 15px; } .application-leader-opinion-head span i { - width: 28px; - height: 28px; - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--theme-primary-active, #255b7d); - border: 1px solid rgba(var(--theme-primary-rgb, 58, 124, 165), .22); - border-radius: 999px; - background: rgba(var(--theme-primary-rgb, 58, 124, 165), .08); - font-size: 18px; + color: #64748b; + font-size: 17px; + line-height: 1; } .application-leader-opinion-head strong { - padding: 4px 10px; - border: 1px solid rgba(var(--theme-primary-rgb, 58, 124, 165), .18); - border-radius: 999px; - background: rgba(var(--theme-primary-rgb, 58, 124, 165), .08); - color: var(--theme-primary-active); - font-weight: 800; + color: #64748b; + font-weight: 600; font-size: 13px; } .application-leader-opinion-timeline { position: relative; display: grid; - gap: 10px; - padding-left: 18px; -} - -.application-leader-opinion-timeline::before { - content: ""; - position: absolute; - top: 6px; - bottom: 6px; - left: 5px; - width: 1px; - background: #dbe4ee; -} - -.application-leader-opinion-timeline.is-single { - padding-left: 0; -} - -.application-leader-opinion-timeline.is-single::before, -.application-leader-opinion-timeline.is-single .application-leader-opinion-event::before { - display: none; + gap: 0; + padding: 2px 0 0; } .application-leader-opinion-event { - --leader-opinion-tone: var(--theme-primary, #3a7ca5); - --leader-opinion-soft-bg: #f8fbff; - --leader-opinion-soft-border: #dbeafe; + --leader-opinion-tone: #16a34a; position: relative; display: grid; - gap: 10px; - padding: 14px 16px 14px 18px; - border: 1px solid #dbe4ee; - border-left: 4px solid var(--leader-opinion-tone); - border-radius: 4px; - background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); - box-shadow: 0 14px 30px rgba(15, 23, 42, .08); - overflow: hidden; + grid-template-columns: 104px 28px minmax(0, 1fr); + gap: 12px; + padding: 0 0 16px; } -.application-leader-opinion-event::before { +.application-leader-opinion-event:last-child { + padding-bottom: 0; +} + +.application-leader-opinion-event-time { + display: grid; + gap: 3px; + justify-items: end; + padding-top: 1px; + color: #64748b; + font-variant-numeric: tabular-nums; + line-height: 1.35; +} + +.application-leader-opinion-event-time strong { + color: #334155; + font-size: 13px; + font-weight: 700; +} + +.application-leader-opinion-event-time em { + color: #94a3b8; + font-size: 12px; + font-style: normal; + font-weight: 600; +} + +.application-leader-opinion-event-rail { + position: relative; + display: flex; + justify-content: center; + padding-top: 1px; +} + +.application-leader-opinion-event-rail::after { content: ""; position: absolute; - top: 17px; - left: -18px; - width: 9px; - height: 9px; - border: 2px solid #ffffff; + top: 24px; + bottom: -16px; + left: 50%; + width: 2px; + transform: translateX(-50%); border-radius: 999px; - background: var(--theme-primary, #3a7ca5); - box-shadow: 0 0 0 1px rgba(var(--theme-primary-rgb, 58, 124, 165), .34); + background: #e2e8f0; +} + +.application-leader-opinion-event:last-child .application-leader-opinion-event-rail::after { + display: none; } .application-leader-opinion-event.danger { --leader-opinion-tone: #dc2626; - --leader-opinion-soft-bg: #fff7f7; - --leader-opinion-soft-border: #fecaca; -} - -.application-leader-opinion-event.danger::before { - background: #dc2626; - box-shadow: 0 0 0 1px rgba(220, 38, 38, .32); } .application-leader-opinion-event.success { --leader-opinion-tone: #16a34a; - --leader-opinion-soft-bg: #f0fdf4; - --leader-opinion-soft-border: #bbf7d0; -} - -.application-leader-opinion-event.success::before { - background: #16a34a; - box-shadow: 0 0 0 1px rgba(22, 163, 74, .32); -} - -.application-leader-opinion-event-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.application-leader-opinion-event-head span { - display: inline-flex; - align-items: center; - gap: 6px; - color: #0f172a; - font-size: 14px; - font-weight: 850; } .application-leader-opinion-event-status { - min-height: 30px; - padding: 4px 10px; - border: 1px solid var(--leader-opinion-soft-border); + position: relative; + z-index: 1; + width: 22px; + height: 22px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--leader-opinion-tone); border-radius: 999px; - background: var(--leader-opinion-soft-bg); - color: var(--leader-opinion-tone); + background: var(--leader-opinion-tone); + color: #ffffff; + box-shadow: 0 0 0 4px #ffffff; } -.application-leader-opinion-event-head i { - color: currentColor; - font-size: 16px; +.application-leader-opinion-event-status i { + font-size: 13px; + line-height: 1; } -.application-leader-opinion-event.danger .application-leader-opinion-event-head i { - color: #dc2626; -} - -.application-leader-opinion-event.success .application-leader-opinion-event-head i { - color: #16a34a; -} - -.application-leader-opinion-event-head time, -.application-leader-opinion-event footer { - color: #64748b; - font-size: 12px; - font-weight: 720; -} - -.application-leader-opinion-event-head time { - padding: 4px 9px; - border: 1px solid #e2e8f0; - border-radius: 999px; - background: #ffffff; - color: #475569; - white-space: nowrap; -} - -.application-leader-opinion-event-body { +.application-leader-opinion-record { + min-width: 0; display: grid; - gap: 5px; - padding: 10px 12px; - border: 1px solid var(--leader-opinion-soft-border); - border-radius: 4px; - background: var(--leader-opinion-soft-bg); + gap: 9px; + padding: 0 0 16px; + border-bottom: 1px solid #edf2f7; + background: #ffffff; } -.application-leader-opinion-event-body span { +.application-leader-opinion-event:last-child .application-leader-opinion-record { + padding-bottom: 0; + border-bottom: 0; +} + +.application-leader-opinion-record-head { + display: grid; + gap: 6px; +} + +.application-leader-opinion-record-title { + min-width: 0; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; +} + +.application-leader-opinion-record-title strong { + color: #0f172a; + font-size: 14px; + font-weight: 700; + line-height: 1.35; +} + +.application-leader-opinion-record-title::after { + content: ""; + width: 6px; + height: 6px; + border-radius: 999px; + background: var(--leader-opinion-tone); +} + +.application-leader-opinion-record-meta { + display: flex; + flex-wrap: wrap; + gap: 6px 16px; + margin: 0; +} + +.application-leader-opinion-record-meta div { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.application-leader-opinion-record-meta dt, +.application-leader-opinion-record-meta dd { + margin: 0; + color: #64748b; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: 1.45; +} + +.application-leader-opinion-record-meta dt { + color: #94a3b8; +} + +.application-leader-opinion-record-meta dd { + color: #475569; +} + +.application-leader-opinion-record p { + margin: 0; + padding: 9px 10px; + border: 1px solid #edf2f7; + border-radius: 4px; + background: #f8fafc; + color: #334155; + font-size: 14px; + font-weight: 500; + line-height: 1.6; +} + +.application-leader-opinion-event-foot { + display: flex; + flex-wrap: wrap; + gap: 10px; color: #64748b; font-size: 12px; - font-weight: 850; line-height: 1.4; } -.application-leader-opinion-event-body p { - margin: 0; - color: #0f172a; - font-size: 15px; - font-weight: 850; - line-height: 1.65; -} - -.application-leader-opinion-event footer { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - .application-leader-opinion-event-foot span { - min-height: 26px; display: inline-flex; align-items: center; - gap: 5px; - padding: 3px 9px; - border: 1px solid #e2e8f0; - border-radius: 999px; - background: #ffffff; - color: #475569; + color: inherit; } -.application-leader-opinion-event-foot i { - color: var(--leader-opinion-tone); - font-size: 14px; +@media (max-width: 720px) { + .application-leader-opinion { + padding-top: 12px; + } + + .application-leader-opinion-event { + grid-template-columns: 76px 24px minmax(0, 1fr); + gap: 8px; + } + + .application-leader-opinion-event-status { + width: 20px; + height: 20px; + } + + .application-leader-opinion-event-time strong { + font-size: 12px; + } + + .application-leader-opinion-event-time em { + font-size: 11px; + } } .detail-expense-table { diff --git a/web/src/utils/applicationApproval.js b/web/src/utils/applicationApproval.js index 20ad206..1d0863d 100644 --- a/web/src/utils/applicationApproval.js +++ b/web/src/utils/applicationApproval.js @@ -37,6 +37,42 @@ function formatDateTime(value) { return `${year}-${month}-${day} ${hours}:${minutes}` } +function formatDateLabel(value) { + const date = toDate(value) + if (!date) { + return '' + } + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` +} + +function formatTimeLabel(value) { + const date = toDate(value) + if (!date) { + return '' + } + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + return `${hours}:${minutes}` +} + +function resolveApprovalRole(event, returned) { + return resolveDisplayName( + event?.operator_position, + event?.operatorPosition, + event?.operator_title, + event?.operatorTitle, + event?.approver_position, + event?.approverPosition, + event?.approval_role, + event?.approvalRole, + event?.previous_approval_stage, + event?.previousApprovalStage + ) || (returned ? '直属领导审批节点' : '直属领导') +} + function getRiskFlags(request) { const flags = request?.riskFlags || request?.risk_flags_json || [] return Array.isArray(flags) ? flags : [] @@ -102,6 +138,8 @@ export function buildLeaderApprovalEvents(request) { request?.managerName ) || '直属领导' const time = formatDateTime(rawTime) + const dateLabel = formatDateLabel(rawTime) || '待记录' + const timeLabel = formatTimeLabel(rawTime) const opinion = normalizeText(event.opinion) || normalizeText(event.leader_opinion || event.leaderOpinion) || normalizeText(event.reason) @@ -115,7 +153,10 @@ export function buildLeaderApprovalEvents(request) { tone: returned ? 'danger' : 'success', title: returned ? '领导退回' : '领导审批通过', operator, + role: resolveApprovalRole(event, returned), time, + dateLabel, + timeLabel, sortAt: rawTime, opinion, returnCount, diff --git a/web/src/views/TravelRequestDetailView.vue b/web/src/views/TravelRequestDetailView.vue index 02d9b2a..74edccb 100644 --- a/web/src/views/TravelRequestDetailView.vue +++ b/web/src/views/TravelRequestDetailView.vue @@ -164,24 +164,36 @@ class="application-leader-opinion-event" :class="event.tone" > -
- + +
+ - {{ event.title }} -
-
- 意见 +
+
+
+ {{ event.title }} +
+
+
+
审批人
+
{{ event.operator }}
+
+
+
节点
+
{{ event.role }}
+
+
+

{{ event.opinion }}

+
+ 第 {{ event.returnCount }} 次退回 +
-
diff --git a/web/src/views/scripts/travelReimbursementAttachmentModel.js b/web/src/views/scripts/travelReimbursementAttachmentModel.js index 60d0ad5..81b1975 100644 --- a/web/src/views/scripts/travelReimbursementAttachmentModel.js +++ b/web/src/views/scripts/travelReimbursementAttachmentModel.js @@ -64,7 +64,7 @@ export function normalizeOcrDocuments(payload) { return documents.slice(0, MAX_OCR_DOCUMENTS).map((item) => ({ filename: item.filename, summary: item.summary, - text: String(item.text || '').slice(0, 240), + text: String(item.text || ''), avg_score: Number(item.avg_score || 0), line_count: Number(item.line_count || 0), document_type: String(item.document_type || 'other').trim() || 'other', diff --git a/web/tests/application-approval-info.test.mjs b/web/tests/application-approval-info.test.mjs index 38893e2..d006334 100644 --- a/web/tests/application-approval-info.test.mjs +++ b/web/tests/application-approval-info.test.mjs @@ -83,9 +83,12 @@ test('buildLeaderApprovalEvents returns leader return and approval timeline in e assert.deepEqual(events.map((event) => event.type), ['returned', 'approved']) assert.deepEqual(events.map((event) => event.tone), ['danger', 'success']) assert.equal(events[0].operator, 'Leader Li') + assert.equal(events[0].role, '直属领导审批节点') assert.equal(events[0].opinion, 'Need clearer budget explanation.') assert.equal(events[0].returnCount, 1) assert.equal(events[0].time, '2026-05-25 09:00') + assert.equal(events[0].dateLabel, '2026-05-25') + assert.equal(events[0].timeLabel, '09:00') assert.equal(Object.hasOwn(events[0], 'sortAt'), false) }) diff --git a/web/tests/attachment-association-confirmation.test.mjs b/web/tests/attachment-association-confirmation.test.mjs index 4c35d03..ea638c9 100644 --- a/web/tests/attachment-association-confirmation.test.mjs +++ b/web/tests/attachment-association-confirmation.test.mjs @@ -210,6 +210,47 @@ test('OCR receipt folder ids are kept for final draft attachment association', ( assert.equal(Object.getOwnPropertyDescriptor(files[0], 'receiptId')?.enumerable, false) }) +test('OCR documents keep full recognized text for backend context', () => { + const longText = [ + '增值税电子发票', + '购买方名称:远光软件股份有限公司', + '销售方名称:上海高铁服务有限公司', + '项目名称:客运服务', + '出发地:武汉', + '到达地:上海', + '乘车日期:2026-02-20', + '车次:G1234', + '座位等级:二等座', + '金额:354.00元', + '税额:10.62元', + '发票号码:12345678901234567890', + '开票日期:2026-02-21', + '购买方纳税人识别号:91440400618256625E', + '销售方纳税人识别号:91310000132234123X', + '备注:本票据用于差旅报销,请核对出发城市、到达城市、车次、座位等级、金额、税额和电子客票号。', + '电子客票号:E1234567890' + ].join('\n') + + assert.ok(longText.length > 240) + + const documents = normalizeOcrDocuments({ + documents: [ + { + filename: 'train-ticket.pdf', + text: longText, + summary: '铁路电子客票 武汉-上海', + document_fields: [ + { key: 'amount', label: '金额', value: '354.00元' }, + { key: 'ticket_no', label: '电子客票号', value: 'E1234567890' } + ] + } + ] + }) + + assert.equal(documents[0].text, longText) + assert.match(documents[0].text, /电子客票号:E1234567890/) +}) + test('receipt files are collected through a single OCR persistence entry before draft association', async () => { const files = [ { name: 'invoice.png' } diff --git a/web/tests/travel-request-detail-leader-approval.test.mjs b/web/tests/travel-request-detail-leader-approval.test.mjs index 47ada28..dc58499 100644 --- a/web/tests/travel-request-detail-leader-approval.test.mjs +++ b/web/tests/travel-request-detail-leader-approval.test.mjs @@ -129,11 +129,20 @@ test('approval-mode detail collects leader opinion inside confirm dialog before assert.match(detailTemplate, /v-for="event in leaderApprovalEvents"/) assert.match(detailTemplate, /class="application-leader-opinion-event"/) assert.match(detailTemplate, /event\.type === 'returned'/) + assert.match(detailTemplate, /class="application-leader-opinion-event-time"/) + assert.match(detailTemplate, /event\.dateLabel/) + assert.match(detailTemplate, /event\.timeLabel/) + assert.match(detailTemplate, /class="application-leader-opinion-event-rail"/) assert.match(detailTemplate, /class="application-leader-opinion-event-status"/) - assert.match(detailTemplate, /class="application-leader-opinion-event-body"/) - assert.match(detailTemplate, /审批意见/) + assert.match(detailTemplate, /:title="event\.title"/) + assert.match(detailTemplate, /class="application-leader-opinion-record"/) + assert.match(detailTemplate, /class="application-leader-opinion-record-head"/) + assert.match(detailTemplate, /class="application-leader-opinion-record-title"/) + assert.match(detailTemplate, /class="application-leader-opinion-record-meta"/) + assert.match(detailTemplate, /event\.operator/) + assert.match(detailTemplate, /event\.role/) + assert.match(detailTemplate, /event\.opinion/) assert.match(detailTemplate, /class="application-leader-opinion-event-foot"/) - assert.match(detailTemplate, /class="application-leader-opinion-operator"/) assert.doesNotMatch(detailTemplate, /leaderApprovalReadonlyText/) assert.doesNotMatch(detailTemplate, /\u5f85\u76f4\u5c5e\u9886\u5bfc\u586b\u5199\u5ba1\u6279\u610f\u89c1/) assert.match(detailTemplate, /领导意见/) @@ -200,22 +209,33 @@ test('approval-mode detail collects leader opinion inside confirm dialog before assert.match(confirmDialog, /\.shared-confirm-card--approval \.shared-confirm-body \{[\s\S]*max-height: min\(270px, calc\(100dvh - 238px\)\);/) assert.match(confirmDialog, /\.shared-confirm-card--approval \.shared-confirm-btn \{[\s\S]*min-width: 118px;[\s\S]*min-height: 38px;/) + const leaderOpinionPanelRule = detailStyles.match(/^\.application-leader-opinion \{[\s\S]*?\n\}/m)?.[0] ?? '' + const leaderOpinionTimelineRule = detailStyles.match(/^\.application-leader-opinion-timeline \{[\s\S]*?\n\}/m)?.[0] ?? '' + const leaderOpinionEventRule = detailStyles.match(/^\.application-leader-opinion-event \{[\s\S]*?\n\}/m)?.[0] ?? '' + assert.match(detailStyles, /\.detail-card-title-with-icon \{[\s\S]*display: inline-flex;[\s\S]*align-items: center;[\s\S]*gap: 8px;/) assert.match(detailStyles, /\.detail-card-title-with-icon i \{[\s\S]*font-size: 18px;[\s\S]*line-height: 1;/) assert.match(detailStyles, /\.application-leader-opinion-head span \{[\s\S]*display: inline-flex;[\s\S]*align-items: center;[\s\S]*gap: 8px;/) + assert.match(leaderOpinionPanelRule, /border-top: 1px solid #e5edf5;/) + assert.match(leaderOpinionPanelRule, /background: #ffffff;/) + assert.doesNotMatch(leaderOpinionPanelRule, /linear-gradient|box-shadow/) assert.doesNotMatch(detailStyles, /\.leader-approval-card/) assert.doesNotMatch(detailStyles, /\.inline-leader-opinion/) - assert.match(detailStyles, /\.application-leader-opinion-timeline \{/) - assert.match(detailStyles, /\.application-leader-opinion-timeline\.is-single \{[\s\S]*padding-left: 0;/) - assert.match(detailStyles, /\.application-leader-opinion-timeline\.is-single::before,[\s\S]*\.application-leader-opinion-timeline\.is-single \.application-leader-opinion-event::before \{[\s\S]*display: none;/) + assert.match(leaderOpinionTimelineRule, /gap: 0;/) + assert.match(leaderOpinionTimelineRule, /padding: 2px 0 0;/) assert.match(detailStyles, /\.application-leader-opinion-event \{/) - assert.match(detailStyles, /\.application-leader-opinion-event \{[\s\S]*border-left: 4px solid var\(--leader-opinion-tone/) - assert.match(detailStyles, /\.application-leader-opinion-event-status \{[\s\S]*border-radius: 999px;/) - assert.match(detailStyles, /\.application-leader-opinion-event-body \{[\s\S]*background: var\(--leader-opinion-soft-bg/) - assert.match(detailStyles, /\.application-leader-opinion-event-body p \{[\s\S]*font-size: 15px;/) - assert.match(detailStyles, /\.application-leader-opinion-event-foot span \{[\s\S]*border-radius: 999px;/) - assert.match(detailStyles, /\.application-leader-opinion-event\.danger::before \{/) - assert.match(detailStyles, /\.application-leader-opinion-event\.success::before \{/) + assert.match(leaderOpinionEventRule, /grid-template-columns: 104px 28px minmax\(0, 1fr\);/) + assert.match(leaderOpinionEventRule, /padding: 0 0 16px;/) + assert.doesNotMatch(leaderOpinionEventRule, /linear-gradient|box-shadow/) + assert.match(detailStyles, /\.application-leader-opinion-event-time \{[\s\S]*justify-items: end;[\s\S]*font-variant-numeric: tabular-nums;/) + assert.match(detailStyles, /\.application-leader-opinion-event-rail \{[\s\S]*justify-content: center;/) + assert.match(detailStyles, /\.application-leader-opinion-event-rail::after \{[\s\S]*width: 2px;[\s\S]*background: #e2e8f0;/) + assert.match(detailStyles, /\.application-leader-opinion-event-status \{[\s\S]*width: 22px;[\s\S]*height: 22px;[\s\S]*border-radius: 999px;/) + assert.match(detailStyles, /\.application-leader-opinion-record \{[\s\S]*border-bottom: 1px solid #edf2f7;[\s\S]*background: #ffffff;/) + assert.match(detailStyles, /\.application-leader-opinion-record-meta \{[\s\S]*display: flex;[\s\S]*gap: 6px 16px;/) + assert.match(detailStyles, /\.application-leader-opinion-record p \{[\s\S]*background: #f8fafc;[\s\S]*font-size: 14px;/) + assert.match(detailStyles, /\.application-leader-opinion-event\.danger \{[\s\S]*--leader-opinion-tone: #dc2626;/) + assert.match(detailStyles, /\.application-leader-opinion-event\.success \{[\s\S]*--leader-opinion-tone: #16a34a;/) assert.match(reimbursementService, /export function approveExpenseClaim\(claimId, payload = \{\}\)/) assert.match(reimbursementService, /\/approve/)