增加审批时间轴&&调整采购单
This commit is contained in:
@@ -169,4 +169,33 @@ class ApprovalRecordsController extends BaseController
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function approvalTimeline(Request $request)
|
||||
{
|
||||
try {
|
||||
$raw = $this->service->serviceApprovalTimeline($request, $request->all());
|
||||
$data = $this->transformData($raw, method_exists($this, 'timelineFormat')
|
||||
? [$this, 'timelineFormat']
|
||||
: null);
|
||||
return $this->success($data);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected function timelineFormat($data)
|
||||
{
|
||||
$result = [];
|
||||
foreach ($data as $record) {
|
||||
$result[] = [
|
||||
'id' => $record['id'],
|
||||
'name' => $record['approver']['name'],
|
||||
'comment' => $record['comment'],
|
||||
'time' => $record['updated_at']
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ class PurchaseOrdersController extends BaseController
|
||||
'contract_file' => $contractFile,
|
||||
'contract_file_url' => $contractFileUrl,
|
||||
'status' => $record['status'],
|
||||
'status_text' => array_flip(\app\common\service\SysDictService::pluckKeyValueByCode('purchase_status'))[$record['status']],
|
||||
'remark' => $record['remark'],
|
||||
'created_at' => date('Y-m-d H:i:s', $record['created_at']),
|
||||
'updated_at' => date('Y-m-d H:i:s', $record['updated_at']),
|
||||
|
||||
@@ -44,7 +44,7 @@ class PurchaseOrders extends Model
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'order_no', 'pi_no', 'buyer_id', 'buyer_name', 'contract_file', 'status', 'remark', 'created_at', 'updated_at'];
|
||||
protected $fillable = ['id', 'order_no', 'pi_no', 'buyer_id', 'buyer_name', 'contract_file', 'status', 'remark', 'is_draft', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
|
||||
@@ -103,14 +103,7 @@ class ApprovalRecordsService
|
||||
'approver',
|
||||
'instance.flows',
|
||||
'instance.initiator',
|
||||
'instance.bizInfo',
|
||||
|
||||
// 'instance.bizInfo.items',
|
||||
// 'instance.bizInfo.items.options',
|
||||
// 'instance.bizInfo.items.moneyValues',
|
||||
// 'instance.bizInfo.accounts',
|
||||
// 'instance.bizInfo.moneyValues',
|
||||
// 'instance.bizInfo.salesperson',
|
||||
'instance.bizInfo'
|
||||
];
|
||||
break;
|
||||
}
|
||||
@@ -178,4 +171,12 @@ class ApprovalRecordsService
|
||||
$result = $service->transferApproval($data['instance_id'], $data['step_index'], $request->user->uid, $data['to_uid'], $data['reason'], 'name');
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public function serviceApprovalTimeline(Request $request, array $data = [])
|
||||
{
|
||||
$instance = \app\common\model\ApprovalInstances::where('biz_id', $data['id'])->where('flow_type', $data['flow_type'])->first();
|
||||
$result = \app\common\model\ApprovalRecords::where('instance_id', $instance->id)->with('approver')->orderBy('id', 'desc')->get();
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,9 @@ class PurchaseOrdersService
|
||||
$data['order_no'] = generateStockNo('CG');
|
||||
$data['buyer_id'] = $request->user->uid;
|
||||
$data['buyer_name'] = User::where('id', $request->user->uid)->value('name');
|
||||
if ($data['is_draft'] == 0) {
|
||||
$data['status'] = 1;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -102,52 +105,6 @@ class PurchaseOrdersService
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新采购单
|
||||
*
|
||||
* @param Request $request
|
||||
* @param array $data
|
||||
* @return mixed
|
||||
*/
|
||||
public function serviceUpdate(Request $request, array $data = [], string $scene = 'update')
|
||||
{
|
||||
// 使用事务确保数据一致性
|
||||
return Db::transaction(function () use ($request, $data, $scene) {
|
||||
$data = $this->preProcess(ucfirst($scene), $data, $request);
|
||||
|
||||
// 构建查询条件
|
||||
$config = $this->buildQueryConfig($request, $scene);
|
||||
|
||||
// 更新主表信息
|
||||
$udata = $data;
|
||||
unset($udata['stock_order']);
|
||||
$updated = $this->logicUpdateBy($config, $udata);
|
||||
|
||||
// 获取更新后的数据
|
||||
$order = $this->logicShow($config);
|
||||
|
||||
// 更新采购单明细
|
||||
if (isset($data['stock_order'])) {
|
||||
// 先删除原有的明细
|
||||
PurchaseOrderItems::where('purchase_order_id', $order->id)->delete();
|
||||
|
||||
// 重新创建明细
|
||||
foreach ($data['stock_order'] as $orderData) {
|
||||
foreach ($orderData['stock_items'] as $item) {
|
||||
PurchaseOrderItems::create([
|
||||
'purchase_order_id' => $order->id,
|
||||
'stock_id' => $orderData['stock_orders_id'],
|
||||
'stock_items_id' => $item['stock_order_items_id'],
|
||||
'stock_items_uqid' => $item['unique_id'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->postProcess(ucfirst($scene), $order, ['is_resubmit'=>$data['is_resubmit']]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询配置
|
||||
*
|
||||
@@ -213,7 +170,7 @@ class PurchaseOrdersService
|
||||
*/
|
||||
protected function afterServiceStore($res)
|
||||
{
|
||||
if ($res->status == 1) {
|
||||
if ($res->is_draft == 0) {
|
||||
$this->startApproval($res, ['resubmitted'=>0]);
|
||||
}
|
||||
return $res;
|
||||
@@ -225,6 +182,69 @@ class PurchaseOrdersService
|
||||
$appro->start('purchase_orders', 'purchase_orders', $item->id, ['resubmitted'=>0], $item->buyer_id);
|
||||
}
|
||||
|
||||
public function beforeServiceUpdate(array $data, Request $request)
|
||||
{
|
||||
if ($data['is_draft'] == 0) {
|
||||
$data['status'] = 1;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新采购单
|
||||
*
|
||||
* @param Request $request
|
||||
* @param array $data
|
||||
* @return mixed
|
||||
*/
|
||||
public function serviceUpdate(Request $request, array $data = [], string $scene = 'update')
|
||||
{
|
||||
// 使用事务确保数据一致性
|
||||
return Db::transaction(function () use ($request, $data, $scene) {
|
||||
$data = $this->preProcess(ucfirst($scene), $data, $request);
|
||||
|
||||
// 构建查询条件
|
||||
$config = $this->buildQueryConfig($request, $scene);
|
||||
|
||||
// 更新主表信息
|
||||
$udata = $data;
|
||||
unset($udata['stock_order']);
|
||||
unset($udata['is_resubmit']);
|
||||
unset($udata['approval_desc']);
|
||||
$this->logicUpdateBy($config, $udata);
|
||||
|
||||
// 获取更新后的数据
|
||||
$order = $this->logicShow($config);
|
||||
|
||||
// 更新采购单明细
|
||||
if (isset($data['stock_order'])) {
|
||||
// 先删除原有的明细
|
||||
PurchaseOrderItems::where('purchase_order_id', $order->id)->delete();
|
||||
|
||||
// 重新创建明细
|
||||
foreach ($data['stock_order'] as $orderData) {
|
||||
foreach ($orderData['stock_items'] as $item) {
|
||||
PurchaseOrderItems::create([
|
||||
'purchase_order_id' => $order->id,
|
||||
'stock_id' => $orderData['stock_orders_id'],
|
||||
'stock_items_id' => $item['stock_order_items_id'],
|
||||
'stock_items_uqid' => $item['unique_id'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->postProcess(
|
||||
ucfirst($scene),
|
||||
$order,
|
||||
[
|
||||
'is_resubmit'=>$data['is_resubmit'],
|
||||
'approval_desc'=>$data['approval_desc']
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
protected function afterServiceUpdate($res, $data)
|
||||
{
|
||||
if ($data['is_resubmit'] == 0 && $res->status == 1) {
|
||||
@@ -238,10 +258,10 @@ class PurchaseOrdersService
|
||||
// 审批服务
|
||||
$appro = new \app\common\utils\approval\service\ApprovalFlowService();
|
||||
// 重新提交
|
||||
$appro->resubmit($instance->id, $instance->initiator_id, ['resubmitted'=>1], $data['remark']);
|
||||
$appro->resubmit($instance->id, $instance->initiator_id, ['resubmitted'=>1], $data['approval_desc']);
|
||||
// 添加到讨论
|
||||
if (!empty($data['remark'])) {
|
||||
$appro->postDiscussion($instance->id, $instance->initiator_id, $data['remark'], false, [], 'name');
|
||||
if (!empty($data['approval_desc'])) {
|
||||
$appro->postDiscussion($instance->id, $instance->initiator_id, $data['approval_desc'], false, [], 'name');
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
|
||||
@@ -808,7 +808,7 @@ class ApprovalFlowService
|
||||
* initiator_name?:?string
|
||||
* }
|
||||
*/
|
||||
public function getSimpleTimeline(int $instanceId, ?string $userNameColumn = null, string $initiatorPlacement = 'prepend'): array
|
||||
public function getSimpleTimeline2(int $instanceId, ?string $userNameColumn = null, string $initiatorPlacement = 'prepend'): array
|
||||
{
|
||||
// 基础数据
|
||||
$instance = Db::table('approval_instances')->where('id', $instanceId)->first();
|
||||
@@ -958,6 +958,193 @@ class ApprovalFlowService
|
||||
}
|
||||
|
||||
|
||||
public function getSimpleTimeline(int $instanceId, ?string $userNameColumn = null, string $initiatorPlacement = 'prepend'): array
|
||||
{
|
||||
// 基础数据
|
||||
$instance = Db::table('approval_instances')->where('id', $instanceId)->first();
|
||||
if (!$instance) throw new BusinessException('审批实例未找到');
|
||||
|
||||
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
|
||||
if (!$flow) throw new BusinessException('审批流程定义未找到');
|
||||
|
||||
$steps = json_decode($flow->steps, true) ?: [];
|
||||
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
|
||||
$curStep = (int)$instance->current_step;
|
||||
$round = (int)$instance->round;
|
||||
|
||||
// 申请人
|
||||
$initiatorId = (int)$instance->initiator_id;
|
||||
$initiatorName = null;
|
||||
if ($userNameColumn) {
|
||||
$initiatorName = Db::table('user')->where('id', $initiatorId)->value($userNameColumn);
|
||||
}
|
||||
|
||||
// 审批记录(仅当前轮次)
|
||||
$records = Db::table('approval_records as ar')
|
||||
->where('ar.instance_id', $instanceId)
|
||||
->where('ar.round', $round)
|
||||
->orderBy('ar.step_index')
|
||||
->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc')
|
||||
->select('ar.*')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
// 一次性拉取姓名,避免 N+1
|
||||
$nameMap = [];
|
||||
if ($userNameColumn && !empty($records)) {
|
||||
$uids = [];
|
||||
foreach ($records as $r) $uids[] = (int)$r->approver_id;
|
||||
$uids = array_values(array_unique($uids));
|
||||
if (!empty($uids)) {
|
||||
$pairs = Db::table('user')->whereIn('id', $uids)->pluck($userNameColumn, 'id')->toArray();
|
||||
foreach ($pairs as $uid => $name) $nameMap[(int)$uid] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
// 记录分组
|
||||
$byStep = [];
|
||||
foreach ($records as $r) {
|
||||
$idx = (int)$r->step_index;
|
||||
$byStep[$idx][] = $r;
|
||||
}
|
||||
|
||||
$timeline = [];
|
||||
|
||||
// 可选:发起申请块
|
||||
if ($initiatorPlacement === 'prepend') {
|
||||
$appliedAt = $instance->created_at ?: null;
|
||||
$timeline[] = [
|
||||
'index' => -1,
|
||||
'name' => '发起申请',
|
||||
'type' => '-',
|
||||
'status' => 'submitted',
|
||||
'started_at' => $appliedAt,
|
||||
'finished_at' => $appliedAt,
|
||||
'duration_seconds' => 0,
|
||||
'initiator_id' => $initiatorId,
|
||||
'initiator_name' => $initiatorName,
|
||||
];
|
||||
}
|
||||
|
||||
// 每一步
|
||||
foreach ($steps as $i => $meta) {
|
||||
$name = $meta['name'] ?? ('Step '.$i);
|
||||
$typeRaw = strtolower($meta['type'] ?? 'all');
|
||||
$type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all';
|
||||
$condStr = $meta['condition'] ?? null;
|
||||
$condOk = $condStr ? $this->evaluateCondition($condStr, $formData) : true;
|
||||
|
||||
$recs = $byStep[$i] ?? [];
|
||||
$hasRecs = !empty($recs);
|
||||
|
||||
$status = 'pending';
|
||||
$startedAt = null;
|
||||
$finishedAt = null;
|
||||
$durationSec = null;
|
||||
|
||||
// ★ 每一步的“审批意见”(只展示有内容的 comment)
|
||||
$opinions = []; // [{approver_id, approver_name?, action, status, comment, time}...]
|
||||
|
||||
if ($hasRecs) {
|
||||
// 统计时间
|
||||
$minCreated = null;
|
||||
$minApproveAt = null; $maxApproveAt = null;
|
||||
$minRejectAt = null;
|
||||
$maxTimeoutAt = null;
|
||||
$cnt = ['pending'=>0,'approved'=>0,'rejected'=>0,'timeout'=>0];
|
||||
|
||||
foreach ($recs as $rr) {
|
||||
// 收集“审批意见”(非空 comment 都算)
|
||||
$cmt = trim((string)($rr->comment ?? ''));
|
||||
if ($cmt !== '') {
|
||||
$opinions[] = [
|
||||
'approver_id' => (int)$rr->approver_id,
|
||||
'approver_name' => $userNameColumn ? ($nameMap[(int)$rr->approver_id] ?? null) : null,
|
||||
'action' => $rr->action, // approve | reject | null
|
||||
'status' => $rr->status, // approved|rejected|pending|skipped|timeout
|
||||
'comment' => $cmt,
|
||||
'time' => $rr->approved_at ?: $rr->created_at,
|
||||
];
|
||||
}
|
||||
|
||||
if ($rr->created_at && (!$minCreated || $rr->created_at < $minCreated)) $minCreated = $rr->created_at;
|
||||
if ($rr->approved_at) {
|
||||
if ($rr->status === 'approved') {
|
||||
if (!$minApproveAt || $rr->approved_at < $minApproveAt) $minApproveAt = $rr->approved_at;
|
||||
if (!$maxApproveAt || $rr->approved_at > $maxApproveAt) $maxApproveAt = $rr->approved_at;
|
||||
} elseif ($rr->status === 'rejected') {
|
||||
if (!$minRejectAt || $rr->approved_at < $minRejectAt) $minRejectAt = $rr->approved_at;
|
||||
} elseif ($rr->status === 'timeout') {
|
||||
if (!$maxTimeoutAt || $rr->approved_at > $maxTimeoutAt) $maxTimeoutAt = $rr->approved_at;
|
||||
}
|
||||
}
|
||||
if (isset($cnt[$rr->status])) $cnt[$rr->status]++;
|
||||
}
|
||||
|
||||
// 时间升序排序 opinions
|
||||
if (!empty($opinions)) {
|
||||
usort($opinions, function($a, $b) {
|
||||
return strcmp($a['time'] ?? '', $b['time'] ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
$startedAt = $minCreated;
|
||||
|
||||
// 总结果(注意:这里按你现在的签名调用,含 round)
|
||||
$outcome = $this->getStepOutcome($instanceId, $round, $i, $type); // 'pending'|'approve'|'reject'
|
||||
if ($outcome === 'reject') {
|
||||
$status = 'rejected';
|
||||
$finishedAt = $minRejectAt;
|
||||
} elseif ($outcome === 'approve') {
|
||||
$status = 'approved';
|
||||
$finishedAt = ($type === 'any') ? $minApproveAt : $maxApproveAt;
|
||||
} else {
|
||||
if ($cnt['timeout'] > 0 && $cnt['approved'] === 0 && $cnt['rejected'] === 0 && $cnt['pending'] === 0) {
|
||||
$status = 'timeout';
|
||||
$finishedAt = $maxTimeoutAt;
|
||||
} else {
|
||||
$status = 'processing';
|
||||
}
|
||||
}
|
||||
|
||||
if ($startedAt && $finishedAt) {
|
||||
$durationSec = max(0, strtotime($finishedAt) - strtotime($startedAt));
|
||||
}
|
||||
} else {
|
||||
// 无记录:根据位置与条件展示
|
||||
if ($i < $curStep) {
|
||||
$status = $condOk ? 'approved' : 'skipped';
|
||||
} elseif ($i === $curStep && $instance->status === 'processing') {
|
||||
$status = 'processing';
|
||||
} else {
|
||||
$status = $condOk ? 'pending' : 'skipped';
|
||||
}
|
||||
}
|
||||
|
||||
$row = [
|
||||
'index' => (int)$i,
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'status' => $status,
|
||||
'started_at' => $startedAt,
|
||||
'finished_at' => $finishedAt,
|
||||
'duration_seconds' => $durationSec,
|
||||
'opinions' => $opinions, // ★ 这就是每个审批意见
|
||||
];
|
||||
|
||||
// 把申请人信息并到第一步(可选)
|
||||
if ($initiatorPlacement === 'attach' && empty($timeline)) {
|
||||
$row['initiator_id'] = $initiatorId;
|
||||
$row['initiator_name'] = $initiatorName;
|
||||
}
|
||||
|
||||
$timeline[] = $row;
|
||||
}
|
||||
|
||||
return $timeline;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 按业务ID获取“最新进展”的极简状态
|
||||
* - 已完结:直接返回实例最终状态
|
||||
|
||||
@@ -319,6 +319,7 @@ Route::group('/admin', function () {
|
||||
Route::any('/todo', [app\admin\controller\ApprovalRecordsController::class, 'todo']);
|
||||
Route::any('/border', [app\admin\controller\ApprovalRecordsController::class, 'businessOrder']);
|
||||
Route::any('/transfer', [app\admin\controller\ApprovalRecordsController::class, 'transferApproval']);
|
||||
Route::any('/timeline', [app\admin\controller\ApprovalRecordsController::class, 'approvalTimeline']);
|
||||
});
|
||||
|
||||
Route::group('/stock/order', function () {
|
||||
|
||||
Reference in New Issue
Block a user