m0_70156489 22 часов назад
Родитель
Сommit
2dbf99b9e7

+ 288 - 59
application/admin/controller/Procuremen.php

@@ -58,7 +58,7 @@ class Procuremen extends Backend
         'auditmanualpick',
         // 与 rfqlist 同权,方法内校验
         'rfqnotifysalesmen', 'rfqnotifysalesman', 'rfqissuesuppliers', 'rfqissue', 'rfqissuesendmail', 'rfqquotes',
-        'rfqappendsuppliers', 'rfqsupplieroptions',
+        'rfqappendsuppliers', 'rfqsupplieroptions', 'rfqsalesmanoptions', 'rfqappendsalesmen',
     ];
 
     public function _initialize()
@@ -262,6 +262,8 @@ class Procuremen extends Backend
             'rfqlist'                 => $this->hasProcuremenPerm(['rfqlist']),
             'rfqnotifysalesmen'       => $this->hasProcuremenPerm(['rfqlist', 'rfqnotifysalesmen']),
             'rfqnotifysalesman'       => $this->hasProcuremenPerm(['rfqlist', 'rfqnotifysalesman']),
+            'rfqsalesmanoptions'      => $this->hasProcuremenPerm(['rfqlist', 'rfqnotifysalesmen', 'rfqsalesmanoptions', 'rfqappendsalesmen']),
+            'rfqappendsalesmen'       => $this->hasProcuremenPerm(['rfqlist', 'rfqnotifysalesmen', 'rfqappendsalesmen']),
             'rfqissuesuppliers'       => $this->hasProcuremenPerm(['rfqlist', 'rfqissue', 'rfqissuesuppliers']),
             'rfqissue'                => $this->hasProcuremenPerm(['rfqlist', 'rfqissue']),
             'rfqissuesendmail'        => $this->hasProcuremenPerm(['rfqlist', 'rfqissue', 'rfqissuesendmail']),
@@ -3069,7 +3071,28 @@ class Procuremen extends Backend
             return ['company' => $cn, 'reason' => $reason];
         }
 
-        return ['company' => '', 'reason' => ''];
+        $ccydh = trim((string)($bundle['ccydh'] ?? ''));
+        if ($ccydh === '') {
+            return ['company' => '', 'reason' => ''];
+        }
+        try {
+            $row = Db::table('purchase_order')
+                ->where('CCYDH', $ccydh)
+                ->where('manual_pick', 1)
+                ->field('pick_company_name,manual_pick_reason')
+                ->order('id', 'asc')
+                ->find();
+        } catch (\Throwable $e) {
+            $row = null;
+        }
+        if (!is_array($row)) {
+            return ['company' => '', 'reason' => ''];
+        }
+
+        return [
+            'company' => trim((string)($row['pick_company_name'] ?? '')),
+            'reason'  => trim((string)($row['manual_pick_reason'] ?? '')),
+        ];
     }
 
     protected function loadBidOpenAdminUser(int $adminId): ?array
