import gradio as gr import requests import pandas as pd from typing import Dict, List, Optional, Tuple import json # Base URL for the API BASE_URL = "https://gljx3devrf.execute-api.us-west-1.amazonaws.com/prod/" class ContractPaymentViewer: def __init__(self): self.current_contract_data = {} self.current_monthly_data = {} def fetch_payment_history(self, contract_number: str) -> Tuple[gr.Dropdown, str, pd.DataFrame]: """ Fetch payment history for a given contract number """ if not contract_number: return gr.Dropdown(choices=[], value=None), "Please enter a contract number", pd.DataFrame() try: # Clean the contract number contract_number = contract_number.strip() # Call the API url = f"{BASE_URL}/contracts/{contract_number}/payments" response = requests.get(url, timeout=10) response.raise_for_status() data = response.json() self.current_contract_data = data # Extract monthly breakdown for dropdown monthly_breakdown = data.get('monthly_breakdown', {}) if not monthly_breakdown: return gr.Dropdown(choices=[], value=None), "No payment history found", pd.DataFrame() # Create choices for dropdown (sorted by date, newest first) choices = sorted(list(monthly_breakdown.keys()), reverse=True) # Create summary info contract_details = data.get('contract_details', {}) summary = data.get('summary', {}) # Clean job description by removing carriage returns job_description = contract_details.get('job_description', 'N/A').replace('\r', ' ') summary_info = f""" ### Contract Summary - **Contract Number:** {contract_details.get('contract_number', contract_number)} - **Contractor:** {contract_details.get('contractor_name', 'N/A')} - **Job Description:** {job_description} - **Total Payments:** {summary.get('total_payments', 0)} - **Total Disbursed:** ${summary.get('total_disbursed', 0):,.2f} - **Total Held:** ${summary.get('total_held', 0):,.2f} - **Estimated Completion:** {contract_details.get('estimated_completion', 'N/A')} - **Last Updated:** {contract_details.get('last_updated', 'N/A')} """ # Create payment history DataFrame payment_history = data.get('payment_history', []) if payment_history: history_df = pd.DataFrame(payment_history) history_df = history_df[['estimate_number', 'type', 'payment_type', 'release_date', 'disbursed_amount', 'held_amount']] history_df.columns = ['Estimate #', 'Type', 'Payment Type', 'Release Date', 'Disbursed Amount', 'Held Amount'] history_df['Disbursed Amount'] = history_df['Disbursed Amount'].apply(lambda x: f"${x:,.2f}") history_df['Held Amount'] = history_df['Held Amount'].apply(lambda x: f"${x:,.2f}") else: history_df = pd.DataFrame() return ( gr.Dropdown(choices=choices, value=choices[0] if choices else None, visible=True, interactive=True), summary_info, history_df ) except requests.exceptions.RequestException as e: return gr.Dropdown(choices=[], value=None), f"Error fetching data: {str(e)}", pd.DataFrame() except Exception as e: return gr.Dropdown(choices=[], value=None), f"Unexpected error: {str(e)}", pd.DataFrame() def fetch_monthly_details(self, contract_number: str, selected_month: str) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame, str]: """ Fetch detailed monthly payment information """ if not contract_number or not selected_month: return pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), "Please select a month" try: # Clean inputs contract_number = contract_number.strip() selected_month = selected_month.strip() # Call the API for monthly details url = f"{BASE_URL}/contracts/{contract_number}/payments/monthly/{selected_month}" response = requests.get(url, timeout=10) response.raise_for_status() monthly_data = response.json() self.current_monthly_data = monthly_data # Process the payment data payment_info = monthly_data.get('payments', [{}])[0] if monthly_data.get('payments') else {} # Create monthly summary DataFrame summary_data = { 'Metric': [ 'Month', 'Payment Count', 'Total Disbursed', 'Total Held', 'Estimate Number', 'Payment Type', 'Release Date' ], 'Value': [ monthly_data.get('month', selected_month), monthly_data.get('payment_count', 0), f"${monthly_data.get('total_disbursed', 0):,.2f}", f"${monthly_data.get('total_held', 0):,.2f}", payment_info.get('estimate_number', 'N/A'), payment_info.get('payment_type', 'N/A'), payment_info.get('release_date', 'N/A') ] } summary_df = pd.DataFrame(summary_data) # Create detailed items DataFrame items_df = pd.DataFrame() if payment_info and 'detailed_estimate' in payment_info: detailed_estimate = payment_info['detailed_estimate'] prime_items = detailed_estimate.get('prime_contract', {}).get('items', []) if prime_items: # Process items for display items_data = [] for item in prime_items: if item.get('amounts', {}).get('this_period', 0) != 0: # Only show items with activity items_data.append({ 'Item #': item.get('item_number', ''), 'Description': item.get('description', '')[:50] + '...' if len(item.get('description', '')) > 50 else item.get('description', ''), 'Unit': item.get('units', ''), 'Unit Price': f"${item.get('unit_price', 0):,.2f}", 'Qty This Period': f"{item.get('quantities', {}).get('this_period', 0):,.2f}", 'Amount This Period': f"${item.get('amounts', {}).get('this_period', 0):,.2f}", 'Job to Date': f"${item.get('amounts', {}).get('job_to_date', 0):,.2f}", '% Complete': f"{item.get('quantities', {}).get('percent_jtd', 0):.1f}%" }) if items_data: items_df = pd.DataFrame(items_data) # Sort by amount this period (descending) items_df['sort_value'] = items_df['Amount This Period'].str.replace('$', '').str.replace(',', '').astype(float) items_df = items_df.sort_values('sort_value', ascending=False).drop('sort_value', axis=1) else: # Create empty dataframe with message items_df = pd.DataFrame({'Message': ['No line items with activity for this period']}) else: items_df = pd.DataFrame({'Message': ['No line items available']}) else: items_df = pd.DataFrame({'Message': ['No line items available']}) # Create deductions DataFrame deductions_df = pd.DataFrame() if payment_info and 'detailed_estimate' in payment_info: deductions = payment_info['detailed_estimate'].get('deductions', []) if deductions: deductions_data = [] for deduction in deductions: deductions_data.append({ 'Estimate #': deduction.get('estimate_no', ''), 'Description': deduction.get('description', ''), 'This Period': f"${deduction.get('this_period', 0):,.2f}", 'Job to Date': f"${deduction.get('job_to_date', 0):,.2f}" }) deductions_df = pd.DataFrame(deductions_data) else: deductions_df = pd.DataFrame({'Message': ['No deductions for this period']}) else: deductions_df = pd.DataFrame({'Message': ['No deductions available']}) # Create change orders DataFrame change_orders_df = pd.DataFrame() if payment_info and 'detailed_estimate' in payment_info: change_orders = payment_info['detailed_estimate'].get('change_orders', {}).get('items', []) if change_orders: change_orders_data = [] for co in change_orders: change_orders_data.append({ 'Change Order #': co.get('change_order_no', ''), 'Description': co.get('description', '')[:50] + '...' if len(co.get('description', '')) > 50 else co.get('description', ''), 'This Period': f"${co.get('this_period_amt', 0):,.2f}", 'Job to Date': f"${co.get('job_to_date_amt', 0):,.2f}", 'Status': co.get('status', 'N/A') }) if change_orders_data: change_orders_df = pd.DataFrame(change_orders_data) # Sort by amount this period (descending) change_orders_df['sort_value'] = change_orders_df['This Period'].str.replace('$', '').str.replace(',', '').astype(float) change_orders_df = change_orders_df.sort_values('sort_value', ascending=False).drop('sort_value', axis=1) else: change_orders_df = pd.DataFrame({'Message': ['No change orders for this period']}) else: change_orders_df = pd.DataFrame({'Message': ['No change orders available']}) else: change_orders_df = pd.DataFrame({'Message': ['No change orders available']}) # Create totals summary if payment_info and 'detailed_estimate' in payment_info: totals = payment_info['detailed_estimate'].get('overall_totals', {}) totals_info = f""" ### Payment Totals for {selected_month} - **Original Contract Amount:** ${totals.get('original', 0):,.2f} - **This Period:** ${totals.get('this_period', 0):,.2f} - **Job to Date:** ${totals.get('job_to_date', 0):,.2f} - **Extra Work This Estimate:** ${totals.get('extra_work_this_estimate', 0):,.2f} - **Extra Work to Date:** ${totals.get('extra_work_to_date', 0):,.2f} """ else: totals_info = "No detailed totals available" return summary_df, items_df, deductions_df, change_orders_df, totals_info except requests.exceptions.RequestException as e: return pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), f"Error fetching monthly details: {str(e)}" except Exception as e: return pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), f"Unexpected error: {str(e)}" # Create instance of the viewer viewer = ContractPaymentViewer() # Create Gradio interface with gr.Blocks(title="Contract Payment Viewer", theme=gr.themes.Soft()) as app: gr.Markdown("# Contract Payment Viewer") gr.Markdown("Enter a contract number to view payment history and detailed monthly breakdowns") with gr.Row(): with gr.Column(scale=1): contract_input = gr.Textbox( label="Contract Number", placeholder="e.g., 03-3F0704", value="03-3F0704" ) fetch_button = gr.Button("Fetch Payment History", variant="primary") with gr.Row(): with gr.Column(): summary_output = gr.Markdown(label="Contract Summary") with gr.Row(): with gr.Column(scale=1): month_dropdown = gr.Dropdown( label="Select Month", choices=[], visible=False ) fetch_monthly_button = gr.Button("Get Monthly Details", variant="secondary", visible=False) with gr.Row(): with gr.Column(): with gr.Accordion("Payment History", open=False): history_table = gr.Dataframe( label="Payment History", wrap=True ) # Monthly details section with gr.Row(visible=False) as monthly_section: with gr.Column(): gr.Markdown("## Monthly Payment Details") with gr.Tabs(): with gr.TabItem("Summary"): monthly_summary_table = gr.Dataframe( label="Monthly Summary", wrap=True ) totals_output = gr.Markdown(label="Payment Totals") with gr.TabItem("Line Items"): items_table = gr.Dataframe( label="Contract Items (This Period Activity)", wrap=True ) with gr.TabItem("Deductions"): deductions_table = gr.Dataframe( label="Deductions", wrap=True ) with gr.TabItem("Change Orders"): change_orders_table = gr.Dataframe( label="Change Orders", wrap=True ) # Event handlers def handle_fetch_history(contract_number): dropdown, summary, history = viewer.fetch_payment_history(contract_number) return { month_dropdown: dropdown, summary_output: summary, history_table: history, fetch_monthly_button: gr.Button(visible=True if dropdown.choices else False), monthly_section: gr.Row(visible=False) } def handle_fetch_monthly(contract_number, selected_month): summary_df, items_df, deductions_df, change_orders_df, totals = viewer.fetch_monthly_details(contract_number, selected_month) return { monthly_summary_table: summary_df, items_table: items_df, deductions_table: deductions_df, change_orders_table: change_orders_df, totals_output: totals, monthly_section: gr.Row(visible=True) } fetch_button.click( fn=handle_fetch_history, inputs=[contract_input], outputs=[month_dropdown, summary_output, history_table, fetch_monthly_button, monthly_section] ) fetch_monthly_button.click( fn=handle_fetch_monthly, inputs=[contract_input, month_dropdown], outputs=[monthly_summary_table, items_table, deductions_table, change_orders_table, totals_output, monthly_section] ) # Auto-fetch monthly details when dropdown changes month_dropdown.change( fn=handle_fetch_monthly, inputs=[contract_input, month_dropdown], outputs=[monthly_summary_table, items_table, deductions_table, change_orders_table, totals_output, monthly_section] ) # Launch the app if __name__ == "__main__": app.launch( share=True, server_name="0.0.0.0", server_port=7860, show_error=True, debug=False, max_threads=10 )