bmc_hub/tests/test_timetracking_manual_time.py
Christian abfb945679 feat(planner): add overdue cases handling and improve case rendering
- Introduced `overdueCases` state to track overdue cases in the planner.
- Enhanced `loadCases` function to fetch and store overdue cases.
- Updated `renderCases` to display overdue cases with appropriate labels.
- Modified case card rendering to highlight overdue cases.

fix(telefoni): ensure WebSocket connection on DOMContentLoaded

- Adjusted WebSocket connection logic to handle cases where the script loads after DOMContentLoaded.

test(tests): add tests for case creation and internal ticket handling

- Added tests to ensure ticket creation commits all optional data.
- Implemented tests for internal ticket creation, ensuring correct customer and user assignments.

feat(migrations): add new tables for email configuration and case assignments

- Created `email_connection_configurations` table for managing email settings.
- Introduced `sag_case_assignees` table to handle individual case assignments.
- Added `sag_case_details` table for structured case type details.
- Updated existing case types to align with new templates and added support for internal tickets.

fix(timetracking): allow manual time creation without case customer

- Modified manual time creation to support entries without a customer.
- Updated settlement options to recommend internal settlement for customerless time.

feat(v4): enhance case detail template with new features

- Added new features to the case detail template, including compact icons and inline relation creation.
- Improved comment actions and time tracking functionalities.
2026-09-20 03:33:10 +02:00

150 lines
6.0 KiB
Python

import asyncio
import importlib
from datetime import datetime, timedelta
from pathlib import Path
timetracking_router = importlib.import_module("app.timetracking.backend.router")
def test_live_timer_rounds_started_minute_up_instead_of_zero():
started = datetime(2026, 8, 30, 10, 0, 0)
entry = {"start_tid": started, "pause_total_seconds": 0, "paused_at": None}
assert timetracking_router._elapsed_minutes_excluding_pause(entry, started) == 0
assert timetracking_router._elapsed_minutes_excluding_pause(entry, started + timedelta(seconds=1)) == 1
assert timetracking_router._elapsed_minutes_excluding_pause(entry, started + timedelta(seconds=60)) == 1
assert timetracking_router._elapsed_minutes_excluding_pause(entry, started + timedelta(seconds=61)) == 2
def test_manual_time_keeps_local_wall_clock_and_creates_one_entry_per_employee(monkeypatch):
captured = {}
monkeypatch.setattr(timetracking_router, "_resolve_case_customer_id", lambda *_args: 77)
def fake_execute_query(query, params):
captured["query"] = query
captured["params"] = params
employee_ids = params[-1]
return [{"id": index + 1, "medarbejder_id": employee_id} for index, employee_id in enumerate(employee_ids)]
monkeypatch.setattr(timetracking_router, "execute_query", fake_execute_query)
result = asyncio.run(
timetracking_router.create_manual_time_v1(
{
"sag_id": 88,
"medarbejder_ids": [12, 34],
"worked_date": "2026-08-18",
"start_tid": "2026-08-18T10:00:00",
"slut_tid": "2026-08-18T11:00:00",
"faktisk_tid_min": 60,
},
current_user={"id": 9, "username": "tester"},
)
)
assert result["count"] == 2
assert [row["medarbejder_id"] for row in result["created"]] == [12, 34]
assert captured["params"][-1] == [12, 34]
assert captured["params"][10] == datetime(2026, 8, 18, 10, 0)
assert captured["params"][11] == datetime(2026, 8, 18, 11, 0)
assert "UNNEST(%s::bigint[])" in captured["query"]
def test_manual_time_bulk_resolves_current_user_and_removes_duplicates(monkeypatch):
captured = {}
monkeypatch.setattr(timetracking_router, "_resolve_case_customer_id", lambda *_args: 77)
def fake_execute_query(_query, params):
captured["employee_ids"] = params[-1]
return [{"id": 1, "medarbejder_id": employee_id} for employee_id in params[-1]]
monkeypatch.setattr(timetracking_router, "execute_query", fake_execute_query)
result = asyncio.run(
timetracking_router.create_manual_time_v1(
{
"sag_id": 88,
"medarbejder_ids": [None, 9, 12, 12],
"faktisk_tid_min": 30,
},
current_user={"id": 9, "username": "tester"},
)
)
assert captured["employee_ids"] == [9, 12]
assert result["count"] == 2
def test_manual_internal_time_can_be_created_without_case_customer(monkeypatch):
captured = {}
monkeypatch.setattr(timetracking_router, "_resolve_case_customer_id", lambda *_args: None)
def fake_execute_query(_query, params):
captured["params"] = params
return [{"id": 1, "customer_id": None, "billable": False}]
monkeypatch.setattr(timetracking_router, "execute_query", fake_execute_query)
result = asyncio.run(
timetracking_router.create_manual_time_v1(
{
"sag_id": 177,
"medarbejder_ids": [None],
"faktisk_tid_min": 15,
"entry_status": "kladde",
},
current_user={"id": 9, "username": "tester"},
)
)
assert result["count"] == 1
assert captured["params"][2] is None
assert captured["params"][8] is False
assert captured["params"][9] == "internal"
assert captured["params"][13] == 0
def test_customerless_time_recommends_internal_settlement():
result = timetracking_router._settlement_options_for_customer(None)
assert result["recommended"]["method"] == "internal"
assert result["prepaid_cards"] == []
assert result["agreements"] == []
def test_case_time_form_sends_local_time_and_multiple_employees():
template = Path("app/modules/sag/templates/detail_v3.html").read_text()
manual_form_script = template.split("async function createManualTimeV1(event)", 1)[1].split(
"document.addEventListener('DOMContentLoaded'", 1
)[0]
assert 'class="form-check-input mt-0 time-v1-employee-option"' in template
assert "{% for user in assignment_users %}" in template
assert "loadTimeV1EmployeeOptions" not in template
assert "medarbejder_ids: employeeIds" in manual_form_script
assert "startObj = `${dateVal}T${tStart}:00`;" in manual_form_script
assert "endObj = `${endDate}T${tEnd}:00`;" in manual_form_script
assert ".toISOString()" not in manual_form_script
quick_time_script = template.split("async function registerQuickTimeFromComment(content)", 1)[1].split(
"function resolveCommentAwaitCustomerStatus", 1
)[0]
assert "startIso = `${dateValue}T${startValue}:00`;" in quick_time_script
assert "endIso = addMinutesToTimeV1LocalIso(startIso, minutes);" in quick_time_script
assert ".toISOString()" not in quick_time_script
def test_bottom_bar_timer_changes_refresh_case_timer_panel():
bottom_bar = Path("static/js/bottom-bar.js").read_text()
case_template = Path("app/modules/sag/templates/detail_v3.html").read_text()
assert "notifyTimerStateChanged('paused')" in bottom_bar
assert "notifyTimerStateChanged('resumed'" in bottom_bar
assert "notifyTimerStateChanged('stopped')" in bottom_bar
assert "window.addEventListener('bb:timer-state-changed'" in case_template
assert "event.key === 'bmc:timer-state-changed'" in case_template
assert "timerChip.classList.toggle('is-hidden', !hasActiveTimer && !hasPausedTimer)" in bottom_bar
assert "timerText.textContent = 'Pauset · ' + name" in bottom_bar
assert "const visibleTimer = timer.active ? timer : (paused[0] || {})" in bottom_bar