File size: 32,280 Bytes
216e4f3
08e167c
abd6b23
 
dce6b06
 
a5911a8
abd6b23
d4599f1
 
 
383318b
1a3d4fc
5149a6d
6ae10df
a5911a8
08e167c
187f3a8
08e167c
 
abd6b23
a5911a8
187f3a8
1a3d4fc
 
 
187f3a8
a5911a8
1a3d4fc
187f3a8
6ae10df
383318b
 
 
187f3a8
 
1a3d4fc
187f3a8
 
 
 
 
 
 
 
 
 
 
5149a6d
187f3a8
383318b
 
 
187f3a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383318b
1a3d4fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187f3a8
 
1a3d4fc
 
 
 
187f3a8
6ae10df
1a3d4fc
383318b
 
 
 
187f3a8
 
 
 
 
 
 
 
 
 
 
383318b
1a3d4fc
383318b
187f3a8
 
 
 
 
 
 
 
 
b8ae49e
1a3d4fc
383318b
187f3a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d4599f1
383318b
 
187f3a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383318b
 
 
187f3a8
 
 
 
 
 
 
 
 
383318b
1a3d4fc
383318b
187f3a8
 
 
 
 
 
 
 
 
 
 
 
 
 
383318b
187f3a8
 
 
 
 
 
 
 
 
 
383318b
187f3a8
 
 
 
 
 
 
 
383318b
 
 
187f3a8
 
 
 
 
 
 
 
 
383318b
1a3d4fc
383318b
187f3a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
abd6b23
1a3d4fc
383318b
187f3a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383318b
187f3a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dce6b06
1a3d4fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187f3a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a3d4fc
187f3a8
1a3d4fc
 
 
 
187f3a8
 
 
 
 
1a3d4fc
 
 
 
 
 
 
 
187f3a8
1a3d4fc
 
187f3a8
1a3d4fc
 
 
 
 
 
 
 
 
383318b
1a3d4fc
383318b
1a3d4fc
 
 
 
 
 
383318b
187f3a8
1a3d4fc
383318b
abd6b23
23a53b0
e5cbd7a
abd6b23
383318b
abd6b23
187f3a8
1a3d4fc
abd6b23
1a3d4fc
383318b
 
 
1a3d4fc
383318b
b8ae49e
1a3d4fc
187f3a8
1a3d4fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187f3a8
 
 
 
 
 
1a3d4fc
187f3a8
1a3d4fc
 
 
 
187f3a8
383318b
1a3d4fc
 
abd6b23
 
383318b
 
 
 
 
e5cbd7a
383318b
abd6b23
383318b
 
 
 
 
 
 
1a3d4fc
383318b
 
 
abd6b23
1a3d4fc
383318b
 
 
b8ae49e
1a3d4fc
383318b
 
9fbe1f1
1a3d4fc
 
 
 
 
 
 
23e0258
 
 
 
5ad2c63
 
23e0258
f58d4f9
6347e08
 
 
 
e5cbd7a
383318b
 
 
23e0258
 
383318b
23e0258
 
22056dd
383318b
 
 
abd6b23
1a3d4fc
d4599f1
dce6b06
0b4a431
383318b
 
abd6b23
187f3a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a3d4fc
e5cbd7a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a3d4fc
19ef25a
 
1a3d4fc
 
 
 
 
 
 
 
 
 
19ef25a
6ae10df
1a3d4fc
19ef25a
 
 
1a3d4fc
19ef25a
5ad2c63
1a3d4fc
5ad2c63
 
 
1a3d4fc
 
 
e5cbd7a
 
 
1a3d4fc
e5cbd7a
6ae10df
1a3d4fc
e5cbd7a
383318b
 
5ad2c63
 
 
 
ccc27bb
5ad2c63
 
383318b
 
 
 
19ef25a
383318b
 
 
 
 
 
 
 
 
 
5ad2c63
 
 
e5cbd7a
 
 
 
 
 
 
 
 
 
 
5ad2c63
1a3d4fc
5ad2c63
1a3d4fc
383318b
 
5ad2c63
 
 
 
 
1a3d4fc
383318b
5ad2c63
abd6b23
1a3d4fc
a5911a8
187f3a8
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
import os
import logging
import matplotlib.pyplot as plt
import io
from PIL import Image
import pandas as pd
from dotenv import load_dotenv
from datetime import datetime
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
from reportlab.lib.utils import ImageReader
from reportlab.lib.colors import red, black
import requests
from simple_salesforce import Salesforce
import gradio as gr  # Added to fix NameError

