feat: 第四阶段财务与人事薪资模块
- 新增2个migration(11张表): finance(6表)/hr(5表) - 新增11个Model + 11个Controller - 新增50条API路由(总计262条) - 新增2个前端API模块 + 10个管理页面 - 迁移运行通过, vite build验证通过
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Finance;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Finance\AccountTransaction;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AccountTransactionController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = AccountTransaction::query()->with(['account', 'customer']);
|
||||
$query->when($request->customer_account_id, fn($q, $v) => $q->where('customer_account_id', $v));
|
||||
$query->when($request->customer_id, fn($q, $v) => $q->where('customer_id', $v));
|
||||
$query->when($request->type, fn($q, $v) => $q->where('type', $v));
|
||||
|
||||
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Finance;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Finance\AccountTransaction;
|
||||
use App\Models\Finance\CustomerAccount;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CustomerAccountController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = CustomerAccount::query()->with(['customer']);
|
||||
$query->when($request->customer_id, fn($q, $v) => $q->where('customer_id', $v));
|
||||
|
||||
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function show(CustomerAccount $customerAccount): JsonResponse
|
||||
{
|
||||
return $this->success($customerAccount->load(['customer', 'transactions']));
|
||||
}
|
||||
|
||||
public function recharge(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'customer_id' => 'required|exists:customers,id',
|
||||
'amount' => 'required|numeric|min:0.01',
|
||||
'pay_method' => 'nullable|integer|in:1,2,3,4',
|
||||
'description' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
return DB::transaction(function () use ($validated) {
|
||||
$account = CustomerAccount::firstOrCreate(
|
||||
['customer_id' => $validated['customer_id'], 'store_id' => auth()->user()->store_id ?? 0],
|
||||
['cash_balance' => 0, 'card_balance' => 0, 'points' => 0]
|
||||
);
|
||||
|
||||
$account->increment('cash_balance', $validated['amount']);
|
||||
$account->refresh();
|
||||
|
||||
AccountTransaction::create([
|
||||
'store_id' => $account->store_id,
|
||||
'customer_account_id' => $account->id,
|
||||
'customer_id' => $validated['customer_id'],
|
||||
'type' => 1,
|
||||
'amount' => $validated['amount'],
|
||||
'balance_after' => $account->cash_balance,
|
||||
'description' => $validated['description'] ?? '账户充值',
|
||||
'operator_id' => auth()->id(),
|
||||
]);
|
||||
|
||||
return $this->success($account);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Finance;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Finance\FinanceCategory;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class FinanceCategoryController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = FinanceCategory::query();
|
||||
$query->when($request->type, fn($q, $v) => $q->where('type', $v));
|
||||
$query->when($request->has('status'), fn($q) => $q->where('status', $request->status));
|
||||
|
||||
$categories = $query->orderBy('sort')->get();
|
||||
|
||||
// 构建树形结构
|
||||
$tree = $categories->where('parent_id', 0)->values()->map(function ($item) use ($categories) {
|
||||
$item->children = $categories->where('parent_id', $item->id)->values();
|
||||
return $item;
|
||||
});
|
||||
|
||||
return $this->success($tree);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:50',
|
||||
'type' => 'required|integer|in:1,2',
|
||||
'parent_id' => 'sometimes|integer',
|
||||
'sort' => 'sometimes|integer',
|
||||
'status' => 'sometimes|integer|in:0,1',
|
||||
]);
|
||||
|
||||
if (!empty($validated['parent_id'])) {
|
||||
$parent = FinanceCategory::find($validated['parent_id']);
|
||||
$validated['level'] = $parent ? $parent->level + 1 : 1;
|
||||
}
|
||||
|
||||
return $this->success(FinanceCategory::create($validated));
|
||||
}
|
||||
|
||||
public function show(FinanceCategory $financeCategory): JsonResponse
|
||||
{
|
||||
return $this->success($financeCategory->load('children'));
|
||||
}
|
||||
|
||||
public function update(Request $request, FinanceCategory $financeCategory): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'sometimes|string|max:50',
|
||||
'type' => 'sometimes|integer|in:1,2',
|
||||
'parent_id' => 'sometimes|integer',
|
||||
'sort' => 'sometimes|integer',
|
||||
'status' => 'sometimes|integer|in:0,1',
|
||||
]);
|
||||
|
||||
$financeCategory->update($validated);
|
||||
|
||||
return $this->success($financeCategory);
|
||||
}
|
||||
|
||||
public function destroy(FinanceCategory $financeCategory): JsonResponse
|
||||
{
|
||||
if ($financeCategory->children()->count() > 0) {
|
||||
return $this->error('存在子分类,无法删除', 40001);
|
||||
}
|
||||
|
||||
$financeCategory->delete();
|
||||
|
||||
return $this->success(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Finance;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Finance\FinanceRecord;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class FinanceRecordController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = FinanceRecord::query()->with(['category']);
|
||||
$query->when($request->type, fn($q, $v) => $q->where('type', $v));
|
||||
$query->when($request->category_id, fn($q, $v) => $q->where('category_id', $v));
|
||||
$query->when($request->has('audit_status'), fn($q) => $q->where('audit_status', $request->audit_status));
|
||||
$query->when($request->start_date, fn($q, $v) => $q->where('record_date', '>=', $v));
|
||||
$query->when($request->end_date, fn($q, $v) => $q->where('record_date', '<=', $v));
|
||||
|
||||
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'record_no' => 'required|string|max:50|unique:finance_records',
|
||||
'category_id' => 'nullable|exists:finance_categories,id',
|
||||
'type' => 'required|integer|in:1,2',
|
||||
'amount' => 'required|numeric|min:0.01',
|
||||
'pay_method' => 'nullable|integer|in:1,2,3,4',
|
||||
'description' => 'nullable|string|max:255',
|
||||
'record_date' => 'required|date',
|
||||
]);
|
||||
|
||||
$validated['operator_id'] = auth()->id();
|
||||
$validated['audit_status'] = 0;
|
||||
|
||||
return $this->success(FinanceRecord::create($validated));
|
||||
}
|
||||
|
||||
public function show(FinanceRecord $financeRecord): JsonResponse
|
||||
{
|
||||
return $this->success($financeRecord->load(['category', 'auditor', 'operator']));
|
||||
}
|
||||
|
||||
public function update(Request $request, FinanceRecord $financeRecord): JsonResponse
|
||||
{
|
||||
if ($financeRecord->audit_status !== 0) {
|
||||
return $this->error('只能修改待审核的记录', 40001);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'category_id' => 'nullable|exists:finance_categories,id',
|
||||
'type' => 'sometimes|integer|in:1,2',
|
||||
'amount' => 'sometimes|numeric|min:0.01',
|
||||
'pay_method' => 'nullable|integer|in:1,2,3,4',
|
||||
'description' => 'nullable|string|max:255',
|
||||
'record_date' => 'sometimes|date',
|
||||
]);
|
||||
|
||||
$financeRecord->update($validated);
|
||||
|
||||
return $this->success($financeRecord);
|
||||
}
|
||||
|
||||
public function destroy(FinanceRecord $financeRecord): JsonResponse
|
||||
{
|
||||
if ($financeRecord->audit_status !== 0) {
|
||||
return $this->error('只能删除待审核的记录', 40001);
|
||||
}
|
||||
|
||||
$financeRecord->delete();
|
||||
|
||||
return $this->success(null);
|
||||
}
|
||||
|
||||
public function audit(Request $request, FinanceRecord $financeRecord): JsonResponse
|
||||
{
|
||||
if ($financeRecord->audit_status !== 0) {
|
||||
return $this->error('该记录已审核', 40001);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'status' => 'required|integer|in:1,2',
|
||||
]);
|
||||
|
||||
$financeRecord->update([
|
||||
'audit_status' => $validated['status'],
|
||||
'audit_user_id' => auth()->id(),
|
||||
'audit_at' => now(),
|
||||
]);
|
||||
|
||||
return $this->success($financeRecord);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Finance;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Finance\Invoice;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class InvoiceController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Invoice::query()->with(['customer']);
|
||||
$query->when($request->invoice_no, fn($q, $v) => $q->where('invoice_no', 'like', "%{$v}%"));
|
||||
$query->when($request->customer_id, fn($q, $v) => $q->where('customer_id', $v));
|
||||
$query->when($request->has('status'), fn($q) => $q->where('status', $request->status));
|
||||
$query->when($request->type, fn($q, $v) => $q->where('type', $v));
|
||||
|
||||
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'invoice_no' => 'required|string|max:50|unique:invoices',
|
||||
'customer_id' => 'nullable|exists:customers,id',
|
||||
'type' => 'sometimes|integer|in:1,2',
|
||||
'amount' => 'required|numeric|min:0',
|
||||
'tax_amount' => 'sometimes|numeric|min:0',
|
||||
'title' => 'nullable|string|max:255',
|
||||
'tax_no' => 'nullable|string|max:50',
|
||||
'related_type' => 'nullable|string|max:50',
|
||||
'related_id' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
$validated['operator_id'] = auth()->id();
|
||||
$validated['status'] = 0;
|
||||
|
||||
return $this->success(Invoice::create($validated));
|
||||
}
|
||||
|
||||
public function show(Invoice $invoice): JsonResponse
|
||||
{
|
||||
return $this->success($invoice->load(['customer', 'operator']));
|
||||
}
|
||||
|
||||
public function update(Request $request, Invoice $invoice): JsonResponse
|
||||
{
|
||||
if ($invoice->status !== 0) {
|
||||
return $this->error('只能修改待开票的发票', 40001);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'customer_id' => 'nullable|exists:customers,id',
|
||||
'type' => 'sometimes|integer|in:1,2',
|
||||
'amount' => 'sometimes|numeric|min:0',
|
||||
'tax_amount' => 'sometimes|numeric|min:0',
|
||||
'title' => 'nullable|string|max:255',
|
||||
'tax_no' => 'nullable|string|max:50',
|
||||
'related_type' => 'nullable|string|max:50',
|
||||
'related_id' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
$invoice->update($validated);
|
||||
|
||||
return $this->success($invoice);
|
||||
}
|
||||
|
||||
public function destroy(Invoice $invoice): JsonResponse
|
||||
{
|
||||
if ($invoice->status !== 0) {
|
||||
return $this->error('只能删除待开票的发票', 40001);
|
||||
}
|
||||
|
||||
$invoice->delete();
|
||||
|
||||
return $this->success(null);
|
||||
}
|
||||
|
||||
public function issue(Invoice $invoice): JsonResponse
|
||||
{
|
||||
if ($invoice->status !== 0) {
|
||||
return $this->error('该发票已开具或已红冲', 40001);
|
||||
}
|
||||
|
||||
$invoice->update([
|
||||
'status' => 1,
|
||||
'issued_at' => now(),
|
||||
]);
|
||||
|
||||
return $this->success($invoice);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Finance;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Finance\PrepaidCard;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PrepaidCardController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = PrepaidCard::query();
|
||||
$query->when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%"));
|
||||
$query->when($request->has('status'), fn($q) => $q->where('status', $request->status));
|
||||
|
||||
return $this->paginate($query->orderBy('sort')->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:100',
|
||||
'face_value' => 'required|numeric|min:0',
|
||||
'sell_price' => 'required|numeric|min:0',
|
||||
'discount_rate' => 'sometimes|numeric|min:0|max:1',
|
||||
'validity_days' => 'sometimes|integer|min:1',
|
||||
'status' => 'sometimes|integer|in:0,1',
|
||||
'sort' => 'sometimes|integer',
|
||||
]);
|
||||
|
||||
return $this->success(PrepaidCard::create($validated));
|
||||
}
|
||||
|
||||
public function show(PrepaidCard $prepaidCard): JsonResponse
|
||||
{
|
||||
return $this->success($prepaidCard);
|
||||
}
|
||||
|
||||
public function update(Request $request, PrepaidCard $prepaidCard): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'sometimes|string|max:100',
|
||||
'face_value' => 'sometimes|numeric|min:0',
|
||||
'sell_price' => 'sometimes|numeric|min:0',
|
||||
'discount_rate' => 'sometimes|numeric|min:0|max:1',
|
||||
'validity_days' => 'sometimes|integer|min:1',
|
||||
'status' => 'sometimes|integer|in:0,1',
|
||||
'sort' => 'sometimes|integer',
|
||||
]);
|
||||
|
||||
$prepaidCard->update($validated);
|
||||
|
||||
return $this->success($prepaidCard);
|
||||
}
|
||||
|
||||
public function destroy(PrepaidCard $prepaidCard): JsonResponse
|
||||
{
|
||||
$prepaidCard->delete();
|
||||
|
||||
return $this->success(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Hr;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Hr\AttendanceRecord;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AttendanceRecordController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = AttendanceRecord::query()->with('user');
|
||||
$query->when($request->user_id, fn($q, $v) => $q->where('user_id', $v));
|
||||
$query->when($request->start_date, fn($q, $v) => $q->where('attendance_date', '>=', $v));
|
||||
$query->when($request->end_date, fn($q, $v) => $q->where('attendance_date', '<=', $v));
|
||||
$query->when($request->has('status'), fn($q) => $q->where('status', $request->status));
|
||||
|
||||
return $this->paginate($query->latest('attendance_date')->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$v = $request->validate([
|
||||
'user_id' => 'required|exists:users,id',
|
||||
'attendance_date' => 'required|date',
|
||||
'clock_in' => 'nullable|date_format:H:i',
|
||||
'clock_out' => 'nullable|date_format:H:i',
|
||||
'status' => 'nullable|integer|in:1,2,3,4,5',
|
||||
'overtime_hours' => 'nullable|numeric|min:0',
|
||||
'remark' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
return $this->success(AttendanceRecord::create($v));
|
||||
}
|
||||
|
||||
public function clockIn(): JsonResponse
|
||||
{
|
||||
$record = AttendanceRecord::firstOrCreate(
|
||||
['user_id' => auth()->id(), 'attendance_date' => now()->toDateString()],
|
||||
['status' => 1]
|
||||
);
|
||||
$record->update(['clock_in' => now()->format('H:i:s')]);
|
||||
|
||||
return $this->success($record);
|
||||
}
|
||||
|
||||
public function clockOut(): JsonResponse
|
||||
{
|
||||
$record = AttendanceRecord::where('user_id', auth()->id())
|
||||
->where('attendance_date', now()->toDateString())
|
||||
->first();
|
||||
|
||||
if (!$record) {
|
||||
return $this->error('今日未打卡上班', 40001);
|
||||
}
|
||||
$record->update(['clock_out' => now()->format('H:i:s')]);
|
||||
|
||||
return $this->success($record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Hr;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Hr\EmployeeProfile;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EmployeeProfileController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = EmployeeProfile::query()->with('user');
|
||||
$query->when($request->user_id, fn($q, $v) => $q->where('user_id', $v));
|
||||
$query->when($request->has('status'), fn($q) => $q->where('status', $request->status));
|
||||
|
||||
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$v = $request->validate([
|
||||
'user_id' => 'required|exists:users,id',
|
||||
'employee_no' => 'nullable|string|max:30',
|
||||
'hire_date' => 'nullable|date',
|
||||
'birth_date' => 'nullable|date',
|
||||
'gender' => 'nullable|integer|in:0,1,2',
|
||||
'id_card' => 'nullable|string|max:20',
|
||||
'education' => 'nullable|string|max:30',
|
||||
'emergency_contact' => 'nullable|string|max:50',
|
||||
'emergency_phone' => 'nullable|string|max:20',
|
||||
'base_salary' => 'nullable|numeric|min:0',
|
||||
'position_salary' => 'nullable|numeric|min:0',
|
||||
'status' => 'nullable|integer|in:1,2,3',
|
||||
]);
|
||||
|
||||
return $this->success(EmployeeProfile::create($v));
|
||||
}
|
||||
|
||||
public function show(EmployeeProfile $employeeProfile): JsonResponse
|
||||
{
|
||||
return $this->success($employeeProfile->load('user'));
|
||||
}
|
||||
|
||||
public function update(Request $request, EmployeeProfile $employeeProfile): JsonResponse
|
||||
{
|
||||
$v = $request->validate([
|
||||
'employee_no' => 'nullable|string|max:30',
|
||||
'hire_date' => 'nullable|date',
|
||||
'birth_date' => 'nullable|date',
|
||||
'gender' => 'nullable|integer|in:0,1,2',
|
||||
'id_card' => 'nullable|string|max:20',
|
||||
'education' => 'nullable|string|max:30',
|
||||
'emergency_contact' => 'nullable|string|max:50',
|
||||
'emergency_phone' => 'nullable|string|max:20',
|
||||
'base_salary' => 'nullable|numeric|min:0',
|
||||
'position_salary' => 'nullable|numeric|min:0',
|
||||
'status' => 'nullable|integer|in:1,2,3',
|
||||
'left_date' => 'nullable|date',
|
||||
]);
|
||||
$employeeProfile->update($v);
|
||||
|
||||
return $this->success($employeeProfile);
|
||||
}
|
||||
|
||||
public function destroy(EmployeeProfile $employeeProfile): JsonResponse
|
||||
{
|
||||
$employeeProfile->delete();
|
||||
return $this->success(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Hr;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Hr\LeaveRequest;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LeaveRequestController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = LeaveRequest::query()->with('user');
|
||||
$query->when($request->user_id, fn($q, $v) => $q->where('user_id', $v));
|
||||
$query->when($request->type, fn($q, $v) => $q->where('type', $v));
|
||||
$query->when($request->has('status'), fn($q) => $q->where('status', $request->status));
|
||||
|
||||
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$v = $request->validate([
|
||||
'user_id' => 'required|exists:users,id',
|
||||
'type' => 'required|integer|in:1,2,3,4,5,6',
|
||||
'start_date' => 'required|date',
|
||||
'end_date' => 'required|date|after_or_equal:start_date',
|
||||
'days' => 'required|numeric|min:0.5',
|
||||
'reason' => 'nullable|string',
|
||||
]);
|
||||
$v['status'] = 0;
|
||||
|
||||
return $this->success(LeaveRequest::create($v));
|
||||
}
|
||||
|
||||
public function show(LeaveRequest $leaveRequest): JsonResponse
|
||||
{
|
||||
return $this->success($leaveRequest->load(['user', 'auditor']));
|
||||
}
|
||||
|
||||
public function update(Request $request, LeaveRequest $leaveRequest): JsonResponse
|
||||
{
|
||||
if ($leaveRequest->status !== 0) {
|
||||
return $this->error('已审批的申请不可修改', 40001);
|
||||
}
|
||||
$v = $request->validate([
|
||||
'type' => 'sometimes|integer|in:1,2,3,4,5,6',
|
||||
'start_date' => 'sometimes|date',
|
||||
'end_date' => 'sometimes|date|after_or_equal:start_date',
|
||||
'days' => 'sometimes|numeric|min:0.5',
|
||||
'reason' => 'nullable|string',
|
||||
]);
|
||||
$leaveRequest->update($v);
|
||||
|
||||
return $this->success($leaveRequest);
|
||||
}
|
||||
|
||||
public function destroy(LeaveRequest $leaveRequest): JsonResponse
|
||||
{
|
||||
if ($leaveRequest->status !== 0) {
|
||||
return $this->error('已审批的申请不可删除', 40001);
|
||||
}
|
||||
$leaveRequest->delete();
|
||||
|
||||
return $this->success(null);
|
||||
}
|
||||
|
||||
public function audit(Request $request, LeaveRequest $leaveRequest): JsonResponse
|
||||
{
|
||||
if ($leaveRequest->status !== 0) {
|
||||
return $this->error('该申请已审批', 40001);
|
||||
}
|
||||
$v = $request->validate([
|
||||
'status' => 'required|integer|in:1,2',
|
||||
]);
|
||||
$leaveRequest->update([
|
||||
'status' => $v['status'],
|
||||
'audit_user_id' => auth()->id(),
|
||||
'audit_at' => now(),
|
||||
]);
|
||||
|
||||
return $this->success($leaveRequest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Hr;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Hr\SalaryRecord;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SalaryRecordController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = SalaryRecord::query()->with('user');
|
||||
$query->when($request->user_id, fn($q, $v) => $q->where('user_id', $v));
|
||||
$query->when($request->year_month, fn($q, $v) => $q->where('year_month', $v));
|
||||
$query->when($request->has('status'), fn($q) => $q->where('status', $request->status));
|
||||
|
||||
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$v = $request->validate([
|
||||
'user_id' => 'required|exists:users,id',
|
||||
'year_month' => 'required|string|max:7',
|
||||
'base_salary' => 'nullable|numeric|min:0',
|
||||
'position_salary' => 'nullable|numeric|min:0',
|
||||
'performance' => 'nullable|numeric|min:0',
|
||||
'overtime_pay' => 'nullable|numeric|min:0',
|
||||
'subsidy' => 'nullable|numeric|min:0',
|
||||
'social_insurance' => 'nullable|numeric|min:0',
|
||||
'housing_fund' => 'nullable|numeric|min:0',
|
||||
'tax' => 'nullable|numeric|min:0',
|
||||
'absence_deduction' => 'nullable|numeric|min:0',
|
||||
'other_deduction' => 'nullable|numeric|min:0',
|
||||
'actual_amount' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
|
||||
return $this->success(SalaryRecord::create($v));
|
||||
}
|
||||
|
||||
public function show(SalaryRecord $salaryRecord): JsonResponse
|
||||
{
|
||||
return $this->success($salaryRecord->load('user'));
|
||||
}
|
||||
|
||||
public function update(Request $request, SalaryRecord $salaryRecord): JsonResponse
|
||||
{
|
||||
if ($salaryRecord->status >= 2) {
|
||||
return $this->error('已发放的工资记录不可修改', 40001);
|
||||
}
|
||||
$v = $request->validate([
|
||||
'base_salary' => 'nullable|numeric|min:0',
|
||||
'position_salary' => 'nullable|numeric|min:0',
|
||||
'performance' => 'nullable|numeric|min:0',
|
||||
'overtime_pay' => 'nullable|numeric|min:0',
|
||||
'subsidy' => 'nullable|numeric|min:0',
|
||||
'social_insurance' => 'nullable|numeric|min:0',
|
||||
'housing_fund' => 'nullable|numeric|min:0',
|
||||
'tax' => 'nullable|numeric|min:0',
|
||||
'absence_deduction' => 'nullable|numeric|min:0',
|
||||
'other_deduction' => 'nullable|numeric|min:0',
|
||||
'actual_amount' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
$salaryRecord->update($v);
|
||||
|
||||
return $this->success($salaryRecord);
|
||||
}
|
||||
|
||||
public function destroy(SalaryRecord $salaryRecord): JsonResponse
|
||||
{
|
||||
if ($salaryRecord->status >= 1) {
|
||||
return $this->error('已确认的工资记录不可删除', 40001);
|
||||
}
|
||||
$salaryRecord->delete();
|
||||
|
||||
return $this->success(null);
|
||||
}
|
||||
|
||||
public function confirm(SalaryRecord $salaryRecord): JsonResponse
|
||||
{
|
||||
if ($salaryRecord->status !== 0) {
|
||||
return $this->error('仅待确认状态可确认', 40001);
|
||||
}
|
||||
$salaryRecord->update([
|
||||
'status' => 1,
|
||||
'confirmed_at' => now(),
|
||||
]);
|
||||
|
||||
return $this->success($salaryRecord);
|
||||
}
|
||||
|
||||
public function pay(SalaryRecord $salaryRecord): JsonResponse
|
||||
{
|
||||
if ($salaryRecord->status !== 1) {
|
||||
return $this->error('仅已确认状态可发放', 40001);
|
||||
}
|
||||
$salaryRecord->update([
|
||||
'status' => 2,
|
||||
'paid_at' => now(),
|
||||
]);
|
||||
|
||||
return $this->success($salaryRecord);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Hr;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Hr\Schedule;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ScheduleController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Schedule::query()->with('user');
|
||||
$query->when($request->user_id, fn($q, $v) => $q->where('user_id', $v));
|
||||
$query->when($request->start_date, fn($q, $v) => $q->where('schedule_date', '>=', $v));
|
||||
$query->when($request->end_date, fn($q, $v) => $q->where('schedule_date', '<=', $v));
|
||||
|
||||
return $this->paginate($query->latest('schedule_date')->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$v = $request->validate([
|
||||
'user_id' => 'required|exists:users,id',
|
||||
'schedule_date' => 'required|date',
|
||||
'shift_type' => 'required|integer|in:1,2,3,4,5',
|
||||
'start_time' => 'nullable|date_format:H:i',
|
||||
'end_time' => 'nullable|date_format:H:i',
|
||||
]);
|
||||
|
||||
$schedule = Schedule::updateOrCreate(
|
||||
['user_id' => $v['user_id'], 'schedule_date' => $v['schedule_date']],
|
||||
$v
|
||||
);
|
||||
|
||||
return $this->success($schedule);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Finance;
|
||||
|
||||
use App\Models\Crm\Customer;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AccountTransaction extends Model
|
||||
{
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'customer_account_id', 'customer_id', 'type',
|
||||
'amount', 'balance_after', 'related_type', 'related_id',
|
||||
'description', 'operator_id',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'integer',
|
||||
'amount' => 'decimal:2',
|
||||
'balance_after' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
|
||||
public function account(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CustomerAccount::class, 'customer_account_id');
|
||||
}
|
||||
|
||||
public function customer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Customer::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Finance;
|
||||
|
||||
use App\Models\Crm\Customer;
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class CustomerAccount extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'customer_id', 'cash_balance', 'card_balance', 'points',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'cash_balance' => 'decimal:2',
|
||||
'card_balance' => 'decimal:2',
|
||||
'points' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function customer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Customer::class);
|
||||
}
|
||||
|
||||
public function transactions(): HasMany
|
||||
{
|
||||
return $this->hasMany(AccountTransaction::class, 'customer_account_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Finance;
|
||||
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class FinanceCategory extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'parent_id', 'name', 'type', 'level', 'sort', 'status',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'integer',
|
||||
'level' => 'integer',
|
||||
'sort' => 'integer',
|
||||
'status' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function children(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Finance;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class FinanceRecord extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'record_no', 'category_id', 'type', 'amount',
|
||||
'pay_method', 'related_type', 'related_id', 'description',
|
||||
'audit_status', 'audit_user_id', 'audit_at', 'operator_id',
|
||||
'record_date',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'integer',
|
||||
'pay_method' => 'integer',
|
||||
'audit_status' => 'integer',
|
||||
'amount' => 'decimal:2',
|
||||
'audit_at' => 'datetime',
|
||||
'record_date' => 'date',
|
||||
];
|
||||
}
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(FinanceCategory::class, 'category_id');
|
||||
}
|
||||
|
||||
public function auditor(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'audit_user_id');
|
||||
}
|
||||
|
||||
public function operator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'operator_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Finance;
|
||||
|
||||
use App\Models\Crm\Customer;
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Invoice extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'invoice_no', 'customer_id', 'type', 'amount',
|
||||
'tax_amount', 'title', 'tax_no', 'related_type', 'related_id',
|
||||
'status', 'operator_id', 'issued_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'integer',
|
||||
'status' => 'integer',
|
||||
'amount' => 'decimal:2',
|
||||
'tax_amount' => 'decimal:2',
|
||||
'issued_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function customer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Customer::class);
|
||||
}
|
||||
|
||||
public function operator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'operator_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Finance;
|
||||
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PrepaidCard extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'name', 'face_value', 'sell_price',
|
||||
'discount_rate', 'validity_days', 'status', 'sort',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'face_value' => 'decimal:2',
|
||||
'sell_price' => 'decimal:2',
|
||||
'discount_rate' => 'decimal:2',
|
||||
'validity_days' => 'integer',
|
||||
'status' => 'integer',
|
||||
'sort' => 'integer',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Hr;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AttendanceRecord extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'user_id', 'attendance_date', 'clock_in', 'clock_out',
|
||||
'status', 'overtime_hours', 'remark',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'integer',
|
||||
'overtime_hours' => 'decimal:1',
|
||||
'attendance_date' => 'date',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Hr;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class EmployeeProfile extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'user_id', 'employee_no', 'hire_date', 'birth_date',
|
||||
'gender', 'id_card', 'education', 'emergency_contact', 'emergency_phone',
|
||||
'base_salary', 'position_salary', 'status', 'left_date',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'hire_date' => 'date',
|
||||
'birth_date' => 'date',
|
||||
'left_date' => 'date',
|
||||
'gender' => 'integer',
|
||||
'status' => 'integer',
|
||||
'base_salary' => 'decimal:2',
|
||||
'position_salary' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Hr;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class LeaveRequest extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'user_id', 'type', 'start_date', 'end_date',
|
||||
'days', 'reason', 'status', 'audit_user_id', 'audit_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'integer',
|
||||
'status' => 'integer',
|
||||
'days' => 'decimal:1',
|
||||
'start_date' => 'date',
|
||||
'end_date' => 'date',
|
||||
'audit_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function auditor(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'audit_user_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Hr;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class SalaryRecord extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'user_id', 'year_month',
|
||||
'base_salary', 'position_salary', 'performance', 'overtime_pay',
|
||||
'subsidy', 'social_insurance', 'housing_fund', 'tax',
|
||||
'absence_deduction', 'other_deduction', 'actual_amount',
|
||||
'status', 'confirmed_at', 'paid_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'base_salary' => 'decimal:2',
|
||||
'position_salary' => 'decimal:2',
|
||||
'performance' => 'decimal:2',
|
||||
'overtime_pay' => 'decimal:2',
|
||||
'subsidy' => 'decimal:2',
|
||||
'social_insurance' => 'decimal:2',
|
||||
'housing_fund' => 'decimal:2',
|
||||
'tax' => 'decimal:2',
|
||||
'absence_deduction' => 'decimal:2',
|
||||
'other_deduction' => 'decimal:2',
|
||||
'actual_amount' => 'decimal:2',
|
||||
'status' => 'integer',
|
||||
'confirmed_at' => 'datetime',
|
||||
'paid_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Hr;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Schedule extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'user_id', 'schedule_date', 'shift_type',
|
||||
'start_time', 'end_time',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'shift_type' => 'integer',
|
||||
'schedule_date' => 'date',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// 收支分类(树形)
|
||||
Schema::create('finance_categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->unsignedBigInteger('parent_id')->default(0)->index();
|
||||
$table->string('name', 50);
|
||||
$table->tinyInteger('type')->default(1)->comment('1=收入 2=支出');
|
||||
$table->tinyInteger('level')->default(1);
|
||||
$table->integer('sort')->default(0);
|
||||
$table->tinyInteger('status')->default(1);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 收支记录
|
||||
Schema::create('finance_records', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->string('record_no', 50)->unique();
|
||||
$table->unsignedBigInteger('category_id')->nullable();
|
||||
$table->tinyInteger('type')->comment('1=收入 2=支出');
|
||||
$table->decimal('amount', 12, 2);
|
||||
$table->tinyInteger('pay_method')->nullable()->comment('1=现金 2=微信 3=支付宝 4=银行');
|
||||
$table->string('related_type', 50)->nullable()->comment('关联类型');
|
||||
$table->unsignedBigInteger('related_id')->nullable();
|
||||
$table->string('description')->nullable();
|
||||
$table->tinyInteger('audit_status')->default(0)->comment('0=待审核 1=已通过 2=已驳回');
|
||||
$table->unsignedBigInteger('audit_user_id')->nullable();
|
||||
$table->timestamp('audit_at')->nullable();
|
||||
$table->unsignedBigInteger('operator_id')->nullable();
|
||||
$table->date('record_date');
|
||||
$table->timestamps();
|
||||
$table->index(['store_id', 'type']);
|
||||
$table->index('record_date');
|
||||
});
|
||||
|
||||
// 客户账户
|
||||
Schema::create('customer_accounts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->unsignedBigInteger('customer_id')->index();
|
||||
$table->decimal('cash_balance', 12, 2)->default(0);
|
||||
$table->decimal('card_balance', 12, 2)->default(0);
|
||||
$table->integer('points')->default(0);
|
||||
$table->timestamps();
|
||||
$table->unique(['store_id', 'customer_id']);
|
||||
});
|
||||
|
||||
// 账户流水
|
||||
Schema::create('account_transactions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->unsignedBigInteger('customer_account_id')->index();
|
||||
$table->unsignedBigInteger('customer_id')->index();
|
||||
$table->tinyInteger('type')->comment('1=充值 2=消费 3=退款 4=积分变动');
|
||||
$table->decimal('amount', 12, 2);
|
||||
$table->decimal('balance_after', 12, 2);
|
||||
$table->string('related_type', 50)->nullable();
|
||||
$table->unsignedBigInteger('related_id')->nullable();
|
||||
$table->string('description')->nullable();
|
||||
$table->unsignedBigInteger('operator_id')->nullable();
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
// 储值卡种
|
||||
Schema::create('prepaid_cards', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->string('name', 100);
|
||||
$table->decimal('face_value', 10, 2);
|
||||
$table->decimal('sell_price', 10, 2);
|
||||
$table->decimal('discount_rate', 5, 2)->default(1.00);
|
||||
$table->integer('validity_days')->default(365);
|
||||
$table->tinyInteger('status')->default(1);
|
||||
$table->integer('sort')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 发票
|
||||
Schema::create('invoices', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->string('invoice_no', 50)->unique();
|
||||
$table->unsignedBigInteger('customer_id')->nullable();
|
||||
$table->tinyInteger('type')->default(1)->comment('1=电子 2=纸质');
|
||||
$table->decimal('amount', 12, 2);
|
||||
$table->decimal('tax_amount', 10, 2)->default(0);
|
||||
$table->string('title')->nullable();
|
||||
$table->string('tax_no', 50)->nullable();
|
||||
$table->string('related_type', 50)->nullable();
|
||||
$table->unsignedBigInteger('related_id')->nullable();
|
||||
$table->tinyInteger('status')->default(0)->comment('0=待开 1=已开 2=已红冲');
|
||||
$table->unsignedBigInteger('operator_id')->nullable();
|
||||
$table->timestamp('issued_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index(['store_id', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('invoices');
|
||||
Schema::dropIfExists('prepaid_cards');
|
||||
Schema::dropIfExists('account_transactions');
|
||||
Schema::dropIfExists('customer_accounts');
|
||||
Schema::dropIfExists('finance_records');
|
||||
Schema::dropIfExists('finance_categories');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// 员工档案
|
||||
Schema::create('employee_profiles', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->unsignedBigInteger('user_id')->index();
|
||||
$table->string('employee_no', 30)->nullable();
|
||||
$table->date('hire_date')->nullable();
|
||||
$table->date('birth_date')->nullable();
|
||||
$table->tinyInteger('gender')->default(0)->comment('0=未知 1=男 2=女');
|
||||
$table->string('id_card', 20)->nullable();
|
||||
$table->string('education', 30)->nullable();
|
||||
$table->string('emergency_contact', 50)->nullable();
|
||||
$table->string('emergency_phone', 20)->nullable();
|
||||
$table->decimal('base_salary', 10, 2)->default(0);
|
||||
$table->decimal('position_salary', 10, 2)->default(0);
|
||||
$table->tinyInteger('status')->default(1)->comment('1=在职 2=试用 3=离职');
|
||||
$table->date('left_date')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 排班
|
||||
Schema::create('schedules', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->unsignedBigInteger('user_id')->index();
|
||||
$table->date('schedule_date');
|
||||
$table->tinyInteger('shift_type')->default(1)->comment('1=早班 2=中班 3=晚班 4=夜班 5=休息');
|
||||
$table->time('start_time')->nullable();
|
||||
$table->time('end_time')->nullable();
|
||||
$table->timestamp('created_at')->nullable();
|
||||
$table->index(['store_id', 'user_id', 'schedule_date']);
|
||||
});
|
||||
|
||||
// 考勤记录
|
||||
Schema::create('attendance_records', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->unsignedBigInteger('user_id')->index();
|
||||
$table->date('attendance_date');
|
||||
$table->time('clock_in')->nullable();
|
||||
$table->time('clock_out')->nullable();
|
||||
$table->tinyInteger('status')->default(1)->comment('1=正常 2=迟到 3=早退 4=旷工 5=请假');
|
||||
$table->decimal('overtime_hours', 5, 1)->default(0);
|
||||
$table->string('remark')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index(['store_id', 'user_id', 'attendance_date']);
|
||||
});
|
||||
|
||||
// 请假申请
|
||||
Schema::create('leave_requests', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->unsignedBigInteger('user_id')->index();
|
||||
$table->tinyInteger('type')->comment('1=年假 2=事假 3=病假 4=调休 5=产假 6=婚假');
|
||||
$table->date('start_date');
|
||||
$table->date('end_date');
|
||||
$table->decimal('days', 4, 1);
|
||||
$table->text('reason')->nullable();
|
||||
$table->tinyInteger('status')->default(0)->comment('0=待审批 1=已批准 2=已拒绝');
|
||||
$table->unsignedBigInteger('audit_user_id')->nullable();
|
||||
$table->timestamp('audit_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 工资记录
|
||||
Schema::create('salary_records', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->unsignedBigInteger('user_id')->index();
|
||||
$table->string('year_month', 7)->comment('YYYY-MM');
|
||||
$table->decimal('base_salary', 10, 2)->default(0);
|
||||
$table->decimal('position_salary', 10, 2)->default(0);
|
||||
$table->decimal('performance', 10, 2)->default(0);
|
||||
$table->decimal('overtime_pay', 10, 2)->default(0);
|
||||
$table->decimal('subsidy', 10, 2)->default(0);
|
||||
$table->decimal('social_insurance', 10, 2)->default(0);
|
||||
$table->decimal('housing_fund', 10, 2)->default(0);
|
||||
$table->decimal('tax', 10, 2)->default(0);
|
||||
$table->decimal('absence_deduction', 10, 2)->default(0);
|
||||
$table->decimal('other_deduction', 10, 2)->default(0);
|
||||
$table->decimal('actual_amount', 10, 2)->default(0);
|
||||
$table->tinyInteger('status')->default(0)->comment('0=待确认 1=已确认 2=已发放');
|
||||
$table->timestamp('confirmed_at')->nullable();
|
||||
$table->timestamp('paid_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['store_id', 'user_id', 'year_month']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('salary_records');
|
||||
Schema::dropIfExists('leave_requests');
|
||||
Schema::dropIfExists('attendance_records');
|
||||
Schema::dropIfExists('schedules');
|
||||
Schema::dropIfExists('employee_profiles');
|
||||
}
|
||||
};
|
||||
@@ -44,6 +44,17 @@ use App\Http\Controllers\Admin\Inventory\MaterialController;
|
||||
use App\Http\Controllers\Admin\Inventory\PurchaseOrderController;
|
||||
use App\Http\Controllers\Admin\Inventory\StockMovementController;
|
||||
use App\Http\Controllers\Admin\Inventory\InventoryController;
|
||||
use App\Http\Controllers\Admin\Finance\FinanceCategoryController;
|
||||
use App\Http\Controllers\Admin\Finance\FinanceRecordController;
|
||||
use App\Http\Controllers\Admin\Finance\CustomerAccountController;
|
||||
use App\Http\Controllers\Admin\Finance\AccountTransactionController;
|
||||
use App\Http\Controllers\Admin\Finance\PrepaidCardController;
|
||||
use App\Http\Controllers\Admin\Finance\InvoiceController;
|
||||
use App\Http\Controllers\Admin\Hr\EmployeeProfileController;
|
||||
use App\Http\Controllers\Admin\Hr\ScheduleController;
|
||||
use App\Http\Controllers\Admin\Hr\AttendanceRecordController;
|
||||
use App\Http\Controllers\Admin\Hr\LeaveRequestController;
|
||||
use App\Http\Controllers\Admin\Hr\SalaryRecordController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -247,4 +258,54 @@ Route::middleware(['auth:sanctum', 'store', 'oplog'])->group(function () {
|
||||
// 库存查询
|
||||
Route::get('inventories', [InventoryController::class, 'index']);
|
||||
});
|
||||
|
||||
// --- 财务管理模块 ---
|
||||
Route::prefix('finance')->group(function () {
|
||||
// 收支分类
|
||||
Route::apiResource('categories', FinanceCategoryController::class);
|
||||
|
||||
// 收支记录
|
||||
Route::apiResource('records', FinanceRecordController::class);
|
||||
Route::put('records/{financeRecord}/audit', [FinanceRecordController::class, 'audit']);
|
||||
|
||||
// 客户账户
|
||||
Route::get('accounts', [CustomerAccountController::class, 'index']);
|
||||
Route::get('accounts/{customerAccount}', [CustomerAccountController::class, 'show']);
|
||||
Route::post('accounts/recharge', [CustomerAccountController::class, 'recharge']);
|
||||
|
||||
// 账户流水
|
||||
Route::get('transactions', [AccountTransactionController::class, 'index']);
|
||||
|
||||
// 储值卡
|
||||
Route::apiResource('prepaid-cards', PrepaidCardController::class);
|
||||
|
||||
// 发票管理
|
||||
Route::apiResource('invoices', InvoiceController::class);
|
||||
Route::put('invoices/{invoice}/issue', [InvoiceController::class, 'issue']);
|
||||
});
|
||||
|
||||
// --- 人事薪资模块 ---
|
||||
Route::prefix('hr')->group(function () {
|
||||
// 员工档案
|
||||
Route::apiResource('profiles', EmployeeProfileController::class);
|
||||
|
||||
// 排班管理
|
||||
Route::get('schedules', [ScheduleController::class, 'index']);
|
||||
Route::post('schedules', [ScheduleController::class, 'store']);
|
||||
|
||||
// 考勤管理
|
||||
Route::get('attendances', [AttendanceRecordController::class, 'index']);
|
||||
Route::post('attendances', [AttendanceRecordController::class, 'store']);
|
||||
Route::post('attendances/clock-in', [AttendanceRecordController::class, 'clockIn']);
|
||||
Route::post('attendances/clock-out', [AttendanceRecordController::class, 'clockOut']);
|
||||
|
||||
// 请假管理
|
||||
Route::apiResource('leaves', LeaveRequestController::class);
|
||||
Route::put('leaves/{leaveRequest}/audit', [LeaveRequestController::class, 'audit']);
|
||||
|
||||
// 工资管理
|
||||
Route::apiResource('salaries', SalaryRecordController::class);
|
||||
Route::put('salaries/{salaryRecord}/confirm', [SalaryRecordController::class, 'confirm']);
|
||||
Route::put('salaries/{salaryRecord}/pay', [SalaryRecordController::class, 'pay']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import request from '@/utils/request.js'
|
||||
|
||||
// ─── Finance Categories ──────────────────────────────────────────────────────
|
||||
export const financeCategoryApi = {
|
||||
getList: (params) => request.get('/finance/categories', { params }),
|
||||
getDetail: (id) => request.get(`/finance/categories/${id}`),
|
||||
create: (data) => request.post('/finance/categories', data),
|
||||
update: (id, data) => request.put(`/finance/categories/${id}`, data),
|
||||
delete: (id) => request.delete(`/finance/categories/${id}`)
|
||||
}
|
||||
|
||||
// ─── Finance Records ─────────────────────────────────────────────────────────
|
||||
export const financeRecordApi = {
|
||||
getList: (params) => request.get('/finance/records', { params }),
|
||||
getDetail: (id) => request.get(`/finance/records/${id}`),
|
||||
create: (data) => request.post('/finance/records', data),
|
||||
update: (id, data) => request.put(`/finance/records/${id}`, data),
|
||||
delete: (id) => request.delete(`/finance/records/${id}`),
|
||||
audit: (id, data) => request.put(`/finance/records/${id}/audit`, data)
|
||||
}
|
||||
|
||||
// ─── Customer Accounts ───────────────────────────────────────────────────────
|
||||
export const customerAccountApi = {
|
||||
getList: (params) => request.get('/finance/accounts', { params }),
|
||||
recharge: (data) => request.post('/finance/accounts/recharge', data)
|
||||
}
|
||||
|
||||
// ─── Account Transactions ────────────────────────────────────────────────────
|
||||
export const accountTransactionApi = {
|
||||
getList: (params) => request.get('/finance/transactions', { params })
|
||||
}
|
||||
|
||||
// ─── Prepaid Cards ───────────────────────────────────────────────────────────
|
||||
export const prepaidCardApi = {
|
||||
getList: (params) => request.get('/finance/prepaid-cards', { params }),
|
||||
getDetail: (id) => request.get(`/finance/prepaid-cards/${id}`),
|
||||
create: (data) => request.post('/finance/prepaid-cards', data),
|
||||
update: (id, data) => request.put(`/finance/prepaid-cards/${id}`, data),
|
||||
delete: (id) => request.delete(`/finance/prepaid-cards/${id}`)
|
||||
}
|
||||
|
||||
// ─── Invoices ────────────────────────────────────────────────────────────────
|
||||
export const invoiceApi = {
|
||||
getList: (params) => request.get('/finance/invoices', { params }),
|
||||
getDetail: (id) => request.get(`/finance/invoices/${id}`),
|
||||
create: (data) => request.post('/finance/invoices', data),
|
||||
update: (id, data) => request.put(`/finance/invoices/${id}`, data),
|
||||
delete: (id) => request.delete(`/finance/invoices/${id}`),
|
||||
issue: (id) => request.put(`/finance/invoices/${id}/issue`)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import request from '@/utils/request.js'
|
||||
|
||||
// ─── Employee Profiles ───────────────────────────────────────────────────────
|
||||
export const employeeProfileApi = {
|
||||
getList: (params) => request.get('/hr/profiles', { params }),
|
||||
getDetail: (id) => request.get(`/hr/profiles/${id}`),
|
||||
create: (data) => request.post('/hr/profiles', data),
|
||||
update: (id, data) => request.put(`/hr/profiles/${id}`, data),
|
||||
delete: (id) => request.delete(`/hr/profiles/${id}`)
|
||||
}
|
||||
|
||||
// ─── Schedules ───────────────────────────────────────────────────────────────
|
||||
export const scheduleApi = {
|
||||
getList: (params) => request.get('/hr/schedules', { params }),
|
||||
create: (data) => request.post('/hr/schedules', data)
|
||||
}
|
||||
|
||||
// ─── Attendance Records ──────────────────────────────────────────────────────
|
||||
export const attendanceApi = {
|
||||
getList: (params) => request.get('/hr/attendances', { params }),
|
||||
create: (data) => request.post('/hr/attendances', data),
|
||||
clockIn: () => request.post('/hr/attendances/clock-in'),
|
||||
clockOut: () => request.post('/hr/attendances/clock-out')
|
||||
}
|
||||
|
||||
// ─── Leave Requests ──────────────────────────────────────────────────────────
|
||||
export const leaveRequestApi = {
|
||||
getList: (params) => request.get('/hr/leaves', { params }),
|
||||
getDetail: (id) => request.get(`/hr/leaves/${id}`),
|
||||
create: (data) => request.post('/hr/leaves', data),
|
||||
update: (id, data) => request.put(`/hr/leaves/${id}`, data),
|
||||
delete: (id) => request.delete(`/hr/leaves/${id}`),
|
||||
audit: (id, data) => request.put(`/hr/leaves/${id}/audit`, data)
|
||||
}
|
||||
|
||||
// ─── Salary Records ──────────────────────────────────────────────────────────
|
||||
export const salaryRecordApi = {
|
||||
getList: (params) => request.get('/hr/salaries', { params }),
|
||||
getDetail: (id) => request.get(`/hr/salaries/${id}`),
|
||||
create: (data) => request.post('/hr/salaries', data),
|
||||
update: (id, data) => request.put(`/hr/salaries/${id}`, data),
|
||||
delete: (id) => request.delete(`/hr/salaries/${id}`),
|
||||
confirm: (id) => request.put(`/hr/salaries/${id}/confirm`),
|
||||
pay: (id) => request.put(`/hr/salaries/${id}/pay`)
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { customerAccountApi, accountTransactionApi } from '@/api/finance.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
const searchForm = reactive({ customer_id: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
// ─── Recharge Dialog ────────────────────────────────────────────────────────
|
||||
const rechargeVisible = ref(false)
|
||||
const rechargeLoading = ref(false)
|
||||
const rechargeFormRef = ref(null)
|
||||
const rechargeForm = reactive({ customer_account_id: null, customer_id: null, amount: 0, pay_method: '' })
|
||||
const rechargeRules = {
|
||||
amount: [{ required: true, message: '请输入充值金额', trigger: 'blur' }]
|
||||
}
|
||||
const payMethods = [
|
||||
{ label: '现金', value: 'cash' },
|
||||
{ label: '微信', value: 'wechat' },
|
||||
{ label: '支付宝', value: 'alipay' },
|
||||
{ label: '银行转账', value: 'bank' },
|
||||
{ label: '储值卡', value: 'card' }
|
||||
]
|
||||
|
||||
// ─── Transaction History Dialog ─────────────────────────────────────────────
|
||||
const txVisible = ref(false)
|
||||
const txLoading = ref(false)
|
||||
const txData = ref([])
|
||||
const txTotal = ref(0)
|
||||
const txAccountId = ref(null)
|
||||
const txPagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const txTypeMap = { 1: '充值', 2: '消费', 3: '退款', 4: '积分变动' }
|
||||
const txTypeType = { 1: 'success', 2: 'warning', 3: 'danger', 4: 'info' }
|
||||
|
||||
// ─── Main List ──────────────────────────────────────────────────────────────
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await customerAccountApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() { Object.assign(searchForm, { customer_id: '' }); handleSearch() }
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
// ─── Recharge ───────────────────────────────────────────────────────────────
|
||||
function handleRecharge(row) {
|
||||
Object.assign(rechargeForm, { customer_account_id: row.id, customer_id: row.customer_id, amount: 0, pay_method: '' })
|
||||
rechargeVisible.value = true
|
||||
}
|
||||
|
||||
async function submitRecharge() {
|
||||
if (!rechargeFormRef.value) return
|
||||
await rechargeFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
rechargeLoading.value = true
|
||||
try {
|
||||
await customerAccountApi.recharge(rechargeForm)
|
||||
ElMessage.success('充值成功')
|
||||
rechargeVisible.value = false
|
||||
fetchList()
|
||||
} finally { rechargeLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Transaction History ────────────────────────────────────────────────────
|
||||
function handleViewTx(row) {
|
||||
txAccountId.value = row.id
|
||||
txPagination.page = 1
|
||||
txVisible.value = true
|
||||
fetchTxList()
|
||||
}
|
||||
|
||||
async function fetchTxList() {
|
||||
txLoading.value = true
|
||||
try {
|
||||
const res = await accountTransactionApi.getList({ customer_account_id: txAccountId.value, ...txPagination })
|
||||
txData.value = res.data?.list || res.data?.data || []
|
||||
txTotal.value = res.data?.total || 0
|
||||
} finally { txLoading.value = false }
|
||||
}
|
||||
|
||||
function handleTxPageChange(val) { txPagination.page = val; fetchTxList() }
|
||||
function handleTxSizeChange(val) { txPagination.per_page = val; txPagination.page = 1; fetchTxList() }
|
||||
|
||||
onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<el-input v-model="searchForm.customer_id" placeholder="客户ID" style="width:160px" clearable @keydown.enter="handleSearch" />
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column label="客户姓名" min-width="120">
|
||||
<template #default="{ row }">{{ row.customer?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cash_balance" label="现金余额" min-width="120" align="right" />
|
||||
<el-table-column prop="card_balance" label="储值卡余额" min-width="120" align="right" />
|
||||
<el-table-column prop="points" label="积分" min-width="100" align="right" />
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleRecharge(row)">充值</el-button>
|
||||
<el-button size="small" type="primary" link @click="handleViewTx(row)">查看流水</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10, 20, 50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
|
||||
<!-- Recharge Dialog -->
|
||||
<el-dialog v-model="rechargeVisible" title="账户充值" width="480px" destroy-on-close>
|
||||
<el-form ref="rechargeFormRef" :model="rechargeForm" :rules="rechargeRules" label-width="90px">
|
||||
<el-form-item label="客户ID">
|
||||
<el-input :model-value="rechargeForm.customer_id" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="充值金额" prop="amount">
|
||||
<el-input-number v-model="rechargeForm.amount" :min="0.01" :precision="2" :step="100" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="支付方式">
|
||||
<el-select v-model="rechargeForm.pay_method" placeholder="请选择支付方式" style="width:100%">
|
||||
<el-option v-for="m in payMethods" :key="m.value" :label="m.label" :value="m.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="rechargeVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="rechargeLoading" @click="submitRecharge">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Transaction History Dialog -->
|
||||
<el-dialog v-model="txVisible" title="账户流水" width="800px" destroy-on-close>
|
||||
<el-table v-loading="txLoading" :data="txData" border stripe style="width:100%">
|
||||
<el-table-column label="类型" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="txTypeType[row.type]" size="small">{{ txTypeMap[row.type] || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="金额" min-width="100" align="right" />
|
||||
<el-table-column prop="balance_after" label="变动后余额" min-width="120" align="right" />
|
||||
<el-table-column prop="description" label="描述" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="created_at" label="时间" min-width="160" />
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="txPagination.page" v-model:page-size="txPagination.per_page" :total="txTotal" :page-sizes="[10, 20, 50]" layout="total,sizes,prev,pager,next,jumper" background style="margin-top:12px" @current-change="handleTxPageChange" @size-change="handleTxSizeChange" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,143 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { financeCategoryApi } from '@/api/finance.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const searchForm = reactive({ type: '', status: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 50 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({ id: null, name: '', type: 1, parent_id: 0, level: 1, sort: 0, status: 1 })
|
||||
const isEdit = ref(false)
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择类型', trigger: 'change' }]
|
||||
}
|
||||
const typeMap = { 1: '收入', 2: '支出' }
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await financeCategoryApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() { Object.assign(searchForm, { type: '', status: '' }); handleSearch() }
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
Object.assign(form, { id: null, name: '', type: 1, parent_id: 0, level: 1, sort: 0, status: 1 })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(row) {
|
||||
isEdit.value = true
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
parent_id: row.parent_id || 0,
|
||||
level: row.level || 1,
|
||||
sort: row.sort || 0,
|
||||
status: row.status
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (isEdit.value) { await financeCategoryApi.update(form.id, form); ElMessage.success('更新成功') }
|
||||
else { await financeCategoryApi.create(form); ElMessage.success('创建成功') }
|
||||
dialogVisible.value = false; fetchList()
|
||||
} finally { submitLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除分类「${row.name}」吗?`, '删除确认', { type: 'warning' })
|
||||
await financeCategoryApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<el-select v-model="searchForm.type" placeholder="类型" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="停用" :value="0" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增分类</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe row-key="id" default-expand-all style="width:100%">
|
||||
<el-table-column prop="name" label="分类名称" min-width="180" />
|
||||
<el-table-column label="类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === 1 ? 'success' : 'danger'" size="small">{{ typeMap[row.type] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="level" label="层级" width="80" align="center" />
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status ? 'success' : 'danger'" size="small">{{ row.status ? '启用' : '停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[20,50,100]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑分类' : '新增分类'" width="450px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
<el-form-item label="分类名称" prop="name"><el-input v-model="form.name" placeholder="请输入分类名称" /></el-form-item>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-select v-model="form.type" style="width:100%">
|
||||
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="上级分类">
|
||||
<el-input-number v-model="form.parent_id" :min="0" style="width:100%" />
|
||||
<div style="font-size:12px;color:var(--color-text-secondary)">0 表示顶级分类</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sort" :min="0" :max="9999" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,118 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { invoiceApi } from '@/api/finance.js'
|
||||
|
||||
const statusMap = { 0: '待开', 1: '已开', 2: '已红冲' }
|
||||
const statusType = { 0: 'warning', 1: 'success', 2: 'danger' }
|
||||
const typeMap = { 1: '电子', 2: '纸质' }
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const searchForm = reactive({ invoice_no: '', status: '', type: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 15 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({ id: null, invoice_no: '', customer_id: '', type: 1, amount: 0, tax_amount: 0, title: '', tax_no: '' })
|
||||
const isEdit = ref(false)
|
||||
const formRules = { invoice_no: [{ required: true, message: '请输入发票号', trigger: 'blur' }] }
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await invoiceApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() { Object.assign(searchForm, { invoice_no: '', status: '', type: '' }); handleSearch() }
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
Object.assign(form, { id: null, invoice_no: '', customer_id: '', type: 1, amount: 0, tax_amount: 0, title: '', tax_no: '' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
async function handleEdit(row) { isEdit.value = true; Object.assign(form, { ...row }); dialogVisible.value = true }
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (isEdit.value) { await invoiceApi.update(form.id, form); ElMessage.success('更新成功') }
|
||||
else { await invoiceApi.create(form); ElMessage.success('创建成功') }
|
||||
dialogVisible.value = false; fetchList()
|
||||
} finally { submitLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除发票「${row.invoice_no}」吗?`, '删除确认', { type: 'warning' })
|
||||
await invoiceApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
|
||||
}
|
||||
|
||||
async function handleIssue(row) {
|
||||
await ElMessageBox.confirm(`确定开具发票「${row.invoice_no}」吗?`, '开票确认', { type: 'info' })
|
||||
await invoiceApi.issue(row.id); ElMessage.success('开票成功'); fetchList()
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<el-input v-model="searchForm.invoice_no" placeholder="发票号" style="width:180px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
<el-select v-model="searchForm.type" placeholder="类型" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增发票</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="invoice_no" label="发票号" min-width="150" />
|
||||
<el-table-column prop="customer_id" label="客户ID" width="100" align="center" />
|
||||
<el-table-column label="类型" width="80" align="center"><template #default="{ row }"><el-tag size="small">{{ typeMap[row.type] }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="amount" label="金额" width="100" align="right" />
|
||||
<el-table-column prop="tax_amount" label="税额" width="100" align="right" />
|
||||
<el-table-column prop="title" label="发票抬头" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="90" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="issued_at" label="开票时间" width="160" />
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status===0" size="small" type="success" link @click="handleIssue(row)">开票</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,15,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑发票':'新增发票'" width="550px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
||||
<el-form-item label="发票号" prop="invoice_no"><el-input v-model="form.invoice_no" placeholder="请输入发票号" /></el-form-item>
|
||||
<el-form-item label="客户ID"><el-input v-model="form.customer_id" placeholder="请输入客户ID" /></el-form-item>
|
||||
<el-form-item label="类型"><el-select v-model="form.type" style="width:100%"><el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" /></el-select></el-form-item>
|
||||
<el-form-item label="金额"><el-input-number v-model="form.amount" :min="0" :precision="2" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="税额"><el-input-number v-model="form.tax_amount" :min="0" :precision="2" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="发票抬头"><el-input v-model="form.title" placeholder="请输入发票抬头" /></el-form-item>
|
||||
<el-form-item label="税号"><el-input v-model="form.tax_no" placeholder="请输入税号" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="dialogVisible=false">取消</el-button><el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button></template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,104 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { prepaidCardApi } from '@/api/finance.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const searchForm = reactive({ name: '', status: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 15 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({ id: null, name: '', face_value: 0, sell_price: 0, discount_rate: 1, validity_days: 365, status: 1, sort: 0 })
|
||||
const isEdit = ref(false)
|
||||
const formRules = { name: [{ required: true, message: '请输入储值卡名称', trigger: 'blur' }] }
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await prepaidCardApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() { Object.assign(searchForm, { name: '', status: '' }); handleSearch() }
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
Object.assign(form, { id: null, name: '', face_value: 0, sell_price: 0, discount_rate: 1, validity_days: 365, status: 1, sort: 0 })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
async function handleEdit(row) { isEdit.value = true; Object.assign(form, { ...row }); dialogVisible.value = true }
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (isEdit.value) { await prepaidCardApi.update(form.id, form); ElMessage.success('更新成功') }
|
||||
else { await prepaidCardApi.create(form); ElMessage.success('创建成功') }
|
||||
dialogVisible.value = false; fetchList()
|
||||
} finally { submitLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除储值卡「${row.name}」吗?`, '删除确认', { type: 'warning' })
|
||||
await prepaidCardApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<el-input v-model="searchForm.name" placeholder="储值卡名称" style="width:180px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="停用" :value="0" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增储值卡</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="name" label="储值卡名称" min-width="150" />
|
||||
<el-table-column prop="face_value" label="面值" width="100" align="right" />
|
||||
<el-table-column prop="sell_price" label="售价" width="100" align="right" />
|
||||
<el-table-column prop="discount_rate" label="折扣率" width="90" align="center" />
|
||||
<el-table-column prop="validity_days" label="有效期(天)" width="100" align="center" />
|
||||
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status?'success':'danger'" size="small">{{ row.status?'启用':'停用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,15,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑储值卡':'新增储值卡'" width="500px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
||||
<el-form-item label="储值卡名称" prop="name"><el-input v-model="form.name" placeholder="请输入储值卡名称" /></el-form-item>
|
||||
<el-form-item label="面值"><el-input-number v-model="form.face_value" :min="0" :precision="2" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="售价"><el-input-number v-model="form.sell_price" :min="0" :precision="2" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="折扣率"><el-input-number v-model="form.discount_rate" :min="0" :max="1" :step="0.01" :precision="2" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="有效期(天)"><el-input-number v-model="form.validity_days" :min="1" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item>
|
||||
<el-form-item label="排序"><el-input-number v-model="form.sort" :min="0" :max="999" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="dialogVisible=false">取消</el-button><el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button></template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,220 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { financeRecordApi } from '@/api/finance.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增收支记录')
|
||||
const submitLoading = ref(false)
|
||||
const auditVisible = ref(false)
|
||||
const currentRecord = ref(null)
|
||||
const isEdit = ref(false)
|
||||
|
||||
const searchForm = reactive({ type: '', audit_status: '', record_no: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
id: null, record_no: '', type: '', category_id: '',
|
||||
amount: 0, pay_method: '', description: '', record_date: ''
|
||||
})
|
||||
const formRules = {
|
||||
record_no: [{ required: true, message: '请输入记录编号', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
|
||||
amount: [{ required: true, message: '请输入金额', trigger: 'blur' }],
|
||||
record_date: [{ required: true, message: '请选择日期', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const auditForm = reactive({ status: 1, audit_remark: '' })
|
||||
|
||||
const typeMap = { 1: '收入', 2: '支出' }
|
||||
const typeType = { 1: 'success', 2: 'danger' }
|
||||
const auditMap = { 0: '待审核', 1: '已通过', 2: '已驳回' }
|
||||
const auditType = { 0: 'warning', 1: 'success', 2: 'danger' }
|
||||
const payMethodMap = { 1: '现金', 2: '微信', 3: '支付宝', 4: '银行转账' }
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await financeRecordApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() { Object.assign(searchForm, { type: '', audit_status: '', record_no: '' }); handleSearch() }
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false; dialogTitle.value = '新增收支记录'
|
||||
Object.assign(form, { id: null, record_no: '', type: '', category_id: '', amount: 0, pay_method: '', description: '', record_date: '' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(row) {
|
||||
if (row.audit_status !== 0) { ElMessage.warning('只能编辑待审核记录'); return }
|
||||
isEdit.value = true; dialogTitle.value = '编辑收支记录'
|
||||
const res = await financeRecordApi.getDetail(row.id)
|
||||
const d = res.data
|
||||
Object.assign(form, {
|
||||
id: d.id, record_no: d.record_no, type: d.type, category_id: d.category_id || '',
|
||||
amount: d.amount, pay_method: d.pay_method || '', description: d.description || '', record_date: d.record_date || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (isEdit.value) { await financeRecordApi.update(form.id, form); ElMessage.success('更新成功') }
|
||||
else { await financeRecordApi.create(form); ElMessage.success('创建成功') }
|
||||
dialogVisible.value = false; fetchList()
|
||||
} finally { submitLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
if (row.audit_status !== 0) { ElMessage.warning('只能删除待审核记录'); return }
|
||||
await ElMessageBox.confirm('确定删除该收支记录吗?', '删除确认', { type: 'warning' })
|
||||
await financeRecordApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
|
||||
}
|
||||
|
||||
function openAudit(row) {
|
||||
currentRecord.value = row
|
||||
Object.assign(auditForm, { status: 1, audit_remark: '' })
|
||||
auditVisible.value = true
|
||||
}
|
||||
|
||||
async function submitAudit() {
|
||||
await financeRecordApi.audit(currentRecord.value.id, auditForm)
|
||||
ElMessage.success('审核完成')
|
||||
auditVisible.value = false; fetchList()
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
onMounted(() => { fetchList() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<el-select v-model="searchForm.type" placeholder="收支类型" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
<el-select v-model="searchForm.audit_status" placeholder="审核状态" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in auditMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
<el-input v-model="searchForm.record_no" placeholder="记录编号" style="width:180px" clearable @keydown.enter="handleSearch" />
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增记录</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="record_no" label="记录编号" min-width="150" />
|
||||
<el-table-column label="类型" width="90" align="center">
|
||||
<template #default="{ row }"><el-tag :type="typeType[row.type]" size="small">{{ typeMap[row.type] || '-' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="category" label="分类" min-width="120">
|
||||
<template #default="{ row }">{{ row.category?.name || row.category || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="金额" min-width="110" align="right" />
|
||||
<el-table-column label="支付方式" width="110" align="center">
|
||||
<template #default="{ row }">{{ payMethodMap[row.pay_method] || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审核状态" width="100" align="center">
|
||||
<template #default="{ row }"><el-tag :type="auditType[row.audit_status]" size="small">{{ auditMap[row.audit_status] }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="record_date" label="记录日期" min-width="120" />
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.audit_status===0" size="small" type="success" link @click="openAudit(row)">审核</el-button>
|
||||
<el-button v-if="row.audit_status===0" size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.audit_status===0" size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
<span v-if="row.audit_status!==0" class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="650px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="记录编号" prop="record_no">
|
||||
<el-input v-model="form.record_no" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-select v-model="form.type" placeholder="选择类型" style="width:100%">
|
||||
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="分类">
|
||||
<el-input v-model="form.category_id" placeholder="分类ID" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="金额" prop="amount">
|
||||
<el-input-number v-model="form.amount" :min="0" :precision="2" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="支付方式">
|
||||
<el-select v-model="form.pay_method" placeholder="选择支付方式" clearable style="width:100%">
|
||||
<el-option v-for="(v, k) in payMethodMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="记录日期" prop="record_date">
|
||||
<el-date-picker v-model="form.record_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="form.description" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Audit -->
|
||||
<el-dialog v-model="auditVisible" title="收支审核" width="450px" destroy-on-close>
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="审核结果">
|
||||
<el-radio-group v-model="auditForm.status">
|
||||
<el-radio :value="1">通过</el-radio>
|
||||
<el-radio :value="2">驳回</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核备注">
|
||||
<el-input v-model="auditForm.audit_remark" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="auditVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitAudit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,141 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { attendanceApi } from '@/api/hr.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const searchForm = reactive({ user_id: '', status: '', date_range: [] })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({ user_id: '', attendance_date: '', clock_in: '', clock_out: '', status: 1, overtime_hours: 0, remark: '' })
|
||||
|
||||
const statusMap = { 1: '正常', 2: '迟到', 3: '早退', 4: '旷工', 5: '请假' }
|
||||
const statusType = { 1: 'success', 2: 'warning', 3: 'warning', 4: 'danger', 5: 'info' }
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { ...pagination, user_id: searchForm.user_id, status: searchForm.status }
|
||||
if (searchForm.date_range?.length === 2) {
|
||||
params.start_date = searchForm.date_range[0]
|
||||
params.end_date = searchForm.date_range[1]
|
||||
}
|
||||
const res = await attendanceApi.getList(params)
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() { Object.assign(searchForm, { user_id: '', status: '', date_range: [] }); handleSearch() }
|
||||
|
||||
async function handleClockIn() {
|
||||
try {
|
||||
await attendanceApi.clockIn()
|
||||
ElMessage.success('上班打卡成功'); fetchList()
|
||||
} catch (e) { ElMessage.error(e.response?.data?.message || '打卡失败') }
|
||||
}
|
||||
|
||||
async function handleClockOut() {
|
||||
try {
|
||||
await attendanceApi.clockOut()
|
||||
ElMessage.success('下班打卡成功'); fetchList()
|
||||
} catch (e) { ElMessage.error(e.response?.data?.message || '打卡失败') }
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
Object.assign(form, { user_id: '', attendance_date: '', clock_in: '', clock_out: '', status: 1, overtime_hours: 0, remark: '' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await attendanceApi.create(form)
|
||||
ElMessage.success('创建成功'); dialogVisible.value = false; fetchList()
|
||||
} finally { submitLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
onMounted(() => { fetchList() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<el-input v-model="searchForm.user_id" placeholder="员工ID" clearable style="width:140px" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
<el-date-picker v-model="searchForm.date_range" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" />
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions">
|
||||
<el-button type="success" @click="handleClockIn">上班打卡</el-button>
|
||||
<el-button type="warning" @click="handleClockOut">下班打卡</el-button>
|
||||
<el-button type="primary" @click="handleAdd">手动录入</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column label="员工" min-width="100">
|
||||
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="attendance_date" label="考勤日期" width="120" />
|
||||
<el-table-column prop="clock_in" label="上班打卡" width="100" />
|
||||
<el-table-column prop="clock_out" label="下班打卡" width="100" />
|
||||
<el-table-column prop="overtime_hours" label="加班(h)" width="90" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" />
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" title="手动录入考勤" width="500px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="{ user_id: [{ required: true, message: '请输入员工ID' }], attendance_date: [{ required: true, message: '请选择考勤日期' }] }" label-width="90px">
|
||||
<el-form-item label="员工ID" prop="user_id">
|
||||
<el-input v-model="form.user_id" placeholder="请输入员工ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="考勤日期" prop="attendance_date">
|
||||
<el-date-picker v-model="form.attendance_date" type="date" value-format="YYYY-MM-DD" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="上班时间">
|
||||
<el-time-picker v-model="form.clock_in" value-format="HH:mm:ss" placeholder="上班打卡时间" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="下班时间">
|
||||
<el-time-picker v-model="form.clock_out" value-format="HH:mm:ss" placeholder="下班打卡时间" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status" style="width:100%">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="加班时长">
|
||||
<el-input-number v-model="form.overtime_hours" :min="0" :max="24" :precision="1" :step="0.5" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,160 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { leaveRequestApi } from '@/api/hr.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const isEdit = ref(false)
|
||||
|
||||
const searchForm = reactive({ type: '', status: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({ id: null, user_id: '', type: 1, start_date: '', end_date: '', days: 1, reason: '' })
|
||||
|
||||
const typeMap = { 1: '年假', 2: '事假', 3: '病假', 4: '调休', 5: '产假', 6: '婚假' }
|
||||
const statusMap = { 0: '待审批', 1: '已批准', 2: '已拒绝' }
|
||||
const statusType = { 0: 'warning', 1: 'success', 2: 'danger' }
|
||||
|
||||
const formRules = {
|
||||
user_id: [{ required: true, message: '请输入员工ID' }],
|
||||
type: [{ required: true, message: '请选择请假类型' }],
|
||||
start_date: [{ required: true, message: '请选择开始日期' }],
|
||||
end_date: [{ required: true, message: '请选择结束日期' }],
|
||||
days: [{ required: true, message: '请输入请假天数' }]
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await leaveRequestApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() { Object.assign(searchForm, { type: '', status: '' }); handleSearch() }
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
Object.assign(form, { id: null, user_id: '', type: 1, start_date: '', end_date: '', days: 1, reason: '' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(row) {
|
||||
isEdit.value = true
|
||||
Object.assign(form, { id: row.id, user_id: row.user_id, type: row.type, start_date: row.start_date, end_date: row.end_date, days: row.days, reason: row.reason })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (isEdit.value) { await leaveRequestApi.update(form.id, form) } else { await leaveRequestApi.create(form) }
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功'); dialogVisible.value = false; fetchList()
|
||||
} finally { submitLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
async function handleAudit(row, status) {
|
||||
const label = status === 1 ? '批准' : '拒绝'
|
||||
await ElMessageBox.confirm(`确定${label}该请假申请吗?`, '审批确认', { type: 'warning' })
|
||||
await leaveRequestApi.audit(row.id, { status })
|
||||
ElMessage.success(`${label}成功`); fetchList()
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm('确定删除该请假申请吗?', '删除确认', { type: 'warning' })
|
||||
await leaveRequestApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
onMounted(() => { fetchList() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<el-select v-model="searchForm.type" placeholder="请假类型" clearable style="width:130px">
|
||||
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
<el-select v-model="searchForm.status" placeholder="审批状态" clearable style="width:130px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<div class="table-actions">
|
||||
<el-button type="primary" @click="handleAdd">新增请假</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column label="员工" min-width="100">
|
||||
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ typeMap[row.type] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="start_date" label="开始日期" width="120" />
|
||||
<el-table-column prop="end_date" label="结束日期" width="120" />
|
||||
<el-table-column prop="days" label="天数" width="70" align="center" />
|
||||
<el-table-column prop="reason" label="事由" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.status === 0">
|
||||
<el-button size="small" type="success" link @click="handleAudit(row, 1)">批准</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleAudit(row, 2)">拒绝</el-button>
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑请假' : '新增请假'" width="500px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
<el-form-item label="员工ID" prop="user_id">
|
||||
<el-input v-model="form.user_id" placeholder="请输入员工ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="请假类型" prop="type">
|
||||
<el-select v-model="form.type" style="width:100%">
|
||||
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="开始日期" prop="start_date">
|
||||
<el-date-picker v-model="form.start_date" type="date" value-format="YYYY-MM-DD" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="结束日期" prop="end_date">
|
||||
<el-date-picker v-model="form.end_date" type="date" value-format="YYYY-MM-DD" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="天数" prop="days">
|
||||
<el-input-number v-model="form.days" :min="0.5" :max="365" :precision="1" :step="0.5" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="事由">
|
||||
<el-input v-model="form.reason" type="textarea" :rows="3" placeholder="请输入请假事由" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,166 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { employeeProfileApi } from '@/api/hr.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增员工档案')
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const searchForm = reactive({ employee_no: '', status: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
id: null, user_id: '', employee_no: '', hire_date: '', birth_date: '',
|
||||
gender: 0, id_card: '', education: '', emergency_contact: '', emergency_phone: '',
|
||||
base_salary: 0, position_salary: 0, status: 1
|
||||
})
|
||||
const isEdit = ref(false)
|
||||
const formRules = {
|
||||
user_id: [{ required: true, message: '请输入用户ID', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const statusMap = { 1: '在职', 2: '试用', 3: '离职' }
|
||||
const statusType = { 1: 'success', 2: 'warning', 3: 'danger' }
|
||||
const genderMap = { 0: '未知', 1: '男', 2: '女' }
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await employeeProfileApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() { Object.assign(searchForm, { employee_no: '', status: '' }); handleSearch() }
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false; dialogTitle.value = '新增员工档案'
|
||||
Object.assign(form, {
|
||||
id: null, user_id: '', employee_no: '', hire_date: '', birth_date: '',
|
||||
gender: 0, id_card: '', education: '', emergency_contact: '', emergency_phone: '',
|
||||
base_salary: 0, position_salary: 0, status: 1
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(row) {
|
||||
isEdit.value = true; dialogTitle.value = '编辑员工档案'
|
||||
const res = await employeeProfileApi.getDetail(row.id)
|
||||
const d = res.data
|
||||
Object.assign(form, {
|
||||
id: d.id, user_id: d.user_id || '', employee_no: d.employee_no || '',
|
||||
hire_date: d.hire_date || '', birth_date: d.birth_date || '',
|
||||
gender: d.gender ?? 0, id_card: d.id_card || '', education: d.education || '',
|
||||
emergency_contact: d.emergency_contact || '', emergency_phone: d.emergency_phone || '',
|
||||
base_salary: d.base_salary || 0, position_salary: d.position_salary || 0, status: d.status || 1
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (isEdit.value) { await employeeProfileApi.update(form.id, form); ElMessage.success('更新成功') }
|
||||
else { await employeeProfileApi.create(form); ElMessage.success('创建成功') }
|
||||
dialogVisible.value = false; fetchList()
|
||||
} finally { submitLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除员工「${row.user?.name || row.employee_no}」的档案吗?`, '删除确认', { type: 'warning' })
|
||||
await employeeProfileApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<el-input v-model="searchForm.employee_no" placeholder="工号" style="width:160px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option v-for="(v,k) in statusMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增员工档案</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column prop="employee_no" label="工号" width="120" />
|
||||
<el-table-column label="姓名" min-width="100">
|
||||
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="性别" width="70" align="center">
|
||||
<template #default="{ row }">{{ genderMap[row.gender] || '未知' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="hire_date" label="入职日期" min-width="110" />
|
||||
<el-table-column prop="education" label="学历" width="90" />
|
||||
<el-table-column prop="base_salary" label="基本工资" width="100" align="right" />
|
||||
<el-table-column prop="position_salary" label="岗位工资" width="100" align="right" />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="用户ID" prop="user_id"><el-input v-model="form.user_id" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="工号"><el-input v-model="form.employee_no" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="入职日期"><el-date-picker v-model="form.hire_date" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="出生日期"><el-date-picker v-model="form.birth_date" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="性别">
|
||||
<el-select v-model="form.gender" style="width:100%">
|
||||
<el-option v-for="(v,k) in genderMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12"><el-form-item label="身份证号"><el-input v-model="form.id_card" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="学历"><el-input v-model="form.education" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="紧急联系人"><el-input v-model="form.emergency_contact" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="紧急电话"><el-input v-model="form.emergency_phone" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="基本工资"><el-input-number v-model="form.base_salary" :min="0" :precision="2" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="岗位工资"><el-input-number v-model="form.position_salary" :min="0" :precision="2" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status" style="width:100%">
|
||||
<el-option v-for="(v,k) in statusMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,274 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { salaryRecordApi, employeeProfileApi } from '@/api/hr.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const employees = ref([])
|
||||
|
||||
const searchForm = reactive({ year_month: '', status: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 20 })
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
id: null, user_id: '', year_month: '',
|
||||
base_salary: 0, position_salary: 0, performance: 0,
|
||||
overtime_pay: 0, subsidy: 0,
|
||||
social_insurance: 0, housing_fund: 0, tax: 0,
|
||||
absence_deduction: 0, other_deduction: 0,
|
||||
actual_amount: 0
|
||||
})
|
||||
const isEdit = ref(false)
|
||||
const autoCompute = ref(true)
|
||||
|
||||
const formRules = {
|
||||
user_id: [{ required: true, message: '请选择员工', trigger: 'change' }],
|
||||
year_month: [{ required: true, message: '请输入工资月份', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const statusMap = { 0: '待确认', 1: '已确认', 2: '已发放' }
|
||||
const statusType = { 0: 'warning', 1: 'primary', 2: 'success' }
|
||||
|
||||
const computedActual = computed(() => {
|
||||
const income = Number(form.base_salary || 0)
|
||||
+ Number(form.position_salary || 0)
|
||||
+ Number(form.performance || 0)
|
||||
+ Number(form.overtime_pay || 0)
|
||||
+ Number(form.subsidy || 0)
|
||||
const deduction = Number(form.social_insurance || 0)
|
||||
+ Number(form.housing_fund || 0)
|
||||
+ Number(form.tax || 0)
|
||||
+ Number(form.absence_deduction || 0)
|
||||
+ Number(form.other_deduction || 0)
|
||||
return Math.round((income - deduction) * 100) / 100
|
||||
})
|
||||
|
||||
watch(computedActual, (val) => {
|
||||
if (autoCompute.value) form.actual_amount = val
|
||||
})
|
||||
|
||||
function getDeductionTotal(row) {
|
||||
return Number(row.social_insurance || 0)
|
||||
+ Number(row.housing_fund || 0)
|
||||
+ Number(row.tax || 0)
|
||||
+ Number(row.absence_deduction || 0)
|
||||
+ Number(row.other_deduction || 0)
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await salaryRecordApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function fetchEmployees() {
|
||||
try {
|
||||
const res = await employeeProfileApi.getList({ per_page: 500 })
|
||||
employees.value = res.data?.list || res.data?.data || []
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() { Object.assign(searchForm, { year_month: '', status: '' }); handleSearch() }
|
||||
|
||||
const defaultForm = {
|
||||
id: null, user_id: '', year_month: '',
|
||||
base_salary: 0, position_salary: 0, performance: 0,
|
||||
overtime_pay: 0, subsidy: 0,
|
||||
social_insurance: 0, housing_fund: 0, tax: 0,
|
||||
absence_deduction: 0, other_deduction: 0,
|
||||
actual_amount: 0
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false; autoCompute.value = true
|
||||
Object.assign(form, { ...defaultForm })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(row) {
|
||||
if (row.status !== 0) { ElMessage.warning('只能编辑待确认记录'); return }
|
||||
isEdit.value = true; autoCompute.value = false
|
||||
const res = await salaryRecordApi.getDetail(row.id)
|
||||
const d = res.data
|
||||
Object.assign(form, {
|
||||
id: d.id, user_id: d.user_id, year_month: d.year_month,
|
||||
base_salary: d.base_salary || 0, position_salary: d.position_salary || 0,
|
||||
performance: d.performance || 0, overtime_pay: d.overtime_pay || 0,
|
||||
subsidy: d.subsidy || 0, social_insurance: d.social_insurance || 0,
|
||||
housing_fund: d.housing_fund || 0, tax: d.tax || 0,
|
||||
absence_deduction: d.absence_deduction || 0, other_deduction: d.other_deduction || 0,
|
||||
actual_amount: d.actual_amount || 0
|
||||
})
|
||||
autoCompute.value = true
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (isEdit.value) { await salaryRecordApi.update(form.id, form); ElMessage.success('更新成功') }
|
||||
else { await salaryRecordApi.create(form); ElMessage.success('创建成功') }
|
||||
dialogVisible.value = false; fetchList()
|
||||
} finally { submitLoading.value = false }
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
if (row.status !== 0) { ElMessage.warning('只能删除待确认记录'); return }
|
||||
await ElMessageBox.confirm('确定删除该工资记录吗?', '删除确认', { type: 'warning' })
|
||||
await salaryRecordApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
|
||||
}
|
||||
|
||||
async function handleConfirm(row) {
|
||||
await ElMessageBox.confirm('确认该工资记录?确认后不可编辑。', '确认操作', { type: 'warning' })
|
||||
await salaryRecordApi.confirm(row.id); ElMessage.success('确认成功'); fetchList()
|
||||
}
|
||||
|
||||
async function handlePay(row) {
|
||||
await ElMessageBox.confirm('确定标记为已发放吗?', '发放确认', { type: 'warning' })
|
||||
await salaryRecordApi.pay(row.id); ElMessage.success('发放成功'); fetchList()
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
|
||||
|
||||
onMounted(() => { fetchList(); fetchEmployees() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<el-input v-model="searchForm.year_month" placeholder="工资月份 (如 2024-03)" style="width:200px" clearable @keydown.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
|
||||
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增工资记录</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column label="员工" min-width="100">
|
||||
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="year_month" label="工资月份" width="110" align="center" />
|
||||
<el-table-column prop="base_salary" label="基本工资" width="110" align="right" />
|
||||
<el-table-column prop="position_salary" label="岗位工资" width="110" align="right" />
|
||||
<el-table-column prop="performance" label="绩效" width="100" align="right" />
|
||||
<el-table-column prop="overtime_pay" label="加班费" width="100" align="right" />
|
||||
<el-table-column prop="subsidy" label="补贴" width="90" align="right" />
|
||||
<el-table-column label="扣除合计" width="110" align="right">
|
||||
<template #default="{ row }">{{ getDeductionTotal(row).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实发金额" width="120" align="right">
|
||||
<template #default="{ row }"><span style="font-weight:700">{{ row.actual_amount }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status===0" size="small" type="success" link @click="handleConfirm(row)">确认</el-button>
|
||||
<el-button v-if="row.status===1" size="small" type="success" link @click="handlePay(row)">发放</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="row.status===0" size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑工资记录' : '新增工资记录'" width="750px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="员工" prop="user_id">
|
||||
<el-select v-model="form.user_id" placeholder="选择员工" filterable style="width:100%">
|
||||
<el-option v-for="e in employees" :key="e.id" :label="e.user?.name || e.name || `员工${e.id}`" :value="e.user_id || e.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="工资月份" prop="year_month">
|
||||
<el-input v-model="form.year_month" placeholder="如 2024-03" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="基本工资">
|
||||
<el-input-number v-model="form.base_salary" :min="0" :precision="2" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="岗位工资">
|
||||
<el-input-number v-model="form.position_salary" :min="0" :precision="2" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="绩效">
|
||||
<el-input-number v-model="form.performance" :min="0" :precision="2" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="加班费">
|
||||
<el-input-number v-model="form.overtime_pay" :min="0" :precision="2" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="补贴">
|
||||
<el-input-number v-model="form.subsidy" :min="0" :precision="2" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24"><el-divider content-position="left">扣除项</el-divider></el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="社保">
|
||||
<el-input-number v-model="form.social_insurance" :min="0" :precision="2" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="公积金">
|
||||
<el-input-number v-model="form.housing_fund" :min="0" :precision="2" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="个税">
|
||||
<el-input-number v-model="form.tax" :min="0" :precision="2" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="缺勤扣款">
|
||||
<el-input-number v-model="form.absence_deduction" :min="0" :precision="2" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="其他扣款">
|
||||
<el-input-number v-model="form.other_deduction" :min="0" :precision="2" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24"><el-divider /></el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="实发金额">
|
||||
<el-input-number v-model="form.actual_amount" :precision="2" style="width:100%" />
|
||||
<div style="font-size:12px;color:var(--color-text-secondary)">自动计算:收入合计 - 扣除合计 = {{ computedActual.toFixed(2) }}</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { scheduleApi } from '@/api/hr.js'
|
||||
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const searchForm = reactive({ user_id: '', start_date: '', end_date: '' })
|
||||
const pagination = reactive({ page: 1, per_page: 31 })
|
||||
|
||||
const form = reactive({ user_id: '', schedule_date: '', shift_type: 1, start_time: '', end_time: '' })
|
||||
|
||||
const shiftMap = { 1: '早班', 2: '中班', 3: '晚班', 4: '夜班', 5: '休息' }
|
||||
const shiftType = { 1: 'success', 2: 'primary', 3: 'warning', 4: 'danger', 5: 'info' }
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await scheduleApi.getList({ ...searchForm, ...pagination })
|
||||
tableData.value = res.data?.list || res.data?.data || []
|
||||
total.value = res.data?.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function handleSearch() { pagination.page = 1; fetchList() }
|
||||
function handleReset() { Object.assign(searchForm, { user_id: '', start_date: '', end_date: '' }); handleSearch() }
|
||||
|
||||
function handleAdd() {
|
||||
Object.assign(form, { user_id: '', schedule_date: '', shift_type: 1, start_time: '', end_time: '' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.user_id || !form.schedule_date || !form.shift_type) { ElMessage.warning('请填写必要信息'); return }
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await scheduleApi.create(form)
|
||||
ElMessage.success('排班成功'); dialogVisible.value = false; fetchList()
|
||||
} finally { submitLoading.value = false }
|
||||
}
|
||||
|
||||
function handlePageChange(val) { pagination.page = val; fetchList() }
|
||||
|
||||
onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="search-bar">
|
||||
<el-input v-model="searchForm.user_id" placeholder="员工ID" style="width:160px" clearable @keydown.enter="handleSearch" />
|
||||
<el-date-picker v-model="searchForm.start_date" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" />
|
||||
<el-date-picker v-model="searchForm.end_date" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" />
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增排班</el-button></div>
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
|
||||
<el-table-column label="员工" min-width="100">
|
||||
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="schedule_date" label="排班日期" width="120" />
|
||||
<el-table-column label="班次" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="shiftType[row.shift_type]" size="small">{{ shiftMap[row.shift_type] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="start_time" label="开始时间" width="100" />
|
||||
<el-table-column prop="end_time" label="结束时间" width="100" />
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="pagination.page" :total="total" layout="total,prev,pager,next" background @current-change="handlePageChange" />
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="新增排班" width="500px" destroy-on-close>
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="员工ID"><el-input v-model="form.user_id" /></el-form-item>
|
||||
<el-form-item label="排班日期"><el-date-picker v-model="form.schedule_date" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="班次">
|
||||
<el-select v-model="form.shift_type" style="width:100%">
|
||||
<el-option v-for="(v,k) in shiftMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="开始时间"><el-time-picker v-model="form.start_time" format="HH:mm" value-format="HH:mm" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="结束时间"><el-time-picker v-model="form.end_time" format="HH:mm" value-format="HH:mm" style="width:100%" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user