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');
|
||||
|
||||
auth()->guard('contact')->user()->fill($request->only(['first_name','last_name']))->save();
|
||||
|
||||
$client = auth()->guard('contact')->user()->client;
|
||||
$client->private_notes = $trial_started;
|
||||
$client->fill($request->all());
|
||||
|
@ -136,11 +136,11 @@ class PortalComposer
|
||||
|
||||
$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'];
|
||||
} else {
|
||||
// } else {
|
||||
$data[] = ['title' => ctrans('texts.subscriptions'), 'url' => 'client.subscriptions.index', 'icon' => 'calendar'];
|
||||
}
|
||||
// }
|
||||
|
||||
if (auth()->guard('contact')->user()->client->getSetting('client_initiated_payments')) {
|
||||
$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 App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Libraries\Currency\Conversion\CurrencyApi;
|
||||
|
||||
/**
|
||||
* App\Models\Task
|
||||
@ -159,27 +160,55 @@ class Task extends BaseModel
|
||||
return $this->morphMany(Document::class, 'documentable');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function assigned_user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class)->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function client()
|
||||
{
|
||||
return $this->belongsTo(Client::class)->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function status()
|
||||
{
|
||||
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) {
|
||||
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()
|
||||
{
|
||||
$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)
|
||||
{
|
||||
$duration = 0;
|
||||
$parts = json_decode($this->time_log) ?: [];
|
||||
$parts = json_decode($this->time_log ?? '{}') ?: [];
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$start_time = $part[0];
|
||||
@ -272,6 +291,26 @@ class Task extends BaseModel
|
||||
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()
|
||||
{
|
||||
|
||||
|
@ -14,6 +14,7 @@ namespace App\Services\Chart;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Quote;
|
||||
use App\Models\Task;
|
||||
|
||||
/**
|
||||
* Class ChartCalculations.
|
||||
@ -170,4 +171,119 @@ trait ChartCalculations
|
||||
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']){
|
||||
'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,
|
||||
'outstanding_invoices' => $results = $this->getOutstandingInvoices($data),
|
||||
'completed_payments' => $results = $this->getCompletedPayments($data),
|
||||
'refunded_payments' => $results = $this->getRefundedPayments($data),
|
||||
'active_quotes' => $results = $this->getActiveQuotes($data),
|
||||
'unapproved_quotes' => $results = $this->getUnapprovedQuotes($data),
|
||||
'logged_tasks' => $results = $this->getLoggedTasks($data),
|
||||
'invoiced_tasks' => $results = $this->getInvoicedTasks($data),
|
||||
'paid_tasks' => $results = 0,
|
||||
'logged_expenses' => $results = 0,
|
||||
'pending_expenses' => $results = 0,
|
||||
|
@ -1451,17 +1451,32 @@ Ensure the default browser behavior of the `hidden` attribute.
|
||||
@csrf
|
||||
<input type="hidden" name="gateway_response"/>
|
||||
<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
|
||||
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="name"
|
||||
placeholder="{{ ctrans('texts.name') }}"
|
||||
name="name"
|
||||
value="{{$client->name}}"
|
||||
id="first_name"
|
||||
placeholder="{{ ctrans('texts.first_name') }}"
|
||||
name="first_name"
|
||||
value="{{ auth()->guard('contact')->user()->first_name}}"
|
||||
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 class="form-group mb-[10px]">
|
||||
<input
|
||||
type="text"
|
||||
@ -1810,7 +1825,7 @@ var elements = stripe.elements({
|
||||
var cardElement = elements.create('card', {
|
||||
value: {
|
||||
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
|
||||
|
||||
if (document.querySelector('input[name=name]').value == '') {
|
||||
if (document.querySelector('input[name=first_name]').value == '') {
|
||||
let errors = document.getElementById('errors');
|
||||
let payNowButton = document.getElementById('pay-now');
|
||||
|
||||
errors.textContent = '';
|
||||
errors.textContent = "{{ ctrans('texts.please_enter_a_name') }}";
|
||||
errors.textContent = "{{ ctrans('texts.please_enter_a_first_name') }}";
|
||||
errors.hidden = false;
|
||||
|
||||
payNowButton.disabled = false;
|
||||
@ -1841,6 +1856,19 @@ var country_value = e.options[e.selectedIndex].value;
|
||||
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');
|
||||
payNowButton = payNowButton;
|
||||
@ -1851,7 +1879,7 @@ var country_value = e.options[e.selectedIndex].value;
|
||||
stripe.handleCardSetup(this.client_secret, cardElement, {
|
||||
payment_method_data: {
|
||||
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() }}',
|
||||
address: {
|
||||
line1: document.querySelector('input[name=address1]').content,
|
||||
|
@ -1450,7 +1450,7 @@ Ensure the default browser behavior of the `hidden` attribute.
|
||||
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]"
|
||||
>
|
||||
Account Login
|
||||
{{ ctrans('texts.return_to_app') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user