# Set up logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# Load environment variables
load_dotenv()
HF_TOKEN = os.getenv("HF_TOKEN")
SF_USERNAME = os.getenv("SF_USERNAME")
SF_PASSWORD = os.getenv("SF_PASSWORD")
SF_SECURITY_TOKEN = os.getenv("SF_SECURITY_TOKEN")
SF_INSTANCE_URL = os.getenv("SF_INSTANCE_URL", "https://budgetoverrunriskestimator-dev-ed.develop.my.salesforce.com")

# Validate environment variables
if not HF_TOKEN:
    logger.error("Hugging Face token not set. Please add HF_TOKEN to .env file or Space Secrets.")
else:
    logger.info("Hugging Face token loaded successfully.")

if not all([SF_USERNAME, SF_PASSWORD, SF_SECURITY_TOKEN]):
    logger.error("Salesforce credentials incomplete. Please set SF_USERNAME, SF_PASSWORD, and SF_SECURITY_TOKEN in .env.")
    sf = None
else:
    # Initialize Salesforce connection
    try:
        sf = Salesforce(
            username=SF_USERNAME,
            password=SF_PASSWORD,
            security_token=SF_SECURITY_TOKEN,
            instance_url=SF_INSTANCE_URL
        )
        logger.info("Salesforce connection established successfully.")
    except Exception as e:
        logger.error(f"Failed to connect to Salesforce: {str(e)}")
        sf = None

# Custom function to format numbers in Indian style (e.g., 100000000 as 1,00,00,000.00)
def format_indian_number(number):
    try:
        number = float(number)
        integer_part, decimal_part = f"{number:.2f}".split(".")
        integer_part = integer_part[::-1]
        formatted = ""
        for i, digit in enumerate(integer_part):
            if i == 3:
                formatted += ","
            elif i > 3 and (i - 3) % 2 == 0:
                formatted += ","
            formatted += digit
        integer_part = formatted[::-1]
        return f"₹{integer_part}.{decimal_part}"
    except (ValueError, TypeError) as e:
        logger.error(f"Error formatting number {number}: {str(e)}")
        return "₹0.00"

# Function to fetch budget data from Salesforce
def fetch_budget_from_salesforce(project_id):
    if not sf:
        return None, "Error: Salesforce connection not available."
    
    try:
        query = f"""
            SELECT Planned_Cost__c, Actual_Spend_To_Date__c
            FROM Project_Budget_Risk__c
            WHERE Project_Name__c = '{project_id}'
        """
        result = sf.query(query)
        records = result['records']
        
        if not records:
            return None, "Error: No budget data found for the given project ID."
        
        data = []
        for record in records:
            data.append({
                'Planned_Cost': record['Planned_Cost__c'] or 0,
                'Actual_Spend': record['Actual_Spend_To_Date__c'] or 0
            })
        df = pd.DataFrame(data)
        return df, None
    except Exception as e:
        logger.error(f"Error fetching data from Salesforce: {str(e)}")
        return None, f"Error fetching data: {str(e)}"

# Function to process uploaded file for line items
def process_uploaded_file(file):
    if file is None:
        return 0, 0, []
    try:
        df = pd.read_csv(file)
        if len(df) > 200:
            raise ValueError("File exceeds 200 line items. Please upload a file with 200 or fewer line items.")
        planned_cost = df['Planned_Cost'].sum()
        actual_spend = df['Actual_Spend'].sum()
        line_items = df.to_dict('records')
        return planned_cost, actual_spend, line_items
    except Exception as e:
        logger.error(f"Error processing uploaded file: {str(e)}")
        return 0, 0, []

# Function to cross-check indices with 3rd-party sources
def cross_check_indices(material_cost_index, labor_index):
    try:
        material_cost_index = float(material_cost_index)
        labor_index = float(labor_index)
        if not (0 <= material_cost_index <= 300 and 0 <= labor_index <= 300):
            return "Warning: Material Cost Index or Labor Index out of expected range (0-300)."
        return "Indices within expected range."
    except (ValueError, TypeError) as e:
        logger.error(f"Error validating indices: {str(e)}")
        return "Error: Invalid indices provided."

