Repository navigation
feat(T95-T98): import receipt approval UI (list, detail, approve, rej… - #33
Conversation
…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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThêm 4 hàm service cho luồng duyệt phiếu nhập và viết lại ChangesLuồng duyệt phiếu nhập
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
src/services/importReceiptService.jssrc/views/ApprovalsView.vue
| 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.') | ||
| } |
There was a problem hiding this comment.
🩺 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
| 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 |
There was a problem hiding this comment.
🗄️ 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 = falseAs 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
| 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 = '' | ||
| } |
There was a problem hiding this comment.
🗄️ 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
…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