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: return json.load(f) except: pass return {"theme": "fallout"} # Default to fallout because it's cooler 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 calculate_checkins(client): 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]") 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) 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 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 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 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: choice = questionary.select( "SELECT ACTION:" if current_theme_name == "fallout" else "What would you like to do?", choices=["Calculate actual work days", "Show profile info", "Switch UI Theme", "Logout and Exit"], style=theme_data["questionary"] ).ask() if choice == "Calculate actual work days": calculate_checkins(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]") else: console.print("[highlight]SYSTEM SHUTDOWN...[/highlight]" if current_theme_name == "fallout" else "Exiting...") break if __name__ == "__main__": main()