diff --git a/app/Helpers/Semester.php b/app/Helpers/Semester.php index 91fca00a..4bff7edd 100644 --- a/app/Helpers/Semester.php +++ b/app/Helpers/Semester.php @@ -129,4 +129,74 @@ public static function date(string $name, bool $start_of_season = true) return Carbon::parse($month_and_day . ' ' . $parsed['year']->year); } + + /** + * Sort chronologically an array of semesters in the 'Semester YY' format + * @param array $semesters + * @return array + */ + public static function sortCollectionWithSemestersKeyChronologically() { + return function ($a, $b) { + // Extract the year and season from both semesters + list($seasonA, $yearA) = explode(' ', $a); + list($seasonB, $yearB) = explode(' ', $b); + + // Define a mapping of seasons to values for sorting + $seasonOrder = [ + 'Spring' => 1, + 'Summer' => 2, + 'Fall' => 3, + 'Winter' => 4, + ]; + + // First, compare the years + if ($yearA != $yearB) { + return $yearA <=> $yearB; + } + + // If the years are the same, compare the seasons using the seasonOrder + return $seasonOrder[$seasonA] <=> $seasonOrder[$seasonB]; + }; + } + + /** + * Sort chronologically an array of semesters in the 'Semester YY' format + * @param array $semesters + * @return array + */ + public static function sortSemestersChronologically($semesters) { + return + usort($semesters, function ($a, $b) { + // Extract year and season + preg_match('/(Spring|Summer|Fall) (\d+)/', $a, $matchesA); + preg_match('/(Spring|Summer|Fall) (\d+)/', $b, $matchesB); + + // Map seasons to an order + $seasonOrder = ['Spring' => 1, 'Summer' => 2, 'Fall' => 3]; + + // Compare by year first + if ($matchesA[2] != $matchesB[2]) { + return $matchesA[2] - $matchesB[2]; + } + + // If years are the same, compare by season + return $seasonOrder[$matchesA[1]] - $seasonOrder[$matchesB[1]]; + }); + } + + /** + * Given a list of semesters, removes the ones that are in the future + */ + public static function removeFutureSemesters($semesters) + { + $current_semester = static::current(); + $current_semester_index = array_search($current_semester, $semesters); + + if ($current_semester_index !== false) { + return array_slice($semesters, 0, $current_semester_index + 1); + } + + return $semesters; + + } } diff --git a/app/Http/Controllers/InsightsController.php b/app/Http/Controllers/InsightsController.php new file mode 100644 index 00000000..3fa52c8c --- /dev/null +++ b/app/Http/Controllers/InsightsController.php @@ -0,0 +1,36 @@ +middleware('auth'); + + $this->middleware('can:viewAdminIndex,App\LogEntry')->only('index'); + } + + /** + * Display a listing of the resource. + */ + public function index(Request $request): ViewContract + { + + $schools_options = StudentDataInsight::getLabels('school')->toArray(); + $semesters_options = StudentDataInsight::getLabels('semester')->toArray(); + $semesters_selected = Semester::removeFutureSemesters($semesters_options); + $title = StudentDataInsight::convertParameterstoTitle($semesters_options, $schools_options); + + return view('insights.index', compact('schools_options', 'semesters_options', 'semesters_selected', 'title')); + } +} diff --git a/app/Http/Livewire/AcceptedAndFollowUpAppsPercentageChart.php b/app/Http/Livewire/AcceptedAndFollowUpAppsPercentageChart.php new file mode 100644 index 00000000..0c0fbddb --- /dev/null +++ b/app/Http/Livewire/AcceptedAndFollowUpAppsPercentageChart.php @@ -0,0 +1,55 @@ +weeks_before_semester_start = 4; + $this->weeks_before_semester_end = 4; + $this->filing_statuses_category_1 = ['accepted', 'follow up']; + $this->filing_statuses_category_2= ['not interested', 'maybe later']; + $this->data = $this->dataset; + $this->labels = ['Accepted & Follow Up', 'Other']; + } + + public function refreshChart5($data, $labels) {} + + public function refreshData5($selected_semesters, $selected_schools, $weeks_before_semester_start, $weeks_before_semester_end) + { + $this->weeks_before_semester_start = $weeks_before_semester_start; + $this->weeks_before_semester_end = $weeks_before_semester_end; + $this->selected_semesters = $selected_semesters; + $this->selected_schools = $selected_schools; + + $this->data = $this->dataset; + $this->emit('refreshChart5', $this->data, $this->labels); + } + + public function getDatasetProperty() + { + $report = new StudentDataInsight(); + return $report->appsCountForTwoCategoriesOfFilingStatus($this->selected_semesters, $this->selected_schools, $this->filing_statuses_category_1, $this->filing_statuses_category_2, $this->weeks_before_semester_start, $this->weeks_before_semester_end); + } + + public function render() + { + return view('livewire.charts.accepted-and-follow-up-apps-percentage-chart'); + } +} diff --git a/app/Http/Livewire/AcceptedStudentAppsPercentageChart.php b/app/Http/Livewire/AcceptedStudentAppsPercentageChart.php new file mode 100644 index 00000000..cb33e58f --- /dev/null +++ b/app/Http/Livewire/AcceptedStudentAppsPercentageChart.php @@ -0,0 +1,55 @@ +selected_semesters = [Semester::current()]; + //$this->selected_schools = $selected_schools; + // $this->selected_filing_statuses = ["accepted"]; + } + + public function refreshChart3($data, $labels) {} + + public function refreshData3($selected_semesters, $selected_schools) { + $this->selected_semesters = $selected_semesters; + $this->selected_schools = $selected_schools; + + $data = $this->getData(); + + $this->data = $data['datasets']; + $this->labels = $data['labels']; + + $this->emit('refreshChart3', $this->data, $this->labels); + } + + public function getData() + { + $report = new StudentDataInsight(); + return $report->transformDataAcceptedInCurrentSeason($this->selected_semesters, $this->selected_schools); + } + + public function render() + { + $data = $this->getData(); + $this->data = $data['datasets']; + $this->labels = $data['labels']; + + return view('livewire.accepted-student-apps-percentage-chart'); + } +} diff --git a/app/Http/Livewire/InsightsFilter.php b/app/Http/Livewire/InsightsFilter.php new file mode 100644 index 00000000..657a89a2 --- /dev/null +++ b/app/Http/Livewire/InsightsFilter.php @@ -0,0 +1,39 @@ +charts_loaded = true; + $this->current_semester = Semester::current(); + } + + public function applyFilters($selected_semesters, $selected_schools, $weeks_before_semester_start, $weeks_before_semester_end) { + $this->charts_loaded = false; + $this->title = StudentDataInsight::convertParameterstoTitle($selected_semesters, $selected_schools); + $this->emitTo('accepted-and-follow-up-apps-percentage-chart', 'refreshData5', $selected_semesters, $selected_schools, $weeks_before_semester_start, $weeks_before_semester_end); + $this->emitTo('student-apps-viewed-not-viewed-chart', 'refreshData4', $selected_semesters, $selected_schools, $weeks_before_semester_start, $weeks_before_semester_end); + $this->emitTo('students-app-count-chart', 'refreshData2', $selected_semesters, $selected_schools, $weeks_before_semester_start, $weeks_before_semester_end); + $this->emitTo('students-app-filing-status-chart', 'refreshData1', $selected_semesters, $selected_schools, $weeks_before_semester_start, $weeks_before_semester_end); + } + + public function render() + { + return view('livewire.charts.insights-filter'); + } +} diff --git a/app/Http/Livewire/StudentAppsViewedNotViewedChart.php b/app/Http/Livewire/StudentAppsViewedNotViewedChart.php new file mode 100644 index 00000000..38b46103 --- /dev/null +++ b/app/Http/Livewire/StudentAppsViewedNotViewedChart.php @@ -0,0 +1,50 @@ +dataset; + $this->data = $data['datasets']; + $this->labels = $data['labels']; + } + + public function refreshChart4($data, $labels) {} + + public function refreshData4($selected_semesters, $selected_schools) { + $this->selected_semesters = $selected_semesters; + $this->selected_schools = $selected_schools; + + $data = $this->dataset; + + $this->data = $data['datasets']; + $this->labels = $data['labels']; + + $this->emit('refreshChart4', $this->data, $this->labels); + } + + public function getDatasetProperty() + { + $report = new StudentDataInsight(); + return $report->appsCountViewedAndNotViewed($this->selected_semesters, $this->selected_schools); + } + + public function render() + { + return view('livewire.charts.student-apps-viewed-not-viewed-chart'); + } +} diff --git a/app/Http/Livewire/StudentsAppCountChart.php b/app/Http/Livewire/StudentsAppCountChart.php new file mode 100644 index 00000000..c61e59c9 --- /dev/null +++ b/app/Http/Livewire/StudentsAppCountChart.php @@ -0,0 +1,49 @@ +dataset; + $this->data = $data['datasets']; + $this->labels = $data['labels']; + } + + public function refreshChart2($data, $labels) {} + + public function refreshData2($selected_semesters, $selected_schools) { + + $this->selected_semesters = $selected_semesters; + $this->selected_schools = $selected_schools; + + $data = $this->dataset; + + $this->data = $data['datasets']; + $this->labels = $data['labels']; + + $this->emit('refreshChart2', $this->data, $this->labels); + } + + public function getDatasetProperty() + { + $report = new StudentDataInsight(); + return $report->appsCountBySemestersAndSchools($this->selected_semesters, $this->selected_schools); + } + + public function render() + { + return view('livewire.charts.students-app-count-chart'); + } +} diff --git a/app/Http/Livewire/StudentsAppFilingStatusChart.php b/app/Http/Livewire/StudentsAppFilingStatusChart.php new file mode 100644 index 00000000..5962a002 --- /dev/null +++ b/app/Http/Livewire/StudentsAppFilingStatusChart.php @@ -0,0 +1,58 @@ +weeks_before_semester_start = 4; + $this->weeks_before_semester_end = 4; + $this->selected_filing_statuses = ["accepted", "maybe later", "not interested", "new", "follow up"]; + + $data = $this->dataset; + $this->data = $data['datasets']; + $this->labels = $data['labels']; + } + + public function refreshChart1($data, $labels) {} + + public function refreshData1($selected_semesters, $selected_schools, $weeks_before_semester_start, $weeks_before_semester_end) { + + $this->weeks_before_semester_start = $weeks_before_semester_start; + $this->weeks_before_semester_end = $weeks_before_semester_end; + $this->selected_semesters = $selected_semesters; + $this->selected_schools = $selected_schools; + + $data = $this->dataset; + + $this->data = $data['datasets']; + $this->labels = $data['labels']; + + $this->emit('refreshChart1', $this->data, $this->labels); + } + + public function getDatasetProperty() + { + $report = new StudentDataInsight(); + return $report->appsCountBySemestersAndSchoolsWithFilingStatus($this->selected_semesters, $this->selected_schools, $this->selected_filing_statuses, $this->weeks_before_semester_start, $this->weeks_before_semester_end); + } + + public function render() + { + return view('livewire.charts.students-app-filing-status-chart'); + } +} diff --git a/app/Insights/StudentApplications/StudentDataInsight.php b/app/Insights/StudentApplications/StudentDataInsight.php new file mode 100644 index 00000000..a5b67eb7 --- /dev/null +++ b/app/Insights/StudentApplications/StudentDataInsight.php @@ -0,0 +1,419 @@ +cachedAppsForSemestersAndSchoolsWithFilingStatuses($semesters_params, $schools_params, $filing_status_category_1, $weeks_before_semester_start, $weeks_before_semester_end)->count(); + $filing_status_category_2_total = $this->cachedAppsForSemestersAndSchoolsWithFilingStatuses($semesters_params, $schools_params, $filing_status_category_2, $weeks_before_semester_start, $weeks_before_semester_end)->count(); + + return [$filing_status_category_1_total, $filing_status_category_2_total]; + } + + /** + * Auxiliary caching and grouping methods for Chart #1 (Doughnut) and Chart #4 (Bar). + */ + + /** + * Retrieve and cache a collection of student applications whose last filing status matches + * the provided filing status parameters for the specified semesters and schools. + * @param array $semesters_params Semesters filter. Example: ["Summer 2023", "Fall 2023"]. + * @param array $schools_params Schools filter. Example: ["BBS", "NSM"]. + * @param array $filing_status_params Filing status filter for the last status of the applications. Example: ['accepted', 'follow up']. + * @return \Illuminate\Support\Collection + */ + public function cachedAppsForSemestersAndSchoolsWithFilingStatuses($semesters_params, $schools_params, $filing_status_params, $weeks_before_semester_start, $weeks_before_semester_end) + { + $sm = implode('-', $semesters_params); + $sch = implode('-', $schools_params); + $fls = implode('-', $filing_status_params); + $wbs = $weeks_before_semester_start; + $wbe = $weeks_before_semester_end; + + return Cache::remember( + "student-apps-for-semesters-schools-with-flstatus-{$sm}-{$sch}-{$fls}-{$wbs}-{$wbe}", + 15 * 60, + fn() => $this->groupAppsBySemestersAndSchoolsWithFilingStatus($semesters_params, $schools_params, $filing_status_params, $weeks_before_semester_start, $weeks_before_semester_end) + ); + } + + /** + * Return a collection of student applications whose last filing status matches + * the provided filing status parameters for the specified semesters and schools. + * @param array $semesters_params Semesters filter. Example: ["Summer 2023", "Fall 2023"]. + * @param array $schools_params Schools filter. Example: ["BBS", "NSM"]. + * @param array $filing_status_params Filing status filter for the last status of the applications. Example: ['accepted', 'follow up']. + * @return \Illuminate\Support\Collection + */ + public function groupAppsBySemestersAndSchoolsWithFilingStatus($semesters_params, $schools_params, $filing_status_params, $weeks_before_semester_start, $weeks_before_semester_end) + { + $students = $this->cachedAppsForSemestersAndSchoolsWithStatsWithUser($semesters_params, $schools_params); + $semesters_params_start_end = $this->semestersParamsStartAndEnd($semesters_params, $weeks_before_semester_start, $weeks_before_semester_end); + $results = []; + + foreach ($semesters_params_start_end as $semester => $semester_start_end) { // Looping through semesters + foreach ($schools_params as $school) { // Looping through schools + $start_date = $semester_start_end['start']; + $end_date = $semester_start_end['end']; + foreach ($filing_status_params as $filing_status) { // Looping through filing statuses + + $students_filtered = $this->filterStudentsBySemesterAndSchool($students, $semester, $school); + $students_filtered->each(function ($student) use ($start_date, $end_date, $filing_status, $semester, &$results) { + // For each student, find the non empty status history in the stats + if (!empty($student->stats->data['status_history'])) { // Each status history equals a filing record for a student app - + // Each status history represents an action of filing the app for a specific status by a faculty member + $matching_updates = collect($student->stats->data['status_history']) //For each status history, group by profile + ->groupBy('profile') //Filter the st-hist for which the last update matches the filing status param and was updated in the given timeframe + ->filter(function($group_by_profile) use ($start_date, $end_date, $filing_status) { + $last_update_by_profile = $this->lastUpdateByProfile($group_by_profile); + $filing_date = Carbon::parse($last_update_by_profile['updated_at']); + return $last_update_by_profile['new_status'] === $filing_status && $filing_date->between($start_date, $end_date); + }); + // There could be multiple matches. For instance, two faculty members file the app as follow up in the same timeframe + foreach ($matching_updates as $m) { + $results[] = $this->lastUpdateBetweenRangeByProfileWithStatus($m->first(), $semester, $student); + } + } + }); + } + } + } + return collect($results); + } + + /** + * Retrieve and cache a collection of student applications for given semesters and schools with stats. + * @param array $semesters_params Semesters filter. Example: ["Summer 2023", "Fall 2023"]. + * @param array $schools_params Schools filter. Example: ["BBS", "NSM"]. + * @return \Illuminate\Support\Collection + */ + public function cachedAppsForSemestersAndSchoolsWithStatsWithUser($semesters_params, $schools_params) + { + $sm = implode('-', $semesters_params); + $sch = implode('-', $schools_params); + + return Cache::remember( + "student-apps-for-semesters-schools-with-stats-{$sm}-{$sch}", + 15 * 60, + fn() => $this->appsForSemestersAndSchools($semesters_params, $schools_params) + ->with('stats') + ->with('user') + ->get() + ); + } + + /** + * Return query builder to retrieve students records with applications ('research_profile') for given semesters and schools. + * @param array $semesters_params Semesters filter. Example: ["Summer 2023", "Fall 2023"]. + * @param array $schools_params Schools filter. Example: ["BBS", "NSM"]. + * @return Illuminate\Database\Eloquent\Builder + */ + public function appsForSemestersAndSchools($semesters_params, $schools_params) + { + if (empty($semesters_params) || empty($schools_params)) { + return Student::query()->whereRaw('1 = 0'); + } + + return Student::query() + ->submitted() + ->withWhereHas('research_profile', function($q) use ($semesters_params, $schools_params) { + $q->where(function($q) use ($semesters_params) { + foreach ($semesters_params as $semester) { + $q->orDataContains('semesters', $semester); + } + }); + $q->where(function($q) use ($schools_params) { + foreach ($schools_params as $school) { + $q->orDataContains('schools', $school); + } + }); + }); + } + + /** + * DOUGHNUT CHART #2 DATA - APPLICATIONS COUNT VIEWED AND NOT VIEWED FOR SEMESTERS AND SCHOOLS + * @param array $semesters_params Semesters filter. Example: ["Summer 2023", "Fall 2023"]. + * @param array $schools_params Schools filter. Example: ["BBS", "NSM"]. + * @return array + */ + public function appsCountViewedAndNotViewed($semesters_params, $schools_params) + { + $submitted_and_viewed = $this->cachedViewedAppsForSemestersAndSchools($semesters_params, $schools_params)->count(); + $submitted_not_viewed = $this->cachedNotViewedAppsForSemestersAndSchools($semesters_params, $schools_params)->count(); + + return [ + 'labels' => [ 'Viewed', 'Not Viewed' ], + 'datasets' => [$submitted_and_viewed, $submitted_not_viewed], + ]; + } + /** + * Retrieve and cache a collection of student applications for given semesters and schools that have been viewed. + * @param array $semesters_params Semesters filter. Example: ["Summer 2023", "Fall 2023"]. + * @param array $schools_params Schools filter. Example: ["BBS", "NSM"]. + * @return \Illuminate\Support\Collection + */ + public function cachedViewedAppsForSemestersAndSchools($semesters_params, $schools_params) + { + $sm = implode('-', $semesters_params); + $sch = implode('-', $schools_params); + + return Cache::remember( + "student-apps-for-semesters-schools-viewed-{$sm}-{$sch}", + 15 * 60, + fn() => $this->appsForSemestersAndSchools($semesters_params, $schools_params) + ->where(function($query) { + $query->whereHas('stats', function($q) { + $q->whereNotNull('data->views'); + $q->where('data->views', '>', 0); + }); + })->get() + ); + } + + /** + * Retrieve and cache a collection of student applications for given semesters and schools that have not been viewed. + * @param array $semesters_params Semesters filter. Example: ["Summer 2023", "Fall 2023"]. + * @param array $schools_params Schools filter. Example: ["BBS", "NSM"]. + * @return \Illuminate\Support\Collection + */ + public function cachedNotViewedAppsForSemestersAndSchools($semesters_params, $schools_params) + { + $sm = implode('-', $semesters_params); + $sch = implode('-', $schools_params); + + return Cache::remember( + "student-apps-for-semesters-schools-not-viewed-{$sm}-{$sch}", + 15 * 60, + fn() => $this->appsForSemestersAndSchools($semesters_params, $schools_params) + ->where(function($query) { + $query->whereHas('stats', function($q) { + $q->whereNull('data->views'); + }); + $query->orDoesntHave('stats'); + })->get() + ); + } + + /** + * BAR CHART #3 DATA - APPLICATIONS COUNT BY SEMESTER AND SCHOOL + * @param array $semesters_params Semesters filter. Example: ["Summer 2023", "Fall 2023"]. + * @param array $schools_params Schools filter. Example: ["BBS", "NSM"]. + * @return array + */ + public function appsCountBySemestersAndSchools($semesters_params, $schools_params) + { + $applications = $this->groupAppsBySemestersAndSchools($semesters_params, $schools_params); + $semesters_sort_closure = Semester::sortCollectionWithSemestersKeyChronologically(); + + $counted_apps = $applications + ->groupBy(['semester', 'school']) + ->sortKeysUsing($semesters_sort_closure) + ->map(function ($semester_group) { + return $semester_group->map(function ($school_group) { + return $school_group->count(); + }); + }); + + $all_semesters = $counted_apps->keys(); + $all_schools = $applications->pluck('school')->unique()->sort()->values(); + + $datasets = $all_schools->mapWithKeys(function ($school) use ($all_semesters) { // Initialize datasets for each school + return [ $school => ['label' => $school, 'data' => array_fill(0, $all_semesters->count(), 0)]]; + })->toArray(); + + foreach ($counted_apps as $semester => $school_counts) { + $semester_index = $all_semesters->search($semester); + foreach ($school_counts as $school => $count) { + $datasets[$school]['data'][$semester_index] = $count; + } + } + + return [ + 'labels' => $all_semesters->toArray(), + 'datasets' => array_values($datasets), + ]; + + } + + /** + * Auxiliary method for Chart #3 to transform student applications by semesters and schools. + * @param array $semesters_params Semesters filter. Example: ["Summer 2023", "Fall 2023"]. + * @param array $schools_params Schools filter. Example: ["BBS", "NSM"]. + * @return \Illuminate\Support\Collection + */ + public function groupAppsBySemestersAndSchools(array $semesters_params, array $schools_params) + { + $students = $this->getCachedAppsForSemestersAndSchools($semesters_params, $schools_params); + $results = []; + + foreach ($semesters_params as $semester) { // Looping through semesters + foreach ($schools_params as $school) { // Looping through schools + + $students_filtered = $this->filterStudentsBySemesterAndSchool($students, $semester, $school); + $students_filtered->each(function ($student) use ($semester, $school, &$results) { + // For each student, find the non empty status history in the stats + $results[] =[ + 'id' => $student->research_profile->id, + 'semester' => $semester, + 'school' => $school, + ]; + }); + } + } + return collect($results); + } + + /** + * Retrieve and cache a collection of student applications for given semesters and schools. + * @param array $semesters_params Semesters filter. Example: ["Summer 2023", "Fall 2023"]. + * @param array $schools_params Schools filter. Example: ["BBS", "NSM"]. + * @return \Illuminate\Support\Collection + */ + public function getCachedAppsForSemestersAndSchools($semesters_params, $schools_params) + { + $sm = implode('-', $semesters_params); + $sch = implode('-', $schools_params); + + return Cache::remember( + "student-apps-for-semesters-schools-{$sm}-{$sch}", + 15 * 60, + fn() => $this->appsForSemestersAndSchools($semesters_params, $schools_params)->get() + ); + } + + /** + * BAR CHART #4 DATA - APPLICATIONS COUNT BY SEMESTER, SCHOOL AND FILING STATUS + * @param array $semesters_params Semesters filter. Example: ["Summer 2023", "Fall 2023"]. + * @param array $schools_params Schools filter. Example: ["BBS", "NSM"]. + * @param array $filing_status_params Filing status filter for the last status of the applications. Example: ['accepted', 'follow up']. + */ + public function appsCountBySemestersAndSchoolsWithFilingStatus($semesters_params, $schools_params, $filing_status_params, $weeks_before_semester_start, $weeks_before_semester_end) + { + $applications = $this->cachedAppsForSemestersAndSchoolsWithFilingStatuses($semesters_params, $schools_params, $filing_status_params, $weeks_before_semester_start, $weeks_before_semester_end); + $semesters_sort_closure = Semester::sortCollectionWithSemestersKeyChronologically(); + $counted_apps = $applications + ->groupBy(['semester', 'filing_status']) + ->sortKeysUsing($semesters_sort_closure) + ->map(function ($semester_group) { + return $semester_group->map(function ($status_group) { + return $status_group->count(); + }); + }); + + $all_semesters = $counted_apps->keys(); + $all_filing_statuses = $applications->pluck('filing_status')->unique()->sort()->values(); + + $datasets = $all_filing_statuses->mapWithKeys(function ($filing_status) use ($all_semesters) { // Initialize datasets for each status + return [$filing_status => ['label' => ucfirst($filing_status), 'data' => array_fill(0, $all_semesters->count(), 0)]]; + })->toArray(); + + foreach ($counted_apps as $semester => $filing_status_counts) { + $semester_index = $all_semesters->search($semester); + foreach ($filing_status_counts as $filing_status => $count) { + $datasets[$filing_status]['data'][$semester_index] = $count; + } + } + + return [ + 'labels' => $all_semesters->toArray(), + 'datasets' => array_values($datasets), + ]; + } + + /** AUXILIARY METHODS */ + public static function getLabels(string $criteria) + { + switch ($criteria) { + case 'school': + return StudentData::uniqueValuesFor('research_profile', 'schools')->sort()->values(); + break; + case 'semester': + return StudentData::uniqueValuesFor('research_profile', 'semesters') + ->sortBy(function($semester, $key) { + return Semester::date($semester)?->toDateString(); + }) + ->values(); + break; + case 'faculty': + return StudentData::uniqueValuesFor('research_profile', 'faculty') + ->sort()->values(); + break; + default: + $semesters = StudentData::uniqueValuesFor('research_profile', 'semesters') + ->map(fn($semester) => Semester::date($semester)?->toDateString()); + return Semester::sortSemestersChronologically($semesters); + break; + } + } + + public function semestersParamsStartAndEnd($semesters_params, $weeks_before_start, $weeks_before_end) : array { + $result = []; + + foreach ($semesters_params as $semester_params) { + + $semester = explode(' ', $semester_params); + $start_date = Carbon::createFromFormat('M j Y', Semester::seasonDates()[$semester[0]][0].' '.$semester[1]) + ->subweeks((int) $weeks_before_start) + ->format('Y-m-d'); + $end_date = Carbon::createFromFormat('M j Y', Semester::seasonDates()[$semester[0]][1].' '.$semester[1]) + ->subweeks((int) $weeks_before_end) + ->format('Y-m-d'); + + $result[$semester_params] = [ 'start' => $start_date, 'end' => $end_date ]; + } + + return $result; + } + + public static function convertParameterstoTitle($semesters_params, $schools_params) + { + $semesters = implode(' | ', $semesters_params); + $schools = implode(' | ', $schools_params); + return [$semesters, $schools]; + } + + public function filterStudentsBySemesterAndSchool($students, $semester, $school): Collection { + return $students->filter(function($student) use ($semester, $school) { + return in_array($semester, $student->research_profile->data['semesters']) && in_array($school, $student->research_profile->data['schools']); + }); + } + + public function lastUpdateBetweenRangeByProfileWithStatus($matching_update, $semester, $student) { + + return [ + 'id' => $student->stats->id, + 'semester' => $semester, + 'school' => $student->research_profile->data['schools'], + 'filing_status' => ucfirst($matching_update['new_status']), + 'updated_at' => $matching_update['updated_at'], + 'profile' => $matching_update['profile'], + 'display_name' => $student->user->display_name, + 'netID' => $student->user->name, + ]; + + } + + public function lastUpdateByProfile($group_by_profile) { + return $group_by_profile->sortByDesc(function ($item) { + return Carbon::parse($item['updated_at']); + })->first(); + } +} diff --git a/app/Profile.php b/app/Profile.php index 04f34c7b..7ee53260 100644 --- a/app/Profile.php +++ b/app/Profile.php @@ -511,6 +511,13 @@ public function scopeStudentsPendingReviewWithSemester($query, $semester) }); } + public function scopeNotAcceptingStudents($query) { + return $query->whereHas('data', function($q) { + $q->where('type', 'information') + ->whereJsonContains('data->not_accepting_students', '1'); + }); + } + /////////////////////////////////// // Mutators & Virtual Attributes // /////////////////////////////////// diff --git a/app/Student.php b/app/Student.php index fbecf415..66a86c09 100644 --- a/app/Student.php +++ b/app/Student.php @@ -313,6 +313,45 @@ public function scopeDataContains($query, $key, $value) return $query; } + public function scopeDataOrContains($query, $key, $value) + { + if ($value !== '') { + $query->whereHas('research_profile', function ($q) use ($key, $value) { + $q->orWhereJsonContains("data->{$key}", $value); + }); + } + + return $query; + } + + public function scopeWithDataSelected($query, $key) + { + $query->whereHas('research_profile', function ($q) use ($key) { + $q->orWhereJsonLength("data->{$key}", ">=", "1"); + }); + + return $query; + } + + // public function scopeWithSemesterSelected($query) + // { + // return $query->whereHas('research_profile', function ($q) { + // $q->whereJsonLength("data->semesters", ">=", "1"); + // }); + // } + // public function scopeWithSchoolSelected($query) + // { + // return $query->whereHas('research_profile', function ($q) { + // $q->whereJsonLength("data->schools", ">=", "1"); + // }); + // } + // public function scopeWithExistingStatus($query) + // { + // return $query->whereHas('stats', function ($q) { + // $q->whereJsonLength("data->status", ">=", "1"); + // }); + // } + public function scopeDataEquals($query, $key, $value) { if ($value !== '') { @@ -352,6 +391,17 @@ public function scopeWithStatusNotInterested($query) return $query->where('profile_student.status', '=', 'not interested'); } + /** + * Query scope for Students whose application status is 'not interested'. To be used through the Profile relation. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeWithStatusAccepted($query) + { + return $query->where('profile_student.status', '=', 'accepted'); + } + /////////////// // Relations // /////////////// diff --git a/app/StudentData.php b/app/StudentData.php index f24b8405..e6e99a78 100644 --- a/app/StudentData.php +++ b/app/StudentData.php @@ -5,7 +5,9 @@ use App\ProfileData; use App\Setting; use App\Student; +use Carbon\Carbon; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; class StudentData extends ProfileData { @@ -102,6 +104,48 @@ public function scopeStats($query) return $query->where('type', 'stats'); } + /** + * Query scope for research profile + * + * @param \Illuminate\Database\Query\Builder $query + * @return \Illuminate\Database\Query\Builder + */ + public function scopeResearchProfileSubmittedWithSemesterSelected($query, $status) + { + return $query->where('type', 'research_profile') + ->whereJsonLength("data->semesters", ">=", "1") + ->whereRelation("student", "status", $status); + } + + /** + * Query scope for research profile + * + * @param \Illuminate\Database\Query\Builder $query + * @return \Illuminate\Database\Query\Builder + */ + public function scopeDataContains($query, $key, $value) + { + if ($value !== '') { + $query->whereJsonContains("data->{$key}", $value); + } + + return $query; + } + + public function scopeOrDataContains($query, $key, $value) + { + if ($value !== '') { + $query->orWhereJsonContains("data->{$key}", $value); + } + + return $query; + } + + public function scopeWithDataLengthGreaterThan($query, $key, $length) + { + return $query->whereJsonLength("data->{$key}", ">=", $length); + } + /////////////// // Relations // /////////////// diff --git a/package-lock.json b/package-lock.json index 9b32e0af..80c8eb6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,9 @@ "packages": { "": { "dependencies": { + "alpinejs": "^3.14.8", + "chart.js": "^4.4.3", + "chartjs-plugin-datalabels": "^2.2.0", "puppeteer": "^16.1.0" }, "devDependencies": { @@ -2128,6 +2131,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -2583,6 +2591,19 @@ "@types/node": "*" } }, + "node_modules/@vue/reactivity": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", + "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", + "dependencies": { + "@vue/shared": "3.1.5" + } + }, + "node_modules/@vue/shared": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", + "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==" + }, "node_modules/@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", @@ -2904,6 +2925,14 @@ "ajv": "^8.8.2" } }, + "node_modules/alpinejs": { + "version": "3.14.8", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.8.tgz", + "integrity": "sha512-wT2fuP2DXpGk/jKaglwy7S/IJpm1FD+b7U6zUrhwErjoq5h27S4dxkJEXVvhbdwyPv9U+3OkUuNLkZT4h2Kfrg==", + "dependencies": { + "@vue/reactivity": "~3.1.1" + } + }, "node_modules/ansi-html-community": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", @@ -3721,6 +3750,25 @@ "node": "*" } }, + "node_modules/chart.js": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz", + "integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chartjs-plugin-datalabels": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz", + "integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==", + "peerDependencies": { + "chart.js": ">=3.0.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -12802,6 +12850,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -13226,6 +13279,19 @@ "@types/node": "*" } }, + "@vue/reactivity": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", + "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", + "requires": { + "@vue/shared": "3.1.5" + } + }, + "@vue/shared": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", + "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==" + }, "@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", @@ -13499,6 +13565,14 @@ "fast-deep-equal": "^3.1.3" } }, + "alpinejs": { + "version": "3.14.8", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.8.tgz", + "integrity": "sha512-wT2fuP2DXpGk/jKaglwy7S/IJpm1FD+b7U6zUrhwErjoq5h27S4dxkJEXVvhbdwyPv9U+3OkUuNLkZT4h2Kfrg==", + "requires": { + "@vue/reactivity": "~3.1.1" + } + }, "ansi-html-community": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", @@ -14122,6 +14196,20 @@ "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", "dev": true }, + "chart.js": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz", + "integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==", + "requires": { + "@kurkle/color": "^0.3.0" + } + }, + "chartjs-plugin-datalabels": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz", + "integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==", + "requires": {} + }, "chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", diff --git a/package.json b/package.json index 48c9d2ea..6890fb51 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,9 @@ "vue-template-compiler": "^2.6.14" }, "dependencies": { + "alpinejs": "^3.14.8", + "chart.js": "^4.4.3", + "chartjs-plugin-datalabels": "^2.2.0", "puppeteer": "^16.1.0" } } diff --git a/public/js/app.js b/public/js/app.js index bceb5ca1..29e0d8dc 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1,5 +1,3425 @@ (self["webpackChunk"] = self["webpackChunk"] || []).push([["/js/app"],{ +/***/ "./node_modules/alpinejs/dist/module.esm.js": +/*!**************************************************!*\ + !*** ./node_modules/alpinejs/dist/module.esm.js ***! + \**************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Alpine: () => (/* binding */ src_default), +/* harmony export */ "default": () => (/* binding */ module_default) +/* harmony export */ }); +// packages/alpinejs/src/scheduler.js +var flushPending = false; +var flushing = false; +var queue = []; +var lastFlushedIndex = -1; +function scheduler(callback) { + queueJob(callback); +} +function queueJob(job) { + if (!queue.includes(job)) + queue.push(job); + queueFlush(); +} +function dequeueJob(job) { + let index = queue.indexOf(job); + if (index !== -1 && index > lastFlushedIndex) + queue.splice(index, 1); +} +function queueFlush() { + if (!flushing && !flushPending) { + flushPending = true; + queueMicrotask(flushJobs); + } +} +function flushJobs() { + flushPending = false; + flushing = true; + for (let i = 0; i < queue.length; i++) { + queue[i](); + lastFlushedIndex = i; + } + queue.length = 0; + lastFlushedIndex = -1; + flushing = false; +} + +// packages/alpinejs/src/reactivity.js +var reactive; +var effect; +var release; +var raw; +var shouldSchedule = true; +function disableEffectScheduling(callback) { + shouldSchedule = false; + callback(); + shouldSchedule = true; +} +function setReactivityEngine(engine) { + reactive = engine.reactive; + release = engine.release; + effect = (callback) => engine.effect(callback, { scheduler: (task) => { + if (shouldSchedule) { + scheduler(task); + } else { + task(); + } + } }); + raw = engine.raw; +} +function overrideEffect(override) { + effect = override; +} +function elementBoundEffect(el) { + let cleanup2 = () => { + }; + let wrappedEffect = (callback) => { + let effectReference = effect(callback); + if (!el._x_effects) { + el._x_effects = /* @__PURE__ */ new Set(); + el._x_runEffects = () => { + el._x_effects.forEach((i) => i()); + }; + } + el._x_effects.add(effectReference); + cleanup2 = () => { + if (effectReference === void 0) + return; + el._x_effects.delete(effectReference); + release(effectReference); + }; + return effectReference; + }; + return [wrappedEffect, () => { + cleanup2(); + }]; +} +function watch(getter, callback) { + let firstTime = true; + let oldValue; + let effectReference = effect(() => { + let value = getter(); + JSON.stringify(value); + if (!firstTime) { + queueMicrotask(() => { + callback(value, oldValue); + oldValue = value; + }); + } else { + oldValue = value; + } + firstTime = false; + }); + return () => release(effectReference); +} + +// packages/alpinejs/src/mutation.js +var onAttributeAddeds = []; +var onElRemoveds = []; +var onElAddeds = []; +function onElAdded(callback) { + onElAddeds.push(callback); +} +function onElRemoved(el, callback) { + if (typeof callback === "function") { + if (!el._x_cleanups) + el._x_cleanups = []; + el._x_cleanups.push(callback); + } else { + callback = el; + onElRemoveds.push(callback); + } +} +function onAttributesAdded(callback) { + onAttributeAddeds.push(callback); +} +function onAttributeRemoved(el, name, callback) { + if (!el._x_attributeCleanups) + el._x_attributeCleanups = {}; + if (!el._x_attributeCleanups[name]) + el._x_attributeCleanups[name] = []; + el._x_attributeCleanups[name].push(callback); +} +function cleanupAttributes(el, names) { + if (!el._x_attributeCleanups) + return; + Object.entries(el._x_attributeCleanups).forEach(([name, value]) => { + if (names === void 0 || names.includes(name)) { + value.forEach((i) => i()); + delete el._x_attributeCleanups[name]; + } + }); +} +function cleanupElement(el) { + el._x_effects?.forEach(dequeueJob); + while (el._x_cleanups?.length) + el._x_cleanups.pop()(); +} +var observer = new MutationObserver(onMutate); +var currentlyObserving = false; +function startObservingMutations() { + observer.observe(document, { subtree: true, childList: true, attributes: true, attributeOldValue: true }); + currentlyObserving = true; +} +function stopObservingMutations() { + flushObserver(); + observer.disconnect(); + currentlyObserving = false; +} +var queuedMutations = []; +function flushObserver() { + let records = observer.takeRecords(); + queuedMutations.push(() => records.length > 0 && onMutate(records)); + let queueLengthWhenTriggered = queuedMutations.length; + queueMicrotask(() => { + if (queuedMutations.length === queueLengthWhenTriggered) { + while (queuedMutations.length > 0) + queuedMutations.shift()(); + } + }); +} +function mutateDom(callback) { + if (!currentlyObserving) + return callback(); + stopObservingMutations(); + let result = callback(); + startObservingMutations(); + return result; +} +var isCollecting = false; +var deferredMutations = []; +function deferMutations() { + isCollecting = true; +} +function flushAndStopDeferringMutations() { + isCollecting = false; + onMutate(deferredMutations); + deferredMutations = []; +} +function onMutate(mutations) { + if (isCollecting) { + deferredMutations = deferredMutations.concat(mutations); + return; + } + let addedNodes = []; + let removedNodes = /* @__PURE__ */ new Set(); + let addedAttributes = /* @__PURE__ */ new Map(); + let removedAttributes = /* @__PURE__ */ new Map(); + for (let i = 0; i < mutations.length; i++) { + if (mutations[i].target._x_ignoreMutationObserver) + continue; + if (mutations[i].type === "childList") { + mutations[i].removedNodes.forEach((node) => { + if (node.nodeType !== 1) + return; + if (!node._x_marker) + return; + removedNodes.add(node); + }); + mutations[i].addedNodes.forEach((node) => { + if (node.nodeType !== 1) + return; + if (removedNodes.has(node)) { + removedNodes.delete(node); + return; + } + if (node._x_marker) + return; + addedNodes.push(node); + }); + } + if (mutations[i].type === "attributes") { + let el = mutations[i].target; + let name = mutations[i].attributeName; + let oldValue = mutations[i].oldValue; + let add2 = () => { + if (!addedAttributes.has(el)) + addedAttributes.set(el, []); + addedAttributes.get(el).push({ name, value: el.getAttribute(name) }); + }; + let remove = () => { + if (!removedAttributes.has(el)) + removedAttributes.set(el, []); + removedAttributes.get(el).push(name); + }; + if (el.hasAttribute(name) && oldValue === null) { + add2(); + } else if (el.hasAttribute(name)) { + remove(); + add2(); + } else { + remove(); + } + } + } + removedAttributes.forEach((attrs, el) => { + cleanupAttributes(el, attrs); + }); + addedAttributes.forEach((attrs, el) => { + onAttributeAddeds.forEach((i) => i(el, attrs)); + }); + for (let node of removedNodes) { + if (addedNodes.some((i) => i.contains(node))) + continue; + onElRemoveds.forEach((i) => i(node)); + } + for (let node of addedNodes) { + if (!node.isConnected) + continue; + onElAddeds.forEach((i) => i(node)); + } + addedNodes = null; + removedNodes = null; + addedAttributes = null; + removedAttributes = null; +} + +// packages/alpinejs/src/scope.js +function scope(node) { + return mergeProxies(closestDataStack(node)); +} +function addScopeToNode(node, data2, referenceNode) { + node._x_dataStack = [data2, ...closestDataStack(referenceNode || node)]; + return () => { + node._x_dataStack = node._x_dataStack.filter((i) => i !== data2); + }; +} +function closestDataStack(node) { + if (node._x_dataStack) + return node._x_dataStack; + if (typeof ShadowRoot === "function" && node instanceof ShadowRoot) { + return closestDataStack(node.host); + } + if (!node.parentNode) { + return []; + } + return closestDataStack(node.parentNode); +} +function mergeProxies(objects) { + return new Proxy({ objects }, mergeProxyTrap); +} +var mergeProxyTrap = { + ownKeys({ objects }) { + return Array.from( + new Set(objects.flatMap((i) => Object.keys(i))) + ); + }, + has({ objects }, name) { + if (name == Symbol.unscopables) + return false; + return objects.some( + (obj) => Object.prototype.hasOwnProperty.call(obj, name) || Reflect.has(obj, name) + ); + }, + get({ objects }, name, thisProxy) { + if (name == "toJSON") + return collapseProxies; + return Reflect.get( + objects.find( + (obj) => Reflect.has(obj, name) + ) || {}, + name, + thisProxy + ); + }, + set({ objects }, name, value, thisProxy) { + const target = objects.find( + (obj) => Object.prototype.hasOwnProperty.call(obj, name) + ) || objects[objects.length - 1]; + const descriptor = Object.getOwnPropertyDescriptor(target, name); + if (descriptor?.set && descriptor?.get) + return descriptor.set.call(thisProxy, value) || true; + return Reflect.set(target, name, value); + } +}; +function collapseProxies() { + let keys = Reflect.ownKeys(this); + return keys.reduce((acc, key) => { + acc[key] = Reflect.get(this, key); + return acc; + }, {}); +} + +// packages/alpinejs/src/interceptor.js +function initInterceptors(data2) { + let isObject2 = (val) => typeof val === "object" && !Array.isArray(val) && val !== null; + let recurse = (obj, basePath = "") => { + Object.entries(Object.getOwnPropertyDescriptors(obj)).forEach(([key, { value, enumerable }]) => { + if (enumerable === false || value === void 0) + return; + if (typeof value === "object" && value !== null && value.__v_skip) + return; + let path = basePath === "" ? key : `${basePath}.${key}`; + if (typeof value === "object" && value !== null && value._x_interceptor) { + obj[key] = value.initialize(data2, path, key); + } else { + if (isObject2(value) && value !== obj && !(value instanceof Element)) { + recurse(value, path); + } + } + }); + }; + return recurse(data2); +} +function interceptor(callback, mutateObj = () => { +}) { + let obj = { + initialValue: void 0, + _x_interceptor: true, + initialize(data2, path, key) { + return callback(this.initialValue, () => get(data2, path), (value) => set(data2, path, value), path, key); + } + }; + mutateObj(obj); + return (initialValue) => { + if (typeof initialValue === "object" && initialValue !== null && initialValue._x_interceptor) { + let initialize = obj.initialize.bind(obj); + obj.initialize = (data2, path, key) => { + let innerValue = initialValue.initialize(data2, path, key); + obj.initialValue = innerValue; + return initialize(data2, path, key); + }; + } else { + obj.initialValue = initialValue; + } + return obj; + }; +} +function get(obj, path) { + return path.split(".").reduce((carry, segment) => carry[segment], obj); +} +function set(obj, path, value) { + if (typeof path === "string") + path = path.split("."); + if (path.length === 1) + obj[path[0]] = value; + else if (path.length === 0) + throw error; + else { + if (obj[path[0]]) + return set(obj[path[0]], path.slice(1), value); + else { + obj[path[0]] = {}; + return set(obj[path[0]], path.slice(1), value); + } + } +} + +// packages/alpinejs/src/magics.js +var magics = {}; +function magic(name, callback) { + magics[name] = callback; +} +function injectMagics(obj, el) { + let memoizedUtilities = getUtilities(el); + Object.entries(magics).forEach(([name, callback]) => { + Object.defineProperty(obj, `$${name}`, { + get() { + return callback(el, memoizedUtilities); + }, + enumerable: false + }); + }); + return obj; +} +function getUtilities(el) { + let [utilities, cleanup2] = getElementBoundUtilities(el); + let utils = { interceptor, ...utilities }; + onElRemoved(el, cleanup2); + return utils; +} + +// packages/alpinejs/src/utils/error.js +function tryCatch(el, expression, callback, ...args) { + try { + return callback(...args); + } catch (e) { + handleError(e, el, expression); + } +} +function handleError(error2, el, expression = void 0) { + error2 = Object.assign( + error2 ?? { message: "No error message given." }, + { el, expression } + ); + console.warn(`Alpine Expression Error: ${error2.message} + +${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); + setTimeout(() => { + throw error2; + }, 0); +} + +// packages/alpinejs/src/evaluator.js +var shouldAutoEvaluateFunctions = true; +function dontAutoEvaluateFunctions(callback) { + let cache = shouldAutoEvaluateFunctions; + shouldAutoEvaluateFunctions = false; + let result = callback(); + shouldAutoEvaluateFunctions = cache; + return result; +} +function evaluate(el, expression, extras = {}) { + let result; + evaluateLater(el, expression)((value) => result = value, extras); + return result; +} +function evaluateLater(...args) { + return theEvaluatorFunction(...args); +} +var theEvaluatorFunction = normalEvaluator; +function setEvaluator(newEvaluator) { + theEvaluatorFunction = newEvaluator; +} +function normalEvaluator(el, expression) { + let overriddenMagics = {}; + injectMagics(overriddenMagics, el); + let dataStack = [overriddenMagics, ...closestDataStack(el)]; + let evaluator = typeof expression === "function" ? generateEvaluatorFromFunction(dataStack, expression) : generateEvaluatorFromString(dataStack, expression, el); + return tryCatch.bind(null, el, expression, evaluator); +} +function generateEvaluatorFromFunction(dataStack, func) { + return (receiver = () => { + }, { scope: scope2 = {}, params = [] } = {}) => { + let result = func.apply(mergeProxies([scope2, ...dataStack]), params); + runIfTypeOfFunction(receiver, result); + }; +} +var evaluatorMemo = {}; +function generateFunctionFromString(expression, el) { + if (evaluatorMemo[expression]) { + return evaluatorMemo[expression]; + } + let AsyncFunction = Object.getPrototypeOf(async function() { + }).constructor; + let rightSideSafeExpression = /^[\n\s]*if.*\(.*\)/.test(expression.trim()) || /^(let|const)\s/.test(expression.trim()) ? `(async()=>{ ${expression} })()` : expression; + const safeAsyncFunction = () => { + try { + let func2 = new AsyncFunction( + ["__self", "scope"], + `with (scope) { __self.result = ${rightSideSafeExpression} }; __self.finished = true; return __self.result;` + ); + Object.defineProperty(func2, "name", { + value: `[Alpine] ${expression}` + }); + return func2; + } catch (error2) { + handleError(error2, el, expression); + return Promise.resolve(); + } + }; + let func = safeAsyncFunction(); + evaluatorMemo[expression] = func; + return func; +} +function generateEvaluatorFromString(dataStack, expression, el) { + let func = generateFunctionFromString(expression, el); + return (receiver = () => { + }, { scope: scope2 = {}, params = [] } = {}) => { + func.result = void 0; + func.finished = false; + let completeScope = mergeProxies([scope2, ...dataStack]); + if (typeof func === "function") { + let promise = func(func, completeScope).catch((error2) => handleError(error2, el, expression)); + if (func.finished) { + runIfTypeOfFunction(receiver, func.result, completeScope, params, el); + func.result = void 0; + } else { + promise.then((result) => { + runIfTypeOfFunction(receiver, result, completeScope, params, el); + }).catch((error2) => handleError(error2, el, expression)).finally(() => func.result = void 0); + } + } + }; +} +function runIfTypeOfFunction(receiver, value, scope2, params, el) { + if (shouldAutoEvaluateFunctions && typeof value === "function") { + let result = value.apply(scope2, params); + if (result instanceof Promise) { + result.then((i) => runIfTypeOfFunction(receiver, i, scope2, params)).catch((error2) => handleError(error2, el, value)); + } else { + receiver(result); + } + } else if (typeof value === "object" && value instanceof Promise) { + value.then((i) => receiver(i)); + } else { + receiver(value); + } +} + +// packages/alpinejs/src/directives.js +var prefixAsString = "x-"; +function prefix(subject = "") { + return prefixAsString + subject; +} +function setPrefix(newPrefix) { + prefixAsString = newPrefix; +} +var directiveHandlers = {}; +function directive(name, callback) { + directiveHandlers[name] = callback; + return { + before(directive2) { + if (!directiveHandlers[directive2]) { + console.warn(String.raw`Cannot find directive \`${directive2}\`. \`${name}\` will use the default order of execution`); + return; + } + const pos = directiveOrder.indexOf(directive2); + directiveOrder.splice(pos >= 0 ? pos : directiveOrder.indexOf("DEFAULT"), 0, name); + } + }; +} +function directiveExists(name) { + return Object.keys(directiveHandlers).includes(name); +} +function directives(el, attributes, originalAttributeOverride) { + attributes = Array.from(attributes); + if (el._x_virtualDirectives) { + let vAttributes = Object.entries(el._x_virtualDirectives).map(([name, value]) => ({ name, value })); + let staticAttributes = attributesOnly(vAttributes); + vAttributes = vAttributes.map((attribute) => { + if (staticAttributes.find((attr) => attr.name === attribute.name)) { + return { + name: `x-bind:${attribute.name}`, + value: `"${attribute.value}"` + }; + } + return attribute; + }); + attributes = attributes.concat(vAttributes); + } + let transformedAttributeMap = {}; + let directives2 = attributes.map(toTransformedAttributes((newName, oldName) => transformedAttributeMap[newName] = oldName)).filter(outNonAlpineAttributes).map(toParsedDirectives(transformedAttributeMap, originalAttributeOverride)).sort(byPriority); + return directives2.map((directive2) => { + return getDirectiveHandler(el, directive2); + }); +} +function attributesOnly(attributes) { + return Array.from(attributes).map(toTransformedAttributes()).filter((attr) => !outNonAlpineAttributes(attr)); +} +var isDeferringHandlers = false; +var directiveHandlerStacks = /* @__PURE__ */ new Map(); +var currentHandlerStackKey = Symbol(); +function deferHandlingDirectives(callback) { + isDeferringHandlers = true; + let key = Symbol(); + currentHandlerStackKey = key; + directiveHandlerStacks.set(key, []); + let flushHandlers = () => { + while (directiveHandlerStacks.get(key).length) + directiveHandlerStacks.get(key).shift()(); + directiveHandlerStacks.delete(key); + }; + let stopDeferring = () => { + isDeferringHandlers = false; + flushHandlers(); + }; + callback(flushHandlers); + stopDeferring(); +} +function getElementBoundUtilities(el) { + let cleanups = []; + let cleanup2 = (callback) => cleanups.push(callback); + let [effect3, cleanupEffect] = elementBoundEffect(el); + cleanups.push(cleanupEffect); + let utilities = { + Alpine: alpine_default, + effect: effect3, + cleanup: cleanup2, + evaluateLater: evaluateLater.bind(evaluateLater, el), + evaluate: evaluate.bind(evaluate, el) + }; + let doCleanup = () => cleanups.forEach((i) => i()); + return [utilities, doCleanup]; +} +function getDirectiveHandler(el, directive2) { + let noop = () => { + }; + let handler4 = directiveHandlers[directive2.type] || noop; + let [utilities, cleanup2] = getElementBoundUtilities(el); + onAttributeRemoved(el, directive2.original, cleanup2); + let fullHandler = () => { + if (el._x_ignore || el._x_ignoreSelf) + return; + handler4.inline && handler4.inline(el, directive2, utilities); + handler4 = handler4.bind(handler4, el, directive2, utilities); + isDeferringHandlers ? directiveHandlerStacks.get(currentHandlerStackKey).push(handler4) : handler4(); + }; + fullHandler.runCleanups = cleanup2; + return fullHandler; +} +var startingWith = (subject, replacement) => ({ name, value }) => { + if (name.startsWith(subject)) + name = name.replace(subject, replacement); + return { name, value }; +}; +var into = (i) => i; +function toTransformedAttributes(callback = () => { +}) { + return ({ name, value }) => { + let { name: newName, value: newValue } = attributeTransformers.reduce((carry, transform) => { + return transform(carry); + }, { name, value }); + if (newName !== name) + callback(newName, name); + return { name: newName, value: newValue }; + }; +} +var attributeTransformers = []; +function mapAttributes(callback) { + attributeTransformers.push(callback); +} +function outNonAlpineAttributes({ name }) { + return alpineAttributeRegex().test(name); +} +var alpineAttributeRegex = () => new RegExp(`^${prefixAsString}([^:^.]+)\\b`); +function toParsedDirectives(transformedAttributeMap, originalAttributeOverride) { + return ({ name, value }) => { + let typeMatch = name.match(alpineAttributeRegex()); + let valueMatch = name.match(/:([a-zA-Z0-9\-_:]+)/); + let modifiers = name.match(/\.[^.\]]+(?=[^\]]*$)/g) || []; + let original = originalAttributeOverride || transformedAttributeMap[name] || name; + return { + type: typeMatch ? typeMatch[1] : null, + value: valueMatch ? valueMatch[1] : null, + modifiers: modifiers.map((i) => i.replace(".", "")), + expression: value, + original + }; + }; +} +var DEFAULT = "DEFAULT"; +var directiveOrder = [ + "ignore", + "ref", + "data", + "id", + "anchor", + "bind", + "init", + "for", + "model", + "modelable", + "transition", + "show", + "if", + DEFAULT, + "teleport" +]; +function byPriority(a, b) { + let typeA = directiveOrder.indexOf(a.type) === -1 ? DEFAULT : a.type; + let typeB = directiveOrder.indexOf(b.type) === -1 ? DEFAULT : b.type; + return directiveOrder.indexOf(typeA) - directiveOrder.indexOf(typeB); +} + +// packages/alpinejs/src/utils/dispatch.js +function dispatch(el, name, detail = {}) { + el.dispatchEvent( + new CustomEvent(name, { + detail, + bubbles: true, + // Allows events to pass the shadow DOM barrier. + composed: true, + cancelable: true + }) + ); +} + +// packages/alpinejs/src/utils/walk.js +function walk(el, callback) { + if (typeof ShadowRoot === "function" && el instanceof ShadowRoot) { + Array.from(el.children).forEach((el2) => walk(el2, callback)); + return; + } + let skip = false; + callback(el, () => skip = true); + if (skip) + return; + let node = el.firstElementChild; + while (node) { + walk(node, callback, false); + node = node.nextElementSibling; + } +} + +// packages/alpinejs/src/utils/warn.js +function warn(message, ...args) { + console.warn(`Alpine Warning: ${message}`, ...args); +} + +// packages/alpinejs/src/lifecycle.js +var started = false; +function start() { + if (started) + warn("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."); + started = true; + if (!document.body) + warn("Unable to initialize. Trying to load Alpine before `
` is available. Did you forget to add `defer` in Alpine's ` +@endpush + +@section('content') +Profiles
\ No newline at end of file diff --git a/resources/views/layout.blade.php b/resources/views/layout.blade.php index 2bc33c50..f538ca28 100644 --- a/resources/views/layout.blade.php +++ b/resources/views/layout.blade.php @@ -70,6 +70,7 @@ + @yield('scripts') @stack('scripts') @if(isset($settings['primary_color']) || isset($settings['secondary_color']) || isset($settings['tertiary_color'] )) diff --git a/resources/views/livewire/charts/accepted-and-follow-up-apps-percentage-chart.blade.php b/resources/views/livewire/charts/accepted-and-follow-up-apps-percentage-chart.blade.php new file mode 100644 index 00000000..90c38633 --- /dev/null +++ b/resources/views/livewire/charts/accepted-and-follow-up-apps-percentage-chart.blade.php @@ -0,0 +1,89 @@ +Showing Results For: {{$title[0]}} | {{$title[1]}}
+ + @php + $style = $charts_loaded ? 'display:none;' : 'display:flex;' + @endphp + + +