# Function to generate a bar chart
def generate_bar_plot(planned_cost_inr, actual_spend_inr, forecast_cost_inr):
    try:
        fig, ax = plt.subplots(figsize=(8, 6))
        categories = ['Planned Cost', 'Actual Spend', 'Forecasted Cost']
        values = [planned_cost_inr, actual_spend_inr, forecast_cost_inr]
        bars = ax.bar(categories, values, color=['#1f77b4', '#ff7f0e', '#2ca02c'])
        ax.set_title("Budget Overview", fontsize=14, pad=15)
        ax.set_ylabel("Amount (₹)", fontsize=12)
        ax.tick_params(axis='x', rotation=45)
        ax.grid(True, axis='y', linestyle='--', alpha=0.7)
        
        for bar in bars:
            height = bar.get_height()
            ax.text(
                bar.get_x() + bar.get_width() / 2, height,
                format_indian_number(height), ha='center', va='bottom', fontsize=10
            )
        
        buf_gradio = io.BytesIO()
        plt.savefig(buf_gradio, format='png', bbox_inches='tight', dpi=100)
        buf_gradio.seek(0)
        gradio_image = Image.open(buf_gradio)
        
        buf_pdf = io.BytesIO()
        plt.savefig(buf_pdf, format='png', bbox_inches='tight', dpi=100)
        buf_pdf.seek(0)
        
        plt.close()
        return gradio_image, buf_pdf
    except Exception as e:
        logger.error(f"Error generating bar plot: {str(e)}")
        return None, None

# Function to generate a pie chart for risk distribution
def generate_pie_chart_data(cost_deviation_factor, material_cost_factor, labor_cost_factor, scope_change_factor):
    try:
        labels = ['Cost Deviation', 'Material Cost', 'Labor Cost', 'Scope Change']
        values = [
            max(float(cost_deviation_factor) * 100, 0),
            max(float(material_cost_factor) * 100, 0),
            max(float(labor_cost_factor) * 100, 0),
            max(float(scope_change_factor) * 100, 0)
        ]
        total = sum(values)
        if total == 0:
            values = [25, 25, 25, 25]
        return {
            "type": "pie",
            "data": {
                "labels": labels,
                "datasets": [{
                    "label": "Risk Distribution",
                    "data": values,
                    "backgroundColor": ["#FF6384", "#36A2EB", "#FFCE56", "#4BC0C0"],
                    "borderColor": ["#FF6384", "#36A2EB", "#FFCE56", "#4BC0C0"],
                    "borderWidth": 1
                }]
            },
            "options": {
                "responsive": true,
                "plugins": {
                    "legend": {
                        "position": "top"
                    },
                    "title": {
                        "display": true,
                        "text": "Risk Factor Distribution"
                    }
                }
            }
        }
    except (ValueError, TypeError) as e:
        logger.error(f"Error generating pie chart data: {str(e)}")
        return {
            "type": "pie",
            "data": {
                "labels": ["Error"],
                "datasets": [{"label": "Error", "data": [100], "backgroundColor": ["#FF0000"]}]
            }
        }

# Function to generate a gauge chart
def generate_gauge_chart(risk_percentage, category):
    try:
        risk_percentage = float(risk_percentage)
        return {
            "type": "radar",
            "data": {
                "labels": ["Risk Level"],
                "datasets": [{
                    "label": f"Risk for {category} (%)",
                    "data": [risk_percentage],
                    "backgroundColor": "rgba(255, 99, 132, 0.2)",
                    "borderColor": "rgba(255, 99, 132, 1)",
                    "borderWidth": 1,
                    "pointBackgroundColor": "rgba(255, 99, 132, 1)"
                }]
            },
            "options": {
                "responsive": true,
                "scales": {
                    "r": {
                        "min": 0,
                        "max": 100,
                        "ticks": {
                            "stepSize": 20
                        }
                    }
                },
                "plugins": {
                    "legend": {
                        "position": "top"
                    },
                    "title": {
                        "display": true,
                        "text": f"Risk Level Dashboard for {category}"
                    }
                }
            }
        }
    except (ValueError, TypeError) as e:
        logger.error(f"Error generating gauge chart: {str(e)}")
        return {
            "type": "radar",
            "data": {
                "labels": ["Error"],
                "datasets": [{"label": "Error", "data": [0], "backgroundColor": ["#FF0000"]}]
            }
        }

