|
|
import urllib
|
|
|
from google.oauth2.service_account import Credentials
|
|
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
|
from googleapiclient.discovery import build
|
|
|
from googleapiclient.errors import HttpError
|
|
|
|
|
|
|
|
|
SCOPES = ["https://www.googleapis.com/auth/spreadsheets.readonly"]
|
|
|
|
|
|
|
|
|
def get_spreatsheed_service(credentials):
|
|
|
return build("sheets", "v4", credentials=credentials, cache_discovery=False)
|
|
|
|
|
|
|
|
|
def get_google_creds():
|
|
|
creds = None
|
|
|
creds = Credentials.from_service_account_file("credentials.json", scopes=SCOPES)
|
|
|
return creds
|
|
|
|
|
|
|
|
|
def process_spreadsheet_data(rows):
|
|
|
processed_data = []
|
|
|
for row in rows:
|
|
|
processed_row = convert_spreadsheet_data(row)
|
|
|
processed_data.append(processed_row) if processed_row else None
|
|
|
return processed_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
|
|
|
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
|
|
|
|
|
|
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}!A5:M5000"
|
|
|
).execute()
|
|
|
|
|
|
values = result.get("values", [])
|
|
|
if not values:
|
|
|
raise ValueError("No data found in sheet")
|
|
|
|
|
|
data = []
|
|
|
current_section = None
|
|
|
current_subsystem = None
|
|
|
|
|
|
|
|
|
for row in values:
|
|
|
# Pad missing columns to avoid index errors
|
|
|
row += [""] * (13 - len(row))
|
|
|
|
|
|
colA, colB, colC, colD, colE, colF, colG, colH, colI, colJ, colK, colL, colM = row
|
|
|
|
|
|
|
|
|
# Detect a SECTION — bold blue rows in Column C
|
|
|
if colC and not colD and not colE:
|
|
|
current_section = colC.strip()
|
|
|
current_subsystem = None
|
|
|
continue
|
|
|
|
|
|
# Detect a SUBSYSTEM — indented header in Column D
|
|
|
if colD and not colE:
|
|
|
current_subsystem = colD.strip()
|
|
|
continue
|
|
|
|
|
|
# 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,
|
|
|
"subsystem": current_subsystem,
|
|
|
"task": task,
|
|
|
"PIC": pic,
|
|
|
"start_date": start_date,
|
|
|
"end_date": finish_date,
|
|
|
"duration": int(duration),
|
|
|
"plan": plan,
|
|
|
"actual": actual,
|
|
|
"gap": gap
|
|
|
})
|
|
|
|
|
|
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}" |