diff --git a/app/Admin/Filters/EmployeeSignFilter.php b/app/Admin/Filters/EmployeeSignFilter.php index ce8f049..099c0ab 100644 --- a/app/Admin/Filters/EmployeeSignFilter.php +++ b/app/Admin/Filters/EmployeeSignFilter.php @@ -37,6 +37,14 @@ class EmployeeSignFilter extends ModelFilter $this->whereBetween('date', [$start, $end]); } + public function month($key) + { + $time = Carbon::createFromFormat('Y-m', $key); + $start = $time->copy()->startOfMonth(); + $end = $time->copy()->endOfMonth(); + $this->whereBetween('date', [$start, $end]); + } + public function signType($key) { $this->whereIn('sign_type', is_array($key) ? $key : explode(',', $key)); diff --git a/app/Admin/Services/EmployeeSignRepairService.php b/app/Admin/Services/EmployeeSignRepairService.php index a4e504b..5986554 100644 --- a/app/Admin/Services/EmployeeSignRepairService.php +++ b/app/Admin/Services/EmployeeSignRepairService.php @@ -4,7 +4,7 @@ namespace App\Admin\Services; use App\Admin\Filters\EmployeeSignRepairFilter; use App\Models\Employee; -use App\Models\EmployeeSignRepair; +use App\Models\{EmployeeSignRepair, EmployeeSign}; use App\Models\WorkflowCheck; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; @@ -17,22 +17,6 @@ class EmployeeSignRepairService extends BaseService protected string $modelFilterName = EmployeeSignRepairFilter::class; - public function store($data): bool - { - $data = $this->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) { // 获取员工所在的门店 @@ -43,28 +27,16 @@ class EmployeeSignRepairService extends BaseService return $data; } - public function preDelete(array $ids): void - { - // 删除审核流程记录 - WorkflowCheck::where('subject_type', (new WorkflowLog)->getMorphClass())->whereIn('subject_id', $ids)->delete(); - } - public function validate($data, $model = null) { - // 验证申请时间是否重叠 - // todo - $unique = Rule::unique('employee_sign_repairs', 'date') - ->where('employee_id', data_get($data, 'employee_id', $model?->employee_id)) - ->where('repair_type', data_get($data, 'repair_type', $model?->repair_type)); $createRules = [ 'employee_id' => ['required'], 'repair_type' => ['required'], - 'date' => ['required', $unique], + 'date' => ['required'], 'store_id' => ['required'], 'reason' => ['required'], ]; $updateRules = [ - 'date' => [$unique->ignore($model?->id)], ]; $message = [ 'date.required' => __('employee_sign_repair.date').'必填', @@ -78,6 +50,8 @@ class EmployeeSignRepairService extends BaseService if ($validator->fails()) { return $validator->errors()->first(); } + // todo 已经打卡不能申请 + // todo 验证申请时间是否重复 return true; } diff --git a/app/Admin/Services/EmployeeSignService.php b/app/Admin/Services/EmployeeSignService.php index cbc70a9..3bc9d88 100644 --- a/app/Admin/Services/EmployeeSignService.php +++ b/app/Admin/Services/EmployeeSignService.php @@ -10,6 +10,7 @@ use App\Models\Employee; use App\Models\EmployeeRest; use App\Models\EmployeeSign; use App\Models\EmployeeSignLog; +use Carbon\Carbon; class EmployeeSignService extends BaseService { @@ -63,8 +64,6 @@ class EmployeeSignService extends BaseService $type = $logs->where('sign_type', SignType::Outside)->count() > 0 ? SignType::Outside : SignType::Normal; } $attributes = [ - 'date' => $date, - 'store_id' => $employee->store_id, 'sign_type' => $type, 'first_time' => $firstTime, 'last_time' => $lastTime, @@ -75,7 +74,60 @@ class EmployeeSignService extends BaseService }, 'remarks' => $remarks, ]; - $employee->signs()->create($attributes); + $employee->signs()->updateOrCreate([ + 'date' => $date, + 'store_id' => $employee->store_id, + ], $attributes); } } + + /** + * 打卡 + * + * @param Employee $user 用户 + * @param SignTime $time 上班/下班 打卡 + * @param mixed $date 打卡时间 + * @param array $options {type: 正常/外勤 打卡, remarks: 备注, position: 位置} + * @return boolean + */ + public function signDay(Employee $user, SignTime $time, $date = '', array $options = []) + { + $date = $date ?: now(); + $log = EmployeeSignLog::create([ + 'store_id' => $user->store_id, + 'employee_id' => $user->id, + 'sign_time' => $time, + 'time' => $date, + 'sign_type' => data_get($options, 'type'), + 'remarks' => data_get($options, 'remarks'), + 'position' => data_get($options, 'position'), + ]); + + // 更新打卡情况 + $sign = EmployeeSign::firstOrCreate([ + 'date' => $date->format('Y-m-d'), + 'store_id' => $user->store_id, + 'employee_id' => $user->id, + ]); + $sign->sign_type = $log->sign_type; + if ($time == SignTime::Morning) { + $sign->first_time = $log->time; + } else if ($time == SignTime::Afternoon) { + $sign->last_time = $log->time; + } + $sign->sign_status = SignStatus::Lose; + if ($sign->first_time && $sign->last_time) { + $sign->sign_status = SignStatus::Normal; + } + $sign->remarks = $log->remarks; + + $sign->save(); + + return true; + } + + public function hasRest(Employee $user, $date) + { + return EmployeeRest::where('employee_id', $user->id)->where('date', $date)->exists(); + } } diff --git a/app/Admin/Services/WorkFlowService.php b/app/Admin/Services/WorkFlowService.php index 2a2b38c..4f23596 100644 --- a/app/Admin/Services/WorkFlowService.php +++ b/app/Admin/Services/WorkFlowService.php @@ -100,6 +100,8 @@ class WorkFlowService extends BaseService 'check_remarks' => data_get($options, 'remarks'), ]); + $check->subject->checkSuccess(); + return true; } diff --git a/app/Enums/SignStatus.php b/app/Enums/SignStatus.php index 6703bbd..61bdd83 100644 --- a/app/Enums/SignStatus.php +++ b/app/Enums/SignStatus.php @@ -2,10 +2,22 @@ namespace App\Enums; +/** + * 打卡状态 + */ enum SignStatus: int { + /** + * 正常打卡 + */ case Normal = 1; + /** + * 缺卡 + */ case Lose = 2; + /** + * 旷工 + */ case Absent = 3; public static function options() diff --git a/app/Http/Controllers/Api/Hr/SignController.php b/app/Http/Controllers/Api/Hr/SignController.php new file mode 100644 index 0000000..728c532 --- /dev/null +++ b/app/Http/Controllers/Api/Hr/SignController.php @@ -0,0 +1,75 @@ +guard()->user(); + $time = $request->filled('time') ? Carbon::createFromFormat('Y-m', $request->input('time')) : now(); + $list = EmployeeSign::where('employee_id', $user->id)->filter(['month' => $time->format('Y-m')])->get(); + $data = []; + $start = $time->copy()->startOfMonth(); + $end = $time->copy()->endOfMonth(); + do { + $info = $list->where(fn($item) => $item->date->format('Y-m-d') == $start->format('Y-m-d'))->first(); + array_push($data, [ + 'date' => $start->format('Y-m-d'), + 'sign_status' => $info ? $info->sign_status : null, + 'first_time' => $info?->first_time->format('H:i'), + 'last_time' => $info?->last_time->format('H:i'), + ]); + $start->addDay(); + } while(!$end->isSameDay($start)); + + return $data; + } + + public function info(Request $request, EmployeeSignService $service) + { + $user = $this->guard()->user(); + $date = now(); + // 上午: 上班打卡, 下午: 下班打卡 + $time = $date->format('H') <= 12 ? SignTime::Morning : SignTime::Afternoon; + // 根据定位的距离判断, 是否外勤 + $type = SignType::Normal; + // 当前位置不在考勤范围内,请选择外勤打卡 + $description = '已进入考勤范围xx店'; + + return compact('time', 'type', 'description'); + } + + public function store(Request $request, EmployeeSignService $service) + { + $request->validate([ + 'type' => ['required'], + 'time' => ['required'], + ]); + $user = $this->guard()->user(); + $time = SignTime::from($request->input('time')); + try { + DB::beginTransaction(); + if (!$service->signDay($user, $time, now(), $request->only(['remarks', 'position', 'type']))) { + throw new RuntimeException($service->getError()); + } + DB::commit(); + return response('', Response::HTTP_OK); + } catch (\Exception $e) { + DB::rollBack(); + throw new RuntimeException($e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Api/Hr/SignRepairController.php b/app/Http/Controllers/Api/Hr/SignRepairController.php new file mode 100644 index 0000000..114f5d0 --- /dev/null +++ b/app/Http/Controllers/Api/Hr/SignRepairController.php @@ -0,0 +1,114 @@ +guard()->user(); + $list = EmployeeSignRepair::with(['workflow']) + ->where('employee_id', $user->id) + ->filter($request->all()) + ->orderBy('id', 'desc') + ->paginate($request->input('per_page')); + return EmployeeSignRepairResource::collection($list); + } + + public function store(Request $request, EmployeeSignRepairService $service) + { + $user = $this->guard()->user(); + $data = $request->all(); + $data['employee_id'] = $user->id; + + try { + DB::beginTransaction(); + $data = $service->resloveData($data); + $result = $service->validate($data); + if ($result !== true) { + throw new RuntimeException($result); + } + $model = EmployeeSignRepair::create($data); + $workflow = WorkFlowService::make(); + if (!$workflow->apply($model->workflow, $user)) { + throw new RuntimeException($workflow->getError()); + } + + DB::commit(); + return response('', Response::HTTP_OK); + } catch (\Exception $e) { + DB::rollBack(); + throw new RuntimeException($e->getMessage()); + } + } + + public function show($id) + { + $info = EmployeeSignRepair::with(['workflow', 'employee', 'store'])->findOrFail($id); + + return EmployeeSignRepairResource::make($info); + } + + public function update($id, Request $request, EmployeeSignRepairService $service) + { + $user = $this->guard()->user(); + $model = EmployeeSignRepair::with(['workflow'])->where('employee_id', $user->id)->findOrFail($id); + if (!$model->canUpdate()) { + throw new RuntimeException('审核中, 无法修改'); + } + + try { + DB::beginTransaction(); + $data = $service->resloveData($data, $model); + $result = $service->validate($data, $model); + if ($result !== true) { + throw new RuntimeException($result); + } + $model->update($data); + $workflow = WorkFlowService::make(); + if (!$workflow->apply($model->workflow, $user)) { + throw new RuntimeException($workflow->getError()); + } + + DB::commit(); + return response('', Response::HTTP_OK); + } catch (\Exception $e) { + DB::rollBack(); + throw new RuntimeException($e->getMessage()); + } + } + + public function destroy($id, EmployeeSignRepairService $service) + { + $user = $this->guard()->user(); + $model = EmployeeSignRepair::with(['workflow'])->where('employee_id', $user->id)->findOrFail($id); + if (!$model->canUpdate()) { + throw new RuntimeException('审核中, 无法删除'); + } + + try { + DB::beginTransaction(); + if (!$service->delete($id)) { + throw new RuntimeException($service->getError()); + } + + DB::commit(); + return response('', Response::HTTP_OK); + } catch (\Exception $e) { + DB::rollBack(); + throw new RuntimeException($e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Api/WorkflowController.php b/app/Http/Controllers/Api/WorkflowController.php new file mode 100644 index 0000000..89fe5a9 --- /dev/null +++ b/app/Http/Controllers/Api/WorkflowController.php @@ -0,0 +1,104 @@ +validate([ + 'subject_type' => 'required', + ]); + $subjectType = $request->input('subject_type'); + $model = Relation::getMorphedModel($subjectType); + $resource = $this->mapResource($subjectType); + + $user = $request->user(); + $query = $model::query()->with(['workflow']) + ->whereHas('workflow', fn($q) => $q->where('check_status', CheckStatus::Processing)) + ->whereHas('workflow.logs', fn($q) => $q->own($user)) + ->orderBy('created_at', 'desc'); + + $list = $query->paginate($request->input('per_page')); + + return $resource::collection($list); + } + + public function show($id, Request $request) + { + $request->validate([ + 'subject_type' => 'required', + ]); + $subjectType = $request->input('subject_type'); + $model = Relation::getMorphedModel($subjectType); + $resource = $this->mapResource($subjectType); + + $include = ['workflow']; + if ($request->input('include')) { + $explodes = explode(',', $request->input('include')); + $include = array_merge($include, $explodes); + } + $info = $model::query()->with($include)->findOrFail($id); + + return $resource::make($info); + } + + public function logs($id, Request $request) + { + $request->validate([ + 'subject_type' => 'required', + ]); + $check = WorkflowCheck::where('subject_type', $request->input('subject_type'))->where('subject_id', $id)->firstOrFail(); + $logs = $check->logs()->sort()->get(); + + return WorkflowLogResource::collection($logs); + } + + public function check($id, Request $request, WorkFlowService $workFlowService) + { + $request->validate([ + 'subject_type' => 'required', + 'status' => ['required'], + 'remarks' => [Rule::requiredIf(fn() => !$request->input('status'))] + ], [ + 'remarks.required_if' => '未通过原因必填', + ]); + $check = WorkflowCheck::where('subject_type', $request->input('subject_type'))->where('subject_id', $id)->firstOrFail(); + $user = $request->user(); + try { + DB::beginTransaction(); + $log = $check->logs()->where('check_status', CheckStatus::Processing)->first(); + if (!$log) { + throw new RuntimeException('审核已经完成'); + } + if (!$workFlowService->check($user, $log, !!$request->input('status'), ['remarks' => $request->input('remarks')])) { + throw new RuntimeException($workFlowService->getError()); + } + + DB::commit(); + return response('', Response::HTTP_OK); + } catch (\Exception $e) { + DB::rollBack(); + throw new RuntimeException($e->getMessage()); + } + } + + protected function mapResource($key) + { + $map = [ + 'reimbursements' => ReimbursementResource::class, + 'employee_sign_repairs' => EmployeeSignRepairResource::class, + ]; + return data_get($map, $key); + } +} diff --git a/app/Http/Resources/EmployeeSignRepairResource.php b/app/Http/Resources/EmployeeSignRepairResource.php new file mode 100644 index 0000000..a91716f --- /dev/null +++ b/app/Http/Resources/EmployeeSignRepairResource.php @@ -0,0 +1,35 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'date' => $this->date->timestamp, + + 'employee_id' => $this->employee_id, + 'employee' => EmployeeResource::make($this->whenLoaded('employee')), + 'store_id' => $this->store_id, + 'store' => StoreResource::make($this->whenLoaded('store')), + + 'reason' => $this->reason, + 'repair_type' => $this->repair_type, + 'sign_type' => $this->sign_type, + 'outside_remarks' => $this->outside_remarks, + 'created_at' => $this->created_at->timestamp, + + 'workflow_check' => WorkflowCheckResource::make($this->whenLoaded('workflow')), + ]; + } +} diff --git a/app/Http/Resources/WorkflowCheckResource.php b/app/Http/Resources/WorkflowCheckResource.php index cf875d1..f65f7ca 100644 --- a/app/Http/Resources/WorkflowCheckResource.php +++ b/app/Http/Resources/WorkflowCheckResource.php @@ -14,22 +14,12 @@ class WorkflowCheckResource extends JsonResource */ public function toArray(Request $request): array { - $resource = $this->mapResource($this->subject_type); return [ 'check_status' => $this->check_status, 'check_status_text' => $this->check_status?->text(), 'checked_at' => $this->checked_at?->getTimestamp(), 'check_remarks' => (string) $this->check_remarks, - 'subject' => $resource ? $resource::make($this->whenLoaded('subject')) : '', 'logs' => WorkflowLogResource::collection($this->whenLoaded('logs')), ]; } - - protected function mapResource($key) - { - $map = [ - 'reimbursements' => ReimbursementResource::class, - ]; - return data_get($map, $key); - } } diff --git a/app/Models/EmployeeSign.php b/app/Models/EmployeeSign.php index 18029d0..5220bc3 100644 --- a/app/Models/EmployeeSign.php +++ b/app/Models/EmployeeSign.php @@ -27,6 +27,11 @@ class EmployeeSign extends Model 'last_time' => 'datetime', ]; + public function modelFilter() + { + return \App\Admin\Filters\EmployeeSignFilter::class; + } + public function store() { return $this->belongsTo(Store::class, 'store_id'); diff --git a/app/Models/EmployeeSignLog.php b/app/Models/EmployeeSignLog.php index 2a198ce..12589bf 100644 --- a/app/Models/EmployeeSignLog.php +++ b/app/Models/EmployeeSignLog.php @@ -18,7 +18,7 @@ class EmployeeSignLog extends Model protected $table = 'employee_sign_logs'; - protected $fillable = ['store_id', 'employee_id', 'sign_type', 'sign_time', 'remarks', 'position', 'time']; + protected $guarded = []; protected $casts = [ 'sign_type' => SignType::class, diff --git a/app/Models/EmployeeSignRepair.php b/app/Models/EmployeeSignRepair.php index 9476d4a..5e6700b 100644 --- a/app/Models/EmployeeSignRepair.php +++ b/app/Models/EmployeeSignRepair.php @@ -2,7 +2,7 @@ namespace App\Models; -use App\Enums\{SignTime}; +use App\Enums\{SignTime, SignType, CheckStatus}; use App\Traits\HasCheckable; use App\Traits\HasDateTimeFormatter; use EloquentFilter\Filterable; @@ -17,14 +17,19 @@ class EmployeeSignRepair extends Model protected $table = 'employee_sign_repairs'; - protected $fillable = ['date', 'store_id', 'employee_id', 'reason', 'repair_type']; + protected $guarded = []; protected $casts = [ - 'date' => 'date:Y-m-d', - 'checked_at' => 'datetime', + 'date' => 'datetime', 'repair_type' => SignTime::class, + 'sign_type' => SignType::class, ]; + public function canUpdate(): bool + { + return in_array($this->workflow?->check_status, [CheckStatus::None, CheckStatus::Fail, CheckStatus::Cancel]); + } + public function modelFilter() { return \App\Admin\Filters\EmployeeSignRepairFilter::class; diff --git a/app/Traits/HasCheckable.php b/app/Traits/HasCheckable.php index d57743f..f90e5c1 100644 --- a/app/Traits/HasCheckable.php +++ b/app/Traits/HasCheckable.php @@ -33,6 +33,11 @@ trait HasCheckable return Str::snake(class_basename(__CLASS__)); } + public function checkSuccess() + { + + } + /** * 关联审核流水 */ 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 aa113a6..c42fa6d 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\{SignType, SignTime, SignStatus}; return new class extends Migration { @@ -16,10 +17,10 @@ return new class extends Migration $table->date('date')->comment('日期'); $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_type')->default(SignType::Normal)->comment('类别(1: 正常打卡, 2: 外勤)'); $table->timestamp('first_time')->nullable()->comment('上班打卡时间'); $table->timestamp('last_time')->nullable()->comment('下班打卡时间'); - $table->unsignedInteger('sign_status')->default(1)->comment('考勤状态(1: 正常, 2: 旷工, 3: 缺卡)'); + $table->unsignedInteger('sign_status')->default(SignStatus::Normal)->comment('考勤状态(1: 正常, 2: 旷工, 3: 缺卡)'); $table->string('remarks')->nullable()->comment('备注'); $table->timestamps(); @@ -30,9 +31,12 @@ return new class extends Migration $table->id(); $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->unsignedInteger('sign_type')->default(SignType::Normal)->comment('类别(1: 正常打卡, 2: 外勤)'); + $table->unsignedInteger('sign_time')->default(SignTime::Morning)->comment('打卡时间(1: 上班, 2: 下班)'); $table->string('remarks')->nullable()->comment('备注'); + $table->string('outside_remarks')->nullable()->comment('外勤备注'); + $table->unsignedInteger('is_repair')->default(0)->comment('是否补卡'); + $table->foreignId('repair_id')->nullable()->comment('补卡记录'); $table->json('position')->comment('打卡位置'); $table->timestamp('time')->comment('打卡时间'); $table->timestamps(); @@ -50,11 +54,13 @@ return new class extends Migration Schema::create('employee_sign_repairs', function (Blueprint $table) { $table->id(); - $table->date('date')->comment('补卡日期'); + $table->datetime('date')->comment('补卡日期'); $table->foreignId('store_id')->comment('门店, stores.id'); $table->foreignId('employee_id')->comment('员工, employees.id'); $table->string('reason')->comment('补卡原因'); - $table->unsignedInteger('repair_type')->default(1)->comment('上班/下班'); + $table->unsignedInteger('repair_type')->default(SignTime::Morning)->comment('上班/下班'); + $table->unsignedInteger('sign_type')->default(SignType::Normal)->comment('类别(1: 正常打卡, 2: 外勤)'); + $table->string('outside_remarks')->nullable()->comment('外勤备注'); $table->timestamps(); diff --git a/routes/api.php b/routes/api.php index 310180b..c1f94a3 100644 --- a/routes/api.php +++ b/routes/api.php @@ -54,9 +54,20 @@ Route::group([ Route::apiResource('hr/employee', \App\Http\Controllers\Api\Hr\EmployeeController::class); }); + // 考勤打卡 + Route::get('hr/sign/info', [\App\Http\Controllers\Api\Hr\SignController::class, 'info']); + Route::get('hr/sign', [\App\Http\Controllers\Api\Hr\SignController::class, 'index']); + Route::post('hr/sign', [\App\Http\Controllers\Api\Hr\SignController::class, 'store']); + + // 补卡申请 + Route::apiResource('hr/sign-repairs', \App\Http\Controllers\Api\Hr\SignRepairController::class); + // 报销管理 - Route::get('reimbursements/check', [\App\Http\Controllers\Api\ReimbursementController::class, 'checkList']); - Route::post('reimbursements/{id}/check', [\App\Http\Controllers\Api\ReimbursementController::class, 'check']); - Route::get('reimbursements/{id}/logs', [\App\Http\Controllers\Api\ReimbursementController::class, 'logs']); Route::apiResource('reimbursements', \App\Http\Controllers\Api\ReimbursementController::class); + + // 审核流程 + Route::get('workflow', [\App\Http\Controllers\Api\WorkflowController::class, 'index']); + Route::get('workflow/{id}', [\App\Http\Controllers\Api\WorkflowController::class, 'show']); + Route::get('workflow/{id}/logs', [\App\Http\Controllers\Api\WorkflowController::class, 'logs']); + Route::post('workflow/{id}/check', [\App\Http\Controllers\Api\WorkflowController::class, 'check']); });