from datetime import datetime, timezone, timedelta from psycopg import AsyncConnection EDIT_GRACE_PERIOD = timedelta(minutes=15) async def list_categories(conn: AsyncConnection, parent_id: int | None = None) -> list[dict]: rows = await (await conn.execute( """SELECT id, parent_id, name, slug, icon FROM issue_categories WHERE (parent_id = %s OR (%s IS NULL AND parent_id IS NULL)) ORDER BY name""", (parent_id, parent_id) )).fetchall() return [{"id": r[0], "parent_id": r[1], "name": r[2], "slug": r[3], "icon": r[4]} for r in rows] async def get_category(conn: AsyncConnection, category_id: int) -> dict | None: row = await (await conn.execute( "SELECT id, parent_id, name, slug, icon FROM issue_categories WHERE id = %s", (category_id,) )).fetchone() if not row: return None return {"id": row[0], "parent_id": row[1], "name": row[2], "slug": row[3], "icon": row[4]} async def create_issue( conn: AsyncConnection, reporter_id: int, title: str, body: str, category_id: int, location_id: int, ) -> dict: cat = await get_category(conn, category_id) if not cat: raise ValueError("Kategori bulunamadı") loc = await (await conn.execute( "SELECT id FROM locations WHERE id = %s", (location_id,) )).fetchone() if not loc: raise ValueError("Konum bulunamadı") row = await (await conn.execute( """INSERT INTO issues (title, body, category_id, location_id, reporter_id) VALUES (%s, %s, %s, %s, %s) RETURNING id, status, created_at""", (title, body, category_id, location_id, reporter_id) )).fetchone() await conn.commit() return {"id": row[0], "status": row[1], "created_at": row[2]} async def get_issue(conn: AsyncConnection, issue_id: int) -> dict | None: row = await (await conn.execute( """SELECT i.id, i.title, i.body, i.status, i.created_at, i.updated_at, i.reporter_id, i.category_id, i.location_id, ic.name AS category_name, ic.slug AS category_slug, l.name AS location_name, (SELECT COUNT(*) FROM issue_votes v WHERE v.issue_id = i.id AND v.vote = 'resolved') AS votes_resolved, (SELECT COUNT(*) FROM issue_votes v WHERE v.issue_id = i.id AND v.vote = 'ongoing') AS votes_ongoing, (SELECT COUNT(*) FROM issue_comments c WHERE c.issue_id = i.id AND NOT c.is_deleted) AS comment_count FROM issues i JOIN issue_categories ic ON ic.id = i.category_id JOIN locations l ON l.id = i.location_id WHERE i.id = %s""", (issue_id,) )).fetchone() if not row: return None return { "id": row[0], "title": row[1], "body": row[2], "status": row[3], "created_at": row[4], "updated_at": row[5], "reporter_id": row[6], "category_id": row[7], "location_id": row[8], "category_name": row[9], "category_slug": row[10], "location_name": row[11], "votes": {"resolved": row[12], "ongoing": row[13]}, "comment_count": row[14], } async def list_issues( conn: AsyncConnection, location_id: int | None = None, category_id: int | None = None, status: str | None = None, reporter_id: int | None = None, limit: int = 20, offset: int = 0, ) -> list[dict]: filters = [] params: list = [] if location_id: filters.append("i.location_id = %s"); params.append(location_id) if category_id: filters.append("i.category_id = %s"); params.append(category_id) if status: filters.append("i.status = %s"); params.append(status) if reporter_id: filters.append("i.reporter_id = %s"); params.append(reporter_id) where = ("WHERE " + " AND ".join(filters)) if filters else "" params += [limit, offset] rows = await (await conn.execute( f"""SELECT i.id, i.title, i.status, i.created_at, ic.name AS category_name, l.name AS location_name, (SELECT COUNT(*) FROM issue_votes v WHERE v.issue_id = i.id) AS vote_count FROM issues i JOIN issue_categories ic ON ic.id = i.category_id JOIN locations l ON l.id = i.location_id {where} ORDER BY i.created_at DESC LIMIT %s OFFSET %s""", params )).fetchall() return [ {"id": r[0], "title": r[1], "status": r[2], "created_at": r[3], "category_name": r[4], "location_name": r[5], "vote_count": r[6]} for r in rows ] async def update_issue( conn: AsyncConnection, issue_id: int, user_id: int, is_admin: bool, title: str | None = None, body: str | None = None, ) -> dict: row = await (await conn.execute( "SELECT reporter_id, created_at, status FROM issues WHERE id = %s", (issue_id,) )).fetchone() if not row: raise ValueError("Sorun bulunamadı") reporter_id, created_at, status = row if not is_admin: if reporter_id != user_id: raise PermissionError("Bu sorunu düzenleme yetkiniz yok") age = datetime.now(timezone.utc) - created_at if age > EDIT_GRACE_PERIOD: raise PermissionError("Düzenleme süresi doldu (15 dakika)") updates = [] params = [] if title is not None: updates.append("title = %s"); params.append(title) if body is not None: updates.append("body = %s"); params.append(body) if not updates: raise ValueError("Güncellenecek alan yok") updates.append("updated_at = NOW()") params.append(issue_id) await conn.execute( f"UPDATE issues SET {', '.join(updates)} WHERE id = %s", params ) await conn.commit() return {"ok": True} async def update_status( conn: AsyncConnection, issue_id: int, new_status: str, ) -> dict: row = await (await conn.execute( "SELECT id FROM issues WHERE id = %s", (issue_id,) )).fetchone() if not row: raise ValueError("Sorun bulunamadı") await conn.execute( "UPDATE issues SET status = %s, updated_at = NOW() WHERE id = %s", (new_status, issue_id) ) await conn.commit() return {"ok": True} async def delete_issue(conn: AsyncConnection, issue_id: int) -> dict: row = await (await conn.execute( "SELECT id FROM issues WHERE id = %s", (issue_id,) )).fetchone() if not row: raise ValueError("Sorun bulunamadı") await conn.execute("DELETE FROM issues WHERE id = %s", (issue_id,)) await conn.commit() return {"ok": True} async def cast_vote(conn: AsyncConnection, issue_id: int, user_id: int, vote: str) -> dict: row = await (await conn.execute( "SELECT id FROM issues WHERE id = %s", (issue_id,) )).fetchone() if not row: raise ValueError("Sorun bulunamadı") await conn.execute( """INSERT INTO issue_votes (issue_id, user_id, vote) VALUES (%s, %s, %s) ON CONFLICT (issue_id, user_id) DO UPDATE SET vote = EXCLUDED.vote""", (issue_id, user_id, vote) ) await conn.commit() return {"ok": True} async def list_comments(conn: AsyncConnection, issue_id: int) -> list[dict]: rows = await (await conn.execute( """SELECT c.id, c.parent_id, c.user_id, c.body, c.is_deleted, c.created_at, c.updated_at FROM issue_comments c WHERE c.issue_id = %s ORDER BY c.created_at ASC""", (issue_id,) )).fetchall() return [ { "id": r[0], "parent_id": r[1], "user_id": r[2], "body": "[silindi]" if r[4] else r[3], "is_deleted": r[4], "created_at": r[5], "updated_at": r[6], } for r in rows ] async def add_comment( conn: AsyncConnection, issue_id: int, user_id: int, body: str, parent_id: int | None = None, ) -> dict: row = await (await conn.execute( "SELECT id FROM issues WHERE id = %s", (issue_id,) )).fetchone() if not row: raise ValueError("Sorun bulunamadı") if parent_id: parent = await (await conn.execute( "SELECT id FROM issue_comments WHERE id = %s AND issue_id = %s AND NOT is_deleted", (parent_id, issue_id) )).fetchone() if not parent: raise ValueError("Yanıt verilen yorum bulunamadı") result = await (await conn.execute( """INSERT INTO issue_comments (issue_id, user_id, body, parent_id) VALUES (%s, %s, %s, %s) RETURNING id, created_at""", (issue_id, user_id, body, parent_id) )).fetchone() await conn.commit() return {"id": result[0], "created_at": result[1]} async def delete_comment(conn: AsyncConnection, comment_id: int, user_id: int, is_admin: bool) -> dict: row = await (await conn.execute( "SELECT user_id FROM issue_comments WHERE id = %s AND NOT is_deleted", (comment_id,) )).fetchone() if not row: raise ValueError("Yorum bulunamadı") if not is_admin and row[0] != user_id: raise PermissionError("Bu yorumu silme yetkiniz yok") await conn.execute( "UPDATE issue_comments SET is_deleted = TRUE, updated_at = NOW() WHERE id = %s", (comment_id,) ) await conn.commit() return {"ok": True}