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)
-