You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
330 lines
13 KiB
330 lines
13 KiB
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Activity;
|
|
use App\Models\ActivitySignup;
|
|
use App\Models\Course;
|
|
use App\Models\CourseSignup;
|
|
use App\Models\Demand;
|
|
use App\Models\DictItem;
|
|
use App\Models\DictType;
|
|
use App\Models\Paper;
|
|
use App\Models\Teacher;
|
|
use App\Models\University;
|
|
use App\Support\ApiResponse;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\JsonResponse;
|
|
class DashboardController extends Controller
|
|
{
|
|
use ApiResponse;
|
|
|
|
public function overview(): JsonResponse
|
|
{
|
|
$today = Carbon::today();
|
|
$monthStart = $today->copy()->startOfMonth();
|
|
$monthEnd = $today->copy()->endOfMonth();
|
|
|
|
$universityQuery = University::query()->where('status', 1);
|
|
$universityTotal = (clone $universityQuery)->count();
|
|
$yangtzeCount = (clone $universityQuery)
|
|
->where(function ($q) {
|
|
$q->whereIn('province', ['上海', '江苏', '浙江'])
|
|
->orWhereIn('city', ['上海', '苏州', '南京', '杭州', '无锡', '宁波']);
|
|
})
|
|
->count();
|
|
|
|
$highValueUniversities = University::query()
|
|
->where('status', 1)
|
|
->whereHas('teachers', function ($q) {
|
|
$q->whereHas('starLevelItem', fn ($sq) => $sq->where('value', '>=', '4'));
|
|
})
|
|
->count();
|
|
|
|
$teacherQuery = Teacher::query();
|
|
$teacherTotal = (clone $teacherQuery)->count();
|
|
$monthNewTeachers = (clone $teacherQuery)
|
|
->whereBetween('created_at', [$monthStart, $monthEnd->endOfDay()])
|
|
->count();
|
|
|
|
$fiveStarTeachers = (clone $teacherQuery)
|
|
->whereHas('starLevelItem', fn ($q) => $q->where('value', '5'))
|
|
->count();
|
|
|
|
$paperTotal = Paper::query()->count();
|
|
$monthNewPapers = Paper::query()
|
|
->whereBetween('created_at', [$monthStart, $monthEnd->endOfDay()])
|
|
->count();
|
|
$pendingLinkPapers = Paper::query()->pendingTeacherLink()->count();
|
|
|
|
$demandTotal = Demand::query()->count();
|
|
$doneStatusId = $this->demandStatusId('done');
|
|
$activeStatusId = $this->demandStatusId('active');
|
|
$demandDone = $doneStatusId
|
|
? Demand::query()->where('status_dict_item_id', $doneStatusId)->count()
|
|
: 0;
|
|
$demandActive = $activeStatusId
|
|
? Demand::query()->where('status_dict_item_id', $activeStatusId)->count()
|
|
: 0;
|
|
$fulfillmentRate = $demandTotal > 0
|
|
? (int) round($demandDone / $demandTotal * 100)
|
|
: 0;
|
|
|
|
$teacherFollowTodo = Teacher::query()
|
|
->where('is_partner', false)
|
|
->whereNotNull('next_follow_date')
|
|
->where('next_follow_date', '<', $today->toDateString())
|
|
->whereHas('starLevelItem', fn ($q) => $q->whereIn('value', ['4', '5']))
|
|
->count();
|
|
|
|
$demandOverdue = $activeStatusId
|
|
? Demand::query()
|
|
->where('status_dict_item_id', $activeStatusId)
|
|
->where('submitted_at', '<', $today->copy()->subDays(7))
|
|
->count()
|
|
: 0;
|
|
$demandWaiting = $demandActive;
|
|
$demandTodo = $demandOverdue + min($demandWaiting, max(0, $demandWaiting - $demandOverdue));
|
|
|
|
$paperTodo = Paper::query()
|
|
->pendingTeacherLink()
|
|
->where('created_at', '>=', $today->copy()->subDays(30))
|
|
->count();
|
|
|
|
$todoTotal = $teacherFollowTodo + $demandTodo + $paperTodo;
|
|
|
|
$courseSessions = $this->countCoursesScheduledInMonth($monthStart, $monthEnd);
|
|
$courseDraft = Course::query()->where('published', 0)->count();
|
|
$activitySessions = $this->countActivitiesScheduledInMonth($monthStart, $monthEnd);
|
|
$activityOpen = Activity::query()
|
|
->where('published', 1)
|
|
->where('signup_end_date', '>=', $today->toDateString())
|
|
->count();
|
|
|
|
$signupTotal = CourseSignup::query()->count() + ActivitySignup::query()->count();
|
|
$lastMonthSignup = CourseSignup::query()
|
|
->whereBetween('created_at', [$monthStart->copy()->subMonth(), $monthStart->copy()->subDay()->endOfDay()])
|
|
->count()
|
|
+ ActivitySignup::query()
|
|
->whereBetween('created_at', [$monthStart->copy()->subMonth(), $monthStart->copy()->subDay()->endOfDay()])
|
|
->count();
|
|
$monthSignup = CourseSignup::query()
|
|
->whereBetween('created_at', [$monthStart, $monthEnd->endOfDay()])
|
|
->count()
|
|
+ ActivitySignup::query()
|
|
->whereBetween('created_at', [$monthStart, $monthEnd->endOfDay()])
|
|
->count();
|
|
$signupDelta = $monthSignup - $lastMonthSignup;
|
|
|
|
$teacherLeads = Teacher::query()
|
|
->whereBetween('created_at', [$monthStart, $monthEnd->endOfDay()])
|
|
->count();
|
|
$demandLeads = Demand::query()
|
|
->whereBetween('created_at', [$monthStart, $monthEnd->endOfDay()])
|
|
->count();
|
|
|
|
$calendarEvents = $this->buildCalendarEvents($monthStart, $monthEnd);
|
|
|
|
return $this->ok([
|
|
'overview' => [
|
|
'universities' => [
|
|
'total' => $universityTotal,
|
|
'yangtze' => $yangtzeCount,
|
|
'high_value' => $highValueUniversities,
|
|
],
|
|
'teachers' => [
|
|
'total' => $teacherTotal,
|
|
'month_new' => $monthNewTeachers,
|
|
'five_star' => $fiveStarTeachers,
|
|
],
|
|
'papers' => [
|
|
'total' => $paperTotal,
|
|
'month_new' => $monthNewPapers,
|
|
'pending_link' => $pendingLinkPapers,
|
|
],
|
|
'demands' => [
|
|
'total' => $demandTotal,
|
|
'done' => $demandDone,
|
|
'active' => $demandActive,
|
|
'fulfillment_rate' => $fulfillmentRate,
|
|
],
|
|
],
|
|
'todos' => [
|
|
'total' => $todoTotal,
|
|
'teacher_follow' => $teacherFollowTodo,
|
|
'demand_process' => $demandTodo,
|
|
'demand_overdue' => $demandOverdue,
|
|
'demand_waiting' => $demandWaiting,
|
|
'paper_data' => $paperTodo,
|
|
],
|
|
'events' => [
|
|
'course_sessions' => $courseSessions,
|
|
'course_draft' => $courseDraft,
|
|
'activity_sessions' => $activitySessions,
|
|
'activity_open' => $activityOpen,
|
|
'signup_total' => $signupTotal,
|
|
'signup_delta' => $signupDelta,
|
|
'teacher_leads' => $teacherLeads,
|
|
'demand_leads' => $demandLeads,
|
|
],
|
|
'calendar' => [
|
|
'year' => (int) $today->year,
|
|
'month' => (int) $today->month,
|
|
'today' => $today->toDateString(),
|
|
'events' => $calendarEvents,
|
|
],
|
|
]);
|
|
}
|
|
|
|
protected function demandStatusId(string $value): ?int
|
|
{
|
|
$typeId = DictType::query()->where('code', 'demand_status')->value('id');
|
|
if (! $typeId) {
|
|
return null;
|
|
}
|
|
|
|
return DictItem::query()
|
|
->where('dict_type_id', $typeId)
|
|
->where('value', $value)
|
|
->where('status', 1)
|
|
->value('id');
|
|
}
|
|
|
|
/**
|
|
* @return list<array{start_date:string,end_date:string,type:string,title:string,entity_id:int,route:string}>
|
|
*/
|
|
protected function buildCalendarEvents(Carbon $monthStart, Carbon $monthEnd): array
|
|
{
|
|
$events = [];
|
|
$seen = [];
|
|
|
|
$pushRange = function (
|
|
?Carbon $start,
|
|
?Carbon $end,
|
|
string $type,
|
|
string $title,
|
|
int $entityId,
|
|
string $route,
|
|
) use (&$events, &$seen, $monthStart, $monthEnd): void {
|
|
if (! $start) {
|
|
return;
|
|
}
|
|
|
|
$title = $this->shortCalendarTitle($title);
|
|
if ($title === '') {
|
|
return;
|
|
}
|
|
|
|
$rangeStart = $start->copy()->startOfDay()->max($monthStart->copy()->startOfDay())->toDateString();
|
|
$rangeEnd = ($end ?? $start)->copy()->startOfDay()->min($monthEnd->copy()->startOfDay())->toDateString();
|
|
if ($rangeStart > $rangeEnd) {
|
|
return;
|
|
}
|
|
|
|
$key = "{$type}|{$entityId}|{$title}|{$rangeStart}|{$rangeEnd}";
|
|
if (isset($seen[$key])) {
|
|
return;
|
|
}
|
|
$seen[$key] = true;
|
|
|
|
$events[] = [
|
|
'start_date' => $rangeStart,
|
|
'end_date' => $rangeEnd,
|
|
'type' => $type,
|
|
'title' => $title,
|
|
'entity_id' => $entityId,
|
|
'route' => $route,
|
|
];
|
|
};
|
|
|
|
Course::query()
|
|
->whereNotNull('teach_start_date')
|
|
->where(function ($q) use ($monthStart, $monthEnd) {
|
|
$q->whereBetween('teach_start_date', [$monthStart->toDateString(), $monthEnd->toDateString()])
|
|
->orWhereBetween('teach_end_date', [$monthStart->toDateString(), $monthEnd->toDateString()])
|
|
->orWhere(function ($q2) use ($monthStart, $monthEnd) {
|
|
$q2->where('teach_start_date', '<=', $monthStart->toDateString())
|
|
->where('teach_end_date', '>=', $monthEnd->toDateString());
|
|
});
|
|
})
|
|
->get(['id', 'title', 'teach_start_date', 'teach_end_date'])
|
|
->each(function (Course $course) use ($pushRange) {
|
|
$pushRange(
|
|
$course->teach_start_date,
|
|
$course->teach_end_date ?? $course->teach_start_date,
|
|
'course',
|
|
$course->title,
|
|
(int) $course->id,
|
|
'/courses',
|
|
);
|
|
});
|
|
|
|
Activity::query()
|
|
->where(function ($q) use ($monthStart, $monthEnd) {
|
|
$q->whereBetween('event_start_date', [$monthStart->toDateString(), $monthEnd->toDateString()])
|
|
->orWhereBetween('event_end_date', [$monthStart->toDateString(), $monthEnd->toDateString()])
|
|
->orWhere(function ($q2) use ($monthStart, $monthEnd) {
|
|
$q2->where('event_start_date', '<=', $monthStart->toDateString())
|
|
->where('event_end_date', '>=', $monthEnd->toDateString());
|
|
});
|
|
})
|
|
->whereNotNull('event_start_date')
|
|
->get(['id', 'title', 'event_start_date', 'event_end_date'])
|
|
->each(function (Activity $activity) use ($pushRange) {
|
|
$pushRange(
|
|
$activity->event_start_date,
|
|
$activity->event_end_date ?? $activity->event_start_date,
|
|
'activity',
|
|
$activity->title,
|
|
(int) $activity->id,
|
|
'/activities',
|
|
);
|
|
});
|
|
|
|
usort(
|
|
$events,
|
|
fn ($a, $b) => [$a['start_date'], $a['type'], $a['title']] <=> [$b['start_date'], $b['type'], $b['title']],
|
|
);
|
|
|
|
return $events;
|
|
}
|
|
|
|
/** 当月有排期的课程数量(按课程去重,非排期天数) */
|
|
protected function countCoursesScheduledInMonth(Carbon $monthStart, Carbon $monthEnd): int
|
|
{
|
|
return Course::query()
|
|
->whereNotNull('teach_start_date')
|
|
->where(function ($q) use ($monthStart, $monthEnd) {
|
|
$q->whereBetween('teach_start_date', [$monthStart->toDateString(), $monthEnd->toDateString()])
|
|
->orWhereBetween('teach_end_date', [$monthStart->toDateString(), $monthEnd->toDateString()])
|
|
->orWhere(function ($q2) use ($monthStart, $monthEnd) {
|
|
$q2->where('teach_start_date', '<=', $monthStart->toDateString())
|
|
->where('teach_end_date', '>=', $monthEnd->toDateString());
|
|
});
|
|
})
|
|
->count();
|
|
}
|
|
|
|
/** 当月有排期的活动数量(按活动去重,非排期天数) */
|
|
protected function countActivitiesScheduledInMonth(Carbon $monthStart, Carbon $monthEnd): int
|
|
{
|
|
return Activity::query()
|
|
->whereNotNull('event_start_date')
|
|
->where(function ($q) use ($monthStart, $monthEnd) {
|
|
$q->whereBetween('event_start_date', [$monthStart->toDateString(), $monthEnd->toDateString()])
|
|
->orWhereBetween('event_end_date', [$monthStart->toDateString(), $monthEnd->toDateString()])
|
|
->orWhere(function ($q2) use ($monthStart, $monthEnd) {
|
|
$q2->where('event_start_date', '<=', $monthStart->toDateString())
|
|
->where('event_end_date', '>=', $monthEnd->toDateString());
|
|
});
|
|
})
|
|
->count();
|
|
}
|
|
|
|
protected function shortCalendarTitle(string $title): string
|
|
{
|
|
return trim($title);
|
|
}
|
|
}
|