fix: форма разбора с историей решений, выбором карточки только для D-кодов и ключом повтора
This commit is contained in:
parent
eefb5e75fe
commit
0d54fcc893
2 changed files with 30 additions and 12 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
// Балл пересчитывает сервер теми же метриками и весами (lct-35): здесь только
|
// Балл пересчитывает сервер теми же метриками и весами (lct-35): здесь только
|
||||||
// решение и его причина. Причина обязательна — она остаётся в отчёте и в CSV/PDF.
|
// решение и его причина. Причина обязательна — она остаётся в отчёте и в CSV/PDF.
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
|
|
||||||
import type { ErrorCode, FindingDecision, SessionReport } from "@/shared/types/generated";
|
import type { ErrorCode, FindingDecision, SessionReport } from "@/shared/types/generated";
|
||||||
|
|
||||||
|
|
@ -34,7 +34,13 @@ export function FindingReview({ report, onChange }: {
|
||||||
: cards.length ? [...CALL_CODES, ...DDS_CODES] : CALL_CODES;
|
: cards.length ? [...CALL_CODES, ...DDS_CODES] : CALL_CODES;
|
||||||
const [reasons, setReasons] = useState<Record<number, string>>({});
|
const [reasons, setReasons] = useState<Record<number, string>>({});
|
||||||
const [code, setCode] = useState<ErrorCode>(codes[0]);
|
const [code, setCode] = useState<ErrorCode>(codes[0]);
|
||||||
const [card, setCard] = useState(cards.length ? "1" : "");
|
// В ДДС все метрики принадлежат карточкам очереди. В связке 112 → ДДС
|
||||||
|
// E-коды относятся к занятию, а D-код без карточки — к живой карточке звонка.
|
||||||
|
const [card, setCard] = useState(report.exercise === "dds" && cards.length ? "1" : "");
|
||||||
|
const cardChoice = cards.length > 0 && (report.exercise === "dds" || code.startsWith("D"));
|
||||||
|
// Один ключ на отметку, пока она не сохранена: повтор после сбоя сети
|
||||||
|
// не добавит её второй раз.
|
||||||
|
const pendingId = useRef<string | null>(null);
|
||||||
const [fact, setFact] = useState("");
|
const [fact, setFact] = useState("");
|
||||||
const [norm, setNorm] = useState("");
|
const [norm, setNorm] = useState("");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
@ -71,10 +77,14 @@ export function FindingReview({ report, onChange }: {
|
||||||
setError("Для отметки нужны факт и норма.");
|
setError("Для отметки нужны факт и норма.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
pendingId.current ??= crypto.randomUUID();
|
||||||
|
const clientId = pendingId.current;
|
||||||
void run(async () => {
|
void run(async () => {
|
||||||
const updated = await send(base, {
|
const updated = await send(base, {
|
||||||
code, fact: fact.trim(), norm: norm.trim(), card: card ? Number(card) : null,
|
code, fact: fact.trim(), norm: norm.trim(), card: cardChoice && card ? Number(card) : null,
|
||||||
|
client_id: clientId,
|
||||||
});
|
});
|
||||||
|
pendingId.current = null;
|
||||||
setFact("");
|
setFact("");
|
||||||
setNorm("");
|
setNorm("");
|
||||||
return updated;
|
return updated;
|
||||||
|
|
@ -97,9 +107,11 @@ export function FindingReview({ report, onChange }: {
|
||||||
<td>
|
<td>
|
||||||
{finding.summary}
|
{finding.summary}
|
||||||
<div className="ref">{finding.fact}{finding.norm && ` · ${finding.norm}`}</div>
|
<div className="ref">{finding.fact}{finding.norm && ` · ${finding.norm}`}</div>
|
||||||
{finding.review && <div className={finding.review.decision === "dismissed" ? "ref" : "state-violated"}>
|
{(finding.reviews ?? []).map((review, reviewIndex, all) => <div key={reviewIndex}
|
||||||
{finding.review.decision === "dismissed" ? "Снята" : "Подтверждена"}: {finding.review.reason}
|
className={reviewIndex < all.length - 1 || review.decision === "dismissed" ? "ref" : "state-violated"}>
|
||||||
</div>}
|
{review.decision === "dismissed" ? "Снята" : "Подтверждена"} ({review.author},{" "}
|
||||||
|
{new Date(review.at).toLocaleString("ru-RU")}): {review.reason}
|
||||||
|
</div>)}
|
||||||
<div className="finding-review-actions">
|
<div className="finding-review-actions">
|
||||||
<input aria-label={`Причина решения по отметке ${index + 1}`} maxLength={1000}
|
<input aria-label={`Причина решения по отметке ${index + 1}`} maxLength={1000}
|
||||||
placeholder="Причина решения" value={reasons[index] ?? ""}
|
placeholder="Причина решения" value={reasons[index] ?? ""}
|
||||||
|
|
@ -117,23 +129,27 @@ export function FindingReview({ report, onChange }: {
|
||||||
<h3>Добавить отметку</h3>
|
<h3>Добавить отметку</h3>
|
||||||
<div className="finding-review-form">
|
<div className="finding-review-form">
|
||||||
<label>Код
|
<label>Код
|
||||||
<select value={code} onChange={(event) => setCode(event.target.value as ErrorCode)}>
|
<select value={code} onChange={(event) => {
|
||||||
|
pendingId.current = null;
|
||||||
|
setCode(event.target.value as ErrorCode);
|
||||||
|
}}>
|
||||||
{codes.map((item) => <option key={item} value={item}>{item}</option>)}
|
{codes.map((item) => <option key={item} value={item}>{item}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
{cards.length > 0 && <label>Карточка
|
{cardChoice && <label>Карточка
|
||||||
<select value={card} onChange={(event) => setCard(event.target.value)}>
|
<select value={card} onChange={(event) => { pendingId.current = null; setCard(event.target.value); }}>
|
||||||
|
{report.exercise !== "dds" && <option value="">занятие (без карточки)</option>}
|
||||||
{cards.map((item, index) => <option key={item.card_id} value={String(index + 1)}>
|
{cards.map((item, index) => <option key={item.card_id} value={String(index + 1)}>
|
||||||
{index + 1}: {item.title || item.scenario_id}
|
{index + 1}: {item.title || item.scenario_id}
|
||||||
</option>)}
|
</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>}
|
</label>}
|
||||||
<label>Факт
|
<label>Факт
|
||||||
<input maxLength={1000} value={fact} onChange={(event) => setFact(event.target.value)}
|
<input maxLength={1000} value={fact} onChange={(event) => { pendingId.current = null; setFact(event.target.value); }}
|
||||||
placeholder="Что сделал курсант" />
|
placeholder="Что сделал курсант" />
|
||||||
</label>
|
</label>
|
||||||
<label>Норма
|
<label>Норма
|
||||||
<input maxLength={1000} value={norm} onChange={(event) => setNorm(event.target.value)}
|
<input maxLength={1000} value={norm} onChange={(event) => { pendingId.current = null; setNorm(event.target.value); }}
|
||||||
placeholder="Как требовалось" />
|
placeholder="Как требовалось" />
|
||||||
</label>
|
</label>
|
||||||
<button type="button" disabled={busy || !fact.trim() || !norm.trim()} onClick={addFinding}>
|
<button type="button" disabled={busy || !fact.trim() || !norm.trim()} onClick={addFinding}>
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,7 @@ export interface DdsCardReport {
|
||||||
card_id: string;
|
card_id: string;
|
||||||
scenario_id: string;
|
scenario_id: string;
|
||||||
score_auto: number;
|
score_auto: number;
|
||||||
|
score_reviewed?: number | null;
|
||||||
reply_text: string;
|
reply_text: string;
|
||||||
metrics: Array<Metric>;
|
metrics: Array<Metric>;
|
||||||
findings: Array<Finding>;
|
findings: Array<Finding>;
|
||||||
|
|
@ -271,7 +272,8 @@ export interface Finding {
|
||||||
card?: number | null;
|
card?: number | null;
|
||||||
service?: string | null;
|
service?: string | null;
|
||||||
author?: string | null;
|
author?: string | null;
|
||||||
review?: FindingReview | null;
|
client_id?: string | null;
|
||||||
|
reviews?: Array<FindingReview>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Решение преподавателя по отметке на разборе. */
|
/** Решение преподавателя по отметке на разборе. */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue