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:
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user