Updates for chart queries

This commit is contained in:
David Bomba 2024-08-05 18:52:34 +10:00
parent a8362bf5b0
commit 2a4dd7e593
7 changed files with 218 additions and 33 deletions

View File

@ -88,6 +88,8 @@ class NinjaPlanController extends Controller
{ {
$trial_started = "Trial Started @ ".now()->format('Y-m-d H:i:s'); $trial_started = "Trial Started @ ".now()->format('Y-m-d H:i:s');
auth()->guard('contact')->user()->fill($request->only(['first_name','last_name']))->save();
$client = auth()->guard('contact')->user()->client; $client = auth()->guard('contact')->user()->client;
$client->private_notes = $trial_started; $client->private_notes = $trial_started;
$client->fill($request->all()); $client->fill($request->all());

View File

@ -136,11 +136,11 @@ class PortalComposer
$data[] = ['title' => ctrans('texts.statement'), 'url' => 'client.statement', 'icon' => 'activity']; $data[] = ['title' => ctrans('texts.statement'), 'url' => 'client.statement', 'icon' => 'activity'];
if (Ninja::isHosted() && auth()->guard('contact')->user()->company->id == config('ninja.ninja_default_company_id')) { // if (Ninja::isHosted() && auth()->guard('contact')->user()->company->id == config('ninja.ninja_default_company_id')) {
$data[] = ['title' => ctrans('texts.plan'), 'url' => 'client.plan', 'icon' => 'credit-card']; $data[] = ['title' => ctrans('texts.plan'), 'url' => 'client.plan', 'icon' => 'credit-card'];
} else { // } else {
$data[] = ['title' => ctrans('texts.subscriptions'), 'url' => 'client.subscriptions.index', 'icon' => 'calendar']; $data[] = ['title' => ctrans('texts.subscriptions'), 'url' => 'client.subscriptions.index', 'icon' => 'calendar'];
} // }
if (auth()->guard('contact')->user()->client->getSetting('client_initiated_payments')) { if (auth()->guard('contact')->user()->client->getSetting('client_initiated_payments')) {
$data[] = ['title' => ctrans('texts.pre_payment'), 'url' => 'client.pre_payments.index', 'icon' => 'dollar-sign']; $data[] = ['title' => ctrans('texts.pre_payment'), 'url' => 'client.pre_payments.index', 'icon' => 'dollar-sign'];

View File

@ -16,6 +16,7 @@ use App\Models\CompanyUser;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use App\Libraries\Currency\Conversion\CurrencyApi;
/** /**
* App\Models\Task * App\Models\Task
@ -159,27 +160,55 @@ class Task extends BaseModel
return $this->morphMany(Document::class, 'documentable'); return $this->morphMany(Document::class, 'documentable');
} }
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function assigned_user() public function assigned_user()
{ {
return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed(); return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed();
} }
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user() public function user()
{ {
return $this->belongsTo(User::class)->withTrashed(); return $this->belongsTo(User::class)->withTrashed();
} }
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function client() public function client()
{ {
return $this->belongsTo(Client::class)->withTrashed(); return $this->belongsTo(Client::class)->withTrashed();
} }
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function status() public function status()
{ {
return $this->belongsTo(TaskStatus::class)->withTrashed(); return $this->belongsTo(TaskStatus::class)->withTrashed();
} }
public function stringStatus() /**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function invoice()
{
return $this->belongsTo(Invoice::class)->withTrashed();
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function project()
{
return $this->belongsTo(Project::class)->withTrashed();
}
public function stringStatus(): string
{ {
if($this->invoice_id) { if($this->invoice_id) {
return '<h5><span class="badge badge-success">'.ctrans('texts.invoiced').'</span></h5>'; return '<h5><span class="badge badge-success">'.ctrans('texts.invoiced').'</span></h5>';
@ -193,16 +222,6 @@ class Task extends BaseModel
} }
public function invoice()
{
return $this->belongsTo(Invoice::class)->withTrashed();
}
public function project()
{
return $this->belongsTo(Project::class)->withTrashed();
}
public function calcStartTime() public function calcStartTime()
{ {
$parts = json_decode($this->time_log) ?: []; $parts = json_decode($this->time_log) ?: [];
@ -230,7 +249,7 @@ class Task extends BaseModel
public function calcDuration($start_time_cutoff = 0, $end_time_cutoff = 0) public function calcDuration($start_time_cutoff = 0, $end_time_cutoff = 0)
{ {
$duration = 0; $duration = 0;
$parts = json_decode($this->time_log) ?: []; $parts = json_decode($this->time_log ?? '{}') ?: [];
foreach ($parts as $part) { foreach ($parts as $part) {
$start_time = $part[0]; $start_time = $part[0];
@ -272,6 +291,26 @@ class Task extends BaseModel
return $this->company->settings->default_task_rate ?? 0; return $this->company->settings->default_task_rate ?? 0;
} }
public function taskCompanyValue(): float
{
$client_currency = $this->client->getSetting('currency_id');
$company_currency = $this->company->getSetting('currency_id');
if($client_currency != $company_currency)
{
$converter = new CurrencyApi();
return $converter->convert($this->taskValue(), $client_currency, $company_currency);
}
return $this->taskValue();
}
public function taskValue(): float
{
return round(($this->calcDuration() / 3600) * $this->getRate(),2);
}
public function processLogs() public function processLogs()
{ {

View File

@ -14,6 +14,7 @@ namespace App\Services\Chart;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\Quote; use App\Models\Quote;
use App\Models\Task;
/** /**
* Class ChartCalculations. * Class ChartCalculations.
@ -170,4 +171,119 @@ trait ChartCalculations
return $result; return $result;
} }
public function getLoggedTasks($data): int|float
{
//tasks with at least 1 timelog entry.
$result = 0;
$calculated = collect();
$q = Task::query()
->withTrashed()
->where('company_id', $this->company->id)
->where('is_deleted',0);
if(in_array($data['period'], ['current,previous'])) {
$q->whereBetween('calculated_start_date', [$data['start_date'], $data['end_date']]);
}
if($data['calculation'] != 'count' && $data['format'] == 'money')
{
if($data['currency_id'] != '999')
{
$q->whereHas('client', function ($query) use ($data){
$query->where('settings->currency_id', $data['currency_id']);
});
}
$calculated = $this->taskMoneyCalculator($q, $data);
}
if($data['calculation'] != 'count' && $data['format'] == 'time')
{
$calculated = $q->get()->map(function ($t){
return $t->calcDuration();
});
}
match ($data['calculation']) {
'sum' => $result = $calculated->sum(),
'avg' => $result = $calculated->avg(),
'count' => $result = $q->count(),
default => $result = 0,
};
return $result;
}
private function taskMoneyCalculator($query, $data)
{
return $query->get()
->when($data['currency_id'] == '999', function ($collection) {
$collection->map(function ($t) {
return $t->taskCompanyValue();
});
})
->when($data['currency_id'] != '999', function ($collection) {
$collection->map(function ($t) {
return $t->taskValue();
});
});
}
public function getInvoicedTasks($data): int|float
{
$result = 0;
$calculated = collect();
$q = Task::query()
->withTrashed()
->where('company_id', $this->company->id)
->where('is_deleted', 0)
->whereHas('invoice');
if(in_array($data['period'], ['current,previous'])) {
$q->whereBetween('calculated_start_date', [$data['start_date'], $data['end_date']]);
}
if($data['calculation'] != 'count' && $data['format'] == 'money') {
if($data['currency_id'] != '999') {
$q->whereHas('client', function ($query) use ($data) {
$query->where('settings->currency_id', $data['currency_id']);
});
}
$calculated = $this->taskMoneyCalculator($q, $data);
}
if($data['calculation'] != 'count' && $data['format'] == 'time') {
$calculated = $q->get()->map(function ($t) {
return $t->calcDuration();
});
}
match ($data['calculation']) {
'sum' => $result = $calculated->sum(),
'avg' => $result = $calculated->avg(),
'count' => $result = $q->count(),
default => $result = 0,
};
return $result;
}
} }

View File

@ -234,13 +234,13 @@ class ChartService
match($data['field']){ match($data['field']){
'active_invoices' => $results = $this->getActiveInvoices($data), 'active_invoices' => $results = $this->getActiveInvoices($data),
'outstanding_invoices' => $results = 0, 'outstanding_invoices' => $results = $this->getOutstandingInvoices($data),
'completed_payments' => $results = 0, 'completed_payments' => $results = $this->getCompletedPayments($data),
'refunded_payments' => $results = 0, 'refunded_payments' => $results = $this->getRefundedPayments($data),
'active_quotes' => $results = 0, 'active_quotes' => $results = $this->getActiveQuotes($data),
'unapproved_quotes' => $results = 0, 'unapproved_quotes' => $results = $this->getUnapprovedQuotes($data),
'logged_tasks' => $results = 0, 'logged_tasks' => $results = $this->getLoggedTasks($data),
'invoiced_tasks' => $results = 0, 'invoiced_tasks' => $results = $this->getInvoicedTasks($data),
'paid_tasks' => $results = 0, 'paid_tasks' => $results = 0,
'logged_expenses' => $results = 0, 'logged_expenses' => $results = 0,
'pending_expenses' => $results = 0, 'pending_expenses' => $results = 0,

View File

@ -1451,17 +1451,32 @@ Ensure the default browser behavior of the `hidden` attribute.
@csrf @csrf
<input type="hidden" name="gateway_response"/> <input type="hidden" name="gateway_response"/>
<div class="alert alert-failure mb-4" hidden="" id="errors"></div> <div class="alert alert-failure mb-4" hidden="" id="errors"></div>
<div class="form-group mb-[10px]"> <div class="form-group mb-[10px] flex">
<div class="w-1/2">
<input <input
type="text" type="text"
class="form-control block w-full px-3 py-2 text-base font-normal text-gray-700 bg-white bg-clip-padding border border-light-grey rounded transition ease-in-out m-0 focus:primary-blue focus:outline-none" class="form-control block w-full px-3 py-2 text-base font-normal text-gray-700 bg-white bg-clip-padding border border-light-grey rounded transition ease-in-out m-0 focus:primary-blue focus:outline-none"
id="name" id="first_name"
placeholder="{{ ctrans('texts.name') }}" placeholder="{{ ctrans('texts.first_name') }}"
name="name" name="first_name"
value="{{$client->name}}" value="{{ auth()->guard('contact')->user()->first_name}}"
required required
/> />
</div> </div>
<div class="w-1/2">
<input
type="text"
class="form-control block w-full px-3 py-2 text-base font-normal text-gray-700 bg-white bg-clip-padding border border-light-grey rounded transition ease-in-out m-0 focus:primary-blue focus:outline-none"
id="lastt_name"
placeholder="{{ ctrans('texts.last_name') }}"
name="last_name"
value="{{ auth()->guard('contact')->user()->last_name}}"
required
/>
</div>
</div>
<div class="form-group mb-[10px]"> <div class="form-group mb-[10px]">
<input <input
type="text" type="text"
@ -1810,7 +1825,7 @@ var elements = stripe.elements({
var cardElement = elements.create('card', { var cardElement = elements.create('card', {
value: { value: {
postalCode: document.querySelector('input[name=postal_code]').content, postalCode: document.querySelector('input[name=postal_code]').content,
name: document.querySelector('input[name=name]').content, name: document.querySelector('input[name=first_name]').content + ' ' + document.querySelector('input[name=last_name]').content,
} }
}); });
@ -1827,12 +1842,12 @@ var country_value = e.options[e.selectedIndex].value;
//make sure the user has entered their name //make sure the user has entered their name
if (document.querySelector('input[name=name]').value == '') { if (document.querySelector('input[name=first_name]').value == '') {
let errors = document.getElementById('errors'); let errors = document.getElementById('errors');
let payNowButton = document.getElementById('pay-now'); let payNowButton = document.getElementById('pay-now');
errors.textContent = ''; errors.textContent = '';
errors.textContent = "{{ ctrans('texts.please_enter_a_name') }}"; errors.textContent = "{{ ctrans('texts.please_enter_a_first_name') }}";
errors.hidden = false; errors.hidden = false;
payNowButton.disabled = false; payNowButton.disabled = false;
@ -1841,6 +1856,19 @@ var country_value = e.options[e.selectedIndex].value;
return; return;
} }
if (document.querySelector('input[name=last_name]').value == '') {
let errors = document.getElementById('errors');
let payNowButton = document.getElementById('pay-now');
errors.textContent = '';
errors.textContent = "{{ ctrans('texts.please_enter_a_last_name') }}";
errors.hidden = false;
payNowButton.disabled = false;
payNowButton.querySelector('svg').classList.add('hidden');
payNowButton.querySelector('span').classList.remove('hidden');
return;
}
let payNowButton = document.getElementById('pay-now'); let payNowButton = document.getElementById('pay-now');
payNowButton = payNowButton; payNowButton = payNowButton;
@ -1851,7 +1879,7 @@ var country_value = e.options[e.selectedIndex].value;
stripe.handleCardSetup(this.client_secret, cardElement, { stripe.handleCardSetup(this.client_secret, cardElement, {
payment_method_data: { payment_method_data: {
billing_details: { billing_details: {
name: document.querySelector('input[name=name]').content, name: document.querySelector('input[name=first_name]').content + ' ' + document.querySelector('input[name=first_name]').content,
email: '{{ $client->present()->email() }}', email: '{{ $client->present()->email() }}',
address: { address: {
line1: document.querySelector('input[name=address1]').content, line1: document.querySelector('input[name=address1]').content,

View File

@ -1450,7 +1450,7 @@ Ensure the default browser behavior of the `hidden` attribute.
type="button" type="button"
class="mx-[auto] max-w-[212px] bg-primary-blue hover:opacity-80 button button-primary bg-primary rounded-sm text-sm transition duration-300 ease-in md:mx-[0]" class="mx-[auto] max-w-[212px] bg-primary-blue hover:opacity-80 button button-primary bg-primary rounded-sm text-sm transition duration-300 ease-in md:mx-[0]"
> >
Account Login {{ ctrans('texts.return_to_app') }}
</a> </a>
</div> </div>
</div> </div>