Record.php 41 KB

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