feat(reject-history): add detail columns, CSV export polish, and metric filter sync

Add EQUIPMENTNAME and REJECTCOMMENT columns to the detail table, list SQL,
and per-LOT base query. Rewrite CSV export to use per-LOT rows with Chinese
headers, BOM UTF-8 encoding, and fetch-based blob download with loading
spinner. Sync trend chart legend filter (reject/defect) to detail table and
export via metric_filter parameter through the full stack. Fix chart sizing
with containLabel and autoresize throttle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
egg
2026-02-22 13:50:54 +08:00
parent 5e5cc487ac
commit 5f6e2a5ce0
10 changed files with 145 additions and 46 deletions

View File

@@ -71,6 +71,7 @@ const loading = reactive({
querying: false, querying: false,
options: false, options: false,
list: false, list: false,
exporting: false,
}); });
const errorMessage = ref(''); const errorMessage = ref('');
@@ -267,12 +268,19 @@ function buildCommonParams({ reason = committedFilters.reason } = {}) {
return buildRejectCommonQueryParams(committedFilters, { reason }); return buildRejectCommonQueryParams(committedFilters, { reason });
} }
function metricFilterParam() {
const mode = paretoMetricMode.value;
if (mode === 'reject' || mode === 'defect') return mode;
return 'all';
}
function buildListParams() { function buildListParams() {
const effectiveReason = detailReason.value || committedFilters.reason; const effectiveReason = detailReason.value || committedFilters.reason;
const params = { const params = {
...buildCommonParams({ reason: effectiveReason }), ...buildCommonParams({ reason: effectiveReason }),
page: page.value, page: page.value,
per_page: DEFAULT_PER_PAGE, per_page: DEFAULT_PER_PAGE,
metric_filter: metricFilterParam(),
}; };
if (selectedTrendDates.value.length > 0) { if (selectedTrendDates.value.length > 0) {
const sorted = [...selectedTrendDates.value].sort(); const sorted = [...selectedTrendDates.value].sort();
@@ -561,7 +569,9 @@ function onTrendDateClick(dateStr) {
function onTrendLegendChange(selected) { function onTrendLegendChange(selected) {
trendLegendSelected.value = selected; trendLegendSelected.value = selected;
page.value = 1;
updateUrlState(); updateUrlState();
void loadListOnly();
} }
function onParetoClick(reason) { function onParetoClick(reason) {
@@ -625,7 +635,9 @@ async function removeFilterChip(chip) {
await loadDataSections(); await loadDataSections();
} }
function exportCsv() { async function exportCsv() {
if (loading.exporting) return;
const effectiveReason = detailReason.value || committedFilters.reason; const effectiveReason = detailReason.value || committedFilters.reason;
const queryParams = buildRejectCommonQueryParams(committedFilters, { reason: effectiveReason }); const queryParams = buildRejectCommonQueryParams(committedFilters, { reason: effectiveReason });
const params = new URLSearchParams(); const params = new URLSearchParams();
@@ -638,8 +650,33 @@ function exportCsv() {
appendArrayParams(params, 'workcenter_groups', queryParams.workcenter_groups || []); appendArrayParams(params, 'workcenter_groups', queryParams.workcenter_groups || []);
appendArrayParams(params, 'packages', queryParams.packages || []); appendArrayParams(params, 'packages', queryParams.packages || []);
appendArrayParams(params, 'reasons', queryParams.reasons || []); appendArrayParams(params, 'reasons', queryParams.reasons || []);
params.set('metric_filter', metricFilterParam());
window.location.href = `/api/reject-history/export?${params.toString()}`; loading.exporting = true;
errorMessage.value = '';
try {
const response = await fetch(`/api/reject-history/export?${params.toString()}`);
if (!response.ok) {
throw new Error('匯出 CSV 失敗');
}
const blob = await response.blob();
const disposition = response.headers.get('Content-Disposition') || '';
const filenameMatch = disposition.match(/filename=(.+?)(?:;|$)/);
const filename = filenameMatch ? filenameMatch[1] : `reject_history_${queryParams.start_date}_to_${queryParams.end_date}.csv`;
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
errorMessage.value = error?.message || '匯出 CSV 失敗';
} finally {
loading.exporting = false;
}
} }
const totalScrapQty = computed(() => { const totalScrapQty = computed(() => {

View File

@@ -42,6 +42,8 @@ function formatNumber(value) {
<th class="col-left">TYPE</th> <th class="col-left">TYPE</th>
<th>PRODUCT</th> <th>PRODUCT</th>
<th>原因</th> <th>原因</th>
<th>EQUIPMENT</th>
<th>COMMENT</th>
<th class="th-expandable" @click="showRejectBreakdown = !showRejectBreakdown"> <th class="th-expandable" @click="showRejectBreakdown = !showRejectBreakdown">
扣帳報廢量 <span class="expand-icon">{{ showRejectBreakdown ? '▾' : '▸' }}</span> 扣帳報廢量 <span class="expand-icon">{{ showRejectBreakdown ? '▾' : '▸' }}</span>
</th> </th>
@@ -65,6 +67,8 @@ function formatNumber(value) {
<td class="col-left">{{ row.PJ_TYPE }}</td> <td class="col-left">{{ row.PJ_TYPE }}</td>
<td>{{ row.PRODUCTNAME || '' }}</td> <td>{{ row.PRODUCTNAME || '' }}</td>
<td>{{ row.LOSSREASONNAME }}</td> <td>{{ row.LOSSREASONNAME }}</td>
<td>{{ row.EQUIPMENTNAME || '' }}</td>
<td>{{ row.REJECTCOMMENT || '' }}</td>
<td>{{ formatNumber(row.REJECT_TOTAL_QTY) }}</td> <td>{{ formatNumber(row.REJECT_TOTAL_QTY) }}</td>
<template v-if="showRejectBreakdown"> <template v-if="showRejectBreakdown">
<td class="td-sub">{{ formatNumber(row.REJECT_QTY) }}</td> <td class="td-sub">{{ formatNumber(row.REJECT_QTY) }}</td>
@@ -77,7 +81,7 @@ function formatNumber(value) {
<td class="cell-nowrap">{{ row.TXN_TIME || row.TXN_DAY }}</td> <td class="cell-nowrap">{{ row.TXN_TIME || row.TXN_DAY }}</td>
</tr> </tr>
<tr v-if="!items || items.length === 0"> <tr v-if="!items || items.length === 0">
<td :colspan="showRejectBreakdown ? 15 : 10" class="placeholder">No data</td> <td :colspan="showRejectBreakdown ? 17 : 12" class="placeholder">No data</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -84,7 +84,10 @@ defineEmits(['apply', 'clear', 'export-csv', 'remove-chip', 'pareto-scope-toggle
<div class="filter-actions"> <div class="filter-actions">
<button class="btn btn-primary" :disabled="loading.querying" @click="$emit('apply')">查詢</button> <button class="btn btn-primary" :disabled="loading.querying" @click="$emit('apply')">查詢</button>
<button class="btn btn-secondary" :disabled="loading.querying" @click="$emit('clear')">清除條件</button> <button class="btn btn-secondary" :disabled="loading.querying" @click="$emit('clear')">清除條件</button>
<button class="btn btn-light btn-export" :disabled="loading.querying" @click="$emit('export-csv')">匯出 CSV</button> <button class="btn btn-light btn-export" :disabled="loading.querying || loading.exporting" @click="$emit('export-csv')">
<template v-if="loading.exporting"><span class="btn-spinner"></span>匯出中...</template>
<template v-else>匯出 CSV</template>
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -31,7 +31,7 @@ const chartOption = computed(() => {
data: ['扣帳報廢量', '不扣帳報廢量'], data: ['扣帳報廢量', '不扣帳報廢量'],
bottom: 0, bottom: 0,
}, },
grid: { left: 48, right: 24, top: 22, bottom: 70 }, grid: { left: 48, right: 24, top: 22, bottom: 70, containLabel: false },
xAxis: { xAxis: {
type: 'category', type: 'category',
data: items.map((item) => item.bucket_date || ''), data: items.map((item) => item.bucket_date || ''),
@@ -97,7 +97,7 @@ function handleLegendChange(params) {
<article class="card"> <article class="card">
<div class="card-header"><div class="card-title">報廢量趨勢</div></div> <div class="card-header"><div class="card-title">報廢量趨勢</div></div>
<div class="card-body chart-wrap"> <div class="card-body chart-wrap">
<VChart :option="chartOption" autoresize @click="handleChartClick" @legendselectchanged="handleLegendChange" /> <VChart :option="chartOption" :autoresize="{ throttle: 100 }" style="width: 100%; height: 100%" @click="handleChartClick" @legendselectchanged="handleLegendChange" />
<div v-if="!hasData && !loading" class="placeholder chart-empty">No data</div> <div v-if="!hasData && !loading" class="placeholder chart-empty">No data</div>
</div> </div>
</article> </article>

View File

@@ -166,6 +166,27 @@
background: #0b5e59; background: #0b5e59;
} }
.btn-export:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.btn-spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid rgba(255, 255, 255, 0.4);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
margin-right: 6px;
vertical-align: middle;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.reject-summary-row { .reject-summary-row {
grid-template-columns: repeat(6, minmax(0, 1fr)); grid-template-columns: repeat(6, minmax(0, 1fr));
} }
@@ -191,8 +212,10 @@
.chart-wrap, .chart-wrap,
.pareto-chart-wrap { .pareto-chart-wrap {
width: 100%;
height: 340px; height: 340px;
position: relative; position: relative;
overflow: hidden;
} }
.pareto-header { .pareto-header {

View File

@@ -333,6 +333,7 @@ def api_reject_history_list():
page = request.args.get("page", 1, type=int) or 1 page = request.args.get("page", 1, type=int) or 1
per_page = request.args.get("per_page", 50, type=int) or 50 per_page = request.args.get("per_page", 50, type=int) or 50
metric_filter = request.args.get("metric_filter", "all").strip().lower() or "all"
try: try:
result = query_list( result = query_list(
@@ -347,6 +348,7 @@ def api_reject_history_list():
include_excluded_scrap=include_excluded_scrap, include_excluded_scrap=include_excluded_scrap,
exclude_material_scrap=exclude_material_scrap, exclude_material_scrap=exclude_material_scrap,
exclude_pb_diode=exclude_pb_diode, exclude_pb_diode=exclude_pb_diode,
metric_filter=metric_filter,
) )
data, meta = _extract_meta( data, meta = _extract_meta(
result, result,
@@ -372,6 +374,7 @@ def api_reject_history_export():
if bool_error: if bool_error:
return jsonify(bool_error[0]), bool_error[1] return jsonify(bool_error[0]), bool_error[1]
metric_filter = request.args.get("metric_filter", "all").strip().lower() or "all"
filename = f"reject_history_{start_date}_to_{end_date}.csv" filename = f"reject_history_{start_date}_to_{end_date}.csv"
try: try:
return Response( return Response(
@@ -385,6 +388,7 @@ def api_reject_history_export():
include_excluded_scrap=include_excluded_scrap, include_excluded_scrap=include_excluded_scrap,
exclude_material_scrap=exclude_material_scrap, exclude_material_scrap=exclude_material_scrap,
exclude_pb_diode=exclude_pb_diode, exclude_pb_diode=exclude_pb_diode,
metric_filter=metric_filter,
), ),
mimetype="text/csv", mimetype="text/csv",
headers={ headers={

View File

@@ -296,6 +296,9 @@ def _list_to_csv(
rows: Iterable[dict[str, Any]], rows: Iterable[dict[str, Any]],
headers: list[str], headers: list[str],
) -> Generator[str, None, None]: ) -> Generator[str, None, None]:
# BOM for UTF-8 so Excel opens the CSV with correct encoding
yield "\ufeff"
buffer = io.StringIO() buffer = io.StringIO()
writer = csv.DictWriter(buffer, fieldnames=headers) writer = csv.DictWriter(buffer, fieldnames=headers)
writer.writeheader() writer.writeheader()
@@ -561,6 +564,19 @@ def query_reason_pareto(
} }
def _apply_metric_filter(where_clause: str, metric_filter: str) -> str:
"""Append metric-type filter (reject / defect) to an existing WHERE clause."""
if metric_filter == "reject":
cond = "b.REJECT_TOTAL_QTY > 0"
elif metric_filter == "defect":
cond = "b.DEFECT_QTY > 0"
else:
return where_clause
if where_clause.strip():
return f"{where_clause} AND {cond}"
return f"WHERE {cond}"
def query_list( def query_list(
*, *,
start_date: str, start_date: str,
@@ -574,6 +590,7 @@ def query_list(
include_excluded_scrap: bool = False, include_excluded_scrap: bool = False,
exclude_material_scrap: bool = True, exclude_material_scrap: bool = True,
exclude_pb_diode: bool = True, exclude_pb_diode: bool = True,
metric_filter: str = "all",
) -> dict[str, Any]: ) -> dict[str, Any]:
_validate_range(start_date, end_date) _validate_range(start_date, end_date)
@@ -590,6 +607,7 @@ def query_list(
exclude_material_scrap=exclude_material_scrap, exclude_material_scrap=exclude_material_scrap,
exclude_pb_diode=exclude_pb_diode, exclude_pb_diode=exclude_pb_diode,
) )
where_clause = _apply_metric_filter(where_clause, metric_filter)
sql = _prepare_sql("list", where_clause=where_clause, base_variant="lot") sql = _prepare_sql("list", where_clause=where_clause, base_variant="lot")
query_params = _common_params( query_params = _common_params(
start_date, start_date,
@@ -615,6 +633,7 @@ def query_list(
"WORKCENTER_GROUP": _normalize_text(row.get("WORKCENTER_GROUP")), "WORKCENTER_GROUP": _normalize_text(row.get("WORKCENTER_GROUP")),
"WORKCENTERNAME": _normalize_text(row.get("WORKCENTERNAME")), "WORKCENTERNAME": _normalize_text(row.get("WORKCENTERNAME")),
"SPECNAME": _normalize_text(row.get("SPECNAME")), "SPECNAME": _normalize_text(row.get("SPECNAME")),
"EQUIPMENTNAME": _normalize_text(row.get("EQUIPMENTNAME")),
"PRODUCTLINENAME": _normalize_text(row.get("PRODUCTLINENAME")), "PRODUCTLINENAME": _normalize_text(row.get("PRODUCTLINENAME")),
"PJ_TYPE": _normalize_text(row.get("PJ_TYPE")), "PJ_TYPE": _normalize_text(row.get("PJ_TYPE")),
"CONTAINERNAME": _normalize_text(row.get("CONTAINERNAME")), "CONTAINERNAME": _normalize_text(row.get("CONTAINERNAME")),
@@ -622,6 +641,7 @@ def query_list(
"PRODUCTNAME": _normalize_text(row.get("PRODUCTNAME")), "PRODUCTNAME": _normalize_text(row.get("PRODUCTNAME")),
"LOSSREASONNAME": _normalize_text(row.get("LOSSREASONNAME")), "LOSSREASONNAME": _normalize_text(row.get("LOSSREASONNAME")),
"LOSSREASON_CODE": _normalize_text(row.get("LOSSREASON_CODE")), "LOSSREASON_CODE": _normalize_text(row.get("LOSSREASON_CODE")),
"REJECTCOMMENT": _normalize_text(row.get("REJECTCOMMENT")),
"MOVEIN_QTY": _as_int(row.get("MOVEIN_QTY")), "MOVEIN_QTY": _as_int(row.get("MOVEIN_QTY")),
"REJECT_QTY": _as_int(row.get("REJECT_QTY")), "REJECT_QTY": _as_int(row.get("REJECT_QTY")),
"STANDBY_QTY": _as_int(row.get("STANDBY_QTY")), "STANDBY_QTY": _as_int(row.get("STANDBY_QTY")),
@@ -661,6 +681,7 @@ def export_csv(
include_excluded_scrap: bool = False, include_excluded_scrap: bool = False,
exclude_material_scrap: bool = True, exclude_material_scrap: bool = True,
exclude_pb_diode: bool = True, exclude_pb_diode: bool = True,
metric_filter: str = "all",
) -> Generator[str, None, None]: ) -> Generator[str, None, None]:
_validate_range(start_date, end_date) _validate_range(start_date, end_date)
@@ -673,7 +694,8 @@ def export_csv(
exclude_material_scrap=exclude_material_scrap, exclude_material_scrap=exclude_material_scrap,
exclude_pb_diode=exclude_pb_diode, exclude_pb_diode=exclude_pb_diode,
) )
sql = _prepare_sql("export", where_clause=where_clause) where_clause = _apply_metric_filter(where_clause, metric_filter)
sql = _prepare_sql("export", where_clause=where_clause, base_variant="lot")
df = read_sql_df(sql, _common_params(start_date, end_date, params)) df = read_sql_df(sql, _common_params(start_date, end_date, params))
rows = [] rows = []
@@ -681,54 +703,52 @@ def export_csv(
for _, row in df.iterrows(): for _, row in df.iterrows():
rows.append( rows.append(
{ {
"TXN_DAY": _to_date_str(row.get("TXN_DAY")), "LOT": _normalize_text(row.get("CONTAINERNAME")),
"TXN_MONTH": _normalize_text(row.get("TXN_MONTH")), "WORKCENTER": _normalize_text(row.get("WORKCENTERNAME")),
"WORKCENTER_GROUP": _normalize_text(row.get("WORKCENTER_GROUP")), "WORKCENTER_GROUP": _normalize_text(row.get("WORKCENTER_GROUP")),
"WORKCENTERNAME": _normalize_text(row.get("WORKCENTERNAME")), "Package": _normalize_text(row.get("PRODUCTLINENAME")),
"SPECNAME": _normalize_text(row.get("SPECNAME")), "FUNCTION": _normalize_text(row.get("PJ_FUNCTION")),
"PRODUCTLINENAME": _normalize_text(row.get("PRODUCTLINENAME")), "TYPE": _normalize_text(row.get("PJ_TYPE")),
"PJ_TYPE": _normalize_text(row.get("PJ_TYPE")), "PRODUCT": _normalize_text(row.get("PRODUCTNAME")),
"LOSSREASONNAME": _normalize_text(row.get("LOSSREASONNAME")), "原因": _normalize_text(row.get("LOSSREASONNAME")),
"LOSSREASON_CODE": _normalize_text(row.get("LOSSREASON_CODE")), "EQUIPMENT": _normalize_text(row.get("EQUIPMENTNAME")),
"MOVEIN_QTY": _as_int(row.get("MOVEIN_QTY")), "COMMENT": _normalize_text(row.get("REJECTCOMMENT")),
"SPEC": _normalize_text(row.get("SPECNAME")),
"REJECT_QTY": _as_int(row.get("REJECT_QTY")), "REJECT_QTY": _as_int(row.get("REJECT_QTY")),
"STANDBY_QTY": _as_int(row.get("STANDBY_QTY")), "STANDBY_QTY": _as_int(row.get("STANDBY_QTY")),
"QTYTOPROCESS_QTY": _as_int(row.get("QTYTOPROCESS_QTY")), "QTYTOPROCESS_QTY": _as_int(row.get("QTYTOPROCESS_QTY")),
"INPROCESS_QTY": _as_int(row.get("INPROCESS_QTY")), "INPROCESS_QTY": _as_int(row.get("INPROCESS_QTY")),
"PROCESSED_QTY": _as_int(row.get("PROCESSED_QTY")), "PROCESSED_QTY": _as_int(row.get("PROCESSED_QTY")),
"REJECT_TOTAL_QTY": _as_int(row.get("REJECT_TOTAL_QTY")), "扣帳報廢量": _as_int(row.get("REJECT_TOTAL_QTY")),
"DEFECT_QTY": _as_int(row.get("DEFECT_QTY")), "不扣帳報廢量": _as_int(row.get("DEFECT_QTY")),
"REJECT_RATE_PCT": round(_as_float(row.get("REJECT_RATE_PCT")), 4), "MOVEIN_QTY": _as_int(row.get("MOVEIN_QTY")),
"DEFECT_RATE_PCT": round(_as_float(row.get("DEFECT_RATE_PCT")), 4), "報廢時間": _to_datetime_str(row.get("TXN_TIME")),
"REJECT_SHARE_PCT": round(_as_float(row.get("REJECT_SHARE_PCT")), 4), "日期": _to_date_str(row.get("TXN_DAY")),
"AFFECTED_LOT_COUNT": _as_int(row.get("AFFECTED_LOT_COUNT")),
"AFFECTED_WORKORDER_COUNT": _as_int(row.get("AFFECTED_WORKORDER_COUNT")),
} }
) )
headers = [ headers = [
"TXN_DAY", "LOT",
"TXN_MONTH", "WORKCENTER",
"WORKCENTER_GROUP", "WORKCENTER_GROUP",
"WORKCENTERNAME", "Package",
"SPECNAME", "FUNCTION",
"PRODUCTLINENAME", "TYPE",
"PJ_TYPE", "PRODUCT",
"LOSSREASONNAME", "原因",
"LOSSREASON_CODE", "EQUIPMENT",
"MOVEIN_QTY", "COMMENT",
"SPEC",
"REJECT_QTY", "REJECT_QTY",
"STANDBY_QTY", "STANDBY_QTY",
"QTYTOPROCESS_QTY", "QTYTOPROCESS_QTY",
"INPROCESS_QTY", "INPROCESS_QTY",
"PROCESSED_QTY", "PROCESSED_QTY",
"REJECT_TOTAL_QTY", "扣帳報廢量",
"DEFECT_QTY", "不扣帳報廢量",
"REJECT_RATE_PCT", "MOVEIN_QTY",
"DEFECT_RATE_PCT", "報廢時間",
"REJECT_SHARE_PCT", "日期",
"AFFECTED_LOT_COUNT",
"AFFECTED_WORKORDER_COUNT",
] ]
return _list_to_csv(rows, headers=headers) return _list_to_csv(rows, headers=headers)

View File

@@ -1,19 +1,24 @@
-- Reject History Export (Unpaginated) -- Reject History Export (Unpaginated, Per-LOT)
-- Template slots: -- Template slots:
-- BASE_QUERY (base reject-history daily dataset SQL) -- BASE_WITH_CTE (lot-level base SQL via performance_daily_lot)
-- WHERE_CLAUSE (QueryBuilder-generated WHERE clause against alias b) -- WHERE_CLAUSE (QueryBuilder-generated WHERE clause against alias b)
{{ BASE_WITH_CTE }} {{ BASE_WITH_CTE }}
SELECT SELECT
b.TXN_TIME,
b.TXN_DAY, b.TXN_DAY,
b.TXN_MONTH, b.CONTAINERNAME,
b.WORKCENTER_GROUP, b.WORKCENTER_GROUP,
b.WORKCENTERNAME, b.WORKCENTERNAME,
b.SPECNAME, b.SPECNAME,
b.EQUIPMENTNAME,
b.PRODUCTLINENAME, b.PRODUCTLINENAME,
b.PJ_FUNCTION,
b.PJ_TYPE, b.PJ_TYPE,
b.PRODUCTNAME,
b.LOSSREASONNAME, b.LOSSREASONNAME,
b.LOSSREASON_CODE, b.LOSSREASON_CODE,
b.REJECTCOMMENT,
b.MOVEIN_QTY, b.MOVEIN_QTY,
b.REJECT_QTY, b.REJECT_QTY,
b.STANDBY_QTY, b.STANDBY_QTY,
@@ -24,13 +29,12 @@ SELECT
b.DEFECT_QTY, b.DEFECT_QTY,
b.REJECT_RATE_PCT, b.REJECT_RATE_PCT,
b.DEFECT_RATE_PCT, b.DEFECT_RATE_PCT,
b.REJECT_SHARE_PCT, b.REJECT_SHARE_PCT
b.AFFECTED_LOT_COUNT,
b.AFFECTED_WORKORDER_COUNT
FROM base b FROM base b
{{ WHERE_CLAUSE }} {{ WHERE_CLAUSE }}
ORDER BY ORDER BY
b.TXN_DAY DESC, b.TXN_DAY DESC,
b.WORKCENTERSEQUENCE_GROUP ASC, b.WORKCENTERSEQUENCE_GROUP ASC,
b.WORKCENTERNAME ASC, b.WORKCENTERNAME ASC,
b.REJECT_TOTAL_QTY DESC b.REJECT_TOTAL_QTY DESC,
b.CONTAINERNAME ASC

View File

@@ -39,6 +39,7 @@ SELECT
p.PRODUCTNAME, p.PRODUCTNAME,
p.LOSSREASONNAME, p.LOSSREASONNAME,
p.LOSSREASON_CODE, p.LOSSREASON_CODE,
p.REJECTCOMMENT,
p.REJECT_EVENT_ROWS, p.REJECT_EVENT_ROWS,
p.AFFECTED_WORKORDER_COUNT, p.AFFECTED_WORKORDER_COUNT,
p.MOVEIN_QTY, p.MOVEIN_QTY,

View File

@@ -44,6 +44,7 @@ reject_raw AS (
NVL(TRIM(r.LOSSREASONNAME), '(未填寫)') NVL(TRIM(r.LOSSREASONNAME), '(未填寫)')
) AS LOSSREASON_CODE, ) AS LOSSREASON_CODE,
NVL(TRIM(r.REJECTCATEGORYNAME), '(未填寫)') AS REJECTCATEGORYNAME, NVL(TRIM(r.REJECTCATEGORYNAME), '(未填寫)') AS REJECTCATEGORYNAME,
TRIM(r.REJECTCOMMENT) AS REJECTCOMMENT,
NVL(r.MOVEINQTY, 0) AS MOVEINQTY, NVL(r.MOVEINQTY, 0) AS MOVEINQTY,
NVL(r.REJECTQTY, 0) AS REJECT_QTY, NVL(r.REJECTQTY, 0) AS REJECT_QTY,
NVL(r.STANDBYQTY, 0) AS STANDBY_QTY, NVL(r.STANDBYQTY, 0) AS STANDBY_QTY,
@@ -91,6 +92,7 @@ daily_agg AS (
LOSSREASONNAME, LOSSREASONNAME,
LOSSREASON_CODE, LOSSREASON_CODE,
REJECTCATEGORYNAME, REJECTCATEGORYNAME,
MAX(REJECTCOMMENT) AS REJECTCOMMENT,
MIN(TXNDATE) AS TXN_TIME, MIN(TXNDATE) AS TXN_TIME,
COUNT(*) AS REJECT_EVENT_ROWS, COUNT(*) AS REJECT_EVENT_ROWS,
COUNT(DISTINCT PJ_WORKORDER) AS AFFECTED_WORKORDER_COUNT, COUNT(DISTINCT PJ_WORKORDER) AS AFFECTED_WORKORDER_COUNT,
@@ -143,6 +145,7 @@ SELECT
LOSSREASONNAME, LOSSREASONNAME,
LOSSREASON_CODE, LOSSREASON_CODE,
REJECTCATEGORYNAME, REJECTCATEGORYNAME,
REJECTCOMMENT,
REJECT_EVENT_ROWS, REJECT_EVENT_ROWS,
AFFECTED_WORKORDER_COUNT, AFFECTED_WORKORDER_COUNT,
MOVEIN_QTY, MOVEIN_QTY,