k96beni commited on
Commit
07958c8
·
verified ·
1 Parent(s): 0ce2935

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +689 -530
app.py CHANGED
@@ -1,9 +1,4 @@
1
- # ChargeNode Data Manager - FÖRBÄTTRAD VERSION
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 inloggningsformulär
311
- st.markdown("### Logga in till Admin Panel")
312
- admin_password = st.text_input("Admin Password", type="password", key="admin_password_input")
313
-
314
- col1, col2 = st.columns([1, 4])
315
- with col1:
316
- if st.button("🔓 Logga in", type="primary"):
317
- if admin_password == ADMIN_PASSWORD:
318
- st.session_state.admin_authenticated = True
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 panel
331
- st.success("✅ Du är inloggad som Admin")
332
 
333
- col1, col2 = st.columns([1, 5])
334
- with col1:
 
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
- with col2:
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
- show_admin_dashboard(log_sheets)
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 show_admin_dashboard(log_sheets):
360
- """Visa dashboard med statistik"""
361
- st.markdown("### 📊 Användningsstatistik")
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 alla loggar
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
- # Metrics rad 1
 
 
 
 
381
  col1, col2, col3, col4 = st.columns(4)
382
 
383
  with col1:
384
- total_activities = len(df)
385
- st.metric("📝 Total aktivitet", total_activities)
386
 
387
  with col2:
388
- unique_users = df['User ID'].nunique()
389
- st.metric("👥 Unika användare", unique_users)
390
 
391
  with col3:
392
- edits = len(df[df['Event Type'] == 'EDIT'])
393
- st.metric("✏️ Editeringar", edits)
394
 
395
  with col4:
396
- logins = len(df[df['Event Type'] == 'LOGIN'])
397
- st.metric("🔐 Inloggningar", logins)
398
 
399
  st.markdown("---")
400
 
401
- # Aktivitet per händelsetyp
402
- st.markdown("### 📈 Aktivitet per typ")
403
  event_counts = df['Event Type'].value_counts()
404
  st.bar_chart(event_counts)
405
 
406
  st.markdown("---")
407
 
