m0_70156489 1 周之前
父節點
當前提交
8ae6a39b96

+ 183 - 46
application/admin/controller/Procuremen.php

@@ -3153,6 +3153,7 @@ class Procuremen extends Backend
                     $pick['selected_ids'],
                     $pick['selected_ids'],
                     $pick['unselected_ids'],
                     $pick['unselected_ids'],
                     (int)$pick['purchase_order_id'],
                     (int)$pick['purchase_order_id'],
+                    false,
                     false
                     false
                 );
                 );
             }
             }
@@ -3163,6 +3164,13 @@ class Procuremen extends Backend
             $this->error($msg !== '' ? $msg : '提交失败');
             $this->error($msg !== '' ? $msg : '提交失败');
         }
         }
 
 
+        try {
+            $bundle = $this->loadOrderBundleForConfirmNotify(array_column($picks, 'scydgy_id'));
+            $this->dispatchPurchaseConfirmPickNotifications($bundle, $results);
+        } catch (\Throwable $e) {
+            Log::write('采购确认短信批量发送异常: ' . $e->getMessage(), 'error');
+        }
+
         $this->pickHiddenScydgySetCache = null;
         $this->pickHiddenScydgySetCache = null;
         $this->success('操作成功', '', ['batch' => $results]);
         $this->success('操作成功', '', ['batch' => $results]);
     }
     }
@@ -3170,7 +3178,7 @@ class Procuremen extends Backend
     /**
     /**
      * @return array<string, mixed>
      * @return array<string, mixed>
      */
      */
-    protected function runPurchaseConfirmPick(int $sid, array $selectedIds, array $unselectedIds, int $purchaseOrderId, bool $manageTransaction = true): array
+    protected function runPurchaseConfirmPick(int $sid, array $selectedIds, array $unselectedIds, int $purchaseOrderId, bool $manageTransaction = true, bool $sendNotifications = true): array
     {
     {
         $poRow = ProcuremenGuard::loadPurchaseOrderOrFail($sid);
         $poRow = ProcuremenGuard::loadPurchaseOrderOrFail($sid);
         ProcuremenGuard::assertWflowPendingApproval($poRow);
         ProcuremenGuard::assertWflowPendingApproval($poRow);
@@ -3281,8 +3289,131 @@ class Procuremen extends Backend
             $poIdFinal > 0 ? $poIdFinal : null
             $poIdFinal > 0 ? $poIdFinal : null
         );
         );
 
 
