Allow independent dates while ARR field reviews are pending
This commit is contained in:
1 parent
fe8735aac2
commit
fb07580c0c
15 files changed
+316
-16
No files matched your search
@@ -12,6 +12,9 @@
|
||||
点击日期文字或右侧日历图标,再点击日历中的某一天即可选择。任务进行中仍可预选下一日期;
|
||||
当前任务结束后需再次点击下载,预选不会自动提交或修改原任务。页面进度始终显示原任务的日期。
|
||||
任务结果待确认时,先检查或继续按钮上标明日期的原任务,避免重复提交。
|
||||
某一天等待人工完善不阻塞其他日期:另选日期后可点击“下载并处理”,此前任务仍保存在“待完善日期”中。
|
||||
点击待完善日期可返回该日继续,刷新后列表仍保留;同一天的未完成任务沿用原编号。
|
||||
页面内切换日期会分别保留尚未保存的输入草稿,刷新前仍应逐项保存;已保存的决定由系统长期保留。
|
||||
完整采集中的缺字段、多个候选或不满足原规则的字段先进入“待完善数据”(`needs_data_review`)。
|
||||
全部阻塞字段通过并确认后,原任务自动继续处理;随后只有缺少pureprice时进入原有价格复核(`needs_review`)。
|
||||
字段完善和价格复核是前后两步,不会用补零或未经确认的空值跳过前一步。
|
||||
|
||||
@@ -368,6 +368,8 @@ class PortalApplication:
|
||||
"default_date": default_report_date(),
|
||||
"business_time_zone": "Asia/Bangkok",
|
||||
"latest_task": self._arr_download_snapshot(self._arr_downloads.latest()),
|
||||
"pending_data_reviews": [self._arr_download_snapshot(task) for task in
|
||||
getattr(self._arr_downloads, "pending_data_reviews", lambda: [])()],
|
||||
}))
|
||||
if method == "POST" and route.path == "/api/arr-downloads":
|
||||
self._require_csrf(normalized_headers)
|
||||
|
||||
@@ -91,6 +91,7 @@ class ARRDownloadCoordinator(Protocol):
|
||||
@property
|
||||
def ready(self) -> bool: ...
|
||||
def latest(self) -> dict | None: ...
|
||||
def pending_data_reviews(self) -> list[dict]: ...
|
||||
def create(self, report_date: str, request_id: str) -> dict: ...
|
||||
def get(self, request_id: str) -> dict: ...
|
||||
def retry(self, request_id: str) -> dict: ...
|
||||
@@ -105,6 +106,9 @@ class UnavailableARRDownloads:
|
||||
def latest(self) -> None:
|
||||
return None
|
||||
|
||||
def pending_data_reviews(self) -> list[dict]:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _unavailable() -> PortalError:
|
||||
return PortalError("ARR_DOWNLOAD_UNAVAILABLE", "自动下载服务暂未就绪", 503)
|
||||
@@ -198,6 +202,12 @@ class PersistentARRDownloads:
|
||||
row = db.execute("SELECT * FROM downloads ORDER BY created_at DESC, rowid DESC LIMIT 1").fetchone()
|
||||
return self._public(row) if row else None
|
||||
|
||||
def pending_data_reviews(self) -> list[dict]:
|
||||
with self._connect() as db:
|
||||
rows = db.execute("""SELECT * FROM downloads WHERE status='needs_data_review'
|
||||
ORDER BY report_date DESC, created_at, request_id""").fetchall()
|
||||
return [self._public(row) for row in rows]
|
||||
|
||||
def get(self, request_id: str) -> dict:
|
||||
validate_request_id(request_id)
|
||||
with self._connect() as db:
|
||||
|
||||
@@ -88,6 +88,7 @@ class PermissionDownloads:
|
||||
return self.queue.ready and self.access.status()["ready"]
|
||||
|
||||
def latest(self): return self.queue.latest()
|
||||
def pending_data_reviews(self): return self.queue.pending_data_reviews()
|
||||
def get(self, request_id): return self.queue.get(request_id)
|
||||
|
||||
@property
|
||||
|
||||
@@ -129,6 +129,9 @@ class SourceDateDownloads:
|
||||
def latest(self):
|
||||
return self.queue.latest()
|
||||
|
||||
def pending_data_reviews(self):
|
||||
return self.queue.pending_data_reviews()
|
||||
|
||||
def get(self, request_id):
|
||||
return self.queue.get(request_id)
|
||||
|
||||
|
||||
+76
-7
@@ -24,6 +24,7 @@
|
||||
arrDownloadStorageKey: "",
|
||||
arrDownloadUsername: "",
|
||||
arrDownloadContextId: "",
|
||||
arrDownloadPendingReviews: [],
|
||||
arrDataReview: null,
|
||||
arrDataReviewRequestId: "",
|
||||
arrDataReviewLoading: false,
|
||||
@@ -31,6 +32,7 @@
|
||||
arrDataReviewDisconnected: false,
|
||||
arrDataReviewFinalizing: false,
|
||||
arrDataReviewDrafts: {},
|
||||
arrDataReviewDraftsByRequest: {},
|
||||
arrDataReviewMessage: "",
|
||||
arrDataReviewMessageError: false,
|
||||
companySourceFile: null,
|
||||
@@ -322,6 +324,53 @@
|
||||
return state.arrDownloadTask?.status === "needs_data_review";
|
||||
}
|
||||
|
||||
function selectedARRDateNeedsDataReview() {
|
||||
return arrDownloadNeedsDataReview() && $("#arr-download-date").value === state.arrDownloadTask.report_date;
|
||||
}
|
||||
|
||||
function rememberARRPendingReview(task) {
|
||||
state.arrDownloadPendingReviews = state.arrDownloadPendingReviews.filter((item) => item.request_id !== task.request_id);
|
||||
if (task.status === "needs_data_review") state.arrDownloadPendingReviews.push(task);
|
||||
state.arrDownloadPendingReviews.sort((a, b) => b.report_date.localeCompare(a.report_date) || a.request_id.localeCompare(b.request_id));
|
||||
}
|
||||
|
||||
function renderARRPendingReviews() {
|
||||
const panel = $("#arr-download-pending");
|
||||
panel.hidden = !state.arrDownloadPendingReviews.length;
|
||||
$("#arr-download-pending-title").textContent = I18N.t("arr_download.pending_dates");
|
||||
const locked = state.arrDownloadBusy || state.arrDownloadLoading || state.arrDataReviewLoading || state.arrDataReviewMutating
|
||||
|| state.arrDataReviewFinalizing || arrDownloadActive() || arrDownloadNeedsReconciliation();
|
||||
$("#arr-download-pending-list").innerHTML = state.arrDownloadPendingReviews.map((task) =>
|
||||
`<button type="button" data-arr-pending-review="${escapeHtml(task.request_id)}" aria-pressed="${state.arrDownloadTask?.request_id === task.request_id}" ${locked ? "disabled" : ""}>${escapeHtml(task.report_date)} · ${escapeHtml(I18N.t("arr_download.pending_date"))}</button>`).join("");
|
||||
}
|
||||
|
||||
async function selectARRPendingReview(requestId) {
|
||||
if (!state.arrDownloadPendingReviews.some((task) => task.request_id === requestId)
|
||||
|| state.arrDownloadBusy || state.arrDownloadLoading || state.arrDataReviewLoading || state.arrDataReviewMutating
|
||||
|| state.arrDataReviewFinalizing || arrDownloadActive() || arrDownloadNeedsReconciliation()) return;
|
||||
const contextId = state.arrDownloadContextId;
|
||||
state.arrDownloadBusy = true;
|
||||
renderARRDownload();
|
||||
try {
|
||||
const task = await api(`/api/arr-downloads/${encodeURIComponent(requestId)}`);
|
||||
if (state.arrDownloadContextId !== contextId) return;
|
||||
state.arrDownloadDateEdited = false;
|
||||
$("#arr-download-date").value = task.report_date;
|
||||
await acceptARRDownloadTask(task, { sync: false });
|
||||
if (arrDownloadNeedsDataReview()) {
|
||||
$("#arr-data-review-panel").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
$("#arr-data-review-panel").focus({ preventScroll: true });
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(error.message, true);
|
||||
} finally {
|
||||
state.arrDownloadBusy = false;
|
||||
renderARRDataReview();
|
||||
renderARRDownload();
|
||||
scheduleARRDownloadPoll();
|
||||
}
|
||||
}
|
||||
|
||||
function handleARRDownloadDateChange() {
|
||||
state.arrDownloadDateEdited = true;
|
||||
// Choosing the next report date must not abandon a running or uncertain request.
|
||||
@@ -347,6 +396,7 @@
|
||||
$("#arr-date-toggle").setAttribute("aria-label", I18N.t("arr_download.choose_date"));
|
||||
$("#arr-download-help").textContent = I18N.t(locked ? "arr_download.submitting"
|
||||
: arrDownloadActive() ? "arr_download.date_next"
|
||||
: arrDownloadNeedsDataReview() ? "arr_download.date_independent"
|
||||
: reconciling ? "arr_download.date_resume" : "arr_download.help",
|
||||
{ date: state.arrDownloadIntent?.report_date });
|
||||
if (locked) closeARRCalendar();
|
||||
@@ -358,7 +408,7 @@
|
||||
if (state.arrDownloadBusy) label = "arr_download.submitting";
|
||||
else if (arrDownloadActive()) label = "arr_download.running";
|
||||
else if (pending || state.arrDownloadDisconnected) label = "arr_download.check";
|
||||
else if (arrDownloadNeedsDataReview()) label = "arr_download.complete_data";
|
||||
else if (selectedARRDateNeedsDataReview()) label = "arr_download.complete_data";
|
||||
else if (arrDownloadCanResume() || task?.status === "not_received") label = "arr_download.retry_date";
|
||||
button.textContent = I18N.t(label, { date: task?.report_date });
|
||||
button.setAttribute("aria-busy", String(state.arrDownloadBusy || arrDownloadActive()));
|
||||
@@ -375,6 +425,7 @@
|
||||
$("#arr-download-review").hidden = task?.status !== "needs_review" || !task.job_id;
|
||||
$("#arr-download-data-review").hidden = !arrDownloadNeedsDataReview();
|
||||
$("#arr-download-data-review").disabled = locked;
|
||||
renderARRPendingReviews();
|
||||
}
|
||||
|
||||
function clearARRDownloadPoll() {
|
||||
@@ -394,6 +445,7 @@
|
||||
|
||||
async function acceptARRDownloadTask(task, { sync = true } = {}) {
|
||||
state.arrDownloadTask = task;
|
||||
rememberARRPendingReview(task);
|
||||
state.arrDownloadDisconnected = false;
|
||||
rememberARRIntent({ request_id: task.request_id, report_date: task.report_date });
|
||||
if (!state.arrDownloadDateEdited) $("#arr-download-date").value = task.report_date;
|
||||
@@ -465,8 +517,13 @@
|
||||
state.arrDownloadTask = null;
|
||||
state.arrDownloadDisconnected = false;
|
||||
state.arrDownloadSynced = "";
|
||||
resetARRDataReview();
|
||||
state.arrDownloadPendingReviews = [];
|
||||
resetARRDataReview({ clearDrafts: true });
|
||||
}
|
||||
state.arrDownloadPendingReviews = Array.isArray(config.pending_data_reviews)
|
||||
? config.pending_data_reviews.filter((task) => task.status === "needs_data_review"
|
||||
&& /^[0-9a-f]{32}$/.test(task.request_id) && validARRDate(task.report_date)) : [];
|
||||
if (state.arrDownloadTask) rememberARRPendingReview(state.arrDownloadTask);
|
||||
if (!state.arrDownloadRestored) {
|
||||
state.arrDownloadRestored = true;
|
||||
let saved;
|
||||
@@ -505,7 +562,7 @@
|
||||
await loadARRDownloadTask();
|
||||
return;
|
||||
}
|
||||
if (arrDownloadNeedsDataReview()) {
|
||||
if (selectedARRDateNeedsDataReview()) {
|
||||
await loadARRDataReview(state.arrDownloadTask.request_id, { scroll: true });
|
||||
return;
|
||||
}
|
||||
@@ -522,6 +579,7 @@
|
||||
}
|
||||
rememberARRIntent(intent); // Persist before the HTTP call, including a lost response.
|
||||
state.arrDownloadTask = null;
|
||||
resetARRDataReview();
|
||||
state.arrDownloadBusy = true;
|
||||
clearARRDownloadPoll();
|
||||
renderARRDownload();
|
||||
@@ -553,7 +611,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function resetARRDataReview() {
|
||||
function resetARRDataReview({ clearDrafts = false } = {}) {
|
||||
if (clearDrafts) state.arrDataReviewDraftsByRequest = {};
|
||||
else if (state.arrDataReviewRequestId) {
|
||||
state.arrDataReviewDraftsByRequest[state.arrDataReviewRequestId] = { ...state.arrDataReviewDrafts };
|
||||
}
|
||||
state.arrDataReview = null;
|
||||
state.arrDataReviewRequestId = "";
|
||||
state.arrDataReviewLoading = false;
|
||||
@@ -572,7 +634,8 @@
|
||||
}
|
||||
|
||||
function arrDataReviewCanFinalize(review = state.arrDataReview) {
|
||||
return Boolean(review && review.status === "editing" && review.can_finalize && Number(review.pending_count) === 0
|
||||
return Boolean(arrDownloadNeedsDataReview() && state.arrDownloadTask.request_id === state.arrDataReviewRequestId
|
||||
&& !state.arrDownloadBusy && review && review.status === "editing" && review.can_finalize && Number(review.pending_count) === 0
|
||||
&& Array.isArray(review.items) && review.items.every((item) => item.confirmed)
|
||||
&& !Object.keys(state.arrDataReviewDrafts).length && !state.arrDataReviewLoading && !state.arrDataReviewMutating
|
||||
&& !state.arrDataReviewDisconnected && !state.arrDataReviewFinalizing);
|
||||
@@ -589,7 +652,8 @@
|
||||
function renderARRDataReview() {
|
||||
const panel = $("#arr-data-review-panel");
|
||||
const review = state.arrDataReview;
|
||||
panel.hidden = !state.arrDataReviewRequestId || !arrDownloadNeedsDataReview();
|
||||
panel.hidden = !state.arrDataReviewRequestId || !arrDownloadNeedsDataReview()
|
||||
|| state.arrDownloadTask.request_id !== state.arrDataReviewRequestId;
|
||||
if (panel.hidden) return;
|
||||
const busy = state.arrDataReviewLoading || state.arrDataReviewMutating;
|
||||
const editable = review?.status === "editing" && !busy && !state.arrDataReviewDisconnected && !state.arrDataReviewFinalizing;
|
||||
@@ -637,6 +701,7 @@
|
||||
if (state.arrDataReviewRequestId !== requestId) {
|
||||
resetARRDataReview();
|
||||
state.arrDataReviewRequestId = requestId;
|
||||
state.arrDataReviewDrafts = { ...state.arrDataReviewDraftsByRequest[requestId] };
|
||||
}
|
||||
const contextId = state.arrDownloadContextId;
|
||||
state.arrDataReviewLoading = true;
|
||||
@@ -3172,7 +3237,7 @@
|
||||
});
|
||||
$("#upload-button").addEventListener("click", handleUpload);
|
||||
$("#arr-download-form").addEventListener("submit", submitARRDownload);
|
||||
$("#arr-download-date").addEventListener("input", () => { state.arrDownloadDateEdited = true; });
|
||||
$("#arr-download-date").addEventListener("input", handleARRDownloadDateChange);
|
||||
$("#arr-download-date").addEventListener("change", handleARRDownloadDateChange);
|
||||
$("#arr-download-review").addEventListener("click", async () => {
|
||||
const jobId = state.arrDownloadTask?.job_id;
|
||||
@@ -3180,6 +3245,10 @@
|
||||
try { await openDailyPriceReview(jobId, true); } catch (error) { showToast(error.message, true); }
|
||||
});
|
||||
$("#arr-download-data-review").addEventListener("click", () => loadARRDataReview(state.arrDownloadTask?.request_id, { scroll: true }));
|
||||
$("#arr-download-pending-list").addEventListener("click", (event) => {
|
||||
const button = event.target.closest("[data-arr-pending-review]");
|
||||
if (button) selectARRPendingReview(button.dataset.arrPendingReview);
|
||||
});
|
||||
$("#arr-data-review-refresh").addEventListener("click", () => loadARRDataReview());
|
||||
$("#arr-data-review-finalize").addEventListener("click", finalizeARRDataReview);
|
||||
$("#arr-data-review-body").addEventListener("input", (event) => {
|
||||
|
||||
@@ -124,6 +124,9 @@
|
||||
"arr_download.needs_review": ["等待人工价格复核", "Awaiting price review", "รอตรวจสอบราคาด้วยตนเอง"],
|
||||
"arr_download.needs_data_review": ["等待人工完善报表字段", "Awaiting source field review", "รอตรวจสอบข้อมูลรายงานด้วยตนเอง"],
|
||||
"arr_download.complete_data": ["完善报表字段", "Complete report fields", "กรอกข้อมูลรายงานให้ครบ"],
|
||||
"arr_download.date_independent": ["可选其他日期下载,当前待完善内容会保留。", "Choose another date to download; this review will be kept.", "เลือกวันอื่นเพื่อดาวน์โหลดได้ ข้อมูลที่รอตรวจสอบจะยังคงอยู่"],
|
||||
"arr_download.pending_dates": ["待完善日期", "Dates awaiting field review", "วันที่รอตรวจสอบข้อมูล"],
|
||||
"arr_download.pending_date": ["待完善", "Complete fields", "รอตรวจสอบ"],
|
||||
"arr_download.source_failed": ["到店数据获取未完成,请继续原任务重试。", "Arrival data could not be fetched. Resume the original task to retry.", "ยังดึงข้อมูลผู้เข้าพักไม่สำเร็จ โปรดดำเนินงานเดิมต่อเพื่อลองอีกครั้ง"],
|
||||
"arr_download.source_interrupted": ["到店数据获取中断,请继续原任务。", "Arrival data fetch was interrupted. Resume the original task.", "การดึงข้อมูลผู้เข้าพักขัดจังหวะ โปรดดำเนินงานเดิมต่อ"],
|
||||
"arr_download.failed": ["任务未完成,请查看任务日志", "Task failed. Check the task log.", "งานไม่สำเร็จ โปรดดูบันทึกงาน"],
|
||||
|
||||
@@ -74,6 +74,10 @@
|
||||
<span class="arr-download-indicator" aria-hidden="true"></span>
|
||||
<span id="arr-download-status">正在连接下载服务…</span>
|
||||
</div>
|
||||
<div class="arr-download-pending" id="arr-download-pending" hidden>
|
||||
<span id="arr-download-pending-title">待完善日期</span>
|
||||
<div id="arr-download-pending-list"></div>
|
||||
</div>
|
||||
<div class="arr-download-actions">
|
||||
<button class="arr-download-review" id="arr-download-data-review" type="button" hidden>完善报表字段</button>
|
||||
<button class="arr-download-review" id="arr-download-review" type="button" hidden>查看价格复核</button>
|
||||
|
||||
@@ -201,6 +201,13 @@ button { color: inherit; }
|
||||
.arr-download-actions .primary-button { min-height: 34px; margin: 0; padding: 0 12px; border-radius: 8px; font-size: 11px; }
|
||||
.arr-download-review { padding: 4px 0; border: 0; color: var(--blue-dark); background: transparent; font-size: 11px; font-weight: 650; cursor: pointer; text-decoration: underline; text-underline-offset: 3px; }
|
||||
.arr-download-review:hover { color: var(--blue); }
|
||||
.arr-download-pending { margin: 10px 0 12px; }
|
||||
.arr-download-pending[hidden] { display: none; }
|
||||
.arr-download-pending > span { display: block; margin-bottom: 5px; color: var(--muted); font-size: 11px; }
|
||||
#arr-download-pending-list { display: flex; flex-wrap: wrap; gap: 6px; max-height: 110px; overflow-y: auto; }
|
||||
#arr-download-pending-list button { padding: 5px 8px; border: 1px solid var(--line); border-radius: 6px; color: var(--blue-dark); background: #fff; font: inherit; font-size: 11px; cursor: pointer; }
|
||||
#arr-download-pending-list button[aria-pressed="true"] { border-color: var(--blue); background: #f0f4ff; }
|
||||
#arr-download-pending-list button:disabled { opacity: .55; cursor: default; }
|
||||
.arr-data-review-panel { margin: 20px 0; border-color: #e6d4b5; overflow: hidden; }
|
||||
.arr-data-review-panel[hidden] { display: none; }
|
||||
.arr-data-review-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; padding: 20px; background: #fffcf5; }
|
||||
|
||||
Reference in new issue
Block a user