diff --git a/app/Admin/Controllers/Hr/RestController.php b/app/Admin/Controllers/Hr/RestController.php index e58c468..82cf04f 100644 --- a/app/Admin/Controllers/Hr/RestController.php +++ b/app/Admin/Controllers/Hr/RestController.php @@ -24,7 +24,6 @@ class RestController extends AdminController $this->createTypeButton('drawer', 'xl')->visible(Admin::user()->can('admin.hr.rests.create')), ...$this->baseHeaderToolBar(), ]) - ->bulkActions([]) ->filter($this->baseFilter()->body([ amis()->GroupControl()->mode('horizontal')->body([ amisMake()->TextControl()->name('employee_name')->label(__('employee_sign.employee_id'))->placeholder(__('employee.name').'/'.__('employee.phone'))->columnRatio(3)->clearable(), diff --git a/app/Admin/Controllers/Hr/SignController.php b/app/Admin/Controllers/Hr/SignController.php index 9f1846b..ee534f7 100644 --- a/app/Admin/Controllers/Hr/SignController.php +++ b/app/Admin/Controllers/Hr/SignController.php @@ -6,8 +6,8 @@ use App\Admin\Controllers\AdminController; use App\Admin\Services\EmployeeSignService; use Slowlyo\OwlAdmin\Renderers\Form; use Slowlyo\OwlAdmin\Renderers\Page; -use App\Enums\{SignType, SignStatus}; use Slowlyo\OwlAdmin\Admin; +use App\Enums\{SignType, SignStatus, SignTime}; /** * 考勤打卡 @@ -36,6 +36,7 @@ class SignController extends AdminController ]), ])) ->columns([ + amisMake()->TableColumn()->name('date')->label(__('employee_sign.date')), amisMake()->TableColumn()->name('store.title')->label(__('employee_sign.store_id')), amisMake()->TableColumn()->name('employee.name')->label(__('employee.name')), amisMake()->TableColumn()->name('sign_type')->label(__('employee_sign.sign_type')) @@ -57,9 +58,39 @@ class SignController extends AdminController public function detail(): Form { - return $this->baseDetail()->title('')->body(amisMake()->Property()->items([ + $detail = amisMake()->Property()->items([ + ['label' => __('employee_sign.date'), 'content' => '${date}'], ['label' => __('employee_sign.store_id'), 'content' => '${store.title}'], ['label' => __('employee.name'), 'content' => '${employee.name}'], - ])); + + ['label' => __('employee_sign.sign_status'), 'content' => amisMake()->Mapping()->name('sign_status')->map(SignStatus::options())], + ['label' => __('employee_sign.sign_type'), 'content' => amisMake()->Mapping()->name('sign_type')->map(SignType::options())], + ['label' => __('employee_sign.remarks'), 'content' => '${remarks}'], + + ['label' => __('employee_sign.first_time'), 'content' => '${first_time}'], + ['label' => __('employee_sign.last_time'), 'content' => '${last_time}', 'span' => 2], + ]); + $logs = amisMake()->Service() + ->id('employee-sign-log-table') + ->initFetch(false) + ->api( + amisMake()->BaseApi()->method('get')->url(admin_url('api/employee-sign-logs'))->data(['date' => '${date}', 'employee_id' => '${employee_id}']) + ) + ->body( + amisMake()->Table()->columns([ + amisMake()->TableColumn()->name('sign_time')->label(__('employee_sign_log.sign_time'))->set('type', 'mapping')->map(SignTime::options()), + amisMake()->TableColumn()->name('time')->label(__('employee_sign_log.time')), + amisMake()->TableColumn()->name('sign_type')->label(__('employee_sign_log.sign_type'))->set('type', 'mapping')->map(SignType::options()), + amisMake()->TableColumn()->name('remarks')->label(__('employee_sign_log.remarks')), + amisMake()->TableColumn()->name('position.address')->label(__('employee_sign_log.position')), + ]) + ); + return $this->baseDetail()->title('')->onEvent([ + 'inited' => [ + 'actions' => [ + ['actionType' => 'reload', 'componentId' => 'employee-sign-log-table'], + ] + ] + ])->body([$detail, amisMake()->Divider()->title(__('employee_sign.log')), $logs]); } } diff --git a/app/Admin/Controllers/Hr/SignLogController.php b/app/Admin/Controllers/Hr/SignLogController.php new file mode 100644 index 0000000..185eb18 --- /dev/null +++ b/app/Admin/Controllers/Hr/SignLogController.php @@ -0,0 +1,17 @@ +input('with', []); + $list = EmployeeSignLog::with($with)->filter($request->all())->orderBy('time', 'asc')->get(); + return $this->response()->success($list); + } +} diff --git a/app/Admin/Controllers/Hr/SignRepairController.php b/app/Admin/Controllers/Hr/SignRepairController.php new file mode 100644 index 0000000..abe6b03 --- /dev/null +++ b/app/Admin/Controllers/Hr/SignRepairController.php @@ -0,0 +1,125 @@ +baseCRUD() + ->tableLayout('fixed') + ->headerToolbar([ + $this->createTypeButton('drawer', 'xl')->visible(Admin::user()->can('admin.hr.repairs.create')), + ...$this->baseHeaderToolBar(), + ]) + ->bulkActions([]) + ->filter($this->baseFilter()->body([ + amis()->GroupControl()->mode('horizontal')->body([ + amisMake()->SelectControl()->name('store_id')->label(__('employee_sign_repair.store_id')) + ->source(admin_url('api/stores?_all=1')) + ->labelField('title') + ->valueField('id') + ->searchable() + ->columnRatio(3) + ->clearable(), + amisMake()->TextControl()->name('employee_name')->label(__('employee_sign_repair.employee_id')) + ->placeholder(__('employee.name').'/'.__('employee.phone')) + ->columnRatio(3) + ->clearable(), + ]), + ])) + ->columns([ + amisMake()->Column()->name('id')->label(__('employee_sign_repair.id')), + amisMake()->Column()->name('store.title')->label(__('employee_sign_repair.store_id')), + amisMake()->Column()->name('employee.name')->label(__('employee_sign_repair.employee_id')), + amisMake()->Column()->name('date')->label(__('employee_sign_repair.date')), + amisMake()->Column()->name('repair_type')->label(__('employee_sign_repair.repair_type')) + ->set('type', 'mapping') + ->map(SignTime::options()), + amisMake()->Column()->name('check_status')->label(__('employee_sign_repair.check_status')) + ->set('type', 'mapping') + ->map(CheckStatus::options()), + $this->rowActions([ + $this->rowShowButton()->visible(Admin::user()->can('admin.hr.repairs.view')), + $this->rowEditTypeButton('drawer', 'xl')->visible(Admin::user()->can('admin.hr.repairs.update')), + $this->rowDeleteButton()->visible(Admin::user()->can('admin.hr.repairs.delete')), + amisMake()->AjaxAction() + ->label('发起审核') + ->level('link') + ->api(amisMake()->BaseApi()->url(admin_url('api/workflow/apply'))->method('post')->data([ + 'subject_type' => (new EmployeeSignRepair)->getMorphClass(), + 'subject_id' => '${id}', + 'user' => '${employee_id}' + ])) + ->confirmText(__('admin.confirm')) + ->visibleOn('${OR(check_status == '.CheckStatus::None->value.', check_status == '.CheckStatus::Cancel->value.', check_status == '.CheckStatus::Fail->value.')}'), + amisMake()->AjaxAction() + ->label('审核通过') + ->level('link') + ->api(amisMake()->BaseApi()->url(admin_url('api/workflow/cancel'))->method('post')->data([ + 'subject_type' => EmployeeSignRepair::class, + 'subject_id' => '${id}', + ])) + ->confirmText(__('admin.confirm')) + ->visibleOn('${check_status == '.CheckStatus::Processing->value.'}'), + amisMake()->AjaxAction() + ->label('审核不通过') + ->level('link') + ->api(amisMake()->BaseApi()->url(admin_url('api/workflow/cancel'))->method('post')->data([ + 'subject_type' => EmployeeSignRepair::class, + 'subject_id' => '${id}', + ])) + ->confirmText(__('admin.confirm')) + ->visibleOn('${check_status == '.CheckStatus::Processing->value.'}'), + amisMake()->AjaxAction() + ->label('取消审核') + ->level('link') + ->api(amisMake()->BaseApi()->url(admin_url('api/workflow/cancel'))->method('post')->data([ + 'subject_type' => EmployeeSignRepair::class, + 'subject_id' => '${id}', + ])) + ->confirmText(__('admin.confirm')) + ->visibleOn('${check_status == '.CheckStatus::Processing->value.'}'), + ]) + ]); + + return $this->baseList($crud); + } + + public function form($edit): Form + { + return $this->baseForm()->title('')->body([ + amisMake()->SelectControl()->name('employee_id')->label(__('employee_sign.employee_id')) + ->source(admin_url('api/employees?_all=1&store_id_gt=0&employee_status='.EmployeeStatus::Online->value)) + ->labelField('name') + ->valueField('id') + ->searchable() + ->joinValues(false) + ->extractValue() + ->required(), + amisMake()->DateControl()->format('YYYY-MM-DD HH:mm:ss')->name('date')->label(__('employee_sign_repair.date'))->required(), + amisMake()->SelectControl()->options(SignTime::options())->name('repair_type')->label(__('employee_sign_repair.repair_type'))->required(), + amisMake()->TextControl()->name('reason')->label(__('employee_sign_repair.reason'))->required(), + ]); + } + + public function detail(): Form + { + $detail = amisMake()->Property()->items([ + ]); + return $this->baseDetail()->title('')->body($detail); + } +} diff --git a/app/Admin/Controllers/Store/EmployeeController.php b/app/Admin/Controllers/Store/EmployeeController.php index eb25642..f202f16 100644 --- a/app/Admin/Controllers/Store/EmployeeController.php +++ b/app/Admin/Controllers/Store/EmployeeController.php @@ -37,13 +37,12 @@ class EmployeeController extends AdminController ]), ])) ->columns([ - amisMake()->TableColumn()->name('id')->label(__('employee.id')), amisMake()->TableColumn()->name('store.title')->label(__('employee.store_id')), amisMake()->TableColumn()->name('name')->label(__('employee.name')), // amisMake()->TableColumn()->name('store.master_id')->label(__('store.master_id'))->set('type', 'tpl')->tpl('${store.master_id == id ? "店长" : "--"}'), amisMake()->TableColumn()->name('phone')->label(__('employee.phone')), $this->rowActions([ - $this->rowDeleteButton()->visible($user->can('admin.store.employees.delete')), + $this->rowDeleteButton()->hiddenOn('${store.master_id == id}')->visible($user->can('admin.store.employees.delete')), ]), ]); diff --git a/app/Admin/Controllers/System/WorkflowController.php b/app/Admin/Controllers/System/WorkflowController.php index 58f78a3..dca3c8e 100644 --- a/app/Admin/Controllers/System/WorkflowController.php +++ b/app/Admin/Controllers/System/WorkflowController.php @@ -9,6 +9,9 @@ use App\Models\Employee; use App\Models\Keyword; use Slowlyo\OwlAdmin\Renderers\Form; use Slowlyo\OwlAdmin\Renderers\Page; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; +use Illuminate\Database\Eloquent\Relations\Relation; /** * 审核流程管理 @@ -86,6 +89,51 @@ class WorkflowController extends AdminController return $this->baseDetail()->title('')->body($detail); } + public function apply(Request $request) + { + $type = $request->input('subject_type'); + $id = $request->input('subject_id'); + $subject = Relation::getMorphedModel($type); + $user = $request->input('user'); + $model = (new $subject)->findOrFail($id); + if ($request->filled('user')) { + $user = Employee::findOrFail($request->input('user')); + } else { + $user = $model->employee; + } + + try { + DB::beginTransaction(); + if (!$this->service->apply($model, $user)) { + return $this->response()->fail($this->service->getError()); + } + DB::commit(); + return $this->response()->success(); + } catch (\Exception $e) { + DB::rollBack(); + return $this->response()->fail($e->getMessage()); + } + } + + public function cancel(Request $request) + { + $type = $request->input('subject_type'); + $id = $request->input('subject_id'); + $model = (new $type)->findOrFail($id); + + try { + DB::beginTransaction(); + if (!$this->service->cancel($model)) { + return $this->response()->fail($this->service->getError()); + } + DB::commit(); + return $this->response()->success(); + } catch (\Exception $e) { + DB::rollBack(); + return $this->response()->fail($e->getMessage()); + } + } + public function getJobOptions() { if (! $this->jobOptions) { diff --git a/app/Admin/Filters/EmployeeFilter.php b/app/Admin/Filters/EmployeeFilter.php index 9c0d4ce..c6a1a53 100644 --- a/app/Admin/Filters/EmployeeFilter.php +++ b/app/Admin/Filters/EmployeeFilter.php @@ -27,6 +27,11 @@ class EmployeeFilter extends ModelFilter $this->where('store_id', $key); } + public function storeIdGt($key) + { + $this->where('store_id', '>', 0); + } + public function masterStore($key) { $this->where('master_store_id', $key); diff --git a/app/Admin/Filters/EmployeeSignLogFilter.php b/app/Admin/Filters/EmployeeSignLogFilter.php new file mode 100644 index 0000000..eb75fde --- /dev/null +++ b/app/Admin/Filters/EmployeeSignLogFilter.php @@ -0,0 +1,27 @@ +where('employee_id', $key); + } + + public function storeId($key) + { + $this->where('store_id', $key); + } + + public function date($key) + { + $date = Carbon::createFromFormat('Y-m-d', $key); + $this->whereBetween('time', [$date->copy()->startOfDay(), $date->copy()->endOfDay()]); + } +} diff --git a/app/Admin/Filters/EmployeeSignRepairFilter.php b/app/Admin/Filters/EmployeeSignRepairFilter.php new file mode 100644 index 0000000..1025e98 --- /dev/null +++ b/app/Admin/Filters/EmployeeSignRepairFilter.php @@ -0,0 +1,27 @@ +where('employee_id', $key); + } + + public function storeId($key) + { + $this->where('store_id', $key); + } + + public function date($key) + { + $date = Carbon::createFromFormat('Y-m-d', $key); + $this->whereBetween('time', [$date->copy()->startOfDay(), $date->copy()->endOfDay()]); + } +} diff --git a/app/Admin/Middleware/CheckPermission.php b/app/Admin/Middleware/CheckPermission.php index ff61c02..664444a 100644 --- a/app/Admin/Middleware/CheckPermission.php +++ b/app/Admin/Middleware/CheckPermission.php @@ -36,7 +36,7 @@ class CheckPermission return $next($request); } - return Admin::response()->fail(__('admin.unauthorized')); + return Admin::response()->fail(__('admin.unauthorized'), ['route' => $request->route()->getName()]); } protected function checkRoutePermission(Request $request): bool diff --git a/app/Admin/Services/EmployeeSignRepairService.php b/app/Admin/Services/EmployeeSignRepairService.php new file mode 100644 index 0000000..1d2d4cf --- /dev/null +++ b/app/Admin/Services/EmployeeSignRepairService.php @@ -0,0 +1,68 @@ +resloveData($data); + + $validate = $this->validate($data); + if ($validate !== true) { + $this->setError($validate); + + return false; + } + + $this->modelName::create($data); + + return true; + } + + public function resloveData($data, $model = null) + { + // 获取员工所在的门店 + if (!isset($data['store_id']) && isset($data['employee_id'])) { + $data['store_id'] = Employee::where('id', $data['employee_id'])->value('store_id'); + } + return $data; + } + + public function validate($data, $model = null) + { + $createRules = [ + 'date' => ['required'], + 'store_id' => ['required'], + 'employee_id' => ['required'], + 'reason' => ['required'], + 'repair_type' => ['required'], + ]; + $updateRules = []; + $message = [ + 'date.required' => __('employee_sign.date') . '必填', + 'store_id.required' => __('employee_sign.store_id') . '必填', + 'employee_id.required' => __('employee_sign.employee_id') . '必填', + 'reason.required' => __('employee_sign.reason') . '必填', + 'repair_type.required' => __('employee_sign.repair_type') . '必填', + ]; + $validator = Validator::make($data, $model ? $updateRules : $createRules, $message); + if ($validator->fails()) { + return $validator->errors()->first(); + } + return true; + } +} \ No newline at end of file diff --git a/app/Admin/Services/EmployeeSignService.php b/app/Admin/Services/EmployeeSignService.php index 03aa0f9..d7acfcc 100644 --- a/app/Admin/Services/EmployeeSignService.php +++ b/app/Admin/Services/EmployeeSignService.php @@ -9,6 +9,7 @@ use App\Models\EmployeeRest; use App\Models\Employee; use App\Enums\SignType; use App\Enums\SignStatus; +use App\Enums\SignTime; class EmployeeSignService extends BaseService { @@ -26,6 +27,7 @@ class EmployeeSignService extends BaseService $date = now()->subDay(); $start = $date->copy()->startOfDay(); $end = $date->copy()->endOfDay(); + // 打卡日志 $list = EmployeeSignLog::whereBetween('time', [$start, $end])->get(); // 休息的员工 $restEmployeeIds = EmployeeRest::whereBetWeen('date', [$start, $end])->pluck('employee_id'); @@ -37,18 +39,18 @@ class EmployeeSignService extends BaseService $status = 0; // 外勤打卡-事由 $remarks = null; - // 上班时间: 12:00 前打卡的都算 + // 上班打卡 $firstTime = null; - if ($item = $logs->where('time', '<=', $date->format('Y-m-d 12:00'))->sortBy('time')->first()) { + if ($item = $logs->where('sign_time', SignTime::Morning)->sortBy('time')->first()) { $firstTime = $item->time; $status ++; if ($item->sign_type == SignType::Outside) { $remarks = $item->remarks; } } - // 下班时间: 24:00 前打卡的都算 + // 下班打卡 $lastTime = null; - if ($item = $logs->where('time', '<=', $date->format('Y-m-d 24:00'))->sortByDesc('time')->first()) { + if ($item = $logs->where('sign_time', SignTime::Afternoon)->sortByDesc('time')->first()) { $lastTime = $item->time; $status ++; if ($item->sign_type == SignType::Outside) { @@ -56,7 +58,7 @@ class EmployeeSignService extends BaseService } } // 打卡类型 - $type = SignType::Absent; + $type = SignType::None; if ($status > 0) { $type = $logs->where('sign_type', SignType::Outside)->count() > 0 ? SignType::Outside : SignType::Normal; } diff --git a/app/Admin/Services/StoreService.php b/app/Admin/Services/StoreService.php index db5aa72..013e41b 100644 --- a/app/Admin/Services/StoreService.php +++ b/app/Admin/Services/StoreService.php @@ -31,7 +31,7 @@ class StoreService extends BaseService $model = $this->modelName::create($data); // 绑定店长 - // Employee::where('id', $data['master_id'])->update(['store_id' => $model->id]); + Employee::where('id', $data['master_id'])->update(['store_id' => $model->id]); return true; } @@ -48,9 +48,9 @@ class StoreService extends BaseService } // 还原以前的店长 - // if (isset($data['master_id']) && $model->master_id != $data['master_id']) { - // Employee::where('id', $model->master_id)->update(['store_id' => 0]); - // } + if (isset($data['master_id']) && $model->master_id != $data['master_id']) { + Employee::where('id', $model->master_id)->update(['store_id' => 0]); + } return $model->update($data); } diff --git a/app/Admin/Services/WorkFlowService.php b/app/Admin/Services/WorkFlowService.php index fcf303f..87f3e36 100644 --- a/app/Admin/Services/WorkFlowService.php +++ b/app/Admin/Services/WorkFlowService.php @@ -2,12 +2,11 @@ namespace App\Admin\Services; -use App\Enums\CheckType; -use App\Models\Employee; -use App\Models\Keyword; -use App\Models\Workflow; +use App\Enums\{CheckType, CheckStatus}; +use App\Models\{Employee, Store, Keyword, Workflow, WorkflowLog}; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; +use App\Contracts\Checkable; class WorkFlowService extends BaseService { @@ -17,10 +16,95 @@ class WorkFlowService extends BaseService protected string $modelFilterName = ''; + /** + * 发起审核申请 + * 1. 待审核 Model 实现 Checkable + * 2. 生成全部的审核流程 + * + * @param Checkable $subject 待审核记录 + * @param Employee $user 申请人 + * + * @return boolean true: 成功, false: 失败, $this->getError(): 错误消息 + */ + public function apply(Checkable $subject, Employee $user) + { + if ($subject->check_status === CheckStatus::Success->value) { + return $this->setError('已经审核通过'); + } + if ($subject->check_status === CheckStatus::Processing->value) { + return $this->setError('正在审核中'); + } + $result = $subject->checkApplyPre(); + if ($result !== true) { + return $this->setError($result); + } + + $workflow = Workflow::where('key', $subject->getCheckKey())->first(); + // 没有配置审核流程, 直接通过 + if (!$workflow || !$workflow->config) { + $subject->checkSuccess(); + return true; + } + $jobs = Keyword::where('parent_key', 'job')->get(); + $config = collect($workflow->config)->sortBy('sort'); + $batchId = WorkflowLog::max('id') + 1; + $subjectData = $subject->toArray(); + foreach($config as $item) { + $checkValue = ''; + $checkName = ''; + // 职位审核 + if ($item['type'] == CheckType::Job->value) { + // 没有门店, 则跳过 + if (!$user->store_id) { + continue; + } + // 所属门店的职位审核 + $store = Store::findOrFail($user->store_id); + $job = $jobs->firstWhere('key', $item['value']); + $checkValue = $store->id . '-' . $job->key; + $checkName = $store->title . '-' . $job->name; + } + // 指定用户审核 + else if ($item['type'] == CheckType::User->value) { + $checkUser = Employee::findOrFail($item['value']); + $checkValue = $checkUser->id; + $checkName = $checkUser->name; + } else { + return $this->setError('未知的审核类型: ' . $item['type']); + break; + } + $subject->workflows()->create([ + 'batch_id' => $batchId, + 'check_type' => $item['type'], + 'check_value' => $checkValue, + 'check_name' => $checkName, + 'user_id' => $user->id, + 'subject_data' => $subjectData, + 'sort' => $item['sort'] + ]); + } + $subject->checkApply(); + return true; + } + + /** + * 取消审核 + */ + public function cancel(Checkable $subject) + { + $subject->workflows()->whereIn('check_status', [CheckStatus::None, CheckStatus::Processing])->delete(); + $subject->checkCancel(); + return true; + } + public function resloveData($data, $model = null) { if (isset($data['config'])) { foreach ($data['config'] as $key => &$item) { + if (!$item) { + $data['config'] = null; + break; + } $item['title'] = match ($item['type']) { CheckType::Job->value => CheckType::Job->text(), CheckType::User->value => CheckType::User->text(), @@ -45,6 +129,7 @@ class WorkFlowService extends BaseService $createRules = [ 'key' => ['required', Rule::unique('workflows', 'key')], 'name' => ['required'], + 'config' => ['required', 'array'], ]; $updateRules = [ 'key' => [Rule::unique('workflows', 'key')->ignore($model?->id)], diff --git a/app/Admin/routes.php b/app/Admin/routes.php index 816eb40..309bd50 100644 --- a/app/Admin/routes.php +++ b/app/Admin/routes.php @@ -5,6 +5,8 @@ use App\Admin\Controllers\Finance\LedgerController; use App\Admin\Controllers\Hr\EmployeeController; use App\Admin\Controllers\Hr\RestController; use App\Admin\Controllers\Hr\SignController; +use App\Admin\Controllers\Hr\SignLogController; +use App\Admin\Controllers\Hr\SignRepairController; use App\Admin\Controllers\Store\DeviceController; use App\Admin\Controllers\Store\EmployeeController as StoreEmployeeController; use App\Admin\Controllers\Store\StoreController; @@ -27,6 +29,8 @@ Route::group([ $router->resource('index', \App\Admin\Controllers\HomeController::class); + $router->get('dashboard', [\App\Admin\Controllers\HomeController::class, 'index'])->name('home'); + /* |-------------------------------------------------------------------------- | 门店管理 @@ -71,6 +75,8 @@ Route::group([ $router->resource('rests', RestController::class)->only(['index', 'create', 'store', 'destroy']); // 打卡情况 $router->resource('signs', SignController::class)->only(['index', 'show']); + // 补卡申请 + $router->resource('repairs', SignRepairController::class); }); /* @@ -140,6 +146,10 @@ Route::group([ ], function (Router $router) { $router->get('stores', [StoreController::class, 'shareList']); $router->get('employees', [EmployeeController::class, 'shareList']); + $router->get('employee-sign-logs', [SignLogController::class, 'shareList']); $router->get('keywords/tree-list', [KeywordController::class, 'getTreeList'])->name('api.keywords.tree-list'); + + $router->post('workflow/apply', [WorkflowController::class, 'apply']); + $router->post('workflow/cancel', [WorkflowController::class, 'cancel']); }); }); diff --git a/app/Console/Commands/EmployeeSign.php b/app/Console/Commands/EmployeeSign.php new file mode 100644 index 0000000..7c10adc --- /dev/null +++ b/app/Console/Commands/EmployeeSign.php @@ -0,0 +1,40 @@ +signResult(); + DB::commit(); + } catch (\Exception $e) { + DB::rollBack(); + logger('app:employee-sign error'); + logger()->error($e); + } + } +} diff --git a/app/Enums/CheckStatus.php b/app/Enums/CheckStatus.php index 692fbcb..b5b3e27 100644 --- a/app/Enums/CheckStatus.php +++ b/app/Enums/CheckStatus.php @@ -2,13 +2,31 @@ namespace App\Enums; +/** + * 审核状态 + */ enum CheckStatus: int { - case None = 0; - case Processing = 1; - case Success = 2; - case Fail = 3; - case Cancel = 4; + /** + * 未审核 + */ + case None = 1; + /** + * 审核中 + */ + case Processing = 2; + /** + * 审核通过 + */ + case Success = 3; + /** + * 审核不通过 + */ + case Fail = 4; + /** + * 已取消 + */ + case Cancel = 5; public static function options(): array { @@ -25,21 +43,4 @@ enum CheckStatus: int { return data_get(self::options(), $this->value); } - - public static function coplorMap() - { - // 'active' | 'inactive' | 'error' | 'success' | 'processing' | 'warning' | - return [ - self::None->value => 'active', - self::Processing->value => 'processing', - self::Success->value => 'success', - self::Fail->value => 'error', - self::Cancel->value => 'inactive', - ]; - } - - public function color() - { - return data_get(self::coplorMap(), $this->value); - } } diff --git a/app/Enums/CheckType.php b/app/Enums/CheckType.php index df2072c..a0df91d 100644 --- a/app/Enums/CheckType.php +++ b/app/Enums/CheckType.php @@ -2,15 +2,24 @@ namespace App\Enums; +/** + * 审核类型 + */ enum CheckType: string { + /** + * 职位 + */ case Job = 'job'; + /** + * 员工 + */ case User = 'user'; public static function options() { return [ - self::Job->value => '职务', + self::Job->value => '职位', self::User->value => '员工', ]; } diff --git a/app/Enums/EmployeeStatus.php b/app/Enums/EmployeeStatus.php index 08d76ba..4413f11 100644 --- a/app/Enums/EmployeeStatus.php +++ b/app/Enums/EmployeeStatus.php @@ -7,7 +7,13 @@ namespace App\Enums; */ enum EmployeeStatus: int { + /** + * 在职 + */ case Online = 1; + /** + * 离职 + */ case Offline = 2; public static function map() diff --git a/app/Enums/SignTime.php b/app/Enums/SignTime.php new file mode 100644 index 0000000..fe64f92 --- /dev/null +++ b/app/Enums/SignTime.php @@ -0,0 +1,31 @@ +value => '上班', + self::Afternoon->value => '下班', + ]; + } + + public function text() + { + return data_get(self::options(), $this->value); + } +} diff --git a/app/Enums/SignType.php b/app/Enums/SignType.php index 426af95..61d3b91 100644 --- a/app/Enums/SignType.php +++ b/app/Enums/SignType.php @@ -2,18 +2,30 @@ namespace App\Enums; +/** + * 打卡类型 + */ enum SignType: int { + /** + * 正常打卡 + */ case Normal = 1; + /** + * 外勤打卡 + */ case Outside = 2; - case Absent = 3; + /** + * 未打卡 + */ + case None = 3; public static function options() { return [ self::Normal->value => '正常打卡', self::Outside->value => '外勤打卡', - self::Absent->value => '旷工', + self::None->value => '未打卡', ]; } @@ -28,8 +40,8 @@ enum SignType: int 'label' => self::Outside->text(), 'color' => '#d97116', ], - self::Absent->value => [ - 'label' => self::Absent->text(), + self::None->value => [ + 'label' => self::None->text(), 'color' => '#a61922', ], ]; diff --git a/app/Models/EmployeeSignLog.php b/app/Models/EmployeeSignLog.php index 63c907d..7db0860 100644 --- a/app/Models/EmployeeSignLog.php +++ b/app/Models/EmployeeSignLog.php @@ -2,28 +2,36 @@ namespace App\Models; -use App\Enums\SignType; +use App\Enums\{SignType, SignTime}; use Illuminate\Database\Eloquent\Model; use App\Traits\HasDateTimeFormatter; use Illuminate\Database\Eloquent\Factories\HasFactory; +use EloquentFilter\Filterable; +use App\Admin\Filters\EmployeeSignLogFilter; /** * 员工-打卡流水 */ class EmployeeSignLog extends Model { - use HasDateTimeFormatter, HasFactory; + use HasDateTimeFormatter, HasFactory, Filterable; protected $table = 'employee_sign_logs'; - protected $fillable = ['store_id', 'employee_id', 'sign_type', 'remarks', 'position', 'time']; + protected $fillable = ['store_id', 'employee_id', 'sign_type', 'sign_time', 'remarks', 'position', 'time']; protected $casts = [ 'sign_type' => SignType::class, + 'sign_time' => SignTime::class, 'position' => 'json', 'time' => 'datetime', ]; + public function modelFilter() + { + return \App\Admin\Filters\EmployeeSignRepairFilter::class; + } + public function store() { return $this->belongsTo(Store::class, 'store_id'); diff --git a/app/Models/EmployeeSignRepair.php b/app/Models/EmployeeSignRepair.php new file mode 100644 index 0000000..4cebd02 --- /dev/null +++ b/app/Models/EmployeeSignRepair.php @@ -0,0 +1,43 @@ + 'date:Y-m-d', + 'checked_at' => 'datetime', + 'repair_type' => SignTime::class + ]; + + public function modelFilter() + { + return EmployeeSignLogFilter::class; + } + + public function store() + { + return $this->belongsTo(Store::class, 'store_id'); + } + + public function employee() + { + return $this->belongsTo(Employee::class, 'employee_id'); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f201014..6d2441d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -31,6 +31,7 @@ class AppServiceProvider extends ServiceProvider Relation::enforceMorphMap( collect([ \App\Models\AdminUser::class, + \App\Models\EmployeeSignRepair::class, ])->mapWithKeys(fn ($model) => [(new $model)->getTable() => $model])->all() ); } diff --git a/app/Traits/HasCheckable.php b/app/Traits/HasCheckable.php new file mode 100644 index 0000000..fc5c729 --- /dev/null +++ b/app/Traits/HasCheckable.php @@ -0,0 +1,114 @@ +update([ + 'check_status' => CheckStatus::Processing, + ]); + } + + public function checkSuccess() + { + $this->update([ + 'check_status' => CheckStatus::Success, + 'checked_at' => now(), + ]); + } + + public function checkFail() + { + $this->update([ + 'check_status' => CheckStatus::Fail, + ]); + } + + public function checkCancel() + { + $this->update([ + 'check_status' => CheckStatus::Cancel, + ]); + } + + public function getCheckKey() + { + return Str::snake(class_basename(__CLASS__)); + } + + public function workflows() + { + return $this->morphMany(WorkflowLog::class, 'subject'); + } + + /** + * 是否允许修改 + * + * @return bool + */ + public function canEdit(): bool + { + return !($this->check_status === CheckStatus::Processing); + } + + /** + * 查询审核通过的记录 + * + * @param Builder $q + */ + public function scopeChecked(Builder $q): Builder + { + return $q->where('check_status', CheckStatus::Success); + } + + /** + * 审核成功前, 会调用该方法 + * 返回错误信息 阻止审核通过 + */ + public function checkSuccessPre() + { + return true; + } + + /** + * 审核申请前, 会调用此方法 + * 返回错误信息 阻止申请 + */ + public function checkApplyPre() + { + return true; + } + + public static function boot() + { + parent::boot(); + static::updating(function ($model) { + // 修改审核通过的内容, 需要重新审核 + if (isset($model->checkListen) && count($model->checkListen) > 0 && $model->isDirty($model->checkListen) && $model->check_status === CheckStatus::Success) { + $model->check_status = CheckStatus::None; + $model->checked_at = null; + } + }); + + static::deleting(function ($model) { + // 删除审核记录 + $model->workflows()->delete(); + }); + } + + /** + * 是否允许删除,已取消或已拒绝的 + * @return bool + */ + public function canDel(): bool + { + return $this->check_status === CheckStatus::Cancel || $this->check_status === CheckStatus::Fail || $this->check_status === CheckStatus::None; + } +} diff --git a/database/factories/EmployeeFactory.php b/database/factories/EmployeeFactory.php index 7150793..2487345 100644 --- a/database/factories/EmployeeFactory.php +++ b/database/factories/EmployeeFactory.php @@ -24,18 +24,18 @@ class EmployeeFactory extends Factory $name = $faker->name; $phone = $faker->phoneNumber(); - // $adminUser = AdminUser::create([ - // 'username' => $phone, - // 'password' => bcrypt($phone), - // 'name' => $name, - // ]); - // $adminUser = AdminUser::first(); + $adminUser = AdminUser::create([ + 'username' => $phone, + // 123456 + 'password' => '$12$exmmsMLDmZO4aNug/a4CquCU8237FNrwMa4S9EawhvO0XaIqZSD9i', + 'name' => $name, + ]); return [ 'name' => $name, 'phone' => $phone, 'prize_images' => ['https://via.placeholder.com/100x100.png'], 'skill_images' => ['https://via.placeholder.com/100x100.png'], - // 'admin_user_id' => $adminUser->id, + 'admin_user_id' => $adminUser->id, 'join_at' => now(), ]; } diff --git a/database/factories/EmployeeSignLogFactory.php b/database/factories/EmployeeSignLogFactory.php index c08d1d9..583a58e 100644 --- a/database/factories/EmployeeSignLogFactory.php +++ b/database/factories/EmployeeSignLogFactory.php @@ -5,7 +5,7 @@ namespace Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; use App\Models\EmployeeSignLog; use App\Models\Employee; -use App\Enums\SignType; +use App\Enums\{SignType, SignTime}; /** * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\EmployeeSign> @@ -21,11 +21,13 @@ class EmployeeSignLogFactory extends Factory public function definition(): array { $employee = Employee::where('store_id', '>', 0)->inRandomOrder()->first(); - $type = $this->faker->randomElement(SignType::class); + $type = $this->faker->randomElement([SignType::Normal, SignType::Outside]); + $time = $this->faker->randomElement(SignTime::class); return [ 'store_id' => $employee->store_id, 'employee_id' => $employee->id, 'sign_type' => $type, + 'sign_time' => $time, 'remarks' => $type == SignType::Outside ? '我在外面的' : '', 'position' => ['province' => '重庆', 'city' => '重庆市', 'address' => '重庆市南川区东城街道办事处东环路三号'], 'time' => $this->faker->dateTimeBetween('-7 days', 'now') diff --git a/database/factories/StoreFactory.php b/database/factories/StoreFactory.php index e63ecb5..2f9cc25 100644 --- a/database/factories/StoreFactory.php +++ b/database/factories/StoreFactory.php @@ -39,7 +39,7 @@ class StoreFactory extends Factory { return $this->afterMaking(function (Store $model) { })->afterCreating(function (Store $model) { - // Employee::where('id', $model->master_id)->update(['store_id' => $model->id]); + Employee::where('id', $model->master_id)->update(['store_id' => $model->id]); }); } } diff --git a/database/migrations/2024_03_27_113404_create_workflows_table.php b/database/migrations/2024_03_27_113404_create_workflows_table.php index 9923afc..01959c2 100644 --- a/database/migrations/2024_03_27_113404_create_workflows_table.php +++ b/database/migrations/2024_03_27_113404_create_workflows_table.php @@ -3,6 +3,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; +use App\Enums\CheckStatus; return new class extends Migration { @@ -34,7 +35,7 @@ return new class extends Migration $table->unsignedBigInteger('check_user_id')->nullable()->comment('实际审核人(admin_users.id)'); $table->timestamp('checked_at')->nullable()->comment('审核时间'); $table->string('remarks')->nullable()->comment('审核备注'); - $table->unsignedTinyInteger('check_status')->default(0)->comment('审核状态(0: 待审核, 1: 审核通过, 2: 审核不通过)'); + $table->unsignedTinyInteger('check_status')->default(CheckStatus::None->value)->comment('审核状态'); $table->unsignedInteger('sort')->default(0)->comment('顺序(asc)'); $table->timestamps(); diff --git a/database/migrations/2024_03_27_140744_create_employee_sign_table.php b/database/migrations/2024_03_27_140744_create_employee_sign_table.php index a590437..94cdc82 100644 --- a/database/migrations/2024_03_27_140744_create_employee_sign_table.php +++ b/database/migrations/2024_03_27_140744_create_employee_sign_table.php @@ -3,6 +3,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; +use App\Enums\CheckStatus; return new class extends Migration { @@ -31,6 +32,7 @@ return new class extends Migration $table->foreignId('store_id')->comment('门店, stores.id'); $table->foreignId('employee_id')->comment('员工, employees.id'); $table->unsignedInteger('sign_type')->default(1)->comment('类别(1: 正常打卡, 2: 外勤)'); + $table->unsignedInteger('sign_time')->default(1)->comment('打卡时间(1: 上班, 2: 下班)'); $table->string('remarks')->nullable()->comment('备注'); $table->json('position')->comment('打卡位置'); $table->timestamp('time')->comment('打卡时间'); @@ -55,7 +57,7 @@ return new class extends Migration $table->string('reason')->comment('补卡原因'); $table->unsignedInteger('repair_type')->default(1)->comment('上班/下班 补卡'); - $table->unsignedInteger('check_status')->default(0)->comment('审核状态'); + $table->unsignedInteger('check_status')->default(CheckStatus::None->value)->comment('审核状态'); $table->timestamp('checked_at')->nullable()->comment('审核通过时间'); $table->timestamps(); diff --git a/database/seeders/AdminPermissionSeeder.php b/database/seeders/AdminPermissionSeeder.php index 4cea8df..7d5de0e 100644 --- a/database/seeders/AdminPermissionSeeder.php +++ b/database/seeders/AdminPermissionSeeder.php @@ -121,7 +121,13 @@ class AdminPermissionSeeder extends Seeder 'icon' => '', 'uri' => '/hr/signs', 'resource' => ['list', 'view'], - ] + ], + 'repairs' => [ + 'name' => '补卡申请', + 'icon' => '', + 'uri' => '/hr/repairs', + 'resource' => true, + ], ], ], diff --git a/database/seeders/AdminSeeder.php b/database/seeders/AdminSeeder.php index 30c4c81..c288d42 100644 --- a/database/seeders/AdminSeeder.php +++ b/database/seeders/AdminSeeder.php @@ -14,6 +14,7 @@ class AdminSeeder extends Seeder */ public function run() { + $now = now(); // 创建初始用户 DB::table('admin_users')->truncate(); DB::table('admin_users')->insert([ @@ -21,6 +22,14 @@ class AdminSeeder extends Seeder 'password' => bcrypt('admin'), 'name' => 'Administrator', ]); + DB::table('employees')->insert([ + 'name' => 'admin', + 'phone' => '12345678900', + 'admin_user_id' => 1, + 'join_at' => $now, + 'created_at' => $now, + 'updated_at' => $now, + ]); // 创建初始角色 DB::table('admin_roles')->truncate(); diff --git a/database/seeders/EmployeeSeeder.php b/database/seeders/EmployeeSeeder.php index de92a6e..77f7401 100644 --- a/database/seeders/EmployeeSeeder.php +++ b/database/seeders/EmployeeSeeder.php @@ -17,7 +17,7 @@ class EmployeeSeeder extends Seeder public function run(): void { DB::table('employee_jobs')->truncate(); - Employee::truncate(); + Employee::where('admin_user_id', '!=', 1)->delete(); (new EmployeeFactory)->count(100)->create(['admin_user_id' => 1]); Store::truncate(); diff --git a/database/seeders/WorkflowSeeder.php b/database/seeders/WorkflowSeeder.php index 33c424d..9148524 100644 --- a/database/seeders/WorkflowSeeder.php +++ b/database/seeders/WorkflowSeeder.php @@ -12,6 +12,13 @@ class WorkflowSeeder extends Seeder */ public function run(): void { + $now = now(); + $config = [ + ["sort" => 1, "type" => "user", "user" => 1, "title" => "员工", "value" => 1, "subTitle" => "Admin"], + ]; Workflow::truncate(); + Workflow::insert([ + ['key' => 'employee_sign_repair', 'name' => '补卡申请', 'config' => json_encode($config), 'created_at' => now(), 'updated_at' => now()] + ]); } } diff --git a/lang/zh_CN/admin.php b/lang/zh_CN/admin.php index fddab02..ec0f1ee 100644 --- a/lang/zh_CN/admin.php +++ b/lang/zh_CN/admin.php @@ -24,6 +24,7 @@ return [ 'delete' => '删除', 'copy' => '复制', 'confirm_delete' => '确认删除选中项?', + 'confirm' => '是否确定?', 'back' => '返回', 'reset' => '重置', 'search' => '搜索', diff --git a/lang/zh_CN/employee_sign.php b/lang/zh_CN/employee_sign.php index 6a518ad..ede0868 100644 --- a/lang/zh_CN/employee_sign.php +++ b/lang/zh_CN/employee_sign.php @@ -6,6 +6,7 @@ return [ 'updated_at' => '更新时间', 'rest' => '休息日', + 'log' => '打卡日志', 'date' => '日期', 'store_id' => '门店', diff --git a/lang/zh_CN/employee_sign_log.php b/lang/zh_CN/employee_sign_log.php new file mode 100644 index 0000000..7fddd64 --- /dev/null +++ b/lang/zh_CN/employee_sign_log.php @@ -0,0 +1,14 @@ + 'ID', + 'created_at' => '创建时间', + 'updated_at' => '更新时间', + 'store_id' => '门店', + 'employee_id' => '员工', + 'sign_type' => '打卡类型', + 'sign_time' => '上班/下班', + 'time' => '打卡时间', + 'remarks' => '事由', + 'position' => '位置', +]; diff --git a/lang/zh_CN/employee_sign_repair.php b/lang/zh_CN/employee_sign_repair.php new file mode 100644 index 0000000..ed07be2 --- /dev/null +++ b/lang/zh_CN/employee_sign_repair.php @@ -0,0 +1,15 @@ + 'ID', + 'created_at' => '创建时间', + 'updated_at' => '更新时间', + + 'date' => '补卡日期', + 'store_id' => '门店', + 'employee_id' => '员工', + 'reason' => '补卡原因', + 'repair_type' => '上班/下班', + 'check_status' => '审核状态', + 'checked_at' => '审核通过时间', +];