-        $confirmNotifyVars = $this->buildPurchaseConfirmNotifyVars($sid, $selectedIds[0] ?? 0);
+        if ($sendNotifications) {
+            try {
+                $bundle = $this->loadOrderBundleForConfirmNotify([$sid]);
+                $this->dispatchPurchaseConfirmPickNotifications($bundle, [[
+                    'scydgy_id'         => $sid,
+                    'selected_ids'      => $selectedIds,
+                    'unselected_ids'    => $unselectedIds,
+                    'purchase_order_id' => $purchaseOrderId,
+                ]]);
+            } catch (\Throwable $e) {
+                Log::write('采购确认短信发送异常 scydgy_id=' . $sid . ' ' . $e->getMessage(), 'error');
+            }
+        }
+
+        Log::write(sprintf(
+            'purchaseConfirmPick scydgy_id=%s purchase_order_id=%d selected_ids=%s unselected_ids=%s',
+            $scydgyId,
+            $purchaseOrderId,
+            json_encode($selectedIds, JSON_UNESCAPED_UNICODE),
+            json_encode($unselectedIds, JSON_UNESCAPED_UNICODE)
+        ), 'notice');
+
+        $pdfPublicPath = '';
+        try {
+            $pdfPublicPath = (string)$this->savePurchaseConfirmDetailPdf($sid, $poIdFinal);
+        } catch (\Throwable $e) {
+            Log::write('采购确认PDF异常: ' . $e->getMessage(), 'error');
+        }
+
+        return [
+            'scydgy_id'            => $scydgyId,
+            'purchase_order_id'    => $purchaseOrderId,
+            'selected_ids'         => $selectedIds,
+            'unselected_ids'       => $unselectedIds,
+            'purchase_confirm_pdf' => $pdfPublicPath,
+        ];
+    }
+
+    /**
+     * 审批通过短信:按本次涉及的工序汇总 process_lines
+     *
+     * @param int[]|string[] $scydgyIds
+     * @return array{ccydh:string, pos:array<int,array>, merge_rows:array<int,array>}
+     */
+    protected function loadOrderBundleForConfirmNotify(array $scydgyIds): array
+    {
+        $empty = ['ccydh' => '', 'pos' => [], 'merge_rows' => []];
+        $scydgyIds = array_values(array_unique(array_filter(array_map('intval', $scydgyIds))));
+        if ($scydgyIds === []) {
+            return $empty;
+        }
+        $pos = [];
+        try {
+            $rows = Db::table('purchase_order')->where('scydgy_id', 'in', $scydgyIds)->order('scydgy_id', 'asc')->select();
+        } catch (\Throwable $e) {
+            $rows = [];
+        }
+        if (is_array($rows)) {
+            foreach ($rows as $po) {
+                if (is_array($po)) {
+                    $pos[] = $po;
+                }
+            }
+        }
+        if ($pos === []) {
+            return $empty;
+        }
+        $ccydh = trim((string)($pos[0]['CCYDH'] ?? ''));
+
+        return [
+            'ccydh'      => $ccydh,
+            'pos'        => $pos,
+            'merge_rows' => $this->scydgyRowsForPurchaseOrders($pos),
+        ];
+    }
 
 
+    /**
+     * 审批通过/未通过短信:同一供应商只发一条,process_lines 含本次全部工序
+     *
+     * @param array{ccydh?:string,pos?:array,merge_rows?:array} $bundle
+     * @param array<int, array<string, mixed>> $picks
+     */
+    protected function dispatchPurchaseConfirmPickNotifications(array $bundle, array $picks): void
+    {
+        if ($picks === []) {
+            return;
+        }
+        $fallbackDetailId = 0;
+        $winners = [];
+        $losers = [];
+        foreach ($picks as $pick) {
+            if (!is_array($pick)) {
+                continue;
+            }
+            $sid = (int)($pick['scydgy_id'] ?? 0);
+            if (!$this->isValidScydgyRowId($sid)) {
+                continue;
+            }
+            $selectedIds = array_values(array_unique(array_filter(array_map('intval', (array)($pick['selected_ids'] ?? [])))));
+            $unselectedIds = array_values(array_unique(array_filter(array_map('intval', (array)($pick['unselected_ids'] ?? [])))));
+            if ($fallbackDetailId <= 0 && $selectedIds !== []) {
+                $fallbackDetailId = (int)$selectedIds[0];
+            }
+            foreach ($selectedIds as $pid) {
+                $dr = $this->purchaseOrderDetai($sid, $pid);
+                if (!is_array($dr)) {
+                    continue;
+                }
+                $winners[$this->notifySupplierDedupeKey($dr)] = $dr;
+            }
+            foreach ($unselectedIds as $pid) {
+                $dr = $this->purchaseOrderDetai($sid, $pid);
+                if (!is_array($dr)) {
+                    continue;
+                }
+                $key = $this->notifySupplierDedupeKey($dr);
+                if (!isset($winners[$key])) {
+                    $losers[$key] = $dr;
+                }
+            }
+        }
+        if ($winners === [] && $losers === []) {
+            return;
+        }
+        $confirmNotifyVars = $this->buildPurchaseConfirmNotifyVars(0, $fallbackDetailId, $bundle);
         $sendSmsSafe = function ($phone, $content) {
         $sendSmsSafe = function ($phone, $content) {
             $phone = trim((string)$phone);
             $phone = trim((string)$phone);
             if ($phone === '') {
             if ($phone === '') {
@@ -3294,12 +3425,7 @@ class Procuremen extends Backend
                 Log::write('采购确认短信失败 phone=' . $phone . ' ' . $e->getMessage(), 'error');
                 Log::write('采购确认短信失败 phone=' . $phone . ' ' . $e->getMessage(), 'error');
             }
             }
         };
         };
-
-        foreach ($selectedIds as $pid) {
-            $dr = $this->purchaseOrderDetai($sid, $pid);
-            if (!is_array($dr)) {
-                continue;
-            }
+        foreach ($winners as $dr) {
             $cname = trim((string)($dr['company_name'] ?? ''));
             $cname = trim((string)($dr['company_name'] ?? ''));
             $ph = trim((string)($dr['phone'] ?? ''));
             $ph = trim((string)($dr['phone'] ?? ''));
             $sms = $this->renderNotifyTemplate('confirm_ok', array_merge($confirmNotifyVars, [
             $sms = $this->renderNotifyTemplate('confirm_ok', array_merge($confirmNotifyVars, [
@@ -3307,13 +3433,9 @@ class Procuremen extends Backend
                 'contact_name' => $this->resolveCustomerContactName($ph, $cname),
                 'contact_name' => $this->resolveCustomerContactName($ph, $cname),
                 'phone'        => $ph,
                 'phone'        => $ph,
             ]));
             ]));
-            $sendSmsSafe((string)($dr['phone'] ?? ''), $sms);
+            $sendSmsSafe($ph, $sms);
         }
         }
-        foreach ($unselectedIds as $pid) {
-            $dr = $this->purchaseOrderDetai($sid, $pid);
-            if (!is_array($dr)) {
-                continue;
-            }
+        foreach ($losers as $dr) {
             $cname = trim((string)($dr['company_name'] ?? ''));
             $cname = trim((string)($dr['company_name'] ?? ''));
             $ph = trim((string)($dr['phone'] ?? ''));
             $ph = trim((string)($dr['phone'] ?? ''));
             $sms = $this->renderNotifyTemplate('confirm_fail', array_merge($confirmNotifyVars, [
             $sms = $this->renderNotifyTemplate('confirm_fail', array_merge($confirmNotifyVars, [
@@ -3321,31 +3443,22 @@ class Procuremen extends Backend
                 'contact_name' => $this->resolveCustomerContactName($ph, $cname),
                 'contact_name' => $this->resolveCustomerContactName($ph, $cname),
                 'phone'        => $ph,
                 'phone'        => $ph,
             ]));
             ]));
-            $sendSmsSafe((string)($dr['phone'] ?? ''), $sms);
+            $sendSmsSafe($ph, $sms);
         }
         }
+    }
 
 
-        Log::write(sprintf(
-            'purchaseConfirmPick scydgy_id=%s purchase_order_id=%d selected_ids=%s unselected_ids=%s',
-            $scydgyId,
-            $purchaseOrderId,
-            json_encode($selectedIds, JSON_UNESCAPED_UNICODE),
-            json_encode($unselectedIds, JSON_UNESCAPED_UNICODE)
-        ), 'notice');
-
-        $pdfPublicPath = '';
-        try {
-            $pdfPublicPath = (string)$this->savePurchaseConfirmDetailPdf($sid, $poIdFinal);
-        } catch (\Throwable $e) {
-            Log::write('采购确认PDF异常: ' . $e->getMessage(), 'error');
+    /**
+     * 短信去重键:优先手机号,否则公司名
+     */
+    protected function notifySupplierDedupeKey(array $detailRow): string
+    {
+        $ph = preg_replace('/\s+/', '', trim((string)($detailRow['phone'] ?? '')));
+        if ($ph !== '') {
+            return 'p:' . $ph;
         }
         }
+        $cn = trim((string)($detailRow['company_name'] ?? ''));
 
 
-        return [
-            'scydgy_id'            => $scydgyId,
-            'purchase_order_id'    => $purchaseOrderId,
-            'selected_ids'         => $selectedIds,
-            'unselected_ids'       => $unselectedIds,
-            'purchase_confirm_pdf' => $pdfPublicPath,
-        ];
+        return 'c:' . $cn;
     }
     }
 
 
     /**
     /**
@@ -4848,17 +4961,38 @@ class Procuremen extends Backend
      *
      *
      * @return array<string, string>
      * @return array<string, string>
      */
      */
