mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Updates for chart queries
This commit is contained in:
parent
a8362bf5b0
commit
2a4dd7e593
@ -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());
|
||||||
|
@ -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'];
|
||||||
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
@ -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,
|
||||||
|
@ -1451,16 +1451,31 @@ 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 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>
|
||||||
<div class="form-group mb-[10px]">
|
<div class="form-group mb-[10px]">
|
||||||
<input
|
<input
|
||||||
@ -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,
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user