feat(console): data-entry screen, and mark hand-entered prices as such
/officer/data-entry — upload, preview, commit, and the batch history, for the feeds no API serves and for the loan book. The screen is built around one rule: nothing is written until someone has seen what will be. Accepted, rejected and overwriting rows are shown as three separate groups, because merging them into one list of rows hides the only two questions the person clicking Commit has — what will not land, and what will land on top of something already there. An overwriting row shows the value it replaces beside the one replacing it. The screen states in plain words that the effective date is the date being reported about rather than today, and that when we learned it is recorded separately — the distinction is invisible in a spreadsheet and expensive to get wrong. Selecting the loan book adds the no-borrower-identity rule. On the feeds page, mandi rows now carry a source: Agmarknet, or a 'Hand' chip for a price somebody typed. By the time a price reaches a feature the two are indistinguishable, so the distinction has to be visible there or it is visible nowhere. The demo fallback deliberately contains rejections. A demo where every upload is clean teaches the wrong thing — a reviewer who only sees green rows will not know the rejection panel exists. tsc, oxlint and npm run build all clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -397,3 +397,62 @@ export interface AppHome {
|
||||
advice: { textEn: string; textHi: string }
|
||||
lastSyncedAt: string | null
|
||||
}
|
||||
|
||||
// ── Manual & bulk data entry ─────────────────────────────────────
|
||||
// The feeds no API serves: NECC egg rates, dairy-federation milk, fodder — and
|
||||
// the loan book, until a lender builds a nightly extract.
|
||||
|
||||
export interface ManualEntryBatch {
|
||||
id: number
|
||||
entryType: 'MARKET_SIGNAL' | 'LOAN_RECORD'
|
||||
filename: string | null
|
||||
uploadedBy: string
|
||||
uploadedAt: string
|
||||
status: 'PREVIEW' | 'COMMITTED' | 'DISCARDED'
|
||||
committedAt: string | null
|
||||
committedBy: string | null
|
||||
rowsParsed: number | null
|
||||
rowsAccepted: number | null
|
||||
rowsRejected: number | null
|
||||
rowsOverwriting: number | null
|
||||
note: string | null
|
||||
}
|
||||
|
||||
export interface ManualEntryLine {
|
||||
id: number
|
||||
rowNumber: number
|
||||
rawDistrict: string | null
|
||||
rawCommodity: string | null
|
||||
rawEffectiveDate: string | null
|
||||
rawModalPrice: string | null
|
||||
effectiveDate: string | null
|
||||
weekStart: string | null
|
||||
modalPrice: number | null
|
||||
minPrice: number | null
|
||||
maxPrice: number | null
|
||||
unit: string | null
|
||||
accepted: boolean
|
||||
rejectReason: string | null
|
||||
rejectDetail: string | null
|
||||
warning: string | null
|
||||
overwriting: boolean
|
||||
/** What an accepted row would replace — a correction is only auditable if this survives. */
|
||||
priorModalPrice: number | null
|
||||
}
|
||||
|
||||
/** Accepted, rejected and overwriting stay apart all the way from the backend. */
|
||||
export interface ManualEntryPreview {
|
||||
batch: ManualEntryBatch
|
||||
accepted: ManualEntryLine[]
|
||||
rejected: ManualEntryLine[]
|
||||
overwriting: ManualEntryLine[]
|
||||
warnings: ManualEntryLine[]
|
||||
}
|
||||
|
||||
/** Multipart, so it bypasses the JSON helpers. */
|
||||
export async function uploadManual(file: File, type: 'MARKET_SIGNAL' | 'LOAN_RECORD') {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
const res = await api.post(`/admin/manual/upload?type=${type}`, form)
|
||||
return res.data as { success: boolean; message: string; data: ManualEntryPreview }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user