Record.php 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120
  1. <?php
  2. namespace App\Http\Livewire;
  3. use Livewire\Component;
  4. use DateInterval;
  5. use DatePeriod;
  6. use DateTime;
  7. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  8. use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
  9. use Illuminate\Support\Facades\Storage;
  10. use Illuminate\Support\Facades\Log;
  11. use Illuminate\Support\Facades\Mail;
  12. use App\Mail\ExportNotification;
  13. use App\Jobs\ExportPrimaNota;
  14. class Record extends Component
  15. {
  16. public $records, $dataId, $totals;
  17. public $in;
  18. public $out;
  19. public $payments = [];
  20. public $fromDate;
  21. public $toDate;
  22. public $appliedFromDate;
  23. public $appliedToDate;
  24. public $exportFromDate;
  25. public $exportToDate;
  26. public $isExporting = false;
  27. public $selectedPeriod = 'OGGI';
  28. public $filterCausals = null;
  29. public $filterMember = null;
  30. public $isFiltering = false;
  31. public array $recordDatas = [];
  32. public array $labels = [];
  33. public array $causals = [];
  34. public $members = array();
  35. public $sendViaEmail = false;
  36. public $exportEmailAddress = '';
  37. public $exportEmailSubject = 'Prima Nota - Export';
  38. protected $rules = [
  39. 'exportEmailAddress' => 'required_if:sendViaEmail,true|email',
  40. 'exportEmailSubject' => 'required_if:sendViaEmail,true|string|max:255',
  41. ];
  42. protected $messages = [
  43. 'exportEmailAddress.required_if' => 'L\'indirizzo email è obbligatorio quando si sceglie di inviare via email.',
  44. 'exportEmailAddress.email' => 'Inserisci un indirizzo email valido.',
  45. 'exportEmailSubject.required_if' => 'L\'oggetto dell\'email è obbligatorio.',
  46. 'exportEmailSubject.max' => 'L\'oggetto dell\'email non può superare i 255 caratteri.',
  47. ];
  48. public function hydrate()
  49. {
  50. $this->emit('load-select');
  51. }
  52. public function mount()
  53. {
  54. $this->fromDate = date("Y-m-d");
  55. $this->toDate = date("Y-m-d");
  56. $this->appliedFromDate = date("Y-m-d");
  57. $this->appliedToDate = date("Y-m-d");
  58. $this->exportFromDate = date("Y-m-d");
  59. $this->exportToDate = date("Y-m-d");
  60. $this->exportEmailSubject = 'Prima Nota - Export del ' . date('d/m/Y');
  61. $this->getCausals(\App\Models\Causal::select('id', 'name')->where('parent_id', null)->get(), 0);
  62. $this->members = \App\Models\Member::select(['id', 'first_name', 'last_name', 'fiscal_code'])->orderBy('last_name')->orderBy('first_name')->get();
  63. $this->payments = \App\Models\PaymentMethod::select('id', 'name', 'type')->where('enabled', true)->where('money', false)->get();
  64. }
  65. private function generateExportData($fromDate, $toDate)
  66. {
  67. $exportRecords = array();
  68. $exportTotals = array();
  69. $exclude_from_records = \App\Models\Member::where('exclude_from_records', true)->pluck('id')->toArray();
  70. $datas = \App\Models\Record::with('member', 'supplier', 'payment_method')
  71. ->select(
  72. 'records.*',
  73. 'records_rows.id as row_id',
  74. 'records_rows.record_id',
  75. 'records_rows.causal_id',
  76. 'records_rows.amount',
  77. 'records_rows.note',
  78. 'records_rows.when',
  79. 'records_rows.vat_id',
  80. 'records_rows.created_at as row_created_at',
  81. 'records_rows.updated_at as row_updated_at'
  82. )
  83. ->join('records_rows', 'records.id', '=', 'records_rows.record_id')
  84. ->whereBetween('date', [$fromDate, $toDate])
  85. ->where(function ($query) {
  86. $query->where('type', 'OUT')
  87. ->orWhere(function ($query) {
  88. $query->where('records.corrispettivo_fiscale', true)
  89. ->orWhere('records.commercial', false);
  90. });
  91. })
  92. ->where(function ($query) use ($exclude_from_records) {
  93. $query->where('type', 'OUT')
  94. ->orWhere(function ($subquery) use ($exclude_from_records) {
  95. $subquery->whereNotIn('member_id', $exclude_from_records);
  96. });
  97. });
  98. if ($this->filterCausals != null && sizeof($this->filterCausals) > 0) {
  99. $causals = array();
  100. foreach ($this->filterCausals as $z) {
  101. $causals[] = $z;
  102. $childs = \App\Models\Causal::where('parent_id', $z)->get();
  103. foreach ($childs as $c) {
  104. $causals[] = $c->id;
  105. $childsX = \App\Models\Causal::where('parent_id', $c->id)->get();
  106. foreach ($childsX as $cX) {
  107. $causals[] = $cX->id;
  108. }
  109. }
  110. }
  111. $datas->whereIn('causal_id', $causals);
  112. }
  113. if ($this->filterMember != null && $this->filterMember > 0) {
  114. $datas->where('member_id', $this->filterMember);
  115. }
  116. $datas = $datas->orderBy('date', 'ASC')
  117. ->orderBy('records.created_at', 'ASC')
  118. ->orderBy('records_rows.id', 'ASC')
  119. ->get();
  120. $groupedData = [];
  121. $causalsCount = [];
  122. foreach ($datas as $idx => $data) {
  123. $causalCheck = \App\Models\Causal::findOrFail($data->causal_id);
  124. $paymentCheck = $data->payment_method->money;
  125. if (!$paymentCheck && ($causalCheck->no_first == null || !$causalCheck->no_first)) {
  126. if (!$data->deleted) {
  127. $amount = $data->amount;
  128. $amount += getVatValue($amount, $data->vat_id);
  129. } else {
  130. $amount = $data->amount;
  131. }
  132. $isCommercial = ($data->commercial == 1 || $data->commercial === '1' || $data->commercial === true);
  133. $typeLabel = $isCommercial ? 'Commerciale' : 'Non Commerciale';
  134. $nominativo = '';
  135. if ($data->type == "IN") {
  136. if ($data->member) {
  137. $nominativo = $data->member->last_name . " " . $data->member->first_name;
  138. }
  139. } else {
  140. if ($data->supplier) {
  141. $nominativo = $data->supplier->name;
  142. }
  143. }
  144. $groupKey = $data->date . '|' . $typeLabel . '|' . $data->payment_method->name . '|' . $data->type . '|' . $nominativo;
  145. if (!isset($groupedData[$groupKey])) {
  146. $groupedData[$groupKey] = [
  147. 'date' => $data->date,
  148. 'type_label' => $typeLabel,
  149. 'payment_method' => $data->payment_method->name,
  150. 'transaction_type' => $data->type,
  151. 'nominativo' => $nominativo,
  152. 'amount' => 0,
  153. 'deleted' => false,
  154. 'causals' => [],
  155. 'notes' => []
  156. ];
  157. $causalsCount[$groupKey] = [];
  158. }
  159. $groupedData[$groupKey]['amount'] += $amount;
  160. $causalsCount[$groupKey][$causalCheck->getTree()] = true;
  161. if (!empty($data->note)) {
  162. $groupedData[$groupKey]['notes'][] = $data->note;
  163. }
  164. if ($data->deleted) {
  165. $groupedData[$groupKey]['deleted'] = true;
  166. }
  167. }
  168. }
  169. foreach ($groupedData as $groupKey => $group) {
  170. $causalsInGroup = array_keys($causalsCount[$groupKey]);
  171. $causalDisplay = $group['type_label'];
  172. if (count($causalsInGroup) > 1) {
  173. $detailDisplay = 'Varie|' . implode('|', $causalsInGroup);
  174. } else {
  175. $detailDisplay = $causalsInGroup[0];
  176. }
  177. $recordKey = $group['date'] . "§" . $causalDisplay . "§" . $group['nominativo'] . "§" . $detailDisplay . "§" . ($group['deleted'] ? 'DELETED' : '') . "§";
  178. if (!isset($exportRecords[$recordKey][$group['payment_method']][$group['transaction_type']])) {
  179. $exportRecords[$recordKey][$group['payment_method']][$group['transaction_type']] = 0;
  180. }
  181. $exportRecords[$recordKey][$group['payment_method']][$group['transaction_type']] += $group['amount'];
  182. if (!isset($exportTotals[$group['payment_method']])) {
  183. $exportTotals[$group['payment_method']]["IN"] = 0;
  184. $exportTotals[$group['payment_method']]["OUT"] = 0;
  185. }
  186. if (!$group['deleted'])
  187. $exportTotals[$group['payment_method']][$group['transaction_type']] += $group['amount'];
  188. }
  189. return $exportRecords;
  190. }
  191. private function generateExportTotals($fromDate, $toDate)
  192. {
  193. $exportTotals = array();
  194. $exclude_from_records = \App\Models\Member::where('exclude_from_records', true)->pluck('id')->toArray();
  195. $datas = \App\Models\Record::with('member', 'supplier', 'payment_method')
  196. ->select(
  197. 'records.*',
  198. 'records_rows.id as row_id',
  199. 'records_rows.record_id',
  200. 'records_rows.causal_id',
  201. 'records_rows.amount',
  202. 'records_rows.note',
  203. 'records_rows.when',
  204. 'records_rows.vat_id',
  205. )
  206. ->join('records_rows', 'records.id', '=', 'records_rows.record_id')
  207. ->whereBetween('date', [$fromDate, $toDate])
  208. ->where(function ($query) {
  209. $query->where('type', 'OUT')
  210. ->orWhere(function ($query) {
  211. $query->where('records.corrispettivo_fiscale', true)
  212. ->orWhere('records.commercial', false);
  213. });
  214. })
  215. ->where(function ($query) use ($exclude_from_records) {
  216. $query->where('type', 'OUT')
  217. ->orWhere(function ($subquery) use ($exclude_from_records) {
  218. $subquery->whereNotIn('member_id', $exclude_from_records);
  219. });
  220. });
  221. if ($this->filterCausals != null && sizeof($this->filterCausals) > 0) {
  222. $causals = array();
  223. foreach ($this->filterCausals as $z) {
  224. $causals[] = $z;
  225. $childs = \App\Models\Causal::where('parent_id', $z)->get();
  226. foreach ($childs as $c) {
  227. $causals[] = $c->id;
  228. $childsX = \App\Models\Causal::where('parent_id', $c->id)->get();
  229. foreach ($childsX as $cX) {
  230. $causals[] = $cX->id;
  231. }
  232. }
  233. }
  234. $datas->whereIn('causal_id', $causals);
  235. }
  236. if ($this->filterMember != null && $this->filterMember > 0) {
  237. $datas->where('member_id', $this->filterMember);
  238. }
  239. $datas = $datas->orderBy('date', 'ASC')
  240. ->orderBy('records.created_at', 'ASC')
  241. ->orderBy('records_rows.id', 'ASC')
  242. ->get();
  243. foreach ($datas as $data) {
  244. $causalCheck = \App\Models\Causal::findOrFail($data->causal_id);
  245. $paymentCheck = $data->payment_method->money;
  246. if (!$paymentCheck && ($causalCheck->no_first == null || !$causalCheck->no_first)) {
  247. if (!$data->deleted) {
  248. $amount = $data->amount;
  249. $amount += getVatValue($amount, $data->vat_id);
  250. } else {
  251. $amount = $data->amount;
  252. }
  253. if (!isset($exportTotals[$data->payment_method->name])) {
  254. $exportTotals[$data->payment_method->name]["IN"] = 0;
  255. $exportTotals[$data->payment_method->name]["OUT"] = 0;
  256. }
  257. if (!$data->deleted)
  258. $exportTotals[$data->payment_method->name][$data->type] += $amount;
  259. }
  260. }
  261. return $exportTotals;
  262. }
  263. public function resetFilters()
  264. {
  265. $this->selectedPeriod = 'OGGI';
  266. $this->filterCausals = [];
  267. $this->filterMember = null;
  268. $today = date("Y-m-d");
  269. $this->fromDate = $today;
  270. $this->toDate = $today;
  271. $this->appliedFromDate = $today;
  272. $this->appliedToDate = $today;
  273. $this->emit('filters-reset');
  274. }
  275. public function applyFilters()
  276. {
  277. $this->isFiltering = true;
  278. $this->setPeriodDates();
  279. $this->appliedFromDate = $this->fromDate;
  280. $this->appliedToDate = $this->toDate;
  281. $this->render();
  282. $this->isFiltering = false;
  283. $this->emit('filters-applied');
  284. }
  285. private function setPeriodDates()
  286. {
  287. $today = now();
  288. switch ($this->selectedPeriod) {
  289. case 'OGGI':
  290. $this->fromDate = $today->format('Y-m-d');
  291. $this->toDate = $today->format('Y-m-d');
  292. break;
  293. case 'IERI':
  294. $yesterday = $today->copy()->subDay();
  295. $this->fromDate = $yesterday->format('Y-m-d');
  296. $this->toDate = $yesterday->format('Y-m-d');
  297. break;
  298. case 'MESE CORRENTE':
  299. $this->fromDate = $today->copy()->startOfMonth()->format('Y-m-d');
  300. $this->toDate = $today->copy()->endOfMonth()->format('Y-m-d');
  301. break;
  302. case 'MESE PRECEDENTE':
  303. $lastMonth = $today->copy()->subMonth();
  304. $this->fromDate = $lastMonth->startOfMonth()->format('Y-m-d');
  305. $this->toDate = $lastMonth->endOfMonth()->format('Y-m-d');
  306. break;
  307. case 'ULTIMO TRIMESTRE':
  308. $this->fromDate = $today->copy()->subMonths(3)->format('Y-m-d');
  309. $this->toDate = $today->format('Y-m-d');
  310. break;
  311. case 'ULTIMO QUADRIMESTRE':
  312. $this->fromDate = $today->copy()->subMonths(4)->format('Y-m-d');
  313. $this->toDate = $today->format('Y-m-d');
  314. break;
  315. }
  316. }
  317. public function getCausals($records, $indentation)
  318. {
  319. foreach ($records as $record) {
  320. $this->causals[] = array('id' => $record->id, 'name' => $record->getTree(), 'text' => $record->getTree(), 'level' => $indentation);
  321. if (count($record->childs))
  322. $this->getCausals($record->childs, $indentation + 1);
  323. }
  324. }
  325. public function getMonth($m)
  326. {
  327. $ret = '';
  328. switch ($m) {
  329. case 1:
  330. $ret = 'Gennaio';
  331. break;
  332. case 2:
  333. $ret = 'Febbraio';
  334. break;
  335. case 3:
  336. $ret = 'Marzo';
  337. break;
  338. case 4:
  339. $ret = 'Aprile';
  340. break;
  341. case 5:
  342. $ret = 'Maggio';
  343. break;
  344. case 6:
  345. $ret = 'Giugno';
  346. break;
  347. case 7:
  348. $ret = 'Luglio';
  349. break;
  350. case 8:
  351. $ret = 'Agosto';
  352. break;
  353. case 9:
  354. $ret = 'Settembre';
  355. break;
  356. case 10:
  357. $ret = 'Ottobre';
  358. break;
  359. case 11:
  360. $ret = 'Novembre';
  361. break;
  362. case 12:
  363. $ret = 'Dicembre';
  364. break;
  365. default:
  366. $ret = '';
  367. break;
  368. }
  369. return $ret;
  370. }
  371. public function render()
  372. {
  373. $month = 0;
  374. $year = 0;
  375. $this->records = array();
  376. $this->totals = array();
  377. $exclude_from_records = \App\Models\Member::where('exclude_from_records', true)->pluck('id')->toArray();
  378. $datas = \App\Models\Record::with('member', 'supplier', 'payment_method')
  379. ->select(
  380. 'records.*',
  381. 'records_rows.id as row_id',
  382. 'records_rows.record_id',
  383. 'records_rows.causal_id',
  384. 'records_rows.amount',
  385. 'records_rows.note',
  386. 'records_rows.when',
  387. 'records_rows.vat_id',
  388. )
  389. ->join('records_rows', 'records.id', '=', 'records_rows.record_id')
  390. ->whereBetween('date', [$this->appliedFromDate, $this->appliedToDate])
  391. ->where(function ($query) {
  392. $query->where('type', 'OUT')
  393. ->orWhere(function ($query) {
  394. $query->where('records.corrispettivo_fiscale', true)
  395. ->orWhere('records.commercial', false);
  396. });
  397. })
  398. ->where(function ($query) use ($exclude_from_records) {
  399. $query->where('type', 'OUT')
  400. ->orWhere(function ($subquery) use ($exclude_from_records) {
  401. $subquery->whereNotIn('member_id', $exclude_from_records);
  402. });
  403. });
  404. if ($this->filterCausals != null && sizeof($this->filterCausals) > 0) {
  405. $causals = array();
  406. foreach ($this->filterCausals as $z) {
  407. $causals[] = $z;
  408. $childs = \App\Models\Causal::where('parent_id', $z)->get();
  409. foreach ($childs as $c) {
  410. $causals[] = $c->id;
  411. $childsX = \App\Models\Causal::where('parent_id', $c->id)->get();
  412. foreach ($childsX as $cX) {
  413. $causals[] = $cX->id;
  414. }
  415. }
  416. }
  417. $datas->whereIn('causal_id', $causals);
  418. }
  419. if ($this->filterMember != null && $this->filterMember > 0) {
  420. $datas->where('member_id', $this->filterMember);
  421. }
  422. $datas = $datas->orderBy('date', 'ASC')
  423. ->orderBy('records.created_at', 'ASC')
  424. ->orderBy('records_rows.id', 'ASC')
  425. ->get();
  426. $groupedData = [];
  427. $causalsCount = [];
  428. $nominativi = [];
  429. foreach ($datas as $idx => $data) {
  430. $causalCheck = \App\Models\Causal::findOrFail($data->causal_id);
  431. $paymentCheck = $data->payment_method->money;
  432. if (!$paymentCheck && ($causalCheck->no_first == null || !$causalCheck->no_first)) {
  433. if (!$data->deleted) {
  434. $amount = $data->amount;
  435. $amount += getVatValue($amount, $data->vat_id);
  436. } else {
  437. $amount = $data->amount;
  438. }
  439. $isCommercial = ($data->commercial == 1 || $data->commercial === '1' || $data->commercial === true);
  440. $typeLabel = $isCommercial ? 'Commerciale' : 'Non Commerciale';
  441. $nominativo = '';
  442. if ($data->type == "IN") {
  443. if ($data->member) {
  444. $nominativo = $data->member->last_name . " " . $data->member->first_name;
  445. }
  446. } else {
  447. if ($data->supplier) {
  448. $nominativo = $data->supplier->name;
  449. }
  450. }
  451. $groupKey = $data->date . '|' . $typeLabel . '|' . $data->payment_method->name . '|' . $data->type . '|' . $nominativo;
  452. if (!isset($groupedData[$groupKey])) {
  453. $groupedData[$groupKey] = [
  454. 'date' => $data->date,
  455. 'type_label' => $typeLabel,
  456. 'payment_method' => $data->payment_method->name,
  457. 'transaction_type' => $data->type,
  458. 'nominativo' => $nominativo,
  459. 'amount' => 0,
  460. 'deleted' => false,
  461. 'causals' => [],
  462. 'notes' => []
  463. ];
  464. $causalsCount[$groupKey] = [];
  465. $nominativi[$groupKey] = $nominativo;
  466. }
  467. $groupedData[$groupKey]['amount'] += $amount;
  468. $causalsCount[$groupKey][$causalCheck->getTree()] = true;
  469. if (!empty($data->note)) {
  470. $groupedData[$groupKey]['notes'][] = $data->note;
  471. }
  472. if ($data->deleted) {
  473. $groupedData[$groupKey]['deleted'] = true;
  474. }
  475. }
  476. }
  477. foreach ($groupedData as $groupKey => $group) {
  478. $causalsInGroup = array_keys($causalsCount[$groupKey]);
  479. $causalDisplay = $group['type_label'];
  480. if (count($causalsInGroup) > 1) {
  481. $detailDisplay = 'Varie|' . implode('|', $causalsInGroup);
  482. } else {
  483. $detailDisplay = $causalsInGroup[0];
  484. }
  485. $recordKey = $group['date'] . "§" . $causalDisplay . "§" . $group['nominativo'] . "§" . $detailDisplay . "§" . ($group['deleted'] ? 'DELETED' : '') . "§";
  486. if (!isset($this->records[$recordKey][$group['payment_method']][$group['transaction_type']])) {
  487. $this->records[$recordKey][$group['payment_method']][$group['transaction_type']] = 0;
  488. }
  489. $this->records[$recordKey][$group['payment_method']][$group['transaction_type']] += $group['amount'];
  490. if (!isset($this->totals[$group['payment_method']])) {
  491. $this->totals[$group['payment_method']]["IN"] = 0;
  492. $this->totals[$group['payment_method']]["OUT"] = 0;
  493. }
  494. if (!$group['deleted'])
  495. $this->totals[$group['payment_method']][$group['transaction_type']] += $group['amount'];
  496. }
  497. return view('livewire.records');
  498. }
  499. private function getLabels($fromDate, $toDate)
  500. {
  501. $begin = new DateTime($fromDate);
  502. $end = new DateTime($toDate);
  503. $interval = DateInterval::createFromDateString('1 day');
  504. $date_range = new DatePeriod($begin, $interval, $end);
  505. foreach ($date_range as $date) {
  506. $labels[] = $date->format('d/M');
  507. }
  508. return $labels;
  509. }
  510. private function getRecordData($type, $fromDate, $toDate)
  511. {
  512. $data = [];
  513. $begin = new DateTime($fromDate);
  514. $end = new DateTime($toDate);
  515. $interval = DateInterval::createFromDateString('1 day');
  516. $date_range = new DatePeriod($begin, $interval, $end);
  517. foreach ($date_range as $date) {
  518. if ($type == 'IN') {
  519. $found = false;
  520. foreach ($this->in as $in) {
  521. if (date("Y-m-d", strtotime($in->date)) == $date->format('Y-m-d')) {
  522. $data[] = number_format($in->total, 0, "", "");
  523. $found = true;
  524. }
  525. }
  526. if (!$found)
  527. $data[] = 0;
  528. }
  529. if ($type == 'OUT') {
  530. $found = false;
  531. foreach ($this->out as $out) {
  532. if (date("Y-m-d", strtotime($out->date)) == $date->format('Y-m-d')) {
  533. $data[] = number_format($out->total, 0, "", "");
  534. $found = true;
  535. }
  536. }
  537. if (!$found)
  538. $data[] = 0;
  539. }
  540. }
  541. return $data;
  542. }
  543. public function openExportModal()
  544. {
  545. $this->exportFromDate = $this->appliedFromDate;
  546. $this->exportToDate = $this->appliedToDate;
  547. // Reset email options
  548. $this->sendViaEmail = false;
  549. $this->exportEmailAddress = $this->getPreferredEmail();
  550. $this->updateEmailSubject();
  551. $this->emit('show-export-modal');
  552. }
  553. public function exportWithDateRange()
  554. {
  555. $this->isExporting = true;
  556. $this->emit('$refresh'); // This forces Livewire to re-render
  557. // Add a small delay to allow the view to update
  558. usleep(100000);
  559. if ($this->sendViaEmail) {
  560. $this->validate([
  561. 'exportEmailAddress' => 'required|email',
  562. 'exportEmailSubject' => 'required|string|max:255',
  563. ]);
  564. }
  565. $this->isExporting = true;
  566. try {
  567. $exportRecords = $this->generateExportData($this->exportFromDate, $this->exportToDate);
  568. $exportTotals = $this->generateExportTotals($this->exportFromDate, $this->exportToDate);
  569. if ($this->sendViaEmail) {
  570. // Dispatch job to background queue
  571. $this->dispatchExportJob($exportRecords, $exportTotals);
  572. } else {
  573. // Direct download (synchronous)
  574. return $this->exportWithData($exportRecords, $exportTotals);
  575. }
  576. } catch (\Illuminate\Validation\ValidationException $e) {
  577. $this->isExporting = false;
  578. throw $e;
  579. } catch (\Exception $e) {
  580. $this->isExporting = false;
  581. Log::error('Export error: ' . $e->getMessage());
  582. if ($this->sendViaEmail) {
  583. $this->emit('export-email-error', 'Errore durante l\'invio dell\'email: ' . $e->getMessage());
  584. } else {
  585. session()->flash('error', 'Errore durante l\'export: ' . $e->getMessage());
  586. }
  587. } finally {
  588. $this->isExporting = false;
  589. $this->emit('export-complete');
  590. $this->emit('hide-export-modal');
  591. }
  592. }
  593. private function getMemberName($memberId)
  594. {
  595. $member = \App\Models\Member::find($memberId);
  596. return $member ? $member->last_name . ' ' . $member->first_name : 'Sconosciuto';
  597. }
  598. private function getCausalsNames($causalIds)
  599. {
  600. if (!is_array($causalIds)) {
  601. return null;
  602. }
  603. $causals = \App\Models\Causal::whereIn('id', $causalIds)->pluck('name')->toArray();
  604. return implode(', ', $causals);
  605. }
  606. public function updatedExportFromDate()
  607. {
  608. $this->updateEmailSubject();
  609. }
  610. public function updatedExportToDate()
  611. {
  612. $this->updateEmailSubject();
  613. }
  614. public function updatedSendViaEmail($value)
  615. {
  616. if ($value && empty($this->exportEmailAddress)) {
  617. $this->exportEmailAddress = $this->getPreferredEmail();
  618. }
  619. }
  620. public function resetEmailForm()
  621. {
  622. $this->sendViaEmail = false;
  623. $this->exportEmailAddress = $this->getPreferredEmail();
  624. $this->updateEmailSubject();
  625. }
  626. private function updateEmailSubject()
  627. {
  628. if (!empty($this->exportFromDate) && !empty($this->exportToDate)) {
  629. $fromFormatted = date('d/m/Y', strtotime($this->exportFromDate));
  630. $toFormatted = date('d/m/Y', strtotime($this->exportToDate));
  631. if ($this->exportFromDate === $this->exportToDate) {
  632. $this->exportEmailSubject = "Prima Nota - Export del {$fromFormatted}";
  633. } else {
  634. $this->exportEmailSubject = "Prima Nota - Export dal {$fromFormatted} al {$toFormatted}";
  635. }
  636. }
  637. }
  638. /**
  639. * Dispatch export job to queue
  640. */
  641. private function dispatchExportJob($exportRecords, $exportTotals)
  642. {
  643. try {
  644. // Prepare filter descriptions for the job
  645. $filterDescriptions = [
  646. 'member' => $this->filterMember ? $this->getMemberName($this->filterMember) : null,
  647. 'causals' => $this->filterCausals ? $this->getCausalsNames($this->filterCausals) : null,
  648. ];
  649. $paymentsArray = $this->payments->map(function ($payment) {
  650. return [
  651. 'id' => $payment->id,
  652. 'name' => $payment->name,
  653. 'type' => $payment->type
  654. ];
  655. })->toArray();
  656. // Dispatch job to background queue
  657. ExportPrimaNota::dispatch(
  658. $exportRecords,
  659. $exportTotals,
  660. $this->exportEmailAddress,
  661. $this->exportEmailSubject,
  662. [
  663. 'from' => date('d/m/Y', strtotime($this->exportFromDate)),
  664. 'to' => date('d/m/Y', strtotime($this->exportToDate))
  665. ],
  666. auth()->id(),
  667. $paymentsArray,
  668. $filterDescriptions
  669. );
  670. $this->emit('export-email-queued');
  671. session()->flash('success', 'Export in corso! Riceverai l\'email a breve alla casella: ' . $this->exportEmailAddress);
  672. Log::info('Export job dispatched', [
  673. 'user_id' => auth()->id(),
  674. 'email' => $this->exportEmailAddress,
  675. 'date_range' => [$this->exportFromDate, $this->exportToDate],
  676. 'total_records' => count($exportRecords)
  677. ]);
  678. } catch (\Exception $e) {
  679. Log::error('Failed to dispatch export job', [
  680. 'user_id' => auth()->id(),
  681. 'email' => $this->exportEmailAddress,
  682. 'error' => $e->getMessage()
  683. ]);
  684. throw new \Exception('Errore nell\'avvio dell\'export: ' . $e->getMessage());
  685. }
  686. }
  687. function export()
  688. {
  689. $exportRecords = $this->generateExportData($this->appliedFromDate, $this->appliedToDate);
  690. $exportTotals = $this->generateExportTotals($this->appliedFromDate, $this->appliedToDate);
  691. return $this->exportWithData($exportRecords, $exportTotals);
  692. }
  693. private function exportWithData($exportRecords, $exportTotals)
  694. {
  695. ini_set('memory_limit', '512M');
  696. gc_enable();
  697. $letters = array('F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA');
  698. $spreadsheet = new Spreadsheet();
  699. $activeWorksheet = $spreadsheet->getActiveSheet();
  700. $activeWorksheet->setCellValue('A1', "Data");
  701. $activeWorksheet->setCellValue('B1', "Causale");
  702. $activeWorksheet->setCellValue('C1', "Dettaglio");
  703. $activeWorksheet->setCellValue('D1', "Nominativo");
  704. $activeWorksheet->setCellValue('E1', "Stato");
  705. $idx = 0;
  706. foreach ($this->payments as $p) {
  707. if ($idx >= count($letters)) {
  708. break;
  709. }
  710. $activeWorksheet->setCellValue($letters[$idx] . '1', $p->name);
  711. $idx++;
  712. if ($idx >= count($letters)) {
  713. break;
  714. }
  715. $activeWorksheet->mergeCells($letters[$idx] . '1:' . $letters[$idx] . '1');
  716. $idx++;
  717. }
  718. $idx = 0;
  719. $activeWorksheet->setCellValue('A2', "");
  720. $activeWorksheet->setCellValue('B2', "");
  721. $activeWorksheet->setCellValue('C2', "");
  722. $activeWorksheet->setCellValue('D2', "");
  723. $activeWorksheet->setCellValue('E2', "");
  724. foreach ($this->payments as $p) {
  725. if ($p->type == 'ALL') {
  726. if ($idx >= count($letters)) {
  727. break;
  728. }
  729. $activeWorksheet->setCellValue($letters[$idx] . '2', "Entrate");
  730. $idx++;
  731. $activeWorksheet->setCellValue($letters[$idx] . '2', "Uscite");
  732. $idx++;
  733. } elseif ($p->type == 'IN') {
  734. if ($idx >= count($letters)) {
  735. break;
  736. }
  737. $activeWorksheet->setCellValue($letters[$idx] . '2', "Entrate");
  738. $idx++;
  739. $activeWorksheet->setCellValue($letters[$idx] . '2', "");
  740. $idx++;
  741. } elseif ($p->type == 'OUT') {
  742. if ($idx >= count($letters)) {
  743. break;
  744. }
  745. $activeWorksheet->setCellValue($letters[$idx] . '2', "");
  746. $idx++;
  747. $activeWorksheet->setCellValue($letters[$idx] . '2', "Uscite");
  748. $idx++;
  749. }
  750. }
  751. $activeWorksheet->getStyle('A1:Q1')->getFont()->setBold(true);
  752. $activeWorksheet->getStyle('A2:Q2')->getFont()->setBold(true);
  753. $count = 3;
  754. $batchSize = 1000;
  755. $recordsProcessed = 0;
  756. $totalRecords = count($exportRecords);
  757. $recordsArray = array_chunk($exportRecords, $batchSize, true);
  758. foreach ($recordsArray as $recordsBatch) {
  759. foreach ($recordsBatch as $causal => $record) {
  760. $check = $causal;
  761. $parts = explode("§", $check);
  762. $d = $parts[0] ?? '';
  763. $c = $parts[1] ?? '';
  764. $j = $parts[2] ?? '';
  765. $det = $parts[3] ?? '';
  766. $deleted = $parts[4] ?? '';
  767. $detailParts = explode('|', $det);
  768. $exportDetail = count($detailParts) > 1 ? implode(', ', array_slice($detailParts, 1)) : $det;
  769. $activeWorksheet->setCellValue('A' . $count, !empty($d) ? date("d/m/Y", strtotime($d)) : '');
  770. $activeWorksheet->setCellValue('B' . $count, $c);
  771. $activeWorksheet->setCellValue('C' . $count, $exportDetail);
  772. $activeWorksheet->setCellValue('D' . $count, $j);
  773. $stato = ($deleted === 'DELETED') ? 'ANNULLATA' : '';
  774. $activeWorksheet->setCellValue('E' . $count, $stato);
  775. if ($stato === 'ANNULLATA') {
  776. $activeWorksheet->getStyle('E' . $count)->getFont()->getColor()->setARGB('FFFF0000');
  777. }
  778. $idx = 0;
  779. foreach ($this->payments as $p) {
  780. if ($idx >= count($letters) - 1) {
  781. break;
  782. }
  783. if (isset($record[$p->name])) {
  784. $inValue = isset($record[$p->name]["IN"]) ? formatPrice($record[$p->name]["IN"]) : "";
  785. $outValue = isset($record[$p->name]["OUT"]) ? formatPrice($record[$p->name]["OUT"]) : "";
  786. $activeWorksheet->setCellValue($letters[$idx] . $count, $inValue);
  787. $idx++;
  788. $activeWorksheet->setCellValue($letters[$idx] . $count, $outValue);
  789. $idx++;
  790. } else {
  791. $activeWorksheet->setCellValue($letters[$idx] . $count, "");
  792. $idx++;
  793. $activeWorksheet->setCellValue($letters[$idx] . $count, "");
  794. $idx++;
  795. }
  796. }
  797. $count++;
  798. $recordsProcessed++;
  799. if ($recordsProcessed % 500 === 0) {
  800. gc_collect_cycles();
  801. }
  802. }
  803. unset($recordsBatch);
  804. gc_collect_cycles();
  805. }
  806. $count++;
  807. $idx = 0;
  808. $activeWorksheet->setCellValue('A' . $count, 'Totale');
  809. $activeWorksheet->setCellValue('B' . $count, '');
  810. $activeWorksheet->setCellValue('C' . $count, '');
  811. $activeWorksheet->setCellValue('D' . $count, '');
  812. $activeWorksheet->setCellValue('E' . $count, '');
  813. foreach ($this->payments as $p) {
  814. if ($idx >= count($letters) - 1) {
  815. break;
  816. }
  817. if (isset($exportTotals[$p->name])) {
  818. if ($p->type == 'ALL') {
  819. $activeWorksheet->setCellValue($letters[$idx] . $count, formatPrice($exportTotals[$p->name]["IN"] ?? 0));
  820. $idx++;
  821. $activeWorksheet->setCellValue($letters[$idx] . $count, formatPrice($exportTotals[$p->name]["OUT"] ?? 0));
  822. $idx++;
  823. } elseif ($p->type == 'IN') {
  824. $activeWorksheet->setCellValue($letters[$idx] . $count, formatPrice($exportTotals[$p->name]["IN"] ?? 0));
  825. $idx++;
  826. $activeWorksheet->setCellValue($letters[$idx] . $count, "");
  827. $idx++;
  828. } elseif ($p->type == 'OUT') {
  829. $activeWorksheet->setCellValue($letters[$idx] . $count, "");
  830. $idx++;
  831. $activeWorksheet->setCellValue($letters[$idx] . $count, formatPrice($exportTotals[$p->name]["OUT"] ?? 0));
  832. $idx++;
  833. }
  834. } else {
  835. if ($p->type == 'ALL') {
  836. $activeWorksheet->setCellValue($letters[$idx] . $count, "0");
  837. $idx++;
  838. $activeWorksheet->setCellValue($letters[$idx] . $count, "0");
  839. $idx++;
  840. } elseif ($p->type == 'IN') {
  841. $activeWorksheet->setCellValue($letters[$idx] . $count, "0");
  842. $idx++;
  843. $activeWorksheet->setCellValue($letters[$idx] . $count, "");
  844. $idx++;
  845. } elseif ($p->type == 'OUT') {
  846. $activeWorksheet->setCellValue($letters[$idx] . $count, "");
  847. $idx++;
  848. $activeWorksheet->setCellValue($letters[$idx] . $count, "0");
  849. $idx++;
  850. }
  851. }
  852. }
  853. $activeWorksheet->getStyle('A' . $count . ':Q' . $count)->getFont()->setBold(true);
  854. $activeWorksheet->getColumnDimension('A')->setWidth(20);
  855. $activeWorksheet->getColumnDimension('B')->setWidth(40);
  856. $activeWorksheet->getColumnDimension('C')->setWidth(40);
  857. $activeWorksheet->getColumnDimension('D')->setWidth(40);
  858. $activeWorksheet->getColumnDimension('E')->setWidth(20);
  859. foreach ($letters as $l) {
  860. $activeWorksheet->getColumnDimension($l)->setWidth(20);
  861. }
  862. $filename = 'prima_nota_' . date("YmdHis") . '.xlsx';
  863. try {
  864. $currentClient = session('currentClient', 'default');
  865. $tempPath = sys_get_temp_dir() . '/' . $filename;
  866. $writer = new Xlsx($spreadsheet);
  867. $writer->save($tempPath);
  868. unset($spreadsheet, $activeWorksheet, $writer);
  869. gc_collect_cycles();
  870. $disk = Storage::disk('s3');
  871. $s3Path = $currentClient . '/prima_nota/' . $filename;
  872. $primaNotaFolderPath = $currentClient . '/prima_nota/.gitkeep';
  873. if (!$disk->exists($primaNotaFolderPath)) {
  874. $disk->put($primaNotaFolderPath, '');
  875. Log::info("Created prima_nota folder for client: {$currentClient}");
  876. }
  877. $fileContent = file_get_contents($tempPath);
  878. $uploaded = $disk->put($s3Path, $fileContent, 'private');
  879. if (!$uploaded) {
  880. throw new \Exception('Failed to upload file to Wasabi S3');
  881. }
  882. Log::info("Prima Nota exported to Wasabi", [
  883. 'client' => $currentClient,
  884. 'path' => $s3Path,
  885. 'size' => filesize($tempPath),
  886. 'records_processed' => $recordsProcessed
  887. ]);
  888. if (file_exists($tempPath)) {
  889. unlink($tempPath);
  890. }
  891. $downloadUrl = $disk->temporaryUrl($s3Path, now()->addHour());
  892. return redirect($downloadUrl);
  893. } catch (\Exception $e) {
  894. Log::error('Error exporting Prima Nota to Wasabi S3', [
  895. 'error' => $e->getMessage(),
  896. 'client' => session('currentClient', 'unknown'),
  897. 'filename' => $filename,
  898. 'records_processed' => $recordsProcessed ?? 0
  899. ]);
  900. $currentClient = session('currentClient', 'default');
  901. $clientFolder = storage_path('app/prima_nota/' . $currentClient);
  902. if (!is_dir($clientFolder)) {
  903. mkdir($clientFolder, 0755, true);
  904. Log::info("Created local client prima_nota folder: {$clientFolder}");
  905. }
  906. $localPath = $clientFolder . '/' . $filename;
  907. if (isset($tempPath) && file_exists($tempPath)) {
  908. rename($tempPath, $localPath);
  909. } else {
  910. $writer = new Xlsx($spreadsheet);
  911. $writer->save($localPath);
  912. unset($spreadsheet, $activeWorksheet, $writer);
  913. }
  914. gc_collect_cycles();
  915. Log::warning("Prima Nota saved locally due to S3 error", [
  916. 'client' => $currentClient,
  917. 'local_path' => $localPath,
  918. 'error' => $e->getMessage()
  919. ]);
  920. session()->flash('warning', 'File salvato localmente a causa di un errore del cloud storage.');
  921. return response()->download($localPath)->deleteFileAfterSend();
  922. }
  923. }
  924. private function getPreferredEmail()
  925. {
  926. // Try multiple sources in order of preference
  927. $email = auth()->user()->email ?? null;
  928. if (empty($email)) {
  929. $email = session('user_email', null);
  930. }
  931. if (empty($email)) {
  932. $member = \App\Models\Member::where('user_id', auth()->id())->first();
  933. $email = $member ? $member->email : null;
  934. }
  935. if (empty($email)) {
  936. // Get from user input or company default
  937. $email = config('mail.default_recipient', '');
  938. }
  939. return $email;
  940. }
  941. }