From d338722880c46cfda5b5f660f5a320ada5b6f0bb Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Sun, 1 Mar 2026 02:28:21 +0200 Subject: [PATCH] Add support to import with xl file format --- backend/main.py | 40 +++++++++++++++++++--- backend/requirements.txt | 1 + frontend/src/components/ImportContacts.jsx | 2 +- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/backend/main.py b/backend/main.py index 0e08096..7cd5951 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1493,6 +1493,32 @@ def _parse_csv_rows(content: bytes) -> list[dict]: return [dict(row) for row in reader] +def _parse_xlsx_rows(content: bytes) -> list[dict]: + """Parse an XLSX (Excel) file and return a list of dicts. + Uses the first sheet; first row is treated as the header. + """ + import openpyxl + wb = openpyxl.load_workbook(io.BytesIO(content), read_only=True, data_only=True) + ws = wb.active + rows = list(ws.iter_rows(values_only=True)) + wb.close() + if not rows: + return [] + # First row = headers; normalise None headers to empty string + headers = [str(h).strip() if h is not None else "" for h in rows[0]] + result = [] + for row in rows[1:]: + # Skip completely empty rows + if all(v is None or str(v).strip() == "" for v in row): + continue + result.append({ + headers[i]: (str(v).strip() if v is not None else "") + for i, v in enumerate(row) + if i < len(headers) and headers[i] # skip header-less columns + }) + return result + + def _parse_json_rows(content: bytes) -> list[dict]: """Parse a JSON file — supports array at root OR {data: [...]}.""" payload = json.loads(content.decode("utf-8-sig", errors="replace")) @@ -1593,16 +1619,20 @@ async def import_contacts( try: if filename.endswith(".json"): raw_rows = _parse_json_rows(content) - elif filename.endswith(".csv") or filename.endswith(".xlsx"): - # For XLSX export from our own app, treat as CSV (xlsx export from - # GuestList produces proper column headers in English) + elif filename.endswith(".xlsx"): + raw_rows = _parse_xlsx_rows(content) + elif filename.endswith(".csv"): raw_rows = _parse_csv_rows(content) else: - # Sniff: try JSON then CSV + # Sniff: try JSON → xlsx magic bytes → CSV try: raw_rows = _parse_json_rows(content) except Exception: - raw_rows = _parse_csv_rows(content) + # XLSX files start with PK (zip magic bytes 50 4B) + if content[:2] == b'PK': + raw_rows = _parse_xlsx_rows(content) + else: + raw_rows = _parse_csv_rows(content) except Exception as exc: raise HTTPException(status_code=422, detail=f"Cannot parse file: {exc}") diff --git a/backend/requirements.txt b/backend/requirements.txt index 0681831..28b8368 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -6,3 +6,4 @@ pydantic[email]>=2.5.0 httpx>=0.25.2 python-dotenv>=1.0.0 python-multipart>=0.0.7 +openpyxl>=3.1.2 diff --git a/frontend/src/components/ImportContacts.jsx b/frontend/src/components/ImportContacts.jsx index b16ce95..b3e20c9 100644 --- a/frontend/src/components/ImportContacts.jsx +++ b/frontend/src/components/ImportContacts.jsx @@ -118,7 +118,7 @@ function ImportContacts({ eventId, onImportComplete }) {