main
panliang 2024-04-13 14:35:10 +08:00
commit f22cbf53cc
18 changed files with 357 additions and 106 deletions

View File

@ -98,16 +98,14 @@ class LedgerController extends AdminController
->url(admin_url('api/workflow/apply'))
->method('post')
->data(['id' => '${workflow.id}'])
),
)
->visibleOn('${actual_commission != null && actual_income != null}'),
$this->cancelAction(),
$this->rowEditLedgerAmountButton()
->visible(Admin::user()->can('admin.finance.ledgers.update_ledger_amount')),
$this->rowEditActualCommissionButton()
->visible(Admin::user()->can('admin.finance.ledgers.update_actual_commission'))
->visibleOn('${OR(workflow.check_status == '.CheckStatus::None->value.', workflow.check_status == '.CheckStatus::Cancel->value.', workflow.check_status == '.CheckStatus::Fail->value.')}'),
$this->rowEditActualIncomeButton()
->visible(Admin::user()->can('admin.finance.ledgers.update_actual_income'))
->visibleOn('${OR(workflow.check_status == '.CheckStatus::None->value.', workflow.check_status == '.CheckStatus::Cancel->value.', workflow.check_status == '.CheckStatus::Fail->value.')}'),
$this->rowEditTypeButton('drawer', 'lg')
->visible(Admin::user()->can('admin.finance.ledgers.update'))
->visibleOn('${ARRAYINCLUDES(['.CheckStatus::None->value.','.CheckStatus::Cancel->value.','.CheckStatus::Fail->value.'], workflow.check_status)}'),
$this->rowShowButton()
->visible(Admin::user()->can('admin.finance.ledgers.view')),
]),
@ -197,26 +195,6 @@ class LedgerController extends AdminController
return $this->response()->success(null, '保存成功');
}
/**
* 修改实际佣金
*/
public function updateActualCommission($id, Request $request)
{
$this->service->update($id, $request->only(['actual_commission']));
return $this->response()->success(null, '保存成功');
}
/**
* 修改实际收益
*/
public function updateActualIncome($id, Request $request)
{
$this->service->update($id, $request->only(['actual_income']));
return $this->response()->success(null, '保存成功');
}
/**
* 编辑总账金额按钮
*/
@ -241,54 +219,4 @@ class LedgerController extends AdminController
])->size('lg')
);
}
/**
* 编辑实际佣金按钮
*/
protected function rowEditActualCommissionButton(): DrawerAction
{
return amis()->DrawerAction()
->icon('fa-regular fa-pen-to-square')
->label(__('finance.ledger.actual_commission'))
->level('link')
->drawer(
amis()->Drawer()->title(__('finance.ledger.actual_commission'))->body([
amis()->Form()->title('')
->api('post:'.admin_url('finance/ledgers/${id}/actual-commission'))
->body([
amis()->NumberControl()
->name('actual_commission')
->label(__('finance.ledger.actual_commission'))
->precision(2)
->showSteps(false)
->required(),
]),
])->size('lg')
);
}
/**
* 编辑实际收益按钮
*/
protected function rowEditActualIncomeButton(): DrawerAction
{
return amis()->DrawerAction()
->icon('fa-regular fa-pen-to-square')
->label(__('finance.ledger.actual_income'))
->level('link')
->drawer(
amis()->Drawer()->title(__('finance.ledger.actual_income'))->body([
amis()->Form()->title('')
->api('post:'.admin_url('finance/ledgers/${id}/actual-income'))
->body([
amis()->NumberControl()
->name('actual_income')
->label(__('finance.ledger.actual_income'))
->precision(2)
->showSteps(false)
->required(),
]),
])->size('lg')
);
}
}

View File

