Benny-Tang commited on
Commit
c404ea1
·
verified ·
1 Parent(s): 6eb2a2c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +503 -172
app.py CHANGED
@@ -1,254 +1,584 @@
 
1
  import gradio as gr
2
  import sqlite3
 
3
  import pandas as pd
4
  from datetime import datetime
 
 
5
 
6
  DB_FILE = "cases.db"
 
7
 
8
- # ------------------ DB Setup ------------------
9
  def init_db():
10
  conn = sqlite3.connect(DB_FILE)
11
  c = conn.cursor()
 
12
  c.execute("""
13
- CREATE TABLE IF NOT EXISTS cases (
14
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15
- citizen_name TEXT,
16
- fine_amount REAL,
17
- description TEXT,
18
- police_evidence TEXT,
19
- citizen_appeal TEXT,
20
- citizen_evidence TEXT,
21
- status TEXT,
22
- created_at TEXT
23
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  """)
25
  conn.commit()
26
  conn.close()
27
 
 
28
  def seed_mock_data():
29
  conn = sqlite3.connect(DB_FILE)
30
  c = conn.cursor()
31
- c.execute("SELECT COUNT(*) FROM cases")
32
  count = c.fetchone()[0]
33
-
34
- if count == 0: # Seed only when DB is empty
35
  cases = [
 
36
  (
37
- "Ali",
38
- 100.0,
39
- "Speeding in a 50 km/h zone / Memandu laju di zon 50 km/j / 在限速50公里的区域超速",
40
- "evidence_photo_speed.png",
41
- None,
42
- None,
43
- "Pending Payment",
44
- datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 
45
  ),
 
46
  (
47
- "Ah Kow",
48
- 250.0,
49
- "Running a red light / Melanggar lampu merah / 闯红灯",
50
- "evidence_redlight_cam.jpg",
51
- "I crossed on yellow, not red / Saya lintas pada lampu kuning, bukan merah / 我是在黄灯时通过,不是红灯",
52
- "appeal_photo.jpg",
53
- "Appealed",
54
- datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 
55
  ),
 
56
  (
57
- "Muthu",
58
- 75.0,
59
- "Illegal parking / Letak kereta haram / 非法停车",
60
- "evidence_parking.jpg",
61
- None,
62
- None,
63
- "Fine Paid",
64
- datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 
65
  ),
66
  ]
67
  c.executemany("""
68
- INSERT INTO cases (
69
- citizen_name, fine_amount, description, police_evidence,
70
- citizen_appeal, citizen_evidence, status, created_at
71
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
 
 
 
 
 
72
  """, cases)
73
  conn.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  conn.close()
75
 
76
- init_db()
77
- seed_mock_data()
 
78
 
79
- # ------------------ Functions ------------------
80
- def police_create_case(citizen_name, fine_amount, description, police_evidence):
81
  conn = sqlite3.connect(DB_FILE)
82
  c = conn.cursor()
 
 
 
 
 
 
 
 
83
  c.execute("""
84
- INSERT INTO cases (citizen_name, fine_amount, description, police_evidence, status, created_at)
85
- VALUES (?, ?, ?, ?, ?, ?)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  """, (
87
- citizen_name, fine_amount, description,
88
- police_evidence if police_evidence else None,
89
- "Pending Payment",
90
- datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 
 
 
 
91
  ))
92
  conn.commit()
93
  case_id = c.lastrowid
94
  conn.close()
95
- return f"✅ Kes telah dicipta dengan ID: {case_id}" # Malay only
 
 
 
96
 
 
97
  def citizen_view_case(case_id):
 
 
 
 
98
  conn = sqlite3.connect(DB_FILE)
99
  c = conn.cursor()
100
- c.execute("SELECT * FROM cases WHERE id=?", (case_id,))
101
  row = c.fetchone()
102
  conn.close()
103
  if not row:
104
  return "❌ Case not found / Kes tidak dijumpai / 案件未找到"
