From a2d7adc8c77ec66d2ffab6a67a15049f1b4abecd Mon Sep 17 00:00:00 2001 From: idriz Date: Sun, 13 Sep 2026 08:20:25 +0200 Subject: [PATCH] =?UTF-8?q?Nova=20aplikacija=20"Raspored=20=C4=8Dasova"=20?= =?UTF-8?q?(raspored)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Više djece, smjene se računaju iz "sidra" (jedan datum + smjena tad; mijenja se sedmično 1,2,1,2...). Odgovara: koje časove ima, kad počinje/završava, koja smjena — za danas/sutra/prekosutra/juče/naziv dana/konkretan datum. - store.py: djeca, sidro->smjena za bilo koji datum, raspored po smjeni/danu, uparivanje imena (padeži), CSV import (kolone dijete..predmet), grid uređivanje. - manifest.py: alat raspored_upit(dijete, kada, sta=casovi|pocetak|kraj|smjena); bosanska gramatika (N časa/časova, akuzativ dana). Sve lokalno (inline). - admin.py + templates: pregled djece (+smjena danas), dodaj/obriši dijete, postavi razred+sidro, grid rasporeda (uredi/dodaj/obriši po času), CSV upload. - Registrovano u load_apps() i ADMIN_APPS. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LzKpYaEsuWDevuNnvSjcFW --- admin/app.py | 3 +- idriz/apps/__init__.py | 3 +- idriz/apps/raspored/__init__.py | 0 idriz/apps/raspored/admin.py | 98 ++++++ idriz/apps/raspored/manifest.py | 135 +++++++++ idriz/apps/raspored/store.py | 282 ++++++++++++++++++ idriz/apps/raspored/templates/raspored.html | 79 +++++ .../raspored/templates/raspored_dijete.html | 78 +++++ 8 files changed, 676 insertions(+), 2 deletions(-) create mode 100644 idriz/apps/raspored/__init__.py create mode 100644 idriz/apps/raspored/admin.py create mode 100644 idriz/apps/raspored/manifest.py create mode 100644 idriz/apps/raspored/store.py create mode 100644 idriz/apps/raspored/templates/raspored.html create mode 100644 idriz/apps/raspored/templates/raspored_dijete.html diff --git a/admin/app.py b/admin/app.py index b38a927..7d946f8 100644 --- a/admin/app.py +++ b/admin/app.py @@ -20,11 +20,12 @@ from flask import ( # Admin stranice mini-app-ova (lagani moduli iz glavnog paketa, bez openai zavisnosti). sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from idriz.apps.obroci import admin as _obroci_admin # noqa: E402 +from idriz.apps.raspored import admin as _raspored_admin # noqa: E402 from idriz.apps.superheroji import admin as _superheroji_admin # noqa: E402 from idriz.apps.vaktija import admin as _vaktija_admin # noqa: E402 # Svaki app-admin modul ima BP (Blueprint) i NAV (stavka lijevog menija). -ADMIN_APPS = [_superheroji_admin, _obroci_admin, _vaktija_admin] +ADMIN_APPS = [_superheroji_admin, _obroci_admin, _vaktija_admin, _raspored_admin] # --- Konfiguracija ----------------------------------------------------------- diff --git a/idriz/apps/__init__.py b/idriz/apps/__init__.py index 81de349..e556100 100644 --- a/idriz/apps/__init__.py +++ b/idriz/apps/__init__.py @@ -10,7 +10,8 @@ from .base import AppContext, AppSpec, AppTool # noqa: F401 def load_apps() -> list[AppSpec]: from .bojanka import manifest as bojanka from .obroci import manifest as obroci + from .raspored import manifest as raspored from .sazetak import manifest as sazetak from .superheroji import manifest as superheroji from .vaktija import manifest as vaktija - return [sazetak.SPEC, bojanka.SPEC, superheroji.SPEC, obroci.SPEC, vaktija.SPEC] + return [sazetak.SPEC, bojanka.SPEC, superheroji.SPEC, obroci.SPEC, vaktija.SPEC, raspored.SPEC] diff --git a/idriz/apps/raspored/__init__.py b/idriz/apps/raspored/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/idriz/apps/raspored/admin.py b/idriz/apps/raspored/admin.py new file mode 100644 index 0000000..111cc5a --- /dev/null +++ b/idriz/apps/raspored/admin.py @@ -0,0 +1,98 @@ +"""Admin za 'Raspored časova': djeca, smjena (sidro), grid rasporeda, CSV import. + +Flask Blueprint, bez openai zavisnosti. +""" +import datetime as dt + +from flask import Blueprint, flash, redirect, render_template, request, url_for + +from . import store + +BP = Blueprint("raspored", __name__, template_folder="templates", url_prefix="/raspored") +NAV = {"endpoint": "raspored.index", "page": "raspored", "title": "Raspored časova", "icon": "📚"} + +BLANK_ROWS = 8 # praznih redova za dodavanje novih časova + + +@BP.route("/", methods=["GET", "POST"]) +def index(): + if request.method == "POST": + action = request.form.get("action") + if action == "add": + if store.add_child(request.form.get("ime", ""), request.form.get("razred", "")): + flash(f"Dodano dijete: {request.form.get('ime').strip()}.") + else: + flash("Dijete već postoji ili ime nije uneseno.") + elif action == "delete": + store.delete_child(request.form.get("ime", "")) + flash(f"Obrisano dijete: {request.form.get('ime')}.") + elif action == "csv": + text = "" + f = request.files.get("csvfile") + if f and f.filename: + text = f.read().decode("utf-8-sig", errors="replace") + elif request.form.get("csvtext", "").strip(): + text = request.form["csvtext"] + if not text.strip(): + flash("Nije priložen CSV (fajl ili tekst).") + else: + res = store.import_csv(text) + if "error" in res: + flash(f"CSV greška: {res['error']}") + else: + flash(f"Uvezeno: {res['casova']} časova za {', '.join(res['djeca'])}. " + f"(Ne zaboravi postaviti smjenu/datum za svako dijete.)") + return redirect(url_for("raspored.index")) + + today = dt.date.today() + djeca = [] + for name in store.list_children(): + ch = store.get_child(name) + sm = store.shift_for(name, today) + djeca.append({ + "ime": name, + "razred": ch.get("razred", ""), + "sidro": ch.get("sidro"), + "smjena_danas": sm, + "broj_casova": sum(len(v) for s in ch["raspored"].values() for v in s.values()), + }) + return render_template("raspored.html", djeca=djeca, danas=today.isoformat(), active_page="raspored") + + +@BP.route("/dijete/", methods=["GET", "POST"]) +def dijete(ime): + ch = store.get_child(ime) + if not ch: + flash("Nepoznato dijete.") + return redirect(url_for("raspored.index")) + + if request.method == "POST": + action = request.form.get("action") + if action == "osnovno": + store.set_osnovno(ime, request.form.get("razred", ""), + request.form.get("sidro_datum", ""), request.form.get("sidro_smjena", "")) + flash("Osnovni podaci sačuvani.") + elif action == "raspored": + n = int(request.form.get("nrows", "0")) + rows = [] + for i in range(n): + if request.form.get(f"r{i}_del"): + continue + rows.append({ + "smjena": request.form.get(f"r{i}_smjena", ""), + "dan": request.form.get(f"r{i}_dan", ""), + "br": request.form.get(f"r{i}_br", ""), + "pocetak": request.form.get(f"r{i}_pocetak", ""), + "kraj": request.form.get(f"r{i}_kraj", ""), + "predmet": request.form.get(f"r{i}_predmet", ""), + }) + saved = store.set_child_schedule(ime, rows) + flash(f"Raspored sačuvan: {saved} časova.") + return redirect(url_for("raspored.dijete", ime=ime)) + + grid = store.child_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", + ) diff --git a/idriz/apps/raspored/manifest.py b/idriz/apps/raspored/manifest.py new file mode 100644 index 0000000..9851311 --- /dev/null +++ b/idriz/apps/raspored/manifest.py @@ -0,0 +1,135 @@ +"""Mini-app 'Raspored časova': odgovara o rasporedu djece za dati dan. + +Podržava više djece; smjena se računa iz sidra (v. store.py). Sva logika je lokalna +(bez API-ja), pa je alat inline. Tačan datum se oslanja na sistemski sat (NTP). +""" +import datetime as dt +import logging +import re + +from ..base import AppContext, AppSpec, AppTool +from . import store + +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". +- 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.""" + +SCHEMA = { + "type": "function", + "name": "raspored_upit", + "description": "Raspored časova za jedno dijete i jedan dan (koji časovi, početak, kraj, smjena).", + "parameters": { + "type": "object", + "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'."}, + }, + "required": ["dijete"], + }, +} + +_REL_FRAZA = {0: "danas", 1: "sutra", 2: "prekosutra", -1: "jučer", -2: "prekjučer"} +# akuzativ za "u " (srijeda/subota/nedjelja mijenjaju oblik) +_DAN_AKUZATIV = ["ponedjeljak", "utorak", "srijedu", "četvrtak", "petak", "subotu", "nedjelju"] + + +def _cas_rijec(n: int) -> str: + if 11 <= n % 100 <= 14: + return "časova" + d = n % 10 + return "čas" if d == 1 else ("časa" if 2 <= d <= 4 else "časova") + + +def _date_fraza(d: dt.date) -> str: + return f"u {_DAN_AKUZATIV[d.weekday()]}, {d.day}.{d.month}." + + +def _parse_kada(s: str) -> tuple[dt.date, str]: + today = dt.date.today() + s = (s or "").strip() + if not s: + return today, "danas" + sn = store.norm(s) + rel = {"danas": 0, "sutra": 1, "prekosutra": 2, "juce": -1, "jucer": -1, "prekjuce": -2, "prekjucer": -2} + if sn in rel: + d = today + dt.timedelta(days=rel[sn]) + return d, _REL_FRAZA[rel[sn]] + try: + d = dt.date.fromisoformat(s) + return d, _date_fraza(d) + except ValueError: + pass + m = re.match(r"^(\d{1,2})\.\s*(\d{1,2})\.?\s*(\d{2,4})?", s) + if m: + dd, mm = int(m.group(1)), int(m.group(2)) + yy = m.group(3) + year = int(yy) if yy else today.year + if year < 100: + year += 2000 + try: + d = dt.date(year, mm, dd) + 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} + if sn in wd: + monday = today - dt.timedelta(days=today.weekday()) + d = monday + dt.timedelta(days=wd[sn]) + return d, _date_fraza(d) + return today, "danas" + + +async def raspored_upit(ctx: AppContext, args: dict) -> str: + ime_izgovoreno = (args.get("dijete") or "").strip() + name = store.match_child(ime_izgovoreno) + if not name: + djeca = store.list_children() + lista = ", ".join(djeca) if djeca else "nijedno dijete nije dodano" + return (f"Ne znam za dijete \"{ime_izgovoreno}\". Poznata djeca: {lista}. " + f"Reci to i pitaj za jedno od njih (ili dodaj dijete u admin panelu).") + + date, fraza = _parse_kada(args.get("kada", "")) + sta = (args.get("sta") or "casovi").strip().lower() + res = store.lessons_for(name, date) + st = res["status"] + + if st == "vikend": + return f'Reci djetetu: "{name} nema nastave {fraza} — vikend je."' + if st == "nema_sidra": + return (f"Za {name} nije postavljena smjena u admin panelu (treba jedan datum i koja je smjena tad bila), " + f"pa ne mogu izračunati raspored. Reci to.") + smjena = res["smjena"] + if st == "nema_casova": + return f'Reci djetetu: "{name} {fraza} nema časova ({smjena}. smjena)."' + + casovi = res["casovi"] + prvi, zadnji = casovi[0], casovi[-1] + + if sta == "smjena": + return f'Reci djetetu: "{name} je {fraza} u {smjena}. smjeni."' + if sta == "pocetak": + return f'Reci djetetu: "{name} {fraza} treba biti u školi u {prvi["pocetak"]} ({smjena}. smjena)."' + if sta == "kraj": + return f'Reci djetetu: "{name} {fraza} završava školu u {zadnji["kraj"]} ({smjena}. smjena)."' + + # casovi — nabroji + stavke = "; ".join(f'{c["br"]}. {c["predmet"]} u {c["pocetak"]}' for c in casovi) + recenica = (f"{name} {fraza} ima {len(casovi)} {_cas_rijec(len(casovi))} u {smjena}. smjeni " + f"(od {prvi['pocetak']} do {zadnji['kraj']}): {stavke}.") + return f'Reci djetetu prirodno na bosanskom, možeš i sažeti ako je dugačko: "{recenica}"' + + +SPEC = AppSpec( + name="raspored", + title="Raspored časova", + instructions=INSTRUCTIONS, + greeting="reći raspored časova za djecu (kad počinju/završavaju, koji časovi)", + tools=[AppTool(SCHEMA, raspored_upit, "inline")], +) diff --git a/idriz/apps/raspored/store.py b/idriz/apps/raspored/store.py new file mode 100644 index 0000000..9cd369e --- /dev/null +++ b/idriz/apps/raspored/store.py @@ -0,0 +1,282 @@ +"""Skladište za app 'Raspored časova': djeca, smjene i raspored. + +Smjena se mijenja sedmično (1,2,1,2...). U bazi se za dijete čuva "sidro": +jedan datum + koja je smjena tad bila. Iz toga se računa smjena za bilo koji datum. +Bez openai/requests (uvozi ga i admin). +""" +import csv +import datetime as dt +import io +import json +import os +import re +import tempfile +import threading +import unicodedata +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) +DAN_PO_WEEKDAY = {0: "Ponedjeljak", 1: "Utorak", 2: "Srijeda", 3: "Četvrtak", 4: "Petak"} +DAN_PUNI = ["ponedjeljak", "utorak", "srijeda", "četvrtak", "petak", "subota", "nedjelja"] + +_lock = threading.Lock() + + +def norm(s: str) -> str: + s = unicodedata.normalize("NFKD", s or "").encode("ascii", "ignore").decode() + return re.sub(r"[^a-z0-9]+", "", s.lower()) + + +_DAN_NORM = {norm(d): d for d in DANI} + + +def canonical_dan(s: str) -> str | None: + return _DAN_NORM.get(norm(s)) + + +# ---------- učitavanje/snimanje ---------- + +def _load() -> dict: + try: + data = json.loads(RASPORED_PATH.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + data = {} + data.setdefault("djeca", {}) + return data + + +def _save(data: dict) -> None: + with _lock: + RASPORED_PATH.parent.mkdir(parents=True, exist_ok=True) + fd, tmp = tempfile.mkstemp(dir=str(RASPORED_PATH.parent), suffix=".tmp") + try: + with os.fdopen(fd, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + os.replace(tmp, RASPORED_PATH) + finally: + if os.path.exists(tmp): + os.unlink(tmp) + + +def _empty_raspored() -> dict: + return {"1": {d: [] for d in DANI}, "2": {d: [] for d in DANI}} + + +# ---------- djeca ---------- + +def list_children() -> list[str]: + return sorted(_load()["djeca"].keys(), key=lambda s: s.lower()) + + +def get_child(name: str) -> dict | None: + return _load()["djeca"].get(name) + + +def add_child(name: str, razred: str = "") -> bool: + name = (name or "").strip() + if not name: + return False + data = _load() + if name in data["djeca"]: + return False + data["djeca"][name] = {"razred": razred.strip(), "sidro": None, "raspored": _empty_raspored()} + _save(data) + return True + + +def delete_child(name: str) -> None: + data = _load() + if name in data["djeca"]: + del data["djeca"][name] + _save(data) + + +def set_osnovno(name: str, razred: str, sidro_datum: str, sidro_smjena) -> None: + """Postavi razred i sidro (datum + smjena tad). Prazan datum/smjena -> obriši sidro.""" + data = _load() + ch = data["djeca"].get(name) + if not ch: + return + ch["razred"] = (razred or "").strip() + try: + d = dt.date.fromisoformat((sidro_datum or "").strip()) + sm = int(sidro_smjena) + ch["sidro"] = {"datum": d.isoformat(), "smjena": sm} if sm in (1, 2) else None + except (ValueError, TypeError): + ch["sidro"] = None + _save(data) + + +def match_child(query: str) -> str | None: + """Toleranto uparivanje imena (padeži/vokativ): najduži zajednički prefiks.""" + q = norm(query) + if not q: + return None + best, best_len = None, 0 + for name in _load()["djeca"]: + n = norm(name) + cp = 0 + for a, b in zip(q, n): + if a == b: + cp += 1 + else: + break + if n == q: + return name + if cp > best_len: + best_len, best = cp, name + # zahtijevaj bar 4 zajednička znaka (ili cijelo kraće ime) + if best and best_len >= min(4, len(best)): + return best + return None + + +# ---------- raspored (uređivanje) ---------- + +def _clean_time(t: str) -> str: + t = (t or "").strip() + m = re.match(r"^(\d{1,2}):(\d{2})$", t) + if m: + return f"{int(m.group(1)):02d}:{m.group(2)}" + return t + + +def set_child_schedule(name: str, rows: list[dict]) -> int: + """Zamijeni cijeli raspored djeteta listom časova {smjena,dan,br,pocetak,kraj,predmet}.""" + data = _load() + ch = data["djeca"].get(name) + if not ch: + return 0 + rasp = _empty_raspored() + n = 0 + for r in rows: + try: + sm = int(r.get("smjena")) + except (ValueError, TypeError): + continue + dan = canonical_dan(r.get("dan", "")) + predmet = (r.get("predmet") or "").strip() + if sm not in (1, 2) or not dan or not predmet: + continue + try: + br = int(r.get("br")) + except (ValueError, TypeError): + br = len(rasp[str(sm)][dan]) + 1 + rasp[str(sm)][dan].append({ + "br": br, + "pocetak": _clean_time(r.get("pocetak", "")), + "kraj": _clean_time(r.get("kraj", "")), + "predmet": predmet, + }) + n += 1 + for sm in ("1", "2"): + for dan in DANI: + rasp[sm][dan].sort(key=lambda c: c["br"]) + ch["raspored"] = rasp + _save(data) + return n + + +def child_rows(name: str) -> list[dict]: + """Svi časovi djeteta kao ravna lista (za admin grid), sortirano smjena/dan/br.""" + ch = get_child(name) + if not ch: + return [] + out = [] + for sm in ("1", "2"): + for dan in DANI: + for c in ch["raspored"].get(sm, {}).get(dan, []): + out.append({"smjena": int(sm), "dan": dan, "br": c["br"], + "pocetak": c["pocetak"], "kraj": c["kraj"], "predmet": c["predmet"]}) + return out + + +# ---------- CSV import ---------- + +CSV_HEADER = ["dijete", "razred", "smjena", "dan", "redni_broj_casa", "pocetak", "kraj", "predmet"] + + +def import_csv(text: str) -> dict: + """Uvezi CSV (kolone CSV_HEADER). Zamijeni raspored za svako dijete iz fajla. + Zadrži postojeće sidro. Vrati sažetak ili {'error':...}.""" + try: + reader = csv.DictReader(io.StringIO(text)) + if not reader.fieldnames: + return {"error": "Prazan CSV."} + headers = [h.strip() for h in reader.fieldnames] + missing = [h for h in CSV_HEADER if h not in headers] + if missing: + return {"error": f"Nedostaju kolone: {', '.join(missing)}."} + po_djetetu: dict[str, dict] = {} + for raw in reader: + row = {k.strip(): (v or "").strip() for k, v in raw.items() if k} + ime = row.get("dijete", "").strip() + if not ime: + continue + entry = po_djetetu.setdefault(ime, {"razred": row.get("razred", ""), "rows": []}) + if row.get("razred"): + entry["razred"] = row["razred"].strip() + entry["rows"].append({ + "smjena": row.get("smjena"), "dan": row.get("dan"), + "br": row.get("redni_broj_casa"), "pocetak": row.get("pocetak"), + "kraj": row.get("kraj"), "predmet": row.get("predmet"), + }) + except Exception as e: # noqa: BLE001 + return {"error": f"Greška pri čitanju CSV-a: {type(e).__name__}"} + + if not po_djetetu: + return {"error": "Nema redova u CSV-u."} + + data = _load() + total = 0 + for ime, entry in po_djetetu.items(): + ch = data["djeca"].get(ime) + if not ch: + ch = {"razred": "", "sidro": None, "raspored": _empty_raspored()} + data["djeca"][ime] = ch + if entry["razred"]: + ch["razred"] = entry["razred"] + _save(data) # snimi novododanu djecu prije set_child_schedule (koji ponovo učitava) + for ime, entry in po_djetetu.items(): + total += set_child_schedule(ime, entry["rows"]) + return {"djeca": sorted(po_djetetu.keys()), "casova": total} + + +# ---------- smjena i raspored za datum ---------- + +def shift_for(name: str, date: dt.date) -> int | None: + ch = get_child(name) + if not ch or not ch.get("sidro"): + return None + try: + anchor = dt.date.fromisoformat(ch["sidro"]["datum"]) + s = int(ch["sidro"]["smjena"]) + except (ValueError, TypeError, KeyError): + return None + mon_a = anchor - dt.timedelta(days=anchor.weekday()) + mon_d = date - dt.timedelta(days=date.weekday()) + weeks = (mon_d - mon_a).days // 7 + if weeks % 2 == 0: + return s + return 2 if s == 1 else 1 + + +def lessons_for(name: str, date: dt.date) -> dict: + """Vrati {'status', 'smjena', 'dan', 'casovi'} za dijete i datum. + status: 'ok' | 'nepoznato_dijete' | 'vikend' | 'nema_sidra' | 'nema_casova'.""" + ch = get_child(name) + if not ch: + return {"status": "nepoznato_dijete"} + if date.weekday() >= 5: + return {"status": "vikend"} + smjena = shift_for(name, date) + if smjena is None: + return {"status": "nema_sidra"} + dan = DAN_PO_WEEKDAY[date.weekday()] + casovi = list(ch["raspored"].get(str(smjena), {}).get(dan, [])) + casovi.sort(key=lambda c: c["br"]) + if not casovi: + return {"status": "nema_casova", "smjena": smjena, "dan": dan} + return {"status": "ok", "smjena": smjena, "dan": dan, "casovi": casovi} diff --git a/idriz/apps/raspored/templates/raspored.html b/idriz/apps/raspored/templates/raspored.html new file mode 100644 index 0000000..deb17c9 --- /dev/null +++ b/idriz/apps/raspored/templates/raspored.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} +{% block title %}Raspored časova — Idriz Admin{% endblock %} +{% block extra_css %} + .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 16px 18px; margin-bottom: 20px; max-width: 720px; } + .panel h2 { margin: 0 0 4px; font-size: 15px; } + .panel p.hint { color: var(--muted); font-size: 12.5px; margin: 0 0 12px; } + table { width: 100%; border-collapse: collapse; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; max-width: 720px; margin-bottom: 20px; } + th, td { text-align: left; padding: 10px 14px; font-size: 14px; } + th { background: #fafbfc; color: var(--muted); font-weight: 600; border-bottom: 1px solid var(--line); } + tr:not(:last-child) td { border-bottom: 1px solid var(--line); } + a.edit { color: var(--accent); text-decoration: none; font-weight: 600; } + input[type=text], textarea { width: 100%; padding: 9px 12px; font-size: 14px; border: 1px solid var(--line); border-radius: 8px; font-family: inherit; } + textarea { min-height: 120px; } + .row { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; } + .row > div { flex: 1; min-width: 140px; } + label { display: block; font-size: 12.5px; color: var(--muted); margin-bottom: 4px; } + button { padding: 9px 18px; font-size: 14px; font-weight: 600; color: #fff; background: var(--accent); border: none; border-radius: 8px; cursor: pointer; } + button:hover { filter: brightness(1.05); } + button.small { padding: 5px 10px; font-size: 12.5px; } + button.danger { background: var(--bad); } + .badge { display:inline-block; padding:2px 9px; border-radius:20px; font-size:12.5px; font-weight:600; } + .b1 { background:#e6f0ff; color:#2f6df6; } .b2 { background:#fdf1dd; color:#c9820a; } + .muted { color: var(--muted); } + .empty { color: var(--muted); font-style: italic; } +{% endblock %} +{% block content %} +

Raspored časova

+

Djeca, smjene i raspored. Smjena se mijenja sedmično — postavi je jednim datumom po djetetu.

+ + {% if djeca %} + + + + {% for d in djeca %} + + + + + + + + + {% endfor %} + +
DijeteRazredSmjena danasČasova
{{ d.ime }}{{ d.razred or "—" }} + {% if d.smjena_danas %}{{ d.smjena_danas }}. smjena + {% else %}nema smjene{% endif %} + {{ d.broj_casova }}Uredi › +
+ + +
+
+ {% else %} +

Još nema dodane djece.

+ {% endif %} + +
+

Dodaj dijete

+
+ +
+
+
+
+
+ +
+

Uvoz CSV-a

+

Kolone: dijete, razred, smjena, dan, redni_broj_casa, pocetak, kraj, predmet. Zamjenjuje raspored za djecu iz fajla; smjenu/datum postavi ručno po uvozu.

+
+ + +

ili zalijepi CSV ovdje:

+ +
+
+
+{% endblock %} diff --git a/idriz/apps/raspored/templates/raspored_dijete.html b/idriz/apps/raspored/templates/raspored_dijete.html new file mode 100644 index 0000000..125a1a2 --- /dev/null +++ b/idriz/apps/raspored/templates/raspored_dijete.html @@ -0,0 +1,78 @@ +{% extends "base.html" %} +{% block title %}{{ ime }} — Raspored — Idriz Admin{% endblock %} +{% block extra_css %} + .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 16px 18px; margin-bottom: 20px; max-width: 900px; } + .panel h2 { margin: 0 0 12px; font-size: 15px; } + .row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; } + .row > div { flex: 0 0 auto; } + label { display: block; font-size: 12.5px; color: var(--muted); margin-bottom: 4px; } + input, select { padding: 8px 10px; font-size: 14px; border: 1px solid var(--line); border-radius: 7px; font-family: inherit; background:#fff; color:var(--ink); } + input[type=text], input[type=date] { min-width: 150px; } + button { padding: 9px 18px; font-size: 14px; font-weight: 600; color: #fff; background: var(--accent); border: none; border-radius: 8px; cursor: pointer; } + button:hover { filter: brightness(1.05); } + a.back { color: var(--muted); text-decoration: none; font-size: 13px; } + table { width: 100%; border-collapse: collapse; font-size: 13.5px; } + th, td { padding: 5px 7px; text-align: left; } + th { color: var(--muted); font-weight: 600; font-size: 12px; border-bottom: 1px solid var(--line); } + td input[type=text], td input[type=time] { min-width: 0; width: 100%; } + .c-sm { width: 70px; } .c-dan { width: 130px; } .c-br { width: 55px; } .c-vr { width: 105px; } .c-del { width: 40px; text-align:center; } + .predmet input { width: 100%; } + .scroll { overflow-x: auto; } +{% endblock %} +{% block content %} + ‹ Nazad na djecu +

{{ ime }}

+

Uređivanje osnovnih podataka i rasporeda.

+ +
+

Osnovno i smjena

+
+ +
+
+
+ +
+
+
+

Iz tog jednog datuma se računa smjena za svaki dan (mijenja se sedmično).

+
+ +
+

Raspored (uredi, dodaj, obriši)

+
+ + +
+ + + + {% for r in grid %} + {% set i = loop.index0 %} + + + + + + + + + + {% endfor %} + +
SmjenaDanBr.PočetakKrajPredmetObriši
+
+
+
+

Prazni redovi na dnu su za dodavanje novih časova. Za brisanje označi "Obriši" pa Sačuvaj.

+
+{% endblock %}