api 审核流程

main
panliang 2024-04-15 20:19:54 +08:00
parent b3177862bc
commit 77205d7fa9
16 changed files with 455 additions and 57 deletions

View File

@ -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));

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -100,6 +100,8 @@ class WorkFlowService extends BaseService
'check_remarks' => data_get($options, 'remarks'),
]);
$check->subject->checkSuccess();
return true;
}

View File

@ -2,10 +2,22 @@
namespace App\Enums;
/**
* 打卡状态
*/
enum SignStatus: int
{
/**
* 正常打卡
*/
case Normal = 1;
/**
* 缺卡
*/
case Lose = 2;
/**
* 旷工
*/
case Absent = 3;
public static function options()

View File

@ -0,0 +1,75 @@
<?php
namespace App\Http\Controllers\Api\Hr;
use App\Http\Controllers\Api\Controller;
use App\Models\{EmployeeSign};
use Illuminate\Http\{Request, Response};
use App\Exceptions\RuntimeException;
use Illuminate\Support\Facades\DB;
use App\Admin\Services\EmployeeSignService;
use App\Enums\{SignTime, SignType, SignStatus};
use Carbon\Carbon;
/**
* 考勤打卡
*/
class SignController extends Controller
{
public function index(Request $request)
{
$user = $this->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());
}
}
}

View File

@ -0,0 +1,114 @@
<?php
namespace App\Http\Controllers\Api\Hr;
use App\Http\Controllers\Api\Controller;
use Illuminate\Http\{Request, Response};
use App\Models\EmployeeSignRepair;
use App\Http\Resources\{EmployeeSignRepairResource, WorkflowLogResource};
use App\Admin\Services\{EmployeeSignRepairService, WorkFlowService};
use Illuminate\Support\Facades\DB;
use App\Exceptions\RuntimeException;
use App\Enums\{CheckStatus};
/**
* 补卡申请
*/
class SignRepairController extends Controller
{
public function index(Request $request)
{
$user = $this->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());
}
}
}

View File

@ -0,0 +1,104 @@
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\{Request, Response};
use Illuminate\Database\Eloquent\Relations\Relation;
use App\Http\Resources\{ReimbursementResource, WorkflowLogResource, EmployeeSignRepairResource};
use App\Enums\CheckStatus;
use App\Models\{WorkflowLog, WorkflowCheck};
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use App\Admin\Services\WorkFlowService;
use App\Exceptions\RuntimeException;
class WorkflowController extends Controller
{
public function index(Request $request)
{
$request->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);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class EmployeeSignRepairResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
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')),
];
}
}

View File

@ -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);
}
}

View File

@ -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');

View File

@ -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,

View File

@ -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;

View File

@ -33,6 +33,11 @@ trait HasCheckable
return Str::snake(class_basename(__CLASS__));
}
public function checkSuccess()
{
}
/**
* 关联审核流水
*/

View File

@ -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();

View File

@ -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']);
});