2026-03-11 12:07:08 +01:00
import requests
import questionary
from rich . console import Console
from rich . table import Table
2026-03-11 21:17:15 +01:00
from rich . theme import Theme
from rich . panel import Panel
from rich . text import Text
2026-03-11 12:07:08 +01:00
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 ( )
2026-03-11 21:30:30 +01:00
CONFIG_FILE = " config.json "
2026-03-11 21:17:15 +01:00
2026-03-11 21:30:30 +01:00
def load_config ( ) :
if os . path . exists ( CONFIG_FILE ) :
try :
with open ( CONFIG_FILE , " r " ) as f :
2026-03-14 11:52:31 +01:00
config = json . load ( f )
if " show_zero_hours " not in config :
config [ " show_zero_hours " ] = False
return config
2026-03-11 21:30:30 +01:00
except :
pass
2026-03-14 11:52:31 +01:00
return { " theme " : " fallout " , " show_zero_hours " : False } # Default to fallout and hide zero hours
2026-03-11 21:30:30 +01:00
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] "
}
}
2026-03-11 21:17:15 +01:00
2026-03-11 21:30:30 +01:00
# 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 " ] )
2026-03-11 21:17:15 +01:00
2026-03-11 21:30:30 +01:00
PIPO_ART = r """
2026-03-11 21:17:15 +01:00
_____ __ __ _____ _____ ____
| _ _ | / \ | \/ | _ _ / ____ | / __ \
| | / \ | \ / | | | | | __ | | | |
| | / / \ \ | | \/ | | | | | | | _ | | | |
_ | | _ / ____ \| | | | _ | | | | __ | | | __ | |
| _____ / _ / \_ \_ | | _ | _____ \_____ | \____ /
"""
2026-03-11 12:07:08 +01:00
2026-03-11 21:17:15 +01:00
def print_header ( ) :
2026-03-11 21:30:30 +01:00
if current_theme_name == " fallout " :
console . print ( Panel ( Text ( PIPO_ART , style = " highlight " ) , border_style = " highlight " , expand = False ) )
console . print ( theme_data [ " header_text " ] )
2026-03-11 21:17:15 +01:00
console . print ( " [dim]-------------------------------------------[/dim] " )
2026-03-11 21:30:30 +01:00
console . print ( theme_data [ " welcome_text " ] )
2026-03-11 21:17:15 +01:00
console . print ( " [dim]-------------------------------------------[/dim] \n " )
2026-03-11 21:30:30 +01:00
# 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 "
2026-03-11 12:07:08 +01:00
def parse_tamigo_date ( date_str ) :
2026-03-11 21:30:30 +01:00
if not date_str : return None
2026-03-11 12:07:08 +01:00
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 ) :
2026-03-11 21:30:30 +01:00
urls = [ f " { BASE_URL } /login/application " , f " { BASE_URL } /Login " , f " { BASE_URL } /login " ]
2026-03-11 12:07:08 +01:00
for url in urls :
2026-03-11 21:30:30 +01:00
payload = { " Email " : email , " Password " : password , " Name " : " TamigoCLI " , " Key " : password }
2026-03-11 12:07:08 +01:00
try :
2026-03-11 21:30:30 +01:00
headers = { " Content-Type " : " application/json " , " Accept " : " application/json " }
2026-03-11 12:07:08 +01:00
response = requests . post ( url , json = payload , headers = headers , timeout = 15 )
if response . status_code == 200 :
2026-03-11 21:30:30 +01:00
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
2026-03-11 12:07:08 +01:00
return False
def get_employee_actual_shifts ( self , start_date_dt , end_date_dt ) :
2026-03-11 21:30:30 +01:00
if not self . session_token : return None
headers = { " x-tamigo-token " : self . session_token , " securitytoken " : self . session_token , " Accept " : " application/json " }
2026-03-11 12:07:08 +01:00
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 " )
2026-03-11 21:30:30 +01:00
if ( end_date_dt - timedelta ( days = i ) ) < start_date_dt - timedelta ( days = 60 ) : break
2026-03-11 12:07:08 +01:00
try :
2026-03-11 21:30:30 +01:00
response = requests . get ( f " { BASE_URL } /actualshifts/past/ { target_date } " , headers = headers )
if response . status_code == 200 : all_shifts . extend ( response . json ( ) )
2026-03-11 12:07:08 +01:00
elif response . status_code == 401 :
2026-03-11 21:30:30 +01:00
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
2026-03-11 12:07:08 +01:00
try :
2026-03-11 21:30:30 +01:00
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
2026-03-11 12:07:08 +01:00
return all_shifts
2026-03-13 22:30:32 +01:00
def select_date_range ( ) :
2026-03-11 12:07:08 +01:00
range_choice = questionary . select (
2026-03-11 21:30:30 +01:00
" 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 " ]
2026-03-11 12:07:08 +01:00
) . ask ( )
end_date_dt = datetime . now ( )
2026-03-11 21:30:30 +01:00
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 )
2026-03-11 12:07:08 +01:00
else :
while True :
2026-03-11 21:30:30 +01:00
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 ( )
2026-03-11 12:07:08 +01:00
try :
2026-03-11 21:30:30 +01:00
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] " )
2026-03-13 22:30:32 +01:00
return start_date_dt , end_date_dt
2026-03-11 12:07:08 +01:00
2026-03-13 22:30:32 +01:00
def get_worked_days ( client , start_date_dt , end_date_dt ) :
2026-03-11 21:30:30 +01:00
with console . status ( " [highlight]INITIALIZING DATA RETRIEVAL...[/highlight] " if current_theme_name == " fallout " else " Fetching data... " ) :
2026-03-11 12:07:08 +01:00
data = client . get_employee_actual_shifts ( start_date_dt , end_date_dt )
2026-03-14 11:52:31 +01:00
show_zero = current_config . get ( " show_zero_hours " , False )
2026-03-11 12:07:08 +01:00
if data :
2026-03-11 21:17:15 +01:00
work_days = { }
2026-03-11 12:07:08 +01:00
for item in data :
2026-03-11 21:30:30 +01:00
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 ( ) :
2026-03-11 12:07:08 +01:00
date_str = dt . strftime ( " % Y- % m- %d " )
2026-03-11 21:30:30 +01:00
hours = float ( item . get ( " ActualShiftHours " ) or item . get ( " CheckInOutShiftHours " ) or item . get ( " Hours " ) or 0 )
2026-03-11 12:07:08 +01:00
if hours == 0 and item . get ( " StartTime " ) and item . get ( " EndTime " ) :
2026-03-11 21:30:30 +01:00
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
2026-03-14 11:52:31 +01:00
# Filter zero hours if requested
if hours == 0 and not show_zero :
continue
2026-03-11 21:30:30 +01:00
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
2026-03-13 22:30:32 +01:00
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 :
2026-03-11 12:07:08 +01:00
if not work_days :
2026-03-11 21:30:30 +01:00
console . print ( " [warning]WARNING: NO LOG ENTRIES DETECTED.[/warning] " )
2026-03-11 12:07:08 +01:00
return
all_dates = sorted ( work_days . keys ( ) , reverse = True )
2026-03-11 21:30:30 +01:00
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 )
2026-03-11 21:17:15 +01:00
table . add_column ( " DATE " , style = " highlight " )
table . add_column ( " HOURS " , justify = " right " )
table . add_column ( " DETAILS " , style = " dim " )
2026-03-11 12:07:08 +01:00
for day in all_dates :
info = work_days [ day ]
2026-03-11 21:30:30 +01:00
table . add_row ( day , f " { info [ ' hours ' ] : .2f } " , str ( info [ ' text ' ] ) . upper ( ) if current_theme_name == " fallout " else str ( info [ ' text ' ] ) )
2026-03-11 12:07:08 +01:00
console . print ( table )
2026-03-11 21:30:30 +01:00
summary_text = f " PERIOD: { start_date_dt . strftime ( ' % Y- % m- %d ' ) } TO { end_date_dt . strftime ( ' % Y- % m- %d ' ) } \n DAYS WORKED: { len ( all_dates ) } \n TOTAL 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] " )
2026-03-11 12:07:08 +01:00
2026-03-13 22:30:32 +01:00
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] " )
2026-03-11 12:07:08 +01:00
def main ( ) :
2026-03-11 21:17:15 +01:00
print_header ( )
2026-03-11 12:07:08 +01:00
client = TamigoClient ( )
2026-03-11 21:30:30 +01:00
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... " ) :
2026-03-11 12:07:08 +01:00
success = client . login ( email , password )
if success :
2026-03-11 21:30:30 +01:00
console . print ( " [success]AUTHENTICATION SUCCESSFUL.[/success] " if current_theme_name == " fallout " else " [success]Login successful![/success] " )
2026-03-11 12:07:08 +01:00
menu ( client )
2026-03-11 21:30:30 +01:00
else : console . print ( " [error]CRITICAL FAILURE: INVALID CREDENTIALS.[/error] " )
2026-03-11 12:07:08 +01:00
def menu ( client ) :
while True :
2026-03-14 11:52:31 +01:00
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 "
2026-03-11 12:07:08 +01:00
choice = questionary . select (
2026-03-11 21:30:30 +01:00
" SELECT ACTION: " if current_theme_name == " fallout " else " What would you like to do? " ,
2026-03-14 11:52:31 +01:00
choices = [ " Calculate actual work days " , " Export hours worked " , " Show profile info " , " Switch UI Theme " , toggle_text , " Logout and Exit " ] ,
2026-03-11 21:30:30 +01:00
style = theme_data [ " questionary " ]
2026-03-11 12:07:08 +01:00
) . ask ( )
2026-03-11 21:30:30 +01:00
if choice == " Calculate actual work days " : calculate_checkins ( client )
2026-03-13 22:30:32 +01:00
elif choice == " Export hours worked " : export_worked_hours ( client )
2026-03-11 12:07:08 +01:00
elif choice == " Show profile info " :
2026-03-11 21:30:30 +01:00
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] " )
2026-03-14 11:52:31 +01:00
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] " )
2026-03-11 12:07:08 +01:00
else :
2026-03-11 21:30:30 +01:00
console . print ( " [highlight]SYSTEM SHUTDOWN...[/highlight] " if current_theme_name == " fallout " else " Exiting... " )
2026-03-11 12:07:08 +01:00
break
if __name__ == " __main__ " :
main ( )