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

2 weeks ago
<?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();
6 days ago
$pendingLinkPapers = Paper::query()->pendingTeacherLink()->count();
2 weeks ago
$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()
6 days ago
->pendingTeacherLink()
2 weeks ago
->where('created_at', '>=', $today->copy()->subDays(30))
->count();
$todoTotal = $teacherFollowTodo + $demandTodo + $paperTodo;
6 days ago
$courseSessions = $this->countCoursesScheduledInMonth($monthStart, $monthEnd);
$courseDraft = Course::query()->where('published', 0)->count();
$activitySessions = $this->countActivitiesScheduledInMonth($monthStart, $monthEnd);
2 weeks ago
$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');
}
/**
6 days ago
* @return list<array{start_date:string,end_date:string,type:string,title:string,entity_id:int,route:string}>
2 weeks ago
*/
protected function buildCalendarEvents(Carbon $monthStart, Carbon $monthEnd): array
{
$events = [];
$seen = [];
6 days ago
$pushRange = function (
?Carbon $start,
?Carbon $end,
2 weeks ago
string $type,
string $title,
int $entityId,
string $route,
6 days ago
) use (&$events, &$seen, $monthStart, $monthEnd): void {
if (! $start) {
return;
}
2 weeks ago
$title = $this->shortCalendarTitle($title);
if ($title === '') {
return;
}
6 days ago
$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}";
2 weeks ago
if (isset($seen[$key])) {
return;
}
$seen[$key] = true;
6 days ago
2 weeks ago
$events[] = [
6 days ago
'start_date' => $rangeStart,
'end_date' => $rangeEnd,
2 weeks ago
'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'])
6 days ago
->each(function (Course $course) use ($pushRange) {
$pushRange(
2 weeks ago
$course->teach_start_date,
$course->teach_end_date ?? $course->teach_start_date,
6 days ago
'course',
$course->title,
(int) $course->id,
'/courses',
2 weeks ago
);
});
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());
});
})
6 days ago
->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',
);
2 weeks ago
});
6 days ago
usort(
$events,
fn ($a, $b) => [$a['start_date'], $a['type'], $a['title']] <=> [$b['start_date'], $b['type'], $b['title']],
);
2 weeks ago
return $events;
}
6 days ago
/** 当月有排期的课程数量(按课程去重,非排期天数) */
protected function countCoursesScheduledInMonth(Carbon $monthStart, Carbon $monthEnd): int
2 weeks ago
{
6 days ago
return Course::query()
2 weeks ago
->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());
});
})
6 days ago
->count();
2 weeks ago
}
6 days ago
/** 当月有排期的活动数量(按活动去重,非排期天数) */
protected function countActivitiesScheduledInMonth(Carbon $monthStart, Carbon $monthEnd): int
2 weeks ago
{
6 days ago
return Activity::query()
->whereNotNull('event_start_date')
2 weeks ago
->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());
});
})
6 days ago
->count();
2 weeks ago
}
6 days ago
protected function shortCalendarTitle(string $title): string
2 weeks ago
{
6 days ago
return trim($title);
2 weeks ago
}
}