From f12b3e4c7c8d55217cc1d21f814a9a3d5fc57251 Mon Sep 17 00:00:00 2001
From: liusheng <337615773@qq.com>
Date: 星期二, 03 十一月 2026 16:02:21 +0800
Subject: [PATCH] 提交Merge branch 'subtask' of http://116.62.18.175:6699/r/~yxh/smartor-web into subtask

---
 sql/v_ward_followup_stats.sql                         |  152 ++++++++++++++++
 src/api/smartor/pendingTask.js                        |    2 
 sql/ward_followup_stats_query.sql                     |  114 ++++++++++++
 src/views/followvisit/discharge/outpatientService.vue |   48 +++-
 sql/p_ward_followup_stats.sql                         |  146 ++++++++++++++++
 src/views/followvisit/discharge/index.vue             |   48 +++-
 6 files changed, 481 insertions(+), 29 deletions(-)

diff --git a/sql/p_ward_followup_stats.sql b/sql/p_ward_followup_stats.sql
new file mode 100644
index 0000000..33cc5fc
--- /dev/null
+++ b/sql/p_ward_followup_stats.sql
@@ -0,0 +1,146 @@
+-- =============================================================================
+-- 鐥呭尯鍑洪櫌闅忚缁熻 路 瀛樺偍杩囩▼锛堟帹鑽愶紝鐪熸鎸夎皟鐢ㄤ紶鍙傦級
+-- =============================================================================
+-- MySQL 瑙嗗浘涓嶈兘甯﹀弬鏁帮紙瑙嗗浘瀹氫箟閲岃繛 @鍙橀噺 閮戒笉璁哥敤锛夛紝瑕併�屼紶鍙傘�嶅氨鐢ㄥ瓨鍌ㄨ繃绋嬨��
+--
+-- 璋冪敤锛�
+--   CALL p_ward_followup_stats('2026-10-04', '2026-11-03', '1', 7);
+--   CALL p_ward_followup_stats('2026-10-04', '2026-11-03', NULL, NULL);  -- 鍙e緞/鍙婃椂澶╂暟鐢ㄩ粯璁ゅ��
+--   锛圡ySQL 瀛樺偍杩囩▼娌℃湁銆屽彲閫夊弬鏁般�嶏紝鍥涗釜鍙傛暟閮藉繀椤诲啓锛屼笉闇�瑕佺殑浼� NULL锛�
+--
+-- 鍙傛暟锛�
+--   p_start_time  鍑洪櫌鏃堕棿璧凤紙鍚級锛�'yyyy-MM-dd' 鎴� 'yyyy-MM-dd HH:mm:ss'
+--   p_end_time    鍑洪櫌鏃堕棿姝紙鍚綋澶� 23:59:59锛夛紝浼� 'yyyy-MM-dd' 鍗冲彲
+--   p_count_style 闅忚鏂瑰紡鍛戒腑鍙e緞锛�1=搴旈殢璁匡紙榛樿锛� 3=浠呮垚鍔� 鍏跺畠=鍏ㄩ儴
+--   p_timely_days 鍙婃椂澶╂暟鍩烘暟锛岄粯璁� 7锛堣ˉ鍋垮ぉ鏁板彟鍔狅級
+-- =============================================================================
+
+DROP PROCEDURE IF EXISTS p_ward_followup_stats;
+
+DELIMITER $$
+CREATE PROCEDURE p_ward_followup_stats(
+  IN p_start_time  DATETIME,
+  IN p_end_time    DATETIME,
+  IN p_count_style VARCHAR(2),
+  IN p_timely_days INT
+)
+BEGIN
+  DECLARE v_start       DATETIME;
+  DECLARE v_end         DATETIME;
+  DECLARE v_style       VARCHAR(2);
+  DECLARE v_timely_days INT;
+
+  -- 璧锋鏃堕棿蹇呬紶锛涙鏃堕棿鎸夈�屽寘鍚綋澶┿�嶅鐞�
+  IF p_start_time IS NULL OR p_end_time IS NULL THEN
+    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'p_start_time / p_end_time 涓嶈兘涓虹┖';
+  END IF;
+
+  SET v_start       = DATE(p_start_time);
+  SET v_end         = DATE_ADD(DATE(p_end_time), INTERVAL 1 DAY);
+  SET v_style       = IFNULL(p_count_style, '1');
+  SET v_timely_days = IFNULL(p_timely_days, 7);
+
+  SELECT
+    t.leavehospitaldistrictname,                                -- 鍑洪櫌鐥呭尯
+    t.deptname,                                                 -- 鍑洪櫌绉戝
+    t.dischargeCount,                                           -- 鍑洪櫌浜烘锛堥娆¢殢璁胯褰曟暟锛�
+    t.nonFollowUp,                                              -- 鏃犻渶闅忚浜烘
+    t.followUpNeeded,                                           -- 搴旈殢璁夸汉娆�
+    t.needFollowUp,                                             -- 棣栨搴旈殢璁匡紙= 搴旈殢璁夸汉娆★級
+    t.pendingFollowUp,                                          -- 棣栨寰呴殢璁�
+    t.followUpSuccess,                                          -- 棣栨闅忚鎴愬姛
+    t.followUpFail,                                             -- 棣栨闅忚澶辫触锛堝惈浜哄伐瓒呮椂锛�
+    t.manualSuccess,                                            -- 棣栨浜哄伐闅忚鎴愬姛
+    t.voiceSuccess,                                             -- 棣栨璇煶闅忚鎴愬姛
+    t.smsSuccess,                                               -- 棣栨鐭俊闅忚鎴愬姛
+    t.weChatSuccess,                                            -- 棣栨寰俊闅忚鎴愬姛
+    -- 闅忚鎴愬姛鐜� = 鎴愬姛 / 搴旈殢璁匡紙鍙畻鎴愬姛锛屼笉鍚け璐ワ級
+    IF(t.needFollowUp = 0, '0.00%',
+       CONCAT(ROUND(100 * t.followUpSuccess / t.needFollowUp, 2), '%'))   AS followUpSuccessRate,
+    -- 娓犻亾鍚堣鎴愬姛鐜� = 鍥涚鏂瑰紡鎴愬姛涔嬪拰 / 鍥涚鏂瑰紡鍛戒腑涔嬪拰
+    IF(t.manual + t.voice + t.sms + t.weChat = 0, '0.00%',
+       CONCAT(ROUND(100 * (t.manualSuccess + t.voiceSuccess + t.smsSuccess + t.weChatSuccess)
+                    / (t.manual + t.voice + t.sms + t.weChat), 2), '%'))   AS channelSuccessRate,
+    -- 鍙婃椂鐜囷細鍙婃椂 = finishtime 涓嶄负绌� 涓� DATEDIFF(finishtime, endtime) <= 鍙婃椂澶╂暟 + 琛ュ伩澶╂暟
+    IF(IFNULL(rt.timely_base, 0) = 0, '0.00%',
+       CONCAT(ROUND(100 * rt.timely_cnt / rt.timely_base, 2), '%'))       AS timelyRate
+  FROM (
+    SELECT
+      r.ward_code                                    AS ward_code,
+      MAX(r.ward_name)                               AS leavehospitaldistrictname,
+      MAX(r.dept_name)                               AS deptname,
+      COUNT(*)                                       AS dischargeCount,
+      SUM(r.f_no)                                    AS nonFollowUp,
+      SUM(r.f_need)                                  AS followUpNeeded,
+      SUM(r.f_need)                                  AS needFollowUp,
+      SUM(IF(r.sendstate = 2, 1, 0))                 AS pendingFollowUp,
+      SUM(r.f_succ)                                  AS followUpSuccess,
+      SUM(r.f_fail)                                  AS followUpFail,
+      SUM(IF(r.ch_hit = 1 AND r.cp = '1', 1, 0))     AS manual,
+      SUM(IF(r.ch_hit = 1 AND r.cp = '3', 1, 0))     AS voice,
+      SUM(IF(r.ch_hit = 1 AND r.cp = '4', 1, 0))     AS sms,
+      SUM(IF(r.ch_hit = 1 AND r.cp = '5', 1, 0))     AS weChat,
+      SUM(IF(r.sendstate = 6 AND r.cp = '1', 1, 0))  AS manualSuccess,
+      SUM(IF(r.sendstate = 6 AND r.cp = '3', 1, 0))  AS voiceSuccess,
+      SUM(IF(r.sendstate = 6 AND r.cp = '4', 1, 0))  AS smsSuccess,
+      SUM(IF(r.sendstate = 6 AND r.cp = '5', 1, 0))  AS weChatSuccess
+    FROM (
+      SELECT
+        ss.leavehospitaldistrictcode                              AS ward_code,
+        ss.leavehospitaldistrictname                              AS ward_name,
+        ss.leaveldeptname                                         AS dept_name,
+        ss.sendstate                                              AS sendstate,
+        ss.current_preachform                                     AS cp,
+        IF(ss.sendstate = 4, 1, 0)                                AS f_no,
+        IF(ss.sendstate IS NOT NULL AND ss.sendstate <> 4, 1, 0)  AS f_need,
+        IF(ss.sendstate = 6, 1, 0)                                AS f_succ,
+        IF(ss.sendstate IN (5, 7), 1, 0)                          AS f_fail,
+        -- 闅忚鏂瑰紡鍛戒腑鏉′欢锛氭柟寮� 1 鎺掗櫎銆屾棤闇�闅忚銆嶏紱鏂瑰紡 3 鍙畻宸插畬鎴愶紱鍏跺畠鏂瑰紡涓嶈闄�
+        IF(v_style = '1',
+           IF(ss.sendstate IS NOT NULL AND ss.sendstate <> 4, 1, 0),
+           IF(v_style = '3',
+              IF(ss.sendstate = 6, 1, 0),
+              IF(ss.sendstate IS NOT NULL, 1, 0)))                AS ch_hit
+      FROM service_subtask ss
+      WHERE ss.del_flag = 0
+        AND ss.service_type = '2'
+        AND ss.visit_count = 1                                  -- 鍙棣栨闅忚
+        AND ss.leavehospitaldistrictcode IS NOT NULL
+        AND ss.endtime >= DATE(v_start)
+        AND ss.endtime <  DATE_ADD(DATE(v_end), INTERVAL 1 DAY)
+        AND ss.orgid = '20001001'
+    ) r
+    GROUP BY r.ward_code
+  ) t
+  LEFT JOIN (
+    -- 鍙婃椂鐜囷細鎸夌梾鍖洪鑱氬悎涓�娆″啀鍏宠仈锛岄伩鍏嶄竴瀵瑰鏀惧ぇ鍒嗘瘝
+    SELECT
+      ss.leavehospitaldistrictcode AS ward_code,
+      COUNT(*) AS timely_base,
+      SUM(IF(ss.finishtime IS NOT NULL
+             AND DATEDIFF(ss.finishtime, ss.endtime) <= v_timely_days + IFNULL(cp.compensate_days, 0),
+             1, 0)) AS timely_cnt
+    FROM service_subtask ss
+    LEFT JOIN (
+      -- 琛ュ伩鏃堕棿锛歷archar 涓斿彲鑳芥槸绌轰覆锛屽厛鎸夊瓙浠诲姟鑱氬悎鎴愪竴琛�
+      SELECT subid,
+             SUM(CAST(COALESCE(NULLIF(TRIM(compensate_time), ''), '0') AS SIGNED)) AS compensate_days
+      FROM service_subtask_preachform
+      WHERE del_flag = 0
+      GROUP BY subid
+    ) cp ON cp.subid = ss.id
+    WHERE ss.del_flag = 0
+      AND ss.service_type = '2'
+      AND ss.visit_count = 1
+      AND ss.leavehospitaldistrictcode IS NOT NULL
+      AND ss.visit_time IS NOT NULL
+      AND ss.sendstate <> 4
+      AND DATE(ss.visit_time) < DATE(NOW())     -- 鎺ュ彛鏈紶 startTime/endTime 鏃剁殑鍥哄畾鏉′欢
+      AND ss.endtime >= DATE(v_start)
+      AND ss.endtime <  DATE_ADD(DATE(v_end), INTERVAL 1 DAY)
+      AND ss.orgid = '20001001'
+    GROUP BY ss.leavehospitaldistrictcode
+  ) rt ON rt.ward_code = t.ward_code
+  ORDER BY t.dischargeCount DESC;
+END$$
+DELIMITER ;
diff --git a/sql/v_ward_followup_stats.sql b/sql/v_ward_followup_stats.sql
new file mode 100644
index 0000000..9b4bd1d
--- /dev/null
+++ b/sql/v_ward_followup_stats.sql
@@ -0,0 +1,152 @@
+-- =============================================================================
+-- 鐥呭尯鍑洪櫌闅忚缁熻 路 鍙紶鍙傝鍥�
+-- =============================================================================
+-- 涓轰粈涔堟槸銆屽弬鏁拌〃 + 瑙嗗浘銆嶈�屼笉鏄� @鍙橀噺锛�
+--   MySQL 瑙嗗浘瀹氫箟閲岀姝㈠紩鐢� @鐢ㄦ埛鍙橀噺 / 绯荤粺鍙橀噺 / 瀛樺偍杩囩▼鍙傛暟锛�
+--   鍚﹀垯鍒涘缓鏃舵姤 ERROR 1351 (HY000): View's SELECT contains a variable or parameter銆�
+--   瀹樻柟鍘熸枃锛氥�孴he SELECT statement cannot refer to system variables or
+--   user-defined variables.銆�
+--   https://dev.mysql.com/doc/refman/8.0/en/create-view.html
+--
+-- 鎵�浠ョ敤鍗曡鍙傛暟琛� report_param 鎵胯浇鍙傛暟锛�
+--   1) REPLACE INTO report_param ... 鏀圭 1 琛岋紙璧锋鏃堕棿 / 鍙e緞 / 鍙婃椂澶╂暟锛�
+--   2) SELECT * FROM v_ward_followup_stats;
+--
+-- 骞跺彂鎻愰啋锛氬弬鏁拌〃鏄叡浜姸鎬侊紝涓や釜浼氳瘽鍚屾椂鏀逛細浜掔浉涓层�傚鐢ㄦ埛骞跺彂璇锋敼鐢�
+--          鍚岀洰褰曠殑 p_ward_followup_stats.sql锛堝瓨鍌ㄨ繃绋嬶紝鐪熸鎸夎皟鐢ㄤ紶鍙傦級銆�
+-- =============================================================================
+
+-- ---------------------------------------------------------------------------
+-- 1. 鍙傛暟琛紙姘歌繙鍙湁 id = 1 杩欎竴琛岋級
+-- ---------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS report_param (
+  id          TINYINT    NOT NULL                COMMENT '鍥哄畾涓� 1',
+  start_time  DATETIME   NOT NULL                COMMENT '鍑洪櫌鏃堕棿璧凤紙鍚級锛屼紶 yyyy-MM-dd 鎴� yyyy-MM-dd HH:mm:ss 鍧囧彲',
+  end_time    DATETIME   NOT NULL                COMMENT '鍑洪櫌鏃堕棿姝紙鍚綋澶� 23:59:59锛夛紝浼� yyyy-MM-dd 鍗冲彲',
+  count_style VARCHAR(2) NOT NULL DEFAULT '1'    COMMENT '闅忚鏂瑰紡鍛戒腑鍙e緞锛�1=搴旈殢璁� 3=浠呮垚鍔� 鍏跺畠=鍏ㄩ儴',
+  timely_days INT        NOT NULL DEFAULT 7      COMMENT '鍙婃椂鐜囩殑澶╂暟鍩烘暟锛堣ˉ鍋垮ぉ鏁板彟鍔狅級',
+  PRIMARY KEY (id)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '鎶ヨ〃鍙傛暟锛堝崟琛岋紝id 鎭掍负 1锛�';
+
+-- ---------------------------------------------------------------------------
+-- 2. 瑙嗗浘
+-- ---------------------------------------------------------------------------
+CREATE OR REPLACE VIEW v_ward_followup_stats AS
+SELECT
+  t.leavehospitaldistrictname,                                -- 鍑洪櫌鐥呭尯
+  t.deptname,                                                 -- 鍑洪櫌绉戝
+  t.dischargeCount,                                           -- 鍑洪櫌浜烘锛堥娆¢殢璁胯褰曟暟锛�
+  t.nonFollowUp,                                              -- 鏃犻渶闅忚浜烘
+  t.followUpNeeded,                                           -- 搴旈殢璁夸汉娆�
+  t.needFollowUp,                                             -- 棣栨搴旈殢璁匡紙= 搴旈殢璁夸汉娆★級
+  t.pendingFollowUp,                                          -- 棣栨寰呴殢璁�
+  t.followUpSuccess,                                          -- 棣栨闅忚鎴愬姛
+  t.followUpFail,                                             -- 棣栨闅忚澶辫触锛堝惈浜哄伐瓒呮椂锛�
+  t.manualSuccess,                                            -- 棣栨浜哄伐闅忚鎴愬姛
+  t.voiceSuccess,                                             -- 棣栨璇煶闅忚鎴愬姛
+  t.smsSuccess,                                               -- 棣栨鐭俊闅忚鎴愬姛
+  t.weChatSuccess,                                            -- 棣栨寰俊闅忚鎴愬姛
+  -- 闅忚鎴愬姛鐜� = 鎴愬姛 / 搴旈殢璁匡紙鍙畻鎴愬姛锛屼笉鍚け璐ワ級
+  IF(t.needFollowUp = 0, '0.00%',
+     CONCAT(ROUND(100 * t.followUpSuccess / t.needFollowUp, 2), '%'))   AS followUpSuccessRate,
+  -- 娓犻亾鍚堣鎴愬姛鐜� = 鍥涚鏂瑰紡鎴愬姛涔嬪拰 / 鍥涚鏂瑰紡鍛戒腑涔嬪拰
+  IF(t.manual + t.voice + t.sms + t.weChat = 0, '0.00%',
+     CONCAT(ROUND(100 * (t.manualSuccess + t.voiceSuccess + t.smsSuccess + t.weChatSuccess)
+                  / (t.manual + t.voice + t.sms + t.weChat), 2), '%'))   AS channelSuccessRate,
+  -- 鍙婃椂鐜囷細鍙婃椂 = finishtime 涓嶄负绌� 涓� DATEDIFF(finishtime, endtime) <= 鍙婃椂澶╂暟 + 琛ュ伩澶╂暟
+  IF(IFNULL(rt.timely_base, 0) = 0, '0.00%',
+     CONCAT(ROUND(100 * rt.timely_cnt / rt.timely_base, 2), '%'))       AS timelyRate
+FROM (
+  SELECT
+    r.ward_code                                    AS ward_code,
+    MAX(r.ward_name)                               AS leavehospitaldistrictname,
+    MAX(r.dept_name)                               AS deptname,
+    COUNT(*)                                       AS dischargeCount,
+    SUM(r.f_no)                                    AS nonFollowUp,
+    SUM(r.f_need)                                  AS followUpNeeded,
+    SUM(r.f_need)                                  AS needFollowUp,
+    SUM(IF(r.sendstate = 2, 1, 0))                 AS pendingFollowUp,
+    SUM(r.f_succ)                                  AS followUpSuccess,
+    SUM(r.f_fail)                                  AS followUpFail,
+    SUM(IF(r.ch_hit = 1 AND r.cp = '1', 1, 0))     AS manual,
+    SUM(IF(r.ch_hit = 1 AND r.cp = '3', 1, 0))     AS voice,
+    SUM(IF(r.ch_hit = 1 AND r.cp = '4', 1, 0))     AS sms,
+    SUM(IF(r.ch_hit = 1 AND r.cp = '5', 1, 0))     AS weChat,
+    SUM(IF(r.sendstate = 6 AND r.cp = '1', 1, 0))  AS manualSuccess,
+    SUM(IF(r.sendstate = 6 AND r.cp = '3', 1, 0))  AS voiceSuccess,
+    SUM(IF(r.sendstate = 6 AND r.cp = '4', 1, 0))  AS smsSuccess,
+    SUM(IF(r.sendstate = 6 AND r.cp = '5', 1, 0))  AS weChatSuccess
+  FROM (
+    SELECT
+      ss.leavehospitaldistrictcode                              AS ward_code,
+      ss.leavehospitaldistrictname                              AS ward_name,
+      ss.leaveldeptname                                         AS dept_name,
+      ss.sendstate                                              AS sendstate,
+      ss.current_preachform                                     AS cp,
+      IF(ss.sendstate = 4, 1, 0)                                AS f_no,
+      IF(ss.sendstate IS NOT NULL AND ss.sendstate <> 4, 1, 0)  AS f_need,
+      IF(ss.sendstate = 6, 1, 0)                                AS f_succ,
+      IF(ss.sendstate IN (5, 7), 1, 0)                          AS f_fail,
+      -- 闅忚鏂瑰紡鍛戒腑鏉′欢锛氭柟寮� 1 鎺掗櫎銆屾棤闇�闅忚銆嶏紱鏂瑰紡 3 鍙畻宸插畬鎴愶紱鍏跺畠鏂瑰紡涓嶈闄�
+      IF(p.count_style = '1',
+         IF(ss.sendstate IS NOT NULL AND ss.sendstate <> 4, 1, 0),
+         IF(p.count_style = '3',
+            IF(ss.sendstate = 6, 1, 0),
+            IF(ss.sendstate IS NOT NULL, 1, 0)))                AS ch_hit
+    FROM service_subtask ss
+    JOIN report_param p ON p.id = 1
+    WHERE ss.del_flag = 0
+      AND ss.service_type = '2'
+      AND ss.visit_count = 1                                  -- 鍙棣栨闅忚
+      AND ss.leavehospitaldistrictcode IS NOT NULL
+      -- >>> 鍙紶鍙傜殑鏃堕棿鑼冨洿锛氬乏闂彸寮�锛宔nd_time 浼犳棩鏈熷嵆鍙惈褰撳ぉ鍏ㄥぉ <<<
+      AND ss.endtime >= DATE(p.start_time)
+      AND ss.endtime <  DATE_ADD(DATE(p.end_time), INTERVAL 1 DAY)
+      AND ss.orgid = '20001001'
+  ) r
+  GROUP BY r.ward_code
+) t
+LEFT JOIN (
+  -- 鍙婃椂鐜囷細鎸夌梾鍖洪鑱氬悎涓�娆″啀鍏宠仈锛岄伩鍏嶄竴瀵瑰鏀惧ぇ鍒嗘瘝
+  SELECT
+    ss.leavehospitaldistrictcode AS ward_code,
+    COUNT(*) AS timely_base,
+    SUM(IF(ss.finishtime IS NOT NULL
+           AND DATEDIFF(ss.finishtime, ss.endtime) <= p.timely_days + IFNULL(cp.compensate_days, 0),
+           1, 0)) AS timely_cnt
+  FROM service_subtask ss
+  JOIN report_param p ON p.id = 1
+  LEFT JOIN (
+    -- 琛ュ伩鏃堕棿锛歷archar 涓斿彲鑳芥槸绌轰覆锛屽厛鎸夊瓙浠诲姟鑱氬悎鎴愪竴琛�
+    SELECT subid,
+           SUM(CAST(COALESCE(NULLIF(TRIM(compensate_time), ''), '0') AS SIGNED)) AS compensate_days
+    FROM service_subtask_preachform
+    WHERE del_flag = 0
+    GROUP BY subid
+  ) cp ON cp.subid = ss.id
+  WHERE ss.del_flag = 0
+    AND ss.service_type = '2'
+    AND ss.visit_count = 1
+    AND ss.leavehospitaldistrictcode IS NOT NULL
+    AND ss.visit_time IS NOT NULL
+    AND ss.sendstate <> 4
+    AND DATE(ss.visit_time) < DATE(NOW())     -- 鎺ュ彛鏈紶 startTime/endTime 鏃剁殑鍥哄畾鏉′欢
+    AND ss.endtime >= DATE(p.start_time)
+    AND ss.endtime <  DATE_ADD(DATE(p.end_time), INTERVAL 1 DAY)
+    AND ss.orgid = '20001001'
+  GROUP BY ss.leavehospitaldistrictcode
+) rt ON rt.ward_code = t.ward_code
+ORDER BY t.dischargeCount DESC;
+
+-- ---------------------------------------------------------------------------
+-- 3. 鐢ㄦ硶
+-- ---------------------------------------------------------------------------
+-- 鏀瑰弬鏁帮紙鍙敼杩欎竴琛岋紝涓や釜鏃ユ湡灏辨槸鍘熸潵鐨� endtime 璧锋锛�
+REPLACE INTO report_param (id, start_time, end_time, count_style, timely_days)
+VALUES (1, '2026-10-04', '2026-11-03', '1', 7);
+
+-- 鏌ユ暟
+SELECT *
+FROM v_ward_followup_stats
+-- WHERE leavehospitaldistrictname LIKE '%楠ㄧ%'   -- 瑙嗗浘澶栫収鏍疯兘鍐嶅姞鏉′欢
+;
diff --git a/sql/ward_followup_stats_query.sql b/sql/ward_followup_stats_query.sql
new file mode 100644
index 0000000..50d4ff0
--- /dev/null
+++ b/sql/ward_followup_stats_query.sql
@@ -0,0 +1,114 @@
+-- =============================================================================
+-- 鐥呭尯鍑洪櫌闅忚缁熻 路 绾煡璇㈢増锛堜細璇濆彉閲忎紶鍙傦紝閫傚悎 Navicat/DataGrip 涓存椂鍙栨暟锛�
+-- =============================================================================
+-- @start_time / @end_time 灏辨槸鍘熸潵鍐欐鐨� endtime 璧锋锛屾敼杩欎袱琛屽嵆鍙��
+-- 娉ㄦ剰锛氳繖涓嶆槸瑙嗗浘锛屽彉閲忓睘浜庡綋鍓嶈繛鎺ワ紝鎹㈣繛鎺ヨ閲嶆柊 SET銆�
+-- =============================================================================
+
+SET @start_time         = '2026-10-04';   -- 鍑洪櫌鏃堕棿璧凤紙鍚級
+SET @end_time           = '2026-11-03';   -- 鍑洪櫌鏃堕棿姝紙鍚綋澶╋級
+SET @followUpCountStyle = '1';            -- 1=搴旈殢璁� 3=浠呮垚鍔� 鍏跺畠=鍏ㄩ儴
+SET @timelyDays         = 7;              -- 鍙婃椂澶╂暟鍩烘暟
+
+SELECT
+  t.leavehospitaldistrictname,                                -- 鍑洪櫌鐥呭尯
+  t.deptname,                                                 -- 鍑洪櫌绉戝
+  t.dischargeCount,                                           -- 鍑洪櫌浜烘锛堥娆¢殢璁胯褰曟暟锛�
+  t.nonFollowUp,                                              -- 鏃犻渶闅忚浜烘
+  t.followUpNeeded,                                           -- 搴旈殢璁夸汉娆�
+  t.needFollowUp,                                             -- 棣栨搴旈殢璁匡紙= 搴旈殢璁夸汉娆★級
+  t.pendingFollowUp,                                          -- 棣栨寰呴殢璁�
+  t.followUpSuccess,                                          -- 棣栨闅忚鎴愬姛
+  t.followUpFail,                                             -- 棣栨闅忚澶辫触锛堝惈浜哄伐瓒呮椂锛�
+  t.manualSuccess,                                            -- 棣栨浜哄伐闅忚鎴愬姛
+  t.voiceSuccess,                                             -- 棣栨璇煶闅忚鎴愬姛
+  t.smsSuccess,                                               -- 棣栨鐭俊闅忚鎴愬姛
+  t.weChatSuccess,                                            -- 棣栨寰俊闅忚鎴愬姛
+  -- 闅忚鎴愬姛鐜� = 鎴愬姛 / 搴旈殢璁匡紙鍙畻鎴愬姛锛屼笉鍚け璐ワ級
+  IF(t.needFollowUp = 0, '0.00%',
+     CONCAT(ROUND(100 * t.followUpSuccess / t.needFollowUp, 2), '%'))   AS followUpSuccessRate,
+  -- 娓犻亾鍚堣鎴愬姛鐜� = 鍥涚鏂瑰紡鎴愬姛涔嬪拰 / 鍥涚鏂瑰紡鍛戒腑涔嬪拰
+  IF(t.manual + t.voice + t.sms + t.weChat = 0, '0.00%',
+     CONCAT(ROUND(100 * (t.manualSuccess + t.voiceSuccess + t.smsSuccess + t.weChatSuccess)
+                  / (t.manual + t.voice + t.sms + t.weChat), 2), '%'))   AS channelSuccessRate,
+  -- 鍙婃椂鐜囷細鍙婃椂 = finishtime 涓嶄负绌� 涓� DATEDIFF(finishtime, endtime) <= 鍙婃椂澶╂暟 + 琛ュ伩澶╂暟
+  IF(IFNULL(rt.timely_base, 0) = 0, '0.00%',
+     CONCAT(ROUND(100 * rt.timely_cnt / rt.timely_base, 2), '%'))       AS timelyRate
+FROM (
+  SELECT
+    r.ward_code                                    AS ward_code,
+    MAX(r.ward_name)                               AS leavehospitaldistrictname,
+    MAX(r.dept_name)                               AS deptname,
+    COUNT(*)                                       AS dischargeCount,
+    SUM(r.f_no)                                    AS nonFollowUp,
+    SUM(r.f_need)                                  AS followUpNeeded,
+    SUM(r.f_need)                                  AS needFollowUp,
+    SUM(IF(r.sendstate = 2, 1, 0))                 AS pendingFollowUp,
+    SUM(r.f_succ)                                  AS followUpSuccess,
+    SUM(r.f_fail)                                  AS followUpFail,
+    SUM(IF(r.ch_hit = 1 AND r.cp = '1', 1, 0))     AS manual,
+    SUM(IF(r.ch_hit = 1 AND r.cp = '3', 1, 0))     AS voice,
+    SUM(IF(r.ch_hit = 1 AND r.cp = '4', 1, 0))     AS sms,
+    SUM(IF(r.ch_hit = 1 AND r.cp = '5', 1, 0))     AS weChat,
+    SUM(IF(r.sendstate = 6 AND r.cp = '1', 1, 0))  AS manualSuccess,
+    SUM(IF(r.sendstate = 6 AND r.cp = '3', 1, 0))  AS voiceSuccess,
+    SUM(IF(r.sendstate = 6 AND r.cp = '4', 1, 0))  AS smsSuccess,
+    SUM(IF(r.sendstate = 6 AND r.cp = '5', 1, 0))  AS weChatSuccess
+  FROM (
+    SELECT
+      ss.leavehospitaldistrictcode                              AS ward_code,
+      ss.leavehospitaldistrictname                              AS ward_name,
+      ss.leaveldeptname                                         AS dept_name,
+      ss.sendstate                                              AS sendstate,
+      ss.current_preachform                                     AS cp,
+      IF(ss.sendstate = 4, 1, 0)                                AS f_no,
+      IF(ss.sendstate IS NOT NULL AND ss.sendstate <> 4, 1, 0)  AS f_need,
+      IF(ss.sendstate = 6, 1, 0)                                AS f_succ,
+      IF(ss.sendstate IN (5, 7), 1, 0)                          AS f_fail,
+      -- 闅忚鏂瑰紡鍛戒腑鏉′欢锛氭柟寮� 1 鎺掗櫎銆屾棤闇�闅忚銆嶏紱鏂瑰紡 3 鍙畻宸插畬鎴愶紱鍏跺畠鏂瑰紡涓嶈闄�
+      IF(@followUpCountStyle = '1',
+         IF(ss.sendstate IS NOT NULL AND ss.sendstate <> 4, 1, 0),
+         IF(@followUpCountStyle = '3',
+            IF(ss.sendstate = 6, 1, 0),
+            IF(ss.sendstate IS NOT NULL, 1, 0)))                AS ch_hit
+    FROM service_subtask ss
+    WHERE ss.del_flag = 0
+      AND ss.service_type = '2'
+      AND ss.visit_count = 1                                  -- 鍙棣栨闅忚
+      AND ss.leavehospitaldistrictcode IS NOT NULL
+      AND ss.endtime >= DATE(@start_time)
+      AND ss.endtime <  DATE_ADD(DATE(@end_time), INTERVAL 1 DAY)
+      AND ss.orgid = '20001001'
+  ) r
+  GROUP BY r.ward_code
+) t
+LEFT JOIN (
+  -- 鍙婃椂鐜囷細鎸夌梾鍖洪鑱氬悎涓�娆″啀鍏宠仈锛岄伩鍏嶄竴瀵瑰鏀惧ぇ鍒嗘瘝
+  SELECT
+    ss.leavehospitaldistrictcode AS ward_code,
+    COUNT(*) AS timely_base,
+    SUM(IF(ss.finishtime IS NOT NULL
+           AND DATEDIFF(ss.finishtime, ss.endtime) <= @timelyDays + IFNULL(cp.compensate_days, 0),
+           1, 0)) AS timely_cnt
+  FROM service_subtask ss
+  LEFT JOIN (
+    -- 琛ュ伩鏃堕棿锛歷archar 涓斿彲鑳芥槸绌轰覆锛屽厛鎸夊瓙浠诲姟鑱氬悎鎴愪竴琛�
+    SELECT subid,
+           SUM(CAST(COALESCE(NULLIF(TRIM(compensate_time), ''), '0') AS SIGNED)) AS compensate_days
+    FROM service_subtask_preachform
+    WHERE del_flag = 0
+    GROUP BY subid
+  ) cp ON cp.subid = ss.id
+  WHERE ss.del_flag = 0
+    AND ss.service_type = '2'
+    AND ss.visit_count = 1
+    AND ss.leavehospitaldistrictcode IS NOT NULL
+    AND ss.visit_time IS NOT NULL
+    AND ss.sendstate <> 4
+    AND DATE(ss.visit_time) < DATE(NOW())     -- 鎺ュ彛鏈紶 startTime/endTime 鏃剁殑鍥哄畾鏉′欢
+    AND ss.endtime >= DATE(@start_time)
+    AND ss.endtime <  DATE_ADD(DATE(@end_time), INTERVAL 1 DAY)
+    AND ss.orgid = '20001001'
+  GROUP BY ss.leavehospitaldistrictcode
+) rt ON rt.ward_code = t.ward_code
+ORDER BY t.dischargeCount DESC;
diff --git a/src/api/smartor/pendingTask.js b/src/api/smartor/pendingTask.js
index d54c40c..1bf84bb 100644
--- a/src/api/smartor/pendingTask.js
+++ b/src/api/smartor/pendingTask.js
@@ -7,7 +7,7 @@
  * 鍚庣鍙e緞锛歴ervice_subtask 涓�
  *   service_type = '2' 鍑洪櫌闅忚 / '3' 闂ㄨ瘖闅忚锛堝垎寮�璁℃暟锛�
  *   current_preachform = '1'锛堜汉宸ワ級
- *   寰呴殢璁跨姸鎬侊紙sendstate in 1,2,3,5,7锛�
+ *   寰呴殢璁跨姸鎬侊紙sendstate in 1,2,3,7锛涘彂閫佸け璐� 5 涓嶈鍏ワ級
  *   鍑洪櫌鏃堕棿 endtime 鏃╀簬绛変簬浠婂ぉ - 7 澶�
  *   鍙粺璁℃渶杩� 4 涓嚭闄㈡湀浠斤紙鍚綋鏈堬級
  *
diff --git a/src/views/followvisit/discharge/index.vue b/src/views/followvisit/discharge/index.vue
index 3fb51d4..27bc525 100644
--- a/src/views/followvisit/discharge/index.vue
+++ b/src/views/followvisit/discharge/index.vue
@@ -1934,7 +1934,7 @@
     },
     /**
      * 鍙充笅瑙掋�屽緟澶勭悊闅忚浠诲姟銆嶅崱鐗囩偣鍑绘暟瀛楄烦杞繃鏉ワ細
-     * 鎸夊嚭闄㈠勾鏈� + 寰呴殢璁匡紙sendstate 1/2/3/5/7锛�+ 鍑洪櫌婊� 7 澶� 杩囨护锛屽彛寰勪笌鍗$墖涓�鑷�
+     * 鎸夊嚭闄㈠勾鏈� + 寰呴殢璁匡紙sendstate 1/2/3/7锛�+ 鍑洪櫌婊� 7 澶� 杩囨护锛屽彛寰勪笌鍗$墖涓�鑷�
      * @returns {boolean} 鏄惁宸叉寜鍗$墖鏉′欢鍙戣捣鏌ヨ
      */
     applyPendingTaskQuery() {
@@ -1952,11 +1952,8 @@
       const serviceType = Number(query.serviceType) === 3 ? 3 : 2;
       this.errtype = undefined;
       const start = month + "-01";
-      let end = this.pendingTaskMonthEnd(month);
-      // 鏈堝垵 7 澶╁唴锛�"浠婂ぉ-7澶�" 浼氭棭浜庢湀鍒濓紝鍏滃簳鎴愭湀鍒濓紝閬垮厤鏃ユ湡鍖洪棿鍊掓寕
-      if (end < start) {
-        end = start;
-      }
+      // 鍗$墖缁熻鐨勬槸銆屽嚭闄㈡弧 7 澶┿�嶇殑浠诲姟锛屾埅姝㈡棩 = min(璇ユ湀鏈�鍚庝竴澶�, 浠婂ぉ-7 澶�)
+      const end = this.pendingTaskMonthEnd(month);
       this.dateRange = [start, end];
       this.topqueryParams.serviceType = serviceType;
       this.topqueryParams.sendstateView = 1;
@@ -1964,24 +1961,47 @@
       // 鍗$墖缁熻鐨勬槸鍏ㄩ儴寰呴殢璁夸换鍔★紙涓嶅尯鍒嗛杞�/澶嶈瘖锛夛紝鍒楄〃杩欓噷鍚屾鏀惧紑
       this.topqueryParams.visitCount = null;
       this.topqueryParams.pageNum = 1;
+      // 褰撴湀杩樻病鍒扮 8 澶╂椂锛岃鏈堜笉鍙兘鏈夈�屽嚭闄㈡弧 7 澶┿�嶇殑浠诲姟锛堝崱鐗囨樉绀� 0锛夛紝鍒楄〃鐩存帴缁欑┖銆�
+      // 鏃ユ湡妗嗕粛鏄剧ず璇ユ湀鐨勮嚜鐒跺尯闂达紝閬垮厤鍑虹幇銆岃捣姝㈠�掓寕銆嶇殑绛涢�夋潯浠躲��
+      if (end < start) {
+        this.dateRange = [start, this.pendingTaskMonthLastDay(month)];
+        this.userList = [];
+        this.total = 0;
+        this.loading = false;
+        this.$message.info("璇ユ湀鏆傛棤鍑洪櫌婊� 7 澶╃殑寰呴殢璁夸换鍔�");
+        return true;
+      }
       this.handleQuery(1);
       return true;
     },
     /**
-     * 鍗$墖鍙e緞鐨勬湀浠界粨鏉熸棩锛�
-     * 褰撴湀鍙粺璁″嚭闄㈡弧 7 澶╃殑锛屾墍浠ュ彇銆屼粖澶� - 7 澶┿�嶏紱鍘嗗彶鏈堜唤鍙栧綋鏈堟渶鍚庝竴澶�
+     * 璇ユ湀鏈�鍚庝竴澶╋紙yyyy-MM-dd锛夛紝鍙敤浜庛�屽綋鏈堣繕娌℃湁婊� 7 澶╀换鍔°�嶆椂灞曠ず姝e父鏈堜唤鍖洪棿
+     */
+    pendingTaskMonthLastDay(month) {
+      const parts = String(month).split("-");
+      const date = new Date(Number(parts[0]), Number(parts[1]), 0);
+      const mm = String(date.getMonth() + 1).padStart(2, "0");
+      const dd = String(date.getDate()).padStart(2, "0");
+      return date.getFullYear() + "-" + mm + "-" + dd;
+    },
+    /**
+     * 鍗$墖鍙e緞鐨勬湀浠界粨鏉熸棩锛氬崱鐗囧彧缁熻銆屽嚭闄㈡弧 7 澶╀粛鏈殢璁裤�嶇殑浠诲姟锛�
+     * 鎵�浠ユ埅姝㈡棩姘歌繙鏄� min(璇ユ湀鏈�鍚庝竴澶�, 浠婂ぉ-7 澶�)锛�
+     * 鍘嗗彶鏈堜唤鍚屾牱瑕佹寜銆屼粖澶�-7 澶┿�嶆埅鏂紝鍚﹀垯鏈堟湯鍑犲ぉ鍑洪櫌鐨勬偅鑰呬細琚畻杩涙潵锛堝崱鐗� 49銆佸垪琛� 77锛夈��
      */
     pendingTaskMonthEnd(month) {
       const parts = String(month).split("-");
       const year = Number(parts[0]);
       const mon = Number(parts[1]);
       const now = new Date();
-      let date;
-      if (year === now.getFullYear() && mon === now.getMonth() + 1) {
-        date = new Date(year, mon - 1, now.getDate() - 7);
-      } else {
-        date = new Date(year, mon, 0);
-      }
+      // 鍑洪櫌婊� 7 澶╋細鍑洪櫌鏃ユ湡 <= 浠婂ぉ-7锛堜笌鍚庣 endtime < 浠婂ぉ-6 00:00 绛変环锛�
+      const cutoff = new Date(
+        now.getFullYear(),
+        now.getMonth(),
+        now.getDate() - 7
+      );
+      const monthEnd = new Date(year, mon, 0);
+      const date = cutoff < monthEnd ? cutoff : monthEnd;
       const mm = String(date.getMonth() + 1).padStart(2, "0");
       const dd = String(date.getDate()).padStart(2, "0");
       return date.getFullYear() + "-" + mm + "-" + dd;
diff --git a/src/views/followvisit/discharge/outpatientService.vue b/src/views/followvisit/discharge/outpatientService.vue
index 243496d..e6fc7d9 100644
--- a/src/views/followvisit/discharge/outpatientService.vue
+++ b/src/views/followvisit/discharge/outpatientService.vue
@@ -1373,7 +1373,7 @@
     },
     /**
      * 鍙充笅瑙掋�屽緟澶勭悊闅忚浠诲姟銆嶅崱鐗囩偣鍑绘暟瀛楄烦杞繃鏉ワ細
-     * 鎸夊嚭闄㈠勾鏈� + 寰呴殢璁匡紙sendstate 1/2/3/5/7锛�+ 鍑洪櫌婊� 7 澶� 杩囨护锛屽彛寰勪笌鍗$墖涓�鑷�
+     * 鎸夊嚭闄㈠勾鏈� + 寰呴殢璁匡紙sendstate 1/2/3/7锛�+ 鍑洪櫌婊� 7 澶� 杩囨护锛屽彛寰勪笌鍗$墖涓�鑷�
      * @returns {boolean} 鏄惁宸叉寜鍗$墖鏉′欢鍙戣捣鏌ヨ
      */
     applyPendingTaskQuery() {
@@ -1391,11 +1391,8 @@
       const serviceType = Number(query.serviceType) === 3 ? 3 : 2;
       this.errtype = undefined;
       const start = month + "-01";
-      let end = this.pendingTaskMonthEnd(month);
-      // 鏈堝垵 7 澶╁唴锛�"浠婂ぉ-7澶�" 浼氭棭浜庢湀鍒濓紝鍏滃簳鎴愭湀鍒濓紝閬垮厤鏃ユ湡鍖洪棿鍊掓寕
-      if (end < start) {
-        end = start;
-      }
+      // 鍗$墖缁熻鐨勬槸銆屽嚭闄㈡弧 7 澶┿�嶇殑浠诲姟锛屾埅姝㈡棩 = min(璇ユ湀鏈�鍚庝竴澶�, 浠婂ぉ-7 澶�)
+      const end = this.pendingTaskMonthEnd(month);
       this.dateRange = [start, end];
       this.topqueryParams.serviceType = serviceType;
       this.topqueryParams.sendstateView = 1;
@@ -1403,24 +1400,47 @@
       // 鍗$墖缁熻鐨勬槸鍏ㄩ儴寰呴殢璁夸换鍔★紙涓嶅尯鍒嗛杞�/澶嶈瘖锛夛紝鍒楄〃杩欓噷鍚屾鏀惧紑
       this.topqueryParams.visitCount = null;
       this.topqueryParams.pageNum = 1;
+      // 褰撴湀杩樻病鍒扮 8 澶╂椂锛岃鏈堜笉鍙兘鏈夈�屽嚭闄㈡弧 7 澶┿�嶇殑浠诲姟锛堝崱鐗囨樉绀� 0锛夛紝鍒楄〃鐩存帴缁欑┖銆�
+      // 鏃ユ湡妗嗕粛鏄剧ず璇ユ湀鐨勮嚜鐒跺尯闂达紝閬垮厤鍑虹幇銆岃捣姝㈠�掓寕銆嶇殑绛涢�夋潯浠躲��
+      if (end < start) {
+        this.dateRange = [start, this.pendingTaskMonthLastDay(month)];
+        this.userList = [];
+        this.total = 0;
+        this.loading = false;
+        this.$message.info("璇ユ湀鏆傛棤鍑洪櫌婊� 7 澶╃殑寰呴殢璁夸换鍔�");
+        return true;
+      }
       this.handleQuery(1);
       return true;
     },
     /**
-     * 鍗$墖鍙e緞鐨勬湀浠界粨鏉熸棩锛�
-     * 褰撴湀鍙粺璁″嚭闄㈡弧 7 澶╃殑锛屾墍浠ュ彇銆屼粖澶� - 7 澶┿�嶏紱鍘嗗彶鏈堜唤鍙栧綋鏈堟渶鍚庝竴澶�
+     * 璇ユ湀鏈�鍚庝竴澶╋紙yyyy-MM-dd锛夛紝鍙敤浜庛�屽綋鏈堣繕娌℃湁婊� 7 澶╀换鍔°�嶆椂灞曠ず姝e父鏈堜唤鍖洪棿
+     */
+    pendingTaskMonthLastDay(month) {
+      const parts = String(month).split("-");
+      const date = new Date(Number(parts[0]), Number(parts[1]), 0);
+      const mm = String(date.getMonth() + 1).padStart(2, "0");
+      const dd = String(date.getDate()).padStart(2, "0");
+      return date.getFullYear() + "-" + mm + "-" + dd;
+    },
+    /**
+     * 鍗$墖鍙e緞鐨勬湀浠界粨鏉熸棩锛氬崱鐗囧彧缁熻銆屽嚭闄㈡弧 7 澶╀粛鏈殢璁裤�嶇殑浠诲姟锛�
+     * 鎵�浠ユ埅姝㈡棩姘歌繙鏄� min(璇ユ湀鏈�鍚庝竴澶�, 浠婂ぉ-7 澶�)锛�
+     * 鍘嗗彶鏈堜唤鍚屾牱瑕佹寜銆屼粖澶�-7 澶┿�嶆埅鏂紝鍚﹀垯鏈堟湯鍑犲ぉ鍑洪櫌鐨勬偅鑰呬細琚畻杩涙潵锛堝崱鐗� 49銆佸垪琛� 77锛夈��
      */
     pendingTaskMonthEnd(month) {
       const parts = String(month).split("-");
       const year = Number(parts[0]);
       const mon = Number(parts[1]);
       const now = new Date();
-      let date;
-      if (year === now.getFullYear() && mon === now.getMonth() + 1) {
-        date = new Date(year, mon - 1, now.getDate() - 7);
-      } else {
-        date = new Date(year, mon, 0);
-      }
+      // 鍑洪櫌婊� 7 澶╋細鍑洪櫌鏃ユ湡 <= 浠婂ぉ-7锛堜笌鍚庣 endtime < 浠婂ぉ-6 00:00 绛変环锛�
+      const cutoff = new Date(
+        now.getFullYear(),
+        now.getMonth(),
+        now.getDate() - 7
+      );
+      const monthEnd = new Date(year, mon, 0);
+      const date = cutoff < monthEnd ? cutoff : monthEnd;
       const mm = String(date.getMonth() + 1).padStart(2, "0");
       const dd = String(date.getDate()).padStart(2, "0");
       return date.getFullYear() + "-" + mm + "-" + dd;

--
Gitblit v1.9.3