Raspored: vannastavne aktivnosti (treninzi, mekteb) + upit "obaveze"

Aktivnosti su fiksnog vremena, sedmične, van smjena, mogu i vikendom.
- store.py: aktivnosti po djetetu (dan iz svih 7 dana, pocetak, kraj, naziv);
  activity_rows/set_activities/activities_for; canonical_dan_any (uklj. subota/nedjelja).
- manifest.py: sta="obaveze" (škola od-do + aktivnosti, sortirano po vremenu) i
  sta="aktivnosti"; rade i vikendom/bez škole. Instrumental oblici dana ("petkom",
  "subotom") u parsiranju.
- admin: editor aktivnosti na stranici djeteta (dodaj/uredi/obriši).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzKpYaEsuWDevuNnvSjcFW
This commit is contained in:
idriz
2026-09-13 08:41:33 +02:00
parent 59376b65d8
commit cb4e021111
4 changed files with 145 additions and 9 deletions

View File

@@ -88,11 +88,27 @@ def dijete(ime):
})
saved = store.set_child_schedule(ime, rows)
flash(f"Raspored sačuvan: {saved} časova.")
elif action == "aktivnosti":
n = int(request.form.get("anrows", "0"))
rows = []
for i in range(n):
if request.form.get(f"a{i}_del"):
continue
rows.append({
"dan": request.form.get(f"a{i}_dan", ""),
"pocetak": request.form.get(f"a{i}_pocetak", ""),
"kraj": request.form.get(f"a{i}_kraj", ""),
"naziv": request.form.get(f"a{i}_naziv", ""),
})
saved = store.set_activities(ime, rows)
flash(f"Aktivnosti sačuvane: {saved}.")
return redirect(url_for("raspored.dijete", ime=ime))
grid = store.child_rows(ime) + [{} for _ in range(BLANK_ROWS)]
agrid = store.activity_rows(ime) + [{} for _ in range(BLANK_ROWS)]
return render_template(
"raspored_dijete.html",
ime=ime, dijete=ch, grid=grid, nrows=len(grid),
dani=store.DANI, active_page="raspored",
agrid=agrid, anrows=len(agrid),
dani=store.DANI, svi_dani=store.SVI_DANI, active_page="raspored",
)

View File