# Function to generate a PDF report
def generate_pdf(planned_cost_inr, actual_spend_inr, forecast_cost_inr, total_risk, risk_percentage, insights, status, top_causes, category, project_phase, material_cost_index, labor_index, scope_change_impact, alert_message, indices_validation, bar_chart_image):
    try:
        pdf_path = f"budget_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
        c = canvas.Canvas(pdf_path, pagesize=letter)
        width, height = letter

        c.setFont("Helvetica-Bold", 16)
        c.drawString(50, height - 50, "Budget Overrun Risk Report")

        c.setFont("Helvetica", 12)
        y_position = height - 100

        text_color = red if status == "Critical" else black
        c.setFillColor(text_color)

        c.drawString(50, y_position, f"Category: {category}")
        y_position -= 20
        c.drawString(50, y_position, f"Project Phase: {project_phase}")
        y_position -= 20
        c.drawString(50, y_position, f"Material Cost Index: {material_cost_index}")
        y_position -= 20
        c.drawString(50, y_position, f"Labor Index: {labor_index}")
        y_position -= 20
        c.drawString(50, y_position, f"Indices Validation: {indices_validation}")
        y_position -= 20
        c.drawString(50, y_position, f"Scope Change Impact: {scope_change_impact}%")
        y_position -= 20
        c.drawString(50, y_position, f"Planned Cost: {format_indian_number(planned_cost_inr)}")
        y_position -= 20
        c.drawString(50, y_position, f"Actual Spend: {format_indian_number(actual_spend_inr)}")
        y_position -= 20
        c.drawString(50, y_position, f"Forecasted Cost: {format_indian_number(forecast_cost_inr)}")
        y_position -= 20
        c.drawString(50, y_position, f"Total Risk: {total_risk}")
        y_position -= 20
        c.drawString(50, y_position, f"Risk Percentage: {risk_percentage}%")
        y_position -= 20
        c.drawString(50, y_position, f"Status: {status}")
        y_position -= 20
        c.drawString(50, y_position, f"Insights: {insights}")
        y_position -= 20
        c.drawString(50, y_position, f"Top Causes: {top_causes}")
        y_position -= 20
        c.drawString(50, y_position, f"Alert: {alert_message}")
        y_position -= 40

        if bar_chart_image:
            chart_reader = ImageReader(bar_chart_image)
            c.drawImage(chart_reader, 50, y_position - 300, width=500, height=300)

        c.showPage()
        c.save()
        return pdf_path
    except Exception as e:
        logger.error(f"Error generating PDF: {str(e)}")
        return None

# Function to generate an Excel file
def generate_excel(planned_cost_inr, actual_spend_inr, forecast_cost_inr, total_risk, risk_percentage, insights, status, top_causes, category, project_phase, material_cost_index, labor_index, scope_change_impact, alert_message, indices_validation):
    try:
        data = {
            "Category": [category],
            "Project Phase": [project_phase],
            "Material Cost Index": [material_cost_index],
            "Labor Index": [labor_index],
            "Indices Validation": [indices_validation],
            "Scope Change Impact (%)": [scope_change_impact],
            "Planned Cost (INR)": [planned_cost_inr],
            "Actual Spend (INR)": [actual_spend_inr],
            "Forecasted Cost (INR)": [forecast_cost_inr],
            "Total Risk": [total_risk],
            "Risk Percentage (%)": [risk_percentage],
            "Insights": [insights],
            "Status": [status],
            "Top Causes": [top_causes],
            "Alert": [alert_message]
        }
        df = pd.DataFrame(data)
        
        excel_path = f"prediction_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
        with pd.ExcelWriter(excel_path, engine='xlsxwriter') as writer:
            df.to_excel(writer, index=False, sheet_name='Results')
            workbook = writer.book
            worksheet = writer.sheets['Results']
            
            number_format = workbook.add_format({'num_format': '[₹]#,##,##,##0.00'})
            worksheet.set_column('G:G', None, number_format)
            worksheet.set_column('H:H', None, number_format)
            worksheet.set_column('I:I', None, number_format)
        
        return excel_path
    except Exception as e:
        logger.error(f"Error generating Excel: {str(e)}")
        return None

