fix: норматив доклада только по выбору
This commit is contained in:
parent
6e7c4f4596
commit
5e459c449b
14 changed files with 100 additions and 21 deletions
|
|
@ -54,6 +54,7 @@ class LessonCriteria(BaseModel):
|
||||||
card_fill_time_limit_seconds: int = Field(default=180, ge=30, le=1800)
|
card_fill_time_limit_seconds: int = Field(default=180, ge=30, le=1800)
|
||||||
dds_card_work_time_limit_seconds: int = Field(default=180, ge=30, le=1800)
|
dds_card_work_time_limit_seconds: int = Field(default=180, ge=30, le=1800)
|
||||||
#: Сколько секунд после доклада бригады даётся на соответствующий статус.
|
#: Сколько секунд после доклада бригады даётся на соответствующий статус.
|
||||||
|
dds_report_reaction_enabled: bool = False
|
||||||
dds_report_reaction_limit_seconds: int = Field(default=45, ge=5, le=600)
|
dds_report_reaction_limit_seconds: int = Field(default=45, ge=5, le=600)
|
||||||
allowed_errors: int = Field(default=0, ge=0, le=50)
|
allowed_errors: int = Field(default=0, ge=0, le=50)
|
||||||
require_correct_grammar: bool = True
|
require_correct_grammar: bool = True
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,7 @@ def evaluate_dispatcher(
|
||||||
expected_decision_reason: str | None = None,
|
expected_decision_reason: str | None = None,
|
||||||
phone_reports: list[PhoneReportRecord] | None = None,
|
phone_reports: list[PhoneReportRecord] | None = None,
|
||||||
report_reaction_ms: int = REPORT_REACTION_MS,
|
report_reaction_ms: int = REPORT_REACTION_MS,
|
||||||
|
report_reaction_enabled: bool = False,
|
||||||
pause_intervals: Sequence[tuple[datetime, datetime]] | None = None,
|
pause_intervals: Sequence[tuple[datetime, datetime]] | None = None,
|
||||||
) -> list[Finding]:
|
) -> list[Finding]:
|
||||||
"""Отметки по работе диспетчера. Пустой список — работа без нарушений.
|
"""Отметки по работе диспетчера. Пустой список — работа без нарушений.
|
||||||
|
|
@ -392,6 +393,7 @@ def evaluate_dispatcher(
|
||||||
)
|
)
|
||||||
|
|
||||||
# D6 — статус по докладу бригады отмечен позже лимита или не отмечен.
|
# D6 — статус по докладу бригады отмечен позже лимита или не отмечен.
|
||||||
|
if report_reaction_enabled:
|
||||||
reaction = report_reaction(service, marks, reports, report_reaction_ms, pause_intervals)
|
reaction = report_reaction(service, marks, reports, report_reaction_ms, pause_intervals)
|
||||||
if reaction is not None and reaction[1] is not None:
|
if reaction is not None and reaction[1] is not None:
|
||||||
findings.append(reaction[1])
|
findings.append(reaction[1])
|
||||||
|
|
@ -407,6 +409,7 @@ def dispatcher_metrics(
|
||||||
*,
|
*,
|
||||||
services: list[str] | None = None,
|
services: list[str] | None = None,
|
||||||
report_reaction_ms: int = REPORT_REACTION_MS,
|
report_reaction_ms: int = REPORT_REACTION_MS,
|
||||||
|
report_reaction_enabled: bool = False,
|
||||||
pause_intervals: Sequence[tuple[datetime, datetime]] | None = None,
|
pause_intervals: Sequence[tuple[datetime, datetime]] | None = None,
|
||||||
) -> list[Metric]:
|
) -> list[Metric]:
|
||||||
"""Числовая часть оценки ДДС; каждый проверяемый шаг имеет факт и норму.
|
"""Числовая часть оценки ДДС; каждый проверяемый шаг имеет факт и норму.
|
||||||
|
|
@ -441,6 +444,7 @@ def dispatcher_metrics(
|
||||||
# Метрика есть только там, где бригада докладывала: без докладов
|
# Метрика есть только там, где бригада докладывала: без докладов
|
||||||
# реагировать не на что, и отсутствие звонков не штраф.
|
# реагировать не на что, и отсутствие звонков не штраф.
|
||||||
reports = [report for report in card.phone_reports if report.service == service]
|
reports = [report for report in card.phone_reports if report.service == service]
|
||||||
|
if report_reaction_enabled:
|
||||||
reaction = report_reaction(service, marks, reports, report_reaction_ms, pause_intervals)
|
reaction = report_reaction(service, marks, reports, report_reaction_ms, pause_intervals)
|
||||||
if reaction is not None:
|
if reaction is not None:
|
||||||
metrics.append(reaction[0])
|
metrics.append(reaction[0])
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,8 @@ def to_csv(report: SessionReport) -> bytes:
|
||||||
row("Занятие", "", "Попытка", report.attempt)
|
row("Занятие", "", "Попытка", report.attempt)
|
||||||
row("Критерии", "", "Лимит решения ДДС, с", report.criteria.decision_time_limit_seconds)
|
row("Критерии", "", "Лимит решения ДДС, с", report.criteria.decision_time_limit_seconds)
|
||||||
row("Критерии", "", "Лимит реакции на доклад бригады, с",
|
row("Критерии", "", "Лимит реакции на доклад бригады, с",
|
||||||
report.criteria.dds_report_reaction_limit_seconds)
|
report.criteria.dds_report_reaction_limit_seconds
|
||||||
|
if report.criteria.dds_report_reaction_enabled else "не применяется")
|
||||||
row("Критерии", "", "Допустимо ошибок", report.criteria.allowed_errors)
|
row("Критерии", "", "Допустимо ошибок", report.criteria.allowed_errors)
|
||||||
row("Критерии", "", "Грамматика обязательна", "да" if report.criteria.require_correct_grammar else "нет")
|
row("Критерии", "", "Грамматика обязательна", "да" if report.criteria.require_correct_grammar else "нет")
|
||||||
row("Критерии", "", "Переопределения весов", report.criteria.score_weights or "стандартные")
|
row("Критерии", "", "Переопределения весов", report.criteria.score_weights or "стандартные")
|
||||||
|
|
@ -172,7 +173,9 @@ def to_pdf(report: SessionReport) -> bytes:
|
||||||
("Режим", report.mode.value),
|
("Режим", report.mode.value),
|
||||||
("Попытка", report.attempt),
|
("Попытка", report.attempt),
|
||||||
("Лимит решения ДДС", f"{report.criteria.decision_time_limit_seconds} с"),
|
("Лимит решения ДДС", f"{report.criteria.decision_time_limit_seconds} с"),
|
||||||
("Лимит реакции на доклад бригады", f"{report.criteria.dds_report_reaction_limit_seconds} с"),
|
("Лимит реакции на доклад бригады",
|
||||||
|
f"{report.criteria.dds_report_reaction_limit_seconds} с"
|
||||||
|
if report.criteria.dds_report_reaction_enabled else "не применяется"),
|
||||||
("Допустимо ошибок", report.criteria.allowed_errors),
|
("Допустимо ошибок", report.criteria.allowed_errors),
|
||||||
("Грамматика обязательна", "да" if report.criteria.require_correct_grammar else "нет"),
|
("Грамматика обязательна", "да" if report.criteria.require_correct_grammar else "нет"),
|
||||||
("Переопределения весов", report.criteria.score_weights or "стандартные"),
|
("Переопределения весов", report.criteria.score_weights or "стандартные"),
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,13 @@ def score_dds_card(state, card: DdsLiveCard) -> DdsCardRecord:
|
||||||
expected_decision_reason=dds_decision.reason,
|
expected_decision_reason=dds_decision.reason,
|
||||||
phone_reports=card.phone_reports,
|
phone_reports=card.phone_reports,
|
||||||
report_reaction_ms=reaction_ms,
|
report_reaction_ms=reaction_ms,
|
||||||
|
report_reaction_enabled=state.criteria.dds_report_reaction_enabled,
|
||||||
pause_intervals=state.pause_intervals,
|
pause_intervals=state.pause_intervals,
|
||||||
)
|
)
|
||||||
metrics = dispatcher_metrics(
|
metrics = dispatcher_metrics(
|
||||||
card, decision_limit_ms, dds_decision.expected, dds_decision.reason,
|
card, decision_limit_ms, dds_decision.expected, dds_decision.reason,
|
||||||
services=services, report_reaction_ms=reaction_ms,
|
services=services, report_reaction_ms=reaction_ms,
|
||||||
|
report_reaction_enabled=state.criteria.dds_report_reaction_enabled,
|
||||||
pause_intervals=state.pause_intervals,
|
pause_intervals=state.pause_intervals,
|
||||||
)
|
)
|
||||||
weighted = GostResult(metrics=metrics, findings=findings)
|
weighted = GostResult(metrics=metrics, findings=findings)
|
||||||
|
|
|
||||||
|
|
@ -234,11 +234,13 @@ async def score_session(
|
||||||
expected_decision_reason=scenario.dds_decision.reason,
|
expected_decision_reason=scenario.dds_decision.reason,
|
||||||
phone_reports=live_card.phone_reports,
|
phone_reports=live_card.phone_reports,
|
||||||
report_reaction_ms=reaction_ms,
|
report_reaction_ms=reaction_ms,
|
||||||
|
report_reaction_enabled=snapshot.criteria.dds_report_reaction_enabled,
|
||||||
pause_intervals=snapshot.pause_intervals,
|
pause_intervals=snapshot.pause_intervals,
|
||||||
))
|
))
|
||||||
result.metrics.extend(dispatcher_metrics(
|
result.metrics.extend(dispatcher_metrics(
|
||||||
live_card, decision_limit_ms, scenario.dds_decision.expected,
|
live_card, decision_limit_ms, scenario.dds_decision.expected,
|
||||||
scenario.dds_decision.reason, services=services, report_reaction_ms=reaction_ms,
|
scenario.dds_decision.reason, services=services, report_reaction_ms=reaction_ms,
|
||||||
|
report_reaction_enabled=snapshot.criteria.dds_report_reaction_enabled,
|
||||||
pause_intervals=snapshot.pause_intervals,
|
pause_intervals=snapshot.pause_intervals,
|
||||||
))
|
))
|
||||||
# Карточки ДДС уже взвешены каждая своим сценарием плюс настройкой
|
# Карточки ДДС уже взвешены каждая своим сценарием плюс настройкой
|
||||||
|
|
|
||||||
|
|
@ -455,6 +455,7 @@ def test_instructor_criteria_change_timer_and_success_threshold(client, monkeypa
|
||||||
"decision_time_limit_seconds": 45,
|
"decision_time_limit_seconds": 45,
|
||||||
"card_fill_time_limit_seconds": 180,
|
"card_fill_time_limit_seconds": 180,
|
||||||
"dds_card_work_time_limit_seconds": 180,
|
"dds_card_work_time_limit_seconds": 180,
|
||||||
|
"dds_report_reaction_enabled": False,
|
||||||
"dds_report_reaction_limit_seconds": 20,
|
"dds_report_reaction_limit_seconds": 20,
|
||||||
"allowed_errors": 50,
|
"allowed_errors": 50,
|
||||||
"require_correct_grammar": False,
|
"require_correct_grammar": False,
|
||||||
|
|
@ -749,7 +750,7 @@ def test_dds_card_over_three_minutes_has_e3_finding_and_actual_deviation(client)
|
||||||
|
|
||||||
|
|
||||||
def test_complete_dds_workflow_with_training_calls_and_status_updates(client):
|
def test_complete_dds_workflow_with_training_calls_and_status_updates(client):
|
||||||
session_id, control = start(client)
|
session_id, control = start(client, criteria={"dds_report_reaction_enabled": True})
|
||||||
try:
|
try:
|
||||||
state = hub.get(session_id)
|
state = hub.get(session_id)
|
||||||
with client.websocket_connect(f"/ws/station/{session_id}") as station:
|
with client.websocket_connect(f"/ws/station/{session_id}") as station:
|
||||||
|
|
@ -835,6 +836,7 @@ def send_status(station, service, status):
|
||||||
def test_dds_report_reaction_goes_from_criteria_to_report(client, reaction, passed):
|
def test_dds_report_reaction_goes_from_criteria_to_report(client, reaction, passed):
|
||||||
"""Лимит из `scenario.start` доходит до метрики, отметки D6 и отчёта."""
|
"""Лимит из `scenario.start` доходит до метрики, отметки D6 и отчёта."""
|
||||||
session_id, control = start(client, criteria={
|
session_id, control = start(client, criteria={
|
||||||
|
"dds_report_reaction_enabled": True,
|
||||||
"dds_report_reaction_limit_seconds": 20,
|
"dds_report_reaction_limit_seconds": 20,
|
||||||
"score_weights": {"dds_report_reaction": 3.5},
|
"score_weights": {"dds_report_reaction": 3.5},
|
||||||
})
|
})
|
||||||
|
|
@ -876,6 +878,7 @@ def test_dds_report_reaction_goes_from_criteria_to_report(client, reaction, pass
|
||||||
|
|
||||||
report = client.get(f"/api/sessions/{session_id}/report").json()
|
report = client.get(f"/api/sessions/{session_id}/report").json()
|
||||||
assert report["criteria"]["dds_report_reaction_limit_seconds"] == 20
|
assert report["criteria"]["dds_report_reaction_limit_seconds"] == 20
|
||||||
|
assert report["criteria"]["dds_report_reaction_enabled"] is True
|
||||||
assert [item for item in report["metrics"]
|
assert [item for item in report["metrics"]
|
||||||
if item["key"] == "dds_report_reaction"] == metrics
|
if item["key"] == "dds_report_reaction"] == metrics
|
||||||
if reaction == "slow":
|
if reaction == "slow":
|
||||||
|
|
@ -889,7 +892,7 @@ def test_dds_report_reaction_goes_from_criteria_to_report(client, reaction, pass
|
||||||
def test_refusal_after_crew_report_does_not_lower_score_without_finding(client):
|
def test_refusal_after_crew_report_does_not_lower_score_without_finding(client):
|
||||||
"""Бригада доложила о прибытии, но работы не проводились: отказ с причиной
|
"""Бригада доложила о прибытии, но работы не проводились: отказ с причиной
|
||||||
— терминальный исход, неотмеченное «Прибытие» не штрафуется."""
|
— терминальный исход, неотмеченное «Прибытие» не штрафуется."""
|
||||||
session_id, control = start(client)
|
session_id, control = start(client, criteria={"dds_report_reaction_enabled": True})
|
||||||
try:
|
try:
|
||||||
state = hub.get(session_id)
|
state = hub.get(session_id)
|
||||||
with client.websocket_connect(f"/ws/station/{session_id}") as station:
|
with client.websocket_connect(f"/ws/station/{session_id}") as station:
|
||||||
|
|
|
||||||
|
|
@ -324,10 +324,12 @@ def reaction(status_log, reports, limit_ms=45_000, pause_intervals=None):
|
||||||
entries=status_log, services=SERVICES, crew_assignments=card.crew_assignments,
|
entries=status_log, services=SERVICES, crew_assignments=card.crew_assignments,
|
||||||
deadline_ms=30_000, elapsed_ms=5_000, phone_reports=reports,
|
deadline_ms=30_000, elapsed_ms=5_000, phone_reports=reports,
|
||||||
report_reaction_ms=limit_ms,
|
report_reaction_ms=limit_ms,
|
||||||
|
report_reaction_enabled=True,
|
||||||
pause_intervals=pause_intervals,
|
pause_intervals=pause_intervals,
|
||||||
)
|
)
|
||||||
metric = next((m for m in dispatcher_metrics(card, 30_000, services=SERVICES,
|
metric = next((m for m in dispatcher_metrics(card, 30_000, services=SERVICES,
|
||||||
report_reaction_ms=limit_ms,
|
report_reaction_ms=limit_ms,
|
||||||
|
report_reaction_enabled=True,
|
||||||
pause_intervals=pause_intervals)
|
pause_intervals=pause_intervals)
|
||||||
if m.key == "dds_report_reaction"), None)
|
if m.key == "dds_report_reaction"), None)
|
||||||
return [f for f in findings if f.metric_key == "dds_report_reaction"], metric
|
return [f for f in findings if f.metric_key == "dds_report_reaction"], metric
|
||||||
|
|
|
||||||
|
|
@ -68,11 +68,21 @@ def test_csv_contains_sections_and_blocks_formula_injection():
|
||||||
assert any(row[0] == "Занятие" and row[3] == "'=1+1" for row in rows)
|
assert any(row[0] == "Занятие" and row[3] == "'=1+1" for row in rows)
|
||||||
assert any(row[0] == "Ошибки" and row[3] == "'+HYPERLINK(\"x\")" for row in rows)
|
assert any(row[0] == "Ошибки" and row[3] == "'+HYPERLINK(\"x\")" for row in rows)
|
||||||
assert any(row[0] == "Самооценка" and row[3] == "'@SUM(1,2)" for row in rows)
|
assert any(row[0] == "Самооценка" and row[3] == "'@SUM(1,2)" for row in rows)
|
||||||
assert ["Критерии", "", "Лимит реакции на доклад бригады, с", "45", ""] in rows
|
assert ["Критерии", "", "Лимит реакции на доклад бригады, с", "не применяется", ""] in rows
|
||||||
assert _cell(" =cmd") == "' =cmd"
|
assert _cell(" =cmd") == "' =cmd"
|
||||||
assert _cell("\tОбычный текст") == "'\tОбычный текст"
|
assert _cell("\tОбычный текст") == "'\tОбычный текст"
|
||||||
|
|
||||||
|
|
||||||
|
def test_csv_shows_enabled_report_reaction_limit():
|
||||||
|
report = sample_report()
|
||||||
|
report.criteria.dds_report_reaction_enabled = True
|
||||||
|
report.criteria.dds_report_reaction_limit_seconds = 20
|
||||||
|
|
||||||
|
rows = list(csv.reader(io.StringIO(to_csv(report).decode("utf-8-sig"))))
|
||||||
|
|
||||||
|
assert ["Критерии", "", "Лимит реакции на доклад бригады, с", "20", ""] in rows
|
||||||
|
|
||||||
|
|
||||||
def test_pdf_supports_cyrillic_and_spans_pages(tmp_path):
|
def test_pdf_supports_cyrillic_and_spans_pages(tmp_path):
|
||||||
data = to_pdf(sample_report(long=True))
|
data = to_pdf(sample_report(long=True))
|
||||||
assert data.startswith(b"%PDF-")
|
assert data.startswith(b"%PDF-")
|
||||||
|
|
@ -91,6 +101,7 @@ def test_pdf_supports_cyrillic_and_spans_pages(tmp_path):
|
||||||
assert "Адрес должен быть уточнён" in extracted
|
assert "Адрес должен быть уточнён" in extracted
|
||||||
assert "Заявитель сообщает о дыме" in extracted
|
assert "Заявитель сообщает о дыме" in extracted
|
||||||
assert "Лимит реакции на доклад бригады" in extracted
|
assert "Лимит реакции на доклад бригады" in extracted
|
||||||
|
assert "не применяется" in extracted
|
||||||
|
|
||||||
|
|
||||||
def test_certificate_pdf_contains_saved_result(tmp_path):
|
def test_certificate_pdf_contains_saved_result(tmp_path):
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ def test_active_dds_session_round_trips_without_losing_work():
|
||||||
|
|
||||||
def test_report_reaction_keeps_times_and_limit_after_restore():
|
def test_report_reaction_keeps_times_and_limit_after_restore():
|
||||||
before = dds_state()
|
before = dds_state()
|
||||||
|
before.criteria.dds_report_reaction_enabled = True
|
||||||
before.criteria.dds_report_reaction_limit_seconds = 20
|
before.criteria.dds_report_reaction_limit_seconds = 20
|
||||||
card = before.desk.active
|
card = before.desk.active
|
||||||
service = before.card_services(card)[0]
|
service = before.card_services(card)[0]
|
||||||
|
|
@ -102,6 +103,7 @@ def test_report_reaction_keeps_times_and_limit_after_restore():
|
||||||
restored = load_state(dump_state(before), now_utc())
|
restored = load_state(dump_state(before), now_utc())
|
||||||
|
|
||||||
assert restored.criteria.dds_report_reaction_limit_seconds == 20
|
assert restored.criteria.dds_report_reaction_limit_seconds == 20
|
||||||
|
assert restored.criteria.dds_report_reaction_enabled
|
||||||
assert restored.desk.active.phone_reports == card.phone_reports
|
assert restored.desk.active.phone_reports == card.phone_reports
|
||||||
|
|
||||||
def reaction(state):
|
def reaction(state):
|
||||||
|
|
@ -118,6 +120,31 @@ def test_report_reaction_keeps_times_and_limit_after_restore():
|
||||||
assert reaction(restored) == reaction(before), "оценка после восстановления та же"
|
assert reaction(restored) == reaction(before), "оценка после восстановления та же"
|
||||||
|
|
||||||
|
|
||||||
|
def test_report_reaction_default_off_keeps_status_completeness_d6():
|
||||||
|
state = dds_state()
|
||||||
|
card = state.desk.active
|
||||||
|
service = state.card_services(card)[0]
|
||||||
|
card.set_service_status(service, ServiceStatus.RESPONDING,
|
||||||
|
"Основание: доклад.\nСведения: бригада выехала.")
|
||||||
|
without_report = state.model_copy(deep=True)
|
||||||
|
card.phone_reports.append(PhoneReportRecord(
|
||||||
|
service=service, crew=card.crew_selected, phase="dispatched",
|
||||||
|
text="Бригада выехала", at=card.status_log[-1].at - timedelta(seconds=70),
|
||||||
|
))
|
||||||
|
|
||||||
|
result = score_dds_card(state, card)
|
||||||
|
baseline = score_dds_card(without_report, without_report.desk.active)
|
||||||
|
|
||||||
|
assert state.criteria.dds_report_reaction_enabled is False
|
||||||
|
assert result.score_auto == baseline.score_auto
|
||||||
|
assert [(item.code, item.metric_key, item.fact) for item in result.findings] == [
|
||||||
|
(item.code, item.metric_key, item.fact) for item in baseline.findings
|
||||||
|
]
|
||||||
|
assert not any(item.key == "dds_report_reaction" for item in result.metrics)
|
||||||
|
assert not any(item.metric_key == "dds_report_reaction" for item in result.findings)
|
||||||
|
assert any(item.code is ErrorCode.D6 for item in result.findings)
|
||||||
|
|
||||||
|
|
||||||
def completed_card() -> DdsCardRecord:
|
def completed_card() -> DdsCardRecord:
|
||||||
return DdsCardRecord(
|
return DdsCardRecord(
|
||||||
card_id=uuid4(),
|
card_id=uuid4(),
|
||||||
|
|
|
||||||
|
|
@ -628,6 +628,7 @@ def test_repeated_pause_and_resume_are_idempotent(clock, monkeypatch):
|
||||||
def test_report_reaction_excludes_pause_after_control_and_checkpoint(clock, monkeypatch):
|
def test_report_reaction_excludes_pause_after_control_and_checkpoint(clock, monkeypatch):
|
||||||
base = datetime(2026, 9, 27, 10, tzinfo=UTC)
|
base = datetime(2026, 9, 27, 10, tzinfo=UTC)
|
||||||
state, card = _dds_state_with_active_card(clock)
|
state, card = _dds_state_with_active_card(clock)
|
||||||
|
state.criteria.dds_report_reaction_enabled = True
|
||||||
service = state.card_services(card)[0]
|
service = state.card_services(card)[0]
|
||||||
monkeypatch.setattr("app.session.dds.now_utc", lambda: base)
|
monkeypatch.setattr("app.session.dds.now_utc", lambda: base)
|
||||||
card.set_service_status(service, ServiceStatus.ACCEPTED, "Карточка принята")
|
card.set_service_status(service, ServiceStatus.ACCEPTED, "Карточка принята")
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,9 @@ export function Debrief({ report, big = false }: { report: SessionReport; big?:
|
||||||
{` ${report.criteria.card_fill_time_limit_seconds ?? 180} с`}</>}
|
{` ${report.criteria.card_fill_time_limit_seconds ?? 180} с`}</>}
|
||||||
{/* 112 → ДДС приходит как exercise "card": работу ДДС выдают карточки пульта. */}
|
{/* 112 → ДДС приходит как exercise "card": работу ДДС выдают карточки пульта. */}
|
||||||
{(report.exercise === "dds" || Boolean(report.card_results?.length)) && <>; статус по докладу бригады —
|
{(report.exercise === "dds" || Boolean(report.card_results?.length)) && <>; статус по докладу бригады —
|
||||||
{` не позже ${report.criteria.dds_report_reaction_limit_seconds ?? 45} с`}</>}
|
{report.criteria.dds_report_reaction_enabled
|
||||||
|
? ` не позже ${report.criteria.dds_report_reaction_limit_seconds ?? 45} с`
|
||||||
|
: " не применяется"}</>}
|
||||||
{report.exercise && report.exercise !== "dds" && <>
|
{report.exercise && report.exercise !== "dds" && <>
|
||||||
; грамматика описания КИО {report.criteria.require_correct_grammar ? "входит" : "не входит"} в оценку
|
; грамматика описания КИО {report.criteria.require_correct_grammar ? "входит" : "не входит"} в оценку
|
||||||
</>}.
|
</>}.
|
||||||
|
|
|
||||||
|
|
@ -47,10 +47,12 @@ const REVIEW_METRICS: MetricOption[] = [
|
||||||
|
|
||||||
export function ScoreWeightsEditor({
|
export function ScoreWeightsEditor({
|
||||||
exercise,
|
exercise,
|
||||||
|
reportReactionEnabled,
|
||||||
values,
|
values,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
exercise: LessonExercise;
|
exercise: LessonExercise;
|
||||||
|
reportReactionEnabled: boolean;
|
||||||
values: Record<string, number>;
|
values: Record<string, number>;
|
||||||
onChange: (weights: Record<string, number>) => void;
|
onChange: (weights: Record<string, number>) => void;
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -63,6 +65,11 @@ export function ScoreWeightsEditor({
|
||||||
? [["Карточка КИО", CARD_METRICS], ["Реагирование ДДС", DDS_METRICS]]
|
? [["Карточка КИО", CARD_METRICS], ["Реагирование ДДС", DDS_METRICS]]
|
||||||
: [["Опрос 112", CALL_METRICS], ["Реагирование ДДС", DDS_METRICS]];
|
: [["Опрос 112", CALL_METRICS], ["Реагирование ДДС", DDS_METRICS]];
|
||||||
groups.push(["Разбор", REVIEW_METRICS]);
|
groups.push(["Разбор", REVIEW_METRICS]);
|
||||||
|
if (!reportReactionEnabled) {
|
||||||
|
for (const group of groups) {
|
||||||
|
group[1] = group[1].filter(({ key }) => key !== "dds_report_reaction");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setWeight(key: string, raw: string) {
|
function setWeight(key: string, raw: string) {
|
||||||
const next = { ...values };
|
const next = { ...values };
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ export function Instructor() {
|
||||||
const [decisionTimeLimit, setDecisionTimeLimit] = useState(30);
|
const [decisionTimeLimit, setDecisionTimeLimit] = useState(30);
|
||||||
const [cardFillTimeLimit, setCardFillTimeLimit] = useState(180);
|
const [cardFillTimeLimit, setCardFillTimeLimit] = useState(180);
|
||||||
const [ddsCardWorkTimeLimit, setDdsCardWorkTimeLimit] = useState(180);
|
const [ddsCardWorkTimeLimit, setDdsCardWorkTimeLimit] = useState(180);
|
||||||
|
const [ddsReportReactionEnabled, setDdsReportReactionEnabled] = useState(false);
|
||||||
const [ddsReportReactionLimit, setDdsReportReactionLimit] = useState(45);
|
const [ddsReportReactionLimit, setDdsReportReactionLimit] = useState(45);
|
||||||
const [allowedErrors, setAllowedErrors] = useState(0);
|
const [allowedErrors, setAllowedErrors] = useState(0);
|
||||||
const [requireCorrectGrammar, setRequireCorrectGrammar] = useState(true);
|
const [requireCorrectGrammar, setRequireCorrectGrammar] = useState(true);
|
||||||
|
|
@ -292,6 +293,7 @@ export function Instructor() {
|
||||||
&& !(exercise === "dds" && wholeTicket)
|
&& !(exercise === "dds" && wholeTicket)
|
||||||
? visibleScenarios.filter((item) => item.outcome === "card").map((item) => item.id)
|
? visibleScenarios.filter((item) => item.outcome === "card").map((item) => item.id)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const reportReactionEnabled = (exercise === "dds" || exercise === "card_dds") && ddsReportReactionEnabled;
|
||||||
const sent = control.start(sessionId, {
|
const sent = control.start(sessionId, {
|
||||||
scenarioId: primaryScenarioId,
|
scenarioId: primaryScenarioId,
|
||||||
mode,
|
mode,
|
||||||
|
|
@ -308,15 +310,18 @@ export function Instructor() {
|
||||||
decision_time_limit_seconds: decisionTimeLimit,
|
decision_time_limit_seconds: decisionTimeLimit,
|
||||||
card_fill_time_limit_seconds: cardFillTimeLimit,
|
card_fill_time_limit_seconds: cardFillTimeLimit,
|
||||||
dds_card_work_time_limit_seconds: ddsCardWorkTimeLimit,
|
dds_card_work_time_limit_seconds: ddsCardWorkTimeLimit,
|
||||||
|
dds_report_reaction_enabled: reportReactionEnabled,
|
||||||
dds_report_reaction_limit_seconds: ddsReportReactionLimit,
|
dds_report_reaction_limit_seconds: ddsReportReactionLimit,
|
||||||
allowed_errors: allowedErrors,
|
allowed_errors: allowedErrors,
|
||||||
require_correct_grammar: requireCorrectGrammar,
|
require_correct_grammar: requireCorrectGrammar,
|
||||||
score_weights: lessonScoreWeights,
|
score_weights: reportReactionEnabled
|
||||||
|
? lessonScoreWeights
|
||||||
|
: Object.fromEntries(Object.entries(lessonScoreWeights).filter(([key]) => key !== "dds_report_reaction")),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (sent) toStart.current = null;
|
if (sent) toStart.current = null;
|
||||||
}, [sessionId, control, scenarioId, extraScenarioIds, mode, trainee, traineeId, trainees.data,
|
}, [sessionId, control, scenarioId, extraScenarioIds, mode, trainee, traineeId, trainees.data,
|
||||||
exercise, decisionTimeLimit, cardFillTimeLimit, ddsCardWorkTimeLimit, ddsReportReactionLimit, allowedErrors, requireCorrectGrammar, wholeTicket, selectedTicketScenarios,
|
exercise, decisionTimeLimit, cardFillTimeLimit, ddsCardWorkTimeLimit, ddsReportReactionEnabled, ddsReportReactionLimit, allowedErrors, requireCorrectGrammar, wholeTicket, selectedTicketScenarios,
|
||||||
ddsArrivalIntervalSeconds, ddsMaxWaiting, lessonScoreWeights, scenarios.data,
|
ddsArrivalIntervalSeconds, ddsMaxWaiting, lessonScoreWeights, scenarios.data,
|
||||||
randomScenario, visibleScenarios]);
|
randomScenario, visibleScenarios]);
|
||||||
|
|
||||||
|
|
@ -664,13 +669,20 @@ export function Instructor() {
|
||||||
onCommit={setDdsCardWorkTimeLimit} />
|
onCommit={setDdsCardWorkTimeLimit} />
|
||||||
<small className="ref">По умолчанию 180 секунд; не заменяет 30-секундное первичное решение.</small>
|
<small className="ref">По умолчанию 180 секунд; не заменяет 30-секундное первичное решение.</small>
|
||||||
</label>}
|
</label>}
|
||||||
{(exercise === "dds" || exercise === "card_dds") && <label className="lesson-criterion">
|
{(exercise === "dds" || exercise === "card_dds") && <>
|
||||||
Реакция на доклад бригады, секунд
|
<label className="mode-choice">
|
||||||
|
<input type="checkbox" checked={ddsReportReactionEnabled}
|
||||||
|
onChange={(event) => setDdsReportReactionEnabled(event.target.checked)} />
|
||||||
|
Оценивать срок отметки статуса после доклада бригады
|
||||||
|
</label>
|
||||||
|
{ddsReportReactionEnabled && <label className="lesson-criterion">
|
||||||
|
Лимит реакции на доклад бригады, секунд
|
||||||
<LessonNumberInput label="Лимит реакции на доклад бригады, секунд"
|
<LessonNumberInput label="Лимит реакции на доклад бригады, секунд"
|
||||||
value={ddsReportReactionLimit} min={5} max={600} fallback={45}
|
value={ddsReportReactionLimit} min={5} max={600} fallback={45}
|
||||||
onCommit={setDdsReportReactionLimit} />
|
onCommit={setDdsReportReactionLimit} />
|
||||||
<small className="ref">По умолчанию 45 секунд: статус по докладу бригады — не позже лимита.</small>
|
<small className="ref">Учебный лимит при включённом критерии; исходное значение — 45 секунд.</small>
|
||||||
</label>}
|
</label>}
|
||||||
|
</>}
|
||||||
<label className="lesson-criterion">
|
<label className="lesson-criterion">
|
||||||
Допустимо нарушенных метрик
|
Допустимо нарушенных метрик
|
||||||
<LessonNumberInput label="Допустимое число ошибок" value={allowedErrors}
|
<LessonNumberInput label="Допустимое число ошибок" value={allowedErrors}
|
||||||
|
|
@ -682,6 +694,7 @@ export function Instructor() {
|
||||||
Грамматика карточки оператора 112 входит в оценку
|
Грамматика карточки оператора 112 входит в оценку
|
||||||
</label>}
|
</label>}
|
||||||
<ScoreWeightsEditor exercise={exercise === "card_dds" ? "card_dds" : exercise}
|
<ScoreWeightsEditor exercise={exercise === "card_dds" ? "card_dds" : exercise}
|
||||||
|
reportReactionEnabled={(exercise === "dds" || exercise === "card_dds") && ddsReportReactionEnabled}
|
||||||
values={lessonScoreWeights} onChange={setLessonScoreWeights} />
|
values={lessonScoreWeights} onChange={setLessonScoreWeights} />
|
||||||
<small className="ref">Нормативы ГОСТ для приёма вызова не изменяются.</small>
|
<small className="ref">Нормативы ГОСТ для приёма вызова не изменяются.</small>
|
||||||
</details>
|
</details>
|
||||||
|
|
|
||||||
|
|
@ -400,6 +400,7 @@ export interface LessonCriteria {
|
||||||
decision_time_limit_seconds?: number;
|
decision_time_limit_seconds?: number;
|
||||||
card_fill_time_limit_seconds?: number;
|
card_fill_time_limit_seconds?: number;
|
||||||
dds_card_work_time_limit_seconds?: number;
|
dds_card_work_time_limit_seconds?: number;
|
||||||
|
dds_report_reaction_enabled?: boolean;
|
||||||
dds_report_reaction_limit_seconds?: number;
|
dds_report_reaction_limit_seconds?: number;
|
||||||
allowed_errors?: number;
|
allowed_errors?: number;
|
||||||
require_correct_grammar?: boolean;
|
require_correct_grammar?: boolean;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue