diff --git a/tamigo.py b/tamigo.py index e77bf9f..8751d39 100644 --- a/tamigo.py +++ b/tamigo.py @@ -14,72 +14,109 @@ from dotenv import load_dotenv # Load environment variables from .env if it exists load_dotenv() -# Fallout Pip-Boy Style Theme -fallout_theme = 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", -}) +CONFIG_FILE = "config.json" -console = Console(theme=fallout_theme) +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 -# Questionary custom style -custom_style = questionary.Style([ - ('qmark', 'fg:#33ff33 bold'), # token in front of the question - ('question', 'fg:#33ff33 bold'), # question text - ('answer', 'fg:#33ff33'), # submitted answer text behind the question - ('pointer', 'fg:#33ff33 bold'), # pointer used in select input - ('highlighted', 'fg:#000000 bg:#33ff33 bold'), # pointed-at choice in select and checkbox - ('selected', 'fg:#33ff33'), # pointed-at choice in select and checkbox - ('separator', 'fg:#004400'), # separator in lists - ('instruction', 'fg:#004400 italic'), # user instructions for checkbox, reorder, etc. - ('text', 'fg:#33ff33'), # plain text - ('disabled', 'fg:#004400 italic') # disabled choices for select and checkbox -]) +def save_config(config): + with open(CONFIG_FILE, "w") as f: + json.dump(config, f) -PIPO_ART = """ +# --- 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""" _____ __ __ _____ _____ ____ |_ _| /\ | \/ |_ _/ ____|/ __ \ | | / \ | \ / | | || | __| | | | | | / /\ \ | |\/| | | || | |_ | | | | _| |_ / ____ \| | | |_| || |__| | |__| | |_____/_/ \_\_| |_|_____\_____|\____/ - - [bold #33ff33]ROBCO INDUSTRIES (TM) TERMLINK PROTOCOL[/bold #33ff33] """ +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 print_header(): - console.print(PIPO_ART, style="highlight") - console.print("[dim]-------------------------------------------[/dim]") - console.print("[highlight]WELCOME TO TAMIGO-OS v1.0.0[/highlight]") - console.print("[dim]-------------------------------------------[/dim]\n") - def parse_tamigo_date(date_str): - """ - Parses Tamigo date formats: - - /Date(1600898400000+0200)/ - - 2023-10-27T08:00:00 - """ - if not date_str: - return None - - # Handle /Date(1600898400000+0200)/ + if not date_str: return None match = re.search(r"/Date\((\d+)([+-]\d+)?\)/", date_str) if match: ms = int(match.group(1)) - # Convert ms to seconds return datetime.fromtimestamp(ms / 1000.0) - - # Handle ISO format try: - # datetime.fromisoformat handles T and optional Z/+offset in Python 3.7+ return datetime.fromisoformat(date_str.replace("Z", "+00:00")) except: return None @@ -88,242 +125,132 @@ class TamigoClient: def __init__(self): self.session_token = None self.user_info = None - self.employee_id = None def login(self, email, password): - urls = [ - f"{BASE_URL}/login/application", - f"{BASE_URL}/Login", - f"{BASE_URL}/login", - ] - + 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 - } + payload = {"Email": email, "Password": password, "Name": "TamigoCLI", "Key": password} try: - headers = { - "Content-Type": "application/json", - "Accept": "application/json" - } + headers = {"Content-Type": "application/json", "Accept": "application/json"} response = requests.post(url, json=payload, headers=headers, timeout=15) - if response.status_code == 200: - try: - 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 - self.employee_id = data.get("EmployeeId") - return True - except json.JSONDecodeError: - text = response.text.strip().strip('"') - if text and len(text) > 20: - self.session_token = text - self.user_info = {"Email": email} - return True - except: - continue - + 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" - } - + 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") - current_dt = end_date_dt - timedelta(days=i) - if current_dt < start_date_dt - timedelta(days=60): - break - - url = f"{BASE_URL}/actualshifts/past/{target_date}" + if (end_date_dt - timedelta(days=i)) < start_date_dt - timedelta(days=60): break try: - response = requests.get(url, headers=headers) - if response.status_code == 200: - data = response.json() - if isinstance(data, list): - all_shifts.extend(data) + 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: - response = requests.get(url, params={"securitytoken": self.session_token}) - if response.status_code == 200: - all_shifts.extend(response.json()) - except: - pass - - start_str = start_date_dt.strftime("%Y-%m-%d") - end_str = end_date_dt.strftime("%Y-%m-%d") - url_period = f"{BASE_URL}/shifts/period/{start_str}/{end_str}" + 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(url_period, headers=headers) - if response.status_code == 200: - data = response.json() - if isinstance(data, list): - all_shifts.extend(data) - except: - pass - + 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:", - choices=[ - "Last 365 days", - "Last 30 days", - "This Month", - "This Year", - "Custom range..." - ], - style=custom_style + "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) + 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):", default=(datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d"), style=custom_style).ask() - end_str = questionary.text("ENTER END DATE (YYYY-MM-DD):", default=datetime.now().strftime("%Y-%m-%d"), style=custom_style).ask() + 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 = datetime.strptime(start_str, "%Y-%m-%d") - end_date_dt = datetime.strptime(end_str, "%Y-%m-%d") - if start_date_dt > end_date_dt: - console.print("[error]ERROR: INVALID CHRONOLOGY. START MUST PRECEDE END.[/error]") - continue - break - except ValueError: - console.print("[error]ERROR: INVALID DATA FORMAT. USE YYYY-MM-DD.[/error]") + 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(f"[highlight]INITIALIZING DATA RETRIEVAL...[/highlight]"): + 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 = {} - requested_start = start_date_dt.date() - requested_end = end_date_dt.date() - for item in data: - raw_date = item.get("Date") or item.get("StartTime") or item.get("CheckInTime") - dt = parse_tamigo_date(raw_date) - - if dt: - item_date = dt.date() - if not (requested_start <= item_date <= requested_end): - continue - + 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") - is_absent = item.get("IsAbsent", False) - hours = (item.get("ActualShiftHours") or item.get("CheckInOutShiftHours") or item.get("Hours") or 0) - + 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 = parse_tamigo_date(item.get("StartTime")) - et = parse_tamigo_date(item.get("EndTime")) - if st and et: - hours = (et - st).total_seconds() / 3600.0 - - has_actual = False - if hours > 0 or item.get("CheckInTime") or item.get("ActualStartTime") or (item.get("StartTime") and not is_absent): - has_actual = True - - if has_actual and not is_absent: - if date_str not in work_days: - work_days[date_str] = {"hours": float(hours), "text": item.get("ActualShiftText") or item.get("ActivityName") or "WORKED"} - else: - work_days[date_str]["hours"] += float(hours) + 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 FOR SPECIFIED PARAMETERS.[/warning]") + 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", show_header=True, header_style="table.header", box=None) + 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()) - + 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')}\n" - summary_text += f"DAYS WORKED: {len(all_dates)}\n" - summary_text += f"TOTAL HOURS: {sum(d['hours'] for d in work_days.values()):.2f}" - - console.print(Panel(summary_text, title="SUMMARY STATISTICS", border_style="highlight", expand=False)) - else: - console.print("[error]FATAL ERROR: DATA RETRIEVAL FAILURE.[/error]") - -def show_profile(client): - if client.user_info: - console.print_json(data=client.user_info) - else: - console.print("[error]ERROR: ACCESS DENIED. SESSION TOKEN NOT FOUND.[/error]") + 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") - if not email: - email = questionary.text("ENTER IDENTIFIER (EMAIL):", style=custom_style).ask() - - password = os.getenv("TAMIGO_PASSWORD") - if not password: - password = questionary.password("ENTER ACCESS KEY:", style=custom_style).ask() - - if not email or not password: - return - - with console.status("[highlight]AUTHENTICATING WITH VAULT-TEC CLOUD...[/highlight]"): + 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. ACCESS GRANTED.[/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]") + else: console.print("[error]CRITICAL FAILURE: INVALID CREDENTIALS.[/error]") def menu(client): while True: choice = questionary.select( - "SELECT ACTION:", - choices=[ - "Calculate actual work days", - "Show profile info", - "Logout and Exit" - ], - style=custom_style + "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) + if choice == "Calculate actual work days": calculate_checkins(client) elif choice == "Show profile info": - show_profile(client) + 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 INITIATED...[/highlight]") + console.print("[highlight]SYSTEM SHUTDOWN...[/highlight]" if current_theme_name == "fallout" else "Exiting...") break if __name__ == "__main__":