DynamicReport.php 8.5 KB

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