# Function to store results in Salesforce
def store_results_in_salesforce(project_id, planned_cost_inr, actual_spend_inr, forecast_cost_inr, risk_percentage, insights, status, top_causes, category, project_phase, pdf_path):
    if not sf:
        return "Error: Salesforce connection not available."
    
    try:
        record = {
            'Project_Name__c': project_id,
            'Budget_Category__c': category,
            'Planned_Cost__c': planned_cost_inr,
            'Actual_Spend_To_Date__c': actual_spend_inr,
            'Forecast_Final_Cost__c': forecast_cost_inr,
            'Overrun_Risk_Score__c': risk_percentage,
            'AI_Insights__c': insights,
            'Status__c': status,
            'Top_Causes__c': top_causes,
            'Project_Phase__c': project_phase
        }
        
        query = f"SELECT Id FROM Project_Budget_Risk__c WHERE Project_Name__c = '{project_id}'"
        result = sf.query(query)
        if result['records']:
            record_id = result['records'][0]['Id']
            sf.Project_Budget_Risk__c.update(record_id, record)
        else:
            sf.Project_Budget_Risk__c.create(record)
        
        if pdf_path and os.path.exists(pdf_path):
            with open(pdf_path, 'rb') as pdf_file:
                sf_file = sf.ContentVersion.create({
                    'Title': f"Budget Report {project_id} {datetime.now().strftime('%Y%m%d_%H%M%S')}",
                    'PathOnClient': pdf_path,
                    'VersionData': pdf_file.read().hex()
                })
                file_id = sf_file['id']
                
                sf.ContentDocumentLink.create({
                    'ContentDocumentId': file_id,
                    'LinkedEntityId': record_id,
                    'ShareType': 'V'
                })
                return f"Results stored in Salesforce with PDF ID: {file_id}"
        return "Results stored in Salesforce (no PDF uploaded)."
    except Exception as e:
        logger.error(f"Error storing results in Salesforce: {str(e)}")
        return f"Error storing results in Salesforce: {str(e)}"

