yusiff commited on
Commit
68178a7
·
verified ·
1 Parent(s): 61ff43d

Upload 4 files

Browse files
Files changed (4) hide show
  1. __pycache__/app.cpython-310.pyc +0 -0
  2. app.py +422 -0
  3. data.csv +6 -0
  4. requirements.txt +5 -0
__pycache__/app.cpython-310.pyc ADDED
Binary file (15.2 kB). View file
 
app.py ADDED
@@ -0,0 +1,422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ """
3
+ Run:
4
+ uvicorn app:app --reload --port 8000
5
+
6
+ This app reads data.csv (expected columns below), computes metrics and serves:
7
+ - / : HTML dashboard (JS + Chart.js)
8
+ - /api/tasks : JSON data for charts
9
+ - /report/pdf : PDF export
10
+ - /report/excel : Excel export
11
+
12
+ Expected CSV columns (case sensitive):
13
+ Task ID,Task Name,Client,Phase,Value,Start Time,End Time,Duration (mins)
14
+
15
+ If Duration (mins) is missing, the app will compute it using Start Time and End Time.
16
+ Date format is flexible (pandas.parse).
17
+ """
18
+
19
+ import io
20
+ import math
21
+ from fastapi import FastAPI, Query
22
+ from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
23
+ from reportlab.lib.pagesizes import A4
24
+ from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
25
+ from reportlab.lib import colors
26
+ from reportlab.lib.styles import getSampleStyleSheet
27
+ from openpyxl import Workbook
28
+ from datetime import datetime
29
+ import pandas as pd
30
+
31
+ app = FastAPI(title="Task Phase Dashboard (CSV)")
32
+
33
+ CSV_PATH = "data.csv" # file should be in the same folder as app.py
34
+
35
+ # Simple HTML template (English labels). Uses Chart.js from CDN.
36
+ HTML_TEMPLATE = """<!doctype html>
37
+ <html lang='en'>
38
+ <head>
39
+ <meta charset='utf-8'>
40
+ <title>Task Phase Dashboard</title>
41
+ <meta name="viewport" content="width=device-width, initial-scale=1">
42
+ <script src='https://cdn.tailwindcss.com'></script>
43
+ <script src='https://cdn.jsdelivr.net/npm/chart.js'></script>
44
+ </head>
45
+ <body class='bg-gray-50 text-left font-sans p-6'>
46
+ <div class='max-w-7xl mx-auto bg-white shadow-lg rounded-2xl p-6'>
47
+ <h2 class='text-2xl font-bold text-sky-600 mb-4 text-center'>📊 Task Phase Dashboard (CSV)</h2>
48
+
49
+ <div class='flex flex-wrap gap-4 items-center justify-between mb-4'>
50
+ <div class='flex gap-2 items-center'>
51
+ <label class='text-sm'>Client:</label>
52
+ <select id='filterClient' onchange='load()' class='px-2 py-1 border rounded'></select>
53
+ <label class='text-sm'>Phase:</label>
54
+ <select id='filterPhase' onchange='load()' class='px-2 py-1 border rounded'></select>
55
+ <button onclick='resetFilters()' class='ml-2 bg-gray-200 px-3 py-1 rounded'>Reset</button>
56
+ </div>
57
+
58
+ <div class='space-x-2'>
59
+ <a href='/report/pdf' target='_blank' class='bg-green-500 hover:bg-green-600 text-white rounded-lg px-4 py-2 transition'>📄 Export PDF</a>
60
+ <a href='/report/excel' target='_blank' class='bg-yellow-500 hover:bg-yellow-600 text-white rounded-lg px-4 py-2 transition'>📊 Export Excel</a>
61
+ </div>
62
+ </div>
63
+
64
+ <div class='grid grid-cols-1 md:grid-cols-3 gap-4 mb-6'>
65
+ <div class='p-4 bg-gray-100 rounded'>
66
+ <div class='text-sm text-gray-600'>Total tasks</div>
67
+ <div id='totalCount' class='text-2xl font-bold'>—</div>
68
+ </div>
69
+ <div class='p-4 bg-gray-100 rounded'>
70
+ <div class='text-sm text-gray-600'>Completed</div>
71
+ <div id='doneCount' class='text-2xl font-bold'>—</div>
72
+ </div>
73
+ <div class='p-4 bg-gray-100 rounded'>
74
+ <div class='text-sm text-gray-600'>Avg Duration (mins)</div>
75
+ <div id='avgDuration' class='text-2xl font-bold'>—</div>
76
+ </div>
77
+ </div>
78
+
79
+ <div class='grid grid-cols-1 lg:grid-cols-2 gap-6'>
80
+ <div class='p-4 bg-white rounded shadow'>
81
+ <h3 class='font-semibold mb-2'>Tasks per Client</h3>
82
+ <canvas id='clientsBar' height='250'></canvas>
83
+ </div>
84
+
85
+ <div class='p-4 bg-white rounded shadow'>
86
+ <h3 class='font-semibold mb-2'>Avg Duration per Client (mins)</h3>
87
+ <canvas id='avgBar' height='250'></canvas>
88
+ </div>
89
+
90
+ <div class='p-4 bg-white rounded shadow'>
91
+ <h3 class='font-semibold mb-2'>Value Distribution</h3>
92
+ <canvas id='valuePie' height='250'></canvas>
93
+ </div>
94
+
95
+ <div class='p-4 bg-white rounded shadow'>
96
+ <h3 class='font-semibold mb-2'>Tasks Over Time (by start date)</h3>
97
+ <canvas id='timeline' height='250'></canvas>
98
+ </div>
99
+
100
+ <div class='p-4 bg-white rounded shadow col-span-1 lg:col-span-2'>
101
+ <h3 class='font-semibold mb-2'>Task List</h3>
102
+ <div class='overflow-x-auto'>
103
+ <table class='min-w-full border text-sm'>
104
+ <thead class='bg-slate-100'>
105
+ <tr>
106
+ <th class='p-2'>Task Name</th>
107
+ <th class='p-2'>Client</th>
108
+ <th class='p-2'>Phase</th>
109
+ <th class='p-2'>Value</th>
110
+ <th class='p-2'>Duration (mins)</th>
111
+ <th class='p-2'>Start</th>
112
+ <th class='p-2'>End</th>
113
+ <th class='p-2'>Delayed</th>
114
+ </tr>
115
+ </thead>
116
+ <tbody id='taskTable'></tbody>
117
+ </table>
118
+ </div>
119
+ </div>
120
+ </div>
121
+
122
+ </div>
123
+
124
+ <script>
125
+ async function load(){
126
+ const client = document.getElementById('filterClient').value || '';
127
+ const phase = document.getElementById('filterPhase').value || '';
128
+ const params = new URLSearchParams();
129
+ if(client) params.set('client', client);
130
+ if(phase) params.set('phase', phase);
131
+
132
+ const res = await fetch('/api/tasks?' + params.toString());
133
+ const d = await res.json();
134
+
135
+ document.getElementById('totalCount').innerText = d.total_count;
136
+ document.getElementById('doneCount').innerText = d.done_count + ' (' + d.progress_percent.toFixed(1) + '%)';
137
+ document.getElementById('avgDuration').innerText = d.avg_duration.toFixed(1) + ' mins';
138
+
139
+ // populate filters if empty
140
+ const clientSel = document.getElementById('filterClient');
141
+ if(clientSel.options.length <= 1){
142
+ clientSel.innerHTML = '<option value=\"\">All</option>';
143
+ d.clients.forEach(c => {
144
+ const o = document.createElement('option'); o.value = c; o.text = c; clientSel.appendChild(o);
145
+ });
146
+ }
147
+ const phaseSel = document.getElementById('filterPhase');
148
+ if(phaseSel.options.length <= 1){
149
+ phaseSel.innerHTML = '<option value=\"\">All</option>';
150
+ d.phases.forEach(p => {
151
+ const o = document.createElement('option'); o.value = p; o.text = p; phaseSel.appendChild(o);
152
+ });
153
+ }
154
+
155
+ // task table
156
+ const tbody = document.getElementById('taskTable'); tbody.innerHTML = '';
157
+ d.tasks.forEach(t => {
158
+ const r = document.createElement('tr');
159
+ r.className = 'border-b';
160
+ r.innerHTML = `<td class='p-2'>${t['Task Name']}</td>
161
+ <td class='p-2'>${t.Client}</td>
162
+ <td class='p-2'>${t.Phase}</td>
163
+ <td class='p-2'>${t.Value}</td>
164
+ <td class='p-2'>${t['Duration (mins)']}</td>
165
+ <td class='p-2'>${t['Start Time']}</td>
166
+ <td class='p-2'>${t['End Time']}</td>
167
+ <td class='p-2'>${t.delayed ? 'Yes' : 'No'}</td>`;
168
+ tbody.appendChild(r);
169
+ });
170
+
171
+ // charts
172
+ const clientsLabels = d.clients;
173
+ const clientsCounts = d.client_counts;
174
+ const avgDurations = d.client_avg_duration;
175
+
176
+ // destroy existing charts if present
177
+ if(window.clientsChart){ window.clientsChart.destroy(); }
178
+ if(window.avgChart){ window.avgChart.destroy(); }
179
+ if(window.valueChart){ window.valueChart.destroy(); }
180
+ if(window.timeChart){ window.timeChart.destroy(); }
181
+
182
+ const ctx1 = document.getElementById('clientsBar').getContext('2d');
183
+ window.clientsChart = new Chart(ctx1, {
184
+ type: 'bar',
185
+ data: { labels: clientsLabels, datasets: [{ label: 'Tasks', data: clientsCounts, backgroundColor: '#3B82F6' }]},
186
+ options: { responsive:true, plugins:{legend:{display:false}}, scales:{y:{beginAtZero:true}}}
187
+ });
188
+
189
+ const ctx2 = document.getElementById('avgBar').getContext('2d');
190
+ window.avgChart = new Chart(ctx2, {
191
+ type: 'bar',
192
+ data: { labels: clientsLabels, datasets: [{ label: 'Avg Duration (mins)', data: avgDurations, backgroundColor: '#10B981' }]},
193
+ options: { indexAxis: 'y', responsive:true, plugins:{legend:{display:false}} }
194
+ });
195
+
196
+ const ctx3 = document.getElementById('valuePie').getContext('2d');
197
+ window.valueChart = new Chart(ctx3, {
198
+ type: 'pie',
199
+ data: { labels: d.values_labels, datasets: [{ data: d.values_counts }]},
200
+ options: { responsive:true }
201
+ });
202
+
203
+ const ctx4 = document.getElementById('timeline').getContext('2d');
204
+ window.timeChart = new Chart(ctx4, {
205
+ type: 'line',
206
+ data: { labels: d.timeline.labels, datasets: [{ label: 'Tasks started', data: d.timeline.counts, fill:true, tension:0.3 }]},
207
+ options: { responsive:true, scales:{y:{beginAtZero:true}}}
208
+ });
209
+ }
210
+
211
+ function resetFilters(){
212
+ document.getElementById('filterClient').value = '';
213
+ document.getElementById('filterPhase').value = '';
214
+ load();
215
+ }
216
+
217
+ load();
218
+ setInterval(load, 5*60*1000);
219
+ </script>
220
+ </body>
221
+ </html>
222
+ """
223
+
224
+ ########### Data processing utilities ###########
225
+
226
+ def read_and_prepare(csv_path=CSV_PATH):
227
+ """
228
+ Reads CSV and returns a DataFrame with normalized columns and derived fields:
229
+ - Duration (mins) ensured
230
+ - Start Time & End Time parsed to datetimes
231
+ - delayed: True if Duration > mean + std (sensible delay detector)
232
+ - considered 'done' if Value contains APPROVED/DONE/COMPLETED (case-insensitive)
233
+ """
234
+ df = pd.read_csv(csv_path, dtype=str).fillna("")
235
+ # Ensure expected columns exist
236
+ expected_cols = ["Task ID","Task Name","Client","Phase","Value","Start Time","End Time","Duration (mins)"]
237
+ for c in expected_cols:
238
+ if c not in df.columns:
239
+ df[c] = ""
240
+
241
+ # parse datetimes
242
+ def try_parse(s):
243
+ try:
244
+ return pd.to_datetime(s, errors="coerce")
245
+ except Exception:
246
+ return pd.NaT
247
+
248
+ df["Start_dt"] = df["Start Time"].apply(try_parse)
249
+ df["End_dt"] = df["End Time"].apply(try_parse)
250
+
251
+ # Duration numeric: if blank try to compute from datetimes
252
+ def duration_minutes(row):
253
+ val = row.get("Duration (mins)")
254
+ try:
255
+ if val is not None and str(val).strip() != "":
256
+ return float(str(val).strip())
257
+ except:
258
+ pass
259
+ s = row.get("Start_dt")
260
+ e = row.get("End_dt")
261
+ if pd.notna(s) and pd.notna(e):
262
+ return (e - s).total_seconds() / 60.0
263
+ return float("nan")
264
+
265
+ df["Duration (mins)"] = df.apply(duration_minutes, axis=1).astype(float)
266
+
267
+ # detect 'done' using keywords in Value or Phase (case-insensitive)
268
+ done_keywords = ["APPROVED", "DONE", "COMPLETED", "DELIVERED"]
269
+ df["is_done"] = df["Value"].str.upper().apply(lambda v: any(k in v for k in done_keywords))
270
+
271
+ # delayed detection: mark as delayed if duration > mean + std (per Phase could be better)
272
+ mean = df["Duration (mins)"].mean(skipna=True)
273
+ std = df["Duration (mins)"].std(skipna=True) if not math.isnan(mean) else 0
274
+ threshold = (mean + std) if not math.isnan(mean) else None
275
+ def detect_delayed(dur):
276
+ if dur is None or pd.isna(dur):
277
+ return False
278
+ if threshold is None:
279
+ return False
280
+ return dur > threshold
281
+ df["delayed"] = df["Duration (mins)"].apply(detect_delayed)
282
+
283
+ # prepare a start_date string for timeline grouping (YYYY-MM-DD)
284
+ df["start_date"] = df["Start_dt"].dt.date.astype(str).fillna("unknown")
285
+
286
+ return df
287
+
288
+ def build_response(df, filter_client=None, filter_phase=None):
289
+ # apply filters
290
+ dff = df.copy()
291
+ if filter_client:
292
+ dff = dff[dff["Client"] == filter_client]
293
+ if filter_phase:
294
+ dff = dff[dff["Phase"] == filter_phase]
295
+
296
+ total_count = len(dff)
297
+ done_count = int(dff["is_done"].sum())
298
+ progress_percent = (done_count / total_count * 100) if total_count else 0
299
+ avg_duration = float(dff["Duration (mins)"].mean(skipna=True) or 0)
300
+
301
+ # Tasks per client
302
+ client_counts_series = dff["Client"].value_counts()
303
+ clients = list(client_counts_series.index)
304
+ client_counts = [int(x) for x in client_counts_series.values]
305
+
306
+ # Avg duration per client
307
+ client_avg = dff.groupby("Client")["Duration (mins)"].mean().reindex(clients).fillna(0).tolist()
308
+
309
+ # Value distribution
310
+ values_counts_series = dff["Value"].value_counts()
311
+ values_labels = list(values_counts_series.index)
312
+ values_counts = [int(x) for x in values_counts_series.values]
313
+
314
+ # timeline (by start_date) sorted
315
+ timeline_series = dff.groupby("start_date").size().sort_index()
316
+ timeline_labels = list(timeline_series.index)
317
+ timeline_counts = [int(x) for x in timeline_series.values]
318
+
319
+ # prepare tasks list (limit to first 1000 for payload safety)
320
+ tasks = dff[["Task ID","Task Name","Client","Phase","Value","Start Time","End Time","Duration (mins)","delayed"]].fillna("").to_dict(orient="records")
321
+
322
+ # unique lists for filters
323
+ clients_all = sorted(df["Client"].unique().tolist())
324
+ phases_all = sorted(df["Phase"].unique().tolist())
325
+
326
+ resp = {
327
+ "total_count": total_count,
328
+ "done_count": done_count,
329
+ "progress_percent": progress_percent,
330
+ "avg_duration": avg_duration,
331
+ "clients": clients_all,
332
+ "phases": phases_all,
333
+ "clients_filtered": clients,
334
+ "client_counts": client_counts,
335
+ "client_avg_duration": [round(x or 0, 1) for x in client_avg],
336
+ "values_labels": values_labels,
337
+ "values_counts": values_counts,
338
+ "timeline": {"labels": timeline_labels, "counts": timeline_counts},
339
+ "tasks": tasks
340
+ }
341
+ return resp
342
+
343
+ ########### End utilities ###########
344
+
345
+ @app.get("/", response_class=HTMLResponse)
346
+ def index():
347
+ return HTML_TEMPLATE
348
+
349
+ @app.get("/api/tasks")
350
+ def api_tasks(client: str = Query(None), phase: str = Query(None)):
351
+ df = read_and_prepare()
352
+ resp = build_response(df, filter_client=client, filter_phase=phase)
353
+ return JSONResponse(resp)
354
+
355
+ @app.get("/report/pdf")
356
+ def report_pdf():
357
+ df = read_and_prepare()
358
+ resp = build_response(df)
359
+ buffer = io.BytesIO()
360
+ doc = SimpleDocTemplate(buffer, pagesize=A4)
361
+ styles = getSampleStyleSheet()
362
+
363
+ elements = [Paragraph('Task Phase Report', styles['Title']), Spacer(1, 8)]
364
+ elements.append(Paragraph(f"Total tasks: {resp['total_count']}", styles['Normal']))
365
+ elements.append(Paragraph(f"Completed: {resp['done_count']} ({resp['progress_percent']:.1f}%)", styles['Normal']))
366
+ elements.append(Paragraph(f"Average duration (mins): {resp['avg_duration']:.1f}", styles['Normal']))
367
+ elements.append(Spacer(1, 12))
368
+
369
+ # add table head
370
+ data_table = [["Task Name", "Client", "Phase", "Value", "Duration (mins)", "Start", "End", "Delayed"]]
371
+ for row in resp['tasks'][:200]: # limit rows in pdf
372
+ data_table.append([
373
+ row.get("Task Name", ""),
374
+ row.get("Client", ""),
375
+ row.get("Phase", ""),
376
+ row.get("Value", ""),
377
+ f"{row.get('Duration (mins)', ''):.1f}" if isinstance(row.get('Duration (mins)'), (int, float)) else row.get('Duration (mins)', ''),
378
+ row.get("Start Time", ""),
379
+ row.get("End Time", ""),
380
+ "Yes" if row.get("delayed") else "No"
381
+ ])
382
+
383
+ table = Table(data_table, repeatRows=1, colWidths=[80,70,60,70,60,80,80,45])
384
+ table.setStyle(TableStyle([
385
+ ('BACKGROUND',(0,0),(-1,0),colors.lightgrey),
386
+ ('GRID',(0,0),(-1,-1),0.25,colors.grey),
387
+ ('ALIGN',(0,0),(-1,-1),'LEFT'),
388
+ ('FONT',(0,0),(-1,0),'Helvetica-Bold')
389
+ ]))
390
+ elements.append(table)
391
+ doc.build(elements)
392
+ buffer.seek(0)
393
+ return StreamingResponse(buffer, media_type='application/pdf',
394
+ headers={'Content-Disposition':'attachment; filename="task_phase_report.pdf"'})
395
+
396
+ @app.get("/report/excel")
397
+ def report_excel():
398
+ df = read_and_prepare()
399
+ resp = build_response(df)
400
+ wb = Workbook()
401
+ ws = wb.active
402
+ ws.title = "Task Report"
403
+
404
+ headers = ["Task ID","Task Name","Client","Phase","Value","Duration (mins)","Start Time","End Time","Delayed"]
405
+ ws.append(headers)
406
+ for row in resp['tasks']:
407
+ ws.append([
408
+ row.get("Task ID",""),
409
+ row.get("Task Name",""),
410
+ row.get("Client",""),
411
+ row.get("Phase",""),
412
+ row.get("Value",""),
413
+ row.get("Duration (mins)",""),
414
+ row.get("Start Time",""),
415
+ row.get("End Time",""),
416
+ "Yes" if row.get("delayed") else "No"
417
+ ])
418
+ buf = io.BytesIO()
419
+ wb.save(buf)
420
+ buf.seek(0)
421
+ return StreamingResponse(buf, media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
422
+ headers={'Content-Disposition':'attachment; filename="task_phase_report.xlsx"'})
data.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ Task ID,Task Name,Client,Phase,Value,Start Time,End Time,Duration (min)
2
+ 86euc18fu,abdelrahman - tt,Ashkal,Creative Stage,Blocking,8/3/2025 23:59,8/4/2025 0:06,7
3
+ 86euc18fk,abdelrahman - kk,Ashkal,Creative Stage,copywriting,8/3/2025 23:55,8/4/2025 0:06,12
4
+ 86euc18fs,abdelrahman - ss,Ashkal,Creative Stage,creative,8/3/2025 23:50,8/4/2025 0:06,17
5
+ 86euc18fp,abdelrahman - hh,youssef,copy stage,creative,8/3/2025 23:50,8/5/2025 0:06,480
6
+ 86euc18f4,yossef,mohamed,image phase,clinet approved,10/6/2025 22:50,11/7/2025 23:50,9900
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi==0.111.0
2
+ uvicorn[standard]==0.24.0
3
+ pandas==2.1.1
4
+ openpyxl==3.1.3
5
+ reportlab==4.1.3