@ -128,8 +128,6 @@ Route::group([
// 上报数据
$router->resource('ledgers', LedgerController::class);
$router->post('ledgers/{ledger}/ledger-amount', [LedgerController::class, 'updateLedgerAmount'])->name('ledgers.update_ledger_amount');
$router->post('ledgers/{ledger}/actual-commission', [LedgerController::class, 'updateActualCommission'])->name('ledgers.update_actual_commission');
$router->post('ledgers/{ledger}/actual-income', [LedgerController::class, 'updateActualIncome'])->name('ledgers.update_actual_income');
// 佣金收入
$router->get('commission-incomes', [CommissionIncomeController::class, 'index'])->name('commission_incomes.index');
// 收支报销

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Api\Account;
use App\Http\Controllers\Api\Controller;
use App\Http\Resources\StoreMasterCommissionResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class StoreMasterCommissionController extends Controller
{
public function index(Request $request)
{
/** @var \App\Models\Employee */
$user = $request->user();
$storeMasterCommissions = $user->storeMasterCommissions()
->onlyApproved()
->orderBy(DB::raw("STR_TO_DATE(month, '%Y-%m')"), 'DESC')
->simplePaginate($request->query('per_page', 20));
return StoreMasterCommissionResource::collection($storeMasterCommissions);
}
}

View File

@ -11,6 +11,7 @@ use Illuminate\Validation\ValidationException;
use App\Enums\UserRole;
use App\Http\Resources\KeywordResource;
use App\Admin\Services\EmployeeService;
use App\Http\Resources\StoreResource;
use Illuminate\Support\Facades\DB;
/**
@ -29,7 +30,7 @@ class UserController extends Controller
'phone' => $user->phone,
'avatar' => $user->avatar,
'jobs' => KeywordResource::collection($user->jobs),
'store' => $user->store ? StoreResource::make($user->store) : null,
'unread_notifications' => 0,
// 身份: user-普通员工, store-店长, admin-管理员
'role' => $user->userRole(),

View File

@ -0,0 +1,197 @@
<?php
namespace App\Http\Controllers\Api\Ledger;
use App\Exceptions\RuntimeException;
use App\Http\Controllers\Api\Controller;
use App\Models\Keyword;
use App\Models\Ledger;
use App\Models\LedgerItem;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Throwable;
class LedgerController extends Controller
{
public function store(Request $request)
{
/** @var \App\Models\Employee */
$user = $request->user();
if (! $user->isStoreMaster()) {
throw new RuntimeException('非店长不可上报数据');
}
// 是否是彩票店数据上报
$isLotteryLedger = $user->store->isLotteryStore();
$validated = $request->validate(
rules: [
'date' => ['bail', 'required', 'date_format:Y-m-d'],
'items' => $isLotteryLedger ? ['bail', 'required', 'array'] : ['bail', 'array'],
'new_customers' => ['bail', 'required', 'int', 'min:0'],
'sales' => ['bail', 'required', 'numeric', 'min:0'],
'expenditure' => ['bail', 'required', 'numeric', 'min:0'],
'handover_amount' => ['bail', 'required', 'numeric', 'min:0'],
'photos' => ['bail', 'required', 'array'],
],
attributes: [
'date' => '日期',
'items' => '彩种数据',
'new_customers' => '新增客户',
'sales' => '销售合计',
'expenditure' => $isLotteryLedger ? '兑奖合计' : '支出合计',
'handover_amount' => '交账金额',
'photos' => '时段报表照片',
],
);
/** @var \Illuminate\Database\Eloquent\Collection */
$lotteryTypes = Keyword::filter(['parent_key' => 'lottery_type'])
->oldest('sort')
->get()
// 过滤未绑定上报数据类型的彩种
->filter(fn (Keyword $type) => (string) $type->value !== '');
// 上报数据项的格式:
// [
// ['id' => '上报数据类型1', 'sales' => '销售金额', 'expenditure' => '兑奖金额'],
// ['id' => '上报数据类型2', 'sales' => '销售金额', 'expenditure' => '兑奖金额'],
// ]
if ($isLotteryLedger) {
$items = collect($validated['items'])->keyBy('id');
/** @var \App\Models\Keyword */
foreach ($lotteryTypes as $lotteryType) {
$item = $items->get($lotteryType->value);
if (is_null($item)) {
throw new RuntimeException("{$lotteryType->name}未填写上报数据");
}
Validator::validate(
data: $item,
rules: [
'sales' => ['bail', 'required', 'numeric', 'min:0'],
'expenditure' => ['bail', 'required', 'numeric', 'min:0'],
],
attributes: [
'sales' => "[$lotteryType->name]销售金额",
'expenditure' => "[$lotteryType->name]兑奖金额",
],
);
}
}
/** @var \App\Models\Ledger|null */
$ledger = Ledger::where('store_id', $user->store_id)
->where('date', $validated['date'])
->first();
if ($ledger && ! $ledger->allowReReport()) {
throw new RuntimeException('上报数据已更新,不可重新上传');
}
$ratio = bcdiv($user->store->profit_ratio, 100, 2);
// 计算预期佣金
$validated['expected_commission'] = bcmul($validated['sales'], $ratio, 2);
// 计算预期收益
$validated['expected_income'] = bcsub($validated['expected_commission'], $validated['expenditure'], 2);
try {
DB::beginTransaction();
if (is_null($ledger)) {
$ledger = Ledger::create(
array_merge($validated, ['store_id' => $user->store_id])
);
} else {
$ledger->update($validated);
$ledger->items()->delete();
}
LedgerItem::insert(
collect(
$isLotteryLedger ? $validated['items'] : [
[
'id' => 'ledger_item_type_other',
'sales' => $ledger->sales,
'expenditure' => $ledger->expenditure,
],
]
)->map(fn ($item) => [
'date' => $ledger->date,
'store_id' => $ledger->store_id,
'ledger_id' => $ledger->id,
'ledger_item_type_id' => $item['id'],
'sales' => $item['sales'],
'expenditure' => $item['expenditure'],
'created_at' => $ledger->updated_at,
'updated_at' => $ledger->updated_at,
])->all()
);
DB::commit();
} catch (Throwable $e) {
DB::rollBack();
throw tap($e, fn ($e) => report($e));
}
return $this->prepareLedger($ledger);
}
public function show(string $date, Request $request)
{
/** @var \App\Models\Employee */
$user = $request->user();
/** @var \App\Models\Ledger|null */
$ledger = Ledger::with(['items'])
->where('store_id', $user->store_id)
->where('date', $date)
->first();
$data = null;
if ($ledger) {
$data = [
'date' => $ledger->date,
'items' => $ledger->items->map(fn ($item) => [
'id' => $item->ledger_item_type_id,
'sales' => $item->sales,
'expenditure' => $item->expenditure,
]),
'new_customers' => $ledger->new_customers,
'sales' => $ledger->sales,
'expenditure' => $ledger->expenditure,
'handover_amount' => $ledger->handover_amount,
'photos' => $ledger->photos,
];
}
return [
'data' => $ledger ? $this->prepareLedger($ledger) : null,
];
}
protected function prepareLedger(Ledger $ledger)
{
return [
'date' => $ledger->date,
'items' => $ledger->items->map(fn ($item) => [
'id' => $item->ledger_item_type_id,
'sales' => $item->sales,
'expenditure' => $item->expenditure,
]),
'new_customers' => $ledger->new_customers,
'sales' => $ledger->sales,
'expenditure' => $ledger->expenditure,
'handover_amount' => $ledger->handover_amount,
'photos' => $ledger->photos,
];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Api\Ledger;
use App\Http\Controllers\Api\Controller;
use App\Models\Keyword;
class LotteryTypeController extends Controller
{
public function index()
{
/** @var \Illuminate\Database\Eloquent\Collection */
$lotteryTypes = Keyword::filter(['parent_key' => 'lottery_type'])->oldest('sort')->get();
return $lotteryTypes
->filter(fn (Keyword $type) => (string) $type->value !== '')
->map(fn ($item) => [
'id' => $item->value,
'name' => $item->name,
]);
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Models\Keyword;
class LotteryTypeController extends Controller
{
public function index()
{
/** @var \Illuminate\Database\Eloquent\Collection */
$lotteryTypes = Keyword::filter(['parent_key' => 'lottery_type'])->oldest('sort')->get();
return $lotteryTypes->map(fn ($item) => [
'id' => $item->value,
'name' => $item->name,
]);
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers\Api\Reimbursement;
use App\Http\Controllers\Api\Controller;
use App\Http\Resources\KeywordResource;
use App\Models\Keyword;
class ReimbursementTypeController extends Controller
{
public function index()
{
$keywords = Keyword::filter(['parent_key' => 'reimbursement_type'])->get();
return KeywordResource::collection($keywords);
}
}

View File

@ -15,7 +15,7 @@ class KeywordResource extends JsonResource
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'id' => $this->key,
'name' => $this->name,
];
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class StoreMasterCommissionResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'month' => $this->month,
'commission' => $this->commission,
'daily_expenses' => $this->daily_expenses,
'employee_expenses' => $this->employee_expenses,
'other_expenses' => $this->other_expenses,
];
}
}

View File

@ -21,6 +21,7 @@ class StoreResource extends JsonResource
'lon' => $this->lon,
'lat' => $this->lat,
'address' => $this->address,
'is_lottery_store' => $this->isLotteryStore(),
];
}
}

