161 lines
5.3 KiB
Python
161 lines
5.3 KiB
Python
"""Styled .xlsx export of job requisitions — openpyxl only.
|
|
|
|
Pure module: no FastAPI imports and no HTTPException.
|
|
|
|
Takes the already-serialized rows from JobPost.fetch_jobs (serialize_job_row
|
|
dicts) so the export always matches what the Jobs screen shows, filters
|
|
included. Returns the finished workbook as bytes for a Response body.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from io import BytesIO
|
|
|
|
from openpyxl import Workbook
|
|
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
|
|
from openpyxl.utils import get_column_letter
|
|
|
|
BRAND_DARK = "0B3D2E" # header/banner green, matches the app chrome
|
|
BRAND_STRIPE = "EFF7F2" # zebra row tint
|
|
BORDER_TINT = "CBDCD2"
|
|
|
|
STATUS_LABELS = {"open": "Open", "closed": "Closed", "on_hold": "On Hold"}
|
|
STATUS_COLORS = {"open": "1B7F4B", "closed": "B3261E", "on_hold": "9A6700"}
|
|
|
|
# (header, column width)
|
|
COLUMNS = [
|
|
("Title", 34),
|
|
("Department", 16),
|
|
("Location", 20),
|
|
("Type", 12),
|
|
("Platform", 12),
|
|
("Vacancies", 11),
|
|
("Experience", 13),
|
|
("Salary", 16),
|
|
("Status", 10),
|
|
("Publishing", 12),
|
|
("Recruiter", 18),
|
|
("Created By", 18),
|
|
("Created", 13),
|
|
("Requirements", 46),
|
|
("Nice to Have", 34),
|
|
("Description", 60),
|
|
]
|
|
|
|
_THIN = Side(style="thin", color=BORDER_TINT)
|
|
_BORDER = Border(left=_THIN, right=_THIN, top=_THIN, bottom=_THIN)
|
|
|
|
|
|
def _experience(row) -> str:
|
|
lo, hi = row.get("experience_min"), row.get("experience_max")
|
|
if lo is None and hi is None:
|
|
return ""
|
|
if lo is not None and hi is not None:
|
|
return f"{lo}-{hi} years"
|
|
return f"{lo if lo is not None else hi}+ years"
|
|
|
|
|
|
def _bullets(items) -> str:
|
|
return "\n".join(f"• {str(i).strip()}" for i in (items or []) if str(i).strip())
|
|
|
|
|
|
def _created(row):
|
|
raw = row.get("created_at")
|
|
if not raw:
|
|
return None
|
|
try:
|
|
return datetime.fromisoformat(raw).replace(tzinfo=None)
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def build_jobs_workbook(rows) -> bytes:
|
|
wb = Workbook()
|
|
ws = wb.active
|
|
ws.title = "Jobs"
|
|
ws.sheet_properties.tabColor = BRAND_DARK
|
|
ws.sheet_view.showGridLines = False
|
|
|
|
last_col = get_column_letter(len(COLUMNS))
|
|
for idx, (_, width) in enumerate(COLUMNS, start=1):
|
|
ws.column_dimensions[get_column_letter(idx)].width = width
|
|
|
|
# Banner
|
|
ws.merge_cells(f"A1:{last_col}1")
|
|
banner = ws["A1"]
|
|
banner.value = "Jobs Export"
|
|
banner.font = Font(size=16, bold=True, color=BRAND_DARK)
|
|
banner.alignment = Alignment(vertical="center")
|
|
ws.row_dimensions[1].height = 30
|
|
|
|
ws.merge_cells(f"A2:{last_col}2")
|
|
sub = ws["A2"]
|
|
sub.value = (
|
|
f"TalentFlow · generated {datetime.now().strftime('%d %b %Y, %H:%M')} · "
|
|
f"{len(rows)} requisition{'s' if len(rows) != 1 else ''}"
|
|
)
|
|
sub.font = Font(size=10, color="6B7A72")
|
|
ws.row_dimensions[3].height = 6
|
|
|
|
# Header
|
|
header_row = 4
|
|
for idx, (label, _) in enumerate(COLUMNS, start=1):
|
|
cell = ws.cell(row=header_row, column=idx, value=label)
|
|
cell.font = Font(bold=True, color="FFFFFF", size=11)
|
|
cell.fill = PatternFill("solid", fgColor=BRAND_DARK)
|
|
cell.alignment = Alignment(horizontal="center", vertical="center")
|
|
cell.border = _BORDER
|
|
ws.row_dimensions[header_row].height = 22
|
|
|
|
# Data
|
|
top = Alignment(vertical="top", wrap_text=False)
|
|
wrap = Alignment(vertical="top", wrap_text=True)
|
|
center = Alignment(horizontal="center", vertical="top")
|
|
for r, row in enumerate(rows, start=header_row + 1):
|
|
status_key = row.get("requisition_status") or ""
|
|
values = [
|
|
row.get("title") or "",
|
|
row.get("department") or "",
|
|
row.get("location") or "",
|
|
row.get("employment_type") or "",
|
|
row.get("platform") or "",
|
|
row.get("vacancies"),
|
|
_experience(row),
|
|
row.get("salary") or "",
|
|
STATUS_LABELS.get(status_key, status_key),
|
|
row.get("status") or "",
|
|
row.get("recruiter_name") or "",
|
|
row.get("created_by_name") or "",
|
|
_created(row),
|
|
_bullets(row.get("requirements")),
|
|
_bullets(row.get("optional_skills")),
|
|
(row.get("description") or "").strip(),
|
|
]
|
|
stripe = r % 2 == 0
|
|
for c, value in enumerate(values, start=1):
|
|
cell = ws.cell(row=r, column=c, value=value)
|
|
cell.border = _BORDER
|
|
cell.alignment = top
|
|
if stripe:
|
|
cell.fill = PatternFill("solid", fgColor=BRAND_STRIPE)
|
|
ws.cell(row=r, column=1).font = Font(bold=True)
|
|
ws.cell(row=r, column=6).alignment = center
|
|
status_cell = ws.cell(row=r, column=9)
|
|
status_cell.alignment = center
|
|
if status_key in STATUS_COLORS:
|
|
status_cell.font = Font(bold=True, color=STATUS_COLORS[status_key])
|
|
created_cell = ws.cell(row=r, column=13)
|
|
if created_cell.value is not None:
|
|
created_cell.number_format = "dd mmm yyyy"
|
|
for c in (14, 15, 16):
|
|
ws.cell(row=r, column=c).alignment = wrap
|
|
|
|
last_row = header_row + max(len(rows), 1)
|
|
ws.auto_filter.ref = f"A{header_row}:{last_col}{last_row}"
|
|
ws.freeze_panes = f"A{header_row + 1}"
|
|
|
|
buf = BytesIO()
|
|
wb.save(buf)
|
|
return buf.getvalue()
|