admin 店员管理

main
panliang 2024-03-28 10:15:20 +08:00
parent 1a377349e9
commit 9305112e84
22 changed files with 233 additions and 185 deletions

View File

@ -51,6 +51,14 @@ class EmployeeController extends AdminController
->api('post:'.admin_url('hr/employees/${id}/leave'))
->visible(Admin::user()->can('admin.hr.employees.leave'))
->visibleOn('${employee_status == '.EmployeeStatus::Online->value.'}'),
amisMake()->AjaxAction()
->label(__('employee.reback'))
->level('link')
->icon('fa fa-sign-out')
->confirmText(__('employee.leave_confirm'))
->api('post:'.admin_url('hr/employees/${id}/leave'))
->visible(Admin::user()->can('admin.hr.employees.leave'))
->visibleOn('${employee_status == '.EmployeeStatus::Offline->value.'}'),
]),
]);
@ -108,11 +116,23 @@ class EmployeeController extends AdminController
public function leave($id, Request $request)
{
$user = Employee::findOrFail($id);
if ($user->employee_status == EmployeeStatus::Online) {
$user->update([
'leave_at' => $request->input('leave_at', now()),
'employee_status' => EmployeeStatus::Offline,
]);
} else {
$user->update([
'leave_at' => null,
'employee_status' => EmployeeStatus::Online,
]);
}
return $this->response()->success(null, '操作成功');
}
public function shareList()
{
return $this->service->listQuery()->get(['id', 'name', 'phone']);
}
}

View File