@@ -14,8 +14,9 @@ log = logging.getLogger("idriz.raspored")
INSTRUCTIONS = """RASPORED ČASOVA (alat: raspored_upit): odgovaraš o školskom rasporedu djece (koje časove imaju, kad počinju/završavaju, koja je smjena). Podržano je više djece.
- Kad dijete/roditelj pita o rasporedu ("koje časove Ahmed ima danas?", "kad Hatidža danas počinje/završava školu?", "koja je smjena za Ahmeda sutra?"), pozovi raspored_upit.
- Argumenti: `dijete` (ime kako je izgovoreno), `kada` (danas/sutra/prekosutra/juce/prekjuce, naziv dana, ili datum kao "15.09." ili "2026-09-15"), i `sta`: "casovi" (koji časovi), "pocetak" (kad mora biti u školi), "kraj" (kad završava), "smjena".
- Ako nije rečeno šta tačno, koristi sta="casovi".
- Kad pita o OBAVEZAMA ("koje obaveze Ahmed ima danas/sutra/petkom?"), koristi sta="obaveze" — alat nabroji školu (od–do) i vannastavne aktivnosti (treninzi, mekteb...) tog dana. Za samo aktivnosti koristi sta="aktivnosti".
- Argumenti: `dijete` (ime kako je izgovoreno), `kada` (danas/sutra/prekosutra/juce/prekjuce, naziv dana i u obliku "petkom"/"subotom", ili datum kao "15.09." ili "2026-09-15"), i `sta`: casovi | pocetak | kraj | smjena | obaveze | aktivnosti.
- Ako nije rečeno šta tačno, koristi sta="casovi". Vannastavne aktivnosti nisu vezane za smjenu (fiksno vrijeme, sedmično, mogu biti i vikendom).
- Alat vraća gotovu rečenicu na bosanskom — izgovori je prirodno. Ne izmišljaj časove ni vremena.
- Ako alat kaže da dijete nije poznato ili da smjena nije postavljena, prenesi to."""
@@ -28,8 +29,8 @@ SCHEMA = {
"properties": {
"dijete": {"type": "string", "description": "Ime djeteta kako je izgovoreno."},
"kada": {"type": "string", "description": "danas, sutra, prekosutra, juce, prekjuce, naziv dana, ili datum (npr. '15.09.' ili '2026-09-15'). Prazno = danas."},
"sta": {"type": "string", "enum": ["casovi", "pocetak", "kraj", "smjena"],
"description": "Šta se traži. Podrazumijevano 'casovi'."},
"sta": {"type": "string", "enum": ["casovi", "pocetak", "kraj", "smjena", "obaveze", "aktivnosti"],
"description": "Šta se traži: 'casovi' (školski časovi), 'pocetak'/'kraj' škole, 'smjena', 'obaveze' (škola + vannastavne aktivnosti zajedno), 'aktivnosti' (samo vannastavne). Podrazumijevano 'casovi'."},
},
"required": ["dijete"],
},
@@ -78,7 +79,16 @@ def _parse_kada(s: str) -> tuple[dt.date, str]:
return d, _date_fraza(d)
except ValueError:
pass
wd = {"ponedjeljak": 0, "utorak": 1, "srijeda": 2, "cetvrtak": 3, "petak": 4, "subota": 5, "nedjelja": 6, "nedelja": 6}
# nominativ + akuzativ + instrumental ("petkom", "subotom"...) -> weekday
wd = {
"ponedjeljak": 0, "ponedjeljkom": 0,
"utorak": 1, "utorkom": 1,
"srijeda": 2, "srijedu": 2, "srijedom": 2,
"cetvrtak": 3, "cetvrtkom": 3,
"petak": 4, "petkom": 4,
"subota": 5, "subotu": 5, "subotom": 5,
"nedjelja": 6, "nedjelju": 6, "nedjeljom": 6, "nedelja": 6, "nedeljom": 6,
}
# naziv dana -> sljedeće NAILAZEĆE pojavljivanje (danas ako je taj dan; inače
# ova sedmica ako još nije prošao, a sljedeća ako jeste — bitno zbog smjena).
# "sljedeći/naredni/idući <dan>" uvijek gura u narednu sedmicu.
@@ -96,6 +106,32 @@ def _parse_kada(s: str) -> tuple[dt.date, str]:
return today, "danas"
def _obaveze(name: str, date: dt.date, fraza: str, samo_aktivnosti: bool) -> str:
stavke = [] # (vrijeme_za_sortiranje, tekst)
napomena = ""
if not samo_aktivnosti:
res = store.lessons_for(name, date)
st = res["status"]
if st == "ok":
casovi = res["casovi"]
stavke.append((casovi[0]["pocetak"],
f"škola od {casovi[0]['pocetak']} do {casovi[-1]['kraj']} ({res['smjena']}. smjena)"))
elif st == "nema_sidra":
napomena = " (za školu nije postavljena smjena u adminu, pa je ne mogu izračunati)"
for a in store.activities_for(name, date):
raspon = f" od {a['pocetak']} do {a['kraj']}" if a.get("pocetak") else ""
stavke.append((a.get("pocetak", "99:99"), f"{a['naziv']}{raspon}"))
stavke.sort(key=lambda x: x[0] or "99:99")
sta_rijec = "vannastavnih aktivnosti" if samo_aktivnosti else "obaveza"
if not stavke:
return f'Reci djetetu: "{name} {fraza} nema {sta_rijec}.{napomena}"'
spisak = "; ".join(t for _, t in stavke)
return f'Reci djetetu prirodno na bosanskom: "{name} {fraza} ima: {spisak}.{napomena}"'
async def raspored_upit(ctx: AppContext, args: dict) -> str:
ime_izgovoreno = (args.get("dijete") or "").strip()
name = store.match_child(ime_izgovoreno)
@@ -107,6 +143,11 @@ async def raspored_upit(ctx: AppContext, args: dict) -> str:
date, fraza = _parse_kada(args.get("kada", ""))
sta = (args.get("sta") or "casovi").strip().lower()
# OBAVEZE / AKTIVNOSTI: rade i vikendom i kad nema škole
if sta in ("obaveze", "aktivnosti"):
return _obaveze(name, date, fraza, samo_aktivnosti=(sta == "aktivnosti"))
res = store.lessons_for(name, date)
st = res["status"]

View File

