|
|
""" |
|
|
Costs Display Component |
|
|
Displays material costs and estimates |
|
|
""" |
|
|
|
|
|
from models import CostData, MaterialCost |
|
|
from typing import Dict, List |
|
|
|
|
|
|
|
|
def format_costs_display(cost_data) -> str: |
|
|
""" |
|
|
Format cost data for display with itemized materials and estimates |
|
|
|
|
|
Args: |
|
|
cost_data: Material cost data (dict or CostData object) |
|
|
|
|
|
Returns: |
|
|
Formatted HTML/Markdown string |
|
|
""" |
|
|
import logging |
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
logger.info(f"format_costs_display received type: {type(cost_data)}") |
|
|
|
|
|
if not cost_data: |
|
|
return "β οΈ Cost data unavailable" |
|
|
|
|
|
|
|
|
success = cost_data.get('success', True) if isinstance(cost_data, dict) else getattr(cost_data, 'success', True) |
|
|
if not success: |
|
|
return "β οΈ Cost data unavailable" |
|
|
|
|
|
html = """ |
|
|
<div style="padding: 20px;"> |
|
|
<h2 style="margin-top: 0; color: #ffffff !important; font-weight: 700;">π° Material Cost Analysis</h2> |
|
|
""" |
|
|
|
|
|
|
|
|
total_estimate = cost_data.get('total_estimate') if isinstance(cost_data, dict) else getattr(cost_data, 'total_estimate', None) |
|
|
if total_estimate: |
|
|
|
|
|
currency = total_estimate.get('currency', 'PHP') if isinstance(total_estimate, dict) else getattr(total_estimate, 'currency', 'PHP') |
|
|
low = total_estimate.get('low', 0) if isinstance(total_estimate, dict) else getattr(total_estimate, 'low', 0) |
|
|
mid = total_estimate.get('mid', 0) if isinstance(total_estimate, dict) else getattr(total_estimate, 'mid', 0) |
|
|
high = total_estimate.get('high', 0) if isinstance(total_estimate, dict) else getattr(total_estimate, 'high', 0) |
|
|
|
|
|
html += f""" |
|
|
<div style="background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); padding: 25px; border-radius: 8px; margin-bottom: 20px; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"> |
|
|
<h3 style="margin-top: 0; color: white; font-weight: 600;">π΅ Total Cost Estimate</h3> |
|
|
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-top: 15px;"> |
|
|
<div style="background: rgba(255,255,255,0.25); padding: 15px; border-radius: 6px; text-align: center;"> |
|
|
<div style="font-size: 0.9em; font-weight: 500;">Low Estimate</div> |
|
|
<div style="font-size: 1.5em; font-weight: bold; margin-top: 5px;"> |
|
|
{currency} {low:,.2f} |
|
|
</div> |
|
|
</div> |
|
|
<div style="background: rgba(255,255,255,0.35); padding: 15px; border-radius: 6px; text-align: center;"> |
|
|
<div style="font-size: 0.9em; font-weight: 500;">Mid Estimate</div> |
|
|
<div style="font-size: 1.5em; font-weight: bold; margin-top: 5px;"> |
|
|
{currency} {mid:,.2f} |
|
|
</div> |
|
|
</div> |
|
|
<div style="background: rgba(255,255,255,0.25); padding: 15px; border-radius: 6px; text-align: center;"> |
|
|
<div style="font-size: 0.9em; font-weight: 500;">High Estimate</div> |
|
|
<div style="font-size: 1.5em; font-weight: bold; margin-top: 5px;"> |
|
|
{currency} {high:,.2f} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
market_conditions = cost_data.get('market_conditions') if isinstance(cost_data, dict) else getattr(cost_data, 'market_conditions', None) |
|
|
last_updated = cost_data.get('last_updated') if isinstance(cost_data, dict) else getattr(cost_data, 'last_updated', None) |
|
|
|
|
|
if market_conditions or last_updated: |
|
|
html += """ |
|
|
<div style="background: white; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 5px solid #f59e0b; border: 1px solid #fde68a;"> |
|
|
<h4 style="margin-top: 0; color: #d97706 !important;">π Market Information</h4> |
|
|
""" |
|
|
if market_conditions: |
|
|
html += f"<p style='margin: 5px 0; color: #0f172a !important;'><strong style='color: #0f172a !important;'>Market Conditions:</strong> {market_conditions}</p>" |
|
|
if last_updated: |
|
|
html += f"<p style='margin: 5px 0; color: #0f172a !important;'><strong style='color: #0f172a !important;'>Last Updated:</strong> {last_updated}</p>" |
|
|
html += """ |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
materials = cost_data.get('materials', []) if isinstance(cost_data, dict) else getattr(cost_data, 'materials', []) |
|
|
if materials: |
|
|
|
|
|
materials_by_category = _group_materials_by_category(materials) |
|
|
|
|
|
for category, materials_list in materials_by_category.items(): |
|
|
|
|
|
category_total = 0 |
|
|
first_currency = 'PHP' |
|
|
for m in materials_list: |
|
|
total_cost = m.get('total_cost', 0) if isinstance(m, dict) else getattr(m, 'total_cost', 0) |
|
|
if total_cost: |
|
|
category_total += total_cost |
|
|
if first_currency == 'PHP': |
|
|
first_currency = m.get('currency', 'PHP') if isinstance(m, dict) else getattr(m, 'currency', 'PHP') |
|
|
|
|
|
html += f""" |
|
|
<div style="background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border: 1px solid #e2e8f0;"> |
|
|
<h3 style="color: #1e40af !important; margin-top: 0; font-weight: 600;"> |
|
|
{_get_category_icon(category)} {category} |
|
|
{f'<span style="float: right; color: #15803d !important; font-weight: 700;">Total: {first_currency} {category_total:,.2f}</span>' if category_total > 0 else ''} |
|
|
</h3> |
|
|
<table style="width: 100%; border-collapse: collapse;"> |
|
|
<thead> |
|
|
<tr style="background: #f8fafc;"> |
|
|
<th style="padding: 12px; text-align: left; border-bottom: 2px solid #cbd5e1; color: #0f172a !important; font-weight: 600;">Material</th> |
|
|
<th style="padding: 12px; text-align: right; border-bottom: 2px solid #cbd5e1; color: #0f172a !important; font-weight: 600;">Unit Price</th> |
|
|
<th style="padding: 12px; text-align: right; border-bottom: 2px solid #cbd5e1; color: #0f172a !important; font-weight: 600;">Quantity</th> |
|
|
<th style="padding: 12px; text-align: right; border-bottom: 2px solid #cbd5e1; color: #0f172a !important; font-weight: 600;">Total Cost</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
""" |
|
|
|
|
|
for material in materials_list: |
|
|
|
|
|
material_name = material.get('material_name', 'Unknown') if isinstance(material, dict) else getattr(material, 'material_name', 'Unknown') |
|
|
source = material.get('source') if isinstance(material, dict) else getattr(material, 'source', None) |
|
|
currency = material.get('currency', 'PHP') if isinstance(material, dict) else getattr(material, 'currency', 'PHP') |
|
|
price_per_unit = material.get('price_per_unit', 0) if isinstance(material, dict) else getattr(material, 'price_per_unit', 0) |
|
|
unit = material.get('unit', 'unit') if isinstance(material, dict) else getattr(material, 'unit', 'unit') |
|
|
quantity_needed = material.get('quantity_needed') if isinstance(material, dict) else getattr(material, 'quantity_needed', None) |
|
|
total_cost = material.get('total_cost') if isinstance(material, dict) else getattr(material, 'total_cost', None) |
|
|
|
|
|
quantity_str = f"{quantity_needed:.2f} {unit}" if quantity_needed else "TBD" |
|
|
total_str = f"{currency} {total_cost:,.2f}" if total_cost else "TBD" |
|
|
|
|
|
html += f""" |
|
|
<tr style="border-bottom: 1px solid #e2e8f0;"> |
|
|
<td style="padding: 12px; color: #0f172a !important;"> |
|
|
<strong style="color: #0f172a !important;">{material_name}</strong> |
|
|
{f'<br><span style="font-size: 0.85em; color: #475569 !important;">Source: {source}</span>' if source else ''} |
|
|
</td> |
|
|
<td style="padding: 12px; text-align: right; color: #0f172a !important;"> |
|
|
{currency} {price_per_unit:,.2f} / {unit} |
|
|
</td> |
|
|
<td style="padding: 12px; text-align: right; color: #0f172a !important;">{quantity_str}</td> |
|
|
<td style="padding: 12px; text-align: right; font-weight: 700; color: #15803d !important;">{total_str}</td> |
|
|
</tr> |
|
|
""" |
|
|
|
|
|
html += """ |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
""" |
|
|
else: |
|
|
html += """ |
|
|
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center; color: #6b7280;"> |
|
|
<p>No material cost data available</p> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
html += """ |
|
|
<div style="background: white; padding: 15px; border-radius: 8px; margin-top: 20px; font-size: 0.9em; border: 1px solid #e2e8f0;"> |
|
|
<p style="margin: 0; color: #0f172a !important;"><strong style="color: #0f172a !important;">Note:</strong> Cost estimates are based on current market conditions and may vary. |
|
|
Actual costs depend on supplier, location, quantity discounts, and market fluctuations. |
|
|
Please consult with local suppliers for accurate quotes.</p> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
return html |
|
|
|
|
|
|
|
|
def _group_materials_by_category(materials: List) -> Dict[str, List]: |
|
|
"""Group materials by category (handles both dict and object formats)""" |
|
|
grouped = {} |
|
|
for material in materials: |
|
|
|
|
|
category = material.get('category', 'Other') if isinstance(material, dict) else getattr(material, 'category', 'Other') |
|
|
if not category: |
|
|
category = "Other" |
|
|
if category not in grouped: |
|
|
grouped[category] = [] |
|
|
grouped[category].append(material) |
|
|
return grouped |
|
|
|
|
|
|
|
|
def _get_category_icon(category: str) -> str: |
|
|
"""Get icon for material category""" |
|
|
icons = { |
|
|
"Structural": "ποΈ", |
|
|
"Roofing": "π ", |
|
|
"Foundation": "β", |
|
|
"Disaster-Resistant Features": "π‘οΈ", |
|
|
"Finishing": "π¨", |
|
|
"Electrical": "β‘", |
|
|
"Plumbing": "π°", |
|
|
"Other": "π¦" |
|
|
} |
|
|
return icons.get(category, "π¦") |
|
|
|