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.
523 lines
21 KiB
523 lines
21 KiB
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Activity;
|
|
use App\Models\ActivitySession;
|
|
use App\Models\ActivitySignup;
|
|
use App\Models\Course;
|
|
use App\Models\CourseCheckinDay;
|
|
use App\Models\CourseSession;
|
|
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()
|
|
->where(function ($q) {
|
|
$q->whereNull('university_id')
|
|
->orDoesntHave('teachers');
|
|
})
|
|
->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()
|
|
->where(function ($q) {
|
|
$q->whereNull('university_id')
|
|
->orDoesntHave('teachers');
|
|
})
|
|
->where('created_at', '>=', $today->copy()->subDays(30))
|
|
->count();
|
|
|
|
$todoTotal = $teacherFollowTodo + $demandTodo + $paperTodo;
|
|
|
|
$courseSessions = $this->countCourseScheduleDaysInMonth($monthStart, $monthEnd);
|
|
$courseDraft = Course::query()->where('published', false)->count();
|
|
$activitySessions = $this->countActivityScheduleDaysInMonth($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{date:string,type:string,title:string,entity_id:int,route:string}>
|
|
*/
|
|
protected function buildCalendarEvents(Carbon $monthStart, Carbon $monthEnd): array
|
|
{
|
|
$events = [];
|
|
$seen = [];
|
|
/** @var array<int, array<string, true>> */
|
|
$courseScheduledDates = [];
|
|
/** @var array<int, array<string, true>> */
|
|
$activityScheduledDates = [];
|
|
|
|
$push = function (
|
|
string $date,
|
|
string $type,
|
|
string $title,
|
|
int $entityId,
|
|
string $route,
|
|
) use (&$events, &$seen): void {
|
|
$title = $this->shortCalendarTitle($title);
|
|
if ($title === '') {
|
|
return;
|
|
}
|
|
$key = "{$date}|{$type}|{$entityId}|{$title}";
|
|
if (isset($seen[$key])) {
|
|
return;
|
|
}
|
|
$seen[$key] = true;
|
|
$events[] = [
|
|
'date' => $date,
|
|
'type' => $type,
|
|
'title' => $title,
|
|
'entity_id' => $entityId,
|
|
'route' => $route,
|
|
];
|
|
};
|
|
|
|
$markCourseDate = function (int $courseId, string $date) use (&$courseScheduledDates): void {
|
|
$courseScheduledDates[$courseId][$date] = true;
|
|
};
|
|
|
|
$markActivityDate = function (int $activityId, string $date) use (&$activityScheduledDates): void {
|
|
$activityScheduledDates[$activityId][$date] = true;
|
|
};
|
|
|
|
$hasCourseDate = function (int $courseId, string $date) use (&$courseScheduledDates): bool {
|
|
return isset($courseScheduledDates[$courseId][$date]);
|
|
};
|
|
|
|
$hasActivityDate = function (int $activityId, string $date) use (&$activityScheduledDates): bool {
|
|
return isset($activityScheduledDates[$activityId][$date]);
|
|
};
|
|
|
|
CourseSession::query()
|
|
->with('course:id,title')
|
|
->whereBetween('starts_at', [$monthStart, $monthEnd->endOfDay()])
|
|
->orderBy('starts_at')
|
|
->get()
|
|
->each(function (CourseSession $session) use ($push, $markCourseDate) {
|
|
if (! $session->starts_at || ! $session->course) {
|
|
return;
|
|
}
|
|
$date = $session->starts_at->toDateString();
|
|
$markCourseDate((int) $session->course_id, $date);
|
|
$push(
|
|
$date,
|
|
'course',
|
|
$session->title ?: $session->course->title,
|
|
(int) $session->course_id,
|
|
'/courses',
|
|
);
|
|
});
|
|
|
|
CourseCheckinDay::query()
|
|
->with('course:id,title')
|
|
->whereBetween('teach_date', [$monthStart->toDateString(), $monthEnd->toDateString()])
|
|
->orderBy('teach_date')
|
|
->get()
|
|
->each(function (CourseCheckinDay $day) use ($push, $markCourseDate) {
|
|
if (! $day->course) {
|
|
return;
|
|
}
|
|
$date = $day->teach_date->toDateString();
|
|
$markCourseDate((int) $day->course_id, $date);
|
|
$push(
|
|
$date,
|
|
'course',
|
|
$day->course->title,
|
|
(int) $day->course_id,
|
|
'/courses',
|
|
);
|
|
});
|
|
|
|
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 ($push, $monthStart, $monthEnd) {
|
|
foreach ($this->eachDateInMonthRange(
|
|
$course->teach_start_date,
|
|
$course->teach_end_date ?? $course->teach_start_date,
|
|
$monthStart,
|
|
$monthEnd,
|
|
) as $date) {
|
|
$push($date, 'course', $course->title, (int) $course->id, '/courses');
|
|
}
|
|
});
|
|
|
|
ActivitySession::query()
|
|
->with('activity:id,title')
|
|
->whereBetween('starts_at', [$monthStart, $monthEnd->endOfDay()])
|
|
->orderBy('starts_at')
|
|
->get()
|
|
->each(function (ActivitySession $session) use ($push, $markActivityDate) {
|
|
if (! $session->starts_at || ! $session->activity) {
|
|
return;
|
|
}
|
|
$date = $session->starts_at->toDateString();
|
|
$activityTitle = $session->activity->title;
|
|
$sessionLabel = trim((string) $session->title) !== '' ? $session->title : '场次';
|
|
$markActivityDate((int) $session->activity_id, $date);
|
|
$push(
|
|
$date,
|
|
'activity',
|
|
$activityTitle.'·'.$sessionLabel,
|
|
(int) $session->activity_id,
|
|
'/activities',
|
|
);
|
|
});
|
|
|
|
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()])
|
|
->orWhereBetween('signup_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());
|
|
});
|
|
})
|
|
->get(['id', 'title', 'event_start_date', 'event_end_date', 'signup_end_date'])
|
|
->each(function (Activity $activity) use ($push, $monthStart, $monthEnd, $hasActivityDate) {
|
|
if ($activity->event_start_date) {
|
|
foreach ($this->eachDateInMonthRange(
|
|
$activity->event_start_date,
|
|
$activity->event_end_date ?? $activity->event_start_date,
|
|
$monthStart,
|
|
$monthEnd,
|
|
) as $date) {
|
|
if ($hasActivityDate((int) $activity->id, $date)) {
|
|
continue;
|
|
}
|
|
$push($date, 'activity', $activity->title, (int) $activity->id, '/activities');
|
|
}
|
|
}
|
|
if ($activity->signup_end_date
|
|
&& $activity->signup_end_date->gte($monthStart)
|
|
&& $activity->signup_end_date->lte($monthEnd)) {
|
|
$push(
|
|
$activity->signup_end_date->toDateString(),
|
|
'activity',
|
|
$activity->title.'·报名截止',
|
|
(int) $activity->id,
|
|
'/activities',
|
|
);
|
|
}
|
|
});
|
|
|
|
usort($events, fn ($a, $b) => [$a['date'], $a['type'], $a['title']] <=> [$b['date'], $b['type'], $b['title']]);
|
|
|
|
return $events;
|
|
}
|
|
|
|
protected function countCourseScheduleDaysInMonth(Carbon $monthStart, Carbon $monthEnd): int
|
|
{
|
|
$dates = [];
|
|
|
|
CourseSession::query()
|
|
->whereBetween('starts_at', [$monthStart, $monthEnd->endOfDay()])
|
|
->get(['starts_at'])
|
|
->each(function (CourseSession $s) use (&$dates) {
|
|
if ($s->starts_at) {
|
|
$dates[$s->starts_at->toDateString()] = true;
|
|
}
|
|
});
|
|
|
|
CourseCheckinDay::query()
|
|
->whereBetween('teach_date', [$monthStart->toDateString(), $monthEnd->toDateString()])
|
|
->get(['teach_date'])
|
|
->each(function (CourseCheckinDay $d) use (&$dates) {
|
|
$dates[$d->teach_date->toDateString()] = true;
|
|
});
|
|
|
|
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(['teach_start_date', 'teach_end_date'])
|
|
->each(function (Course $c) use (&$dates, $monthStart, $monthEnd) {
|
|
foreach ($this->eachDateInMonthRange(
|
|
$c->teach_start_date,
|
|
$c->teach_end_date ?? $c->teach_start_date,
|
|
$monthStart,
|
|
$monthEnd,
|
|
) as $date) {
|
|
$dates[$date] = true;
|
|
}
|
|
});
|
|
|
|
return count($dates);
|
|
}
|
|
|
|
protected function countActivityScheduleDaysInMonth(Carbon $monthStart, Carbon $monthEnd): int
|
|
{
|
|
$dates = [];
|
|
/** @var array<int, array<string, true>> */
|
|
$activityScheduledDates = [];
|
|
|
|
ActivitySession::query()
|
|
->whereBetween('starts_at', [$monthStart, $monthEnd->endOfDay()])
|
|
->get(['activity_id', 'starts_at'])
|
|
->each(function (ActivitySession $s) use (&$dates, &$activityScheduledDates) {
|
|
if (! $s->starts_at) {
|
|
return;
|
|
}
|
|
$date = $s->starts_at->toDateString();
|
|
$dates[$date] = true;
|
|
$activityScheduledDates[(int) $s->activity_id][$date] = true;
|
|
});
|
|
|
|
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()])
|
|
->orWhereBetween('signup_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());
|
|
});
|
|
})
|
|
->get(['id', 'event_start_date', 'event_end_date', 'signup_end_date'])
|
|
->each(function (Activity $a) use (&$dates, &$activityScheduledDates, $monthStart, $monthEnd) {
|
|
if ($a->event_start_date) {
|
|
foreach ($this->eachDateInMonthRange(
|
|
$a->event_start_date,
|
|
$a->event_end_date ?? $a->event_start_date,
|
|
$monthStart,
|
|
$monthEnd,
|
|
) as $date) {
|
|
if (isset($activityScheduledDates[(int) $a->id][$date])) {
|
|
continue;
|
|
}
|
|
$dates[$date] = true;
|
|
}
|
|
}
|
|
if ($a->signup_end_date
|
|
&& $a->signup_end_date->gte($monthStart)
|
|
&& $a->signup_end_date->lte($monthEnd)) {
|
|
$dates[$a->signup_end_date->toDateString()] = true;
|
|
}
|
|
});
|
|
|
|
return count($dates);
|
|
}
|
|
|
|
/**
|
|
* @return list<string> Y-m-d
|
|
*/
|
|
protected function eachDateInMonthRange(
|
|
?Carbon $start,
|
|
?Carbon $end,
|
|
Carbon $monthStart,
|
|
Carbon $monthEnd,
|
|
): array {
|
|
if (! $start) {
|
|
return [];
|
|
}
|
|
|
|
$from = $start->copy()->startOfDay()->max($monthStart->copy()->startOfDay());
|
|
$to = ($end ?? $start)->copy()->startOfDay()->min($monthEnd->copy()->startOfDay());
|
|
if ($from->gt($to)) {
|
|
return [];
|
|
}
|
|
|
|
$dates = [];
|
|
for ($cursor = $from->copy(); $cursor->lte($to); $cursor->addDay()) {
|
|
$dates[] = $cursor->toDateString();
|
|
if (count($dates) >= 31) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $dates;
|
|
}
|
|
|
|
protected function shortCalendarTitle(string $title, int $max = 20): string
|
|
{
|
|
$title = trim($title);
|
|
if ($title === '') {
|
|
return '';
|
|
}
|
|
|
|
return mb_strlen($title) > $max ? mb_substr($title, 0, $max) : $title;
|
|
}
|
|
}
|