feat: add flexible ratio input for line item quality ratings
This commit is contained in:
parent
f2f2ac3310
commit
6ae16c1808
|
|
@ -40,6 +40,15 @@ def _to_decimal(value: str | None) -> Decimal | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _format_decimal_compact(value: Decimal | None) -> str:
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
text = format(value.normalize(), "f")
|
||||||
|
if "." in text:
|
||||||
|
text = text.rstrip("0").rstrip(".")
|
||||||
|
return text or "0"
|
||||||
|
|
||||||
|
|
||||||
def _line_item_extra(item: DocumentLineItem) -> dict:
|
def _line_item_extra(item: DocumentLineItem) -> dict:
|
||||||
return dict(item.raw_json or {})
|
return dict(item.raw_json or {})
|
||||||
|
|
||||||
|
|
@ -49,6 +58,28 @@ def _line_item_quality_rating(item: DocumentLineItem) -> str:
|
||||||
return "" if value is None else str(value)
|
return "" if value is None else str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _line_item_quality_rating_value(item: DocumentLineItem) -> str:
|
||||||
|
extra = _line_item_extra(item)
|
||||||
|
value = extra.get("quality_rating_value")
|
||||||
|
if value is not None:
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
raw = _to_decimal(extra.get("quality_rating"))
|
||||||
|
return "" if raw is None else _format_decimal_compact(raw)
|
||||||
|
|
||||||
|
|
||||||
|
def _line_item_quality_rating_scale(item: DocumentLineItem) -> str:
|
||||||
|
extra = _line_item_extra(item)
|
||||||
|
scale = extra.get("quality_rating_scale")
|
||||||
|
if scale is not None and str(scale).strip():
|
||||||
|
return str(scale)
|
||||||
|
|
||||||
|
raw = _to_decimal(extra.get("quality_rating"))
|
||||||
|
if raw is None:
|
||||||
|
return "10"
|
||||||
|
return "100" if raw > 10 else "10"
|
||||||
|
|
||||||
|
|
||||||
def _line_item_quality_note(item: DocumentLineItem) -> str:
|
def _line_item_quality_note(item: DocumentLineItem) -> str:
|
||||||
value = _line_item_extra(item).get("quality_note")
|
value = _line_item_extra(item).get("quality_note")
|
||||||
return "" if value is None else str(value)
|
return "" if value is None else str(value)
|
||||||
|
|
@ -105,6 +136,8 @@ def _build_row(item: DocumentLineItem) -> dict | None:
|
||||||
"category": item.category or "",
|
"category": item.category or "",
|
||||||
"confidence": "",
|
"confidence": "",
|
||||||
"quality_rating": _line_item_quality_rating(item),
|
"quality_rating": _line_item_quality_rating(item),
|
||||||
|
"quality_rating_value": _line_item_quality_rating_value(item),
|
||||||
|
"quality_rating_scale": _line_item_quality_rating_scale(item),
|
||||||
"quality_note": _line_item_quality_note(item),
|
"quality_note": _line_item_quality_note(item),
|
||||||
"quality_status": _line_item_quality_status(item),
|
"quality_status": _line_item_quality_status(item),
|
||||||
"is_reviewed": bool(_line_item_extra(item).get("reviewed_at")),
|
"is_reviewed": bool(_line_item_extra(item).get("reviewed_at")),
|
||||||
|
|
@ -260,7 +293,8 @@ def save_line_item_review(
|
||||||
rating_min: str = Form(""),
|
rating_min: str = Form(""),
|
||||||
rating_max: str = Form(""),
|
rating_max: str = Form(""),
|
||||||
return_to: str = Form("list"),
|
return_to: str = Form("list"),
|
||||||
quality_rating: str = Form(""),
|
quality_rating_value: str = Form(""),
|
||||||
|
quality_rating_scale: str = Form(""),
|
||||||
quality_note: str = Form(""),
|
quality_note: str = Form(""),
|
||||||
is_approved: str = Form(""),
|
is_approved: str = Form(""),
|
||||||
is_excluded: str = Form(""),
|
is_excluded: str = Form(""),
|
||||||
|
|
|
||||||
|
|
@ -6083,3 +6083,151 @@ table {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* ===== end line item queue header relayout ===== */
|
/* ===== end line item queue header relayout ===== */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* ===== line item queue card polish ===== */
|
||||||
|
.line-queue-card-head {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
|
gap: 0.72rem 0.85rem;
|
||||||
|
align-items: start;
|
||||||
|
margin-bottom: 0.62rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-queue-title-block {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-queue-kicker {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.12;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 0.08rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-queue-title {
|
||||||
|
margin: 0 !important;
|
||||||
|
font-size: 0.96rem;
|
||||||
|
line-height: 1.02;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-queue-flags {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.16rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-top: 0.02rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-queue-flag {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 0.95rem auto;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 0.38rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.02;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-queue-flag input {
|
||||||
|
width: 0.92rem;
|
||||||
|
height: 0.92rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-queue-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.28rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-queue-badges .badge {
|
||||||
|
font-size: 0.74rem;
|
||||||
|
padding: 0.2rem 0.48rem;
|
||||||
|
line-height: 1.04;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-rating-input-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto 5.2rem;
|
||||||
|
gap: 0.38rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-rating-slash {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #6b7280;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-rating-input-row input {
|
||||||
|
height: 2.25rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-rating-denominator {
|
||||||
|
border-radius: 0.92rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-rating-scale-toggle {
|
||||||
|
min-width: 4.5rem;
|
||||||
|
border-radius: 0.92rem;
|
||||||
|
border: 1px solid #cfd6df;
|
||||||
|
background: #fff;
|
||||||
|
color: #111827;
|
||||||
|
padding: 0 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-rating-scale-toggle:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.line-queue-card-head {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
|
gap: 0.52rem 0.62rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-queue-kicker {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-queue-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-queue-flag {
|
||||||
|
font-size: 0.74rem;
|
||||||
|
grid-template-columns: 0.9rem auto;
|
||||||
|
column-gap: 0.32rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-queue-flag input {
|
||||||
|
width: 0.86rem;
|
||||||
|
height: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-queue-badges .badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.18rem 0.42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-rating-input-row input {
|
||||||
|
height: 2.15rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-rating-input-row {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto 4.7rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* ===== end line item queue card polish ===== */
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{% block title %}Document Processor{% endblock %}</title>
|
<title>{% block title %}Document Processor{% endblock %}</title>
|
||||||
<link rel="stylesheet" href="/static/app.css?v=166">
|
<link rel="stylesheet" href="/static/app.css?v=171">
|
||||||
<link rel="stylesheet" href="/static/app-shell.css?v=158">
|
<link rel="stylesheet" href="/static/app-shell.css?v=158">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Line Items{% endblock %}
|
{% block title %}Line Items
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
|
@ -210,31 +217,31 @@
|
||||||
<form method="post" action="/line-items/{{ row.line_item_id }}/review">
|
<form method="post" action="/line-items/{{ row.line_item_id }}/review">
|
||||||
<input type="hidden" name="return_to" value="queue">
|
<input type="hidden" name="return_to" value="queue">
|
||||||
|
|
||||||
<div class="queue-review-header">
|
<div class="line-queue-card-head">
|
||||||
<div class="queue-review-title-block">
|
<div class="line-queue-title-block">
|
||||||
<div class="queue-review-meta">{{ row.transaction_date }} · {{ row.merchant }}</div>
|
<div class="line-queue-kicker">{{ row.transaction_date }} · {{ row.merchant }}</div>
|
||||||
<h3 class="card-title queue-review-title">{{ row.description }}</h3>
|
<h3 class="card-title line-queue-title">{{ row.description }}</h3>
|
||||||
{% if row.raw_description and row.raw_description != row.description %}
|
{% if row.raw_description and row.raw_description != row.description %}
|
||||||
<div class="queue-review-raw">{{ row.raw_description }}</div>
|
<div class="queue-review-raw">{{ row.raw_description }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="queue-review-flags">
|
<div class="line-queue-flags">
|
||||||
<label class="queue-flag-row">
|
<label class="line-queue-flag">
|
||||||
<input type="checkbox" name="is_approved" value="1" {% if row.is_approved %}checked{% endif %}>
|
<input type="checkbox" name="is_approved" value="1" {% if row.is_approved %}checked{% endif %}>
|
||||||
<span>Approved</span>
|
<span>Approved</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="queue-flag-row">
|
<label class="line-queue-flag">
|
||||||
<input type="checkbox" name="is_excluded" value="1" {% if row.is_excluded %}checked{% endif %}>
|
<input type="checkbox" name="is_excluded" value="1" {% if row.is_excluded %}checked{% endif %}>
|
||||||
<span>Excluded</span>
|
<span>Excluded</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="queue-flag-row">
|
<label class="line-queue-flag">
|
||||||
<input type="checkbox" name="is_na" value="1" {% if row.is_na %}checked{% endif %}>
|
<input type="checkbox" name="is_na" value="1" {% if row.is_na %}checked{% endif %}>
|
||||||
<span>N/A</span>
|
<span>N/A</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="badges queue-review-badges">
|
<div class="line-queue-badges">
|
||||||
{% if row.category %}
|
{% if row.category %}
|
||||||
<span class="badge">{{ row.category }}</span>
|
<span class="badge">{{ row.category }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -249,8 +256,27 @@
|
||||||
|
|
||||||
<div class="queue-review-body">
|
<div class="queue-review-body">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="queue_quality_rating_{{ row.line_item_id }}">Quality rating</label>
|
<label for="queue_quality_rating_value_{{ row.line_item_id }}">Quality rating</label>
|
||||||
<input id="queue_quality_rating_{{ row.line_item_id }}" class="queue-review-rating-input" type="text" name="quality_rating" value="{{ row.quality_rating }}" placeholder="e.g. 8.5 or 4/5">
|
<div class="queue-rating-input-row">
|
||||||
|
<input
|
||||||
|
class="queue-rating-value queue-review-rating-input"
|
||||||
|
id="queue_quality_rating_value_{{ row.line_item_id }}"
|
||||||
|
type="text"
|
||||||
|
name="quality_rating_value"
|
||||||
|
value="{{ row.quality_rating_value }}"
|
||||||
|
placeholder="8.5"
|
||||||
|
oninput="queueRatingAutoDenominator(this, document.getElementById('queue_quality_rating_scale_{{ row.line_item_id }}'))"
|
||||||
|
>
|
||||||
|
<span class="queue-rating-slash">/</span>
|
||||||
|
<input
|
||||||
|
class="queue-rating-denominator"
|
||||||
|
id="queue_quality_rating_scale_{{ row.line_item_id }}"
|
||||||
|
type="text"
|
||||||
|
name="quality_rating_scale"
|
||||||
|
value="{{ row.quality_rating_scale or '10' }}"
|
||||||
|
placeholder="10"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
|
|
@ -273,4 +299,31 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function queueRatingAutoDenominator(valueEl, denomEl) {
|
||||||
|
if (!valueEl || !denomEl) return;
|
||||||
|
|
||||||
|
const raw = String(valueEl.value || "").trim();
|
||||||
|
if (!raw) return;
|
||||||
|
|
||||||
|
const value = Number(raw);
|
||||||
|
if (!Number.isFinite(value)) return;
|
||||||
|
|
||||||
|
const current = String(denomEl.value || "").trim();
|
||||||
|
|
||||||
|
if (current === "" || current === "10" || current === "100") {
|
||||||
|
if (value >= 0 && value <= 10) {
|
||||||
|
denomEl.value = "10";
|
||||||
|
} else if (value > 10 && value <= 100) {
|
||||||
|
denomEl.value = "100";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue