maker-checker

 """

╔══════════════════════════════════════════════════════════════════╗
║ MAKER-CHECKER CHECKLIST – v6 (single file) ║
║ ║
║ Sections: ║
║ 1. Imports & app setup ║
║ 2. Database (schema, seed, helpers, auto-trigger) ║
║ 3. Auth (login, logout, decorators) ║
║ 4. Templates (all HTML embedded as constants) ║
║ 5. Module: Super Admin (/sa/*) ║
║ 6. Module: Admin (/admin/*) ║
║ 7. Module: Maker-Checker(/mc/*) ← SA+Admin can also act ║
║ 8. Module: Shared (dashboard, audit, health) ║
║ 9. Entry point ║
╚══════════════════════════════════════════════════════════════════╝

Changes in v6
─────────────
• SA Tasks tab removed — merged into Admin Templates tab.
SA now sees all teams there; Admin sees only their teams.
• mc_tasks query fixed:
- SA → all tasks across all teams
- Admin → all tasks for their teams
- Maker/Checker → only tasks where they are assigned
• Task detail access: SA/Admin can open ANY task; maker/checker only theirs.
• Workflow actions in task detail now also work for SA/Admin when they are
the maker or checker on that task, or when they are admin of that team.
• Consistent "You" label in task list now works correctly for all roles.
"""

# ═══════════════════════════════════════════════════════════════════
# 1. IMPORTS & APP SETUP
# ═══════════════════════════════════════════════════════════════════
import os, sqlite3, hashlib
from datetime import datetime, timedelta
from functools import wraps
from flask import (Flask, request, session, redirect,
url_for, flash, render_template_string, jsonify, g)
from jinja2 import DictLoader

app = Flask(__name__)
app.secret_key = os.environ.get("SECRET_KEY", "change-in-production")
DATABASE = os.environ.get("DATABASE_URL", "maker_checker.db")


# ═══════════════════════════════════════════════════════════════════
# 2. DATABASE
# ═══════════════════════════════════════════════════════════════════

def get_db():
if "db" not in g:
g.db = sqlite3.connect(DATABASE)
g.db.row_factory = sqlite3.Row
g.db.execute("PRAGMA foreign_keys = ON")
return g.db

@app.teardown_appcontext
def close_db(e=None):
db = g.pop("db", None)
if db: db.close()

def hash_pw(p): return hashlib.sha256(p.encode()).hexdigest()
def now(): return datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
def today(): return datetime.utcnow().strftime("%Y-%m-%d")

# ── Schema ────────────────────────────────────────────────────────
SCHEMA = """
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
full_name TEXT,
system_role TEXT NOT NULL DEFAULT 'user',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS user_roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
role TEXT NOT NULL,
created_at TEXT NOT NULL,
UNIQUE(user_id, role)
);
CREATE TABLE IF NOT EXISTS teams (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
created_by INTEGER REFERENCES users(id),
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS team_members (
id INTEGER PRIMARY KEY AUTOINCREMENT,
team_id INTEGER NOT NULL REFERENCES teams(id),
user_id INTEGER NOT NULL REFERENCES users(id),
role TEXT NOT NULL,
added_at TEXT NOT NULL,
UNIQUE(team_id, user_id)
);
CREATE TABLE IF NOT EXISTS task_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
team_id INTEGER NOT NULL REFERENCES teams(id),
title TEXT NOT NULL,
description TEXT,
frequency TEXT NOT NULL DEFAULT 'once',
maker_id INTEGER REFERENCES users(id),
checker_id INTEGER REFERENCES users(id),
is_active INTEGER DEFAULT 1,
created_by INTEGER NOT NULL REFERENCES users(id),
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
template_id INTEGER REFERENCES task_templates(id),
team_id INTEGER NOT NULL REFERENCES teams(id),
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'outstanding',
maker_id INTEGER REFERENCES users(id),
checker_id INTEGER REFERENCES users(id),
due_date TEXT,
remarks TEXT,
cancel_reason TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id),
entity_type TEXT NOT NULL,
entity_id INTEGER,
action TEXT NOT NULL,
details TEXT,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS app_config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
config_key TEXT UNIQUE NOT NULL,
config_value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
"""

def init_db():
db = get_db()
db.executescript(SCHEMA)
db.commit()
_seed(db)

def _seed(db):
if not db.execute("SELECT id FROM users WHERE username='superadmin'").fetchone():
db.execute(
"INSERT INTO users (username,password_hash,full_name,system_role,created_at) VALUES (?,?,?,?,?)",
("superadmin", hash_pw("super123"), "Super Admin", "super_admin", now())
)
for k, v in [("app_name", "Maker Checker"), ("allow_self_approval", "false")]:
db.execute(
"INSERT OR IGNORE INTO app_config (config_key,config_value,updated_at) VALUES (?,?,?)",
(k, v, now())
)
db.commit()

# ── Auto-trigger ──────────────────────────────────────────────────
def trigger_due_tasks():
"""
Creates outstanding task instances for active templates
whose current period has no open task yet.
once – one task ever
daily – one per calendar day
weekly – one per Mon-Sun week (triggers Monday)
monthly – one per calendar month (triggers 1st)
"""
db = get_db()
for t in db.execute("SELECT * FROM task_templates WHERE is_active=1").fetchall():
freq = t["frequency"]
due = None
if freq == "once":
if db.execute("SELECT id FROM tasks WHERE template_id=?",
(t["id"],)).fetchone():
continue
period_start = None
elif freq == "daily":
period_start = today() + " 00:00:00"
due = (datetime.utcnow() + timedelta(days=1)).strftime("%Y-%m-%d")
elif freq == "weekly":
mon = datetime.utcnow() - timedelta(days=datetime.utcnow().weekday())
period_start = mon.strftime("%Y-%m-%d") + " 00:00:00"
due = (mon + timedelta(days=6)).strftime("%Y-%m-%d")
elif freq == "monthly":
period_start = datetime.utcnow().strftime("%Y-%m") + "-01 00:00:00"
due = (datetime.utcnow() + timedelta(days=30)).strftime("%Y-%m-%d")
else:
continue

if period_start:
if db.execute(
"SELECT id FROM tasks WHERE template_id=? AND created_at>=? AND status!='closed'",
(t["id"], period_start)
).fetchone():
continue

db.execute(
"""INSERT INTO tasks
(template_id,team_id,title,description,status,
maker_id,checker_id,due_date,created_at,updated_at)
VALUES (?,?,?,?,'outstanding',?,?,?,?,?)""",
(t["id"], t["team_id"], t["title"], t["description"],
t["maker_id"], t["checker_id"], due, now(), now())
)
db.commit()
log_action(None, "task", None, "auto_trigger",
f"Triggered: {t['title']} ({freq})")

# ── DB helpers ─────────────────────────────────────────────────────
def log_action(user_id, entity, eid, action, details=""):
get_db().execute(
"INSERT INTO audit_log (user_id,entity_type,entity_id,action,details,created_at)"
" VALUES (?,?,?,?,?,?)",
(user_id, entity, eid, action, details, now())
)
get_db().commit()

def cfg(key, default=""):
r = get_db().execute("SELECT config_value FROM app_config WHERE config_key=?", (key,)).fetchone()
return r["config_value"] if r else default

def get_user(uid):
return get_db().execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone()

def functional_roles(uid):
return [r["role"] for r in get_db().execute(
"SELECT role FROM user_roles WHERE user_id=?", (uid,)).fetchall()]

def is_super(u): return bool(u and u["system_role"] == "super_admin")
def is_any_admin(u): return is_super(u) or (u and "admin" in functional_roles(u["id"]))

def admin_teams(u):
"""Returns teams this user administers (all teams for SA)."""
if is_super(u):
return get_db().execute("SELECT * FROM teams ORDER BY name").fetchall()
return get_db().execute(
"SELECT t.* FROM teams t JOIN team_members tm ON tm.team_id=t.id "
"WHERE tm.user_id=? AND tm.role='admin' ORDER BY t.name", (u["id"],)
).fetchall()

def admin_team_ids(u):
return [t["id"] for t in admin_teams(u)]

def is_admin_of_team(u, team_id):
if is_super(u): return True
return get_db().execute(
"SELECT id FROM team_members WHERE team_id=? AND user_id=? AND role='admin'",
(team_id, u["id"])
).fetchone() is not None


# ═══════════════════════════════════════════════════════════════════
# 3. AUTH (decorators + login/logout routes)
# ═══════════════════════════════════════════════════════════════════

def current_user():
uid = session.get("user_id")
return get_user(uid) if uid else None

def login_required(f):
@wraps(f)
def w(*a, **k):
if not current_user():
return redirect(url_for("login"))
return f(*a, **k)
return w

def super_required(f):
@wraps(f)
def w(*a, **k):
if not is_super(current_user()):
flash("Super-admin access required.")
return redirect(url_for("dashboard"))
return f(*a, **k)
return w

def admin_required(f):
@wraps(f)
def w(*a, **k):
if not is_any_admin(current_user()):
flash("Admin access required.")
return redirect(url_for("dashboard"))
return f(*a, **k)
return w

@app.context_processor
def _nav_ctx():
"""Inject nav variables into every template render."""
u = current_user()
badge, can_admin, roles_str = 0, False, ""
if u:
roles = functional_roles(u["id"])
roles_str = ", ".join(roles) if roles else "user"
can_admin = is_any_admin(u)
# Badge = tasks where I have something to do
badge = get_db().execute(
"SELECT COUNT(*) c FROM tasks "
"WHERE status IN ('outstanding','submitted','cancel_requested') "
"AND (maker_id=? OR checker_id=?)", (u["id"], u["id"])
).fetchone()["c"]
return dict(me=u, can_admin=can_admin, badge=badge,
roles_str=roles_str, app_name=cfg("app_name","Maker Checker"))

@app.route("/login", methods=["GET","POST"])
def login():
init_db()
if request.method == "POST":
u = get_db().execute(
"SELECT * FROM users WHERE username=? AND password_hash=?",
(request.form["username"], hash_pw(request.form["password"]))
).fetchone()
if u:
session["user_id"] = u["id"]
trigger_due_tasks()
log_action(u["id"], "auth", u["id"], "login", "Logged in")
return redirect(url_for("dashboard"))
flash("Invalid username or password.")
return _render(T_LOGIN)

@app.route("/logout")
def logout():
u = current_user()
if u: log_action(u["id"], "auth", u["id"], "logout", "Logged out")
session.clear()
return redirect(url_for("login"))

@app.route("/")
def home():
return redirect(url_for("dashboard") if current_user() else url_for("login"))


# ═══════════════════════════════════════════════════════════════════
# 4. TEMPLATES (all HTML — edit this section to change UI)
# ═══════════════════════════════════════════════════════════════════

T_BASE = """
<!doctype html><html><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{% block title %}{% endblock %} | {{ app_name }}</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:Arial,sans-serif;background:#f0f2f5;color:#1e293b;font-size:14px}
.nav{background:#1e293b;padding:0 20px;display:flex;align-items:center;height:52px;flex-wrap:wrap}
.nav .brand{color:#fff;font-weight:700;font-size:15px;margin-right:16px;text-decoration:none}
.nav a{color:#94a3b8;text-decoration:none;padding:0 9px;line-height:52px;font-size:13px}
.nav a:hover{color:#fff;background:#0f172a}
.nav .right{margin-left:auto;display:flex;align-items:center;gap:10px}
.wrap{max-width:1200px;margin:0 auto;padding:24px 16px}
.flash{padding:10px 14px;background:#fef3c7;border-left:4px solid #f59e0b;border-radius:4px;margin-bottom:14px;font-size:13px}
.card{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:20px;margin-bottom:16px}
.card h3{font-size:15px;color:#1e293b;margin-bottom:14px;padding-bottom:8px;border-bottom:1px solid #f1f5f9}
.g2{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px}
.g4{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:14px;margin-bottom:20px}
.kpi{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:16px;text-align:center}
.kpi .n{font-size:30px;font-weight:700;color:#2563eb}.kpi .l{color:#64748b;font-size:12px;margin-top:3px}
table{width:100%;border-collapse:collapse;font-size:13px}
th{background:#f8fafc;font-weight:600;color:#374151;padding:9px 12px;text-align:left;border-bottom:2px solid #e2e8f0}
td{padding:9px 12px;border-bottom:1px solid #f1f5f9;vertical-align:middle}
tr:hover td{background:#f8fafc}
label{font-weight:600;font-size:12px;color:#374151;display:block;margin-bottom:3px;margin-top:10px}
label:first-child{margin-top:0}
input,select,textarea{width:100%;padding:8px 10px;border:1px solid #cbd5e1;border-radius:5px;font-size:13px}
input:focus,select:focus,textarea:focus{outline:none;border-color:#2563eb}
.btn{display:inline-block;padding:8px 16px;border-radius:5px;font-size:13px;font-weight:600;
cursor:pointer;border:none;text-decoration:none;text-align:center;line-height:1.4}
.btn-p{background:#2563eb;color:#fff}.btn-p:hover{background:#1d4ed8}
.btn-g{background:#059669;color:#fff}.btn-g:hover{background:#047857}
.btn-r{background:#dc2626;color:#fff}.btn-r:hover{background:#b91c1c}
.btn-w{background:#d97706;color:#fff}.btn-w:hover{background:#b45309}
.btn-s{background:#64748b;color:#fff}.btn-s:hover{background:#475569}
.btn-sm{padding:5px 10px;font-size:12px}
.st{font-size:15px;font-weight:700;color:#1e293b;margin:24px 0 10px;padding-bottom:6px;border-bottom:2px solid #e2e8f0}
.acts{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
.empty{text-align:center;padding:28px;color:#94a3b8;font-size:13px}
h2{font-size:20px;font-weight:700;margin-bottom:20px;color:#1e293b}
.bdg{background:#dc2626;color:#fff;border-radius:999px;font-size:10px;padding:1px 5px;margin-left:3px}
.p{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.3px}
.p-outstanding{background:#fef3c7;color:#92400e}.p-submitted{background:#dbeafe;color:#1e40af}
.p-approved{background:#d1fae5;color:#065f46}.p-rejected{background:#fee2e2;color:#991b1b}
.p-closed{background:#f1f5f9;color:#475569}.p-cancel_requested{background:#ffe4e6;color:#9f1239}
.p-admin{background:#dbeafe;color:#1e40af}.p-maker{background:#d1fae5;color:#065f46}
.p-checker{background:#fef3c7;color:#92400e}.p-super_admin{background:#f3e8ff;color:#6b21a8}
.p-once{background:#e0f2fe;color:#0369a1}.p-daily{background:#fce7f3;color:#9d174d}
.p-weekly{background:#ede9fe;color:#5b21b6}.p-monthly{background:#fef9c3;color:#713f12}
.info{background:#f0fdf4;border-left:4px solid #22c55e;padding:10px 14px;border-radius:4px;margin-bottom:12px;font-size:13px}
.warn-box{background:#fff7ed;border-left:4px solid #f97316;padding:10px 14px;border-radius:4px;margin-bottom:12px;font-size:13px}
.err-box{background:#fef2f2;border-left:4px solid #dc2626;padding:10px 14px;border-radius:4px;margin-bottom:12px;font-size:13px}
</style></head><body>
<div class="nav">
<a class="brand" href="{{ url_for('dashboard') }}">{{ app_name }}</a>
{% if me %}
<a href="{{ url_for('dashboard') }}">Dashboard</a>
{% if me.system_role == 'super_admin' %}
<a href="{{ url_for('sa_users') }}">Users</a>
<a href="{{ url_for('sa_teams') }}">Teams</a>
<a href="{{ url_for('sa_audit') }}">Full&nbsp;Audit</a>
<a href="{{ url_for('sa_config') }}">Config</a>
{% endif %}
{% if can_admin %}
<a href="{{ url_for('admin_team') }}">My&nbsp;Team</a>
<a href="{{ url_for('admin_tasks') }}">Templates</a>
{% endif %}
<a href="{{ url_for('mc_tasks') }}">
All&nbsp;Tasks{% if badge > 0 %}<span class="bdg">{{ badge }}</span>{% endif %}
</a>
<div class="right">
<span class="p p-{{ me.system_role }}">
{% if me.system_role == 'super_admin' %}Super Admin{% else %}{{ roles_str }}{% endif %}
</span>
<span style="color:#94a3b8;font-size:13px">{{ me.username }}</span>
<a href="{{ url_for('logout') }}">Logout</a>
</div>
{% endif %}
</div>
<div class="wrap">
{% with msgs = get_flashed_messages() %}
{% if msgs %}{% for m in msgs %}<div class="flash">{{ m }}</div>{% endfor %}{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div></body></html>
"""

# ── Login ─────────────────────────────────────────────────────────
T_LOGIN = """
{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block content %}
<div style="max-width:380px;margin:80px auto">
<div class="card">
<h3 style="text-align:center;margin-bottom:20px">Sign In</h3>
<form method="post">
<label>Username</label><input name="username" autofocus style="margin-bottom:10px">
<label>Password</label><input name="password" type="password" style="margin-bottom:16px">
<button class="btn btn-p" style="width:100%">Login</button>
</form>
<p style="margin-top:14px;color:#94a3b8;font-size:12px;text-align:center">
Default: superadmin / super123
</p>
</div></div>
{% endblock %}
"""

# ── Dashboard ─────────────────────────────────────────────────────
T_DASHBOARD = """
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h2>Dashboard</h2>
<div class="g4">
<div class="kpi"><div class="n">{{ my_out }}</div><div class="l">My Outstanding</div></div>
<div class="kpi"><div class="n">{{ my_chk }}</div><div class="l">Awaiting My Approval</div></div>
<div class="kpi"><div class="n">{{ my_can }}</div><div class="l">Cancel Requests</div></div>
<div class="kpi"><div class="n">{{ total_open }}</div><div class="l">Total Open Tasks</div></div>
<div class="kpi"><div class="n">{{ total_teams }}</div><div class="l">Teams</div></div>
</div>
<div class="st">Recent Activity (last 10)</div>
<table><tr><th>Time</th><th>User</th><th>Entity</th><th>Action</th><th>Details</th></tr>
{% for r in recent %}
<tr><td>{{r.created_at}}</td><td>{{r.username or 'system'}}</td>
<td>{{r.entity_type}}</td><td>{{r.action}}</td><td>{{r.details or ''}}</td></tr>
{% else %}<tr><td colspan=5 class="empty">No activity yet.</td></tr>{% endfor %}
</table>
{% endblock %}
"""

# ── SA: Users ─────────────────────────────────────────────────────
T_SA_USERS = """
{% extends "base.html" %}{% block title %}Users & Roles{% endblock %}
{% block content %}
<h2>Users &amp; Roles</h2>
<div class="card"><h3>Create New User</h3>
<form method="post">
<input type="hidden" name="action" value="create_user">
<div class="g2">
<div>
<label>Username</label><input name="username" required>
<label>Full Name</label><input name="full_name">
</div>
<div>
<label>Password</label><input name="password" type="password" required>
<label>Assign Roles (can have multiple)</label>
<div style="display:flex;gap:16px;margin-top:6px">
<label style="font-weight:normal"><input type="checkbox" name="roles" value="admin"> Admin</label>
<label style="font-weight:normal"><input type="checkbox" name="roles" value="maker"> Maker</label>
<label style="font-weight:normal"><input type="checkbox" name="roles" value="checker"> Checker</label>
</div>
</div>
</div>
<button class="btn btn-p" style="margin-top:12px">Create User</button>
</form></div>

<div class="st">All Users</div>
<table>
<tr><th>ID</th><th>Username</th><th>Full Name</th><th>System</th><th>Roles</th><th>Actions</th></tr>
{% for u in all_users %}
<tr>
<td>{{u.id}}</td><td>{{u.username}}</td><td>{{u.full_name or ''}}</td>
<td>{% if u.system_role=='super_admin' %}<span class="p p-super_admin">Super Admin</span>{% endif %}</td>
<td>{% for r in roles_map[u.id] %}<span class="p p-{{r}}">{{r}}</span> {% else %}<span style="color:#94a3b8">none</span>{% endfor %}</td>
<td>
{% if u.system_role != 'super_admin' %}
<details><summary class="btn btn-sm btn-s" style="list-style:none;display:inline-block">Edit</summary>
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:14px;margin-top:6px;min-width:260px">
<form method="post">
<input type="hidden" name="action" value="update_roles">
<input type="hidden" name="user_id" value="{{u.id}}">
<b style="font-size:12px">Roles:</b><br>
<label style="font-weight:normal;margin-top:6px">
<input type="checkbox" name="roles" value="admin" {{'checked' if 'admin' in roles_map[u.id]}}> Admin</label>
<label style="font-weight:normal">
<input type="checkbox" name="roles" value="maker" {{'checked' if 'maker' in roles_map[u.id]}}> Maker</label>
<label style="font-weight:normal">
<input type="checkbox" name="roles" value="checker" {{'checked' if 'checker' in roles_map[u.id]}}> Checker</label>
<br><button class="btn btn-sm btn-p" style="margin-top:8px">Save Roles</button>
</form>
<hr style="margin:10px 0;border-color:#e2e8f0">
<form method="post">
<input type="hidden" name="action" value="reset_password">
<input type="hidden" name="user_id" value="{{u.id}}">
<input name="new_password" placeholder="New password" style="margin-bottom:6px">
<button class="btn btn-sm btn-w">Reset Password</button>
</form>
<hr style="margin:10px 0;border-color:#e2e8f0">
<form method="post" onsubmit="return confirm('Delete this user permanently?')">
<input type="hidden" name="action" value="delete_user">
<input type="hidden" name="user_id" value="{{u.id}}">
<button class="btn btn-sm btn-r">Delete User</button>
</form>
</div></details>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endblock %}
"""

# ── SA: Teams ─────────────────────────────────────────────────────
T_SA_TEAMS = """
{% extends "base.html" %}{% block title %}Teams{% endblock %}
{% block content %}
<h2>Teams</h2>
<div class="g2">
<div class="card"><h3>Create Team</h3>
<form method="post"><input type="hidden" name="action" value="create_team">
<label>Team Name</label><input name="name" required>
<label>Description</label><textarea name="description" rows="2"></textarea>
<button class="btn btn-p" style="margin-top:10px">Create Team</button>
</form>
</div>
<div class="card"><h3>Assign Admin to Team</h3>
<p style="font-size:12px;color:#64748b;margin-bottom:8px">
Only users with the <b>Admin</b> role are listed.
</p>
<form method="post"><input type="hidden" name="action" value="assign_admin">
<label>Team</label>
<select name="team_id">{% for t in all_teams %}<option value="{{t.id}}">{{t.name}}</option>{% endfor %}</select>
<label>Admin User</label>
<select name="user_id">
{% for u in admin_users %}<option value="{{u.id}}">{{u.username}} ({{u.full_name or ''}})</option>
{% else %}<option>No admin-role users yet</option>{% endfor %}
</select>
<button class="btn btn-p" style="margin-top:10px">Assign Admin</button>
</form>
</div>
</div>
<div class="st">All Teams</div>
<table><tr><th>ID</th><th>Name</th><th>Description</th><th>Created By</th><th>Members</th><th>Action</th></tr>
{% for t in all_teams %}
<tr>
<td>{{t.id}}</td><td>{{t.name}}</td><td>{{t.description or ''}}</td>
<td>{{t.created_by_name or ''}}</td>
<td>{% for m in members_map[t.id] %}<span class="p p-{{m.role}}">{{m.username}}</span> {% else %}-{% endfor %}</td>
<td>
<form method="post" onsubmit="return confirm('Delete team + all its tasks?')">
<input type="hidden" name="action" value="delete_team">
<input type="hidden" name="team_id" value="{{t.id}}">
<button class="btn btn-sm btn-r">Delete</button>
</form>
</td>
</tr>{% endfor %}
</table>
{% endblock %}
"""

# ── SA: Full Audit ────────────────────────────────────────────────
T_SA_AUDIT = """
{% extends "base.html" %}{% block title %}Full Audit{% endblock %}
{% block content %}
<h2>Full Audit Trail</h2>
<table><tr><th>ID</th><th>Time</th><th>User</th><th>Entity</th><th>ID</th><th>Action</th><th>Details</th></tr>
{% for r in rows %}
<tr><td>{{r.id}}</td><td>{{r.created_at}}</td><td>{{r.username or 'system'}}</td>
<td>{{r.entity_type}}</td><td>{{r.entity_id or '-'}}</td><td>{{r.action}}</td><td>{{r.details or ''}}</td></tr>
{% else %}<tr><td colspan=7 class="empty">No records.</td></tr>{% endfor %}
</table>{% endblock %}
"""

# ── SA: Config ────────────────────────────────────────────────────
T_SA_CONFIG = """
{% extends "base.html" %}{% block title %}Config{% endblock %}
{% block content %}
<h2>App Config</h2>
<div class="card">
<table><tr><th>Key</th><th style="width:60%">Value</th></tr>
{% for c in cfgs %}
<tr><td><b>{{c.config_key}}</b></td>
<td><form method="post" style="display:flex;gap:8px;align-items:center">
<input type="hidden" name="config_key" value="{{c.config_key}}">
<input name="config_value" value="{{c.config_value}}" style="margin:0">
<button class="btn btn-sm btn-p">Save</button>
</form></td></tr>
{% endfor %}
</table></div>
{% endblock %}
"""

# ── Admin: My Team ────────────────────────────────────────────────
T_ADMIN_TEAM = """
{% extends "base.html" %}{% block title %}My Team{% endblock %}
{% block content %}
<h2>My Team</h2>
{% for t in team_data %}
<div class="card"><h3>Team: {{ t.name }}</h3>
<div class="g2">
<div>
<div class="st" style="margin-top:0">Add Member</div>
<div class="info">
<b>Users with maker role:</b> {{ t.maker_names }}<br>
<b>Users with checker role:</b> {{ t.checker_names }}
</div>
<form method="post">
<input type="hidden" name="action" value="add_member">
<input type="hidden" name="team_id" value="{{ t.id }}">
<label>Username (must exist &amp; have the role below)</label>
<input name="username" required>
<label>Role in Team</label>
<select name="role">
<option value="maker">Maker</option>
<option value="checker">Checker</option>
</select>
<button class="btn btn-p" style="margin-top:10px">Add to Team</button>
</form>
</div>
<div>
<div class="st" style="margin-top:0">Current Members</div>
<table><tr><th>Username</th><th>Full Name</th><th>Role</th><th>Action</th></tr>
{% for m in t.members %}
<tr>
<td>{{m.username}}</td><td>{{m.full_name or ''}}</td>
<td><span class="p p-{{m.role}}">{{m.role}}</span></td>
<td>
<form method="post">
<input type="hidden" name="action" value="remove_member">
<input type="hidden" name="team_id" value="{{ t.id }}">
<input type="hidden" name="user_id" value="{{m.user_id}}">
<button class="btn btn-sm btn-r">Remove</button>
</form>
</td>
</tr>
{% else %}<tr><td colspan=4 class="empty">No members yet.</td></tr>
{% endfor %}
</table>
</div>
</div>
</div>
{% else %}
<div class="empty">No teams assigned to you. Ask super-admin to assign you as admin.</div>
{% endfor %}
{% endblock %}
"""

# ── Admin: Task Templates + Triggered Tasks ───────────────────────
T_ADMIN_TASKS = """
{% extends "base.html" %}{% block title %}Task Templates{% endblock %}
{% block content %}
<h2>Task Templates
{% if is_sa %}<small style="font-size:13px;color:#64748b;font-weight:normal"> — all teams (Super Admin)</small>{% endif %}
</h2>
<div class="card"><h3>Create Template</h3>
<form method="post"><input type="hidden" name="action" value="create_template">
<div class="g2">
<div>
<label>Team</label>
<select name="team_id">{% for t in my_teams %}<option value="{{t.id}}">{{t.name}}</option>{% endfor %}</select>
<label>Title</label><input name="title" required>
<label>Description</label><textarea name="description" rows="2"></textarea>
</div>
<div>
<label>Frequency</label>
<select name="frequency">
<option value="once">Once (one-time)</option>
<option value="daily">Daily</option>
<option value="weekly">Weekly — triggers every Monday</option>
<option value="monthly">Monthly — triggers 1st of each month</option>
</select>
<label>Default Maker</label>
<select name="maker_id"><option value="">-- none --</option>
{% for t in my_teams %}{% for m in mbyt[t.id] %}{% if m.role=='maker' %}
<option value="{{m.id}}">{{m.username}} [{{t.name}}]</option>
{% endif %}{% endfor %}{% endfor %}
</select>
<label>Default Checker</label>
<select name="checker_id"><option value="">-- none --</option>
{% for t in my_teams %}{% for m in mbyt[t.id] %}{% if m.role=='checker' %}
<option value="{{m.id}}">{{m.username}} [{{t.name}}]</option>
{% endif %}{% endfor %}{% endfor %}
</select>
</div>
</div>
<button class="btn btn-p" style="margin-top:12px">Create &amp; Trigger First Task</button>
</form></div>

<div class="st">All Templates</div>
<table><tr><th>ID</th><th>Team</th><th>Title</th><th>Freq</th><th>Maker</th><th>Checker</th><th>Active</th><th>Actions</th></tr>
{% for t in templates %}
<tr>
<td>{{t.id}}</td><td>{{t.team_name}}</td><td>{{t.title}}</td>
<td><span class="p p-{{t.frequency}}">{{t.frequency}}</span></td>
<td>{{t.maker_name or '-'}}</td><td>{{t.checker_name or '-'}}</td>
<td><span class="p {{'p-approved' if t.is_active else 'p-closed'}}">{{'Active' if t.is_active else 'Off'}}</span></td>
<td class="acts">
<form method="post" style="display:inline">
<input type="hidden" name="action" value="toggle">
<input type="hidden" name="tpl_id" value="{{t.id}}">
<button class="btn btn-sm btn-w">{{'Deactivate' if t.is_active else 'Activate'}}</button>
</form>
<form method="post" style="display:inline" onsubmit="return confirm('Delete template?')">
<input type="hidden" name="action" value="delete_tpl">
<input type="hidden" name="tpl_id" value="{{t.id}}">
<button class="btn btn-sm btn-r">Delete</button>
</form>
</td>
</tr>{% endfor %}
</table>

<div class="st">All Triggered Tasks for my teams</div>
<table>
<tr><th>ID</th><th>Team</th><th>Title</th><th>Status</th><th>Maker</th><th>Checker</th><th>Due</th><th>Actions</th></tr>
{% for t in triggered %}
<tr>
<td>{{t.id}}</td><td>{{t.team_name}}</td>
<td><a href="{{ url_for('mc_task', task_id=t.id) }}" style="color:#2563eb">{{t.title}}</a></td>
<td><span class="p p-{{t.status}}">{{t.status}}</span></td>
<td>{{t.maker_name or '-'}}</td><td>{{t.checker_name or '-'}}</td><td>{{t.due_date or '-'}}</td>
<td class="acts">
<a href="{{ url_for('mc_task', task_id=t.id) }}" class="btn btn-sm btn-s">Open</a>
<form method="post">
<input type="hidden" name="action" value="reassign">
<input type="hidden" name="task_id" value="{{t.id}}">
<select name="new_maker_id" style="width:auto;padding:4px;display:inline">
{% for team in my_teams %}{% for m in mbyt[team.id] %}{% if m.role=='maker' %}
<option value="{{m.id}}" {{'selected' if t.maker_id==m.id else ''}}>{{m.username}}</option>
{% endif %}{% endfor %}{% endfor %}
</select>
<button class="btn btn-sm btn-w">Reassign Maker</button>
</form>
</td>
</tr>{% endfor %}
{% if not triggered %}<tr><td colspan=8 class="empty">No tasks yet.</td></tr>{% endif %}
</table>
{% endblock %}
"""

# ── MC: All Tasks (visible scope depends on role) ─────────────────
T_MC_TASKS = """
{% extends "base.html" %}{% block title %}All Tasks{% endblock %}
{% block content %}
<h2>All Tasks
<small style="font-size:13px;color:#64748b;font-weight:normal">
{% if is_sa %}— all teams (Super Admin)
{% elif is_admin %}— your teams
{% else %}— assigned to you{% endif %}
</small>
</h2>
<table>
<tr><th>ID</th><th>Team</th><th>Title</th><th>Status</th><th>Maker</th><th>Checker</th><th>Due</th><th>Open</th></tr>
{% for t in tasks %}
<tr>
<td>{{t.id}}</td><td>{{t.team_name}}</td>
<td><a href="{{ url_for('mc_task', task_id=t.id) }}" style="color:#2563eb">{{t.title}}</a></td>
<td><span class="p p-{{t.status}}">{{t.status}}</span></td>
<td>
{{'★ You' if t.maker_id==me.id else (t.maker_name or '-')}}
</td>
<td>
{{'★ You' if t.checker_id==me.id else (t.checker_name or '-')}}
</td>
<td>{{t.due_date or '-'}}</td>
<td><a href="{{ url_for('mc_task', task_id=t.id) }}" class="btn btn-sm btn-s">Open</a></td>
</tr>
{% else %}
<tr><td colspan=8 class="empty">No tasks in scope.</td></tr>
{% endfor %}
</table>
{% endblock %}
"""

# ── MC: Task Detail ───────────────────────────────────────────────
T_MC_TASK = """
{% extends "base.html" %}{% block title %}Task #{{task.id}}{% endblock %}
{% block content %}
<h2>Task #{{task.id}}: {{task.title}}</h2>
<div class="g2">
<div class="card"><h3>Details</h3>
<table>
<tr><td style="width:130px"><b>Team</b></td><td>{{team_name}}</td></tr>
<tr><td><b>Description</b></td><td>{{task.description or '-'}}</td></tr>
<tr><td><b>Maker</b></td><td>{{maker_name}}{% if task.maker_id==me.id %} <span class="p p-maker">You</span>{% endif %}</td></tr>
<tr><td><b>Checker</b></td><td>{{checker_name}}{% if task.checker_id==me.id %} <span class="p p-checker">You</span>{% endif %}</td></tr>
<tr><td><b>Due Date</b></td><td>{{task.due_date or '-'}}</td></tr>
<tr><td><b>Status</b></td><td><span class="p p-{{task.status}}">{{task.status}}</span></td></tr>
<tr><td><b>Created</b></td><td>{{task.created_at}}</td></tr>
<tr><td><b>Updated</b></td><td>{{task.updated_at}}</td></tr>
</table>
{% if task.remarks %}
<div class="err-box" style="margin-top:12px"><b>Remark:</b> {{task.remarks}}</div>{% endif %}
{% if task.cancel_reason %}
<div class="warn-box" style="margin-top:12px"><b>Cancel reason:</b> {{task.cancel_reason}}</div>{% endif %}
</div>

<div class="card"><h3>Workflow Actions</h3>
{# ── MAKER actions ── #}
{% if task.status == 'outstanding' and (task.maker_id == me.id or is_team_admin) %}
<p class="info" style="margin-bottom:10px">
Acting as <b>Maker</b>{% if is_team_admin and task.maker_id != me.id %} (admin override){% endif %}
</p>
<form method="post" style="margin-bottom:14px">
<input type="hidden" name="action" value="submit">
<button class="btn btn-g">✓ Submit for Checking</button>
</form>
<hr style="border-color:#f1f5f9;margin-bottom:14px">
<form method="post">
<input type="hidden" name="action" value="request_cancel">
<label>Request Cancellation — provide reason</label>
<input name="cancel_reason" required style="margin-bottom:8px">
<button class="btn btn-w">Request Cancellation</button>
</form>

{% elif task.status == 'rejected' and (task.maker_id == me.id or is_team_admin) %}
<div class="err-box">Task was rejected. Fix and resubmit.</div>
<p class="info" style="margin-bottom:10px">
Acting as <b>Maker</b>{% if is_team_admin and task.maker_id != me.id %} (admin override){% endif %}
</p>
<form method="post">
<input type="hidden" name="action" value="reopen">
<button class="btn btn-w">↩ Rework &amp; Reopen</button>
</form>

{% elif task.status == 'submitted' and (task.checker_id == me.id or is_team_admin) %}
<p class="info" style="margin-bottom:10px">
Acting as <b>Checker</b>{% if is_team_admin and task.checker_id != me.id %} (admin override){% endif %}
</p>
<form method="post" style="margin-bottom:14px">
<input type="hidden" name="action" value="approve">
<button class="btn btn-g">✓ Approve</button>
</form>
<hr style="border-color:#f1f5f9;margin-bottom:14px">
<form method="post">
<input type="hidden" name="action" value="reject">
<label>Rejection Remarks</label>
<input name="remarks" required style="margin-bottom:8px">
<button class="btn btn-r">✗ Reject</button>
</form>

{% elif task.status == 'cancel_requested' and (task.checker_id == me.id or is_team_admin) %}
<div class="warn-box">⚠ Maker has requested cancellation.</div>
<p class="info" style="margin-bottom:10px">
Acting as <b>Checker</b>{% if is_team_admin and task.checker_id != me.id %} (admin override){% endif %}
</p>
<form method="post" style="margin-bottom:14px">
<input type="hidden" name="action" value="approve_cancel">
<button class="btn btn-g">✓ Approve Cancellation (close task)</button>
</form>
<hr style="border-color:#f1f5f9;margin-bottom:14px">
<form method="post">
<input type="hidden" name="action" value="reject_cancel">
<label>Reason for rejecting cancellation</label>
<input name="reject_cancel_reason" required style="margin-bottom:8px">
<button class="btn btn-r">✗ Reject Cancellation</button>
</form>

{% else %}
<p style="color:#94a3b8;font-size:13px">
No actions available for your role at this stage.
{% if task.status in ('approved','closed') %}
<br><br><span class="p p-{{task.status}}">{{task.status}}</span> — task is complete.
{% endif %}
</p>
{% endif %}
</div>
</div>

<div class="st">Audit Trail for this task</div>
<table><tr><th>Time</th><th>User</th><th>Action</th><th>Details</th></tr>
{% for a in audit %}
<tr><td>{{a.created_at}}</td><td>{{a.username or 'system'}}</td>
<td>{{a.action}}</td><td>{{a.details or ''}}</td></tr>
{% else %}<tr><td colspan=4 class="empty">No audit records yet.</td></tr>{% endfor %}
</table>
{% endblock %}
"""

# ── Shared: Audit ─────────────────────────────────────────────────
T_AUDIT = """
{% extends "base.html" %}{% block title %}Audit{% endblock %}
{% block content %}
<h2>Audit Trail (last 500)</h2>
<table><tr><th>ID</th><th>Time</th><th>User</th><th>Entity</th><th>ID</th><th>Action</th><th>Details</th></tr>
{% for r in rows %}
<tr><td>{{r.id}}</td><td>{{r.created_at}}</td><td>{{r.username or 'system'}}</td>
<td>{{r.entity_type}}</td><td>{{r.entity_id or '-'}}</td><td>{{r.action}}</td><td>{{r.details or ''}}</td></tr>
{% else %}<tr><td colspan=7 class="empty">No records.</td></tr>{% endfor %}
</table>{% endblock %}
"""

def _render(template_str, **ctx):
"""Render any template string using a base template loader."""
loader = DictLoader({"base.html": T_BASE})
app.jinja_loader = loader
app.jinja_env.loader = loader
return render_template_string(template_str, **ctx)


# ═══════════════════════════════════════════════════════════════════
# 5. MODULE: SUPER ADMIN (/sa/*)
# ═══════════════════════════════════════════════════════════════════

@app.route("/sa/users", methods=["GET","POST"])
@super_required
def sa_users():
db = get_db()
cu = current_user()

if request.method == "POST":
a = request.form.get("action")

if a == "create_user":
try:
db.execute(
"INSERT INTO users (username,password_hash,full_name,system_role,created_at)"
" VALUES (?,?,?,'user',?)",
(request.form["username"], hash_pw(request.form["password"]),
request.form.get("full_name",""), now())
)
db.commit()
uid = db.execute("SELECT id FROM users WHERE username=?",
(request.form["username"],)).fetchone()["id"]
for r in request.form.getlist("roles"):
db.execute("INSERT OR IGNORE INTO user_roles (user_id,role,created_at) VALUES (?,?,?)",
(uid, r, now()))
db.commit()
log_action(cu["id"], "user", uid, "create_user", request.form["username"])
flash("User created.")
except Exception as e:
flash(f"Error: {e}")

elif a == "update_roles":
uid = int(request.form["user_id"])
db.execute("DELETE FROM user_roles WHERE user_id=?", (uid,))
for r in request.form.getlist("roles"):
db.execute("INSERT OR IGNORE INTO user_roles (user_id,role,created_at) VALUES (?,?,?)",
(uid, r, now()))
db.commit()
log_action(cu["id"], "user", uid, "update_roles",
",".join(request.form.getlist("roles")))
flash("Roles updated.")

elif a == "reset_password":
uid = int(request.form["user_id"])
db.execute("UPDATE users SET password_hash=? WHERE id=?",
(hash_pw(request.form["new_password"]), uid))
db.commit()
log_action(cu["id"], "user", uid, "reset_password", "Reset by super-admin")
flash("Password reset.")

elif a == "delete_user":
uid = int(request.form["user_id"])
for tbl in ("user_roles", "team_members"):
db.execute(f"DELETE FROM {tbl} WHERE user_id=?", (uid,))
db.execute("DELETE FROM users WHERE id=? AND system_role!='super_admin'", (uid,))
db.commit()
log_action(cu["id"], "user", uid, "delete_user", "Deleted")
flash("User deleted.")

return redirect(url_for("sa_users"))

all_users = db.execute("SELECT * FROM users ORDER BY system_role DESC,username").fetchall()
roles_map = {u["id"]: functional_roles(u["id"]) for u in all_users}
return _render(T_SA_USERS, all_users=all_users, roles_map=roles_map)


@app.route("/sa/teams", methods=["GET","POST"])
@super_required
def sa_teams():
db = get_db()
cu = current_user()

if request.method == "POST":
a = request.form.get("action")

if a == "create_team":
db.execute(
"INSERT INTO teams (name,description,created_by,created_at) VALUES (?,?,?,?)",
(request.form["name"], request.form.get("description",""), cu["id"], now())
)
db.commit()
tid = db.execute("SELECT last_insert_rowid() id").fetchone()["id"]
log_action(cu["id"], "team", tid, "create_team", request.form["name"])
flash("Team created.")

elif a == "assign_admin":
tid = int(request.form["team_id"])
uid = int(request.form["user_id"])
db.execute(
"INSERT OR REPLACE INTO team_members (team_id,user_id,role,added_at) VALUES (?,?,'admin',?)",
(tid, uid, now())
)
db.commit()
uname = db.execute("SELECT username FROM users WHERE id=?", (uid,)).fetchone()["username"]
log_action(cu["id"], "team", tid, "assign_admin", f"{uname} → admin")
flash("Admin assigned.")

elif a == "delete_team":
tid = int(request.form["team_id"])
for tbl in ("task_templates", "tasks", "team_members"):
db.execute(f"DELETE FROM {tbl} WHERE team_id=?", (tid,))
db.execute("DELETE FROM teams WHERE id=?", (tid,))
db.commit()
log_action(cu["id"], "team", tid, "delete_team", "Deleted")
flash("Team deleted.")

return redirect(url_for("sa_teams"))

all_teams = db.execute(
"SELECT t.*,u.username created_by_name FROM teams t "
"LEFT JOIN users u ON u.id=t.created_by ORDER BY t.id DESC"
).fetchall()
admin_users = db.execute(
"SELECT u.* FROM users u JOIN user_roles ur ON ur.user_id=u.id "
"WHERE ur.role='admin' ORDER BY u.username"
).fetchall()
members_map = {t["id"]: db.execute(
"SELECT u.username,tm.role FROM team_members tm "
"JOIN users u ON u.id=tm.user_id WHERE tm.team_id=?", (t["id"],)
).fetchall() for t in all_teams}
return _render(T_SA_TEAMS, all_teams=all_teams,
admin_users=admin_users, members_map=members_map)


@app.route("/sa/audit")
@super_required
def sa_audit():
rows = get_db().execute(
"SELECT a.*,u.username FROM audit_log a "
"LEFT JOIN users u ON u.id=a.user_id ORDER BY a.id DESC LIMIT 2000"
).fetchall()
return _render(T_SA_AUDIT, rows=rows)


@app.route("/sa/config", methods=["GET","POST"])
@super_required
def sa_config():
db = get_db()
cu = current_user()
if request.method == "POST":
db.execute(
"UPDATE app_config SET config_value=?,updated_at=? WHERE config_key=?",
(request.form["config_value"], now(), request.form["config_key"])
)
db.commit()
log_action(cu["id"], "config", None, "update", request.form["config_key"])
flash("Config saved.")
return redirect(url_for("sa_config"))
cfgs = db.execute("SELECT * FROM app_config ORDER BY config_key").fetchall()
return _render(T_SA_CONFIG, cfgs=cfgs)


