diff --git a/backend/app/Http/Controllers/Admin/Care/CareExceptionController.php b/backend/app/Http/Controllers/Admin/Care/CareExceptionController.php new file mode 100644 index 0000000..ebad6e9 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Care/CareExceptionController.php @@ -0,0 +1,68 @@ +with(['profile.customer', 'reporter', 'handler']); + $query->when($request->care_profile_id, fn($q, $v) => $q->where('care_profile_id', $v)); + $query->when($request->level, fn($q, $v) => $q->where('level', $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 + { + $validated = $request->validate([ + 'care_profile_id' => 'required|exists:care_profiles,id', + 'level' => 'required|integer|in:1,2,3', + 'description' => 'required|string', + 'images' => 'nullable|array', + ]); + $validated['reporter_id'] = auth()->id(); + $validated['status'] = 0; + + return $this->success(CareException::create($validated)); + } + + public function show(CareException $careException): JsonResponse + { + return $this->success($careException->load(['profile.customer', 'reporter', 'handler'])); + } + + public function update(Request $request, CareException $careException): JsonResponse + { + $validated = $request->validate([ + 'level' => 'sometimes|integer|in:1,2,3', + 'description' => 'sometimes|string', + 'images' => 'nullable|array', + ]); + $careException->update($validated); + return $this->success($careException); + } + + public function handle(Request $request, CareException $careException): JsonResponse + { + if ($careException->status === 2) { + return $this->error('该异常已处理', 40001); + } + $validated = $request->validate([ + 'handle_result' => 'required|string', + ]); + $careException->update([ + 'handle_result' => $validated['handle_result'], + 'handler_id' => auth()->id(), + 'handle_at' => now(), + 'status' => 2, + ]); + return $this->success($careException); + } +} diff --git a/backend/app/Http/Controllers/Admin/Care/CarePlanController.php b/backend/app/Http/Controllers/Admin/Care/CarePlanController.php new file mode 100644 index 0000000..a56ef6a --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Care/CarePlanController.php @@ -0,0 +1,59 @@ +with(['profile.customer', 'nurse']); + $query->when($request->care_profile_id, fn($q, $v) => $q->where('care_profile_id', $v)); + $query->when($request->plan_date, fn($q, $v) => $q->whereDate('plan_date', $v)); + $query->when($request->nurse_id, fn($q, $v) => $q->where('nurse_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 + { + $validated = $request->validate([ + 'care_profile_id' => 'required|exists:care_profiles,id', + 'plan_date' => 'required|date', + 'stage' => 'nullable|string|max:50', + 'items' => 'nullable|array', + 'nurse_id' => 'nullable|exists:users,id', + ]); + $validated['status'] = 0; + return $this->success(CarePlan::create($validated)); + } + + public function show(CarePlan $carePlan): JsonResponse + { + return $this->success($carePlan->load(['profile.customer', 'nurse', 'records'])); + } + + public function update(Request $request, CarePlan $carePlan): JsonResponse + { + $validated = $request->validate([ + 'plan_date' => 'sometimes|date', + 'stage' => 'nullable|string|max:50', + 'items' => 'nullable|array', + 'nurse_id' => 'nullable|exists:users,id', + 'status' => 'sometimes|integer|in:0,1,2', + ]); + $carePlan->update($validated); + return $this->success($carePlan); + } + + public function destroy(CarePlan $carePlan): JsonResponse + { + $carePlan->delete(); + return $this->success(null); + } +} diff --git a/backend/app/Http/Controllers/Admin/Care/CareProfileController.php b/backend/app/Http/Controllers/Admin/Care/CareProfileController.php new file mode 100644 index 0000000..e2042ca --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Care/CareProfileController.php @@ -0,0 +1,68 @@ +with('customer'); + $query->when($request->customer_id, fn($q, $v) => $q->where('customer_id', $v)); + $query->when($request->type, fn($q, $v) => $q->where('type', $v)); + $query->when($request->risk_level, fn($q, $v) => $q->where('risk_level', $v)); + + return $this->paginate($query->latest()->paginate($request->input('per_page', 15))); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'customer_id' => 'required|exists:customers,id', + 'reservation_id' => 'nullable|exists:reservations,id', + 'type' => 'required|integer|in:1,2', + 'baby_name' => 'nullable|string|max:50', + 'baby_gender' => 'nullable|integer|in:1,2', + 'baby_birthday' => 'nullable|date', + 'birth_weight' => 'nullable|numeric|min:0', + 'birth_method' => 'nullable|integer|in:1,2', + 'allergies' => 'nullable|string', + 'medical_history' => 'nullable|string', + 'risk_level' => 'sometimes|integer|in:0,1,2', + 'assessment' => 'nullable|array', + ]); + return $this->success(CareProfile::create($validated)); + } + + public function show(CareProfile $careProfile): JsonResponse + { + return $this->success($careProfile->load(['customer', 'plans.nurse', 'records', 'exceptions', 'healthMetrics'])); + } + + public function update(Request $request, CareProfile $careProfile): JsonResponse + { + $validated = $request->validate([ + 'baby_name' => 'nullable|string|max:50', + 'baby_gender' => 'nullable|integer|in:1,2', + 'baby_birthday' => 'nullable|date', + 'birth_weight' => 'nullable|numeric|min:0', + 'birth_method' => 'nullable|integer|in:1,2', + 'allergies' => 'nullable|string', + 'medical_history' => 'nullable|string', + 'risk_level' => 'sometimes|integer|in:0,1,2', + 'assessment' => 'nullable|array', + ]); + $careProfile->update($validated); + return $this->success($careProfile); + } + + public function destroy(CareProfile $careProfile): JsonResponse + { + $careProfile->delete(); + return $this->success(null); + } +} diff --git a/backend/app/Http/Controllers/Admin/Care/CareRecordController.php b/backend/app/Http/Controllers/Admin/Care/CareRecordController.php new file mode 100644 index 0000000..ae60c3a --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Care/CareRecordController.php @@ -0,0 +1,62 @@ +with(['profile.customer', 'nurse']); + $query->when($request->care_profile_id, fn($q, $v) => $q->where('care_profile_id', $v)); + $query->when($request->care_plan_id, fn($q, $v) => $q->where('care_plan_id', $v)); + $query->when($request->type, fn($q, $v) => $q->where('type', $v)); + $query->when($request->nurse_id, fn($q, $v) => $q->where('nurse_id', $v)); + + return $this->paginate($query->latest()->paginate($request->input('per_page', 15))); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'care_profile_id' => 'required|exists:care_profiles,id', + 'care_plan_id' => 'nullable|exists:care_plans,id', + 'type' => 'nullable|integer|in:1,2', + 'items' => 'nullable|array', + 'remark' => 'nullable|string', + 'images' => 'nullable|array', + 'recorded_at' => 'sometimes|date', + ]); + $validated['nurse_id'] = auth()->id(); + $validated['recorded_at'] = $validated['recorded_at'] ?? now(); + + return $this->success(CareRecord::create($validated)); + } + + public function show(CareRecord $careRecord): JsonResponse + { + return $this->success($careRecord->load(['profile.customer', 'plan', 'nurse'])); + } + + public function update(Request $request, CareRecord $careRecord): JsonResponse + { + $validated = $request->validate([ + 'type' => 'nullable|integer|in:1,2', + 'items' => 'nullable|array', + 'remark' => 'nullable|string', + 'images' => 'nullable|array', + ]); + $careRecord->update($validated); + return $this->success($careRecord); + } + + public function destroy(CareRecord $careRecord): JsonResponse + { + $careRecord->delete(); + return $this->success(null); + } +} diff --git a/backend/app/Http/Controllers/Admin/Care/HealthMetricController.php b/backend/app/Http/Controllers/Admin/Care/HealthMetricController.php new file mode 100644 index 0000000..14c8ab9 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Care/HealthMetricController.php @@ -0,0 +1,38 @@ +with('recorder'); + $query->when($request->care_profile_id, fn($q, $v) => $q->where('care_profile_id', $v)); + $query->when($request->metric_type, fn($q, $v) => $q->where('metric_type', $v)); + $query->when($request->start_date, fn($q, $v) => $q->where('recorded_at', '>=', $v)); + $query->when($request->end_date, fn($q, $v) => $q->where('recorded_at', '<=', $v)); + + return $this->paginate($query->latest('recorded_at')->paginate($request->input('per_page', 50))); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'care_profile_id' => 'required|exists:care_profiles,id', + 'metric_type' => 'required|string|max:30', + 'value' => 'required|numeric', + 'unit' => 'nullable|string|max:10', + 'recorded_at' => 'sometimes|date', + 'remark' => 'nullable|string|max:255', + ]); + $validated['recorder_id'] = auth()->id(); + $validated['recorded_at'] = $validated['recorded_at'] ?? now(); + + return $this->success(HealthMetric::create($validated)); + } +} diff --git a/backend/app/Http/Controllers/Admin/Crm/ChannelController.php b/backend/app/Http/Controllers/Admin/Crm/ChannelController.php new file mode 100644 index 0000000..d8422d4 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Crm/ChannelController.php @@ -0,0 +1,59 @@ +when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$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 + { + $validated = $request->validate([ + 'name' => 'required|string|max:50', + 'type' => 'required|integer|in:1,2,3', + 'status' => 'sometimes|integer|in:0,1', + ]); + + $channel = Channel::create($validated); + + return $this->success($channel); + } + + public function show(Channel $channel): JsonResponse + { + return $this->success($channel->load('leads')); + } + + public function update(Request $request, Channel $channel): JsonResponse + { + $validated = $request->validate([ + 'name' => 'sometimes|string|max:50', + 'type' => 'sometimes|integer|in:1,2,3', + 'status' => 'sometimes|integer|in:0,1', + ]); + + $channel->update($validated); + + return $this->success($channel); + } + + public function destroy(Channel $channel): JsonResponse + { + $channel->delete(); + + return $this->success(null); + } +} diff --git a/backend/app/Http/Controllers/Admin/Crm/ComplaintController.php b/backend/app/Http/Controllers/Admin/Crm/ComplaintController.php new file mode 100644 index 0000000..293c0a1 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Crm/ComplaintController.php @@ -0,0 +1,74 @@ +with(['customer', 'handler']); + $query->when($request->customer_id, fn($q, $v) => $q->where('customer_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 + { + $validated = $request->validate([ + 'customer_id' => 'required|exists:customers,id', + 'type' => 'nullable|integer|in:1,2,3,4', + 'content' => 'required|string', + 'images' => 'nullable|array', + ]); + $validated['status'] = 0; + + return $this->success(Complaint::create($validated)); + } + + public function show(Complaint $complaint): JsonResponse + { + return $this->success($complaint->load(['customer', 'handler'])); + } + + public function update(Request $request, Complaint $complaint): JsonResponse + { + $validated = $request->validate([ + 'type' => 'nullable|integer|in:1,2,3,4', + 'content' => 'sometimes|string', + 'images' => 'nullable|array', + ]); + $complaint->update($validated); + return $this->success($complaint); + } + + public function destroy(Complaint $complaint): JsonResponse + { + $complaint->delete(); + return $this->success(null); + } + + public function handle(Request $request, Complaint $complaint): JsonResponse + { + if ($complaint->status >= 2) { + return $this->error('该投诉已处理', 40001); + } + $validated = $request->validate([ + 'handle_result' => 'required|string', + 'status' => 'required|integer|in:2,3', + ]); + $complaint->update([ + 'handle_result' => $validated['handle_result'], + 'status' => $validated['status'], + 'handler_id' => auth()->id(), + 'handle_at' => now(), + ]); + return $this->success($complaint); + } +} diff --git a/backend/app/Http/Controllers/Admin/Crm/ContractController.php b/backend/app/Http/Controllers/Admin/Crm/ContractController.php new file mode 100644 index 0000000..d05218f --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Crm/ContractController.php @@ -0,0 +1,97 @@ +with(['customer', 'auditor']); + $query->when($request->contract_no, fn($q, $v) => $q->where('contract_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)); + + return $this->paginate($query->latest()->paginate($request->input('per_page', 15))); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'customer_id' => 'required|exists:customers,id', + 'contract_no' => 'required|string|max:50|unique:contracts', + 'package_name' => 'nullable|string|max:100', + 'total_amount' => 'required|numeric|min:0', + 'discount_amount' => 'sometimes|numeric|min:0', + 'actual_amount' => 'required|numeric|min:0', + 'days' => 'nullable|integer|min:1', + 'check_in_date' => 'nullable|date', + 'check_out_date' => 'nullable|date|after_or_equal:check_in_date', + 'remark' => 'nullable|string', + ]); + $validated['created_by'] = auth()->id(); + $validated['status'] = 0; + + return $this->success(Contract::create($validated)); + } + + public function show(Contract $contract): JsonResponse + { + return $this->success($contract->load(['customer', 'auditor', 'creator'])); + } + + public function update(Request $request, Contract $contract): JsonResponse + { + if ($contract->status !== 0) { + return $this->error('只能修改待审核的合同', 40001); + } + $validated = $request->validate([ + 'package_name' => 'nullable|string|max:100', + 'total_amount' => 'sometimes|numeric|min:0', + 'discount_amount' => 'sometimes|numeric|min:0', + 'actual_amount' => 'sometimes|numeric|min:0', + 'days' => 'nullable|integer|min:1', + 'check_in_date' => 'nullable|date', + 'check_out_date' => 'nullable|date|after_or_equal:check_in_date', + 'remark' => 'nullable|string', + ]); + $contract->update($validated); + return $this->success($contract); + } + + public function destroy(Contract $contract): JsonResponse + { + if ($contract->status !== 0) { + return $this->error('只能删除待审核的合同', 40001); + } + $contract->delete(); + return $this->success(null); + } + + public function audit(Request $request, Contract $contract): JsonResponse + { + if ($contract->status !== 0) { + return $this->error('该合同已审核', 40001); + } + $validated = $request->validate([ + 'status' => 'required|integer|in:1,2', + 'audit_remark' => 'nullable|string|max:255', + ]); + $contract->update([ + 'status' => $validated['status'], + 'audit_user_id' => auth()->id(), + 'audit_at' => now(), + 'audit_remark' => $validated['audit_remark'] ?? null, + ]); + + if ($validated['status'] === 1) { + $contract->customer->update(['status' => 2]); + } + + return $this->success($contract); + } +} diff --git a/backend/app/Http/Controllers/Admin/Crm/CustomerController.php b/backend/app/Http/Controllers/Admin/Crm/CustomerController.php new file mode 100644 index 0000000..fa52f36 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Crm/CustomerController.php @@ -0,0 +1,86 @@ +with(['owner']); + $query->when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%")); + $query->when($request->phone, fn($q, $v) => $q->where('phone', 'like', "%{$v}%")); + $query->when($request->status, fn($q, $v) => $q->where('status', $v)); + $query->when($request->owner_id, fn($q, $v) => $q->where('owner_id', $v)); + + return $this->paginate($query->latest()->paginate($request->input('per_page', 15))); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'lead_id' => 'nullable|exists:leads,id', + 'name' => 'required|string|max:50', + 'phone' => 'required|string|max:20', + 'id_card' => 'nullable|string|max:20', + 'wechat' => 'nullable|string|max:50', + 'birthday' => 'nullable|date', + 'expected_date' => 'nullable|date', + 'actual_date' => 'nullable|date', + 'baby_count' => 'sometimes|integer|min:1', + 'tags' => 'nullable|array', + 'remark' => 'nullable|string', + 'families' => 'nullable|array', + 'families.*.name' => 'nullable|string|max:50', + 'families.*.phone' => 'nullable|string|max:20', + 'families.*.relation' => 'nullable|string|max:20', + 'families.*.is_emergency' => 'nullable|boolean', + ]); + + $families = $validated['families'] ?? []; + unset($validated['families']); + $validated['status'] = 1; + + $customer = Customer::create($validated); + if ($families) { + $customer->families()->createMany($families); + } + + return $this->success($customer->load('families')); + } + + public function show(Customer $customer): JsonResponse + { + return $this->success($customer->load(['owner', 'families', 'contracts', 'complaints'])); + } + + public function update(Request $request, Customer $customer): JsonResponse + { + $validated = $request->validate([ + 'name' => 'sometimes|string|max:50', + 'phone' => 'sometimes|string|max:20', + 'id_card' => 'nullable|string|max:20', + 'wechat' => 'nullable|string|max:50', + 'birthday' => 'nullable|date', + 'expected_date' => 'nullable|date', + 'actual_date' => 'nullable|date', + 'baby_count' => 'sometimes|integer|min:1', + 'tags' => 'nullable|array', + 'status' => 'sometimes|integer|in:1,2,3,4,5', + 'owner_id' => 'nullable|exists:users,id', + 'remark' => 'nullable|string', + ]); + $customer->update($validated); + return $this->success($customer); + } + + public function destroy(Customer $customer): JsonResponse + { + $customer->delete(); + return $this->success(null); + } +} diff --git a/backend/app/Http/Controllers/Admin/Crm/LeadController.php b/backend/app/Http/Controllers/Admin/Crm/LeadController.php new file mode 100644 index 0000000..13eb84d --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Crm/LeadController.php @@ -0,0 +1,93 @@ +with(['channel', 'owner']); + $query->when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%")); + $query->when($request->phone, fn($q, $v) => $q->where('phone', 'like', "%{$v}%")); + $query->when($request->status, fn($q, $v) => $q->where('status', $v)); + $query->when($request->channel_id, fn($q, $v) => $q->where('channel_id', $v)); + $query->when($request->owner_id, fn($q, $v) => $q->where('owner_id', $v)); + + return $this->paginate($query->latest()->paginate($request->input('per_page', 15))); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'channel_id' => 'nullable|exists:channels,id', + 'name' => 'required|string|max:50', + 'phone' => 'required|string|max:20', + 'wechat' => 'nullable|string|max:50', + 'expected_date' => 'nullable|date', + 'source' => 'nullable|string|max:50', + 'remark' => 'nullable|string', + ]); + $validated['created_by'] = auth()->id(); + $validated['status'] = 1; + + return $this->success(Lead::create($validated)); + } + + public function show(Lead $lead): JsonResponse + { + return $this->success($lead->load(['channel', 'owner', 'follows.user'])); + } + + public function update(Request $request, Lead $lead): JsonResponse + { + $validated = $request->validate([ + 'channel_id' => 'nullable|exists:channels,id', + 'name' => 'sometimes|string|max:50', + 'phone' => 'sometimes|string|max:20', + 'wechat' => 'nullable|string|max:50', + 'expected_date' => 'nullable|date', + 'source' => 'nullable|string|max:50', + 'status' => 'sometimes|integer|in:1,2,3,4', + 'remark' => 'nullable|string', + ]); + $lead->update($validated); + return $this->success($lead); + } + + public function destroy(Lead $lead): JsonResponse + { + $lead->delete(); + return $this->success(null); + } + + public function follow(Request $request, Lead $lead): JsonResponse + { + $validated = $request->validate([ + 'type' => 'nullable|integer|in:1,2,3,4', + 'content' => 'nullable|string', + 'next_follow_at' => 'nullable|date', + ]); + $validated['lead_id'] = $lead->id; + $validated['user_id'] = auth()->id(); + + $follow = LeadFollow::create($validated); + $lead->update(['status' => 2]); + + return $this->success($follow); + } + + public function assign(Request $request, Lead $lead): JsonResponse + { + $validated = $request->validate([ + 'owner_id' => 'required|exists:users,id', + ]); + $lead->update($validated); + return $this->success($lead); + } +} diff --git a/backend/app/Http/Controllers/Admin/Crm/QuestionnaireController.php b/backend/app/Http/Controllers/Admin/Crm/QuestionnaireController.php new file mode 100644 index 0000000..b79f1a8 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Crm/QuestionnaireController.php @@ -0,0 +1,80 @@ +when($request->title, fn($q, $v) => $q->where('title', 'like', "%{$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 + { + $validated = $request->validate([ + 'title' => 'required|string|max:100', + 'description' => 'nullable|string', + 'questions' => 'nullable|array', + 'type' => 'nullable|integer|in:1,2,3,4', + 'status' => 'sometimes|integer|in:0,1', + ]); + return $this->success(QuestionnaireTemplate::create($validated)); + } + + public function show(QuestionnaireTemplate $questionnaire): JsonResponse + { + return $this->success($questionnaire->load('answers.customer')); + } + + public function update(Request $request, QuestionnaireTemplate $questionnaire): JsonResponse + { + $validated = $request->validate([ + 'title' => 'sometimes|string|max:100', + 'description' => 'nullable|string', + 'questions' => 'nullable|array', + 'type' => 'nullable|integer|in:1,2,3,4', + 'status' => 'sometimes|integer|in:0,1', + ]); + $questionnaire->update($validated); + return $this->success($questionnaire); + } + + public function destroy(QuestionnaireTemplate $questionnaire): JsonResponse + { + $questionnaire->delete(); + return $this->success(null); + } + + public function submitAnswer(Request $request): JsonResponse + { + $validated = $request->validate([ + 'questionnaire_template_id' => 'required|exists:questionnaire_templates,id', + 'customer_id' => 'required|exists:customers,id', + 'answers' => 'nullable|array', + 'score' => 'nullable|numeric|min:0|max:100', + ]); + $validated['status'] = 1; + + return $this->success(QuestionnaireAnswer::create($validated)); + } + + public function answers(Request $request): JsonResponse + { + $query = QuestionnaireAnswer::query()->with(['template', 'customer']); + $query->when($request->questionnaire_template_id, fn($q, $v) => $q->where('questionnaire_template_id', $v)); + $query->when($request->customer_id, fn($q, $v) => $q->where('customer_id', $v)); + + return $this->paginate($query->latest()->paginate($request->input('per_page', 15))); + } +} diff --git a/backend/app/Http/Controllers/Admin/Meal/DailyMealPlanController.php b/backend/app/Http/Controllers/Admin/Meal/DailyMealPlanController.php new file mode 100644 index 0000000..a489349 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Meal/DailyMealPlanController.php @@ -0,0 +1,85 @@ +with(['customer', 'review']); + $query->when($request->customer_id, fn($q, $v) => $q->where('customer_id', $v)); + $query->when($request->plan_date, fn($q, $v) => $q->whereDate('plan_date', $v)); + $query->when($request->meal_type, fn($q, $v) => $q->where('meal_type', $v)); + $query->when($request->has('status'), fn($q) => $q->where('status', $request->status)); + + return $this->paginate($query->latest()->paginate($request->input('per_page', 30))); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'customer_id' => 'required|exists:customers,id', + 'plan_date' => 'required|date', + 'meal_type' => 'required|integer|in:1,2,3,4,5', + 'dishes' => 'nullable|array', + 'special_note' => 'nullable|string', + ]); + $validated['status'] = 0; + + return $this->success(DailyMealPlan::create($validated)); + } + + public function show(DailyMealPlan $dailyMealPlan): JsonResponse + { + return $this->success($dailyMealPlan->load(['customer', 'review'])); + } + + public function update(Request $request, DailyMealPlan $dailyMealPlan): JsonResponse + { + if ($dailyMealPlan->status >= 2) { + return $this->error('已送达的排餐不可修改', 40001); + } + $validated = $request->validate([ + 'meal_type' => 'sometimes|integer|in:1,2,3,4,5', + 'dishes' => 'nullable|array', + 'special_note' => 'nullable|string', + ]); + $dailyMealPlan->update($validated); + return $this->success($dailyMealPlan); + } + + public function destroy(DailyMealPlan $dailyMealPlan): JsonResponse + { + if ($dailyMealPlan->status >= 1) { + return $this->error('已备餐的排餐不可删除', 40001); + } + $dailyMealPlan->delete(); + return $this->success(null); + } + + public function deliver(DailyMealPlan $dailyMealPlan): JsonResponse + { + if ($dailyMealPlan->status >= 2) { + return $this->error('已送达', 40001); + } + $dailyMealPlan->update([ + 'status' => 2, + 'deliver_at' => now(), + ]); + return $this->success($dailyMealPlan); + } + + public function updateStatus(Request $request, DailyMealPlan $dailyMealPlan): JsonResponse + { + $validated = $request->validate([ + 'status' => 'required|integer|in:0,1,2,3', + ]); + $dailyMealPlan->update($validated); + return $this->success($dailyMealPlan); + } +} diff --git a/backend/app/Http/Controllers/Admin/Meal/DishController.php b/backend/app/Http/Controllers/Admin/Meal/DishController.php new file mode 100644 index 0000000..7ca054f --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Meal/DishController.php @@ -0,0 +1,63 @@ +when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%")); + $query->when($request->category, fn($q, $v) => $q->where('category', $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 + { + $validated = $request->validate([ + 'name' => 'required|string|max:50', + 'category' => 'nullable|string|max:30', + 'description' => 'nullable|string', + 'image' => 'nullable|string|max:255', + 'ingredients' => 'nullable|array', + 'contraindications' => 'nullable|array', + 'price' => 'sometimes|numeric|min:0', + 'status' => 'sometimes|integer|in:0,1', + ]); + return $this->success(Dish::create($validated)); + } + + public function show(Dish $dish): JsonResponse + { + return $this->success($dish); + } + + public function update(Request $request, Dish $dish): JsonResponse + { + $validated = $request->validate([ + 'name' => 'sometimes|string|max:50', + 'category' => 'nullable|string|max:30', + 'description' => 'nullable|string', + 'image' => 'nullable|string|max:255', + 'ingredients' => 'nullable|array', + 'contraindications' => 'nullable|array', + 'price' => 'sometimes|numeric|min:0', + 'status' => 'sometimes|integer|in:0,1', + ]); + $dish->update($validated); + return $this->success($dish); + } + + public function destroy(Dish $dish): JsonResponse + { + $dish->delete(); + return $this->success(null); + } +} diff --git a/backend/app/Http/Controllers/Admin/Meal/MealPlanTemplateController.php b/backend/app/Http/Controllers/Admin/Meal/MealPlanTemplateController.php new file mode 100644 index 0000000..17a878d --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Meal/MealPlanTemplateController.php @@ -0,0 +1,55 @@ +when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%")); + $query->when($request->stage, fn($q, $v) => $q->where('stage', $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 + { + $validated = $request->validate([ + 'name' => 'required|string|max:50', + 'stage' => 'nullable|string|max:30', + 'meals' => 'nullable|array', + 'status' => 'sometimes|integer|in:0,1', + ]); + return $this->success(MealPlanTemplate::create($validated)); + } + + public function show(MealPlanTemplate $mealPlanTemplate): JsonResponse + { + return $this->success($mealPlanTemplate); + } + + public function update(Request $request, MealPlanTemplate $mealPlanTemplate): JsonResponse + { + $validated = $request->validate([ + 'name' => 'sometimes|string|max:50', + 'stage' => 'nullable|string|max:30', + 'meals' => 'nullable|array', + 'status' => 'sometimes|integer|in:0,1', + ]); + $mealPlanTemplate->update($validated); + return $this->success($mealPlanTemplate); + } + + public function destroy(MealPlanTemplate $mealPlanTemplate): JsonResponse + { + $mealPlanTemplate->delete(); + return $this->success(null); + } +} diff --git a/backend/app/Http/Controllers/Admin/Meal/MealReviewController.php b/backend/app/Http/Controllers/Admin/Meal/MealReviewController.php new file mode 100644 index 0000000..1cb6272 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Meal/MealReviewController.php @@ -0,0 +1,32 @@ +with(['mealPlan', 'customer']); + $query->when($request->customer_id, fn($q, $v) => $q->where('customer_id', $v)); + $query->when($request->daily_meal_plan_id, fn($q, $v) => $q->where('daily_meal_plan_id', $v)); + $query->when($request->score, fn($q, $v) => $q->where('score', $v)); + + return $this->paginate($query->latest('created_at')->paginate($request->input('per_page', 15))); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'daily_meal_plan_id' => 'required|exists:daily_meal_plans,id', + 'customer_id' => 'required|exists:customers,id', + 'score' => 'nullable|integer|min:1|max:5', + 'content' => 'nullable|string', + ]); + return $this->success(MealReview::create($validated)); + } +} diff --git a/backend/app/Http/Controllers/Admin/Room/CallRecordController.php b/backend/app/Http/Controllers/Admin/Room/CallRecordController.php new file mode 100644 index 0000000..87a5209 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Room/CallRecordController.php @@ -0,0 +1,48 @@ +with(['room', 'customer', 'handler']); + $query->when($request->room_id, fn($q, $v) => $q->where('room_id', $v)); + $query->when($request->type, fn($q, $v) => $q->where('type', $v)); + $query->when($request->status, fn($q, $v) => $q->where('status', $v)); + + return $this->paginate($query->latest()->paginate($request->input('per_page', 15))); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'room_id' => 'required|exists:rooms,id', + 'customer_id' => 'nullable|exists:customers,id', + 'type' => 'nullable|integer|in:1,2,3,4', + 'remark' => 'nullable|string|max:255', + ]); + $validated['status'] = 0; + + return $this->success(CallRecord::create($validated)); + } + + public function handle(Request $request, CallRecord $callRecord): JsonResponse + { + if ($callRecord->status === 2) { + return $this->error('该呼叫已完成', 40001); + } + $callRecord->update([ + 'status' => 2, + 'handler_id' => auth()->id(), + 'handle_at' => now(), + 'remark' => $request->input('remark', $callRecord->remark), + ]); + return $this->success($callRecord); + } +} diff --git a/backend/app/Http/Controllers/Admin/Room/CustomerOutingController.php b/backend/app/Http/Controllers/Admin/Room/CustomerOutingController.php new file mode 100644 index 0000000..706f6d3 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Room/CustomerOutingController.php @@ -0,0 +1,49 @@ +with(['reservation.room', 'customer']); + $query->when($request->reservation_id, fn($q, $v) => $q->where('reservation_id', $v)); + $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 store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'reservation_id' => 'required|exists:reservations,id', + 'customer_id' => 'required|exists:customers,id', + 'out_at' => 'required|date', + 'expected_back_at' => 'nullable|date|after:out_at', + 'reason' => 'nullable|string', + 'risk_note' => 'nullable|string', + ]); + $validated['created_by'] = auth()->id(); + + return $this->success(CustomerOuting::create($validated)); + } + + public function show(CustomerOuting $customerOuting): JsonResponse + { + return $this->success($customerOuting->load(['reservation.room', 'customer'])); + } + + public function recordBack(Request $request, CustomerOuting $customerOuting): JsonResponse + { + if ($customerOuting->actual_back_at) { + return $this->error('已记录返回时间', 40001); + } + $customerOuting->update(['actual_back_at' => $request->input('actual_back_at', now())]); + return $this->success($customerOuting); + } +} diff --git a/backend/app/Http/Controllers/Admin/Room/ReservationController.php b/backend/app/Http/Controllers/Admin/Room/ReservationController.php new file mode 100644 index 0000000..f7ca8da --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Room/ReservationController.php @@ -0,0 +1,113 @@ +with(['customer', 'room.roomType']); + $query->when($request->customer_id, fn($q, $v) => $q->where('customer_id', $v)); + $query->when($request->room_id, fn($q, $v) => $q->where('room_id', $v)); + $query->when($request->status, fn($q, $v) => $q->where('status', $v)); + $query->when($request->check_in_date, fn($q, $v) => $q->whereDate('check_in_date', $v)); + + return $this->paginate($query->latest()->paginate($request->input('per_page', 15))); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'customer_id' => 'required|exists:customers,id', + 'contract_id' => 'nullable|exists:contracts,id', + 'room_id' => 'required|exists:rooms,id', + 'check_in_date' => 'required|date', + 'check_out_date' => 'required|date|after_or_equal:check_in_date', + 'remark' => 'nullable|string', + ]); + $validated['created_by'] = auth()->id(); + $validated['status'] = 0; + + $reservation = Reservation::create($validated); + Room::find($validated['room_id'])->update(['status' => 2]); + + return $this->success($reservation); + } + + public function show(Reservation $reservation): JsonResponse + { + return $this->success($reservation->load(['customer', 'room.roomType', 'contract', 'outings'])); + } + + public function update(Request $request, Reservation $reservation): JsonResponse + { + if ($reservation->status >= 2) { + return $this->error('已退房或已取消的预定不可修改', 40001); + } + $validated = $request->validate([ + 'room_id' => 'sometimes|exists:rooms,id', + 'check_in_date' => 'sometimes|date', + 'check_out_date' => 'sometimes|date|after_or_equal:check_in_date', + 'remark' => 'nullable|string', + ]); + $reservation->update($validated); + return $this->success($reservation); + } + + public function destroy(Reservation $reservation): JsonResponse + { + if ($reservation->status === 1) { + return $this->error('已入住的预定不可删除', 40001); + } + $reservation->room->update(['status' => 1]); + $reservation->delete(); + return $this->success(null); + } + + public function checkIn(Reservation $reservation): JsonResponse + { + if ($reservation->status !== 0) { + return $this->error('只有预定状态可以办理入住', 40001); + } + $reservation->update([ + 'status' => 1, + 'actual_check_in' => now(), + ]); + $reservation->room->update(['status' => 3]); + $reservation->customer->update(['status' => 3]); + + return $this->success($reservation); + } + + public function checkOut(Reservation $reservation): JsonResponse + { + if ($reservation->status !== 1) { + return $this->error('只有入住状态可以办理退房', 40001); + } + $reservation->update([ + 'status' => 2, + 'actual_check_out' => now(), + ]); + $reservation->room->update(['status' => 5]); + $reservation->customer->update(['status' => 4]); + + return $this->success($reservation); + } + + public function cancel(Request $request, Reservation $reservation): JsonResponse + { + if ($reservation->status >= 2) { + return $this->error('已退房或已取消的预定不可取消', 40001); + } + $reservation->update(['status' => 3, 'remark' => $request->input('remark', $reservation->remark)]); + $reservation->room->update(['status' => 1]); + + return $this->success($reservation); + } +} diff --git a/backend/app/Http/Controllers/Admin/Room/RoomController.php b/backend/app/Http/Controllers/Admin/Room/RoomController.php new file mode 100644 index 0000000..43273c6 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Room/RoomController.php @@ -0,0 +1,69 @@ +with('roomType'); + $query->when($request->room_type_id, fn($q, $v) => $q->where('room_type_id', $v)); + $query->when($request->floor, fn($q, $v) => $q->where('floor', $v)); + $query->when($request->status, fn($q, $v) => $q->where('status', $v)); + $query->when($request->number, fn($q, $v) => $q->where('number', 'like', "%{$v}%")); + + return $this->paginate($query->orderBy('sort')->paginate($request->input('per_page', 50))); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'room_type_id' => 'required|exists:room_types,id', + 'floor' => 'nullable|string|max:10', + 'number' => 'required|string|max:20', + 'status' => 'sometimes|integer|in:1,2,3,4,5', + 'sort' => 'sometimes|integer', + ]); + return $this->success(Room::create($validated)); + } + + public function show(Room $room): JsonResponse + { + return $this->success($room->load(['roomType', 'reservations.customer'])); + } + + public function update(Request $request, Room $room): JsonResponse + { + $validated = $request->validate([ + 'room_type_id' => 'sometimes|exists:room_types,id', + 'floor' => 'nullable|string|max:10', + 'number' => 'sometimes|string|max:20', + 'sort' => 'sometimes|integer', + ]); + $room->update($validated); + return $this->success($room); + } + + public function destroy(Room $room): JsonResponse + { + if ($room->status === 3) { + return $this->error('入住中的房间无法删除', 40001); + } + $room->delete(); + return $this->success(null); + } + + public function updateStatus(Request $request, Room $room): JsonResponse + { + $validated = $request->validate([ + 'status' => 'required|integer|in:1,2,3,4,5', + ]); + $room->update($validated); + return $this->success($room); + } +} diff --git a/backend/app/Http/Controllers/Admin/Room/RoomTypeController.php b/backend/app/Http/Controllers/Admin/Room/RoomTypeController.php new file mode 100644 index 0000000..c219188 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/Room/RoomTypeController.php @@ -0,0 +1,63 @@ +withCount('rooms'); + $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:50', + 'price' => 'nullable|numeric|min:0', + 'description' => 'nullable|string', + 'images' => 'nullable|array', + 'facilities' => 'nullable|array', + 'sort' => 'sometimes|integer', + 'status' => 'sometimes|integer|in:0,1', + ]); + return $this->success(RoomType::create($validated)); + } + + public function show(RoomType $roomType): JsonResponse + { + return $this->success($roomType->load('rooms')); + } + + public function update(Request $request, RoomType $roomType): JsonResponse + { + $validated = $request->validate([ + 'name' => 'sometimes|string|max:50', + 'price' => 'nullable|numeric|min:0', + 'description' => 'nullable|string', + 'images' => 'nullable|array', + 'facilities' => 'nullable|array', + 'sort' => 'sometimes|integer', + 'status' => 'sometimes|integer|in:0,1', + ]); + $roomType->update($validated); + return $this->success($roomType); + } + + public function destroy(RoomType $roomType): JsonResponse + { + if ($roomType->rooms()->exists()) { + return $this->error('该房型下有房间,无法删除', 40001); + } + $roomType->delete(); + return $this->success(null); + } +} diff --git a/backend/app/Models/Care/CareException.php b/backend/app/Models/Care/CareException.php new file mode 100644 index 0000000..b0ff8b5 --- /dev/null +++ b/backend/app/Models/Care/CareException.php @@ -0,0 +1,40 @@ + 'integer', + 'images' => 'array', + 'status' => 'integer', + 'handle_at' => 'datetime', + ]; + } + + public function profile(): BelongsTo + { + return $this->belongsTo(CareProfile::class, 'care_profile_id'); + } + + public function reporter(): BelongsTo + { + return $this->belongsTo(User::class, 'reporter_id'); + } + + public function handler(): BelongsTo + { + return $this->belongsTo(User::class, 'handler_id'); + } +} diff --git a/backend/app/Models/Care/CarePlan.php b/backend/app/Models/Care/CarePlan.php new file mode 100644 index 0000000..da23c7f --- /dev/null +++ b/backend/app/Models/Care/CarePlan.php @@ -0,0 +1,40 @@ + 'date', + 'items' => 'array', + 'status' => 'integer', + ]; + } + + public function profile(): BelongsTo + { + return $this->belongsTo(CareProfile::class, 'care_profile_id'); + } + + public function nurse(): BelongsTo + { + return $this->belongsTo(User::class, 'nurse_id'); + } + + public function records(): HasMany + { + return $this->hasMany(CareRecord::class); + } +} diff --git a/backend/app/Models/Care/CareProfile.php b/backend/app/Models/Care/CareProfile.php new file mode 100644 index 0000000..8cebf76 --- /dev/null +++ b/backend/app/Models/Care/CareProfile.php @@ -0,0 +1,61 @@ + 'integer', + 'baby_gender' => 'integer', + 'baby_birthday' => 'datetime', + 'birth_weight' => 'decimal:2', + 'birth_method' => 'integer', + 'risk_level' => 'integer', + 'assessment' => 'array', + ]; + } + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } + + public function reservation(): BelongsTo + { + return $this->belongsTo(Reservation::class); + } + + public function plans(): HasMany + { + return $this->hasMany(CarePlan::class); + } + + public function records(): HasMany + { + return $this->hasMany(CareRecord::class); + } + + public function exceptions(): HasMany + { + return $this->hasMany(CareException::class); + } + + public function healthMetrics(): HasMany + { + return $this->hasMany(HealthMetric::class); + } +} diff --git a/backend/app/Models/Care/CareRecord.php b/backend/app/Models/Care/CareRecord.php new file mode 100644 index 0000000..7818660 --- /dev/null +++ b/backend/app/Models/Care/CareRecord.php @@ -0,0 +1,40 @@ + 'integer', + 'items' => 'array', + 'images' => 'array', + 'recorded_at' => 'datetime', + ]; + } + + public function profile(): BelongsTo + { + return $this->belongsTo(CareProfile::class, 'care_profile_id'); + } + + public function plan(): BelongsTo + { + return $this->belongsTo(CarePlan::class, 'care_plan_id'); + } + + public function nurse(): BelongsTo + { + return $this->belongsTo(User::class, 'nurse_id'); + } +} diff --git a/backend/app/Models/Care/HealthMetric.php b/backend/app/Models/Care/HealthMetric.php new file mode 100644 index 0000000..4e52e74 --- /dev/null +++ b/backend/app/Models/Care/HealthMetric.php @@ -0,0 +1,35 @@ + 'decimal:2', + 'recorded_at' => 'datetime', + ]; + } + + public function profile(): BelongsTo + { + return $this->belongsTo(CareProfile::class, 'care_profile_id'); + } + + public function recorder(): BelongsTo + { + return $this->belongsTo(User::class, 'recorder_id'); + } +} diff --git a/backend/app/Models/Crm/Channel.php b/backend/app/Models/Crm/Channel.php new file mode 100644 index 0000000..0270639 --- /dev/null +++ b/backend/app/Models/Crm/Channel.php @@ -0,0 +1,29 @@ + 'integer', + 'status' => 'integer', + ]; + } + + public function leads(): HasMany + { + return $this->hasMany(Lead::class); + } +} diff --git a/backend/app/Models/Crm/Complaint.php b/backend/app/Models/Crm/Complaint.php new file mode 100644 index 0000000..d4d02d2 --- /dev/null +++ b/backend/app/Models/Crm/Complaint.php @@ -0,0 +1,38 @@ + 'integer', + 'images' => 'array', + 'status' => 'integer', + 'handle_at' => 'datetime', + ]; + } + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } + + public function handler(): BelongsTo + { + return $this->belongsTo(User::class, 'handler_id'); + } +} diff --git a/backend/app/Models/Crm/Contract.php b/backend/app/Models/Crm/Contract.php new file mode 100644 index 0000000..4d04faa --- /dev/null +++ b/backend/app/Models/Crm/Contract.php @@ -0,0 +1,50 @@ + 'decimal:2', + 'discount_amount' => 'decimal:2', + 'actual_amount' => 'decimal:2', + 'days' => 'integer', + 'check_in_date' => 'date', + 'check_out_date' => 'date', + 'status' => 'integer', + 'audit_at' => 'datetime', + ]; + } + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } + + public function auditor(): BelongsTo + { + return $this->belongsTo(User::class, 'audit_user_id'); + } + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } +} diff --git a/backend/app/Models/Crm/Customer.php b/backend/app/Models/Crm/Customer.php new file mode 100644 index 0000000..d92adec --- /dev/null +++ b/backend/app/Models/Crm/Customer.php @@ -0,0 +1,63 @@ + 'date', + 'expected_date' => 'date', + 'actual_date' => 'date', + 'baby_count' => 'integer', + 'tags' => 'array', + 'status' => 'integer', + ]; + } + + public function lead(): BelongsTo + { + return $this->belongsTo(Lead::class); + } + + public function owner(): BelongsTo + { + return $this->belongsTo(User::class, 'owner_id'); + } + + public function families(): HasMany + { + return $this->hasMany(CustomerFamily::class); + } + + public function contracts(): HasMany + { + return $this->hasMany(Contract::class); + } + + public function questionnaireAnswers(): HasMany + { + return $this->hasMany(QuestionnaireAnswer::class); + } + + public function complaints(): HasMany + { + return $this->hasMany(Complaint::class); + } +} diff --git a/backend/app/Models/Crm/CustomerFamily.php b/backend/app/Models/Crm/CustomerFamily.php new file mode 100644 index 0000000..778a9f9 --- /dev/null +++ b/backend/app/Models/Crm/CustomerFamily.php @@ -0,0 +1,27 @@ + 'boolean', + ]; + } + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } +} diff --git a/backend/app/Models/Crm/Lead.php b/backend/app/Models/Crm/Lead.php new file mode 100644 index 0000000..8b01d8f --- /dev/null +++ b/backend/app/Models/Crm/Lead.php @@ -0,0 +1,53 @@ + 'date', + 'status' => 'integer', + ]; + } + + public function channel(): BelongsTo + { + return $this->belongsTo(Channel::class); + } + + public function owner(): BelongsTo + { + return $this->belongsTo(User::class, 'owner_id'); + } + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function follows(): HasMany + { + return $this->hasMany(LeadFollow::class); + } + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } +} diff --git a/backend/app/Models/Crm/LeadFollow.php b/backend/app/Models/Crm/LeadFollow.php new file mode 100644 index 0000000..148e138 --- /dev/null +++ b/backend/app/Models/Crm/LeadFollow.php @@ -0,0 +1,35 @@ + 'integer', + 'next_follow_at' => 'datetime', + 'created_at' => 'datetime', + ]; + } + + public function lead(): BelongsTo + { + return $this->belongsTo(Lead::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/backend/app/Models/Crm/QuestionnaireAnswer.php b/backend/app/Models/Crm/QuestionnaireAnswer.php new file mode 100644 index 0000000..cb244e1 --- /dev/null +++ b/backend/app/Models/Crm/QuestionnaireAnswer.php @@ -0,0 +1,33 @@ + 'array', + 'score' => 'decimal:2', + 'status' => 'integer', + ]; + } + + public function template(): BelongsTo + { + return $this->belongsTo(QuestionnaireTemplate::class, 'questionnaire_template_id'); + } + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } +} diff --git a/backend/app/Models/Crm/QuestionnaireTemplate.php b/backend/app/Models/Crm/QuestionnaireTemplate.php new file mode 100644 index 0000000..2c7c7cb --- /dev/null +++ b/backend/app/Models/Crm/QuestionnaireTemplate.php @@ -0,0 +1,30 @@ + 'array', + 'type' => 'integer', + 'status' => 'integer', + ]; + } + + public function answers(): HasMany + { + return $this->hasMany(QuestionnaireAnswer::class); + } +} diff --git a/backend/app/Models/Meal/DailyMealPlan.php b/backend/app/Models/Meal/DailyMealPlan.php new file mode 100644 index 0000000..a965a5d --- /dev/null +++ b/backend/app/Models/Meal/DailyMealPlan.php @@ -0,0 +1,40 @@ + 'date', + 'meal_type' => 'integer', + 'dishes' => 'array', + 'status' => 'integer', + 'deliver_at' => 'datetime', + ]; + } + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } + + public function review(): HasOne + { + return $this->hasOne(MealReview::class); + } +} diff --git a/backend/app/Models/Meal/Dish.php b/backend/app/Models/Meal/Dish.php new file mode 100644 index 0000000..a494def --- /dev/null +++ b/backend/app/Models/Meal/Dish.php @@ -0,0 +1,26 @@ + 'array', + 'contraindications' => 'array', + 'price' => 'decimal:2', + 'status' => 'integer', + ]; + } +} diff --git a/backend/app/Models/Meal/MealPlanTemplate.php b/backend/app/Models/Meal/MealPlanTemplate.php new file mode 100644 index 0000000..d5c840f --- /dev/null +++ b/backend/app/Models/Meal/MealPlanTemplate.php @@ -0,0 +1,23 @@ + 'array', + 'status' => 'integer', + ]; + } +} diff --git a/backend/app/Models/Meal/MealReview.php b/backend/app/Models/Meal/MealReview.php new file mode 100644 index 0000000..84e1aae --- /dev/null +++ b/backend/app/Models/Meal/MealReview.php @@ -0,0 +1,33 @@ + 'integer', + ]; + } + + public function mealPlan(): BelongsTo + { + return $this->belongsTo(DailyMealPlan::class, 'daily_meal_plan_id'); + } + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } +} diff --git a/backend/app/Models/Room/CallRecord.php b/backend/app/Models/Room/CallRecord.php new file mode 100644 index 0000000..0a46602 --- /dev/null +++ b/backend/app/Models/Room/CallRecord.php @@ -0,0 +1,43 @@ + 'integer', + 'status' => 'integer', + 'handle_at' => 'datetime', + ]; + } + + public function room(): BelongsTo + { + return $this->belongsTo(Room::class); + } + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } + + public function handler(): BelongsTo + { + return $this->belongsTo(User::class, 'handler_id'); + } +} diff --git a/backend/app/Models/Room/CustomerOuting.php b/backend/app/Models/Room/CustomerOuting.php new file mode 100644 index 0000000..7485545 --- /dev/null +++ b/backend/app/Models/Room/CustomerOuting.php @@ -0,0 +1,40 @@ + 'datetime', + 'expected_back_at' => 'datetime', + 'actual_back_at' => 'datetime', + ]; + } + + public function reservation(): BelongsTo + { + return $this->belongsTo(Reservation::class); + } + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } +} diff --git a/backend/app/Models/Room/Reservation.php b/backend/app/Models/Room/Reservation.php new file mode 100644 index 0000000..05ab9b2 --- /dev/null +++ b/backend/app/Models/Room/Reservation.php @@ -0,0 +1,59 @@ + 'date', + 'check_out_date' => 'date', + 'actual_check_in' => 'datetime', + 'actual_check_out' => 'datetime', + 'status' => 'integer', + ]; + } + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } + + public function contract(): BelongsTo + { + return $this->belongsTo(Contract::class); + } + + public function room(): BelongsTo + { + return $this->belongsTo(Room::class); + } + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function outings(): HasMany + { + return $this->hasMany(CustomerOuting::class); + } +} diff --git a/backend/app/Models/Room/Room.php b/backend/app/Models/Room/Room.php new file mode 100644 index 0000000..9e23492 --- /dev/null +++ b/backend/app/Models/Room/Room.php @@ -0,0 +1,41 @@ + 'integer', + 'sort' => 'integer', + ]; + } + + public function roomType(): BelongsTo + { + return $this->belongsTo(RoomType::class); + } + + public function reservations(): HasMany + { + return $this->hasMany(Reservation::class); + } + + public function callRecords(): HasMany + { + return $this->hasMany(CallRecord::class); + } +} diff --git a/backend/app/Models/Room/RoomType.php b/backend/app/Models/Room/RoomType.php new file mode 100644 index 0000000..763c682 --- /dev/null +++ b/backend/app/Models/Room/RoomType.php @@ -0,0 +1,33 @@ + 'decimal:2', + 'images' => 'array', + 'facilities' => 'array', + 'sort' => 'integer', + 'status' => 'integer', + ]; + } + + public function rooms(): HasMany + { + return $this->hasMany(Room::class); + } +} diff --git a/backend/database/migrations/2024_01_02_000001_create_crm_tables.php b/backend/database/migrations/2024_01_02_000001_create_crm_tables.php new file mode 100644 index 0000000..257932e --- /dev/null +++ b/backend/database/migrations/2024_01_02_000001_create_crm_tables.php @@ -0,0 +1,164 @@ +id(); + $table->foreignId('store_id')->constrained(); + $table->string('name', 50); + $table->tinyInteger('type')->nullable()->comment('1线上 2线下 3转介绍'); + $table->tinyInteger('status')->default(1); + $table->timestamps(); + }); + + // 线索 + Schema::create('leads', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained(); + $table->unsignedBigInteger('channel_id')->nullable(); + $table->string('name', 50); + $table->string('phone', 20); + $table->string('wechat', 50)->nullable(); + $table->date('expected_date')->nullable()->comment('预产期'); + $table->string('source', 50)->nullable(); + $table->tinyInteger('status')->default(1)->comment('1新建 2跟进中 3已转化 4无效'); + $table->unsignedBigInteger('owner_id')->nullable()->comment('负责销售'); + $table->text('remark')->nullable(); + $table->unsignedBigInteger('created_by')->nullable(); + $table->timestamps(); + $table->index(['store_id', 'status']); + $table->index('owner_id'); + $table->index('phone'); + }); + + // 线索跟进记录 + Schema::create('lead_follows', function (Blueprint $table) { + $table->id(); + $table->foreignId('lead_id')->constrained()->onDelete('cascade'); + $table->unsignedBigInteger('user_id'); + $table->tinyInteger('type')->nullable()->comment('1电话 2微信 3到店 4其他'); + $table->text('content')->nullable(); + $table->timestamp('next_follow_at')->nullable(); + $table->timestamp('created_at')->nullable(); + $table->index('lead_id'); + }); + + // 客户 + Schema::create('customers', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained(); + $table->unsignedBigInteger('lead_id')->nullable(); + $table->string('name', 50); + $table->string('phone', 20); + $table->string('id_card', 20)->nullable(); + $table->string('wechat', 50)->nullable(); + $table->date('birthday')->nullable(); + $table->date('expected_date')->nullable(); + $table->date('actual_date')->nullable(); + $table->tinyInteger('baby_count')->default(1); + $table->json('tags')->nullable(); + $table->tinyInteger('status')->default(1)->comment('1潜在 2签约 3在住 4离店 5无效'); + $table->unsignedBigInteger('owner_id')->nullable(); + $table->text('remark')->nullable(); + $table->timestamps(); + $table->softDeletes(); + $table->index(['store_id', 'status']); + $table->index('phone'); + }); + + // 客户家属 + Schema::create('customer_families', function (Blueprint $table) { + $table->id(); + $table->foreignId('customer_id')->constrained()->onDelete('cascade'); + $table->string('name', 50)->nullable(); + $table->string('phone', 20)->nullable(); + $table->string('relation', 20)->nullable(); + $table->tinyInteger('is_emergency')->default(0); + $table->timestamps(); + $table->index('customer_id'); + }); + + // 合同 + Schema::create('contracts', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained(); + $table->foreignId('customer_id')->constrained(); + $table->string('contract_no', 50)->unique(); + $table->string('package_name', 100)->nullable(); + $table->decimal('total_amount', 12, 2); + $table->decimal('discount_amount', 12, 2)->default(0); + $table->decimal('actual_amount', 12, 2); + $table->integer('days')->nullable(); + $table->date('check_in_date')->nullable(); + $table->date('check_out_date')->nullable(); + $table->tinyInteger('status')->default(0)->comment('0待审 1通过 2无效 3已退'); + $table->unsignedBigInteger('audit_user_id')->nullable(); + $table->timestamp('audit_at')->nullable(); + $table->string('audit_remark', 255)->nullable(); + $table->text('remark')->nullable(); + $table->unsignedBigInteger('created_by')->nullable(); + $table->timestamps(); + $table->index('store_id'); + $table->index('customer_id'); + }); + + // 问卷模板 + Schema::create('questionnaire_templates', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('store_id')->nullable(); + $table->string('title', 100); + $table->text('description')->nullable(); + $table->json('questions')->nullable(); + $table->tinyInteger('type')->nullable()->comment('1入住前 2入住中 3离店 4回访'); + $table->tinyInteger('status')->default(1); + $table->timestamps(); + }); + + // 问卷回收 + Schema::create('questionnaire_answers', function (Blueprint $table) { + $table->id(); + $table->foreignId('questionnaire_template_id')->constrained('questionnaire_templates')->onDelete('cascade'); + $table->foreignId('customer_id')->constrained(); + $table->json('answers')->nullable(); + $table->decimal('score', 5, 2)->nullable(); + $table->tinyInteger('status')->default(0)->comment('0未完成 1已提交'); + $table->timestamps(); + }); + + // 投诉 + Schema::create('complaints', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained(); + $table->foreignId('customer_id')->constrained(); + $table->tinyInteger('type')->nullable()->comment('1服务 2膳食 3环境 4其他'); + $table->text('content'); + $table->json('images')->nullable(); + $table->tinyInteger('status')->default(0)->comment('0待处理 1处理中 2已解决 3已关闭'); + $table->unsignedBigInteger('handler_id')->nullable(); + $table->text('handle_result')->nullable(); + $table->timestamp('handle_at')->nullable(); + $table->timestamps(); + $table->index(['store_id', 'status']); + }); + } + + public function down(): void + { + Schema::dropIfExists('complaints'); + Schema::dropIfExists('questionnaire_answers'); + Schema::dropIfExists('questionnaire_templates'); + Schema::dropIfExists('contracts'); + Schema::dropIfExists('customer_families'); + Schema::dropIfExists('customers'); + Schema::dropIfExists('lead_follows'); + Schema::dropIfExists('leads'); + Schema::dropIfExists('channels'); + } +}; diff --git a/backend/database/migrations/2024_01_02_000002_create_room_tables.php b/backend/database/migrations/2024_01_02_000002_create_room_tables.php new file mode 100644 index 0000000..b56fee6 --- /dev/null +++ b/backend/database/migrations/2024_01_02_000002_create_room_tables.php @@ -0,0 +1,95 @@ +id(); + $table->foreignId('store_id')->constrained(); + $table->string('name', 50); + $table->decimal('price', 12, 2)->nullable()->comment('日单价'); + $table->text('description')->nullable(); + $table->json('images')->nullable(); + $table->json('facilities')->nullable(); + $table->integer('sort')->default(0); + $table->tinyInteger('status')->default(1); + $table->timestamps(); + }); + + // 房间 + Schema::create('rooms', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained(); + $table->foreignId('room_type_id')->constrained(); + $table->string('floor', 10)->nullable(); + $table->string('number', 20); + $table->tinyInteger('status')->default(1)->comment('1空房 2已预定 3入住 4维修 5清洁'); + $table->integer('sort')->default(0); + $table->timestamps(); + $table->unique(['store_id', 'number']); + }); + + // 预定/入住记录 + Schema::create('reservations', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained(); + $table->foreignId('customer_id')->constrained(); + $table->unsignedBigInteger('contract_id')->nullable(); + $table->foreignId('room_id')->constrained(); + $table->date('check_in_date'); + $table->date('check_out_date'); + $table->timestamp('actual_check_in')->nullable(); + $table->timestamp('actual_check_out')->nullable(); + $table->tinyInteger('status')->default(0)->comment('0预定 1已入住 2已退房 3已取消'); + $table->text('remark')->nullable(); + $table->unsignedBigInteger('created_by')->nullable(); + $table->timestamps(); + $table->index(['room_id', 'check_in_date', 'check_out_date']); + $table->index('customer_id'); + }); + + // 客户外出记录 + Schema::create('customer_outings', function (Blueprint $table) { + $table->id(); + $table->foreignId('reservation_id')->constrained(); + $table->foreignId('customer_id')->constrained(); + $table->timestamp('out_at'); + $table->timestamp('expected_back_at')->nullable(); + $table->timestamp('actual_back_at')->nullable(); + $table->text('reason')->nullable(); + $table->text('risk_note')->nullable(); + $table->unsignedBigInteger('created_by')->nullable(); + $table->timestamps(); + }); + + // 呼叫记录 + Schema::create('call_records', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained(); + $table->foreignId('room_id')->constrained(); + $table->unsignedBigInteger('customer_id')->nullable(); + $table->tinyInteger('type')->nullable()->comment('1护理 2清洁 3送水 4其他'); + $table->tinyInteger('status')->default(0)->comment('0待处理 1处理中 2已完成'); + $table->unsignedBigInteger('handler_id')->nullable(); + $table->timestamp('handle_at')->nullable(); + $table->string('remark', 255)->nullable(); + $table->timestamps(); + $table->index(['store_id', 'status']); + }); + } + + public function down(): void + { + Schema::dropIfExists('call_records'); + Schema::dropIfExists('customer_outings'); + Schema::dropIfExists('reservations'); + Schema::dropIfExists('rooms'); + Schema::dropIfExists('room_types'); + } +}; diff --git a/backend/database/migrations/2024_01_02_000003_create_care_tables.php b/backend/database/migrations/2024_01_02_000003_create_care_tables.php new file mode 100644 index 0000000..18532ea --- /dev/null +++ b/backend/database/migrations/2024_01_02_000003_create_care_tables.php @@ -0,0 +1,99 @@ +id(); + $table->foreignId('customer_id')->constrained(); + $table->unsignedBigInteger('reservation_id')->nullable(); + $table->tinyInteger('type')->comment('1妈妈 2宝宝'); + $table->string('baby_name', 50)->nullable(); + $table->tinyInteger('baby_gender')->nullable()->comment('1男 2女'); + $table->dateTime('baby_birthday')->nullable(); + $table->decimal('birth_weight', 5, 2)->nullable(); + $table->tinyInteger('birth_method')->nullable()->comment('1顺产 2剖宫产'); + $table->text('allergies')->nullable(); + $table->text('medical_history')->nullable(); + $table->tinyInteger('risk_level')->default(0)->comment('0正常 1低风险 2高风险'); + $table->json('assessment')->nullable(); + $table->timestamps(); + $table->index('customer_id'); + }); + + // 护理计划 + Schema::create('care_plans', function (Blueprint $table) { + $table->id(); + $table->foreignId('care_profile_id')->constrained()->onDelete('cascade'); + $table->date('plan_date'); + $table->string('stage', 50)->nullable()->comment('阶段:产后第X天'); + $table->json('items')->nullable(); + $table->unsignedBigInteger('nurse_id')->nullable(); + $table->tinyInteger('status')->default(0)->comment('0待执行 1执行中 2已完成'); + $table->timestamps(); + $table->index(['care_profile_id', 'plan_date']); + }); + + // 护理记录 + Schema::create('care_records', function (Blueprint $table) { + $table->id(); + $table->foreignId('care_profile_id')->constrained()->onDelete('cascade'); + $table->unsignedBigInteger('care_plan_id')->nullable(); + $table->tinyInteger('type')->nullable()->comment('1妈妈护理 2宝宝护理'); + $table->json('items')->nullable(); + $table->text('remark')->nullable(); + $table->json('images')->nullable(); + $table->unsignedBigInteger('nurse_id'); + $table->timestamp('recorded_at'); + $table->timestamps(); + $table->index('care_profile_id'); + $table->index('recorded_at'); + }); + + // 护理异常 + Schema::create('care_exceptions', function (Blueprint $table) { + $table->id(); + $table->foreignId('care_profile_id')->constrained()->onDelete('cascade'); + $table->unsignedBigInteger('reporter_id'); + $table->tinyInteger('level')->comment('1一般 2重要 3紧急'); + $table->text('description'); + $table->json('images')->nullable(); + $table->tinyInteger('status')->default(0)->comment('0上报 1处理中 2已解决'); + $table->unsignedBigInteger('handler_id')->nullable(); + $table->text('handle_result')->nullable(); + $table->timestamp('handle_at')->nullable(); + $table->timestamps(); + $table->index('care_profile_id'); + }); + + // 健康指标记录 + Schema::create('health_metrics', function (Blueprint $table) { + $table->id(); + $table->foreignId('care_profile_id')->constrained()->onDelete('cascade'); + $table->string('metric_type', 30)->comment('temperature/weight/jaundice/blood_pressure'); + $table->decimal('value', 8, 2); + $table->string('unit', 10)->nullable(); + $table->timestamp('recorded_at'); + $table->unsignedBigInteger('recorder_id')->nullable(); + $table->string('remark', 255)->nullable(); + $table->timestamp('created_at')->nullable(); + $table->index(['care_profile_id', 'metric_type']); + $table->index('recorded_at'); + }); + } + + public function down(): void + { + Schema::dropIfExists('health_metrics'); + Schema::dropIfExists('care_exceptions'); + Schema::dropIfExists('care_records'); + Schema::dropIfExists('care_plans'); + Schema::dropIfExists('care_profiles'); + } +}; diff --git a/backend/database/migrations/2024_01_02_000004_create_meal_tables.php b/backend/database/migrations/2024_01_02_000004_create_meal_tables.php new file mode 100644 index 0000000..8d7f4a2 --- /dev/null +++ b/backend/database/migrations/2024_01_02_000004_create_meal_tables.php @@ -0,0 +1,73 @@ +id(); + $table->foreignId('store_id')->constrained(); + $table->string('name', 50); + $table->string('category', 30)->nullable()->comment('汤/主食/配菜/甜品'); + $table->text('description')->nullable(); + $table->string('image', 255)->nullable(); + $table->json('ingredients')->nullable(); + $table->json('contraindications')->nullable()->comment('禁忌'); + $table->decimal('price', 8, 2)->default(0); + $table->tinyInteger('status')->default(1); + $table->timestamps(); + $table->index('store_id'); + }); + + // 排餐模板 + Schema::create('meal_plan_templates', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained(); + $table->string('name', 50); + $table->string('stage', 30)->nullable()->comment('适用阶段'); + $table->json('meals')->nullable(); + $table->tinyInteger('status')->default(1); + $table->timestamps(); + }); + + // 每日排餐 + Schema::create('daily_meal_plans', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained(); + $table->foreignId('customer_id')->constrained(); + $table->date('plan_date'); + $table->tinyInteger('meal_type')->comment('1早餐 2午餐 3下午茶 4晚餐 5宵夜'); + $table->json('dishes')->nullable(); + $table->text('special_note')->nullable(); + $table->tinyInteger('status')->default(0)->comment('0待备餐 1已备餐 2已送达 3未用'); + $table->timestamp('deliver_at')->nullable(); + $table->timestamps(); + $table->index(['customer_id', 'plan_date']); + $table->index(['store_id', 'plan_date']); + }); + + // 膳食评价 + Schema::create('meal_reviews', function (Blueprint $table) { + $table->id(); + $table->foreignId('daily_meal_plan_id')->constrained()->onDelete('cascade'); + $table->foreignId('customer_id')->constrained(); + $table->tinyInteger('score')->nullable()->comment('1-5星'); + $table->text('content')->nullable(); + $table->timestamp('created_at')->nullable(); + $table->index('daily_meal_plan_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('meal_reviews'); + Schema::dropIfExists('daily_meal_plans'); + Schema::dropIfExists('meal_plan_templates'); + Schema::dropIfExists('dishes'); + } +}; diff --git a/backend/routes/api.php b/backend/routes/api.php index 5b0bb29..e5982e4 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -11,6 +11,26 @@ use App\Http\Controllers\Admin\System\MenuController; use App\Http\Controllers\Admin\System\PermissionController; use App\Http\Controllers\Admin\System\DictionaryController; use App\Http\Controllers\Admin\System\OperationLogController; +use App\Http\Controllers\Admin\Crm\ChannelController; +use App\Http\Controllers\Admin\Crm\LeadController; +use App\Http\Controllers\Admin\Crm\CustomerController; +use App\Http\Controllers\Admin\Crm\ContractController; +use App\Http\Controllers\Admin\Crm\QuestionnaireController; +use App\Http\Controllers\Admin\Crm\ComplaintController; +use App\Http\Controllers\Admin\Room\RoomTypeController; +use App\Http\Controllers\Admin\Room\RoomController; +use App\Http\Controllers\Admin\Room\ReservationController; +use App\Http\Controllers\Admin\Room\CustomerOutingController; +use App\Http\Controllers\Admin\Room\CallRecordController; +use App\Http\Controllers\Admin\Care\CareProfileController; +use App\Http\Controllers\Admin\Care\CarePlanController; +use App\Http\Controllers\Admin\Care\CareRecordController; +use App\Http\Controllers\Admin\Care\CareExceptionController; +use App\Http\Controllers\Admin\Care\HealthMetricController; +use App\Http\Controllers\Admin\Meal\DishController; +use App\Http\Controllers\Admin\Meal\MealPlanTemplateController; +use App\Http\Controllers\Admin\Meal\DailyMealPlanController; +use App\Http\Controllers\Admin\Meal\MealReviewController; /* |-------------------------------------------------------------------------- @@ -66,4 +86,94 @@ Route::middleware(['auth:sanctum', 'store', 'oplog'])->group(function () { // 操作日志 Route::get('operation-logs', [OperationLogController::class, 'index']); }); + + // --- CRM 客户关系模块 --- + Route::prefix('crm')->group(function () { + // 渠道管理 + Route::apiResource('channels', ChannelController::class); + + // 线索管理 + Route::apiResource('leads', LeadController::class); + Route::post('leads/{lead}/follow', [LeadController::class, 'follow']); + Route::put('leads/{lead}/assign', [LeadController::class, 'assign']); + + // 客户管理 + Route::apiResource('customers', CustomerController::class); + + // 合同管理 + Route::apiResource('contracts', ContractController::class); + Route::put('contracts/{contract}/audit', [ContractController::class, 'audit']); + + // 问卷管理 + Route::apiResource('questionnaires', QuestionnaireController::class); + Route::post('questionnaire-answers', [QuestionnaireController::class, 'submitAnswer']); + Route::get('questionnaire-answers', [QuestionnaireController::class, 'answers']); + + // 投诉管理 + Route::apiResource('complaints', ComplaintController::class); + Route::put('complaints/{complaint}/handle', [ComplaintController::class, 'handle']); + }); + + // --- 房务管理模块 --- + Route::prefix('room')->group(function () { + // 房型管理 + Route::apiResource('room-types', RoomTypeController::class); + + // 房间管理 + Route::apiResource('rooms', RoomController::class); + Route::put('rooms/{room}/status', [RoomController::class, 'updateStatus']); + + // 预定管理 + Route::apiResource('reservations', ReservationController::class); + Route::put('reservations/{reservation}/check-in', [ReservationController::class, 'checkIn']); + Route::put('reservations/{reservation}/check-out', [ReservationController::class, 'checkOut']); + Route::put('reservations/{reservation}/cancel', [ReservationController::class, 'cancel']); + + // 外出记录 + Route::apiResource('customer-outings', CustomerOutingController::class)->only(['index', 'store', 'show']); + Route::put('customer-outings/{customerOuting}/back', [CustomerOutingController::class, 'recordBack']); + + // 呼叫记录 + Route::get('call-records', [CallRecordController::class, 'index']); + Route::post('call-records', [CallRecordController::class, 'store']); + Route::put('call-records/{callRecord}/handle', [CallRecordController::class, 'handle']); + }); + + // --- 护理管理模块 --- + Route::prefix('care')->group(function () { + // 护理档案 + Route::apiResource('profiles', CareProfileController::class); + + // 护理计划 + Route::apiResource('plans', CarePlanController::class); + + // 护理记录 + Route::apiResource('records', CareRecordController::class); + + // 护理异常 + Route::apiResource('exceptions', CareExceptionController::class)->except(['destroy']); + Route::put('exceptions/{careException}/handle', [CareExceptionController::class, 'handle']); + + // 健康指标 + Route::get('health-metrics', [HealthMetricController::class, 'index']); + Route::post('health-metrics', [HealthMetricController::class, 'store']); + }); + + // --- 月子餐模块 --- + Route::prefix('meal')->group(function () { + // 菜品库 + Route::apiResource('dishes', DishController::class); + + // 排餐模板 + Route::apiResource('templates', MealPlanTemplateController::class); + + // 每日排餐 + Route::apiResource('daily-plans', DailyMealPlanController::class); + Route::put('daily-plans/{dailyMealPlan}/deliver', [DailyMealPlanController::class, 'deliver']); + Route::put('daily-plans/{dailyMealPlan}/status', [DailyMealPlanController::class, 'updateStatus']); + + // 膳食评价 + Route::get('reviews', [MealReviewController::class, 'index']); + Route::post('reviews', [MealReviewController::class, 'store']); + }); }); diff --git a/frontend/src/api/care.js b/frontend/src/api/care.js new file mode 100644 index 0000000..fd06fc7 --- /dev/null +++ b/frontend/src/api/care.js @@ -0,0 +1,43 @@ +import request from '@/utils/request.js' + +// ─── Care Profiles ──────────────────────────────────────────────────────────── +export const careProfileApi = { + getList: (params) => request.get('/care/profiles', { params }), + getDetail: (id) => request.get(`/care/profiles/${id}`), + create: (data) => request.post('/care/profiles', data), + update: (id, data) => request.put(`/care/profiles/${id}`, data), + delete: (id) => request.delete(`/care/profiles/${id}`) +} + +// ─── Care Plans ─────────────────────────────────────────────────────────────── +export const carePlanApi = { + getList: (params) => request.get('/care/plans', { params }), + getDetail: (id) => request.get(`/care/plans/${id}`), + create: (data) => request.post('/care/plans', data), + update: (id, data) => request.put(`/care/plans/${id}`, data), + delete: (id) => request.delete(`/care/plans/${id}`) +} + +// ─── Care Records ───────────────────────────────────────────────────────────── +export const careRecordApi = { + getList: (params) => request.get('/care/records', { params }), + getDetail: (id) => request.get(`/care/records/${id}`), + create: (data) => request.post('/care/records', data), + update: (id, data) => request.put(`/care/records/${id}`, data), + delete: (id) => request.delete(`/care/records/${id}`) +} + +// ─── Care Exceptions ────────────────────────────────────────────────────────── +export const careExceptionApi = { + getList: (params) => request.get('/care/exceptions', { params }), + getDetail: (id) => request.get(`/care/exceptions/${id}`), + create: (data) => request.post('/care/exceptions', data), + update: (id, data) => request.put(`/care/exceptions/${id}`, data), + handle: (id, data) => request.put(`/care/exceptions/${id}/handle`, data) +} + +// ─── Health Metrics ─────────────────────────────────────────────────────────── +export const healthMetricApi = { + getList: (params) => request.get('/care/health-metrics', { params }), + create: (data) => request.post('/care/health-metrics', data) +} diff --git a/frontend/src/api/crm.js b/frontend/src/api/crm.js new file mode 100644 index 0000000..6b77770 --- /dev/null +++ b/frontend/src/api/crm.js @@ -0,0 +1,61 @@ +import request from '@/utils/request.js' + +// ─── Channels ───────────────────────────────────────────────────────────────── +export const channelApi = { + getList: (params) => request.get('/crm/channels', { params }), + getDetail: (id) => request.get(`/crm/channels/${id}`), + create: (data) => request.post('/crm/channels', data), + update: (id, data) => request.put(`/crm/channels/${id}`, data), + delete: (id) => request.delete(`/crm/channels/${id}`) +} + +// ─── Leads ──────────────────────────────────────────────────────────────────── +export const leadApi = { + getList: (params) => request.get('/crm/leads', { params }), + getDetail: (id) => request.get(`/crm/leads/${id}`), + create: (data) => request.post('/crm/leads', data), + update: (id, data) => request.put(`/crm/leads/${id}`, data), + delete: (id) => request.delete(`/crm/leads/${id}`), + follow: (id, data) => request.post(`/crm/leads/${id}/follow`, data), + assign: (id, data) => request.put(`/crm/leads/${id}/assign`, data) +} + +// ─── Customers ──────────────────────────────────────────────────────────────── +export const customerApi = { + getList: (params) => request.get('/crm/customers', { params }), + getDetail: (id) => request.get(`/crm/customers/${id}`), + create: (data) => request.post('/crm/customers', data), + update: (id, data) => request.put(`/crm/customers/${id}`, data), + delete: (id) => request.delete(`/crm/customers/${id}`) +} + +// ─── Contracts ──────────────────────────────────────────────────────────────── +export const contractApi = { + getList: (params) => request.get('/crm/contracts', { params }), + getDetail: (id) => request.get(`/crm/contracts/${id}`), + create: (data) => request.post('/crm/contracts', data), + update: (id, data) => request.put(`/crm/contracts/${id}`, data), + delete: (id) => request.delete(`/crm/contracts/${id}`), + audit: (id, data) => request.put(`/crm/contracts/${id}/audit`, data) +} + +// ─── Questionnaires ─────────────────────────────────────────────────────────── +export const questionnaireApi = { + getList: (params) => request.get('/crm/questionnaires', { params }), + getDetail: (id) => request.get(`/crm/questionnaires/${id}`), + create: (data) => request.post('/crm/questionnaires', data), + update: (id, data) => request.put(`/crm/questionnaires/${id}`, data), + delete: (id) => request.delete(`/crm/questionnaires/${id}`), + submitAnswer: (data) => request.post('/crm/questionnaire-answers', data), + getAnswers: (params) => request.get('/crm/questionnaire-answers', { params }) +} + +// ─── Complaints ─────────────────────────────────────────────────────────────── +export const complaintApi = { + getList: (params) => request.get('/crm/complaints', { params }), + getDetail: (id) => request.get(`/crm/complaints/${id}`), + create: (data) => request.post('/crm/complaints', data), + update: (id, data) => request.put(`/crm/complaints/${id}`, data), + delete: (id) => request.delete(`/crm/complaints/${id}`), + handle: (id, data) => request.put(`/crm/complaints/${id}/handle`, data) +} diff --git a/frontend/src/api/meal.js b/frontend/src/api/meal.js new file mode 100644 index 0000000..e17aee0 --- /dev/null +++ b/frontend/src/api/meal.js @@ -0,0 +1,36 @@ +import request from '@/utils/request.js' + +// ─── Dishes ─────────────────────────────────────────────────────────────────── +export const dishApi = { + getList: (params) => request.get('/meal/dishes', { params }), + getDetail: (id) => request.get(`/meal/dishes/${id}`), + create: (data) => request.post('/meal/dishes', data), + update: (id, data) => request.put(`/meal/dishes/${id}`, data), + delete: (id) => request.delete(`/meal/dishes/${id}`) +} + +// ─── Meal Plan Templates ────────────────────────────────────────────────────── +export const mealTemplateApi = { + getList: (params) => request.get('/meal/templates', { params }), + getDetail: (id) => request.get(`/meal/templates/${id}`), + create: (data) => request.post('/meal/templates', data), + update: (id, data) => request.put(`/meal/templates/${id}`, data), + delete: (id) => request.delete(`/meal/templates/${id}`) +} + +// ─── Daily Meal Plans ───────────────────────────────────────────────────────── +export const dailyMealPlanApi = { + getList: (params) => request.get('/meal/daily-plans', { params }), + getDetail: (id) => request.get(`/meal/daily-plans/${id}`), + create: (data) => request.post('/meal/daily-plans', data), + update: (id, data) => request.put(`/meal/daily-plans/${id}`, data), + delete: (id) => request.delete(`/meal/daily-plans/${id}`), + deliver: (id) => request.put(`/meal/daily-plans/${id}/deliver`), + updateStatus: (id, data) => request.put(`/meal/daily-plans/${id}/status`, data) +} + +// ─── Meal Reviews ───────────────────────────────────────────────────────────── +export const mealReviewApi = { + getList: (params) => request.get('/meal/reviews', { params }), + create: (data) => request.post('/meal/reviews', data) +} diff --git a/frontend/src/api/room.js b/frontend/src/api/room.js new file mode 100644 index 0000000..7e264c4 --- /dev/null +++ b/frontend/src/api/room.js @@ -0,0 +1,47 @@ +import request from '@/utils/request.js' + +// ─── Room Types ─────────────────────────────────────────────────────────────── +export const roomTypeApi = { + getList: (params) => request.get('/room/room-types', { params }), + getDetail: (id) => request.get(`/room/room-types/${id}`), + create: (data) => request.post('/room/room-types', data), + update: (id, data) => request.put(`/room/room-types/${id}`, data), + delete: (id) => request.delete(`/room/room-types/${id}`) +} + +// ─── Rooms ──────────────────────────────────────────────────────────────────── +export const roomApi = { + getList: (params) => request.get('/room/rooms', { params }), + getDetail: (id) => request.get(`/room/rooms/${id}`), + create: (data) => request.post('/room/rooms', data), + update: (id, data) => request.put(`/room/rooms/${id}`, data), + delete: (id) => request.delete(`/room/rooms/${id}`), + updateStatus: (id, data) => request.put(`/room/rooms/${id}/status`, data) +} + +// ─── Reservations ───────────────────────────────────────────────────────────── +export const reservationApi = { + getList: (params) => request.get('/room/reservations', { params }), + getDetail: (id) => request.get(`/room/reservations/${id}`), + create: (data) => request.post('/room/reservations', data), + update: (id, data) => request.put(`/room/reservations/${id}`, data), + delete: (id) => request.delete(`/room/reservations/${id}`), + checkIn: (id) => request.put(`/room/reservations/${id}/check-in`), + checkOut: (id) => request.put(`/room/reservations/${id}/check-out`), + cancel: (id, data) => request.put(`/room/reservations/${id}/cancel`, data) +} + +// ─── Customer Outings ───────────────────────────────────────────────────────── +export const customerOutingApi = { + getList: (params) => request.get('/room/customer-outings', { params }), + getDetail: (id) => request.get(`/room/customer-outings/${id}`), + create: (data) => request.post('/room/customer-outings', data), + recordBack: (id, data) => request.put(`/room/customer-outings/${id}/back`, data) +} + +// ─── Call Records ───────────────────────────────────────────────────────────── +export const callRecordApi = { + getList: (params) => request.get('/room/call-records', { params }), + create: (data) => request.post('/room/call-records', data), + handle: (id, data) => request.put(`/room/call-records/${id}/handle`, data) +} diff --git a/frontend/src/views/care/plans/index.vue b/frontend/src/views/care/plans/index.vue new file mode 100644 index 0000000..adef4ba --- /dev/null +++ b/frontend/src/views/care/plans/index.vue @@ -0,0 +1,85 @@ + + + diff --git a/frontend/src/views/care/profiles/index.vue b/frontend/src/views/care/profiles/index.vue new file mode 100644 index 0000000..e572992 --- /dev/null +++ b/frontend/src/views/care/profiles/index.vue @@ -0,0 +1,136 @@ + + + diff --git a/frontend/src/views/care/records/index.vue b/frontend/src/views/care/records/index.vue new file mode 100644 index 0000000..dcca480 --- /dev/null +++ b/frontend/src/views/care/records/index.vue @@ -0,0 +1,76 @@ + + + diff --git a/frontend/src/views/crm/channels/index.vue b/frontend/src/views/crm/channels/index.vue new file mode 100644 index 0000000..f9c876c --- /dev/null +++ b/frontend/src/views/crm/channels/index.vue @@ -0,0 +1,94 @@ + + + diff --git a/frontend/src/views/crm/complaints/index.vue b/frontend/src/views/crm/complaints/index.vue new file mode 100644 index 0000000..18130a6 --- /dev/null +++ b/frontend/src/views/crm/complaints/index.vue @@ -0,0 +1,116 @@ + + + diff --git a/frontend/src/views/crm/contracts/index.vue b/frontend/src/views/crm/contracts/index.vue new file mode 100644 index 0000000..8c93411 --- /dev/null +++ b/frontend/src/views/crm/contracts/index.vue @@ -0,0 +1,184 @@ + + + diff --git a/frontend/src/views/crm/customers/index.vue b/frontend/src/views/crm/customers/index.vue new file mode 100644 index 0000000..968bfc0 --- /dev/null +++ b/frontend/src/views/crm/customers/index.vue @@ -0,0 +1,190 @@ + + + diff --git a/frontend/src/views/crm/leads/index.vue b/frontend/src/views/crm/leads/index.vue new file mode 100644 index 0000000..90062b5 --- /dev/null +++ b/frontend/src/views/crm/leads/index.vue @@ -0,0 +1,251 @@ + + + diff --git a/frontend/src/views/meal/daily-plans/index.vue b/frontend/src/views/meal/daily-plans/index.vue new file mode 100644 index 0000000..880696a --- /dev/null +++ b/frontend/src/views/meal/daily-plans/index.vue @@ -0,0 +1,104 @@ + + + diff --git a/frontend/src/views/meal/dishes/index.vue b/frontend/src/views/meal/dishes/index.vue new file mode 100644 index 0000000..cf38995 --- /dev/null +++ b/frontend/src/views/meal/dishes/index.vue @@ -0,0 +1,90 @@ + + + diff --git a/frontend/src/views/room/reservations/index.vue b/frontend/src/views/room/reservations/index.vue new file mode 100644 index 0000000..20ff6c9 --- /dev/null +++ b/frontend/src/views/room/reservations/index.vue @@ -0,0 +1,122 @@ + + + diff --git a/frontend/src/views/room/room-types/index.vue b/frontend/src/views/room/room-types/index.vue new file mode 100644 index 0000000..03aa264 --- /dev/null +++ b/frontend/src/views/room/room-types/index.vue @@ -0,0 +1,92 @@ + + + diff --git a/frontend/src/views/room/rooms/index.vue b/frontend/src/views/room/rooms/index.vue new file mode 100644 index 0000000..62fd72e --- /dev/null +++ b/frontend/src/views/room/rooms/index.vue @@ -0,0 +1,119 @@ + + +