diff --git a/app/Http/Controllers/ChartController.php b/app/Http/Controllers/ChartController.php index 1ad2d1423155..394e762d9749 100644 --- a/app/Http/Controllers/ChartController.php +++ b/app/Http/Controllers/ChartController.php @@ -11,8 +11,9 @@ namespace App\Http\Controllers; -use App\Http\Requests\Chart\ShowChartRequest; use App\Services\Chart\ChartService; +use App\Http\Requests\Chart\ShowChartRequest; +use App\Http\Requests\Chart\ShowCalculatedFieldRequest; class ChartController extends BaseController { @@ -65,5 +66,15 @@ class ChartController extends BaseController return response()->json($cs->chart_summary($request->input('start_date'), $request->input('end_date')), 200); } + public function calculatedField(ShowCalculatedFieldRequest $request) + { + /** @var \App\Models\User auth()->user() */ + $user = auth()->user(); + $cs = new ChartService($user->company(), $user, $user->isAdmin()); + $result = $cs->getCalculatedField($request->all()); + + return response()->json($result, 200); + + } } diff --git a/app/Http/Requests/Chart/ShowCalculatedFieldRequest.php b/app/Http/Requests/Chart/ShowCalculatedFieldRequest.php new file mode 100644 index 000000000000..0980dcd3173c --- /dev/null +++ b/app/Http/Requests/Chart/ShowCalculatedFieldRequest.php @@ -0,0 +1,78 @@ +user */ + $user = auth()->user(); + + return $user->isAdmin() || $user->hasPermission('view_dashboard'); + } + + public function rules() + { + return [ + 'date_range' => 'bail|sometimes|string|in:last7_days,last30_days,last365_days,this_month,last_month,this_quarter,last_quarter,this_year,last_year,all_time,custom', + 'start_date' => 'bail|sometimes|date', + 'end_date' => 'bail|sometimes|date', + 'field' => 'required|bail|in:active_invoices, outstanding_invoices, completed_payments, refunded_payments, active_quotes, unapproved_quotes, logged_tasks, invoiced_tasks, paid_tasks, logged_expenses, pending_expenses, invoiced_expenses, invoice_paid_expenses', + 'calculation' => 'required|bail|in:sum,avg,count', + 'period' => 'required|bail|in:current,previous,total', + 'format' => 'sometimes|bail|in:time,money', + ]; + } + + public function prepareForValidation() + { + + /**@var \App\Models\User auth()->user */ + $user = auth()->user(); + + $input = $this->all(); + + if(isset($input['date_range'])) { + $dates = $this->calculateStartAndEndDates($input, $user->company()); + $input['start_date'] = $dates[0]; + $input['end_date'] = $dates[1]; + } + + if (! isset($input['start_date'])) { + $input['start_date'] = now()->subDays(20)->format('Y-m-d'); + } + + if (! isset($input['end_date'])) { + $input['end_date'] = now()->format('Y-m-d'); + } + + if(isset($input['period']) && $input['period'] == 'previous') + { + $dates = $this->calculatePreviousPeriodStartAndEndDates($input, $user->company()); + $input['start_date'] = $dates[0]; + $input['end_date'] = $dates[1]; + } + + $this->replace($input); + } +} diff --git a/app/Services/Chart/ChartCalculations.php b/app/Services/Chart/ChartCalculations.php new file mode 100644 index 000000000000..92cbbd93c1fc --- /dev/null +++ b/app/Services/Chart/ChartCalculations.php @@ -0,0 +1,173 @@ +withTrashed() + ->where('company_id', $this->company->id) + ->where('is_deleted', 0) + ->whereIn('status_id', [2,3,4]); + + if(in_array($data['period'],['current,previous'])) + $q->whereBetween('date', [$data['start_date'], $data['end_date']]); + + match ($data['calculation']) { + 'sum' => $result = $q->sum('amount'), + 'avg' => $result = $q->avg('amount'), + 'count' => $result = $q->count(), + default => $result = 0, + }; + + return $result; + + } + + public function getOutstandingInvoices($data): int|float + { + $result = 0; + + $q = Invoice::query() + ->withTrashed() + ->where('company_id', $this->company->id) + ->where('is_deleted', 0) + ->whereIn('status_id', [2,3]); + + if(in_array($data['period'],['current,previous'])) + $q->whereBetween('date', [$data['start_date'], $data['end_date']]); + + match ($data['calculation']) { + 'sum' => $result = $q->sum('balance'), + 'avg' => $result = $q->avg('balance'), + 'count' => $result = $q->count(), + default => $result = 0, + }; + + return $result; + + } + + public function getCompletedPayments($data): int|float + { + $result = 0; + + $q = Payment::query() + ->withTrashed() + ->where('company_id', $this->company->id) + ->where('is_deleted', 0) + ->where('status_id', 4); + + if(in_array($data['period'],['current,previous'])) + $q->whereBetween('date', [$data['start_date'], $data['end_date']]); + + match ($data['calculation']) { + 'sum' => $result = $q->sum('amount'), + 'avg' => $result = $q->avg('amount'), + 'count' => $result = $q->count(), + default => $result = 0, + }; + + return $result; + + } + + public function getRefundedPayments($data): int|float + { + $result = 0; + + $q = Payment::query() + ->withTrashed() + ->where('company_id', $this->company->id) + ->where('is_deleted', 0) + ->whereIn('status_id', [5,6]); + + if(in_array($data['period'],['current,previous'])) + $q->whereBetween('date', [$data['start_date'], $data['end_date']]); + + match ($data['calculation']) { + 'sum' => $result = $q->sum('refunded'), + 'avg' => $result = $q->avg('refunded'), + 'count' => $result = $q->count(), + default => $result = 0, + }; + + return $result; + + } + + public function getActiveQuotes($data): int|float + { + $result = 0; + + $q = Quote::query() + ->withTrashed() + ->where('company_id', $this->company->id) + ->where('is_deleted', 0) + ->whereIn('status_id', [2,3]) + ->where(function ($qq){ + $qq->where('due_date', '>=', now()->toDateString())->orWhereNull('due_date'); + }); + + if(in_array($data['period'],['current,previous'])) + $q->whereBetween('date', [$data['start_date'], $data['end_date']]); + + match ($data['calculation']) { + 'sum' => $result = $q->sum('refunded'), + 'avg' => $result = $q->avg('refunded'), + 'count' => $result = $q->count(), + default => $result = 0, + }; + + return $result; + + } + + public function getUnapprovedQuotes($data): int|float + { + $result = 0; + + $q = Quote::query() + ->withTrashed() + ->where('company_id', $this->company->id) + ->where('is_deleted', 0) + ->whereIn('status_id', [2]) + ->where(function ($qq){ + $qq->where('due_date', '>=', now()->toDateString())->orWhereNull('due_date'); + }); + + if(in_array($data['period'],['current,previous'])) + $q->whereBetween('date', [$data['start_date'], $data['end_date']]); + + match ($data['calculation']) { + 'sum' => $result = $q->sum('refunded'), + 'avg' => $result = $q->avg('refunded'), + 'count' => $result = $q->count(), + default => $result = 0, + }; + + return $result; + + } +} \ No newline at end of file diff --git a/app/Services/Chart/ChartService.php b/app/Services/Chart/ChartService.php index 479f28e13a22..4bf080f950e6 100644 --- a/app/Services/Chart/ChartService.php +++ b/app/Services/Chart/ChartService.php @@ -14,12 +14,17 @@ namespace App\Services\Chart; use App\Models\Client; use App\Models\Company; use App\Models\Expense; +use App\Models\Invoice; +use App\Models\Payment; +use App\Models\Quote; +use App\Models\Task; use App\Models\User; use Illuminate\Support\Facades\Cache; class ChartService { use ChartQueries; + use ChartCalculations; public function __construct(public Company $company, private User $user, private bool $is_admin) { @@ -71,7 +76,7 @@ class ChartService return $final_currencies; } - + /* Chart Data */ public function chart_summary($start_date, $end_date): array { @@ -207,4 +212,44 @@ class ChartService return ''; } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * calculatedField + * + * @param array $data - + * + * field - list of fields for calculation + * period - current/previous + * calculation - sum/count/avg + * + * date_range - this_month + * or + * start_date - end_date + */ + public function getCalculatedField(array $data) + { + $results = 0; + + match($data['field']){ + 'active_invoices' => $results = $this->getActiveInvoices($data), + 'outstanding_invoices' => $results = 0, + 'completed_payments' => $results = 0, + 'refunded_payments' => $results = 0, + 'active_quotes' => $results = 0, + 'unapproved_quotes' => $results = 0, + 'logged_tasks' => $results = 0, + 'invoiced_tasks' => $results = 0, + 'paid_tasks' => $results = 0, + 'logged_expenses' => $results = 0, + 'pending_expenses' => $results = 0, + 'invoiced_expenses' => $results = 0, + 'invoice_paid_expenses' => $results = 0, + default => $results = 0, + }; + + return $results; + } + } diff --git a/app/Utils/SystemHealth.php b/app/Utils/SystemHealth.php index 2cbbe30313fa..46aac5fa4abb 100644 --- a/app/Utils/SystemHealth.php +++ b/app/Utils/SystemHealth.php @@ -85,6 +85,7 @@ class SystemHealth 'file_permissions' => (string) self::checkFileSystem(), 'exchange_rate_api_not_configured' => (bool)self::checkCurrencySanity(), 'api_version' => (string) config('ninja.app_version'), + 'is_docker' => (bool) config('ninja.is_docker'), ]; } diff --git a/app/Utils/Traits/MakesDates.php b/app/Utils/Traits/MakesDates.php index 25bec419a744..1e7b551ad6a3 100644 --- a/app/Utils/Traits/MakesDates.php +++ b/app/Utils/Traits/MakesDates.php @@ -146,7 +146,6 @@ trait MakesDates } - return match ($data['date_range']) { EmailStatement::LAST7 => [now()->startOfDay()->subDays(7)->format('Y-m-d'), now()->startOfDay()->format('Y-m-d')], EmailStatement::LAST30 => [now()->startOfDay()->subDays(30)->format('Y-m-d'), now()->startOfDay()->format('Y-m-d')], @@ -162,4 +161,37 @@ trait MakesDates }; } + public function calculatePreviousPeriodStartAndEndDates(array $data, ?Company $company = null): array + { + + //override for financial years + if($data['date_range'] == 'this_year') { + + $first_month_of_year = $company ? $company?->first_month_of_year : 1; + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + $fin_year_start->subYearNoOverflow(); + + if(now()->subYear()->lt($fin_year_start)) { + $fin_year_start->subYearNoOverflow(); + } + + } + + return match ($data['date_range']) { + EmailStatement::LAST7 => [now()->startOfDay()->subDays(14)->format('Y-m-d'), now()->subDays(7)->startOfDay()->format('Y-m-d')], + EmailStatement::LAST30 => [now()->startOfDay()->subDays(60)->format('Y-m-d'), now()->subDays(30)->startOfDay()->format('Y-m-d')], + EmailStatement::LAST365 => [now()->startOfDay()->subDays(739)->format('Y-m-d'), now()->subDays(365)->startOfDay()->format('Y-m-d')], + EmailStatement::THIS_MONTH => [now()->startOfDay()->subMonthNoOverflow()->firstOfMonth()->format('Y-m-d'), now()->startOfDay()->subMonthNoOverflow()->lastOfMonth()->format('Y-m-d')], + EmailStatement::LAST_MONTH => [now()->startOfDay()->subMonthsNoOverflow(2)->firstOfMonth()->format('Y-m-d'), now()->startOfDay()->subMonthNoOverflow()->lastOfMonth()->format('Y-m-d')], + EmailStatement::THIS_QUARTER => [now()->startOfDay()->subQuarterNoOverflow()->startOfQuarter()->format('Y-m-d'), now()->startOfDay()->subQuarterNoOverflow()->endOfQuarter()->format('Y-m-d')], + EmailStatement::LAST_QUARTER => [now()->startOfDay()->subQuartersNoOverflow(2)->startOfQuarter()->format('Y-m-d'), now()->startOfDay()->subQuartersNoOverflow(2)->endOfQuarter()->format('Y-m-d')], + EmailStatement::THIS_YEAR => [$fin_year_start->subYear()->format('Y-m-d'), $fin_year_start->copy()->subDay()->format('Y-m-d')], + EmailStatement::LAST_YEAR => [$fin_year_start->subYear(2)->format('Y-m-d'), $fin_year_start->copy()->subYear()->subDay()->format('Y-m-d')], + EmailStatement::CUSTOM_RANGE => [$data['start_date'], $data['end_date']], + default => [now()->startOfDay()->firstOfMonth()->format('Y-m-d'), now()->startOfDay()->lastOfMonth()->format('Y-m-d')], + }; + + } + } diff --git a/resources/views/portal/ninja2020/auth/register.blade.php b/resources/views/portal/ninja2020/auth/register.blade.php index f18b63a7e012..0dc41c225a32 100644 --- a/resources/views/portal/ninja2020/auth/register.blade.php +++ b/resources/views/portal/ninja2020/auth/register.blade.php @@ -81,7 +81,7 @@ name="country_id"> @foreach(App\Utils\TranslationHelper::getCountries() as $country) -