"""Журнал изменений. История, которую 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}" )