feat: 第五阶段办公协同/统计报表/知识库模块
- 新增3个migration(9张表): office(6表)/report(1表)/kb(2表) - 新增9个Model + 9个Controller - 新增43条API路由(总计305条) - 新增3个前端API模块 + 10个管理页面 - 迁移运行通过, vite build验证通过
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Kb;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Kb\KbArticle;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class KbArticleController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = KbArticle::query()->with(['category', 'author']);
|
||||
$query->when($request->category_id, fn($q, $v) => $q->where('category_id', $v));
|
||||
$query->when($request->filled('status'), fn($q) => $q->where('status', $request->status));
|
||||
$query->when($request->filled('visibility'), fn($q) => $q->where('visibility', $request->visibility));
|
||||
$query->when($request->keyword, fn($q, $v) => $q->where('title', 'like', "%{$v}%"));
|
||||
|
||||
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'content' => 'nullable|string',
|
||||
'category_id' => 'nullable|exists:kb_categories,id',
|
||||
'visibility' => 'nullable|integer|in:1,2',
|
||||
'cover_image' => 'nullable|string|max:255',
|
||||
'attachments' => 'nullable|array',
|
||||
]);
|
||||
$validated['author_id'] = auth()->id();
|
||||
$validated['status'] = 0;
|
||||
|
||||
$article = KbArticle::create($validated);
|
||||
return $this->success($article->load(['category', 'author']));
|
||||
}
|
||||
|
||||
public function show(KbArticle $kbArticle): JsonResponse
|
||||
{
|
||||
$kbArticle->increment('views');
|
||||
return $this->success($kbArticle->load(['category', 'author']));
|
||||
}
|
||||
|
||||
public function update(Request $request, KbArticle $kbArticle): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'sometimes|string|max:255',
|
||||
'content' => 'nullable|string',
|
||||
'category_id' => 'nullable|exists:kb_categories,id',
|
||||
'visibility' => 'nullable|integer|in:1,2',
|
||||
'cover_image' => 'nullable|string|max:255',
|
||||
'attachments' => 'nullable|array',
|
||||
]);
|
||||
$kbArticle->update($validated);
|
||||
return $this->success($kbArticle);
|
||||
}
|
||||
|
||||
public function destroy(KbArticle $kbArticle): JsonResponse
|
||||
{
|
||||
$kbArticle->delete();
|
||||
return $this->success(null);
|
||||
}
|
||||
|
||||
public function publish(KbArticle $kbArticle): JsonResponse
|
||||
{
|
||||
if ($kbArticle->status >= 2) {
|
||||
return $this->error('当前状态不允许发布');
|
||||
}
|
||||
$kbArticle->update(['status' => 2, 'published_at' => now()]);
|
||||
return $this->success($kbArticle);
|
||||
}
|
||||
|
||||
public function offline(KbArticle $kbArticle): JsonResponse
|
||||
{
|
||||
if ($kbArticle->status !== 2) {
|
||||
return $this->error('仅已发布文章可下架');
|
||||
}
|
||||
$kbArticle->update(['status' => 3]);
|
||||
return $this->success($kbArticle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Kb;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Kb\KbCategory;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class KbCategoryController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = KbCategory::query()->with('children');
|
||||
$query->when($request->filled('status'), fn($q) => $q->where('status', $request->status));
|
||||
$query->where('parent_id', 0);
|
||||
|
||||
return $this->success($query->orderBy('sort')->orderBy('id')->get());
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:100',
|
||||
'parent_id' => 'nullable|integer|min:0',
|
||||
'sort' => 'nullable|integer',
|
||||
'status' => 'nullable|integer|in:0,1',
|
||||
]);
|
||||
$validated['parent_id'] = $validated['parent_id'] ?? 0;
|
||||
|
||||
$category = KbCategory::create($validated);
|
||||
return $this->success($category);
|
||||
}
|
||||
|
||||
public function show(KbCategory $kbCategory): JsonResponse
|
||||
{
|
||||
return $this->success($kbCategory->load('children'));
|
||||
}
|
||||
|
||||
public function update(Request $request, KbCategory $kbCategory): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'sometimes|string|max:100',
|
||||
'parent_id' => 'nullable|integer|min:0',
|
||||
'sort' => 'nullable|integer',
|
||||
'status' => 'nullable|integer|in:0,1',
|
||||
]);
|
||||
$kbCategory->update($validated);
|
||||
return $this->success($kbCategory);
|
||||
}
|
||||
|
||||
public function destroy(KbCategory $kbCategory): JsonResponse
|
||||
{
|
||||
if ($kbCategory->children()->exists()) {
|
||||
return $this->error('请先删除子分类');
|
||||
}
|
||||
if ($kbCategory->articles()->exists()) {
|
||||
return $this->error('该分类下还有文章,无法删除');
|
||||
}
|
||||
$kbCategory->delete();
|
||||
return $this->success(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Office;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Office\Announcement;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AnnouncementController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Announcement::query()->with('author');
|
||||
$query->when($request->type, fn($q, $v) => $q->where('type', $v));
|
||||
$query->when($request->has('publish_status'), fn($q) => $q->where('publish_status', $request->publish_status));
|
||||
|
||||
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'content' => 'required|string',
|
||||
'type' => 'sometimes|integer|in:1,2,3',
|
||||
'is_top' => 'sometimes|boolean',
|
||||
]);
|
||||
$validated['author_id'] = auth()->id();
|
||||
|
||||
return $this->success(Announcement::create($validated));
|
||||
}
|
||||
|
||||
public function show(Announcement $announcement): JsonResponse
|
||||
{
|
||||
return $this->success($announcement->load('author'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Announcement $announcement): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'sometimes|string|max:255',
|
||||
'content' => 'sometimes|string',
|
||||
'type' => 'sometimes|integer|in:1,2,3',
|
||||
'is_top' => 'sometimes|boolean',
|
||||
]);
|
||||
$announcement->update($validated);
|
||||
|
||||
return $this->success($announcement);
|
||||
}
|
||||
|
||||
public function destroy(Announcement $announcement): JsonResponse
|
||||
{
|
||||
$announcement->delete();
|
||||
return $this->success(null);
|
||||
}
|
||||
|
||||
public function publish(Announcement $announcement): JsonResponse
|
||||
{
|
||||
if ($announcement->publish_status === 1) {
|
||||
return $this->error('该公告已发布', 40001);
|
||||
}
|
||||
$announcement->update([
|
||||
'publish_status' => 1,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
return $this->success($announcement);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Office;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Office\Approval;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ApprovalController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Approval::query()->with(['template', 'applicant']);
|
||||
$query->when($request->has('status'), fn($q) => $q->where('status', $request->status));
|
||||
$query->when($request->applicant_id, fn($q, $v) => $q->where('applicant_id', $v));
|
||||
|
||||
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'template_id' => 'nullable|exists:approval_templates,id',
|
||||
'form_data' => 'nullable|array',
|
||||
'related_type' => 'nullable|string|max:50',
|
||||
'related_id' => 'nullable|integer',
|
||||
]);
|
||||
$validated['applicant_id'] = auth()->id();
|
||||
$validated['status'] = 0;
|
||||
|
||||
return $this->success(Approval::create($validated));
|
||||
}
|
||||
|
||||
public function show(Approval $approval): JsonResponse
|
||||
{
|
||||
return $this->success($approval->load(['template', 'applicant', 'nodes.approver']));
|
||||
}
|
||||
|
||||
public function update(Request $request, Approval $approval): JsonResponse
|
||||
{
|
||||
if ($approval->status > 1) {
|
||||
return $this->error('该审批已完结,无法修改', 40001);
|
||||
}
|
||||
$validated = $request->validate([
|
||||
'title' => 'sometimes|string|max:255',
|
||||
'form_data' => 'nullable|array',
|
||||
]);
|
||||
$approval->update($validated);
|
||||
|
||||
return $this->success($approval);
|
||||
}
|
||||
|
||||
public function destroy(Approval $approval): JsonResponse
|
||||
{
|
||||
if ($approval->status > 1) {
|
||||
return $this->error('该审批已完结,无法删除', 40001);
|
||||
}
|
||||
$approval->delete();
|
||||
|
||||
return $this->success(null);
|
||||
}
|
||||
|
||||
public function approve(Approval $approval): JsonResponse
|
||||
{
|
||||
$node = $approval->nodes()
|
||||
->where('approver_id', auth()->id())
|
||||
->where('action', 0)
|
||||
->where('node_order', $approval->current_node)
|
||||
->first();
|
||||
|
||||
if (!$node) {
|
||||
return $this->error('无待处理的审批节点', 40001);
|
||||
}
|
||||
|
||||
$node->update(['action' => 1, 'acted_at' => now()]);
|
||||
|
||||
$pendingNodes = $approval->nodes()->where('action', 0)->exists();
|
||||
if (!$pendingNodes) {
|
||||
$approval->update(['status' => 2]);
|
||||
} else {
|
||||
$approval->update([
|
||||
'status' => 1,
|
||||
'current_node' => $approval->current_node + 1,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->success($approval->load('nodes'));
|
||||
}
|
||||
|
||||
public function reject(Request $request, Approval $approval): JsonResponse
|
||||
{
|
||||
$node = $approval->nodes()
|
||||
->where('approver_id', auth()->id())
|
||||
->where('action', 0)
|
||||
->where('node_order', $approval->current_node)
|
||||
->first();
|
||||
|
||||
if (!$node) {
|
||||
return $this->error('无待处理的审批节点', 40001);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'comment' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$node->update([
|
||||
'action' => 2,
|
||||
'comment' => $validated['comment'] ?? null,
|
||||
'acted_at' => now(),
|
||||
]);
|
||||
$approval->update(['status' => 3]);
|
||||
|
||||
return $this->success($approval->load('nodes'));
|
||||
}
|
||||
|
||||
public function withdraw(Approval $approval): JsonResponse
|
||||
{
|
||||
if ($approval->status > 1) {
|
||||
return $this->error('该审批已完结,无法撤回', 40001);
|
||||
}
|
||||
$approval->update(['status' => 4]);
|
||||
|
||||
return $this->success($approval);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Office;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Office\ApprovalTemplate;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ApprovalTemplateController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = ApprovalTemplate::query();
|
||||
$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->orderBy('sort')->latest()->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:100',
|
||||
'type' => 'required|integer|in:1,2,3,4,5',
|
||||
'form_fields' => 'nullable|array',
|
||||
'flow_nodes' => 'nullable|array',
|
||||
'status' => 'sometimes|integer|in:0,1',
|
||||
'sort' => 'sometimes|integer',
|
||||
]);
|
||||
|
||||
return $this->success(ApprovalTemplate::create($validated));
|
||||
}
|
||||
|
||||
public function show(ApprovalTemplate $approvalTemplate): JsonResponse
|
||||
{
|
||||
return $this->success($approvalTemplate);
|
||||
}
|
||||
|
||||
public function update(Request $request, ApprovalTemplate $approvalTemplate): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'sometimes|string|max:100',
|
||||
'type' => 'sometimes|integer|in:1,2,3,4,5',
|
||||
'form_fields' => 'nullable|array',
|
||||
'flow_nodes' => 'nullable|array',
|
||||
'status' => 'sometimes|integer|in:0,1',
|
||||
'sort' => 'sometimes|integer',
|
||||
]);
|
||||
$approvalTemplate->update($validated);
|
||||
|
||||
return $this->success($approvalTemplate);
|
||||
}
|
||||
|
||||
public function destroy(ApprovalTemplate $approvalTemplate): JsonResponse
|
||||
{
|
||||
$approvalTemplate->delete();
|
||||
return $this->success(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Office;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Office\Handover;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HandoverController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Handover::query()->with(['handoverUser', 'receiverUser']);
|
||||
$query->when($request->shift_date, fn($q, $v) => $q->where('shift_date', $v));
|
||||
$query->when($request->shift_type, fn($q, $v) => $q->where('shift_type', $v));
|
||||
|
||||
return $this->paginate($query->latest('created_at')->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'shift_date' => 'required|date',
|
||||
'shift_type' => 'required|integer|in:1,2,3,4',
|
||||
'content' => 'required|string',
|
||||
'issues' => 'nullable|string',
|
||||
]);
|
||||
$validated['handover_user_id'] = auth()->id();
|
||||
|
||||
return $this->success(Handover::create($validated));
|
||||
}
|
||||
|
||||
public function show(Handover $handover): JsonResponse
|
||||
{
|
||||
return $this->success($handover->load(['handoverUser', 'receiverUser']));
|
||||
}
|
||||
|
||||
public function confirm(Handover $handover): JsonResponse
|
||||
{
|
||||
if ($handover->status === 1) {
|
||||
return $this->error('该交接班已确认', 40001);
|
||||
}
|
||||
$handover->update([
|
||||
'receiver_user_id' => auth()->id(),
|
||||
'status' => 1,
|
||||
'confirmed_at' => now(),
|
||||
]);
|
||||
|
||||
return $this->success($handover);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Office;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Office\Notification;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NotificationController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Notification::query()->where('user_id', auth()->id());
|
||||
$query->when($request->type, fn($q, $v) => $q->where('type', $v));
|
||||
$query->when($request->has('is_read'), fn($q) => $q->where('is_read', $request->boolean('is_read')));
|
||||
|
||||
return $this->paginate($query->latest('created_at')->paginate($request->input('per_page', 15)));
|
||||
}
|
||||
|
||||
public function markRead(Notification $notification): JsonResponse
|
||||
{
|
||||
if ($notification->user_id !== auth()->id()) {
|
||||
return $this->error('无权操作', 40003, 403);
|
||||
}
|
||||
$notification->update(['is_read' => true, 'read_at' => now()]);
|
||||
|
||||
return $this->success($notification);
|
||||
}
|
||||
|
||||
public function markAllRead(): JsonResponse
|
||||
{
|
||||
Notification::query()
|
||||
->where('user_id', auth()->id())
|
||||
->where('is_read', false)
|
||||
->update(['is_read' => true, 'read_at' => now()]);
|
||||
|
||||
return $this->success(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Report;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Crm\Contract;
|
||||
use App\Models\Crm\Customer;
|
||||
use App\Models\Crm\Lead;
|
||||
use App\Models\Finance\FinanceRecord;
|
||||
use App\Models\Room\Room;
|
||||
use App\Models\Room\RoomType;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
public function overview(Request $request): JsonResponse
|
||||
{
|
||||
$data = [
|
||||
'customer_count' => Customer::count(),
|
||||
'contract_amount' => Contract::where('status', 2)->sum('actual_amount'),
|
||||
'room_count' => Room::count(),
|
||||
'occupied_count' => Room::where('status', 3)->count(),
|
||||
'today_revenue' => FinanceRecord::where('type', 1)->where('audit_status', 1)
|
||||
->whereDate('record_date', today())->sum('amount'),
|
||||
'month_revenue' => FinanceRecord::where('type', 1)->where('audit_status', 1)
|
||||
->whereMonth('record_date', now()->month)
|
||||
->whereYear('record_date', now()->year)->sum('amount'),
|
||||
];
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
public function revenue(Request $request): JsonResponse
|
||||
{
|
||||
$year = $request->input('year', now()->year);
|
||||
$records = FinanceRecord::where('audit_status', 1)
|
||||
->whereYear('record_date', $year)
|
||||
->selectRaw('MONTH(record_date) as month, type, SUM(amount) as total')
|
||||
->groupBy('month', 'type')
|
||||
->get();
|
||||
return $this->success($records);
|
||||
}
|
||||
|
||||
public function occupancy(Request $request): JsonResponse
|
||||
{
|
||||
$data = RoomType::withCount([
|
||||
'rooms',
|
||||
'rooms as occupied_count' => fn($q) => $q->where('status', 3),
|
||||
])->get();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
public function crmConversion(Request $request): JsonResponse
|
||||
{
|
||||
$data = [
|
||||
'total_leads' => Lead::count(),
|
||||
'following' => Lead::where('status', 2)->count(),
|
||||
'contracted' => Lead::where('status', 5)->count(),
|
||||
'by_channel' => Lead::selectRaw('channel_id, COUNT(*) as total, SUM(CASE WHEN status=5 THEN 1 ELSE 0 END) as converted')
|
||||
->groupBy('channel_id')
|
||||
->with('channel:id,name')
|
||||
->get(),
|
||||
];
|
||||
return $this->success($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Report;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Report\ReportExport;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ReportExportController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = ReportExport::query()->with('operator');
|
||||
$query->when($request->type, fn($q, $v) => $q->where('type', $v));
|
||||
$query->when($request->filled('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
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'required|string|max:50',
|
||||
'params' => 'nullable|array',
|
||||
]);
|
||||
$validated['operator_id'] = auth()->id();
|
||||
$validated['status'] = 0;
|
||||
|
||||
$export = ReportExport::create($validated);
|
||||
return $this->success($export);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Kb;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class KbArticle extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'category_id', 'title', 'content', 'author_id',
|
||||
'status', 'visibility', 'cover_image', 'attachments',
|
||||
'views', 'likes', 'favorites', 'published_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'integer',
|
||||
'visibility' => 'integer',
|
||||
'views' => 'integer',
|
||||
'likes' => 'integer',
|
||||
'favorites' => 'integer',
|
||||
'attachments' => 'array',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(KbCategory::class, 'category_id');
|
||||
}
|
||||
|
||||
public function author(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'author_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Kb;
|
||||
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class KbCategory extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'parent_id', 'name', 'sort', 'status',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'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');
|
||||
}
|
||||
|
||||
public function articles(): HasMany
|
||||
{
|
||||
return $this->hasMany(KbArticle::class, 'category_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Office;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Announcement extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'title', 'content', 'type', 'is_top',
|
||||
'publish_status', 'published_at', 'author_id',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'integer',
|
||||
'publish_status' => 'integer',
|
||||
'is_top' => 'boolean',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function author(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'author_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Office;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Approval extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'template_id', 'title', 'applicant_id', 'form_data',
|
||||
'status', 'current_node', 'related_type', 'related_id',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'integer',
|
||||
'current_node' => 'integer',
|
||||
'form_data' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function template(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ApprovalTemplate::class, 'template_id');
|
||||
}
|
||||
|
||||
public function applicant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'applicant_id');
|
||||
}
|
||||
|
||||
public function nodes(): HasMany
|
||||
{
|
||||
return $this->hasMany(ApprovalNode::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Office;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ApprovalNode extends Model
|
||||
{
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'approval_id', 'node_order', 'approver_id', 'action', 'comment', 'acted_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'node_order' => 'integer',
|
||||
'action' => 'integer',
|
||||
'acted_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function approval(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Approval::class);
|
||||
}
|
||||
|
||||
public function approver(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'approver_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Office;
|
||||
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ApprovalTemplate extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'name', 'type', 'form_fields', 'flow_nodes', 'status', 'sort',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'integer',
|
||||
'status' => 'integer',
|
||||
'sort' => 'integer',
|
||||
'form_fields' => 'array',
|
||||
'flow_nodes' => 'array',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Office;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Handover extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'shift_date', 'shift_type', 'handover_user_id',
|
||||
'receiver_user_id', 'content', 'issues', 'status', 'confirmed_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'shift_type' => 'integer',
|
||||
'status' => 'integer',
|
||||
'shift_date' => 'date',
|
||||
'confirmed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function handoverUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'handover_user_id');
|
||||
}
|
||||
|
||||
public function receiverUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'receiver_user_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Office;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Notification extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $table = 'notifications';
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'user_id', 'title', 'content', 'type',
|
||||
'related_type', 'related_id', 'is_read', 'read_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'integer',
|
||||
'is_read' => 'boolean',
|
||||
'read_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Report;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\BelongsToStore;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ReportExport extends Model
|
||||
{
|
||||
use BelongsToStore;
|
||||
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'store_id', 'name', 'type', 'params', 'file_path', 'status', 'operator_id',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'params' => 'array',
|
||||
'status' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function operator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'operator_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?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('approval_templates', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->string('name', 100);
|
||||
$table->tinyInteger('type')->comment('1=请假 2=采购 3=合同 4=退款 5=换房');
|
||||
$table->json('form_fields')->nullable();
|
||||
$table->json('flow_nodes')->nullable();
|
||||
$table->tinyInteger('status')->default(1);
|
||||
$table->integer('sort')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 审批记录
|
||||
Schema::create('approvals', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->unsignedBigInteger('template_id')->nullable();
|
||||
$table->string('title');
|
||||
$table->unsignedBigInteger('applicant_id')->index();
|
||||
$table->json('form_data')->nullable();
|
||||
$table->tinyInteger('status')->default(0)->comment('0=待审批 1=审批中 2=已通过 3=已驳回 4=已撤回');
|
||||
$table->integer('current_node')->default(0);
|
||||
$table->string('related_type', 50)->nullable();
|
||||
$table->unsignedBigInteger('related_id')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 审批节点
|
||||
Schema::create('approval_nodes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('approval_id')->index();
|
||||
$table->integer('node_order')->default(0);
|
||||
$table->unsignedBigInteger('approver_id');
|
||||
$table->tinyInteger('action')->default(0)->comment('0=待处理 1=通过 2=驳回');
|
||||
$table->string('comment')->nullable();
|
||||
$table->timestamp('acted_at')->nullable();
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
// 公告
|
||||
Schema::create('announcements', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->string('title');
|
||||
$table->text('content');
|
||||
$table->tinyInteger('type')->default(1)->comment('1=通知 2=公告 3=制度');
|
||||
$table->boolean('is_top')->default(false);
|
||||
$table->tinyInteger('publish_status')->default(0)->comment('0=草稿 1=已发布');
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->unsignedBigInteger('author_id')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 交接班
|
||||
Schema::create('handovers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->date('shift_date');
|
||||
$table->tinyInteger('shift_type')->comment('1=早班 2=中班 3=晚班 4=夜班');
|
||||
$table->unsignedBigInteger('handover_user_id');
|
||||
$table->unsignedBigInteger('receiver_user_id')->nullable();
|
||||
$table->text('content');
|
||||
$table->text('issues')->nullable();
|
||||
$table->tinyInteger('status')->default(0)->comment('0=待确认 1=已确认');
|
||||
$table->timestamp('confirmed_at')->nullable();
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
// 消息通知
|
||||
Schema::create('notifications', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->unsignedBigInteger('user_id')->index();
|
||||
$table->string('title');
|
||||
$table->string('content')->nullable();
|
||||
$table->tinyInteger('type')->default(1)->comment('1=系统 2=审批 3=待办 4=提醒');
|
||||
$table->string('related_type', 50)->nullable();
|
||||
$table->unsignedBigInteger('related_id')->nullable();
|
||||
$table->boolean('is_read')->default(false);
|
||||
$table->timestamp('read_at')->nullable();
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('notifications');
|
||||
Schema::dropIfExists('handovers');
|
||||
Schema::dropIfExists('announcements');
|
||||
Schema::dropIfExists('approval_nodes');
|
||||
Schema::dropIfExists('approvals');
|
||||
Schema::dropIfExists('approval_templates');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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('report_exports', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->string('name');
|
||||
$table->string('type', 50)->comment('report type key');
|
||||
$table->json('params')->nullable();
|
||||
$table->string('file_path')->nullable();
|
||||
$table->tinyInteger('status')->default(0)->comment('0=生成中 1=已完成 2=失败');
|
||||
$table->unsignedBigInteger('operator_id')->nullable();
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('report_exports');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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('kb_categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->unsignedBigInteger('parent_id')->default(0)->index();
|
||||
$table->string('name', 100);
|
||||
$table->integer('sort')->default(0);
|
||||
$table->tinyInteger('status')->default(1);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('kb_articles', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('store_id')->index();
|
||||
$table->unsignedBigInteger('category_id')->nullable()->index();
|
||||
$table->string('title');
|
||||
$table->longText('content')->nullable();
|
||||
$table->unsignedBigInteger('author_id')->nullable();
|
||||
$table->tinyInteger('status')->default(0)->comment('0=草稿 1=待审核 2=已发布 3=已下架');
|
||||
$table->tinyInteger('visibility')->default(1)->comment('1=内部 2=公开');
|
||||
$table->string('cover_image')->nullable();
|
||||
$table->json('attachments')->nullable();
|
||||
$table->unsignedInteger('views')->default(0);
|
||||
$table->unsignedInteger('likes')->default(0);
|
||||
$table->unsignedInteger('favorites')->default(0);
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->fullText(['title', 'content']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('kb_articles');
|
||||
Schema::dropIfExists('kb_categories');
|
||||
}
|
||||
};
|
||||
@@ -55,6 +55,15 @@ 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;
|
||||
use App\Http\Controllers\Admin\Office\ApprovalTemplateController;
|
||||
use App\Http\Controllers\Admin\Office\ApprovalController;
|
||||
use App\Http\Controllers\Admin\Office\AnnouncementController;
|
||||
use App\Http\Controllers\Admin\Office\HandoverController;
|
||||
use App\Http\Controllers\Admin\Office\NotificationController;
|
||||
use App\Http\Controllers\Admin\Report\ReportController;
|
||||
use App\Http\Controllers\Admin\Report\ReportExportController;
|
||||
use App\Http\Controllers\Admin\Kb\KbCategoryController;
|
||||
use App\Http\Controllers\Admin\Kb\KbArticleController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -308,4 +317,55 @@ Route::middleware(['auth:sanctum', 'store', 'oplog'])->group(function () {
|
||||
Route::put('salaries/{salaryRecord}/confirm', [SalaryRecordController::class, 'confirm']);
|
||||
Route::put('salaries/{salaryRecord}/pay', [SalaryRecordController::class, 'pay']);
|
||||
});
|
||||
|
||||
// --- 办公协同模块 ---
|
||||
Route::prefix('office')->group(function () {
|
||||
// 审批模板
|
||||
Route::apiResource('approval-templates', ApprovalTemplateController::class);
|
||||
|
||||
// 审批管理
|
||||
Route::apiResource('approvals', ApprovalController::class)->except(['update']);
|
||||
Route::put('approvals/{approval}/approve', [ApprovalController::class, 'approve']);
|
||||
Route::put('approvals/{approval}/reject', [ApprovalController::class, 'reject']);
|
||||
Route::put('approvals/{approval}/withdraw', [ApprovalController::class, 'withdraw']);
|
||||
|
||||
// 公告管理
|
||||
Route::apiResource('announcements', AnnouncementController::class);
|
||||
Route::put('announcements/{announcement}/publish', [AnnouncementController::class, 'publish']);
|
||||
|
||||
// 交接班
|
||||
Route::get('handovers', [HandoverController::class, 'index']);
|
||||
Route::post('handovers', [HandoverController::class, 'store']);
|
||||
Route::get('handovers/{handover}', [HandoverController::class, 'show']);
|
||||
Route::put('handovers/{handover}/confirm', [HandoverController::class, 'confirm']);
|
||||
|
||||
// 消息通知
|
||||
Route::get('notifications', [NotificationController::class, 'index']);
|
||||
Route::put('notifications/{notification}/read', [NotificationController::class, 'markRead']);
|
||||
Route::put('notifications/read-all', [NotificationController::class, 'markAllRead']);
|
||||
});
|
||||
|
||||
// --- 统计报表模块 ---
|
||||
Route::prefix('report')->group(function () {
|
||||
// 聚合报表
|
||||
Route::get('overview', [ReportController::class, 'overview']);
|
||||
Route::get('revenue', [ReportController::class, 'revenue']);
|
||||
Route::get('occupancy', [ReportController::class, 'occupancy']);
|
||||
Route::get('crm-conversion', [ReportController::class, 'crmConversion']);
|
||||
|
||||
// 导出记录
|
||||
Route::get('exports', [ReportExportController::class, 'index']);
|
||||
Route::post('exports', [ReportExportController::class, 'store']);
|
||||
});
|
||||
|
||||
// --- 知识库模块 ---
|
||||
Route::prefix('kb')->group(function () {
|
||||
// 知识分类
|
||||
Route::apiResource('categories', KbCategoryController::class);
|
||||
|
||||
// 知识文章
|
||||
Route::apiResource('articles', KbArticleController::class);
|
||||
Route::put('articles/{kbArticle}/publish', [KbArticleController::class, 'publish']);
|
||||
Route::put('articles/{kbArticle}/offline', [KbArticleController::class, 'offline']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user