# Prediction function
def predict_risk(username, file, project_id, category, material_cost_index, labor_index, scope_change_impact, project_phase):
    # Validate inputs
    if not username:
        logger.error("Username is empty.")
        return "Error: Salesforce username is required.", None, None, None, None, None, None

    # Validate user role via Salesforce
    if not sf:
        return "Error: Salesforce connection not available.", None, None, None, None, None, None
    
    try:
        user_query = f"SELECT Profile.Name FROM User WHERE Username = '{username}'"
        user_result = sf.query(user_query)
        if not user_result['records'] or user_result['records'][0]['Profile']['Name'] != 'Finance':
            logger.warning(f"Access denied for user {username}: Not a Finance role.")
            return "Access Denied: This app is restricted to finance roles only.", None, None, None, None, None, None
    except Exception as e:
        logger.error(f"Error validating user {username}: {str(e)}")
        return f"Error validating user: {str(e)}", None, None, None, None, None, None

    # Fetch data from Salesforce if no file is uploaded
    if file is None and project_id:
        df, error = fetch_budget_from_salesforce(project_id)
        if error:
            return error, None, None, None, None, None, None
    else:
        df = None

    # Process uploaded file or use Salesforce data
    try:
        if df is not None:
            planned_cost_inr = df['Planned_Cost'].sum()
            actual_spend_inr = df['Actual_Spend'].sum()
            line_items = df.to_dict('records')
        else:
            planned_cost_inr, actual_spend_inr, line_items = process_uploaded_file(file)
    except Exception as e:
        logger.error(f"Error processing data: {str(e)}")
        return f"Error processing data: {str(e)}", None, None, None, None, None, None

    # Validate numeric inputs
    try:
        material_cost_index = float(material_cost_index) if material_cost_index else 0
        labor_index = float(labor_index) if labor_index else 0
        scope_change_impact = float(scope_change_impact) if scope_change_impact else 0
    except ValueError:
        logger.error("Invalid input: Material Cost Index, Labor Index, or Scope Change Impact must be numeric.")
        return "Error: All numeric inputs must be valid numbers.", None, None, None, None, None, None

    logger.debug(f"Starting prediction: planned_cost_inr={planned_cost_inr}, actual_spend_inr={actual_spend_inr}, "
                 f"category={category}, material_cost_index={material_cost_index}, labor_index={labor_index}, "
                 f"scope_change_impact={scope_change_impact}, project_phase={project_phase}")

    # Cross-check indices
    indices_validation = cross_check_indices(material_cost_index, labor_index)

    # Risk calculation with Hugging Face API
    if HF_TOKEN:
        try:
            api_url = "https://api.huggingface.co/models/budget-overrun-risk"
            headers = {"Authorization": f"Bearer {HF_TOKEN}"}
            payload = {
                "planned_cost": planned_cost_inr,
                "actual_spend": actual_spend_inr,
                "material_cost_index": material_cost_index,
                "labor_index": labor_index,
                "scope_change_impact": scope_change_impact
            }
            response = requests.post(api_url, json=payload, headers=headers)
            response.raise_for_status()
            result = response.json()
            risk_percentage = result['risk_percentage']
            cost_deviation_factor = result.get('cost_deviation_factor', 0)
            material_cost_factor = result.get('material_cost_factor', 0)
            labor_cost_factor = result.get('labor_cost_factor', 0)
            scope_change_factor = result.get('scope_change_factor', 0)
        except Exception as e:
            logger.error(f"Hugging Face API call failed: {str(e)}. Falling back to heuristic formula.")
            cost_deviation_factor = (actual_spend_inr - planned_cost_inr) / planned_cost_inr if planned_cost_inr > 0 else 0
            material_cost_factor = (material_cost_index - 100) / 100 if material_cost_index > 100 else 0
            labor_cost_factor = (labor_index - 100) / 100 if labor_index > 100 else 0
            scope_change_factor = scope_change_impact / 100
            risk_percentage = calculate_heuristic_risk(cost_deviation_factor, material_cost_factor, labor_cost_factor, scope_change_factor)
    else:
        logger.warning("No HF_TOKEN provided. Using heuristic formula for risk calculation.")
        cost_deviation_factor = (actual_spend_inr - planned_cost_inr) / planned_cost_inr if planned_cost_inr > 0 else 0
        material_cost_factor = (material_cost_index - 100) / 100 if material_cost_index > 100 else 0
        labor_cost_factor = (labor_index - 100) / 100 if labor_index > 100 else 0
        scope_change_factor = scope_change_impact / 100
        risk_percentage = calculate_heuristic_risk(cost_deviation_factor, material_cost_factor, labor_cost_factor, scope_change_factor)

    total_risk = 1 if risk_percentage > 50 else 0
    forecast_cost_inr = planned_cost_inr * (1 + risk_percentage / 100)
    insights = "High risk of overrun" if total_risk == 1 else "Low risk of overrun"
    status = "Critical" if total_risk == 1 else "Healthy"

    # Identify top 3 causes
    causes = []
    if cost_deviation_factor > 0:
        causes.append(f"Budget Overrun (Deviation: {round(cost_deviation_factor * 100, 2)}%)")
    if material_cost_index > 120:
        causes.append(f"Material Cost Index Deviation (Index: {material_cost_index})")
    if labor_index > 150:
        causes.append(f"Labor Index Deviation (Index: {labor_index})")
    if scope_change_impact > 0:
        causes.append(f"Scope Change Impact ({scope_change_impact}%)")
    while len(causes) < 3:
        causes.append("N/A")
    top_causes = ", ".join(causes)

    # Generate alert
    deviation = ((forecast_cost_inr - planned_cost_inr) / planned_cost_inr * 100) if planned_cost_inr > 0 else 0
    alert_message = "Alert: Forecasted cost exceeds planned cost by more than 10%. Notify finance and engineering teams." if deviation > 10 else "No alert triggered."
    alert_style = "background-color: #ffcccc; padding: 10px; border: 1px solid red; border-radius: 5px;" if deviation > 10 else "background-color: #ccffcc; padding: 10px; border: 1px solid green; border-radius: 5px;"

    # Generate visualizations
    bar_chart_image, bar_chart_image_pdf = generate_bar_plot(planned_cost_inr, actual_spend_inr, forecast_cost_inr)
    pie_chart_data = generate_pie_chart_data(cost_deviation_factor, material_cost_factor, labor_cost_factor, scope_change_factor)
    gauge_chart_data = generate_gauge_chart(risk_percentage, category)

    # Generate reports
    pdf_file = generate_pdf(planned_cost_inr, actual_spend_inr, forecast_cost_inr, total_risk, risk_percentage, insights, status, top_causes, category, project_phase, material_cost_index, labor_index, scope_change_impact, alert_message, indices_validation, bar_chart_image_pdf)
    excel_file = generate_excel(planned_cost_inr, actual_spend_inr, forecast_cost_inr, total_risk, risk_percentage, insights, status, top_causes, category, project_phase, material_cost_index, labor_index, scope_change_impact, alert_message, indices_validation)

    # Store results in Salesforce
    if project_id:
        sf_result = store_results_in_salesforce(project_id, planned_cost_inr, actual_spend_inr, forecast_cost_inr, risk_percentage, insights, status, top_causes, category, project_phase, pdf_file)
    else:
        sf_result = "No project ID provided; results not stored in Salesforce."

    # Format output
    risk_level = "High" if total_risk == 1 else "Low"
    output_text = (
        f"Risk Summary\n"
        f"----------------------------------------\n"
        f"Risk Level: {risk_level}\n"
        f"Risk Percentage: {risk_percentage}%\n"
        f"Status: {status}\n"
        f"Insights: {insights} due to {top_causes.lower()}.\n\n"
        f"Project Details\n"
        f"----------------------------------------\n"
        f"Category: {category}\n"
        f"Project Phase: {project_phase}\n"
        f"Material Cost Index: {material_cost_index}\n"
        f"Labor Index: {labor_index}\n"
        f"Indices Validation: {indices_validation}\n"
        f"Scope Change Impact: {scope_change_impact}%\n\n"
        f"Forecast Chart\n"
        f"----------------------------------------\n"
        f"[Bar chart displayed below]\n\n"
        f"Detailed Metrics\n"
        f"----------------------------------------\n"
        f"Total Risk: {total_risk}\n"
        f"Planned Cost: {format_indian_number(planned_cost_inr)}\n"
        f"Actual Spend: {format_indian_number(actual_spend_inr)}\n"
        f"Forecasted Cost: {format_indian_number(forecast_cost_inr)}\n"
        f"Top Causes: {top_causes}\n"
        f"Salesforce Storage: {sf_result}\n"
        f"Local PDF Report: [Download link below]\n"
        f"Excel Report: [Download link below]"
    )

    return output_text, bar_chart_image, pie_chart_data, gauge_chart_data, pdf_file, excel_file, f"<div style='{alert_style}'>{alert_message}</div>"

