From e4a8ddf8cea69886e0810005443b8930e887d2a3 Mon Sep 17 00:00:00 2001
From: sanj <67624670+iodrift@users.noreply.github.com>
Date: Mon, 28 Oct 2024 12:13:09 -0700
Subject: [PATCH] Auto-update: Mon Oct 28 12:13:09 PDT 2024

---
 .gitignore               |   1 +
 sijapi/routers/timing.py | 157 ++++++++++++++++++++++++++++++++++++++-
 2 files changed, 157 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index e3d6881..468fbd4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@ sijapi/testbed/
 khoj/
 r2r/
 podcast/sideloads/*
+sijapi/routers/reverse_directory.json
 
 **/.env
 **/.config.yaml
diff --git a/sijapi/routers/timing.py b/sijapi/routers/timing.py
index f76e604..dc14007 100644
--- a/sijapi/routers/timing.py
+++ b/sijapi/routers/timing.py
@@ -37,6 +37,8 @@ timing = APIRouter(tags=["private"])
 
 script_directory = os.path.dirname(os.path.abspath(__file__))
 
+PHONE_LOOKUP_PATH = os.path.join(script_directory, "reverse_directory.json")
+
 # Configuration constants
 pacific = pytz.timezone('America/Los_Angeles')
 
@@ -545,6 +547,159 @@ def parse_input(fields, project_name_mappings, start_times_by_date):
     return json_entries
 
 
+
+def clean_phone_number(phone: str) -> str:
+    """Clean phone number by removing special characters and handling country code."""
+    # Remove all special characters
+    cleaned = re.sub(r'[\(\)\s\+\-\.]', '', phone)
+    
+    # Handle 11-digit numbers starting with 1
+    if len(cleaned) == 11 and cleaned.startswith('1'):
+        cleaned = cleaned[1:]
+        
+    return cleaned
+
+def load_phone_lookup() -> Dict[str, str]:
+    """Load and process the phone lookup dictionary from file."""
+    try:
+        with open(PHONE_LOOKUP_PATH, 'r') as f:
+            phone_lookup = json.load(f)
+            # Clean the phone numbers in the lookup dictionary
+            return {
+                clean_phone_number(phone): name 
+                for phone, name in phone_lookup.items()
+            }
+    except FileNotFoundError:
+        l.warning(f"Phone lookup file not found at {PHONE_LOOKUP_PATH}")
+        return {}
+    except json.JSONDecodeError:
+        l.error(f"Invalid JSON in phone lookup file at {PHONE_LOOKUP_PATH}")
+        return {}
+
+async def ensure_project_exists(project_name: str) -> str:
+    """Check if project exists, create if it doesn't, and return project ID."""
+    url = f"{TIMING_API_URL}/projects"
+    headers = {
+        "Authorization": f"Bearer {TIMING_API_KEY}",
+        "Content-Type": "application/json",
+        "Accept": "application/json",
+        'X-Time-Zone': 'America/Los_Angeles'
+    }
+
+    # First check if project exists
+    async with httpx.AsyncClient() as client:
+        response = await client.get(url, headers=headers)
+        projects = response.json().get('data', [])
+        
+        for project in projects:
+            if project['title'] == project_name:
+                return project['id']
+        
+        # Project doesn't exist, create it
+        project_data = {
+            "title": project_name,
+            "color": "#007AFF",  # Nice blue color for phone calls
+            "icon": "📞"
+        }
+        
+        response = await client.post(url, headers=headers, json=project_data)
+        return response.json()['data']['id']
+
+@timing.post("/time/att_csv")
+async def process_att_csv(
+    file: UploadFile = File(...)
+):
+    """Process AT&T CSV file and post phone calls to Timing."""
+    
+    # Load the phone lookup data from file
+    cleaned_lookup = load_phone_lookup()
+    
+    # Ensure the Phone Calls project exists
+    project_name = "📞 Phone Calls"
+    try:
+        project_id = await ensure_project_exists(project_name)
+    except Exception as e:
+        l.error(f"Failed to ensure project exists: {e}")
+        raise HTTPException(
+            status_code=500,
+            detail="Failed to create or find Phone Calls project"
+        )
+
+    # Read and process the CSV
+    content = await file.read()
+    content = content.decode('utf-8')
+    
+    # Split into lines and find where the actual data starts
+    lines = content.split('\n')
+    try:
+        start_index = next(
+            i for i, line in enumerate(lines) 
+            if line.startswith('Incoming/Outgoing,Date,Time')
+        )
+    except StopIteration:
+        raise HTTPException(
+            status_code=400,
+            detail="Invalid AT&T CSV format - couldn't find header row"
+        )
+    
+    # Process only the relevant lines
+    csv_reader = csv.DictReader(lines[start_index:])
+    
+    entries = []
+    for row in csv_reader:
+        if not row.get('Contact'):  # Skip empty rows
+            continue
+            
+        # Clean the phone number for comparison
+        clean_phone = clean_phone_number(row['Contact'])
+        
+        # Look up the name or use the phone number
+        contact_name = cleaned_lookup.get(clean_phone, row['Contact'])
+        
+        # Parse the date and time
+        date_str = row['Date'].strip('"')
+        time_str = row['Time'].strip()
+        try:
+            dt = datetime.strptime(f"{date_str} {time_str}", "%b %d, %Y %I:%M %p")
+        except ValueError as e:
+            l.warning(f"Failed to parse date/time: {date_str} {time_str}")
+            continue
+        
+        # Calculate end time based on minutes
+        try:
+            minutes = int(row['Minutes'])
+            end_dt = dt + timedelta(minutes=minutes)
+        except ValueError:
+            l.warning(f"Invalid minutes value: {row['Minutes']}")
+            continue
+        
+        # Create the time entry
+        entry = {
+            "start_date": dt.strftime("%Y-%m-%dT%H:%M:%S-07:00"),
+            "end_date": end_dt.strftime("%Y-%m-%dT%H:%M:%S-07:00"),
+            "project_id": project_id,
+            "title": f"{row['Incoming/Outgoing']} - {contact_name}",
+            "notes": f"Call via {row['Type']} from {row['Location']}",
+            "replace_existing": False
+        }
+        
+        # Post to Timing API
+        try:
+            status, response = await post_time_entry_to_timing(entry)
+            if status != 200:
+                l.warning(f"Failed to post entry: {response}")
+        except Exception as e:
+            l.error(f"Error posting entry: {e}")
+            
+        entries.append(entry)
+    
+    return {
+        "message": f"Processed {len(entries)} phone calls",
+        "entries": entries,
+        "lookup_matches": sum(1 for e in entries if e['title'].split(' - ')[1] in cleaned_lookup.values())
+    }
+
+
 async def post_time_entry_to_timing(entry):
     url = f"{TIMING_API_URL}/time-entries"  # The URL for posting time entries
     headers = {
@@ -574,4 +729,4 @@ def flag_emoji(country_code: str):
 
 @timing.head("/time/")
 async def read_root():
-    return {}
\ No newline at end of file
+    return {}