105
- return pd.DataFrame([row], columns=[
106
- "ID", "Citizen", "Fine", "Description", "Police Evidence",
107
- "Citizen Appeal", "Citizen Evidence", "Status", "Created At"
108
- ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
- def citizen_pay_fine(case_id):
 
 
 
 
 
111
  conn = sqlite3.connect(DB_FILE)
112
  c = conn.cursor()
113
- c.execute("UPDATE cases SET status=? WHERE id=?", ("Fine Paid", case_id))
 
 
 
 
114
  conn.commit()
115
  conn.close()
116
- return f"💰 Fine for case {case_id} marked as paid. / Kes {case_id} ditandakan telah dibayar. / 案件 {case_id} 已缴清罚款。"
 
117
 
118
- def citizen_appeal(case_id, appeal_text, evidence_file):
 
 
 
 
 
 
119
  conn = sqlite3.connect(DB_FILE)
120
  c = conn.cursor()
121
- c.execute("""
122
- UPDATE cases
123
- SET citizen_appeal=?, citizen_evidence=?, status=?
124
- WHERE id=?
125
- """, (
126
- appeal_text, evidence_file if evidence_file else None,
127
- "Appealed", case_id
128
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  conn.commit()
130
  conn.close()
131
- return f"📤 Appeal submitted for case {case_id} / Rayuan dihantar untuk kes {case_id} / 案件 {case_id} 已提交申诉"
 
 
132
 
133
- def magistrate_dashboard():
 
134
  conn = sqlite3.connect(DB_FILE)
135
  c = conn.cursor()
136
- c.execute("SELECT * FROM cases WHERE status='Appealed'")
137
  rows = c.fetchall()
138
  conn.close()
139
  if not rows:
140
- return "No appealed cases. / Tiada kes rayuan. / 没有申诉案件"
141
- return pd.DataFrame(rows, columns=[
142
- "ID", "Citizen", "Fine", "Description", "Police Evidence",
143
- "Citizen Appeal", "Citizen Evidence", "Status", "Created At"
144
- ])
145
-
146
- def magistrate_decide(case_id, decision):
147
- new_status = {
148
- "Approve Appeal": "Appeal Approved",
149
- "Reject Appeal": "Appeal Rejected",
150
- "Escalate Hearing": "Hearing Scheduled"
151
- }[decision]
152
  conn = sqlite3.connect(DB_FILE)
153
  c = conn.cursor()
154
- c.execute("UPDATE cases SET status=? WHERE id=?", (new_status, case_id))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  conn.commit()
156
  conn.close()
157
- return f"⚖️ Case {case_id} updated: {new_status}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
  # ------------------ Gradio UI ------------------
160
- with gr.Blocks() as demo:
161
- gr.Markdown("# 🚦 SwiftCase.ai – Smart Traffic Justice Platform")
162
 
163
- # Citizen Portal (All languages shown at once)
 
164
  with gr.Tab("Citizen Portal / Portal Warga / 市民入口"):
165
- case_id_input = gr.Number(
166
- label="Enter Case ID / Masukkan ID Kes / 输入案件编号", precision=0
167
- )
168
  view_btn = gr.Button("View Case / Lihat Kes / 查看案件")
169
  case_output = gr.Dataframe()
170
  view_btn.click(citizen_view_case, inputs=case_id_input, outputs=case_output)
171
 
172
  with gr.Row():
173
- pay_btn = gr.Button("Pay Fine / Bayar Saman / 缴纳罚款")
174
- appeal_text = gr.Textbox(label="Appeal Text / Teks Rayuan / 申诉内容")
175
- appeal_file = gr.File(
176
- label="Upload Appeal Evidence / Muat Naik Bukti Rayuan / 上传申诉证据",
177
- type="filepath"
178
- )
179
- appeal_btn = gr.Button("Submit Appeal / Hantar Rayuan / 提交申诉")
180
-
181
- pay_result = gr.Textbox(
182
- label="Result / Keputusan / 结果"
183
- )
184
- appeal_result = gr.Textbox(
185
- label="Appeal Result / Keputusan Rayuan / 申诉结果"
186
- )
187
-
188
- pay_btn.click(citizen_pay_fine, inputs=case_id_input, outputs=pay_result)
189
- appeal_btn.click(
190
- citizen_appeal, inputs=[case_id_input, appeal_text, appeal_file],
191
- outputs=appeal_result
192
- )
193
-
194
- # Police Portal (Bahasa Melayu only)
195
- with gr.Tab("Portal Polis Trafik"):
196
- gr.Markdown("## Portal Polis Trafik (Bahasa Melayu Sahaja)")
197
-
198
- citizen_name = gr.Textbox(label="Nama Warga")
199
- fine_amount = gr.Number(label="Jumlah Saman")
200
- description = gr.Textbox(label="Huraian")
201
- police_file = gr.File(label="Muat Naik Bukti", type="filepath")
202
- create_case_btn = gr.Button("Cipta Kes")
203
- police_result = gr.Textbox(label="Keputusan")
204
- create_case_btn.click(
205
- police_create_case,
206
- inputs=[citizen_name, fine_amount, description, police_file],
207
- outputs=police_result
208
- )
209
-
210
- # Magistrate Portal (English + Malay via dropdown)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  with gr.Tab("Magistrate Portal / Portal Majistret"):
212
- lang_mag = gr.Dropdown(
213
- ["English", "Bahasa Melayu"], value="English",
214
- label="Language / Bahasa"
215
- )
216
-
217
- dashboard_btn = gr.Button("Refresh Appeals Dashboard")
218
- dashboard_output = gr.Dataframe()
219
- dashboard_btn.click(magistrate_dashboard, outputs=dashboard_output)
220
-
221
- case_id_mag = gr.Number(label="Case ID", precision=0)
222
- decision = gr.Dropdown(
223
- ["Approve Appeal", "Reject Appeal", "Escalate Hearing"],
224
- label="Decision"
225
- )
226
- decide_btn = gr.Button("Submit Decision")
227
- decision_result = gr.Textbox(label="Decision Result")
228
- decide_btn.click(
229
- magistrate_decide, inputs=[case_id_mag, decision], outputs=decision_result
230
- )
231
-
232
- def update_mag_labels(lang):
233
- if lang == "English":
234
- return (
235
- "Refresh Appeals Dashboard",
236
- "Case ID",
237
- "Decision",
238
- "Submit Decision"
239
- )
240
- else:
241
- return (
242
- "Segarkan Papan Pemuka Rayuan",
243
- "ID Kes",
244
- "Keputusan",
245
- "Hantar Keputusan"
246
- )
247
-
248
- lang_mag.change(
249
- update_mag_labels, inputs=lang_mag,
250
- outputs=[dashboard_btn, case_id_mag, decision, decide_btn]
251
- )
252
 
253
  demo.launch()
254
 
@@ -258,3 +588,4 @@ demo.launch()
258
 
259
 
260
 
 
 
1
+ # app.py
2
  import gradio as gr
3
  import sqlite3
4
+ import json
5
  import pandas as pd
6
  from datetime import datetime
7
+ import os
8
+ import random
9
 
10
  DB_FILE = "cases.db"
11
+ STANDARD_COMPOUND_CENTS = 30000 # RM300.00 stored as cents
12
 
13
+ # ------------------ DB Setup & Schema ------------------
14
  def init_db():
15
  conn = sqlite3.connect(DB_FILE)
16
  c = conn.cursor()
17
+ # summons_cases
18
  c.execute("""
19
+ CREATE TABLE IF NOT EXISTS summons_cases (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ summons_no TEXT UNIQUE,
22
+ issuing_agency TEXT,
23
+ rta_section TEXT,
24
+ offender_name TEXT,
25
+ offender_ic TEXT,
26
+ offender_address TEXT,
27
+ vehicle_plate TEXT,
28
+ vehicle_type TEXT,
29
+ offence_code TEXT,
30
+ offence_desc_en TEXT,
31
+ offence_desc_ms TEXT,
32
+ offence_desc_zh TEXT,
33
+ offence_datetime TEXT,
34
+ offence_location TEXT,
35
+ district TEXT,
36
+ evidence_media TEXT,
37
+ base_fine_cents INTEGER,
38
+ payable_fine_cents INTEGER,
39
+ due_date TEXT,
40
+ status TEXT,
41
+ created_at TEXT,
42
+ updated_at TEXT
43
+ )
44
+ """)
45
+ # appeals
46
+ c.execute("""
47
+ CREATE TABLE IF NOT EXISTS appeals (
48
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
49
+ case_id INTEGER,
50
+ appeal_text_en TEXT,
51
+ appeal_text_ms TEXT,
52
+ appeal_text_zh TEXT,
53
+ attachments TEXT,
54
+ submitted_at TEXT
55
+ )
56
+ """)
57
+ # judge_reviews
58
+ c.execute("""
59
+ CREATE TABLE IF NOT EXISTS judge_reviews (
60
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
61
+ case_id INTEGER,
62
+ reviewer_type TEXT,
63
+ reviewer_id TEXT,
64
+ decision TEXT,
65
+ reduced_fine_cents INTEGER,
66
+ rationale TEXT,
67
+ decided_at TEXT,
68
+ authority_consent TEXT
69
+ )
70
+ """)
71
+ # pdrm_reviews
72
+ c.execute("""
73
+ CREATE TABLE IF NOT EXISTS pdrm_reviews (
74
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
75
+ case_id INTEGER,
76
+ decision TEXT,
77
+ reduced_fine_cents INTEGER,
78
+ alasan TEXT,
79
+ reviewer_id TEXT,
80
+ decided_at TEXT
81
+ )
82
+ """)
83
+ # payments
84
+ c.execute("""
85
+ CREATE TABLE IF NOT EXISTS payments (
86
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
87
+ case_id INTEGER,
88
+ channel TEXT,
89
+ amount_cents INTEGER,
90
+ currency TEXT,
91
+ provider_ref TEXT,
92
+ status TEXT,
93
+ paid_at TEXT
94
+ )
95
+ """)
96
+ # receipts
97
+ c.execute("""
98
+ CREATE TABLE IF NOT EXISTS receipts (
99
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
100
+ payment_id INTEGER,
101
+ case_id INTEGER,
102
+ receipt_no TEXT,
103
+ issued_at TEXT
104
+ )
105
+ """)
106
+ # audit_logs
107
+ c.execute("""
108
+ CREATE TABLE IF NOT EXISTS audit_logs (
109
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
110
+ case_id INTEGER,
111
+ actor TEXT,
112
+ action TEXT,
113
+ details TEXT,
114
+ created_at TEXT
115
+ )
116
  """)
117
  conn.commit()
118
  conn.close()
119
 
120
+ # ------------------ Seeding multilingual demo cases ------------------
121
  def seed_mock_data():
122
  conn = sqlite3.connect(DB_FILE)
123
  c = conn.cursor()
124
+ c.execute("SELECT COUNT(*) FROM summons_cases")
125
  count = c.fetchone()[0]
126
+ if count == 0:
127
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
128
  cases = [
129
+ # Ali (ISSUED)
130
  (
131
+ "K250901234", "PDRM", "APJ 1987",
132
+ "Ali", "900101-01-0001", "12, Jalan Kenanga 3, Taman Seri, 75450 Melaka",
133
+ "VDD8821", "Pick-up", "LORONG-001",
134
+ "Not obeying correct lane usage",
135
+ "Tidak mematuhi lorong yang betul",
136
+ "未按规定车道行驶",
137
+ "2025-07-22T05:33:00+08:00", "Jalan Lembongan", "Melaka Tengah",
138
+ json.dumps(["uploads/kes1-plate.jpg"]),
139
+ 300, 300, "2025-09-22", "ISSUED", now, now
140
  ),
141
+ # Ah Kow (APPEAL_PENDING)
142
  (
143
+ "K250901235", "PDRM", "APJ 1987",
144
+ "Ah Kow", "881212-02-2222", "45, Taman Melur, 75400 Melaka",
145
+ "MCD3210", "Car", "REDLIGHT-01",
146
+ "Running a red light",
147
+ "Melanggar lampu merah",
148
+ "闯红灯",
149
+ "2025-08-01T18:12:00+08:00", "Persiaran Bukit Baru", "Melaka Tengah",
150
+ json.dumps(["uploads/kes2-camera.jpg"]),
151
+ 300, 300, "2025-10-01", "APPEAL_PENDING", now, now
152
  ),
153
+ # Muthu (APPEAL_APPROVED_REDUCED)
154
  (
155
+ "K250901236", "PDRM", "APJ 1987",
156
+ "Muthu", "850505-03-3333", "25, Jalan Seri Indah, 75410 Melaka",
157
+ "VDL5500", "Motorcycle", "PARK-001",
158
+ "Illegal parking",
159
+ "Letak kereta haram",
160
+ "非法停车",
161
+ "2025-07-30T09:45:00+08:00", "Jalan Merdeka", "Melaka Tengah",
162
+ json.dumps(["uploads/kes3-park.jpg"]),
163
+ 300, 100, "2025-09-30", "APPEAL_APPROVED_REDUCED", now, now
164
  ),
165
  ]
166
  c.executemany("""
167
+ INSERT INTO summons_cases (
168
+ summons_no, issuing_agency, rta_section,
169
+ offender_name, offender_ic, offender_address,
170
+ vehicle_plate, vehicle_type, offence_code,
171
+ offence_desc_en, offence_desc_ms, offence_desc_zh,
172
+ offence_datetime, offence_location, district,
173
+ evidence_media, base_fine_cents, payable_fine_cents,
174
+ due_date, status, created_at, updated_at
175
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
176
  """, cases)
177
  conn.commit()
178
+ # Add an appeal row for case 2 (Ah Kow)
179
+ c.execute("SELECT id FROM summons_cases WHERE summons_no=?", ("K250901235",))
180
+ row = c.fetchone()
181
+ if row:
182
+ case2_id = row[0]
183
+ c.execute("""
184
+ INSERT INTO appeals (case_id, appeal_text_en, appeal_text_ms, appeal_text_zh, attachments, submitted_at)
185
+ VALUES (?, ?, ?, ?, ?, ?)
186
+ """, (
187
+ case2_id,
188
+ "I crossed on yellow, not red",
189
+ "Saya lintas pada lampu kuning, bukan merah",
190
+ "我是在黄灯时通过,不是红灯",
191
+ json.dumps(["uploads/appeal2-dashcam.mp4"]),
192
+ now
193
+ ))
194
+ conn.commit()
195
+ # audit log for appeal submitted
196
+ write_audit(case2_id, "citizen", "APPEAL_SUBMITTED", "Appeal submitted by citizen (seed).")
197
  conn.close()
198
 
199
+ # ------------------ Utility helpers ------------------
200
+ def now_ts():
201
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
202
 
203
+ def write_audit(case_id, actor, action, details):
204
+ # For PDRM actions, ensure details are in Bahasa Melayu when actor==police or action starts with PDRM_
205
  conn = sqlite3.connect(DB_FILE)
206
  c = conn.cursor()
207
+ if actor == "police" or action.startswith("PDRM_") or action.startswith("PDRMDECISION") or action.startswith("PAYMENT_PDRM"):
208
+ # Expect details in BM already from callers; but if not, wrap generically
209
+ details_text = details
210
+ # If it's a dict, dump directly
211
+ if isinstance(details, dict):
212
+ details_text = json.dumps(details, ensure_ascii=False)
213
+ else:
214
+ details_text = details if isinstance(details, str) else json.dumps(details, ensure_ascii=False)
215
  c.execute("""
216
+ INSERT INTO audit_logs (case_id, actor, action, details, created_at)
217
+ VALUES (?, ?, ?, ?, ?)
218
+ """, (case_id, actor, action, details_text, now_ts()))
219
+ conn.commit()
220
+ conn.close()
221
+
222
+ def generate_receipt_no():
223
+ return f"RCP{datetime.now().strftime('%Y%m%d')}-{random.randint(1000,9999)}"
224
+
225
+ # ------------------ Core Operations ------------------
226
+
227
+ # Police: create case (BM-only response)
228
+ def police_create_case(citizen_name, citizen_ic, citizen_address, vehicle_plate, vehicle_type,
229
+ offence_code, offence_desc_ms, offence_desc_en, offence_desc_zh,
230
+ offence_datetime, offence_location, district, evidence_file):
231
+ conn = sqlite3.connect(DB_FILE)
232
+ c = conn.cursor()
233
+ summons_no = f"K{datetime.now().strftime('%y%m%d')}{random.randint(1000,9999)}"
234
+ created = now_ts()
235
+ base_cents = STANDARD_COMPOUND_CENTS
236
+ payable_cents = base_cents
237
+ c.execute("""
238
+ INSERT INTO summons_cases (
239
+ summons_no, issuing_agency, rta_section,
240
+ offender_name, offender_ic, offender_address,
241
+ vehicle_plate, vehicle_type, offence_code,
242
+ offence_desc_en, offence_desc_ms, offence_desc_zh,
243
+ offence_datetime, offence_location, district,
244
+ evidence_media, base_fine_cents, payable_fine_cents,
245
+ due_date, status, created_at, updated_at
246
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
247
  """, (
248
+ summons_no, "PDRM", "APJ 1987",
249
+ citizen_name, citizen_ic, citizen_address,
250
+ vehicle_plate, vehicle_type, offence_code,
251
+ offence_desc_en, offence_desc_ms, offence_desc_zh,
252
+ offence_datetime, offence_location, district,
253
+ json.dumps([evidence_file]) if evidence_file else json.dumps([]),
254
+ base_cents, payable_cents, (datetime.now().replace(year=datetime.now().year+1)).strftime("%Y-%m-%d"),
255
+ "ISSUED", created, created
256
  ))
257
  conn.commit()
258
  case_id = c.lastrowid
259
  conn.close()
260
+ # Audit log in BM
261
+ write_audit(case_id, "police", "CASE_CREATED",
262
+ f"Kes saman telah dikeluarkan oleh PDRM, nombor saman {summons_no}, pegawai membuat rekod.")
263
+ return f"✅ Kes dicipta (ID dalaman: {case_id}) — Nombor Saman: {summons_no}"
264
 
265
+ # Citizen: view case (multilingual output)
266
  def citizen_view_case(case_id):
267
+ try:
268
+ cid = int(case_id)
269
+ except:
270
+ return "❌ Invalid Case ID / ID Kes tidak sah / 无效的案件编号"
271
  conn = sqlite3.connect(DB_FILE)
272
  c = conn.cursor()
273
+ c.execute("SELECT * FROM summons_cases WHERE id=?", (cid,))
274
  row = c.fetchone()
275
  conn.close()
276
  if not row:
277
  return "❌ Case not found / Kes tidak dijumpai / 案件未找到"
278
+ cols = [d[0] for d in c.description] if False else [
279
+ "id","summons_no","issuing_agency","rta_section","offender_name","offender_ic","offender_address",
280
+ "vehicle_plate","vehicle_type","offence_code","offence_desc_en","offence_desc_ms","offence_desc_zh",
281
+ "offence_datetime","offence_location","district","evidence_media","base_fine_cents","payable_fine_cents",
282
+ "due_date","status","created_at","updated_at"
283
+ ]
284
+ # Build display dict with trilingual description
285
+ display = {
286
+ "ID": row[0],
287
+ "Summons No": row[1],
288
+ "Offender": row[4],
289
+ "IC": row[5],
290
+ "Address": row[6],
291
+ "Vehicle Plate": row[7],
292
+ "Vehicle Type": row[8],
293
+ "Offence (EN / BM / ZH)": f"{row[10]} / {row[11]} / {row[12]}",
294
+ "DateTime": row[13],
295
+ "Location": row[14],
296
+ "District": row[15],
297
+ "Evidence (files)": json.loads(row[16]) if row[16] else [],
298
+ "Base Fine (RM)": f"{row[17]/100:.2f}",
299
+ "Payable (RM)": f"{row[18]/100:.2f}",
300
+ "Due Date": row[19],
301
+ "Status": row[20],
302
+ "Created At": row[21]
303
+ }
304
+ return pd.DataFrame([display])
305
 
306
+ # Citizen: submit appeal to magistrate (judicial appeal)
307
+ def citizen_submit_judicial_appeal(case_id, appeal_text_en, appeal_text_ms, appeal_text_zh, appeal_files):
308
+ try:
309
+ cid = int(case_id)
310
+ except:
311
+ return "❌ Invalid Case ID / ID Kes tidak sah / 无效的案件编号"
312
  conn = sqlite3.connect(DB_FILE)
313
  c = conn.cursor()
314
+ c.execute("INSERT INTO appeals (case_id, appeal_text_en, appeal_text_ms, appeal_text_zh, attachments, submitted_at) VALUES (?, ?, ?, ?, ?, ?)",
315
+ (cid, appeal_text_en, appeal_text_ms, appeal_text_zh,
316
+ json.dumps([appeal_files]) if appeal_files else json.dumps([]),
317
+ now_ts()))
318
+ c.execute("UPDATE summons_cases SET status=?, updated_at=? WHERE id=?", ("APPEAL_PENDING", now_ts(), cid))
319
  conn.commit()
320
  conn.close()
321
+ write_audit(cid, "citizen", "APPEAL_SUBMITTED", "Rayuan ke mahkamah telah dihantar oleh pengguna.")
322
+ return f"📤 Rayuan ke mahkamah telah dihantar untuk kes {case_id} / Appeal submitted."
323
 
324
+ # Citizen: submit appeal to PDRM (compound appeal) before paying
325
+ def citizen_submit_pdrm_appeal(case_id, appeal_text_ms, appeal_files):
326
+ try:
327
+ cid = int(case_id)
328
+ except:
329
+ return "❌ Invalid Case ID / ID Kes tidak sah / 无效的案件编号"
330
+ # For PDRM appeals we store in appeals table as well (tri-lingual fields optional)
331
  conn = sqlite3.connect(DB_FILE)
332
  c = conn.cursor()
333
+ c.execute("INSERT INTO appeals (case_id, appeal_text_en, appeal_text_ms, appeal_text_zh, attachments, submitted_at) VALUES (?, ?, ?, ?, ?, ?)",
334
+ (cid, "", appeal_text_ms, "", json.dumps([appeal_files]) if appeal_files else json.dumps([]), now_ts()))
335
+ # mark status to PDRM_APPEAL_PENDING
336
+ c.execute("UPDATE summons_cases SET status=?, updated_at=? WHERE id=?", ("PDRM_APPEAL_PENDING", now_ts(), cid))
337
+ conn.commit()
338
+ conn.close()
339
+ write_audit(cid, "citizen", "APPEAL_PDRM_SUBMITTED", "Rayuan kompaun telah dihantar oleh pengguna ke PDRM.")
340
+ return f"📤 Rayuan kompaun (PDRM) telah dihantar untuk kes {case_id}."
341
+
342
+ # PDRM officer: review PDRM appeals in police portal (BM only)
343
+ def police_list_pdrm_appeals():
344
+ conn = sqlite3.connect(DB_FILE)
345
+ c = conn.cursor()
346
+ c.execute("SELECT id, summons_no, offender_name, payable_fine_cents, status FROM summons_cases WHERE status IN ('PDRM_APPEAL_PENDING','ISSUED')")
347
+ rows = c.fetchall()
348
+ conn.close()
349
+ if not rows:
350
+ return "Tiada rayuan PDRM yang sedang menunggu / No PDRM appeals pending"
351
+ df = pd.DataFrame(rows, columns=["CaseID","SummonsNo","Offender","PayableCents","Status"])
352
+ df["PayableRM"] = df["PayableCents"].apply(lambda x: f"RM{x/100:.2f}")
353
+ return df
354
+
355
+ def police_decide_pdrm(case_id, decision_choice, reduced_amount_rm, officer_id, alasan_ms):
356
+ # decision_choice: "tolak" or "dikurangkan"
357
+ try:
358
+ cid = int(case_id)
359
+ except:
360
+ return "❌ ID Kes tidak sah"
361
+ conn = sqlite3.connect(DB_FILE)
362
+ c = conn.cursor()
363
+ if decision_choice == "tolak":
364
+ # keep payable as base; set status to ISSUED (or PDRM_REJECTED)
365
+ c.execute("SELECT base_fine_cents, summons_no FROM summons_cases WHERE id=?", (cid,))
366
+ row = c.fetchone()
367
+ if not row:
368
+ conn.close()
369
+ return "Kes tidak dijumpai"
370
+ base = row[0]
371
+ c.execute("UPDATE summons_cases SET payable_fine_cents=?, status=?, updated_at=? WHERE id=?", (base, "ISSUED", now_ts(), cid))
372
+ c.execute("INSERT INTO pdrm_reviews (case_id, decision, reduced_fine_cents, alasan, reviewer_id, decided_at) VALUES (?, ?, ?, ?, ?, ?)",
373
+ (cid, "ditolak", None, alasan_ms, officer_id, now_ts()))
374
+ conn.commit()
375
+ conn.close()
376
+ write_audit(cid, "police", "PDRM_DECISION",
377
+ f"Rayuan kompaun ditolak oleh pegawai {officer_id}.")
378
+ return f"🛑 Rayuan ditolak. Jumlah kompaun kekal RM{base/100:.2f}"
379
+ else:
380
+ # dikurangkan
381
+ reduced_cents = int(float(reduced_amount_rm) * 100)
382
+ c.execute("UPDATE summons_cases SET payable_fine_cents=?, status=?, updated_at=? WHERE id=?", (reduced_cents, "ISSUED", now_ts(), cid))
383
+ c.execute("INSERT INTO pdrm_reviews (case_id, decision, reduced_fine_cents, alasan, reviewer_id, decided_at) VALUES (?, ?, ?, ?, ?, ?)",
384
+ (cid, "dikurangkan", reduced_cents, alasan_ms, officer_id, now_ts()))
385
+ conn.commit()
386
+ conn.close()
387
+ write_audit(cid, "police", "PDRM_DECISION",
388
+ f"Rayuan kompaun diluluskan; jumlah dikurangkan kepada RM{reduced_cents/100:.2f} oleh pegawai {officer_id}.")
389
+ return f"✅ Rayuan diluluskan. Jumlah kompaun kini RM{reduced_cents/100:.2f}"
390
+
391
+ # Citizen: pay now (to PDRM) - simulated payment
392
+ def citizen_pay_now(case_id, pay_channel="PDRM_GATEWAY"):
393
+ try:
394
+ cid = int(case_id)
395
+ except:
396
+ return "❌ Invalid Case ID / ID Kes tidak sah"
397
+ conn = sqlite3.connect(DB_FILE)
398
+ c = conn.cursor()
399
+ c.execute("SELECT payable_fine_cents, summons_no FROM summons_cases WHERE id=?", (cid,))
400
+ r = c.fetchone()
401
+ if not r:
402
+ conn.close()
403
+ return "❌ Case not found / Kes tidak dijumpai"
404
+ amount = r[0]
405
+ if amount <= 0:
406
+ conn.close()
407
+ return "Tiada bayaran diperlukan / No payment required"
408
+ # Simulate payment success
409
+ provider_ref = f"TX{datetime.now().strftime('%Y%m%d%H%M%S')}{random.randint(100,999)}"
410
+ paid_at = now_ts()
411
+ c.execute("INSERT INTO payments (case_id, channel, amount_cents, currency, provider_ref, status, paid_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
412
+ (cid, pay_channel, amount, "MYR", provider_ref, "success", paid_at))
413
+ payment_id = c.lastrowid
414
+ # Create receipt if channel is PDRM_GATEWAY
415
+ receipt_no = generate_receipt_no()
416
+ c.execute("INSERT INTO receipts (payment_id, case_id, receipt_no, issued_at) VALUES (?, ?, ?, ?)",
417
+ (payment_id, cid, receipt_no, now_ts()))
418
+ # Update case status to PAID -> CLOSED
419
+ c.execute("UPDATE summons_cases SET status=?, updated_at=? WHERE id=?", ("PAID", now_ts(), cid))
420
  conn.commit()
421
  conn.close()
422
+ # Audit logs in BM for payments to PDRM
423
+ write_audit(cid, "police", "PAYMENT_PDRM", f"Pembayaran berjaya dibuat kepada PDRM, jumlah RM{amount/100:.2f}, resit nombor {receipt_no}.")
424
+ return f"💳 Pembayaran berjaya (RM{amount/100:.2f}). Resit: {receipt_no}"
425
 
426
+ # Magistrate: list appealed cases
427
+ def magistrate_list_appeals():
428
  conn = sqlite3.connect(DB_FILE)
429
  c = conn.cursor()
430
+ c.execute("SELECT id, summons_no, offender_name, payable_fine_cents, status FROM summons_cases WHERE status IN ('APPEAL_PENDING','UNDER_REVIEW')")
431
  rows = c.fetchall()
432
  conn.close()
433
  if not rows:
434
+ return "No appealed cases."
435
+ df = pd.DataFrame(rows, columns=["CaseID","SummonsNo","Offender","PayableCents","Status"])
436
+ df["PayableRM"] = df["PayableCents"].apply(lambda x: f"RM{x/100:.2f}")
437
+ return df
438
+
439
+ # Magistrate: decide (human or AI)
440
+ def magistrate_decide(case_id, reviewer_type, reviewer_id, decision_choice, reduced_amount_rm, rationale, authority_consent):
441
+ try:
442
+ cid = int(case_id)
443
+ except:
444
+ return " Invalid Case ID"
 
445
  conn = sqlite3.connect(DB_FILE)
446
  c = conn.cursor()
447
+ # Map decision_choice to DB-friendly
448
+ if decision_choice == "Approve No Fine":
449
+ new_status = "APPEAL_APPROVED_NO_FINE"
450
+ reduced_cents = 0
451
+ elif decision_choice == "Approve Reduced Fine":
452
+ reduced_cents = int(float(reduced_amount_rm) * 100)
453
+ new_status = "APPEAL_APPROVED_REDUCED"
454
+ else:
455
+ # Reject
456
+ c.execute("SELECT base_fine_cents FROM summons_cases WHERE id=?", (cid,))
457
+ r = c.fetchone()
458
+ if not r:
459
+ conn.close()
460
+ return "Kes tidak dijumpai"
461
+ reduced_cents = r[0]
462
+ new_status = "APPEAL_REJECTED"
463
+ # Insert judge review
464
+ c.execute("""INSERT INTO judge_reviews (case_id, reviewer_type, reviewer_id, decision, reduced_fine_cents, rationale, decided_at, authority_consent)
465
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
466
+ (cid, reviewer_type, reviewer_id, decision_choice.lower().replace(" ", "_"), reduced_cents, rationale, now_ts(), authority_consent))
467
+ # Update case payable and status
468
+ c.execute("UPDATE summons_cases SET payable_fine_cents=?, status=?, updated_at=? WHERE id=?", (reduced_cents, new_status, now_ts(), cid))
469
  conn.commit()
470
  conn.close()
471
+ write_audit(cid, "judge", "DECISION_MADE", f"Keputusan oleh {reviewer_type} ({reviewer_id}): {decision_choice}. Rationale: {rationale}")
472
+ return f"⚖️ Keputusan dicatat untuk kes {case_id}: {decision_choice}. Jumlah kini RM{reduced_cents/100:.2f}"
473
+
474
+ # Utility: show audit logs (BM for police actions)
475
+ def show_audit_logs(case_id):
476
+ try:
477
+ cid = int(case_id)
478
+ except:
479
+ return "❌ Invalid Case ID"
480
+ conn = sqlite3.connect(DB_FILE)
481
+ df = pd.read_sql_query("SELECT created_at, actor, action, details FROM audit_logs WHERE case_id=? ORDER BY id ASC", conn, params=(cid,))
482
+ conn.close()
483
+ if df.empty:
484
+ return "No logs found."
485
+ return df
486
 
487
  # ------------------ Gradio UI ------------------
488
+ init_db()
489
+ seed_mock_data()
490
 
491
+ with gr.Blocks() as demo:
492
+ gr.Markdown("# 🚦 SwiftCase.ai – Real-world Traffic Offence MVP")
493
  with gr.Tab("Citizen Portal / Portal Warga / 市民入口"):
494
+ gr.Markdown("**Semua label & arahan dipaparkan dalam 3 bahasa untuk kemudahan pengguna.**")
495
+ case_id_input = gr.Number(label="Enter Case ID / Masukkan ID Kes / 输入案件编号", precision=0)
 
496
  view_btn = gr.Button("View Case / Lihat Kes / 查看案件")
497
  case_output = gr.Dataframe()
498
  view_btn.click(citizen_view_case, inputs=case_id_input, outputs=case_output)
499
 
500
  with gr.Row():
501
+ pay_now_btn = gr.Button("Pay Compound Now (Bayar Kompaun) / 缴纳罚款")
502
+ appeal_pdrm_btn = gr.Button("Appeal to PDRM (Rayuan Kompaun) / 向PDRM申诉")
503
+ appeal_mag_btn = gr.Button("Appeal to Magistrate (Rayuan ke Mahkamah) / 向法官申诉")
504
+ # PDRM appeal inputs
505
+ appeal_pdrm_text = gr.Textbox(label="Alasan Rayuan (BM) / Appeal Reason (BM) / 申诉理由(马)")
506
+ appeal_pdrm_file = gr.File(label="Upload Evidence (filepath)", type="filepath")
507
+ # Judicial appeal inputs
508
+ appeal_jud_en = gr.Textbox(label="Appeal Text (EN) / 申诉(英)")
509
+ appeal_jud_ms = gr.Textbox(label="Appeal Text (BM) / 申诉(马)")
510
+ appeal_jud_zh = gr.Textbox(label="Appeal Text (ZH) / 申诉(中)")
511
+ appeal_jud_file = gr.File(label="Upload Evidence for Judicial Appeal", type="filepath")
512
+
513
+ pay_result = gr.Textbox(label="Result / Keputusan / 结果")
514
+ pdrm_appeal_result = gr.Textbox(label="PDRM Appeal Result / Keputusan Rayuan PDRM")
515
+ jud_appeal_result = gr.Textbox(label="Judicial Appeal Result / Keputusan Rayuan Mahkamah")
516
+
517
+ pay_now_btn.click(citizen_pay_now, inputs=case_id_input, outputs=pay_result)
518
+ appeal_pdrm_btn.click(citizen_submit_pdrm_appeal, inputs=[case_id_input, appeal_pdrm_text, appeal_pdrm_file], outputs=pdrm_appeal_result)
519
+ appeal_mag_btn.click(citizen_submit_judicial_appeal, inputs=[case_id_input, appeal_jud_en, appeal_jud_ms, appeal_jud_zh, appeal_jud_file], outputs=jud_appeal_result)
520
+
521
+ with gr.Accordion("View Audit Logs / Lihat Log / 查看日志", open=False):
522
+ audit_out = gr.Dataframe()
523
+ show_audit_btn = gr.Button("Show Logs / Tunjuk Log / 显示日志")
524
+ show_audit_btn.click(show_audit_logs, inputs=case_id_input, outputs=audit_out)
525
+
526
+ with gr.Tab("Portal Polis Trafik (Bahasa Melayu sahaja)"):
527
+ gr.Markdown("**Portal untuk PDRM: muat naik saman, semak rayuan kompaun, buat keputusan. Semua log PDRM dalam Bahasa Melayu.**")
528
+ # Create case
529
+ pol_name = gr.Textbox(label="Nama Warga (Nama penuh)")
530
+ pol_ic = gr.Textbox(label="No KP")
531
+ pol_addr = gr.Textbox(label="Alamat")
532
+ pol_plate = gr.Textbox(label="Nombor Plat")
533
+ pol_vtype = gr.Textbox(label="Jenis Kenderaan")
534
+ pol_off_code = gr.Textbox(label="Kod Kesalahan")
535
+ pol_off_desc_ms = gr.Textbox(label="Huraian Kesalahan (BM)")
536
+ pol_off_desc_en = gr.Textbox(label="Offence Description (EN) - optional")
537
+ pol_off_desc_zh = gr.Textbox(label="Offence Description (ZH) - optional")
538
+ pol_datetime = gr.Textbox(label="Tarikh/Masa (ISO) - e.g. 2025-07-22T05:33:00+08:00")
539
+ pol_location = gr.Textbox(label="Lokasi")
540
+ pol_district = gr.Textbox(label="Daerah")
541
+ pol_evidence = gr.File(label="Muat Naik Bukti (filepath)", type="filepath")
542
+ create_pol_btn = gr.Button("Cipta Kes (Cipta Saman)")
543
+ pol_create_result = gr.Textbox(label="Keputusan")
544
+ create_pol_btn.click(police_create_case, inputs=[pol_name, pol_ic, pol_addr, pol_plate, pol_vtype, pol_off_code,
545
+ pol_off_desc_ms, pol_off_desc_en, pol_off_desc_zh,
546
+ pol_datetime, pol_location, pol_district, pol_evidence],
547
+ outputs=pol_create_result)
548
+
549
+ gr.Markdown("### Senarai rayuan kompaun / PDRM appeals waiting")
550
+ list_pdrm_btn = gr.Button("Semak Rayuan PDRM")
551
+ pdrm_list_out = gr.Dataframe()
552
+ list_pdrm_btn.click(police_list_pdrm_appeals, outputs=pdrm_list_out)
553
+
554
+ # Police decision inputs
555
+ pol_dec_caseid = gr.Number(label="Case ID untuk keputusan", precision=0)
556
+ pol_dec_choice = gr.Radio(["tolak","dikurangkan"], label="Keputusan (tolak / dikurangkan)")
557
+ pol_reduced_rm = gr.Number(label="Jumlah dikurangkan (RM) - jika dikurangkan", value=150)
558
+ pol_officer_id = gr.Textbox(label="ID Pegawai")
559
+ pol_alasan = gr.Textbox(label="Alasan (BM) untuk keputusan")
560
+ pol_decide_btn = gr.Button("Hantar Keputusan PDRM")
561
+ pol_decide_out = gr.Textbox(label="Keputusan PDRM")
562
+ pol_decide_btn.click(police_decide_pdrm, inputs=[pol_dec_caseid, pol_dec_choice, pol_reduced_rm, pol_officer_id, pol_alasan], outputs=pol_decide_out)
563
+
564
  with gr.Tab("Magistrate Portal / Portal Majistret"):
565
+ gr.Markdown("Magistrate / Judge portal: review judicial appeals.")
566
+ mag_list_btn = gr.Button("Refresh Appeals Dashboard")
567
+ mag_list_out = gr.Dataframe()
568
+ mag_list_btn.click(magistrate_list_appeals, outputs=mag_list_out)
569
+
570
+ mag_case_id = gr.Number(label="Case ID", precision=0)
571
+ mag_reviewer_type = gr.Radio(["human_judge", "ai_agent"], label="Reviewer Type")
572
+ mag_reviewer_id = gr.Textbox(label="Reviewer ID / Model")
573
+ mag_decision = gr.Radio(["Approve No Fine", "Approve Reduced Fine", "Reject"], label="Decision")
574
+ mag_reduced_rm = gr.Number(label="Reduced Amount (RM) if applicable", value=100)
575
+ mag_rationale = gr.Textbox(label="Rationale / Sebab")
576
+ mag_authority = gr.Textbox(label="Authority Consent (e.g., AGC-ID) if AI used")
577
+ mag_decide_btn = gr.Button("Record Decision")
578
+ mag_decide_out = gr.Textbox(label="Decision Result")
579
+ mag_decide_btn.click(magistrate_decide, inputs=[mag_case_id, mag_reviewer_type, mag_reviewer_id, mag_decision, mag_reduced_rm, mag_rationale, mag_authority], outputs=mag_decide_out)
580
+
581
+ gr.Markdown("-----\n**Notes:** This is an MVP prototype. Payments are simulated. All PDRM logs/actions are recorded in Bahasa Melayu for auditability.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
 
583
  demo.launch()
584
 
 
588
 
589
 
590
 
591
+