DynamicReport.php 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. <?php
  2. namespace App\Http\Livewire;
  3. use App\Http\Middleware\TenantMiddleware;
  4. use Livewire\Component;
  5. use Carbon\Carbon;
  6. use Carbon\CarbonPeriod;
  7. class DynamicReport extends Component
  8. {
  9. public $filters = [
  10. 'courses' => [],
  11. 'levels' => [],
  12. // 'types' => [],
  13. 'seasons' => [],
  14. 'months' => [],
  15. ];
  16. public $filter_options = [
  17. 'courses' => [],
  18. 'levels' => [],
  19. // 'types' => [],
  20. 'seasons' => [],
  21. 'months' => [],
  22. ];
  23. public $chart = [
  24. 'labels' => [],
  25. 'datasets' => [],
  26. ];
  27. public function boot()
  28. {
  29. app(TenantMiddleware::class)->setupTenantConnection();
  30. }
  31. public function mount()
  32. {
  33. $this->resetReport();
  34. $this->initFiltersOptions();
  35. }
  36. public function resetReport()
  37. {
  38. $this->chart = [
  39. 'labels' => [],
  40. 'datasets' => []
  41. ];
  42. }
  43. public function initFiltersOptions()
  44. {
  45. // courses
  46. $this->filter_options['courses'] = \App\Models\Course::query()
  47. ->select('name')
  48. ->where('enabled', true)
  49. ->whereNull('deleted_at')
  50. ->groupBy('name')
  51. ->orderBy('name')
  52. ->pluck('name')
  53. ->values()
  54. ->toArray();
  55. // levels
  56. $this->filter_options['levels'] = \App\Models\CourseLevel::query()
  57. ->where('enabled', true)
  58. ->orderBy('name')
  59. ->get(['id', 'name'])
  60. ->toArray();
  61. // types
  62. // $this->filter_options['types'] = \App\Models\CourseType::query()
  63. // ->where('enabled', true)
  64. // ->orderBy('name')
  65. // ->get(['id', 'name'])
  66. // ->toArray();
  67. // seasons
  68. $this->filter_options['seasons'] = $this->deriveSeasonOptionsFromCourseDateFrom();
  69. // months
  70. $this->filter_options['months'] = array_map(
  71. fn($m) => ['id' => $m, 'name' => $this->monthLabels()[$m]],
  72. $this->fiscalMonths()
  73. );
  74. }
  75. public function applyFilters()
  76. {
  77. $monthsToShow = $this->fiscalMonths();
  78. if (!empty($this->filters['months'])) {
  79. $selected = array_map('intval', $this->filters['months']);
  80. $monthsToShow = array_values(array_filter(
  81. $monthsToShow,
  82. fn($m) => in_array($m, $selected, true)
  83. ));
  84. }
  85. $labels = array_map(fn($m) => $this->monthLabels()[$m], $monthsToShow);
  86. $selectedSeasons = $this->filters['seasons'] ?? [];
  87. $selectedSeasons = array_values(array_filter($selectedSeasons));
  88. $rows = \App\Models\MemberCourse::query()
  89. ->join('courses', 'courses.id', '=', 'member_courses.course_id')
  90. ->leftJoin('course_levels', 'course_levels.id', '=', 'courses.course_level_id')
  91. // ->leftJoin('course_types', 'course_types.id', '=', 'courses.course_type_id')
  92. ->whereNull('courses.deleted_at')
  93. ->where('courses.enabled', 1)
  94. ->when(!empty($this->filters['courses']), fn($q) => $q->whereIn('courses.name', $this->filters['courses']))
  95. ->when(!empty($this->filters['levels']), fn($q) => $q->whereIn('courses.course_level_id', $this->filters['levels']))
  96. // ->when(!empty($this->filters['types']), fn($q) => $q->whereIn('courses.course_type_id', $this->filters['types']))
  97. ->select([
  98. 'member_courses.id as member_course_id',
  99. 'member_courses.member_id',
  100. 'member_courses.months',
  101. 'member_courses.date_from as member_date_from',
  102. 'member_courses.date_to as member_date_to',
  103. 'courses.id as course_id',
  104. 'courses.name as course_name',
  105. 'courses.date_from',
  106. 'courses.date_to',
  107. 'course_levels.name as level_name',
  108. // 'course_types.name as type_name',
  109. ])
  110. ->get();
  111. // dd($rows->toSql(), $rows->getBindings());
  112. $bucket = [];
  113. foreach ($rows as $r) {
  114. $seasonLabel = $this->fiscalSeasonFromDates($r->date_from, $r->date_to);
  115. if (!empty($selectedSeasons)) {
  116. if (!$seasonLabel || !in_array($seasonLabel, $selectedSeasons, true)) {
  117. continue;
  118. }
  119. }
  120. $activeMonths = [];
  121. $member_date_from = Carbon::parse($r->member_date_from);
  122. $member_date_to = Carbon::parse($r->member_date_to);
  123. $period = CarbonPeriod::create($member_date_from->startOfMonth(), '1 month', $member_date_to->startOfMonth());
  124. foreach ($period as $date) {
  125. $activeMonths[] = $date->month;
  126. }
  127. if (empty($activeMonths)) continue;
  128. if (!empty($this->filters['months'])) {
  129. $activeMonths = array_values(array_intersect($activeMonths, array_map('intval', $this->filters['months'])));
  130. if (empty($activeMonths)) continue;
  131. }
  132. $seriesKey = $this->makeSeriesKey($r, $seasonLabel);
  133. foreach ($activeMonths as $m) {
  134. if (!in_array($m, $monthsToShow, true)) continue;
  135. $bucket[$seriesKey][$m][$r->member_course_id] = true;
  136. }
  137. }
  138. $datasets = [];
  139. foreach ($bucket as $seriesKey => $monthsMap) {
  140. $data = [];
  141. foreach ($monthsToShow as $m) {
  142. $data[] = isset($monthsMap[$m]) ? count($monthsMap[$m]) : 0;
  143. }
  144. $datasets[] = [
  145. 'label' => $seriesKey,
  146. 'data' => $data,
  147. ];
  148. }
  149. $this->chart = [
  150. 'labels' => $labels,
  151. 'datasets' => $datasets,
  152. ];
  153. $this->dispatchBrowserEvent('dynamic-report:updated', [
  154. 'chart' => $this->chart,
  155. ]);
  156. }
  157. private function fiscalSeasonFromDates(?string $dateFrom, ?string $dateTo): ?string
  158. {
  159. if (!$dateFrom) return null;
  160. $from = \Carbon\Carbon::parse($dateFrom);
  161. $to = $dateTo ? \Carbon\Carbon::parse($dateTo) : null;
  162. // sicurezza: se date_to esiste ed è prima di date_from, ignoriamo date_to
  163. if ($to && $to->lessThan($from)) {
  164. $to = null;
  165. }
  166. /**
  167. * Strategia:
  168. * - la stagione è determinata dal "cuore" del corso
  169. * - se date_to esiste, prendiamo il punto medio
  170. * - altrimenti usiamo date_from
  171. */
  172. if ($to) {
  173. $midTimestamp = (int) (($from->timestamp + $to->timestamp) / 2);
  174. $ref = \Carbon\Carbon::createFromTimestamp($midTimestamp);
  175. } else {
  176. $ref = $from;
  177. }
  178. $startYear = ($ref->month >= 9) ? $ref->year : ($ref->year - 1);
  179. return $startYear . '-' . ($startYear + 1);
  180. }
  181. private function makeSeriesKey($r, ?string $seasonLabel): string
  182. {
  183. $parts = [];
  184. if (!empty($this->filters['courses'])) $parts[] = $r->course_name ?? '';
  185. if (!empty($this->filters['levels'])) $parts[] = $r->level_name ?? '';
  186. // if (!empty($this->filters['types'])) $parts[] = $r->type_name ?? '';
  187. if (!empty($this->filters['seasons']) && $seasonLabel) $parts[] = $seasonLabel;
  188. if (empty(array_filter($parts))) {
  189. $parts[] = $r->course_name ?? 'Totale';
  190. }
  191. $parts = array_values(array_filter($parts, fn($p) => trim($p) !== ''));
  192. return implode(' - ', $parts);
  193. }
  194. private function deriveSeasonOptionsFromCourseDateFrom()
  195. {
  196. $dates = \App\Models\Course::query()
  197. ->where('enabled', 1)
  198. ->whereNull('deleted_at')
  199. ->whereNotNull('date_from')
  200. ->whereNotNull('date_to')
  201. ->pluck('date_to', 'date_from')
  202. ->all();
  203. $seasons = [];
  204. foreach ($dates as $d_from => $d_to) {
  205. $s = $this->fiscalSeasonFromDates($d_from, $d_to);
  206. if ($s) $seasons[$s] = true;
  207. }
  208. $out = array_keys($seasons);
  209. rsort($out);
  210. return $out;
  211. }
  212. private function fiscalMonths()
  213. {
  214. return [9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8];
  215. }
  216. private function monthLabels()
  217. {
  218. return [
  219. 1 => 'Gen',
  220. 2 => 'Feb',
  221. 3 => 'Mar',
  222. 4 => 'Apr',
  223. 5 => 'Mag',
  224. 6 => 'Giu',
  225. 7 => 'Lug',
  226. 8 => 'Ago',
  227. 9 => 'Set',
  228. 10 => 'Ott',
  229. 11 => 'Nov',
  230. 12 => 'Dic',
  231. ];
  232. }
  233. public function render()
  234. {
  235. return view('livewire.dynamic_report');
  236. }
  237. }