From 1ec20ee53ebc48a091b02752b2ae4cd25a6a7078 Mon Sep 17 00:00:00 2001 From: v6ole Date: Mon, 17 Aug 2026 09:17:03 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E4=BF=9D=E5=AD=98=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E6=9C=AA=E6=8F=90=E4=BA=A4=E6=94=B9=E5=8A=A8?= =?UTF-8?q?=EF=BC=88=E4=BC=81=E5=BE=AE=E5=AE=9A=E5=90=91=E6=8E=A8=E9=80=81?= =?UTF-8?q?+=E6=90=9C=E7=B4=A2=E7=AD=9B=E9=80=89UI=E7=AD=89=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 保存合并前工作树中遗留的进行中改动,避免分支合并时丢失: - scheduler: 填报汇总改为定向推送给支局长/领导(而非广播@all) - router: JWT token 校验修复,bind token(UUID)不被误剥离 - 工作计划/计划列表: 搜索+筛选+分页 UI - 各列表 API 增加 search 参数支持 - docker-compose.backend.yml + backend/.dockerignore 纳入版本管理 Co-Authored-By: Claude --- backend/.dockerignore | 25 ++ backend/Dockerfile | 3 + backend/app/api/dashboard.py | 2 +- backend/app/api/key_visits.py | 2 + backend/app/api/mini_business.py | 2 + backend/app/api/visits.py | 6 +- backend/app/api/wecom.py | 5 +- backend/app/api/work_plans.py | 2 + backend/app/schemas/key_visit.py | 1 + backend/app/schemas/mini_business.py | 1 + backend/app/schemas/visit.py | 1 + backend/app/schemas/work_plan.py | 1 + backend/app/services/dashboard.py | 28 +- backend/app/services/scheduler.py | 48 +-- backend/app/services/scheduler_manager.py | 8 +- backend/app/services/wecom.py | 7 +- docker-compose.backend.yml | 22 ++ frontend/src/router/index.ts | 16 +- frontend/src/views/Login.vue | 12 +- frontend/src/views/desktop/Dashboard.vue | 7 +- .../src/views/desktop/ManagerWorkspace.vue | 14 + frontend/src/views/desktop/WorkPlans.vue | 357 +++++++++--------- frontend/src/views/mobile/Home.vue | 16 +- frontend/src/views/mobile/PlansList.vue | 164 ++++++-- frontend/src/views/mobile/VisitForm.vue | 5 +- 25 files changed, 494 insertions(+), 261 deletions(-) create mode 100644 backend/.dockerignore create mode 100644 docker-compose.backend.yml diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..5bf2115 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,25 @@ +__pycache__ +*.pyc +*.pyo +*.egg-info +.eggs +dist +build +.venv +venv +env +.pytest_cache +tests +htmlcov +.coverage +node_modules +.vite +.git +.gitignore +.vscode +.idea +.env +.env.* +.ruff_cache +.DS_Store +Thumbs.db diff --git a/backend/Dockerfile b/backend/Dockerfile index 7a63d27..66a0d7b 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,5 +1,8 @@ FROM python:3.12-slim +ENV TZ=Asia/Shanghai +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + WORKDIR /app COPY requirements.txt . diff --git a/backend/app/api/dashboard.py b/backend/app/api/dashboard.py index 147915b..4da0405 100644 --- a/backend/app/api/dashboard.py +++ b/backend/app/api/dashboard.py @@ -19,7 +19,7 @@ async def dashboard_stats( ): """Get dashboard card statistics. Pass reference_date (YYYY-MM-DD) for historical weeks.""" ref = date.fromisoformat(reference_date) if reference_date else None - stats = await get_dashboard_stats(db, ref) + stats = await get_dashboard_stats(db, ref, current_user["user_id"], current_user["role"]) return stats diff --git a/backend/app/api/key_visits.py b/backend/app/api/key_visits.py index 79cbf5e..5e7f4a2 100644 --- a/backend/app/api/key_visits.py +++ b/backend/app/api/key_visits.py @@ -103,6 +103,8 @@ async def update_key_visit( old_snapshot = {"customer_id": str(k.customer_id), "urgency_level": k.urgency_level, "description": k.description, "progress_status": k.progress_status, "planned_date": k.planned_date, "planned_visitor": k.planned_visitor, "visit_target": k.visit_target} update_data = data.model_dump(exclude_unset=True) + if "manager_id" in update_data and update_data["manager_id"]: + update_data["manager_id"] = uuid.UUID(update_data["manager_id"]) for key, v in update_data.items(): setattr(k, key, v) new_snapshot = {"customer_id": str(k.customer_id), "urgency_level": k.urgency_level, "description": k.description, "progress_status": k.progress_status, "planned_date": k.planned_date, "planned_visitor": k.planned_visitor, "visit_target": k.visit_target} diff --git a/backend/app/api/mini_business.py b/backend/app/api/mini_business.py index bb2830e..c8ebd47 100644 --- a/backend/app/api/mini_business.py +++ b/backend/app/api/mini_business.py @@ -101,6 +101,8 @@ async def update_mini_business( old_snapshot = {"customer_id": str(m.customer_id), "product_type": m.product_type, "amount": m.amount, "follow_up_detail": m.follow_up_detail, "status": m.status, "expected_revenue_date": m.expected_revenue_date} update_data = data.model_dump(exclude_unset=True) + if "manager_id" in update_data and update_data["manager_id"]: + update_data["manager_id"] = uuid.UUID(update_data["manager_id"]) for k, v in update_data.items(): setattr(m, k, v) new_snapshot = {"customer_id": str(m.customer_id), "product_type": m.product_type, "amount": m.amount, "follow_up_detail": m.follow_up_detail, "status": m.status, "expected_revenue_date": m.expected_revenue_date} diff --git a/backend/app/api/visits.py b/backend/app/api/visits.py index c33541b..3050610 100644 --- a/backend/app/api/visits.py +++ b/backend/app/api/visits.py @@ -247,11 +247,14 @@ async def update_visit( "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, + "manager_id": str(visit.manager_id) if visit.manager_id else "", } update_data = data.model_dump(exclude_unset=True) if "visit_date" in update_data and update_data["visit_date"]: update_data["visit_date"] = parse_date(update_data["visit_date"]) + if "manager_id" in update_data and update_data["manager_id"]: + update_data["manager_id"] = uuid.UUID(update_data["manager_id"]) for key, value in update_data.items(): setattr(visit, key, value) @@ -262,6 +265,7 @@ async def update_visit( "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, + "manager_id": str(visit.manager_id) if visit.manager_id else "", } changes = compute_diff(old_snapshot, new_snapshot) if changes: @@ -282,7 +286,7 @@ async def update_visit( # Audit log (before commit — part of same transaction) cust = await db.execute(select(Customer.name).where(Customer.id == visit.customer_id)) - await log_audit(db, "visit", visit.id, f"{cust.scalar_one_or_none() or ''} ({visit.visit_date})", "update", current_user["user_id"], current_user["name"], ", ".join([c["field"] for c in changes]) if changes else "") + await log_audit(db, "visit", visit.id, f"{cust.scalar_one_or_none() or ''} ({visit.visit_date})", "update", current_user["user_id"], current_user["name"], ", ".join(changes.keys()) if changes else "") await db.commit() await db.refresh(visit) return await _enrich_visit(visit, db) diff --git a/backend/app/api/wecom.py b/backend/app/api/wecom.py index b207aab..d94afdc 100644 --- a/backend/app/api/wecom.py +++ b/backend/app/api/wecom.py @@ -332,8 +332,9 @@ async def oauth_callback( ) user = result.scalar_one_or_none() if not user: - # Not bound — redirect to bind page - bind_url = f"https://qj.dhdx.fun/wecom-bind?wecom_userid={wecom_userid}" + # Not bound — generate a one-time bind token and redirect to bind page + bind_token = await store_bind_token(db, wecom_userid) + bind_url = f"https://qj.dhdx.fun/wecom-bind?token={bind_token}&wecom_userid={wecom_userid}" from fastapi.responses import RedirectResponse return RedirectResponse(url=bind_url) diff --git a/backend/app/api/work_plans.py b/backend/app/api/work_plans.py index 6d83fab..6bf1a90 100644 --- a/backend/app/api/work_plans.py +++ b/backend/app/api/work_plans.py @@ -117,6 +117,8 @@ async def update_work_plan( update_data = data.model_dump(exclude_unset=True) if "plan_date" in update_data and update_data["plan_date"]: update_data["plan_date"] = parse_date(update_data["plan_date"]) + if "manager_id" in update_data and update_data["manager_id"]: + update_data["manager_id"] = uuid.UUID(update_data["manager_id"]) for k, v in update_data.items(): setattr(wp, k, v) new_snapshot = {"customer_id": str(wp.customer_id), "plan_content": wp.plan_content, "plan_date": str(wp.plan_date), "status": wp.status} diff --git a/backend/app/schemas/key_visit.py b/backend/app/schemas/key_visit.py index c7d7454..3b5f8ec 100644 --- a/backend/app/schemas/key_visit.py +++ b/backend/app/schemas/key_visit.py @@ -21,6 +21,7 @@ class KeyVisitUpdate(BaseModel): planned_date: Optional[str] = None planned_visitor: Optional[str] = None visit_target: Optional[str] = None + manager_id: Optional[str] = None class KeyVisitOut(BaseModel): diff --git a/backend/app/schemas/mini_business.py b/backend/app/schemas/mini_business.py index a0719aa..4ae03ef 100644 --- a/backend/app/schemas/mini_business.py +++ b/backend/app/schemas/mini_business.py @@ -19,6 +19,7 @@ class MiniBusinessUpdate(BaseModel): follow_up_detail: Optional[str] = None status: Optional[str] = None expected_revenue_date: Optional[str] = None + manager_id: Optional[str] = None class MiniBusinessOut(BaseModel): diff --git a/backend/app/schemas/visit.py b/backend/app/schemas/visit.py index 68f5a4e..e101abb 100644 --- a/backend/app/schemas/visit.py +++ b/backend/app/schemas/visit.py @@ -30,6 +30,7 @@ class VisitUpdate(BaseModel): companions: Optional[list[uuid.UUID]] = None companion_names: Optional[list[str]] = None photos: Optional[list[str]] = None + manager_id: Optional[str] = None class VisitOut(BaseModel): diff --git a/backend/app/schemas/work_plan.py b/backend/app/schemas/work_plan.py index 1816798..87e6e43 100644 --- a/backend/app/schemas/work_plan.py +++ b/backend/app/schemas/work_plan.py @@ -16,6 +16,7 @@ class WorkPlanUpdate(BaseModel): plan_content: Optional[str] = None plan_date: Optional[str] = None status: Optional[str] = None + manager_id: Optional[str] = None class WorkPlanOut(BaseModel): diff --git a/backend/app/services/dashboard.py b/backend/app/services/dashboard.py index 8dd3e03..fc9f9cc 100644 --- a/backend/app/services/dashboard.py +++ b/backend/app/services/dashboard.py @@ -12,7 +12,7 @@ from app.models.key_visit import KeyVisit from app.models.daily_note import DailyNote from app.models.leave import Leave from app.services.holidays import load_holiday_sets -from app.utils.timezone import today_cst, is_working_day +from app.utils.timezone import today_cst, is_working_day, parse_date def get_week_range(reference_date: date | None = None): @@ -22,7 +22,7 @@ def get_week_range(reference_date: date | None = None): return monday, sunday -async def get_dashboard_stats(db: AsyncSession, reference_date: date | None = None) -> dict: +async def get_dashboard_stats(db: AsyncSession, reference_date: date | None = None, user_id: str = "", role: str = "") -> dict: """Get dashboard statistics for a given week (defaults to current).""" monday, sunday = get_week_range(reference_date) today = date.today() @@ -51,12 +51,22 @@ async def get_dashboard_stats(db: AsyncSession, reference_date: date | None = No ) )).scalar() or 0 + # Overdue plans (status=计划中, plan_date < today). Managers see only own. + overdue_q = select(func.count(WorkPlan.id)).where( + WorkPlan.status == "计划中", + WorkPlan.plan_date < today, + ) + if role == "manager": + overdue_q = overdue_q.where(WorkPlan.manager_id == UUID(user_id)) + overdue_plans = (await db.execute(overdue_q)).scalar() or 0 + return { "week_visits": visits_count, "work_plans": plans_count, "mini_business": mini_biz_count, "key_visits": key_visit_count, "week_leaves": leaves_count, + "overdue_plans": overdue_plans, "week_start": str(monday), "week_end": str(sunday), } @@ -180,8 +190,18 @@ async def get_weekly_report( visits_data = [] for gid, gvisits in groups.items(): - # Determine the "primary" — first record in group (the creator's) - primary = gvisits[0] + # Determine the "primary" — the creator's record (not a companion copy) + def _is_companion_copy(v) -> bool: + content = (v.communication_content or "").strip() + # New-style: has "(协同XXX)" suffix + if content.endswith(")") and "(协同" in content: + return True + # Old-style: empty draft created for companion + if not content: + return True + return False + creator_records = [v for v in gvisits if not _is_companion_copy(v)] + primary = creator_records[0] if creator_records else gvisits[0] companion_names_resolved = [user_map.get(c, str(c)) for c in (primary.companions or [])] companion_names_resolved.extend(primary.companion_names or []) diff --git a/backend/app/services/scheduler.py b/backend/app/services/scheduler.py index 33e192c..2d237f4 100644 --- a/backend/app/services/scheduler.py +++ b/backend/app/services/scheduler.py @@ -80,33 +80,33 @@ async def check_daily_reporting(db: AsyncSession) -> dict: f"📋 今日填报提醒\n\n{desc}\n未填报:{names_text}\n\n请尽快完成填报 🙏\nhttps://qj.dhdx.fun/m", ) - # 2. Send summary to director + # 2. Send summary to directors/leaders only (not broadcast to @all) directors = await db.execute( select(User).where(User.role.in_(["director", "leader"]), User.wecom_userid.isnot(None)) ) - for d in directors.scalars().all(): - if managers: - effective_total = len(managers) - len(on_leave_names) - if effective_total > 0: - pct = len(reported_map) / effective_total * 100 - else: - pct = 100.0 - leave_note = "" - if on_leave_names: - leave_note = f"\n> 请假中(已豁免):{len(on_leave_names)}人\n" - leave_note += "".join(f"- {n} (请假)\n" for n in on_leave_names) - summary = ( - f"## 📊 今日填报汇总\n\n" - f"> 日期:{today}\n" - f"> 填报率:**{pct:.0f}%** ({len(reported_map)}/{effective_total})\n" - ) - if leave_note: - summary += leave_note + "\n" - if not_reported: - summary += "**未填报:**\n" + "".join(f"- {m.name}\n" for m in not_reported) - else: - summary += "✅ 全体已完成今日填报" - await wecom_client.send_markdown_message(summary) # broadcast for director visibility + director_ids = [d.wecom_userid for d in directors.scalars().all()] + if managers and director_ids: + effective_total = len(managers) - len(on_leave_names) + if effective_total > 0: + pct = len(reported_map) / effective_total * 100 + else: + pct = 100.0 + leave_note = "" + if on_leave_names: + leave_note = f"\n> 请假中(已豁免):{len(on_leave_names)}人\n" + leave_note += "".join(f"- {n} (请假)\n" for n in on_leave_names) + summary = ( + f"## 📊 今日填报汇总\n\n" + f"> 日期:{today}\n" + f"> 填报率:**{pct:.0f}%** ({len(reported_map)}/{effective_total})\n" + ) + if leave_note: + summary += leave_note + "\n" + if not_reported: + summary += "**未填报:**\n" + "".join(f"- {m.name}\n" for m in not_reported) + else: + summary += "✅ 全体已完成今日填报" + await wecom_client.send_markdown_message(summary, user_ids=director_ids) return { "status": "ok", diff --git a/backend/app/services/scheduler_manager.py b/backend/app/services/scheduler_manager.py index b992ef0..45a42d6 100644 --- a/backend/app/services/scheduler_manager.py +++ b/backend/app/services/scheduler_manager.py @@ -9,7 +9,7 @@ def get_scheduler() -> AsyncIOScheduler: """Lazy-init and return the global scheduler.""" global _scheduler if _scheduler is None: - _scheduler = AsyncIOScheduler() + _scheduler = AsyncIOScheduler(timezone="Asia/Shanghai") return _scheduler @@ -28,7 +28,7 @@ async def reschedule_daily_check(hour: int, minute: int): async with async_session() as db: await check_daily_reporting(db) - sched.add_job(_wrapper, "cron", hour=hour, minute=minute, id="daily_check") + sched.add_job(_wrapper, "cron", hour=hour, minute=minute, id="daily_check", timezone="Asia/Shanghai") def start_scheduler(notification_hour: int = 17, notification_minute: int = 30): @@ -46,8 +46,8 @@ def start_scheduler(notification_hour: int = 17, notification_minute: int = 30): async with async_session() as db: await check_overdue_plans(db) - sched.add_job(_daily, "cron", hour=notification_hour, minute=notification_minute, id="daily_check") - sched.add_job(_overdue, "cron", hour=9, minute=0, id="overdue_check") + sched.add_job(_daily, "cron", hour=notification_hour, minute=notification_minute, id="daily_check", timezone="Asia/Shanghai") + sched.add_job(_overdue, "cron", hour=9, minute=0, id="overdue_check", timezone="Asia/Shanghai") sched.start() diff --git a/backend/app/services/wecom.py b/backend/app/services/wecom.py index 1094602..ffe0635 100644 --- a/backend/app/services/wecom.py +++ b/backend/app/services/wecom.py @@ -139,13 +139,14 @@ class WecomClient: except Exception: return False - async def send_markdown_message(self, content: str) -> bool: - """Send a markdown message to all users in the app (broadcast).""" + async def send_markdown_message(self, content: str, user_ids: list[str] | None = None) -> bool: + """Send a markdown message. If user_ids is provided, send to those users; otherwise broadcast to @all.""" if not settings.WECOM_AGENT_ID: return False try: + touser = "|".join(user_ids) if user_ids else "@all" body = { - "touser": "@all", + "touser": touser, "msgtype": "markdown", "agentid": int(settings.WECOM_AGENT_ID), "markdown": {"content": content}, diff --git a/docker-compose.backend.yml b/docker-compose.backend.yml new file mode 100644 index 0000000..2e01ef3 --- /dev/null +++ b/docker-compose.backend.yml @@ -0,0 +1,22 @@ +# 企迹 (qiji) — 后端生产部署 +# PostgreSQL / MinIO / Casdoor / 前端 均为外部服务,仅容器化运行 FastAPI 后端 +# 用法: docker compose -f docker-compose.backend.yml up -d +# +# 使用 host 网络模式: +# - 可直接访问 10.10.10.x 外部服务 (DB/MinIO/Casdoor) +# - FRP frpc 同为 host 网络,127.0.0.1:8002 直达 + +services: + backend: + build: ./backend + container_name: qiji-backend + restart: unless-stopped + network_mode: host + env_file: + - ./backend/.env + command: sh -c "alembic upgrade head && exec uvicorn app.main:app --host 0.0.0.0 --port 8002" + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index b86f048..e281d4d 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -82,8 +82,10 @@ router.beforeEach((to, _from, next) => { const auth = useAuthStore() // Handle WeChat Work OAuth silent login callback: ?token=JWT + // Only strip the token if it's a valid JWT — bind tokens (UUID hex) must + // survive so pages like WecomBind can read them. const tokenParam = to.query.token as string - if (tokenParam) { + if (tokenParam && tokenParam.split('.').length === 3) { // Save token to auth store (the JWT contains user_id, name, role, theme) try { const payload = JSON.parse(atob(tokenParam.split('.')[1])) @@ -94,12 +96,12 @@ router.beforeEach((to, _from, next) => { role: payload.role, theme: payload.theme, }) - } catch (_) { /* invalid token, ignore */ } - // Remove token from URL - const cleanQuery = { ...to.query } - delete cleanQuery.token - next({ path: to.path, query: cleanQuery, replace: true }) - return + // Remove token from URL only on successful JWT decode + const cleanQuery = { ...to.query } + delete cleanQuery.token + next({ path: to.path, query: cleanQuery, replace: true }) + return + } catch (_) { /* invalid JWT — leave token for page to handle */ } } if (to.meta.public) { diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index 0f2ba6c..1787bed 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -86,13 +86,15 @@ onMounted(async () => { } // ── WeChat Work silent login: redirect to OAuth (no user interaction needed) ── + // Skip OAuth when user is on the bind flow — OAuth for unbound users redirects + // right back to /wecom-bind without logging them in, creating an infinite loop. if (isWecom()) { const targetPath = (route.query.redirect as string) || '/m' - const endpoint = import.meta.env.VITE_CASDOOR_ENDPOINT || '' - // In production, the backend builds the OAuth URL; use the API - const apiBase = window.location.origin - window.location.href = `${apiBase}/api/wecom/oauth-url?redirect=${encodeURIComponent(targetPath)}` - return + if (!targetPath.startsWith('/wecom-bind')) { + const apiBase = window.location.origin + window.location.href = `${apiBase}/api/wecom/oauth-url?redirect=${encodeURIComponent(targetPath)}` + return + } } // ── Auto-redirect to Casdoor (no button needed) ── diff --git a/frontend/src/views/desktop/Dashboard.vue b/frontend/src/views/desktop/Dashboard.vue index a8716c4..88c8012 100644 --- a/frontend/src/views/desktop/Dashboard.vue +++ b/frontend/src/views/desktop/Dashboard.vue @@ -9,7 +9,7 @@ import api from '@/api/index' const router = useRouter() const auth = useAuthStore() const loading = ref(false) -const stats = ref({ week_visits: 0, work_plans: 0, mini_business: 0, key_visits: 0, week_leaves: 0, week_start: '', week_end: '' }) +const stats = ref({ week_visits: 0, work_plans: 0, mini_business: 0, key_visits: 0, week_leaves: 0, overdue_plans: 0, week_start: '', week_end: '' }) const progress = ref([]) const weekOffset = ref(0) // 0 = current week, -1 = last week, etc. @@ -145,8 +145,8 @@ function rowState(p: any): 'full' | 'catching' | 'missing' | 'on_leave' | 'rest_
- {{ stats.work_plans }} - 工作计划 + {{ stats.work_plans }}/{{ stats.overdue_plans }} + 工作计划
@@ -296,6 +296,7 @@ function rowState(p: any): 'full' | 'catching' | 'missing' | 'on_leave' | 'rest_ font-family: var(--font-body); font-size: 12px; color: var(--warm-gray); letter-spacing: 0.04em; } +.stat-overdue { font-size: 16px; color: var(--vermilion); font-family: var(--font-mono); } /* ═══ Progress Grid — responsive 2-column ═══ */ .progress-grid { diff --git a/frontend/src/views/desktop/ManagerWorkspace.vue b/frontend/src/views/desktop/ManagerWorkspace.vue index c3fce66..ce8c0b1 100644 --- a/frontend/src/views/desktop/ManagerWorkspace.vue +++ b/frontend/src/views/desktop/ManagerWorkspace.vue @@ -111,6 +111,10 @@ function openEdit(type: string, item: any) { if (item.customer_id && item.customer_name && !customers.value.find((c: any) => c.id === item.customer_id)) { customers.value.unshift({ id: item.customer_id, name: item.customer_name }) } + // Ensure assigned manager is in the select options (avoid showing raw UUID) + if (item.manager_id && item.manager_name && !allUsers.value.find((u: any) => u.id === item.manager_id)) { + allUsers.value.unshift({ id: item.manager_id, name: item.manager_name }) + } dialogTimeRange.value = null if ((type === 'visit' || type === 'note') && item.time_range && item.time_range.includes('-')) { const parts = item.time_range.split('-') @@ -479,6 +483,16 @@ const notesByDate = computed(() => { + + + + + + + + + +