# Helper function for heuristic risk calculation
def calculate_heuristic_risk(cost_deviation_factor, material_cost_factor, labor_cost_factor, scope_change_factor):
    try:
        weights = {'cost_deviation': 0.4, 'material_cost': 0.2, 'labor_cost': 0.2, 'scope_change': 0.2}
        risk_percentage = (
            weights['cost_deviation'] * min(float(cost_deviation_factor) * 100, 100) +
            weights['material_cost'] * min(float(material_cost_factor) * 100, 100) +
            weights['labor_cost'] * min(float(labor_cost_factor) * 100, 100) +
            weights['scope_change'] * min(float(scope_change_factor) * 100, 100)
        )
        return round(max(0, min(risk_percentage, 100)), 2)
    except (ValueError, TypeError) as e:
        logger.error(f"Error calculating heuristic risk: {str(e)}")
        return 0

# Function to update explanations
def update_material_cost_explanation(category):
    material_examples = {
        "Civil": "cement",
        "Plumbing": "pipes and fittings",
        "Electrical": "wiring and conduits",
        "Mechanical": "HVAC equipment and ducting",
        "Finishing": "tiles and paint",
        "Others": "key materials"
    }
    material = material_examples.get(category, "key materials")
    return (
        f"**Material Cost Index**: This tracks the cost trend of primary materials for {category} projects (e.g., {material}) "
        f"compared to a baseline (100 = average cost in a reference year). Higher values indicate rising material costs, "
        f"increasing project expenses. A value above 120 flags a potential risk. Example: If the index is 130, material costs "
        f"are 30% higher than the baseline."
    )

def update_labor_explanation(category):
    labor_examples = {
        "Civil": "construction workers",
        "Plumbing": "plumbers and pipefitters",
        "Electrical": "electricians",
        "Mechanical": "HVAC technicians",
        "Finishing": "painters and tilers",
        "Others": "specialized labor"
    }
    labor = labor_examples.get(category, "specialized labor")
    return (
        f"**Labor Index**: This tracks the cost trend of labor for {category} projects (e.g., {labor}) compared to a baseline "
        f"(100 = average cost in a reference year). Higher values indicate rising labor costs, increasing project expenses. "
        f"A value above 150 flags a potential risk. Example: If the index is 160, labor costs are 60% higher than the baseline."
    )

