AbsenceReport.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. <?php
  2. namespace App\Http\Livewire;
  3. use App\Http\Middleware\TenantMiddleware;
  4. use Illuminate\Support\Carbon;
  5. use Livewire\Component;
  6. class AbsenceReport extends Component
  7. {
  8. public $records;
  9. public $record_assenze;
  10. public $original_record_assenze;
  11. public $fiscalStartMonth = 9;
  12. public $year;
  13. public $search;
  14. public function boot()
  15. {
  16. app(TenantMiddleware::class)->setupTenantConnection();
  17. }
  18. public function mount()
  19. {
  20. setlocale(LC_ALL, 'it_IT');
  21. $this->record_assenze = [];
  22. $end_day = now()->yesterday();
  23. $limit = $end_day->endOfDay();
  24. $this->year = ($end_day->month >= $this->fiscalStartMonth) ? $end_day->year : $end_day->year - 1;
  25. $dayMap = [
  26. 'lun' => 1,
  27. 'mar' => 2,
  28. 'mer' => 3,
  29. 'gio' => 4,
  30. 'ven' => 5,
  31. 'sab' => 6,
  32. 'dom' => 0,
  33. ];
  34. try {
  35. $courses = \App\Models\Course::whereDate('date_from', '<=', $limit)
  36. ->whereDate('date_to', '>=', $limit)
  37. ->where('active', true)
  38. ->where('enabled', true)
  39. ->get();
  40. foreach ($courses as $course) {
  41. $course_members = \App\Models\MemberCourse::with(['member' => function ($q) {
  42. $q->where(function ($q) {
  43. $q->where('is_archived', false)->orWhereNull('is_archived');
  44. })->where(function ($q) {
  45. $q->where('is_deleted', false)->orWhereNull('is_deleted');
  46. });
  47. }])
  48. ->where('course_id', $course->id)
  49. ->whereHas('member', function ($q) {
  50. $q->where(function ($q) {
  51. $q->where('is_archived', false)->orWhereNull('is_archived');
  52. })->where(function ($q) {
  53. $q->where('is_deleted', false)->orWhereNull('is_deleted');
  54. });
  55. })
  56. ->whereDate('date_from', '<=', $limit)
  57. ->whereDate('date_to', '>=', Carbon::parse($course->date_from)->startOfDay())
  58. ->get();
  59. if ($course_members->isEmpty()) {
  60. continue;
  61. }
  62. $courseCalendars = \App\Models\Calendar::where('course_id', $course->id)->where('from', '<=', $limit)->where('status', '<>', 99)->get();
  63. if ($courseCalendars->isEmpty()) {
  64. continue;
  65. }
  66. $calendarIndex = [];
  67. foreach ($courseCalendars as $cal) {
  68. $fromKey = Carbon::parse($cal->from)->toDateTimeString();
  69. $toKey = Carbon::parse($cal->to)->toDateTimeString();
  70. $key = $fromKey . '|' . $toKey;
  71. $calendarIndex[$key] = $cal;
  72. }
  73. $memberIds = $course_members->pluck('member_id')->unique()->values()->all();
  74. $presences = \App\Models\Presence::query()
  75. ->whereIn('calendar_id', $courseCalendars->pluck('id')->all())
  76. ->whereIn('member_id', $memberIds)
  77. ->where('status', '<>', 99)
  78. ->get(['member_id', 'calendar_id']);
  79. $presenceIndex = [];
  80. foreach ($presences as $p) {
  81. $presenceIndex[$p->member_id . '|' . $p->calendar_id] = true;
  82. }
  83. $makeups = \App\Models\Presence::query()
  84. ->join('calendars', 'presences.calendar_id', '=', 'calendars.id')
  85. ->where('presences.motivation_course_id', $course->id)
  86. ->whereIn('presences.member_id', $memberIds)
  87. ->where('presences.status', '<>', 99)
  88. ->where('calendars.from', '<=', $limit)
  89. ->where('calendars.status', '<>', 99)
  90. ->selectRaw('presences.member_id, MAX(calendars.from) as last_makeup_from')
  91. ->groupBy('presences.member_id')
  92. ->get();
  93. $lastMakeupByMember = [];
  94. foreach ($makeups as $m) {
  95. $lastMakeupByMember[$m->member_id] = Carbon::parse($m->last_makeup_from);
  96. }
  97. $this->record_assenze[$course->id] = [
  98. 'course' => [
  99. 'id' => $course->id,
  100. 'name' => $course->getDetailsName(),
  101. ],
  102. 'members' => [],
  103. ];
  104. $courseWhen = json_decode($course->when ?? '[]', true);
  105. if (!is_array($courseWhen) || empty($courseWhen)) {
  106. unset($this->record_assenze[$course->id]);
  107. continue;
  108. }
  109. foreach ($course_members as $course_member) {
  110. $this->record_assenze[$course->id]['members'][$course_member->id] = [
  111. 'member' => [
  112. 'id' => $course_member->member->id,
  113. 'first_name' => $course_member->member->first_name,
  114. 'last_name' => $course_member->member->last_name,
  115. ],
  116. 'count' => 0,
  117. 'dates' => [],
  118. ];
  119. $mid = $course_member->member_id;
  120. $rangeStart = Carbon::parse(max($course->date_from, $course_member->date_from))->startOfDay();
  121. $rangeEnd = Carbon::parse(min($course->date_to, $course_member->date_to, $limit->toDateString()))->endOfDay();
  122. if ($rangeStart->gt($rangeEnd)) {
  123. unset($this->record_assenze[$course->id]['members'][$course_member->id]);
  124. continue;
  125. }
  126. $memberCalendars = [];
  127. foreach ($courseWhen as $period) {
  128. $days = $period['day'] ?? [];
  129. if (empty($days)) continue;
  130. $fromTime = $period['from'] ?? null;
  131. $toTime = $period['to'] ?? null;
  132. if (!$fromTime || !$toTime) continue;
  133. $ranges = $this->generateDateRanges(
  134. $rangeStart,
  135. $rangeEnd,
  136. $days,
  137. $dayMap,
  138. $fromTime,
  139. $toTime
  140. );
  141. foreach ($ranges as $range) {
  142. $key = $range['from']->toDateTimeString() . '|' . $range['to']->toDateTimeString();
  143. if (isset($calendarIndex[$key])) {
  144. $memberCalendars[] = $calendarIndex[$key];
  145. }
  146. }
  147. }
  148. if (empty($memberCalendars)) {
  149. unset($this->record_assenze[$course->id]['members'][$course_member->id]);
  150. continue;
  151. }
  152. usort($memberCalendars, fn($a, $b) => $b->to <=> $a->to);
  153. $lastAttendance = $lastMakeupByMember[$mid] ?? null;
  154. foreach ($memberCalendars as $calendar) {
  155. $lessonFrom = Carbon::parse($calendar->from);
  156. if ($lastAttendance && $lessonFrom->lte($lastAttendance)) {
  157. break;
  158. }
  159. $pKey = $mid . '|' . $calendar->id;
  160. if (isset($presenceIndex[$pKey])) {
  161. break;
  162. }
  163. // assenza vera
  164. $this->record_assenze[$course->id]['members'][$course_member->id]['count']++;
  165. $this->record_assenze[$course->id]['members'][$course_member->id]['dates'][] = [
  166. 'calendar_id' => $calendar->id,
  167. 'date' => $lessonFrom->translatedFormat('d/m'),
  168. ];
  169. }
  170. if ($this->record_assenze[$course->id]['members'][$course_member->id]['count'] < 2) {
  171. unset($this->record_assenze[$course->id]['members'][$course_member->id]);
  172. }
  173. }
  174. if (empty($this->record_assenze[$course->id]['members'])) {
  175. unset($this->record_assenze[$course->id]);
  176. } else {
  177. $members = $this->record_assenze[$course->id]['members'];
  178. usort($members, function ($a, $b) {
  179. if ($a['count'] !== $b['count']) {
  180. return $b['count'] <=> $a['count'];
  181. }
  182. $last = strcmp($a['member']['last_name'], $b['member']['last_name']);
  183. if ($last !== 0) return $last;
  184. return strcmp($a['member']['first_name'], $b['member']['first_name']);
  185. });
  186. $this->record_assenze[$course->id]['members'] = $members;
  187. }
  188. }
  189. usort($this->record_assenze, fn($a, $b) => $a['course']['name'] <=> $b['course']['name']);
  190. } catch (\Throwable $e) {
  191. dd($e->getMessage());
  192. }
  193. $this->original_record_assenze = $this->record_assenze;
  194. }
  195. public function render()
  196. {
  197. setlocale(LC_ALL, 'it_IT');
  198. return view('livewire.absence_report');
  199. }
  200. public function applySearch()
  201. {
  202. $this->search = trim($this->search);
  203. $this->filter();
  204. }
  205. public function resetSearch()
  206. {
  207. $this->search = '';
  208. $this->record_assenze = $this->original_record_assenze;
  209. }
  210. protected function filter()
  211. {
  212. $this->record_assenze = $this->original_record_assenze;
  213. if ($this->search === '') return;
  214. $needle = mb_strtolower($this->search);
  215. foreach ($this->record_assenze as $courseId => $courseData) {
  216. $courseData['members'] = array_values(array_filter($courseData['members'], function ($m) use ($needle) {
  217. $full = mb_strtolower((trim($m['member']['last_name']) ?? '') . ' ' . (trim($m['member']['first_name']) ?? ''));
  218. $full2 = mb_strtolower((trim($m['member']['first_name']) ?? '') . ' ' . (trim($m['member']['last_name']) ?? ''));
  219. return str_contains($full, $needle) || str_contains($full2, $needle);
  220. }));
  221. if (empty($courseData['members'])) {
  222. unset($this->record_assenze[$courseId]);
  223. } else {
  224. $this->record_assenze[$courseId] = $courseData;
  225. }
  226. }
  227. }
  228. protected function generateDateRanges($rangeStart, $rangeEnd, $days, $dayMap, $fromTime, $toTime)
  229. {
  230. $allowedDow = collect($days)
  231. ->map(fn($d) => $dayMap[$d] ?? null)
  232. ->filter(fn($v) => $v !== null)
  233. ->unique()
  234. ->values()
  235. ->all();
  236. // parse orari
  237. $fromC = Carbon::parse($fromTime);
  238. $toC = Carbon::parse($toTime);
  239. $fromHour = $fromC->hour;
  240. $fromMinute = $fromC->minute;
  241. $fromSecond = $fromC->second;
  242. $toHour = $toC->hour;
  243. $toMinute = $toC->minute;
  244. $toSecond = $toC->second;
  245. $ranges = [];
  246. foreach ($allowedDow as $dow) {
  247. $current = $rangeStart->copy()->startOfDay();
  248. $offset = ($dow - $current->dayOfWeek + 7) % 7;
  249. $current->addDays($offset);
  250. while ($current->lte($rangeEnd)) {
  251. $fromDateTime = $current->copy()->setTime($fromHour, $fromMinute, $fromSecond);
  252. $toDateTime = $current->copy()->setTime($toHour, $toMinute, $toSecond);
  253. if ($fromDateTime->lt($rangeStart)) {
  254. $current->addWeek();
  255. continue;
  256. }
  257. if ($fromDateTime->gt($rangeEnd)) {
  258. break;
  259. }
  260. $ranges[] = [
  261. 'from' => $fromDateTime,
  262. 'to' => $toDateTime,
  263. ];
  264. $current->addWeek();
  265. }
  266. }
  267. usort($ranges, fn($a, $b) => $a['from'] <=> $b['from']);
  268. return $ranges;
  269. }
  270. }