@ -9,7 +9,7 @@ use Slowlyo\OwlAdmin\Renderers\Form;
use Slowlyo\OwlAdmin\Renderers\Page;
/**
* 休息管理
* 休息管理
*/
class RestController extends AdminController
{
@ -46,7 +46,7 @@ class RestController extends AdminController
{
return $this->baseForm()->title('')->body([
amisMake()->SelectControl()->name('employees')->label(__('employee_sign.employee_id'))
->source(admin_url('hr/employees?_action=getData&_all=1&employee_status='.EmployeeStatus::Online->value))
->source(admin_url('api/employees?_all=1&employee_status='.EmployeeStatus::Online->value))
->labelField('name')
->valueField('id')
->searchable()

View File

@ -0,0 +1,52 @@
<?php
namespace App\Admin\Controllers\Hr;
use App\Admin\Controllers\AdminController;
use App\Admin\Services\EmployeeSignService;
use Slowlyo\OwlAdmin\Renderers\Form;
use Slowlyo\OwlAdmin\Renderers\Page;
/**
* 考勤打卡
*/
class SignController extends AdminController
{
protected string $serviceName = EmployeeSignService::class;
public function list(): Page
{
$crud = $this->baseCRUD()
->tableLayout('fixed')
->headerToolbar([
...$this->baseHeaderToolBar(),
])
->bulkActions([])
->filter($this->baseFilter()->body([
amis()->GroupControl()->mode('horizontal')->body([
amisMake()->SelectControl()->source(admin_url('api/stores?_all=1'))->labelField('title')->valueField('id')->searchable()->name('store_id')->label(__('employee_sign.store_id'))->columnRatio(3)->clearable(),
amisMake()->TextControl()->name('employee_name')->label(__('employee_sign.employee_id'))->placeholder(__('employee.name').'/'.__('employee.phone'))->columnRatio(3)->clearable(),
amisMake()->DateRangeControl()->name('date_range')->label(__('employee_sign.date'))->columnRatio(3)->clearable(),
]),
]))
->columns([
amisMake()->TableColumn()->name('store.name')->label(__('employee_sign.store_id')),
amisMake()->TableColumn()->name('employee.name')->label(__('employee.name')),
amisMake()->TableColumn()->name('sign_type')->label(__('employee_sign.sign_type')),
amisMake()->TableColumn()->name('first_time')->label(__('employee_sign.first_time')),
amisMake()->TableColumn()->name('last_time')->label(__('employee_sign.last_time')),
amisMake()->TableColumn()->name('sign_status')->label(__('employee_sign.sign_status')),
$this->rowActions([
$this->rowShowButton(),
]),
]);
return $this->baseList($crud);
}
public function detail(): Form
{
return $this->baseDetail()->title('')->body(amisMake()->Property()->items([
]));
}
}

View File

@ -27,7 +27,7 @@ class DeviceController extends AdminController
->filter($this->baseFilter()->body([
amis()->GroupControl()->mode('horizontal')->body([
amisMake()->SelectControl()->name('store_id')->label(__('store_device.store_id'))
->source(admin_url('store/stores?_action=getData&_all=1'))
->source(admin_url('api/stores?_all=1'))
->labelField('title')
->valueField('id')
->columnRatio(3)
@ -55,7 +55,7 @@ class DeviceController extends AdminController
{
return $this->baseForm()->title('')->body([
amisMake()->SelectControl()->name('store_id')->label(__('store_device.store_id'))
->source(admin_url('store/stores?_action=getData&_all=1'))
->source(admin_url('api/stores?_all=1'))
->labelField('title')
->valueField('id')
->required(),

View File

@ -27,7 +27,7 @@ class EmployeeController extends AdminController
->filter($this->baseFilter()->body([
amis()->GroupControl()->mode('horizontal')->body([
amisMake()->SelectControl()->name('store_id')->label(__('employee.store_id'))
->source(admin_url('store/stores?_action=getData&_all=1'))
->source(admin_url('api/stores?_all=1'))
->labelField('title')
->valueField('id')
->columnRatio(3)
@ -38,8 +38,8 @@ class EmployeeController extends AdminController
->columns([
amisMake()->TableColumn()->name('id')->label(__('employee.id')),
amisMake()->TableColumn()->name('store.title')->label(__('employee.store_id')),
amisMake()->TableColumn()->name('employee.name')->label(__('store.employees')),
amisMake()->TableColumn()->name('employee.phone')->label(__('employee.phone')),
amisMake()->TableColumn()->name('name')->label(__('store.employees')),
amisMake()->TableColumn()->name('phone')->label(__('employee.phone')),
$this->rowActions([
// $this->rowEditButton(true)->visible(Admin::user()->can('admin.store.employees.update')),
$this->rowDeleteButton()->visible(Admin::user()->can('admin.store.employees.delete')),
@ -53,14 +53,18 @@ class EmployeeController extends AdminController
{
return $this->baseForm()->title('')->body([
amisMake()->SelectControl()->name('store_id')->label(__('employee.store_id'))
->source(admin_url('store/stores?_action=getData&_all=1'))
->source(admin_url('api/stores?_all=1'))
->labelField('title')
->valueField('id')
->required(),
amisMake()->SelectControl()->name('employee_id')->label(__('store.employees'))
->source(admin_url('hr/employees?_action=getData&_all=1'))
->source(admin_url('api/employees?_all=1&store_id=0&master_store_id=0&enable=1'))
->labelField('name')
->valueField('id')
->multiple()
->joinValues(false)
->extractValue()
->searchable()
->required(),
]);
}

View File

@ -80,7 +80,7 @@ class StoreController extends AdminController
return $this->baseForm()->title('')->body([
amisMake()->TextControl()->name('title')->label(__('store.title'))->required(),
amisMake()->SelectControl()->name('master_id')->label(__('store.master_id'))
->source(admin_url('hr/employees?_action=getData&_all=1&employee_status='.EmployeeStatus::Online->value))
->source(admin_url('api/employees?_all=1&employee_status='.EmployeeStatus::Online->value))
->labelField('name')
->valueField('id')
->searchable()

View File

@ -22,8 +22,23 @@ class EmployeeFilter extends ModelFilter
$this->whereLike('phone', $key);
}
public function store($key)
{
$this->where('store_id', $key);
}
public function masterStore($key)
{
$this->where('master_store_id', $key);
}
public function employeeStatus($key)
{
$this->whereIn('employee_status', is_array($key) ? $key : explode(',', $key));
}
public function enable($key)
{
$this->query->enable();
}
}

View File

@ -2,9 +2,10 @@
namespace App\Admin\Filters;
use Carbon\Carbon;
use EloquentFilter\ModelFilter;
class StoreEmployeeFilter extends ModelFilter
class EmployeeSignFilter extends ModelFilter
{
protected $drop_id = false;
@ -27,4 +28,12 @@ class StoreEmployeeFilter extends ModelFilter
{
$this->where('store_id', $key);
}
public function dateRange($dates)
{
$dates = explode(',', $dates);
$start = Carbon::createFromTimestamp(data_get($dates, 0, time()))->startOfDay();
$end = Carbon::createFromTimestamp(data_get($dates, 1, time()))->endOfDay();
$this->whereBetween('date', [$start, $end]);
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Admin\Services;
use App\Admin\Filters\EmployeeSignFilter;
use App\Models\EmployeeSign;
class EmployeeSignService extends BaseService
{
protected array $withRelationships = ['employee', 'store'];
protected string $modelName = EmployeeSign::class;
protected string $modelFilterName = EmployeeSignFilter::class;
}

View File

@ -2,35 +2,79 @@
namespace App\Admin\Services;
use App\Admin\Filters\StoreEmployeeFilter;
use App\Models\StoreEmployee;
use App\Admin\Filters\EmployeeFilter;
use App\Models\Employee;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class StoreEmployeeService extends BaseService
{
protected array $withRelationships = ['store', 'employee'];
protected array $withRelationships = ['store'];
protected string $modelName = StoreEmployee::class;
protected string $modelName = Employee::class;
protected string $modelFilterName = StoreEmployeeFilter::class;
protected string $modelFilterName = EmployeeFilter::class;
public function listQuery()
{
$model = $this->getModel();
$filter = $this->getModelFilter();
$query = $this->query()->where('store_id', '>', 0);
if ($this->withRelationships) {
$query->with($this->withRelationships);
}
if ($filter) {
$query->filter(request()->input(), $filter);
}
if ($this->modelSortAble) {
$query->sort();
}
$this->sortable($query);
return $query;
}
public function store($data): bool
{
$storeId = $data['store_id'];
$employees = Employee::whereIn('id', $data['employee_id'])->get();
foreach ($employees as $employee) {
if ($employee->store_id > 0 && $employee->store_id != $storeId) {
$this->setError($employee->name . ' 已经有门店了');
return false;
}
if ($employee->master_store_id > 0) {
$this->setError($employee->name . ' 已经是店长了');
return false;
}
}
Employee::whereIn('id', $data['employee_id'])->update(['store_id' => $storeId]);
return true;
}
public function delete(string $ids): mixed
{
$id = explode(',', $ids);
Employee::whereIn('id', $id)->update(['store_id' => 0]);
return true;
}
public function validate($data, $model = null)
{
$createRules = [
'employee_id' => ['required'],
'store_id' => ['required'],
'employee_id' => ['required', Rule::unique('store_employees', 'employee_id')],
];
$updateRules = [
'employee_id' => [Rule::unique('store_employees', 'employee_id')->ignore($model, 'employee_id')],
];
$validator = Validator::make($data, $model ? $updateRules : $createRules, [
'employee_id.unique' => '已经是店员了',
]);
$validator = Validator::make($data, $model ? $updateRules : $createRules);
if ($validator->fails()) {
return $validator->errors()->first();
}
return true;
}
}

View File

@ -3,7 +3,6 @@
namespace App\Admin\Services;
use App\Admin\Filters\StoreFilter;
use App\Enums\StoreRole;
use App\Models\Employee;
use App\Models\Store;
use Illuminate\Support\Facades\DB;
@ -18,72 +17,6 @@ class StoreService extends BaseService
protected string $modelFilterName = StoreFilter::class;
public function store($data): bool
{
$data = $this->resloveData($data);
$validate = $this->validate($data);
if ($validate !== true) {
$this->setError($validate);
return false;
}
$model = $this->modelName::create($data);
// 设置店长
$model->employees()->attach([$data['master_id'] => ['role' => StoreRole::Master]]);
return true;
}
public function update($primaryKey, $data): bool
{
$model = $this->query()->whereKey($primaryKey)->firstOrFail();
$data = $this->resloveData($data, $model);
$validate = $this->validate($data, $model);
if ($validate !== true) {
$this->setError($validate);
return false;
}
// 修改店长
if (isset($data['master_id']) && $data['master_id'] != $model->master_id) {
$model->employees()->detach($model->master_id);
$model->employees()->attach([$data['master_id'] => ['role' => StoreRole::Master]]);
}
return $model->update($data);
}
// 添加员工
public function attachEmployee(Store $store, array $employeeIds)
{
$data = [];
$employees = Employee::whereIn('id', $employeeIds)->get();
// 每个员工只能有一个门店
foreach ($employees as $employee) {
if (DB::table('store_employees')->where('employee_id', $employee->id)->exists()) {
$this->setError($employee->name.' 已经是店员');
return false;
}
$data[$employee->id] = ['role' => StoreRole::Employee];
}
$store->employees()->attach($data);
return true;
}
// 移除员工
public function destroyEmployee(Store $store, array $employeeIds)
{
$store->employees()->detach($employeeIds);
return true;
}
public function resloveData($data, $model = null)
{
if (isset($data['location'])) {
@ -92,6 +25,11 @@ class StoreService extends BaseService
$data['address'] = data_get($data['location'], 'address');
}
// 绑定店长
if (isset($data['master_id'])) {
Employee::where('id', $data['master_id'])->update(['master_store_id' => $model->id]);
}
return $data;
}
@ -122,8 +60,8 @@ class StoreService extends BaseService
public function preDelete(array $ids)
{
// 删除店员
DB::table('store_employees')->whereIn('store_id', $ids)->delete();
// 修改员工关联
Employee::whereIn('store_id', $ids)->update(['store_id' => 0]);
return true;
}

View File

@ -4,6 +4,7 @@ use App\Admin\Controllers\BaseKeywordController;
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\Store\DeviceController;
use App\Admin\Controllers\Store\EmployeeController as StoreEmployeeController;
use App\Admin\Controllers\Store\StoreController;
@ -66,7 +67,10 @@ Route::group([
$router->post('employees/{id}/leave', [EmployeeController::class, 'leave'])->name('employees.leave');
// 职位管理
$router->resource('jobs', BaseKeywordController::class);
$router->resource('rests', RestController::class);
// 休息管理
$router->resource('rests', RestController::class)->only(['index', 'create', 'store', 'destroy']);
// 打卡情况
$router->resource('signs', SignController::class)->only(['index', 'show']);
});
/*
@ -135,6 +139,7 @@ Route::group([
'prefix' => 'api',
], function (Router $router) {
$router->get('stores', [StoreController::class, 'shareList']);
$router->get('employees', [EmployeeController::class, 'shareList']);
$router->get('keywords/tree-list', [KeywordController::class, 'getTreeList'])->name('api.keywords.tree-list');
});
});

View File

@ -1,35 +0,0 @@
<?php
namespace App\Enums;
/**
* 店员身份
*/
enum StoreRole: int
{
case Master = 1;
case Employee = 2;
public static function map()
{
return [
self::Master->value => '店长',
self::Employee->value => '店员',
];
}
public static function options()
{
$list = [];
foreach (self::map() as $key => $value) {
array_push($list, ['label' => $value, 'value' => $key]);
}
return $list;
}
public function text()
{
return data_get(self::map(), $this->value);
}
}

View File

@ -19,7 +19,7 @@ class Employee extends Model
const JOB_KEY = 'job';
protected $fillable = ['name', 'phone', 'prize_images', 'skill_images', 'employee_status', 'admin_user_id', 'leave_at', 'join_at', 'remarks'];
protected $fillable = ['store_id', 'master_store_id', 'name', 'phone', 'prize_images', 'skill_images', 'employee_status', 'admin_user_id', 'leave_at', 'join_at', 'remarks'];
protected $casts = [
'employee_status' => EmployeeStatus::class,
@ -53,11 +53,16 @@ class Employee extends Model
return $this->belongsTo(AdminUser::class, 'admin_user_id');
}
// 关联的门店
public function stores()
// 关联的门店(店员)
public function store()
{
// role(1: 店长, 2: 店员)
return $this->belongsToMany(Store::class, 'store_employees', 'employee_id', 'store_id')->withPivot(['role']);
return $this->belongsTo(Store::class, 'store_id');
}
// 管理的门店(店长)
public function masterStore()
{
return $this->belongsTo(Store::class, 'master_store_id');
}
public function scopeEnable($q)

View File

@ -6,13 +6,14 @@ use App\Enums\SignStatus;
use App\Enums\SignType;
use App\Traits\HasDateTimeFormatter;
use Illuminate\Database\Eloquent\Model;
use EloquentFilter\Filterable;
/**
* 员工-打卡情况
*/
class EmployeeSign extends Model
{
use HasDateTimeFormatter;
use HasDateTimeFormatter, Filterable;
protected $table = 'employee_sign_dates';

View File

@ -7,13 +7,14 @@ use App\Traits\HasDateTimeFormatter;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
/**
* 门店
*/
class Store extends Model
{
use Filterable, HasDateTimeFormatter;
use Filterable, HasDateTimeFormatter, HasFactory;
protected $fillable = ['title', 'master_id', 'category_id', 'business_id', 'level_id', 'region', 'lon', 'lat', 'address', 'profit_ratio', 'profit_money', 'business_status'];
@ -53,7 +54,7 @@ class Store extends Model
public function employees()
{
// role(1: 店长, 2: 店员)
return $this->belongsToMany(Employee::class, 'store_employees', 'store_id', 'employee_id')->using(StoreEmployee::class);
return $this->hasMany(Employee::class, 'store_id');
}
protected function businessStatusText(): Attribute

View File

@ -1,28 +0,0 @@
<?php
namespace App\Models;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Relations\Pivot;
/**
* 店员
*/
class StoreEmployee extends Pivot
{
use Filterable;
protected $table = 'store_employees';
public $incrementing = true;
public function store()
{
return $this->belongsTo(Store::class, 'store_id');
}
public function employee()
{
return $this->belongsTo(Employee::class, 'employee_id');
}
}

View File

@ -5,7 +5,6 @@ namespace Database\Factories;
use App\Models\Employee;
use App\Models\Keyword;
use App\Models\Store;
use App\Models\StoreEmployee;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
@ -22,10 +21,7 @@ class StoreFactory extends Factory
*/
public function definition(): array
{
do {
$master = Employee::inRandomOrder()->first();
} while (StoreEmployee::where('employee_id', $master->id)->exists());
$master = Employee::where('store_id', 0)->inRandomOrder()->first();
return [
'title' => $this->faker->word(),
'master_id' => $master->id,
@ -38,4 +34,12 @@ class StoreFactory extends Factory
'address' => '重庆市南川区东城街道办事处东环路三号',
];
}
public function configure(): static
{
return $this->afterMaking(function (Store $model) {
})->afterCreating(function (Store $model) {
Employee::where('id', $model->master_id)->update(['master_store_id' => $model->id]);
});
}
}

View File

@ -13,6 +13,8 @@ return new class extends Migration
{
Schema::create('employees', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->default(0)->comment('所属门店, stores.id');
$table->foreignId('master_store_id')->default(0)->comment('管理的门店, stores.id');
$table->string('name')->comment('姓名');
$table->string('phone')->comment('电话');
$table->json('prize_images')->nullable()->comment('荣誉证书');

View File

@ -28,14 +28,6 @@ return new class extends Migration
$table->timestamps();
$table->comment('门店');
});
Schema::create('store_employees', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id');
$table->foreignId('employee_id');
$table->unsignedInteger('role')->default(2)->comment('身份(1: 店长, 2: 店员)');
$table->comment('门店-店员');
});
}
/**
@ -44,6 +36,5 @@ return new class extends Migration
public function down(): void
{
Schema::dropIfExists('stores');
Schema::dropIfExists('store_employees');
}
};

View File

@ -114,8 +114,14 @@ class AdminPermissionSeeder extends Seeder
'name' => '休息管理',
'icon' => '',
'uri' => '/hr/rests',
'resource' => true,
'resource' => ['list', 'create', 'delete'],
],
'signs' => [
'name' => '考勤打卡',
'icon' => '',
'uri' => '/hr/signs',
'resource' => ['list', 'view'],
]
],
],

View File

@ -20,8 +20,7 @@ class EmployeeSeeder extends Seeder
Employee::truncate();
(new EmployeeFactory)->count(100)->create(['admin_user_id' => 1]);
DB::table('store_employees')->truncate();
Store::truncate();
(new StoreFactory)->count(10)->create();
Store::factory()->count(10)->create();
}
}