@@ -17,7 +17,8 @@ from pathlib import Path
RASPORED_PATH = Path(os.environ.get("IDRIZ_RASPORED_PATH", str(Path.home() / ".local/share/idriz/raspored.json")))
DANI = ["Ponedjeljak", "Utorak", "Srijeda", "Četvrtak", "Petak"] # radni dani (redoslijed)
DANI = ["Ponedjeljak", "Utorak", "Srijeda", "Četvrtak", "Petak"] # školski (radni) dani
SVI_DANI = ["Ponedjeljak", "Utorak", "Srijeda", "Četvrtak", "Petak", "Subota", "Nedjelja"] # za aktivnosti
DAN_PO_WEEKDAY = {0: "Ponedjeljak", 1: "Utorak", 2: "Srijeda", 3: "Četvrtak", 4: "Petak"}
DAN_PUNI = ["ponedjeljak", "utorak", "srijeda", "četvrtak", "petak", "subota", "nedjelja"]
@@ -30,12 +31,18 @@ def norm(s: str) -> str:
_DAN_NORM = {norm(d): d for d in DANI}
_DAN_ANY_NORM = {norm(d): d for d in SVI_DANI}
def canonical_dan(s: str) -> str | None:
return _DAN_NORM.get(norm(s))
def canonical_dan_any(s: str) -> str | None:
"""Kao canonical_dan ali dopušta i subotu/nedjelju (za aktivnosti)."""
return _DAN_ANY_NORM.get(norm(s))
# ---------- učitavanje/snimanje ----------
def _load() -> dict:
@@ -81,7 +88,8 @@ def add_child(name: str, razred: str = "") -> bool:
data = _load()
if name in data["djeca"]:
return False
data["djeca"][name] = {"razred": razred.strip(), "sidro": None, "raspored": _empty_raspored()}
data["djeca"][name] = {"razred": razred.strip(), "sidro": None,
"raspored": _empty_raspored(), "aktivnosti": []}
_save(data)
return True
@@ -193,6 +201,47 @@ def child_rows(name: str) -> list[dict]:
return out
# ---------- vannastavne aktivnosti (fiksno vrijeme, sedmično, van smjena) ----------
def activity_rows(name: str) -> list[dict]:
ch = get_child(name)
if not ch:
return []
akt = list(ch.get("aktivnosti", []))
akt.sort(key=lambda a: (SVI_DANI.index(a["dan"]) if a.get("dan") in SVI_DANI else 9, a.get("pocetak", "")))
return akt
def set_activities(name: str, rows: list[dict]) -> int:
"""Zamijeni listu aktivnosti djeteta. Svaka: {dan, pocetak, kraj, naziv}. Dan može biti i vikend."""
data = _load()
ch = data["djeca"].get(name)
if not ch:
return 0
out = []
for r in rows:
dan = canonical_dan_any(r.get("dan", ""))
naziv = (r.get("naziv") or "").strip()
if not dan or not naziv:
continue
out.append({"dan": dan, "pocetak": _clean_time(r.get("pocetak", "")),
"kraj": _clean_time(r.get("kraj", "")), "naziv": naziv})
out.sort(key=lambda a: (SVI_DANI.index(a["dan"]), a["pocetak"]))
ch["aktivnosti"] = out
_save(data)
return len(out)
def activities_for(name: str, date: dt.date) -> list[dict]:
ch = get_child(name)
if not ch:
return []
dan = SVI_DANI[date.weekday()]
akt = [a for a in ch.get("aktivnosti", []) if a.get("dan") == dan]
akt.sort(key=lambda a: a.get("pocetak", ""))
return akt
# ---------- CSV import ----------
CSV_HEADER = ["dijete", "razred", "smjena", "dan", "redni_broj_casa", "pocetak", "kraj", "predmet"]
@@ -234,7 +283,7 @@ def import_csv(text: str) -> dict:
for ime, entry in po_djetetu.items():
ch = data["djeca"].get(ime)
if not ch:
ch = {"razred": "", "sidro": None, "raspored": _empty_raspored()}
ch = {"razred": "", "sidro": None, "raspored": _empty_raspored(), "aktivnosti": []}
data["djeca"][ime] = ch
if entry["razred"]:
ch["razred"] = entry["razred"]

View File

@@ -75,4 +75,34 @@
</form>
<p class="sub" style="margin-top:10px;">Prazni redovi na dnu su za dodavanje novih časova. Za brisanje označi "Obriši" pa Sačuvaj.</p>
</div>
<div class="panel">
<h2>Vannastavne aktivnosti (treninzi, mekteb...)</h2>
<p class="sub" style="margin:-6px 0 12px;">Fiksno vrijeme, ponavljaju se sedmično, nisu vezane za smjenu — mogu biti i vikendom.</p>
<form method="post">
<input type="hidden" name="action" value="aktivnosti">
<input type="hidden" name="anrows" value="{{ anrows }}">
<div class="scroll">
<table>
<thead><tr><th class="c-dan">Dan</th><th class="c-vr">Početak</th><th class="c-vr">Kraj</th><th class="predmet">Naziv</th><th class="c-del">Obriši</th></tr></thead>
<tbody>
{% for a in agrid %}
{% set i = loop.index0 %}
<tr>
<td class="c-dan"><select name="a{{ i }}_dan">
<option value="" {{ 'selected' if not a.dan else '' }}>—</option>
{% for d in svi_dani %}<option value="{{ d }}" {{ 'selected' if a.dan == d else '' }}>{{ d }}</option>{% endfor %}
</select></td>
<td class="c-vr"><input type="time" name="a{{ i }}_pocetak" value="{{ a.pocetak if a.pocetak is defined else '' }}"></td>
<td class="c-vr"><input type="time" name="a{{ i }}_kraj" value="{{ a.kraj if a.kraj is defined else '' }}"></td>
<td class="predmet"><input type="text" name="a{{ i }}_naziv" value="{{ a.naziv if a.naziv is defined else '' }}" placeholder="npr. Trening, Mekteb"></td>
<td class="c-del"><input type="checkbox" name="a{{ i }}_del"></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div style="margin-top:14px;"><button type="submit">Sačuvaj aktivnosti</button></div>
</form>
</div>
{% endblock %}