feat: 完整功能迭代 — 移动端+PC端全面优化
新增: - 今日纪要 (6分类+彩色标签+时间选择器) - 客户经理PC端工作台 (我的数据,5模块CRUD) - 用户管理页 (支局长修改角色) - 客户备注/收支费用(金额+单位)/联系人管理 - 拜访人姓名+电话字段 - 客户导入导出/旧周报导入模板下载 - 序号列+分页(25/50/100) 修复/优化: - Hash路由→HTML5 History, Casdoor回调正常 - 时区统一为Asia/Shanghai (today_cst) - 数据库懒加载→selectinload预加载 - 日期解析兼容ISO datetime字符串 - 文件上传定位修复 (position:relative) - 表单button type='button'防止误提交 - 分管领导权限(只读客户档案,不可编辑) - 侧边栏折叠+SVG图标 - 拜访方式/紧急度统一chip风格 - 时间范围统一为el-time-picker(is-range) - 全局CSS设计变量+box-sizing修复滚动条 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -93,9 +93,21 @@ async def list_customers(
|
||||
offset = (page - 1) * page_size
|
||||
query = base_query.order_by(Customer.name).offset(offset).limit(page_size)
|
||||
result = await db.execute(query)
|
||||
items = result.scalars().all()
|
||||
|
||||
# Enrich with manager names
|
||||
if items:
|
||||
mgr_result = await db.execute(
|
||||
select(CustomerAssignment.customer_id, User.name)
|
||||
.join(User, CustomerAssignment.manager_id == User.id)
|
||||
.where(CustomerAssignment.customer_id.in_([c.id for c in items]), CustomerAssignment.role == "primary")
|
||||
)
|
||||
mgr_map = {str(cid): name for cid, name in mgr_result.all()}
|
||||
for item in items:
|
||||
item.primary_manager_name = mgr_map.get(str(item.id), None)
|
||||
|
||||
return CustomerListResponse(
|
||||
items=result.scalars().all(),
|
||||
items=items,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
@@ -350,6 +362,8 @@ async def create_customer(
|
||||
current_user: dict = Depends(require_any_role),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if current_user["role"] == "leader":
|
||||
raise HTTPException(status_code=403, detail="分管领导无法创建客户")
|
||||
"""Create a new customer with optional contacts and manager assignment."""
|
||||
import uuid
|
||||
customer = Customer(
|
||||
@@ -390,6 +404,8 @@ async def update_customer(
|
||||
current_user: dict = Depends(require_any_role),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if current_user["role"] == "leader":
|
||||
raise HTTPException(status_code=403, detail="分管领导无法编辑客户")
|
||||
result = await db.execute(
|
||||
select(Customer).where(Customer.id == customer_id).options(selectinload(Customer.contacts))
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import io
|
||||
import uuid
|
||||
from fastapi import APIRouter, Depends, File, UploadFile, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import get_current_user, require_director
|
||||
@@ -8,6 +10,54 @@ from app.services.excel_import import import_from_excel
|
||||
router = APIRouter(prefix="/import", tags=["Import"])
|
||||
|
||||
|
||||
@router.get("/template")
|
||||
async def download_weekly_report_template():
|
||||
"""Download a 4-sheet weekly report import template."""
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font
|
||||
|
||||
wb = Workbook()
|
||||
header_font = Font(bold=True)
|
||||
|
||||
# Sheet 1: 每日拜访记录
|
||||
ws1 = wb.active
|
||||
ws1.title = "每日拜访记录"
|
||||
ws1.append(["客户单位", "拜访日期", "拜访方式", "时间范围", "拜访人姓名", "拜访人电话", "沟通内容", "客户需求", "同访人员", "客户经理"])
|
||||
for c in ws1[1]: c.font = header_font
|
||||
ws1.append(["XX科技有限公司", "2026-06-23", "上门", "9:00-10:00", "韦柳柏", "13800000000", "沟通了解云桌面需求", "希望扩容", "", "韦矍森"])
|
||||
ws1.column_dimensions['A'].width = 20; ws1.column_dimensions['E'].width = 30; ws1.column_dimensions['F'].width = 20
|
||||
|
||||
# Sheet 2: 下周工作计划
|
||||
ws2 = wb.create_sheet("下周工作计划")
|
||||
ws2.append(["客户单位", "工作计划", "计划拜访时间", "客户经理", "状态"])
|
||||
for c in ws2[1]: c.font = header_font
|
||||
ws2.append(["XX科技有限公司", "跟进云桌面扩容方案", "2026-06-30", "韦柳柏", "计划中"])
|
||||
ws2.column_dimensions['A'].width = 20; ws2.column_dimensions['B'].width = 35
|
||||
|
||||
# Sheet 3: 小微业务商机
|
||||
ws3 = wb.create_sheet("小微业务商机")
|
||||
ws3.append(["客户单位", "产品类型", "金额", "跟进内容具体情况", "跟进状态", "客户经理", "预计列收时间"])
|
||||
for c in ws3[1]: c.font = header_font
|
||||
ws3.append(["XX科技有限公司", "云桌面", "5000元/月", "确认技术方案中", "跟进中", "韦柳柏", "2026Q3"])
|
||||
ws3.column_dimensions['A'].width = 20; ws3.column_dimensions['D'].width = 30
|
||||
|
||||
# Sheet 4: 要客拜访计划
|
||||
ws4 = wb.create_sheet("要客拜访计划")
|
||||
ws4.append(["客户单位", "紧急重要度", "内容描述", "进展状态", "计划拜访时间", "计划拜访人", "拜访对象", "客户经理"])
|
||||
for c in ws4[1]: c.font = header_font
|
||||
ws4.append(["XX科技有限公司", "重要", "拜访技术负责人确认方案", "未开始", "2026-07-01", "韦柳柏", "王局长", "韦矍森"])
|
||||
ws4.column_dimensions['A'].width = 20; ws4.column_dimensions['C'].width = 30
|
||||
|
||||
output = io.BytesIO()
|
||||
wb.save(output)
|
||||
output.seek(0)
|
||||
return StreamingResponse(
|
||||
output,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=weekly_report_template.xlsx"},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/weekly-report")
|
||||
async def import_weekly_report(
|
||||
file: UploadFile = File(...),
|
||||
@@ -20,7 +70,6 @@ async def import_weekly_report(
|
||||
|
||||
content = await file.read()
|
||||
|
||||
# Preview first: parse headers
|
||||
try:
|
||||
import openpyxl
|
||||
wb = openpyxl.load_workbook(io.BytesIO(content), data_only=True)
|
||||
@@ -28,16 +77,12 @@ async def import_weekly_report(
|
||||
for sheet_name in wb.sheetnames:
|
||||
ws = wb[sheet_name]
|
||||
headers = [str(cell.value) for cell in ws[1]]
|
||||
row_count = ws.max_row - 1 # minus header
|
||||
row_count = ws.max_row - 1
|
||||
preview[sheet_name] = {"headers": headers, "row_count": row_count}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Failed to parse Excel: {str(e)}")
|
||||
|
||||
# Import data
|
||||
stats = await import_from_excel(db, content, uuid.UUID(current_user["user_id"]))
|
||||
stats["preview"] = preview
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
import io
|
||||
|
||||
@@ -34,6 +34,8 @@ async def _enrich_visit(visit: Visit, db: AsyncSession) -> dict:
|
||||
"visit_date": visit.visit_date,
|
||||
"visit_method": visit.visit_method,
|
||||
"time_range": visit.time_range,
|
||||
"visitor_name": visit.visitor_name or "",
|
||||
"visitor_phone": visit.visitor_phone or "",
|
||||
"communication_content": visit.communication_content,
|
||||
"customer_demand": visit.customer_demand,
|
||||
"companions": visit.companions,
|
||||
|
||||
Reference in New Issue
Block a user