from __future__ import annotations import json import re import unicodedata from dataclasses import dataclass from datetime import date, timedelta from pathlib import Path from .config import ROOT, client_dir from .history import now_local from .table import print_table GLOBAL_REMINDERS_PATH = ROOT / "reminders.jsonl" @dataclass(frozen=True) class Reminder: id: str created_at: str due_date: str text: str client: str status: str @classmethod def from_dict(cls, data: dict) -> "Reminder": return cls( id=str(data.get("id", "")), created_at=str(data.get("created_at", "")), due_date=str(data.get("due_date", "")), text=str(data.get("text", "")), client=str(data.get("client", "")), status=str(data.get("status", "active")), ) def to_dict(self) -> dict: return { "id": self.id, "created_at": self.created_at, "due_date": self.due_date, "text": self.text, "client": self.client, "status": self.status, } def normalize_text(value: str) -> str: normalized = unicodedata.normalize("NFKD", value) without_marks = "".join(ch for ch in normalized if not unicodedata.combining(ch)) return without_marks.lower() def reminder_path(domain: str | None = None) -> Path: if domain: base = client_dir(domain) return base / "reminders.jsonl" return GLOBAL_REMINDERS_PATH def load_reminders(domain: str | None = None, include_global: bool = False) -> list[Reminder]: paths = [] if include_global: paths.append(GLOBAL_REMINDERS_PATH) paths.append(reminder_path(domain)) reminders: list[Reminder] = [] for path in paths: if not path.exists(): continue for line in path.read_text(encoding="utf-8").splitlines(): if not line.strip(): continue try: reminders.append(Reminder.from_dict(json.loads(line))) except json.JSONDecodeError: continue reminders.sort(key=lambda item: (item.due_date, item.created_at, item.id)) return reminders def parse_reminder_text(raw_text: str) -> tuple[date, str]: text = " ".join(raw_text.split()).strip() if not text: raise ValueError("Brak tresci przypomnienia.") today = now_local().date() normalized = normalize_text(text) due = today matched_prefix = "" patterns = [ (r"^za\s+(\d+)\s+dni(?:\s+|$)", "days"), (r"^za\s+(\d+)\s+dzien(?:\s+|$)", "days"), (r"^za\s+(\d+)\s+tygodnie(?:\s+|$)", "weeks"), (r"^za\s+(\d+)\s+tygodni(?:\s+|$)", "weeks"), (r"^za\s+(\d+)\s+tydzien(?:\s+|$)", "weeks"), (r"^za\s+(\d+)\s+miesiace(?:\s+|$)", "months"), (r"^za\s+(\d+)\s+miesiecy(?:\s+|$)", "months"), (r"^za\s+(\d+)\s+miesiac(?:\s+|$)", "months"), ] for pattern, unit in patterns: match = re.search(pattern, normalized) if not match: continue amount = int(match.group(1)) if unit == "days": due = today + timedelta(days=amount) elif unit == "weeks": due = today + timedelta(weeks=amount) else: due = today + timedelta(days=amount * 30) matched_prefix = text[: match.end()].strip() break if not matched_prefix: if normalized.startswith("jutro"): due = today + timedelta(days=1) matched_prefix = text[:5].strip() elif normalized.startswith("dzisiaj"): due = today matched_prefix = text[:7].strip() note = text[len(matched_prefix) :].strip() if matched_prefix else text note_normalized = normalize_text(note) for prefix in [ "przypomnij mi o ", "przypomnij o ", "przypomnienie o ", "o ", ]: if note_normalized.startswith(prefix): note = note[len(prefix) :].strip() break if not note: note = text return due, note def add_reminder(raw_text: str, domain: str | None = None) -> Reminder: due, note = parse_reminder_text(raw_text) ts = now_local() reminder = Reminder( id=f"rem_{ts.strftime('%Y%m%d%H%M%S')}", created_at=ts.isoformat(timespec="seconds"), due_date=due.isoformat(), text=note, client=domain or "", status="active", ) path = reminder_path(domain) path.parent.mkdir(parents=True, exist_ok=True) with path.open("a", encoding="utf-8") as f: f.write(json.dumps(reminder.to_dict(), ensure_ascii=False) + "\n") return reminder def active_reminders_for_client(domain: str) -> list[Reminder]: return [item for item in load_reminders(domain, include_global=True) if item.status == "active"] def reminder_status_label(reminder: Reminder) -> str: today = now_local().date() try: due = date.fromisoformat(reminder.due_date) except ValueError: return "brak daty" days = (due - today).days if days < 0: return f"po terminie {abs(days)} dni" if days == 0: return "dzisiaj" if days == 1: return "jutro" return f"za {days} dni" def print_client_reminders(domain: str, limit: int = 12) -> None: reminders = active_reminders_for_client(domain) if not reminders: return print("\nPrzypomnienia") shown = reminders[:limit] print_table( ["Termin", "Status", "Zakres", "Notatka"], [ [ reminder.due_date, reminder_status_label(reminder), reminder.client or "globalne", reminder.text, ] for reminder in shown ], ) if len(reminders) > len(shown): print(f"... oraz {len(reminders) - len(shown)} kolejnych przypomnien")