# Custom CSS
custom_css = """
#submit-button {
    background-color: #FFD700 !important;
    color: #333 !important;
    width: 150px !important;
    height: 40px !important;
    border: none !important;
    border-radius: 5px !important;
    font-size: 16px !important;
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
}
#custom_css:hover {
    background-color: #E6C200 !important;
}
"""

# Gradio interface
with gr.Blocks(title="Budget Overrun Risk Estimator", css=custom_css) as demo:
    gr.Markdown("# Budget Overrun Risk Estimator")
    gr.Markdown("Upload a CSV file or provide a Project ID to fetch budget line items from Salesforce. All numeric fields are required.")

    with gr.Row():
        with gr.Column():
            username_input = gr.Textbox(label="Salesforce Username", placeholder="Enter your Salesforce username")
            project_id_input = gr.Textbox(label="Project ID (Optional)", placeholder="Enter Project ID to fetch data from Salesforce")
            file_input = gr.File(label="Upload Budget Line Items (CSV, Optional if Project ID provided)", file_types=[".csv"])
            category_input = gr.Dropdown(label="Category", choices=["Civil", "Electrical", "Plumbing", "Mechanical", "Finishing", "Others"], value="Plumbing")
            
            material_cost_input = gr.Textbox(label="Material Cost Index", placeholder="Enter material cost index (e.g., 120)")
            material_cost_explanation = gr.Markdown(update_material_cost_explanation("Plumbing"))
            
            labor_index_input = gr.Textbox(label="Labor Index", placeholder="Enter labor index (e.g., 130)")
            labor_index_explanation = gr.Markdown(update_labor_explanation("Plumbing"))
            
            scope_change_input = gr.Textbox(label="Scope Change Impact (%)", placeholder="Enter scope change impact as a percentage (e.g., 10 for 10%)")
            
            project_phase_input = gr.Dropdown(label="Project Phase", choices=["Planning", "Execution", "Closure"], value="Planning")

            with gr.Row():
                clear_button = gr.Button("Clear")
                submit_button = gr.Button("Submit", elem_id="submit-button")

        with gr.Column():
            gr.Markdown("## Dashboard")
            gauge_chart_output = gr.Plot(label="Risk Level Dashboard")
            
            gr.Markdown("## Prediction Results")
            output_text = gr.Textbox(label="Prediction Results", lines=20, max_lines=30)
            
            gr.Markdown("## Forecast Chart")
            bar_chart_output = gr.Image(label="Budget Overview (Bar Chart)")
            
            gr.Markdown("## Risk Distribution")
            pie_chart_output = gr.Plot(label="Risk Factor Distribution (Pie Chart)")
            
            gr.Markdown("## Alerts")
            alert_output = gr.HTML(label="Alert Notification")
            
            output_pdf = gr.File(label="Download Local PDF Report")
            output_excel = gr.File(label="Download Excel Report")

    category_input.change(
        fn=update_material_cost_explanation,
        inputs=category_input,
        outputs=material_cost_explanation
    )
    category_input.change(
        fn=update_labor_explanation,
        inputs=category_input,
        outputs=labor_index_explanation
    )

    clear_button.click(
        fn=lambda: ("", "", None, "Plumbing", "", "", "", "Planning", "", None, None, None, "", ""),
        outputs=[
            username_input, project_id_input, file_input, category_input, material_cost_input, labor_index_input,
            scope_change_input, project_phase_input, output_text, bar_chart_output,
            pie_chart_output, gauge_chart_output, output_pdf, output_excel, alert_output
        ]
    )

    submit_button.click(
        fn=predict_risk,
        inputs=[username_input, file_input, project_id_input, category_input, material_cost_input, labor_index_input, scope_change_input, project_phase_input],
        outputs=[output_text, bar_chart_output, pie_chart_output, gauge_chart_output, output_pdf, output_excel, alert_output]
    )

# Launch the app
if __name__ == "__main__":
    try:
        demo.launch(
            server_name="0.0.0.0",
            server_port=7860,
            share=False,
            auth_message="Please log in with your Salesforce credentials.",
            allowed_paths=["/home/user/app"],
            ssr_mode=False
        )
    except Exception as e:
        logger.error(f"Failed to launch Gradio app: {str(e)}")