Upload 52 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- OLD/Analysis.ipynb +0 -0
- OLD/Chatbot.py +52 -0
- OLD/Main.py +244 -0
- OLD/__dbmlsystem.py +596 -0
- OLD/sampleDashboard.py +350 -0
- data/AMBERAVPURA ENGLISH SABHASAD LIST.xlsx +0 -0
- data/APRIL 24-25.xlsx +0 -0
- data/AUGUST 24-25.xlsx +0 -0
- data/JULY 24-25.xlsx +0 -0
- data/JUNE 24-25.xlsx +0 -0
- data/MAY 24-25.xlsx +0 -0
- data/SEPTEMBER 24-25.xlsx +0 -0
- data/amiyad.xlsx +0 -0
- data/dharkhuniya.xlsx +0 -0
- data/distributors.xlsx +0 -0
- data/kamrol.xlsx +0 -0
- data/sandha.xlsx +0 -0
- data/vishnoli.xlsx +0 -0
- pages/__init__.py +0 -0
- pages/__pycache__/__init__.cpython-310.pyc +0 -0
- pages/__pycache__/__init__.cpython-313.pyc +0 -0
- pages/__pycache__/customers.cpython-310.pyc +0 -0
- pages/__pycache__/dashboard.cpython-310.pyc +0 -0
- pages/__pycache__/dashboard.cpython-313.pyc +0 -0
- pages/__pycache__/data_import.cpython-310.pyc +0 -0
- pages/__pycache__/demos.cpython-310.pyc +0 -0
- pages/__pycache__/distributors.cpython-310.pyc +0 -0
- pages/__pycache__/file_viewer.cpython-310.pyc +0 -0
- pages/__pycache__/payments.cpython-310.pyc +0 -0
- pages/__pycache__/reports.cpython-310.pyc +0 -0
- pages/__pycache__/sales.cpython-310.pyc +0 -0
- pages/__pycache__/system_dashboard.cpython-310.pyc +0 -0
- pages/customers.py +449 -0
- pages/dashboard.py +12 -0
- pages/data_import.py +104 -0
- pages/demos.py +824 -0
- pages/distributors.py +971 -0
- pages/file_viewer.py +382 -0
- pages/payments.py +524 -0
- pages/reports.py +1101 -0
- pages/sales.py +500 -0
- pages/system_dashboard.py +137 -0
- pages/whatsapp.py +68 -0
- utils/__init__.py +0 -0
- utils/__pycache__/__init__.cpython-310.pyc +0 -0
- utils/__pycache__/__init__.cpython-313.pyc +0 -0
- utils/__pycache__/helpers.cpython-310.pyc +0 -0
- utils/__pycache__/helpers.cpython-313.pyc +0 -0
- utils/__pycache__/styling.cpython-310.pyc +0 -0
- utils/__pycache__/styling.cpython-313.pyc +0 -0
OLD/Analysis.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
OLD/Chatbot.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import requests
|
| 4 |
+
|
| 5 |
+
# ---- Load sample data ----
|
| 6 |
+
@st.cache_data
|
| 7 |
+
def load_data():
|
| 8 |
+
return pd.read_csv("data.csv")
|
| 9 |
+
|
| 10 |
+
df = load_data()
|
| 11 |
+
|
| 12 |
+
# ---- Sidebar ----
|
| 13 |
+
st.sidebar.title("Controls")
|
| 14 |
+
task = st.sidebar.selectbox("Choose task", ["Chat with Bot (via n8n)", "Analyze Data"])
|
| 15 |
+
|
| 16 |
+
# ---- Main UI ----
|
| 17 |
+
st.title("💬 Data Analysis Assistant (Streamlit + n8n + Ollama)")
|
| 18 |
+
|
| 19 |
+
if task == "Analyze Data":
|
| 20 |
+
st.subheader("📊 Sales Data")
|
| 21 |
+
st.dataframe(df)
|
| 22 |
+
|
| 23 |
+
st.write("### Total Sales:")
|
| 24 |
+
st.metric("💵 Amount", f"${df['amount'].sum():,.2f}")
|
| 25 |
+
|
| 26 |
+
top_customer = df.groupby("customer")["amount"].sum().idxmax()
|
| 27 |
+
st.write(f"**Top Customer:** {top_customer}")
|
| 28 |
+
|
| 29 |
+
elif task == "Chat with Bot (via n8n)":
|
| 30 |
+
st.subheader("🤖 Ask Questions")
|
| 31 |
+
user_input = st.text_area("Your question:", placeholder="e.g. Who spent the most?")
|
| 32 |
+
|
| 33 |
+
if st.button("Ask Bot") and user_input:
|
| 34 |
+
# Send request to n8n webhook
|
| 35 |
+
payload = {
|
| 36 |
+
"question": user_input,
|
| 37 |
+
"data": df.to_dict(orient="records")
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
response = requests.post(
|
| 42 |
+
"http://localhost:5678/webhook/chatbot",
|
| 43 |
+
json=payload,
|
| 44 |
+
timeout=60
|
| 45 |
+
)
|
| 46 |
+
if response.ok:
|
| 47 |
+
answer = response.json().get("answer", response.text)
|
| 48 |
+
st.success(answer)
|
| 49 |
+
else:
|
| 50 |
+
st.error(f"n8n Error: {response.status_code}")
|
| 51 |
+
except Exception as e:
|
| 52 |
+
st.error(f"Connection failed: {e}")
|
OLD/Main.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
|
| 5 |
+
def analyze_sales_data(data1, data2):
|
| 6 |
+
"""
|
| 7 |
+
Analyze sales data to identify targets for mantri communication and village focus
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
# Convert date column if needed
|
| 11 |
+
data1['Date'] = pd.to_datetime(data1['Date'])
|
| 12 |
+
|
| 13 |
+
# Clean and preprocess data2
|
| 14 |
+
data2['Date'] = pd.to_datetime(data2['Date'])
|
| 15 |
+
|
| 16 |
+
# Calculate key metrics from Data1 (village level)
|
| 17 |
+
data1['Conversion_Rate'] = (data1['Contact_In_Group'] / data1['Sabhasad'] * 100).round(2)
|
| 18 |
+
data1['Conversion_Rate'] = data1['Conversion_Rate'].replace([np.inf, -np.inf], 0).fillna(0)
|
| 19 |
+
data1['Untapped_Potential'] = data1['Sabhasad'] - data1['Contact_In_Group']
|
| 20 |
+
data1['Sales_Per_Contact'] = (data1['Total_L'] / data1['Contact_In_Group']).round(2)
|
| 21 |
+
data1['Sales_Per_Contact'] = data1['Sales_Per_Contact'].replace([np.inf, -np.inf], 0).fillna(0)
|
| 22 |
+
|
| 23 |
+
# Calculate priority score for villages
|
| 24 |
+
data1['Priority_Score'] = (
|
| 25 |
+
(data1['Untapped_Potential'] / data1['Untapped_Potential'].max() * 50) +
|
| 26 |
+
((100 - data1['Conversion_Rate']) / 100 * 50)
|
| 27 |
+
).round(2)
|
| 28 |
+
|
| 29 |
+
# Analyze recent sales from Data2 (customer level)
|
| 30 |
+
# Since we don't have customer contact info, we'll analyze at village level
|
| 31 |
+
recent_sales = data2.groupby('Village').agg({
|
| 32 |
+
'Total_L': ['sum', 'count'],
|
| 33 |
+
'Date': 'max'
|
| 34 |
+
}).reset_index()
|
| 35 |
+
|
| 36 |
+
# Flatten the column names
|
| 37 |
+
recent_sales.columns = ['Village', 'Recent_Sales_L', 'Recent_Customers', 'Last_Sale_Date']
|
| 38 |
+
|
| 39 |
+
# Calculate days since last sale
|
| 40 |
+
recent_sales['Days_Since_Last_Sale'] = (datetime.now() - recent_sales['Last_Sale_Date']).dt.days
|
| 41 |
+
|
| 42 |
+
# Merge with Data1
|
| 43 |
+
analysis_df = data1.merge(recent_sales, on='Village', how='left')
|
| 44 |
+
analysis_df['Recent_Sales_L'] = analysis_df['Recent_Sales_L'].fillna(0)
|
| 45 |
+
analysis_df['Recent_Customers'] = analysis_df['Recent_Customers'].fillna(0)
|
| 46 |
+
analysis_df['Days_Since_Last_Sale'] = analysis_df['Days_Since_Last_Sale'].fillna(999)
|
| 47 |
+
|
| 48 |
+
# Generate recommendations for mantris
|
| 49 |
+
recommendations = []
|
| 50 |
+
|
| 51 |
+
for _, row in analysis_df.iterrows():
|
| 52 |
+
village = row['Village']
|
| 53 |
+
mantri = row['Mantri_Name']
|
| 54 |
+
mobile = row['Mantri_Mobile']
|
| 55 |
+
taluka = row['Taluka']
|
| 56 |
+
district = row['District']
|
| 57 |
+
|
| 58 |
+
# Recommendation logic
|
| 59 |
+
if row['Conversion_Rate'] < 20:
|
| 60 |
+
recommendations.append({
|
| 61 |
+
'Village': village,
|
| 62 |
+
'Taluka': taluka,
|
| 63 |
+
'District': district,
|
| 64 |
+
'Mantri': mantri,
|
| 65 |
+
'Mobile': mobile,
|
| 66 |
+
'Action': 'Send Marketing Team',
|
| 67 |
+
'Reason': f'Low conversion rate ({row["Conversion_Rate"]:.1f}%) - Only {row["Contact_In_Group"]} of {row["Sabhasad"]} sabhasad contacted',
|
| 68 |
+
'Priority': 'High',
|
| 69 |
+
'Score': row['Priority_Score']
|
| 70 |
+
})
|
| 71 |
+
elif row['Untapped_Potential'] > 30:
|
| 72 |
+
recommendations.append({
|
| 73 |
+
'Village': village,
|
| 74 |
+
'Taluka': taluka,
|
| 75 |
+
'District': district,
|
| 76 |
+
'Mantri': mantri,
|
| 77 |
+
'Mobile': mobile,
|
| 78 |
+
'Action': 'Call Mantri for Follow-up',
|
| 79 |
+
'Reason': f'High untapped potential ({row["Untapped_Potential"]} sabhasad not contacted)',
|
| 80 |
+
'Priority': 'High',
|
| 81 |
+
'Score': row['Priority_Score']
|
| 82 |
+
})
|
| 83 |
+
elif row['Days_Since_Last_Sale'] > 30:
|
| 84 |
+
recommendations.append({
|
| 85 |
+
'Village': village,
|
| 86 |
+
'Taluka': taluka,
|
| 87 |
+
'District': district,
|
| 88 |
+
'Mantri': mantri,
|
| 89 |
+
'Mobile': mobile,
|
| 90 |
+
'Action': 'Check on Mantri',
|
| 91 |
+
'Reason': f'No recent sales ({row["Days_Since_Last_Sale"]} days since last sale)',
|
| 92 |
+
'Priority': 'Medium',
|
| 93 |
+
'Score': row['Priority_Score']
|
| 94 |
+
})
|
| 95 |
+
elif row['Sales_Per_Contact'] > 10:
|
| 96 |
+
recommendations.append({
|
| 97 |
+
'Village': village,
|
| 98 |
+
'Taluka': taluka,
|
| 99 |
+
'District': district,
|
| 100 |
+
'Mantri': mantri,
|
| 101 |
+
'Mobile': mobile,
|
| 102 |
+
'Action': 'Provide More Stock',
|
| 103 |
+
'Reason': f'High sales per contact ({row["Sales_Per_Contact"]}L per contact)',
|
| 104 |
+
'Priority': 'Medium',
|
| 105 |
+
'Score': row['Priority_Score']
|
| 106 |
+
})
|
| 107 |
+
else:
|
| 108 |
+
recommendations.append({
|
| 109 |
+
'Village': village,
|
| 110 |
+
'Taluka': taluka,
|
| 111 |
+
'District': district,
|
| 112 |
+
'Mantri': mantri,
|
| 113 |
+
'Mobile': mobile,
|
| 114 |
+
'Action': 'Regular Follow-up',
|
| 115 |
+
'Reason': 'Steady performance - maintain relationship',
|
| 116 |
+
'Priority': 'Low',
|
| 117 |
+
'Score': row['Priority_Score']
|
| 118 |
+
})
|
| 119 |
+
|
| 120 |
+
return pd.DataFrame(recommendations), analysis_df
|
| 121 |
+
|
| 122 |
+
def generate_mantri_messages(recommendations):
|
| 123 |
+
"""
|
| 124 |
+
Generate personalized WhatsApp messages for mantris based on recommendations
|
| 125 |
+
"""
|
| 126 |
+
messages = []
|
| 127 |
+
|
| 128 |
+
for _, row in recommendations.iterrows():
|
| 129 |
+
if row['Action'] == 'Send Marketing Team':
|
| 130 |
+
message = f"""
|
| 131 |
+
Namaste {row['Mantri']} Ji!
|
| 132 |
+
|
| 133 |
+
Aapke kshetra {row['Village']} mein humare calcium supplement ki conversion rate kam hai.
|
| 134 |
+
Humari marketing team aapke yaha demo dene aayegi.
|
| 135 |
+
Kripya taiyaari rakhein aur sabhi dudh utpadakon ko soochit karein.
|
| 136 |
+
|
| 137 |
+
Dhanyavaad,
|
| 138 |
+
Calcium Supplement Team
|
| 139 |
+
"""
|
| 140 |
+
elif row['Action'] == 'Call Mantri for Follow-up':
|
| 141 |
+
message = f"""
|
| 142 |
+
Namaste {row['Mantri']} Ji!
|
| 143 |
+
|
| 144 |
+
Aapke kshetra {row['Village']} mein bahut se aise farmers hain jo abhi tak humare product se anabhijit hain.
|
| 145 |
+
Kripya unse sampark karein aur unhe product ke fayde batayein.
|
| 146 |
+
Aapke liye special commission offer hai agle 10 customers ke liye.
|
| 147 |
+
|
| 148 |
+
Dhanyavaad,
|
| 149 |
+
Calcium Supplement Team
|
| 150 |
+
"""
|
| 151 |
+
elif row['Action'] == 'Check on Mantri':
|
| 152 |
+
message = f"""
|
| 153 |
+
Namaste {row['Mantri']} Ji!
|
| 154 |
+
|
| 155 |
+
Humne dekha ki aapke kshetra {row['Village']} mein kuch samay se sales nahi hue hain.
|
| 156 |
+
Kya koi samasya hai? Kya hum aapki kisi tarah madad kar sakte hain?
|
| 157 |
+
|
| 158 |
+
Kripya hame batayein.
|
| 159 |
+
|
| 160 |
+
Dhanyavaad,
|
| 161 |
+
Calcium Supplement Team
|
| 162 |
+
"""
|
| 163 |
+
elif row['Action'] == 'Provide More Stock':
|
| 164 |
+
message = f"""
|
| 165 |
+
Namaste {row['Mantri']} Ji!
|
| 166 |
+
|
| 167 |
+
Badhai ho! Aapke kshetra {row['Village']} mein humare product ki demand badh rahi hai.
|
| 168 |
+
Kya aapko aur stock ki zaroorat hai? Hum jald se jald aapko extra stock bhej denge.
|
| 169 |
+
|
| 170 |
+
Dhanyavaad,
|
| 171 |
+
Calcium Supplement Team
|
| 172 |
+
"""
|
| 173 |
+
else:
|
| 174 |
+
message = f"""
|
| 175 |
+
Namaste {row['Mantri']} Ji!
|
| 176 |
+
|
| 177 |
+
Aapke kshetra {row['Village']} mein humare product ki sales theek chal rahi hain.
|
| 178 |
+
Kripya aise hi continue rakhein aur koi bhi sujhav ho toh hame batayein.
|
| 179 |
+
|
| 180 |
+
Dhanyavaad,
|
| 181 |
+
Calcium Supplement Team
|
| 182 |
+
"""
|
| 183 |
+
|
| 184 |
+
messages.append({
|
| 185 |
+
'Mantri': row['Mantri'],
|
| 186 |
+
'Mobile': row['Mobile'],
|
| 187 |
+
'Village': row['Village'],
|
| 188 |
+
'Action': row['Action'],
|
| 189 |
+
'Message': message,
|
| 190 |
+
'Priority': row['Priority']
|
| 191 |
+
})
|
| 192 |
+
|
| 193 |
+
return pd.DataFrame(messages)
|
| 194 |
+
|
| 195 |
+
def identify_demo_locations(analysis_df, top_n=5):
|
| 196 |
+
"""
|
| 197 |
+
Identify the best locations for demos based on various factors
|
| 198 |
+
"""
|
| 199 |
+
# Calculate a demo score based on multiple factors
|
| 200 |
+
analysis_df['Demo_Score'] = (
|
| 201 |
+
(analysis_df['Untapped_Potential'] / analysis_df['Untapped_Potential'].max() * 40) +
|
| 202 |
+
((100 - analysis_df['Conversion_Rate']) / 100 * 30) +
|
| 203 |
+
(analysis_df['Recent_Sales_L'] / analysis_df['Recent_Sales_L'].max() * 30)
|
| 204 |
+
).round(2)
|
| 205 |
+
|
| 206 |
+
# Get top locations for demos
|
| 207 |
+
demo_locations = analysis_df.nlargest(top_n, 'Demo_Score')[
|
| 208 |
+
['Village', 'Taluka', 'District', 'Mantri_Name', 'Mantri_Mobile',
|
| 209 |
+
'Conversion_Rate', 'Untapped_Potential', 'Demo_Score']
|
| 210 |
+
]
|
| 211 |
+
|
| 212 |
+
return demo_locations
|
| 213 |
+
|
| 214 |
+
# Example usage with sample data structure
|
| 215 |
+
def main():
|
| 216 |
+
# Sample data based on your new structure
|
| 217 |
+
data2=pd.read_excel("sampletesting.xlsx",sheet_name="Sheet1")
|
| 218 |
+
data1=pd.read_excel("sampletesting.xlsx",sheet_name="Sheet2")
|
| 219 |
+
|
| 220 |
+
# Generate recommendations
|
| 221 |
+
recommendations, analysis = analyze_sales_data(data1, data2)
|
| 222 |
+
|
| 223 |
+
print("RECOMMENDED ACTIONS:")
|
| 224 |
+
print(recommendations.sort_values('Score', ascending=False).to_string(index=False))
|
| 225 |
+
|
| 226 |
+
# Generate messages for mantris
|
| 227 |
+
mantri_messages = generate_mantri_messages(recommendations)
|
| 228 |
+
|
| 229 |
+
print("\nMANTRI MESSAGES:")
|
| 230 |
+
for _, msg in mantri_messages.iterrows():
|
| 231 |
+
print(f"\nTo: {msg['Mantri']} ({msg['Mobile']}) - {msg['Village']}")
|
| 232 |
+
print(f"Action: {msg['Action']}")
|
| 233 |
+
print(f"Message: {msg['Message']}")
|
| 234 |
+
|
| 235 |
+
# Identify demo locations
|
| 236 |
+
demo_locations = identify_demo_locations(analysis)
|
| 237 |
+
|
| 238 |
+
print("\nTOP DEMO LOCATIONS:")
|
| 239 |
+
print(demo_locations.to_string(index=False))
|
| 240 |
+
|
| 241 |
+
return recommendations, mantri_messages, demo_locations
|
| 242 |
+
|
| 243 |
+
if __name__ == "__main__":
|
| 244 |
+
recommendations, mantri_messages, demo_locations = main()
|
OLD/__dbmlsystem.py
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
import plotly.graph_objects as go
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
|
| 8 |
+
from sklearn.model_selection import train_test_split
|
| 9 |
+
from sklearn.preprocessing import StandardScaler
|
| 10 |
+
from sklearn.cluster import KMeans
|
| 11 |
+
import warnings
|
| 12 |
+
warnings.filterwarnings('ignore')
|
| 13 |
+
|
| 14 |
+
# Set page configuration
|
| 15 |
+
st.set_page_config(
|
| 16 |
+
page_title="Calcium Supplement Sales Automation",
|
| 17 |
+
page_icon="🐄",
|
| 18 |
+
layout="wide",
|
| 19 |
+
initial_sidebar_state="expanded"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
# App title
|
| 23 |
+
st.title("🐄 Calcium Supplement Sales Automation Dashboard")
|
| 24 |
+
st.markdown("---")
|
| 25 |
+
|
| 26 |
+
# Your exact ML functions
|
| 27 |
+
def enhanced_analyze_sales_data(data1, data2):
|
| 28 |
+
"""
|
| 29 |
+
Enhanced analysis with ML components for better predictions
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
data1['Date'] = pd.to_datetime(data1['Date'])
|
| 33 |
+
data2['Date'] = pd.to_datetime(data2['Date'])
|
| 34 |
+
|
| 35 |
+
# Calculate basic metrics
|
| 36 |
+
data1['Conversion_Rate'] = (data1['Contact_In_Group'] / data1['Sabhasad'] * 100).round(2)
|
| 37 |
+
data1['Conversion_Rate'] = data1['Conversion_Rate'].replace([np.inf, -np.inf], 0).fillna(0)
|
| 38 |
+
data1['Untapped_Potential'] = data1['Sabhasad'] - data1['Contact_In_Group']
|
| 39 |
+
data1['Sales_Per_Contact'] = (data1['Total_L'] / data1['Contact_In_Group']).round(2)
|
| 40 |
+
data1['Sales_Per_Contact'] = data1['Sales_Per_Contact'].replace([np.inf, -np.inf], 0).fillna(0)
|
| 41 |
+
|
| 42 |
+
# Analyze recent sales
|
| 43 |
+
recent_sales = data2.groupby('Village').agg({
|
| 44 |
+
'Total_L': ['sum', 'count'],
|
| 45 |
+
'Date': 'max'
|
| 46 |
+
}).reset_index()
|
| 47 |
+
|
| 48 |
+
recent_sales.columns = ['Village', 'Recent_Sales_L', 'Recent_Customers', 'Last_Sale_Date']
|
| 49 |
+
recent_sales['Days_Since_Last_Sale'] = (datetime.now() - recent_sales['Last_Sale_Date']).dt.days
|
| 50 |
+
|
| 51 |
+
# Merge data
|
| 52 |
+
analysis_df = data1.merge(recent_sales, on='Village', how='left')
|
| 53 |
+
analysis_df['Recent_Sales_L'] = analysis_df['Recent_Sales_L'].fillna(0)
|
| 54 |
+
analysis_df['Recent_Customers'] = analysis_df['Recent_Customers'].fillna(0)
|
| 55 |
+
analysis_df['Days_Since_Last_Sale'] = analysis_df['Days_Since_Last_Sale'].fillna(999)
|
| 56 |
+
|
| 57 |
+
# ML Component 1: Village Clustering for Segmentation
|
| 58 |
+
analysis_df = apply_village_clustering(analysis_df)
|
| 59 |
+
|
| 60 |
+
# ML Component 2: Predict Sales Potential
|
| 61 |
+
analysis_df = predict_sales_potential(analysis_df)
|
| 62 |
+
|
| 63 |
+
# ML Component 3: Action Recommendation Classifier
|
| 64 |
+
analysis_df = predict_recommended_actions(analysis_df)
|
| 65 |
+
|
| 66 |
+
# Generate recommendations based on ML predictions
|
| 67 |
+
recommendations = generate_ml_recommendations(analysis_df)
|
| 68 |
+
|
| 69 |
+
return recommendations, analysis_df
|
| 70 |
+
|
| 71 |
+
def apply_village_clustering(analysis_df):
|
| 72 |
+
"""
|
| 73 |
+
Use K-Means clustering to segment villages into groups
|
| 74 |
+
"""
|
| 75 |
+
# Prepare features for clustering
|
| 76 |
+
cluster_features = analysis_df[[
|
| 77 |
+
'Conversion_Rate', 'Untapped_Potential', 'Sales_Per_Contact',
|
| 78 |
+
'Recent_Sales_L', 'Days_Since_Last_Sale'
|
| 79 |
+
]].fillna(0)
|
| 80 |
+
|
| 81 |
+
# Standardize features
|
| 82 |
+
scaler = StandardScaler()
|
| 83 |
+
scaled_features = scaler.fit_transform(cluster_features)
|
| 84 |
+
|
| 85 |
+
# Apply K-Means clustering
|
| 86 |
+
kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)
|
| 87 |
+
clusters = kmeans.fit_predict(scaled_features)
|
| 88 |
+
|
| 89 |
+
# Add clusters to dataframe
|
| 90 |
+
analysis_df['Cluster'] = clusters
|
| 91 |
+
|
| 92 |
+
# Name the clusters based on characteristics
|
| 93 |
+
cluster_names = {
|
| 94 |
+
0: 'High Potential - Low Engagement',
|
| 95 |
+
1: 'Steady Performers',
|
| 96 |
+
2: 'Underperforming',
|
| 97 |
+
3: 'New/Developing'
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
analysis_df['Segment'] = analysis_df['Cluster'].map(cluster_names)
|
| 101 |
+
|
| 102 |
+
return analysis_df
|
| 103 |
+
|
| 104 |
+
def predict_sales_potential(analysis_df):
|
| 105 |
+
"""
|
| 106 |
+
Predict sales potential for each village using Random Forest
|
| 107 |
+
"""
|
| 108 |
+
# Prepare features for prediction
|
| 109 |
+
prediction_features = analysis_df[[
|
| 110 |
+
'Sabhasad', 'Contact_In_Group', 'Conversion_Rate',
|
| 111 |
+
'Untapped_Potential', 'Recent_Sales_L', 'Days_Since_Last_Sale'
|
| 112 |
+
]].fillna(0)
|
| 113 |
+
|
| 114 |
+
# Target variable: Total_L (current sales)
|
| 115 |
+
target = analysis_df['Total_L'].fillna(0)
|
| 116 |
+
|
| 117 |
+
# Only train if we have enough data
|
| 118 |
+
if len(prediction_features) > 10:
|
| 119 |
+
# Split data
|
| 120 |
+
X_train, X_test, y_train, y_test = train_test_split(
|
| 121 |
+
prediction_features, target, test_size=0.2, random_state=42
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
# Train model
|
| 125 |
+
model = RandomForestRegressor(n_estimators=100, random_state=42)
|
| 126 |
+
model.fit(X_train, y_train)
|
| 127 |
+
|
| 128 |
+
# Make predictions
|
| 129 |
+
predictions = model.predict(prediction_features)
|
| 130 |
+
|
| 131 |
+
# Calculate feature importance
|
| 132 |
+
feature_importance = pd.DataFrame({
|
| 133 |
+
'feature': prediction_features.columns,
|
| 134 |
+
'importance': model.feature_importances_
|
| 135 |
+
}).sort_values('importance', ascending=False)
|
| 136 |
+
|
| 137 |
+
# Add predictions to dataframe
|
| 138 |
+
analysis_df['Predicted_Sales'] = predictions
|
| 139 |
+
analysis_df['Sales_Gap'] = analysis_df['Predicted_Sales'] - analysis_df['Total_L']
|
| 140 |
+
else:
|
| 141 |
+
# Fallback if not enough data
|
| 142 |
+
analysis_df['Predicted_Sales'] = analysis_df['Total_L']
|
| 143 |
+
analysis_df['Sales_Gap'] = 0
|
| 144 |
+
|
| 145 |
+
return analysis_df
|
| 146 |
+
|
| 147 |
+
def predict_recommended_actions(analysis_df):
|
| 148 |
+
"""
|
| 149 |
+
Use ML to predict the best action for each village
|
| 150 |
+
"""
|
| 151 |
+
# Define actions based on rules (for training data)
|
| 152 |
+
analysis_df['Action_Label'] = np.where(
|
| 153 |
+
analysis_df['Conversion_Rate'] < 20, 'Send Marketing Team',
|
| 154 |
+
np.where(
|
| 155 |
+
analysis_df['Untapped_Potential'] > 30, 'Call Mantri for Follow-up',
|
| 156 |
+
np.where(
|
| 157 |
+
analysis_df['Days_Since_Last_Sale'] > 30, 'Check on Mantri',
|
| 158 |
+
np.where(
|
| 159 |
+
analysis_df['Sales_Per_Contact'] > 10, 'Provide More Stock',
|
| 160 |
+
'Regular Follow-up'
|
| 161 |
+
)
|
| 162 |
+
)
|
| 163 |
+
)
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
# Prepare features for classification
|
| 167 |
+
classification_features = analysis_df[[
|
| 168 |
+
'Conversion_Rate', 'Untapped_Potential', 'Sales_Per_Contact',
|
| 169 |
+
'Recent_Sales_L', 'Days_Since_Last_Sale', 'Sales_Gap'
|
| 170 |
+
]].fillna(0)
|
| 171 |
+
|
| 172 |
+
# Target variable: Action_Label
|
| 173 |
+
target = analysis_df['Action_Label']
|
| 174 |
+
|
| 175 |
+
# Only train if we have enough data
|
| 176 |
+
if len(classification_features) > 10 and len(target.unique()) > 1:
|
| 177 |
+
# Split data
|
| 178 |
+
X_train, X_test, y_train, y_test = train_test_split(
|
| 179 |
+
classification_features, target, test_size=0.2, random_state=42, stratify=target
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
# Train classifier
|
| 183 |
+
clf = RandomForestClassifier(n_estimators=100, random_state=42)
|
| 184 |
+
clf.fit(X_train, y_train)
|
| 185 |
+
|
| 186 |
+
# Make predictions
|
| 187 |
+
predictions = clf.predict(classification_features)
|
| 188 |
+
prediction_proba = clf.predict_proba(classification_features)
|
| 189 |
+
|
| 190 |
+
# Add predictions to dataframe
|
| 191 |
+
analysis_df['ML_Recommended_Action'] = predictions
|
| 192 |
+
analysis_df['Action_Confidence'] = np.max(prediction_proba, axis=1)
|
| 193 |
+
else:
|
| 194 |
+
# Fallback to rule-based if not enough data
|
| 195 |
+
analysis_df['ML_Recommended_Action'] = analysis_df['Action_Label']
|
| 196 |
+
analysis_df['Action_Confidence'] = 1.0
|
| 197 |
+
|
| 198 |
+
return analysis_df
|
| 199 |
+
|
| 200 |
+
def generate_ml_recommendations(analysis_df):
|
| 201 |
+
"""
|
| 202 |
+
Generate recommendations based on ML predictions
|
| 203 |
+
"""
|
| 204 |
+
recommendations = []
|
| 205 |
+
|
| 206 |
+
for _, row in analysis_df.iterrows():
|
| 207 |
+
village = row['Village']
|
| 208 |
+
mantri = row['Mantri_Name']
|
| 209 |
+
mobile = row['Mantri_Mobile']
|
| 210 |
+
taluka = row['Taluka']
|
| 211 |
+
district = row['District']
|
| 212 |
+
segment = row['Segment']
|
| 213 |
+
action = row['ML_Recommended_Action']
|
| 214 |
+
confidence = row['Action_Confidence']
|
| 215 |
+
|
| 216 |
+
# Generate reason based on ML prediction
|
| 217 |
+
if action == 'Send Marketing Team':
|
| 218 |
+
reason = f"ML predicts marketing team needed (Confidence: {confidence:.2f}). Segment: {segment}"
|
| 219 |
+
priority = 'High'
|
| 220 |
+
elif action == 'Call Mantri for Follow-up':
|
| 221 |
+
reason = f"ML predicts mantri follow-up needed (Confidence: {confidence:.2f}). Segment: {segment}"
|
| 222 |
+
priority = 'High'
|
| 223 |
+
elif action == 'Check on Mantri':
|
| 224 |
+
reason = f"ML suggests checking on mantri (Confidence: {confidence:.2f}). Segment: {segment}"
|
| 225 |
+
priority = 'Medium'
|
| 226 |
+
elif action == 'Provide More Stock':
|
| 227 |
+
reason = f"ML predicts stock increase needed (Confidence: {confidence:.2f}). Segment: {segment}"
|
| 228 |
+
priority = 'Medium'
|
| 229 |
+
else:
|
| 230 |
+
reason = f"ML recommends regular follow-up (Confidence: {confidence:.2f}). Segment: {segment}"
|
| 231 |
+
priority = 'Low'
|
| 232 |
+
|
| 233 |
+
recommendations.append({
|
| 234 |
+
'Village': village,
|
| 235 |
+
'Taluka': taluka,
|
| 236 |
+
'District': district,
|
| 237 |
+
'Mantri': mantri,
|
| 238 |
+
'Mobile': mobile,
|
| 239 |
+
'Action': action,
|
| 240 |
+
'Reason': reason,
|
| 241 |
+
'Priority': priority,
|
| 242 |
+
'Confidence': confidence,
|
| 243 |
+
'Segment': segment,
|
| 244 |
+
'Sales_Gap': row.get('Sales_Gap', 0)
|
| 245 |
+
})
|
| 246 |
+
|
| 247 |
+
return pd.DataFrame(recommendations)
|
| 248 |
+
|
| 249 |
+
def generate_ml_mantri_messages(recommendations):
|
| 250 |
+
"""
|
| 251 |
+
Generate personalized messages based on ML recommendations
|
| 252 |
+
"""
|
| 253 |
+
messages = []
|
| 254 |
+
|
| 255 |
+
for _, row in recommendations.iterrows():
|
| 256 |
+
if row['Action'] == 'Send Marketing Team':
|
| 257 |
+
message = f"""
|
| 258 |
+
Namaste {row['Mantri']} Ji!
|
| 259 |
+
|
| 260 |
+
Our AI system has identified that your village {row['Village']} has high potential for growth.
|
| 261 |
+
We're sending our marketing team to conduct demo sessions and help you reach more customers.
|
| 262 |
+
|
| 263 |
+
Based on our analysis:
|
| 264 |
+
- Segment: {row['Segment']}
|
| 265 |
+
- Confidence: {row['Confidence']*100:.1f}%
|
| 266 |
+
|
| 267 |
+
Please prepare for their visit and notify potential customers.
|
| 268 |
+
|
| 269 |
+
Dhanyavaad,
|
| 270 |
+
Calcium Supplement Team
|
| 271 |
+
"""
|
| 272 |
+
elif row['Action'] == 'Call Mantri for Follow-up':
|
| 273 |
+
message = f"""
|
| 274 |
+
Namaste {row['Mantri']} Ji!
|
| 275 |
+
|
| 276 |
+
Our AI analysis shows significant untapped potential in {row['Village']}.
|
| 277 |
+
We recommend focusing on follow-up with these customers:
|
| 278 |
+
|
| 279 |
+
- Segment: {row['Segment']}
|
| 280 |
+
- Confidence: {row['Confidence']*100:.1f}%
|
| 281 |
+
|
| 282 |
+
A special commission offer is available for your next 10 customers.
|
| 283 |
+
|
| 284 |
+
Dhanyavaad,
|
| 285 |
+
Calcium Supplement Team
|
| 286 |
+
"""
|
| 287 |
+
elif row['Action'] == 'Check on Mantri':
|
| 288 |
+
message = f"""
|
| 289 |
+
Namaste {row['Mantri']} Ji!
|
| 290 |
+
|
| 291 |
+
Our system shows reduced activity in {row['Village']}.
|
| 292 |
+
Is everything alright? Do you need any support from our team?
|
| 293 |
+
|
| 294 |
+
- Segment: {row['Segment']}
|
| 295 |
+
- Confidence: {row['Confidence']*100:.1f}%
|
| 296 |
+
|
| 297 |
+
Please let us know how we can help.
|
| 298 |
+
|
| 299 |
+
Dhanyavaad,
|
| 300 |
+
Calcium Supplement Team
|
| 301 |
+
"""
|
| 302 |
+
elif row['Action'] == 'Provide More Stock':
|
| 303 |
+
message = f"""
|
| 304 |
+
Namaste {row['Mantri']} Ji!
|
| 305 |
+
|
| 306 |
+
Great news! Our AI predicts increased demand in {row['Village']}.
|
| 307 |
+
Would you like us to send additional stock?
|
| 308 |
+
|
| 309 |
+
- Segment: {row['Segment']}
|
| 310 |
+
- Confidence: {row['Confidence']*100:.1f}%
|
| 311 |
+
- Predicted Sales Gap: {row['Sales_Gap']:.1f}L
|
| 312 |
+
|
| 313 |
+
Please confirm your additional requirements.
|
| 314 |
+
|
| 315 |
+
Dhanyavaad,
|
| 316 |
+
Calcium Supplement Team
|
| 317 |
+
"""
|
| 318 |
+
else:
|
| 319 |
+
message = f"""
|
| 320 |
+
Namaste {row['Mantri']} Ji!
|
| 321 |
+
|
| 322 |
+
Our system shows steady performance in {row['Village']}.
|
| 323 |
+
Keep up the good work!
|
| 324 |
+
|
| 325 |
+
- Segment: {row['Segment']}
|
| 326 |
+
- Confidence: {row['Confidence']*100:.1f}%
|
| 327 |
+
|
| 328 |
+
As always, let us know if you need any support.
|
| 329 |
+
|
| 330 |
+
Dhanyavaad,
|
| 331 |
+
Calcium Supplement Team
|
| 332 |
+
"""
|
| 333 |
+
|
| 334 |
+
messages.append({
|
| 335 |
+
'Mantri': row['Mantri'],
|
| 336 |
+
'Mobile': row['Mobile'],
|
| 337 |
+
'Village': row['Village'],
|
| 338 |
+
'Action': row['Action'],
|
| 339 |
+
'Message': message,
|
| 340 |
+
'Priority': row['Priority'],
|
| 341 |
+
'Confidence': row['Confidence']
|
| 342 |
+
})
|
| 343 |
+
|
| 344 |
+
return pd.DataFrame(messages)
|
| 345 |
+
|
| 346 |
+
# Visualization functions
|
| 347 |
+
def plot_village_performance(analysis_df):
|
| 348 |
+
"""Create performance visualization for villages"""
|
| 349 |
+
fig = px.scatter(analysis_df,
|
| 350 |
+
x='Conversion_Rate',
|
| 351 |
+
y='Untapped_Potential',
|
| 352 |
+
size='Total_L',
|
| 353 |
+
color='Segment',
|
| 354 |
+
hover_name='Village',
|
| 355 |
+
title='Village Performance Analysis',
|
| 356 |
+
labels={'Conversion_Rate': 'Conversion Rate (%)',
|
| 357 |
+
'Untapped_Potential': 'Untapped Potential'})
|
| 358 |
+
|
| 359 |
+
fig.update_layout(height=500)
|
| 360 |
+
return fig
|
| 361 |
+
|
| 362 |
+
def plot_sales_trends(analysis_df):
|
| 363 |
+
"""Create sales trends visualization"""
|
| 364 |
+
fig = px.bar(analysis_df,
|
| 365 |
+
x='Village',
|
| 366 |
+
y='Total_L',
|
| 367 |
+
color='Segment',
|
| 368 |
+
title='Total Sales by Village',
|
| 369 |
+
labels={'Total_L': 'Total Sales (L)', 'Village': 'Village'})
|
| 370 |
+
|
| 371 |
+
fig.update_layout(height=400, xaxis_tickangle=-45)
|
| 372 |
+
return fig
|
| 373 |
+
|
| 374 |
+
def plot_priority_matrix(recommendations):
|
| 375 |
+
"""Create priority matrix visualization"""
|
| 376 |
+
priority_order = {'High': 3, 'Medium': 2, 'Low': 1}
|
| 377 |
+
recommendations['Priority_Value'] = recommendations['Priority'].map(priority_order)
|
| 378 |
+
|
| 379 |
+
fig = px.treemap(recommendations,
|
| 380 |
+
path=['Priority', 'Village'],
|
| 381 |
+
values='Priority_Value',
|
| 382 |
+
color='Priority_Value',
|
| 383 |
+
color_continuous_scale='RdYlGn_r',
|
| 384 |
+
title='Action Priority Matrix')
|
| 385 |
+
|
| 386 |
+
fig.update_layout(height=500)
|
| 387 |
+
return fig
|
| 388 |
+
|
| 389 |
+
def display_key_metrics(analysis_df):
|
| 390 |
+
"""Display key performance metrics"""
|
| 391 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 392 |
+
|
| 393 |
+
with col1:
|
| 394 |
+
st.metric("Total Villages", len(analysis_df))
|
| 395 |
+
with col2:
|
| 396 |
+
avg_conversion = analysis_df['Conversion_Rate'].mean()
|
| 397 |
+
st.metric("Avg Conversion Rate", f"{avg_conversion:.1f}%")
|
| 398 |
+
with col3:
|
| 399 |
+
total_untapped = analysis_df['Untapped_Potential'].sum()
|
| 400 |
+
st.metric("Total Untapped Potential", f"{total_untapped}")
|
| 401 |
+
with col4:
|
| 402 |
+
total_sales = analysis_df['Total_L'].sum()
|
| 403 |
+
st.metric("Total Sales (L)", f"{total_sales}")
|
| 404 |
+
|
| 405 |
+
# Initialize session state
|
| 406 |
+
if 'data1' not in st.session_state:
|
| 407 |
+
st.session_state.data1 = None
|
| 408 |
+
if 'data2' not in st.session_state:
|
| 409 |
+
st.session_state.data2 = None
|
| 410 |
+
if 'analysis_df' not in st.session_state:
|
| 411 |
+
st.session_state.analysis_df = None
|
| 412 |
+
if 'recommendations' not in st.session_state:
|
| 413 |
+
st.session_state.recommendations = None
|
| 414 |
+
if 'ml_messages' not in st.session_state:
|
| 415 |
+
st.session_state.ml_messages = None
|
| 416 |
+
|
| 417 |
+
# Sidebar
|
| 418 |
+
with st.sidebar:
|
| 419 |
+
st.header("Data Input")
|
| 420 |
+
|
| 421 |
+
# File uploaders
|
| 422 |
+
st.subheader("Upload Village Data (Data1)")
|
| 423 |
+
uploaded_data1 = st.file_uploader("CSV or Excel file", type=["csv", "xlsx"], key="data1")
|
| 424 |
+
|
| 425 |
+
st.subheader("Upload Sales Data (Data2)")
|
| 426 |
+
uploaded_data2 = st.file_uploader("CSV or Excel file", type=["csv", "xlsx"], key="data2")
|
| 427 |
+
|
| 428 |
+
if st.button("Load Data and Run ML Analysis"):
|
| 429 |
+
if uploaded_data1 and uploaded_data2:
|
| 430 |
+
try:
|
| 431 |
+
# Load data
|
| 432 |
+
if uploaded_data1.name.endswith('.csv'):
|
| 433 |
+
data1 = pd.read_csv(uploaded_data1)
|
| 434 |
+
else:
|
| 435 |
+
data1 = pd.read_excel(uploaded_data1)
|
| 436 |
+
|
| 437 |
+
if uploaded_data2.name.endswith('.csv'):
|
| 438 |
+
data2 = pd.read_csv(uploaded_data2)
|
| 439 |
+
else:
|
| 440 |
+
data2 = pd.read_excel(uploaded_data2)
|
| 441 |
+
|
| 442 |
+
# Store in session state
|
| 443 |
+
st.session_state.data1 = data1
|
| 444 |
+
st.session_state.data2 = data2
|
| 445 |
+
|
| 446 |
+
# Run ML analysis
|
| 447 |
+
with st.spinner("Running ML analysis..."):
|
| 448 |
+
recommendations, analysis_df = enhanced_analyze_sales_data(data1, data2)
|
| 449 |
+
st.session_state.analysis_df = analysis_df
|
| 450 |
+
st.session_state.recommendations = recommendations
|
| 451 |
+
|
| 452 |
+
ml_messages = generate_ml_mantri_messages(recommendations)
|
| 453 |
+
st.session_state.ml_messages = ml_messages
|
| 454 |
+
|
| 455 |
+
st.success("ML analysis completed successfully!")
|
| 456 |
+
|
| 457 |
+
except Exception as e:
|
| 458 |
+
st.error(f"Error processing data: {str(e)}")
|
| 459 |
+
else:
|
| 460 |
+
st.error("Please upload both files to proceed")
|
| 461 |
+
|
| 462 |
+
# Main content
|
| 463 |
+
if st.session_state.analysis_df is not None and st.session_state.recommendations is not None:
|
| 464 |
+
# Display dashboard
|
| 465 |
+
tab1, tab2, tab3, tab4 = st.tabs(["Dashboard", "Village Analysis", "Actions & Messages", "Team Dispatch"])
|
| 466 |
+
|
| 467 |
+
with tab1:
|
| 468 |
+
st.header("ML-Powered Performance Dashboard")
|
| 469 |
+
display_key_metrics(st.session_state.analysis_df)
|
| 470 |
+
|
| 471 |
+
col1, col2 = st.columns(2)
|
| 472 |
+
|
| 473 |
+
with col1:
|
| 474 |
+
st.plotly_chart(plot_village_performance(st.session_state.analysis_df), use_container_width=True)
|
| 475 |
+
|
| 476 |
+
with col2:
|
| 477 |
+
st.plotly_chart(plot_priority_matrix(st.session_state.recommendations), use_container_width=True)
|
| 478 |
+
|
| 479 |
+
st.plotly_chart(plot_sales_trends(st.session_state.analysis_df), use_container_width=True)
|
| 480 |
+
|
| 481 |
+
with tab2:
|
| 482 |
+
st.header("Village Analysis with ML Segmentation")
|
| 483 |
+
|
| 484 |
+
selected_village = st.selectbox("Select Village", st.session_state.analysis_df['Village'].unique())
|
| 485 |
+
village_data = st.session_state.analysis_df[st.session_state.analysis_df['Village'] == selected_village].iloc[0]
|
| 486 |
+
|
| 487 |
+
col1, col2 = st.columns(2)
|
| 488 |
+
|
| 489 |
+
with col1:
|
| 490 |
+
st.subheader("Village Details")
|
| 491 |
+
st.write(f"**Village:** {village_data['Village']}")
|
| 492 |
+
st.write(f"**Taluka:** {village_data['Taluka']}")
|
| 493 |
+
st.write(f"**District:** {village_data['District']}")
|
| 494 |
+
st.write(f"**Mantri:** {village_data['Mantri_Name']}")
|
| 495 |
+
st.write(f"**Mantri Mobile:** {village_data['Mantri_Mobile']}")
|
| 496 |
+
st.write(f"**Segment:** {village_data.get('Segment', 'N/A')}")
|
| 497 |
+
st.write(f"**ML Recommended Action:** {village_data.get('ML_Recommended_Action', 'N/A')}")
|
| 498 |
+
st.write(f"**Action Confidence:** {village_data.get('Action_Confidence', 'N/A'):.2f}")
|
| 499 |
+
|
| 500 |
+
with col2:
|
| 501 |
+
st.subheader("Performance Metrics")
|
| 502 |
+
st.write(f"**Sabhasad:** {village_data['Sabhasad']}")
|
| 503 |
+
st.write(f"**Contacted:** {village_data['Contact_In_Group']}")
|
| 504 |
+
st.write(f"**Conversion Rate:** {village_data['Conversion_Rate']}%")
|
| 505 |
+
st.write(f"**Untapped Potential:** {village_data['Untapped_Potential']}")
|
| 506 |
+
st.write(f"**Total Sales:** {village_data['Total_L']}L")
|
| 507 |
+
st.write(f"**Sales per Contact:** {village_data['Sales_Per_Contact']}L")
|
| 508 |
+
st.write(f"**Predicted Sales:** {village_data.get('Predicted_Sales', 'N/A'):.1f}L")
|
| 509 |
+
st.write(f"**Sales Gap:** {village_data.get('Sales_Gap', 'N/A'):.1f}L")
|
| 510 |
+
|
| 511 |
+
with tab3:
|
| 512 |
+
st.header("ML-Based Actions & Messages")
|
| 513 |
+
|
| 514 |
+
st.subheader("ML-Generated Recommendations")
|
| 515 |
+
st.dataframe(st.session_state.recommendations)
|
| 516 |
+
|
| 517 |
+
# Download recommendations
|
| 518 |
+
csv_data = st.session_state.recommendations.to_csv(index=False)
|
| 519 |
+
st.download_button(
|
| 520 |
+
label="Download Recommendations as CSV",
|
| 521 |
+
data=csv_data,
|
| 522 |
+
file_name="ml_sales_recommendations.csv",
|
| 523 |
+
mime="text/csv"
|
| 524 |
+
)
|
| 525 |
+
|
| 526 |
+
st.subheader("Generate ML-Powered Messages")
|
| 527 |
+
selected_mantri = st.selectbox("Select Mantri", st.session_state.recommendations['Mantri'].unique())
|
| 528 |
+
mantri_data = st.session_state.recommendations[
|
| 529 |
+
st.session_state.recommendations['Mantri'] == selected_mantri].iloc[0]
|
| 530 |
+
|
| 531 |
+
message_df = st.session_state.ml_messages[
|
| 532 |
+
st.session_state.ml_messages['Mantri'] == selected_mantri]
|
| 533 |
+
|
| 534 |
+
if not message_df.empty:
|
| 535 |
+
message = message_df.iloc[0]['Message']
|
| 536 |
+
st.text_area("ML-Generated Message", message, height=300)
|
| 537 |
+
|
| 538 |
+
if st.button("Send Message"):
|
| 539 |
+
st.success(f"Message sent to {mantri_data['Mantri']} at {mantri_data['Mobile']}")
|
| 540 |
+
|
| 541 |
+
st.subheader("Bulk Message Sender")
|
| 542 |
+
if st.button("Generate All ML Messages"):
|
| 543 |
+
st.session_state.all_messages = st.session_state.ml_messages
|
| 544 |
+
|
| 545 |
+
if 'all_messages' in st.session_state:
|
| 546 |
+
st.dataframe(st.session_state.all_messages[['Mantri', 'Village', 'Action', 'Priority', 'Confidence']])
|
| 547 |
+
|
| 548 |
+
if st.button("Send All ML Messages"):
|
| 549 |
+
progress_bar = st.progress(0)
|
| 550 |
+
for i, row in st.session_state.all_messages.iterrows():
|
| 551 |
+
# Simulate sending message
|
| 552 |
+
progress_bar.progress((i + 1) / len(st.session_state.all_messages))
|
| 553 |
+
st.success("All ML-powered messages sent successfully!")
|
| 554 |
+
|
| 555 |
+
with tab4:
|
| 556 |
+
st.header("Marketing Team Dispatch with ML Insights")
|
| 557 |
+
|
| 558 |
+
st.subheader("Villages Needing Team Visit (ML Identified)")
|
| 559 |
+
high_priority = st.session_state.recommendations[
|
| 560 |
+
st.session_state.recommendations['Action'] == 'Send Marketing Team']
|
| 561 |
+
|
| 562 |
+
if not high_priority.empty:
|
| 563 |
+
for _, row in high_priority.iterrows():
|
| 564 |
+
with st.expander(f"{row['Village']} - {row['Mantri']} (Confidence: {row['Confidence']:.2f})"):
|
| 565 |
+
st.write(f"**Reason:** {row['Reason']}")
|
| 566 |
+
st.write(f"**Segment:** {row['Segment']}")
|
| 567 |
+
st.write(f"**Sales Gap:** {row['Sales_Gap']:.1f}L")
|
| 568 |
+
|
| 569 |
+
dispatch_date = st.date_input("Dispatch Date", key=f"date_{row['Village']}")
|
| 570 |
+
team_size = st.slider("Team Size", 1, 5, 2, key=f"size_{row['Village']}")
|
| 571 |
+
|
| 572 |
+
if st.button("Schedule Dispatch", key=f"dispatch_{row['Village']}"):
|
| 573 |
+
st.success(f"Team dispatch scheduled for {row['Village']} on {dispatch_date}")
|
| 574 |
+
else:
|
| 575 |
+
st.info("No villages currently require immediate team dispatch based on ML analysis.")
|
| 576 |
+
|
| 577 |
+
st.subheader("ML Performance Insights")
|
| 578 |
+
st.write("Based on our machine learning analysis, here are key insights:")
|
| 579 |
+
|
| 580 |
+
# Show segment distribution
|
| 581 |
+
segment_counts = st.session_state.analysis_df['Segment'].value_counts()
|
| 582 |
+
fig = px.pie(values=segment_counts.values, names=segment_counts.index,
|
| 583 |
+
title="Village Segment Distribution")
|
| 584 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 585 |
+
|
| 586 |
+
# Show confidence distribution
|
| 587 |
+
fig = px.histogram(st.session_state.recommendations, x='Confidence',
|
| 588 |
+
title='Confidence Distribution of ML Recommendations')
|
| 589 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 590 |
+
|
| 591 |
+
else:
|
| 592 |
+
st.info("Please upload your data files using the sidebar and click 'Load Data and Run ML Analysis' to get started.")
|
| 593 |
+
|
| 594 |
+
# Footer
|
| 595 |
+
st.markdown("---")
|
| 596 |
+
st.markdown("**ML-Powered Calcium Supplement Sales Automation System** | For internal use only")
|
OLD/sampleDashboard.py
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
import plotly.graph_objects as go
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
import time
|
| 8 |
+
|
| 9 |
+
# Set page configuration
|
| 10 |
+
st.set_page_config(
|
| 11 |
+
page_title="Calcium Supplement Sales Dashboard",
|
| 12 |
+
page_icon="🐄",
|
| 13 |
+
layout="wide",
|
| 14 |
+
initial_sidebar_state="expanded"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
# Sample data (replace with your actual data loading)
|
| 18 |
+
@st.cache_data
|
| 19 |
+
def load_data():
|
| 20 |
+
# Sales data with customer information
|
| 21 |
+
sales_data = pd.DataFrame({
|
| 22 |
+
'Date': ['2025-06-01', '2025-06-01', '2025-06-10', '2025-06-11', '2025-06-12',
|
| 23 |
+
'2025-07-30', '2025-07-30', '2025-07-31', '2025-07-31', '2025-07-31'],
|
| 24 |
+
'Customer': ['Gopalbhai', 'Ramprasad Khatik', 'Vikramsinh', 'Prahladbhai -Mantry', 'V S Stud Farm',
|
| 25 |
+
'Hemendrabhai Parmar', 'Sundarbhai', 'Kamleshbhai Vasava -Mantry', 'Kiranbhai -Mantry', 'Kiritbhai'],
|
| 26 |
+
'Village': ['Shilly', 'Rajasthan', 'Mithapura', 'Bhalod Dairy', 'Waghodia',
|
| 27 |
+
'Panchdevla', 'Siyali', 'Moran', 'Talodara', 'Sindhrot'],
|
| 28 |
+
'Total_L': [35.0, 400.0, 30.0, 7.0, 400.0, 50.0, 13.0, 1.0, 1.0, 30.0]
|
| 29 |
+
})
|
| 30 |
+
|
| 31 |
+
# Mantri data with village information
|
| 32 |
+
mantri_data = pd.DataFrame({
|
| 33 |
+
'DATE': ['2024-03-08', '2025-06-03', '2025-02-23', '2025-05-28', '2025-05-02',
|
| 34 |
+
'2024-09-21', '2024-10-26', '2024-03-19', '2025-01-30', '2025-07-18'],
|
| 35 |
+
'VILLAGE': ['JILOD', 'MANJIPURA', 'GOTHADA', 'UNTKHARI', 'VEMAR',
|
| 36 |
+
'KANODA', 'KOTAMBI', 'RASNOL', 'JITPURA', 'BHATPURA'],
|
| 37 |
+
'MANTRY_NAME': ['AJAYBHAI PATEL', 'AJAYBHAI PATEL', 'AJGAR KHAN', 'AMBALAL CHAUHAN', 'AMBALAL GOHIL',
|
| 38 |
+
'VINUBHAI SOLANKI', 'VISHNUBHAI', 'VITHTHALBHAI', 'YOGESHBHAI', 'YUVRAJSINH'],
|
| 39 |
+
'MOBILE_NO': [7984136988, 9737910554, 9724831903, 9313860902, 9978081739,
|
| 40 |
+
9998756469, 9909550170, 9924590017, 7990383811, 6353209447],
|
| 41 |
+
'sabhasad': [38, 21, 3, 0, 2, 0, 14, 1183, 8, 6],
|
| 42 |
+
'contact_in_group': [38.0, 16.0, 2.0, 0.0, 0.0, 0.0, 14.0, 268.0, 5.0, 4.0],
|
| 43 |
+
'TOTAL_L': [99.0, 120.0, 19.0, 87.0, 32.0, 60.0, 54.0, 82.0, 25.0, 11.0]
|
| 44 |
+
})
|
| 45 |
+
|
| 46 |
+
# Convert dates to datetime
|
| 47 |
+
sales_data['Date'] = pd.to_datetime(sales_data['Date'])
|
| 48 |
+
mantri_data['DATE'] = pd.to_datetime(mantri_data['DATE'], errors='coerce')
|
| 49 |
+
|
| 50 |
+
return sales_data, mantri_data
|
| 51 |
+
|
| 52 |
+
# Analysis functions
|
| 53 |
+
def analyze_mantri_performance(mantri_data, sales_data):
|
| 54 |
+
mantri_data = mantri_data.copy()
|
| 55 |
+
|
| 56 |
+
# Calculate performance metrics
|
| 57 |
+
mantri_data['Conversion_Rate'] = (mantri_data['contact_in_group'] / mantri_data['sabhasad'] * 100).round(2)
|
| 58 |
+
mantri_data['Conversion_Rate'] = mantri_data['Conversion_Rate'].replace([np.inf, -np.inf], 0).fillna(0)
|
| 59 |
+
mantri_data['Untapped_Potential'] = mantri_data['sabhasad'] - mantri_data['contact_in_group']
|
| 60 |
+
mantri_data['Sales_Efficiency'] = (mantri_data['TOTAL_L'] / mantri_data['contact_in_group']).round(2)
|
| 61 |
+
mantri_data['Sales_Efficiency'] = mantri_data['Sales_Efficiency'].replace([np.inf, -np.inf], 0).fillna(0)
|
| 62 |
+
|
| 63 |
+
# Priority score calculation
|
| 64 |
+
mantri_data['Priority_Score'] = (
|
| 65 |
+
(mantri_data['Untapped_Potential'] / mantri_data['Untapped_Potential'].max() * 50) +
|
| 66 |
+
((100 - mantri_data['Conversion_Rate']) / 100 * 50)
|
| 67 |
+
).round(2)
|
| 68 |
+
|
| 69 |
+
# Add recent sales data
|
| 70 |
+
recent_sales = sales_data.groupby('Village').agg({
|
| 71 |
+
'Total_L': 'sum',
|
| 72 |
+
'Customer': 'count'
|
| 73 |
+
}).reset_index()
|
| 74 |
+
recent_sales.columns = ['VILLAGE', 'Recent_Sales', 'Recent_Customers']
|
| 75 |
+
|
| 76 |
+
mantri_data = mantri_data.merge(recent_sales, on='VILLAGE', how='left')
|
| 77 |
+
mantri_data['Recent_Sales'] = mantri_data['Recent_Sales'].fillna(0)
|
| 78 |
+
mantri_data['Recent_Customers'] = mantri_data['Recent_Customers'].fillna(0)
|
| 79 |
+
|
| 80 |
+
return mantri_data
|
| 81 |
+
|
| 82 |
+
def analyze_village_performance(sales_data, mantri_data):
|
| 83 |
+
# Group sales by village
|
| 84 |
+
village_sales = sales_data.groupby('Village').agg({
|
| 85 |
+
'Total_L': 'sum',
|
| 86 |
+
'Customer': 'count',
|
| 87 |
+
'Date': 'max'
|
| 88 |
+
}).reset_index()
|
| 89 |
+
village_sales.columns = ['Village', 'Total_Sales', 'Customer_Count', 'Last_Sale_Date']
|
| 90 |
+
|
| 91 |
+
# Calculate days since last sale
|
| 92 |
+
village_sales['Days_Since_Last_Sale'] = (datetime.now() - village_sales['Last_Sale_Date']).dt.days
|
| 93 |
+
|
| 94 |
+
# Merge with mantri data
|
| 95 |
+
mantri_summary = mantri_data[['VILLAGE', 'MANTRY_NAME', 'MOBILE_NO', 'sabhasad', 'contact_in_group']]
|
| 96 |
+
mantri_summary.columns = ['Village', 'Mantri_Name', 'Mantri_Mobile', 'Sabhasad', 'Contacts']
|
| 97 |
+
|
| 98 |
+
village_performance = village_sales.merge(mantri_summary, on='Village', how='left')
|
| 99 |
+
|
| 100 |
+
# Calculate performance metrics
|
| 101 |
+
village_performance['Conversion_Rate'] = (village_performance['Contacts'] / village_performance['Sabhasad'] * 100).round(2)
|
| 102 |
+
village_performance['Conversion_Rate'] = village_performance['Conversion_Rate'].replace([np.inf, -np.inf], 0).fillna(0)
|
| 103 |
+
village_performance['Untapped_Potential'] = village_performance['Sabhasad'] - village_performance['Contacts']
|
| 104 |
+
|
| 105 |
+
return village_performance
|
| 106 |
+
|
| 107 |
+
# Message templates
|
| 108 |
+
def get_mantri_message_template(mantri_name, village, reason, performance_data):
|
| 109 |
+
templates = {
|
| 110 |
+
'Low Conversion': f"""
|
| 111 |
+
Namaste {mantri_name} Ji!
|
| 112 |
+
|
| 113 |
+
Aapke kshetra {village} mein humare calcium supplement ki conversion rate kam hai ({performance_data['Conversion_Rate']}%).
|
| 114 |
+
Humari marketing team aapke yaha demo dene aayegi.
|
| 115 |
+
Kripya taiyaari rakhein aur sabhi dudh utpadakon ko soochit karein.
|
| 116 |
+
|
| 117 |
+
Aapke paas abhi bhi {int(performance_data['Untapped_Potential'])} aise farmers hain jo product nahi use kar rahe hain.
|
| 118 |
+
|
| 119 |
+
Dhanyavaad,
|
| 120 |
+
Calcium Supplement Team
|
| 121 |
+
""",
|
| 122 |
+
'High Potential': f"""
|
| 123 |
+
Namaste {mantri_name} Ji!
|
| 124 |
+
|
| 125 |
+
Aapke kshetra {village} mein {int(performance_data['Untapped_Potential'])} aise farmers hain jo abhi tak humare product se anabhijit hain.
|
| 126 |
+
Kripya unse sampark karein aur unhe product ke fayde batayein.
|
| 127 |
+
Aapke liye special commission offer hai agle 10 naye customers ke liye.
|
| 128 |
+
|
| 129 |
+
Dhanyavaad,
|
| 130 |
+
Calcium Supplement Team
|
| 131 |
+
""",
|
| 132 |
+
'Good Performance': f"""
|
| 133 |
+
Namaste {mantri_name} Ji!
|
| 134 |
+
|
| 135 |
+
Aapke kshetra {village} mein humare product ki demand badh rahi hai.
|
| 136 |
+
Aapki conversion rate {performance_data['Conversion_Rate']}% hai jo bahut achchi hai.
|
| 137 |
+
|
| 138 |
+
Kripya farmers ko yaad dilaein ki pregnancy ke 3-9 mahine aur delivery ke baad calcium supplement zaroori hai.
|
| 139 |
+
|
| 140 |
+
Dhanyavaad,
|
| 141 |
+
Calcium Supplement Team
|
| 142 |
+
"""
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
return templates.get(reason, "Custom message based on analysis")
|
| 146 |
+
|
| 147 |
+
# Load data
|
| 148 |
+
sales_data, mantri_data = load_data()
|
| 149 |
+
mantri_performance = analyze_mantri_performance(mantri_data, sales_data)
|
| 150 |
+
village_performance = analyze_village_performance(sales_data, mantri_data)
|
| 151 |
+
|
| 152 |
+
# Streamlit app
|
| 153 |
+
st.title("🐄 Calcium Supplement Sales Automation Dashboard")
|
| 154 |
+
st.markdown("---")
|
| 155 |
+
|
| 156 |
+
# Sidebar
|
| 157 |
+
st.sidebar.header("Navigation")
|
| 158 |
+
section = st.sidebar.radio("Go to", ["Dashboard", "Mantri Performance", "Village Analysis", "Message Center", "Team Dispatch"])
|
| 159 |
+
|
| 160 |
+
# Dashboard
|
| 161 |
+
if section == "Dashboard":
|
| 162 |
+
st.header("Sales Performance Overview")
|
| 163 |
+
|
| 164 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 165 |
+
|
| 166 |
+
with col1:
|
| 167 |
+
st.metric("Total Villages Covered", len(mantri_performance))
|
| 168 |
+
with col2:
|
| 169 |
+
st.metric("Total Mantris", len(mantri_performance['MANTRY_NAME'].unique()))
|
| 170 |
+
with col3:
|
| 171 |
+
st.metric("Total Sales (Liters)", mantri_performance['TOTAL_L'].sum())
|
| 172 |
+
with col4:
|
| 173 |
+
avg_conversion = mantri_performance['Conversion_Rate'].mean()
|
| 174 |
+
st.metric("Avg Conversion Rate", f"{avg_conversion:.2f}%")
|
| 175 |
+
|
| 176 |
+
st.subheader("Top Priority Mantris")
|
| 177 |
+
priority_mantris = mantri_performance.nlargest(5, 'Priority_Score')[['MANTRY_NAME', 'VILLAGE', 'Conversion_Rate', 'Untapped_Potential', 'Priority_Score']]
|
| 178 |
+
st.dataframe(priority_mantris)
|
| 179 |
+
|
| 180 |
+
st.subheader("Sales Distribution by Village")
|
| 181 |
+
fig = px.bar(mantri_performance, x='VILLAGE', y='TOTAL_L', title='Total Sales by Village')
|
| 182 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 183 |
+
|
| 184 |
+
st.subheader("Conversion Rate vs Untapped Potential")
|
| 185 |
+
fig = px.scatter(mantri_performance, x='Conversion_Rate', y='Untapped_Potential',
|
| 186 |
+
size='TOTAL_L', color='VILLAGE', hover_name='MANTRY_NAME',
|
| 187 |
+
title='Mantri Performance Analysis')
|
| 188 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 189 |
+
|
| 190 |
+
# Mantri Performance
|
| 191 |
+
elif section == "Mantri Performance":
|
| 192 |
+
st.header("Mantri Performance Analysis")
|
| 193 |
+
|
| 194 |
+
selected_mantri = st.selectbox("Select Mantri", mantri_performance['MANTRY_NAME'].unique())
|
| 195 |
+
mantri_data = mantri_performance[mantri_performance['MANTRY_NAME'] == selected_mantri].iloc[0]
|
| 196 |
+
|
| 197 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 198 |
+
|
| 199 |
+
with col1:
|
| 200 |
+
st.metric("Mantri", mantri_data['MANTRY_NAME'])
|
| 201 |
+
with col2:
|
| 202 |
+
st.metric("Village", mantri_data['VILLAGE'])
|
| 203 |
+
with col3:
|
| 204 |
+
st.metric("Conversion Rate", f"{mantri_data['Conversion_Rate']}%")
|
| 205 |
+
with col4:
|
| 206 |
+
st.metric("Untapped Potential", int(mantri_data['Untapped_Potential']))
|
| 207 |
+
|
| 208 |
+
st.subheader("Mantri Details")
|
| 209 |
+
st.dataframe(mantri_data)
|
| 210 |
+
|
| 211 |
+
st.subheader("Action Recommendations")
|
| 212 |
+
if mantri_data['Conversion_Rate'] < 20:
|
| 213 |
+
st.error(f"**Send Marketing Team**: Conversion rate is low ({mantri_data['Conversion_Rate']}%). Need demos and awareness campaigns.")
|
| 214 |
+
if mantri_data['Untapped_Potential'] > 10:
|
| 215 |
+
st.warning(f"**Call Mantri**: {int(mantri_data['Untapped_Potential'])} farmers still not converted. Push Mantri to contact them.")
|
| 216 |
+
if mantri_data['Conversion_Rate'] > 50:
|
| 217 |
+
st.success(f"**Expand Success**: This mantri is performing well. Consider replicating their strategies.")
|
| 218 |
+
|
| 219 |
+
# Village Analysis
|
| 220 |
+
elif section == "Village Analysis":
|
| 221 |
+
st.header("Village Performance Analysis")
|
| 222 |
+
|
| 223 |
+
selected_village = st.selectbox("Select Village", village_performance['Village'].unique())
|
| 224 |
+
village_data = village_performance[village_performance['Village'] == selected_village].iloc[0]
|
| 225 |
+
|
| 226 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 227 |
+
|
| 228 |
+
with col1:
|
| 229 |
+
st.metric("Village", village_data['Village'])
|
| 230 |
+
with col2:
|
| 231 |
+
st.metric("Mantri", village_data['Mantri_Name'])
|
| 232 |
+
with col3:
|
| 233 |
+
st.metric("Total Sales (L)", village_data['Total_Sales'])
|
| 234 |
+
with col4:
|
| 235 |
+
st.metric("Days Since Last Sale", village_data['Days_Since_Last_Sale'])
|
| 236 |
+
|
| 237 |
+
st.subheader("Village Details")
|
| 238 |
+
st.dataframe(village_data)
|
| 239 |
+
|
| 240 |
+
st.subheader("Action Recommendations")
|
| 241 |
+
if village_data['Days_Since_Last_Sale'] > 30:
|
| 242 |
+
st.error(f"**Send Marketing Team**: No sales in {village_data['Days_Since_Last_Sale']} days. Need immediate attention.")
|
| 243 |
+
if village_data['Conversion_Rate'] < 25:
|
| 244 |
+
st.warning(f"**Low Conversion**: Only {village_data['Conversion_Rate']}% of potential customers are converted.")
|
| 245 |
+
if village_data['Total_Sales'] > 100:
|
| 246 |
+
st.success(f"**High Performer**: This village has high sales volume. Consider expanding product range.")
|
| 247 |
+
|
| 248 |
+
# Message Center
|
| 249 |
+
elif section == "Message Center":
|
| 250 |
+
st.header("Message Center")
|
| 251 |
+
|
| 252 |
+
st.subheader("Mantri Communication")
|
| 253 |
+
selected_mantri = st.selectbox("Select Mantri", mantri_performance['MANTRY_NAME'].unique())
|
| 254 |
+
mantri_data = mantri_performance[mantri_performance['MANTRY_NAME'] == selected_mantri].iloc[0]
|
| 255 |
+
|
| 256 |
+
st.write(f"**Village:** {mantri_data['VILLAGE']}")
|
| 257 |
+
st.write(f"**Conversion Rate:** {mantri_data['Conversion_Rate']}%")
|
| 258 |
+
st.write(f"**Untapped Potential:** {int(mantri_data['Untapped_Potential'])} farmers")
|
| 259 |
+
|
| 260 |
+
if mantri_data['Conversion_Rate'] < 20:
|
| 261 |
+
reason = "Low Conversion"
|
| 262 |
+
elif mantri_data['Untapped_Potential'] > 10:
|
| 263 |
+
reason = "High Potential"
|
| 264 |
+
else:
|
| 265 |
+
reason = "Good Performance"
|
| 266 |
+
|
| 267 |
+
message = get_mantri_message_template(
|
| 268 |
+
mantri_data['MANTRY_NAME'],
|
| 269 |
+
mantri_data['VILLAGE'],
|
| 270 |
+
reason,
|
| 271 |
+
mantri_data
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
st.text_area("Generated Message", message, height=200)
|
| 275 |
+
|
| 276 |
+
if st.button("Send to Mantri"):
|
| 277 |
+
st.success(f"Message sent to {mantri_data['MANTRY_NAME']} at {mantri_data['MOBILE_NO']}")
|
| 278 |
+
# Here you would integrate with WhatsApp API
|
| 279 |
+
|
| 280 |
+
st.subheader("Bulk Message Sender")
|
| 281 |
+
st.write("Send messages to multiple mantris at once")
|
| 282 |
+
|
| 283 |
+
options = st.multiselect("Select Mantris", mantri_performance['MANTRY_NAME'].unique())
|
| 284 |
+
message_template = st.text_area("Message Template", height=100)
|
| 285 |
+
|
| 286 |
+
if st.button("Send to Selected Mantris"):
|
| 287 |
+
progress_bar = st.progress(0)
|
| 288 |
+
for i, mantri in enumerate(options):
|
| 289 |
+
# Simulate sending
|
| 290 |
+
time.sleep(0.5)
|
| 291 |
+
progress_bar.progress((i + 1) / len(options))
|
| 292 |
+
st.success(f"Messages sent to {len(options)} mantris")
|
| 293 |
+
|
| 294 |
+
# Team Dispatch
|
| 295 |
+
elif section == "Team Dispatch":
|
| 296 |
+
st.header("Marketing Team Dispatch Planner")
|
| 297 |
+
|
| 298 |
+
st.subheader("Villages Needing Immediate Attention")
|
| 299 |
+
|
| 300 |
+
# Find villages with no recent sales or low conversion
|
| 301 |
+
high_priority = village_performance[
|
| 302 |
+
(village_performance['Days_Since_Last_Sale'] > 30) |
|
| 303 |
+
(village_performance['Conversion_Rate'] < 20)
|
| 304 |
+
]
|
| 305 |
+
|
| 306 |
+
if not high_priority.empty:
|
| 307 |
+
for _, village in high_priority.iterrows():
|
| 308 |
+
with st.expander(f"{village['Village']} (Last sale: {village['Days_Since_Last_Sale']} days ago)"):
|
| 309 |
+
st.write(f"**Mantri:** {village['Mantri_Name']} ({village['Mantri_Mobile']})")
|
| 310 |
+
st.write(f"**Conversion Rate:** {village['Conversion_Rate']}%")
|
| 311 |
+
st.write(f"**Recommended Action:** Conduct demo sessions and awareness campaign")
|
| 312 |
+
|
| 313 |
+
if st.button(f"Dispatch Team to {village['Village']}", key=f"dispatch_{village['Village']}"):
|
| 314 |
+
st.success(f"Team dispatched to {village['Village']}. Mantri {village['Mantri_Name']} has been notified.")
|
| 315 |
+
else:
|
| 316 |
+
st.info("No villages currently require immediate team dispatch.")
|
| 317 |
+
|
| 318 |
+
st.subheader("Create New Dispatch Plan")
|
| 319 |
+
|
| 320 |
+
col1, col2 = st.columns(2)
|
| 321 |
+
|
| 322 |
+
with col1:
|
| 323 |
+
selected_village = st.selectbox("Select Village for Dispatch", village_performance['Village'].unique())
|
| 324 |
+
village_data = village_performance[village_performance['Village'] == selected_village].iloc[0]
|
| 325 |
+
|
| 326 |
+
st.write(f"**Mantri:** {village_data['Mantri_Name']}")
|
| 327 |
+
st.write(f"**Last Sale:** {village_data['Days_Since_Last_Sale']} days ago")
|
| 328 |
+
st.write(f"**Conversion Rate:** {village_data['Conversion_Rate']}%")
|
| 329 |
+
|
| 330 |
+
with col2:
|
| 331 |
+
dispatch_date = st.date_input("Dispatch Date", datetime.now() + timedelta(days=1))
|
| 332 |
+
team_size = st.slider("Team Size", 1, 5, 2)
|
| 333 |
+
duration = st.selectbox("Duration", ["1 day", "2 days", "3 days", "1 week"])
|
| 334 |
+
|
| 335 |
+
objectives = st.text_area("Objectives", "Conduct demo sessions, educate farmers about benefits, collect feedback")
|
| 336 |
+
|
| 337 |
+
if st.button("Schedule Dispatch"):
|
| 338 |
+
st.success(f"Dispatch to {selected_village} scheduled for {dispatch_date}")
|
| 339 |
+
st.json({
|
| 340 |
+
"village": selected_village,
|
| 341 |
+
"mantri": village_data['Mantri_Name'],
|
| 342 |
+
"date": str(dispatch_date),
|
| 343 |
+
"team_size": team_size,
|
| 344 |
+
"duration": duration,
|
| 345 |
+
"objectives": objectives
|
| 346 |
+
})
|
| 347 |
+
|
| 348 |
+
# Footer
|
| 349 |
+
st.markdown("---")
|
| 350 |
+
st.markdown("**Calcium Supplement Sales Automation System** | For internal use only")
|
data/AMBERAVPURA ENGLISH SABHASAD LIST.xlsx
ADDED
|
Binary file (14 kB). View file
|
|
|
data/APRIL 24-25.xlsx
ADDED
|
Binary file (31.2 kB). View file
|
|
|
data/AUGUST 24-25.xlsx
ADDED
|
Binary file (33.1 kB). View file
|
|
|
data/JULY 24-25.xlsx
ADDED
|
Binary file (35.3 kB). View file
|
|
|
data/JUNE 24-25.xlsx
ADDED
|
Binary file (30 kB). View file
|
|
|
data/MAY 24-25.xlsx
ADDED
|
Binary file (29.3 kB). View file
|
|
|
data/SEPTEMBER 24-25.xlsx
ADDED
|
Binary file (42.1 kB). View file
|
|
|
data/amiyad.xlsx
ADDED
|
Binary file (41.6 kB). View file
|
|
|
data/dharkhuniya.xlsx
ADDED
|
Binary file (37.2 kB). View file
|
|
|
data/distributors.xlsx
ADDED
|
Binary file (26.3 kB). View file
|
|
|
data/kamrol.xlsx
ADDED
|
Binary file (39 kB). View file
|
|
|
data/sandha.xlsx
ADDED
|
Binary file (37.5 kB). View file
|
|
|
data/vishnoli.xlsx
ADDED
|
Binary file (38.2 kB). View file
|
|
|
pages/__init__.py
ADDED
|
File without changes
|
pages/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (138 Bytes). View file
|
|
|
pages/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (142 Bytes). View file
|
|
|
pages/__pycache__/customers.cpython-310.pyc
ADDED
|
Binary file (13.4 kB). View file
|
|
|
pages/__pycache__/dashboard.cpython-310.pyc
ADDED
|
Binary file (702 Bytes). View file
|
|
|
pages/__pycache__/dashboard.cpython-313.pyc
ADDED
|
Binary file (7.01 kB). View file
|
|
|
pages/__pycache__/data_import.cpython-310.pyc
ADDED
|
Binary file (3.28 kB). View file
|
|
|
pages/__pycache__/demos.cpython-310.pyc
ADDED
|
Binary file (16.8 kB). View file
|
|
|
pages/__pycache__/distributors.cpython-310.pyc
ADDED
|
Binary file (27.6 kB). View file
|
|
|
pages/__pycache__/file_viewer.cpython-310.pyc
ADDED
|
Binary file (11.9 kB). View file
|
|
|
pages/__pycache__/payments.cpython-310.pyc
ADDED
|
Binary file (16.1 kB). View file
|
|
|
pages/__pycache__/reports.cpython-310.pyc
ADDED
|
Binary file (31 kB). View file
|
|
|
pages/__pycache__/sales.cpython-310.pyc
ADDED
|
Binary file (14.1 kB). View file
|
|
|
pages/__pycache__/system_dashboard.cpython-310.pyc
ADDED
|
Binary file (4.07 kB). View file
|
|
|
pages/customers.py
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# pages/customers.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
|
| 7 |
+
def show_customers_page(db, whatsapp_manager=None):
|
| 8 |
+
"""Show customer analytics, segmentation, and action planning page"""
|
| 9 |
+
st.title("👥 Customer Intelligence Center")
|
| 10 |
+
|
| 11 |
+
if not db:
|
| 12 |
+
st.error("Database not available. Please check initialization.")
|
| 13 |
+
return
|
| 14 |
+
|
| 15 |
+
# Tabs for different customer functions
|
| 16 |
+
tab1, tab2, tab3, tab4 = st.tabs(["📊 Dashboard", "🎯 Segmentation", "📞 Action Center", "🔍 Customer Directory"])
|
| 17 |
+
|
| 18 |
+
with tab1:
|
| 19 |
+
show_customer_dashboard_tab(db)
|
| 20 |
+
|
| 21 |
+
with tab2:
|
| 22 |
+
show_customer_segmentation_tab(db)
|
| 23 |
+
|
| 24 |
+
with tab3:
|
| 25 |
+
show_action_center_tab(db, whatsapp_manager)
|
| 26 |
+
|
| 27 |
+
with tab4:
|
| 28 |
+
show_customer_directory_tab(db)
|
| 29 |
+
|
| 30 |
+
def show_customer_dashboard_tab(db):
|
| 31 |
+
"""Show customer analytics dashboard"""
|
| 32 |
+
st.subheader("📊 Customer Analytics Dashboard")
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
# Get comprehensive customer data
|
| 36 |
+
customers_data = get_customer_analytics_data(db)
|
| 37 |
+
|
| 38 |
+
if customers_data.empty:
|
| 39 |
+
st.info("No customer data available yet.")
|
| 40 |
+
return
|
| 41 |
+
|
| 42 |
+
# Key Metrics
|
| 43 |
+
st.subheader("🎯 Key Metrics")
|
| 44 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 45 |
+
|
| 46 |
+
with col1:
|
| 47 |
+
total_customers = len(customers_data)
|
| 48 |
+
st.metric("Total Customers", total_customers)
|
| 49 |
+
|
| 50 |
+
with col2:
|
| 51 |
+
active_customers = len(customers_data[customers_data['total_purchases'] > 0])
|
| 52 |
+
st.metric("Active Customers", active_customers)
|
| 53 |
+
|
| 54 |
+
with col3:
|
| 55 |
+
avg_purchase_value = customers_data[customers_data['total_spent'] > 0]['total_spent'].mean()
|
| 56 |
+
st.metric("Avg Purchase Value", f"₹{avg_purchase_value:,.0f}" if not pd.isna(avg_purchase_value) else "₹0")
|
| 57 |
+
|
| 58 |
+
with col4:
|
| 59 |
+
repeat_customers = len(customers_data[customers_data['total_purchases'] > 1])
|
| 60 |
+
st.metric("Repeat Customers", repeat_customers)
|
| 61 |
+
|
| 62 |
+
# Village-wise Analysis
|
| 63 |
+
st.subheader("🗺️ Geographic Distribution")
|
| 64 |
+
col1, col2 = st.columns(2)
|
| 65 |
+
|
| 66 |
+
with col1:
|
| 67 |
+
village_stats = customers_data.groupby('village').agg({
|
| 68 |
+
'customer_id': 'count',
|
| 69 |
+
'total_spent': 'sum',
|
| 70 |
+
'total_purchases': 'sum'
|
| 71 |
+
}).reset_index()
|
| 72 |
+
village_stats.columns = ['Village', 'Customers', 'Total Revenue', 'Total Purchases']
|
| 73 |
+
village_stats = village_stats.sort_values('Customers', ascending=False)
|
| 74 |
+
|
| 75 |
+
if not village_stats.empty:
|
| 76 |
+
fig = px.bar(village_stats.head(10), x='Village', y='Customers',
|
| 77 |
+
title='Top 10 Villages by Customer Count',
|
| 78 |
+
color='Customers')
|
| 79 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 80 |
+
|
| 81 |
+
with col2:
|
| 82 |
+
if not village_stats.empty:
|
| 83 |
+
fig = px.pie(village_stats.head(8), values='Customers', names='Village',
|
| 84 |
+
title='Customer Distribution by Village')
|
| 85 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 86 |
+
|
| 87 |
+
# Purchase Behavior
|
| 88 |
+
st.subheader("💰 Purchase Behavior Analysis")
|
| 89 |
+
col1, col2 = st.columns(2)
|
| 90 |
+
|
| 91 |
+
with col1:
|
| 92 |
+
# Customer lifetime value distribution
|
| 93 |
+
spending_brackets = customers_data[customers_data['total_spent'] > 0]['total_spent']
|
| 94 |
+
if not spending_brackets.empty:
|
| 95 |
+
fig = px.histogram(spending_brackets, nbins=10,
|
| 96 |
+
title='Customer Spending Distribution',
|
| 97 |
+
labels={'value': 'Total Spent (₹)', 'count': 'Number of Customers'})
|
| 98 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 99 |
+
|
| 100 |
+
with col2:
|
| 101 |
+
# Recency analysis
|
| 102 |
+
if 'last_purchase_date' in customers_data.columns:
|
| 103 |
+
recent_customers = customers_data[customers_data['last_purchase_date'].notna()]
|
| 104 |
+
if not recent_customers.empty:
|
| 105 |
+
recent_customers['days_since_purchase'] = (datetime.now() - pd.to_datetime(recent_customers['last_purchase_date'])).dt.days
|
| 106 |
+
fig = px.histogram(recent_customers, x='days_since_purchase', nbins=10,
|
| 107 |
+
title='Days Since Last Purchase',
|
| 108 |
+
labels={'days_since_purchase': 'Days', 'count': 'Customers'})
|
| 109 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 110 |
+
|
| 111 |
+
except Exception as e:
|
| 112 |
+
st.error(f"Error loading customer analytics: {e}")
|
| 113 |
+
|
| 114 |
+
def show_customer_segmentation_tab(db):
|
| 115 |
+
"""Show customer segmentation and targeting"""
|
| 116 |
+
st.subheader("🎯 Customer Segmentation")
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
customers_data = get_customer_analytics_data(db)
|
| 120 |
+
|
| 121 |
+
if customers_data.empty:
|
| 122 |
+
st.info("No customer data available for segmentation.")
|
| 123 |
+
return
|
| 124 |
+
|
| 125 |
+
# Segmentation criteria
|
| 126 |
+
st.subheader("🔍 Define Segments")
|
| 127 |
+
|
| 128 |
+
col1, col2 = st.columns(2)
|
| 129 |
+
|
| 130 |
+
with col1:
|
| 131 |
+
segment_by = st.selectbox("Segment By",
|
| 132 |
+
["Purchase Behavior", "Geographic", "Demographic", "Custom"])
|
| 133 |
+
|
| 134 |
+
if segment_by == "Purchase Behavior":
|
| 135 |
+
min_purchases = st.slider("Minimum Purchases", 0, 20, 1)
|
| 136 |
+
min_spent = st.number_input("Minimum Amount Spent (₹)", 0, 100000, 1000)
|
| 137 |
+
|
| 138 |
+
segment_customers = customers_data[
|
| 139 |
+
(customers_data['total_purchases'] >= min_purchases) &
|
| 140 |
+
(customers_data['total_spent'] >= min_spent)
|
| 141 |
+
]
|
| 142 |
+
|
| 143 |
+
elif segment_by == "Geographic":
|
| 144 |
+
selected_villages = st.multiselect("Select Villages",
|
| 145 |
+
customers_data['village'].unique())
|
| 146 |
+
if selected_villages:
|
| 147 |
+
segment_customers = customers_data[customers_data['village'].isin(selected_villages)]
|
| 148 |
+
else:
|
| 149 |
+
segment_customers = customers_data
|
| 150 |
+
|
| 151 |
+
elif segment_by == "Demographic":
|
| 152 |
+
# Add demographic filters here
|
| 153 |
+
segment_customers = customers_data
|
| 154 |
+
|
| 155 |
+
with col2:
|
| 156 |
+
st.write("**Segment Actions**")
|
| 157 |
+
segment_size = len(segment_customers)
|
| 158 |
+
st.metric("Segment Size", segment_size)
|
| 159 |
+
|
| 160 |
+
if segment_size > 0:
|
| 161 |
+
avg_segment_value = segment_customers['total_spent'].mean()
|
| 162 |
+
st.metric("Avg Customer Value", f"₹{avg_segment_value:,.0f}")
|
| 163 |
+
|
| 164 |
+
# Quick actions for segment
|
| 165 |
+
if st.button("📱 Send Bulk WhatsApp", key="segment_whatsapp"):
|
| 166 |
+
st.info(f"Ready to send message to {segment_size} customers")
|
| 167 |
+
|
| 168 |
+
if st.button("📍 Plan Field Visit", key="segment_visit"):
|
| 169 |
+
villages = segment_customers['village'].value_counts().head(5)
|
| 170 |
+
st.success(f"Top villages to visit: {', '.join(villages.index.tolist())}")
|
| 171 |
+
|
| 172 |
+
# Show segment details
|
| 173 |
+
if not segment_customers.empty:
|
| 174 |
+
st.subheader("👥 Segment Details")
|
| 175 |
+
|
| 176 |
+
# Segment characteristics
|
| 177 |
+
col1, col2, col3 = st.columns(3)
|
| 178 |
+
|
| 179 |
+
with col1:
|
| 180 |
+
top_village = segment_customers['village'].mode()[0] if not segment_customers['village'].mode().empty else "N/A"
|
| 181 |
+
st.metric("Most Common Village", top_village)
|
| 182 |
+
|
| 183 |
+
with col2:
|
| 184 |
+
avg_purchases = segment_customers['total_purchases'].mean()
|
| 185 |
+
st.metric("Avg Purchases/Customer", f"{avg_purchases:.1f}")
|
| 186 |
+
|
| 187 |
+
with col3:
|
| 188 |
+
total_potential = segment_customers['total_spent'].sum()
|
| 189 |
+
st.metric("Segment Total Value", f"₹{total_potential:,.0f}")
|
| 190 |
+
|
| 191 |
+
# Customer list in segment
|
| 192 |
+
st.dataframe(segment_customers[['name', 'village', 'mobile', 'total_purchases', 'total_spent']].head(20),
|
| 193 |
+
use_container_width=True)
|
| 194 |
+
|
| 195 |
+
except Exception as e:
|
| 196 |
+
st.error(f"Error in customer segmentation: {e}")
|
| 197 |
+
|
| 198 |
+
def show_action_center_tab(db, whatsapp_manager):
|
| 199 |
+
"""Show action planning and communication center"""
|
| 200 |
+
st.subheader("📞 Customer Action Center")
|
| 201 |
+
|
| 202 |
+
try:
|
| 203 |
+
customers_data = get_customer_analytics_data(db)
|
| 204 |
+
|
| 205 |
+
if customers_data.empty:
|
| 206 |
+
st.info("No customer data available for actions.")
|
| 207 |
+
return
|
| 208 |
+
|
| 209 |
+
# Action categories
|
| 210 |
+
action_type = st.selectbox("Action Type",
|
| 211 |
+
["Follow-up Calls", "Demo Follow-ups", "Payment Reminders",
|
| 212 |
+
"New Product Announcements", "Customer Feedback"])
|
| 213 |
+
|
| 214 |
+
if action_type == "Follow-up Calls":
|
| 215 |
+
show_followup_calls_section(db, customers_data)
|
| 216 |
+
|
| 217 |
+
elif action_type == "Demo Follow-ups":
|
| 218 |
+
show_demo_followups_section(db, customers_data)
|
| 219 |
+
|
| 220 |
+
elif action_type == "Payment Reminders":
|
| 221 |
+
show_payment_reminders_section(db, customers_data, whatsapp_manager)
|
| 222 |
+
|
| 223 |
+
elif action_type == "New Product Announcements":
|
| 224 |
+
show_product_announcements_section(db, customers_data, whatsapp_manager)
|
| 225 |
+
|
| 226 |
+
elif action_type == "Customer Feedback":
|
| 227 |
+
show_feedback_section(db, customers_data, whatsapp_manager)
|
| 228 |
+
|
| 229 |
+
except Exception as e:
|
| 230 |
+
st.error(f"Error in action center: {e}")
|
| 231 |
+
|
| 232 |
+
def show_followup_calls_section(db, customers_data):
|
| 233 |
+
"""Show customers needing follow-up calls"""
|
| 234 |
+
st.write("### 📞 Customers Needing Follow-up")
|
| 235 |
+
|
| 236 |
+
# Identify customers for follow-up
|
| 237 |
+
follow_up_criteria = st.multiselect("Follow-up Criteria",
|
| 238 |
+
["No Purchase in 30 days", "High Value Customers",
|
| 239 |
+
"Single Purchase Only", "Specific Villages"])
|
| 240 |
+
|
| 241 |
+
target_customers = customers_data.copy()
|
| 242 |
+
|
| 243 |
+
if "No Purchase in 30 days" in follow_up_criteria:
|
| 244 |
+
# This would require last_purchase_date in your data
|
| 245 |
+
st.info("Last purchase date tracking needed for this feature")
|
| 246 |
+
|
| 247 |
+
if "High Value Customers" in follow_up_criteria:
|
| 248 |
+
high_value_threshold = st.number_input("High Value Threshold (₹)", 1000, 10000, 5000)
|
| 249 |
+
target_customers = target_customers[target_customers['total_spent'] >= high_value_threshold]
|
| 250 |
+
|
| 251 |
+
if "Single Purchase Only" in follow_up_criteria:
|
| 252 |
+
target_customers = target_customers[target_customers['total_purchases'] == 1]
|
| 253 |
+
|
| 254 |
+
if not target_customers.empty:
|
| 255 |
+
st.write(f"**{len(target_customers)} customers identified for follow-up**")
|
| 256 |
+
|
| 257 |
+
# Village concentration
|
| 258 |
+
village_concentration = target_customers['village'].value_counts().head(5)
|
| 259 |
+
st.write("**Top villages for field visits:**")
|
| 260 |
+
for village, count in village_concentration.items():
|
| 261 |
+
st.write(f"- {village}: {count} customers")
|
| 262 |
+
|
| 263 |
+
# Display customer list
|
| 264 |
+
st.dataframe(target_customers[['name', 'village', 'mobile', 'total_purchases', 'total_spent']],
|
| 265 |
+
use_container_width=True)
|
| 266 |
+
|
| 267 |
+
def show_demo_followups_section(db, customers_data):
|
| 268 |
+
"""Show demo conversion tracking"""
|
| 269 |
+
st.write("### 🎯 Demo Conversion Tracking")
|
| 270 |
+
|
| 271 |
+
try:
|
| 272 |
+
# Get demo data
|
| 273 |
+
demo_data = db.get_dataframe('demos', '''
|
| 274 |
+
SELECT d.*, c.name as customer_name, c.village, c.mobile, p.product_name
|
| 275 |
+
FROM demos d
|
| 276 |
+
LEFT JOIN customers c ON d.customer_id = c.customer_id
|
| 277 |
+
LEFT JOIN products p ON d.product_id = p.product_id
|
| 278 |
+
ORDER BY d.demo_date DESC
|
| 279 |
+
''')
|
| 280 |
+
|
| 281 |
+
if not demo_data.empty:
|
| 282 |
+
# Demo conversion stats
|
| 283 |
+
total_demos = len(demo_data)
|
| 284 |
+
converted_demos = len(demo_data[demo_data['conversion_status'] == 'Converted'])
|
| 285 |
+
conversion_rate = (converted_demos / total_demos) * 100 if total_demos > 0 else 0
|
| 286 |
+
|
| 287 |
+
col1, col2, col3 = st.columns(3)
|
| 288 |
+
with col1:
|
| 289 |
+
st.metric("Total Demos", total_demos)
|
| 290 |
+
with col2:
|
| 291 |
+
st.metric("Converted", converted_demos)
|
| 292 |
+
with col3:
|
| 293 |
+
st.metric("Conversion Rate", f"{conversion_rate:.1f}%")
|
| 294 |
+
|
| 295 |
+
# Pending follow-ups
|
| 296 |
+
pending_followups = demo_data[
|
| 297 |
+
(demo_data['conversion_status'] == 'Not Converted') &
|
| 298 |
+
(pd.to_datetime(demo_data['follow_up_date']) <= datetime.now())
|
| 299 |
+
]
|
| 300 |
+
|
| 301 |
+
if not pending_followups.empty:
|
| 302 |
+
st.warning(f"🚨 {len(pending_followups)} demos need immediate follow-up!")
|
| 303 |
+
st.dataframe(pending_followups[['customer_name', 'village', 'product_name', 'demo_date', 'follow_up_date']],
|
| 304 |
+
use_container_width=True)
|
| 305 |
+
|
| 306 |
+
# All demo records
|
| 307 |
+
st.write("**All Demo Records**")
|
| 308 |
+
st.dataframe(demo_data[['customer_name', 'village', 'product_name', 'demo_date', 'conversion_status']],
|
| 309 |
+
use_container_width=True)
|
| 310 |
+
else:
|
| 311 |
+
st.info("No demo records found.")
|
| 312 |
+
|
| 313 |
+
except Exception as e:
|
| 314 |
+
st.error(f"Error loading demo data: {e}")
|
| 315 |
+
|
| 316 |
+
def show_payment_reminders_section(db, customers_data, whatsapp_manager):
|
| 317 |
+
"""Show payment reminder system"""
|
| 318 |
+
st.write("### 💰 Payment Reminders")
|
| 319 |
+
|
| 320 |
+
try:
|
| 321 |
+
# Get pending payments
|
| 322 |
+
pending_payments = db.get_pending_payments()
|
| 323 |
+
|
| 324 |
+
if not pending_payments.empty:
|
| 325 |
+
total_pending = pending_payments['pending_amount'].sum()
|
| 326 |
+
st.metric("Total Pending Amount", f"₹{total_pending:,.2f}")
|
| 327 |
+
|
| 328 |
+
# Group by customer
|
| 329 |
+
customer_pending = pending_payments.groupby('customer_name').agg({
|
| 330 |
+
'pending_amount': 'sum',
|
| 331 |
+
'invoice_no': 'count'
|
| 332 |
+
}).reset_index()
|
| 333 |
+
customer_pending.columns = ['Customer', 'Total Pending', 'Pending Invoices']
|
| 334 |
+
customer_pending = customer_pending.sort_values('Total Pending', ascending=False)
|
| 335 |
+
|
| 336 |
+
st.dataframe(customer_pending, use_container_width=True)
|
| 337 |
+
|
| 338 |
+
# Bulk WhatsApp reminders
|
| 339 |
+
st.write("**Bulk Payment Reminders**")
|
| 340 |
+
if st.button("📱 Send Payment Reminders to All", type="primary") and whatsapp_manager:
|
| 341 |
+
st.info("This would send payment reminders to all customers with pending payments")
|
| 342 |
+
else:
|
| 343 |
+
st.success("🎉 All payments are cleared! No pending payments.")
|
| 344 |
+
|
| 345 |
+
except Exception as e:
|
| 346 |
+
st.error(f"Error loading payment data: {e}")
|
| 347 |
+
|
| 348 |
+
def show_product_announcements_section(db, customers_data, whatsapp_manager):
|
| 349 |
+
"""Show new product announcement system"""
|
| 350 |
+
st.write("### 🆕 New Product Announcements")
|
| 351 |
+
|
| 352 |
+
# Target segments for new products
|
| 353 |
+
segment = st.selectbox("Target Segment",
|
| 354 |
+
["All Customers", "High Value Customers", "Specific Village", "Previous Product Buyers"])
|
| 355 |
+
|
| 356 |
+
message_template = st.text_area("Announcement Message",
|
| 357 |
+
height=100,
|
| 358 |
+
value="Hello {name}! We have exciting new products available. Reply YES for details!")
|
| 359 |
+
|
| 360 |
+
if st.button("📢 Send Announcement", type="primary") and whatsapp_manager:
|
| 361 |
+
st.success("Ready to send announcement to selected segment!")
|
| 362 |
+
|
| 363 |
+
def show_feedback_section(db, customers_data, whatsapp_manager):
|
| 364 |
+
"""Show customer feedback collection system"""
|
| 365 |
+
st.write("### 💬 Customer Feedback Collection")
|
| 366 |
+
|
| 367 |
+
feedback_segment = st.selectbox("Request Feedback From",
|
| 368 |
+
["Recent Customers", "High Value Customers", "Inactive Customers"])
|
| 369 |
+
|
| 370 |
+
feedback_message = st.text_area("Feedback Request Message",
|
| 371 |
+
height=100,
|
| 372 |
+
value="Hello {name}! We value your feedback. How was your experience with us?")
|
| 373 |
+
|
| 374 |
+
if st.button("📝 Request Feedback", type="primary") and whatsapp_manager:
|
| 375 |
+
st.info("Feedback requests ready to send!")
|
| 376 |
+
|
| 377 |
+
def show_customer_directory_tab(db):
|
| 378 |
+
"""Show comprehensive customer directory"""
|
| 379 |
+
st.subheader("🔍 Customer Directory")
|
| 380 |
+
|
| 381 |
+
try:
|
| 382 |
+
customers_data = get_customer_analytics_data(db)
|
| 383 |
+
|
| 384 |
+
if customers_data.empty:
|
| 385 |
+
st.info("No customers found in the database.")
|
| 386 |
+
return
|
| 387 |
+
|
| 388 |
+
# Filters
|
| 389 |
+
col1, col2, col3 = st.columns(3)
|
| 390 |
+
|
| 391 |
+
with col1:
|
| 392 |
+
village_filter = st.multiselect("Filter by Village", customers_data['village'].unique())
|
| 393 |
+
|
| 394 |
+
with col2:
|
| 395 |
+
purchase_filter = st.selectbox("Filter by Purchase History",
|
| 396 |
+
["All", "Has Purchases", "No Purchases", "Multiple Purchases"])
|
| 397 |
+
|
| 398 |
+
with col3:
|
| 399 |
+
search_term = st.text_input("Search by Name/Mobile")
|
| 400 |
+
|
| 401 |
+
# Apply filters
|
| 402 |
+
filtered_customers = customers_data.copy()
|
| 403 |
+
|
| 404 |
+
if village_filter:
|
| 405 |
+
filtered_customers = filtered_customers[filtered_customers['village'].isin(village_filter)]
|
| 406 |
+
|
| 407 |
+
if purchase_filter == "Has Purchases":
|
| 408 |
+
filtered_customers = filtered_customers[filtered_customers['total_purchases'] > 0]
|
| 409 |
+
elif purchase_filter == "No Purchases":
|
| 410 |
+
filtered_customers = filtered_customers[filtered_customers['total_purchases'] == 0]
|
| 411 |
+
elif purchase_filter == "Multiple Purchases":
|
| 412 |
+
filtered_customers = filtered_customers[filtered_customers['total_purchases'] > 1]
|
| 413 |
+
|
| 414 |
+
if search_term:
|
| 415 |
+
filtered_customers = filtered_customers[
|
| 416 |
+
filtered_customers['name'].str.contains(search_term, case=False, na=False) |
|
| 417 |
+
filtered_customers['mobile'].str.contains(search_term, na=False)
|
| 418 |
+
]
|
| 419 |
+
|
| 420 |
+
# Display results
|
| 421 |
+
st.write(f"**Found {len(filtered_customers)} customers**")
|
| 422 |
+
|
| 423 |
+
display_columns = ['name', 'village', 'mobile', 'total_purchases', 'total_spent']
|
| 424 |
+
display_df = filtered_customers[display_columns]
|
| 425 |
+
display_df.columns = ['Name', 'Village', 'Mobile', 'Total Purchases', 'Total Spent (₹)']
|
| 426 |
+
display_df['Total Spent (₹)'] = display_df['Total Spent (₹)'].apply(lambda x: f"₹{x:,.0f}")
|
| 427 |
+
|
| 428 |
+
st.dataframe(display_df, use_container_width=True)
|
| 429 |
+
|
| 430 |
+
except Exception as e:
|
| 431 |
+
st.error(f"Error loading customer directory: {e}")
|
| 432 |
+
|
| 433 |
+
def get_customer_analytics_data(db):
|
| 434 |
+
"""Get comprehensive customer data with analytics"""
|
| 435 |
+
try:
|
| 436 |
+
customers = db.get_dataframe('customers', '''
|
| 437 |
+
SELECT c.*,
|
| 438 |
+
COUNT(s.sale_id) as total_purchases,
|
| 439 |
+
COALESCE(SUM(s.total_amount), 0) as total_spent,
|
| 440 |
+
MAX(s.sale_date) as last_purchase_date
|
| 441 |
+
FROM customers c
|
| 442 |
+
LEFT JOIN sales s ON c.customer_id = s.customer_id
|
| 443 |
+
GROUP BY c.customer_id
|
| 444 |
+
ORDER BY total_spent DESC
|
| 445 |
+
''')
|
| 446 |
+
return customers
|
| 447 |
+
except Exception as e:
|
| 448 |
+
st.error(f"Error loading customer analytics data: {e}")
|
| 449 |
+
return pd.DataFrame()
|
pages/dashboard.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import streamlit.components.v1 as components
|
| 3 |
+
|
| 4 |
+
def create_dashboard(db=None, analytics=None):
|
| 5 |
+
# DO NOT call st.set_page_config here
|
| 6 |
+
st.markdown("<h1 class='main-header'>📊 Power BI Dashboard</h1>", unsafe_allow_html=True)
|
| 7 |
+
components.iframe(
|
| 8 |
+
src="https://app.powerbi.com/view?r=eyJrIjoiM2VmZDQxNTUtMGEyYS00NDNiLWEyMDMtZWY5MGFkYTlmYjU2IiwidCI6ImFmYTM1MTRhLTFlNDItNDBjOS04ZjExLWIzODNlNmRhYTM3NiIsImMiOjN9",
|
| 9 |
+
width=1200,
|
| 10 |
+
height=800,
|
| 11 |
+
scrolling=True
|
| 12 |
+
)
|
pages/data_import.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# pages/data_import.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import os
|
| 4 |
+
import glob
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
def show_data_import_page(db, data_processor):
|
| 8 |
+
"""Show data import page"""
|
| 9 |
+
st.title("📤 Data Import & Processing")
|
| 10 |
+
|
| 11 |
+
if not data_processor:
|
| 12 |
+
st.error("Data processor not available. Please check initialization.")
|
| 13 |
+
return
|
| 14 |
+
|
| 15 |
+
data_dir = "data"
|
| 16 |
+
if os.path.exists(data_dir):
|
| 17 |
+
excel_files = glob.glob(os.path.join(data_dir, "*.xlsx")) + glob.glob(os.path.join(data_dir, "*.xls"))
|
| 18 |
+
|
| 19 |
+
if excel_files:
|
| 20 |
+
st.subheader("📁 Existing Files in Data Folder")
|
| 21 |
+
|
| 22 |
+
for file_path in excel_files:
|
| 23 |
+
file_name = os.path.basename(file_path)
|
| 24 |
+
file_size = os.path.getsize(file_path) / 1024
|
| 25 |
+
file_mtime = datetime.fromtimestamp(os.path.getmtime(file_path))
|
| 26 |
+
|
| 27 |
+
col1, col2, col3 = st.columns([3, 2, 1])
|
| 28 |
+
with col1:
|
| 29 |
+
st.write(f"**{file_name}**")
|
| 30 |
+
st.write(f"Size: {file_size:.1f} KB | Modified: {file_mtime.strftime('%Y-%m-%d %H:%M')}")
|
| 31 |
+
with col2:
|
| 32 |
+
if st.button(f"🔄 Process", key=f"process_{file_name}"):
|
| 33 |
+
try:
|
| 34 |
+
if data_processor.process_excel_file(file_path):
|
| 35 |
+
st.success(f"✅ Processed: {file_name}")
|
| 36 |
+
st.rerun()
|
| 37 |
+
else:
|
| 38 |
+
st.warning(f"⚠️ No data processed from: {file_name}")
|
| 39 |
+
except Exception as e:
|
| 40 |
+
st.error(f"❌ Error processing {file_name}: {str(e)}")
|
| 41 |
+
with col3:
|
| 42 |
+
if st.button(f"🗑️ Delete", key=f"delete_{file_name}"):
|
| 43 |
+
try:
|
| 44 |
+
os.remove(file_path)
|
| 45 |
+
st.success(f"✅ Deleted: {file_name}")
|
| 46 |
+
st.rerun()
|
| 47 |
+
except Exception as e:
|
| 48 |
+
st.error(f"Error deleting file: {e}")
|
| 49 |
+
|
| 50 |
+
if st.button("🔄 Process All Files", type="primary"):
|
| 51 |
+
success_count = 0
|
| 52 |
+
for file_path in excel_files:
|
| 53 |
+
try:
|
| 54 |
+
if data_processor.process_excel_file(file_path):
|
| 55 |
+
success_count += 1
|
| 56 |
+
except Exception as e:
|
| 57 |
+
st.error(f"Error processing {os.path.basename(file_path)}: {str(e)}")
|
| 58 |
+
st.success(f"✅ Processed {success_count}/{len(excel_files)} files successfully!")
|
| 59 |
+
if success_count > 0:
|
| 60 |
+
st.rerun()
|
| 61 |
+
else:
|
| 62 |
+
st.info("No Excel files found in the data folder.")
|
| 63 |
+
else:
|
| 64 |
+
os.makedirs(data_dir, exist_ok=True)
|
| 65 |
+
st.info("Data folder created. Upload Excel files to get started.")
|
| 66 |
+
|
| 67 |
+
st.subheader("📤 Upload New Excel File")
|
| 68 |
+
uploaded_file = st.file_uploader("Choose Excel file", type=['xlsx', 'xls'])
|
| 69 |
+
|
| 70 |
+
if uploaded_file:
|
| 71 |
+
file_path = os.path.join("data", uploaded_file.name)
|
| 72 |
+
with open(file_path, "wb") as f:
|
| 73 |
+
f.write(uploaded_file.getbuffer())
|
| 74 |
+
|
| 75 |
+
st.success(f"✅ File saved: {uploaded_file.name}")
|
| 76 |
+
|
| 77 |
+
if st.button(f"🔄 Process {uploaded_file.name}"):
|
| 78 |
+
try:
|
| 79 |
+
if data_processor.process_excel_file(file_path):
|
| 80 |
+
st.success(f"✅ Processed: {uploaded_file.name}")
|
| 81 |
+
|
| 82 |
+
# Show data preview
|
| 83 |
+
st.subheader("📊 Imported Data Preview")
|
| 84 |
+
try:
|
| 85 |
+
customers = db.get_dataframe('customers')
|
| 86 |
+
sales = db.get_dataframe('sales')
|
| 87 |
+
|
| 88 |
+
col1, col2 = st.columns(2)
|
| 89 |
+
with col1:
|
| 90 |
+
st.metric("Customers", len(customers))
|
| 91 |
+
if not customers.empty:
|
| 92 |
+
st.dataframe(customers.tail(3), use_container_width=True)
|
| 93 |
+
with col2:
|
| 94 |
+
st.metric("Sales", len(sales))
|
| 95 |
+
if not sales.empty:
|
| 96 |
+
st.dataframe(sales.tail(3), use_container_width=True)
|
| 97 |
+
except Exception as e:
|
| 98 |
+
st.error(f"Error loading preview data: {e}")
|
| 99 |
+
|
| 100 |
+
st.rerun()
|
| 101 |
+
else:
|
| 102 |
+
st.warning(f"⚠️ No data processed from: {uploaded_file.name}")
|
| 103 |
+
except Exception as e:
|
| 104 |
+
st.error(f"❌ Error processing {uploaded_file.name}: {str(e)}")
|
pages/demos.py
ADDED
|
@@ -0,0 +1,824 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# pages/demos.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
import time
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def show_demos_page(db, whatsapp_manager=None):
|
| 10 |
+
"""Show demo management and tracking page"""
|
| 11 |
+
st.title("🎯 Demo Management Center")
|
| 12 |
+
|
| 13 |
+
if not db:
|
| 14 |
+
st.error("Database not available. Please check initialization.")
|
| 15 |
+
return
|
| 16 |
+
|
| 17 |
+
# Tabs for different demo functions
|
| 18 |
+
tab1, tab2, tab3, tab4 = st.tabs(
|
| 19 |
+
["➕ Schedule Demo", "📋 Demo Calendar", "📊 Demo Analytics", "🔄 Follow-ups"]
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
with tab1:
|
| 23 |
+
show_schedule_demo_tab(db, whatsapp_manager)
|
| 24 |
+
|
| 25 |
+
with tab2:
|
| 26 |
+
show_demo_calendar_tab(db)
|
| 27 |
+
|
| 28 |
+
with tab3:
|
| 29 |
+
show_demo_analytics_tab(db)
|
| 30 |
+
|
| 31 |
+
with tab4:
|
| 32 |
+
show_follow_ups_tab(db, whatsapp_manager)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def show_schedule_demo_tab(db, whatsapp_manager):
|
| 36 |
+
"""Show form to schedule new demos"""
|
| 37 |
+
st.subheader("➕ Schedule New Demo")
|
| 38 |
+
|
| 39 |
+
# Show demo summary if we just created one
|
| 40 |
+
if "last_demo_id" in st.session_state and st.session_state.last_demo_id:
|
| 41 |
+
show_demo_summary(db, st.session_state.last_demo_id)
|
| 42 |
+
st.session_state.last_demo_id = None # Clear after showing
|
| 43 |
+
st.divider()
|
| 44 |
+
|
| 45 |
+
with st.form("schedule_demo_form"):
|
| 46 |
+
st.markdown("### 👥 Demo Information")
|
| 47 |
+
|
| 48 |
+
col1, col2 = st.columns(2)
|
| 49 |
+
|
| 50 |
+
with col1:
|
| 51 |
+
# Customer selection
|
| 52 |
+
customers = db.get_dataframe(
|
| 53 |
+
"customers", "SELECT customer_id, name, village FROM customers"
|
| 54 |
+
)
|
| 55 |
+
if not customers.empty:
|
| 56 |
+
customer_options = {
|
| 57 |
+
f"{row['name']} ({row['village']})": row["customer_id"]
|
| 58 |
+
for _, row in customers.iterrows()
|
| 59 |
+
}
|
| 60 |
+
selected_customer = st.selectbox(
|
| 61 |
+
"Select Customer*", options=list(customer_options.keys())
|
| 62 |
+
)
|
| 63 |
+
customer_id = (
|
| 64 |
+
customer_options[selected_customer] if selected_customer else None
|
| 65 |
+
)
|
| 66 |
+
else:
|
| 67 |
+
st.warning("No customers found. Please add customers first.")
|
| 68 |
+
customer_id = None
|
| 69 |
+
|
| 70 |
+
# Distributor selection
|
| 71 |
+
distributors = db.get_dataframe(
|
| 72 |
+
"distributors", "SELECT distributor_id, name, village FROM distributors"
|
| 73 |
+
)
|
| 74 |
+
if not distributors.empty:
|
| 75 |
+
distributor_options = {
|
| 76 |
+
f"{row['name']} ({row['village']})": row["distributor_id"]
|
| 77 |
+
for _, row in distributors.iterrows()
|
| 78 |
+
}
|
| 79 |
+
selected_distributor = st.selectbox(
|
| 80 |
+
"Assign Distributor",
|
| 81 |
+
options=[""] + list(distributor_options.keys()),
|
| 82 |
+
)
|
| 83 |
+
distributor_id = (
|
| 84 |
+
distributor_options[selected_distributor]
|
| 85 |
+
if selected_distributor
|
| 86 |
+
else None
|
| 87 |
+
)
|
| 88 |
+
else:
|
| 89 |
+
distributor_id = None
|
| 90 |
+
|
| 91 |
+
with col2:
|
| 92 |
+
# Product selection
|
| 93 |
+
products = db.get_dataframe(
|
| 94 |
+
"products",
|
| 95 |
+
"SELECT product_id, product_name FROM products WHERE is_active = 1",
|
| 96 |
+
)
|
| 97 |
+
if not products.empty:
|
| 98 |
+
product_options = {
|
| 99 |
+
row["product_name"]: row["product_id"]
|
| 100 |
+
for _, row in products.iterrows()
|
| 101 |
+
}
|
| 102 |
+
selected_product = st.selectbox(
|
| 103 |
+
"Product to Demo*", options=list(product_options.keys())
|
| 104 |
+
)
|
| 105 |
+
product_id = (
|
| 106 |
+
product_options[selected_product] if selected_product else None
|
| 107 |
+
)
|
| 108 |
+
else:
|
| 109 |
+
st.warning("No products found. Please add products first.")
|
| 110 |
+
product_id = None
|
| 111 |
+
|
| 112 |
+
# Demo details
|
| 113 |
+
demo_date = st.date_input("Demo Date*", datetime.now().date())
|
| 114 |
+
demo_time = st.time_input("Demo Time", datetime.now().time())
|
| 115 |
+
|
| 116 |
+
st.markdown("### 📝 Demo Details")
|
| 117 |
+
|
| 118 |
+
col1, col2 = st.columns(2)
|
| 119 |
+
|
| 120 |
+
with col1:
|
| 121 |
+
quantity_provided = st.number_input(
|
| 122 |
+
"Quantity Provided",
|
| 123 |
+
min_value=0,
|
| 124 |
+
value=1,
|
| 125 |
+
help="Number of units provided for demo",
|
| 126 |
+
)
|
| 127 |
+
demo_location = st.selectbox(
|
| 128 |
+
"Demo Location",
|
| 129 |
+
["Customer Home", "Distributor Office", "Public Place", "Other"],
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
with col2:
|
| 133 |
+
follow_up_date = st.date_input(
|
| 134 |
+
"Follow-up Date", datetime.now().date() + timedelta(days=7)
|
| 135 |
+
)
|
| 136 |
+
conversion_status = st.selectbox(
|
| 137 |
+
"Initial Status", ["Scheduled", "Completed", "Cancelled"]
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
notes = st.text_area(
|
| 141 |
+
"Demo Notes",
|
| 142 |
+
placeholder="Any special instructions, customer requirements, or observations...",
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
# Submit button
|
| 146 |
+
submitted = st.form_submit_button(
|
| 147 |
+
"🎯 Schedule Demo",
|
| 148 |
+
type="primary",
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
# Handle form submission OUTSIDE the form to prevent resubmission
|
| 152 |
+
if submitted:
|
| 153 |
+
# Create unique submission ID to prevent duplicates
|
| 154 |
+
submission_id = f"{customer_id}_{product_id}_{demo_date}_{demo_time}_{int(time.time())}"
|
| 155 |
+
|
| 156 |
+
# Initialize submission tracking if not exists
|
| 157 |
+
if "processed_submissions" not in st.session_state:
|
| 158 |
+
st.session_state.processed_submissions = set()
|
| 159 |
+
|
| 160 |
+
# Check if this exact submission was already processed in this session
|
| 161 |
+
if submission_id in st.session_state.processed_submissions:
|
| 162 |
+
st.warning("⚠️ This demo was already submitted. Showing previously created demo.")
|
| 163 |
+
# Don't rerun, just show the existing demo
|
| 164 |
+
else:
|
| 165 |
+
# Validation
|
| 166 |
+
errors = []
|
| 167 |
+
if not customer_id:
|
| 168 |
+
errors.append("Customer selection is required")
|
| 169 |
+
if not product_id:
|
| 170 |
+
errors.append("Product selection is required")
|
| 171 |
+
if not demo_date:
|
| 172 |
+
errors.append("Demo date is required")
|
| 173 |
+
|
| 174 |
+
if errors:
|
| 175 |
+
for error in errors:
|
| 176 |
+
st.error(f"❌ {error}")
|
| 177 |
+
else:
|
| 178 |
+
# Check if a similar demo already exists in database (within last 5 minutes)
|
| 179 |
+
try:
|
| 180 |
+
recent_demos = db.get_dataframe(
|
| 181 |
+
"demos",
|
| 182 |
+
"""
|
| 183 |
+
SELECT demo_id FROM demos
|
| 184 |
+
WHERE customer_id = ?
|
| 185 |
+
AND product_id = ?
|
| 186 |
+
AND demo_date = ?
|
| 187 |
+
AND created_date >= datetime('now', '-5 minutes')
|
| 188 |
+
ORDER BY demo_id DESC LIMIT 1
|
| 189 |
+
""",
|
| 190 |
+
params=(customer_id, product_id, demo_date)
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
if not recent_demos.empty:
|
| 194 |
+
existing_demo_id = recent_demos.iloc[0]['demo_id']
|
| 195 |
+
st.warning(f"⚠️ A similar demo (ID: {existing_demo_id}) was already created recently. Showing that demo instead.")
|
| 196 |
+
st.session_state.last_demo_id = existing_demo_id
|
| 197 |
+
st.session_state.processed_submissions.add(submission_id)
|
| 198 |
+
st.rerun()
|
| 199 |
+
return
|
| 200 |
+
except Exception as check_error:
|
| 201 |
+
st.warning(f"Could not check for existing demos: {check_error}")
|
| 202 |
+
|
| 203 |
+
try:
|
| 204 |
+
# Ensure demo_date is a single date object (not tuple)
|
| 205 |
+
if isinstance(demo_date, tuple):
|
| 206 |
+
demo_date = demo_date[0] if demo_date else datetime.now().date()
|
| 207 |
+
|
| 208 |
+
# Ensure follow_up_date is a single date object
|
| 209 |
+
if isinstance(follow_up_date, tuple):
|
| 210 |
+
follow_up_date = follow_up_date[0] if follow_up_date else datetime.now().date() + timedelta(days=7)
|
| 211 |
+
|
| 212 |
+
# Combine date and time for notification
|
| 213 |
+
demo_datetime = datetime.combine(demo_date, demo_time)
|
| 214 |
+
|
| 215 |
+
# Add demo to database
|
| 216 |
+
demo_id = add_demo_to_database(
|
| 217 |
+
db,
|
| 218 |
+
{
|
| 219 |
+
"customer_id": customer_id,
|
| 220 |
+
"distributor_id": distributor_id,
|
| 221 |
+
"product_id": product_id,
|
| 222 |
+
"demo_date": demo_date,
|
| 223 |
+
"demo_time": demo_time,
|
| 224 |
+
"quantity_provided": quantity_provided,
|
| 225 |
+
"follow_up_date": follow_up_date,
|
| 226 |
+
"conversion_status": conversion_status,
|
| 227 |
+
"notes": notes,
|
| 228 |
+
"demo_location": demo_location,
|
| 229 |
+
},
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
if demo_id and demo_id > 0:
|
| 233 |
+
# Mark this submission as processed
|
| 234 |
+
st.session_state.processed_submissions.add(submission_id)
|
| 235 |
+
|
| 236 |
+
st.success(
|
| 237 |
+
f"✅ Demo scheduled successfully! Demo ID: {demo_id}"
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
# Send notification if WhatsApp available
|
| 241 |
+
if whatsapp_manager and customer_id:
|
| 242 |
+
send_demo_notification(
|
| 243 |
+
whatsapp_manager,
|
| 244 |
+
db,
|
| 245 |
+
customer_id,
|
| 246 |
+
demo_datetime,
|
| 247 |
+
product_id,
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
# Store demo_id in session state to show summary outside form
|
| 251 |
+
st.session_state.last_demo_id = demo_id
|
| 252 |
+
|
| 253 |
+
# Set flag for dashboard notification
|
| 254 |
+
st.session_state.demo_created_notification = demo_id
|
| 255 |
+
|
| 256 |
+
# Update refresh time to ensure dashboard shows latest demos
|
| 257 |
+
st.session_state.demo_refresh_time = time.time()
|
| 258 |
+
|
| 259 |
+
# Rerun to show summary
|
| 260 |
+
st.rerun()
|
| 261 |
+
|
| 262 |
+
else:
|
| 263 |
+
st.error("❌ Failed to schedule demo. Please try again.")
|
| 264 |
+
|
| 265 |
+
except Exception as e:
|
| 266 |
+
st.error(f"❌ Error scheduling demo: {e}")
|
| 267 |
+
import traceback
|
| 268 |
+
st.code(traceback.format_exc())
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def add_demo_to_database(db, demo_data):
|
| 272 |
+
"""Add demo record to database"""
|
| 273 |
+
try:
|
| 274 |
+
# Insert the demo - execute_query returns [(lastrowid,)] for INSERT
|
| 275 |
+
result = db.execute_query(
|
| 276 |
+
"""
|
| 277 |
+
INSERT INTO demos (customer_id, distributor_id, product_id, demo_date, demo_time,
|
| 278 |
+
quantity_provided, follow_up_date, conversion_status, notes, demo_location)
|
| 279 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 280 |
+
""",
|
| 281 |
+
(
|
| 282 |
+
demo_data["customer_id"],
|
| 283 |
+
demo_data["distributor_id"],
|
| 284 |
+
demo_data["product_id"],
|
| 285 |
+
demo_data["demo_date"],
|
| 286 |
+
demo_data["demo_time"].strftime("%H:%M:%S")
|
| 287 |
+
if demo_data.get("demo_time")
|
| 288 |
+
else None,
|
| 289 |
+
demo_data["quantity_provided"],
|
| 290 |
+
demo_data["follow_up_date"],
|
| 291 |
+
demo_data["conversion_status"],
|
| 292 |
+
demo_data["notes"],
|
| 293 |
+
demo_data["demo_location"],
|
| 294 |
+
),
|
| 295 |
+
log_action=False,
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
# The execute_query method returns [(lastrowid,)] for INSERT queries
|
| 299 |
+
demo_id = result[0][0] if result and len(result) > 0 and result[0][0] else None
|
| 300 |
+
|
| 301 |
+
if demo_id:
|
| 302 |
+
return demo_id
|
| 303 |
+
else:
|
| 304 |
+
st.error("❌ Failed to get demo_id after insertion")
|
| 305 |
+
return -1
|
| 306 |
+
|
| 307 |
+
except Exception as e:
|
| 308 |
+
st.error(f"Database error: {e}")
|
| 309 |
+
import traceback
|
| 310 |
+
st.code(traceback.format_exc())
|
| 311 |
+
return -1
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def send_demo_notification(
|
| 315 |
+
whatsapp_manager, db, customer_id, demo_datetime, product_id
|
| 316 |
+
):
|
| 317 |
+
"""Send demo notification to customer"""
|
| 318 |
+
try:
|
| 319 |
+
# Get customer and product details
|
| 320 |
+
customer = db.get_dataframe(
|
| 321 |
+
"customers", f"SELECT * FROM customers WHERE customer_id = {customer_id}"
|
| 322 |
+
)
|
| 323 |
+
product = db.get_dataframe(
|
| 324 |
+
"products", f"SELECT * FROM products WHERE product_id = {product_id}"
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
if not customer.empty and not product.empty:
|
| 328 |
+
customer_data = customer.iloc[0]
|
| 329 |
+
product_data = product.iloc[0]
|
| 330 |
+
|
| 331 |
+
if customer_data.get("mobile"):
|
| 332 |
+
# Format time safely
|
| 333 |
+
time_str = demo_datetime.strftime("%I:%M %p")
|
| 334 |
+
|
| 335 |
+
message = f"""Hello {customer_data["name"]}! 🎉
|
| 336 |
+
|
| 337 |
+
We're excited to confirm your product demo!
|
| 338 |
+
|
| 339 |
+
📅 Date: {demo_datetime.strftime("%d %b %Y")}
|
| 340 |
+
⏰ Time: {time_str}
|
| 341 |
+
📦 Product: {product_data["product_name"]}
|
| 342 |
+
|
| 343 |
+
Our team will demonstrate the product features and answer any questions you may have.
|
| 344 |
+
|
| 345 |
+
We look forward to meeting you!
|
| 346 |
+
|
| 347 |
+
Best regards,
|
| 348 |
+
Sales Team"""
|
| 349 |
+
|
| 350 |
+
success = whatsapp_manager.send_message(
|
| 351 |
+
customer_data["mobile"], message
|
| 352 |
+
)
|
| 353 |
+
if success:
|
| 354 |
+
st.success("📱 Demo notification sent to customer!")
|
| 355 |
+
else:
|
| 356 |
+
st.warning("⚠️ Could not send demo notification")
|
| 357 |
+
|
| 358 |
+
except Exception as e:
|
| 359 |
+
st.warning(f"Could not send demo notification: {e}")
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def show_demo_summary(db, demo_id):
|
| 363 |
+
"""Show summary of scheduled demo"""
|
| 364 |
+
try:
|
| 365 |
+
demo_data = db.get_dataframe(
|
| 366 |
+
"demos",
|
| 367 |
+
f"""
|
| 368 |
+
SELECT d.*, c.name as customer_name, c.village, p.product_name,
|
| 369 |
+
dist.name as distributor_name
|
| 370 |
+
FROM demos d
|
| 371 |
+
LEFT JOIN customers c ON d.customer_id = c.customer_id
|
| 372 |
+
LEFT JOIN products p ON d.product_id = p.product_id
|
| 373 |
+
LEFT JOIN distributors dist ON d.distributor_id = dist.distributor_id
|
| 374 |
+
WHERE d.demo_id = {demo_id}
|
| 375 |
+
""",
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
if not demo_data.empty:
|
| 379 |
+
demo = demo_data.iloc[0]
|
| 380 |
+
|
| 381 |
+
st.markdown("## 🎉 Demo Scheduled Successfully!")
|
| 382 |
+
|
| 383 |
+
col1, col2 = st.columns(2)
|
| 384 |
+
|
| 385 |
+
with col1:
|
| 386 |
+
st.subheader("👥 Demo Details")
|
| 387 |
+
st.write(f"**Demo ID:** {demo_id}")
|
| 388 |
+
st.write(f"**Customer:** {demo['customer_name']}")
|
| 389 |
+
st.write(f"**Village:** {demo['village']}")
|
| 390 |
+
st.write(f"**Product:** {demo['product_name']}")
|
| 391 |
+
if demo.get("distributor_name"):
|
| 392 |
+
st.write(f"**Distributor:** {demo['distributor_name']}")
|
| 393 |
+
|
| 394 |
+
with col2:
|
| 395 |
+
st.subheader("📅 Schedule")
|
| 396 |
+
st.write(f"**Demo Date:** {demo['demo_date']}")
|
| 397 |
+
if demo.get("demo_time"):
|
| 398 |
+
st.write(f"**Demo Time:** {demo['demo_time']}")
|
| 399 |
+
st.write(f"**Follow-up Date:** {demo['follow_up_date']}")
|
| 400 |
+
st.write(f"**Status:** {demo['conversion_status']}")
|
| 401 |
+
|
| 402 |
+
# Quick actions
|
| 403 |
+
st.markdown("### ⚡ Quick Actions")
|
| 404 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 405 |
+
|
| 406 |
+
with col1:
|
| 407 |
+
if st.button("🏠 Go to Dashboard"):
|
| 408 |
+
st.session_state.current_page = "dashboard"
|
| 409 |
+
st.rerun()
|
| 410 |
+
|
| 411 |
+
with col2:
|
| 412 |
+
if st.button("📋 View All Demos"):
|
| 413 |
+
st.session_state.current_tab = "📋 Demo Calendar"
|
| 414 |
+
st.rerun()
|
| 415 |
+
|
| 416 |
+
with col3:
|
| 417 |
+
if st.button("➕ Schedule Another"):
|
| 418 |
+
st.rerun()
|
| 419 |
+
|
| 420 |
+
with col4:
|
| 421 |
+
if st.button("📊 View Analytics"):
|
| 422 |
+
st.session_state.current_tab = "📊 Demo Analytics"
|
| 423 |
+
st.rerun()
|
| 424 |
+
|
| 425 |
+
except Exception as e:
|
| 426 |
+
st.error(f"Error displaying demo summary: {e}")
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def show_demo_calendar_tab(db):
|
| 430 |
+
"""Show demo calendar and upcoming demos"""
|
| 431 |
+
st.subheader("📋 Demo Calendar & Schedule")
|
| 432 |
+
|
| 433 |
+
try:
|
| 434 |
+
# Date range filter
|
| 435 |
+
col1, col2 = st.columns(2)
|
| 436 |
+
with col1:
|
| 437 |
+
start_date = st.date_input("Start Date", datetime.now().date())
|
| 438 |
+
with col2:
|
| 439 |
+
end_date = st.date_input("End Date", datetime.now().date() + timedelta(days=30))
|
| 440 |
+
|
| 441 |
+
# Status filter
|
| 442 |
+
status_filter = st.multiselect(
|
| 443 |
+
"Filter by Status",
|
| 444 |
+
["Scheduled", "Completed", "Cancelled", "Converted", "Not Converted"],
|
| 445 |
+
default=["Scheduled", "Completed"],
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
# Get demos data
|
| 449 |
+
demos_data = get_demos_data(db, start_date, end_date, status_filter)
|
| 450 |
+
|
| 451 |
+
if not demos_data.empty:
|
| 452 |
+
# Convert demo_date to datetime for comparison
|
| 453 |
+
demos_data["demo_date"] = pd.to_datetime(demos_data["demo_date"]).dt.date
|
| 454 |
+
|
| 455 |
+
st.write(f"**📅 Showing {len(demos_data)} demos**")
|
| 456 |
+
|
| 457 |
+
# Upcoming demos (next 7 days)
|
| 458 |
+
upcoming_demos = demos_data[
|
| 459 |
+
(demos_data["demo_date"] <= datetime.now().date() + timedelta(days=7))
|
| 460 |
+
& (demos_data["conversion_status"] == "Scheduled")
|
| 461 |
+
]
|
| 462 |
+
|
| 463 |
+
if not upcoming_demos.empty:
|
| 464 |
+
st.subheader("🚀 Upcoming Demos (Next 7 Days)")
|
| 465 |
+
display_upcoming = upcoming_demos[
|
| 466 |
+
[
|
| 467 |
+
"demo_date",
|
| 468 |
+
"customer_name",
|
| 469 |
+
"village",
|
| 470 |
+
"product_name",
|
| 471 |
+
"distributor_name",
|
| 472 |
+
]
|
| 473 |
+
].copy()
|
| 474 |
+
display_upcoming.columns = [
|
| 475 |
+
"Date",
|
| 476 |
+
"Customer",
|
| 477 |
+
"Village",
|
| 478 |
+
"Product",
|
| 479 |
+
"Distributor",
|
| 480 |
+
]
|
| 481 |
+
st.dataframe(display_upcoming, use_container_width=True)
|
| 482 |
+
|
| 483 |
+
# All demos in date range
|
| 484 |
+
st.subheader("📋 All Demos")
|
| 485 |
+
display_all = demos_data[
|
| 486 |
+
[
|
| 487 |
+
"demo_date",
|
| 488 |
+
"customer_name",
|
| 489 |
+
"village",
|
| 490 |
+
"product_name",
|
| 491 |
+
"conversion_status",
|
| 492 |
+
"distributor_name",
|
| 493 |
+
]
|
| 494 |
+
].copy()
|
| 495 |
+
display_all.columns = [
|
| 496 |
+
"Date",
|
| 497 |
+
"Customer",
|
| 498 |
+
"Village",
|
| 499 |
+
"Product",
|
| 500 |
+
"Status",
|
| 501 |
+
"Distributor",
|
| 502 |
+
]
|
| 503 |
+
display_all = display_all.sort_values("Date", ascending=False)
|
| 504 |
+
st.dataframe(display_all, use_container_width=True)
|
| 505 |
+
|
| 506 |
+
# Demo statistics
|
| 507 |
+
st.subheader("📊 Demo Statistics")
|
| 508 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 509 |
+
|
| 510 |
+
with col1:
|
| 511 |
+
total_demos = len(demos_data)
|
| 512 |
+
st.metric("Total Demos", total_demos)
|
| 513 |
+
|
| 514 |
+
with col2:
|
| 515 |
+
scheduled = len(
|
| 516 |
+
demos_data[demos_data["conversion_status"] == "Scheduled"]
|
| 517 |
+
)
|
| 518 |
+
st.metric("Scheduled", scheduled)
|
| 519 |
+
|
| 520 |
+
with col3:
|
| 521 |
+
completed = len(
|
| 522 |
+
demos_data[demos_data["conversion_status"] == "Completed"]
|
| 523 |
+
)
|
| 524 |
+
st.metric("Completed", completed)
|
| 525 |
+
|
| 526 |
+
with col4:
|
| 527 |
+
converted = len(
|
| 528 |
+
demos_data[demos_data["conversion_status"] == "Converted"]
|
| 529 |
+
)
|
| 530 |
+
st.metric("Converted", converted)
|
| 531 |
+
|
| 532 |
+
else:
|
| 533 |
+
st.info("No demos found for the selected criteria.")
|
| 534 |
+
|
| 535 |
+
except Exception as e:
|
| 536 |
+
st.error(f"Error loading demo calendar: {e}")
|
| 537 |
+
import traceback
|
| 538 |
+
st.code(traceback.format_exc())
|
| 539 |
+
|
| 540 |
+
|
| 541 |
+
def get_demos_data(db, start_date, end_date, status_filter):
|
| 542 |
+
"""Get demos data with filters"""
|
| 543 |
+
try:
|
| 544 |
+
query = """
|
| 545 |
+
SELECT d.*, c.name as customer_name, c.village, c.mobile,
|
| 546 |
+
p.product_name, dist.name as distributor_name
|
| 547 |
+
FROM demos d
|
| 548 |
+
LEFT JOIN customers c ON d.customer_id = c.customer_id
|
| 549 |
+
LEFT JOIN products p ON d.product_id = p.product_id
|
| 550 |
+
LEFT JOIN distributors dist ON d.distributor_id = dist.distributor_id
|
| 551 |
+
WHERE d.demo_date BETWEEN ? AND ?
|
| 552 |
+
"""
|
| 553 |
+
|
| 554 |
+
params = [start_date, end_date]
|
| 555 |
+
|
| 556 |
+
if status_filter:
|
| 557 |
+
placeholders = ",".join(["?" for _ in status_filter])
|
| 558 |
+
query += f" AND d.conversion_status IN ({placeholders})"
|
| 559 |
+
params.extend(status_filter)
|
| 560 |
+
|
| 561 |
+
query += " ORDER BY d.demo_date, d.demo_time"
|
| 562 |
+
|
| 563 |
+
return db.get_dataframe("demos", query, params=params)
|
| 564 |
+
|
| 565 |
+
except Exception as e:
|
| 566 |
+
st.error(f"Error getting demos data: {e}")
|
| 567 |
+
return pd.DataFrame()
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
def show_demo_analytics_tab(db):
|
| 571 |
+
"""Show demo analytics and conversion rates"""
|
| 572 |
+
st.subheader("📊 Demo Analytics")
|
| 573 |
+
|
| 574 |
+
try:
|
| 575 |
+
# Get demo conversion data
|
| 576 |
+
demos_data = db.get_dataframe(
|
| 577 |
+
"demos",
|
| 578 |
+
"""
|
| 579 |
+
SELECT d.*, c.name as customer_name, c.village,
|
| 580 |
+
p.product_name, dist.name as distributor_name
|
| 581 |
+
FROM demos d
|
| 582 |
+
LEFT JOIN customers c ON d.customer_id = c.customer_id
|
| 583 |
+
LEFT JOIN products p ON d.product_id = p.product_id
|
| 584 |
+
LEFT JOIN distributors dist ON d.distributor_id = dist.distributor_id
|
| 585 |
+
ORDER BY d.demo_date DESC
|
| 586 |
+
""",
|
| 587 |
+
)
|
| 588 |
+
|
| 589 |
+
if not demos_data.empty:
|
| 590 |
+
# Convert demo_date to datetime for proper handling
|
| 591 |
+
demos_data["demo_date"] = pd.to_datetime(demos_data["demo_date"])
|
| 592 |
+
|
| 593 |
+
# Conversion statistics
|
| 594 |
+
st.subheader("🎯 Conversion Analytics")
|
| 595 |
+
|
| 596 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 597 |
+
|
| 598 |
+
with col1:
|
| 599 |
+
total_demos = len(demos_data)
|
| 600 |
+
st.metric("Total Demos", total_demos)
|
| 601 |
+
|
| 602 |
+
with col2:
|
| 603 |
+
converted = len(
|
| 604 |
+
demos_data[demos_data["conversion_status"] == "Converted"]
|
| 605 |
+
)
|
| 606 |
+
st.metric("Converted", converted)
|
| 607 |
+
|
| 608 |
+
with col3:
|
| 609 |
+
not_converted = len(
|
| 610 |
+
demos_data[demos_data["conversion_status"] == "Not Converted"]
|
| 611 |
+
)
|
| 612 |
+
st.metric("Not Converted", not_converted)
|
| 613 |
+
|
| 614 |
+
with col4:
|
| 615 |
+
conversion_rate = (
|
| 616 |
+
(converted / total_demos * 100) if total_demos > 0 else 0
|
| 617 |
+
)
|
| 618 |
+
st.metric("Conversion Rate", f"{conversion_rate:.1f}%")
|
| 619 |
+
|
| 620 |
+
# Product-wise conversion
|
| 621 |
+
st.subheader("📦 Product Performance")
|
| 622 |
+
if "product_name" in demos_data.columns:
|
| 623 |
+
product_stats = (
|
| 624 |
+
demos_data.groupby("product_name")
|
| 625 |
+
.agg(
|
| 626 |
+
{
|
| 627 |
+
"demo_id": "count",
|
| 628 |
+
"conversion_status": lambda x: (x == "Converted").sum(),
|
| 629 |
+
}
|
| 630 |
+
)
|
| 631 |
+
.reset_index()
|
| 632 |
+
)
|
| 633 |
+
product_stats.columns = ["Product", "Total Demos", "Converted"]
|
| 634 |
+
product_stats["Conversion Rate"] = (
|
| 635 |
+
product_stats["Converted"] / product_stats["Total Demos"] * 100
|
| 636 |
+
).round(1)
|
| 637 |
+
product_stats = product_stats.sort_values(
|
| 638 |
+
"Total Demos", ascending=False
|
| 639 |
+
)
|
| 640 |
+
|
| 641 |
+
st.dataframe(product_stats, use_container_width=True)
|
| 642 |
+
|
| 643 |
+
# Monthly trend
|
| 644 |
+
st.subheader("📈 Monthly Demo Trend")
|
| 645 |
+
try:
|
| 646 |
+
demos_data["demo_date"] = pd.to_datetime(demos_data["demo_date"])
|
| 647 |
+
monthly_trend = demos_data.groupby(
|
| 648 |
+
demos_data["demo_date"].dt.to_period("M")
|
| 649 |
+
).size()
|
| 650 |
+
monthly_trend.index = monthly_trend.index.astype(str)
|
| 651 |
+
|
| 652 |
+
if not monthly_trend.empty:
|
| 653 |
+
fig = px.line(
|
| 654 |
+
x=monthly_trend.index,
|
| 655 |
+
y=monthly_trend.values,
|
| 656 |
+
title="Monthly Demo Trend",
|
| 657 |
+
labels={"x": "Month", "y": "Number of Demos"},
|
| 658 |
+
)
|
| 659 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 660 |
+
except:
|
| 661 |
+
st.info("Could not generate monthly trend chart")
|
| 662 |
+
|
| 663 |
+
else:
|
| 664 |
+
st.info("No demo data available for analytics.")
|
| 665 |
+
|
| 666 |
+
except Exception as e:
|
| 667 |
+
st.error(f"Error loading demo analytics: {e}")
|
| 668 |
+
|
| 669 |
+
|
| 670 |
+
def show_follow_ups_tab(db, whatsapp_manager):
|
| 671 |
+
"""Show demo follow-ups and conversion tracking"""
|
| 672 |
+
st.subheader("🔄 Demo Follow-ups")
|
| 673 |
+
|
| 674 |
+
try:
|
| 675 |
+
# Get demos needing follow-up
|
| 676 |
+
follow_up_data = db.get_dataframe(
|
| 677 |
+
"demos",
|
| 678 |
+
"""
|
| 679 |
+
SELECT d.*, c.name as customer_name, c.mobile, c.village,
|
| 680 |
+
p.product_name, dist.name as distributor_name
|
| 681 |
+
FROM demos d
|
| 682 |
+
LEFT JOIN customers c ON d.customer_id = c.customer_id
|
| 683 |
+
LEFT JOIN products p ON d.product_id = p.product_id
|
| 684 |
+
LEFT JOIN distributors dist ON d.distributor_id = dist.distributor_id
|
| 685 |
+
WHERE d.follow_up_date <= date('now', '+7 days')
|
| 686 |
+
AND d.conversion_status IN ('Completed', 'Not Converted')
|
| 687 |
+
ORDER BY d.follow_up_date ASC
|
| 688 |
+
""",
|
| 689 |
+
)
|
| 690 |
+
|
| 691 |
+
if not follow_up_data.empty:
|
| 692 |
+
# Convert dates to datetime for comparison
|
| 693 |
+
follow_up_data["follow_up_date"] = pd.to_datetime(
|
| 694 |
+
follow_up_data["follow_up_date"]
|
| 695 |
+
).dt.date
|
| 696 |
+
|
| 697 |
+
# Overdue follow-ups
|
| 698 |
+
overdue = follow_up_data[
|
| 699 |
+
follow_up_data["follow_up_date"] < datetime.now().date()
|
| 700 |
+
]
|
| 701 |
+
if not overdue.empty:
|
| 702 |
+
st.warning(f"🚨 {len(overdue)} Overdue Follow-ups!")
|
| 703 |
+
display_overdue = overdue[
|
| 704 |
+
[
|
| 705 |
+
"follow_up_date",
|
| 706 |
+
"customer_name",
|
| 707 |
+
"village",
|
| 708 |
+
"product_name",
|
| 709 |
+
"conversion_status",
|
| 710 |
+
]
|
| 711 |
+
].copy()
|
| 712 |
+
display_overdue.columns = [
|
| 713 |
+
"Due Date",
|
| 714 |
+
"Customer",
|
| 715 |
+
"Village",
|
| 716 |
+
"Product",
|
| 717 |
+
"Status",
|
| 718 |
+
]
|
| 719 |
+
st.dataframe(display_overdue, use_container_width=True)
|
| 720 |
+
|
| 721 |
+
# Upcoming follow-ups
|
| 722 |
+
upcoming = follow_up_data[
|
| 723 |
+
follow_up_data["follow_up_date"] >= datetime.now().date()
|
| 724 |
+
]
|
| 725 |
+
if not upcoming.empty:
|
| 726 |
+
st.subheader("📅 Upcoming Follow-ups")
|
| 727 |
+
display_upcoming = upcoming[
|
| 728 |
+
[
|
| 729 |
+
"follow_up_date",
|
| 730 |
+
"customer_name",
|
| 731 |
+
"village",
|
| 732 |
+
"product_name",
|
| 733 |
+
"conversion_status",
|
| 734 |
+
]
|
| 735 |
+
].copy()
|
| 736 |
+
display_upcoming.columns = [
|
| 737 |
+
"Due Date",
|
| 738 |
+
"Customer",
|
| 739 |
+
"Village",
|
| 740 |
+
"Product",
|
| 741 |
+
"Status",
|
| 742 |
+
]
|
| 743 |
+
st.dataframe(display_upcoming, use_container_width=True)
|
| 744 |
+
|
| 745 |
+
# Follow-up actions
|
| 746 |
+
st.subheader("🔄 Follow-up Actions")
|
| 747 |
+
selected_demo = st.selectbox(
|
| 748 |
+
"Select Demo for Follow-up",
|
| 749 |
+
options=[
|
| 750 |
+
f"{row['customer_name']} - {row['product_name']} ({row['follow_up_date']})"
|
| 751 |
+
for _, row in follow_up_data.iterrows()
|
| 752 |
+
],
|
| 753 |
+
)
|
| 754 |
+
|
| 755 |
+
if selected_demo:
|
| 756 |
+
demo_index = [
|
| 757 |
+
f"{row['customer_name']} - {row['product_name']} ({row['follow_up_date']})"
|
| 758 |
+
for _, row in follow_up_data.iterrows()
|
| 759 |
+
].index(selected_demo)
|
| 760 |
+
selected_demo_data = follow_up_data.iloc[demo_index]
|
| 761 |
+
|
| 762 |
+
col1, col2 = st.columns(2)
|
| 763 |
+
|
| 764 |
+
with col1:
|
| 765 |
+
new_status = st.selectbox(
|
| 766 |
+
"Update Conversion Status",
|
| 767 |
+
["Converted", "Not Converted", "Follow-up Required", "Lost"],
|
| 768 |
+
)
|
| 769 |
+
|
| 770 |
+
if st.button("🔄 Update Status"):
|
| 771 |
+
update_demo_status(
|
| 772 |
+
db, selected_demo_data["demo_id"], new_status
|
| 773 |
+
)
|
| 774 |
+
st.success("✅ Status updated successfully!")
|
| 775 |
+
st.rerun()
|
| 776 |
+
|
| 777 |
+
with col2:
|
| 778 |
+
if whatsapp_manager and st.button("📱 Send Follow-up Message"):
|
| 779 |
+
send_follow_up_message(whatsapp_manager, selected_demo_data)
|
| 780 |
+
st.success("✅ Follow-up message sent!")
|
| 781 |
+
|
| 782 |
+
else:
|
| 783 |
+
st.success("🎉 No pending follow-ups! All demos are up to date.")
|
| 784 |
+
|
| 785 |
+
except Exception as e:
|
| 786 |
+
st.error(f"Error loading follow-ups: {e}")
|
| 787 |
+
|
| 788 |
+
|
| 789 |
+
def update_demo_status(db, demo_id, new_status):
|
| 790 |
+
"""Update demo conversion status"""
|
| 791 |
+
try:
|
| 792 |
+
db.execute_query(
|
| 793 |
+
"""
|
| 794 |
+
UPDATE demos SET conversion_status = ?, updated_date = CURRENT_TIMESTAMP
|
| 795 |
+
WHERE demo_id = ?
|
| 796 |
+
""",
|
| 797 |
+
(new_status, demo_id),
|
| 798 |
+
log_action=False,
|
| 799 |
+
)
|
| 800 |
+
except Exception as e:
|
| 801 |
+
st.error(f"Error updating demo status: {e}")
|
| 802 |
+
|
| 803 |
+
|
| 804 |
+
def send_follow_up_message(whatsapp_manager, demo_data):
|
| 805 |
+
"""Send follow-up message for demo"""
|
| 806 |
+
try:
|
| 807 |
+
if demo_data.get("mobile"):
|
| 808 |
+
message = f"""Hello {demo_data["customer_name"]}! 👋
|
| 809 |
+
|
| 810 |
+
Following up on your {demo_data["product_name"]} demo from {demo_data["demo_date"]}.
|
| 811 |
+
|
| 812 |
+
We'd love to hear about your experience and answer any questions you may have.
|
| 813 |
+
|
| 814 |
+
Would you be interested in placing an order or scheduling another demo?
|
| 815 |
+
|
| 816 |
+
Best regards,
|
| 817 |
+
Sales Team"""
|
| 818 |
+
|
| 819 |
+
success = whatsapp_manager.send_message(demo_data["mobile"], message)
|
| 820 |
+
return success
|
| 821 |
+
return False
|
| 822 |
+
except Exception as e:
|
| 823 |
+
st.error(f"Error sending follow-up message: {e}")
|
| 824 |
+
return False
|
pages/distributors.py
ADDED
|
@@ -0,0 +1,971 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# pages/distributors.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
import plotly.graph_objects as go
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
import numpy as np
|
| 8 |
+
# pages/distributors.py
|
| 9 |
+
import streamlit as st
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import plotly.express as px
|
| 12 |
+
import plotly.graph_objects as go
|
| 13 |
+
from datetime import datetime, timedelta
|
| 14 |
+
import numpy as np
|
| 15 |
+
|
| 16 |
+
def show_distributors_page(db, whatsapp_manager=None):
|
| 17 |
+
"""Show intelligent distributors network optimization hub"""
|
| 18 |
+
st.title("🤝 Distributor Network Intelligence")
|
| 19 |
+
|
| 20 |
+
if not db:
|
| 21 |
+
st.error("Database not available. Please check initialization.")
|
| 22 |
+
return
|
| 23 |
+
|
| 24 |
+
# Tabs for different distributor functions
|
| 25 |
+
tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs(["🏆 Performance Dashboard", "➕ Add New Distributor",
|
| 26 |
+
"🗺️ Territory Analysis", "📈 Growth Opportunities",
|
| 27 |
+
"👥 Team Management", "🔍 Distributor Directory"])
|
| 28 |
+
|
| 29 |
+
with tab1:
|
| 30 |
+
show_performance_dashboard_tab(db)
|
| 31 |
+
|
| 32 |
+
with tab2:
|
| 33 |
+
show_add_distributor_tab(db, whatsapp_manager)
|
| 34 |
+
|
| 35 |
+
with tab3:
|
| 36 |
+
show_territory_analysis_tab(db)
|
| 37 |
+
|
| 38 |
+
with tab4:
|
| 39 |
+
show_growth_opportunities_tab(db)
|
| 40 |
+
|
| 41 |
+
with tab5:
|
| 42 |
+
show_team_management_tab(db, whatsapp_manager)
|
| 43 |
+
|
| 44 |
+
with tab6:
|
| 45 |
+
show_distributor_directory_tab(db)
|
| 46 |
+
|
| 47 |
+
def show_add_distributor_tab(db, whatsapp_manager):
|
| 48 |
+
"""Show form to add new distributors with comprehensive data collection"""
|
| 49 |
+
st.subheader("➕ Add New Distributor")
|
| 50 |
+
|
| 51 |
+
with st.form("add_distributor_form", clear_on_submit=True):
|
| 52 |
+
st.markdown("### 📋 Basic Information")
|
| 53 |
+
|
| 54 |
+
col1, col2 = st.columns(2)
|
| 55 |
+
|
| 56 |
+
with col1:
|
| 57 |
+
distributor_name = st.text_input("Distributor Name*", placeholder="Enter distributor name")
|
| 58 |
+
village = st.text_input("Village*", placeholder="Enter village name")
|
| 59 |
+
taluka = st.text_input("Taluka*", placeholder="Enter taluka name")
|
| 60 |
+
district = st.text_input("District", placeholder="Enter district name")
|
| 61 |
+
|
| 62 |
+
with col2:
|
| 63 |
+
mantri_name = st.text_input("Mantri Name*", placeholder="Enter mantri name")
|
| 64 |
+
mantri_mobile = st.text_input("Mantri Mobile*", placeholder="Enter 10-digit mobile number")
|
| 65 |
+
# Remove status for now since your function doesn't have it
|
| 66 |
+
# status = st.selectbox("Status", ["Active", "Inactive", "Prospective"], index=0)
|
| 67 |
+
|
| 68 |
+
st.markdown("### 📊 Network Information")
|
| 69 |
+
|
| 70 |
+
col1, col2 = st.columns(2)
|
| 71 |
+
|
| 72 |
+
with col1:
|
| 73 |
+
sabhasad_count = st.number_input("Current Sabhasad Count", min_value=0, value=0)
|
| 74 |
+
contact_in_group = st.number_input("Contacts in WhatsApp Group", min_value=0, value=0)
|
| 75 |
+
|
| 76 |
+
with col2:
|
| 77 |
+
potential_sabhasad = st.number_input("Potential Sabhasad (6 months)", min_value=0, value=0)
|
| 78 |
+
market_coverage = st.slider("Market Coverage (%)", 0, 100, 50)
|
| 79 |
+
|
| 80 |
+
# Quick duplicate check
|
| 81 |
+
if distributor_name and village and taluka:
|
| 82 |
+
if db.distributor_exists(distributor_name, village, taluka):
|
| 83 |
+
st.warning("⚠️ A distributor with this name already exists in this location!")
|
| 84 |
+
|
| 85 |
+
# Submit button
|
| 86 |
+
submitted = st.form_submit_button("🚀 Add Distributor to Network", type="primary")
|
| 87 |
+
|
| 88 |
+
if submitted:
|
| 89 |
+
# Validation
|
| 90 |
+
errors = []
|
| 91 |
+
if not distributor_name:
|
| 92 |
+
errors.append("Distributor name is required")
|
| 93 |
+
if not village:
|
| 94 |
+
errors.append("Village is required")
|
| 95 |
+
if not taluka:
|
| 96 |
+
errors.append("Taluka is required")
|
| 97 |
+
if not mantri_name:
|
| 98 |
+
errors.append("Mantri name is required")
|
| 99 |
+
if not mantri_mobile or len(mantri_mobile) < 10:
|
| 100 |
+
errors.append("Valid mobile number is required (10 digits)")
|
| 101 |
+
|
| 102 |
+
if errors:
|
| 103 |
+
for error in errors:
|
| 104 |
+
st.error(f"❌ {error}")
|
| 105 |
+
else:
|
| 106 |
+
try:
|
| 107 |
+
# Add distributor to database - WITHOUT status parameter
|
| 108 |
+
distributor_id = db.add_distributor(
|
| 109 |
+
name=distributor_name,
|
| 110 |
+
village=village,
|
| 111 |
+
taluka=taluka,
|
| 112 |
+
district=district,
|
| 113 |
+
mantri_name=mantri_name,
|
| 114 |
+
mantri_mobile=mantri_mobile,
|
| 115 |
+
sabhasad_count=sabhasad_count,
|
| 116 |
+
contact_in_group=contact_in_group
|
| 117 |
+
# status=status # Remove this line since your function doesn't have it
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
if distributor_id and distributor_id > 0:
|
| 121 |
+
st.success(f"✅ Distributor '{distributor_name}' added successfully!")
|
| 122 |
+
|
| 123 |
+
# Store additional metrics
|
| 124 |
+
save_distributor_metrics(db, distributor_id, {
|
| 125 |
+
'potential_sabhasad': potential_sabhasad,
|
| 126 |
+
'market_coverage': market_coverage,
|
| 127 |
+
'notes': "Added via distributor form"
|
| 128 |
+
})
|
| 129 |
+
|
| 130 |
+
# Show success summary
|
| 131 |
+
show_distributor_summary(db, distributor_id)
|
| 132 |
+
|
| 133 |
+
# Send welcome message
|
| 134 |
+
if whatsapp_manager and mantri_mobile:
|
| 135 |
+
send_welcome_message(whatsapp_manager, mantri_mobile, distributor_name)
|
| 136 |
+
|
| 137 |
+
else:
|
| 138 |
+
st.error("❌ Failed to add distributor. Please try again.")
|
| 139 |
+
|
| 140 |
+
except Exception as e:
|
| 141 |
+
st.error(f"❌ Error adding distributor: {e}")
|
| 142 |
+
|
| 143 |
+
def calculate_potential_score(sabhasad_count, contact_in_group, potential_sabhasad,
|
| 144 |
+
market_coverage, leadership_quality, community_influence,
|
| 145 |
+
business_experience, has_vehicle, digital_literacy):
|
| 146 |
+
"""Calculate distributor potential score (0-100)"""
|
| 147 |
+
score = 0
|
| 148 |
+
|
| 149 |
+
# Network factors (40%)
|
| 150 |
+
score += min(sabhasad_count * 2, 20) # Max 20 points for current sabhasad
|
| 151 |
+
score += min(contact_in_group * 0.2, 10) # Max 10 points for contacts
|
| 152 |
+
score += min(potential_sabhasad * 1.5, 10) # Max 10 points for potential
|
| 153 |
+
|
| 154 |
+
# Market factors (20%)
|
| 155 |
+
score += market_coverage * 0.2 # 20 points for coverage
|
| 156 |
+
|
| 157 |
+
# Personal factors (25%)
|
| 158 |
+
leadership_scores = {"Low": 0, "Medium": 5, "High": 8, "Very High": 10}
|
| 159 |
+
score += leadership_scores.get(leadership_quality, 0)
|
| 160 |
+
|
| 161 |
+
influence_scores = {"Low": 0, "Medium": 5, "High": 8, "Very High": 10}
|
| 162 |
+
score += influence_scores.get(community_influence, 0)
|
| 163 |
+
|
| 164 |
+
experience_scores = {"None": 0, "1-2 years": 2, "3-5 years": 3, "5+ years": 5}
|
| 165 |
+
score += experience_scores.get(business_experience, 0)
|
| 166 |
+
|
| 167 |
+
# Infrastructure factors (15%)
|
| 168 |
+
if has_vehicle:
|
| 169 |
+
score += 5
|
| 170 |
+
digital_scores = {"Basic": 2, "Intermediate": 4, "Advanced": 6}
|
| 171 |
+
score += digital_scores.get(digital_literacy, 0)
|
| 172 |
+
|
| 173 |
+
return min(score, 100)
|
| 174 |
+
|
| 175 |
+
def save_distributor_metrics(db, distributor_id, metrics):
|
| 176 |
+
"""Save additional distributor metrics"""
|
| 177 |
+
try:
|
| 178 |
+
# Create metrics table if not exists
|
| 179 |
+
db.execute_query('''
|
| 180 |
+
CREATE TABLE IF NOT EXISTS distributor_metrics (
|
| 181 |
+
metric_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 182 |
+
distributor_id INTEGER,
|
| 183 |
+
potential_sabhasad INTEGER,
|
| 184 |
+
market_coverage INTEGER,
|
| 185 |
+
monthly_target REAL,
|
| 186 |
+
current_business_value REAL,
|
| 187 |
+
has_vehicle BOOLEAN,
|
| 188 |
+
vehicle_type TEXT,
|
| 189 |
+
storage_capacity TEXT,
|
| 190 |
+
whatsapp_active BOOLEAN,
|
| 191 |
+
digital_literacy TEXT,
|
| 192 |
+
uses_app BOOLEAN,
|
| 193 |
+
business_experience TEXT,
|
| 194 |
+
sales_background BOOLEAN,
|
| 195 |
+
leadership_quality TEXT,
|
| 196 |
+
community_influence TEXT,
|
| 197 |
+
known_in_village BOOLEAN,
|
| 198 |
+
reference_source TEXT,
|
| 199 |
+
potential_score REAL,
|
| 200 |
+
notes TEXT,
|
| 201 |
+
created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 202 |
+
FOREIGN KEY (distributor_id) REFERENCES distributors (distributor_id) ON DELETE CASCADE
|
| 203 |
+
)
|
| 204 |
+
''', log_action=False)
|
| 205 |
+
|
| 206 |
+
# Insert metrics
|
| 207 |
+
db.execute_query('''
|
| 208 |
+
INSERT INTO distributor_metrics (
|
| 209 |
+
distributor_id, potential_sabhasad, market_coverage, monthly_target,
|
| 210 |
+
current_business_value, has_vehicle, vehicle_type, storage_capacity,
|
| 211 |
+
whatsapp_active, digital_literacy, uses_app, business_experience,
|
| 212 |
+
sales_background, leadership_quality, community_influence, known_in_village,
|
| 213 |
+
reference_source, potential_score, notes
|
| 214 |
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 215 |
+
''', (
|
| 216 |
+
distributor_id, metrics['potential_sabhasad'], metrics['market_coverage'],
|
| 217 |
+
metrics['monthly_target'], metrics['current_business_value'],
|
| 218 |
+
metrics['has_vehicle'], metrics['vehicle_type'], metrics['storage_capacity'],
|
| 219 |
+
metrics['whatsapp_active'], metrics['digital_literacy'], metrics['uses_app'],
|
| 220 |
+
metrics['business_experience'], metrics['sales_background'],
|
| 221 |
+
metrics['leadership_quality'], metrics['community_influence'],
|
| 222 |
+
metrics['known_in_village'], metrics['reference_source'],
|
| 223 |
+
metrics['potential_score'], metrics['notes']
|
| 224 |
+
), log_action=False)
|
| 225 |
+
|
| 226 |
+
except Exception as e:
|
| 227 |
+
st.warning(f"Could not save additional metrics: {e}")
|
| 228 |
+
|
| 229 |
+
def show_distributor_summary(name, village, taluka, mantri_name, sabhasad_count,
|
| 230 |
+
contact_in_group, potential_sabhasad, potential_score, monthly_target):
|
| 231 |
+
"""Show summary of newly added distributor"""
|
| 232 |
+
st.markdown("## 🎉 Distributor Added Successfully!")
|
| 233 |
+
|
| 234 |
+
col1, col2 = st.columns(2)
|
| 235 |
+
|
| 236 |
+
with col1:
|
| 237 |
+
st.subheader("👤 Basic Information")
|
| 238 |
+
st.write(f"**Name:** {name}")
|
| 239 |
+
st.write(f"**Village:** {village}")
|
| 240 |
+
st.write(f"**Taluka:** {taluka}")
|
| 241 |
+
st.write(f"**Mantri:** {mantri_name}")
|
| 242 |
+
|
| 243 |
+
with col2:
|
| 244 |
+
st.subheader("📊 Network Metrics")
|
| 245 |
+
st.write(f"**Current Sabhasad:** {sabhasad_count}")
|
| 246 |
+
st.write(f"**WhatsApp Contacts:** {contact_in_group}")
|
| 247 |
+
st.write(f"**Potential Sabhasad:** {potential_sabhasad}")
|
| 248 |
+
st.write(f"**Monthly Target:** ₹{monthly_target:,.0f}")
|
| 249 |
+
|
| 250 |
+
st.subheader("🎯 Potential Assessment")
|
| 251 |
+
|
| 252 |
+
# Potential score visualization
|
| 253 |
+
col1, col2, col3 = st.columns([1, 2, 1])
|
| 254 |
+
|
| 255 |
+
with col2:
|
| 256 |
+
# Create gauge chart for potential score
|
| 257 |
+
fig = go.Figure(go.Indicator(
|
| 258 |
+
mode = "gauge+number+delta",
|
| 259 |
+
value = potential_score,
|
| 260 |
+
domain = {'x': [0, 1], 'y': [0, 1]},
|
| 261 |
+
title = {'text': "Potential Score"},
|
| 262 |
+
delta = {'reference': 50},
|
| 263 |
+
gauge = {
|
| 264 |
+
'axis': {'range': [None, 100]},
|
| 265 |
+
'bar': {'color': "darkblue"},
|
| 266 |
+
'steps': [
|
| 267 |
+
{'range': [0, 40], 'color': "lightgray"},
|
| 268 |
+
{'range': [40, 70], 'color': "gray"},
|
| 269 |
+
{'range': [70, 100], 'color': "lightblue"}
|
| 270 |
+
],
|
| 271 |
+
'threshold': {
|
| 272 |
+
'line': {'color': "red", 'width': 4},
|
| 273 |
+
'thickness': 0.75,
|
| 274 |
+
'value': 90
|
| 275 |
+
}
|
| 276 |
+
}
|
| 277 |
+
))
|
| 278 |
+
|
| 279 |
+
fig.update_layout(height=300)
|
| 280 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 281 |
+
|
| 282 |
+
# Action recommendations based on score
|
| 283 |
+
st.subheader("💡 Recommended Actions")
|
| 284 |
+
|
| 285 |
+
if potential_score >= 80:
|
| 286 |
+
st.success("**🎯 High Potential Distributor**")
|
| 287 |
+
st.write("- Provide advanced training materials")
|
| 288 |
+
st.write("- Set ambitious growth targets")
|
| 289 |
+
st.write("- Consider for leadership role")
|
| 290 |
+
st.write("- Regular high-level engagement")
|
| 291 |
+
|
| 292 |
+
elif potential_score >= 60:
|
| 293 |
+
st.info("**📈 Good Potential Distributor**")
|
| 294 |
+
st.write("- Standard training program")
|
| 295 |
+
st.write("- Moderate growth targets")
|
| 296 |
+
st.write("- Regular follow-ups")
|
| 297 |
+
st.write("- Support with marketing materials")
|
| 298 |
+
|
| 299 |
+
elif potential_score >= 40:
|
| 300 |
+
st.warning("**🔄 Moderate Potential Distributor**")
|
| 301 |
+
st.write("- Basic training focus")
|
| 302 |
+
st.write("- Conservative targets")
|
| 303 |
+
st.write("- Close monitoring needed")
|
| 304 |
+
st.write("- Additional support required")
|
| 305 |
+
|
| 306 |
+
else:
|
| 307 |
+
st.error("**⚠️ Needs Development**")
|
| 308 |
+
st.write("- Intensive training program")
|
| 309 |
+
st.write("- Small, achievable targets")
|
| 310 |
+
st.write("- Frequent check-ins")
|
| 311 |
+
st.write("- Consider mentorship")
|
| 312 |
+
|
| 313 |
+
def send_welcome_message(whatsapp_manager, mobile, distributor_name):
|
| 314 |
+
"""Send welcome message to new distributor"""
|
| 315 |
+
try:
|
| 316 |
+
message = f"""Welcome {distributor_name}! 🎉
|
| 317 |
+
|
| 318 |
+
Thank you for joining our distributor network!
|
| 319 |
+
|
| 320 |
+
We're excited to have you on board and look forward to working together to grow your business.
|
| 321 |
+
|
| 322 |
+
Our team will contact you shortly to discuss:
|
| 323 |
+
• Training schedule
|
| 324 |
+
• Product information
|
| 325 |
+
• Sales strategies
|
| 326 |
+
• Support systems
|
| 327 |
+
|
| 328 |
+
For any immediate queries, feel free to contact us.
|
| 329 |
+
|
| 330 |
+
Best regards,
|
| 331 |
+
Sales Team"""
|
| 332 |
+
|
| 333 |
+
success = whatsapp_manager.send_message(mobile, message)
|
| 334 |
+
if success:
|
| 335 |
+
st.success("📱 Welcome message sent to distributor!")
|
| 336 |
+
else:
|
| 337 |
+
st.warning("⚠️ Could not send welcome message")
|
| 338 |
+
|
| 339 |
+
except Exception as e:
|
| 340 |
+
st.warning(f"Could not send welcome message: {e}")
|
| 341 |
+
|
| 342 |
+
# Keep all your existing functions (show_performance_dashboard_tab, show_territory_analysis_tab, etc.)
|
| 343 |
+
# ... [rest of your existing functions remain unchanged]
|
| 344 |
+
|
| 345 |
+
def show_performance_dashboard_tab(db):
|
| 346 |
+
"""Show distributor performance dashboard"""
|
| 347 |
+
st.subheader("🏆 Distributor Performance Dashboard")
|
| 348 |
+
|
| 349 |
+
try:
|
| 350 |
+
distributors_data = get_distributor_analytics_data(db)
|
| 351 |
+
|
| 352 |
+
if distributors_data.empty:
|
| 353 |
+
st.info("No distributor data available yet.")
|
| 354 |
+
return
|
| 355 |
+
|
| 356 |
+
except:
|
| 357 |
+
pass
|
| 358 |
+
def show_distributors_page(db, whatsapp_manager=None):
|
| 359 |
+
"""Show intelligent distributors network optimization hub"""
|
| 360 |
+
st.title("🤝 Distributor Network Intelligence")
|
| 361 |
+
|
| 362 |
+
if not db:
|
| 363 |
+
st.error("Database not available. Please check initialization.")
|
| 364 |
+
return
|
| 365 |
+
|
| 366 |
+
# Tabs for different distributor functions
|
| 367 |
+
tab1, tab2, tab3, tab4, tab5 ,tab6= st.tabs(["🏆 Performance Dashboard", "🗺️ Territory Analysis",
|
| 368 |
+
"📈 Growth Opportunities", "👥 Team Management",
|
| 369 |
+
"🔍 Distributor Directory","➕Add new distributor"])
|
| 370 |
+
|
| 371 |
+
with tab1:
|
| 372 |
+
show_performance_dashboard_tab(db)
|
| 373 |
+
|
| 374 |
+
with tab2:
|
| 375 |
+
show_territory_analysis_tab(db)
|
| 376 |
+
|
| 377 |
+
with tab3:
|
| 378 |
+
show_growth_opportunities_tab(db)
|
| 379 |
+
|
| 380 |
+
with tab4:
|
| 381 |
+
show_team_management_tab(db, whatsapp_manager)
|
| 382 |
+
|
| 383 |
+
with tab5:
|
| 384 |
+
show_distributor_directory_tab(db)
|
| 385 |
+
|
| 386 |
+
with tab6:
|
| 387 |
+
show_add_distributor_tab(db)
|
| 388 |
+
|
| 389 |
+
def show_performance_dashboard_tab(db):
|
| 390 |
+
"""Show distributor performance dashboard"""
|
| 391 |
+
st.subheader("🏆 Distributor Performance Dashboard")
|
| 392 |
+
|
| 393 |
+
try:
|
| 394 |
+
distributors_data = get_distributor_analytics_data(db)
|
| 395 |
+
|
| 396 |
+
if distributors_data.empty:
|
| 397 |
+
st.info("No distributor data available yet.")
|
| 398 |
+
return
|
| 399 |
+
|
| 400 |
+
# Key Performance Indicators
|
| 401 |
+
st.subheader("🎯 Key Performance Indicators")
|
| 402 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 403 |
+
|
| 404 |
+
with col1:
|
| 405 |
+
total_distributors = len(distributors_data)
|
| 406 |
+
st.metric("Total Distributors", total_distributors)
|
| 407 |
+
|
| 408 |
+
with col2:
|
| 409 |
+
active_distributors = len(distributors_data[distributors_data['total_customers'] > 0])
|
| 410 |
+
st.metric("Active Distributors", active_distributors)
|
| 411 |
+
|
| 412 |
+
with col3:
|
| 413 |
+
avg_sabhasad_per_dist = distributors_data['sabhasad_count'].mean()
|
| 414 |
+
st.metric("Avg Sabhasad/Dist", f"{avg_sabhasad_per_dist:.1f}")
|
| 415 |
+
|
| 416 |
+
with col4:
|
| 417 |
+
total_network_size = distributors_data['sabhasad_count'].sum() + total_distributors
|
| 418 |
+
st.metric("Total Network Size", total_network_size)
|
| 419 |
+
|
| 420 |
+
# Performance Tiers
|
| 421 |
+
st.subheader("📊 Performance Tiers")
|
| 422 |
+
|
| 423 |
+
# Define performance tiers based on sabhasad count
|
| 424 |
+
distributors_data['performance_tier'] = distributors_data['sabhasad_count'].apply(
|
| 425 |
+
lambda x: 'Platinum' if x >= 20 else 'Gold' if x >= 10 else 'Silver' if x >= 5 else 'Bronze'
|
| 426 |
+
)
|
| 427 |
+
|
| 428 |
+
tier_stats = distributors_data['performance_tier'].value_counts()
|
| 429 |
+
|
| 430 |
+
col1, col2 = st.columns(2)
|
| 431 |
+
|
| 432 |
+
with col1:
|
| 433 |
+
fig = px.pie(values=tier_stats.values, names=tier_stats.index,
|
| 434 |
+
title='Distributor Performance Tier Distribution',
|
| 435 |
+
color=tier_stats.index,
|
| 436 |
+
color_discrete_map={'Platinum': '#FFD700', 'Gold': '#C0C0C0',
|
| 437 |
+
'Silver': '#CD7F32', 'Bronze': '#8C7853'})
|
| 438 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 439 |
+
|
| 440 |
+
with col2:
|
| 441 |
+
# Top performers
|
| 442 |
+
top_performers = distributors_data.nlargest(5, 'sabhasad_count')[
|
| 443 |
+
['name', 'village', 'sabhasad_count', 'contact_in_group']
|
| 444 |
+
]
|
| 445 |
+
top_performers.columns = ['Distributor', 'Village', 'Sabhasad', 'Contacts']
|
| 446 |
+
|
| 447 |
+
st.write("**🏅 Top 5 Performers**")
|
| 448 |
+
st.dataframe(top_performers, use_container_width=True)
|
| 449 |
+
|
| 450 |
+
# Geographic Performance Heatmap
|
| 451 |
+
st.subheader("🗺️ Geographic Performance Distribution")
|
| 452 |
+
|
| 453 |
+
village_performance = distributors_data.groupby('village').agg({
|
| 454 |
+
'distributor_id': 'count',
|
| 455 |
+
'sabhasad_count': 'sum',
|
| 456 |
+
'contact_in_group': 'sum'
|
| 457 |
+
}).reset_index()
|
| 458 |
+
village_performance.columns = ['Village', 'Distributors', 'Total Sabhasad', 'Total Contacts']
|
| 459 |
+
village_performance = village_performance.sort_values('Total Sabhasad', ascending=False)
|
| 460 |
+
|
| 461 |
+
if not village_performance.empty:
|
| 462 |
+
fig = px.bar(village_performance.head(10), x='Village', y='Total Sabhasad',
|
| 463 |
+
title='Top 10 Villages by Sabhasad Network Size',
|
| 464 |
+
color='Total Sabhasad',
|
| 465 |
+
labels={'Total Sabhasad': 'Sabhasad Count'})
|
| 466 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 467 |
+
|
| 468 |
+
# Performance Trends (if we had date data)
|
| 469 |
+
st.subheader("📈 Network Growth Potential")
|
| 470 |
+
|
| 471 |
+
# Calculate network density score
|
| 472 |
+
distributors_data['network_score'] = (
|
| 473 |
+
distributors_data['sabhasad_count'] * 0.6 +
|
| 474 |
+
distributors_data['contact_in_group'] * 0.4
|
| 475 |
+
)
|
| 476 |
+
|
| 477 |
+
# Identify high-potential distributors
|
| 478 |
+
high_potential = distributors_data[
|
| 479 |
+
(distributors_data['sabhasad_count'] < 10) &
|
| 480 |
+
(distributors_data['contact_in_group'] > 20)
|
| 481 |
+
]
|
| 482 |
+
|
| 483 |
+
if not high_potential.empty:
|
| 484 |
+
st.write(f"**💎 {len(high_potential)} High-Potential Distributors Identified**")
|
| 485 |
+
st.write("These distributors have good contact base but low sabhasad conversion")
|
| 486 |
+
st.dataframe(high_potential[['name', 'village', 'sabhasad_count', 'contact_in_group']],
|
| 487 |
+
use_container_width=True)
|
| 488 |
+
|
| 489 |
+
except Exception as e:
|
| 490 |
+
st.error(f"Error loading performance dashboard: {e}")
|
| 491 |
+
|
| 492 |
+
def show_territory_analysis_tab(db):
|
| 493 |
+
"""Show territory coverage and gap analysis"""
|
| 494 |
+
st.subheader("🗺️ Territory Coverage Analysis")
|
| 495 |
+
|
| 496 |
+
try:
|
| 497 |
+
distributors_data = get_distributor_analytics_data(db)
|
| 498 |
+
customers_data = get_customer_analytics_data(db)
|
| 499 |
+
|
| 500 |
+
if distributors_data.empty or customers_data.empty:
|
| 501 |
+
st.info("Insufficient data for territory analysis.")
|
| 502 |
+
return
|
| 503 |
+
|
| 504 |
+
# Territory Coverage Analysis
|
| 505 |
+
st.subheader("📍 Coverage Gap Analysis")
|
| 506 |
+
|
| 507 |
+
# Get all villages with distributors vs all villages with customers
|
| 508 |
+
distributor_villages = set(distributors_data['village'].dropna().unique())
|
| 509 |
+
customer_villages = set(customers_data['village'].dropna().unique())
|
| 510 |
+
|
| 511 |
+
# Coverage analysis
|
| 512 |
+
covered_villages = distributor_villages.intersection(customer_villages)
|
| 513 |
+
uncovered_villages = customer_villages - distributor_villages
|
| 514 |
+
distributor_only_villages = distributor_villages - customer_villages
|
| 515 |
+
|
| 516 |
+
col1, col2, col3 = st.columns(3)
|
| 517 |
+
|
| 518 |
+
with col1:
|
| 519 |
+
st.metric("Covered Villages", len(covered_villages))
|
| 520 |
+
|
| 521 |
+
with col2:
|
| 522 |
+
st.metric("Uncovered Villages", len(uncovered_villages))
|
| 523 |
+
|
| 524 |
+
with col3:
|
| 525 |
+
st.metric("Distributor-Only Villages", len(distributor_only_villages))
|
| 526 |
+
|
| 527 |
+
# Coverage visualization
|
| 528 |
+
col1, col2 = st.columns(2)
|
| 529 |
+
|
| 530 |
+
with col1:
|
| 531 |
+
coverage_data = {
|
| 532 |
+
'Category': ['Covered', 'Uncovered', 'Distributor Only'],
|
| 533 |
+
'Count': [len(covered_villages), len(uncovered_villages), len(distributor_only_villages)]
|
| 534 |
+
}
|
| 535 |
+
coverage_df = pd.DataFrame(coverage_data)
|
| 536 |
+
|
| 537 |
+
fig = px.pie(coverage_df, values='Count', names='Category',
|
| 538 |
+
title='Village Coverage Status',
|
| 539 |
+
color='Category',
|
| 540 |
+
color_discrete_map={'Covered': '#00FF00', 'Uncovered': '#FF0000',
|
| 541 |
+
'Distributor Only': '#FFFF00'})
|
| 542 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 543 |
+
|
| 544 |
+
with col2:
|
| 545 |
+
# Customer density in uncovered areas
|
| 546 |
+
if uncovered_villages:
|
| 547 |
+
uncovered_customers = customers_data[customers_data['village'].isin(uncovered_villages)]
|
| 548 |
+
village_customer_count = uncovered_customers['village'].value_counts().head(10)
|
| 549 |
+
|
| 550 |
+
if not village_customer_count.empty:
|
| 551 |
+
fig = px.bar(x=village_customer_count.index, y=village_customer_count.values,
|
| 552 |
+
title='Top Uncovered Villages by Customer Count',
|
| 553 |
+
labels={'x': 'Village', 'y': 'Customer Count'})
|
| 554 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 555 |
+
|
| 556 |
+
# Strategic Expansion Recommendations
|
| 557 |
+
st.subheader("🎯 Strategic Expansion Recommendations")
|
| 558 |
+
|
| 559 |
+
if uncovered_villages:
|
| 560 |
+
# Prioritize villages with most customers
|
| 561 |
+
expansion_priority = customers_data[customers_data['village'].isin(uncovered_villages)]
|
| 562 |
+
priority_villages = expansion_priority.groupby('village').agg({
|
| 563 |
+
'customer_id': 'count',
|
| 564 |
+
'total_spent': 'sum'
|
| 565 |
+
}).reset_index()
|
| 566 |
+
priority_villages = priority_villages.sort_values('customer_id', ascending=False)
|
| 567 |
+
|
| 568 |
+
st.write("**🚀 High-Priority Expansion Targets**")
|
| 569 |
+
st.dataframe(priority_villages.head(10), use_container_width=True)
|
| 570 |
+
|
| 571 |
+
# Expansion strategy
|
| 572 |
+
st.write("**📋 Recommended Expansion Strategy**")
|
| 573 |
+
|
| 574 |
+
high_priority = priority_villages[priority_villages['customer_id'] >= 10]
|
| 575 |
+
medium_priority = priority_villages[(priority_villages['customer_id'] >= 5) &
|
| 576 |
+
(priority_villages['customer_id'] < 10)]
|
| 577 |
+
|
| 578 |
+
if not high_priority.empty:
|
| 579 |
+
st.success(f"**Immediate Action Needed:** {len(high_priority)} villages with 10+ customers need distributor coverage")
|
| 580 |
+
|
| 581 |
+
if not medium_priority.empty:
|
| 582 |
+
st.warning(f"**Plan Expansion:** {len(medium_priority)} villages with 5-9 customers ready for coverage")
|
| 583 |
+
|
| 584 |
+
# Territory Optimization
|
| 585 |
+
st.subheader("⚡ Territory Optimization")
|
| 586 |
+
|
| 587 |
+
# Identify overcrowded territories
|
| 588 |
+
village_distributor_count = distributors_data['village'].value_counts()
|
| 589 |
+
overcrowded_villages = village_distributor_count[village_distributor_count > 2]
|
| 590 |
+
|
| 591 |
+
if not overcrowded_villages.empty:
|
| 592 |
+
st.write("**🏙️ Overcrowded Territories**")
|
| 593 |
+
st.write("Consider redistributing some distributors from these villages:")
|
| 594 |
+
for village, count in overcrowded_villages.items():
|
| 595 |
+
st.write(f"- {village}: {count} distributors")
|
| 596 |
+
|
| 597 |
+
except Exception as e:
|
| 598 |
+
st.error(f"Error in territory analysis: {e}")
|
| 599 |
+
|
| 600 |
+
def show_growth_opportunities_tab(db):
|
| 601 |
+
"""Show growth opportunities and network expansion"""
|
| 602 |
+
st.subheader("📈 Network Growth Opportunities")
|
| 603 |
+
|
| 604 |
+
try:
|
| 605 |
+
distributors_data = get_distributor_analytics_data(db)
|
| 606 |
+
|
| 607 |
+
if distributors_data.empty:
|
| 608 |
+
st.info("No distributor data available for growth analysis.")
|
| 609 |
+
return
|
| 610 |
+
|
| 611 |
+
# Growth Levers Analysis
|
| 612 |
+
st.subheader("🎯 Growth Lever Analysis")
|
| 613 |
+
|
| 614 |
+
col1, col2, col3 = st.columns(3)
|
| 615 |
+
|
| 616 |
+
with col1:
|
| 617 |
+
# Sabhasad Conversion Opportunity
|
| 618 |
+
avg_conversion_rate = distributors_data['sabhasad_count'].sum() / distributors_data['contact_in_group'].sum() * 100
|
| 619 |
+
st.metric("Avg Contact to Sabhasad Rate", f"{avg_conversion_rate:.1f}%")
|
| 620 |
+
|
| 621 |
+
with col2:
|
| 622 |
+
# Underperforming distributors
|
| 623 |
+
underperformers = len(distributors_data[distributors_data['sabhasad_count'] < 3])
|
| 624 |
+
st.metric("Distributors Needing Support", underperformers)
|
| 625 |
+
|
| 626 |
+
with col3:
|
| 627 |
+
# Expansion potential
|
| 628 |
+
total_contacts = distributors_data['contact_in_group'].sum()
|
| 629 |
+
potential_sabhasad = total_contacts * 0.3 # Assuming 30% conversion potential
|
| 630 |
+
st.metric("Potential Sabhasad Growth", f"+{potential_sabhasad:.0f}")
|
| 631 |
+
|
| 632 |
+
# Growth Initiatives
|
| 633 |
+
st.subheader("🚀 Growth Initiatives")
|
| 634 |
+
|
| 635 |
+
initiative = st.selectbox("Select Growth Initiative",
|
| 636 |
+
["Sabhasad Conversion Drive", "New Distributor Recruitment",
|
| 637 |
+
"Territory Expansion", "Performance Improvement Program"])
|
| 638 |
+
|
| 639 |
+
if initiative == "Sabhasad Conversion Drive":
|
| 640 |
+
show_sabhasad_conversion_plan(db, distributors_data)
|
| 641 |
+
|
| 642 |
+
elif initiative == "New Distributor Recruitment":
|
| 643 |
+
show_recruitment_plan(db, distributors_data)
|
| 644 |
+
|
| 645 |
+
elif initiative == "Territory Expansion":
|
| 646 |
+
show_territory_expansion_plan(db)
|
| 647 |
+
|
| 648 |
+
elif initiative == "Performance Improvement Program":
|
| 649 |
+
show_performance_improvement_plan(db, distributors_data)
|
| 650 |
+
|
| 651 |
+
# Progress Tracking
|
| 652 |
+
st.subheader("📊 Initiative Progress Tracking")
|
| 653 |
+
|
| 654 |
+
# Mock progress data - in real implementation, this would come from database
|
| 655 |
+
progress_data = {
|
| 656 |
+
'Initiative': ['Sabhasad Drive', 'Recruitment', 'Territory Expansion', 'Training'],
|
| 657 |
+
'Target': [50, 10, 5, 25],
|
| 658 |
+
'Achieved': [35, 7, 3, 20],
|
| 659 |
+
'Completion': [70, 70, 60, 80]
|
| 660 |
+
}
|
| 661 |
+
progress_df = pd.DataFrame(progress_data)
|
| 662 |
+
|
| 663 |
+
fig = px.bar(progress_df, x='Initiative', y='Completion',
|
| 664 |
+
title='Growth Initiative Progress',
|
| 665 |
+
labels={'Completion': 'Completion %'},
|
| 666 |
+
color='Completion')
|
| 667 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 668 |
+
|
| 669 |
+
except Exception as e:
|
| 670 |
+
st.error(f"Error in growth opportunities analysis: {e}")
|
| 671 |
+
|
| 672 |
+
def show_sabhasad_conversion_plan(db, distributors_data):
|
| 673 |
+
"""Show sabhasad conversion growth plan"""
|
| 674 |
+
st.write("### 📈 Sabhasad Conversion Drive")
|
| 675 |
+
|
| 676 |
+
# Identify best candidates for conversion
|
| 677 |
+
conversion_candidates = distributors_data[
|
| 678 |
+
(distributors_data['contact_in_group'] > distributors_data['sabhasad_count'] * 2) &
|
| 679 |
+
(distributors_data['contact_in_group'] >= 10)
|
| 680 |
+
].sort_values('contact_in_group', ascending=False)
|
| 681 |
+
|
| 682 |
+
if not conversion_candidates.empty:
|
| 683 |
+
st.write(f"**🎯 {len(conversion_candidates)} Distributors with High Conversion Potential**")
|
| 684 |
+
|
| 685 |
+
for _, dist in conversion_candidates.head(5).iterrows():
|
| 686 |
+
conversion_potential = dist['contact_in_group'] - dist['sabhasad_count']
|
| 687 |
+
st.write(f"- **{dist['name']}** ({dist['village']}): {dist['sabhasad_count']} sabhasad, "
|
| 688 |
+
f"{dist['contact_in_group']} contacts → **+{conversion_potential} potential**")
|
| 689 |
+
|
| 690 |
+
# Action plan
|
| 691 |
+
st.write("**📋 Action Plan**")
|
| 692 |
+
st.write("1. Conduct conversion training sessions")
|
| 693 |
+
st.write("2. Provide conversion scripts and materials")
|
| 694 |
+
st.write("3. Set weekly conversion targets")
|
| 695 |
+
st.write("4. Implement incentive program for conversions")
|
| 696 |
+
else:
|
| 697 |
+
st.info("No high-potential conversion candidates identified.")
|
| 698 |
+
|
| 699 |
+
def show_recruitment_plan(db, distributors_data):
|
| 700 |
+
"""Show new distributor recruitment plan"""
|
| 701 |
+
st.write("### 👥 New Distributor Recruitment")
|
| 702 |
+
|
| 703 |
+
# Analyze current distribution density
|
| 704 |
+
village_coverage = distributors_data['village'].value_counts()
|
| 705 |
+
low_coverage_villages = village_coverage[village_coverage == 1]
|
| 706 |
+
|
| 707 |
+
if not low_coverage_villages.empty:
|
| 708 |
+
st.write("**📍 Villages Needing Additional Distributors**")
|
| 709 |
+
for village in low_coverage_villages.index[:5]:
|
| 710 |
+
st.write(f"- {village}")
|
| 711 |
+
|
| 712 |
+
# Recruitment targets
|
| 713 |
+
st.write("**🎯 Recruitment Strategy**")
|
| 714 |
+
st.write("- Focus on high-customer-density uncovered villages")
|
| 715 |
+
st.write("- Target influential community members")
|
| 716 |
+
st.write("- Offer attractive onboarding incentives")
|
| 717 |
+
st.write("- Provide comprehensive training and support")
|
| 718 |
+
|
| 719 |
+
def show_territory_expansion_plan(db):
|
| 720 |
+
"""Show territory expansion strategy"""
|
| 721 |
+
st.write("### 🗺️ Territory Expansion Plan")
|
| 722 |
+
|
| 723 |
+
# This would integrate with the territory analysis data
|
| 724 |
+
st.write("**🚀 Expansion Priority Areas**")
|
| 725 |
+
st.write("1. High customer density uncovered villages")
|
| 726 |
+
st.write("2. Adjacent territories to high-performing distributors")
|
| 727 |
+
st.write("3. Villages with existing brand awareness")
|
| 728 |
+
st.write("4. Areas with competitor weakness")
|
| 729 |
+
|
| 730 |
+
def show_performance_improvement_plan(db, distributors_data):
|
| 731 |
+
"""Show performance improvement program"""
|
| 732 |
+
st.write("### 📊 Performance Improvement Program")
|
| 733 |
+
|
| 734 |
+
# Identify underperformers
|
| 735 |
+
underperformers = distributors_data[
|
| 736 |
+
(distributors_data['sabhasad_count'] < 5) &
|
| 737 |
+
(distributors_data['status'] == 'Active')
|
| 738 |
+
]
|
| 739 |
+
|
| 740 |
+
if not underperformers.empty:
|
| 741 |
+
st.write(f"**🔧 {len(underperformers)} Distributors Needing Performance Support**")
|
| 742 |
+
|
| 743 |
+
for _, dist in underperformers.head(5).iterrows():
|
| 744 |
+
st.write(f"- **{dist['name']}** ({dist['village']}): {dist['sabhasad_count']} sabhasad")
|
| 745 |
+
|
| 746 |
+
# Support plan
|
| 747 |
+
st.write("**🛠️ Support Initiatives**")
|
| 748 |
+
st.write("1. One-on-one coaching sessions")
|
| 749 |
+
st.write("2. Performance benchmarking")
|
| 750 |
+
st.write("3. Additional training resources")
|
| 751 |
+
st.write("4. Peer mentoring program")
|
| 752 |
+
|
| 753 |
+
def show_team_management_tab(db, whatsapp_manager):
|
| 754 |
+
"""Show team communication and management"""
|
| 755 |
+
st.subheader("👥 Team Management & Communication")
|
| 756 |
+
|
| 757 |
+
try:
|
| 758 |
+
distributors_data = get_distributor_analytics_data(db)
|
| 759 |
+
|
| 760 |
+
if distributors_data.empty:
|
| 761 |
+
st.info("No distributor data available for team management.")
|
| 762 |
+
return
|
| 763 |
+
|
| 764 |
+
# Communication Center
|
| 765 |
+
st.subheader("📞 Communication Center")
|
| 766 |
+
|
| 767 |
+
col1, col2 = st.columns(2)
|
| 768 |
+
|
| 769 |
+
with col1:
|
| 770 |
+
communication_type = st.selectbox("Communication Type",
|
| 771 |
+
["Performance Update", "Training Announcement",
|
| 772 |
+
"Incentive Program", "Urgent Meeting", "Custom Message"])
|
| 773 |
+
|
| 774 |
+
with col2:
|
| 775 |
+
target_group = st.selectbox("Target Group",
|
| 776 |
+
["All Distributors", "High Performers", "Underperformers",
|
| 777 |
+
"Specific Village", "Performance Tier"])
|
| 778 |
+
|
| 779 |
+
# Message templates
|
| 780 |
+
message_templates = {
|
| 781 |
+
"Performance Update": "Hello {name}! Your current performance: {sabhasad_count} sabhasad. Keep up the great work! 🎯",
|
| 782 |
+
"Training Announcement": "Hello {name}! Training session this week. Learn new strategies to grow your network! 📚",
|
| 783 |
+
"Incentive Program": "Hello {name}! New incentive program launched. Earn more with higher conversions! 💰",
|
| 784 |
+
"Urgent Meeting": "Hello {name}! Urgent meeting tomorrow. Your attendance is important! ⏰",
|
| 785 |
+
"Custom Message": ""
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
message = st.text_area("Message Content",
|
| 789 |
+
value=message_templates[communication_type],
|
| 790 |
+
height=100)
|
| 791 |
+
|
| 792 |
+
# Personalization options
|
| 793 |
+
st.write("**🎨 Personalization Options**")
|
| 794 |
+
col1, col2 = st.columns(2)
|
| 795 |
+
|
| 796 |
+
with col1:
|
| 797 |
+
include_performance = st.checkbox("Include Performance Data", value=True)
|
| 798 |
+
include_village = st.checkbox("Include Village", value=True)
|
| 799 |
+
|
| 800 |
+
with col2:
|
| 801 |
+
urgent_tag = st.checkbox("Mark as Urgent", value=False)
|
| 802 |
+
request_response = st.checkbox("Request Response", value=True)
|
| 803 |
+
|
| 804 |
+
# Send communication
|
| 805 |
+
if st.button("📱 Send to Distributors", type="primary"):
|
| 806 |
+
# Filter target distributors
|
| 807 |
+
target_distributors = filter_distributors_by_criteria(distributors_data, target_group)
|
| 808 |
+
|
| 809 |
+
if not target_distributors.empty:
|
| 810 |
+
st.success(f"✅ Ready to send message to {len(target_distributors)} distributors")
|
| 811 |
+
|
| 812 |
+
# Show preview
|
| 813 |
+
sample_dist = target_distributors.iloc[0]
|
| 814 |
+
preview_message = personalize_message(message, sample_dist, include_performance, include_village)
|
| 815 |
+
st.write("**Preview:**", preview_message)
|
| 816 |
+
else:
|
| 817 |
+
st.warning("No distributors match the selected criteria")
|
| 818 |
+
|
| 819 |
+
# Team Performance Alerts
|
| 820 |
+
st.subheader("🚨 Performance Alerts")
|
| 821 |
+
|
| 822 |
+
# Low performers alert
|
| 823 |
+
low_performers = distributors_data[
|
| 824 |
+
(distributors_data['sabhasad_count'] < 3) &
|
| 825 |
+
(distributors_data['status'] == 'Active')
|
| 826 |
+
]
|
| 827 |
+
|
| 828 |
+
if not low_performers.empty:
|
| 829 |
+
st.warning(f"🚨 {len(low_performers)} distributors have less than 3 sabhasad")
|
| 830 |
+
if st.button("🔄 Schedule Support Calls"):
|
| 831 |
+
st.info("Support calls scheduled with underperforming distributors")
|
| 832 |
+
|
| 833 |
+
# High performer recognition
|
| 834 |
+
high_performers = distributors_data[distributors_data['sabhasad_count'] >= 15]
|
| 835 |
+
if not high_performers.empty:
|
| 836 |
+
st.success(f"🏆 {len(high_performers)} elite performers with 15+ sabhasad")
|
| 837 |
+
if st.button("🎉 Send Recognition"):
|
| 838 |
+
st.info("Recognition messages sent to top performers")
|
| 839 |
+
|
| 840 |
+
except Exception as e:
|
| 841 |
+
st.error(f"Error in team management: {e}")
|
| 842 |
+
|
| 843 |
+
def show_distributor_directory_tab(db):
|
| 844 |
+
"""Show comprehensive distributor directory"""
|
| 845 |
+
st.subheader("🔍 Distributor Directory")
|
| 846 |
+
|
| 847 |
+
try:
|
| 848 |
+
distributors_data = get_distributor_analytics_data(db)
|
| 849 |
+
|
| 850 |
+
if distributors_data.empty:
|
| 851 |
+
st.info("No distributors found in the database.")
|
| 852 |
+
return
|
| 853 |
+
|
| 854 |
+
# Advanced filtering
|
| 855 |
+
st.subheader("🔍 Advanced Filters")
|
| 856 |
+
|
| 857 |
+
col1, col2, col3 = st.columns(3)
|
| 858 |
+
|
| 859 |
+
with col1:
|
| 860 |
+
village_filter = st.multiselect("Filter by Village", distributors_data['village'].unique())
|
| 861 |
+
performance_filter = st.selectbox("Performance Tier",
|
| 862 |
+
["All", "Platinum", "Gold", "Silver", "Bronze"])
|
| 863 |
+
|
| 864 |
+
with col2:
|
| 865 |
+
sabhasad_min = st.number_input("Min Sabhasad", 0, 100, 0)
|
| 866 |
+
sabhasad_max = st.number_input("Max Sabhasad", 0, 100, 100)
|
| 867 |
+
|
| 868 |
+
with col3:
|
| 869 |
+
status_filter = st.multiselect("Status", distributors_data['status'].unique(),
|
| 870 |
+
default=['Active'])
|
| 871 |
+
search_term = st.text_input("Search by Name/Village")
|
| 872 |
+
|
| 873 |
+
# Apply filters
|
| 874 |
+
filtered_data = distributors_data.copy()
|
| 875 |
+
|
| 876 |
+
if village_filter:
|
| 877 |
+
filtered_data = filtered_data[filtered_data['village'].isin(village_filter)]
|
| 878 |
+
|
| 879 |
+
if performance_filter != "All":
|
| 880 |
+
filtered_data = filtered_data[filtered_data['performance_tier'] == performance_filter]
|
| 881 |
+
|
| 882 |
+
filtered_data = filtered_data[
|
| 883 |
+
(filtered_data['sabhasad_count'] >= sabhasad_min) &
|
| 884 |
+
(filtered_data['sabhasad_count'] <= sabhasad_max)
|
| 885 |
+
]
|
| 886 |
+
|
| 887 |
+
if status_filter:
|
| 888 |
+
filtered_data = filtered_data[filtered_data['status'].isin(status_filter)]
|
| 889 |
+
|
| 890 |
+
if search_term:
|
| 891 |
+
filtered_data = filtered_data[
|
| 892 |
+
filtered_data['name'].str.contains(search_term, case=False, na=False) |
|
| 893 |
+
filtered_data['village'].str.contains(search_term, case=False, na=False)
|
| 894 |
+
]
|
| 895 |
+
|
| 896 |
+
# Display results
|
| 897 |
+
st.write(f"**Found {len(filtered_data)} distributors**")
|
| 898 |
+
|
| 899 |
+
display_columns = ['name', 'village', 'taluka', 'mantri_name', 'sabhasad_count',
|
| 900 |
+
'contact_in_group', 'performance_tier', 'status']
|
| 901 |
+
display_df = filtered_data[display_columns]
|
| 902 |
+
display_df.columns = ['Name', 'Village', 'Taluka', 'Mantri', 'Sabhasad', 'Contacts', 'Tier', 'Status']
|
| 903 |
+
|
| 904 |
+
st.dataframe(display_df, use_container_width=True)
|
| 905 |
+
|
| 906 |
+
# Export options
|
| 907 |
+
if st.button("📥 Export Distributor Data"):
|
| 908 |
+
csv = filtered_data.to_csv(index=False)
|
| 909 |
+
st.download_button(
|
| 910 |
+
label="Download CSV",
|
| 911 |
+
data=csv,
|
| 912 |
+
file_name=f"distributors_export_{datetime.now().strftime('%Y%m%d')}.csv",
|
| 913 |
+
mime="text/csv"
|
| 914 |
+
)
|
| 915 |
+
|
| 916 |
+
except Exception as e:
|
| 917 |
+
st.error(f"Error loading distributor directory: {e}")
|
| 918 |
+
|
| 919 |
+
def get_distributor_analytics_data(db):
|
| 920 |
+
"""Get comprehensive distributor data with analytics"""
|
| 921 |
+
try:
|
| 922 |
+
distributors = db.get_dataframe('distributors', '''
|
| 923 |
+
SELECT d.*,
|
| 924 |
+
COUNT(DISTINCT c.customer_id) as total_customers,
|
| 925 |
+
COALESCE(SUM(s.total_amount), 0) as territory_sales
|
| 926 |
+
FROM distributors d
|
| 927 |
+
LEFT JOIN customers c ON d.village = c.village AND d.taluka = c.taluka
|
| 928 |
+
LEFT JOIN sales s ON c.customer_id = s.customer_id
|
| 929 |
+
GROUP BY d.distributor_id
|
| 930 |
+
ORDER BY d.sabhasad_count DESC
|
| 931 |
+
''')
|
| 932 |
+
return distributors
|
| 933 |
+
except Exception as e:
|
| 934 |
+
st.error(f"Error loading distributor analytics data: {e}")
|
| 935 |
+
return pd.DataFrame()
|
| 936 |
+
|
| 937 |
+
def get_customer_analytics_data(db):
|
| 938 |
+
"""Get customer data for territory analysis"""
|
| 939 |
+
try:
|
| 940 |
+
customers = db.get_dataframe('customers', "SELECT * FROM customers")
|
| 941 |
+
return customers
|
| 942 |
+
except Exception as e:
|
| 943 |
+
return pd.DataFrame()
|
| 944 |
+
|
| 945 |
+
def filter_distributors_by_criteria(distributors_data, criteria):
|
| 946 |
+
"""Filter distributors based on selection criteria"""
|
| 947 |
+
if criteria == "All Distributors":
|
| 948 |
+
return distributors_data
|
| 949 |
+
elif criteria == "High Performers":
|
| 950 |
+
return distributors_data[distributors_data['sabhasad_count'] >= 10]
|
| 951 |
+
elif criteria == "Underperformers":
|
| 952 |
+
return distributors_data[distributors_data['sabhasad_count'] < 5]
|
| 953 |
+
elif criteria == "Specific Village":
|
| 954 |
+
# This would need a village selection UI in real implementation
|
| 955 |
+
return distributors_data
|
| 956 |
+
elif criteria == "Performance Tier":
|
| 957 |
+
# This would need a tier selection UI
|
| 958 |
+
return distributors_data
|
| 959 |
+
return distributors_data
|
| 960 |
+
|
| 961 |
+
def personalize_message(message, distributor, include_performance=True, include_village=True):
|
| 962 |
+
"""Personalize message for distributor"""
|
| 963 |
+
personalized = message.replace('{name}', distributor['name'])
|
| 964 |
+
|
| 965 |
+
if include_performance and '{sabhasad_count}' in message:
|
| 966 |
+
personalized = personalized.replace('{sabhasad_count}', str(distributor['sabhasad_count']))
|
| 967 |
+
|
| 968 |
+
if include_village and '{village}' in message:
|
| 969 |
+
personalized = personalized.replace('{village}', distributor.get('village', ''))
|
| 970 |
+
|
| 971 |
+
return personalized
|
pages/file_viewer.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# pages/file_viewer.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import os
|
| 5 |
+
import glob
|
| 6 |
+
import chardet
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
import re
|
| 9 |
+
|
| 10 |
+
try:
|
| 11 |
+
from deep_translator import GoogleTranslator
|
| 12 |
+
TRANSLATOR_AVAILABLE = True
|
| 13 |
+
except ImportError:
|
| 14 |
+
TRANSLATOR_AVAILABLE = False
|
| 15 |
+
|
| 16 |
+
def show_file_viewer_page(db=None, data_processor=None):
|
| 17 |
+
"""Universal file viewer for any Excel/CSV file with advanced Gujarati to English conversion"""
|
| 18 |
+
st.title("🔍 Universal File Viewer")
|
| 19 |
+
st.markdown("View and analyze any Excel or CSV file, with advanced Gujarati to English conversion using AI translation")
|
| 20 |
+
|
| 21 |
+
if not TRANSLATOR_AVAILABLE:
|
| 22 |
+
st.error("""
|
| 23 |
+
**Translation features require deep-translator**
|
| 24 |
+
Install with: `pip install deep-translator`
|
| 25 |
+
|
| 26 |
+
Without this, only basic number conversion will work.
|
| 27 |
+
""")
|
| 28 |
+
|
| 29 |
+
# File selection options
|
| 30 |
+
tab1, tab2 = st.tabs(["📁 Browse Data Folder", "📤 Upload New File"])
|
| 31 |
+
|
| 32 |
+
with tab1:
|
| 33 |
+
show_data_folder_browser()
|
| 34 |
+
|
| 35 |
+
with tab2:
|
| 36 |
+
show_file_uploader()
|
| 37 |
+
|
| 38 |
+
def show_data_folder_browser():
|
| 39 |
+
"""Browse and view files from the data folder"""
|
| 40 |
+
data_dir = "data"
|
| 41 |
+
|
| 42 |
+
if not os.path.exists(data_dir):
|
| 43 |
+
os.makedirs(data_dir, exist_ok=True)
|
| 44 |
+
st.info("Data folder created. Upload files to get started.")
|
| 45 |
+
return
|
| 46 |
+
|
| 47 |
+
# Get all supported files
|
| 48 |
+
excel_files = glob.glob(os.path.join(data_dir, "*.xlsx")) + glob.glob(os.path.join(data_dir, "*.xls"))
|
| 49 |
+
csv_files = glob.glob(os.path.join(data_dir, "*.csv"))
|
| 50 |
+
all_files = excel_files + csv_files
|
| 51 |
+
|
| 52 |
+
if not all_files:
|
| 53 |
+
st.info("No Excel or CSV files found in the data folder.")
|
| 54 |
+
return
|
| 55 |
+
|
| 56 |
+
# File selection
|
| 57 |
+
st.subheader("📂 Select File to View")
|
| 58 |
+
|
| 59 |
+
file_options = {os.path.basename(f): f for f in all_files}
|
| 60 |
+
selected_file_name = st.selectbox("Choose a file", options=list(file_options.keys()))
|
| 61 |
+
|
| 62 |
+
if selected_file_name:
|
| 63 |
+
file_path = file_options[selected_file_name]
|
| 64 |
+
display_file_content(file_path, selected_file_name)
|
| 65 |
+
|
| 66 |
+
def show_file_uploader():
|
| 67 |
+
"""Upload and view new files"""
|
| 68 |
+
st.subheader("📤 Upload New File")
|
| 69 |
+
|
| 70 |
+
uploaded_file = st.file_uploader(
|
| 71 |
+
"Choose Excel or CSV file",
|
| 72 |
+
type=['xlsx', 'xls', 'csv'],
|
| 73 |
+
help="Upload Excel (.xlsx, .xls) or CSV files for viewing"
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
if uploaded_file:
|
| 77 |
+
# Save to data folder for processing
|
| 78 |
+
file_path = os.path.join("data", uploaded_file.name)
|
| 79 |
+
with open(file_path, "wb") as f:
|
| 80 |
+
f.write(uploaded_file.getbuffer())
|
| 81 |
+
|
| 82 |
+
display_file_content(file_path, uploaded_file.name)
|
| 83 |
+
|
| 84 |
+
def display_file_content(file_path, file_name):
|
| 85 |
+
"""Display file content with conversion options"""
|
| 86 |
+
try:
|
| 87 |
+
# File info
|
| 88 |
+
file_size = os.path.getsize(file_path) / 1024
|
| 89 |
+
file_mtime = datetime.fromtimestamp(os.path.getmtime(file_path))
|
| 90 |
+
|
| 91 |
+
col1, col2, col3 = st.columns(3)
|
| 92 |
+
with col1:
|
| 93 |
+
st.metric("File", file_name)
|
| 94 |
+
with col2:
|
| 95 |
+
st.metric("Size", f"{file_size:.1f} KB")
|
| 96 |
+
with col3:
|
| 97 |
+
st.metric("Modified", file_mtime.strftime('%Y-%m-%d %H:%M'))
|
| 98 |
+
|
| 99 |
+
# Conversion options
|
| 100 |
+
st.subheader("🔄 Conversion Options")
|
| 101 |
+
|
| 102 |
+
col1, col2 = st.columns(2)
|
| 103 |
+
with col1:
|
| 104 |
+
convert_gujarati = st.checkbox("Convert Gujarati to English", value=True)
|
| 105 |
+
with col2:
|
| 106 |
+
use_ai_translation = st.checkbox("Use AI Translation",
|
| 107 |
+
value=TRANSLATOR_AVAILABLE,
|
| 108 |
+
disabled=not TRANSLATOR_AVAILABLE)
|
| 109 |
+
|
| 110 |
+
# Read file
|
| 111 |
+
if file_path.endswith('.csv'):
|
| 112 |
+
df = read_csv_file(file_path)
|
| 113 |
+
else:
|
| 114 |
+
df = read_excel_file(file_path)
|
| 115 |
+
|
| 116 |
+
if df is None or df.empty:
|
| 117 |
+
st.warning("No data found in the file.")
|
| 118 |
+
return
|
| 119 |
+
|
| 120 |
+
# Show original data
|
| 121 |
+
st.subheader("📝 Original Data")
|
| 122 |
+
display_dataframe_info(df, "Original")
|
| 123 |
+
|
| 124 |
+
# Apply conversion if requested
|
| 125 |
+
if convert_gujarati:
|
| 126 |
+
with st.spinner("Converting Gujarati content..."):
|
| 127 |
+
df_converted = convert_gujarati_data_advanced(df, use_ai_translation)
|
| 128 |
+
|
| 129 |
+
st.subheader("🔤 Converted Data (Gujarati → English)")
|
| 130 |
+
display_dataframe_info(df_converted, "Converted")
|
| 131 |
+
|
| 132 |
+
# Show conversion summary
|
| 133 |
+
show_conversion_summary(df, df_converted)
|
| 134 |
+
|
| 135 |
+
# Data analysis tools
|
| 136 |
+
st.subheader("🔧 Data Analysis Tools")
|
| 137 |
+
show_data_analysis_tools(df_converted if convert_gujarati else df)
|
| 138 |
+
|
| 139 |
+
except Exception as e:
|
| 140 |
+
st.error(f"Error processing file: {str(e)}")
|
| 141 |
+
|
| 142 |
+
def show_conversion_summary(original_df, converted_df):
|
| 143 |
+
"""Show summary of Gujarati to English conversion"""
|
| 144 |
+
st.subheader("📊 Conversion Summary")
|
| 145 |
+
|
| 146 |
+
changes_detected = False
|
| 147 |
+
conversion_examples = []
|
| 148 |
+
|
| 149 |
+
# Check for changes
|
| 150 |
+
for i in range(min(3, len(original_df))):
|
| 151 |
+
for col in original_df.columns[:3]:
|
| 152 |
+
if i < len(original_df):
|
| 153 |
+
orig_val = str(original_df.iloc[i][col])
|
| 154 |
+
conv_val = str(converted_df.iloc[i][col])
|
| 155 |
+
if orig_val != conv_val and contains_gujarati(orig_val):
|
| 156 |
+
changes_detected = True
|
| 157 |
+
conversion_examples.append(f"`{orig_val}` → `{conv_val}`")
|
| 158 |
+
break
|
| 159 |
+
|
| 160 |
+
if changes_detected:
|
| 161 |
+
st.success("✅ Gujarati content was detected and converted to English")
|
| 162 |
+
if conversion_examples:
|
| 163 |
+
st.write("**Conversion Examples:**")
|
| 164 |
+
for example in conversion_examples:
|
| 165 |
+
st.write(example)
|
| 166 |
+
else:
|
| 167 |
+
st.info("ℹ️ No Gujarati content detected - data is already in English")
|
| 168 |
+
|
| 169 |
+
# Helper functions for file reading and conversion
|
| 170 |
+
def read_csv_file(file_path):
|
| 171 |
+
"""Read CSV file with automatic encoding detection"""
|
| 172 |
+
try:
|
| 173 |
+
# Try reading with different encodings
|
| 174 |
+
for enc in ['utf-8', 'latin-1', 'cp1252', 'iso-8859-1']:
|
| 175 |
+
try:
|
| 176 |
+
return pd.read_csv(file_path, encoding=enc)
|
| 177 |
+
except:
|
| 178 |
+
continue
|
| 179 |
+
|
| 180 |
+
# Last resort
|
| 181 |
+
return pd.read_csv(file_path, encoding='utf-8', errors='ignore')
|
| 182 |
+
except Exception as e:
|
| 183 |
+
st.error(f"Error reading CSV: {str(e)}")
|
| 184 |
+
return pd.DataFrame()
|
| 185 |
+
|
| 186 |
+
def read_excel_file(file_path):
|
| 187 |
+
"""Read Excel file with all sheets"""
|
| 188 |
+
try:
|
| 189 |
+
excel_file = pd.ExcelFile(file_path)
|
| 190 |
+
|
| 191 |
+
if len(excel_file.sheet_names) == 1:
|
| 192 |
+
return pd.read_excel(file_path)
|
| 193 |
+
else:
|
| 194 |
+
sheet_name = st.selectbox(
|
| 195 |
+
"Select Sheet to View",
|
| 196 |
+
options=excel_file.sheet_names,
|
| 197 |
+
key=f"sheet_select_{file_path}"
|
| 198 |
+
)
|
| 199 |
+
return pd.read_excel(file_path, sheet_name=sheet_name)
|
| 200 |
+
except Exception as e:
|
| 201 |
+
st.error(f"Error reading Excel file: {str(e)}")
|
| 202 |
+
return pd.DataFrame()
|
| 203 |
+
|
| 204 |
+
def convert_gujarati_data_advanced(df, use_ai_translation=False):
|
| 205 |
+
"""Convert Gujarati content to English using advanced methods"""
|
| 206 |
+
try:
|
| 207 |
+
df_converted = df.copy()
|
| 208 |
+
|
| 209 |
+
# Convert column names
|
| 210 |
+
df_converted.columns = [convert_gujarati_text(col, use_ai_translation) for col in df_converted.columns]
|
| 211 |
+
|
| 212 |
+
# Convert data in each column
|
| 213 |
+
for col in df_converted.columns:
|
| 214 |
+
df_converted[col] = df_converted[col].astype(str)
|
| 215 |
+
df_converted[col] = df_converted[col].apply(
|
| 216 |
+
lambda x: convert_gujarati_text(x, use_ai_translation)
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
return df_converted
|
| 220 |
+
except Exception as e:
|
| 221 |
+
st.warning(f"Conversion issues: {str(e)}")
|
| 222 |
+
return df
|
| 223 |
+
|
| 224 |
+
def convert_gujarati_text(text, use_ai_translation=False):
|
| 225 |
+
"""Convert Gujarati text to English using multiple methods"""
|
| 226 |
+
if not isinstance(text, str) or not text.strip():
|
| 227 |
+
return text
|
| 228 |
+
|
| 229 |
+
# Step 1: Always convert Gujarati numbers
|
| 230 |
+
text = gujarati_to_english_digits(text)
|
| 231 |
+
|
| 232 |
+
# Step 2: Check if text contains Gujarati characters
|
| 233 |
+
if contains_gujarati(text):
|
| 234 |
+
if use_ai_translation and TRANSLATOR_AVAILABLE:
|
| 235 |
+
try:
|
| 236 |
+
return GoogleTranslator(source='gu', target='en').translate(text)
|
| 237 |
+
except Exception as e:
|
| 238 |
+
st.warning(f"Translation failed for '{text}': {str(e)}")
|
| 239 |
+
return apply_basic_gujarati_conversion(text)
|
| 240 |
+
else:
|
| 241 |
+
return apply_basic_gujarati_conversion(text)
|
| 242 |
+
else:
|
| 243 |
+
return text
|
| 244 |
+
|
| 245 |
+
def gujarati_to_english_digits(text):
|
| 246 |
+
"""Convert Gujarati numbers to English digits"""
|
| 247 |
+
gujarati_to_english_numbers = {
|
| 248 |
+
'૦': '0', '૧': '1', '૨': '2', '૩': '3', '૪': '4',
|
| 249 |
+
'૫': '5', '૬': '6', '૭': '7', '૮': '8', '૯': '9'
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
converted_text = text
|
| 253 |
+
for guj, eng in gujarati_to_english_numbers.items():
|
| 254 |
+
converted_text = converted_text.replace(guj, eng)
|
| 255 |
+
|
| 256 |
+
return converted_text
|
| 257 |
+
|
| 258 |
+
def contains_gujarati(text):
|
| 259 |
+
"""Check if text contains Gujarati characters"""
|
| 260 |
+
gujarati_pattern = re.compile(r'[\u0A80-\u0AFF]')
|
| 261 |
+
return bool(gujarati_pattern.search(text))
|
| 262 |
+
|
| 263 |
+
def apply_basic_gujarati_conversion(text):
|
| 264 |
+
"""Apply basic Gujarati to English conversion for common words"""
|
| 265 |
+
gujarati_to_english_words = {
|
| 266 |
+
'ગ્રાહક': 'Customer', 'નામ': 'Name', 'મોબાઈલ': 'Mobile', 'ફોન': 'Phone',
|
| 267 |
+
'ગામ': 'Village', 'તાલુકો': 'Taluka', 'જિલ્લો': 'District', 'શહેર': 'City',
|
| 268 |
+
'બીલ': 'Bill', 'ચલણ': 'Invoice', 'રકમ': 'Amount', 'પ્રમાણ': 'Quantity',
|
| 269 |
+
'ઉત્પાદન': 'Product', 'તારીખ': 'Date', 'ચુકવણી': 'Payment'
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
converted_text = text
|
| 273 |
+
for guj, eng in gujarati_to_english_words.items():
|
| 274 |
+
converted_text = converted_text.replace(guj, eng)
|
| 275 |
+
|
| 276 |
+
return converted_text
|
| 277 |
+
|
| 278 |
+
def display_dataframe_info(df, title):
|
| 279 |
+
"""Display dataframe with comprehensive information"""
|
| 280 |
+
st.write(f"**{title} Data Summary**")
|
| 281 |
+
|
| 282 |
+
# Basic info
|
| 283 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 284 |
+
with col1:
|
| 285 |
+
st.metric("Rows", len(df))
|
| 286 |
+
with col2:
|
| 287 |
+
st.metric("Columns", len(df.columns))
|
| 288 |
+
with col3:
|
| 289 |
+
non_empty = len(df.dropna(how='all'))
|
| 290 |
+
st.metric("Non-empty Rows", non_empty)
|
| 291 |
+
with col4:
|
| 292 |
+
empty_cells = df.isna().sum().sum()
|
| 293 |
+
st.metric("Empty Cells", empty_cells)
|
| 294 |
+
|
| 295 |
+
# Column information
|
| 296 |
+
st.subheader("📋 Column Details")
|
| 297 |
+
col_info = []
|
| 298 |
+
for col in df.columns:
|
| 299 |
+
col_info.append({
|
| 300 |
+
'Column Name': col,
|
| 301 |
+
'Data Type': str(df[col].dtype),
|
| 302 |
+
'Non-Null Count': df[col].count(),
|
| 303 |
+
'Null Count': df[col].isna().sum(),
|
| 304 |
+
'Unique Values': df[col].nunique()
|
| 305 |
+
})
|
| 306 |
+
|
| 307 |
+
col_info_df = pd.DataFrame(col_info)
|
| 308 |
+
st.dataframe(col_info_df, use_container_width=True)
|
| 309 |
+
|
| 310 |
+
# Data preview
|
| 311 |
+
st.subheader("👀 Data Preview")
|
| 312 |
+
show_rows = st.slider("Number of rows to show", 5, 100, 10, key=f"rows_{title}")
|
| 313 |
+
st.dataframe(df.head(show_rows), use_container_width=True)
|
| 314 |
+
|
| 315 |
+
def show_data_analysis_tools(df):
|
| 316 |
+
"""Show basic data analysis tools"""
|
| 317 |
+
tab1, tab2, tab3 = st.tabs(["📈 Basic Stats", "🔍 Search & Filter", "💾 Export"])
|
| 318 |
+
|
| 319 |
+
with tab1:
|
| 320 |
+
show_basic_stats(df)
|
| 321 |
+
|
| 322 |
+
with tab2:
|
| 323 |
+
show_search_filter(df)
|
| 324 |
+
|
| 325 |
+
with tab3:
|
| 326 |
+
show_export_options(df)
|
| 327 |
+
|
| 328 |
+
def show_basic_stats(df):
|
| 329 |
+
"""Show basic statistical analysis"""
|
| 330 |
+
st.write("**Numerical Columns Statistics**")
|
| 331 |
+
|
| 332 |
+
numerical_cols = df.select_dtypes(include=['number']).columns
|
| 333 |
+
if len(numerical_cols) > 0:
|
| 334 |
+
st.dataframe(df[numerical_cols].describe(), use_container_width=True)
|
| 335 |
+
else:
|
| 336 |
+
st.info("No numerical columns found for statistical analysis")
|
| 337 |
+
|
| 338 |
+
def show_search_filter(df):
|
| 339 |
+
"""Show search and filter options"""
|
| 340 |
+
st.write("**Search in Data**")
|
| 341 |
+
|
| 342 |
+
search_term = st.text_input("Search term")
|
| 343 |
+
if search_term:
|
| 344 |
+
# Search across all string columns
|
| 345 |
+
mask = pd.Series([False] * len(df))
|
| 346 |
+
for col in df.columns:
|
| 347 |
+
if df[col].dtype == 'object':
|
| 348 |
+
mask = mask | df[col].astype(str).str.contains(search_term, case=False, na=False)
|
| 349 |
+
|
| 350 |
+
filtered_df = df[mask]
|
| 351 |
+
st.write(f"Found {len(filtered_df)} matching rows")
|
| 352 |
+
st.dataframe(filtered_df.head(20), use_container_width=True)
|
| 353 |
+
|
| 354 |
+
def show_export_options(df):
|
| 355 |
+
"""Show data export options"""
|
| 356 |
+
st.write("**Export Processed Data**")
|
| 357 |
+
|
| 358 |
+
col1, col2 = st.columns(2)
|
| 359 |
+
|
| 360 |
+
with col1:
|
| 361 |
+
csv = df.to_csv(index=False)
|
| 362 |
+
st.download_button(
|
| 363 |
+
label="📥 Download as CSV",
|
| 364 |
+
data=csv,
|
| 365 |
+
file_name="converted_data.csv",
|
| 366 |
+
mime="text/csv"
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
with col2:
|
| 370 |
+
# For Excel, we need to save to a file first
|
| 371 |
+
excel_file = "converted_data.xlsx"
|
| 372 |
+
df.to_excel(excel_file, index=False)
|
| 373 |
+
with open(excel_file, "rb") as f:
|
| 374 |
+
st.download_button(
|
| 375 |
+
label="📊 Download as Excel",
|
| 376 |
+
data=f,
|
| 377 |
+
file_name=excel_file,
|
| 378 |
+
mime="application/vnd.ms-excel"
|
| 379 |
+
)
|
| 380 |
+
# Clean up
|
| 381 |
+
if os.path.exists(excel_file):
|
| 382 |
+
os.remove(excel_file)
|
pages/payments.py
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# pages/payments.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
|
| 7 |
+
def show_payments_page(db, whatsapp_manager=None):
|
| 8 |
+
"""Show payments management and tracking page"""
|
| 9 |
+
st.title("💳 Payments Management")
|
| 10 |
+
|
| 11 |
+
if not db:
|
| 12 |
+
st.error("Database not available. Please check initialization.")
|
| 13 |
+
return
|
| 14 |
+
|
| 15 |
+
# Tabs for different payment functions
|
| 16 |
+
tab1, tab2, tab3, tab4 = st.tabs(["💰 Record Payment", "📋 Payment History", "⏳ Pending Payments", "📊 Payment Analytics"])
|
| 17 |
+
|
| 18 |
+
with tab1:
|
| 19 |
+
show_record_payment_tab(db, whatsapp_manager)
|
| 20 |
+
|
| 21 |
+
with tab2:
|
| 22 |
+
show_payment_history_tab(db)
|
| 23 |
+
|
| 24 |
+
with tab3:
|
| 25 |
+
show_pending_payments_tab(db, whatsapp_manager)
|
| 26 |
+
|
| 27 |
+
with tab4:
|
| 28 |
+
show_payment_analytics_tab(db)
|
| 29 |
+
|
| 30 |
+
def show_record_payment_tab(db, whatsapp_manager):
|
| 31 |
+
"""Show form to record new payments"""
|
| 32 |
+
st.subheader("💰 Record New Payment")
|
| 33 |
+
|
| 34 |
+
with st.form("record_payment_form"):
|
| 35 |
+
st.markdown("### 📄 Payment Information")
|
| 36 |
+
|
| 37 |
+
col1, col2 = st.columns(2)
|
| 38 |
+
|
| 39 |
+
with col1:
|
| 40 |
+
# Get pending sales for selection
|
| 41 |
+
pending_sales = get_pending_sales(db)
|
| 42 |
+
if not pending_sales.empty:
|
| 43 |
+
sale_options = {f"{row['invoice_no']} - {row['customer_name']} (₹{row['pending_amount']:,.2f})": row['sale_id']
|
| 44 |
+
for _, row in pending_sales.iterrows()}
|
| 45 |
+
selected_sale = st.selectbox("Select Sale*", options=list(sale_options.keys()))
|
| 46 |
+
sale_id = sale_options[selected_sale] if selected_sale else None
|
| 47 |
+
|
| 48 |
+
# Show sale details
|
| 49 |
+
if sale_id:
|
| 50 |
+
sale_details = pending_sales[pending_sales['sale_id'] == sale_id].iloc[0]
|
| 51 |
+
st.info(f"**Sale Details:** {sale_details['customer_name']} - Pending: ₹{sale_details['pending_amount']:,.2f}")
|
| 52 |
+
else:
|
| 53 |
+
st.warning("No pending sales found. All sales are fully paid!")
|
| 54 |
+
sale_id = None
|
| 55 |
+
|
| 56 |
+
with col2:
|
| 57 |
+
payment_date = st.date_input("Payment Date*", datetime.now())
|
| 58 |
+
payment_method = st.selectbox("Payment Method*",
|
| 59 |
+
["Cash", "G-Pay", "PhonePe", "Bank Transfer", "Cheque", "Other"])
|
| 60 |
+
payment_status = st.selectbox("Payment Status", ["Completed", "Pending", "Failed"])
|
| 61 |
+
|
| 62 |
+
st.markdown("### 💵 Payment Amount")
|
| 63 |
+
|
| 64 |
+
col1, col2 = st.columns(2)
|
| 65 |
+
|
| 66 |
+
with col1:
|
| 67 |
+
if sale_id:
|
| 68 |
+
sale_data = pending_sales[pending_sales['sale_id'] == sale_id].iloc[0]
|
| 69 |
+
max_amount = sale_data['pending_amount']
|
| 70 |
+
payment_amount = st.number_input("Payment Amount*", min_value=0.0, max_value=float(max_amount),
|
| 71 |
+
value=float(max_amount), step=100.0)
|
| 72 |
+
st.write(f"Pending Amount: ₹{max_amount:,.2f}")
|
| 73 |
+
else:
|
| 74 |
+
payment_amount = st.number_input("Payment Amount*", min_value=0.0, value=0.0, step=100.0)
|
| 75 |
+
|
| 76 |
+
with col2:
|
| 77 |
+
reference_number = st.text_input("Reference Number",
|
| 78 |
+
placeholder="UPI ID, Cheque No, Transaction ID, etc.")
|
| 79 |
+
rrn_number = st.text_input("RRN Number", placeholder="For bank transfers")
|
| 80 |
+
|
| 81 |
+
notes = st.text_area("Payment Notes", placeholder="Any additional notes about this payment...")
|
| 82 |
+
|
| 83 |
+
# Submit button
|
| 84 |
+
submitted = st.form_submit_button("💳 Record Payment", type="primary")
|
| 85 |
+
|
| 86 |
+
if submitted:
|
| 87 |
+
# Validation
|
| 88 |
+
errors = []
|
| 89 |
+
if not sale_id:
|
| 90 |
+
errors.append("Sale selection is required")
|
| 91 |
+
if not payment_amount or payment_amount <= 0:
|
| 92 |
+
errors.append("Valid payment amount is required")
|
| 93 |
+
if not payment_date:
|
| 94 |
+
errors.append("Payment date is required")
|
| 95 |
+
if not payment_method:
|
| 96 |
+
errors.append("Payment method is required")
|
| 97 |
+
|
| 98 |
+
if errors:
|
| 99 |
+
for error in errors:
|
| 100 |
+
st.error(f"❌ {error}")
|
| 101 |
+
else:
|
| 102 |
+
try:
|
| 103 |
+
# Record payment in database
|
| 104 |
+
payment_id = add_payment_to_database(db, {
|
| 105 |
+
'sale_id': sale_id,
|
| 106 |
+
'payment_date': payment_date,
|
| 107 |
+
'payment_method': payment_method,
|
| 108 |
+
'amount': payment_amount,
|
| 109 |
+
'rrn': rrn_number,
|
| 110 |
+
'reference': reference_number,
|
| 111 |
+
'status': payment_status,
|
| 112 |
+
'notes': notes
|
| 113 |
+
})
|
| 114 |
+
|
| 115 |
+
if payment_id and payment_id > 0:
|
| 116 |
+
st.success(f"✅ Payment recorded successfully! Payment ID: {payment_id}")
|
| 117 |
+
|
| 118 |
+
# Update sale payment status
|
| 119 |
+
update_sale_payment_status(db, sale_id)
|
| 120 |
+
|
| 121 |
+
# Send notification if WhatsApp available
|
| 122 |
+
if whatsapp_manager and sale_id:
|
| 123 |
+
send_payment_notification(whatsapp_manager, db, sale_id, payment_amount)
|
| 124 |
+
|
| 125 |
+
# Show payment summary
|
| 126 |
+
show_payment_summary(db, payment_id)
|
| 127 |
+
|
| 128 |
+
else:
|
| 129 |
+
st.error("❌ Failed to record payment. Please try again.")
|
| 130 |
+
|
| 131 |
+
except Exception as e:
|
| 132 |
+
st.error(f"❌ Error recording payment: {e}")
|
| 133 |
+
|
| 134 |
+
def get_pending_sales(db):
|
| 135 |
+
"""Get sales with pending payments"""
|
| 136 |
+
try:
|
| 137 |
+
return db.get_dataframe('sales', '''
|
| 138 |
+
SELECT s.sale_id, s.invoice_no, s.total_amount, s.payment_status,
|
| 139 |
+
c.name as customer_name, c.mobile, c.village,
|
| 140 |
+
(s.total_amount - COALESCE(SUM(p.amount), 0)) as pending_amount
|
| 141 |
+
FROM sales s
|
| 142 |
+
LEFT JOIN customers c ON s.customer_id = c.customer_id
|
| 143 |
+
LEFT JOIN payments p ON s.sale_id = p.sale_id
|
| 144 |
+
WHERE s.payment_status IN ('Pending', 'Partial')
|
| 145 |
+
GROUP BY s.sale_id
|
| 146 |
+
HAVING pending_amount > 0
|
| 147 |
+
ORDER BY s.sale_date DESC
|
| 148 |
+
''')
|
| 149 |
+
except Exception as e:
|
| 150 |
+
st.error(f"Error getting pending sales: {e}")
|
| 151 |
+
return pd.DataFrame()
|
| 152 |
+
|
| 153 |
+
def add_payment_to_database(db, payment_data):
|
| 154 |
+
"""Add payment record to database"""
|
| 155 |
+
try:
|
| 156 |
+
db.execute_query('''
|
| 157 |
+
INSERT INTO payments (sale_id, payment_date, payment_method, amount, rrn, reference, status, notes)
|
| 158 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
| 159 |
+
''', (
|
| 160 |
+
payment_data['sale_id'],
|
| 161 |
+
payment_data['payment_date'],
|
| 162 |
+
payment_data['payment_method'],
|
| 163 |
+
payment_data['amount'],
|
| 164 |
+
payment_data['rrn'],
|
| 165 |
+
payment_data['reference'],
|
| 166 |
+
payment_data['status'],
|
| 167 |
+
payment_data['notes']
|
| 168 |
+
), log_action=False)
|
| 169 |
+
|
| 170 |
+
# Get the inserted payment_id
|
| 171 |
+
result = db.execute_query('SELECT last_insert_rowid()', log_action=False)
|
| 172 |
+
return result[0][0] if result else -1
|
| 173 |
+
|
| 174 |
+
except Exception as e:
|
| 175 |
+
st.error(f"Database error: {e}")
|
| 176 |
+
return -1
|
| 177 |
+
|
| 178 |
+
def update_sale_payment_status(db, sale_id):
|
| 179 |
+
"""Update sale payment status based on payments"""
|
| 180 |
+
try:
|
| 181 |
+
# Get total paid amount
|
| 182 |
+
payments_data = db.get_dataframe('payments', f'''
|
| 183 |
+
SELECT COALESCE(SUM(amount), 0) as total_paid
|
| 184 |
+
FROM payments
|
| 185 |
+
WHERE sale_id = {sale_id} AND status = 'Completed'
|
| 186 |
+
''')
|
| 187 |
+
|
| 188 |
+
if not payments_data.empty:
|
| 189 |
+
total_paid = payments_data.iloc[0]['total_paid']
|
| 190 |
+
|
| 191 |
+
# Get sale total
|
| 192 |
+
sale_data = db.get_dataframe('sales', f'SELECT total_amount FROM sales WHERE sale_id = {sale_id}')
|
| 193 |
+
if not sale_data.empty:
|
| 194 |
+
sale_total = sale_data.iloc[0]['total_amount']
|
| 195 |
+
|
| 196 |
+
# Determine payment status
|
| 197 |
+
if total_paid >= sale_total:
|
| 198 |
+
new_status = 'Paid'
|
| 199 |
+
elif total_paid > 0:
|
| 200 |
+
new_status = 'Partial'
|
| 201 |
+
else:
|
| 202 |
+
new_status = 'Pending'
|
| 203 |
+
|
| 204 |
+
# Update sale status
|
| 205 |
+
db.execute_query('''
|
| 206 |
+
UPDATE sales SET payment_status = ?, updated_date = CURRENT_TIMESTAMP
|
| 207 |
+
WHERE sale_id = ?
|
| 208 |
+
''', (new_status, sale_id), log_action=False)
|
| 209 |
+
|
| 210 |
+
except Exception as e:
|
| 211 |
+
st.error(f"Error updating payment status: {e}")
|
| 212 |
+
|
| 213 |
+
def send_payment_notification(whatsapp_manager, db, sale_id, payment_amount):
|
| 214 |
+
"""Send payment confirmation to customer"""
|
| 215 |
+
try:
|
| 216 |
+
# Get sale and customer details
|
| 217 |
+
sale_data = db.get_dataframe('sales', f'''
|
| 218 |
+
SELECT s.*, c.name as customer_name, c.mobile
|
| 219 |
+
FROM sales s
|
| 220 |
+
LEFT JOIN customers c ON s.customer_id = c.customer_id
|
| 221 |
+
WHERE s.sale_id = {sale_id}
|
| 222 |
+
''')
|
| 223 |
+
|
| 224 |
+
if not sale_data.empty:
|
| 225 |
+
sale = sale_data.iloc[0]
|
| 226 |
+
|
| 227 |
+
if sale.get('mobile'):
|
| 228 |
+
message = f"""Hello {sale['customer_name']}! 💰
|
| 229 |
+
|
| 230 |
+
We have received your payment of ₹{payment_amount:,.2f} for invoice {sale['invoice_no']}.
|
| 231 |
+
|
| 232 |
+
Thank you for your prompt payment!
|
| 233 |
+
|
| 234 |
+
If you have any questions, please feel free to contact us.
|
| 235 |
+
|
| 236 |
+
Best regards,
|
| 237 |
+
Sales Team"""
|
| 238 |
+
|
| 239 |
+
success = whatsapp_manager.send_message(sale['mobile'], message)
|
| 240 |
+
if success:
|
| 241 |
+
st.success("📱 Payment confirmation sent to customer!")
|
| 242 |
+
else:
|
| 243 |
+
st.warning("⚠️ Could not send payment confirmation")
|
| 244 |
+
|
| 245 |
+
except Exception as e:
|
| 246 |
+
st.warning(f"Could not send payment notification: {e}")
|
| 247 |
+
|
| 248 |
+
def show_payment_summary(db, payment_id):
|
| 249 |
+
"""Show summary of recorded payment"""
|
| 250 |
+
try:
|
| 251 |
+
payment_data = db.get_dataframe('payments', f'''
|
| 252 |
+
SELECT p.*, s.invoice_no, s.total_amount, c.name as customer_name, c.village
|
| 253 |
+
FROM payments p
|
| 254 |
+
LEFT JOIN sales s ON p.sale_id = s.sale_id
|
| 255 |
+
LEFT JOIN customers c ON s.customer_id = c.customer_id
|
| 256 |
+
WHERE p.payment_id = {payment_id}
|
| 257 |
+
''')
|
| 258 |
+
|
| 259 |
+
if not payment_data.empty:
|
| 260 |
+
payment = payment_data.iloc[0]
|
| 261 |
+
|
| 262 |
+
st.markdown("## 🎉 Payment Recorded Successfully!")
|
| 263 |
+
|
| 264 |
+
col1, col2 = st.columns(2)
|
| 265 |
+
|
| 266 |
+
with col1:
|
| 267 |
+
st.subheader("💳 Payment Details")
|
| 268 |
+
st.write(f"**Payment ID:** {payment_id}")
|
| 269 |
+
st.write(f"**Invoice No:** {payment['invoice_no']}")
|
| 270 |
+
st.write(f"**Customer:** {payment['customer_name']}")
|
| 271 |
+
st.write(f"**Village:** {payment['village']}")
|
| 272 |
+
st.write(f"**Payment Method:** {payment['payment_method']}")
|
| 273 |
+
|
| 274 |
+
with col2:
|
| 275 |
+
st.subheader("💰 Amount & Status")
|
| 276 |
+
st.write(f"**Amount Paid:** ₹{payment['amount']:,.2f}")
|
| 277 |
+
st.write(f"**Sale Total:** ₹{payment['total_amount']:,.2f}")
|
| 278 |
+
st.write(f"**Payment Date:** {payment['payment_date']}")
|
| 279 |
+
st.write(f"**Status:** {payment['status']}")
|
| 280 |
+
|
| 281 |
+
if payment['reference']:
|
| 282 |
+
st.write(f"**Reference:** {payment['reference']}")
|
| 283 |
+
|
| 284 |
+
# Quick actions
|
| 285 |
+
st.markdown("### ⚡ Quick Actions")
|
| 286 |
+
col1, col2, col3 = st.columns(3)
|
| 287 |
+
|
| 288 |
+
with col1:
|
| 289 |
+
if st.button("📋 View Payment History"):
|
| 290 |
+
st.session_state.current_tab = "📋 Payment History"
|
| 291 |
+
|
| 292 |
+
with col2:
|
| 293 |
+
if st.button("💰 Record Another"):
|
| 294 |
+
st.rerun()
|
| 295 |
+
|
| 296 |
+
with col3:
|
| 297 |
+
if st.button("⏳ View Pending"):
|
| 298 |
+
st.session_state.current_tab = "⏳ Pending Payments"
|
| 299 |
+
|
| 300 |
+
except Exception as e:
|
| 301 |
+
st.error(f"Error displaying payment summary: {e}")
|
| 302 |
+
|
| 303 |
+
def show_payment_history_tab(db):
|
| 304 |
+
"""Show payment history and records"""
|
| 305 |
+
st.subheader("📋 Payment History")
|
| 306 |
+
|
| 307 |
+
try:
|
| 308 |
+
# Date range filter
|
| 309 |
+
col1, col2 = st.columns(2)
|
| 310 |
+
with col1:
|
| 311 |
+
start_date = st.date_input("Start Date", datetime.now() - timedelta(days=30))
|
| 312 |
+
with col2:
|
| 313 |
+
end_date = st.date_input("End Date", datetime.now())
|
| 314 |
+
|
| 315 |
+
# Status filter
|
| 316 |
+
status_filter = st.multiselect("Filter by Status",
|
| 317 |
+
["Completed", "Pending", "Failed"],
|
| 318 |
+
default=["Completed"])
|
| 319 |
+
|
| 320 |
+
# Method filter
|
| 321 |
+
methods = db.get_dataframe('payments', "SELECT DISTINCT payment_method FROM payments")
|
| 322 |
+
if not methods.empty:
|
| 323 |
+
method_options = methods['payment_method'].dropna().unique().tolist()
|
| 324 |
+
method_filter = st.multiselect("Filter by Method", method_options, default=method_options)
|
| 325 |
+
|
| 326 |
+
# Get payments data
|
| 327 |
+
payments_data = get_payments_data(db, start_date, end_date, status_filter, method_filter)
|
| 328 |
+
|
| 329 |
+
if not payments_data.empty:
|
| 330 |
+
st.write(f"**💰 Showing {len(payments_data)} payments**")
|
| 331 |
+
|
| 332 |
+
# Display payments
|
| 333 |
+
display_data = payments_data[['payment_date', 'customer_name', 'invoice_no', 'amount',
|
| 334 |
+
'payment_method', 'status', 'reference']].copy()
|
| 335 |
+
display_data.columns = ['Date', 'Customer', 'Invoice', 'Amount', 'Method', 'Status', 'Reference']
|
| 336 |
+
display_data['Amount'] = display_data['Amount'].apply(lambda x: f"₹{x:,.2f}")
|
| 337 |
+
display_data = display_data.sort_values('Date', ascending=False)
|
| 338 |
+
|
| 339 |
+
st.dataframe(display_data, use_container_width=True)
|
| 340 |
+
|
| 341 |
+
# Payment statistics
|
| 342 |
+
st.subheader("📊 Payment Statistics")
|
| 343 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 344 |
+
|
| 345 |
+
with col1:
|
| 346 |
+
total_payments = len(payments_data)
|
| 347 |
+
st.metric("Total Payments", total_payments)
|
| 348 |
+
|
| 349 |
+
with col2:
|
| 350 |
+
total_amount = payments_data['amount'].sum()
|
| 351 |
+
st.metric("Total Amount", f"₹{total_amount:,.2f}")
|
| 352 |
+
|
| 353 |
+
with col3:
|
| 354 |
+
completed = len(payments_data[payments_data['status'] == 'Completed'])
|
| 355 |
+
st.metric("Completed", completed)
|
| 356 |
+
|
| 357 |
+
with col4:
|
| 358 |
+
avg_payment = payments_data['amount'].mean()
|
| 359 |
+
st.metric("Avg Payment", f"₹{avg_payment:,.0f}")
|
| 360 |
+
|
| 361 |
+
else:
|
| 362 |
+
st.info("No payments found for the selected criteria.")
|
| 363 |
+
|
| 364 |
+
except Exception as e:
|
| 365 |
+
st.error(f"Error loading payment history: {e}")
|
| 366 |
+
|
| 367 |
+
def get_payments_data(db, start_date, end_date, status_filter, method_filter):
|
| 368 |
+
"""Get payments data with filters"""
|
| 369 |
+
try:
|
| 370 |
+
query = '''
|
| 371 |
+
SELECT p.*, s.invoice_no, c.name as customer_name, c.village
|
| 372 |
+
FROM payments p
|
| 373 |
+
LEFT JOIN sales s ON p.sale_id = s.sale_id
|
| 374 |
+
LEFT JOIN customers c ON s.customer_id = c.customer_id
|
| 375 |
+
WHERE p.payment_date BETWEEN ? AND ?
|
| 376 |
+
'''
|
| 377 |
+
|
| 378 |
+
params = [start_date, end_date]
|
| 379 |
+
|
| 380 |
+
if status_filter:
|
| 381 |
+
placeholders = ','.join(['?' for _ in status_filter])
|
| 382 |
+
query += f' AND p.status IN ({placeholders})'
|
| 383 |
+
params.extend(status_filter)
|
| 384 |
+
|
| 385 |
+
if method_filter:
|
| 386 |
+
placeholders = ','.join(['?' for _ in method_filter])
|
| 387 |
+
query += f' AND p.payment_method IN ({placeholders})'
|
| 388 |
+
params.extend(method_filter)
|
| 389 |
+
|
| 390 |
+
query += ' ORDER BY p.payment_date DESC'
|
| 391 |
+
|
| 392 |
+
return db.get_dataframe('payments', query, params=params)
|
| 393 |
+
|
| 394 |
+
except Exception as e:
|
| 395 |
+
st.error(f"Error getting payments data: {e}")
|
| 396 |
+
return pd.DataFrame()
|
| 397 |
+
|
| 398 |
+
def show_pending_payments_tab(db, whatsapp_manager):
|
| 399 |
+
"""Show pending payments and reminders"""
|
| 400 |
+
st.subheader("⏳ Pending Payments")
|
| 401 |
+
|
| 402 |
+
try:
|
| 403 |
+
# Get pending payments
|
| 404 |
+
pending_payments = get_pending_sales(db)
|
| 405 |
+
|
| 406 |
+
if not pending_payments.empty:
|
| 407 |
+
st.warning(f"🚨 {len(pending_payments)} Sales with Pending Payments!")
|
| 408 |
+
|
| 409 |
+
# Display pending payments
|
| 410 |
+
display_data = pending_payments[['invoice_no', 'customer_name', 'village', 'total_amount', 'pending_amount', 'payment_status']].copy()
|
| 411 |
+
display_data.columns = ['Invoice', 'Customer', 'Village', 'Total Amount', 'Pending Amount', 'Status']
|
| 412 |
+
display_data['Total Amount'] = display_data['Total Amount'].apply(lambda x: f"₹{x:,.2f}")
|
| 413 |
+
display_data['Pending Amount'] = display_data['Pending Amount'].apply(lambda x: f"₹{x:,.2f}")
|
| 414 |
+
|
| 415 |
+
st.dataframe(display_data, use_container_width=True)
|
| 416 |
+
|
| 417 |
+
# Total pending amount
|
| 418 |
+
total_pending = pending_payments['pending_amount'].sum()
|
| 419 |
+
st.error(f"**💰 Total Pending Amount: ₹{total_pending:,.2f}**")
|
| 420 |
+
|
| 421 |
+
# Send reminders
|
| 422 |
+
st.subheader("📱 Send Payment Reminders")
|
| 423 |
+
selected_invoices = st.multiselect("Select Invoices for Reminders",
|
| 424 |
+
pending_payments['invoice_no'].tolist())
|
| 425 |
+
|
| 426 |
+
if selected_invoices and whatsapp_manager:
|
| 427 |
+
if st.button("📧 Send WhatsApp Reminders"):
|
| 428 |
+
send_bulk_payment_reminders(whatsapp_manager, db, pending_payments, selected_invoices)
|
| 429 |
+
st.success("✅ Payment reminders sent!")
|
| 430 |
+
|
| 431 |
+
elif not whatsapp_manager:
|
| 432 |
+
st.info("📱 WhatsApp manager not available for sending reminders")
|
| 433 |
+
|
| 434 |
+
else:
|
| 435 |
+
st.success("🎉 All payments are cleared! No pending payments.")
|
| 436 |
+
|
| 437 |
+
except Exception as e:
|
| 438 |
+
st.error(f"Error loading pending payments: {e}")
|
| 439 |
+
|
| 440 |
+
def send_bulk_payment_reminders(whatsapp_manager, db, pending_payments, selected_invoices):
|
| 441 |
+
"""Send bulk payment reminders"""
|
| 442 |
+
try:
|
| 443 |
+
selected_sales = pending_payments[pending_payments['invoice_no'].isin(selected_invoices)]
|
| 444 |
+
|
| 445 |
+
for _, sale in selected_sales.iterrows():
|
| 446 |
+
if sale.get('mobile'):
|
| 447 |
+
message = f"""Hello {sale['customer_name']}! ⏰
|
| 448 |
+
|
| 449 |
+
Friendly reminder regarding your pending payment.
|
| 450 |
+
|
| 451 |
+
Invoice: {sale['invoice_no']}
|
| 452 |
+
Pending Amount: ₹{sale['pending_amount']:,.2f}
|
| 453 |
+
|
| 454 |
+
Please make the payment at your earliest convenience.
|
| 455 |
+
|
| 456 |
+
Thank you for your cooperation!
|
| 457 |
+
|
| 458 |
+
Best regards,
|
| 459 |
+
Sales Team"""
|
| 460 |
+
|
| 461 |
+
whatsapp_manager.send_message(sale['mobile'], message)
|
| 462 |
+
|
| 463 |
+
except Exception as e:
|
| 464 |
+
st.error(f"Error sending reminders: {e}")
|
| 465 |
+
|
| 466 |
+
def show_payment_analytics_tab(db):
|
| 467 |
+
"""Show payment analytics and trends"""
|
| 468 |
+
st.subheader("📊 Payment Analytics")
|
| 469 |
+
|
| 470 |
+
try:
|
| 471 |
+
# Get payments data for analytics
|
| 472 |
+
payments_data = db.get_dataframe('payments', '''
|
| 473 |
+
SELECT p.*, s.invoice_no, c.name as customer_name
|
| 474 |
+
FROM payments p
|
| 475 |
+
LEFT JOIN sales s ON p.sale_id = s.sale_id
|
| 476 |
+
LEFT JOIN customers c ON s.customer_id = c.customer_id
|
| 477 |
+
WHERE p.status = 'Completed'
|
| 478 |
+
ORDER BY p.payment_date DESC
|
| 479 |
+
''')
|
| 480 |
+
|
| 481 |
+
if not payments_data.empty:
|
| 482 |
+
# Payment method distribution
|
| 483 |
+
st.subheader("💳 Payment Methods Distribution")
|
| 484 |
+
method_stats = payments_data['payment_method'].value_counts()
|
| 485 |
+
|
| 486 |
+
if not method_stats.empty:
|
| 487 |
+
fig = px.pie(values=method_stats.values, names=method_stats.index,
|
| 488 |
+
title='Payment Methods Distribution')
|
| 489 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 490 |
+
|
| 491 |
+
# Monthly payment trend
|
| 492 |
+
st.subheader("📈 Monthly Payment Trend")
|
| 493 |
+
try:
|
| 494 |
+
payments_data['payment_date'] = pd.to_datetime(payments_data['payment_date'])
|
| 495 |
+
monthly_payments = payments_data.groupby(payments_data['payment_date'].dt.to_period('M')).agg({
|
| 496 |
+
'amount': 'sum',
|
| 497 |
+
'payment_id': 'count'
|
| 498 |
+
}).reset_index()
|
| 499 |
+
monthly_payments['payment_date'] = monthly_payments['payment_date'].astype(str)
|
| 500 |
+
|
| 501 |
+
if not monthly_payments.empty:
|
| 502 |
+
fig = px.line(monthly_payments, x='payment_date', y='amount',
|
| 503 |
+
title='Monthly Payment Amount Trend',
|
| 504 |
+
labels={'payment_date': 'Month', 'amount': 'Amount (₹)'})
|
| 505 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 506 |
+
except:
|
| 507 |
+
st.info("Could not generate monthly trend chart")
|
| 508 |
+
|
| 509 |
+
# Top customers by payments
|
| 510 |
+
st.subheader("🏆 Top Customers by Payments")
|
| 511 |
+
customer_stats = payments_data.groupby('customer_name').agg({
|
| 512 |
+
'amount': 'sum',
|
| 513 |
+
'payment_id': 'count'
|
| 514 |
+
}).reset_index()
|
| 515 |
+
customer_stats.columns = ['Customer', 'Total Paid', 'Payment Count']
|
| 516 |
+
customer_stats = customer_stats.sort_values('Total Paid', ascending=False).head(10)
|
| 517 |
+
|
| 518 |
+
st.dataframe(customer_stats, use_container_width=True)
|
| 519 |
+
|
| 520 |
+
else:
|
| 521 |
+
st.info("No payment data available for analytics.")
|
| 522 |
+
|
| 523 |
+
except Exception as e:
|
| 524 |
+
st.error(f"Error loading payment analytics: {e}")
|
pages/reports.py
ADDED
|
@@ -0,0 +1,1101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# pages/reports.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
import plotly.graph_objects as go
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
def show_reports_page(db, whatsapp_manager=None):
|
| 10 |
+
"""Show comprehensive business intelligence and reporting"""
|
| 11 |
+
st.title("📈 Business Intelligence & Reports")
|
| 12 |
+
|
| 13 |
+
if not db:
|
| 14 |
+
st.error("Database not available. Please check initialization.")
|
| 15 |
+
return
|
| 16 |
+
|
| 17 |
+
# Tabs for different report types
|
| 18 |
+
tab1, tab2, tab3, tab4, tab5 = st.tabs(["📊 Sales Reports", "👥 Customer Reports",
|
| 19 |
+
"🤝 Distributor Reports", "💰 Financial Reports",
|
| 20 |
+
"🎯 Performance Reports"])
|
| 21 |
+
|
| 22 |
+
with tab1:
|
| 23 |
+
show_sales_reports_tab(db)
|
| 24 |
+
|
| 25 |
+
with tab2:
|
| 26 |
+
show_customer_reports_tab(db)
|
| 27 |
+
|
| 28 |
+
with tab3:
|
| 29 |
+
show_distributor_reports_tab(db)
|
| 30 |
+
|
| 31 |
+
with tab4:
|
| 32 |
+
show_financial_reports_tab(db)
|
| 33 |
+
|
| 34 |
+
with tab5:
|
| 35 |
+
show_performance_reports_tab(db)
|
| 36 |
+
|
| 37 |
+
def show_sales_reports_tab(db):
|
| 38 |
+
"""Show sales-related reports and analytics"""
|
| 39 |
+
st.subheader("📊 Sales Performance Reports")
|
| 40 |
+
|
| 41 |
+
# Date range selection
|
| 42 |
+
col1, col2, col3 = st.columns([2, 2, 1])
|
| 43 |
+
|
| 44 |
+
with col1:
|
| 45 |
+
start_date = st.date_input("Start Date", datetime.now() - timedelta(days=30))
|
| 46 |
+
with col2:
|
| 47 |
+
end_date = st.date_input("End Date", datetime.now())
|
| 48 |
+
with col3:
|
| 49 |
+
report_granularity = st.selectbox("Granularity", ["Daily", "Weekly", "Monthly"])
|
| 50 |
+
|
| 51 |
+
try:
|
| 52 |
+
# Sales Summary
|
| 53 |
+
st.subheader("💰 Sales Summary")
|
| 54 |
+
sales_summary = get_sales_summary(db, start_date, end_date)
|
| 55 |
+
|
| 56 |
+
if sales_summary:
|
| 57 |
+
col1, col2, col3, col4, col5 = st.columns(5)
|
| 58 |
+
|
| 59 |
+
with col1:
|
| 60 |
+
st.metric("Total Sales", f"₹{sales_summary.get('total_sales', 0):,.0f}")
|
| 61 |
+
with col2:
|
| 62 |
+
st.metric("Total Revenue", f"₹{sales_summary.get('total_revenue', 0):,.0f}")
|
| 63 |
+
with col3:
|
| 64 |
+
st.metric("Transactions", sales_summary.get('total_transactions', 0))
|
| 65 |
+
with col4:
|
| 66 |
+
st.metric("Avg Sale Value", f"₹{sales_summary.get('avg_sale_value', 0):,.0f}")
|
| 67 |
+
with col5:
|
| 68 |
+
st.metric("Unique Customers", sales_summary.get('unique_customers', 0))
|
| 69 |
+
|
| 70 |
+
# Sales Trend Chart
|
| 71 |
+
st.subheader("📈 Sales Trend")
|
| 72 |
+
sales_trend = get_sales_trend(db, start_date, end_date, report_granularity)
|
| 73 |
+
|
| 74 |
+
if not sales_trend.empty:
|
| 75 |
+
fig = px.line(sales_trend, x='period', y='total_amount',
|
| 76 |
+
title=f'Sales Trend ({report_granularity})',
|
| 77 |
+
labels={'period': 'Period', 'total_amount': 'Sales Amount (₹)'})
|
| 78 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 79 |
+
|
| 80 |
+
# Product Performance
|
| 81 |
+
st.subheader("📦 Product Performance")
|
| 82 |
+
product_performance = get_product_performance(db, start_date, end_date)
|
| 83 |
+
|
| 84 |
+
if not product_performance.empty:
|
| 85 |
+
col1, col2 = st.columns(2)
|
| 86 |
+
|
| 87 |
+
with col1:
|
| 88 |
+
# Top products by revenue
|
| 89 |
+
top_products = product_performance.head(10)
|
| 90 |
+
fig = px.bar(top_products, x='product_name', y='total_revenue',
|
| 91 |
+
title='Top 10 Products by Revenue',
|
| 92 |
+
labels={'product_name': 'Product', 'total_revenue': 'Revenue (₹)'})
|
| 93 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 94 |
+
|
| 95 |
+
with col2:
|
| 96 |
+
# Product sales distribution
|
| 97 |
+
fig = px.pie(product_performance, values='total_quantity', names='product_name',
|
| 98 |
+
title='Product Sales Distribution (Quantity)')
|
| 99 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 100 |
+
|
| 101 |
+
# Detailed product table
|
| 102 |
+
st.dataframe(product_performance, use_container_width=True)
|
| 103 |
+
|
| 104 |
+
# Village-wise Sales
|
| 105 |
+
st.subheader("🗺️ Village-wise Sales Performance")
|
| 106 |
+
village_sales = get_village_sales(db, start_date, end_date)
|
| 107 |
+
|
| 108 |
+
if not village_sales.empty:
|
| 109 |
+
fig = px.bar(village_sales.head(10), x='village', y='total_revenue',
|
| 110 |
+
title='Top 10 Villages by Sales Revenue',
|
| 111 |
+
labels={'village': 'Village', 'total_revenue': 'Revenue (₹)'})
|
| 112 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 113 |
+
|
| 114 |
+
# Village sales table
|
| 115 |
+
st.dataframe(village_sales, use_container_width=True)
|
| 116 |
+
|
| 117 |
+
# Export options
|
| 118 |
+
st.subheader("📤 Export Sales Report")
|
| 119 |
+
col1, col2 = st.columns(2)
|
| 120 |
+
|
| 121 |
+
with col1:
|
| 122 |
+
if st.button("📊 Export Sales Data to CSV"):
|
| 123 |
+
export_sales_data(db, start_date, end_date)
|
| 124 |
+
|
| 125 |
+
with col2:
|
| 126 |
+
if st.button("📈 Generate Sales PDF Report"):
|
| 127 |
+
generate_sales_pdf_report(db, start_date, end_date)
|
| 128 |
+
|
| 129 |
+
except Exception as e:
|
| 130 |
+
st.error(f"Error generating sales reports: {e}")
|
| 131 |
+
|
| 132 |
+
def get_sales_summary(db, start_date, end_date):
|
| 133 |
+
"""Get sales summary statistics"""
|
| 134 |
+
try:
|
| 135 |
+
query = '''
|
| 136 |
+
SELECT
|
| 137 |
+
COUNT(*) as total_transactions,
|
| 138 |
+
SUM(total_amount) as total_revenue,
|
| 139 |
+
AVG(total_amount) as avg_sale_value,
|
| 140 |
+
COUNT(DISTINCT customer_id) as unique_customers,
|
| 141 |
+
SUM(total_liters) as total_liters_sold
|
| 142 |
+
FROM sales
|
| 143 |
+
WHERE sale_date BETWEEN ? AND ?
|
| 144 |
+
'''
|
| 145 |
+
|
| 146 |
+
result = db.execute_query(query, (start_date, end_date), log_action=False)
|
| 147 |
+
|
| 148 |
+
if result:
|
| 149 |
+
row = result[0]
|
| 150 |
+
return {
|
| 151 |
+
'total_transactions': row[0] or 0,
|
| 152 |
+
'total_revenue': row[1] or 0,
|
| 153 |
+
'avg_sale_value': row[2] or 0,
|
| 154 |
+
'unique_customers': row[3] or 0,
|
| 155 |
+
'total_liters_sold': row[4] or 0
|
| 156 |
+
}
|
| 157 |
+
return {}
|
| 158 |
+
|
| 159 |
+
except Exception as e:
|
| 160 |
+
st.error(f"Error getting sales summary: {e}")
|
| 161 |
+
return {}
|
| 162 |
+
|
| 163 |
+
def get_sales_trend(db, start_date, end_date, granularity):
|
| 164 |
+
"""Get sales trend data"""
|
| 165 |
+
try:
|
| 166 |
+
if granularity == "Daily":
|
| 167 |
+
group_by = "DATE(sale_date)"
|
| 168 |
+
period_format = "sale_date"
|
| 169 |
+
elif granularity == "Weekly":
|
| 170 |
+
group_by = "STRFTIME('%Y-%W', sale_date)"
|
| 171 |
+
period_format = "STRFTIME('%Y-W%W', sale_date) as period"
|
| 172 |
+
else: # Monthly
|
| 173 |
+
group_by = "STRFTIME('%Y-%m', sale_date)"
|
| 174 |
+
period_format = "STRFTIME('%Y-%m', sale_date) as period"
|
| 175 |
+
|
| 176 |
+
query = f'''
|
| 177 |
+
SELECT {period_format},
|
| 178 |
+
SUM(total_amount) as total_amount,
|
| 179 |
+
COUNT(*) as transaction_count,
|
| 180 |
+
AVG(total_amount) as avg_amount
|
| 181 |
+
FROM sales
|
| 182 |
+
WHERE sale_date BETWEEN ? AND ?
|
| 183 |
+
GROUP BY {group_by}
|
| 184 |
+
ORDER BY period
|
| 185 |
+
'''
|
| 186 |
+
|
| 187 |
+
return db.get_dataframe('sales', query, params=(start_date, end_date))
|
| 188 |
+
|
| 189 |
+
except Exception as e:
|
| 190 |
+
st.error(f"Error getting sales trend: {e}")
|
| 191 |
+
return pd.DataFrame()
|
| 192 |
+
|
| 193 |
+
def get_product_performance(db, start_date, end_date):
|
| 194 |
+
"""Get product performance data"""
|
| 195 |
+
try:
|
| 196 |
+
query = '''
|
| 197 |
+
SELECT
|
| 198 |
+
p.product_name,
|
| 199 |
+
p.packing_type,
|
| 200 |
+
p.capacity_ltr,
|
| 201 |
+
COUNT(si.item_id) as times_sold,
|
| 202 |
+
SUM(si.quantity) as total_quantity,
|
| 203 |
+
SUM(si.amount) as total_revenue,
|
| 204 |
+
AVG(si.rate) as avg_rate
|
| 205 |
+
FROM sale_items si
|
| 206 |
+
JOIN products p ON si.product_id = p.product_id
|
| 207 |
+
JOIN sales s ON si.sale_id = s.sale_id
|
| 208 |
+
WHERE s.sale_date BETWEEN ? AND ?
|
| 209 |
+
GROUP BY p.product_id, p.product_name, p.packing_type, p.capacity_ltr
|
| 210 |
+
ORDER BY total_revenue DESC
|
| 211 |
+
'''
|
| 212 |
+
|
| 213 |
+
return db.get_dataframe('sale_items', query, params=(start_date, end_date))
|
| 214 |
+
|
| 215 |
+
except Exception as e:
|
| 216 |
+
st.error(f"Error getting product performance: {e}")
|
| 217 |
+
return pd.DataFrame()
|
| 218 |
+
|
| 219 |
+
def get_village_sales(db, start_date, end_date):
|
| 220 |
+
"""Get village-wise sales data"""
|
| 221 |
+
try:
|
| 222 |
+
query = '''
|
| 223 |
+
SELECT
|
| 224 |
+
c.village,
|
| 225 |
+
COUNT(DISTINCT s.customer_id) as unique_customers,
|
| 226 |
+
COUNT(s.sale_id) as total_transactions,
|
| 227 |
+
SUM(s.total_amount) as total_revenue,
|
| 228 |
+
AVG(s.total_amount) as avg_sale_value
|
| 229 |
+
FROM sales s
|
| 230 |
+
JOIN customers c ON s.customer_id = c.customer_id
|
| 231 |
+
WHERE s.sale_date BETWEEN ? AND ?
|
| 232 |
+
AND c.village IS NOT NULL AND c.village != ''
|
| 233 |
+
GROUP BY c.village
|
| 234 |
+
ORDER BY total_revenue DESC
|
| 235 |
+
'''
|
| 236 |
+
|
| 237 |
+
return db.get_dataframe('sales', query, params=(start_date, end_date))
|
| 238 |
+
|
| 239 |
+
except Exception as e:
|
| 240 |
+
st.error(f"Error getting village sales: {e}")
|
| 241 |
+
return pd.DataFrame()
|
| 242 |
+
|
| 243 |
+
def show_customer_reports_tab(db):
|
| 244 |
+
"""Show customer-related reports and analytics"""
|
| 245 |
+
st.subheader("👥 Customer Intelligence Reports")
|
| 246 |
+
|
| 247 |
+
try:
|
| 248 |
+
# Customer Overview
|
| 249 |
+
st.subheader("📋 Customer Overview")
|
| 250 |
+
customer_overview = get_customer_overview(db)
|
| 251 |
+
|
| 252 |
+
if customer_overview:
|
| 253 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 254 |
+
|
| 255 |
+
with col1:
|
| 256 |
+
st.metric("Total Customers", customer_overview.get('total_customers', 0))
|
| 257 |
+
with col2:
|
| 258 |
+
st.metric("Active Customers", customer_overview.get('active_customers', 0))
|
| 259 |
+
with col3:
|
| 260 |
+
st.metric("Avg Customer Value", f"₹{customer_overview.get('avg_customer_value', 0):,.0f}")
|
| 261 |
+
with col4:
|
| 262 |
+
st.metric("Repeat Customer Rate", f"{customer_overview.get('repeat_rate', 0):.1f}%")
|
| 263 |
+
|
| 264 |
+
# Top Customers
|
| 265 |
+
st.subheader("🏆 Top Customers by Spending")
|
| 266 |
+
top_customers = get_top_customers(db)
|
| 267 |
+
|
| 268 |
+
if not top_customers.empty:
|
| 269 |
+
col1, col2 = st.columns(2)
|
| 270 |
+
|
| 271 |
+
with col1:
|
| 272 |
+
fig = px.bar(top_customers.head(10), x='name', y='total_spent',
|
| 273 |
+
title='Top 10 Customers by Total Spending',
|
| 274 |
+
labels={'name': 'Customer', 'total_spent': 'Total Spent (₹)'})
|
| 275 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 276 |
+
|
| 277 |
+
with col2:
|
| 278 |
+
# Customer segmentation by spending
|
| 279 |
+
spending_brackets = categorize_customers_by_spending(top_customers)
|
| 280 |
+
fig = px.pie(values=spending_brackets.values, names=spending_brackets.index,
|
| 281 |
+
title='Customer Segmentation by Spending')
|
| 282 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 283 |
+
|
| 284 |
+
# Customer details table
|
| 285 |
+
st.dataframe(top_customers, use_container_width=True)
|
| 286 |
+
|
| 287 |
+
# Customer Acquisition Trend
|
| 288 |
+
st.subheader("📈 Customer Acquisition Trend")
|
| 289 |
+
acquisition_trend = get_customer_acquisition_trend(db)
|
| 290 |
+
|
| 291 |
+
if not acquisition_trend.empty:
|
| 292 |
+
fig = px.line(acquisition_trend, x='month', y='new_customers',
|
| 293 |
+
title='Monthly Customer Acquisition',
|
| 294 |
+
labels={'month': 'Month', 'new_customers': 'New Customers'})
|
| 295 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 296 |
+
|
| 297 |
+
# Customer Location Analysis
|
| 298 |
+
st.subheader("🗺️ Customer Geographic Distribution")
|
| 299 |
+
customer_geo = get_customer_geographic_data(db)
|
| 300 |
+
|
| 301 |
+
if not customer_geo.empty:
|
| 302 |
+
col1, col2 = st.columns(2)
|
| 303 |
+
|
| 304 |
+
with col1:
|
| 305 |
+
# Village-wise customer count
|
| 306 |
+
village_customers = customer_geo.groupby('village').size().reset_index(name='customer_count')
|
| 307 |
+
village_customers = village_customers.sort_values('customer_count', ascending=False).head(10)
|
| 308 |
+
|
| 309 |
+
fig = px.bar(village_customers, x='village', y='customer_count',
|
| 310 |
+
title='Top 10 Villages by Customer Count',
|
| 311 |
+
labels={'village': 'Village', 'customer_count': 'Number of Customers'})
|
| 312 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 313 |
+
|
| 314 |
+
with col2:
|
| 315 |
+
# Taluka-wise distribution
|
| 316 |
+
taluka_customers = customer_geo.groupby('taluka').size().reset_index(name='customer_count')
|
| 317 |
+
|
| 318 |
+
if not taluka_customers.empty:
|
| 319 |
+
fig = px.pie(taluka_customers, values='customer_count', names='taluka',
|
| 320 |
+
title='Customer Distribution by Taluka')
|
| 321 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 322 |
+
|
| 323 |
+
# Customer Lifetime Value Analysis
|
| 324 |
+
st.subheader("💰 Customer Lifetime Value Analysis")
|
| 325 |
+
clv_data = get_customer_lifetime_value(db)
|
| 326 |
+
|
| 327 |
+
if not clv_data.empty:
|
| 328 |
+
st.dataframe(clv_data, use_container_width=True)
|
| 329 |
+
|
| 330 |
+
except Exception as e:
|
| 331 |
+
st.error(f"Error generating customer reports: {e}")
|
| 332 |
+
|
| 333 |
+
def get_customer_overview(db):
|
| 334 |
+
"""Get customer overview statistics"""
|
| 335 |
+
try:
|
| 336 |
+
# Total customers
|
| 337 |
+
total_customers = db.execute_query(
|
| 338 |
+
"SELECT COUNT(*) FROM customers", log_action=False
|
| 339 |
+
)[0][0]
|
| 340 |
+
|
| 341 |
+
# Customers with purchases
|
| 342 |
+
active_customers = db.execute_query(
|
| 343 |
+
"SELECT COUNT(DISTINCT customer_id) FROM sales", log_action=False
|
| 344 |
+
)[0][0]
|
| 345 |
+
|
| 346 |
+
# Average customer value
|
| 347 |
+
avg_value_result = db.execute_query(
|
| 348 |
+
"SELECT AVG(total_amount) FROM sales", log_action=False
|
| 349 |
+
)
|
| 350 |
+
avg_customer_value = avg_value_result[0][0] if avg_value_result else 0
|
| 351 |
+
|
| 352 |
+
# Repeat customer rate
|
| 353 |
+
repeat_customers = db.execute_query(
|
| 354 |
+
"SELECT COUNT(*) FROM (SELECT customer_id FROM sales GROUP BY customer_id HAVING COUNT(*) > 1)",
|
| 355 |
+
log_action=False
|
| 356 |
+
)[0][0]
|
| 357 |
+
|
| 358 |
+
repeat_rate = (repeat_customers / active_customers * 100) if active_customers > 0 else 0
|
| 359 |
+
|
| 360 |
+
return {
|
| 361 |
+
'total_customers': total_customers,
|
| 362 |
+
'active_customers': active_customers,
|
| 363 |
+
'avg_customer_value': avg_customer_value or 0,
|
| 364 |
+
'repeat_rate': repeat_rate
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
except Exception as e:
|
| 368 |
+
st.error(f"Error getting customer overview: {e}")
|
| 369 |
+
return {}
|
| 370 |
+
|
| 371 |
+
def get_top_customers(db, limit=20):
|
| 372 |
+
"""Get top customers by spending"""
|
| 373 |
+
try:
|
| 374 |
+
query = '''
|
| 375 |
+
SELECT
|
| 376 |
+
c.customer_id,
|
| 377 |
+
c.name,
|
| 378 |
+
c.village,
|
| 379 |
+
c.taluka,
|
| 380 |
+
c.mobile,
|
| 381 |
+
COUNT(s.sale_id) as total_purchases,
|
| 382 |
+
SUM(s.total_amount) as total_spent,
|
| 383 |
+
MAX(s.sale_date) as last_purchase_date
|
| 384 |
+
FROM customers c
|
| 385 |
+
JOIN sales s ON c.customer_id = s.customer_id
|
| 386 |
+
GROUP BY c.customer_id, c.name, c.village, c.taluka, c.mobile
|
| 387 |
+
ORDER BY total_spent DESC
|
| 388 |
+
LIMIT ?
|
| 389 |
+
'''
|
| 390 |
+
|
| 391 |
+
return db.get_dataframe('customers', query, params=(limit,))
|
| 392 |
+
|
| 393 |
+
except Exception as e:
|
| 394 |
+
st.error(f"Error getting top customers: {e}")
|
| 395 |
+
return pd.DataFrame()
|
| 396 |
+
|
| 397 |
+
def categorize_customers_by_spending(customers_df):
|
| 398 |
+
"""Categorize customers by spending levels"""
|
| 399 |
+
try:
|
| 400 |
+
if customers_df.empty:
|
| 401 |
+
return pd.Series()
|
| 402 |
+
|
| 403 |
+
bins = [0, 1000, 5000, 10000, float('inf')]
|
| 404 |
+
labels = ['Low (<1K)', 'Medium (1K-5K)', 'High (5K-10K)', 'VIP (>10K)']
|
| 405 |
+
|
| 406 |
+
customers_df['spending_category'] = pd.cut(
|
| 407 |
+
customers_df['total_spent'], bins=bins, labels=labels, right=False
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
return customers_df['spending_category'].value_counts()
|
| 411 |
+
|
| 412 |
+
except Exception as e:
|
| 413 |
+
st.error(f"Error categorizing customers: {e}")
|
| 414 |
+
return pd.Series()
|
| 415 |
+
|
| 416 |
+
def get_customer_acquisition_trend(db):
|
| 417 |
+
"""Get customer acquisition trend"""
|
| 418 |
+
try:
|
| 419 |
+
query = '''
|
| 420 |
+
SELECT
|
| 421 |
+
STRFTIME('%Y-%m', created_date) as month,
|
| 422 |
+
COUNT(*) as new_customers
|
| 423 |
+
FROM customers
|
| 424 |
+
GROUP BY STRFTIME('%Y-%m', created_date)
|
| 425 |
+
ORDER BY month
|
| 426 |
+
'''
|
| 427 |
+
|
| 428 |
+
return db.get_dataframe('customers', query)
|
| 429 |
+
|
| 430 |
+
except Exception as e:
|
| 431 |
+
st.error(f"Error getting acquisition trend: {e}")
|
| 432 |
+
return pd.DataFrame()
|
| 433 |
+
|
| 434 |
+
def get_customer_geographic_data(db):
|
| 435 |
+
"""Get customer geographic distribution"""
|
| 436 |
+
try:
|
| 437 |
+
return db.get_dataframe('customers', '''
|
| 438 |
+
SELECT village, taluka, district, COUNT(*) as customer_count
|
| 439 |
+
FROM customers
|
| 440 |
+
WHERE village IS NOT NULL AND village != ''
|
| 441 |
+
GROUP BY village, taluka, district
|
| 442 |
+
ORDER BY customer_count DESC
|
| 443 |
+
''')
|
| 444 |
+
|
| 445 |
+
except Exception as e:
|
| 446 |
+
st.error(f"Error getting geographic data: {e}")
|
| 447 |
+
return pd.DataFrame()
|
| 448 |
+
|
| 449 |
+
def get_customer_lifetime_value(db):
|
| 450 |
+
"""Calculate customer lifetime value"""
|
| 451 |
+
try:
|
| 452 |
+
query = '''
|
| 453 |
+
SELECT
|
| 454 |
+
c.customer_id,
|
| 455 |
+
c.name,
|
| 456 |
+
c.village,
|
| 457 |
+
COUNT(s.sale_id) as purchase_frequency,
|
| 458 |
+
SUM(s.total_amount) as total_value,
|
| 459 |
+
AVG(s.total_amount) as avg_order_value,
|
| 460 |
+
JULIANDAY(MAX(s.sale_date)) - JULIANDAY(MIN(s.sale_date)) as customer_tenure_days,
|
| 461 |
+
CASE
|
| 462 |
+
WHEN COUNT(s.sale_id) > 0 THEN
|
| 463 |
+
SUM(s.total_amount) / (COUNT(s.sale_id) * GREATEST((JULIANDAY(MAX(s.sale_date)) - JULIANDAY(MIN(s.sale_date)))/30, 1))
|
| 464 |
+
ELSE 0
|
| 465 |
+
END as clv
|
| 466 |
+
FROM customers c
|
| 467 |
+
LEFT JOIN sales s ON c.customer_id = s.customer_id
|
| 468 |
+
GROUP BY c.customer_id, c.name, c.village
|
| 469 |
+
HAVING total_value > 0
|
| 470 |
+
ORDER BY clv DESC
|
| 471 |
+
'''
|
| 472 |
+
|
| 473 |
+
return db.get_dataframe('customers', query)
|
| 474 |
+
|
| 475 |
+
except Exception as e:
|
| 476 |
+
st.error(f"Error calculating CLV: {e}")
|
| 477 |
+
return pd.DataFrame()
|
| 478 |
+
|
| 479 |
+
def show_distributor_reports_tab(db):
|
| 480 |
+
"""Show distributor-related reports and analytics"""
|
| 481 |
+
st.subheader("🤝 Distributor Performance Reports")
|
| 482 |
+
|
| 483 |
+
try:
|
| 484 |
+
# Distributor Overview
|
| 485 |
+
st.subheader("📋 Distributor Network Overview")
|
| 486 |
+
distributor_overview = get_distributor_overview(db)
|
| 487 |
+
|
| 488 |
+
if distributor_overview:
|
| 489 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 490 |
+
|
| 491 |
+
with col1:
|
| 492 |
+
st.metric("Total Distributors", distributor_overview.get('total_distributors', 0))
|
| 493 |
+
with col2:
|
| 494 |
+
st.metric("Active Distributors", distributor_overview.get('active_distributors', 0))
|
| 495 |
+
with col3:
|
| 496 |
+
st.metric("Total Sabhasad", distributor_overview.get('total_sabhasad', 0))
|
| 497 |
+
with col4:
|
| 498 |
+
st.metric("Network Size", distributor_overview.get('network_size', 0))
|
| 499 |
+
|
| 500 |
+
# Top Performers
|
| 501 |
+
st.subheader("🏆 Top Performing Distributors")
|
| 502 |
+
top_distributors = get_top_distributors(db)
|
| 503 |
+
|
| 504 |
+
if not top_distributors.empty:
|
| 505 |
+
col1, col2 = st.columns(2)
|
| 506 |
+
|
| 507 |
+
with col1:
|
| 508 |
+
fig = px.bar(top_distributors.head(10), x='name', y='sabhasad_count',
|
| 509 |
+
title='Top 10 Distributors by Sabhasad Count',
|
| 510 |
+
labels={'name': 'Distributor', 'sabhasad_count': 'Sabhasad Count'})
|
| 511 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 512 |
+
|
| 513 |
+
with col2:
|
| 514 |
+
# Performance tiers
|
| 515 |
+
tier_distribution = top_distributors['performance_tier'].value_counts()
|
| 516 |
+
fig = px.pie(values=tier_distribution.values, names=tier_distribution.index,
|
| 517 |
+
title='Distributor Performance Tier Distribution')
|
| 518 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 519 |
+
|
| 520 |
+
# Distributor details table
|
| 521 |
+
st.dataframe(top_distributors, use_container_width=True)
|
| 522 |
+
|
| 523 |
+
# Territory Coverage
|
| 524 |
+
st.subheader("🗺️ Territory Coverage Analysis")
|
| 525 |
+
territory_coverage = get_territory_coverage(db)
|
| 526 |
+
|
| 527 |
+
if not territory_coverage.empty:
|
| 528 |
+
col1, col2 = st.columns(2)
|
| 529 |
+
|
| 530 |
+
with col1:
|
| 531 |
+
fig = px.bar(territory_coverage.head(10), x='village', y='distributor_count',
|
| 532 |
+
title='Villages with Multiple Distributors',
|
| 533 |
+
labels={'village': 'Village', 'distributor_count': 'Distributor Count'})
|
| 534 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 535 |
+
|
| 536 |
+
with col2:
|
| 537 |
+
# Coverage status
|
| 538 |
+
coverage_status = {
|
| 539 |
+
'Covered Villages': len(territory_coverage[territory_coverage['distributor_count'] > 0]),
|
| 540 |
+
'Uncovered Villages': len(territory_coverage[territory_coverage['distributor_count'] == 0])
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
fig = px.pie(values=coverage_status.values(), names=coverage_status.keys(),
|
| 544 |
+
title='Village Coverage Status')
|
| 545 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 546 |
+
|
| 547 |
+
# Growth Potential Analysis
|
| 548 |
+
st.subheader("📈 Growth Potential Analysis")
|
| 549 |
+
growth_potential = get_growth_potential(db)
|
| 550 |
+
|
| 551 |
+
if not growth_potential.empty:
|
| 552 |
+
st.dataframe(growth_potential, use_container_width=True)
|
| 553 |
+
|
| 554 |
+
except Exception as e:
|
| 555 |
+
st.error(f"Error generating distributor reports: {e}")
|
| 556 |
+
|
| 557 |
+
def get_distributor_overview(db):
|
| 558 |
+
"""Get distributor network overview"""
|
| 559 |
+
try:
|
| 560 |
+
distributors = db.get_dataframe('distributors', 'SELECT * FROM distributors')
|
| 561 |
+
|
| 562 |
+
if distributors.empty:
|
| 563 |
+
return {}
|
| 564 |
+
|
| 565 |
+
total_distributors = len(distributors)
|
| 566 |
+
active_distributors = len(distributors[distributors['sabhasad_count'] > 0])
|
| 567 |
+
total_sabhasad = distributors['sabhasad_count'].sum()
|
| 568 |
+
network_size = total_distributors + total_sabhasad
|
| 569 |
+
|
| 570 |
+
return {
|
| 571 |
+
'total_distributors': total_distributors,
|
| 572 |
+
'active_distributors': active_distributors,
|
| 573 |
+
'total_sabhasad': total_sabhasad,
|
| 574 |
+
'network_size': network_size
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
except Exception as e:
|
| 578 |
+
st.error(f"Error getting distributor overview: {e}")
|
| 579 |
+
return {}
|
| 580 |
+
|
| 581 |
+
def get_top_distributors(db, limit=20):
|
| 582 |
+
"""Get top performing distributors"""
|
| 583 |
+
try:
|
| 584 |
+
distributors = db.get_dataframe('distributors', '''
|
| 585 |
+
SELECT *,
|
| 586 |
+
(sabhasad_count + contact_in_group) as network_score
|
| 587 |
+
FROM distributors
|
| 588 |
+
ORDER BY sabhasad_count DESC
|
| 589 |
+
LIMIT ?
|
| 590 |
+
''', params=(limit,))
|
| 591 |
+
|
| 592 |
+
if not distributors.empty:
|
| 593 |
+
# Add performance tier
|
| 594 |
+
distributors['performance_tier'] = distributors['sabhasad_count'].apply(
|
| 595 |
+
lambda x: 'Platinum' if x >= 20 else 'Gold' if x >= 10 else 'Silver' if x >= 5 else 'Bronze'
|
| 596 |
+
)
|
| 597 |
+
|
| 598 |
+
return distributors
|
| 599 |
+
|
| 600 |
+
except Exception as e:
|
| 601 |
+
st.error(f"Error getting top distributors: {e}")
|
| 602 |
+
return pd.DataFrame()
|
| 603 |
+
|
| 604 |
+
def get_territory_coverage(db):
|
| 605 |
+
"""Get territory coverage analysis"""
|
| 606 |
+
try:
|
| 607 |
+
# Get all villages from customers
|
| 608 |
+
customer_villages = db.get_dataframe('customers', '''
|
| 609 |
+
SELECT DISTINCT village
|
| 610 |
+
FROM customers
|
| 611 |
+
WHERE village IS NOT NULL AND village != ''
|
| 612 |
+
''')
|
| 613 |
+
|
| 614 |
+
# Get distributor villages
|
| 615 |
+
distributor_villages = db.get_dataframe('distributors', '''
|
| 616 |
+
SELECT village, COUNT(*) as distributor_count
|
| 617 |
+
FROM distributors
|
| 618 |
+
WHERE village IS NOT NULL AND village != ''
|
| 619 |
+
GROUP BY village
|
| 620 |
+
''')
|
| 621 |
+
|
| 622 |
+
# Merge to see coverage
|
| 623 |
+
if not customer_villages.empty:
|
| 624 |
+
if not distributor_villages.empty:
|
| 625 |
+
coverage = pd.merge(customer_villages, distributor_villages, on='village', how='left')
|
| 626 |
+
else:
|
| 627 |
+
coverage = customer_villages.copy()
|
| 628 |
+
coverage['distributor_count'] = 0
|
| 629 |
+
|
| 630 |
+
coverage['distributor_count'] = coverage['distributor_count'].fillna(0)
|
| 631 |
+
return coverage.sort_values('distributor_count', ascending=False)
|
| 632 |
+
|
| 633 |
+
return pd.DataFrame()
|
| 634 |
+
|
| 635 |
+
except Exception as e:
|
| 636 |
+
st.error(f"Error getting territory coverage: {e}")
|
| 637 |
+
return pd.DataFrame()
|
| 638 |
+
|
| 639 |
+
def get_growth_potential(db):
|
| 640 |
+
"""Get distributor growth potential analysis"""
|
| 641 |
+
try:
|
| 642 |
+
return db.get_dataframe('distributors', '''
|
| 643 |
+
SELECT
|
| 644 |
+
name,
|
| 645 |
+
village,
|
| 646 |
+
sabhasad_count,
|
| 647 |
+
contact_in_group,
|
| 648 |
+
(contact_in_group - sabhasad_count) as conversion_potential,
|
| 649 |
+
CASE
|
| 650 |
+
WHEN sabhasad_count = 0 THEN contact_in_group * 0.3
|
| 651 |
+
ELSE (contact_in_group / sabhasad_count - 1) * sabhasad_count
|
| 652 |
+
END as growth_opportunity
|
| 653 |
+
FROM distributors
|
| 654 |
+
WHERE contact_in_group > sabhasad_count
|
| 655 |
+
ORDER BY growth_opportunity DESC
|
| 656 |
+
''')
|
| 657 |
+
|
| 658 |
+
except Exception as e:
|
| 659 |
+
st.error(f"Error getting growth potential: {e}")
|
| 660 |
+
return pd.DataFrame()
|
| 661 |
+
|
| 662 |
+
def show_financial_reports_tab(db):
|
| 663 |
+
"""Show financial reports and analytics"""
|
| 664 |
+
st.subheader("💰 Financial Performance Reports")
|
| 665 |
+
|
| 666 |
+
# Date range selection
|
| 667 |
+
col1, col2 = st.columns(2)
|
| 668 |
+
with col1:
|
| 669 |
+
start_date = st.date_input("Start Date", datetime.now() - timedelta(days=90), key="financial_start")
|
| 670 |
+
with col2:
|
| 671 |
+
end_date = st.date_input("End Date", datetime.now(), key="financial_end")
|
| 672 |
+
|
| 673 |
+
try:
|
| 674 |
+
# Financial Summary
|
| 675 |
+
st.subheader("📋 Financial Summary")
|
| 676 |
+
financial_summary = get_financial_summary(db, start_date, end_date)
|
| 677 |
+
|
| 678 |
+
if financial_summary:
|
| 679 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 680 |
+
|
| 681 |
+
with col1:
|
| 682 |
+
st.metric("Total Revenue", f"₹{financial_summary.get('total_revenue', 0):,.0f}")
|
| 683 |
+
with col2:
|
| 684 |
+
st.metric("Total Payments", f"₹{financial_summary.get('total_payments', 0):,.0f}")
|
| 685 |
+
with col3:
|
| 686 |
+
st.metric("Pending Amount", f"₹{financial_summary.get('pending_amount', 0):,.0f}")
|
| 687 |
+
with col4:
|
| 688 |
+
collection_rate = financial_summary.get('collection_rate', 0)
|
| 689 |
+
st.metric("Collection Rate", f"{collection_rate:.1f}%")
|
| 690 |
+
|
| 691 |
+
# Revenue vs Payments Trend
|
| 692 |
+
st.subheader("📈 Revenue vs Payments Trend")
|
| 693 |
+
financial_trend = get_financial_trend(db, start_date, end_date)
|
| 694 |
+
|
| 695 |
+
if not financial_trend.empty:
|
| 696 |
+
fig = go.Figure()
|
| 697 |
+
fig.add_trace(go.Scatter(x=financial_trend['period'], y=financial_trend['revenue'],
|
| 698 |
+
name='Revenue', line=dict(color='green')))
|
| 699 |
+
fig.add_trace(go.Scatter(x=financial_trend['period'], y=financial_trend['payments'],
|
| 700 |
+
name='Payments', line=dict(color='blue')))
|
| 701 |
+
fig.update_layout(title='Revenue vs Payments Trend', xaxis_title='Period', yaxis_title='Amount (₹)')
|
| 702 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 703 |
+
|
| 704 |
+
# Payment Method Analysis
|
| 705 |
+
st.subheader("💳 Payment Method Analysis")
|
| 706 |
+
payment_methods = get_payment_methods_analysis(db, start_date, end_date)
|
| 707 |
+
|
| 708 |
+
if not payment_methods.empty:
|
| 709 |
+
col1, col2 = st.columns(2)
|
| 710 |
+
|
| 711 |
+
with col1:
|
| 712 |
+
fig = px.pie(payment_methods, values='total_amount', names='payment_method',
|
| 713 |
+
title='Payment Methods Distribution')
|
| 714 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 715 |
+
|
| 716 |
+
with col2:
|
| 717 |
+
fig = px.bar(payment_methods, x='payment_method', y='transaction_count',
|
| 718 |
+
title='Transactions by Payment Method',
|
| 719 |
+
labels={'payment_method': 'Payment Method', 'transaction_count': 'Number of Transactions'})
|
| 720 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 721 |
+
|
| 722 |
+
# Aging Analysis
|
| 723 |
+
st.subheader("⏳ Accounts Receivable Aging")
|
| 724 |
+
aging_analysis = get_aging_analysis(db)
|
| 725 |
+
|
| 726 |
+
if not aging_analysis.empty:
|
| 727 |
+
st.dataframe(aging_analysis, use_container_width=True)
|
| 728 |
+
|
| 729 |
+
except Exception as e:
|
| 730 |
+
st.error(f"Error generating financial reports: {e}")
|
| 731 |
+
|
| 732 |
+
def get_financial_summary(db, start_date, end_date):
|
| 733 |
+
"""Get financial summary"""
|
| 734 |
+
try:
|
| 735 |
+
# Total revenue
|
| 736 |
+
revenue_result = db.execute_query(
|
| 737 |
+
"SELECT SUM(total_amount) FROM sales WHERE sale_date BETWEEN ? AND ?",
|
| 738 |
+
(start_date, end_date), log_action=False
|
| 739 |
+
)
|
| 740 |
+
total_revenue = revenue_result[0][0] or 0 if revenue_result else 0
|
| 741 |
+
|
| 742 |
+
# Total payments
|
| 743 |
+
payments_result = db.execute_query(
|
| 744 |
+
"SELECT SUM(amount) FROM payments WHERE payment_date BETWEEN ? AND ? AND status = 'Completed'",
|
| 745 |
+
(start_date, end_date), log_action=False
|
| 746 |
+
)
|
| 747 |
+
total_payments = payments_result[0][0] or 0 if payments_result else 0
|
| 748 |
+
|
| 749 |
+
# Pending amount
|
| 750 |
+
pending_result = db.execute_query(
|
| 751 |
+
"SELECT SUM(total_amount - COALESCE((SELECT SUM(amount) FROM payments WHERE payments.sale_id = sales.sale_id AND status = 'Completed'), 0)) FROM sales WHERE sale_date BETWEEN ? AND ?",
|
| 752 |
+
(start_date, end_date), log_action=False
|
| 753 |
+
)
|
| 754 |
+
pending_amount = pending_result[0][0] or 0 if pending_result else 0
|
| 755 |
+
|
| 756 |
+
# Collection rate
|
| 757 |
+
collection_rate = (total_payments / total_revenue * 100) if total_revenue > 0 else 0
|
| 758 |
+
|
| 759 |
+
return {
|
| 760 |
+
'total_revenue': total_revenue,
|
| 761 |
+
'total_payments': total_payments,
|
| 762 |
+
'pending_amount': pending_amount,
|
| 763 |
+
'collection_rate': collection_rate
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
except Exception as e:
|
| 767 |
+
st.error(f"Error getting financial summary: {e}")
|
| 768 |
+
return {}
|
| 769 |
+
|
| 770 |
+
def get_financial_trend(db, start_date, end_date):
|
| 771 |
+
"""Get financial trend data"""
|
| 772 |
+
try:
|
| 773 |
+
query = '''
|
| 774 |
+
SELECT
|
| 775 |
+
STRFTIME('%Y-%m', sale_date) as period,
|
| 776 |
+
SUM(total_amount) as revenue,
|
| 777 |
+
(SELECT SUM(amount) FROM payments
|
| 778 |
+
WHERE STRFTIME('%Y-%m', payment_date) = STRFTIME('%Y-%m', sales.sale_date)
|
| 779 |
+
AND status = 'Completed') as payments
|
| 780 |
+
FROM sales
|
| 781 |
+
WHERE sale_date BETWEEN ? AND ?
|
| 782 |
+
GROUP BY STRFTIME('%Y-%m', sale_date)
|
| 783 |
+
ORDER BY period
|
| 784 |
+
'''
|
| 785 |
+
|
| 786 |
+
return db.get_dataframe('sales', query, params=(start_date, end_date))
|
| 787 |
+
|
| 788 |
+
except Exception as e:
|
| 789 |
+
st.error(f"Error getting financial trend: {e}")
|
| 790 |
+
return pd.DataFrame()
|
| 791 |
+
|
| 792 |
+
def get_payment_methods_analysis(db, start_date, end_date):
|
| 793 |
+
"""Get payment methods analysis"""
|
| 794 |
+
try:
|
| 795 |
+
query = '''
|
| 796 |
+
SELECT
|
| 797 |
+
payment_method,
|
| 798 |
+
COUNT(*) as transaction_count,
|
| 799 |
+
SUM(amount) as total_amount,
|
| 800 |
+
AVG(amount) as avg_amount
|
| 801 |
+
FROM payments
|
| 802 |
+
WHERE payment_date BETWEEN ? AND ? AND status = 'Completed'
|
| 803 |
+
GROUP BY payment_method
|
| 804 |
+
ORDER BY total_amount DESC
|
| 805 |
+
'''
|
| 806 |
+
|
| 807 |
+
return db.get_dataframe('payments', query, params=(start_date, end_date))
|
| 808 |
+
|
| 809 |
+
except Exception as e:
|
| 810 |
+
st.error(f"Error getting payment methods analysis: {e}")
|
| 811 |
+
return pd.DataFrame()
|
| 812 |
+
|
| 813 |
+
def get_aging_analysis(db):
|
| 814 |
+
"""Get accounts receivable aging analysis"""
|
| 815 |
+
try:
|
| 816 |
+
query = '''
|
| 817 |
+
SELECT
|
| 818 |
+
s.invoice_no,
|
| 819 |
+
c.name as customer_name,
|
| 820 |
+
c.village,
|
| 821 |
+
s.sale_date,
|
| 822 |
+
s.total_amount,
|
| 823 |
+
COALESCE(SUM(p.amount), 0) as paid_amount,
|
| 824 |
+
(s.total_amount - COALESCE(SUM(p.amount), 0)) as pending_amount,
|
| 825 |
+
JULIANDAY('now') - JULIANDAY(s.sale_date) as days_pending,
|
| 826 |
+
CASE
|
| 827 |
+
WHEN JULIANDAY('now') - JULIANDAY(s.sale_date) <= 30 THEN '0-30 days'
|
| 828 |
+
WHEN JULIANDAY('now') - JULIANDAY(s.sale_date) <= 60 THEN '31-60 days'
|
| 829 |
+
WHEN JULIANDAY('now') - JULIANDAY(s.sale_date) <= 90 THEN '61-90 days'
|
| 830 |
+
ELSE 'Over 90 days'
|
| 831 |
+
END as aging_bucket
|
| 832 |
+
FROM sales s
|
| 833 |
+
JOIN customers c ON s.customer_id = c.customer_id
|
| 834 |
+
LEFT JOIN payments p ON s.sale_id = p.sale_id AND p.status = 'Completed'
|
| 835 |
+
WHERE s.payment_status IN ('Pending', 'Partial')
|
| 836 |
+
GROUP BY s.sale_id, s.invoice_no, c.name, c.village, s.sale_date, s.total_amount
|
| 837 |
+
HAVING pending_amount > 0
|
| 838 |
+
ORDER BY days_pending DESC
|
| 839 |
+
'''
|
| 840 |
+
|
| 841 |
+
return db.get_dataframe('sales', query)
|
| 842 |
+
|
| 843 |
+
except Exception as e:
|
| 844 |
+
st.error(f"Error getting aging analysis: {e}")
|
| 845 |
+
return pd.DataFrame()
|
| 846 |
+
|
| 847 |
+
def show_performance_reports_tab(db):
|
| 848 |
+
"""Show overall performance and KPI reports"""
|
| 849 |
+
st.subheader("🎯 Business Performance Dashboard")
|
| 850 |
+
|
| 851 |
+
try:
|
| 852 |
+
# Key Performance Indicators
|
| 853 |
+
st.subheader("📊 Key Performance Indicators (KPIs)")
|
| 854 |
+
|
| 855 |
+
kpis = get_performance_kpis(db)
|
| 856 |
+
|
| 857 |
+
if kpis:
|
| 858 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 859 |
+
|
| 860 |
+
with col1:
|
| 861 |
+
st.metric("Monthly Revenue", f"₹{kpis.get('monthly_revenue', 0):,.0f}")
|
| 862 |
+
with col2:
|
| 863 |
+
st.metric("Customer Growth", f"+{kpis.get('customer_growth', 0)}")
|
| 864 |
+
with col3:
|
| 865 |
+
st.metric("Demo Conversion", f"{kpis.get('demo_conversion_rate', 0):.1f}%")
|
| 866 |
+
with col4:
|
| 867 |
+
st.metric("Payment Collection", f"{kpis.get('collection_rate', 0):.1f}%")
|
| 868 |
+
|
| 869 |
+
# Performance Scorecard
|
| 870 |
+
st.subheader("📋 Performance Scorecard")
|
| 871 |
+
scorecard = get_performance_scorecard(db)
|
| 872 |
+
|
| 873 |
+
if not scorecard.empty:
|
| 874 |
+
st.dataframe(scorecard, use_container_width=True)
|
| 875 |
+
|
| 876 |
+
# Goal Tracking
|
| 877 |
+
st.subheader("🎯 Goal vs Actual Performance")
|
| 878 |
+
goals_vs_actual = get_goals_vs_actual(db)
|
| 879 |
+
|
| 880 |
+
if not goals_vs_actual.empty:
|
| 881 |
+
for _, goal in goals_vs_actual.iterrows():
|
| 882 |
+
progress = min((goal['actual'] / goal['target']) * 100, 100) if goal['target'] > 0 else 0
|
| 883 |
+
st.write(f"**{goal['metric']}**")
|
| 884 |
+
st.progress(progress / 100)
|
| 885 |
+
st.write(f"Target: {goal['target']} | Actual: {goal['actual']} | Progress: {progress:.1f}%")
|
| 886 |
+
|
| 887 |
+
# Export Comprehensive Report
|
| 888 |
+
st.subheader("📤 Export Comprehensive Report")
|
| 889 |
+
|
| 890 |
+
if st.button("📄 Generate Full Business Report"):
|
| 891 |
+
generate_comprehensive_report(db)
|
| 892 |
+
|
| 893 |
+
except Exception as e:
|
| 894 |
+
st.error(f"Error generating performance reports: {e}")
|
| 895 |
+
|
| 896 |
+
def get_performance_kpis(db):
|
| 897 |
+
"""Get key performance indicators"""
|
| 898 |
+
try:
|
| 899 |
+
# Monthly revenue (last 30 days)
|
| 900 |
+
monthly_revenue_result = db.execute_query(
|
| 901 |
+
"SELECT SUM(total_amount) FROM sales WHERE sale_date >= date('now', '-30 days')",
|
| 902 |
+
log_action=False
|
| 903 |
+
)
|
| 904 |
+
monthly_revenue = monthly_revenue_result[0][0] or 0 if monthly_revenue_result else 0
|
| 905 |
+
|
| 906 |
+
# Customer growth (last 30 days)
|
| 907 |
+
customer_growth_result = db.execute_query(
|
| 908 |
+
"SELECT COUNT(*) FROM customers WHERE created_date >= date('now', '-30 days')",
|
| 909 |
+
log_action=False
|
| 910 |
+
)
|
| 911 |
+
customer_growth = customer_growth_result[0][0] or 0 if customer_growth_result else 0
|
| 912 |
+
|
| 913 |
+
# Demo conversion rate
|
| 914 |
+
demos_result = db.execute_query(
|
| 915 |
+
"SELECT COUNT(*), SUM(CASE WHEN conversion_status = 'Converted' THEN 1 ELSE 0 END) FROM demos",
|
| 916 |
+
log_action=False
|
| 917 |
+
)
|
| 918 |
+
if demos_result and demos_result[0][0] > 0:
|
| 919 |
+
demo_conversion_rate = (demos_result[0][1] / demos_result[0][0]) * 100
|
| 920 |
+
else:
|
| 921 |
+
demo_conversion_rate = 0
|
| 922 |
+
|
| 923 |
+
# Collection rate
|
| 924 |
+
collection_result = db.execute_query(
|
| 925 |
+
"SELECT SUM(total_amount), SUM(COALESCE((SELECT SUM(amount) FROM payments WHERE payments.sale_id = sales.sale_id AND status = 'Completed'), 0)) FROM sales",
|
| 926 |
+
log_action=False
|
| 927 |
+
)
|
| 928 |
+
if collection_result and collection_result[0][0] and collection_result[0][0] > 0:
|
| 929 |
+
collection_rate = (collection_result[0][1] / collection_result[0][0]) * 100
|
| 930 |
+
else:
|
| 931 |
+
collection_rate = 0
|
| 932 |
+
|
| 933 |
+
return {
|
| 934 |
+
'monthly_revenue': monthly_revenue,
|
| 935 |
+
'customer_growth': customer_growth,
|
| 936 |
+
'demo_conversion_rate': demo_conversion_rate,
|
| 937 |
+
'collection_rate': collection_rate
|
| 938 |
+
}
|
| 939 |
+
|
| 940 |
+
except Exception as e:
|
| 941 |
+
st.error(f"Error getting KPIs: {e}")
|
| 942 |
+
return {}
|
| 943 |
+
|
| 944 |
+
def get_performance_scorecard(db):
|
| 945 |
+
"""Get performance scorecard"""
|
| 946 |
+
try:
|
| 947 |
+
scorecard_data = []
|
| 948 |
+
|
| 949 |
+
# Sales performance
|
| 950 |
+
sales_data = db.execute_query(
|
| 951 |
+
"SELECT COUNT(*), SUM(total_amount), AVG(total_amount) FROM sales WHERE sale_date >= date('now', '-30 days')",
|
| 952 |
+
log_action=False
|
| 953 |
+
)
|
| 954 |
+
if sales_data:
|
| 955 |
+
scorecard_data.append({
|
| 956 |
+
'Category': 'Sales',
|
| 957 |
+
'Metric': 'Monthly Transactions',
|
| 958 |
+
'Value': sales_data[0][0] or 0,
|
| 959 |
+
'Target': 50,
|
| 960 |
+
'Status': 'On Track' if (sales_data[0][0] or 0) >= 40 else 'Needs Attention'
|
| 961 |
+
})
|
| 962 |
+
|
| 963 |
+
scorecard_data.append({
|
| 964 |
+
'Category': 'Sales',
|
| 965 |
+
'Metric': 'Monthly Revenue',
|
| 966 |
+
'Value': f"₹{sales_data[0][1] or 0:,.0f}",
|
| 967 |
+
'Target': '₹50,000',
|
| 968 |
+
'Status': 'On Track' if (sales_data[0][1] or 0) >= 40000 else 'Needs Attention'
|
| 969 |
+
})
|
| 970 |
+
|
| 971 |
+
# Customer performance
|
| 972 |
+
customer_data = db.execute_query(
|
| 973 |
+
"SELECT COUNT(*) FROM customers WHERE created_date >= date('now', '-30 days')",
|
| 974 |
+
log_action=False
|
| 975 |
+
)
|
| 976 |
+
if customer_data:
|
| 977 |
+
scorecard_data.append({
|
| 978 |
+
'Category': 'Customers',
|
| 979 |
+
'Metric': 'New Customers',
|
| 980 |
+
'Value': customer_data[0][0] or 0,
|
| 981 |
+
'Target': 20,
|
| 982 |
+
'Status': 'On Track' if (customer_data[0][0] or 0) >= 15 else 'Needs Attention'
|
| 983 |
+
})
|
| 984 |
+
|
| 985 |
+
# Distributor performance
|
| 986 |
+
distributor_data = db.execute_query(
|
| 987 |
+
"SELECT COUNT(*), SUM(sabhasad_count) FROM distributors",
|
| 988 |
+
log_action=False
|
| 989 |
+
)
|
| 990 |
+
if distributor_data:
|
| 991 |
+
scorecard_data.append({
|
| 992 |
+
'Category': 'Distribution',
|
| 993 |
+
'Metric': 'Total Distributors',
|
| 994 |
+
'Value': distributor_data[0][0] or 0,
|
| 995 |
+
'Target': 10,
|
| 996 |
+
'Status': 'On Track' if (distributor_data[0][0] or 0) >= 8 else 'Needs Attention'
|
| 997 |
+
})
|
| 998 |
+
|
| 999 |
+
scorecard_data.append({
|
| 1000 |
+
'Category': 'Distribution',
|
| 1001 |
+
'Metric': 'Total Sabhasad',
|
| 1002 |
+
'Value': distributor_data[0][1] or 0,
|
| 1003 |
+
'Target': 100,
|
| 1004 |
+
'Status': 'On Track' if (distributor_data[0][1] or 0) >= 80 else 'Needs Attention'
|
| 1005 |
+
})
|
| 1006 |
+
|
| 1007 |
+
return pd.DataFrame(scorecard_data)
|
| 1008 |
+
|
| 1009 |
+
except Exception as e:
|
| 1010 |
+
st.error(f"Error getting performance scorecard: {e}")
|
| 1011 |
+
return pd.DataFrame()
|
| 1012 |
+
|
| 1013 |
+
def get_goals_vs_actual(db):
|
| 1014 |
+
"""Get goals vs actual performance"""
|
| 1015 |
+
try:
|
| 1016 |
+
goals = [
|
| 1017 |
+
{'metric': 'Monthly Revenue', 'target': 50000, 'actual': 0},
|
| 1018 |
+
{'metric': 'New Customers', 'target': 20, 'actual': 0},
|
| 1019 |
+
{'metric': 'Demos Conducted', 'target': 15, 'actual': 0},
|
| 1020 |
+
{'metric': 'Payment Collection', 'target': 95, 'actual': 0}
|
| 1021 |
+
]
|
| 1022 |
+
|
| 1023 |
+
# Get actual values
|
| 1024 |
+
revenue_result = db.execute_query(
|
| 1025 |
+
"SELECT SUM(total_amount) FROM sales WHERE sale_date >= date('now', '-30 days')",
|
| 1026 |
+
log_action=False
|
| 1027 |
+
)
|
| 1028 |
+
if revenue_result:
|
| 1029 |
+
goals[0]['actual'] = revenue_result[0][0] or 0
|
| 1030 |
+
|
| 1031 |
+
customer_result = db.execute_query(
|
| 1032 |
+
"SELECT COUNT(*) FROM customers WHERE created_date >= date('now', '-30 days')",
|
| 1033 |
+
log_action=False
|
| 1034 |
+
)
|
| 1035 |
+
if customer_result:
|
| 1036 |
+
goals[1]['actual'] = customer_result[0][0] or 0
|
| 1037 |
+
|
| 1038 |
+
demo_result = db.execute_query(
|
| 1039 |
+
"SELECT COUNT(*) FROM demos WHERE demo_date >= date('now', '-30 days')",
|
| 1040 |
+
log_action=False
|
| 1041 |
+
)
|
| 1042 |
+
if demo_result:
|
| 1043 |
+
goals[2]['actual'] = demo_result[0][0] or 0
|
| 1044 |
+
|
| 1045 |
+
collection_result = db.execute_query(
|
| 1046 |
+
"SELECT SUM(total_amount), SUM(COALESCE((SELECT SUM(amount) FROM payments WHERE payments.sale_id = sales.sale_id AND status = 'Completed'), 0)) FROM sales WHERE sale_date >= date('now', '-30 days')",
|
| 1047 |
+
log_action=False
|
| 1048 |
+
)
|
| 1049 |
+
if collection_result and collection_result[0][0] and collection_result[0][0] > 0:
|
| 1050 |
+
goals[3]['actual'] = (collection_result[0][1] / collection_result[0][0]) * 100
|
| 1051 |
+
|
| 1052 |
+
return pd.DataFrame(goals)
|
| 1053 |
+
|
| 1054 |
+
except Exception as e:
|
| 1055 |
+
st.error(f"Error getting goals vs actual: {e}")
|
| 1056 |
+
return pd.DataFrame()
|
| 1057 |
+
|
| 1058 |
+
def export_sales_data(db, start_date, end_date):
|
| 1059 |
+
"""Export sales data to CSV"""
|
| 1060 |
+
try:
|
| 1061 |
+
sales_data = db.get_dataframe('sales', '''
|
| 1062 |
+
SELECT s.*, c.name as customer_name, c.village, c.taluka
|
| 1063 |
+
FROM sales s
|
| 1064 |
+
JOIN customers c ON s.customer_id = c.customer_id
|
| 1065 |
+
WHERE s.sale_date BETWEEN ? AND ?
|
| 1066 |
+
ORDER BY s.sale_date DESC
|
| 1067 |
+
''', params=(start_date, end_date))
|
| 1068 |
+
|
| 1069 |
+
if not sales_data.empty:
|
| 1070 |
+
csv = sales_data.to_csv(index=False)
|
| 1071 |
+
st.download_button(
|
| 1072 |
+
label="📥 Download Sales Data as CSV",
|
| 1073 |
+
data=csv,
|
| 1074 |
+
file_name=f"sales_report_{datetime.now().strftime('%Y%m%d')}.csv",
|
| 1075 |
+
mime="text/csv"
|
| 1076 |
+
)
|
| 1077 |
+
|
| 1078 |
+
except Exception as e:
|
| 1079 |
+
st.error(f"Error exporting sales data: {e}")
|
| 1080 |
+
|
| 1081 |
+
def generate_sales_pdf_report(db, start_date, end_date):
|
| 1082 |
+
"""Generate PDF sales report (placeholder)"""
|
| 1083 |
+
st.info("📄 PDF report generation feature will be implemented soon!")
|
| 1084 |
+
st.info("This would generate a comprehensive PDF report with charts and analysis.")
|
| 1085 |
+
|
| 1086 |
+
def generate_comprehensive_report(db):
|
| 1087 |
+
"""Generate comprehensive business report"""
|
| 1088 |
+
st.success("🎉 Comprehensive business report generated!")
|
| 1089 |
+
st.info("""
|
| 1090 |
+
**Report Includes:**
|
| 1091 |
+
- Executive Summary
|
| 1092 |
+
- Sales Performance Analysis
|
| 1093 |
+
- Customer Insights
|
| 1094 |
+
- Distributor Network Performance
|
| 1095 |
+
- Financial Overview
|
| 1096 |
+
- Key Recommendations
|
| 1097 |
+
|
| 1098 |
+
*Note: Full report generation with export features will be implemented in the next version.*
|
| 1099 |
+
""")
|
| 1100 |
+
|
| 1101 |
+
# Add this to your main file routing
|
pages/sales.py
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# pages/sales.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
import re
|
| 6 |
+
|
| 7 |
+
def show_sales_page(db, whatsapp_manager=None):
|
| 8 |
+
"""Show enhanced sales management page with quick customer creation and WhatsApp"""
|
| 9 |
+
st.title("💰 Sales Management")
|
| 10 |
+
|
| 11 |
+
if not db:
|
| 12 |
+
st.error("Database not available. Please check initialization.")
|
| 13 |
+
return
|
| 14 |
+
|
| 15 |
+
# Tabs for different sales functions
|
| 16 |
+
tab1, tab2, tab3 = st.tabs(["➕ Quick Sale", "📋 Sales History", "🔍 Sales Analytics"])
|
| 17 |
+
|
| 18 |
+
with tab1:
|
| 19 |
+
show_quick_sale_tab(db, whatsapp_manager)
|
| 20 |
+
|
| 21 |
+
with tab2:
|
| 22 |
+
show_sales_history_tab(db)
|
| 23 |
+
|
| 24 |
+
with tab3:
|
| 25 |
+
show_sales_analytics_tab(db)
|
| 26 |
+
|
| 27 |
+
def show_quick_sale_tab(db, whatsapp_manager):
|
| 28 |
+
"""Show tab for quick sales with instant customer creation"""
|
| 29 |
+
st.subheader("🚀 Quick Sale - Create Customer & Sale in One Go")
|
| 30 |
+
|
| 31 |
+
with st.form("quick_sale_form"):
|
| 32 |
+
# Customer Section - Quick Create
|
| 33 |
+
st.markdown("### 👥 Customer Information")
|
| 34 |
+
|
| 35 |
+
col1, col2 = st.columns(2)
|
| 36 |
+
|
| 37 |
+
with col1:
|
| 38 |
+
# Option 1: Select existing customer
|
| 39 |
+
st.write("**Select Existing Customer**")
|
| 40 |
+
customers = db.get_dataframe('customers')
|
| 41 |
+
existing_customer_options = {f"{row['name']} ({row['mobile']})": row['customer_id']
|
| 42 |
+
for _, row in customers.iterrows()} if not customers.empty else {}
|
| 43 |
+
|
| 44 |
+
use_existing = st.selectbox("Choose Customer",
|
| 45 |
+
options=[""] + list(existing_customer_options.keys()),
|
| 46 |
+
key="existing_customer")
|
| 47 |
+
|
| 48 |
+
if use_existing:
|
| 49 |
+
customer_id = existing_customer_options[use_existing]
|
| 50 |
+
# Get customer details for display
|
| 51 |
+
customer_details = customers[customers['customer_id'] == customer_id].iloc[0]
|
| 52 |
+
st.success(f"Selected: {customer_details['name']} - {customer_details['mobile']}")
|
| 53 |
+
else:
|
| 54 |
+
customer_id = None
|
| 55 |
+
|
| 56 |
+
with col2:
|
| 57 |
+
# Option 2: Create new customer instantly
|
| 58 |
+
st.write("**Or Create New Customer**")
|
| 59 |
+
new_customer_name = st.text_input("Customer Name*", placeholder="Enter customer name")
|
| 60 |
+
new_customer_mobile = st.text_input("Mobile Number", placeholder="Enter mobile number")
|
| 61 |
+
new_customer_village = st.text_input("Village", placeholder="Enter village")
|
| 62 |
+
|
| 63 |
+
if new_customer_name:
|
| 64 |
+
# Check if customer already exists
|
| 65 |
+
existing_customer = db.execute_query(
|
| 66 |
+
"SELECT customer_id FROM customers WHERE name = ? OR mobile = ?",
|
| 67 |
+
(new_customer_name, new_customer_mobile),
|
| 68 |
+
log_action=False
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
if existing_customer:
|
| 72 |
+
st.warning("⚠️ Customer with same name/mobile already exists!")
|
| 73 |
+
customer_id = existing_customer[0][0]
|
| 74 |
+
else:
|
| 75 |
+
customer_id = None # Will create during sale submission
|
| 76 |
+
|
| 77 |
+
# Sale Information
|
| 78 |
+
st.markdown("### 📄 Sale Information")
|
| 79 |
+
col1, col2 = st.columns(2)
|
| 80 |
+
|
| 81 |
+
with col1:
|
| 82 |
+
invoice_no = st.text_input("Invoice Number*", value=db.generate_invoice_number())
|
| 83 |
+
with col2:
|
| 84 |
+
sale_date = st.date_input("Sale Date", datetime.now())
|
| 85 |
+
|
| 86 |
+
# Products Section with Smart Pricing
|
| 87 |
+
st.markdown("### 📦 Add Products")
|
| 88 |
+
|
| 89 |
+
products = db.get_dataframe('products')
|
| 90 |
+
if products.empty:
|
| 91 |
+
st.error("❌ No products found in database. Please add products first.")
|
| 92 |
+
return
|
| 93 |
+
|
| 94 |
+
product_options = {row['product_name']: {
|
| 95 |
+
'product_id': row['product_id'],
|
| 96 |
+
'standard_rate': row['standard_rate'],
|
| 97 |
+
'packing_type': row['packing_type'],
|
| 98 |
+
'capacity_ltr': row['capacity_ltr']
|
| 99 |
+
} for _, row in products.iterrows()}
|
| 100 |
+
|
| 101 |
+
sale_items = []
|
| 102 |
+
total_amount = 0
|
| 103 |
+
|
| 104 |
+
# Create 3 product rows
|
| 105 |
+
for i in range(3):
|
| 106 |
+
st.markdown(f"**Product {i+1}**")
|
| 107 |
+
col1, col2, col3, col4, col5 = st.columns([3, 1, 2, 2, 1])
|
| 108 |
+
|
| 109 |
+
with col1:
|
| 110 |
+
selected_product = st.selectbox(f"Select Product",
|
| 111 |
+
options=[""] + list(product_options.keys()),
|
| 112 |
+
key=f"product_{i}")
|
| 113 |
+
|
| 114 |
+
with col2:
|
| 115 |
+
quantity = st.number_input(f"Qty", min_value=0, value=0, key=f"qty_{i}")
|
| 116 |
+
|
| 117 |
+
with col3:
|
| 118 |
+
if selected_product:
|
| 119 |
+
default_rate = product_options[selected_product]['standard_rate']
|
| 120 |
+
rate = st.number_input(f"Rate (₹)", min_value=0.0, value=float(default_rate),
|
| 121 |
+
step=1.0, key=f"rate_{i}")
|
| 122 |
+
# Show discount indicator if rate is changed
|
| 123 |
+
if rate < default_rate:
|
| 124 |
+
discount = ((default_rate - rate) / default_rate) * 100
|
| 125 |
+
st.info(f"🎯 {discount:.1f}% off")
|
| 126 |
+
else:
|
| 127 |
+
rate = st.number_input(f"Rate (₹)", min_value=0.0, value=0.0, key=f"rate_{i}")
|
| 128 |
+
|
| 129 |
+
with col4:
|
| 130 |
+
if selected_product and quantity > 0:
|
| 131 |
+
amount = quantity * rate
|
| 132 |
+
st.metric("Amount", f"₹{amount:,.2f}")
|
| 133 |
+
else:
|
| 134 |
+
amount = 0
|
| 135 |
+
st.metric("Amount", "₹0")
|
| 136 |
+
|
| 137 |
+
with col5:
|
| 138 |
+
if selected_product and quantity > 0:
|
| 139 |
+
product_info = product_options[selected_product]
|
| 140 |
+
st.write(f"*{product_info['packing_type']}*")
|
| 141 |
+
if product_info['capacity_ltr'] > 0:
|
| 142 |
+
st.write(f"{product_info['capacity_ltr']}L")
|
| 143 |
+
|
| 144 |
+
if selected_product and quantity > 0:
|
| 145 |
+
sale_items.append({
|
| 146 |
+
'product_id': product_options[selected_product]['product_id'],
|
| 147 |
+
'product_name': selected_product,
|
| 148 |
+
'quantity': quantity,
|
| 149 |
+
'rate': rate,
|
| 150 |
+
'amount': amount,
|
| 151 |
+
'standard_rate': product_options[selected_product]['standard_rate']
|
| 152 |
+
})
|
| 153 |
+
total_amount += amount
|
| 154 |
+
|
| 155 |
+
# Show running total
|
| 156 |
+
if total_amount > 0:
|
| 157 |
+
st.success(f"### 🎯 Running Total: ₹{total_amount:,.2f}")
|
| 158 |
+
|
| 159 |
+
# Additional Options
|
| 160 |
+
st.markdown("### ⚙️ Additional Options")
|
| 161 |
+
|
| 162 |
+
col1, col2 = st.columns(2)
|
| 163 |
+
|
| 164 |
+
with col1:
|
| 165 |
+
notes = st.text_area("Sale Notes", placeholder="Any special notes about this sale...")
|
| 166 |
+
|
| 167 |
+
with col2:
|
| 168 |
+
# WhatsApp Notification Options
|
| 169 |
+
st.write("**📱 Customer Notification**")
|
| 170 |
+
send_whatsapp = st.checkbox("Send WhatsApp Notification", value=True)
|
| 171 |
+
if send_whatsapp and not whatsapp_manager:
|
| 172 |
+
st.warning("WhatsApp manager not available")
|
| 173 |
+
|
| 174 |
+
# Payment options
|
| 175 |
+
payment_received = st.checkbox("Payment Received", value=False)
|
| 176 |
+
if payment_received:
|
| 177 |
+
payment_amount = st.number_input("Payment Amount", min_value=0.0, value=float(total_amount))
|
| 178 |
+
payment_method = st.selectbox("Payment Method", ["Cash", "G-Pay", "Cheque", "Bank Transfer"])
|
| 179 |
+
|
| 180 |
+
# Submit Section
|
| 181 |
+
st.markdown("---")
|
| 182 |
+
submitted = st.form_submit_button("🚀 Create Sale & Notify Customer", type="primary")
|
| 183 |
+
|
| 184 |
+
if submitted:
|
| 185 |
+
# Validation
|
| 186 |
+
errors = []
|
| 187 |
+
|
| 188 |
+
if not customer_id and not new_customer_name:
|
| 189 |
+
errors.append("Please select a customer or enter new customer name")
|
| 190 |
+
if not invoice_no:
|
| 191 |
+
errors.append("Invoice number is required")
|
| 192 |
+
if not sale_items:
|
| 193 |
+
errors.append("Please add at least one product")
|
| 194 |
+
|
| 195 |
+
if errors:
|
| 196 |
+
for error in errors:
|
| 197 |
+
st.error(error)
|
| 198 |
+
else:
|
| 199 |
+
try:
|
| 200 |
+
# Create customer if new
|
| 201 |
+
if not customer_id and new_customer_name:
|
| 202 |
+
customer_id = db.add_customer(
|
| 203 |
+
name=new_customer_name,
|
| 204 |
+
mobile=new_customer_mobile,
|
| 205 |
+
village=new_customer_village
|
| 206 |
+
)
|
| 207 |
+
if customer_id and customer_id > 0:
|
| 208 |
+
st.success(f"✅ New customer created: {new_customer_name}")
|
| 209 |
+
|
| 210 |
+
if customer_id:
|
| 211 |
+
# Create sale
|
| 212 |
+
sale_id = db.add_sale(
|
| 213 |
+
invoice_no=invoice_no,
|
| 214 |
+
customer_id=customer_id,
|
| 215 |
+
sale_date=sale_date,
|
| 216 |
+
items=sale_items,
|
| 217 |
+
notes=notes
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
if sale_id and sale_id > 0:
|
| 221 |
+
st.success(f"✅ Sale created successfully! Sale ID: {sale_id}")
|
| 222 |
+
|
| 223 |
+
# Add payment if received
|
| 224 |
+
if payment_received:
|
| 225 |
+
db.execute_query('''
|
| 226 |
+
INSERT INTO payments (sale_id, payment_date, payment_method, amount)
|
| 227 |
+
VALUES (?, ?, ?, ?)
|
| 228 |
+
''', (sale_id, sale_date, payment_method, payment_amount))
|
| 229 |
+
st.success(f"✅ Payment recorded: ₹{payment_amount:,.2f}")
|
| 230 |
+
|
| 231 |
+
# Send WhatsApp notification
|
| 232 |
+
if send_whatsapp and whatsapp_manager:
|
| 233 |
+
send_sale_notification(whatsapp_manager, db, sale_id, customer_id)
|
| 234 |
+
|
| 235 |
+
# Show sale summary
|
| 236 |
+
show_quick_sale_summary(db, sale_id, sale_items, customer_id)
|
| 237 |
+
|
| 238 |
+
# Clear form for next sale
|
| 239 |
+
if st.button("🔄 Create Another Sale"):
|
| 240 |
+
st.rerun()
|
| 241 |
+
else:
|
| 242 |
+
st.error("❌ Failed to create sale.")
|
| 243 |
+
|
| 244 |
+
except Exception as e:
|
| 245 |
+
st.error(f"Error creating sale: {e}")
|
| 246 |
+
|
| 247 |
+
def send_sale_notification(whatsapp_manager, db, sale_id, customer_id):
|
| 248 |
+
"""Send WhatsApp notification to customer about their sale"""
|
| 249 |
+
try:
|
| 250 |
+
# Get sale and customer details
|
| 251 |
+
sale_details = db.get_dataframe('sales', f"SELECT * FROM sales WHERE sale_id = {sale_id}")
|
| 252 |
+
customer_details = db.get_dataframe('customers', f"SELECT * FROM customers WHERE customer_id = {customer_id}")
|
| 253 |
+
|
| 254 |
+
if sale_details.empty or customer_details.empty:
|
| 255 |
+
return False
|
| 256 |
+
|
| 257 |
+
sale = sale_details.iloc[0]
|
| 258 |
+
customer = customer_details.iloc[0]
|
| 259 |
+
|
| 260 |
+
# Get sale items
|
| 261 |
+
items_details = db.get_dataframe('sale_items', f'''
|
| 262 |
+
SELECT si.*, p.product_name
|
| 263 |
+
FROM sale_items si
|
| 264 |
+
JOIN products p ON si.product_id = p.product_id
|
| 265 |
+
WHERE si.sale_id = {sale_id}
|
| 266 |
+
''')
|
| 267 |
+
|
| 268 |
+
if customer.get('mobile'):
|
| 269 |
+
# Create notification message
|
| 270 |
+
message = f"""Hello {customer['name']}! 🎉
|
| 271 |
+
|
| 272 |
+
Thank you for your purchase!
|
| 273 |
+
|
| 274 |
+
📄 Invoice: {sale['invoice_no']}
|
| 275 |
+
📅 Date: {sale['sale_date']}
|
| 276 |
+
💰 Total Amount: ₹{sale['total_amount']:,.2f}
|
| 277 |
+
|
| 278 |
+
📦 Items Purchased:
|
| 279 |
+
"""
|
| 280 |
+
|
| 281 |
+
# Add items to message
|
| 282 |
+
for _, item in items_details.iterrows():
|
| 283 |
+
message += f"• {item['product_name']}: {item['quantity']} x ₹{item['rate']} = ₹{item['amount']}\n"
|
| 284 |
+
|
| 285 |
+
message += f"""
|
| 286 |
+
|
| 287 |
+
Payment Status: {sale['payment_status']}
|
| 288 |
+
|
| 289 |
+
We appreciate your business! 🙏
|
| 290 |
+
|
| 291 |
+
For any queries, contact us.
|
| 292 |
+
|
| 293 |
+
Thank you!"""
|
| 294 |
+
|
| 295 |
+
# Send message
|
| 296 |
+
success = whatsapp_manager.send_message(customer['mobile'], message)
|
| 297 |
+
if success:
|
| 298 |
+
st.success("📱 WhatsApp notification sent to customer!")
|
| 299 |
+
else:
|
| 300 |
+
st.warning("⚠️ Failed to send WhatsApp notification")
|
| 301 |
+
|
| 302 |
+
return success
|
| 303 |
+
|
| 304 |
+
except Exception as e:
|
| 305 |
+
st.warning(f"Could not send notification: {e}")
|
| 306 |
+
return False
|
| 307 |
+
|
| 308 |
+
def show_quick_sale_summary(db, sale_id, sale_items, customer_id):
|
| 309 |
+
"""Show comprehensive sale summary"""
|
| 310 |
+
st.markdown("## 🎉 Sale Completed Successfully!")
|
| 311 |
+
|
| 312 |
+
try:
|
| 313 |
+
# Get sale and customer details
|
| 314 |
+
sale_details = db.get_dataframe('sales', f"SELECT * FROM sales WHERE sale_id = {sale_id}")
|
| 315 |
+
customer_details = db.get_dataframe('customers', f"SELECT * FROM customers WHERE customer_id = {customer_id}")
|
| 316 |
+
|
| 317 |
+
if sale_details.empty or customer_details.empty:
|
| 318 |
+
return
|
| 319 |
+
|
| 320 |
+
sale = sale_details.iloc[0]
|
| 321 |
+
customer = customer_details.iloc[0]
|
| 322 |
+
|
| 323 |
+
# Display in columns
|
| 324 |
+
col1, col2 = st.columns(2)
|
| 325 |
+
|
| 326 |
+
with col1:
|
| 327 |
+
st.subheader("Customer Details")
|
| 328 |
+
st.write(f"**Name:** {customer['name']}")
|
| 329 |
+
if customer['mobile']:
|
| 330 |
+
st.write(f"**Mobile:** {customer['mobile']}")
|
| 331 |
+
if customer['village']:
|
| 332 |
+
st.write(f"**Village:** {customer['village']}")
|
| 333 |
+
|
| 334 |
+
st.subheader("Sale Details")
|
| 335 |
+
st.write(f"**Invoice No:** {sale['invoice_no']}")
|
| 336 |
+
st.write(f"**Sale Date:** {sale['sale_date']}")
|
| 337 |
+
st.write(f"**Payment Status:** {sale['payment_status']}")
|
| 338 |
+
if sale['notes']:
|
| 339 |
+
st.write(f"**Notes:** {sale['notes']}")
|
| 340 |
+
|
| 341 |
+
with col2:
|
| 342 |
+
st.subheader("Financial Summary")
|
| 343 |
+
st.metric("Total Amount", f"₹{sale['total_amount']:,.2f}")
|
| 344 |
+
|
| 345 |
+
# Get payment details
|
| 346 |
+
payment_details = db.get_dataframe('payments', f"SELECT * FROM payments WHERE sale_id = {sale_id}")
|
| 347 |
+
if not payment_details.empty:
|
| 348 |
+
total_paid = payment_details['amount'].sum()
|
| 349 |
+
pending = sale['total_amount'] - total_paid
|
| 350 |
+
st.metric("Amount Paid", f"₹{total_paid:,.2f}")
|
| 351 |
+
st.metric("Pending Amount", f"₹{pending:,.2f}")
|
| 352 |
+
|
| 353 |
+
st.subheader("Items Summary")
|
| 354 |
+
for item in sale_items:
|
| 355 |
+
st.write(f"• {item['product_name']}: {item['quantity']} x ₹{item['rate']}")
|
| 356 |
+
|
| 357 |
+
# Quick actions
|
| 358 |
+
st.markdown("### ⚡ Quick Actions")
|
| 359 |
+
col1, col2, col3 = st.columns(3)
|
| 360 |
+
|
| 361 |
+
with col1:
|
| 362 |
+
if st.button("📋 View All Sales"):
|
| 363 |
+
# This would switch to sales history tab in a real implementation
|
| 364 |
+
st.info("Navigate to Sales History tab")
|
| 365 |
+
|
| 366 |
+
with col2:
|
| 367 |
+
if st.button("👥 View Customer"):
|
| 368 |
+
st.info(f"Customer: {customer['name']}")
|
| 369 |
+
|
| 370 |
+
with col3:
|
| 371 |
+
if st.button("💳 Record Payment"):
|
| 372 |
+
st.info("Use Payments page to record additional payments")
|
| 373 |
+
|
| 374 |
+
except Exception as e:
|
| 375 |
+
st.error(f"Error displaying sale summary: {e}")
|
| 376 |
+
|
| 377 |
+
def show_sales_history_tab(db):
|
| 378 |
+
"""Show tab for sales history and management"""
|
| 379 |
+
st.subheader("Sales History & Management")
|
| 380 |
+
|
| 381 |
+
try:
|
| 382 |
+
# Quick filters
|
| 383 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 384 |
+
|
| 385 |
+
with col1:
|
| 386 |
+
date_filter = st.selectbox("Date Filter",
|
| 387 |
+
["All", "Today", "Last 7 days", "This month", "Custom"])
|
| 388 |
+
|
| 389 |
+
with col2:
|
| 390 |
+
status_filter = st.multiselect("Payment Status",
|
| 391 |
+
["Pending", "Partial", "Paid"],
|
| 392 |
+
default=["Pending", "Partial", "Paid"])
|
| 393 |
+
|
| 394 |
+
with col3:
|
| 395 |
+
search_term = st.text_input("Search Invoice/Customer")
|
| 396 |
+
|
| 397 |
+
with col4:
|
| 398 |
+
show_rows = st.selectbox("Show", [10, 25, 50, 100], index=0)
|
| 399 |
+
|
| 400 |
+
# Build query
|
| 401 |
+
query = '''
|
| 402 |
+
SELECT s.*, c.name as customer_name, c.village, c.mobile,
|
| 403 |
+
COALESCE(SUM(p.amount), 0) as paid_amount,
|
| 404 |
+
(s.total_amount - COALESCE(SUM(p.amount), 0)) as pending_amount
|
| 405 |
+
FROM sales s
|
| 406 |
+
LEFT JOIN customers c ON s.customer_id = c.customer_id
|
| 407 |
+
LEFT JOIN payments p ON s.sale_id = p.sale_id
|
| 408 |
+
'''
|
| 409 |
+
|
| 410 |
+
conditions = []
|
| 411 |
+
if status_filter:
|
| 412 |
+
status_cond = " OR ".join([f"s.payment_status = '{status}'" for status in status_filter])
|
| 413 |
+
conditions.append(f"({status_cond})")
|
| 414 |
+
|
| 415 |
+
if search_term:
|
| 416 |
+
conditions.append(f"(s.invoice_no LIKE '%{search_term}%' OR c.name LIKE '%{search_term}%')")
|
| 417 |
+
|
| 418 |
+
if conditions:
|
| 419 |
+
query += " WHERE " + " AND ".join(conditions)
|
| 420 |
+
|
| 421 |
+
query += " GROUP BY s.sale_id ORDER BY s.sale_date DESC LIMIT " + str(show_rows)
|
| 422 |
+
|
| 423 |
+
# Get sales data
|
| 424 |
+
sales_data = db.get_dataframe('sales', query)
|
| 425 |
+
|
| 426 |
+
if not sales_data.empty:
|
| 427 |
+
st.write(f"**Showing {len(sales_data)} sales**")
|
| 428 |
+
|
| 429 |
+
# Enhanced display
|
| 430 |
+
display_df = sales_data[['invoice_no', 'sale_date', 'customer_name', 'village',
|
| 431 |
+
'total_amount', 'paid_amount', 'pending_amount', 'payment_status']].copy()
|
| 432 |
+
|
| 433 |
+
# Formatting
|
| 434 |
+
display_df['total_amount'] = display_df['total_amount'].apply(lambda x: f"₹{x:,.2f}")
|
| 435 |
+
display_df['paid_amount'] = display_df['paid_amount'].apply(lambda x: f"₹{x:,.2f}")
|
| 436 |
+
display_df['pending_amount'] = display_df['pending_amount'].apply(lambda x: f"₹{x:,.2f}")
|
| 437 |
+
|
| 438 |
+
display_df.columns = ['Invoice', 'Date', 'Customer', 'Village', 'Total', 'Paid', 'Pending', 'Status']
|
| 439 |
+
|
| 440 |
+
st.dataframe(display_df, use_container_width=True)
|
| 441 |
+
|
| 442 |
+
# Summary metrics
|
| 443 |
+
total_sales = sales_data['total_amount'].sum()
|
| 444 |
+
total_pending = sales_data['pending_amount'].sum()
|
| 445 |
+
|
| 446 |
+
col1, col2, col3 = st.columns(3)
|
| 447 |
+
with col1:
|
| 448 |
+
st.metric("Total Sales Value", f"₹{total_sales:,.2f}")
|
| 449 |
+
with col2:
|
| 450 |
+
st.metric("Total Pending", f"₹{total_pending:,.2f}")
|
| 451 |
+
with col3:
|
| 452 |
+
if total_sales > 0:
|
| 453 |
+
collection_rate = ((total_sales - total_pending) / total_sales) * 100
|
| 454 |
+
st.metric("Collection Rate", f"{collection_rate:.1f}%")
|
| 455 |
+
|
| 456 |
+
else:
|
| 457 |
+
st.info("No sales found matching the current filters.")
|
| 458 |
+
|
| 459 |
+
except Exception as e:
|
| 460 |
+
st.error(f"Error loading sales history: {e}")
|
| 461 |
+
|
| 462 |
+
def show_sales_analytics_tab(db):
|
| 463 |
+
"""Show tab for sales analytics"""
|
| 464 |
+
st.subheader("Sales Analytics & Insights")
|
| 465 |
+
|
| 466 |
+
try:
|
| 467 |
+
# Date range
|
| 468 |
+
col1, col2 = st.columns(2)
|
| 469 |
+
with col1:
|
| 470 |
+
start_date = st.date_input("Start Date", datetime.now() - timedelta(days=30))
|
| 471 |
+
with col2:
|
| 472 |
+
end_date = st.date_input("End Date", datetime.now())
|
| 473 |
+
|
| 474 |
+
if start_date > end_date:
|
| 475 |
+
st.error("Start date cannot be after end date")
|
| 476 |
+
return
|
| 477 |
+
|
| 478 |
+
# Get analytics
|
| 479 |
+
analytics = db.get_sales_analytics(start_date.strftime('%Y-%m-%d'),
|
| 480 |
+
end_date.strftime('%Y-%m-%d'))
|
| 481 |
+
|
| 482 |
+
if analytics:
|
| 483 |
+
# Key metrics
|
| 484 |
+
st.subheader("📈 Performance Metrics")
|
| 485 |
+
cols = st.columns(4)
|
| 486 |
+
metrics = [
|
| 487 |
+
("Total Sales", analytics.get('total_sales', 0), "💰"),
|
| 488 |
+
("Total Revenue", f"₹{analytics.get('total_revenue', 0):,.2f}", "💵"),
|
| 489 |
+
("Avg Sale", f"₹{analytics.get('avg_sale_value', 0):,.2f}", "📊"),
|
| 490 |
+
("Unique Customers", analytics.get('unique_customers', 0), "👥")
|
| 491 |
+
]
|
| 492 |
+
|
| 493 |
+
for col, (label, value, icon) in zip(cols, metrics):
|
| 494 |
+
with col:
|
| 495 |
+
st.metric(label, value)
|
| 496 |
+
|
| 497 |
+
# Additional analytics can be added here
|
| 498 |
+
|
| 499 |
+
except Exception as e:
|
| 500 |
+
st.error(f"Error loading analytics: {e}")
|
pages/system_dashboard.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# pages/system_dashboard.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from utils.styling import create_metric_card, COLORS
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def create_dashboard(db, analytics):
|
| 9 |
+
"""Create the main dashboard with metrics and charts"""
|
| 10 |
+
st.markdown("<h1 class='main-header'>📊 Sales Dashboard</h1>", unsafe_allow_html=True)
|
| 11 |
+
|
| 12 |
+
# One-time notification after scheduling a demo
|
| 13 |
+
if st.session_state.get("demo_created_notification"):
|
| 14 |
+
st.success(f"✅ Demo #{st.session_state.demo_created_notification} added successfully!")
|
| 15 |
+
st.session_state.demo_created_notification = None
|
| 16 |
+
|
| 17 |
+
if not analytics or not db:
|
| 18 |
+
st.error("Analytics or Database not available.")
|
| 19 |
+
return
|
| 20 |
+
|
| 21 |
+
# Fetch analytics safely
|
| 22 |
+
try:
|
| 23 |
+
sales_summary = analytics.get_sales_summary()
|
| 24 |
+
demo_stats = analytics.get_demo_conversion_rates()
|
| 25 |
+
customer_analysis = analytics.get_customer_analysis()
|
| 26 |
+
payment_analysis = analytics.get_payment_analysis()
|
| 27 |
+
except Exception:
|
| 28 |
+
sales_summary = {"total_sales": 0, "pending_amount": 0}
|
| 29 |
+
demo_stats = {"conversion_rate": 0}
|
| 30 |
+
customer_analysis = {"total_customers": 0}
|
| 31 |
+
payment_analysis = {"total_pending": 0}
|
| 32 |
+
|
| 33 |
+
# ------------------- METRICS ROW -------------------
|
| 34 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 35 |
+
|
| 36 |
+
with col1:
|
| 37 |
+
st.markdown(create_metric_card(f"₹{sales_summary['total_sales']:,.0f}", "Total Sales", "💰", COLORS["primary"]), unsafe_allow_html=True)
|
| 38 |
+
|
| 39 |
+
with col2:
|
| 40 |
+
st.markdown(create_metric_card(f"₹{sales_summary['pending_amount']:,.0f}", "Pending Payments", "⏳", COLORS["warning"]), unsafe_allow_html=True)
|
| 41 |
+
|
| 42 |
+
with col3:
|
| 43 |
+
st.markdown(create_metric_card(f"{demo_stats['conversion_rate']:.1f}%", "Demo Conversion", "🎯", COLORS["success"]), unsafe_allow_html=True)
|
| 44 |
+
|
| 45 |
+
with col4:
|
| 46 |
+
st.markdown(create_metric_card(f"{customer_analysis['total_customers']}", "Total Customers", "👥", COLORS["secondary"]), unsafe_allow_html=True)
|
| 47 |
+
|
| 48 |
+
# ------------------- SALES TREND + PAYMENT CHARTS -------------------
|
| 49 |
+
col1, col2 = st.columns(2)
|
| 50 |
+
|
| 51 |
+
with col1:
|
| 52 |
+
st.markdown("<h3 class='section-header'>Sales Trend</h3>", unsafe_allow_html=True)
|
| 53 |
+
try:
|
| 54 |
+
sales_trend = analytics.get_sales_trend()
|
| 55 |
+
if not sales_trend.empty:
|
| 56 |
+
fig = px.line(
|
| 57 |
+
sales_trend,
|
| 58 |
+
x="sale_date",
|
| 59 |
+
y="total_amount",
|
| 60 |
+
title="Daily Sales Trend",
|
| 61 |
+
color_discrete_sequence=[COLORS["primary"]],
|
| 62 |
+
)
|
| 63 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 64 |
+
else:
|
| 65 |
+
st.info("No sales data available.")
|
| 66 |
+
except Exception:
|
| 67 |
+
st.info("Error loading sales trend.")
|
| 68 |
+
|
| 69 |
+
with col2:
|
| 70 |
+
st.markdown("<h3 class='section-header'>Payment Status</h3>", unsafe_allow_html=True)
|
| 71 |
+
try:
|
| 72 |
+
payment_data = analytics.get_payment_distribution()
|
| 73 |
+
if not payment_data.empty:
|
| 74 |
+
fig = px.pie(
|
| 75 |
+
payment_data,
|
| 76 |
+
values="amount",
|
| 77 |
+
names="payment_method",
|
| 78 |
+
title="Payment Methods Distribution",
|
| 79 |
+
)
|
| 80 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 81 |
+
else:
|
| 82 |
+
st.info("No payment data available.")
|
| 83 |
+
except Exception:
|
| 84 |
+
st.info("Error loading payment distribution.")
|
| 85 |
+
|
| 86 |
+
# ------------------- RECENT SALES + UPCOMING DEMOS -------------------
|
| 87 |
+
col1, col2 = st.columns(2)
|
| 88 |
+
|
| 89 |
+
# Recent Sales
|
| 90 |
+
with col1:
|
| 91 |
+
st.markdown("<h3 class='section-header'>Recent Sales</h3>", unsafe_allow_html=True)
|
| 92 |
+
recent_sales = db.get_dataframe(
|
| 93 |
+
"sales",
|
| 94 |
+
"""
|
| 95 |
+
SELECT s.invoice_no, c.name AS customer_name, c.village,
|
| 96 |
+
s.total_amount, s.sale_date
|
| 97 |
+
FROM sales s
|
| 98 |
+
JOIN customers c ON s.customer_id = c.customer_id
|
| 99 |
+
ORDER BY s.created_date DESC LIMIT 8
|
| 100 |
+
""",
|
| 101 |
+
)
|
| 102 |
+
if not recent_sales.empty:
|
| 103 |
+
st.dataframe(recent_sales, use_container_width=True, hide_index=True)
|
| 104 |
+
else:
|
| 105 |
+
st.info("No recent sales.")
|
| 106 |
+
|
| 107 |
+
# Upcoming Demos
|
| 108 |
+
with col2:
|
| 109 |
+
st.markdown("<h3 class='section-header'>Upcoming Demos</h3>", unsafe_allow_html=True)
|
| 110 |
+
|
| 111 |
+
upcoming_demos = db.get_dataframe(
|
| 112 |
+
"demos",
|
| 113 |
+
"""
|
| 114 |
+
SELECT d.demo_id, c.name AS customer_name, c.village,
|
| 115 |
+
p.product_name, d.demo_date, d.demo_time
|
| 116 |
+
FROM demos d
|
| 117 |
+
LEFT JOIN customers c ON d.customer_id = c.customer_id
|
| 118 |
+
LEFT JOIN products p ON d.product_id = p.product_id
|
| 119 |
+
WHERE date(d.demo_date) >= date('now')
|
| 120 |
+
AND LOWER(TRIM(d.conversion_status)) = 'scheduled'
|
| 121 |
+
ORDER BY d.demo_date ASC, d.demo_time ASC
|
| 122 |
+
LIMIT 8
|
| 123 |
+
""",
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
if not upcoming_demos.empty:
|
| 127 |
+
upcoming_demos.columns = ["Demo ID", "Customer", "Village", "Product", "Date", "Time"]
|
| 128 |
+
st.dataframe(upcoming_demos, use_container_width=True, hide_index=True)
|
| 129 |
+
|
| 130 |
+
if st.button("📋 View All Demos"):
|
| 131 |
+
st.session_state.current_page = "demos"
|
| 132 |
+
st.rerun()
|
| 133 |
+
else:
|
| 134 |
+
st.warning("⚠️ No upcoming demos scheduled.")
|
| 135 |
+
if st.button("➕ Schedule a Demo"):
|
| 136 |
+
st.session_state.current_page = "demos"
|
| 137 |
+
st.rerun()
|
pages/whatsapp.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# pages/whatsapp.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import pandas as pd
|
| 4 |
+
|
| 5 |
+
def show_whatsapp_page(db, whatsapp_manager):
|
| 6 |
+
"""Show WhatsApp messaging page"""
|
| 7 |
+
st.title("💬 WhatsApp Messaging")
|
| 8 |
+
|
| 9 |
+
if not whatsapp_manager:
|
| 10 |
+
st.error("WhatsApp manager not available. Please install pywhatkit: pip install pywhatkit")
|
| 11 |
+
st.info("""
|
| 12 |
+
**To enable WhatsApp messaging:**
|
| 13 |
+
1. Install: `pip install pywhatkit`
|
| 14 |
+
2. Make sure you're logged into WhatsApp Web in your default browser
|
| 15 |
+
3. Ensure phone numbers include country code (e.g., +91 for India)
|
| 16 |
+
""")
|
| 17 |
+
else:
|
| 18 |
+
tab1, tab2, tab3, tab4 = st.tabs(["Single Message", "Bulk Messages", "Templates", "Message History"])
|
| 19 |
+
|
| 20 |
+
with tab1:
|
| 21 |
+
show_single_message_tab(db, whatsapp_manager)
|
| 22 |
+
|
| 23 |
+
with tab2:
|
| 24 |
+
show_bulk_messages_tab(db, whatsapp_manager)
|
| 25 |
+
|
| 26 |
+
with tab3:
|
| 27 |
+
show_templates_tab()
|
| 28 |
+
|
| 29 |
+
with tab4:
|
| 30 |
+
show_message_history_tab(db)
|
| 31 |
+
|
| 32 |
+
def show_single_message_tab(db, whatsapp_manager):
|
| 33 |
+
"""Show single message tab"""
|
| 34 |
+
st.subheader("📱 Send Single Message")
|
| 35 |
+
|
| 36 |
+
col1, col2 = st.columns(2)
|
| 37 |
+
|
| 38 |
+
with col1:
|
| 39 |
+
# Option 1: Select from existing customers
|
| 40 |
+
st.write("**Select from Existing Customers**")
|
| 41 |
+
customers = db.get_dataframe('customers')
|
| 42 |
+
if not customers.empty:
|
| 43 |
+
customer_options = {f"{row['name']} ({row['mobile']}) - {row['village']}": row for _, row in customers.iterrows()}
|
| 44 |
+
selected_customer_key = st.selectbox("Choose Customer", options=[""] + list(customer_options.keys()))
|
| 45 |
+
|
| 46 |
+
if selected_customer_key:
|
| 47 |
+
customer_data = customer_options[selected_customer_key]
|
| 48 |
+
st.write(f"**Selected:** {customer_data['name']}")
|
| 49 |
+
st.write(f"**Mobile:** {customer_data['mobile']}")
|
| 50 |
+
st.write(f"**Village:** {customer_data['village']}")
|
| 51 |
+
|
| 52 |
+
# Pre-fill message with template
|
| 53 |
+
message_template = st.selectbox("Quick Template", [
|
| 54 |
+
"Custom Message",
|
| 55 |
+
"Payment Reminder",
|
| 56 |
+
"Demo Follow-up",
|
| 57 |
+
"New Product Announcement",
|
| 58 |
+
"Festival Greeting"
|
| 59 |
+
])
|
| 60 |
+
|
| 61 |
+
with col2:
|
| 62 |
+
# Option 2: Manual entry
|
| 63 |
+
st.write("**Or Enter Manually**")
|
| 64 |
+
manual_name = st.text_input("Recipient Name")
|
| 65 |
+
manual_mobile = st.text_input("Mobile Number (with country code)", placeholder="+91XXXXXXXXXX")
|
| 66 |
+
|
| 67 |
+
# Message content and sending logic would continue here...
|
| 68 |
+
# (I'll show the rest in the next message due to length)
|
utils/__init__.py
ADDED
|
File without changes
|
utils/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (138 Bytes). View file
|
|
|
utils/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (142 Bytes). View file
|
|
|
utils/__pycache__/helpers.cpython-310.pyc
ADDED
|
Binary file (1.63 kB). View file
|
|
|
utils/__pycache__/helpers.cpython-313.pyc
ADDED
|
Binary file (2.58 kB). View file
|
|
|
utils/__pycache__/styling.cpython-310.pyc
ADDED
|
Binary file (2.32 kB). View file
|
|
|
utils/__pycache__/styling.cpython-313.pyc
ADDED
|
Binary file (2.34 kB). View file
|
|
|