import requests import questionary from rich.console import Console from rich.table import Table from rich.theme import Theme from rich.panel import Panel from rich.text import Text import json import os import re from datetime import datetime, timedelta from dotenv import load_dotenv # Load environment variables from .env if it exists load_dotenv() CONFIG_FILE = "config.json" def load_config(): if os.path.exists(CONFIG_FILE): try: with open(CONFIG_FILE, "r") as f: config = json.load(f) if "show_zero_hours" not in config: config["show_zero_hours"] = False return config except: pass return {"theme": "fallout", "show_zero_hours": False} # Default to fallout and hide zero hours def save_config(config): with open(CONFIG_FILE, "w") as f: json.dump(config, f) # --- Themes --- THEMES = { "fallout": { "rich": Theme({ "info": "bold green", "warning": "bold yellow", "error": "bold red", "success": "bold green", "dim": "dim green", "highlight": "bold #33ff33", "table.header": "bold #33ff33", "table.title": "bold #33ff33", }), "questionary": questionary.Style([ ('qmark', 'fg:#33ff33 bold'), ('question', 'fg:#33ff33 bold'), ('answer', 'fg:#ffb000 bold'), ('pointer', 'fg:#33ff33 bold'), ('highlighted', 'fg:#000000 bg:#33ff33 bold'), # Reversed: Black on Green ('selected', 'fg:#33ff33'), ('separator', 'fg:#004400'), ('instruction', 'fg:#004400 italic'), ('text', 'fg:#ffb000'), ('disabled', 'fg:#004400 italic') ]), "header_text": "[bold #33ff33]ROBCO INDUSTRIES (TM) TERMLINK PROTOCOL[/bold #33ff33]", "welcome_text": "[highlight]WELCOME TO TAMIGO-OS v1.0.0[/highlight]" }, "normal": { "rich": Theme({ "info": "blue", "warning": "yellow", "error": "red", "success": "green", "dim": "dim white", "highlight": "bold blue", "table.header": "bold magenta", "table.title": "bold cyan", }), "questionary": questionary.Style([]), # Default style "header_text": "[bold blue]Tamigo CLI Official Interface[/bold blue]", "welcome_text": "[bold blue]Welcome to Tamigo CLI[/bold blue]" } } # Initialize with default or loaded config current_config = load_config() current_theme_name = current_config.get("theme", "fallout") theme_data = THEMES[current_theme_name] console = Console(theme=theme_data["rich"]) PIPO_ART = r""" _____ __ __ _____ _____ ____ |_ _| /\ | \/ |_ _/ ____|/ __ \ | | / \ | \ / | | || | __| | | | | | / /\ \ | |\/| | | || | |_ | | | | _| |_ / ____ \| | | |_| || |__| | |__| | |_____/_/ \_\_| |_|_____\_____|\____/ """ def print_header(): if current_theme_name == "fallout": console.print(Panel(Text(PIPO_ART, style="highlight"), border_style="highlight", expand=False)) console.print(theme_data["header_text"]) console.print("[dim]-------------------------------------------[/dim]") console.print(theme_data["welcome_text"]) console.print("[dim]-------------------------------------------[/dim]\n") # Re-initialize globals when theme changes def apply_theme(name): global console, theme_data, current_theme_name current_theme_name = name theme_data = THEMES[name] console = Console(theme=theme_data["rich"]) current_config["theme"] = name save_config(current_config) BASE_URL = "https://api.tamigo.com" def parse_tamigo_date(date_str): if not date_str: return None match = re.search(r"/Date\((\d+)([+-]\d+)?\)/", date_str) if match: ms = int(match.group(1)) return datetime.fromtimestamp(ms / 1000.0) try: return datetime.fromisoformat(date_str.replace("Z", "+00:00")) except: return None class TamigoClient: def __init__(self): self.session_token = None self.user_info = None def login(self, email, password): urls = [f"{BASE_URL}/login/application", f"{BASE_URL}/Login", f"{BASE_URL}/login"] for url in urls: payload = {"Email": email, "Password": password, "Name": "TamigoCLI", "Key": password} try: headers = {"Content-Type": "application/json", "Accept": "application/json"} response = requests.post(url, json=payload, headers=headers, timeout=15) if response.status_code == 200: data = response.json() self.session_token = data.get("SessionToken") or data.get("securitytoken") or data.get("Token") if self.session_token: self.user_info = data return True except: continue return False def get_employee_actual_shifts(self, start_date_dt, end_date_dt): if not self.session_token: return None headers = {"x-tamigo-token": self.session_token, "securitytoken": self.session_token, "Accept": "application/json"} all_shifts = [] days_diff = (end_date_dt - start_date_dt).days for i in range(0, days_diff + 1, 60): target_date = (end_date_dt - timedelta(days=i)).strftime("%Y-%m-%d") if (end_date_dt - timedelta(days=i)) < start_date_dt - timedelta(days=60): break try: response = requests.get(f"{BASE_URL}/actualshifts/past/{target_date}", headers=headers) if response.status_code == 200: all_shifts.extend(response.json()) elif response.status_code == 401: resp = requests.get(f"{BASE_URL}/actualshifts/past/{target_date}", params={"securitytoken": self.session_token}) if resp.status_code == 200: all_shifts.extend(resp.json()) except: pass try: response = requests.get(f"{BASE_URL}/shifts/period/{start_date_dt.strftime('%Y-%m-%d')}/{end_date_dt.strftime('%Y-%m-%d')}", headers=headers) if response.status_code == 200: all_shifts.extend(response.json()) except: pass return all_shifts def select_date_range(): range_choice = questionary.select( "SELECT TEMPORAL RANGE:" if current_theme_name == "fallout" else "Select period:", choices=["Last 365 days", "Last 30 days", "This Month", "This Year", "Custom range..."], style=theme_data["questionary"] ).ask() end_date_dt = datetime.now() if range_choice == "Last 365 days": start_date_dt = end_date_dt - timedelta(days=365) elif range_choice == "Last 30 days": start_date_dt = end_date_dt - timedelta(days=30) elif range_choice == "This Month": start_date_dt = end_date_dt.replace(day=1) elif range_choice == "This Year": start_date_dt = end_date_dt.replace(month=1, day=1) else: while True: start_str = questionary.text("ENTER START DATE (YYYY-MM-DD):" if current_theme_name == "fallout" else "Start date (YYYY-MM-DD):", default=(datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d"), style=theme_data["questionary"]).ask() end_str = questionary.text("ENTER END DATE (YYYY-MM-DD):" if current_theme_name == "fallout" else "End date (YYYY-MM-DD):", default=datetime.now().strftime("%Y-%m-%d"), style=theme_data["questionary"]).ask() try: start_date_dt, end_date_dt = datetime.strptime(start_str, "%Y-%m-%d"), datetime.strptime(end_str, "%Y-%m-%d") if start_date_dt <= end_date_dt: break console.print("[error]ERROR: INVALID CHRONOLOGY.[/error]") except: console.print("[error]ERROR: INVALID DATA FORMAT.[/error]") return start_date_dt, end_date_dt def get_worked_days(client, start_date_dt, end_date_dt): with console.status("[highlight]INITIALIZING DATA RETRIEVAL...[/highlight]" if current_theme_name == "fallout" else "Fetching data..."): data = client.get_employee_actual_shifts(start_date_dt, end_date_dt) show_zero = current_config.get("show_zero_hours", False) if data: work_days = {} for item in data: dt = parse_tamigo_date(item.get("Date") or item.get("StartTime") or item.get("CheckInTime")) if dt and start_date_dt.date() <= dt.date() <= end_date_dt.date(): date_str = dt.strftime("%Y-%m-%d") hours = float(item.get("ActualShiftHours") or item.get("CheckInOutShiftHours") or item.get("Hours") or 0) if hours == 0 and item.get("StartTime") and item.get("EndTime"): st, et = parse_tamigo_date(item.get("StartTime")), parse_tamigo_date(item.get("EndTime")) if st and et: hours = (et - st).total_seconds() / 3600.0 # Filter zero hours if requested if hours == 0 and not show_zero: continue if (hours > 0 or item.get("CheckInTime") or item.get("ActualStartTime") or item.get("StartTime")) and not item.get("IsAbsent", False): if date_str not in work_days: work_days[date_str] = {"hours": hours, "text": item.get("ActualShiftText") or item.get("ActivityName") or "WORKED"} else: work_days[date_str]["hours"] += hours return work_days return None def calculate_checkins(client): start_date_dt, end_date_dt = select_date_range() work_days = get_worked_days(client, start_date_dt, end_date_dt) if work_days is not None: if not work_days: console.print("[warning]WARNING: NO LOG ENTRIES DETECTED.[/warning]") return all_dates = sorted(work_days.keys(), reverse=True) table = Table(title="LOG ENTRIES: ACTUAL WORKED SHIFTS" if current_theme_name == "fallout" else "Recent Work Days", show_header=True, header_style="table.header", box=None if current_theme_name == "fallout" else None) table.add_column("DATE", style="highlight") table.add_column("HOURS", justify="right") table.add_column("DETAILS", style="dim") for day in all_dates: info = work_days[day] table.add_row(day, f"{info['hours']:.2f}", str(info['text']).upper() if current_theme_name == "fallout" else str(info['text'])) console.print(table) summary_text = f"PERIOD: {start_date_dt.strftime('%Y-%m-%d')} TO {end_date_dt.strftime('%Y-%m-%d')}\nDAYS WORKED: {len(all_dates)}\nTOTAL HOURS: {sum(d['hours'] for d in work_days.values()):.2f}" console.print(Panel(summary_text, title="SUMMARY STATISTICS" if current_theme_name == "fallout" else "Work Statistics", border_style="highlight", expand=False)) else: console.print("[error]FATAL ERROR: DATA RETRIEVAL FAILURE.[/error]") def export_worked_hours(client): start_date_dt, end_date_dt = select_date_range() work_days = get_worked_days(client, start_date_dt, end_date_dt) if work_days is not None: if not work_days: console.print("[warning]WARNING: NO LOG ENTRIES DETECTED.[/warning]") return format_choice = questionary.select( "SELECT EXPORT FORMAT:" if current_theme_name == "fallout" else "Select export format:", choices=["CSV", "JSON", "XLSX"], style=theme_data["questionary"] ).ask() default_filename = f"tamigo_export_{start_date_dt.strftime('%Y%m%d')}_{end_date_dt.strftime('%Y%m%d')}" filename = questionary.text( "ENTER FILENAME:" if current_theme_name == "fallout" else "Enter filename:", default=f"{default_filename}.{format_choice.lower()}", style=theme_data["questionary"] ).ask() if not filename: return try: if format_choice == "CSV": import csv with open(filename, 'w', newline='') as f: writer = csv.writer(f) writer.writerow(["Date", "Hours", "Details"]) for day in sorted(work_days.keys()): info = work_days[day] writer.writerow([day, f"{info['hours']:.2f}", info['text']]) elif format_choice == "XLSX": from openpyxl import Workbook from openpyxl.styles import Font wb = Workbook() ws = wb.active ws.title = "Worked Hours" # Header headers = ["Date", "Hours", "Details"] ws.append(headers) for cell in ws[1]: cell.font = Font(bold=True) # Data for day in sorted(work_days.keys()): info = work_days[day] ws.append([day, info['hours'], info['text']]) wb.save(filename) else: with open(filename, 'w') as f: json.dump(work_days, f, indent=4) console.print(f"[success]DATA EXPORTED SUCCESSFULLY TO: {filename}[/success]") except Exception as e: console.print(f"[error]ERROR SAVING FILE: {str(e)}[/error]") else: console.print("[error]FATAL ERROR: DATA RETRIEVAL FAILURE.[/error]") def main(): print_header() client = TamigoClient() email = os.getenv("TAMIGO_EMAIL") or questionary.text("ENTER IDENTIFIER (EMAIL):" if current_theme_name == "fallout" else "Email:", style=theme_data["questionary"]).ask() password = os.getenv("TAMIGO_PASSWORD") or questionary.password("ENTER ACCESS KEY:" if current_theme_name == "fallout" else "Password:", style=theme_data["questionary"]).ask() if not email or not password: return with console.status("[highlight]AUTHENTICATING...[/highlight]" if current_theme_name == "fallout" else "Logging in..."): success = client.login(email, password) if success: console.print("[success]AUTHENTICATION SUCCESSFUL.[/success]" if current_theme_name == "fallout" else "[success]Login successful![/success]") menu(client) else: console.print("[error]CRITICAL FAILURE: INVALID CREDENTIALS.[/error]") def menu(client): while True: show_zero = current_config.get("show_zero_hours", False) toggle_text = "HIDE ZERO-HOUR DAYS" if show_zero else "SHOW ZERO-HOUR DAYS" if current_theme_name != "fallout": toggle_text = "Hide zero-hour days" if show_zero else "Show zero-hour days" choice = questionary.select( "SELECT ACTION:" if current_theme_name == "fallout" else "What would you like to do?", choices=["Calculate actual work days", "Export hours worked", "Show profile info", "Switch UI Theme", toggle_text, "Logout and Exit"], style=theme_data["questionary"] ).ask() if choice == "Calculate actual work days": calculate_checkins(client) elif choice == "Export hours worked": export_worked_hours(client) elif choice == "Show profile info": if client.user_info: console.print_json(data=client.user_info) else: console.print("[error]ERROR: NO PROFILE INFO.[/error]") elif choice == "Switch UI Theme": new_theme = "normal" if current_theme_name == "fallout" else "fallout" apply_theme(new_theme) console.print(f"[success]UI THEME SET TO: {new_theme.upper()}[/success]") elif choice == toggle_text: current_config["show_zero_hours"] = not show_zero save_config(current_config) state = "VISIBLE" if not show_zero else "HIDDEN" console.print(f"[success]ZERO-HOUR DAYS ARE NOW {state}.[/success]" if current_theme_name == "fallout" else f"[success]Zero-hour days are now {state.lower()}.[/success]") else: console.print("[highlight]SYSTEM SHUTDOWN...[/highlight]" if current_theme_name == "fallout" else "Exiting...") break if __name__ == "__main__": main()