-    protected function buildPurchaseConfirmNotifyVars(int $sid, int $fallbackDetailId = 0): array
+    protected function buildPurchaseConfirmNotifyVars(int $sid, int $fallbackDetailId = 0, ?array $bundle = null): array
     {
     {
-        $po = null;
-        try {
-            $po = Db::table('purchase_order')->where('scydgy_id', $sid)->find();
-        } catch (\Throwable $e) {
-            $po = null;
+        if ($bundle === null || !is_array($bundle['merge_rows'] ?? null) || ($bundle['merge_rows'] ?? []) === []) {
+            $bundle = $this->loadOrderBundleForConfirmNotify($sid > 0 ? [$sid] : []);
         }
         }
-        $ccydh = is_array($po) ? trim((string)($po['CCYDH'] ?? '')) : '';
-        $cyjmc = is_array($po) ? trim((string)($po['CYJMC'] ?? '')) : '';
-        if (($ccydh === '' || $cyjmc === '') && $fallbackDetailId > 0) {
+        $mergeRows = is_array($bundle['merge_rows'] ?? null) ? $bundle['merge_rows'] : [];
+        $pos = is_array($bundle['pos'] ?? null) ? $bundle['pos'] : [];
+        $ccydh = trim((string)($bundle['ccydh'] ?? ''));
+        $cyjmc = '';
+        foreach ($mergeRows as $mr) {
+            if (!is_array($mr)) {
+                continue;
+            }
+            if ($ccydh === '') {
+                $ccydh = trim((string)($mr['CCYDH'] ?? ''));
+            }
+            if ($cyjmc === '') {
+                $cyjmc = trim((string)($mr['CYJMC'] ?? ''));
+            }
+        }
+        if ($pos !== []) {
+            $firstPo = $pos[0];
+            if (is_array($firstPo)) {
+                if ($ccydh === '') {
+                    $ccydh = trim((string)($firstPo['CCYDH'] ?? ''));
+                }
+                if ($cyjmc === '') {
+                    $cyjmc = trim((string)($firstPo['CYJMC'] ?? ''));
+                }
+            }
+        }
+        if (($ccydh === '' || $cyjmc === '') && $fallbackDetailId > 0 && $sid > 0) {
             $any = $this->purchaseOrderDetai($sid, $fallbackDetailId);
             $any = $this->purchaseOrderDetai($sid, $fallbackDetailId);
             if (is_array($any)) {
             if (is_array($any)) {
                 if ($ccydh === '') {
                 if ($ccydh === '') {
@@ -4869,14 +5003,17 @@ class Procuremen extends Backend
                 }
                 }
             }
             }
         }
         }
-        $mergeRows = is_array($po) && $po !== [] ? [$po] : [];
         $processPlain = $mergeRows !== [] ? $this->buildProcessLinesPlain($mergeRows) : '';
         $processPlain = $mergeRows !== [] ? $this->buildProcessLinesPlain($mergeRows) : '';
         $processLines = $this->buildPurchaseConfirmProcessLinesText($ccydh, $cyjmc, $processPlain);
         $processLines = $this->buildPurchaseConfirmProcessLinesText($ccydh, $cyjmc, $processPlain);
+        $cgymc = '';
+        if (count($mergeRows) === 1 && is_array($mergeRows[0])) {
+            $cgymc = trim((string)($mergeRows[0]['CGYMC'] ?? ''));
+        }
 
 
         return [
         return [
             'ccydh'              => $ccydh,
             'ccydh'              => $ccydh,
             'cyjmc'              => $cyjmc,
             'cyjmc'              => $cyjmc,
-            'cgymc'              => is_array($po) ? trim((string)($po['CGYMC'] ?? '')) : '',
+            'cgymc'              => $cgymc,
             'process_lines'      => $processLines,
             'process_lines'      => $processLines,
             'process_lines_html' => '',
             'process_lines_html' => '',
         ];
         ];

+ 60 - 27
application/admin/view/procuremen/details_dialog_shell.html

@@ -8,7 +8,17 @@
 {include file="procuremen/details_fragment" /}
 {include file="procuremen/details_fragment" /}
 <script>
 <script>
 require(['jquery'], function ($) {
 require(['jquery'], function ($) {
-    function fitProcuremenDetailsLayer() {
+    function measureDetailsContentHeight() {
+        var $wrap = $('.procuremen-details-wrap');
+        if (!$wrap.length) {
+            return Math.ceil(document.body.scrollHeight || 0);
+        }
+        var wrapH = Math.ceil($wrap.outerHeight(true) || 0);
+        var scrollH = Math.ceil(document.body.scrollHeight || 0);
+        return Math.max(wrapH, scrollH);
+    }
+
+    function applyDetailsLayerLayout() {
         try {
         try {
             var index = parent.Layer.getFrameIndex(window.name);
             var index = parent.Layer.getFrameIndex(window.name);
             if (!index) {
             if (!index) {
@@ -19,41 +29,57 @@ require(['jquery'], function ($) {
             if (!$layer.length) {
             if (!$layer.length) {
                 return;
                 return;
             }
             }
-            var isMax = $layer.hasClass('layui-layer-max');
             var viewH = $parentWin.height();
             var viewH = $parentWin.height();
-            var contentH = Math.ceil($('body').prop('scrollHeight') || 0);
+            var titleH = $layer.find('.layui-layer-title').outerHeight() || 42;
+            var btnH = $layer.find('.layui-layer-btn').outerHeight() || 0;
+            var contentH = measureDetailsContentHeight();
             if (contentH < 120) {
             if (contentH < 120) {
                 return;
                 return;
             }
             }
-            if (isMax) {
-                parent.Layer.style(index, {
-                    top: 0,
-                    height: viewH
-                });
-                $('html, body').css({
-                    height: viewH + 'px',
-                    overflowY: 'auto',
-                    overflowX: 'hidden'
-                });
-                return;
-            }
-            var titleH = $layer.find('.layui-layer-title').outerHeight() || 42;
-            var maxBodyH = Math.max(320, viewH - 24);
-            var layerH = Math.min(contentH + titleH + 2, maxBodyH);
-            var top = Math.max(0, Math.floor((viewH - layerH) / 2));
+            var isMax = $layer.hasClass('layui-layer-max');
+            var edge = isMax ? 8 : 24;
+            var availableH = Math.max(320, viewH - titleH - btnH - edge);
+            var iframeH = Math.min(contentH + 12, availableH);
+            var layerH = titleH + btnH + iframeH;
+            var top = isMax ? 0 : Math.max(0, Math.floor((viewH - layerH) / 2));
             parent.Layer.style(index, {
             parent.Layer.style(index, {
                 height: layerH,
                 height: layerH,
                 top: top
                 top: top
             });
             });
+            var $content = $layer.find('.layui-layer-content');
+            $content.css({
+                height: iframeH + 'px',
+                overflow: 'hidden',
+                padding: 0,
+                margin: 0
+            });
+            $layer.find('iframe').css({
+                height: iframeH + 'px',
+                width: '100%',
+                display: 'block',
+                border: 0
+            });
+            var needScroll = contentH + 12 > iframeH;
             $('html, body').css({
             $('html, body').css({
-                height: 'auto',
-                overflowY: 'visible',
-                overflowX: 'hidden'
+                height: needScroll ? iframeH + 'px' : 'auto',
+                minHeight: '0',
+                maxHeight: needScroll ? iframeH + 'px' : 'none',
+                overflowX: 'hidden',
+                overflowY: needScroll ? 'auto' : 'visible',
+                WebkitOverflowScrolling: 'touch',
+                background: '#fff'
             });
             });
         } catch (e) {
         } catch (e) {
         }
         }
     }
     }
 
 
+    function scheduleDetailsLayerLayout() {
+        applyDetailsLayerLayout();
+        setTimeout(applyDetailsLayerLayout, 80);
+        setTimeout(applyDetailsLayerLayout, 220);
+        setTimeout(applyDetailsLayerLayout, 480);
+    }
+
     function bindLayerResize() {
     function bindLayerResize() {
         try {
         try {
             var index = parent.Layer.getFrameIndex(window.name);
             var index = parent.Layer.getFrameIndex(window.name);
@@ -62,20 +88,27 @@ require(['jquery'], function ($) {
             }
             }
             var $layer = parent.$('#layui-layer' + index);
             var $layer = parent.$('#layui-layer' + index);
             $layer.find('.layui-layer-maxmin').off('click.procuremenDetails').on('click.procuremenDetails', function () {
             $layer.find('.layui-layer-maxmin').off('click.procuremenDetails').on('click.procuremenDetails', function () {
-                setTimeout(fitProcuremenDetailsLayer, 80);
+                scheduleDetailsLayerLayout();
             });
             });
         } catch (e) {
         } catch (e) {
         }
         }
     }
     }
 
 
     $(function () {
     $(function () {
-        fitProcuremenDetailsLayer();
+        scheduleDetailsLayerLayout();
         bindLayerResize();
         bindLayerResize();
-        setTimeout(fitProcuremenDetailsLayer, 120);
-        setTimeout(fitProcuremenDetailsLayer, 400);
         $(window).off('resize.procuremenDetails').on('resize.procuremenDetails', function () {
         $(window).off('resize.procuremenDetails').on('resize.procuremenDetails', function () {
-            setTimeout(fitProcuremenDetailsLayer, 80);
+            scheduleDetailsLayerLayout();
         });
         });
+        if (typeof window.ResizeObserver !== 'undefined') {
+            var $wrap = $('.procuremen-details-wrap');
+            if ($wrap.length) {
+                var ro = new ResizeObserver(function () {
+                    scheduleDetailsLayerLayout();
+                });
+                ro.observe($wrap[0]);
+            }
+        }
     });
     });
 });
 });
 </script>
 </script>

+ 12 - 2
application/admin/view/procuremen/details_fragment.html

@@ -3,11 +3,15 @@
     html.procuremen-details-dialog body.procuremen-details-dialog {
     html.procuremen-details-dialog body.procuremen-details-dialog {
         height: auto !important;
         height: auto !important;
         min-height: 0 !important;
         min-height: 0 !important;
+        margin: 0;
         overflow-x: hidden;
         overflow-x: hidden;
+        overflow-y: auto;
+        -webkit-overflow-scrolling: touch;
+        background: #fff;
     }
     }
     body.is-dialog .procuremen-details-wrap {
     body.is-dialog .procuremen-details-wrap {
         margin: 0;
         margin: 0;
-        padding: 12px 14px 16px;
+        padding: 12px 14px 28px;
     }
     }
     .procuremen-details-wrap .section-title {
     .procuremen-details-wrap .section-title {
         font-size: 14px;
         font-size: 14px;
@@ -215,6 +219,12 @@
     .procuremen-details-wrap .detail-supplier-table {
     .procuremen-details-wrap .detail-supplier-table {
         min-width: 1180px;
         min-width: 1180px;
     }
     }
+    .procuremen-details-wrap .details-supplier-table-wrap {
+        overflow-x: auto;
+        overflow-y: visible;
+        -webkit-overflow-scrolling: touch;
+        margin-bottom: 4px;
+    }
     .procuremen-details-wrap .detail-supplier-table td.detail-quote-empty {
     .procuremen-details-wrap .detail-supplier-table td.detail-quote-empty {
         color: #e67e22;
         color: #e67e22;
     }
     }
@@ -366,7 +376,7 @@
     </div>
     </div>
 
 
     <div class="section-title">供应商</div>
     <div class="section-title">供应商</div>
-    <div class="table-responsive">
+    <div class="details-supplier-table-wrap table-responsive">
         <table class="table table-bordered table-condensed detail-mini detail-supplier-table">
         <table class="table table-bordered table-condensed detail-mini detail-supplier-table">
             <thead>
             <thead>
             <tr>
             <tr>