Update app.py
Browse files
app.py
CHANGED
|
@@ -1,9 +1,4 @@
|
|
| 1 |
-
#
|
| 2 |
-
# Förbättringar:
|
| 3 |
-
# 1. Robust verifiering som hanterar None vs '' korrekt
|
| 4 |
-
# 2. Förhindrar dubbla sparningar med saving_in_progress lock
|
| 5 |
-
# 3. Enklare och tydligare status-indikator
|
| 6 |
-
|
| 7 |
import streamlit as st
|
| 8 |
import gspread
|
| 9 |
from google.oauth2.service_account import Credentials
|
|
@@ -307,48 +302,35 @@ def show_admin_panel(log_sheets):
|
|
| 307 |
st.session_state.admin_authenticated = False
|
| 308 |
|
| 309 |
if not st.session_state.admin_authenticated:
|
| 310 |
-
# Visa
|
| 311 |
-
st.markdown("###
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
st.rerun()
|
| 320 |
-
else:
|
| 321 |
-
st.error("❌ Fel lösenord")
|
| 322 |
-
|
| 323 |
-
with col2:
|
| 324 |
-
if st.button("◀️ Tillbaka till huvudsidan"):
|
| 325 |
-
st.session_state.show_admin = False
|
| 326 |
st.rerun()
|
| 327 |
-
|
|
|
|
| 328 |
return
|
| 329 |
|
| 330 |
-
# Admin är inloggad - visa
|
| 331 |
-
st.
|
| 332 |
|
| 333 |
-
|
| 334 |
-
|
|
|
|
| 335 |
if st.button("🚪 Logga ut"):
|
| 336 |
st.session_state.admin_authenticated = False
|
| 337 |
-
st.session_state.show_admin = False
|
| 338 |
st.rerun()
|
| 339 |
|
| 340 |
-
|
| 341 |
-
if st.button("◀️ Tillbaka till huvudsidan"):
|
| 342 |
-
st.session_state.show_admin = False
|
| 343 |
-
st.rerun()
|
| 344 |
-
|
| 345 |
-
st.markdown("---")
|
| 346 |
-
|
| 347 |
-
# Tabbar för olika admin-funktioner
|
| 348 |
tab1, tab2, tab3 = st.tabs(["📊 Dashboard", "📋 Aktivitetslogg", "⚡ Performance"])
|
| 349 |
|
| 350 |
with tab1:
|
| 351 |
-
|
| 352 |
|
| 353 |
with tab2:
|
| 354 |
show_activity_log(log_sheets)
|
|
@@ -356,16 +338,16 @@ def show_admin_panel(log_sheets):
|
|
| 356 |
with tab3:
|
| 357 |
show_performance_log(log_sheets)
|
| 358 |
|
| 359 |
-
def
|
| 360 |
-
"""Visa
|
| 361 |
-
st.markdown("### 📊
|
| 362 |
|
| 363 |
if not log_sheets or not log_sheets.logs_sheet:
|
| 364 |
st.warning("⚠️ Ingen loggdata tillgänglig")
|
| 365 |
return
|
| 366 |
|
| 367 |
try:
|
| 368 |
-
# Hämta
|
| 369 |
all_records = log_sheets.logs_sheet.get_all_records()
|
| 370 |
|
| 371 |
if not all_records:
|
|
@@ -377,42 +359,43 @@ def show_admin_dashboard(log_sheets):
|
|
| 377 |
# Konvertera timestamp till datetime
|
| 378 |
df['Timestamp'] = pd.to_datetime(df['Timestamp'], errors='coerce')
|
| 379 |
|
| 380 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
col1, col2, col3, col4 = st.columns(4)
|
| 382 |
|
| 383 |
with col1:
|
| 384 |
-
|
| 385 |
-
st.metric("📝
|
| 386 |
|
| 387 |
with col2:
|
| 388 |
-
|
| 389 |
-
st.metric("
|
| 390 |
|
| 391 |
with col3:
|
| 392 |
-
|
| 393 |
-
st.metric("
|
| 394 |
|
| 395 |
with col4:
|
| 396 |
-
|
| 397 |
-
st.metric("
|
| 398 |
|
| 399 |
st.markdown("---")
|
| 400 |
|
| 401 |
-
#
|
| 402 |
-
st.markdown("###
|
| 403 |
event_counts = df['Event Type'].value_counts()
|
| 404 |
st.bar_chart(event_counts)
|
| 405 |
|
| 406 |
st.markdown("---")
|
| 407 |
|
| 408 |
-
#
|
| 409 |
-
st.markdown("###
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
use_container_width=True,
|
| 414 |
-
hide_index=True
|
| 415 |
-
)
|
| 416 |
|
| 417 |
st.markdown("---")
|
| 418 |
|
|
@@ -716,24 +699,100 @@ def get_total_outlets(company_areas):
|
|
| 716 |
total += int(float(str(val)))
|
| 717 |
except:
|
| 718 |
continue
|
| 719 |
-
|
| 720 |
return total
|
| 721 |
|
| 722 |
-
def
|
| 723 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 724 |
try:
|
| 725 |
# Ta bort dolda kolumner
|
| 726 |
export_df = company_areas.drop(columns=HIDDEN_COLUMNS, errors='ignore')
|
| 727 |
|
| 728 |
-
# Skapa
|
|
|
|
| 729 |
wb = Workbook()
|
| 730 |
ws = wb.active
|
| 731 |
ws.title = "ChargeNode Data"
|
| 732 |
|
| 733 |
# Färger
|
| 734 |
-
header_fill = PatternFill(start_color="
|
| 735 |
-
readonly_fill = PatternFill(start_color="
|
| 736 |
-
editable_fill = PatternFill(start_color="
|
| 737 |
|
| 738 |
# Lägg till headers
|
| 739 |
for col_idx, col_name in enumerate(export_df.columns, 1):
|
|
@@ -762,7 +821,6 @@ def export_to_excel(company_areas):
|
|
| 762 |
)
|
| 763 |
ws.column_dimensions[ws.cell(row=1, column=col_idx).column_letter].width = min(max_length + 2, 50)
|
| 764 |
|
| 765 |
-
output = io.BytesIO()
|
| 766 |
wb.save(output)
|
| 767 |
output.seek(0)
|
| 768 |
return output.getvalue()
|
|
@@ -840,18 +898,19 @@ def batch_update_cells_with_tracking(sheet, changes, df, editor_email, log_sheet
|
|
| 840 |
"""
|
| 841 |
Uppdatera flera celler samtidigt med ändringsloggning och omfattande felhantering
|
| 842 |
|
| 843 |
-
|
| 844 |
-
- Robust verifiering som hanterar None vs '' korrekt
|
| 845 |
-
- Bättre felhantering
|
| 846 |
"""
|
| 847 |
if not changes:
|
| 848 |
return True, "Inga ändringar att spara"
|
| 849 |
|
|
|
|
| 850 |
start_time = time.time()
|
|
|
|
|
|
|
| 851 |
print(f"[DEBUG] Försöker uppdatera {len(changes)} ändringar")
|
| 852 |
|
| 853 |
try:
|
| 854 |
-
# Test 1: Verifiera sheet
|
| 855 |
try:
|
| 856 |
headers = sheet.row_values(1)
|
| 857 |
if not headers:
|
|
@@ -863,19 +922,21 @@ def batch_update_cells_with_tracking(sheet, changes, df, editor_email, log_sheet
|
|
| 863 |
print(f"[ERROR] {error_msg}")
|
| 864 |
return False, error_msg
|
| 865 |
|
| 866 |
-
# Test 2: Verifiera kolumner
|
| 867 |
for change in changes:
|
| 868 |
col_name = change['column']
|
| 869 |
if col_name not in headers:
|
| 870 |
-
error_msg = f"❌ Kolumnen '{col_name}' finns inte i Google Sheets."
|
| 871 |
print(f"[ERROR] {error_msg}")
|
| 872 |
return False, error_msg
|
| 873 |
|
| 874 |
-
# Hitta kolumnindex för
|
| 875 |
last_update_col_idx = headers.index('Last update') + 1 if 'Last update' in headers else None
|
| 876 |
editor_col_idx = headers.index('Editor') + 1 if 'Editor' in headers else None
|
| 877 |
|
|
|
|
| 878 |
current_time = datetime.now(SWEDISH_TZ).strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
| 879 |
updates = []
|
| 880 |
|
| 881 |
# Förbered alla uppdateringar
|
|
@@ -884,6 +945,7 @@ def batch_update_cells_with_tracking(sheet, changes, df, editor_email, log_sheet
|
|
| 884 |
col_name = change['column']
|
| 885 |
new_value = change['new_value']
|
| 886 |
|
|
|
|
| 887 |
try:
|
| 888 |
df_position = df[df.index == row_idx].index[0]
|
| 889 |
sheet_row = df_position + 2
|
|
@@ -892,14 +954,18 @@ def batch_update_cells_with_tracking(sheet, changes, df, editor_email, log_sheet
|
|
| 892 |
print(f"[ERROR] {error_msg}")
|
| 893 |
return False, error_msg
|
| 894 |
|
|
|
|
| 895 |
if col_name in headers:
|
| 896 |
col_idx = headers.index(col_name) + 1
|
| 897 |
|
| 898 |
-
#
|
| 899 |
-
value_to_write =
|
| 900 |
if col_name == 'Telefon':
|
|
|
|
|
|
|
| 901 |
print(f"[DEBUG] Telefonnummer sparas som ren text: '{value_to_write}'")
|
| 902 |
|
|
|
|
| 903 |
updates.append({
|
| 904 |
'range': f'{gspread.utils.rowcol_to_a1(sheet_row, col_idx)}',
|
| 905 |
'values': [[value_to_write]]
|
|
@@ -911,7 +977,7 @@ def batch_update_cells_with_tracking(sheet, changes, df, editor_email, log_sheet
|
|
| 911 |
|
| 912 |
print(f"[DEBUG] Totalt {len(updates)} uppdateringar förberedda")
|
| 913 |
|
| 914 |
-
# Batch update med retry-logik
|
| 915 |
max_retries = 3
|
| 916 |
batch_size = 50
|
| 917 |
update_successful = False
|
|
@@ -924,9 +990,11 @@ def batch_update_cells_with_tracking(sheet, changes, df, editor_email, log_sheet
|
|
| 924 |
for i in range(0, len(updates), batch_size):
|
| 925 |
batch = updates[i:i + batch_size]
|
| 926 |
print(f"[DEBUG] Uppdaterar batch {i//batch_size + 1}: {len(batch)} celler")
|
|
|
|
| 927 |
sheet.batch_update(batch, value_input_option='RAW')
|
| 928 |
time.sleep(REQUEST_DELAY)
|
| 929 |
|
|
|
|
| 930 |
update_successful = True
|
| 931 |
print(f"[SUCCESS] Batch-uppdatering lyckades på försök {attempt + 1}")
|
| 932 |
break
|
|
@@ -936,10 +1004,11 @@ def batch_update_cells_with_tracking(sheet, changes, df, editor_email, log_sheet
|
|
| 936 |
print(f"[ERROR] API-fel på försök {attempt + 1}: {last_error}")
|
| 937 |
if attempt < max_retries - 1:
|
| 938 |
wait_time = (attempt + 1) * 2
|
| 939 |
-
print(f"[DEBUG] Väntar {wait_time} sekunder...")
|
| 940 |
time.sleep(wait_time)
|
|
|
|
| 941 |
else:
|
| 942 |
-
error_msg = f"❌ API-fel efter {max_retries} försök: {last_error}"
|
| 943 |
return False, error_msg
|
| 944 |
|
| 945 |
except Exception as e:
|
|
@@ -947,16 +1016,18 @@ def batch_update_cells_with_tracking(sheet, changes, df, editor_email, log_sheet
|
|
| 947 |
print(f"[ERROR] Oväntat fel på försök {attempt + 1}: {last_error}")
|
| 948 |
if attempt < max_retries - 1:
|
| 949 |
time.sleep(2)
|
|
|
|
| 950 |
else:
|
| 951 |
-
error_msg = f"❌ Oväntat fel: {last_error}"
|
| 952 |
return False, error_msg
|
| 953 |
|
|
|
|
| 954 |
if not update_successful:
|
| 955 |
-
error_msg = f"❌
|
| 956 |
print(f"[ERROR] {error_msg}")
|
| 957 |
return False, error_msg
|
| 958 |
|
| 959 |
-
# Uppdatera Last update och Editor
|
| 960 |
affected_rows = set([change['row_idx'] for change in changes])
|
| 961 |
tracking_updates = []
|
| 962 |
|
|
@@ -977,51 +1048,49 @@ def batch_update_cells_with_tracking(sheet, changes, df, editor_email, log_sheet
|
|
| 977 |
'values': [[editor_email]]
|
| 978 |
})
|
| 979 |
except Exception as e:
|
| 980 |
-
print(f"[WARNING] Kunde inte förbereda tracking för rad {row_idx}: {str(e)}")
|
| 981 |
|
| 982 |
if tracking_updates:
|
| 983 |
try:
|
| 984 |
print(f"[DEBUG] Uppdaterar {len(tracking_updates)} tracking-fält")
|
| 985 |
for i in range(0, len(tracking_updates), batch_size):
|
| 986 |
batch = tracking_updates[i:i + batch_size]
|
|
|
|
| 987 |
sheet.batch_update(batch, value_input_option='RAW')
|
| 988 |
time.sleep(REQUEST_DELAY)
|
| 989 |
print("[SUCCESS] Tracking-uppdatering lyckades")
|
| 990 |
except Exception as e:
|
|
|
|
|
|
|
| 991 |
print(f"[WARNING] Tracking-uppdatering misslyckades: {str(e)}")
|
|
|
|
| 992 |
|
| 993 |
-
#
|
| 994 |
-
# FÖRBÄTTRAD VERIFIERING - Hanterar None vs '' korrekt
|
| 995 |
-
# ============================================================================
|
| 996 |
try:
|
| 997 |
verify_change = changes[0]
|
| 998 |
df_position = df[df.index == verify_change['row_idx']].index[0]
|
| 999 |
sheet_row = df_position + 2
|
| 1000 |
col_idx = headers.index(verify_change['column']) + 1
|
| 1001 |
|
|
|
|
| 1002 |
cell_range = gspread.utils.rowcol_to_a1(sheet_row, col_idx)
|
| 1003 |
verified_value = sheet.acell(cell_range).value
|
| 1004 |
-
expected_value = verify_change['new_value']
|
| 1005 |
|
| 1006 |
-
|
| 1007 |
-
# Google Sheets returnerar None för tomma celler
|
| 1008 |
-
verified_normalized = '' if verified_value is None else str(verified_value)
|
| 1009 |
-
expected_normalized = '' if expected_value is None or expected_value == 'nan' else str(expected_value)
|
| 1010 |
|
| 1011 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1012 |
|
| 1013 |
-
|
| 1014 |
-
# Endast varning, inte kritiskt fel
|
| 1015 |
-
print(f"[WARNING] Verifieringsskillnad - kan bero på formateringsregler i Google Sheets")
|
| 1016 |
-
# Fortsätt ändå eftersom uppdateringen faktiskt lyckades
|
| 1017 |
-
else:
|
| 1018 |
-
print("[SUCCESS] Verifiering lyckades!")
|
| 1019 |
|
| 1020 |
except Exception as e:
|
| 1021 |
-
print(f"[WARNING] Kunde inte verifiera: {str(e)}")
|
| 1022 |
-
# Fortsätt ändå
|
| 1023 |
|
| 1024 |
-
# Logga
|
| 1025 |
if log_sheets and account_id:
|
| 1026 |
company_name = st.session_state.get('company_name', '')
|
| 1027 |
for change in changes:
|
|
@@ -1030,85 +1099,102 @@ def batch_update_cells_with_tracking(sheet, changes, df, editor_email, log_sheet
|
|
| 1030 |
'EDIT',
|
| 1031 |
account_id,
|
| 1032 |
company_name=company_name,
|
| 1033 |
-
|
|
|
|
| 1034 |
column=change['column'],
|
| 1035 |
old_value=change['old_value'],
|
| 1036 |
-
new_value=change['new_value']
|
|
|
|
| 1037 |
)
|
| 1038 |
|
| 1039 |
-
# Performance
|
| 1040 |
duration = time.time() - start_time
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
|
|
|
| 1048 |
|
| 1049 |
success_msg = f"✅ {len(changes)} ändringar har verifierats och sparats i Google Sheets"
|
| 1050 |
print(f"[SUCCESS] {success_msg}")
|
| 1051 |
return True, success_msg
|
| 1052 |
|
| 1053 |
except Exception as e:
|
| 1054 |
-
error_msg =
|
| 1055 |
-
print(f"[ERROR] {error_msg}")
|
| 1056 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1057 |
|
| 1058 |
def get_column_config(df):
|
| 1059 |
-
"""
|
| 1060 |
-
Skapa kolumnkonfiguration för st.data_editor med editerbara och readonly kolumner
|
| 1061 |
-
"""
|
| 1062 |
config = {}
|
| 1063 |
|
| 1064 |
-
# Beskrivningar för editerbara
|
| 1065 |
editable_descriptions = {
|
| 1066 |
-
'Namn': '
|
| 1067 |
-
'Email adress': 'Ange e-postadress till
|
| 1068 |
-
'Telefon': '
|
| 1069 |
-
'Tillgänglighet': '
|
| 1070 |
-
'Access info': '
|
| 1071 |
-
'Fritext meddelande': '
|
| 1072 |
-
'Main Contact': '
|
| 1073 |
}
|
| 1074 |
|
| 1075 |
-
# Beskrivningar för readonly
|
| 1076 |
readonly_descriptions = {
|
| 1077 |
-
'Fastighetsnamn': '
|
| 1078 |
-
'Address': '
|
| 1079 |
-
'Stad': '
|
| 1080 |
-
'Metod': '
|
| 1081 |
-
'# Uttag': 'Antal ladduttag
|
| 1082 |
-
'Hårdvara': '
|
| 1083 |
-
'Vecka': 'Planerad
|
| 1084 |
-
'Indikativt datum': '
|
| 1085 |
-
'Status': '
|
| 1086 |
-
'Last update': '
|
| 1087 |
-
'Editor': 'Senaste redigerare
|
| 1088 |
}
|
| 1089 |
|
| 1090 |
# Konfigurera editerbara kolumner
|
| 1091 |
for col in EDITABLE_COLUMNS:
|
| 1092 |
if col in df.columns:
|
| 1093 |
-
if col == '
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
help=editable_descriptions.get(col, ''),
|
| 1097 |
-
width="medium",
|
| 1098 |
-
required=False
|
| 1099 |
-
)
|
| 1100 |
-
elif col == 'Telefon':
|
| 1101 |
-
config[col] = st.column_config.TextColumn(
|
| 1102 |
col,
|
| 1103 |
help=editable_descriptions.get(col, ''),
|
| 1104 |
width="medium",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1105 |
required=False
|
| 1106 |
)
|
| 1107 |
elif col == 'Main Contact':
|
|
|
|
| 1108 |
config[col] = st.column_config.SelectboxColumn(
|
| 1109 |
col,
|
| 1110 |
help=editable_descriptions.get(col, ''),
|
| 1111 |
-
width="
|
| 1112 |
options=[
|
| 1113 |
"",
|
| 1114 |
"Huvudkontakt"
|
|
@@ -1371,44 +1457,35 @@ if 'log_sheets' not in st.session_state:
|
|
| 1371 |
if 'show_admin' not in st.session_state:
|
| 1372 |
st.session_state.show_admin = False
|
| 1373 |
|
| 1374 |
-
# ============================================================================
|
| 1375 |
-
# FÖRBÄTTRAD show_save_status_indicator - ENKLARE OCH MER ROBUST
|
| 1376 |
-
# ============================================================================
|
| 1377 |
-
|
| 1378 |
def show_save_status_indicator():
|
| 1379 |
-
"""
|
| 1380 |
-
Visa status-indikator för auto-save med timestamp
|
| 1381 |
-
FÖRBÄTTRING: Enklare logik, alltid visar senaste sparning korrekt
|
| 1382 |
-
"""
|
| 1383 |
if st.session_state.saving_in_progress:
|
| 1384 |
st.markdown("""
|
| 1385 |
<div class='auto-save-indicator saving'>
|
| 1386 |
-
💾 Sparar ändringar...
|
| 1387 |
</div>
|
| 1388 |
""", unsafe_allow_html=True)
|
| 1389 |
-
|
| 1390 |
elif st.session_state.last_save_time:
|
| 1391 |
-
# Visa alltid senaste sparning med timestamp
|
| 1392 |
-
timestamp = st.session_state.last_save_time.strftime('%Y-%m-%d %H:%M:%S')
|
| 1393 |
time_diff = (datetime.now() - st.session_state.last_save_time).total_seconds()
|
|
|
|
| 1394 |
|
| 1395 |
-
#
|
| 1396 |
-
if time_diff < 5:
|
| 1397 |
st.markdown(f"""
|
| 1398 |
<div class='auto-save-indicator saved'>
|
| 1399 |
-
✅
|
| 1400 |
</div>
|
| 1401 |
""", unsafe_allow_html=True)
|
| 1402 |
else:
|
| 1403 |
st.markdown(f"""
|
| 1404 |
<div class='auto-save-indicator'>
|
| 1405 |
-
💾 Senast sparad:
|
| 1406 |
</div>
|
| 1407 |
""", unsafe_allow_html=True)
|
| 1408 |
else:
|
| 1409 |
st.markdown("""
|
| 1410 |
<div class='auto-save-indicator'>
|
| 1411 |
-
💾
|
| 1412 |
</div>
|
| 1413 |
""", unsafe_allow_html=True)
|
| 1414 |
|
|
@@ -1422,229 +1499,326 @@ sheet, service_account_email, spreadsheet = get_google_sheet()
|
|
| 1422 |
if sheet is None:
|
| 1423 |
st.stop()
|
| 1424 |
|
| 1425 |
-
# Initiera LogSheets efter
|
| 1426 |
-
if st.session_state.log_sheets is None:
|
| 1427 |
try:
|
| 1428 |
-
print("[MONITORING] Initierar LogSheets...")
|
| 1429 |
st.session_state.log_sheets = LogSheets(spreadsheet)
|
| 1430 |
-
st.session_state.service_account_email = service_account_email
|
| 1431 |
print("[MONITORING] LogSheets initierat")
|
| 1432 |
except Exception as e:
|
| 1433 |
print(f"[MONITORING ERROR] Kunde inte initiera LogSheets: {e}")
|
| 1434 |
st.session_state.log_sheets = None
|
| 1435 |
|
| 1436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1437 |
|
| 1438 |
-
#
|
| 1439 |
if st.session_state.show_admin:
|
| 1440 |
-
|
| 1441 |
-
st.
|
| 1442 |
-
|
| 1443 |
-
|
| 1444 |
-
if st.session_state.df is None:
|
| 1445 |
-
start_time = time.time()
|
| 1446 |
-
st.session_state.df = get_sheet_data(sheet)
|
| 1447 |
-
duration = time.time() - start_time
|
| 1448 |
-
safe_log_performance(log_sheets, 'LOAD_DATA', duration)
|
| 1449 |
|
| 1450 |
-
|
| 1451 |
-
|
| 1452 |
-
|
| 1453 |
-
df = st.session_state.df
|
| 1454 |
|
| 1455 |
-
#
|
| 1456 |
st.markdown("""
|
| 1457 |
<div class='main-header'>
|
| 1458 |
<h1>⚡ ChargeNode Data Manager</h1>
|
| 1459 |
-
<p style='
|
| 1460 |
</div>
|
| 1461 |
""", unsafe_allow_html=True)
|
| 1462 |
|
| 1463 |
-
#
|
| 1464 |
-
|
| 1465 |
-
|
| 1466 |
-
if st.button("🔐 Admin Panel"):
|
| 1467 |
-
st.session_state.show_admin = True
|
| 1468 |
-
st.rerun()
|
| 1469 |
|
| 1470 |
-
#
|
| 1471 |
-
|
| 1472 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1473 |
|
|
|
|
| 1474 |
if not st.session_state.authenticated:
|
| 1475 |
-
st.markdown("<
|
| 1476 |
|
| 1477 |
-
|
|
|
|
| 1478 |
|
| 1479 |
-
|
| 1480 |
-
|
| 1481 |
-
|
| 1482 |
-
|
| 1483 |
-
|
| 1484 |
-
|
| 1485 |
-
|
| 1486 |
-
|
| 1487 |
-
|
| 1488 |
-
|
| 1489 |
-
|
| 1490 |
-
|
| 1491 |
-
|
| 1492 |
-
|
| 1493 |
-
|
| 1494 |
-
st.markdown("<br>", unsafe_allow_html=True)
|
| 1495 |
-
|
| 1496 |
-
if st.button("🔓 Logga in", type="primary", use_container_width=True):
|
| 1497 |
-
account_id = validate_password(df, password)
|
| 1498 |
-
|
| 1499 |
-
if account_id:
|
| 1500 |
-
st.session_state.authenticated = True
|
| 1501 |
-
st.session_state.account_id = account_id
|
| 1502 |
-
|
| 1503 |
-
# Hämta företagsnamn
|
| 1504 |
-
company_data = df[df['Account ID'] == account_id]
|
| 1505 |
-
if not company_data.empty and 'Företagsnamn' in company_data.columns:
|
| 1506 |
-
company_name = company_data.iloc[0]['Företagsnamn']
|
| 1507 |
-
st.session_state.company_name = company_name
|
| 1508 |
-
|
| 1509 |
-
# Logga inloggning
|
| 1510 |
-
safe_log_activity(
|
| 1511 |
-
log_sheets,
|
| 1512 |
-
'LOGIN',
|
| 1513 |
-
account_id,
|
| 1514 |
-
company_name=st.session_state.company_name
|
| 1515 |
-
)
|
| 1516 |
|
| 1517 |
-
|
| 1518 |
-
|
| 1519 |
-
|
| 1520 |
-
|
| 1521 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1522 |
|
| 1523 |
-
st.
|
| 1524 |
-
|
| 1525 |
-
#
|
| 1526 |
-
|
| 1527 |
-
|
| 1528 |
-
|
| 1529 |
-
|
| 1530 |
-
|
| 1531 |
-
|
| 1532 |
-
|
| 1533 |
-
|
| 1534 |
-
|
| 1535 |
-
if company_areas.empty:
|
| 1536 |
-
st.error("❌ Inga områden hittades för ditt Account ID")
|
| 1537 |
-
st.stop()
|
| 1538 |
-
|
| 1539 |
-
# Spara original data för jämförelse (första gången)
|
| 1540 |
-
if st.session_state.original_data is None:
|
| 1541 |
-
st.session_state.original_data = company_areas.drop(columns=HIDDEN_COLUMNS, errors='ignore').copy()
|
| 1542 |
-
|
| 1543 |
-
# Visa välkomstmeddelande
|
| 1544 |
-
col1, col2, col3 = st.columns([3, 1, 1])
|
| 1545 |
-
|
| 1546 |
-
with col1:
|
| 1547 |
-
st.markdown(f"### Välkommen, **{company_name}**! 👋")
|
| 1548 |
-
|
| 1549 |
-
with col2:
|
| 1550 |
-
if st.button("🚪 Logga ut"):
|
| 1551 |
-
# Logga utloggning
|
| 1552 |
-
safe_log_activity(log_sheets, 'LOGOUT', account_id, company_name=company_name)
|
| 1553 |
-
|
| 1554 |
-
st.session_state.authenticated = False
|
| 1555 |
-
st.session_state.account_id = None
|
| 1556 |
-
st.session_state.company_name = None
|
| 1557 |
-
st.session_state.original_data = None
|
| 1558 |
-
st.session_state.last_save_time = None
|
| 1559 |
-
st.rerun()
|
| 1560 |
-
|
| 1561 |
-
# Visa info och statistik
|
| 1562 |
-
total_outlets = get_total_outlets(company_areas)
|
| 1563 |
-
first_date, last_date = get_migration_interval(company_areas)
|
| 1564 |
-
|
| 1565 |
-
st.markdown("<br>", unsafe_allow_html=True)
|
| 1566 |
-
|
| 1567 |
-
# Info-box med migreringsinformation
|
| 1568 |
-
migration_info = f"""
|
| 1569 |
-
<div class='info-box'>
|
| 1570 |
-
<h3>📍 Din migreringsinformation</h3>
|
| 1571 |
-
<p><strong>Antal områden:</strong> {len(company_areas)}</p>
|
| 1572 |
-
<p><strong>Totalt antal ladduttag:</strong> {total_outlets}</p>
|
| 1573 |
-
"""
|
| 1574 |
|
| 1575 |
-
if first_date and last_date:
|
| 1576 |
-
migration_info += f"""
|
| 1577 |
-
<p><strong>Migrering börjar:</strong> {first_date.strftime('%Y-%m-%d')}</p>
|
| 1578 |
-
<p><strong>Migrering slutar:</strong> {last_date.strftime('%Y-%m-%d')}</p>
|
| 1579 |
-
"""
|
| 1580 |
else:
|
| 1581 |
-
|
| 1582 |
-
|
| 1583 |
-
""
|
| 1584 |
-
|
| 1585 |
-
|
| 1586 |
-
|
| 1587 |
-
|
| 1588 |
-
|
| 1589 |
-
|
| 1590 |
-
|
| 1591 |
-
st.
|
| 1592 |
-
|
| 1593 |
-
|
| 1594 |
-
|
| 1595 |
-
|
| 1596 |
-
|
| 1597 |
-
|
| 1598 |
-
|
| 1599 |
-
st.markdown("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1600 |
st.markdown("""
|
| 1601 |
-
<div class='
|
| 1602 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1603 |
</div>
|
| 1604 |
""", unsafe_allow_html=True)
|
| 1605 |
|
| 1606 |
-
|
| 1607 |
-
|
| 1608 |
-
|
| 1609 |
-
|
| 1610 |
-
|
| 1611 |
-
|
| 1612 |
-
|
| 1613 |
-
|
| 1614 |
-
|
|
|
|
|
|
|
|
|
|
| 1615 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1616 |
|
| 1617 |
-
|
| 1618 |
-
|
| 1619 |
-
|
| 1620 |
-
|
| 1621 |
-
|
| 1622 |
-
|
| 1623 |
-
|
| 1624 |
-
|
| 1625 |
-
|
| 1626 |
-
|
| 1627 |
-
|
| 1628 |
-
|
| 1629 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1630 |
st.markdown("""
|
| 1631 |
<div class='excel-info-box'>
|
| 1632 |
-
|
|
|
|
| 1633 |
</div>
|
| 1634 |
""", unsafe_allow_html=True)
|
| 1635 |
|
| 1636 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1637 |
|
| 1638 |
-
|
|
|
|
| 1639 |
start_time = time.time()
|
| 1640 |
-
|
| 1641 |
-
|
| 1642 |
-
|
| 1643 |
-
|
| 1644 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1645 |
|
| 1646 |
-
if
|
| 1647 |
-
|
|
|
|
|
|
|
| 1648 |
st.session_state.saving_in_progress = True
|
| 1649 |
|
| 1650 |
success, message = batch_update_cells_with_tracking(
|
|
@@ -1681,194 +1855,179 @@ with col_excel2:
|
|
| 1681 |
<small>Kontrollera din internetanslutning och försök igen.</small>
|
| 1682 |
</div>
|
| 1683 |
""", unsafe_allow_html=True)
|
| 1684 |
-
|
| 1685 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1686 |
|
| 1687 |
-
|
| 1688 |
-
|
| 1689 |
-
|
| 1690 |
-
|
| 1691 |
-
|
| 1692 |
-
|
| 1693 |
-
|
| 1694 |
-
|
| 1695 |
-
|
| 1696 |
-
|
| 1697 |
-
|
| 1698 |
-
|
| 1699 |
-
|
| 1700 |
-
|
| 1701 |
-
|
| 1702 |
-
|
| 1703 |
-
|
| 1704 |
-
|
| 1705 |
-
|
| 1706 |
-
|
| 1707 |
-
|
| 1708 |
-
|
| 1709 |
-
|
| 1710 |
-
|
| 1711 |
-
|
| 1712 |
-
|
| 1713 |
-
|
| 1714 |
-
|
| 1715 |
-
|
| 1716 |
-
|
| 1717 |
-
|
| 1718 |
-
|
| 1719 |
-
|
| 1720 |
-
|
| 1721 |
-
|
| 1722 |
-
|
| 1723 |
-
|
| 1724 |
-
|
| 1725 |
-
|
| 1726 |
-
<
|
| 1727 |
-
<
|
| 1728 |
-
|
| 1729 |
-
<li>Skriv din information i cellen</li>
|
| 1730 |
-
<li><strong>Tryck på Enter ELLER klicka i en annan cell</strong></li>
|
| 1731 |
-
<li>Informationen sparas automatiskt när du lämnar cellen</li>
|
| 1732 |
-
<li>Du ser ett grönt bekräftelsemeddelande när det är sparat ✅</li>
|
| 1733 |
-
</ol>
|
| 1734 |
-
<p style='margin: 0; margin-top: 0.8rem; font-weight: bold; color: #E65100;'>
|
| 1735 |
-
💡 Tips: Stäng inte sidan förrän du sett bekräftelsemeddelandet!
|
| 1736 |
-
</p>
|
| 1737 |
-
</div>
|
| 1738 |
-
""", unsafe_allow_html=True)
|
| 1739 |
-
|
| 1740 |
-
st.markdown("""
|
| 1741 |
-
Kolumner med **🔒** (gult hänglås) kan inte editeras. Klicka i andra fält för att redigera.
|
| 1742 |
-
|
| 1743 |
-
**Kontrollera att uppgifterna i låsta celler stämmer.** Om något verkar fel, mejla till [migration@chargenode.eu](mailto:migration@chargenode.eu) så hjälper vi dig.
|
| 1744 |
-
""")
|
| 1745 |
-
st.markdown("<br>", unsafe_allow_html=True)
|
| 1746 |
-
|
| 1747 |
-
# Ta bort dolda kolumner och sätt kolumnordning med Fastighetsnamn först
|
| 1748 |
-
company_areas_display = company_areas.drop(columns=HIDDEN_COLUMNS, errors='ignore')
|
| 1749 |
-
|
| 1750 |
-
# Skapa kolumnordning med Fastighetsnamn först (för bättre synlighet vid scrolling)
|
| 1751 |
-
column_order = ['Fastighetsnamn'] + [col for col in company_areas_display.columns if col != 'Fastighetsnamn']
|
| 1752 |
-
company_areas_display = company_areas_display[column_order]
|
| 1753 |
-
|
| 1754 |
-
column_config = get_column_config(company_areas_display)
|
| 1755 |
-
|
| 1756 |
-
# Status-indikator precis ovanför tabellen för bättre synlighet
|
| 1757 |
-
st.markdown("<br>", unsafe_allow_html=True)
|
| 1758 |
-
show_save_status_indicator()
|
| 1759 |
-
st.markdown("<br>", unsafe_allow_html=True)
|
| 1760 |
-
|
| 1761 |
-
# Container för att förhindra scroll-hopp
|
| 1762 |
-
table_container = st.container()
|
| 1763 |
-
|
| 1764 |
-
with table_container:
|
| 1765 |
-
# Visa tabellen med full höjd
|
| 1766 |
-
edited_data = st.data_editor(
|
| 1767 |
-
company_areas_display,
|
| 1768 |
-
column_config=column_config,
|
| 1769 |
-
use_container_width=True,
|
| 1770 |
-
num_rows="fixed",
|
| 1771 |
-
hide_index=True,
|
| 1772 |
-
key="data_editor",
|
| 1773 |
-
height=600 # Större höjd för bättre visning
|
| 1774 |
-
)
|
| 1775 |
-
|
| 1776 |
-
# ============================================================================
|
| 1777 |
-
# FÖRBÄTTRAD AUTO-SAVE LOGIK - Förhindrar dubbla sparningar
|
| 1778 |
-
# ============================================================================
|
| 1779 |
-
|
| 1780 |
-
# Auto-save med robust felhantering OCH locking
|
| 1781 |
-
if (st.session_state.original_data is not None and
|
| 1782 |
-
not st.session_state.saving_in_progress): # Förhindra parallella sparningar
|
| 1783 |
|
| 1784 |
-
|
|
|
|
| 1785 |
|
| 1786 |
-
|
| 1787 |
-
|
| 1788 |
-
|
| 1789 |
-
print(f"[DEBUG] Original data rader: {len(st.session_state.original_data)}, Edited data rader: {len(edited_data)}")
|
| 1790 |
|
| 1791 |
-
|
| 1792 |
-
|
| 1793 |
-
|
| 1794 |
-
|
| 1795 |
-
|
| 1796 |
-
|
| 1797 |
-
|
| 1798 |
-
|
| 1799 |
-
|
| 1800 |
-
|
| 1801 |
-
|
| 1802 |
-
|
| 1803 |
-
|
| 1804 |
-
|
| 1805 |
-
|
| 1806 |
-
|
| 1807 |
-
st.error(f"Fel vid jämförelse av {col} på rad {idx}: {e}")
|
| 1808 |
-
print(f"[ERROR] Fel vid jämförelse av {col} på rad {idx}: {e}")
|
| 1809 |
|
| 1810 |
-
|
| 1811 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1812 |
|
| 1813 |
-
|
| 1814 |
-
|
| 1815 |
-
|
| 1816 |
|
| 1817 |
-
|
| 1818 |
-
|
| 1819 |
-
|
| 1820 |
-
|
| 1821 |
-
|
| 1822 |
-
|
| 1823 |
-
|
| 1824 |
-
|
| 1825 |
-
|
| 1826 |
-
|
| 1827 |
-
|
| 1828 |
-
|
| 1829 |
-
|
| 1830 |
-
|
| 1831 |
-
|
| 1832 |
-
|
| 1833 |
-
|
| 1834 |
-
|
| 1835 |
-
|
| 1836 |
-
|
| 1837 |
-
|
| 1838 |
-
|
| 1839 |
-
|
| 1840 |
-
|
| 1841 |
-
|
| 1842 |
-
|
| 1843 |
-
|
| 1844 |
-
|
| 1845 |
-
|
|
|
|
|
|
|
| 1846 |
|
| 1847 |
-
#
|
| 1848 |
-
|
| 1849 |
-
|
| 1850 |
-
|
| 1851 |
-
|
| 1852 |
-
|
| 1853 |
-
|
| 1854 |
-
|
|
|
|
| 1855 |
|
| 1856 |
-
|
| 1857 |
-
|
| 1858 |
-
|
| 1859 |
-
|
| 1860 |
-
|
| 1861 |
-
|
| 1862 |
-
|
| 1863 |
-
|
| 1864 |
-
st.
|
| 1865 |
-
|
| 1866 |
-
|
| 1867 |
-
|
| 1868 |
-
|
| 1869 |
-
|
| 1870 |
-
|
| 1871 |
-
|
| 1872 |
-
|
| 1873 |
-
|
| 1874 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# App funkade med admin panel.Dock loggar den inte tillbaka data i låsta fält.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import streamlit as st
|
| 3 |
import gspread
|
| 4 |
from google.oauth2.service_account import Credentials
|
|
|
|
| 302 |
st.session_state.admin_authenticated = False
|
| 303 |
|
| 304 |
if not st.session_state.admin_authenticated:
|
| 305 |
+
# Visa login-formulär
|
| 306 |
+
st.markdown("### Inloggning krävs")
|
| 307 |
+
password = st.text_input("Admin-lösenord", type="password", key="admin_password")
|
| 308 |
+
|
| 309 |
+
if st.button("Logga in som Admin"):
|
| 310 |
+
if password == ADMIN_PASSWORD:
|
| 311 |
+
st.session_state.admin_authenticated = True
|
| 312 |
+
st.success("✅ Admin-inloggning lyckades!")
|
| 313 |
+
time.sleep(0.5)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
st.rerun()
|
| 315 |
+
else:
|
| 316 |
+
st.error("❌ Felaktigt lösenord")
|
| 317 |
return
|
| 318 |
|
| 319 |
+
# Admin är inloggad - visa dashboard
|
| 320 |
+
st.markdown("---")
|
| 321 |
|
| 322 |
+
# Logout knapp
|
| 323 |
+
col1, col2 = st.columns([4, 1])
|
| 324 |
+
with col2:
|
| 325 |
if st.button("🚪 Logga ut"):
|
| 326 |
st.session_state.admin_authenticated = False
|
|
|
|
| 327 |
st.rerun()
|
| 328 |
|
| 329 |
+
# Tabs för olika vyer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
tab1, tab2, tab3 = st.tabs(["📊 Dashboard", "📋 Aktivitetslogg", "⚡ Performance"])
|
| 331 |
|
| 332 |
with tab1:
|
| 333 |
+
show_dashboard(log_sheets)
|
| 334 |
|
| 335 |
with tab2:
|
| 336 |
show_activity_log(log_sheets)
|
|
|
|
| 338 |
with tab3:
|
| 339 |
show_performance_log(log_sheets)
|
| 340 |
|
| 341 |
+
def show_dashboard(log_sheets):
|
| 342 |
+
"""Visa realtidsstatistik dashboard"""
|
| 343 |
+
st.markdown("### 📊 Realtidsstatistik")
|
| 344 |
|
| 345 |
if not log_sheets or not log_sheets.logs_sheet:
|
| 346 |
st.warning("⚠️ Ingen loggdata tillgänglig")
|
| 347 |
return
|
| 348 |
|
| 349 |
try:
|
| 350 |
+
# Hämta loggdata
|
| 351 |
all_records = log_sheets.logs_sheet.get_all_records()
|
| 352 |
|
| 353 |
if not all_records:
|
|
|
|
| 359 |
# Konvertera timestamp till datetime
|
| 360 |
df['Timestamp'] = pd.to_datetime(df['Timestamp'], errors='coerce')
|
| 361 |
|
| 362 |
+
# Dagens datum
|
| 363 |
+
today = datetime.now(SWEDISH_TZ).date()
|
| 364 |
+
df['Date'] = df['Timestamp'].dt.date
|
| 365 |
+
|
| 366 |
+
# Metrics
|
| 367 |
col1, col2, col3, col4 = st.columns(4)
|
| 368 |
|
| 369 |
with col1:
|
| 370 |
+
total_events = len(df)
|
| 371 |
+
st.metric("📝 Totala händelser", f"{total_events:,}")
|
| 372 |
|
| 373 |
with col2:
|
| 374 |
+
today_events = len(df[df['Date'] == today])
|
| 375 |
+
st.metric("📅 Händelser idag", f"{today_events:,}")
|
| 376 |
|
| 377 |
with col3:
|
| 378 |
+
unique_users = df['User ID'].nunique()
|
| 379 |
+
st.metric("👥 Unika användare", f"{unique_users:,}")
|
| 380 |
|
| 381 |
with col4:
|
| 382 |
+
total_edits = len(df[df['Event Type'] == 'EDIT'])
|
| 383 |
+
st.metric("✏️ Totala redigeringar", f"{total_edits:,}")
|
| 384 |
|
| 385 |
st.markdown("---")
|
| 386 |
|
| 387 |
+
# Händelser per typ
|
| 388 |
+
st.markdown("### 📊 Händelser per typ")
|
| 389 |
event_counts = df['Event Type'].value_counts()
|
| 390 |
st.bar_chart(event_counts)
|
| 391 |
|
| 392 |
st.markdown("---")
|
| 393 |
|
| 394 |
+
# Aktivitet över tid (senaste 7 dagarna)
|
| 395 |
+
st.markdown("### 📈 Aktivitet senaste 7 dagarna")
|
| 396 |
+
last_7_days = df[df['Date'] >= (today - pd.Timedelta(days=7))]
|
| 397 |
+
daily_activity = last_7_days.groupby('Date').size()
|
| 398 |
+
st.line_chart(daily_activity)
|
|
|
|
|
|
|
|
|
|
| 399 |
|
| 400 |
st.markdown("---")
|
| 401 |
|
|
|
|
| 699 |
total += int(float(str(val)))
|
| 700 |
except:
|
| 701 |
continue
|
|
|
|
| 702 |
return total
|
| 703 |
|
| 704 |
+
def get_hardware_summary(company_areas):
|
| 705 |
+
"""Hämta sammanfattning av hårdvara"""
|
| 706 |
+
if 'Hårdvara' not in company_areas.columns:
|
| 707 |
+
return "Information saknas"
|
| 708 |
+
|
| 709 |
+
# Samla alla hårdvaror i en lista
|
| 710 |
+
hardware_list = []
|
| 711 |
+
for val in company_areas['Hårdvara']:
|
| 712 |
+
if val and str(val).strip() != '' and str(val).strip() != '0':
|
| 713 |
+
hardware_list.append(str(val).strip())
|
| 714 |
+
|
| 715 |
+
if not hardware_list:
|
| 716 |
+
return "Information saknas"
|
| 717 |
+
|
| 718 |
+
# Om alla är samma, visa bara den hårdvaran en gång
|
| 719 |
+
if len(set(hardware_list)) == 1:
|
| 720 |
+
return hardware_list[0]
|
| 721 |
+
|
| 722 |
+
# Annars visa alla unika hårdvaror separerade med komma
|
| 723 |
+
unique_hardware = list(set(hardware_list))
|
| 724 |
+
return ", ".join(unique_hardware)
|
| 725 |
+
|
| 726 |
+
def get_main_contact_name(company_areas):
|
| 727 |
+
"""Hämta namnet på huvudkontakten"""
|
| 728 |
+
if 'Main Contact' not in company_areas.columns or 'Namn' not in company_areas.columns:
|
| 729 |
+
return "Ej angiven"
|
| 730 |
+
|
| 731 |
+
# Hitta raden där Main Contact = "Huvudkontakt"
|
| 732 |
+
main_contacts = company_areas[company_areas['Main Contact'] == 'Huvudkontakt']
|
| 733 |
+
|
| 734 |
+
if not main_contacts.empty:
|
| 735 |
+
name = main_contacts.iloc[0]['Namn']
|
| 736 |
+
if name and str(name).strip() != '':
|
| 737 |
+
return str(name).strip()
|
| 738 |
+
|
| 739 |
+
return "Ej angiven"
|
| 740 |
+
|
| 741 |
+
def get_migration_summary(company_areas):
|
| 742 |
+
"""Hämta sammanfattning av migrationsmetod"""
|
| 743 |
+
if 'Metod' not in company_areas.columns:
|
| 744 |
+
return "Information saknas"
|
| 745 |
+
|
| 746 |
+
# Hämta metoder
|
| 747 |
+
methods = []
|
| 748 |
+
for val in company_areas['Metod']:
|
| 749 |
+
if val and str(val).strip() != '' and str(val).strip() != '0':
|
| 750 |
+
methods.append(str(val).strip())
|
| 751 |
+
|
| 752 |
+
if not methods:
|
| 753 |
+
return "Information saknas"
|
| 754 |
+
|
| 755 |
+
# Räkna områden per metod
|
| 756 |
+
from collections import Counter
|
| 757 |
+
method_counts = Counter(methods)
|
| 758 |
+
|
| 759 |
+
# Räkna totalt antal uttag
|
| 760 |
+
total_outlets = get_total_outlets(company_areas)
|
| 761 |
+
|
| 762 |
+
# Räkna antal unika orter
|
| 763 |
+
unique_cities = set()
|
| 764 |
+
if 'Stad' in company_areas.columns:
|
| 765 |
+
for val in company_areas['Stad']:
|
| 766 |
+
if val and str(val).strip() != '' and str(val).strip() != '0':
|
| 767 |
+
unique_cities.add(str(val).strip())
|
| 768 |
+
|
| 769 |
+
num_cities = len(unique_cities) if unique_cities else 1
|
| 770 |
+
|
| 771 |
+
# Skapa formaterad text
|
| 772 |
+
summary_parts = []
|
| 773 |
+
for method, count in method_counts.items():
|
| 774 |
+
summary_parts.append(f"{count}st {method} migrationer")
|
| 775 |
+
|
| 776 |
+
methods_text = ", ".join(summary_parts)
|
| 777 |
+
|
| 778 |
+
return f"{methods_text} om totalt {total_outlets} uttag på {num_cities} {'ort' if num_cities == 1 else 'orter'}"
|
| 779 |
+
|
| 780 |
+
def export_to_excel(company_areas, account_id):
|
| 781 |
+
"""Exportera data till Excel med formatering"""
|
| 782 |
try:
|
| 783 |
# Ta bort dolda kolumner
|
| 784 |
export_df = company_areas.drop(columns=HIDDEN_COLUMNS, errors='ignore')
|
| 785 |
|
| 786 |
+
# Skapa Excel-fil
|
| 787 |
+
output = io.BytesIO()
|
| 788 |
wb = Workbook()
|
| 789 |
ws = wb.active
|
| 790 |
ws.title = "ChargeNode Data"
|
| 791 |
|
| 792 |
# Färger
|
| 793 |
+
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
| 794 |
+
readonly_fill = PatternFill(start_color="E7E6E6", end_color="E7E6E6", fill_type="solid")
|
| 795 |
+
editable_fill = PatternFill(start_color="FFFFFF", end_color="FFFFFF", fill_type="solid")
|
| 796 |
|
| 797 |
# Lägg till headers
|
| 798 |
for col_idx, col_name in enumerate(export_df.columns, 1):
|
|
|
|
| 821 |
)
|
| 822 |
ws.column_dimensions[ws.cell(row=1, column=col_idx).column_letter].width = min(max_length + 2, 50)
|
| 823 |
|
|
|
|
| 824 |
wb.save(output)
|
| 825 |
output.seek(0)
|
| 826 |
return output.getvalue()
|
|
|
|
| 898 |
"""
|
| 899 |
Uppdatera flera celler samtidigt med ändringsloggning och omfattande felhantering
|
| 900 |
|
| 901 |
+
MONITORING: Loggar alla ändringar till Google Sheets
|
|
|
|
|
|
|
| 902 |
"""
|
| 903 |
if not changes:
|
| 904 |
return True, "Inga ändringar att spara"
|
| 905 |
|
| 906 |
+
# Performance tracking start
|
| 907 |
start_time = time.time()
|
| 908 |
+
|
| 909 |
+
# Logga för debugging
|
| 910 |
print(f"[DEBUG] Försöker uppdatera {len(changes)} ändringar")
|
| 911 |
|
| 912 |
try:
|
| 913 |
+
# Test 1: Verifiera att sheet är tillgängligt
|
| 914 |
try:
|
| 915 |
headers = sheet.row_values(1)
|
| 916 |
if not headers:
|
|
|
|
| 922 |
print(f"[ERROR] {error_msg}")
|
| 923 |
return False, error_msg
|
| 924 |
|
| 925 |
+
# Test 2: Verifiera att alla kolumner finns
|
| 926 |
for change in changes:
|
| 927 |
col_name = change['column']
|
| 928 |
if col_name not in headers:
|
| 929 |
+
error_msg = f"❌ Kolumnen '{col_name}' finns inte i Google Sheets. Kontakta migration@chargenode.eu"
|
| 930 |
print(f"[ERROR] {error_msg}")
|
| 931 |
return False, error_msg
|
| 932 |
|
| 933 |
+
# Hitta kolumnindex för Last update och Editor
|
| 934 |
last_update_col_idx = headers.index('Last update') + 1 if 'Last update' in headers else None
|
| 935 |
editor_col_idx = headers.index('Editor') + 1 if 'Editor' in headers else None
|
| 936 |
|
| 937 |
+
# Svensk tid för timestamp
|
| 938 |
current_time = datetime.now(SWEDISH_TZ).strftime('%Y-%m-%d %H:%M:%S')
|
| 939 |
+
|
| 940 |
updates = []
|
| 941 |
|
| 942 |
# Förbered alla uppdateringar
|
|
|
|
| 945 |
col_name = change['column']
|
| 946 |
new_value = change['new_value']
|
| 947 |
|
| 948 |
+
# Hitta rätt rad i sheetet
|
| 949 |
try:
|
| 950 |
df_position = df[df.index == row_idx].index[0]
|
| 951 |
sheet_row = df_position + 2
|
|
|
|
| 954 |
print(f"[ERROR] {error_msg}")
|
| 955 |
return False, error_msg
|
| 956 |
|
| 957 |
+
# Hitta kolumnindex
|
| 958 |
if col_name in headers:
|
| 959 |
col_idx = headers.index(col_name) + 1
|
| 960 |
|
| 961 |
+
# Speciell hantering för Telefon-kolumnen: ALLTID som ren text
|
| 962 |
+
value_to_write = new_value
|
| 963 |
if col_name == 'Telefon':
|
| 964 |
+
# Skicka som ren text, ingen apostrof behövs
|
| 965 |
+
value_to_write = str(new_value) if new_value else ''
|
| 966 |
print(f"[DEBUG] Telefonnummer sparas som ren text: '{value_to_write}'")
|
| 967 |
|
| 968 |
+
# Lägg till huvuduppdateringen
|
| 969 |
updates.append({
|
| 970 |
'range': f'{gspread.utils.rowcol_to_a1(sheet_row, col_idx)}',
|
| 971 |
'values': [[value_to_write]]
|
|
|
|
| 977 |
|
| 978 |
print(f"[DEBUG] Totalt {len(updates)} uppdateringar förberedda")
|
| 979 |
|
| 980 |
+
# Batch update alla ändringar med retry-logik
|
| 981 |
max_retries = 3
|
| 982 |
batch_size = 50
|
| 983 |
update_successful = False
|
|
|
|
| 990 |
for i in range(0, len(updates), batch_size):
|
| 991 |
batch = updates[i:i + batch_size]
|
| 992 |
print(f"[DEBUG] Uppdaterar batch {i//batch_size + 1}: {len(batch)} celler")
|
| 993 |
+
# Använd 'RAW' för att behandla all data som ren text (behåller ledande nollor)
|
| 994 |
sheet.batch_update(batch, value_input_option='RAW')
|
| 995 |
time.sleep(REQUEST_DELAY)
|
| 996 |
|
| 997 |
+
# Om vi kom hit så lyckades uppdateringen
|
| 998 |
update_successful = True
|
| 999 |
print(f"[SUCCESS] Batch-uppdatering lyckades på försök {attempt + 1}")
|
| 1000 |
break
|
|
|
|
| 1004 |
print(f"[ERROR] API-fel på försök {attempt + 1}: {last_error}")
|
| 1005 |
if attempt < max_retries - 1:
|
| 1006 |
wait_time = (attempt + 1) * 2
|
| 1007 |
+
print(f"[DEBUG] Väntar {wait_time} sekunder innan nästa försök...")
|
| 1008 |
time.sleep(wait_time)
|
| 1009 |
+
continue
|
| 1010 |
else:
|
| 1011 |
+
error_msg = f"❌ API-fel efter {max_retries} försök: {last_error}. Vänta en minut och försök igen."
|
| 1012 |
return False, error_msg
|
| 1013 |
|
| 1014 |
except Exception as e:
|
|
|
|
| 1016 |
print(f"[ERROR] Oväntat fel på försök {attempt + 1}: {last_error}")
|
| 1017 |
if attempt < max_retries - 1:
|
| 1018 |
time.sleep(2)
|
| 1019 |
+
continue
|
| 1020 |
else:
|
| 1021 |
+
error_msg = f"❌ Oväntat fel vid uppdatering: {last_error}"
|
| 1022 |
return False, error_msg
|
| 1023 |
|
| 1024 |
+
# KRITISK CHECK: Verifiera att uppdateringen lyckades
|
| 1025 |
if not update_successful:
|
| 1026 |
+
error_msg = f"❌ Uppdateringen misslyckades efter {max_retries} försök. Senaste fel: {last_error}"
|
| 1027 |
print(f"[ERROR] {error_msg}")
|
| 1028 |
return False, error_msg
|
| 1029 |
|
| 1030 |
+
# Uppdatera Last update och Editor för alla påverkade rader
|
| 1031 |
affected_rows = set([change['row_idx'] for change in changes])
|
| 1032 |
tracking_updates = []
|
| 1033 |
|
|
|
|
| 1048 |
'values': [[editor_email]]
|
| 1049 |
})
|
| 1050 |
except Exception as e:
|
| 1051 |
+
print(f"[WARNING] Kunde inte förbereda tracking update för rad {row_idx}: {str(e)}")
|
| 1052 |
|
| 1053 |
if tracking_updates:
|
| 1054 |
try:
|
| 1055 |
print(f"[DEBUG] Uppdaterar {len(tracking_updates)} tracking-fält")
|
| 1056 |
for i in range(0, len(tracking_updates), batch_size):
|
| 1057 |
batch = tracking_updates[i:i + batch_size]
|
| 1058 |
+
# Använd 'RAW' för ren text
|
| 1059 |
sheet.batch_update(batch, value_input_option='RAW')
|
| 1060 |
time.sleep(REQUEST_DELAY)
|
| 1061 |
print("[SUCCESS] Tracking-uppdatering lyckades")
|
| 1062 |
except Exception as e:
|
| 1063 |
+
# Om timestamp-uppdateringen misslyckas, fortsätt ändå
|
| 1064 |
+
# eftersom huvuddatan är sparad
|
| 1065 |
print(f"[WARNING] Tracking-uppdatering misslyckades: {str(e)}")
|
| 1066 |
+
pass
|
| 1067 |
|
| 1068 |
+
# FINAL VERIFICATION: Läs tillbaka en av ändringarna för att verifiera
|
|
|
|
|
|
|
| 1069 |
try:
|
| 1070 |
verify_change = changes[0]
|
| 1071 |
df_position = df[df.index == verify_change['row_idx']].index[0]
|
| 1072 |
sheet_row = df_position + 2
|
| 1073 |
col_idx = headers.index(verify_change['column']) + 1
|
| 1074 |
|
| 1075 |
+
# Läs tillbaka värdet
|
| 1076 |
cell_range = gspread.utils.rowcol_to_a1(sheet_row, col_idx)
|
| 1077 |
verified_value = sheet.acell(cell_range).value
|
|
|
|
| 1078 |
|
| 1079 |
+
print(f"[DEBUG] Verifiering: Förväntade '{verify_change['new_value']}', fick '{verified_value}'")
|
|
|
|
|
|
|
|
|
|
| 1080 |
|
| 1081 |
+
# Jämför värdena (som strängar)
|
| 1082 |
+
if str(verified_value) != str(verify_change['new_value']):
|
| 1083 |
+
error_msg = f"⚠️ VARNING: Verifiering misslyckades! Värdet sparades möjligen inte korrekt. Kontakta migration@chargenode.eu"
|
| 1084 |
+
print(f"[ERROR] {error_msg}")
|
| 1085 |
+
return False, error_msg
|
| 1086 |
|
| 1087 |
+
print("[SUCCESS] Verifiering lyckades - data är sparad!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1088 |
|
| 1089 |
except Exception as e:
|
| 1090 |
+
print(f"[WARNING] Kunde inte verifiera uppdatering: {str(e)}")
|
| 1091 |
+
# Fortsätt ändå eftersom huvuduppdateringen lyckades
|
| 1092 |
|
| 1093 |
+
# MONITORING: Logga alla redigeringar
|
| 1094 |
if log_sheets and account_id:
|
| 1095 |
company_name = st.session_state.get('company_name', '')
|
| 1096 |
for change in changes:
|
|
|
|
| 1099 |
'EDIT',
|
| 1100 |
account_id,
|
| 1101 |
company_name=company_name,
|
| 1102 |
+
details=f"Redigerade {change['column']}",
|
| 1103 |
+
row_number=str(change['row_idx']),
|
| 1104 |
column=change['column'],
|
| 1105 |
old_value=change['old_value'],
|
| 1106 |
+
new_value=change['new_value'],
|
| 1107 |
+
status='SUCCESS'
|
| 1108 |
)
|
| 1109 |
|
| 1110 |
+
# Performance tracking end
|
| 1111 |
duration = time.time() - start_time
|
| 1112 |
+
if log_sheets and account_id:
|
| 1113 |
+
safe_log_performance(
|
| 1114 |
+
log_sheets,
|
| 1115 |
+
'BATCH_UPDATE',
|
| 1116 |
+
duration,
|
| 1117 |
+
user_id=account_id,
|
| 1118 |
+
details=f"{len(changes)} ändringar"
|
| 1119 |
+
)
|
| 1120 |
|
| 1121 |
success_msg = f"✅ {len(changes)} ändringar har verifierats och sparats i Google Sheets"
|
| 1122 |
print(f"[SUCCESS] {success_msg}")
|
| 1123 |
return True, success_msg
|
| 1124 |
|
| 1125 |
except Exception as e:
|
| 1126 |
+
error_msg = str(e)
|
| 1127 |
+
print(f"[ERROR] Oväntat exception: {error_msg}")
|
| 1128 |
+
|
| 1129 |
+
# MONITORING: Logga fel
|
| 1130 |
+
if log_sheets and account_id:
|
| 1131 |
+
safe_log_activity(
|
| 1132 |
+
log_sheets,
|
| 1133 |
+
'EDIT',
|
| 1134 |
+
account_id,
|
| 1135 |
+
details=f"Fel vid batch update: {error_msg}",
|
| 1136 |
+
status='ERROR'
|
| 1137 |
+
)
|
| 1138 |
+
|
| 1139 |
+
if "quota" in error_msg.lower() or "rate" in error_msg.lower():
|
| 1140 |
+
return False, "❌ För många förfrågningar. Vänta 1 minut och försök igen."
|
| 1141 |
+
elif "permission" in error_msg.lower() or "access" in error_msg.lower():
|
| 1142 |
+
return False, "❌ Åtkomstfel. Kontakta migration@chargenode.eu"
|
| 1143 |
+
else:
|
| 1144 |
+
return False, f"❌ Fel vid sparande: {error_msg}. Kontakta migration@chargenode.eu om problemet kvarstår."
|
| 1145 |
|
| 1146 |
def get_column_config(df):
|
| 1147 |
+
"""Generera kolumnkonfiguration för data_editor med beskrivningar och dropdowns"""
|
|
|
|
|
|
|
| 1148 |
config = {}
|
| 1149 |
|
| 1150 |
+
# Beskrivningar för editerbara kolumner
|
| 1151 |
editable_descriptions = {
|
| 1152 |
+
'Namn': 'Huvudkontaktperson för detta område. Personen vi kontaktar vid frågor eller problem.',
|
| 1153 |
+
'Email adress': 'Ange individuell e-postadress till kontaktpersonen för detta område.',
|
| 1154 |
+
'Telefon': 'Telefonnummer till huvudkontaktpersonen. Format: +46XXXXXXXXX eller 07X-XXXXXXX',
|
| 1155 |
+
'Tillgänglighet': 'Välj i dropdown-menyn om detta är ett område som vi alltid kan komma åt, eller om det är stängt. Om det är stängt var vänlig fyll i fältet Access info.',
|
| 1156 |
+
'Access info': 'Om området inte alltid är tillgängligt, vänligen ange hur vi kan få tillgång. T.ex. "Nycklar finns i receptionen" eller "Ring Pelle på 07XXXXXXXX när ni är på gång".',
|
| 1157 |
+
'Fritext meddelande': 'Övrig information eller meddelanden som är viktiga för oss att veta.',
|
| 1158 |
+
'Main Contact': 'Ange om denna person är huvudansvarig för kontot. Välj "Huvudkontakt" för helst endast en person per företag.'
|
| 1159 |
}
|
| 1160 |
|
| 1161 |
+
# Beskrivningar för readonly kolumner
|
| 1162 |
readonly_descriptions = {
|
| 1163 |
+
'Fastighetsnamn': 'Namnet på fastigheten där laddstationerna finns',
|
| 1164 |
+
'Address': 'Gatuadress för området',
|
| 1165 |
+
'Stad': 'Ort/stad där området finns',
|
| 1166 |
+
'Metod': 'Migrationsmetod som används',
|
| 1167 |
+
'# Uttag': 'Antal ladduttag på området',
|
| 1168 |
+
'Hårdvara': 'Typ av laddstationer',
|
| 1169 |
+
'Vecka': 'Planerad vecka för migrering',
|
| 1170 |
+
'Indikativt datum': 'Planerat datum för migrering',
|
| 1171 |
+
'Status': 'Nuvarande status för området',
|
| 1172 |
+
'Last update': 'Senaste uppdatering',
|
| 1173 |
+
'Editor': 'Senaste redigerare'
|
| 1174 |
}
|
| 1175 |
|
| 1176 |
# Konfigurera editerbara kolumner
|
| 1177 |
for col in EDITABLE_COLUMNS:
|
| 1178 |
if col in df.columns:
|
| 1179 |
+
if col == 'Tillgänglighet':
|
| 1180 |
+
# Dropdown för tillgänglighet - tillåter tom värde
|
| 1181 |
+
config[col] = st.column_config.SelectboxColumn(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1182 |
col,
|
| 1183 |
help=editable_descriptions.get(col, ''),
|
| 1184 |
width="medium",
|
| 1185 |
+
options=[
|
| 1186 |
+
"", # Tomt alternativ för att kunna rensa
|
| 1187 |
+
"Alltid tillgängligt",
|
| 1188 |
+
"Stängt - se Access info"
|
| 1189 |
+
],
|
| 1190 |
required=False
|
| 1191 |
)
|
| 1192 |
elif col == 'Main Contact':
|
| 1193 |
+
# Dropdown för huvudkontakt
|
| 1194 |
config[col] = st.column_config.SelectboxColumn(
|
| 1195 |
col,
|
| 1196 |
help=editable_descriptions.get(col, ''),
|
| 1197 |
+
width="medium",
|
| 1198 |
options=[
|
| 1199 |
"",
|
| 1200 |
"Huvudkontakt"
|
|
|
|
| 1457 |
if 'show_admin' not in st.session_state:
|
| 1458 |
st.session_state.show_admin = False
|
| 1459 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1460 |
def show_save_status_indicator():
|
| 1461 |
+
"""Visa status-indikator för auto-save med timestamp"""
|
|
|
|
|
|
|
|
|
|
| 1462 |
if st.session_state.saving_in_progress:
|
| 1463 |
st.markdown("""
|
| 1464 |
<div class='auto-save-indicator saving'>
|
| 1465 |
+
💾 Sparar Excel-ändringar...
|
| 1466 |
</div>
|
| 1467 |
""", unsafe_allow_html=True)
|
| 1468 |
+
|
| 1469 |
elif st.session_state.last_save_time:
|
|
|
|
|
|
|
| 1470 |
time_diff = (datetime.now() - st.session_state.last_save_time).total_seconds()
|
| 1471 |
+
timestamp = st.session_state.last_save_time.strftime('%Y-%m-%d %H:%M:%S')
|
| 1472 |
|
| 1473 |
+
if time_diff < 3: # Visa "Sparad" i 3 sekunder
|
|
|
|
| 1474 |
st.markdown(f"""
|
| 1475 |
<div class='auto-save-indicator saved'>
|
| 1476 |
+
✅ Sparad automatiskt! <span style='font-size: 0.85em; opacity: 0.8;'>({timestamp})</span>
|
| 1477 |
</div>
|
| 1478 |
""", unsafe_allow_html=True)
|
| 1479 |
else:
|
| 1480 |
st.markdown(f"""
|
| 1481 |
<div class='auto-save-indicator'>
|
| 1482 |
+
💾 Auto-sparar vid ändringar <span style='font-size: 0.85em; opacity: 0.7;'>• Senast sparad: {timestamp}</span>
|
| 1483 |
</div>
|
| 1484 |
""", unsafe_allow_html=True)
|
| 1485 |
else:
|
| 1486 |
st.markdown("""
|
| 1487 |
<div class='auto-save-indicator'>
|
| 1488 |
+
💾 Auto-sparar vid ändringar
|
| 1489 |
</div>
|
| 1490 |
""", unsafe_allow_html=True)
|
| 1491 |
|
|
|
|
| 1499 |
if sheet is None:
|
| 1500 |
st.stop()
|
| 1501 |
|
| 1502 |
+
# Initiera LogSheets efter anslutning
|
| 1503 |
+
if st.session_state.log_sheets is None and spreadsheet is not None:
|
| 1504 |
try:
|
|
|
|
| 1505 |
st.session_state.log_sheets = LogSheets(spreadsheet)
|
|
|
|
| 1506 |
print("[MONITORING] LogSheets initierat")
|
| 1507 |
except Exception as e:
|
| 1508 |
print(f"[MONITORING ERROR] Kunde inte initiera LogSheets: {e}")
|
| 1509 |
st.session_state.log_sheets = None
|
| 1510 |
|
| 1511 |
+
# ADMIN PANEL CHECK - Tidig i execution
|
| 1512 |
+
# Lägg till Admin Panel knapp i sidebar
|
| 1513 |
+
with st.sidebar:
|
| 1514 |
+
st.markdown("### ⚙️ System")
|
| 1515 |
+
if st.button("🔐 Admin Panel", use_container_width=True):
|
| 1516 |
+
st.session_state.show_admin = True
|
| 1517 |
+
st.rerun()
|
| 1518 |
|
| 1519 |
+
# Om admin panel ska visas, visa den och stoppa
|
| 1520 |
if st.session_state.show_admin:
|
| 1521 |
+
# Lägg till "Tillbaka"-knapp
|
| 1522 |
+
if st.button("← Tillbaka till huvudsidan"):
|
| 1523 |
+
st.session_state.show_admin = False
|
| 1524 |
+
st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1525 |
|
| 1526 |
+
show_admin_panel(st.session_state.log_sheets)
|
| 1527 |
+
st.stop()
|
|
|
|
|
|
|
| 1528 |
|
| 1529 |
+
# Normal app flow fortsätter här
|
| 1530 |
st.markdown("""
|
| 1531 |
<div class='main-header'>
|
| 1532 |
<h1>⚡ ChargeNode Data Manager</h1>
|
| 1533 |
+
<p style='margin: 0.5rem 0 0 0; font-size: 1.1rem;'>Inloggad för uppdatering av företagsdata</p>
|
| 1534 |
</div>
|
| 1535 |
""", unsafe_allow_html=True)
|
| 1536 |
|
| 1537 |
+
# Spara service account email i session state
|
| 1538 |
+
if service_account_email and not st.session_state.service_account_email:
|
| 1539 |
+
st.session_state.service_account_email = service_account_email
|
|
|
|
|
|
|
|
|
|
| 1540 |
|
| 1541 |
+
# Hämta data
|
| 1542 |
+
if st.session_state.df is None:
|
| 1543 |
+
start_time = time.time()
|
| 1544 |
+
with st.spinner("⏳ Laddar data från Google Sheets..."):
|
| 1545 |
+
st.session_state.df = get_sheet_data(sheet)
|
| 1546 |
+
if st.session_state.df is None:
|
| 1547 |
+
st.stop()
|
| 1548 |
+
|
| 1549 |
+
# MONITORING: Logga datainläsning performance
|
| 1550 |
+
duration = time.time() - start_time
|
| 1551 |
+
safe_log_performance(
|
| 1552 |
+
st.session_state.log_sheets,
|
| 1553 |
+
'LOAD_DATA',
|
| 1554 |
+
duration,
|
| 1555 |
+
details=f"Laddade {len(st.session_state.df)} rader"
|
| 1556 |
+
)
|
| 1557 |
+
|
| 1558 |
+
df = st.session_state.df
|
| 1559 |
|
| 1560 |
+
# Login system
|
| 1561 |
if not st.session_state.authenticated:
|
| 1562 |
+
st.markdown("<div class='login-container'>", unsafe_allow_html=True)
|
| 1563 |
|
| 1564 |
+
st.markdown("### 🔐 Företagsinloggning")
|
| 1565 |
+
st.markdown("Ange ditt unika företagslösenord för att få tillgång till era områden.")
|
| 1566 |
|
| 1567 |
+
password_input = st.text_input(
|
| 1568 |
+
"Lösenord",
|
| 1569 |
+
type="password",
|
| 1570 |
+
placeholder="Ange lösenord...",
|
| 1571 |
+
key="password_input"
|
| 1572 |
+
)
|
| 1573 |
+
|
| 1574 |
+
login_button = st.button("🚀 Logga in", use_container_width=True, type="primary")
|
| 1575 |
+
|
| 1576 |
+
st.markdown("<div style='margin-top: 1rem; text-align: center; color: #666; font-size: 0.9rem;'>Har du inte fått ditt lösenord? Kontakta oss på <a href='mailto:migration@chargenode.eu'>migration@chargenode.eu</a></div>", unsafe_allow_html=True)
|
| 1577 |
+
|
| 1578 |
+
if login_button:
|
| 1579 |
+
if password_input:
|
| 1580 |
+
with st.spinner("🔍 Validerar lösenord..."):
|
| 1581 |
+
account_id = validate_password(df, password_input)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1582 |
|
| 1583 |
+
if account_id:
|
| 1584 |
+
# Testa Google Sheets-anslutningen
|
| 1585 |
+
connection_ok, connection_msg = test_sheet_connection(sheet)
|
| 1586 |
+
|
| 1587 |
+
if not connection_ok:
|
| 1588 |
+
st.error(f"❌ {connection_msg}")
|
| 1589 |
+
st.error("Kontakta migration@chargenode.eu om problemet kvarstår.")
|
| 1590 |
+
st.stop()
|
| 1591 |
+
|
| 1592 |
+
st.session_state.authenticated = True
|
| 1593 |
+
st.session_state.account_id = account_id
|
| 1594 |
+
|
| 1595 |
+
# Hämta företagsnamn
|
| 1596 |
+
company_areas = get_company_areas(df, account_id)
|
| 1597 |
+
if not company_areas.empty and 'Företagsnamn' in company_areas.columns:
|
| 1598 |
+
st.session_state.company_name = company_areas.iloc[0]['Företagsnamn']
|
| 1599 |
+
|
| 1600 |
+
# Spara original data för ändringsdetektering
|
| 1601 |
+
st.session_state.original_data = company_areas.drop(columns=HIDDEN_COLUMNS, errors='ignore').copy()
|
| 1602 |
+
|
| 1603 |
+
# MONITORING: Logga login
|
| 1604 |
+
safe_log_activity(
|
| 1605 |
+
st.session_state.log_sheets,
|
| 1606 |
+
'LOGIN',
|
| 1607 |
+
account_id,
|
| 1608 |
+
company_name=st.session_state.company_name,
|
| 1609 |
+
details=f"Inloggning från {service_account_email}"
|
| 1610 |
+
)
|
| 1611 |
+
|
| 1612 |
+
st.success(f"✅ Inloggning lyckades! {connection_msg}")
|
| 1613 |
+
time.sleep(0.5)
|
| 1614 |
+
st.rerun()
|
| 1615 |
+
else:
|
| 1616 |
+
st.error("❌ Felaktigt lösenord. Försök igen eller kontakta migration@chargenode.eu")
|
| 1617 |
+
|
| 1618 |
+
# MONITORING: Logga misslyckat login-försök
|
| 1619 |
+
safe_log_activity(
|
| 1620 |
+
st.session_state.log_sheets,
|
| 1621 |
+
'LOGIN',
|
| 1622 |
+
'UNKNOWN',
|
| 1623 |
+
details="Misslyckat inloggningsförsök",
|
| 1624 |
+
status='ERROR'
|
| 1625 |
+
)
|
| 1626 |
+
else:
|
| 1627 |
+
st.warning("⚠️ Vänligen ange ett lösenord")
|
| 1628 |
|
| 1629 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 1630 |
+
|
| 1631 |
+
# Footer med info
|
| 1632 |
+
st.markdown("<hr>", unsafe_allow_html=True)
|
| 1633 |
+
st.markdown("""
|
| 1634 |
+
<div style='text-align: center; padding: 1rem;'>
|
| 1635 |
+
<p style='color: #999999; font-size: 0.9rem;'>
|
| 1636 |
+
🔒 Säker anslutning • Data krypteras med TLS •
|
| 1637 |
+
<a href='https://www.chargenode.se' target='_blank'>ChargeNode.se</a>
|
| 1638 |
+
</p>
|
| 1639 |
+
</div>
|
| 1640 |
+
""", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1641 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1642 |
else:
|
| 1643 |
+
# Användaren är inloggad
|
| 1644 |
+
account_id = st.session_state.account_id
|
| 1645 |
+
company_name = st.session_state.company_name or f"Företag {account_id}"
|
| 1646 |
+
service_account_email = st.session_state.service_account_email or 'Web User'
|
| 1647 |
+
log_sheets = st.session_state.log_sheets
|
| 1648 |
+
|
| 1649 |
+
# Hämta företagets områden
|
| 1650 |
+
company_areas = get_company_areas(df, account_id)
|
| 1651 |
+
|
| 1652 |
+
if company_areas.empty:
|
| 1653 |
+
st.error(f"❌ Inga områden hittades för Account ID: {account_id}")
|
| 1654 |
+
st.stop()
|
| 1655 |
+
|
| 1656 |
+
# Välkomstmeddelande med företagsinfo
|
| 1657 |
+
hardware_summary = get_hardware_summary(company_areas)
|
| 1658 |
+
main_contact_name = get_main_contact_name(company_areas)
|
| 1659 |
+
migration_summary = get_migration_summary(company_areas)
|
| 1660 |
+
|
| 1661 |
+
st.markdown(f"""
|
| 1662 |
+
### 👋 Välkommen, **{company_name}**!
|
| 1663 |
+
**Migration av:** {hardware_summary} | **Huvudansvarig för kontot:** {main_contact_name}
|
| 1664 |
+
|
| 1665 |
+
**Migrationsmetod:** {migration_summary}
|
| 1666 |
+
""", unsafe_allow_html=False)
|
| 1667 |
+
|
| 1668 |
+
# Viktig information - UPPDATERAD med tydligare instruktioner
|
| 1669 |
st.markdown("""
|
| 1670 |
+
<div class='warning-box'>
|
| 1671 |
+
<h3>⚠️ VIKTIGT - Läs innan du börjar!</h3>
|
| 1672 |
+
<p><strong style='color: #FF5722;'>ALLA VITA RUTOR MÅSTE FYLLAS I!</strong></p>
|
| 1673 |
+
<p>Kolumner med <strong>🔒 (gult hänglås)</strong> kan du inte ändra på.</p>
|
| 1674 |
+
<p>Övriga kolumner kan du klicka i och redigera.</p>
|
| 1675 |
+
<p><strong>Ändringar sparas automatiskt</strong> när du klickar utanför ett fält.</p>
|
| 1676 |
+
</div>
|
| 1677 |
+
""", unsafe_allow_html=True)
|
| 1678 |
+
|
| 1679 |
+
# Informationsruta om fält
|
| 1680 |
+
st.markdown("""
|
| 1681 |
+
<div class='info-box'>
|
| 1682 |
+
<h3>📋 Vad ska fyllas i?</h3>
|
| 1683 |
+
<p><strong>Namn:</strong> Huvudkontaktperson för detta område. Personen vi kontaktar vid frågor eller problem.</p>
|
| 1684 |
+
<p><strong>Email adress:</strong> Ange individuell e-postadress till kontaktpersonen för detta område.</p>
|
| 1685 |
+
<p><strong>Telefon:</strong> Telefonnummer till huvudkontaktpersonen. Format: +46XXXXXXXXX eller 07X-XXXXXXX</p>
|
| 1686 |
+
<p><strong>Tillgänglighet:</strong> Välj i dropdown-menyn om detta är ett område som vi alltid kan komma åt,
|
| 1687 |
+
eller om det är stängt. Om det är stängt var vänlig fyll i fältet Access info.</p>
|
| 1688 |
+
<p><strong>Access info:</strong> Om området inte alltid är tillgängligt, vänligen ange hur vi kan få tillgång.
|
| 1689 |
+
T.ex. "Nycklar finns i receptionen" eller "Ring Pelle på 07XXXXXXXX när ni är på gång".</p>
|
| 1690 |
+
<p><strong>Main Contact:</strong> Ange om denna person är huvudansvarig för kontot.
|
| 1691 |
+
Välj "Huvudkontakt" för helst endast en person per företag.</p>
|
| 1692 |
</div>
|
| 1693 |
""", unsafe_allow_html=True)
|
| 1694 |
|
| 1695 |
+
# Beräkna värden för metrics
|
| 1696 |
+
total_outlets = get_total_outlets(company_areas)
|
| 1697 |
+
first_date, last_date = get_migration_interval(company_areas)
|
| 1698 |
+
|
| 1699 |
+
# Metrics rad
|
| 1700 |
+
col1, col2, col3 = st.columns([2, 2.5, 1.5])
|
| 1701 |
+
|
| 1702 |
+
with col1:
|
| 1703 |
+
st.metric(
|
| 1704 |
+
label="📍 Totalt områden",
|
| 1705 |
+
value=len(company_areas),
|
| 1706 |
+
help="Antal områden kopplade till ditt företag"
|
| 1707 |
)
|
| 1708 |
+
|
| 1709 |
+
with col2:
|
| 1710 |
+
if first_date and last_date:
|
| 1711 |
+
date_range = f"{first_date.strftime('%Y-%m-%d')} → {last_date.strftime('%Y-%m-%d')}"
|
| 1712 |
+
else:
|
| 1713 |
+
date_range = "Ej angivet"
|
| 1714 |
|
| 1715 |
+
st.metric(
|
| 1716 |
+
label="📅 Indikerat migrationsintervall",
|
| 1717 |
+
value=date_range if len(date_range) < 30 else "Se nedan",
|
| 1718 |
+
help="Första och sista datum för planerad migrering"
|
| 1719 |
+
)
|
| 1720 |
+
|
| 1721 |
+
if first_date and last_date and len(date_range) >= 30:
|
| 1722 |
+
st.markdown(f"<p style='font-size: 0.8rem; color: #666;'>Från {first_date.strftime('%Y-%m-%d')} till {last_date.strftime('%Y-%m-%d')}</p>", unsafe_allow_html=True)
|
| 1723 |
+
|
| 1724 |
+
with col3:
|
| 1725 |
+
st.metric(
|
| 1726 |
+
label="🔌 Totalt antal uttag",
|
| 1727 |
+
value=total_outlets,
|
| 1728 |
+
help="Summa av alla uttag för dina områden"
|
| 1729 |
+
)
|
| 1730 |
+
|
| 1731 |
+
# Auto-save status - ENKEL FEEDBACK baserat på last_save_time
|
| 1732 |
+
col_status = st.columns(1)[0]
|
| 1733 |
+
|
| 1734 |
+
with col_status:
|
| 1735 |
+
show_save_status_indicator()
|
| 1736 |
+
|
| 1737 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 1738 |
+
|
| 1739 |
+
# Excel info-ruta och knappar
|
| 1740 |
st.markdown("""
|
| 1741 |
<div class='excel-info-box'>
|
| 1742 |
+
<strong>💡 Jobba i Excel:</strong> Om du hellre jobbar i Excel kan du ladda ner filen här,
|
| 1743 |
+
uppdatera fälten och ladda upp filen igen.
|
| 1744 |
</div>
|
| 1745 |
""", unsafe_allow_html=True)
|
| 1746 |
|
| 1747 |
+
col_download, col_upload, col_logout = st.columns([1, 1, 1])
|
| 1748 |
+
|
| 1749 |
+
with col_download:
|
| 1750 |
+
# Excel Export knapp
|
| 1751 |
+
excel_data = export_to_excel(company_areas, account_id)
|
| 1752 |
+
if excel_data:
|
| 1753 |
+
# Custom download button handler with monitoring
|
| 1754 |
+
download_clicked = st.download_button(
|
| 1755 |
+
label="📥 Ladda ner Excel",
|
| 1756 |
+
data=excel_data,
|
| 1757 |
+
file_name=f"chargenode_data_{account_id}_{datetime.now().strftime('%Y%m%d')}.xlsx",
|
| 1758 |
+
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 1759 |
+
help="Ladda ner som Excel-fil för att fylla i offline",
|
| 1760 |
+
use_container_width=True
|
| 1761 |
+
)
|
| 1762 |
+
|
| 1763 |
+
# MONITORING: Logga download (när knappen klickas)
|
| 1764 |
+
if download_clicked:
|
| 1765 |
+
safe_log_activity(
|
| 1766 |
+
log_sheets,
|
| 1767 |
+
'DOWNLOAD',
|
| 1768 |
+
account_id,
|
| 1769 |
+
company_name=company_name,
|
| 1770 |
+
details=f"Laddade ner Excel med {len(company_areas)} områden"
|
| 1771 |
+
)
|
| 1772 |
+
|
| 1773 |
+
with col_upload:
|
| 1774 |
+
# Excel Upload knapp
|
| 1775 |
+
uploaded_file = st.file_uploader(
|
| 1776 |
+
"📤 Ladda upp Excel",
|
| 1777 |
+
type=['xlsx'],
|
| 1778 |
+
help="Ladda upp Excel-filen du redigerat",
|
| 1779 |
+
label_visibility="visible"
|
| 1780 |
+
)
|
| 1781 |
+
|
| 1782 |
+
with col_logout:
|
| 1783 |
+
if st.button("🚪 Logga ut", use_container_width=True):
|
| 1784 |
+
# MONITORING: Logga logout
|
| 1785 |
+
safe_log_activity(
|
| 1786 |
+
log_sheets,
|
| 1787 |
+
'LOGOUT',
|
| 1788 |
+
account_id,
|
| 1789 |
+
company_name=company_name,
|
| 1790 |
+
details="Utloggning"
|
| 1791 |
+
)
|
| 1792 |
+
|
| 1793 |
+
st.session_state.authenticated = False
|
| 1794 |
+
st.session_state.account_id = None
|
| 1795 |
+
st.session_state.df = None
|
| 1796 |
+
st.session_state.company_name = None
|
| 1797 |
+
st.session_state.original_data = None
|
| 1798 |
+
st.session_state.last_save_time = None
|
| 1799 |
+
st.session_state.saving_in_progress = False
|
| 1800 |
+
st.session_state.service_account_email = None
|
| 1801 |
+
st.rerun()
|
| 1802 |
|
| 1803 |
+
# Hantera uppladdad Excel-fil
|
| 1804 |
+
if uploaded_file is not None:
|
| 1805 |
start_time = time.time()
|
| 1806 |
+
with st.spinner("⏳ Bearbetar Excel-fil..."):
|
| 1807 |
+
changes = process_uploaded_excel(uploaded_file, df, account_id)
|
| 1808 |
+
|
| 1809 |
+
# MONITORING: Logga upload
|
| 1810 |
+
safe_log_activity(
|
| 1811 |
+
log_sheets,
|
| 1812 |
+
'UPLOAD',
|
| 1813 |
+
account_id,
|
| 1814 |
+
company_name=company_name,
|
| 1815 |
+
details=f"Laddade upp Excel med {len(changes) if changes else 0} ändringar"
|
| 1816 |
+
)
|
| 1817 |
|
| 1818 |
+
if changes:
|
| 1819 |
+
st.info(f"📊 Hittade {len(changes)} ändringar i Excel-filen")
|
| 1820 |
+
|
| 1821 |
+
if st.button("✅ Spara ändringar från Excel", type="primary", use_container_width=True):
|
| 1822 |
st.session_state.saving_in_progress = True
|
| 1823 |
|
| 1824 |
success, message = batch_update_cells_with_tracking(
|
|
|
|
| 1855 |
<small>Kontrollera din internetanslutning och försök igen.</small>
|
| 1856 |
</div>
|
| 1857 |
""", unsafe_allow_html=True)
|
| 1858 |
+
elif changes is not None:
|
| 1859 |
+
st.info("ℹ️ Inga ändringar hittades i Excel-filen")
|
| 1860 |
+
|
| 1861 |
+
# Performance logging
|
| 1862 |
+
duration = time.time() - start_time
|
| 1863 |
+
safe_log_performance(
|
| 1864 |
+
log_sheets,
|
| 1865 |
+
'PROCESS_EXCEL',
|
| 1866 |
+
duration,
|
| 1867 |
+
user_id=account_id,
|
| 1868 |
+
details=f"{len(changes) if changes else 0} ändringar"
|
| 1869 |
+
)
|
| 1870 |
|
| 1871 |
+
st.markdown("<hr>", unsafe_allow_html=True)
|
| 1872 |
+
|
| 1873 |
+
# Information
|
| 1874 |
+
with st.expander("ℹ️ Hjälp och Information", expanded=False):
|
| 1875 |
+
col_help1, col_help2 = st.columns(2)
|
| 1876 |
+
|
| 1877 |
+
with col_help1:
|
| 1878 |
+
st.markdown("#### 🔒 Låsta fält (kan ej editeras)")
|
| 1879 |
+
st.markdown("Dessa fält har **🔒 (gult hänglås)** i kolumnrubriken och kan inte ändras:")
|
| 1880 |
+
visible_readonly = [col for col in READONLY_COLUMNS if col not in HIDDEN_COLUMNS]
|
| 1881 |
+
for col in visible_readonly[:15]:
|
| 1882 |
+
if col in company_areas.columns:
|
| 1883 |
+
st.markdown(f"• 🔒 {col}")
|
| 1884 |
+
if len(visible_readonly) > 15:
|
| 1885 |
+
st.markdown(f"*...och {len(visible_readonly) - 15} fält till*")
|
| 1886 |
+
|
| 1887 |
+
with col_help2:
|
| 1888 |
+
st.markdown("#### ✏️ Editerbara fält")
|
| 1889 |
+
st.markdown("Dessa fält kan du klicka i och redigera:")
|
| 1890 |
+
visible_editable = [col for col in EDITABLE_COLUMNS if col not in HIDDEN_COLUMNS]
|
| 1891 |
+
for col in visible_editable:
|
| 1892 |
+
if col in company_areas.columns:
|
| 1893 |
+
st.markdown(f"• ✏️ {col}")
|
| 1894 |
+
|
| 1895 |
+
# Huvudtabell
|
| 1896 |
+
st.markdown("## 📊 Dataöversikt")
|
| 1897 |
+
|
| 1898 |
+
# VIKTIGT: Instruktionsruta för hur sparning fungerar
|
| 1899 |
+
st.markdown("""
|
| 1900 |
+
<div style='background-color: #FFF4E6; border-left: 5px solid #FF9800; padding: 1.2rem; margin-bottom: 1.5rem; border-radius: 5px;'>
|
| 1901 |
+
<h3 style='margin-top: 0; color: #E65100;'>⚠️ VIKTIGT: Så här sparar du dina ändringar</h3>
|
| 1902 |
+
<ol style='margin-bottom: 0.5rem; font-size: 1.05rem;'>
|
| 1903 |
+
<li>Skriv din information i cellen</li>
|
| 1904 |
+
<li><strong>Tryck på Enter ELLER klicka i en annan cell</strong></li>
|
| 1905 |
+
<li>Informationen sparas automatiskt när du lämnar cellen</li>
|
| 1906 |
+
<li>Du ser ett grönt bekräftelsemeddelande när det är sparat ✅</li>
|
| 1907 |
+
</ol>
|
| 1908 |
+
<p style='margin: 0; margin-top: 0.8rem; font-weight: bold; color: #E65100;'>
|
| 1909 |
+
💡 Tips: Stäng inte sidan förrän du sett bekräftelsemeddelandet!
|
| 1910 |
+
</p>
|
| 1911 |
+
</div>
|
| 1912 |
+
""", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1913 |
|
| 1914 |
+
st.markdown("""
|
| 1915 |
+
Kolumner med **🔒** (gult hänglås) kan inte editeras. Klicka i andra fält för att redigera.
|
| 1916 |
|
| 1917 |
+
**Kontrollera att uppgifterna i låsta celler stämmer.** Om något verkar fel, mejla till [migration@chargenode.eu](mailto:migration@chargenode.eu) så hjälper vi dig.
|
| 1918 |
+
""")
|
| 1919 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
|
|
|
| 1920 |
|
| 1921 |
+
# Ta bort dolda kolumner och sätt kolumnordning med Fastighetsnamn först
|
| 1922 |
+
company_areas_display = company_areas.drop(columns=HIDDEN_COLUMNS, errors='ignore')
|
| 1923 |
+
|
| 1924 |
+
# Skapa kolumnordning med Fastighetsnamn först (för bättre synlighet vid scrolling)
|
| 1925 |
+
column_order = ['Fastighetsnamn'] + [col for col in company_areas_display.columns if col != 'Fastighetsnamn']
|
| 1926 |
+
company_areas_display = company_areas_display[column_order]
|
| 1927 |
+
|
| 1928 |
+
column_config = get_column_config(company_areas_display)
|
| 1929 |
+
|
| 1930 |
+
# Status-indikator precis ovanför tabellen för bättre synlighet
|
| 1931 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 1932 |
+
show_save_status_indicator()
|
| 1933 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 1934 |
+
|
| 1935 |
+
# Container för att förhindra scroll-hopp
|
| 1936 |
+
table_container = st.container()
|
|
|
|
|
|
|
| 1937 |
|
| 1938 |
+
with table_container:
|
| 1939 |
+
# Visa tabellen med full höjd
|
| 1940 |
+
edited_data = st.data_editor(
|
| 1941 |
+
company_areas_display,
|
| 1942 |
+
column_config=column_config,
|
| 1943 |
+
use_container_width=True,
|
| 1944 |
+
num_rows="fixed",
|
| 1945 |
+
hide_index=True,
|
| 1946 |
+
key="data_editor",
|
| 1947 |
+
height=600 # Större höjd för bättre visning
|
| 1948 |
+
)
|
| 1949 |
|
| 1950 |
+
# Auto-save med robust felhantering
|
| 1951 |
+
if st.session_state.original_data is not None and not st.session_state.saving_in_progress:
|
| 1952 |
+
changes_list = []
|
| 1953 |
|
| 1954 |
+
# DEBUG: Visa om vi kollar efter ändringar
|
| 1955 |
+
print(f"[DEBUG] === AUTO-SAVE CHECK START ===")
|
| 1956 |
+
print(f"[DEBUG] saving_in_progress: {st.session_state.saving_in_progress}")
|
| 1957 |
+
print(f"[DEBUG] last_save_time: {st.session_state.last_save_time}")
|
| 1958 |
+
print(f"[DEBUG] Original data rader: {len(st.session_state.original_data)}, Edited data rader: {len(edited_data)}")
|
| 1959 |
+
|
| 1960 |
+
for idx in range(len(st.session_state.original_data)):
|
| 1961 |
+
for col in company_areas_display.columns:
|
| 1962 |
+
if col not in READONLY_COLUMNS:
|
| 1963 |
+
try:
|
| 1964 |
+
orig_val = str(st.session_state.original_data.iloc[idx][col])
|
| 1965 |
+
edit_val = str(edited_data.iloc[idx][col])
|
| 1966 |
+
|
| 1967 |
+
if orig_val != edit_val:
|
| 1968 |
+
print(f"[DEBUG] Ändring hittad! Rad {idx}, Kolumn '{col}': '{orig_val}' → '{edit_val}'")
|
| 1969 |
+
changes_list.append({
|
| 1970 |
+
'row_idx': st.session_state.original_data.index[idx],
|
| 1971 |
+
'column': col,
|
| 1972 |
+
'old_value': orig_val,
|
| 1973 |
+
'new_value': edit_val
|
| 1974 |
+
})
|
| 1975 |
+
except Exception as e:
|
| 1976 |
+
st.error(f"Fel vid jämförelse av {col} på rad {idx}: {e}")
|
| 1977 |
+
print(f"[ERROR] Fel vid jämförelse av {col} på rad {idx}: {e}")
|
| 1978 |
+
|
| 1979 |
+
print(f"[DEBUG] Totalt {len(changes_list)} ändringar hittade")
|
| 1980 |
+
print(f"[DEBUG] === AUTO-SAVE CHECK END ===")
|
| 1981 |
+
|
| 1982 |
+
if changes_list:
|
| 1983 |
+
try:
|
| 1984 |
+
print(f"[DEBUG] Startar sparning av {len(changes_list)} ändringar")
|
| 1985 |
|
| 1986 |
+
# Batch update med ändringsloggning och felhantering
|
| 1987 |
+
success, message = batch_update_cells_with_tracking(
|
| 1988 |
+
sheet,
|
| 1989 |
+
changes_list,
|
| 1990 |
+
df,
|
| 1991 |
+
service_account_email,
|
| 1992 |
+
log_sheets,
|
| 1993 |
+
account_id
|
| 1994 |
+
)
|
| 1995 |
|
| 1996 |
+
if success:
|
| 1997 |
+
print(f"[DEBUG] Sparning lyckades!")
|
| 1998 |
+
st.session_state.last_save_time = datetime.now()
|
| 1999 |
+
|
| 2000 |
+
# Uppdatera data
|
| 2001 |
+
st.cache_resource.clear()
|
| 2002 |
+
new_df = get_sheet_data(sheet)
|
| 2003 |
+
if new_df is not None:
|
| 2004 |
+
st.session_state.df = new_df
|
| 2005 |
+
company_areas = get_company_areas(new_df, account_id)
|
| 2006 |
+
st.session_state.original_data = company_areas.drop(columns=HIDDEN_COLUMNS, errors='ignore').copy()
|
| 2007 |
+
|
| 2008 |
+
print(f"[DEBUG] Data uppdaterad, kör rerun...")
|
| 2009 |
+
|
| 2010 |
+
# Toast-meddelande som syns även efter rerun
|
| 2011 |
+
st.toast(f"✅ {len(changes_list)} ändringar sparade!", icon="✅")
|
| 2012 |
+
st.rerun()
|
| 2013 |
+
else:
|
| 2014 |
+
print(f"[ERROR] Sparning misslyckades: {message}")
|
| 2015 |
+
st.error(f"❌ {message}")
|
| 2016 |
+
|
| 2017 |
+
except Exception as e:
|
| 2018 |
+
# Fånga ALLA oväntade fel
|
| 2019 |
+
print(f"[CRITICAL ERROR] Auto-save kraschade: {str(e)}")
|
| 2020 |
+
st.error(f"❌ Ett oväntat fel uppstod vid sparning: {str(e)}")
|
| 2021 |
+
|
| 2022 |
+
# Footer
|
| 2023 |
+
st.markdown("<hr>", unsafe_allow_html=True)
|
| 2024 |
+
st.markdown(f"""
|
| 2025 |
+
<div style='text-align: center; padding: 1rem;'>
|
| 2026 |
+
<p style='color: #999999; font-size: 0.9rem;'>
|
| 2027 |
+
🔒 Företagsdata •
|
| 2028 |
+
Inloggad som <strong style='color: #333;'>{company_name}</strong> •
|
| 2029 |
+
<strong style='color: #333;'>{len(company_areas)}</strong> områden •
|
| 2030 |
+
<strong style='color: #333;'>{total_outlets}</strong> uttag
|
| 2031 |
+
</p>
|
| 2032 |
+
</div>
|
| 2033 |
+
""", unsafe_allow_html=True)
|