408
- # Senaste aktiviteter
409
- st.markdown("### 🕐 Senaste aktiviteter")
410
- recent_df = df.sort_values('Timestamp', ascending=False).head(20)
411
- st.dataframe(
412
- recent_df[['Timestamp', 'Event Type', 'User ID', 'Company Name', 'Column', 'Status']],
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 export_to_excel(company_areas):
723
- """Exportera data till Excel med färgkodning och formatering"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
724
  try:
725
  # Ta bort dolda kolumner
726
  export_df = company_areas.drop(columns=HIDDEN_COLUMNS, errors='ignore')
727
 
728
- # Skapa workbook
 
729
  wb = Workbook()
730
  ws = wb.active
731
  ws.title = "ChargeNode Data"
732
 
733
  # Färger
734
- header_fill = PatternFill(start_color="00A651", end_color="00A651", fill_type="solid")
735
- readonly_fill = PatternFill(start_color="FFF4E6", end_color="FFF4E6", fill_type="solid")
736
- editable_fill = PatternFill(start_color="E8F5E9", end_color="E8F5E9", fill_type="solid")
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
- FÖRBÄTTRAD VERSION:
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-anslutning
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 tracking
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
- # Hantera värden (inkl. telefonnummer som text)
899
- value_to_write = str(new_value) if new_value else ''
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"❌ Uppdatering misslyckades efter {max_retries} försök"
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
- # Normalisera värden för jämförelse
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
- print(f"[DEBUG] Verifiering: Förväntat='{expected_normalized}', Faktiskt='{verified_normalized}'")
 
 
 
 
1012
 
1013
- if verified_normalized != expected_normalized:
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å - verifiering är bara en extra kontroll
1023
 
1024
- # Logga ändringar
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
- row_number=change['row_idx'],
 
1034
  column=change['column'],
1035
  old_value=change['old_value'],
1036
- new_value=change['new_value']
 
1037
  )
1038
 
1039
- # Performance logging
1040
  duration = time.time() - start_time
1041
- safe_log_performance(
1042
- log_sheets,
1043
- 'BATCH_UPDATE',
1044
- duration,
1045
- user_id=account_id,
1046
- details=f"{len(changes)} ändringar"
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 = f"❌ Oväntat fel: {str(e)}"
1055
- print(f"[ERROR] {error_msg}")
1056
- return False, error_msg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 fält
1065
  editable_descriptions = {
1066
- 'Namn': 'Ange kontaktperson för detta område',
1067
- 'Email adress': 'Ange e-postadress till kontaktperson',
1068
- 'Telefon': 'Ange telefonnummer (ex: 0701234567)',
1069
- 'Tillgänglighet': 'Hur nås området? (ex: "Alltid öppet", "Stängt - se Access info")',
1070
- 'Access info': 'Detaljerad information om tillgänglighet och åtkomst',
1071
- 'Fritext meddelande': 'Eventuella extra kommentarer eller information',
1072
- 'Main Contact': 'Markera om detta är huvudkontakten'
1073
  }
1074
 
1075
- # Beskrivningar för readonly fält
1076
  readonly_descriptions = {
1077
- 'Fastighetsnamn': 'Namn på fastigheten (kan ej ändras)',
1078
- 'Address': 'Adress till laddplatsen (kan ej ändras)',
1079
- 'Stad': 'Stad där laddplatsen finns (kan ej ändras)',
1080
- 'Metod': 'Installatonsmetod (kan ej ändras)',
1081
- '# Uttag': 'Antal ladduttag (kan ej ändras)',
1082
- 'Hårdvara': 'Hårdvarumodell (kan ej ändras)',
1083
- 'Vecka': 'Planerad installationsvecka (kan ej ändras)',
1084
- 'Indikativt datum': 'Uppskattat datum för installation (kan ej ändras)',
1085
- 'Status': 'Aktuell status (uppdateras automatiskt)',
1086
- 'Last update': 'Senast uppdaterad (genereras automatiskt)',
1087
- 'Editor': 'Senaste redigerare (genereras automatiskt)'
1088
  }
1089
 
1090
  # Konfigurera editerbara kolumner
1091
  for col in EDITABLE_COLUMNS:
1092
  if col in df.columns:
1093
- if col == 'Email adress':
1094
- config[col] = st.column_config.TextColumn(
1095
- col,
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="small",
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
- # Grön i 5 sekunder, sedan grå
1396
- if time_diff < 5:
1397
  st.markdown(f"""
1398
  <div class='auto-save-indicator saved'>
1399
- Sparat! <span style='font-size: 0.85em; opacity: 0.8;'>({timestamp})</span>
1400
  </div>
1401
  """, unsafe_allow_html=True)
1402
  else:
1403
  st.markdown(f"""
1404
  <div class='auto-save-indicator'>
1405
- 💾 Senast sparad: <strong>{timestamp}</strong>
1406
  </div>
1407
  """, unsafe_allow_html=True)
1408
  else:
1409
  st.markdown("""
1410
  <div class='auto-save-indicator'>
1411
- 💾 Redo att spara ändringar
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 att spreadsheet har hämtats
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
- log_sheets = st.session_state.log_sheets
 
 
 
 
 
 
1437
 
1438
- # Kontrollera om admin-panelen ska visas
1439
  if st.session_state.show_admin:
1440
- show_admin_panel(log_sheets)
1441
- st.stop()
1442
-
1443
- # Ladda data
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
- if st.session_state.df is None:
1451
- st.stop()
1452
-
1453
- df = st.session_state.df
1454
 
1455
- # Huvudheader
1456
  st.markdown("""
1457
  <div class='main-header'>
1458
  <h1>⚡ ChargeNode Data Manager</h1>
1459
- <p style='font-size: 1.1rem; margin-top: 0.5rem;'>Hantera dina laddområden för migration</p>
1460
  </div>
1461
  """, unsafe_allow_html=True)
1462
 
1463
- # Admin-knapp (synlig för alla, men kräver lösenord)
1464
- col1, col2, col3, col4, col5 = st.columns([1,1,1,1,1])
1465
- with col5:
1466
- if st.button("🔐 Admin Panel"):
1467
- st.session_state.show_admin = True
1468
- st.rerun()
1469
 
1470
- # ============================================================================
1471
- # LOGIN SECTION
1472
- # ============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1473
 
 
1474
  if not st.session_state.authenticated:
1475
- st.markdown("<br>", unsafe_allow_html=True)
1476
 
1477
- col1, col2, col3 = st.columns([1, 2, 1])
 
1478
 
1479
- with col2:
1480
- st.markdown("""
1481
- <div class='login-container'>
1482
- <h2 style='text-align: center; color: var(--chargenode-green); margin-bottom: 1.5rem;'>🔐 Logga in</h2>
1483
- </div>
1484
- """, unsafe_allow_html=True)
1485
-
1486
- st.markdown("""
1487
- Använd ditt <strong>Account ID</strong> som lösenord. Du hittar det i det mejl du fick från ChargeNode.
1488
- """)
1489
-
1490
- st.markdown("<br>", unsafe_allow_html=True)
1491
-
1492
- password = st.text_input("Account ID / Lösenord", type="password", key="password_input")
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
- st.success("✅ Inloggning lyckades!")
1518
- time.sleep(1)
1519
- st.rerun()
1520
- else:
1521
- st.error("❌ Felaktigt lösenord. Kontrollera att du använder rätt Account ID.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1522
 
1523
- st.stop()
1524
-
1525
- # ============================================================================
1526
- # AUTHENTICATED USER SECTION
1527
- # ============================================================================
1528
-
1529
- account_id = st.session_state.account_id
1530
- company_name = st.session_state.company_name
1531
-
1532
- # Hämta företagets områden
1533
- company_areas = get_company_areas(df, account_id)
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
- migration_info += """
1582
- <p><strong>Migreringsdatum:</strong> Ej tillgängligt</p>
1583
- """
1584
-
1585
- migration_info += """
1586
- </div>
1587
- """
1588
-
1589
- st.markdown(migration_info, unsafe_allow_html=True)
1590
-
1591
- st.markdown("<hr>", unsafe_allow_html=True)
1592
-
1593
- # Excel Export och Upload sektion
1594
- st.markdown("## 📤 Export och Import")
1595
-
1596
- col_excel1, col_excel2 = st.columns(2)
1597
-
1598
- with col_excel1:
1599
- st.markdown("### 📥 Ladda ner som Excel")
 
 
 
 
 
 
 
1600
  st.markdown("""
1601
- <div class='excel-info-box'>
1602
- 💡 <strong>Tips:</strong> Editerbara fält är markerade med <span style='background-color: #E8F5E9; padding: 0.2rem 0.4rem; border-radius: 3px;'>grön bakgrund</span> i Excel-filen.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1603
  </div>
1604
  """, unsafe_allow_html=True)
1605
 
1606
- excel_data = export_to_excel(company_areas)
1607
- if excel_data:
1608
- st.download_button(
1609
- label="⬇️ Ladda ner Excel",
1610
- data=excel_data,
1611
- file_name=f"chargenode_data_{company_name.replace(' ', '_')}_{datetime.now().strftime('%Y%m%d')}.xlsx",
1612
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1613
- type="primary",
1614
- use_container_width=True
 
 
 
1615
  )
 
 
 
 
 
 
1616
 
1617
- # Logga nedladdning
1618
- if st.session_state.get('_last_download_logged') != account_id:
1619
- safe_log_activity(
1620
- log_sheets,
1621
- 'DOWNLOAD',
1622
- account_id,
1623
- company_name=company_name,
1624
- details="Excel export"
1625
- )
1626
- st.session_state._last_download_logged = account_id
1627
-
1628
- with col_excel2:
1629
- st.markdown("### 📤 Ladda upp redigerad Excel")
 
 
 
 
 
 
 
 
 
 
 
 
1630
  st.markdown("""
1631
  <div class='excel-info-box'>
1632
- ℹ️ Ladda upp din redigerade Excel-fil här för att uppdatera flera områden samtidigt.
 
1633
  </div>
1634
  """, unsafe_allow_html=True)
1635
 
1636
- uploaded_file = st.file_uploader("Välj Excel-fil", type=['xlsx'], key="excel_uploader")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1637
 
1638
- if uploaded_file:
 
1639
  start_time = time.time()
1640
-
1641
- changes = process_uploaded_excel(uploaded_file, df, account_id)
1642
-
1643
- if changes:
1644
- st.success(f"✅ Fann {len(changes)} ändringar i Excel-filen")
 
 
 
 
 
 
1645
 
1646
- if st.button("💾 Spara alla ändringar från Excel", type="primary", use_container_width=True):
1647
- if not st.session_state.saving_in_progress:
 
 
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
- elif changes is not None:
1685
- st.info("ℹ️ Inga ändringar hittades i Excel-filen")
 
 
 
 
 
 
 
 
 
 
1686
 
1687
- # Performance logging
1688
- duration = time.time() - start_time
1689
- safe_log_performance(
1690
- log_sheets,
1691
- 'PROCESS_EXCEL',
1692
- duration,
1693
- user_id=account_id,
1694
- details=f"{len(changes) if changes else 0} ändringar"
1695
- )
1696
-
1697
- st.markdown("<hr>", unsafe_allow_html=True)
1698
-
1699
- # Information
1700
- with st.expander("ℹ️ Hjälp och Information", expanded=False):
1701
- col_help1, col_help2 = st.columns(2)
1702
-
1703
- with col_help1:
1704
- st.markdown("#### 🔒 Låsta fält (kan ej editeras)")
1705
- st.markdown("Dessa fält har **🔒 (gult hänglås)** i kolumnrubriken och kan inte ändras:")
1706
- visible_readonly = [col for col in READONLY_COLUMNS if col not in HIDDEN_COLUMNS]
1707
- for col in visible_readonly[:15]:
1708
- if col in company_areas.columns:
1709
- st.markdown(f"• 🔒 {col}")
1710
- if len(visible_readonly) > 15:
1711
- st.markdown(f"*...och {len(visible_readonly) - 15} fält till*")
1712
-
1713
- with col_help2:
1714
- st.markdown("#### ✏️ Editerbara fält")
1715
- st.markdown("Dessa fält kan du klicka i och redigera:")
1716
- visible_editable = [col for col in EDITABLE_COLUMNS if col not in HIDDEN_COLUMNS]
1717
- for col in visible_editable:
1718
- if col in company_areas.columns:
1719
- st.markdown(f"• ✏️ {col}")
1720
-
1721
- # Huvudtabell
1722
- st.markdown("## 📊 Dataöversikt")
1723
-
1724
- # VIKTIGT: Instruktionsruta för hur sparning fungerar
1725
- st.markdown("""
1726
- <div style='background-color: #FFF4E6; border-left: 5px solid #FF9800; padding: 1.2rem; margin-bottom: 1.5rem; border-radius: 5px;'>
1727
- <h3 style='margin-top: 0; color: #E65100;'>⚠️ VIKTIGT: Så här sparar du dina ändringar</h3>
1728
- <ol style='margin-bottom: 0.5rem; font-size: 1.05rem;'>
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
- changes_list = []
 
1785
 
1786
- print(f"[DEBUG] === AUTO-SAVE CHECK START ===")
1787
- print(f"[DEBUG] saving_in_progress: {st.session_state.saving_in_progress}")
1788
- print(f"[DEBUG] last_save_time: {st.session_state.last_save_time}")
1789
- print(f"[DEBUG] Original data rader: {len(st.session_state.original_data)}, Edited data rader: {len(edited_data)}")
1790
 
1791
- for idx in range(len(st.session_state.original_data)):
1792
- for col in company_areas_display.columns:
1793
- if col not in READONLY_COLUMNS:
1794
- try:
1795
- orig_val = str(st.session_state.original_data.iloc[idx][col])
1796
- edit_val = str(edited_data.iloc[idx][col])
1797
-
1798
- if orig_val != edit_val:
1799
- print(f"[DEBUG] Ändring hittad! Rad {idx}, Kolumn '{col}': '{orig_val}' → '{edit_val}'")
1800
- changes_list.append({
1801
- 'row_idx': st.session_state.original_data.index[idx],
1802
- 'column': col,
1803
- 'old_value': orig_val,
1804
- 'new_value': edit_val
1805
- })
1806
- except Exception as e:
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
- print(f"[DEBUG] Totalt {len(changes_list)} ändringar hittade")
1811
- print(f"[DEBUG] === AUTO-SAVE CHECK END ===")
 
 
 
 
 
 
 
 
 
1812
 
1813
- if changes_list:
1814
- # Sätt lock INNAN sparning för att förhindra dubbla sparningar
1815
- st.session_state.saving_in_progress = True
1816
 
1817
- try:
1818
- print(f"[DEBUG] Startar sparning av {len(changes_list)} ändringar")
1819
-
1820
- # Batch update med ändringsloggning och felhantering
1821
- success, message = batch_update_cells_with_tracking(
1822
- sheet,
1823
- changes_list,
1824
- df,
1825
- service_account_email,
1826
- log_sheets,
1827
- account_id
1828
- )
1829
-
1830
- if success:
1831
- print(f"[DEBUG] Sparning lyckades!")
1832
- st.session_state.last_save_time = datetime.now()
1833
-
1834
- # Uppdatera data
1835
- st.cache_resource.clear()
1836
- new_df = get_sheet_data(sheet)
1837
- if new_df is not None:
1838
- st.session_state.df = new_df
1839
- company_areas = get_company_areas(new_df, account_id)
1840
- st.session_state.original_data = company_areas.drop(columns=HIDDEN_COLUMNS, errors='ignore').copy()
1841
-
1842
- # Släpp lock och rerun
1843
- st.session_state.saving_in_progress = False
1844
-
1845
- print(f"[DEBUG] Data uppdaterad, kör rerun...")
 
 
1846
 
1847
- # Toast-meddelande som syns även efter rerun
1848
- st.toast(f"✅ {len(changes_list)} ändringar sparade!", icon="✅")
1849
- st.rerun()
1850
- else:
1851
- # Släpp lock vid fel
1852
- st.session_state.saving_in_progress = False
1853
- print(f"[ERROR] Sparning misslyckades: {message}")
1854
- st.error(f"❌ {message}")
 
1855
 
1856
- except Exception as e:
1857
- # Släpp lock vid crash
1858
- st.session_state.saving_in_progress = False
1859
- # Fånga ALLA oväntade fel
1860
- print(f"[CRITICAL ERROR] Auto-save kraschade: {str(e)}")
1861
- st.error(f"❌ Ett oväntat fel uppstod vid sparning: {str(e)}")
1862
-
1863
- # Footer
1864
- st.markdown("<hr>", unsafe_allow_html=True)
1865
- st.markdown(f"""
1866
- <div style='text-align: center; padding: 1rem;'>
1867
- <p style='color: #999999; font-size: 0.9rem;'>
1868
- 🔒 Företagsdata
1869
- Inloggad som <strong style='color: #333;'>{company_name}</strong> •
1870
- <strong style='color: #333;'>{len(company_areas)}</strong> områden
1871
- <strong style='color: #333;'>{total_outlets}</strong> uttag
1872
- </p>
1873
- </div>
1874
- """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 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)