# ═══════════════════════════════════════════════════════════════════
# 6. MODULE: ADMIN (/admin/*)
# ═══════════════════════════════════════════════════════════════════

@app.route("/admin/team", methods=["GET","POST"])
@admin_required
def admin_team():
db = get_db()
cu = current_user()
my_teams = admin_teams(cu)

if request.method == "POST":
a = request.form.get("action")
tid = int(request.form.get("team_id", 0))

if a == "add_member":
uname = request.form["username"].strip()
role = request.form["role"]
target = db.execute("SELECT * FROM users WHERE username=?", (uname,)).fetchone()
if not target:
flash(f"User '{uname}' not found.")
elif role not in functional_roles(target["id"]):
flash(f"'{uname}' does not have the '{role}' system role. "
"Ask super-admin to assign it first.")
else:
db.execute(
"INSERT OR REPLACE INTO team_members (team_id,user_id,role,added_at) VALUES (?,?,?,?)",
(tid, target["id"], role, now())
)
db.commit()
log_action(cu["id"], "team_member", tid, "add_member",
f"{uname} as {role}")
flash(f"{uname} added as {role}.")

elif a == "remove_member":
uid = int(request.form["user_id"])
db.execute("DELETE FROM team_members WHERE team_id=? AND user_id=?", (tid, uid))
db.commit()
log_action(cu["id"], "team_member", tid, "remove_member", f"uid={uid}")
flash("Member removed.")

return redirect(url_for("admin_team"))

all_makers = db.execute(
"SELECT u.* FROM users u JOIN user_roles ur ON ur.user_id=u.id "
"WHERE ur.role='maker' ORDER BY u.username"
).fetchall()
all_checkers = db.execute(
"SELECT u.* FROM users u JOIN user_roles ur ON ur.user_id=u.id "
"WHERE ur.role='checker' ORDER BY u.username"
).fetchall()

team_data = []
for t in my_teams:
members = db.execute(
"SELECT tm.*,u.username,u.full_name FROM team_members tm "
"JOIN users u ON u.id=tm.user_id WHERE tm.team_id=? AND tm.role != 'admin' "
"ORDER BY tm.role,u.username", (t["id"],)
).fetchall()
row = dict(t)
row["members"] = members
row["maker_names"] = ", ".join(m["username"] for m in all_makers) or "none"
row["checker_names"] = ", ".join(c["username"] for c in all_checkers) or "none"
team_data.append(row)

return _render(T_ADMIN_TEAM, team_data=team_data)


@app.route("/admin/tasks", methods=["GET","POST"])
@admin_required
def admin_tasks():
db = get_db()
cu = current_user()
my_teams = admin_teams(cu)

if request.method == "POST":
a = request.form.get("action")

if a == "create_template":
db.execute(
"INSERT INTO task_templates (team_id,title,description,frequency,"
"maker_id,checker_id,is_active,created_by,created_at) VALUES (?,?,?,?,?,?,1,?,?)",
(int(request.form["team_id"]), request.form["title"],
request.form.get("description",""), request.form["frequency"],
request.form.get("maker_id") or None,
request.form.get("checker_id") or None,
cu["id"], now())
)
db.commit()
trigger_due_tasks()
log_action(cu["id"], "task_template", None, "create", request.form["title"])
flash("Template created and first task triggered.")

elif a == "toggle":
tpl_id = int(request.form["tpl_id"])
cur = db.execute("SELECT is_active FROM task_templates WHERE id=?",
(tpl_id,)).fetchone()
db.execute("UPDATE task_templates SET is_active=? WHERE id=?",
(0 if cur["is_active"] else 1, tpl_id))
db.commit()
flash("Template toggled.")

elif a == "delete_tpl":
tpl_id = int(request.form["tpl_id"])
db.execute("DELETE FROM task_templates WHERE id=?", (tpl_id,))
db.commit()
flash("Template deleted.")

elif a == "reassign":
task_id = int(request.form["task_id"])
new_mid = int(request.form["new_maker_id"])
db.execute("UPDATE tasks SET maker_id=?,updated_at=? WHERE id=?",
(new_mid, now(), task_id))
db.commit()
nm = db.execute("SELECT username FROM users WHERE id=?", (new_mid,)).fetchone()
log_action(cu["id"], "task", task_id, "reassign_maker", f"→ {nm['username']}")
flash("Maker reassigned.")

return redirect(url_for("admin_tasks"))

team_ids = [t["id"] for t in my_teams]
templates, triggered, mbyt = [], [], {}
if team_ids:
ph = ",".join("?" * len(team_ids))
templates = db.execute(
f"SELECT tp.*,t.name team_name,um.username maker_name,uc.username checker_name "
f"FROM task_templates tp "
f"LEFT JOIN teams t ON t.id=tp.team_id "
f"LEFT JOIN users um ON um.id=tp.maker_id "
f"LEFT JOIN users uc ON uc.id=tp.checker_id "
f"WHERE tp.team_id IN ({ph}) ORDER BY tp.id DESC", team_ids
).fetchall()
triggered = db.execute(
f"SELECT tk.*,t.name team_name,um.username maker_name,uc.username checker_name "
f"FROM tasks tk "
f"LEFT JOIN teams t ON t.id=tk.team_id "
f"LEFT JOIN users um ON um.id=tk.maker_id "
f"LEFT JOIN users uc ON uc.id=tk.checker_id "
f"WHERE tk.team_id IN ({ph}) ORDER BY "
f"CASE tk.status WHEN 'outstanding' THEN 1 WHEN 'submitted' THEN 2 "
f"WHEN 'cancel_requested' THEN 3 WHEN 'rejected' THEN 4 ELSE 5 END, tk.id DESC",
team_ids
).fetchall()
for t in my_teams:
mbyt[t["id"]] = db.execute(
"SELECT tm.role,u.id,u.username FROM team_members tm "
"JOIN users u ON u.id=tm.user_id "
"WHERE tm.team_id=? AND tm.role IN ('maker','checker') ORDER BY tm.role,u.username",
(t["id"],)
).fetchall()

return _render(T_ADMIN_TASKS, my_teams=my_teams, templates=templates,
triggered=triggered, mbyt=mbyt,
is_sa=is_super(cu))


# ═══════════════════════════════════════════════════════════════════
# 7. MODULE: MAKER-CHECKER (/mc/*)
# SA → sees ALL tasks
# Admin → sees tasks for their teams
# Others→ sees only tasks where they are maker or checker
# ═══════════════════════════════════════════════════════════════════

def _tasks_for_user(u):
"""Return tasks visible to this user with consistent column set."""
db = get_db()
base = (
"SELECT tk.*,t.name team_name,um.username maker_name,uc.username checker_name "
"FROM tasks tk "
"LEFT JOIN teams t ON t.id =tk.team_id "
"LEFT JOIN users um ON um.id=tk.maker_id "
"LEFT JOIN users uc ON uc.id=tk.checker_id "
)
order = (
"ORDER BY CASE tk.status "
"WHEN 'outstanding' THEN 1 WHEN 'submitted' THEN 2 "
"WHEN 'cancel_requested' THEN 3 WHEN 'rejected' THEN 4 ELSE 5 END, tk.id DESC"
)
if is_super(u):
return db.execute(f"{base} {order}").fetchall()

tids = admin_team_ids(u)
if tids:
# Admin: all tasks for their teams + any personally assigned tasks
ph = ",".join("?" * len(tids))
return db.execute(
f"{base} WHERE tk.team_id IN ({ph}) OR tk.maker_id=? OR tk.checker_id=? {order}",
tids + [u["id"], u["id"]]
).fetchall()