@@ -8740,6 +8763,25 @@ class Procuremen extends Backend
     protected function loadRfqSalesmanNameOptions(): array
     {
         $out = [];
+        foreach ($this->loadRfqSalesmanContactOptions() as $row) {
+            $name = trim((string)($row['name'] ?? ''));
+            if ($name !== '') {
+                $out[] = $name;
+            }
+        }
+
+        return $out;
+    }
+
+    /**
+     * 业务员角色组下管理员:姓名、邮箱、手机
+     *
+     * @return array<int, array{name:string,email:string,mobile:string}>
+     */
+    protected function loadRfqSalesmanContactOptions(): array
+    {
+        $out = [];
+        $seen = [];
         $groupId = $this->rfqNotifySalesmanAuthGroupId();
         if ($groupId <= 0) {
             return [];
@@ -8762,7 +8804,7 @@ class Procuremen extends Backend
                 ->where('status', 'normal')
                 ->where('id', 'in', $adminIds)
                 ->order('id', 'asc')
-                ->field('id,nickname,username')
+                ->field('id,nickname,username,email,mobile')
                 ->select();
         } catch (\Throwable $e) {
             return [];
@@ -8778,13 +8820,18 @@ class Procuremen extends Backend
             if ($name === '') {
                 $name = trim((string)($row['username'] ?? ''));
             }
-            if ($name === '' || isset($out[$name])) {
+            if ($name === '' || isset($seen[$name])) {
                 continue;
             }
-            $out[$name] = $name;
+            $seen[$name] = true;
+            $out[] = [
+                'name'   => $name,
+                'email'  => trim((string)($row['email'] ?? '')),
+                'mobile' => preg_replace('/\s+/', '', trim((string)($row['mobile'] ?? ''))),
+            ];
         }
 
-        return array_values($out);
+        return $out;
     }
 
     /**
@@ -9200,7 +9247,7 @@ class Procuremen extends Backend
     }
 
     /**
-     * 查询询价 — 弹窗:列出本单「通知业务员」姓名与邮箱
+     * 查询询价 — 弹窗:可选通知业务员名单(勾选后确认通知)
      */
     public function rfqnotifysalesmen()
     {
@@ -9214,8 +9261,27 @@ class Procuremen extends Backend
             $this->error('仅支持手工新增询价单');
         }
         $po = $this->loadManualRfqOrderForNotify($sid);
-        $names = $this->splitRfqSalesmanNames(trim((string)($po['notify_salesman'] ?? '')));
-        $list = $this->resolveRfqSalesmanContactRows($names);
+        $selectedMap = [];
+        foreach ($this->splitRfqSalesmanNames(trim((string)($po['notify_salesman'] ?? ''))) as $n) {
+            $selectedMap[$n] = true;
+        }
+        $list = [];
+        foreach ($this->loadRfqSalesmanContactOptions() as $row) {
+            $name = trim((string)($row['name'] ?? ''));
+            if ($name === '') {
+                continue;
+            }
+            $email = trim((string)($row['email'] ?? ''));
+            $mobile = trim((string)($row['mobile'] ?? ''));
+            $hasEmail = $email !== '' && (bool)filter_var($email, FILTER_VALIDATE_EMAIL);
+            $list[] = [
+                'name'       => $name,
+                'email'      => $email,
+                'mobile'     => $mobile,
+                'can_select' => $hasEmail ? 1 : 0,
+                'checked'    => (!empty($selectedMap[$name]) && $hasEmail) ? 1 : 0,
+            ];
+        }
         $canNotify = $this->canRfqNotifySalesman($po);
 
         $this->success('', null, [
@@ -9229,7 +9295,114 @@ class Procuremen extends Backend
     }
 
     /**
-     * 查询询价 — 邮箱通知业务员(支持单人 / 全部 notify_all=1)
+     * 查询询价 — 补加通知业务员可选名单
+     */
+    public function rfqsalesmanoptions()
+    {
+        if (!$this->hasProcuremenPerm(['rfqlist', 'rfqnotifysalesmen', 'rfqsalesmanoptions', 'rfqappendsalesmen'])) {
+            $this->error(__('You have no permission'));
+        }
+        $out = [];
+        foreach ($this->loadRfqSalesmanContactOptions() as $row) {
+            $email = trim((string)($row['email'] ?? ''));
+            $mobile = trim((string)($row['mobile'] ?? ''));
+            $hasEmail = $email !== '' && (bool)filter_var($email, FILTER_VALIDATE_EMAIL);
+            $out[] = [
+                'name'       => (string)($row['name'] ?? ''),
+                'email'      => $email,
+                'mobile'     => $mobile,
+                'can_select' => $hasEmail ? 1 : 0,
+            ];
+        }
+        $this->success('', null, ['list' => $out]);
+    }
+
+    /**
+     * 查询询价 — 补加通知业务员(合并更新 notify_salesman)
+     */
+    public function rfqappendsalesmen()
+    {
+        if (!$this->request->isPost()) {
+            $this->error(__('Invalid parameters'));
+        }
+        if (!$this->hasProcuremenPerm(['rfqlist', 'rfqnotifysalesmen', 'rfqappendsalesmen'])) {
+            $this->error(__('You have no permission'));
+        }
+        $this->ensurePurchaseOrderNotifySalesmanColumn();
+        $sid = (int)$this->request->post('scydgy_id', 0);
+        if ($sid >= 0) {
+            $this->error('仅支持手工新增询价单');
+        }
+        $po = $this->loadManualRfqOrderForNotify($sid);
+        if (ProcuremenStatus::isPoCompleted($po['status'] ?? '')) {
+            $this->error('订单已完结,无法添加业务员');
+        }
+
+        $raw = $this->request->post('salesmen/a', []);
+        if (!is_array($raw) || $raw === []) {
+            $salesmenJson = $this->request->post('salesmen_json', '');
+            if (is_string($salesmenJson) && $salesmenJson !== '') {
+                $decoded = json_decode($salesmenJson, true);
+                $raw = is_array($decoded) ? $decoded : [];
+            }
+        }
+        $addNames = [];
+        foreach ((array)$raw as $item) {
+            if (is_array($item)) {
+                $n = trim((string)($item['name'] ?? ''));
+            } else {
+                $n = trim((string)$item);
+            }
+            if ($n !== '') {
+                $addNames[] = $n;
+            }
+        }
+        $addNames = array_values(array_unique($addNames));
+        if ($addNames === []) {
+            $this->error('请选择要添加的业务员');
+        }
+
+        $allowedOptions = $this->loadRfqSalesmanContactOptions();
+        $allowedMap = [];
+        foreach ($allowedOptions as $opt) {
+            $n = trim((string)($opt['name'] ?? ''));
+            if ($n === '') {
+                continue;
+            }
+            $email = trim((string)($opt['email'] ?? ''));
+            $allowedMap[$n] = ($email !== '' && (bool)filter_var($email, FILTER_VALIDATE_EMAIL));
+        }
+        foreach ($addNames as $n) {
+            if (!array_key_exists($n, $allowedMap)) {
+                $this->error('业务员不在可选名单中:' . $n);
+            }
+            if (!$allowedMap[$n]) {
+                $this->error('业务员未填写有效邮箱,无法添加:' . $n);
+            }
+        }
+
+        $existingRaw = trim((string)($po['notify_salesman'] ?? ''));
+        $merged = array_merge($this->splitRfqSalesmanNames($existingRaw), $addNames);
+        $mergedStr = $this->joinRfqMultiSelectValues($merged);
+
+        try {
+            Db::table('purchase_order')->where('scydgy_id', $sid)->update(['notify_salesman' => $mergedStr]);
+        } catch (\Throwable $e) {
+            $this->error('保存失败:' . $e->getMessage());
+        }
+
+        $poId = (int)($po['id'] ?? $po['ID'] ?? 0);
+        $this->addOrderLog($sid, 'rfq_append_salesman', '添加通知业务员:' . implode('、', $addNames), $poId > 0 ? $poId : null);
+
+        $this->success('已添加业务员', null, [
+            'scydgy_id'       => $sid,
+            'notify_salesman' => $mergedStr,
+            'added_count'     => count($addNames),
+        ]);
+    }
+
+    /**
+     * 查询询价 — 勾选业务员后邮箱通知(salesmen_json)
      */
     public function rfqnotifysalesman()
     {
@@ -9242,59 +9415,80 @@ class Procuremen extends Backend
         $this->ensurePurchaseOrderNotifySalesmanColumn();
         $this->ensurePurchaseOrderRfqSalesmanNotifiedColumn();
         $sid = (int)$this->request->post('scydgy_id', 0);
-        $notifyAll = (int)$this->request->post('notify_all', 0) === 1;
-        $adminId = (int)$this->request->post('admin_id', 0);
-        $name = trim((string)$this->request->post('name', ''));
         if ($sid >= 0) {
             $this->error('仅支持手工新增询价单');
         }
         $po = $this->loadManualRfqOrderForNotify($sid);
         $this->assertRfqCanNotifySalesman($po);
-        $allowed = $this->splitRfqSalesmanNames(trim((string)($po['notify_salesman'] ?? '')));
-        if ($allowed === []) {
-            $this->error('该订单未指定通知业务员');
-        }
 
-        $allContacts = $this->resolveRfqSalesmanContactRows($allowed);
-        $targets = [];
-        if ($notifyAll) {
-            foreach ($allContacts as $row) {
-                $em = trim((string)($row['email'] ?? ''));
-                if ($em !== '' && filter_var($em, FILTER_VALIDATE_EMAIL)) {
-                    $targets[] = $row;
-                }
-            }
-            if ($targets === []) {
-                $this->error('名单中业务员均未填写有效邮箱');
+        $raw = $this->request->post('salesmen/a', []);
+        if (!is_array($raw) || $raw === []) {
+            $salesmenJson = $this->request->post('salesmen_json', '');
+            if (is_string($salesmenJson) && $salesmenJson !== '') {
+                $decoded = json_decode($salesmenJson, true);
+                $raw = is_array($decoded) ? $decoded : [];
             }
-        } else {
-            $contact = null;
-            if ($adminId > 0) {
-                foreach ($allContacts as $row) {
-                    if ((int)($row['admin_id'] ?? 0) === $adminId) {
-                        $contact = $row;
-                        break;
-                    }
-                }
+        }
+        // 兼容旧参数:notify_all / 单人 name
+        if ((!is_array($raw) || $raw === []) && (int)$this->request->post('notify_all', 0) === 1) {
+            $raw = $this->splitRfqSalesmanNames(trim((string)($po['notify_salesman'] ?? '')));
+        }
+        if ((!is_array($raw) || $raw === [])) {
+            $oneName = trim((string)$this->request->post('name', ''));
+            if ($oneName !== '') {
+                $raw = [$oneName];
             }
-            if ($contact === null && $name !== '') {
-                foreach ($allContacts as $row) {
-                    if (trim((string)($row['name'] ?? '')) === $name) {
-                        $contact = $row;
-                        break;
-                    }
-                }
+        }
+        $selectedNames = [];
+        foreach ((array)$raw as $item) {
+            if (is_array($item)) {
+                $n = trim((string)($item['name'] ?? ''));
+            } else {
+                $n = trim((string)$item);
             }
-            if ($contact === null) {
-                $this->error('业务员不在本单通知名单中');
+            if ($n !== '') {
+                $selectedNames[] = $n;
             }
-            $toEmail = trim((string)($contact['email'] ?? ''));
-            $toName = trim((string)($contact['name'] ?? ''));
-            if ($toEmail === '' || !filter_var($toEmail, FILTER_VALIDATE_EMAIL)) {
-                $this->error(($toName !== '' ? $toName : '该业务员') . ' 未填写有效邮箱');
+        }
+        $selectedNames = array_values(array_unique($selectedNames));
+        if ($selectedNames === []) {
+            $this->error('请选择要通知的业务员');
+        }
+
+        $optionMap = [];
+        foreach ($this->loadRfqSalesmanContactOptions() as $opt) {
+            $n = trim((string)($opt['name'] ?? ''));
+            if ($n === '') {
+                continue;
             }
-            $targets[] = $contact;
+            $optionMap[$n] = $opt;
+        }
+        $targets = [];
+        foreach ($selectedNames as $n) {
+            if (!isset($optionMap[$n])) {
+                $this->error('业务员不在可选名单中:' . $n);
+            }
+            $email = trim((string)($optionMap[$n]['email'] ?? ''));
+            if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
+                $this->error('业务员未填写有效邮箱,无法通知:' . $n);
+            }
+            $resolved = $this->resolveRfqSalesmanContactRows([$n]);
+            $targets[] = $resolved[0] ?? [
+                'admin_id' => 0,
+                'name'     => $n,
+                'email'    => $email,
+            ];
+        }
+
+        $mergedStr = $this->joinRfqMultiSelectValues($selectedNames);
+        try {
+            Db::table('purchase_order')->where('scydgy_id', $sid)->update([
+                'notify_salesman' => $mergedStr,
+            ]);
+        } catch (\Throwable $e) {
+            $this->error('保存通知业务员失败:' . $e->getMessage());
         }
+        $po['notify_salesman'] = $mergedStr;
 
         $mailConfig = $this->loadMailerConfig();
         if (empty($mailConfig['host'])) {
@@ -9391,6 +9585,7 @@ class Procuremen extends Backend
         }
         $this->success($msg, null, [
             'scydgy_id'                => $sid,
+            'notify_salesman'          => $mergedStr,
             'rfq_salesman_notified'    => 1,
             'rfq_salesman_notify_time' => $now,
             'notify_time_text'         => $now,
@@ -11736,17 +11931,48 @@ class Procuremen extends Backend
         unset($r);
 
         $pickedCompanyName = '';
+        $manualPicked = 0;
+        $manualPickReason = '';
+        $quoteVisible = 0;
+        $bidOpenVerified = 0;
         if ($isPurchaseConfirm) {
-            foreach ($confirmBundle['pos'] ?? [] as $poRow) {
-                if (!is_array($poRow)) {
-                    continue;
+            // 确认包为空时,用当前主单兜底,避免指定说明读不到
+            if (($confirmBundle['ccydh'] ?? '') === '' && is_array($po)) {
+                $confirmBundle['ccydh'] = trim((string)($po['CCYDH'] ?? ''));
+            }
+            if (($confirmBundle['pos'] ?? []) === [] && is_array($po)) {
+                $confirmBundle['pos'] = [$po];
+            }
+            $manualPickInfo = $this->resolveProcuremenManualPickInfo($confirmBundle);
+            $manualPicked = $this->isProcuremenManualPickedForBundle($confirmBundle) ? 1 : 0;
+            $manualPickReason = (string)($manualPickInfo['reason'] ?? '');
+            if ($manualPickInfo['company'] !== '') {
+                $pickedCompanyName = $manualPickInfo['company'];
+            }
+            if ($pickedCompanyName === '') {
+                foreach ($confirmBundle['pos'] ?? [] as $poRow) {
+                    if (!is_array($poRow)) {
+                        continue;
+                    }
+                    $pn = trim((string)($poRow['pick_company_name'] ?? ''));
+                    if ($pn !== '') {
+                        $pickedCompanyName = $pn;
+                        break;
+                    }
                 }
-                $pn = trim((string)($poRow['pick_company_name'] ?? ''));
-                if ($pn !== '') {
-                    $pickedCompanyName = $pn;
-                    break;
+            }
+            // 再兜底:直接读当前主单字段
+            if (is_array($po) && (int)($po['manual_pick'] ?? 0) === 1) {
+                $manualPicked = 1;
+                if ($manualPickReason === '') {
+                    $manualPickReason = trim((string)($po['manual_pick_reason'] ?? ''));
+                }
+                if ($pickedCompanyName === '') {
+                    $pickedCompanyName = trim((string)($po['pick_company_name'] ?? ''));
                 }
             }
+            $bidOpenVerified = $this->isProcuremenBidOpenVerifiedForBundle($confirmBundle) ? 1 : 0;
+            $quoteVisible = ($bidOpenVerified || $manualPicked) ? 1 : 0;
         }
 
         $this->view->assign('rows', $rows ?: []);
@@ -11762,7 +11988,10 @@ class Procuremen extends Backend
         $this->view->assign('confirmPickGroups', $this->maskSupplierContactList($confirmPickGroups));
         $this->view->assign('confirmPickCount', count($confirmPickGroups));
         $this->view->assign('pickedCompanyName', $pickedCompanyName);
-        $this->view->assign('bidOpenVerified', ($isPurchaseConfirm && $this->isProcuremenBidOpenVerifiedForBundle($confirmBundle)) ? 1 : 0);
+        $this->view->assign('manualPicked', $manualPicked);
+        $this->view->assign('manualPickReason', $manualPickReason);
+        $this->view->assign('bidOpenVerified', $bidOpenVerified);
+        $this->view->assign('quoteVisible', $quoteVisible);
         $orderScoreRule = ProcuremenSupplierScore::resolveRuleForCcydh((string)($confirmBundle['ccydh'] ?? ''));
         $this->view->assign('orderScoreRuleName', (string)($orderScoreRule['name'] ?? ''));
         $this->view->assign('supplierScoreRuleText', ProcuremenSupplierScore::formatRuleFormulaTextForGroups($confirmPickGroups, $orderScoreRule));

+ 268 - 201
application/admin/controller/Supplierservicescore.php

@@ -469,14 +469,15 @@ class Supplierservicescore extends Backend
     }
 
     /**
-     * 某供应商某月参与评分的订单明细(开标/指定后按时间写入的评分记录)
-     * 一单一行(多工序顿号间隔);按开标/指定时间倒序,不把整月汇总成一行
+     * 本月订单明细:已下发订单按「下发批次」(pick_time) 分行;
+     * 同一批次内多工序顿号合并,不同下发时间不合并。
+     * 仅返回已做质量评分且已完结的订单。
      *
      * @return array<int, array<string, mixed>>
      */
     /**
-     * 本月订单明细:已下发订单按「下发批次」(pick_time) 分行;
-     * 同一批次内多工序顿号合并,不同下发时间不合并。
+     * 本月订单明细:与质量得分同源 —— 该供应商本月「质量评分」订单
+     * (不再依赖招标评分表,也不强制已完结;与 calcQualityScoreFromInbound 一致)
      *
      * @return array<int, array<string, mixed>>
      */
@@ -488,49 +489,127 @@ class Supplierservicescore extends Backend
             return [];
         }
         ProcuremenSupplierScore::ensureSchema();
+
+        // 1) 与质量分同一来源:入库/质量评分表
         try {
-            $scoreRows = \think\Db::table(ProcuremenSupplierScore::TABLE_SCORE)
-                ->where('ym', $ym)
+            $scoreRows = \think\Db::table('purchase_order_inbound_score')
                 ->where('company_name', $companyName)
-                ->field('ccydh,score,rank_no,quality_score,price_score,price_sum,lead_score,lead_days_sum,createtime')
-                ->order('createtime', 'desc')
-                ->order('ccydh', 'asc')
+                ->where(function ($q) use ($ym) {
+                    $q->where('updatetime', 'like', $ym . '%')
+                        ->whereOr('createtime', 'like', $ym . '%');
+                })
+                ->field('scydgy_id,ccydh,cyjmc,cgymc,result,delivery_status,customer_complaint,order_interrupt,remark,createtime,updatetime')
+                ->order('id', 'desc')
                 ->select();
         } catch (\Throwable $e) {
-            return [];
+            try {
+                $scoreRows = \think\Db::table('purchase_order_inbound_score')
+                    ->where('company_name', $companyName)
+                    ->where(function ($q) use ($ym) {
+                        $q->where('updatetime', 'like', $ym . '%')
+                            ->whereOr('createtime', 'like', $ym . '%');
+                    })
+                    ->field('scydgy_id,ccydh,cyjmc,cgymc,result,remark,createtime,updatetime')
+                    ->order('id', 'desc')
+                    ->select();
+            } catch (\Throwable $e2) {
+                $scoreRows = [];
+            }
         }
         if (!is_array($scoreRows) || $scoreRows === []) {
             return [];
         }
-        $scoreByCcydh = [];
+
+        /** @var array<string, array<string, mixed>> $byCcydh */
+        $byCcydh = [];
+        $sidSet = [];
         foreach ($scoreRows as $r) {
             if (!is_array($r)) {
                 continue;
             }
-            $c = trim((string)($r['ccydh'] ?? ''));
-            // 手工新增订单号 YW… 不进本月订单明细
-            if ($c === '' || stripos($c, 'YW') === 0) {
+            $ccydh = trim((string)($r['ccydh'] ?? ''));
+            $result = trim((string)($r['result'] ?? ''));
+            if ($ccydh === '' || ($result !== '合格' && $result !== '不合格')) {
                 continue;
             }
-            // 同单可能有重复评分行时保留最新一条(已按 createtime desc)
-            if (!isset($scoreByCcydh[$c])) {
-                $scoreByCcydh[$c] = $r;
+            $sid = (int)($r['scydgy_id'] ?? 0);
+            if ($sid > 0) {
+                $sidSet[$sid] = true;
+            }
+            $scoreTime = trim((string)($r['updatetime'] ?? ''));
+            if ($scoreTime === '' || preg_match('/^0000-00-00/', $scoreTime)) {
+                $scoreTime = trim((string)($r['createtime'] ?? ''));
+            }
+            if (!isset($byCcydh[$ccydh])) {
+                $ds = trim((string)($r['delivery_status'] ?? ''));
+                if ($ds !== '准时' && $ds !== '滞后') {
+                    $ds = '';
+                }
+                $byCcydh[$ccydh] = [
+                    'CCYDH'              => $ccydh,
+                    'scydgy_id'          => $sid > 0 ? $sid : 0,
+                    'scydgy_ids'         => $sid > 0 ? [$sid] : [],
+                    'CYJMC'              => trim((string)($r['cyjmc'] ?? '')),
+                    'CGYMC_list'         => [],
+                    'inbound_result'     => $result,
+                    'delivery_status'    => $ds,
+                    'customer_complaint' => (int)($r['customer_complaint'] ?? 0) === 1 ? 1 : 0,
+                    'order_interrupt'    => (int)($r['order_interrupt'] ?? 0) === 1 ? 1 : 0,
+                    'inbound_remark'     => trim((string)($r['remark'] ?? '')),
+                    'score_time'         => $scoreTime,
+                ];
+            }
+            $g = &$byCcydh[$ccydh];
+            // 任一工序不合格 → 整单不合格(与质量分统计一致)
+            if ($result === '不合格') {
+                $g['inbound_result'] = '不合格';
+            }
+            if ($sid > 0) {
+                if ($g['scydgy_id'] <= 0) {
+                    $g['scydgy_id'] = $sid;
+                }
+                if (!in_array($sid, $g['scydgy_ids'], true)) {
+                    $g['scydgy_ids'][] = $sid;
+                }
+            }
+            $gymc = trim((string)($r['cgymc'] ?? ''));
+            if ($gymc !== '' && !in_array($gymc, $g['CGYMC_list'], true)) {
+                $g['CGYMC_list'][] = $gymc;
+            }
+            if ($g['CYJMC'] === '') {
+                $g['CYJMC'] = trim((string)($r['cyjmc'] ?? ''));
             }
+            if ($g['inbound_remark'] === '') {
+                $g['inbound_remark'] = trim((string)($r['remark'] ?? ''));
+            }
+            if ($g['delivery_status'] === '') {
+                $ds = trim((string)($r['delivery_status'] ?? ''));
+                if ($ds === '准时' || $ds === '滞后') {
+                    $g['delivery_status'] = $ds;
+                }
+            }
+            if ((int)($r['customer_complaint'] ?? 0) === 1) {
+                $g['customer_complaint'] = 1;
+            }
+            if ((int)($r['order_interrupt'] ?? 0) === 1) {
+                $g['order_interrupt'] = 1;
+            }
+            if ($scoreTime !== '' && ($g['score_time'] === '' || strcmp($scoreTime, $g['score_time']) > 0)) {
+                $g['score_time'] = $scoreTime;
+            }
+            unset($g);
         }
-        if ($scoreByCcydh === []) {
+        if ($byCcydh === []) {
             return [];
         }
-        $manualCcydh = [];
-        /** @var array<string, array<string, array<string, mixed>>> $batchesByCcydh */
-        $batchesByCcydh = [];
-        /** @var array<int, array<string, mixed>> $issuedPoRows */
-        $issuedPoRows = [];
-        $sidList = [];
+
+        // 2) 补充采购单展示字段
+        $poByCcydh = [];
         try {
             $poRows = \think\Db::table('purchase_order')
-                ->where('CCYDH', 'in', array_keys($scoreByCcydh))
+                ->where('CCYDH', 'in', array_keys($byCcydh))
                 ->whereRaw('(mod_rq IS NULL OR TRIM(CAST(mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')')
-                ->field('CCYDH,CYJMC,CGYMC,CCLBMMC,CDW,This_quantity,ceilingPrice,pick_company_name,wflow_status,status,pick_time,scydgy_id')
+                ->field('CCYDH,CYJMC,CGYMC,CCLBMMC,CDW,pick_company_name,wflow_status,status,pick_time,scydgy_id')
                 ->order('id', 'asc')
                 ->select();
         } catch (\Throwable $e) {
@@ -542,193 +621,158 @@ class Supplierservicescore extends Backend
                     continue;
                 }
                 $c = trim((string)($po['CCYDH'] ?? ''));
-                if ($c === '' || !isset($scoreByCcydh[$c])) {
+                if ($c === '' || !isset($byCcydh[$c])) {
                     continue;
                 }
+                if (!isset($poByCcydh[$c])) {
+                    $poByCcydh[$c] = [
+                        'CYJMC'             => '',
+                        'CCLBMMC'           => '',
+                        'CGYMC_list'        => [],
+                        'CDW'               => '',
+                        'pick_company_name' => '',
+                        'wflow_status'      => '',
+                        'status'            => '',
+                        'pick_time'         => '',
+                        'completed'         => false,
+                        'picked_match'      => false,
+                        'scydgy_id'         => 0,
+                    ];
+                }
+                $p = &$poByCcydh[$c];
                 $sid = (int)($po['scydgy_id'] ?? 0);
-                if ($sid < 0) {
-                    $manualCcydh[$c] = true;
-                    continue;
+                if ($p['scydgy_id'] <= 0 && $sid > 0) {
+                    $p['scydgy_id'] = $sid;
                 }
-                if (isset($manualCcydh[$c])) {
-                    continue;
+                if ($p['CYJMC'] === '') {
+                    $p['CYJMC'] = trim((string)($po['CYJMC'] ?? ''));
                 }
-                $pickTime = trim((string)($po['pick_time'] ?? ''));
-                if ($pickTime === '' || preg_match('/^0000-00-00/', $pickTime)) {
-                    continue;
+                if ($p['CCLBMMC'] === '') {
+                    $p['CCLBMMC'] = trim((string)($po['CCLBMMC'] ?? ''));
                 }
-                $issuedPoRows[] = $po;
-                if ($sid > 0) {
-                    $sidList[$sid] = true;
+                if ($p['CDW'] === '') {
+                    $p['CDW'] = trim((string)($po['CDW'] ?? ''));
                 }
-            }
-        }
-        // 按工序首次下发日志时间分批(避免整单 pick_time 被后次下发覆盖后合到一行)
-        $firstIssueBySid = $this->loadFirstIssueTimeByScydgyIds(array_keys($sidList));
-        foreach ($issuedPoRows as $po) {
-            if (isset($manualCcydh[trim((string)($po['CCYDH'] ?? ''))])) {
-                continue;
-            }
-            $c = trim((string)($po['CCYDH'] ?? ''));
-            $sid = (int)($po['scydgy_id'] ?? 0);
-            $pickTime = trim((string)($po['pick_time'] ?? ''));
-            $batchKey = '';
-            if ($sid > 0 && isset($firstIssueBySid[$sid])) {
-                $batchKey = $firstIssueBySid[$sid];
-            }
-            if ($batchKey === '') {
-                $batchKey = $pickTime;
-            }
-            if ($batchKey === '' || preg_match('/^0000-00-00/', $batchKey)) {
-                continue;
-            }
-            // 同一分钟内视为同一次下发(循环写入可能差 1 秒)
-            $batchGroup = $batchKey;
-            if (preg_match('/^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})/', $batchKey, $m)) {
-                $batchGroup = $m[1];
-            }
-            if (!isset($batchesByCcydh[$c])) {
-                $batchesByCcydh[$c] = [];
-            }
-            if (!isset($batchesByCcydh[$c][$batchGroup])) {
-                $batchesByCcydh[$c][$batchGroup] = [
-                    'scydgy_id'         => $sid > 0 ? $sid : 0,
-                    'scydgy_ids'        => [],
-                    'CYJMC'             => trim((string)($po['CYJMC'] ?? '')),
-                    'CCLBMMC'           => trim((string)($po['CCLBMMC'] ?? '')),
-                    'CGYMC_list'        => [],
-                    'CDW'               => trim((string)($po['CDW'] ?? '')),
-                    'pick_company_name' => trim((string)($po['pick_company_name'] ?? '')),
-                    'wflow_status'      => trim((string)($po['wflow_status'] ?? '')),
-                    'status'            => trim((string)($po['status'] ?? '')),
-                    'pick_time'         => $batchKey,
-                    'completed'         => false,
-                    'picked_match'      => false,
-                ];
-            }
-            $batch = &$batchesByCcydh[$c][$batchGroup];
-            if ($batch['scydgy_id'] <= 0 && $sid > 0) {
-                $batch['scydgy_id'] = $sid;
-            }
-            if ($sid > 0 && !in_array($sid, $batch['scydgy_ids'], true)) {
-                $batch['scydgy_ids'][] = $sid;
-            }
-            // 展示时间取该批次内最早时间
-            if ($batch['pick_time'] === '' || strcmp($batchKey, $batch['pick_time']) < 0) {
-                $batch['pick_time'] = $batchKey;
-            }
-            if ($batch['CYJMC'] === '') {
-                $batch['CYJMC'] = trim((string)($po['CYJMC'] ?? ''));
-            }
-            if ($batch['CCLBMMC'] === '') {
-                $batch['CCLBMMC'] = trim((string)($po['CCLBMMC'] ?? ''));
-            }
-            $g = trim((string)($po['CGYMC'] ?? ''));
-            if ($g !== '' && !in_array($g, $batch['CGYMC_list'], true)) {
-                $batch['CGYMC_list'][] = $g;
-            }
-            $pn = trim((string)($po['pick_company_name'] ?? ''));
-            if ($pn !== '') {
-                if ($batch['pick_company_name'] === '') {
-                    $batch['pick_company_name'] = $pn;
-                }
-                if ($pn === $companyName) {
-                    $batch['picked_match'] = true;
+                $g = trim((string)($po['CGYMC'] ?? ''));
+                if ($g !== '' && !in_array($g, $p['CGYMC_list'], true)) {
+                    $p['CGYMC_list'][] = $g;
                 }
-            }
-            if (\app\common\library\ProcuremenStatus::isPoCompleted($po['status'] ?? '')) {
-                $batch['completed'] = true;
-                $batch['status'] = \app\common\library\ProcuremenStatus::PO_COMPLETED;
-            }
-            if (\app\common\library\ProcuremenStatus::isWflowApproved($po['wflow_status'] ?? '')) {
-                $batch['wflow_status'] = \app\common\library\ProcuremenStatus::WFLOW_APPROVED;
-            } elseif ($batch['wflow_status'] === '' && trim((string)($po['wflow_status'] ?? '')) !== '') {
-                $batch['wflow_status'] = trim((string)$po['wflow_status']);
-            }
-            unset($batch);
-        }
-
-        $out = [];
-        $allSids = [];
-        foreach ($batchesByCcydh as $ccydhBatches) {
-            if (!is_array($ccydhBatches)) {
-                continue;
-            }
-            foreach ($ccydhBatches as $batch) {
-                if (!is_array($batch)) {
-                    continue;
+                $pn = trim((string)($po['pick_company_name'] ?? ''));
+                if ($pn !== '') {
+                    if ($p['pick_company_name'] === '') {
+                        $p['pick_company_name'] = $pn;
+                    }
+                    if ($pn === $companyName) {
+                        $p['picked_match'] = true;
+                    }
                 }
-                foreach ((array)($batch['scydgy_ids'] ?? []) as $sidItem) {
-                    $sidItem = (int)$sidItem;
-                    if ($sidItem > 0) {
-                        $allSids[$sidItem] = true;
+                $pt = trim((string)($po['pick_time'] ?? ''));
+                if ($pt !== '' && !preg_match('/^0000-00-00/', $pt)) {
+                    if ($p['pick_time'] === '' || strcmp($pt, $p['pick_time']) < 0) {
+                        $p['pick_time'] = $pt;
                     }
                 }
-                $one = (int)($batch['scydgy_id'] ?? 0);
-                if ($one > 0) {
-                    $allSids[$one] = true;
+                if (\app\common\library\ProcuremenStatus::isPoCompleted($po['status'] ?? '')) {
+                    $p['completed'] = true;
+                    $p['status'] = \app\common\library\ProcuremenStatus::PO_COMPLETED;
+                } elseif ($p['status'] === '' && trim((string)($po['status'] ?? '')) !== '') {
+                    $p['status'] = trim((string)$po['status']);
                 }
+                if (\app\common\library\ProcuremenStatus::isWflowApproved($po['wflow_status'] ?? '')) {
+                    $p['wflow_status'] = \app\common\library\ProcuremenStatus::WFLOW_APPROVED;
+                } elseif ($p['wflow_status'] === '' && trim((string)($po['wflow_status'] ?? '')) !== '') {
+                    $p['wflow_status'] = trim((string)$po['wflow_status']);
+                }
+                unset($p);
             }
         }
-        $inboundBySid = $this->loadInboundResultByScydgyIds(array_keys($allSids));
 
-        foreach ($scoreByCcydh as $ccydh => $r) {
-            if (isset($manualCcydh[$ccydh]) || !isset($batchesByCcydh[$ccydh])) {
-                continue;
-            }
-            foreach ($batchesByCcydh[$ccydh] as $batch) {
-                if (!is_array($batch)) {
+        // 3) 可选:招标评分表补排名/分项(没有也不影响展示)
+        $bidByCcydh = [];
+        try {
+            $bidRows = \think\Db::table(ProcuremenSupplierScore::TABLE_SCORE)
+                ->where('ym', $ym)
+                ->where('company_name', $companyName)
+                ->where('ccydh', 'in', array_keys($byCcydh))
+                ->field('ccydh,score,rank_no,quality_score,price_score,price_sum,lead_score,lead_days_sum,createtime')
+                ->order('createtime', 'desc')
+                ->select();
+        } catch (\Throwable $e) {
+            $bidRows = [];
+        }
+        if (is_array($bidRows)) {
+            foreach ($bidRows as $br) {
+                if (!is_array($br)) {
                     continue;
                 }
-                $pickTimeRaw = trim((string)($batch['pick_time'] ?? ''));
-                if ($pickTimeRaw === '') {
+                $c = trim((string)($br['ccydh'] ?? ''));
+                if ($c === '' || isset($bidByCcydh[$c])) {
                     continue;
                 }
-                $gymcList = is_array($batch['CGYMC_list'] ?? null) ? $batch['CGYMC_list'] : [];
-                // 与详情页一致:采购确认通过或已完结后才展示中标/未中标
-                $showBid = !empty($batch['completed'])
-                    || \app\common\library\ProcuremenStatus::isPoCompleted($batch['status'] ?? '')
-                    || \app\common\library\ProcuremenStatus::isWflowApproved($batch['wflow_status'] ?? '');
-                $isPicked = !empty($batch['picked_match']) ? 1 : 0;
-                $inboundResult = '';
-                $sidCandidates = is_array($batch['scydgy_ids'] ?? null) ? $batch['scydgy_ids'] : [];
-                $mainSid = (int)($batch['scydgy_id'] ?? 0);
-                if ($mainSid > 0 && !in_array($mainSid, $sidCandidates, true)) {
-                    $sidCandidates[] = $mainSid;
-                }
-                foreach ($sidCandidates as $sidItem) {
-                    $sidItem = (int)$sidItem;
-                    if ($sidItem > 0 && isset($inboundBySid[$sidItem]) && $inboundBySid[$sidItem] !== '') {
-                        $inboundResult = $inboundBySid[$sidItem];
-                        break;
-                    }
-                }
-                $out[] = [
-                    'seq_no'            => 0,
-                    'scydgy_id'         => (int)($batch['scydgy_id'] ?? 0),
-                    'CCYDH'             => $ccydh,
-                    'CYJMC'             => (string)($batch['CYJMC'] ?? ''),
-                    'CCLBMMC'           => (string)($batch['CCLBMMC'] ?? ''),
-                    'CGYMC'             => $gymcList !== [] ? implode('、', $gymcList) : '',
-                    'CDW'               => (string)($batch['CDW'] ?? ''),
-                    'pick_company_name' => (string)($batch['pick_company_name'] ?? ''),
-                    'wflow_status'      => (string)($batch['wflow_status'] ?? ''),
-                    'status'            => (string)($batch['status'] ?? ''),
-                    'progress_text'     => $this->formatMonthOrderProgressText($batch),
-                    'pick_time'         => $pickTimeRaw,
-                    'issue_time'        => \app\common\library\ProcuremenTime::formatDisplayDateTime($pickTimeRaw),
-                    'score'             => $r['score'] ?? null,
-                    'rank_no'           => (int)($r['rank_no'] ?? 0),
-                    'quality_score'     => $r['quality_score'] ?? null,
-                    'price_score'       => $r['price_score'] ?? null,
-                    'price_sum'         => $r['price_sum'] ?? null,
-                    'lead_score'        => $r['lead_score'] ?? null,
-                    'lead_days_sum'     => $r['lead_days_sum'] ?? null,
-                    'is_picked'         => $isPicked,
-                    'show_bid_result'   => $showBid ? 1 : 0,
-                    'inbound_result'    => $inboundResult,
-                ];
+                $bidByCcydh[$c] = $br;
+            }
+        }
+
+        $out = [];
+        foreach ($byCcydh as $ccydh => $g) {
+            $po = $poByCcydh[$ccydh] ?? [];
+            $bid = $bidByCcydh[$ccydh] ?? [];
+            $gymcList = is_array($g['CGYMC_list'] ?? null) ? $g['CGYMC_list'] : [];
+            if ($gymcList === [] && is_array($po['CGYMC_list'] ?? null)) {
+                $gymcList = $po['CGYMC_list'];
+            }
+            $batch = [
+                'completed'    => !empty($po['completed']),
+                'status'       => (string)($po['status'] ?? ''),
+                'wflow_status' => (string)($po['wflow_status'] ?? ''),
+            ];
+            $progressText = $this->formatMonthOrderProgressText($batch);
+            // 已质量评分:进度优先显示已完结,否则显示实际流程
+            if ($progressText === '待入库评分') {
+                $progressText = \app\common\library\ProcuremenStatus::PO_COMPLETED;
+            }
+            $showBid = !empty($po['completed'])
+                || \app\common\library\ProcuremenStatus::isPoCompleted($po['status'] ?? '')
+                || \app\common\library\ProcuremenStatus::isWflowApproved($po['wflow_status'] ?? '');
+            $pickTimeRaw = trim((string)($po['pick_time'] ?? ''));
+            if ($pickTimeRaw === '') {
+                $pickTimeRaw = trim((string)($g['score_time'] ?? ''));
+            }
+            $sid = (int)($g['scydgy_id'] ?? 0);
+            if ($sid <= 0) {
+                $sid = (int)($po['scydgy_id'] ?? 0);
             }
+            $out[] = [
+                'seq_no'              => 0,
+                'scydgy_id'           => $sid,
+                'CCYDH'               => $ccydh,
+                'CYJMC'               => (string)(($g['CYJMC'] ?? '') !== '' ? $g['CYJMC'] : ($po['CYJMC'] ?? '')),
+                'CCLBMMC'             => (string)($po['CCLBMMC'] ?? ''),
+                'CGYMC'               => $gymcList !== [] ? implode('、', $gymcList) : '',
+                'CDW'                 => (string)($po['CDW'] ?? ''),
+                'pick_company_name'   => (string)(($po['pick_company_name'] ?? '') !== '' ? $po['pick_company_name'] : $companyName),
+                'wflow_status'        => (string)($po['wflow_status'] ?? ''),
+                'status'              => (string)($po['status'] ?? ''),
+                'progress_text'       => $progressText,
+                'pick_time'           => $pickTimeRaw,
+                'issue_time'          => $pickTimeRaw !== ''
+                    ? \app\common\library\ProcuremenTime::formatDisplayDateTime($pickTimeRaw)
+                    : '',
+                'score'               => $bid['score'] ?? null,
+                'rank_no'             => (int)($bid['rank_no'] ?? 0),
+                'quality_score'       => $bid['quality_score'] ?? null,
+                'price_score'         => $bid['price_score'] ?? null,
+                'price_sum'           => $bid['price_sum'] ?? null,
+                'lead_score'          => $bid['lead_score'] ?? null,
+                'lead_days_sum'       => $bid['lead_days_sum'] ?? null,
+                'is_picked'           => !empty($po['picked_match']) ? 1 : 1,
+                'show_bid_result'     => $showBid ? 1 : 0,
+                'inbound_result'      => (string)($g['inbound_result'] ?? ''),
+                'delivery_status'     => (string)($g['delivery_status'] ?? ''),
+                'customer_complaint'  => (int)($g['customer_complaint'] ?? 0),
+                'order_interrupt'     => (int)($g['order_interrupt'] ?? 0),
+                'inbound_remark'      => (string)($g['inbound_remark'] ?? ''),
+            ];
         }
 
         usort($out, static function ($a, $b) {
@@ -737,10 +781,8 @@ class Supplierservicescore extends Backend
             if ($ta !== $tb) {
                 return strcmp($tb, $ta);
             }
-            $ca = (string)($a['CCYDH'] ?? '');
-            $cb = (string)($b['CCYDH'] ?? '');
 
-            return strcmp($ca, $cb);
+            return strcmp((string)($a['CCYDH'] ?? ''), (string)($b['CCYDH'] ?? ''));
         });
         $total = count($out);
         $seq = $total;
@@ -840,10 +882,10 @@ class Supplierservicescore extends Backend
     }
 
     /**
-     * 批量读取入库评分结果(合格/不合格
+     * 批量读取质量评分明细(是否合格 / 交货情况 / 客户投诉 / 订单中断 / 备注
      *
      * @param int[] $scydgyIds
-     * @return array<int, string> scydgy_id => 合格|不合格
+     * @return array<int, array{result:string,delivery_status:string,customer_complaint:int,order_interrupt:int,remark:string}>
      */
     protected function loadInboundResultByScydgyIds(array $scydgyIds): array
     {
@@ -855,10 +897,25 @@ class Supplierservicescore extends Backend
         try {
             $rows = \think\Db::table('purchase_order_inbound_score')
                 ->where('scydgy_id', 'in', $ids)
-                ->field('scydgy_id,result')
+                ->field('scydgy_id,result,delivery_status,customer_complaint,order_interrupt,remark')
                 ->select();
         } catch (\Throwable $e) {
-            return [];
+            // 兼容旧表无新列
+            try {
+                $rows = \think\Db::table('purchase_order_inbound_score')
+                    ->where('scydgy_id', 'in', $ids)
+                    ->field('scydgy_id,result,customer_complaint,order_interrupt,remark')
+                    ->select();
+            } catch (\Throwable $e2) {
+                try {
+                    $rows = \think\Db::table('purchase_order_inbound_score')
+                        ->where('scydgy_id', 'in', $ids)
+                        ->field('scydgy_id,result,remark')
+                        ->select();
+                } catch (\Throwable $e3) {
+                    return [];
+                }
+            }
         }
         if (!is_array($rows)) {
             return [];
@@ -872,7 +929,17 @@ class Supplierservicescore extends Backend
             if ($sid <= 0 || ($result !== '合格' && $result !== '不合格')) {
                 continue;
             }
-            $out[$sid] = $result;
+            $ds = trim((string)($r['delivery_status'] ?? ''));
+            if ($ds !== '准时' && $ds !== '滞后') {
+                $ds = '';
+            }
+            $out[$sid] = [
+                'result'             => $result,
+                'delivery_status'    => $ds,
+                'customer_complaint' => (int)($r['customer_complaint'] ?? 0) === 1 ? 1 : 0,
+                'order_interrupt'    => (int)($r['order_interrupt'] ?? 0) === 1 ? 1 : 0,
+                'remark'             => trim((string)($r['remark'] ?? '')),
+            ];
         }
 
         return $out;

+ 1 - 1
application/admin/controller/auth/Admin.php

@@ -191,7 +191,7 @@ class Admin extends Backend
                     $adminValidate = \think\Loader::validate('Admin');
                     $adminValidate->rule([
                         'username' => 'require|regex:\w{3,30}|unique:admin,username,' . $row->id,
-                        'email'    => 'require|email|unique:admin,email,' . $row->id,
+                        'email'    => 'email|unique:admin,email,' . $row->id,
                         'mobile'   => 'regex:1[3-9]\d{9}|unique:admin,mobile,' . $row->id,
                         'password' => 'regex:\S{32}',
                     ]);

+ 1 - 1
application/admin/validate/Admin.php

@@ -14,7 +14,7 @@ class Admin extends Validate
         'username' => 'require|regex:\w{3,30}|unique:admin',
         'nickname' => 'require',
         'password' => 'require|regex:\S{32}',
-        'email'    => 'require|email|unique:admin,email',
+        'email'    => 'email|unique:admin,email',
         'mobile'   => 'regex:1[3-9]\d{9}|unique:admin,mobile',
     ];
 

+ 1 - 1
application/admin/view/auth/admin/add.html

@@ -15,7 +15,7 @@
     <div class="form-group">
         <label for="email" class="control-label col-xs-12 col-sm-2">{:__('Email')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input type="email" class="form-control" id="email" name="row[email]" value="" data-rule="required;email" />
+            <input type="email" class="form-control" id="email" name="row[email]" value="" data-rule="email" />
         </div>
     </div>
     <div class="form-group">

+ 1 - 1
application/admin/view/auth/admin/edit.html

@@ -15,7 +15,7 @@
     <div class="form-group">
         <label for="email" class="control-label col-xs-12 col-sm-2">{:__('Email')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input type="email" class="form-control" id="email" name="row[email]" value="{$row.email|htmlentities}" data-rule="required;email" />
+            <input type="email" class="form-control" id="email" name="row[email]" value="{$row.email|htmlentities}" data-rule="email" />
         </div>
     </div>
     <div class="form-group">

+ 20 - 20
application/admin/view/procuremen/add.html

@@ -147,32 +147,32 @@
                 <input class="form-control" name="row[CDF]" type="text" autocomplete="off">
             </div>
         </div>
-        <div class="form-group">
-            <label class="control-label col-xs-12 col-sm-2">选择供应商:</label>
-            <div class="col-xs-12 col-sm-8">
-                <select class="form-control selectpicker" name="row[cGzzxMc][]" multiple data-live-search="true" data-dropup-auto="false" data-width="100%" data-selected-text-format="values" title="请选择供应商">
-                    {volist name="rfqSupplierOptions" id="supName"}
-                    <option value="{$supName|htmlentities}">{$supName|htmlentities}</option>
-                    {/volist}
-                </select>
-            </div>
-        </div>
+<!--        <div class="form-group">-->
+<!--            <label class="control-label col-xs-12 col-sm-2">选择供应商:</label>-->
+<!--            <div class="col-xs-12 col-sm-8">-->
+<!--                <select class="form-control selectpicker" name="row[cGzzxMc][]" multiple data-live-search="true" data-dropup-auto="false" data-width="100%" data-selected-text-format="values" title="请选择供应商">-->
+<!--                    {volist name="rfqSupplierOptions" id="supName"}-->
+<!--                    <option value="{$supName|htmlentities}">{$supName|htmlentities}</option>-->
+<!--                    {/volist}-->
+<!--                </select>-->
+<!--            </div>-->
+<!--        </div>-->
         <div class="form-group">
             <label class="control-label col-xs-12 col-sm-2">需求发起人:</label>
             <div class="col-xs-12 col-sm-8">
                 <input class="form-control" name="row[cywyxm]" type="text" value="{$adminNickname|default=''|htmlentities}" readonly>
             </div>
         </div>
-        <div class="form-group">
-            <label class="control-label col-xs-12 col-sm-2">通知业务员:</label>
-            <div class="col-xs-12 col-sm-8">
-                <select class="form-control selectpicker" name="row[notify_salesman][]" multiple data-live-search="true" data-dropup-auto="false" data-width="100%" data-selected-text-format="values" title="请选择业务员">
-                    {volist name="rfqSalesmanOptions" id="smName"}
-                    <option value="{$smName|htmlentities}">{$smName|htmlentities}</option>
-                    {/volist}
-                </select>
-            </div>
-        </div>
+<!--        <div class="form-group">-->
+<!--            <label class="control-label col-xs-12 col-sm-2">通知业务员:</label>-->
+<!--            <div class="col-xs-12 col-sm-8">-->
+<!--                <select class="form-control selectpicker" name="row[notify_salesman][]" multiple data-live-search="true" data-dropup-auto="false" data-width="100%" data-selected-text-format="values" title="请选择业务员">-->
+<!--                    {volist name="rfqSalesmanOptions" id="smName"}-->
+<!--                    <option value="{$smName|htmlentities}">{$smName|htmlentities}</option>-->
+<!--                    {/volist}-->
+<!--                </select>-->
+<!--            </div>-->
+<!--        </div>-->
         <div class="form-group">
             <label class="control-label col-xs-12 col-sm-2">备注:</label>
             <div class="col-xs-12 col-sm-8">

+ 33 - 20
application/admin/view/procuremen/audit_issue.html

@@ -34,19 +34,21 @@
         color: #245269;
     }
     .audit-issue-wrap .audit-manual-pick-inline {
+        display: flex;
+        flex-direction: column;
+        gap: 2px;
         flex: 1 1 220px;
         min-width: 0;
-        margin-left: 4px;
+        margin-left: 8px;
         font-size: 13px;
         font-weight: 400;
         color: #555;
-        line-height: 1.5;
+        line-height: 1.45;
         white-space: normal;
         vertical-align: middle;
     }
-    .audit-issue-wrap .audit-manual-pick-inline .audit-manual-pick-inline-sep {
-        margin: 0 8px;
-        color: #ccc;
+    .audit-issue-wrap .audit-manual-pick-inline .audit-manual-pick-line {
+        display: block;
     }
     .audit-issue-wrap .audit-manual-pick-inline strong {
         color: #333;
@@ -171,34 +173,46 @@
         z-index: 62;
     }
     .audit-issue-wrap .audit-delivery-deadline-input {
-        width: 148px;
-        min-width: 132px;
+        width: 128px;
+        min-width: 128px;
+        max-width: 128px;
         height: 32px;
-        padding: 4px 8px;
+        padding: 4px 6px;
         margin-top: 0;
+        box-sizing: border-box;
     }
     .audit-issue-wrap .procuremen-sys-rq-split .procuremen-sys-rq-fields {
         display: flex;
-        flex-wrap: wrap;
+        flex-wrap: nowrap;
         align-items: center;
-        gap: 8px;
+        gap: 4px;
     }
     .audit-issue-wrap .procuremen-sys-rq-split .procuremen-sys-rq-date {
-        width: 148px;
-        min-width: 132px;
+        width: 128px;
+        min-width: 128px;
+        max-width: 128px;
         height: 32px;
-        padding: 4px 8px;
+        padding: 4px 6px;
+        box-sizing: border-box;
+    }
+    .audit-issue-wrap .procuremen-sys-rq-split .procuremen-sys-rq-date::-webkit-calendar-picker-indicator {
+        margin: 0 0 0 2px;
+        padding: 0;
+        width: 14px;
+        cursor: default;
     }
     .audit-issue-wrap .procuremen-sys-rq-split .procuremen-sys-rq-time-group {
         display: inline-flex;
         align-items: center;
-        gap: 4px;
+        gap: 2px;
+        flex-shrink: 0;
     }
     .audit-issue-wrap .procuremen-sys-rq-split .procuremen-sys-rq-hour,
     .audit-issue-wrap .procuremen-sys-rq-split .procuremen-sys-rq-minute {
-        width: 62px;
+        width: 54px;
+        min-width: 54px;
         height: 32px;
-        padding: 4px 6px;
+        padding: 4px 4px;
     }
     .audit-issue-wrap .procuremen-sys-rq-split .procuremen-sys-rq-colon {
         color: #666;
@@ -233,7 +247,7 @@
         color: #e67e22;
     }
     .audit-issue-wrap .audit-deadline-item.audit-bid-open-item {
-        align-items: center;
+        align-items: flex-start;
         margin-top: 0;
         flex: 1 1 320px;
         min-width: 0;
@@ -422,11 +436,10 @@
                 <span class="audit-bid-open-verified-tag"><i class="fa fa-check-circle"></i> 已指定供应商</span>
                 <span class="audit-manual-pick-inline">
                     {if !empty($pickedCompanyName)}
-                    <strong>指定供应商:</strong>{$pickedCompanyName|htmlentities}
+                    <span class="audit-manual-pick-line"><strong>指定供应商:</strong>{$pickedCompanyName|htmlentities}</span>
                     {/if}
                     {if !empty($manualPickReason)}
-                    {if !empty($pickedCompanyName)}<span class="audit-manual-pick-inline-sep">|</span>{/if}
-                    <strong>指定说明:</strong>{$manualPickReason|htmlentities}
+                    <span class="audit-manual-pick-line"><strong>指定说明:</strong>{$manualPickReason|htmlentities}</span>
                     {/if}
                 </span>
                 {else /}

+ 133 - 28
application/admin/view/procuremen/outward_detail.html

@@ -47,8 +47,9 @@
         margin: 0;
         font-size: 12px;
         table-layout: fixed;
-        width: 100%;
-        min-width: 1100px;
+        width: 1680px;
+        min-width: 1680px;
+        max-width: none;
     }
     .outward-confirm-process-table th:nth-child(3),
     .outward-confirm-process-table td.outward-process-name,
@@ -68,6 +69,35 @@
         line-height: 1.45;
         padding: 6px 8px;
     }
+    .outward-confirm-company-table th.col-supplier-name,
+    .outward-confirm-company-table td.col-supplier-name {
+        width: 200px;
+        min-width: 200px;
+        word-break: normal;
+        overflow-wrap: break-word;
+        white-space: normal;
+        line-height: 1.4;
+    }
+    .outward-confirm-company-table th.col-supplier-email,
+    .outward-confirm-company-table td.col-supplier-email {
+        width: 160px;
+        min-width: 160px;
+        word-break: break-all;
+        white-space: normal;
+        line-height: 1.35;
+    }
+    .outward-confirm-company-table th.col-supplier-user,
+    .outward-confirm-company-table td.col-supplier-user {
+        width: 72px;
+        min-width: 72px;
+        white-space: nowrap;
+    }
+    .outward-confirm-company-table th.col-supplier-phone,
+    .outward-confirm-company-table td.col-supplier-phone {
+        width: 110px;
+        min-width: 110px;
+        white-space: nowrap;
+    }
     .outward-confirm-process-table th,
     .outward-confirm-company-table th {
         background: #f5f5f5;
@@ -118,17 +148,18 @@
     .outward-confirm-deadline-row {
         display: flex;
         align-items: center;
-        flex-wrap: wrap;
-        gap: 10px 20px;
+        flex-wrap: nowrap;
+        gap: 10px 16px;
         margin: 0 0 10px;
         font-size: 13px;
         width: 100%;
     }
     .outward-confirm-deadline-item {
         display: flex;
-        flex-wrap: wrap;
+        flex-wrap: nowrap;
         align-items: center;
         gap: 8px 10px;
+        flex-shrink: 0;
     }
     .outward-confirm-deadline-row .deadline-label {
         font-weight: 600;
@@ -139,6 +170,12 @@
     .outward-confirm-deadline-item.outward-order-type-item {
         align-items: center;
     }
+    .outward-confirm-deadline-item.outward-manual-pick-item {
+        align-items: flex-start;
+        flex: 1 1 280px;
+        min-width: 0;
+        flex-shrink: 1;
+    }
     .outward-confirm-order-type-text {
         display: inline-block;
         min-height: 30px;
@@ -147,37 +184,92 @@
         font-weight: 600;
         color: #333;
         vertical-align: middle;
+        white-space: nowrap;
+    }
+    .outward-manual-pick-inline {
+        display: flex;
+        flex-direction: column;
+        gap: 2px;
+        flex: 1 1 180px;
+        min-width: 0;
+        margin-left: 8px;
+        font-size: 13px;
+        font-weight: 400;
+        color: #555;
+        line-height: 1.45;
+        white-space: normal;
+        vertical-align: middle;
+    }
+    .outward-manual-pick-inline .outward-manual-pick-line {
+        display: block;
+    }
+    .outward-manual-pick-inline strong {
+        color: #333;
+        font-weight: 600;
+    }
+    .outward-manual-pick-tag {
+        flex-shrink: 0;
+        display: inline-block;
+        height: auto;
+        line-height: 1.45;
+        margin-right: 0;
+        padding: 0;
+        border-radius: 0;
+        background: transparent;
+        color: #3c763d;
+        font-size: 15px;
+        font-weight: 600;
+        white-space: nowrap;
+        align-self: flex-start;
+        padding-top: 1px;
+    }
+    .outward-manual-pick-tag .fa {
+        font-size: 16px;
+        margin-right: 4px;
+        vertical-align: -1px;
     }
     .outward-confirm-delivery-deadline-input {
-        width: 148px;
-        min-width: 132px;
+        width: 128px;
+        min-width: 128px;
+        max-width: 128px;
         height: 32px;
-        padding: 4px 8px;
+        padding: 4px 6px;
         font-size: 13px;
+        box-sizing: border-box;
     }
     .outward-confirm-deadline-wrap.procuremen-sys-rq-split .procuremen-sys-rq-fields {
         display: flex;
-        flex-wrap: wrap;
+        flex-wrap: nowrap;
         align-items: center;
-        gap: 8px;
+        gap: 4px;
     }
     .outward-confirm-deadline-wrap.procuremen-sys-rq-split .procuremen-sys-rq-date {
-        width: 148px;
-        min-width: 132px;
+        width: 128px;
+        min-width: 128px;
+        max-width: 128px;
         height: 32px;
-        padding: 4px 8px;
+        padding: 4px 6px;
         font-size: 13px;
+        box-sizing: border-box;
+    }
+    .outward-confirm-deadline-wrap.procuremen-sys-rq-split .procuremen-sys-rq-date::-webkit-calendar-picker-indicator {
+        margin: 0 0 0 2px;
+        padding: 0;
+        width: 14px;
+        cursor: default;
     }
     .outward-confirm-deadline-wrap.procuremen-sys-rq-split .procuremen-sys-rq-time-group {
         display: inline-flex;
         align-items: center;
-        gap: 4px;
+        gap: 2px;
+        flex-shrink: 0;
     }
     .outward-confirm-deadline-wrap.procuremen-sys-rq-split .procuremen-sys-rq-hour,
     .outward-confirm-deadline-wrap.procuremen-sys-rq-split .procuremen-sys-rq-minute {
-        width: 62px;
+        width: 54px;
+        min-width: 54px;
         height: 32px;
-        padding: 4px 6px;
+        padding: 4px 4px;
         font-size: 13px;
     }
     .outward-confirm-deadline-wrap.procuremen-sys-rq-split .procuremen-sys-rq-colon {
@@ -347,6 +439,19 @@
             <span class="outward-confirm-order-type-text" title="{$supplierScoreRuleText|default=''|htmlentities}">{$orderScoreRuleName|htmlentities}</span>
         </div>
         {/if}
+        {if condition="$manualPicked"}
+        <div class="outward-confirm-deadline-item outward-manual-pick-item">
+            <span class="outward-manual-pick-tag"><i class="fa fa-check-circle"></i> 已指定供应商</span>
+            <span class="outward-manual-pick-inline">
+                {if condition="$pickedCompanyName"}
+                <span class="outward-manual-pick-line"><strong>指定供应商:</strong>{$pickedCompanyName|htmlentities}</span>
+                {/if}
+                {if condition="$manualPickReason"}
+                <span class="outward-manual-pick-line"><strong>指定说明:</strong>{$manualPickReason|htmlentities}</span>
+                {/if}
+            </span>
+        </div>
+        {/if}
     </div>
 
     <p class="audit-notify-tip">
@@ -359,10 +464,10 @@
             <thead>
             <tr>
                 <th class="text-center" style="width:52px;">选定</th>
-                <th style="min-width:140px;">供应商名称</th>
-                <th style="width:88px;">姓名</th>
-                <th style="min-width:160px;">邮箱</th>
-                <th style="width:120px;">手机号</th>
+                <th class="col-supplier-name">供应商名称</th>
+                <th class="col-supplier-user">姓名</th>
+                <th class="col-supplier-email">邮箱</th>
+                <th class="col-supplier-phone">手机号</th>
                 <th class="text-center col-rank" style="width:64px;">排名</th>
                 <th class="text-center" style="width:88px;">质量得分</th>
                 <th class="text-center" style="width:80px;" title="价格得分=(最低单价合计/本供应商单价合计)×价格权重%×100">价格得分</th>
@@ -387,14 +492,14 @@
                                {if condition="$pickedCompanyName neq '' && $pickedCompanyName eq $co.name"} checked="checked"{/if}/>
                     </label>
                 </td>
-                <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{$co.name|default=''|htmlentities}</td>
-                <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{$co.username|default=''|htmlentities}</td>
-                <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{:htmlentities(mask_email(isset($co['email']) ? $co['email'] : ''))}</td>
-                <td{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{:htmlentities(mask_phone(isset($co['phone']) ? $co['phone'] : ''))}</td>
-                <td class="text-center col-rank"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;font-size:17px;font-weight:700;">{if condition="!empty($bidOpenVerified)"}{$co.service_rank_text|default=''|htmlentities}{/if}</td>
-                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{if condition="!empty($bidOpenVerified)"}{$co.service_quality_score_text|default=''|htmlentities}{/if}</td>
-                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;"{if condition="!empty($bidOpenVerified)"} title="单价合计 {$co.service_price_sum_text|default=''|htmlentities}"{/if}>{if condition="!empty($bidOpenVerified)"}{$co.service_price_score_text|default=''|htmlentities}{/if}</td>
-                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;font-weight:600;">{if condition="!empty($bidOpenVerified)"}{$co.service_score_text|default=''|htmlentities}{/if}</td>
+                <td class="col-supplier-name"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{$co.name|default=''|htmlentities}</td>
+                <td class="col-supplier-user"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{$co.username|default=''|htmlentities}</td>
+                <td class="col-supplier-email"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{:htmlentities(mask_email(isset($co['email']) ? $co['email'] : ''))}</td>
+                <td class="col-supplier-phone"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{:htmlentities(mask_phone(isset($co['phone']) ? $co['phone'] : ''))}</td>
+                <td class="text-center col-rank"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;font-size:17px;font-weight:700;">{if condition="!empty($quoteVisible)"}{$co.service_rank_text|default=''|htmlentities}{/if}</td>
+                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;">{if condition="!empty($quoteVisible)"}{$co.service_quality_score_text|default=''|htmlentities}{/if}</td>
+                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;"{if condition="!empty($quoteVisible)"} title="单价合计 {$co.service_price_sum_text|default=''|htmlentities}"{/if}>{if condition="!empty($quoteVisible)"}{$co.service_price_score_text|default=''|htmlentities}{/if}</td>
+                <td class="text-center"{if condition="$co.display_rowspan gt 1"} rowspan="{$co.display_rowspan}"{/if} style="vertical-align:middle;font-weight:600;">{if condition="!empty($quoteVisible)"}{$co.service_score_text|default=''|htmlentities}{/if}</td>
                 {/if}
                 <td class="outward-process-name">{$ql.cgymc|default=''|htmlentities}</td>
                 <td class="text-center{if condition="!empty($ql.amount_quote_pending)"} outward-quote-empty{elseif condition="$ql.amount_filled neq 1"} outward-quote-empty{/if}">{$ql.unit_price_text|default='未填写'|htmlentities}</td>

+ 0 - 2
application/admin/view/procuremen/rfqlist.html

@@ -87,8 +87,6 @@
         }
         .procuremen-rfq-notify-wrap {
             padding: 12px 16px 8px;
-            height: 270px;
-            overflow: auto;
             box-sizing: border-box;
         }
         .procuremen-rfq-notify-wrap table {

+ 22 - 34
application/admin/view/supplierservicescore/index.html

@@ -38,40 +38,31 @@
         margin: 0 auto !important;
         float: none !important;
     }
-    .supplierservicescore-page .bootstrap-table .table td .ssc-final-score-input,
-    .supplierservicescore-page .bootstrap-table .table td .ssc-score-grade-select {
-        overflow: visible;
+    /* 操作列固定右侧,横向滚动时始终可见 */
+    .supplierservicescore-page .bootstrap-table .fixed-table-header th.ssc-operate-col,
+    .supplierservicescore-page .bootstrap-table .fixed-table-body td.ssc-operate-col,
+    .supplierservicescore-page .bootstrap-table th[data-field="operate"],
+    .supplierservicescore-page .bootstrap-table td[data-field="operate"] {
+        position: sticky;
+        right: 0;
+        background: #fff;
+        z-index: 5;
+        overflow: visible !important;
+        box-shadow: -4px 0 8px rgba(0, 0, 0, 0.06);
+        border-left: 1px solid #e8e8e8 !important;
     }
-    /* 分数输入、评分等级下拉在单元格内水平居中 */
-    .supplierservicescore-page .bootstrap-table .table td .ssc-quality-score-input,
-    .supplierservicescore-page .bootstrap-table .table td .ssc-price-score-input,
-    .supplierservicescore-page .bootstrap-table .table td .ssc-delivery-score-input,
-    .supplierservicescore-page .bootstrap-table .table td .ssc-value-added-score-input,
-    .supplierservicescore-page .bootstrap-table .table td .ssc-final-score-input,
-    .supplierservicescore-page .bootstrap-table .table td .ssc-performance-score-input {
-        display: block !important;
-        width: 96px !important;
-        max-width: 96px;
-        margin: 0 auto !important;
-        float: none !important;
-        text-align: center;
-        height: 28px;
-        padding: 2px 6px;
+    .supplierservicescore-page .bootstrap-table .fixed-table-header th.ssc-operate-col,
+    .supplierservicescore-page .bootstrap-table th[data-field="operate"] {
+        z-index: 6;
+        background: #f5f5f5;
     }
-    .supplierservicescore-page .bootstrap-table .table td .ssc-score-grade-select {
-        display: block !important;
-        width: 96px !important;
-        max-width: 96px;
-        margin: 0 auto !important;
-        float: none !important;
-        height: 28px;
-        padding: 2px 6px;
-        text-align: center;
-        text-align-last: center;
-        -moz-text-align-last: center;
+    .supplierservicescore-page .bootstrap-table .table-striped > tbody > tr:nth-of-type(odd) > td.ssc-operate-col,
+    .supplierservicescore-page .bootstrap-table .table-striped > tbody > tr:nth-of-type(odd) > td[data-field="operate"] {
+        background: #f9f9f9;
     }
-    .supplierservicescore-page .bootstrap-table .table td .ssc-score-grade-select option {
-        text-align: center;
+    .supplierservicescore-page .bootstrap-table .table-hover > tbody > tr:hover > td.ssc-operate-col,
+    .supplierservicescore-page .bootstrap-table .table-hover > tbody > tr:hover > td[data-field="operate"] {
+        background: #f5f5f5;
     }
 </style>
 <div class="panel panel-default panel-intro supplierservicescore-page">
@@ -84,9 +75,6 @@
                     <a href="javascript:;" class="btn btn-success btn-export-review" id="btn-export-review" title="按月份导出供应商评审表">
                         <i class="fa fa-download"></i> 导出供应商评审表
                     </a>
-                    <a href="javascript:;" class="btn btn-info" id="btn-save-final-score" title="保存当前页最终得分">
-                        <i class="fa fa-save"></i> 保存
-                    </a>
                     <label style="margin:0 6px 0 10px;font-weight:normal;" for="export-review-ym">查询月份</label>
                     <input type="month" id="export-review-ym" class="form-control input-sm" style="display:inline-block;width:150px;height:30px;vertical-align:middle;" title="选择月份后自动查询该月评分"/>
                 </span>

+ 66 - 8
application/admin/view/supplierservicescore/monthorders.html

@@ -1,6 +1,6 @@
 <style>
     .ssc-month-orders-wrap {
-        padding: 12px 16px 20px;
+        padding: 12px 16px 8px;
     }
     .ssc-month-orders-head {
         margin-bottom: 12px;
@@ -14,8 +14,15 @@
         font-weight: 600;
         color: #333;
     }
+    .ssc-month-orders-wrap .table-responsive {
+        width: 100%;
+        overflow-x: auto;
+        -webkit-overflow-scrolling: touch;
+        border: 1px solid #eee;
+    }
     .ssc-month-orders-table {
         margin-bottom: 0;
+        min-width: 1280px;
     }
     .ssc-month-orders-table > thead > tr > th,
     .ssc-month-orders-table > tbody > tr > td {
@@ -29,6 +36,28 @@
         word-break: break-all;
         max-width: 220px;
     }
+    /* 操作列固定右侧,左右滚动时始终可见 */
+    .ssc-month-orders-table > thead > tr > th.ssc-operate-col,
+    .ssc-month-orders-table > tbody > tr > td.ssc-operate-col {
+        position: sticky;
+        right: 0;
+        z-index: 3;
+        min-width: 88px;
+        width: 88px;
+        background: #fff;
+        box-shadow: -4px 0 8px rgba(0, 0, 0, 0.06);
+        border-left: 1px solid #e8e8e8 !important;
+    }
+    .ssc-month-orders-table > thead > tr > th.ssc-operate-col {
+        z-index: 4;
+        background: #f5f5f5;
+    }
+    .ssc-month-orders-table.table-striped > tbody > tr:nth-of-type(odd) > td.ssc-operate-col {
+        background: #f9f9f9;
+    }
+    .ssc-month-orders-table.table-hover > tbody > tr:hover > td.ssc-operate-col {
+        background: #f5f5f5;
+    }
     .ssc-month-orders-empty {
         padding: 48px 16px;
         text-align: center;
@@ -61,9 +90,13 @@
                 <th>交货得分</th>
                 <th>状态</th>
                 <th>是否合格</th>
+                <th>交货情况</th>
+                <th>客户是否投诉</th>
+                <th>订单是否中断</th>
+                <th>备注</th>
                 <th>当前进度</th>
                 <th>下发时间</th>
-                <th style="width:80px;">操作</th>
+                <th class="ssc-operate-col">操作</th>
             </tr>
             </thead>
             <tbody>
@@ -74,7 +107,7 @@
                 <td class="ssc-name-cell">{$row.CYJMC|default=''|htmlentities}</td>
                 <td>{$row.CCLBMMC|default=''|htmlentities}</td>
                 <td class="ssc-name-cell">{$row.CGYMC|default=''|htmlentities}</td>
-                <td>{if $row.rank_no}{$row.rank_no}{else/}—{/if}</td>
+                <td>{if $row.rank_no}{$row.rank_no}{/if}</td>
                 <td>{$row.quality_score|default=''}</td>
                 <td>{$row.price_score|default=''}</td>
                 <td>{$row.lead_score|default=''}</td>
@@ -94,6 +127,28 @@
                     <span class="label label-danger">不合格</span>
                     {/if}
                 </td>
+                <td>
+                    {if condition="$row.delivery_status eq '准时'"}
+                    <span class="label label-success">准时</span>
+                    {elseif condition="$row.delivery_status eq '滞后'"/}
+                    <span class="label label-warning">滞后</span>
+                    {/if}
+                </td>
+                <td>
+                    {if condition="$row.customer_complaint eq 1"}
+                    <span class="label label-warning">是</span>
+                    {else/}
+                    否
+                    {/if}
+                </td>
+                <td>
+                    {if condition="$row.order_interrupt eq 1"}
+                    <span class="label label-warning">是</span>
+                    {else/}
+                    否
+                    {/if}
+                </td>
+                <td class="ssc-name-cell">{if $row.inbound_remark}{$row.inbound_remark|htmlentities}{/if}</td>
                 <td>
                     {if condition="$row.progress_text eq '已完结'"}
                     <span class="ssc-progress-done">已完结</span>
@@ -101,16 +156,14 @@
                     {$row.progress_text|default=''|htmlentities}
                     {/if}
                 </td>
-                <td>{if $row.issue_time}{$row.issue_time|htmlentities}{else/}—{/if}</td>
-                <td>
+                <td>{if $row.issue_time}{$row.issue_time|htmlentities}{/if}</td>
+                <td class="ssc-operate-col">
                     {if condition="$row.scydgy_id gt 0"}
                     <a href="javascript:;" class="btn btn-xs btn-info btn-ssc-order-details"
                        data-scydgy-id="{$row.scydgy_id}"
                        title="详情">
                         <i class="fa fa-file-text-o"></i> 详情
                     </a>
-                    {else/}
-                    —
                     {/if}
                 </td>
             </tr>
@@ -119,6 +172,11 @@
         </table>
     </div>
     {else/}
-    <div class="ssc-month-orders-empty">该供应商本月暂无订单评分记录</div>
+    <div class="ssc-month-orders-empty">该供应商本月暂无订单</div>
     {/if}
 </div>
+<div class="layer-footer clearfix">
+    <div class="pull-right">
+        <button type="button" style="margin-right:20px" class="btn btn-default btn-embossed btn-ssc-month-orders-close">关闭</button>
+    </div>
+</div>

+ 2 - 2
application/common/library/ProcuremenOperLog.php

@@ -41,7 +41,7 @@ class ProcuremenOperLog
             'pick_soft_delete'       => '初选删除',
             'save_qty_price'         => '保存数量限价',
             'mark_complete'          => '直接完结',
-            'inbound_score'          => '入库评分',
+            'inbound_score'          => '质量评分',
             'audit_abandon'          => '审批重新下发',
             'archive_abandon'        => '历史重新下发',
         ];
@@ -68,7 +68,7 @@ class ProcuremenOperLog
             'pick_soft_delete'      => ['初选删除', 'pick_soft_delete'],
             'save_qty_price'        => ['保存数量限价', 'save_qty_price'],
             'mark_complete'         => ['直接完结', '完结', 'mark_complete'],
-            'inbound_score'         => ['入库评分', '是否合格', 'inbound_score'],
+            'inbound_score'         => ['质量评分', '入库评分', '是否合格', 'inbound_score'],
             'audit_abandon'         => ['审批重新下发', '重新下发', 'audit_abandon'],
             'archive_abandon'       => ['历史重新下发', '存证重新下发', 'archive_abandon'],
         ];

+ 136 - 59
application/index/controller/Index.php

@@ -16,7 +16,7 @@ use think\Validate;
 /**
  * 手机端:协助明细(purchase_order_detail)验证码 / 账号密码登录 + 列表
  * 普通用户(customer):登录后进报价列表,可填单价/交期
- * 管理员(admin):登录后进「外协入库评分」,对待入库订单选合格/不合格
+ * 管理员(admin):登录后进「质量评分」,对订单评合格/不合格、客户投诉、订单中断
  */
 class Index extends Frontend
 {
@@ -673,7 +673,7 @@ class Index extends Frontend
     }
 
     /**
-     * 按登录身份决定首页:供应商→报价列表;管理员→外协入库评分
+     * 按登录身份决定首页:供应商→报价列表;管理员→质量评分
      *
      * @param array<string, mixed> $user
      * @param string               $redirectPathOrUrl
@@ -2710,7 +2710,7 @@ class Index extends Frontend
 
             return;
         }
-        // 管理员进外协入库评分,不进供应商报价列表
+        // 管理员进质量评分,不进供应商报价列表
         if (!empty($user['is_admin'])) {
             $this->redirect(url('index/index/inboundscore', '', '', true));
 
@@ -3791,7 +3791,7 @@ class Index extends Frontend
     }
 
     /**
-     * 外协入库评分页(仅管理员)
+     * 质量评分页(仅管理员)
      */
     public function inboundscore()
     {
@@ -3824,8 +3824,8 @@ class Index extends Frontend
     }
 
     /**
-     * 外协入库评分列表 JSON(仅管理员;tab=pending|pass|fail
-     * pass/fail 仅返回当前账号操作过的记录
+     * 质量评分列表 JSON(仅管理员;tab=pending|scored
+     * scored 仅返回当前账号操作过的已评分记录
      */
     public function inboundscorelist()
     {
@@ -3833,9 +3833,13 @@ class Index extends Frontend
         $this->mprocEnsureInboundScoreTable();
         $q = trim((string)$this->request->request('q', ''));
         $tab = trim((string)$this->request->request('tab', 'pending'));
-        if (!in_array($tab, ['pending', 'pass', 'fail'], true)) {
+        if (!in_array($tab, ['pending', 'scored', 'pass', 'fail'], true)) {
             $tab = 'pending';
         }
+        // 兼容旧 tab:合格/不合格并入已评分
+        if ($tab === 'pass' || $tab === 'fail') {
+            $tab = 'scored';
+        }
         $adminId = (int)($user['admin_id'] ?? 0);
         $rows = $this->mprocLoadInboundScoreRows($tab, $q, $adminId);
         $this->success('ok', '', [
@@ -3847,7 +3851,8 @@ class Index extends Frontend
     }
 
     /**
-     * 保存外协入库评分(仅管理员,POST:ccydh 或 scydgy_id、result=合格|不合格)
+     * 保存质量评分(仅管理员)
+     * POST:ccydh 或 scydgy_id、result=合格|不合格、delivery_status=准时|滞后、customer_complaint=0|1、order_interrupt=0|1、remark
      * 同一订单号下多道工序一并评分、一并完结(与历史存证一单一行一致)
      */
     public function inboundscoresave()
@@ -3861,7 +3866,26 @@ class Index extends Frontend
         $sid = (int)$this->request->post('scydgy_id', 0);
         $result = trim((string)$this->request->post('result', ''));
         if ($result !== '合格' && $result !== '不合格') {
-            $this->error('请选择合格或不合格');
+            $this->error('请选择是否合格');
+        }
+        $deliveryStatus = trim((string)$this->request->post('delivery_status', ''));
+        if ($deliveryStatus !== '准时' && $deliveryStatus !== '滞后') {
+            $this->error('请选择交货情况');
+        }
+        $customerComplaint = (int)$this->request->post('customer_complaint', -1);
+        $orderInterrupt = (int)$this->request->post('order_interrupt', -1);
+        if ($customerComplaint !== 0 && $customerComplaint !== 1) {
+            $this->error('请选择客户是否投诉');
+        }
+        if ($orderInterrupt !== 0 && $orderInterrupt !== 1) {
+            $this->error('请选择订单是否中断');
+        }
+        $remark = trim((string)$this->request->post('remark', ''));
+        if ($result === '不合格' && $remark === '') {
+            $this->error('请填写不合格原因');
+        }
+        if (mb_strlen($remark) > 500) {
+            $remark = mb_substr($remark, 0, 500);
         }
         $approvedVals = ProcuremenStatus::wflowApprovedValues();
         $approvedIn = implode(',', array_map(static function ($v) {
@@ -3935,16 +3959,20 @@ class Index extends Frontend
                 }
                 $g = trim((string)($po['CGYMC'] ?? ''));
                 $data = [
-                    'scydgy_id'         => $rowSid,
-                    'purchase_order_id' => (int)($po['id'] ?? 0),
-                    'ccydh'             => trim((string)($po['CCYDH'] ?? $ccydh)),
-                    'cyjmc'             => trim((string)($po['CYJMC'] ?? '')),
-                    'cgymc'             => $g,
-                    'company_name'      => trim((string)($po['pick_company_name'] ?? '')),
-                    'result'            => $result,
-                    'admin_id'          => $adminId,
-                    'admin_name'        => $adminName,
-                    'updatetime'        => $now,
+                    'scydgy_id'           => $rowSid,
+                    'purchase_order_id'   => (int)($po['id'] ?? 0),
+                    'ccydh'               => trim((string)($po['CCYDH'] ?? $ccydh)),
+                    'cyjmc'               => trim((string)($po['CYJMC'] ?? '')),
+                    'cgymc'               => $g,
+                    'company_name'        => trim((string)($po['pick_company_name'] ?? '')),
+                    'result'              => $result,
+                    'delivery_status'     => $deliveryStatus,
+                    'customer_complaint'  => $customerComplaint,
+                    'order_interrupt'     => $orderInterrupt,
+                    'remark'              => $remark,
+                    'admin_id'            => $adminId,
+                    'admin_name'          => $adminName,
+                    'updatetime'          => $now,
                 ];
                 $exist = Db::table('purchase_order_inbound_score')->where('scydgy_id', $rowSid)->find();
                 if (is_array($exist) && $exist !== []) {
@@ -3970,10 +3998,17 @@ class Index extends Frontend
             Log::record('inboundscoresave fail: ' . $e->getMessage(), 'error');
             $this->error('保存失败,请稍后重试');
         }
+        $logDetail = '质量评分:是否合格=' . $result
+            . ';交货情况=' . $deliveryStatus
+            . ';客户投诉:' . ($customerComplaint === 1 ? '是' : '否')
+            . ';订单中断:' . ($orderInterrupt === 1 ? '是' : '否');
+        if ($remark !== '') {
+            $logDetail .= ';备注:' . $remark;
+        }
         \app\common\library\ProcuremenOperLog::write(
             $firstSid,
             'inbound_score',
-            '入库评分:' . $result,
+            $logDetail,
             $firstPoId > 0 ? $firstPoId : null,
             [$adminId, $adminName]
         );
@@ -3997,10 +4032,14 @@ class Index extends Frontend
             }
         }
         $this->success('操作成功', '', [
-            'ccydh'  => $ccydh,
-            'result' => $result,
-            'count'  => $saved,
-            'status' => '已完结',
+            'ccydh'              => $ccydh,
+            'result'             => $result,
+            'delivery_status'    => $deliveryStatus,
+            'customer_complaint' => $customerComplaint,
+            'order_interrupt'    => $orderInterrupt,
+            'remark'             => $remark,
+            'count'              => $saved,
+            'status'             => '已完结',
         ]);
     }
 
@@ -4028,13 +4067,9 @@ class Index extends Frontend
         }
         try {
             Db::query('SELECT 1 FROM `purchase_order_inbound_score` LIMIT 1');
-            $ok = true;
-
-            return;
         } catch (\Throwable $e) {
-        }
-        try {
-            Db::execute("CREATE TABLE IF NOT EXISTS `purchase_order_inbound_score` (
+            try {
+                Db::execute("CREATE TABLE IF NOT EXISTS `purchase_order_inbound_score` (
   `id` int unsigned NOT NULL AUTO_INCREMENT,
   `scydgy_id` int NOT NULL DEFAULT 0 COMMENT '工序ID',
   `purchase_order_id` int unsigned NOT NULL DEFAULT 0 COMMENT 'purchase_order.id',
@@ -4043,6 +4078,9 @@ class Index extends Frontend
   `cgymc` varchar(255) NOT NULL DEFAULT '' COMMENT '工序名称',
   `company_name` varchar(255) NOT NULL DEFAULT '' COMMENT '中标供应商',
   `result` varchar(16) NOT NULL DEFAULT '' COMMENT '合格/不合格',
+  `delivery_status` varchar(16) NOT NULL DEFAULT '' COMMENT '交货情况 准时/滞后',
+  `customer_complaint` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '客户是否投诉 1=是',
+  `order_interrupt` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '订单是否中断 1=是',
   `admin_id` int unsigned NOT NULL DEFAULT 0,
   `admin_name` varchar(64) NOT NULL DEFAULT '',
   `remark` varchar(500) NOT NULL DEFAULT '',
@@ -4053,16 +4091,33 @@ class Index extends Frontend
   KEY `idx_ccydh` (`ccydh`),
   KEY `idx_result` (`result`),
   KEY `idx_updatetime` (`updatetime`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='外协入库评分'");
-            $ok = true;
-        } catch (\Throwable $e) {
-            Log::record('mprocEnsureInboundScoreTable: ' . $e->getMessage(), 'error');
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='质量评分'");
+            } catch (\Throwable $e2) {
+                Log::record('mprocEnsureInboundScoreTable: ' . $e2->getMessage(), 'error');
+
+                return;
+            }
         }
+        foreach ([
+            'delivery_status'    => "ADD COLUMN `delivery_status` varchar(16) NOT NULL DEFAULT '' COMMENT '交货情况 准时/滞后' AFTER `result`",
+            'customer_complaint' => "ADD COLUMN `customer_complaint` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '客户是否投诉 1=是' AFTER `delivery_status`",
+            'order_interrupt'    => "ADD COLUMN `order_interrupt` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '订单是否中断 1=是' AFTER `customer_complaint`",
+        ] as $col => $ddl) {
+            try {
+                $cols = Db::query("SHOW COLUMNS FROM `purchase_order_inbound_score` LIKE '{$col}'");
+                if (!is_array($cols) || $cols === []) {
+                    Db::execute("ALTER TABLE `purchase_order_inbound_score` {$ddl}");
+                }
+            } catch (\Throwable $e) {
+                Log::record("mprocEnsureInboundScoreTable {$col}: " . $e->getMessage(), 'error');
+            }
+        }
+        $ok = true;
     }
 
     /**
-     * 外协入库评分列表:按订单号合并(一单一行,工序顿号合并)
-     * tab=pending 待评分;pass/fail 仅当前管理员操作过的合格/不合格
+     * 质量评分列表:按订单号合并(一单一行,工序顿号合并)
+     * tab=pending 待评分;scored 已评分(仅当前管理员操作过的)
      *
      * @return array<int, array<string, mixed>>
      */
@@ -4070,9 +4125,12 @@ class Index extends Frontend
     {
         $this->mprocEnsureInboundScoreTable();
         $keyword = trim($keyword);
-        if (!in_array($tab, ['pending', 'pass', 'fail'], true)) {
+        if (!in_array($tab, ['pending', 'scored', 'pass', 'fail'], true)) {
             $tab = 'pending';
         }
+        if ($tab === 'pass' || $tab === 'fail') {
+            $tab = 'scored';
+        }
         try {
             $approvedVals = ProcuremenStatus::wflowApprovedValues();
             $approvedIn = implode(',', array_map(static function ($v) {
@@ -4112,7 +4170,7 @@ class Index extends Frontend
             try {
                 $scores = Db::table('purchase_order_inbound_score')
                     ->where('scydgy_id', 'in', array_keys($sids))
-                    ->field('scydgy_id,result,admin_id,admin_name,updatetime,createtime')
+                    ->field('scydgy_id,result,delivery_status,customer_complaint,order_interrupt,remark,admin_id,admin_name,updatetime,createtime')
                     ->select();
             } catch (\Throwable $e) {
                 $scores = [];
@@ -4158,6 +4216,10 @@ class Index extends Frontend
                     'results'           => [],
                     'score_admin_ids'   => [],
                     'score_time_raw'    => '',
+                    'customer_complaint'=> 0,
+                    'order_interrupt'   => 0,
+                    'delivery_status'   => '',
+                    'remark'            => '',
                 ];
             }
             $g = &$groups[$ccydh];
@@ -4193,6 +4255,20 @@ class Index extends Frontend
                 if ($aid > 0) {
                     $g['score_admin_ids'][$aid] = true;
                 }
+                if ((int)($sc['customer_complaint'] ?? 0) === 1) {
+                    $g['customer_complaint'] = 1;
+                }
+                if ((int)($sc['order_interrupt'] ?? 0) === 1) {
+                    $g['order_interrupt'] = 1;
+                }
+                $ds = trim((string)($sc['delivery_status'] ?? ''));
+                if ($ds !== '' && $g['delivery_status'] === '') {
+                    $g['delivery_status'] = $ds;
+                }
+                $rm = trim((string)($sc['remark'] ?? ''));
+                if ($rm !== '' && $g['remark'] === '') {
+                    $g['remark'] = $rm;
+                }
                 $st = trim((string)($sc['updatetime'] ?? ''));
                 if ($st === '') {
                     $st = trim((string)($sc['createtime'] ?? ''));
@@ -4236,14 +4312,11 @@ class Index extends Frontend
             if ($tab === 'pending' && $orderResult !== '') {
                 continue;
             }
-            if ($tab === 'pass' && $orderResult !== '合格') {
-                continue;
-            }
-            if ($tab === 'fail' && $orderResult !== '不合格') {
-                continue;
-            }
-            // 合格/不合格:仅当前账号操作过的
-            if ($tab === 'pass' || $tab === 'fail') {
+            if ($tab === 'scored') {
+                if ($orderResult !== '合格' && $orderResult !== '不合格') {
+                    continue;
+                }
+                // 已评分:仅当前账号操作过的
                 if ($adminId <= 0) {
                     continue;
                 }
@@ -4257,23 +4330,27 @@ class Index extends Frontend
                 ? \app\common\library\ProcuremenTime::formatDisplayDateTime($scoreTimeRaw)
                 : '';
             $out[] = [
-                'CCYDH'             => $ccydh,
-                'scydgy_id'         => (int)($g['scydgy_id'] ?? 0),
-                'scydgy_ids'        => array_values(array_unique(array_map('intval', $g['scydgy_ids'] ?? []))),
-                'CYJMC'             => $cyjmc,
-                'CGYMC'             => $gymcList !== [] ? implode('、', $gymcList) : '',
-                'CCLBMMC'           => (string)($g['CCLBMMC'] ?? ''),
-                'pick_company_name' => $company,
-                'pick_time'         => (string)($g['pick_time'] ?? ''),
-                'score_time'        => $scoreTime !== '' ? $scoreTime : $scoreTimeRaw,
-                'score_time_raw'    => $scoreTimeRaw,
-                'process_count'     => (int)($g['process_count'] ?? 0),
-                'result'            => $orderResult,
+                'CCYDH'              => $ccydh,
+                'scydgy_id'          => (int)($g['scydgy_id'] ?? 0),
+                'scydgy_ids'         => array_values(array_unique(array_map('intval', $g['scydgy_ids'] ?? []))),
+                'CYJMC'              => $cyjmc,
+                'CGYMC'              => $gymcList !== [] ? implode('、', $gymcList) : '',
+                'CCLBMMC'            => (string)($g['CCLBMMC'] ?? ''),
+                'pick_company_name'  => $company,
+                'pick_time'          => (string)($g['pick_time'] ?? ''),
+                'score_time'         => $scoreTime !== '' ? $scoreTime : $scoreTimeRaw,
+                'score_time_raw'     => $scoreTimeRaw,
+                'process_count'      => (int)($g['process_count'] ?? 0),
+                'result'             => $orderResult,
+                'customer_complaint' => (int)($g['customer_complaint'] ?? 0),
+                'order_interrupt'    => (int)($g['order_interrupt'] ?? 0),
+                'delivery_status'    => (string)($g['delivery_status'] ?? ''),
+                'remark'             => (string)($g['remark'] ?? ''),
             ];
         }
 
         usort($out, static function ($a, $b) use ($tab) {
-            if ($tab === 'pass' || $tab === 'fail') {
+            if ($tab === 'scored') {
                 $ta = (string)($a['score_time_raw'] ?? '');
                 $tb = (string)($b['score_time_raw'] ?? '');
                 if ($ta !== $tb) {

+ 268 - 68
application/index/view/index/inboundscore.html

@@ -3,7 +3,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
-    <title>外协入库评分</title>
+    <title>质量评分</title>
     <style>
         * { box-sizing: border-box; }
         html, body { margin: 0; height: 100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f5f5; color: #333; }
@@ -71,6 +71,7 @@
         }
         .card .result-tag.pass { background: #27ae60; }
         .card .result-tag.fail { background: #e74c3c; }
+        .card .score-extra { margin-top: 6px; font-size: 12px; color: #666; line-height: 1.5; }
         .card .score-time { margin-top: 6px; font-size: 12px; color: #888; }
         .main {
             flex: 1; min-width: 0; overflow-y: auto; -webkit-overflow-scrolling: touch;
@@ -92,9 +93,8 @@
             font-size: 13px; font-weight: 600; color: #fff; font-family: inherit;
             -webkit-tap-highlight-color: transparent;
         }
-        .btn-pass { background: #27ae60; }
-        .btn-fail { background: #e74c3c; }
-        .btn-pass:disabled, .btn-fail:disabled { opacity: .55; }
+        .btn-score { background: #3c8dbc; }
+        .btn-score:disabled { opacity: .55; }
         .me-panel { padding: 16px 14px 24px; max-width: 480px; margin: 0 auto; }
         .me-card { background: #fff; border-radius: 12px; padding: 18px 16px; box-shadow: 0 1px 6px rgba(0,0,0,.06); }
         .me-row { font-size: 14px; line-height: 1.7; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
@@ -121,33 +121,97 @@
         .tabbar .tabbar-btn.active { color: #3c8dbc; font-weight: 600; border-top-color: #3c8dbc; background: #f8fbfd; }
         .confirm-mask {
             display: none; position: fixed; inset: 0; z-index: 2000;
-            background: rgba(0,0,0,.45); align-items: center; justify-content: center;
-            padding: 24px;
+            background: rgba(0,0,0,.45); align-items: flex-end; justify-content: center;
+            padding: 0 28px;
+            padding-bottom: 0;
         }
         .confirm-mask.show { display: flex; }
         .confirm-box {
-            width: 100%; max-width: 320px; background: #fff; border-radius: 12px;
-            padding: 18px 16px 14px; box-shadow: 0 8px 28px rgba(0,0,0,.18);
+            width: 100%; max-width: 340px; background: #fff;
+            border-radius: 12px 12px 0 0;
+            box-shadow: 0 -4px 28px rgba(0,0,0,.18);
+            max-height: min(90vh, 720px);
+            display: flex; flex-direction: column;
+            overflow: hidden;
+            padding-bottom: var(--safe-bottom);
+        }
+        .confirm-box-body {
+            flex: 1 1 auto;
+            min-height: 0;
+            overflow-y: auto;
+            -webkit-overflow-scrolling: touch;
+            padding: 18px 16px 12px;
         }
         .confirm-box .confirm-ord {
             font-size: 15px; font-weight: 700; color: #222; line-height: 1.45;
             word-break: break-all; margin: 0 0 8px; text-align: left;
         }
         .confirm-box .confirm-meta {
-            font-size: 13px; color: #555; line-height: 1.55; margin: 0 0 14px; text-align: left;
+            font-size: 13px; color: #555; line-height: 1.55; margin: 0 0 12px; text-align: left;
         }
         .confirm-box .confirm-meta b { color: #333; font-weight: 600; }
-        .confirm-box .confirm-msg {
-            text-align: center; font-size: 15px; color: #333; line-height: 1.5;
-            margin: 0 0 14px; font-weight: 600;
+        .score-field {
+            margin: 0 0 12px;
+            display: flex;
+            align-items: center;
+            gap: 10px;
+        }
+        .score-field .label {
+            flex: 0 0 auto;
+            min-width: 7.2em;
+            font-size: 13px; font-weight: 600; color: #333;
+            margin: 0; line-height: 1.35;
+        }
+        .score-field .label .req { color: #e74c3c; margin-left: 2px; }
+        .score-opts {
+            flex: 1 1 auto;
+            display: flex; gap: 8px;
+            min-width: 0;
+        }
+        .score-opt {
+            flex: 1; position: relative;
+        }
+        .score-opt input {
+            position: absolute; opacity: 0; pointer-events: none;
+        }
+        .score-opt span {
+            display: block; text-align: center; padding: 8px 6px; border-radius: 8px;
+            border: 1px solid #d9d9d9; background: #fafafa; font-size: 13px; font-weight: 600;
+            color: #555;
+        }
+        .score-opt input:checked + span {
+            border-color: #3c8dbc; background: #eaf5fb; color: #3c8dbc;
+        }
+        .score-opt.pass input:checked + span { border-color: #27ae60; background: #eafaf1; color: #27ae60; }
+        .score-opt.fail input:checked + span { border-color: #e74c3c; background: #fdecea; color: #e74c3c; }
+        .score-field-remark {
+            flex-direction: column;
+            align-items: stretch;
+            gap: 8px;
+        }
+        .score-field-remark .label {
+            min-width: 0;
+        }
+        .score-remark {
+            width: 100%; min-height: 72px; padding: 10px 12px; border: 1px solid #d9d9d9;
+            border-radius: 8px; font-size: 14px; line-height: 1.5; resize: vertical;
+            font-family: inherit; background: #fff; color: #333;
+        }
+        .score-remark:focus { outline: none; border-color: #3c8dbc; }
+        .confirm-box .confirm-acts {
+            flex: 0 0 auto;
+            display: flex; gap: 10px;
+            padding: 12px 16px;
+            border-top: 1px solid #eee;
+            background: #fff;
         }
-        .confirm-box .confirm-acts { display: flex; gap: 10px; }
         .confirm-box .confirm-acts button {
-            flex: 1; padding: 10px 8px; border: none; border-radius: 8px;
-            font-size: 14px; font-weight: 600; font-family: inherit;
+            flex: 1; padding: 12px 8px; border: none; border-radius: 8px;
+            font-size: 15px; font-weight: 600; font-family: inherit;
         }
         .confirm-cancel { background: #f0f0f0; color: #555; }
         .confirm-ok { background: #3c8dbc; color: #fff; }
+        .confirm-ok:disabled { opacity: .55; }
         .empty { text-align: center; color: #999; padding: 48px 16px; font-size: 14px; }
         .toast {
             position: fixed; left: 50%; bottom: 90px; transform: translateX(-50%);
@@ -162,7 +226,7 @@
 </head>
 <body class="layout-orders">
 <div class="bar">
-    <h1 id="barTitle">外协入库评分</h1>
+    <h1 id="barTitle">质量评分</h1>
     <a href="{:url('index/index/logout')}" class="bar-logout" id="barLogout">退出</a>
 </div>
 
@@ -176,8 +240,7 @@
     <div class="body-wrap">
         <div class="side" id="listCats">
             <button type="button" class="cat active" data-list-tab="pending">待评分</button>
-            <button type="button" class="cat" data-list-tab="pass">合格</button>
-            <button type="button" class="cat" data-list-tab="fail">不合格</button>
+            <button type="button" class="cat" data-list-tab="scored">已评分</button>
         </div>
         <div class="main" id="listMain">
             {if $rows}
@@ -185,12 +248,11 @@
             <div class="card" data-ccydh="{$row.CCYDH|htmlentities}">
                 <div class="ord">{$row.CCYDH|default=''|htmlentities}{if $row.CYJMC} {$row.CYJMC|htmlentities}{/if}</div>
                 <div class="meta">
-                    <div><b>工序:</b>{$row.CGYMC|default='—'|htmlentities}</div>
                     <div><b>供应商:</b>{$row.pick_company_name|default='—'|htmlentities}</div>
+                    <div><b>工序:</b>{$row.CGYMC|default='—'|htmlentities}</div>
                 </div>
                 <div class="actions">
-                    <button type="button" class="btn-pass" data-result="合格">合格</button>
-                    <button type="button" class="btn-fail" data-result="不合格">不合格</button>
+                    <button type="button" class="btn-score">评分</button>
                 </div>
             </div>
             {/volist}
@@ -217,14 +279,71 @@
 </nav>
 
 <div class="toast" id="toast"></div>
-<div class="confirm-mask" id="confirmMask">
+<div class="confirm-mask" id="scoreMask">
     <div class="confirm-box">
-        <div class="confirm-ord" id="confirmOrd"></div>
-        <div class="confirm-meta" id="confirmMeta"></div>
-        <p class="confirm-msg" id="confirmMsg">是否确认?</p>
+        <div class="confirm-box-body">
+            <div class="confirm-ord" id="scoreOrd"></div>
+            <div class="confirm-meta" id="scoreMeta"></div>
+            <div class="score-field">
+                <span class="label">是否合格<span class="req">*</span></span>
+                <div class="score-opts">
+                    <label class="score-opt pass">
+                        <input type="radio" name="score_result" value="合格">
+                        <span>合格</span>
+                    </label>
+                    <label class="score-opt fail">
+                        <input type="radio" name="score_result" value="不合格">
+                        <span>不合格</span>
+                    </label>
+                </div>
+            </div>
+            <div class="score-field">
+                <span class="label">交货情况<span class="req">*</span></span>
+                <div class="score-opts">
+                    <label class="score-opt pass">
+                        <input type="radio" name="delivery_status" value="准时">
+                        <span>准时</span>
+                    </label>
+                    <label class="score-opt fail">
+                        <input type="radio" name="delivery_status" value="滞后">
+                        <span>滞后</span>
+                    </label>
+                </div>
+            </div>
+            <div class="score-field">
+                <span class="label">客户是否投诉<span class="req">*</span></span>
+                <div class="score-opts">
+                    <label class="score-opt">
+                        <input type="radio" name="customer_complaint" value="0" checked>
+                        <span>否</span>
+                    </label>
+                    <label class="score-opt">
+                        <input type="radio" name="customer_complaint" value="1">
+                        <span>是</span>
+                    </label>
+                </div>
+            </div>
+            <div class="score-field">
+                <span class="label">订单是否中断<span class="req">*</span></span>
+                <div class="score-opts">
+                    <label class="score-opt">
+                        <input type="radio" name="order_interrupt" value="0" checked>
+                        <span>否</span>
+                    </label>
+                    <label class="score-opt">
+                        <input type="radio" name="order_interrupt" value="1">
+                        <span>是</span>
+                    </label>
+                </div>
+            </div>
+            <div class="score-field score-field-remark">
+                <span class="label">备注<span class="req" id="remarkReq" style="display:none;">*</span></span>
+                <textarea class="score-remark" id="scoreRemark" maxlength="500" placeholder=""></textarea>
+            </div>
+        </div>
         <div class="confirm-acts">
-            <button type="button" class="confirm-cancel" id="confirmCancel">取消</button>
-            <button type="button" class="confirm-ok" id="confirmOk">确认</button>
+            <button type="button" class="confirm-cancel" id="scoreCancel">取消</button>
+            <button type="button" class="confirm-ok" id="scoreOk">提交</button>
         </div>
     </div>
 </div>
@@ -250,7 +369,6 @@
         } catch (e) {}
     }
 
-    // 退出前清掉本地 token,否则登录页会用 localStorage 自动恢复登录
     document.querySelectorAll('a.btn-me-logout, a.bar-logout').forEach(function (a) {
         a.addEventListener('click', function () {
             clearLocalLoginToken();
@@ -279,11 +397,14 @@
         return no || name || '—';
     }
 
+    function ynText(v) {
+        return parseInt(v, 10) === 1 ? '是' : '否';
+    }
+
     var currentTab = 'pending';
 
     function emptyText(tab) {
-        if (tab === 'pass') return '暂无合格订单';
-        if (tab === 'fail') return '暂无不合格订单';
+        if (tab === 'scored') return '暂无已评分订单';
         return '暂无待评分订单';
     }
 
@@ -300,18 +421,21 @@
             html += '<div class="card" data-ccydh="' + esc(row.CCYDH || '') + '">';
             html += '<div class="ord">' + esc(titleText(row)) + '</div>';
             html += '<div class="meta">';
-            html += '<div><b>工序:</b>' + esc(row.CGYMC || '—') + '</div>';
             html += '<div><b>供应商:</b>' + esc(row.pick_company_name || '—') + '</div>';
+            html += '<div><b>工序:</b>' + esc(row.CGYMC || '—') + '</div>';
             html += '</div>';
             if (pending) {
-                html += '<div class="actions">'
-                    + '<button type="button" class="btn-pass" data-result="合格">合格</button>'
-                    + '<button type="button" class="btn-fail" data-result="不合格">不合格</button>'
-                    + '</div>';
+                html += '<div class="actions"><button type="button" class="btn-score">评分</button></div>';
             } else {
                 var res = String(row.result || '');
                 var cls = res === '不合格' ? 'fail' : 'pass';
                 html += '<span class="result-tag ' + cls + '">' + esc(res || '—') + '</span>';
+                html += '<div class="score-extra">交货情况:' + esc(row.delivery_status || '—')
+                    + ' 客户投诉:' + esc(ynText(row.customer_complaint))
+                    + ' 订单中断:' + esc(ynText(row.order_interrupt)) + '</div>';
+                if (row.remark) {
+                    html += '<div class="score-extra">备注:' + esc(row.remark) + '</div>';
+                }
                 if (row.score_time) {
                     html += '<div class="score-time">评定时间:' + esc(row.score_time) + '</div>';
                 }
@@ -398,22 +522,51 @@
             document.body.className = 'layout-orders';
             paneOrders.style.display = 'flex';
             paneMe.style.display = 'none';
-            if (barTitle) barTitle.textContent = '外协入库评分';
+            if (barTitle) barTitle.textContent = '质量评分';
         }
     });
 
-    var confirmMask = document.getElementById('confirmMask');
-    var confirmOrd = document.getElementById('confirmOrd');
-    var confirmMeta = document.getElementById('confirmMeta');
-    var confirmMsg = document.getElementById('confirmMsg');
-    var confirmPending = null;
+    var scoreMask = document.getElementById('scoreMask');
+    var scoreOrd = document.getElementById('scoreOrd');
+    var scoreMeta = document.getElementById('scoreMeta');
+    var scorePendingCard = null;
 
     function textOf(el) {
         return el ? String(el.textContent || '').replace(/\s+/g, ' ').trim() : '';
     }
 
-    function askConfirm(card, result, onOk) {
-        confirmPending = onOk || null;
+    function checkedVal(name) {
+        var el = document.querySelector('input[name="' + name + '"]:checked');
+        return el ? String(el.value || '') : '';
+    }
+
+    function syncRemarkRequired() {
+        var result = checkedVal('score_result');
+        var need = result === '不合格';
+        var reqEl = document.getElementById('remarkReq');
+        var remarkEl = document.getElementById('scoreRemark');
+        if (reqEl) reqEl.style.display = need ? '' : 'none';
+        if (remarkEl) {
+            remarkEl.placeholder = need ? '请说明不合格原因' : '';
+        }
+    }
+
+    function resetScoreForm() {
+        var resultRadios = document.querySelectorAll('input[name="score_result"]');
+        resultRadios.forEach(function (r) { r.checked = false; });
+        var deliveryRadios = document.querySelectorAll('input[name="delivery_status"]');
+        deliveryRadios.forEach(function (r) { r.checked = false; });
+        var complaintNo = document.querySelector('input[name="customer_complaint"][value="0"]');
+        var interruptNo = document.querySelector('input[name="order_interrupt"][value="0"]');
+        if (complaintNo) complaintNo.checked = true;
+        if (interruptNo) interruptNo.checked = true;
+        var remarkEl = document.getElementById('scoreRemark');
+        if (remarkEl) remarkEl.value = '';
+        syncRemarkRequired();
+    }
+
+    function openScoreForm(card) {
+        scorePendingCard = card || null;
         var ord = textOf(card.querySelector('.ord')) || '—';
         var metaLines = card.querySelectorAll('.meta > div');
         var gymc = '—';
@@ -423,34 +576,44 @@
             if (t.indexOf('工序:') === 0) gymc = t.slice(3).trim() || '—';
             if (t.indexOf('供应商:') === 0) company = t.slice(4).trim() || '—';
         }
-        if (confirmOrd) confirmOrd.textContent = ord;
-        if (confirmMeta) {
-            confirmMeta.innerHTML = '<div><b>工序:</b>' + esc(gymc) + '</div>'
-                + '<div><b>供应商:</b>' + esc(company) + '</div>';
+        if (scoreOrd) scoreOrd.textContent = ord;
+        if (scoreMeta) {
+            scoreMeta.innerHTML = '<div><b>供应商:</b>' + esc(company) + '</div>'
+                + '<div><b>工序:</b>' + esc(gymc) + '</div>';
         }
-        if (confirmMsg) confirmMsg.textContent = '是否确认评定为「' + result + '」?';
-        if (confirmMask) confirmMask.classList.add('show');
+        resetScoreForm();
+        if (scoreMask) scoreMask.classList.add('show');
     }
-    function closeConfirm() {
-        confirmPending = null;
-        if (confirmMask) confirmMask.classList.remove('show');
+
+    function closeScoreForm() {
+        scorePendingCard = null;
+        if (scoreMask) scoreMask.classList.remove('show');
     }
-    document.getElementById('confirmCancel').addEventListener('click', closeConfirm);
-    document.getElementById('confirmOk').addEventListener('click', function () {
-        var fn = confirmPending;
-        closeConfirm();
-        if (typeof fn === 'function') fn();
+
+    document.getElementById('scoreCancel').addEventListener('click', closeScoreForm);
+    scoreMask.addEventListener('click', function (e) {
+        if (e.target === scoreMask) closeScoreForm();
     });
-    confirmMask.addEventListener('click', function (e) {
-        if (e.target === confirmMask) closeConfirm();
+    document.querySelectorAll('input[name="score_result"]').forEach(function (r) {
+        r.addEventListener('change', syncRemarkRequired);
     });
 
-    function submitScore(card, result) {
+    function submitScore(card, payload) {
         var ccydh = card.getAttribute('data-ccydh');
-        if (!ccydh || !result) return;
+        if (!ccydh || !payload || !payload.result) return;
         var buttons = card.querySelectorAll('button');
         buttons.forEach(function (b) { b.disabled = true; });
-        postForm(saveUrl, { ccydh: ccydh, result: result }).then(function (ret) {
+        var okBtn = document.getElementById('scoreOk');
+        if (okBtn) okBtn.disabled = true;
+        postForm(saveUrl, {
+            ccydh: ccydh,
+            result: payload.result,
+            delivery_status: payload.delivery_status,
+            customer_complaint: payload.customer_complaint,
+            order_interrupt: payload.order_interrupt,
+            remark: payload.remark || ''
+        }).then(function (ret) {
+            if (okBtn) okBtn.disabled = false;
             if (!ret || (ret.code !== 1 && ret.code !== '1')) {
                 buttons.forEach(function (b) { b.disabled = false; });
                 if (ret && /登录/.test(String(ret.msg || ''))) {
@@ -460,27 +623,64 @@
                 showToast(ret && ret.msg ? ret.msg : '保存失败', 'err');
                 return;
             }
+            closeScoreForm();
             showToast(ret.msg || '已保存', 'ok');
             card.parentNode && card.parentNode.removeChild(card);
             if (!document.querySelector('#listMain .card')) {
                 document.getElementById('listMain').innerHTML = '<div class="empty">暂无待评分订单</div>';
             }
         }).catch(function () {
+            if (okBtn) okBtn.disabled = false;
             buttons.forEach(function (b) { b.disabled = false; });
             showToast('网络错误', 'err');
         });
     }
 
+    document.getElementById('scoreOk').addEventListener('click', function () {
+        var card = scorePendingCard;
+        if (!card) return;
+        var result = checkedVal('score_result');
+        if (!result) {
+            showToast('请选择是否合格', 'err');
+            return;
+        }
+        var deliveryStatus = checkedVal('delivery_status');
+        if (deliveryStatus !== '准时' && deliveryStatus !== '滞后') {
+            showToast('请选择交货情况', 'err');
+            return;
+        }
+        var complaint = checkedVal('customer_complaint');
+        var interrupt = checkedVal('order_interrupt');
+        if (complaint !== '0' && complaint !== '1') {
+            showToast('请选择客户是否投诉', 'err');
+            return;
+        }
+        if (interrupt !== '0' && interrupt !== '1') {
+            showToast('请选择订单是否中断', 'err');
+            return;
+        }
+        var remark = ((document.getElementById('scoreRemark') || {}).value || '').trim();
+        if (result === '不合格' && !remark) {
+            showToast('请填写不合格原因', 'err');
+            var remarkEl = document.getElementById('scoreRemark');
+            if (remarkEl) remarkEl.focus();
+            return;
+        }
+        submitScore(card, {
+            result: result,
+            delivery_status: deliveryStatus,
+            customer_complaint: complaint,
+            order_interrupt: interrupt,
+            remark: remark
+        });
+    });
+
     document.getElementById('listMain').addEventListener('click', function (e) {
-        var btn = e.target.closest('button[data-result]');
+        var btn = e.target.closest('button.btn-score');
         if (!btn) return;
         var card = btn.closest('.card');
         if (!card) return;
-        var result = btn.getAttribute('data-result');
-        if (!result) return;
-        askConfirm(card, result, function () {
-            submitScore(card, result);
-        });
+        openScoreForm(card);
     });
 })();
 </script>

+ 100 - 78
public/assets/js/backend/procuremen.js

@@ -2926,38 +2926,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         return s === '' ? '' : rfqEscHtml(s);
                     }
                 },
-                {
-                    field: 'progress_text',
-                    title: '状态',
-                    operate: false,
-                    width: 100,
-                    align: 'center',
-                    halign: 'center',
-                    formatter: function (value) {
-                        var v = value == null ? '' : String(value);
-                        if (v === '已完结' || v === '已通知业务员' || v === '已通知') {
-                            return '<span class="label label-success">' + v + '</span>';
-                        }
-                        if (v === '供应商已报价') {
-                            return '<span class="label label-primary">' + v + '</span>';
-                        }
-                        if (v === '待报价' || v === '待询价' || v === '待通知') {
-                            return '<span class="label label-warning">' + v + '</span>';
-                        }
-                        if (v === '待入库评分' || v === '已下发待确认') {
-                            return '<span class="label label-info">' + v + '</span>';
-                        }
-                        if (v === '待采购确认' || v === '待确认供应商') {
-                            return '<span class="label label-warning">' + v + '</span>';
-                        }
-                        return v ? '<span class="label label-default">' + v + '</span>' : '';
-                    }
-                },
-                {field: 'CDW', title: '单位', operate: 'LIKE', width: 70, align: 'center', halign: 'center', valign: 'middle', class: 'procuremen-rfq-num-col'},
-                {field: 'CDF', title: '订法', operate: 'LIKE', width: 80, align: 'center', halign: 'center', valign: 'middle', class: 'procuremen-rfq-num-col'},
                 {
                     field: 'supplier_name',
-                    title: '选择供应商',
+                    title: '供应商',
                     // 计算字段,搜索请用备注/编号;避免 filter 打到不存在的列导致整表为空
                     operate: false,
                     width: 180,
@@ -2967,8 +2938,6 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         return rfqMultiLinesCell(v, this.width || 180);
                     }
                 },
-                {field: 'MBZ', title: '备注', operate: 'LIKE', width: 120, class: 'autocontent', formatter: Table.api.formatter.content},
-                {field: 'cywyxm', title: '需求发起人', operate: 'LIKE', width: 90, align: 'center', halign: 'center'},
                 {
                     field: 'notify_salesman',
                     title: '通知业务员',
@@ -2980,6 +2949,10 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         return rfqMultiLinesCell(v, this.width || 140);
                     }
                 },
+                {field: 'CDW', title: '单位', operate: 'LIKE', width: 70, align: 'center', halign: 'center', valign: 'middle', class: 'procuremen-rfq-num-col'},
+                {field: 'CDF', title: '订法', operate: 'LIKE', width: 80, align: 'center', halign: 'center', valign: 'middle', class: 'procuremen-rfq-num-col'},
+                {field: 'MBZ', title: '备注', operate: 'LIKE', width: 140, class: 'autocontent', formatter: Table.api.formatter.content},
+                {field: 'cywyxm', title: '需求发起人', operate: 'LIKE', width: 90, align: 'center', halign: 'center'},
                 {
                     field: 'createtime',
                     title: '需求下发时间',
@@ -3005,7 +2978,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 {
                     field: 'operate',
                     title: '操作',
-                    width: 200,
+                    width: 280,
                     align: 'center',
                     halign: 'center',
                     operate: false,
@@ -3017,15 +2990,16 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                             return '';
                         }
                         var progress = String(row.progress_text || '');
-                        // 询价 → 查看报价(弹窗内再通知业务员)
                         var canIssue = progress !== '已完结' && progress !== '待入库评分' && progress !== '待采购确认';
                         var html = '<span class="procuremen-rfq-op-btns">';
                         if (canIssue) {
                             html += '<a href="javascript:;" class="btn btn-xs btn-info btn-rfq-issue" data-scydgy-id="'
-                                + rfqEscAttr(sid) + '" title="向选择的供应商邮箱通知询价">下发询价</a>';
+                                + rfqEscAttr(sid) + '">询价</a>';
                         }
                         html += '<a href="javascript:;" class="btn btn-xs btn-warning btn-rfq-quotes" data-scydgy-id="'
-                            + rfqEscAttr(sid) + '" title="查看供应商报价">查看报价</a>';
+                            + rfqEscAttr(sid) + '">查看报价</a>';
+                        html += '<a href="javascript:;" class="btn btn-xs btn-success btn-rfq-notify-salesman" data-scydgy-id="'
+                            + rfqEscAttr(sid) + '">通知业务员</a>';
                         html += '</span>';
                         return html;
                     },
@@ -3643,14 +3617,17 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         area: ['920px', '520px'],
                         shadeClose: true,
                         content: html,
-                        // 第一个主题色按钮=通知业务员;第二个默认为白底关闭
-                        btn: ['通知业务员', '关闭'],
-                        yes: function () {
-                            // 保留「查看报价」弹窗,再叠开通知业务员
-                            rfqOpenNotifySalesman(sid);
-                            return false;
+                        btn: ['关闭'],
+                        btnAlign: 'r',
+                        success: function (layero) {
+                            layero.find('.layui-layer-btn0').css({
+                                background: '#fff',
+                                color: '#333',
+                                border: '1px solid #dedede',
+                                'border-radius': '2px'
+                            });
                         },
-                        btn2: function (index) {
+                        yes: function (index) {
                             Layer.close(index);
                         }
                     });
@@ -3658,6 +3635,57 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 });
             });
 
+            function rfqBuildNotifyHtml(data, sid) {
+                var list = (data && data.list) ? data.list : [];
+                var html = '<div class="procuremen-rfq-notify-wrap" data-scydgy-id="' + rfqEscAttr(sid) + '">'
+                    + '<input type="text" class="form-control input-sm procuremen-rfq-notify-search" placeholder="搜索业务员" style="margin-bottom:10px;"/>';
+                if (!list.length) {
+                    html += '<div class="procuremen-rfq-notify-empty" style="padding:60px 12px;">暂无可选业务员</div>';
+                } else {
+                    html += '<div class="procuremen-rfq-notify-list" style="max-height:420px;overflow:auto;border:1px solid #eee;">'
+                        + '<table class="table table-bordered table-condensed" style="margin-bottom:0;">'
+                        + '<thead><tr>'
+                        + '<th style="width:36px;" class="text-center"></th>'
+                        + '<th style="width:88px;">业务员</th>'
+                        + '<th>邮箱</th>'
+                        + '<th style="width:110px;">手机号</th>'
+                        + '</tr></thead><tbody>';
+                    list.forEach(function (item) {
+                        var name = item && item.name != null ? String(item.name).trim() : '';
+                        var email = item && item.email != null ? String(item.email).trim() : '';
+                        var mobile = item && item.mobile != null ? String(item.mobile).trim() : '';
+                        var canSelect = item && parseInt(item.can_select, 10) === 1;
+                        var checked = item && parseInt(item.checked, 10) === 1;
+                        var emailShow = email ? maskProcuremenEmail(email) : '';
+                        var mobileShow = mobile ? maskProcuremenPhone(mobile) : '';
+                        html += '<tr class="procuremen-rfq-notify-item' + (canSelect ? '' : ' text-muted') + '">'
+                            + '<td class="text-center">'
+                            + '<input type="checkbox" class="rfq-notify-sm-cb" value="' + rfqEscAttr(name) + '"'
+                            + (canSelect ? '' : ' disabled')
+                            + (canSelect && checked ? ' checked' : '')
+                            + ' style="margin:0;"/>'
+                            + '</td>'
+                            + '<td>' + rfqEscHtml(name) + '</td>'
+                            + '<td style="word-break:break-all;">'
+                            + (emailShow ? rfqEscHtml(emailShow) : '<span class="text-muted">未填写</span>')
+                            + '</td>'
+                            + '<td>'
+                            + (mobileShow ? rfqEscHtml(mobileShow) : '<span class="text-muted">未填写</span>')
+                            + '</td>'
+                            + '</tr>';
+                    });
+                    html += '</tbody></table></div>';
+                }
+                html += '</div>';
+                var hasSendable = false;
+                list.forEach(function (item) {
+                    if (item && parseInt(item.can_select, 10) === 1) {
+                        hasSendable = true;
+                    }
+                });
+                return {html: html, list: list, hasSendable: hasSendable};
+            }
+
             function rfqOpenNotifySalesman(sid) {
                 if (!sid) {
                     return;
@@ -3671,53 +3699,47 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         Layer.msg('需先询价且供应商报价后才能通知业务员');
                         return false;
                     }
-                    var list = (data && data.list) ? data.list : [];
-                    var html;
-                    if (!list.length) {
-                        html = '<div class="procuremen-rfq-notify-wrap"><div class="procuremen-rfq-notify-empty">未指定通知业务员</div></div>';
-                    } else {
-                        html = '<div class="procuremen-rfq-notify-wrap" data-scydgy-id="' + rfqEscAttr(sid) + '">'
-                            + '<table class="table table-bordered table-condensed" style="margin-bottom:0;">'
-                            + '<thead><tr><th style="width:36%;">业务员</th><th>邮箱</th></tr></thead><tbody>';
-                        list.forEach(function (item) {
-                            var name = item && item.name != null ? String(item.name) : '';
-                            var email = item && item.email != null ? String(item.email) : '';
-                            var emailShow = email ? maskProcuremenEmail(email) : '';
-                            html += '<tr>'
-                                + '<td>' + rfqEscHtml(name) + '</td>'
-                                + '<td style="word-break:break-all;">' + (emailShow ? rfqEscHtml(emailShow) : '<span class="text-muted">未填写邮箱</span>') + '</td>'
-                                + '</tr>';
-                        });
-                        html += '</tbody></table></div>';
-                    }
-                    var hasSendable = false;
-                    list.forEach(function (item) {
-                        var email = item && item.email != null ? String(item.email).trim() : '';
-                        if (email) {
-                            hasSendable = true;
-                        }
-                    });
+                    var built = rfqBuildNotifyHtml(data, sid);
+                    var hasSendable = built.hasSendable;
                     Layer.open({
                         type: 1,
                         title: '通知业务员',
-                        area: ['560px', '380px'],
+                        area: ['720px', '620px'],
                         shadeClose: true,
-                        content: html,
-                        btn: ['邮箱通知', '关闭'],
-                        yes: function (index) {
-                            if (!list.length) {
-                                Layer.msg('未指定通知业务员');
+                        content: built.html,
+                        btn: ['确认', '关闭'],
+                        success: function (layero) {
+                            var $search = $(layero).find('.procuremen-rfq-notify-search');
+                            var $items = $(layero).find('.procuremen-rfq-notify-item');
+                            $search.off('input.rfqNotifySmSearch').on('input.rfqNotifySmSearch', function () {
+                                var kw = $.trim($(this).val() || '').toLowerCase();
+                                $items.each(function () {
+                                    var text = $.trim($(this).text() || '').toLowerCase();
+                                    $(this).toggle(!kw || text.indexOf(kw) !== -1);
+                                });
+                            });
+                        },
+                        yes: function (index, layero) {
+                            if (!hasSendable) {
+                                Layer.msg('暂无带邮箱的业务员可通知');
                                 return false;
                             }
-                            if (!hasSendable) {
-                                Layer.msg('业务员未填写有效邮箱');
+                            var selected = [];
+                            $(layero).find('.rfq-notify-sm-cb:checked:not(:disabled)').each(function () {
+                                var v = $.trim($(this).val() || '');
+                                if (v) {
+                                    selected.push(v);
+                                }
+                            });
+                            if (!selected.length) {
+                                Layer.msg('请至少勾选一位业务员');
                                 return false;
                             }
                             Fast.api.ajax({
                                 url: 'procuremen/rfqnotifysalesman',
                                 data: {
                                     scydgy_id: sid,
-                                    notify_all: 1
+                                    salesmen_json: JSON.stringify(selected)
                                 },
                                 loading: true
                             }, function () {

+ 49 - 207
public/assets/js/backend/supplierservicescore.js

@@ -40,13 +40,6 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     .replace(/</g, '&lt;')
                     .replace(/>/g, '&gt;');
             }
-            function scoreInputHtml(company, ym, cssClass, val) {
-                var show = (val == null || val === '') ? '' : String(val);
-                return '<input type="text" inputmode="decimal" class="form-control input-sm ' + cssClass + '"'
-                    + ' data-company="' + escAttr(company) + '"'
-                    + ' data-ym="' + escAttr(ym || '') + '"'
-                    + ' value="' + escAttr(show) + '"/>';
-            }
             function scoreCellVal(row, textKey, valueKey) {
                 var text = row[textKey];
                 if (text != null && String(text).trim() !== '') {
@@ -107,27 +100,37 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 });
                 return any ? Math.round(sum * 100) / 100 : null;
             }
-            function findScoreRow($el) {
-                return $el.closest('tr');
+            function displayScore(row, textKey, valueKey) {
+                return scoreCellVal(row, textKey, valueKey);
             }
-            function recalcRowFromInputs($tr, opts) {
-                opts = opts || {};
-                if (!$tr || !$tr.length) {
-                    return;
-                }
-                var q = parseScoreInput($tr.find('input.ssc-quality-score-input').val());
-                var p = parseScoreInput($tr.find('input.ssc-price-score-input').val());
-                var d = parseScoreInput($tr.find('input.ssc-delivery-score-input').val());
-                var v = parseScoreInput($tr.find('input.ssc-value-added-score-input').val());
-                var final = sumFourScores(q, p, d, v);
-                var $final = $tr.find('input.ssc-final-score-input');
-                var $grade = $tr.find('select.ssc-score-grade-select');
-                if ($final.length && opts.updateFinal !== false) {
-                    $final.val(final == null ? '' : formatScoreNum(final));
-                }
-                if ($grade.length && opts.updateGrade !== false) {
-                    $grade.val(gradeFromFinal(final));
-                }
+            function displayFinalScore(row) {
+                var val = scoreCellVal(row, 'final_score_text', 'final_score');
+                if (val !== '') {
+                    return val;
+                }
+                var sum = sumFourScores(
+                    parseScoreInput(scoreCellVal(row, 'quality_score_text', 'quality_score')),
+                    parseScoreInput(scoreCellVal(row, 'price_score_text', 'price_score')),
+                    parseScoreInput(scoreCellVal(row, 'delivery_score_text', 'delivery_score')),
+                    parseScoreInput(scoreCellVal(row, 'value_added_score_text', 'value_added_score'))
+                );
+                return sum == null ? '' : formatScoreNum(sum);
+            }
+            function displayGrade(row) {
+                var cur = String(row.score_grade || '').trim().toUpperCase();
+                if (['A', 'B', 'C', 'D'].indexOf(cur) >= 0) {
+                    return cur;
+                }
+                var finalVal = parseScoreInput(scoreCellVal(row, 'final_score_text', 'final_score'));
+                if (finalVal == null) {
+                    finalVal = sumFourScores(
+                        parseScoreInput(scoreCellVal(row, 'quality_score_text', 'quality_score')),
+                        parseScoreInput(scoreCellVal(row, 'price_score_text', 'price_score')),
+                        parseScoreInput(scoreCellVal(row, 'delivery_score_text', 'delivery_score')),
+                        parseScoreInput(scoreCellVal(row, 'value_added_score_text', 'value_added_score'))
+                    );
+                }
+                return gradeFromFinal(finalVal);
             }
             function rowYm(row) {
                 var v = String(row && row.ym ? row.ym : '').trim();
@@ -136,85 +139,6 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 }
                 return resolveYm();
             }
-            function collectFinalRows() {
-                var rows = [];
-                var byKey = {};
-                function ensure(company, ym) {
-                    var key = String(ym || '') + '|' + company;
-                    if (!byKey[key]) {
-                        byKey[key] = {
-                            company_name: company,
-                            ym: ym || '',
-                            quality_score: '',
-                            price_score: '',
-                            delivery_score: '',
-                            value_added_score: '',
-                            final_score: '',
-                            score_grade: ''
-                        };
-                    }
-                    return byKey[key];
-                }
-                function pickYm($el) {
-                    var ym = String($el.data('ym') || '').trim();
-                    if (/^\d{4}-\d{2}$/.test(ym)) {
-                        return ym;
-                    }
-                    return resolveYm();
-                }
-                $('#table').find('input.ssc-quality-score-input').each(function () {
-                    var $el = $(this);
-                    var company = String($el.data('company') || '').trim();
-                    if (!company) {
-                        return;
-                    }
-                    ensure(company, pickYm($el)).quality_score = String($el.val() || '').trim();
-                });
-                $('#table').find('input.ssc-price-score-input').each(function () {
-                    var $el = $(this);
-                    var company = String($el.data('company') || '').trim();
-                    if (!company) {
-                        return;
-                    }
-                    ensure(company, pickYm($el)).price_score = String($el.val() || '').trim();
-                });
-                $('#table').find('input.ssc-delivery-score-input').each(function () {
-                    var $el = $(this);
-                    var company = String($el.data('company') || '').trim();
-                    if (!company) {
-                        return;
-                    }
-                    ensure(company, pickYm($el)).delivery_score = String($el.val() || '').trim();
-                });
-                $('#table').find('input.ssc-value-added-score-input').each(function () {
-                    var $el = $(this);
-                    var company = String($el.data('company') || '').trim();
-                    if (!company) {
-                        return;
-                    }
-                    ensure(company, pickYm($el)).value_added_score = String($el.val() || '').trim();
-                });
-                $('#table').find('input.ssc-final-score-input').each(function () {
-                    var $el = $(this);
-                    var company = String($el.data('company') || '').trim();
-                    if (!company) {
-                        return;
-                    }
-                    ensure(company, pickYm($el)).final_score = String($el.val() || '').trim();
-                });
-                $('#table').find('select.ssc-score-grade-select').each(function () {
-                    var $el = $(this);
-                    var company = String($el.data('company') || '').trim();
-                    if (!company) {
-                        return;
-                    }
-                    ensure(company, pickYm($el)).score_grade = String($el.val() || '').trim().toUpperCase();
-                });
-                $.each(byKey, function (_, row) {
-                    rows.push(row);
-                });
-                return rows;
-            }
 
             var $ymInput = $('#export-review-ym');
             if ($ymInput.length && !normalizeYm($ymInput.val())) {
@@ -290,8 +214,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         class: 'text-center',
                         width: 120,
                         formatter: function (value, row) {
-                            var company = row.company_name || '';
-                            return scoreInputHtml(company, rowYm(row), 'ssc-quality-score-input', scoreCellVal(row, 'quality_score_text', 'quality_score'));
+                            return displayScore(row, 'quality_score_text', 'quality_score');
                         }
                     },
                     {
@@ -304,8 +227,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         class: 'text-center',
                         width: 120,
                         formatter: function (value, row) {
-                            var company = row.company_name || '';
-                            return scoreInputHtml(company, rowYm(row), 'ssc-price-score-input', scoreCellVal(row, 'price_score_text', 'price_score'));
+                            return displayScore(row, 'price_score_text', 'price_score');
                         }
                     },
                     {
@@ -318,8 +240,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         class: 'text-center',
                         width: 120,
                         formatter: function (value, row) {
-                            var company = row.company_name || '';
-                            return scoreInputHtml(company, rowYm(row), 'ssc-delivery-score-input', scoreCellVal(row, 'delivery_score_text', 'delivery_score'));
+                            return displayScore(row, 'delivery_score_text', 'delivery_score');
                         }
                     },
                     {
@@ -332,8 +253,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         class: 'text-center',
                         width: 120,
                         formatter: function (value, row) {
-                            var company = row.company_name || '';
-                            return scoreInputHtml(company, rowYm(row), 'ssc-value-added-score-input', scoreCellVal(row, 'value_added_score_text', 'value_added_score'));
+                            return displayScore(row, 'value_added_score_text', 'value_added_score');
                         }
                     },
                     {
@@ -346,18 +266,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         class: 'text-center',
                         width: 130,
                         formatter: function (value, row) {
-                            var company = row.company_name || '';
-                            var val = scoreCellVal(row, 'final_score_text', 'final_score');
-                            if (val === '') {
-                                var sum = sumFourScores(
-                                    parseScoreInput(scoreCellVal(row, 'quality_score_text', 'quality_score')),
-                                    parseScoreInput(scoreCellVal(row, 'price_score_text', 'price_score')),
-                                    parseScoreInput(scoreCellVal(row, 'delivery_score_text', 'delivery_score')),
-                                    parseScoreInput(scoreCellVal(row, 'value_added_score_text', 'value_added_score'))
-                                );
-                                val = sum == null ? '' : formatScoreNum(sum);
-                            }
-                            return scoreInputHtml(company, rowYm(row), 'ssc-final-score-input', val);
+                            return displayFinalScore(row);
                         }
                     },
                     {
@@ -370,37 +279,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         class: 'text-center',
                         width: 120,
                         formatter: function (value, row) {
-                            var company = row.company_name || '';
-                            var ym = rowYm(row);
-                            var cur = String(row.score_grade || '').trim().toUpperCase();
-                            if (['A', 'B', 'C', 'D'].indexOf(cur) < 0) {
-                                var finalVal = parseScoreInput(scoreCellVal(row, 'final_score_text', 'final_score'));
-                                if (finalVal == null) {
-                                    finalVal = sumFourScores(
-                                        parseScoreInput(scoreCellVal(row, 'quality_score_text', 'quality_score')),
-                                        parseScoreInput(scoreCellVal(row, 'price_score_text', 'price_score')),
-                                        parseScoreInput(scoreCellVal(row, 'delivery_score_text', 'delivery_score')),
-                                        parseScoreInput(scoreCellVal(row, 'value_added_score_text', 'value_added_score'))
-                                    );
-                                }
-                                cur = gradeFromFinal(finalVal);
-                            }
-                            var opts = [
-                                {v: '', t: '请选择'},
-                                {v: 'A', t: 'A'},
-                                {v: 'B', t: 'B'},
-                                {v: 'C', t: 'C'},
-                                {v: 'D', t: 'D'}
-                            ];
-                            var html = '<select class="form-control input-sm ssc-score-grade-select"'
-                                + ' data-company="' + escAttr(company) + '"'
-                                + ' data-ym="' + escAttr(ym) + '">';
-                            opts.forEach(function (o) {
-                                html += '<option value="' + o.v + '"' + (cur === o.v ? ' selected' : '') + '>'
-                                    + o.t + '</option>';
-                            });
-                            html += '</select>';
-                            return html;
+                            return displayGrade(row);
                         }
                     },
                     {
@@ -434,7 +313,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                         align: 'center',
                         halign: 'center',
                         valign: 'middle',
-                        class: 'text-center',
+                        class: 'text-center ssc-operate-col',
                         width: 130,
                         formatter: function (value, row) {
                             var company = String(row && row.company_name ? row.company_name : '').trim();
@@ -466,19 +345,6 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
             });
             Table.api.bindevent(table);
 
-            // 四项分数变动 → 自动汇总最终得分并定级
-            $(document).off('input.sscAutoScore change.sscAutoScore', '#table')
-                .on('input.sscAutoScore change.sscAutoScore', '#table input.ssc-quality-score-input, #table input.ssc-price-score-input, #table input.ssc-delivery-score-input, #table input.ssc-value-added-score-input', function () {
-                    recalcRowFromInputs(findScoreRow($(this)));
-                });
-            // 手工改最终得分 → 同步定级(仍可再手动改等级)
-            $(document).off('input.sscFinalGrade change.sscFinalGrade', '#table')
-                .on('input.sscFinalGrade change.sscFinalGrade', '#table input.ssc-final-score-input', function () {
-                    var $tr = findScoreRow($(this));
-                    var final = parseScoreInput($(this).val());
-                    $tr.find('select.ssc-score-grade-select').val(gradeFromFinal(final));
-                });
-
             $(document).off('click.sscMonthOrders', '.btn-ssc-month-orders').on('click.sscMonthOrders', '.btn-ssc-month-orders', function (e) {
                 e.preventDefault();
                 e.stopPropagation();
@@ -496,7 +362,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                     + '?ym=' + encodeURIComponent(ym)
                     + '&company_name=' + encodeURIComponent(company);
                 var title = '本月订单明细';
-                Backend.api.open(url, title, {area: ['92%', '82%'], maxmin: true});
+                Backend.api.open(url, title, {area: ['100%', '100%'], maxmin: true});
                 return false;
             });
 
@@ -522,47 +388,23 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
                 window.location.href = Fast.api.fixurl('supplierservicescore/export') + '?ym=' + encodeURIComponent(ym);
                 return false;
             });
-
-            $(document).off('click.supplierScoreSaveFinal', '#btn-save-final-score').on('click.supplierScoreSaveFinal', '#btn-save-final-score', function (e) {
-                e.preventDefault();
-                e.stopPropagation();
-                var ym = resolveYm();
-                if (!/^\d{4}-\d{2}$/.test(ym)) {
-                    Toastr.error('请选择查询月份');
-                    return false;
-                }
-                var rows = collectFinalRows();
-                if (!rows.length) {
-                    Toastr.warning('当前没有可保存的数据');
-                    return false;
-                }
-                var $btn = $(this);
-                if ($btn.data('loading')) {
-                    return false;
-                }
-                $btn.data('loading', 1).prop('disabled', true);
-                Fast.api.ajax({
-                    url: 'supplierservicescore/savefinalbatch',
-                    type: 'POST',
-                    data: {
-                        ym: ym,
-                        rows_json: JSON.stringify(rows)
-                    }
-                }, function () {
-                    $btn.data('loading', 0).prop('disabled', false);
-                    table.bootstrapTable('refresh');
-                    return true;
-                }, function () {
-                    $btn.data('loading', 0).prop('disabled', false);
-                });
-                return false;
-            });
         },
 
         /**
          * 本月订单明细弹层:按开标/指定时间列出各订单,点详情打开订单详情
          */
         monthorders: function () {
+            $(document).off('click.sscMonthOrdersClose', '.btn-ssc-month-orders-close')
+                .on('click.sscMonthOrdersClose', '.btn-ssc-month-orders-close', function (e) {
+                    e.preventDefault();
+                    if (typeof Fast !== 'undefined' && Fast.api && typeof Fast.api.close === 'function') {
+                        Fast.api.close();
+                    } else if (parent && parent.Layer) {
+                        var index = parent.Layer.getFrameIndex(window.name);
+                        parent.Layer.close(index);
+                    }
+                    return false;
+                });
             $(document).off('click.sscOrderDetails', '.btn-ssc-order-details').on('click.sscOrderDetails', '.btn-ssc-order-details', function (e) {
                 e.preventDefault();
                 e.stopPropagation();