From d969059f22044b7fc396b1516e6bf2084a2b2cf5 Mon Sep 17 00:00:00 2001 From: Cizz22 Date: Fri, 21 Nov 2025 17:18:43 +0700 Subject: [PATCH] fix --- Jenkinsfile | 10 ++ src/overhaul_gantt/service.py | 54 ++++++++-- src/overhaul_gantt/utils.py | 198 +++++++++++++++++++++++----------- 3 files changed, 189 insertions(+), 73 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 1856588..709e03c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -61,6 +61,16 @@ pipeline { """ } } + + // stage('Watchtower Deployment') { + // steps { + // sh """ + // # Push both tags + // docker push ${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:${GIT_COMMIT_HASH} + // docker push ${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:latest + // """ + // } + // } // stage('Deploy') { // steps { diff --git a/src/overhaul_gantt/service.py b/src/overhaul_gantt/service.py index 00dd155..a69c3df 100644 --- a/src/overhaul_gantt/service.py +++ b/src/overhaul_gantt/service.py @@ -53,24 +53,60 @@ from .utils import fetch_all_sections, get_google_creds, get_spreatsheed_service async def get_gantt_performance_chart(*, spreadsheet_id = "1gZXuwA97zU1v4QBv56wKeiqadc6skHUucGKYG8qVFRk"): creds = get_google_creds() - RANGE_NAME = "'2024 kurva s'!N79:BJ83" # Or just "2024 schedule" - GANTT_DATA_NAME = "2024 schedule" + RANGE_NAME = "'SUMMARY'!I28:AZ32" # Or just "2024 schedule" + GANTT_DATA_NAME = "ACTUAL PROGRESS" try: service = get_spreatsheed_service(creds) sheet = service.spreadsheets() - response = sheet.values().get(spreadsheetId=spreadsheet_id, range=RANGE_NAME).execute() + + response = sheet.values().get( + spreadsheetId=spreadsheet_id, + range=RANGE_NAME + ).execute() + values = response.get("values", []) - keys = ['day', 'time', 'plan', 'actual', 'gap'] - transposed = list(zip(*values)) - results = [dict(zip(keys, result)) for result in transposed] - + + if len(values) < 4: + raise Exception("Spreadsheet format invalid: need 4 rows (DAY, DATE, PLAN, ACTUAL).") + + # Extract rows + day_row = values[0][1:] + date_row = values[1][1:] + plan_row = values[3][1:] + actual_row = values[4][1:] + + + + total_days = len(day_row) + + # PAD rows so lengths match day count + date_row += [""] * (total_days - len(date_row)) + plan_row += [""] * (total_days - len(plan_row)) + actual_row += [""] * (total_days - len(actual_row)) + + results = [] + + for i in range(total_days): + + day = day_row[i] + date = date_row[i] + plan = plan_row[i] + actual = actual_row[i] if actual_row[i] else "0%" # <-- FIX HERE + + results.append({ + "day": day, + "date": date, + "plan": plan, + "actual": actual + }) + except Exception as e: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + processed_data = process_spreadsheet_data(results) gantt_data = fetch_all_sections(service=service, spreadsheet_id=spreadsheet_id, sheet_name=GANTT_DATA_NAME) - return processed_data, gantt_data diff --git a/src/overhaul_gantt/utils.py b/src/overhaul_gantt/utils.py index c1e7eec..e166131 100644 --- a/src/overhaul_gantt/utils.py +++ b/src/overhaul_gantt/utils.py @@ -22,53 +22,81 @@ def process_spreadsheet_data(rows): processed_data = [] for row in rows: processed_row = convert_spreadsheet_data(row) - processed_data.append(processed_row) + processed_data.append(processed_row) if processed_row else None return processed_data -def convert_spreadsheet_data(data): +from datetime import datetime + +from datetime import datetime + +def convert_spreadsheet_data(data, default_year=None): + """ + Convert spreadsheet row into structured data. + Expected keys: day, date, plan, actual + """ + + # Skip header or invalid rows + if not data.get("day") or not data["day"].isdigit(): + return None + result = {} - # Convert day to integer - result['day'] = int(data['day']) - - # Convert time to a datetime object - from datetime import datetime - # Assuming Indonesian format with month names - # Replace Indonesian month names with English if needed - month_mapping = { - 'Januari': 'January', 'Februari': 'February', 'Maret': 'March', - 'April': 'April', 'Mei': 'May', 'Juni': 'June', - 'Juli': 'July', 'Agustus': 'August', 'September': 'September', - 'Oktober': 'October', 'November': 'November', 'Desember': 'December' - } + # Convert day + result["day"] = int(data["day"]) + + # Determine default year + if default_year is None: + default_year = datetime.now().year + + date_str = data.get("date", "").strip() + + # ---------- DATE HANDLING ---------- + # Accept formats like: "Nov 20", "Dec 3", "Jan 1" + parsed_date = None - time_str = data['time'] - for indo, eng in month_mapping.items(): - time_str = time_str.replace(indo, eng) - - # Format: "Sabtu, Juli 13, 2024" -> "Saturday, July 13, 2024" - # Removing the day of week to simplify parsing - time_str = time_str.split(', ', 1)[1] # Remove "Sabtu, " - result['time'] = datetime.strptime(time_str, '%B %d, %Y') - - # Convert percentage strings to floats - # Handling format like "0,12%" -> 0.12 - for key in ['plan', 'actual', 'gap']: - # Replace comma with dot (European to US decimal notation) - value = data[key].replace(',', '.') - # Remove percentage sign - value = value.rstrip('%') - # Convert to float - result[key] = float(value) / 100 # Divide by 100 to get the actual decimal value + if date_str: + try: + parsed_date = datetime.strptime(f"{date_str} {default_year}", "%b %d %Y") + except ValueError: + try: + parsed_date = datetime.strptime(f"{date_str} {default_year}", "%B %d %Y") + except: + parsed_date = None + + # YEAR ROLLOVER (Dec → Jan next year) + if parsed_date and parsed_date.month == 1 and "Dec" in data.get("date", ""): + parsed_date = parsed_date.replace(year=default_year + 1) + + result["date"] = parsed_date + + # ---------- PERCENT HANDLING ---------- + def parse_percent(value): + if not value: + return 0.0 + v = value.strip().replace(",", ".").replace("%", "") + try: + return float(v) / 100.0 + except: + return 0.0 + + result["plan"] = parse_percent(data.get("plan", "0")) + result["actual"] = parse_percent(data.get("actual", "0")) + + # Gap calculation + result["gap"] = result["actual"] - result["plan"] return result + + def fetch_all_sections(service, spreadsheet_id, sheet_name): + # Fetch a wide range including columns A–L result = service.spreadsheets().values().get( spreadsheetId=spreadsheet_id, - range=f"{sheet_name}!A18:1000" # start from row 19, adjust Z if needed + range=f"{sheet_name}!A5:M5000" ).execute() + values = result.get("values", []) if not values: raise ValueError("No data found in sheet") @@ -78,50 +106,92 @@ def fetch_all_sections(service, spreadsheet_id, sheet_name): current_subsystem = None - for i, row in enumerate(values): - row = [c.strip() for c in row] # clean whitespace + for row in values: + # Pad missing columns to avoid index errors + row += [""] * (13 - len(row)) - # Skip completely empty rows - if all(c == "" for c in row): - continue + colA, colB, colC, colD, colE, colF, colG, colH, colI, colJ, colK, colL, colM = row + - # Detect section headers (usually in col A, all caps) - if row[0]: - # skip unwanted section title - if row[0] == "U4 PLANNED OUTAGE 2024": - continue - current_section = row[0] + # Detect a SECTION — bold blue rows in Column C + if colC and not colD and not colE: + current_section = colC.strip() current_subsystem = None - header = None continue - # Detect subsystem (non-all-caps in col A) - if row[1]: - current_subsystem = row[1] + # Detect a SUBSYSTEM — indented header in Column D + if colD and not colE: + current_subsystem = colD.strip() continue - - # Otherwise, treat as task row (must have header defined) - if row[2]: - task = row[2] - pic = row[3] - start_date = row[4] - finish_date = row[5] - duration = row[6] - plan = row[7] - actual = row[8] - gap = row[9] - + + # Detect a TASK — Column E populated + if colE: + task = colE.strip() + pic = colF.strip() + start_date = indo_formatted_date(colG.strip()) + finish_date = indo_formatted_date(colH.strip()) + duration = colI.strip() + plan = colK.strip() + actual = colL.strip() + gap = colM.strip() + data.append({ - "system": current_section, + "section": current_section, "subsystem": current_subsystem, "task": task, "PIC": pic, "start_date": start_date, "end_date": finish_date, - "duration": duration, + "duration": int(duration), "plan": plan, "actual": actual, "gap": gap }) - return data \ No newline at end of file + return data + + +def indo_formatted_date(date_str, base_year=2025): + """ + Convert short date like 'Nov 20', '30-Dec', 'Jan 1' + into: 'Rabu, November 20, 2025' + If month is January, year becomes 2026. + """ + + # Month mappings + eng_to_indo_month = { + "Jan": "Januari", "Feb": "Februari", "Mar": "Maret", "Apr": "April", + "May": "Mei", "Jun": "Juni", "Jul": "Juli", "Aug": "Agustus", + "Sep": "September", "Oct": "Oktober", "Nov": "November", "Dec": "Desember" + } + + indo_days = { + 0: "Senin", + 1: "Selasa", + 2: "Rabu", + 3: "Kamis", + 4: "Jumat", + 5: "Sabtu", + 6: "Minggu" + } + + # Normalize formats ("30-Dec" → "Dec 30") + if "-" in date_str: + d, m = date_str.split("-") + date_str = f"{m} {d}" + + # Parse using English abbreviation + try: + dt = datetime.strptime(f"{date_str} {base_year}", "%b %d %Y") + except: + return None + + # Handle year rollover (Jan -> next year) + if dt.month == 1: + dt = dt.replace(year=base_year + 1) + + # Convert to Indonesian components + day_name = indo_days[dt.weekday()] + month_name = eng_to_indo_month[dt.strftime("%b")] + + return f"{day_name}, {month_name} {dt.day}, {dt.year}" \ No newline at end of file