@@ -520,6 +520,165 @@ public function vatReport(Request $request): Response
520520 ]);
521521 }
522522
523+ public function cashFlowForecast (Request $ request ): Response
524+ {
525+ $ this ->authorize ('viewAny ' , Invoice::class);
526+
527+ $ weeks = (int ) $ request ->get ('weeks ' , 12 );
528+ $ openingBalance = (float ) $ request ->get ('opening_balance ' , 0 );
529+ $ from = now ()->startOfDay ();
530+ $ to = now ()->addWeeks ($ weeks )->endOfDay ();
531+
532+ // Collect open invoices (inflows) due within horizon
533+ $ invoices = Invoice::with ('contact ' )
534+ ->whereIn ('status ' , ['sent ' , 'partial ' ])
535+ ->whereBetween ('due_date ' , [$ from ->toDateString (), $ to ->toDateString ()])
536+ ->get ();
537+
538+ // Collect open bills (outflows) due within horizon
539+ $ bills = Bill::with ('contact ' )
540+ ->whereIn ('status ' , ['received ' , 'partial ' ])
541+ ->whereBetween ('due_date ' , [$ from ->toDateString (), $ to ->toDateString ()])
542+ ->get ();
543+
544+ // Build weekly buckets
545+ $ buckets = [];
546+ for ($ i = 0 ; $ i < $ weeks ; $ i ++) {
547+ $ weekStart = now ()->addWeeks ($ i )->startOfWeek ()->toDateString ();
548+ $ weekEnd = now ()->addWeeks ($ i )->endOfWeek ()->toDateString ();
549+ $ buckets [$ weekStart ] = [
550+ 'week_start ' => $ weekStart ,
551+ 'week_end ' => $ weekEnd ,
552+ 'inflows ' => [],
553+ 'outflows ' => [],
554+ ];
555+ }
556+
557+ // Place invoices into their week bucket
558+ foreach ($ invoices as $ inv ) {
559+ $ due = $ inv ->due_date instanceof \Carbon \Carbon
560+ ? $ inv ->due_date ->toDateString ()
561+ : (string ) $ inv ->due_date ;
562+ foreach ($ buckets as $ weekStart => $ bucket ) {
563+ if ($ due >= $ bucket ['week_start ' ] && $ due <= $ bucket ['week_end ' ]) {
564+ $ buckets [$ weekStart ]['inflows ' ][] = [
565+ 'reference ' => $ inv ->reference ,
566+ 'contact ' => $ inv ->contact ?->name ?? '— ' ,
567+ 'due_date ' => $ due ,
568+ 'amount ' => $ inv ->total - $ inv ->amount_paid ,
569+ ];
570+ break ;
571+ }
572+ }
573+ }
574+
575+ // Place bills into their week bucket
576+ foreach ($ bills as $ bill ) {
577+ $ due = $ bill ->due_date instanceof \Carbon \Carbon
578+ ? $ bill ->due_date ->toDateString ()
579+ : (string ) $ bill ->due_date ;
580+ foreach ($ buckets as $ weekStart => $ bucket ) {
581+ if ($ due >= $ bucket ['week_start ' ] && $ due <= $ bucket ['week_end ' ]) {
582+ $ buckets [$ weekStart ]['outflows ' ][] = [
583+ 'reference ' => $ bill ->reference ,
584+ 'contact ' => $ bill ->contact ?->name ?? '— ' ,
585+ 'due_date ' => $ due ,
586+ 'amount ' => $ bill ->total - $ bill ->amount_paid ,
587+ ];
588+ break ;
589+ }
590+ }
591+ }
592+
593+ // Compute running balance per week
594+ $ balance = $ openingBalance ;
595+ $ result = [];
596+ foreach ($ buckets as $ bucket ) {
597+ $ inflow = array_sum (array_column ($ bucket ['inflows ' ], 'amount ' ));
598+ $ outflow = array_sum (array_column ($ bucket ['outflows ' ], 'amount ' ));
599+ $ balance += $ inflow - $ outflow ;
600+ $ result [] = [
601+ 'week_start ' => $ bucket ['week_start ' ],
602+ 'week_end ' => $ bucket ['week_end ' ],
603+ 'inflows ' => $ bucket ['inflows ' ],
604+ 'outflows ' => $ bucket ['outflows ' ],
605+ 'total_inflow ' => round ($ inflow , 2 ),
606+ 'total_outflow ' => round ($ outflow , 2 ),
607+ 'net ' => round ($ inflow - $ outflow , 2 ),
608+ 'closing_balance ' => round ($ balance , 2 ),
609+ ];
610+ }
611+
612+ return Inertia::render ('Finance/Reports/CashFlowForecast ' , [
613+ 'buckets ' => $ result ,
614+ 'openingBalance ' => $ openingBalance ,
615+ 'weeks ' => $ weeks ,
616+ 'totalInflow ' => round (array_sum (array_column ($ result , 'total_inflow ' )), 2 ),
617+ 'totalOutflow ' => round (array_sum (array_column ($ result , 'total_outflow ' )), 2 ),
618+ ]);
619+ }
620+
621+ public function exportCashFlowForecast (Request $ request ): \Symfony \Component \HttpFoundation \StreamedResponse
622+ {
623+ $ this ->authorize ('viewAny ' , Invoice::class);
624+
625+ $ weeks = (int ) $ request ->get ('weeks ' , 12 );
626+ $ openingBalance = (float ) $ request ->get ('opening_balance ' , 0 );
627+ $ from = now ()->startOfDay ();
628+ $ to = now ()->addWeeks ($ weeks )->endOfDay ();
629+
630+ $ invoices = Invoice::whereIn ('status ' , ['sent ' , 'partial ' ])
631+ ->whereBetween ('due_date ' , [$ from ->toDateString (), $ to ->toDateString ()])->get ();
632+ $ bills = Bill::whereIn ('status ' , ['received ' , 'partial ' ])
633+ ->whereBetween ('due_date ' , [$ from ->toDateString (), $ to ->toDateString ()])->get ();
634+
635+ // Rebuild buckets same as above, minimal version for export
636+ $ buckets = [];
637+ for ($ i = 0 ; $ i < $ weeks ; $ i ++) {
638+ $ ws = now ()->addWeeks ($ i )->startOfWeek ()->toDateString ();
639+ $ we = now ()->addWeeks ($ i )->endOfWeek ()->toDateString ();
640+ $ buckets [$ ws ] = ['week_start ' => $ ws , 'week_end ' => $ we , 'inflow ' => 0.0 , 'outflow ' => 0.0 ];
641+ }
642+ foreach ($ invoices as $ inv ) {
643+ $ due = $ inv ->due_date instanceof \Carbon \Carbon ? $ inv ->due_date ->toDateString () : (string ) $ inv ->due_date ;
644+ foreach ($ buckets as $ ws => &$ b ) {
645+ if ($ due >= $ b ['week_start ' ] && $ due <= $ b ['week_end ' ]) {
646+ $ b ['inflow ' ] += $ inv ->total - $ inv ->amount_paid ;
647+ break ;
648+ }
649+ }
650+ }
651+ foreach ($ bills as $ bill ) {
652+ $ due = $ bill ->due_date instanceof \Carbon \Carbon ? $ bill ->due_date ->toDateString () : (string ) $ bill ->due_date ;
653+ foreach ($ buckets as $ ws => &$ b ) {
654+ if ($ due >= $ b ['week_start ' ] && $ due <= $ b ['week_end ' ]) {
655+ $ b ['outflow ' ] += $ bill ->total - $ bill ->amount_paid ;
656+ break ;
657+ }
658+ }
659+ }
660+
661+ $ balance = $ openingBalance ;
662+ $ rows = [];
663+ foreach ($ buckets as $ b ) {
664+ $ balance += $ b ['inflow ' ] - $ b ['outflow ' ];
665+ $ rows [] = [
666+ $ b ['week_start ' ],
667+ $ b ['week_end ' ],
668+ round ($ b ['inflow ' ], 2 ),
669+ round ($ b ['outflow ' ], 2 ),
670+ round ($ b ['inflow ' ] - $ b ['outflow ' ], 2 ),
671+ round ($ balance , 2 ),
672+ ];
673+ }
674+
675+ return $ this ->streamCsv (
676+ 'cash-flow-forecast.csv ' ,
677+ ['Week Start ' , 'Week End ' , 'Inflows ' , 'Outflows ' , 'Net ' , 'Closing Balance ' ],
678+ $ rows
679+ );
680+ }
681+
523682 // ─── CSV Export Methods ───────────────────────────────────────────────────
524683
525684 public function exportProfitLoss (Request $ request ): \Symfony \Component \HttpFoundation \StreamedResponse
0 commit comments