View File

@ -13,7 +13,6 @@ use Illuminate\Database\Eloquent\Model;
use Laravel\Sanctum\HasApiTokens;
use Slowlyo\OwlAdmin\Models\AdminUser;
use App\Enums\UserRole;
use Illuminate\Support\Str;
/**
* 员工
@ -77,6 +76,14 @@ class Employee extends Model implements AuthenticatableContract
return $this->hasMany(EmployeeSign::class, 'employee_id');
}
/**
* 店长佣金提成
*/
public function storeMasterCommissions()
{
return $this->hasMany(StoreMasterCommission::class, 'store_master_id');
}
// 管理的门店(店长)
// public function masterStore()
// {

View File

@ -19,6 +19,9 @@ class Ledger extends Model
protected $attributes = [
'new_customers' => 0,
'ledger_amount' => null,
'actual_commission' => null,
'actual_income' => null,
];
protected $fillable = [
@ -46,6 +49,14 @@ class Ledger extends Model
return $this->hasMany(LedgerItem::class);
}
/**
* 是否允许重新上报
*/
public function allowReReport(): bool
{
return is_null($this->ledger_amount) && is_null($this->actual_commission) && is_null($this->actual_income);
}
protected function ledgerDifference(): Attribute
{
return Attribute::make(

View File

@ -62,6 +62,19 @@ class Store extends Model
return $this->hasMany(Employee::class, 'store_id');
}
public function ledgers()
{
return $this->hasMany(Ledger::class);
}
/**
* 确认此门店是否是彩票店
*/
public function isLotteryStore(): bool
{
return preg_match('/^store_category_lottery_/', $this->category_id);
}
protected function businessStatusText(): Attribute
{
return new Attribute(

View File

@ -2,9 +2,11 @@
namespace App\Models;
use App\Enums\CheckStatus;
use App\Traits\HasCheckable;
use App\Traits\HasDateTimeFormatter;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -23,6 +25,14 @@ class StoreMasterCommission extends Model
'other_expenses',
];
/**
* 仅查询审核通过的店长佣金提成
*/
public function scopeOnlyApproved(Builder $query): void
{
$query->whereRelation('workflow', 'check_status', '=', CheckStatus::Success);
}
public function store()
{
return $this->belongsTo(Store::class, 'store_id');

View File

@ -3,6 +3,7 @@
namespace App\Providers;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@ -20,6 +21,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
JsonResource::withoutWrapping();
$this->definePolymorphicTypes();
}

View File

@ -201,8 +201,6 @@ class AdminPermissionSeeder extends Seeder
'uri' => '/finance/ledgers',
'resource' => ['list', 'update', 'view'],
'children' => [
'update_actual_commission' => '编辑实际佣金',
'update_actual_income' => '编辑实际收益',
'update_ledger_amount' => '编辑总账金额',
],
],

View File

@ -1,9 +1,12 @@
<?php
use App\Http\Controllers\Api\Account\StoreMasterCommissionController;
use App\Http\Controllers\Api\Auth\AccessTokenController;
use App\Http\Controllers\Api\ComplaintController;
use App\Http\Controllers\Api\FeedbackController;
use App\Http\Controllers\Api\LotteryTypeController;
use App\Http\Controllers\Api\Ledger\LedgerController;
use App\Http\Controllers\Api\Ledger\LotteryTypeController;
use App\Http\Controllers\Api\Reimbursement\ReimbursementTypeController;
use App\Http\Controllers\Api\StatsController;
use Illuminate\Support\Facades\Route;
@ -23,10 +26,21 @@ Route::group([
// 我的门店列表
Route::get('auth/stores', [\App\Http\Controllers\Api\Auth\UserController::class, 'storeList']);
// 个人账户 - 佣金提成
Route::get('/account/store-master-commissions', [StoreMasterCommissionController::class, 'index']);
Route::get('/stats/dashboard', [StatsController::class, 'dashboard']);
// 彩种类型
Route::get('lottery-types', [LotteryTypeController::class, 'index']);
// 数据上报
Route::post('/ledger/ledgers', [LedgerController::class, 'store']);
Route::get('/ledger/ledgers/{date}', [LedgerController::class, 'show']);
Route::get('/ledger/lottery-types', [LotteryTypeController::class, 'index']);
// 报销管理
Route::group(['prefix' => 'reimbursement'], function () {
Route::get('reimbursement-types', [ReimbursementTypeController::class, 'index']);
});
// 举报投诉
Route::post('complaints', [ComplaintController::class, 'store']);
// 意见箱