Skip to content

feat(T95-T98): import receipt approval UI (list, detail, approve, rej… - #33

Merged
Tonfa137206 merged 2 commits into
devfrom
feature/T95-import-receipt-approval-ui
Jun 29, 2026
Merged

Tonfa137206 merged 2 commits into
devfrom
feature/T95-import-receipt-approval-ui

Conversation

@Tonfa137206

@Tonfa137206 Tonfa137206 commented Jun 29, 2026 •

Copy link
Copy Markdown
Collaborator

…ect modal)

  • T95: pending-approval list view with status filter and pagination

  • T96: approval detail modal (info + product table)

  • T97: approve integration (two-level) with refresh

  • T98: reject modal with required-reason validation

  • add approval service functions in importReceiptService.js

Summary by CodeRabbit

  • Tính năng mới
    • Thêm màn hình quản lý phiếu nhập chờ duyệt, hỗ trợ xem danh sách, lọc theo trạng thái và phân trang.
    • Bổ sung xem chi tiết phiếu, thao tác duyệt và từ chối trực tiếp trên giao diện.
    • Khi từ chối, yêu cầu nhập lý do và kiểm tra hợp lệ trước khi gửi.
  • Cải tiến
    • Tự động làm mới danh sách sau khi duyệt/từ chối và hiển thị thông báo kết quả rõ ràng.
    • Tự điều hướng về đăng nhập khi phiên hết hạn (khi gọi API).

…ect modal)

- T95: pending-approval list view with status filter and pagination

- T96: approval detail modal (info + product table)

- T97: approve integration (two-level) with refresh

- T98: reject modal with required-reason validation

- add approval service functions in importReceiptService.js
@coderabbitai

coderabbitai Bot commented Jun 29, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ee530709-2000-443b-8bbb-1f97a57495f2

📥 Commits

Reviewing files that changed from the base of the PR and between 11e1cd2 and a9534c7.

📒 Files selected for processing (1)
  • src/services/importReceiptService.js

📝 Walkthrough

Walkthrough

Thêm 4 hàm service cho luồng duyệt phiếu nhập và viết lại ApprovalsView.vue từ màn hình tĩnh thành màn hình có danh sách, chi tiết, duyệt, từ chối, phân trang và lọc trạng thái.

Changes

Luồng duyệt phiếu nhập

Layer / File(s) Summary
4 API service duyệt/từ chối
src/services/importReceiptService.js
Thêm getPendingApprovals, getApprovalDetail, approveImportReceipt, rejectImportReceipt; tất cả dùng importReceiptClient, getAuthorizationHeader, và normalizeImportReceiptError.
Script setup ApprovalsView
src/views/ApprovalsView.vue
State phân trang/lọc/trạng thái thao tác, tải danh sách khi mount, mở modal chi tiết, duyệt, validate + submit từ chối, chuyển /login khi 401, helper format.
Template, modal và CSS ApprovalsView
src/views/ApprovalsView.vue
Filter bar, bảng phiếu + phân trang, modal chi tiết có nút duyệt/từ chối khi chờ duyệt, modal nhập lý do từ chối, và CSS scoped responsive.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Tiêu đề bám sát thay đổi chính: thêm UI duyệt phiếu nhập gồm danh sách, chi tiết, duyệt và từ chối.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/T95-import-receipt-approval-ui

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/services/importReceiptService.js`:
- Around line 97-145: Thêm cấu hình timeout cho importReceiptClient để các API
duyệt/từ chối không bị treo khi mạng kẹt. Cập nhật client khởi tạo (nơi tạo
importReceiptClient) để đặt timeout mặc định phù hợp, và đảm bảo các hàm
getPendingApprovals, getApprovalDetail, approveImportReceipt,
rejectImportReceipt vẫn dùng client đó. Nếu cần, xử lý lỗi timeout qua
normalizeImportReceiptError để UI nhận được thông báo rõ ràng thay vì đứng ở
trạng thái loading.

In `@src/views/ApprovalsView.vue`:
- Around line 130-149: The approval/rejection flow in ApprovalsView is not
locked while an action is in progress, so users can trigger duplicate or
conflicting requests from other rows or the detail modal. Update the action
handlers (handleApprove, handleReject, and the shared action state usage) to
guard against concurrent actions by disabling the relevant buttons/UI whenever
actionState.action is set and actionState.receiptId is active, and keep the
modal controls and row actions in sync with that pending state.
- Around line 61-80: The async approval/receipt loading flow in ApprovalsView
can let an older request overwrite newer state when filters, pagination, or
detail views change quickly. Update fetchPendingApprovals and the related
detail-loading path so each request is tracked/cancelled (or guarded with a
request token/version) and only the latest response is allowed to assign
receipts, totalPages, totalElements, or detail state. Make sure the symbols
fetchPendingApprovals and the detail-fetch method used around the receipt detail
view both ignore stale responses before mutating reactive state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: eba87720-a43d-49b3-a645-04c65bcbc5c4

📥 Commits

Reviewing files that changed from the base of the PR and between 39186d5 and 11e1cd2.

📒 Files selected for processing (2)
  • src/services/importReceiptService.js
  • src/views/ApprovalsView.vue

Comment on lines +97 to +145
export async function getPendingApprovals({ page = 0, size = 10, status = '' } = {}) {
try {
const { data } = await importReceiptClient.get('/api/import-receipts/pending-approval', {
headers: getAuthorizationHeader(),
params: {
page,
size,
status: status || undefined,
},
})
return data
} catch (error) {
throw normalizeImportReceiptError(error, 'Không thể tải danh sách phiếu chờ duyệt.')
}
}

export async function getApprovalDetail(receiptId) {
try {
const { data } = await importReceiptClient.get(`/api/import-receipts/${receiptId}/approval-detail`, {
headers: getAuthorizationHeader(),
})
return data
} catch (error) {
throw normalizeImportReceiptError(error, 'Không thể tải chi tiết phiếu chờ duyệt.')
}
}

export async function approveImportReceipt(receiptId) {
try {
const { data } = await importReceiptClient.put(`/api/import-receipts/${receiptId}/approve`, null, {
headers: getAuthorizationHeader(),
})
return data
} catch (error) {
throw normalizeImportReceiptError(error, 'Không thể duyệt phiếu nhập.')
}
}

export async function rejectImportReceipt(receiptId, reason) {
try {
const { data } = await importReceiptClient.put(
`/api/import-receipts/${receiptId}/reject`,
{ reason },
{ headers: getAuthorizationHeader() },
)
return data
} catch (error) {
throw normalizeImportReceiptError(error, 'Không thể từ chối phiếu nhập.')
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Thêm timeout cho các API duyệt/từ chối.

Các hàm mới dùng importReceiptClient, nhưng client hiện chưa cấu hình timeout ở Line 6-10. Nếu kết nối treo, UI có thể kẹt ở trạng thái đang tải/đang gửi.

Gợi ý sửa nhanh
 const importReceiptClient = axios.create({
   baseURL: API_BASE_URL,
+  timeout: 15000,
   headers: {
     'Content-Type': 'application/json',
   },
 })

As per path instructions, src/services/**: “Kiểm tra loading/timeout/network error/HTTP error”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/importReceiptService.js` around lines 97 - 145, Thêm cấu hình
timeout cho importReceiptClient để các API duyệt/từ chối không bị treo khi mạng
kẹt. Cập nhật client khởi tạo (nơi tạo importReceiptClient) để đặt timeout mặc
định phù hợp, và đảm bảo các hàm getPendingApprovals, getApprovalDetail,
approveImportReceipt, rejectImportReceipt vẫn dùng client đó. Nếu cần, xử lý lỗi
timeout qua normalizeImportReceiptError để UI nhận được thông báo rõ ràng thay
vì đứng ở trạng thái loading.

