Update app.py
Browse files
app.py
CHANGED
|
@@ -1110,6 +1110,60 @@ def batch_update_cells_with_tracking(sheet, changes, df, editor_email, log_sheet
|
|
| 1110 |
else:
|
| 1111 |
return False, f"❌ Fel vid sparande: {error_msg}. Kontakta migration@chargenode.eu om problemet kvarstår."
|
| 1112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1113 |
def get_column_config(df):
|
| 1114 |
"""Generera kolumnkonfiguration för data_editor med beskrivningar och dropdowns"""
|
| 1115 |
config = {}
|
|
@@ -1210,13 +1264,15 @@ st.markdown("""
|
|
| 1210 |
}
|
| 1211 |
|
| 1212 |
/* Röd triangel-indikator för editerbara kolumner */
|
| 1213 |
-
/*
|
| 1214 |
-
[data-testid="stDataFrame"] thead th
|
| 1215 |
position: relative;
|
| 1216 |
-
background-color: #
|
|
|
|
|
|
|
| 1217 |
}
|
| 1218 |
|
| 1219 |
-
[data-testid="stDataFrame"] thead th
|
| 1220 |
content: "";
|
| 1221 |
position: absolute;
|
| 1222 |
top: 0;
|
|
@@ -1229,9 +1285,52 @@ st.markdown("""
|
|
| 1229 |
z-index: 10;
|
| 1230 |
}
|
| 1231 |
|
| 1232 |
-
/* Hover-effekt för editerbara
|
| 1233 |
-
[data-testid="stDataFrame"] thead th
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1234 |
background-color: #FFF3E0 !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1235 |
}
|
| 1236 |
|
| 1237 |
.excel-info-box {
|
|
@@ -1882,10 +1981,162 @@ else:
|
|
| 1882 |
|
| 1883 |
**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.
|
| 1884 |
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1885 |
st.markdown("<br>", unsafe_allow_html=True)
|
| 1886 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1887 |
# Ta bort dolda kolumner och sätt kolumnordning med Fastighetsnamn först
|
| 1888 |
-
company_areas_display =
|
| 1889 |
|
| 1890 |
# Skapa kolumnordning med Fastighetsnamn först (för bättre synlighet vid scrolling)
|
| 1891 |
column_order = ['Fastighetsnamn'] + [col for col in company_areas_display.columns if col != 'Fastighetsnamn']
|
|
@@ -1893,6 +2144,15 @@ else:
|
|
| 1893 |
|
| 1894 |
column_config = get_column_config(company_areas_display)
|
| 1895 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1896 |
# Container för att förhindra scroll-hopp
|
| 1897 |
table_container = st.container()
|
| 1898 |
|
|
|
|
| 1110 |
else:
|
| 1111 |
return False, f"❌ Fel vid sparande: {error_msg}. Kontakta migration@chargenode.eu om problemet kvarstår."
|
| 1112 |
|
| 1113 |
+
def calculate_completion_stats(df, editable_cols):
|
| 1114 |
+
"""Beräkna kompletteringsstatistik för editerbara kolumner"""
|
| 1115 |
+
stats = {}
|
| 1116 |
+
|
| 1117 |
+
for col in editable_cols:
|
| 1118 |
+
if col in df.columns:
|
| 1119 |
+
total = len(df)
|
| 1120 |
+
# Räkna celler som har värde (inte nan, inte tom sträng)
|
| 1121 |
+
filled = df[col].apply(lambda x: bool(x and str(x).strip() != '')).sum()
|
| 1122 |
+
stats[col] = {
|
| 1123 |
+
'filled': filled,
|
| 1124 |
+
'total': total,
|
| 1125 |
+
'percentage': (filled / total * 100) if total > 0 else 0
|
| 1126 |
+
}
|
| 1127 |
+
|
| 1128 |
+
# Totala stats
|
| 1129 |
+
total_cells = sum(s['total'] for s in stats.values())
|
| 1130 |
+
total_filled = sum(s['filled'] for s in stats.values())
|
| 1131 |
+
overall_percentage = (total_filled / total_cells * 100) if total_cells > 0 else 0
|
| 1132 |
+
|
| 1133 |
+
return {
|
| 1134 |
+
'by_column': stats,
|
| 1135 |
+
'overall': {
|
| 1136 |
+
'filled': total_filled,
|
| 1137 |
+
'total': total_cells,
|
| 1138 |
+
'percentage': overall_percentage
|
| 1139 |
+
}
|
| 1140 |
+
}
|
| 1141 |
+
|
| 1142 |
+
def get_incomplete_rows(df, editable_cols):
|
| 1143 |
+
"""Hitta rader där minst ett editerbart fält är tomt"""
|
| 1144 |
+
incomplete_mask = pd.Series([False] * len(df), index=df.index)
|
| 1145 |
+
|
| 1146 |
+
for col in editable_cols:
|
| 1147 |
+
if col in df.columns:
|
| 1148 |
+
# Markera rader där cellen är tom
|
| 1149 |
+
empty_mask = df[col].apply(lambda x: not bool(x and str(x).strip() != ''))
|
| 1150 |
+
incomplete_mask = incomplete_mask | empty_mask
|
| 1151 |
+
|
| 1152 |
+
return incomplete_mask
|
| 1153 |
+
|
| 1154 |
+
def style_empty_cells(df, editable_cols):
|
| 1155 |
+
"""Returnera styling-funktion för att markera tomma celler"""
|
| 1156 |
+
def highlight_empty(row):
|
| 1157 |
+
styles = [''] * len(row)
|
| 1158 |
+
for idx, (col_name, value) in enumerate(row.items()):
|
| 1159 |
+
if col_name in editable_cols:
|
| 1160 |
+
# Markera om värdet är tomt eller None
|
| 1161 |
+
if pd.isna(value) or str(value).strip() == '' or str(value).strip() == '0':
|
| 1162 |
+
styles[idx] = 'background-color: #FFE5E5; border: 2px solid #DC3545;'
|
| 1163 |
+
return styles
|
| 1164 |
+
|
| 1165 |
+
return highlight_empty
|
| 1166 |
+
|
| 1167 |
def get_column_config(df):
|
| 1168 |
"""Generera kolumnkonfiguration för data_editor med beskrivningar och dropdowns"""
|
| 1169 |
config = {}
|
|
|
|
| 1264 |
}
|
| 1265 |
|
| 1266 |
/* Röd triangel-indikator för editerbara kolumner */
|
| 1267 |
+
/* Markera editerbara kolumnrubriker */
|
| 1268 |
+
[data-testid="stDataFrame"] thead th[aria-label*="🔺"] {
|
| 1269 |
position: relative;
|
| 1270 |
+
background-color: #FFF4E6 !important;
|
| 1271 |
+
border: 2px solid #FF9800 !important;
|
| 1272 |
+
font-weight: bold !important;
|
| 1273 |
}
|
| 1274 |
|
| 1275 |
+
[data-testid="stDataFrame"] thead th[aria-label*="🔺"]::after {
|
| 1276 |
content: "";
|
| 1277 |
position: absolute;
|
| 1278 |
top: 0;
|
|
|
|
| 1285 |
z-index: 10;
|
| 1286 |
}
|
| 1287 |
|
| 1288 |
+
/* Hover-effekt för editerbara kolumnrubriker */
|
| 1289 |
+
[data-testid="stDataFrame"] thead th[aria-label*="🔺"]:hover {
|
| 1290 |
+
background-color: #FFE5CC !important;
|
| 1291 |
+
}
|
| 1292 |
+
|
| 1293 |
+
/* VIKTIGT: Markera alla CELLER i editerbara kolumner med ljusorange bakgrund */
|
| 1294 |
+
/* Vi använder nth-child för att rikta in oss på specifika kolumner */
|
| 1295 |
+
/* Detta måste justeras baserat på vilka kolumner som är editerbara */
|
| 1296 |
+
[data-testid="stDataFrame"] tbody td {
|
| 1297 |
+
position: relative;
|
| 1298 |
+
}
|
| 1299 |
+
|
| 1300 |
+
/* Lägg till en subtil border på alla celler för bättre läsbarhet */
|
| 1301 |
+
[data-testid="stDataFrame"] td,
|
| 1302 |
+
[data-testid="stDataFrame"] th {
|
| 1303 |
+
border: 1px solid #E0E0E0 !important;
|
| 1304 |
+
}
|
| 1305 |
+
|
| 1306 |
+
/* Styla editerbara celler i data_editor - de som inte är disabled */
|
| 1307 |
+
div[data-testid="stDataFrameResizable"] input:not(:disabled),
|
| 1308 |
+
div[data-testid="stDataFrameResizable"] textarea:not(:disabled) {
|
| 1309 |
+
background-color: #FFF9F0 !important;
|
| 1310 |
+
border: 2px solid #FFB74D !important;
|
| 1311 |
+
border-radius: 4px;
|
| 1312 |
+
}
|
| 1313 |
+
|
| 1314 |
+
/* Hover-effekt på editerbara celler */
|
| 1315 |
+
div[data-testid="stDataFrameResizable"] input:not(:disabled):hover,
|
| 1316 |
+
div[data-testid="stDataFrameResizable"] textarea:not(:disabled):hover {
|
| 1317 |
background-color: #FFF3E0 !important;
|
| 1318 |
+
border-color: #FF9800 !important;
|
| 1319 |
+
}
|
| 1320 |
+
|
| 1321 |
+
/* Focus-effekt på editerbara celler */
|
| 1322 |
+
div[data-testid="stDataFrameResizable"] input:not(:disabled):focus,
|
| 1323 |
+
div[data-testid="stDataFrameResizable"] textarea:not(:disabled):focus {
|
| 1324 |
+
background-color: #FFFFFF !important;
|
| 1325 |
+
border-color: #FF5722 !important;
|
| 1326 |
+
box-shadow: 0 0 0 2px rgba(255, 87, 34, 0.2) !important;
|
| 1327 |
+
}
|
| 1328 |
+
|
| 1329 |
+
/* Disabled/readonly celler får neutral färg */
|
| 1330 |
+
div[data-testid="stDataFrameResizable"] input:disabled,
|
| 1331 |
+
div[data-testid="stDataFrameResizable"] textarea:disabled {
|
| 1332 |
+
background-color: #F5F5F5 !important;
|
| 1333 |
+
border: 1px solid #E0E0E0 !important;
|
| 1334 |
}
|
| 1335 |
|
| 1336 |
.excel-info-box {
|
|
|
|
| 1981 |
|
| 1982 |
**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.
|
| 1983 |
""")
|
| 1984 |
+
|
| 1985 |
+
# ============================================================================
|
| 1986 |
+
# KOMPLETTERINGSSTATISTIK - Ny sektion
|
| 1987 |
+
# ============================================================================
|
| 1988 |
+
st.markdown("---")
|
| 1989 |
+
st.markdown("### 📈 Kompletteringsstatus")
|
| 1990 |
+
|
| 1991 |
+
# Beräkna komplettering
|
| 1992 |
+
visible_editable = [col for col in EDITABLE_COLUMNS if col in company_areas.columns and col not in HIDDEN_COLUMNS]
|
| 1993 |
+
|
| 1994 |
+
if visible_editable:
|
| 1995 |
+
# Total komplettering
|
| 1996 |
+
total_cells = len(company_areas) * len(visible_editable)
|
| 1997 |
+
filled_cells = 0
|
| 1998 |
+
|
| 1999 |
+
for col in visible_editable:
|
| 2000 |
+
filled_cells += company_areas[col].notna().sum()
|
| 2001 |
+
# Räkna också icke-tomma strängar
|
| 2002 |
+
filled_cells += (company_areas[col].astype(str).str.strip() != '').sum()
|
| 2003 |
+
# Dra bort dubbelräkning
|
| 2004 |
+
filled_cells -= company_areas[col].notna().sum()
|
| 2005 |
+
|
| 2006 |
+
# Bättre beräkning: räkna celler som har värde OCH inte är tomma strängar
|
| 2007 |
+
filled_cells = 0
|
| 2008 |
+
for col in visible_editable:
|
| 2009 |
+
filled_cells += ((company_areas[col].notna()) & (company_areas[col].astype(str).str.strip() != '')).sum()
|
| 2010 |
+
|
| 2011 |
+
completion_pct = (filled_cells / total_cells * 100) if total_cells > 0 else 0
|
| 2012 |
+
|
| 2013 |
+
# Metrics i kolumner
|
| 2014 |
+
metric_col1, metric_col2, metric_col3, metric_col4 = st.columns(4)
|
| 2015 |
+
|
| 2016 |
+
with metric_col1:
|
| 2017 |
+
st.metric(
|
| 2018 |
+
label="✅ Kompletteringsgrad",
|
| 2019 |
+
value=f"{completion_pct:.0f}%",
|
| 2020 |
+
help="Procent av alla editerbara fält som är ifyllda"
|
| 2021 |
+
)
|
| 2022 |
+
|
| 2023 |
+
with metric_col2:
|
| 2024 |
+
st.metric(
|
| 2025 |
+
label="📝 Ifyllda fält",
|
| 2026 |
+
value=f"{filled_cells}/{total_cells}",
|
| 2027 |
+
help="Antal ifyllda fält av totalt antal editerbara fält"
|
| 2028 |
+
)
|
| 2029 |
+
|
| 2030 |
+
with metric_col3:
|
| 2031 |
+
# Räkna kompletta rader (alla editerbara fält ifyllda)
|
| 2032 |
+
complete_rows = 0
|
| 2033 |
+
for idx in range(len(company_areas)):
|
| 2034 |
+
row_complete = True
|
| 2035 |
+
for col in visible_editable:
|
| 2036 |
+
val = company_areas.iloc[idx][col]
|
| 2037 |
+
if pd.isna(val) or str(val).strip() == '':
|
| 2038 |
+
row_complete = False
|
| 2039 |
+
break
|
| 2040 |
+
if row_complete:
|
| 2041 |
+
complete_rows += 1
|
| 2042 |
+
|
| 2043 |
+
st.metric(
|
| 2044 |
+
label="✨ Kompletta områden",
|
| 2045 |
+
value=f"{complete_rows}/{len(company_areas)}",
|
| 2046 |
+
help="Områden där alla editerbara fält är ifyllda"
|
| 2047 |
+
)
|
| 2048 |
+
|
| 2049 |
+
with metric_col4:
|
| 2050 |
+
incomplete_rows = len(company_areas) - complete_rows
|
| 2051 |
+
st.metric(
|
| 2052 |
+
label="⚠️ Ofullständiga områden",
|
| 2053 |
+
value=incomplete_rows,
|
| 2054 |
+
help="Områden som behöver mer information"
|
| 2055 |
+
)
|
| 2056 |
+
|
| 2057 |
+
# Progress bar
|
| 2058 |
+
st.progress(completion_pct / 100, text=f"Total komplettering: {completion_pct:.1f}%")
|
| 2059 |
+
|
| 2060 |
+
# Status per kolumn - Expanderbar sektion
|
| 2061 |
+
with st.expander("📋 Detaljerad status per fält", expanded=False):
|
| 2062 |
+
st.markdown("**Se hur många områden som har fyllt i varje fält:**")
|
| 2063 |
+
|
| 2064 |
+
for col in visible_editable:
|
| 2065 |
+
filled = ((company_areas[col].notna()) & (company_areas[col].astype(str).str.strip() != '')).sum()
|
| 2066 |
+
total = len(company_areas)
|
| 2067 |
+
pct = (filled / total * 100) if total > 0 else 0
|
| 2068 |
+
|
| 2069 |
+
# Färgkod baserat på komplettering
|
| 2070 |
+
if pct >= 90:
|
| 2071 |
+
status_emoji = "🟢"
|
| 2072 |
+
elif pct >= 50:
|
| 2073 |
+
status_emoji = "🟡"
|
| 2074 |
+
else:
|
| 2075 |
+
status_emoji = "🔴"
|
| 2076 |
+
|
| 2077 |
+
col_left, col_right = st.columns([4, 1])
|
| 2078 |
+
with col_left:
|
| 2079 |
+
st.progress(pct / 100, text=f"{status_emoji} {col}")
|
| 2080 |
+
with col_right:
|
| 2081 |
+
st.caption(f"{filled}/{total} ({pct:.0f}%)")
|
| 2082 |
+
|
| 2083 |
+
# Filter-sektion
|
| 2084 |
+
st.markdown("---")
|
| 2085 |
+
filter_col1, filter_col2 = st.columns([2, 3])
|
| 2086 |
+
|
| 2087 |
+
with filter_col1:
|
| 2088 |
+
view_filter = st.radio(
|
| 2089 |
+
"🔍 Visa:",
|
| 2090 |
+
["📋 Alla områden", "⚠️ Endast ofullständiga", "✅ Endast kompletta"],
|
| 2091 |
+
horizontal=False,
|
| 2092 |
+
help="Filtrera tabellen för att fokusera på det som behöver göras"
|
| 2093 |
+
)
|
| 2094 |
+
|
| 2095 |
+
with filter_col2:
|
| 2096 |
+
if view_filter == "⚠️ Endast ofullständiga":
|
| 2097 |
+
st.info("💡 **Tips:** Fokusera på att fylla i de röd-markerade fälten i dessa områden först!")
|
| 2098 |
+
elif view_filter == "✅ Endast kompletta":
|
| 2099 |
+
st.success("🎉 **Bra jobbat!** Dessa områden har all nödvändig information ifylld.")
|
| 2100 |
+
else:
|
| 2101 |
+
st.info(f"📊 Visar alla {len(company_areas)} områden")
|
| 2102 |
+
|
| 2103 |
st.markdown("<br>", unsafe_allow_html=True)
|
| 2104 |
|
| 2105 |
+
# Applicera filter baserat på användarens val
|
| 2106 |
+
company_areas_filtered = company_areas.copy()
|
| 2107 |
+
|
| 2108 |
+
if visible_editable and 'view_filter' in locals():
|
| 2109 |
+
if view_filter == "⚠️ Endast ofullständiga":
|
| 2110 |
+
# Filtrera till rader där minst ett editerbart fält är tomt
|
| 2111 |
+
mask = pd.Series([False] * len(company_areas))
|
| 2112 |
+
for idx in range(len(company_areas)):
|
| 2113 |
+
for col in visible_editable:
|
| 2114 |
+
val = company_areas.iloc[idx][col]
|
| 2115 |
+
if pd.isna(val) or str(val).strip() == '':
|
| 2116 |
+
mask.iloc[idx] = True
|
| 2117 |
+
break
|
| 2118 |
+
company_areas_filtered = company_areas[mask]
|
| 2119 |
+
|
| 2120 |
+
if len(company_areas_filtered) == 0:
|
| 2121 |
+
st.success("🎉 Fantastiskt! Alla områden är kompletta!")
|
| 2122 |
+
st.balloons()
|
| 2123 |
+
|
| 2124 |
+
elif view_filter == "✅ Endast kompletta":
|
| 2125 |
+
# Filtrera till rader där alla editerbara fält är ifyllda
|
| 2126 |
+
mask = pd.Series([True] * len(company_areas))
|
| 2127 |
+
for idx in range(len(company_areas)):
|
| 2128 |
+
for col in visible_editable:
|
| 2129 |
+
val = company_areas.iloc[idx][col]
|
| 2130 |
+
if pd.isna(val) or str(val).strip() == '':
|
| 2131 |
+
mask.iloc[idx] = False
|
| 2132 |
+
break
|
| 2133 |
+
company_areas_filtered = company_areas[mask]
|
| 2134 |
+
|
| 2135 |
+
if len(company_areas_filtered) == 0:
|
| 2136 |
+
st.warning("⚠️ Inga kompletta områden än. Börja fylla i de editerbara fälten!")
|
| 2137 |
+
|
| 2138 |
# Ta bort dolda kolumner och sätt kolumnordning med Fastighetsnamn först
|
| 2139 |
+
company_areas_display = company_areas_filtered.drop(columns=HIDDEN_COLUMNS, errors='ignore')
|
| 2140 |
|
| 2141 |
# Skapa kolumnordning med Fastighetsnamn först (för bättre synlighet vid scrolling)
|
| 2142 |
column_order = ['Fastighetsnamn'] + [col for col in company_areas_display.columns if col != 'Fastighetsnamn']
|
|
|
|
| 2144 |
|
| 2145 |
column_config = get_column_config(company_areas_display)
|
| 2146 |
|
| 2147 |
+
# Visa antal rader som visas
|
| 2148 |
+
if len(company_areas_display) > 0:
|
| 2149 |
+
st.markdown(f"""
|
| 2150 |
+
<div style='background-color: #E3F2FD; padding: 1rem; border-radius: 8px; margin: 1rem 0; border-left: 4px solid #2196F3;'>
|
| 2151 |
+
<strong>📍 Visar {len(company_areas_display)} av {len(company_areas)} områden</strong><br>
|
| 2152 |
+
<small>💡 <strong>Tips:</strong> Editerbara celler (🔺) har orange bakgrund och ram. Klicka i en cell för att börja redigera. Ändringar sparas automatiskt.</small>
|
| 2153 |
+
</div>
|
| 2154 |
+
""", unsafe_allow_html=True)
|
| 2155 |
+
|
| 2156 |
# Container för att förhindra scroll-hopp
|
| 2157 |
table_container = st.container()
|
| 2158 |
|