# Plain maker/checker
return db.execute(
f"{base} WHERE tk.maker_id=? OR tk.checker_id=? {order}",
(u["id"], u["id"])
).fetchall()


@app.route("/mc/tasks")
@login_required
def mc_tasks():
trigger_due_tasks()
u = current_user()
tasks = _tasks_for_user(u)
return _render(T_MC_TASKS, tasks=tasks, me=u,
is_sa=is_super(u),
is_admin=is_any_admin(u))


@app.route("/mc/task/<int:task_id>", methods=["GET","POST"])
@login_required
def mc_task(task_id):
u = current_user()
db = get_db()
t = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
if not t:
return "Task not found", 404

# Access check: SA and team admins can always open; others only if assigned
is_team_admin = is_admin_of_team(u, t["team_id"])
if not is_team_admin and u["id"] not in (t["maker_id"], t["checker_id"]):
flash("You do not have access to this task.")
return redirect(url_for("mc_tasks"))

if request.method == "POST":
act = request.form.get("action")

# Helper: can this user act as maker on this task?
can_make = (u["id"] == t["maker_id"]) or is_team_admin
can_check = (u["id"] == t["checker_id"]) or is_team_admin

if act == "submit" and t["status"] == "outstanding" and can_make:
db.execute("UPDATE tasks SET status='submitted',updated_at=? WHERE id=?",
(now(), task_id))
db.commit()
log_action(u["id"], "task", task_id, "submit", "Submitted for checking")
flash("Submitted.")

elif act == "reopen" and t["status"] == "rejected" and can_make:
db.execute("UPDATE tasks SET status='outstanding',remarks=NULL,updated_at=? WHERE id=?",
(now(), task_id))
db.commit()
log_action(u["id"], "task", task_id, "reopen", "Reopened after rejection")
flash("Task reopened.")

elif act == "request_cancel" and can_make \
and t["status"] not in ("approved","closed","cancel_requested"):
reason = request.form.get("cancel_reason","").strip()
if not reason:
flash("Please provide a cancellation reason.")
else:
db.execute(
"UPDATE tasks SET status='cancel_requested',cancel_reason=?,updated_at=? WHERE id=?",
(reason, now(), task_id)
)
db.commit()
log_action(u["id"], "task", task_id, "request_cancel", reason)
flash("Cancellation request sent.")

elif act == "approve" and t["status"] == "submitted" and can_check:
if cfg("allow_self_approval","false") != "true" and u["id"] == t["maker_id"]:
flash("Self-approval is disabled (set allow_self_approval=true in Config to enable).")
else:
db.execute("UPDATE tasks SET status='approved',updated_at=? WHERE id=?",
(now(), task_id))
db.commit()
log_action(u["id"], "task", task_id, "approve", "Approved")
flash("Task approved.")

elif act == "reject" and t["status"] == "submitted" and can_check:
remarks = request.form.get("remarks","Rejected").strip()
db.execute("UPDATE tasks SET status='rejected',remarks=?,updated_at=? WHERE id=?",
(remarks, now(), task_id))
db.commit()
log_action(u["id"], "task", task_id, "reject", remarks)
flash("Task rejected.")

elif act == "approve_cancel" and t["status"] == "cancel_requested" and can_check:
db.execute("UPDATE tasks SET status='closed',updated_at=? WHERE id=?",
(now(), task_id))
db.commit()
log_action(u["id"], "task", task_id, "approve_cancel",
f"Cancellation approved. Reason: {t['cancel_reason']}")
flash("Cancellation approved — task closed.")

elif act == "reject_cancel" and t["status"] == "cancel_requested" and can_check:
remark = request.form.get("reject_cancel_reason","Cancellation rejected")
db.execute(
"UPDATE tasks SET status='outstanding',cancel_reason=NULL,remarks=?,updated_at=? WHERE id=?",
(remark, now(), task_id)
)
db.commit()
log_action(u["id"], "task", task_id, "reject_cancel", f"Rejected: {remark}")
flash("Cancellation rejected — task back to outstanding.")

else:
flash("Action not permitted at this stage.")

return redirect(url_for("mc_task", task_id=task_id))

# GET — reload task after redirect
t = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
team_row = db.execute("SELECT name FROM teams WHERE id=?", (t["team_id"],)).fetchone()
maker_row = db.execute("SELECT username FROM users WHERE id=?", (t["maker_id"],)).fetchone() if t["maker_id"] else None
checker_row = db.execute("SELECT username FROM users WHERE id=?", (t["checker_id"],)).fetchone() if t["checker_id"] else None
audit = db.execute(
"SELECT a.*,u.username FROM audit_log a "
"LEFT JOIN users u ON u.id=a.user_id "
"WHERE entity_type='task' AND entity_id=? ORDER BY a.id DESC", (task_id,)
).fetchall()
return _render(T_MC_TASK,
task=t,
team_name = team_row["name"] if team_row else "-",
maker_name = maker_row["username"] if maker_row else "-",
checker_name = checker_row["username"] if checker_row else "-",
is_team_admin= is_team_admin,
audit=audit, me=u)


# ═══════════════════════════════════════════════════════════════════
# 8. MODULE: SHARED (dashboard, audit, health)
# ═══════════════════════════════════════════════════════════════════

@app.route("/dashboard")
@login_required
def dashboard():
trigger_due_tasks()
u = current_user()
db = get_db()
# KPIs scoped to what the user can see
tids = admin_team_ids(u)
if is_super(u):
total_open = db.execute("SELECT COUNT(*) c FROM tasks WHERE status NOT IN ('closed','approved')").fetchone()["c"]
elif tids:
ph = ",".join("?" * len(tids))
total_open = db.execute(f"SELECT COUNT(*) c FROM tasks WHERE status NOT IN ('closed','approved') AND team_id IN ({ph})", tids).fetchone()["c"]
else:
total_open = 0

my_out = db.execute("SELECT COUNT(*) c FROM tasks WHERE status='outstanding' AND maker_id=?", (u["id"],)).fetchone()["c"]
my_chk = db.execute("SELECT COUNT(*) c FROM tasks WHERE status='submitted' AND checker_id=?", (u["id"],)).fetchone()["c"]
my_can = db.execute("SELECT COUNT(*) c FROM tasks WHERE status='cancel_requested' AND checker_id=?", (u["id"],)).fetchone()["c"]
total_teams = db.execute("SELECT COUNT(*) c FROM teams").fetchone()["c"]
recent = db.execute(
"SELECT a.*,u.username FROM audit_log a "
"LEFT JOIN users u ON u.id=a.user_id ORDER BY a.id DESC LIMIT 10"
).fetchall()
return _render(T_DASHBOARD, my_out=my_out, my_chk=my_chk, my_can=my_can,
total_open=total_open, total_teams=total_teams, recent=recent)


@app.route("/audit")
@login_required
def audit():
rows = get_db().execute(
"SELECT a.*,u.username FROM audit_log a "
"LEFT JOIN users u ON u.id=a.user_id ORDER BY a.id DESC LIMIT 500"
).fetchall()
return _render(T_AUDIT, rows=rows)


@app.route("/health")
def health():
return jsonify({"status": "ok", "time": now()})


# ═══════════════════════════════════════════════════════════════════
# 9. ENTRY POINT
# ═══════════════════════════════════════════════════════════════════

if __name__ == "__main__":
with app.app_context():
init_db()
trigger_due_tasks()
app.run(
host = "0.0.0.0",
port = int(os.environ.get("PORT", 5000)),
debug = False
)

Comments