Phase 1: Workflows UI, Dedup/Merge UI, Import/Export UI, Print/PDF
- Workflows UI: full page with definitions/instances tabs, step editor, instance detail with approve/reject - Dedup/Merge UI: duplicate detection, side-by-side comparison, field-level merge dialog, merge history - Import/Export UI: import wizard (dry-run preview), export panel (CSV/XLSX), backend export route added - Print/PDF: PrintButton component, print.css, integrated in Contacts/Calendar/Reports/ContactDetail - Backend: GET /api/v1/export endpoint, export_companies_csv() service function - Routes: /workflows, /contacts/dedup, /import-export registered - Menu items: Workflows, Import/Export, Duplikate added to automation plugin manifest - IMPLEMENTATION_PLAN.md: audit-corrected plan for all 14 remaining features
This commit is contained in:
@@ -70,7 +70,29 @@ class AutomationPlugin(BasePlugin):
|
||||
"agents:execute",
|
||||
],
|
||||
is_core=False,
|
||||
menu_items=[],
|
||||
menu_items=[
|
||||
FrontendMenuItem(
|
||||
label_key="nav.workflows",
|
||||
label="Workflows",
|
||||
path="/workflows",
|
||||
icon="Workflow",
|
||||
order=52,
|
||||
),
|
||||
FrontendMenuItem(
|
||||
label_key="nav.importExport",
|
||||
label="Import / Export",
|
||||
path="/import-export",
|
||||
icon="ArrowUpDown",
|
||||
order=53,
|
||||
),
|
||||
FrontendMenuItem(
|
||||
label_key="nav.dedupMerge",
|
||||
label="Duplikate",
|
||||
path="/contacts/dedup",
|
||||
icon="Copy",
|
||||
order=54,
|
||||
),
|
||||
],
|
||||
page_routes=[
|
||||
FrontendPageRoute(
|
||||
path="/automation",
|
||||
@@ -82,6 +104,16 @@ class AutomationPlugin(BasePlugin):
|
||||
component="@/pages/AgentDashboard",
|
||||
order=51,
|
||||
),
|
||||
FrontendPageRoute(
|
||||
path="/workflows",
|
||||
component="@/pages/Workflows",
|
||||
order=52,
|
||||
),
|
||||
FrontendPageRoute(
|
||||
path="/import-export",
|
||||
component="@/pages/ImportExport",
|
||||
order=53,
|
||||
),
|
||||
],
|
||||
settings_pages=[
|
||||
FrontendSettingsPage(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import uuid
|
||||
|
||||
from fastapi import (
|
||||
@@ -10,9 +11,11 @@ from fastapi import (
|
||||
File,
|
||||
Form,
|
||||
HTTPException,
|
||||
Query,
|
||||
UploadFile,
|
||||
)
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.responses import StreamingResponse
|
||||
|
||||
from app.core.db import get_db
|
||||
from app.deps import require_permission
|
||||
@@ -73,3 +76,76 @@ async def import_csv_preview(
|
||||
dry_run=True,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/export")
|
||||
async def export_data(
|
||||
entity_type: str = Query("contacts", pattern="^(contacts|companies)$"),
|
||||
format: str = Query("csv", pattern="^(csv|xlsx)$"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(require_permission("import_export:read")),
|
||||
):
|
||||
"""Export contacts or companies as CSV or XLSX file download.
|
||||
|
||||
Query params:
|
||||
- entity_type: 'contacts' or 'companies' (default: contacts)
|
||||
- format: 'csv' or 'xlsx' (default: csv)
|
||||
"""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
|
||||
# Determine filename based on entity_type
|
||||
filename = f"{entity_type}_export.csv"
|
||||
|
||||
# Fetch CSV data from the appropriate service function
|
||||
if entity_type == "contacts":
|
||||
csv_data = await import_export_service.export_contacts_csv(db, tenant_id)
|
||||
elif entity_type == "companies":
|
||||
csv_data = await import_export_service.export_companies_csv(db, tenant_id)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported entity_type: {entity_type}")
|
||||
|
||||
# XLSX format handling
|
||||
if format == "xlsx":
|
||||
try:
|
||||
import openpyxl
|
||||
|
||||
wb = openpyxl.Workbook()
|
||||
ws = wb.active
|
||||
ws.title = entity_type.capitalize()
|
||||
|
||||
# Parse the CSV string and populate the worksheet
|
||||
import csv as _csv
|
||||
|
||||
reader = _csv.reader(io.StringIO(csv_data))
|
||||
for row in reader:
|
||||
ws.append(row)
|
||||
|
||||
xlsx_buffer = io.BytesIO()
|
||||
wb.save(xlsx_buffer)
|
||||
xlsx_buffer.seek(0)
|
||||
|
||||
xlsx_filename = f"{entity_type}_export.xlsx"
|
||||
return StreamingResponse(
|
||||
xlsx_buffer,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": f'attachment; filename="{xlsx_filename}"'},
|
||||
)
|
||||
except ImportError:
|
||||
# openpyxl not available — fall back to CSV with a warning header
|
||||
media_type = "text/csv"
|
||||
headers = {
|
||||
"Content-Disposition": f'attachment; filename="{filename}"',
|
||||
"X-Export-Warning": "openpyxl not installed, falling back to CSV format",
|
||||
}
|
||||
return StreamingResponse(
|
||||
iter([csv_data.encode("utf-8")]),
|
||||
media_type=media_type,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
# Default: CSV format
|
||||
return StreamingResponse(
|
||||
iter([csv_data.encode("utf-8")]),
|
||||
media_type="text/csv",
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||||
)
|
||||
|
||||
@@ -252,3 +252,42 @@ async def export_contacts_csv(
|
||||
]
|
||||
)
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
async def export_companies_csv(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
) -> str:
|
||||
"""Export companies (Contact.type == 'company') as CSV string."""
|
||||
q = (
|
||||
select(Contact)
|
||||
.where(
|
||||
Contact.tenant_id == tenant_id,
|
||||
Contact.deleted_at.is_(None),
|
||||
Contact.type == "company",
|
||||
)
|
||||
.order_by(Contact.name)
|
||||
)
|
||||
result = await db.execute(q)
|
||||
companies = result.scalars().all()
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(
|
||||
["id", "type", "name", "email", "phone", "website", "city", "postalcode", "country"]
|
||||
)
|
||||
for c in companies:
|
||||
writer.writerow(
|
||||
[
|
||||
str(c.id),
|
||||
c.type or "company",
|
||||
c.name or "",
|
||||
c.email_1 or "",
|
||||
c.phone_1 or "",
|
||||
c.website or "",
|
||||
c.mailing_city or "",
|
||||
c.mailing_postalcode or "",
|
||||
c.mailing_country or "",
|
||||
]
|
||||
)
|
||||
return output.getvalue()
|
||||
|
||||
Reference in New Issue
Block a user