food-records/app/ui/page_journal.py

149 lines
5.5 KiB
Python
Raw Normal View History

"""Журнал изменений.
История, которую git по зашифрованному файлу дать не может: коммиты хранят
непрозрачные снимки, а внутри суток они ещё и схлопываются в один. Здесь видно,
что именно менялось, когда и с какой машины.
"""
from __future__ import annotations
from PySide6.QtWidgets import QComboBox, QLineEdit, QVBoxLayout, QWidget
from ..models import JournalEntry
from . import theme
from . import widgets as w
ENTITY_LABELS = {
"product": "Товары",
"batch": "Закупки",
"sale": "Продажи",
"counterparty": "Контрагенты",
"bakery_payment": "Платежи пекарне",
"settings": "Настройки",
"journal": "Журнал",
}
ACTION_LABELS = {
"product.create": "товар создан",
"product.update": "товар изменён",
"product.price_change": "цены изменены",
"product.archive": "товар в архив",
"product.delete": "товар удалён",
"counterparty.create": "контрагент создан",
"counterparty.update": "контрагент изменён",
"counterparty.delete": "контрагент удалён",
"batch.create": "закупка создана",
"batch.update": "закупка изменена",
"batch.close": "партия закрыта",
"batch.reopen": "партия открыта",
"batch.delete": "закупка удалена",
"sale.create": "продажа создана",
"sale.update": "продажа изменена",
"sale.delete": "продажа удалена",
"sale.payment_add": "принята оплата",
"sale.payment_delete": "оплата удалена",
"bakery_payment.add": "платёж пекарне",
"bakery_payment.delete": "платёж пекарне удалён",
"settings.update": "настройки изменены",
"journal.prune": "журнал обрезан",
}
ACTION_COLORS = {
"delete": theme.DANGER,
"create": theme.OK,
"add": theme.OK,
}
def describe(entry: JournalEntry) -> str:
"""Свернуть изменения в одну строку таблицы."""
parts = []
for change in entry.changes:
if change.old and change.new:
parts.append(f"{change.field}: {change.old}{change.new}")
elif change.new:
parts.append(f"{change.field}: {change.new}")
elif change.old:
parts.append(f"{change.field}: было {change.old}")
return "; ".join(parts)
def _color_for(action: str) -> str:
tail = action.rsplit(".", 1)[-1]
for needle, color in ACTION_COLORS.items():
if needle in tail:
return color
return ""
class JournalPage(QWidget):
def __init__(self, ctx, parent=None):
super().__init__(parent)
self.ctx = ctx
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 18, 20, 18)
layout.setSpacing(12)
layout.addWidget(w.heading("Журнал"))
layout.addWidget(
w.label(
"Что менялось в базе. Git хранит только зашифрованные снимки — "
"подробности есть здесь.",
"dim",
)
)
self.search = QLineEdit()
self.search.setPlaceholderText("Поиск: булка с маком, 30,00, Вася…")
self.search.setClearButtonEnabled(True)
self.search.textChanged.connect(self.refresh)
self.entity = QComboBox()
self.entity.addItem("Все разделы", None)
for key, title in ENTITY_LABELS.items():
self.entity.addItem(title, key)
self.entity.currentIndexChanged.connect(self.refresh)
self.count_label = w.label("", "dim")
layout.addWidget(w.row(self.search, self.entity, None, self.count_label, stretch_at=0))
self.table = w.table(
["Когда", "Машина", "Что произошло", "Объект", "Изменения"], stretch_column=4
)
self.table.setSortingEnabled(False) # журнал всегда в хронологии
layout.addWidget(self.table, 1)
def refresh(self) -> None:
needle = self.search.text().strip().lower()
wanted = self.entity.currentData()
rows = []
total = 0
for entry in reversed(self.ctx.vault.doc.journal):
total += 1
if wanted and entry.entity != wanted:
continue
description = describe(entry)
action = ACTION_LABELS.get(entry.action, entry.action)
haystack = " ".join([action, entry.label, description, entry.host]).lower()
if needle and needle not in haystack:
continue
when = entry.ts.replace("T", " ")[:16]
rows.append(
[
w.text_item(when),
w.text_item(entry.host, theme.MUTED),
w.text_item(action, _color_for(entry.action)),
w.text_item(entry.label),
w.text_item(description),
]
)
w.fill(self.table, rows)
self.count_label.setText(
f"показано {len(rows)} из {total}" if len(rows) != total else f"записей: {total}"
)