Source: Path instructions

Comment on lines +61 to +80
async function fetchPendingApprovals() {
isLoading.value = true
errorMessage.value = ''
actionMessage.value = ''
actionErrorMessage.value = ''
try {
const data = await getPendingApprovals({
page: page.value,
size: size.value,
status: filters.status,
})
receipts.value = data.content || []
totalPages.value = data.totalPages || 0
totalElements.value = data.totalElements || 0
} catch (error) {
receipts.value = []
errorMessage.value = error.message
if (error.status === 401) router.replace('/login')
} finally {
isLoading.value = false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Chặn response cũ ghi đè state mới.

Nếu người dùng đổi lọc/chuyển trang hoặc mở chi tiết nhiều phiếu liên tiếp, request cũ có thể về sau và ghi đè danh sách/chi tiết hiện tại. Trường hợp chi tiết bị ghi đè có thể dẫn tới duyệt nhầm phiếu.

Gợi ý sửa nhanh
+let listRequestId = 0
+let detailRequestId = 0
+
 async function fetchPendingApprovals() {
+  const requestId = ++listRequestId
   isLoading.value = true
   errorMessage.value = ''
   actionMessage.value = ''
   actionErrorMessage.value = ''
   try {
     const data = await getPendingApprovals({
       page: page.value,
       size: size.value,
       status: filters.status,
     })
+    if (requestId !== listRequestId) return
     receipts.value = data.content || []
     totalPages.value = data.totalPages || 0
     totalElements.value = data.totalElements || 0
   } catch (error) {
+    if (requestId !== listRequestId) return
     receipts.value = []
     errorMessage.value = error.message
     if (error.status === 401) router.replace('/login')
   } finally {
-    isLoading.value = false
+    if (requestId === listRequestId) isLoading.value = false
   }
 }
 
 async function openDetail(receipt) {
+  const requestId = ++detailRequestId
   detailState.open = true
   detailState.loading = true
   detailState.error = ''
   detailState.receipt = null
   try {
-    detailState.receipt = await getApprovalDetail(receipt.id)
+    const detail = await getApprovalDetail(receipt.id)
+    if (requestId !== detailRequestId) return
+    detailState.receipt = detail
   } catch (error) {
+    if (requestId !== detailRequestId) return
     detailState.error = error.message || 'Không thể tải chi tiết phiếu.'
     if (error.status === 401) router.replace('/login')
   } finally {
-    detailState.loading = false
+    if (requestId === detailRequestId) detailState.loading = false
   }
 }
 
 function closeDetail() {
+  detailRequestId += 1
   detailState.open = false

As per path instructions, src/views/**: “Kiểm tra loading, empty state, error state…” và **/*: “Ưu tiên lỗi ảnh hưởng thật: correctness… async flow, state consistency”.

Also applies to: 108-120

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/views/ApprovalsView.vue` around lines 61 - 80, The async approval/receipt
loading flow in ApprovalsView can let an older request overwrite newer state
when filters, pagination, or detail views change quickly. Update
fetchPendingApprovals and the related detail-loading path so each request is
tracked/cancelled (or guarded with a request token/version) and only the latest
response is allowed to assign receipts, totalPages, totalElements, or detail
state. Make sure the symbols fetchPendingApprovals and the detail-fetch method
used around the receipt detail view both ignore stale responses before mutating
reactive state.

Source: Path instructions

Comment on lines +130 to +149
async function handleApprove(receipt) {
if (!isPendingApproval(receipt.status)) return
if (!window.confirm(`${approveLabel(receipt.status)} phiếu ${receipt.code}?`)) return

actionState.receiptId = receipt.id
actionState.action = 'approve'
actionMessage.value = ''
actionErrorMessage.value = ''
try {
await approveImportReceipt(receipt.id)
closeDetail()
await fetchPendingApprovals()
actionMessage.value = `Đã duyệt phiếu ${receipt.code} thành công.`
} catch (error) {
actionErrorMessage.value = error.message || 'Thao tác thất bại, vui lòng thử lại.'
if (error.status === 401) router.replace('/login')
} finally {
actionState.receiptId = null
actionState.action = ''
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Khóa thao tác duyệt/từ chối khi đang có action chạy.

actionState chỉ lưu 1 phiếu, nhưng UI vẫn cho bấm action ở phiếu khác và modal detail không disable nút. Với API duyệt/từ chối là thao tác đổi trạng thái, double-click hoặc chạy song song có thể gửi request trùng/sai cấp.

Gợi ý sửa nhanh
 const hasNextPage = computed(() => page.value + 1 < totalPages.value)
+const hasActionInProgress = computed(() => actionState.receiptId !== null || rejectState.submitting)
 
 async function handleApprove(receipt) {
+  if (hasActionInProgress.value) return
   if (!isPendingApproval(receipt.status)) return
   if (!window.confirm(`${approveLabel(receipt.status)} phiếu ${receipt.code}?`)) return
 
 function openRejectModal(receipt) {
+  if (hasActionInProgress.value || !isPendingApproval(receipt.status)) return
   rejectState.open = true
-<button class="btn btn-sm" type="button" :disabled="isAnyActionRunning(row)" `@click`="openDetail(row)">Xem</button>
-<button class="btn btn-sm btn-primary" type="button" :disabled="isAnyActionRunning(row)" `@click`="handleApprove(row)">
+<button class="btn btn-sm" type="button" :disabled="hasActionInProgress" `@click`="openDetail(row)">Xem</button>
+<button class="btn btn-sm btn-primary" type="button" :disabled="hasActionInProgress" `@click`="handleApprove(row)">
   {{ isActionRunning(row, 'approve') ? 'Đang duyệt...' : approveLabel(row.status) }}
 </button>
-<button class="btn btn-sm btn-danger" type="button" :disabled="isAnyActionRunning(row)" `@click`="openRejectModal(row)">Từ chối</button>
+<button class="btn btn-sm btn-danger" type="button" :disabled="hasActionInProgress" `@click`="openRejectModal(row)">Từ chối</button>
-<button class="btn btn-danger" type="button" `@click`="openRejectModal(detailState.receipt)">Từ chối</button>
-<button class="btn btn-primary" type="button" `@click`="handleApprove(detailState.receipt)">{{ approveLabel(detailState.receipt.status) }}</button>
+<button class="btn btn-danger" type="button" :disabled="hasActionInProgress" `@click`="openRejectModal(detailState.receipt)">Từ chối</button>
+<button class="btn btn-primary" type="button" :disabled="hasActionInProgress" `@click`="handleApprove(detailState.receipt)">{{ approveLabel(detailState.receipt.status) }}</button>

As per path instructions, **/*: “Ưu tiên lỗi ảnh hưởng thật: correctness… async flow, state consistency”.

Also applies to: 153-159, 264-270, 339-341

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/views/ApprovalsView.vue` around lines 130 - 149, The approval/rejection
flow in ApprovalsView is not locked while an action is in progress, so users can
trigger duplicate or conflicting requests from other rows or the detail modal.
Update the action handlers (handleApprove, handleReject, and the shared action
state usage) to guard against concurrent actions by disabling the relevant
buttons/UI whenever actionState.action is set and actionState.receiptId is
active, and keep the modal controls and row actions in sync with that pending
state.

Source: Path instructions

…eipt-approval-ui to resolve conflict in importReceiptService.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant