monthly budget, monthly report, unified report calculation (#2684)

This commit is contained in:
Kevin Papst
2021-08-06 18:38:41 +02:00
committed by GitHub
parent 21f785638c
commit cefd747e91
196 changed files with 5031 additions and 2167 deletions

View File

@@ -6,25 +6,15 @@
{% block main %}
{% set inUse = (stats.recordAmount > 0) %}
{% set inUse = (stats.counter > 0) %}
{% set params = {
'%activity%': activity.name,
'%project%': '-',
'%customer%': '-',
'%records%': stats.recordAmount,
'%duration%': stats.recordDuration|duration
'%records%': stats.counter,
'%duration%': stats.duration|duration
} %}
{% if activity.project is not null %}
{% set params = params|merge({
'%project%': activity.project.name,
'%customer%': activity.project.customer.name,
}) %}
{% endif %}
{{ include(app.request.xmlHttpRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
'message': ("admin_activity.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
'message': ("delete_warning.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
'form': form,
'used': inUse,
'back': path('admin_activity')

View File

@@ -78,7 +78,7 @@
{% endembed %}
{% if stats is not null %}
{% set currency = null %}
{% set currency = kimai_config.getCustomerDefaultCurrency() %}
{% if activity.project is not null %}
{% set currency = activity.project.customer.currency %}
{% endif %}

View File

@@ -30,12 +30,15 @@
{% endif %}
{% if form.budget is defined %}
<div class="row">
<div class="col-md-6">
<div class="col-md-4">
{{ form_row(form.budget) }}
</div>
<div class="col-md-6">
<div class="col-md-4">
{{ form_row(form.timeBudget) }}
</div>
<div class="col-md-4">
{{ form_row(form.budgetType) }}
</div>
</div>
{% endif %}
{{ form_row(form.visible) }}

View File

@@ -41,7 +41,7 @@
{% block page_content_before %}
{% set event = trigger(constant('App\\Event\\ThemeEvent::CONTENT_BEFORE')) %}
{{ event.content|raw }}
<div class="toolbar-pad no-print">
<div class="no-print">
{% block main_before %}{% endblock %}
</div>
{% endblock %}

View File

@@ -6,18 +6,15 @@
{% block main %}
{% set inUse = (stats.recordAmount > 0) %}
{% set inUse = (stats.counter > 0) %}
{% set params = {
'%activity%': stats.activityAmount,
'%project%': stats.projectAmount,
'%customer%': customer.name,
'%records%': stats.recordAmount,
'%duration%': stats.recordDuration|duration
'%records%': stats.counter,
'%duration%': stats.duration|duration
} %}
{{ include(app.request.xmlHttpRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
'message': ("admin_customer.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
'message': ("delete_warning.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
'form': form,
'used': inUse,
'back': path('admin_customer')

View File

@@ -69,12 +69,15 @@
</div>
{% if form.budget is defined %}
<div class="row">
<div class="col-md-6">
<div class="col-md-4">
{{ form_row(form.budget) }}
</div>
<div class="col-md-6">
<div class="col-md-4">
{{ form_row(form.timeBudget) }}
</div>
<div class="col-md-4">
{{ form_row(form.budgetType) }}
</div>
</div>
{% endif %}
{{ form_row(form.visible) }}

View File

@@ -2,34 +2,153 @@
{% import "macros/progressbar.html.twig" as progress %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}
{{ 'label.timeBudget'|trans }}
{% if entity.timeBudget > 0 %}
{{ widgets.label(entity.timeBudget|duration, 'gray') }}
{% endif %}
{{ 'label.timeBudget'|trans }} {% if entity.isMonthlyBudget() %}({{ 'label.budgetType_month'|trans }}){% endif %}
{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_attributes %}id="time_budget_box"{% endblock %}
{% block box_body %}
{{ progress.progressbar(stats.recordDuration, stats.durationBillable, 'label.billable'|trans, stats.durationBillable|duration ~ ' / ' ~ stats.recordDuration|duration, true) }}
{% if entity.timeBudget > 0 %}
{{ progress.progressbar(entity.timeBudget, stats.durationBillable, 'label.timeBudget'|trans, stats.durationBillable|duration ~ ' / ' ~ entity.timeBudget|duration) }}
{% endif %}
{% set durationTrans = entity.isMonthlyBudget() ? 'stats.durationMonth' : 'stats.durationTotal' %}
<div class="row">
<div class="col-xs-12 col-sm-5">
<table class="table table-hover dataTable">
<tr>
<td>{{ 'label.timeBudget'|trans }}</td>
<td class="text-nowrap text-right">
{% if entity.timeBudget > 0 %}
{{ entity.timeBudget|duration }}
{% else %}
-
{% endif %}
</td>
<td class="text-nowrap text-right">
{% if entity.timeBudget > 0 %}
100%
{% else %}
-
{% endif %}
</td>
</tr>
{% set totalPercentReached = 100 %}
{% if entity.timeBudget > 0 %}
{% set totalPercentReached = (stats.durationBillable / (entity.timeBudget / 100)) %}
{% endif %}
<tr>
<td>{{ durationTrans|trans }}</td>
<td class="text-nowrap text-right">{{ stats.duration|duration }}</td>
<td class="text-nowrap text-right">{{ totalPercentReached|number_format(2) }}%</td>
</tr>
{% set percentReached = 0 %}
{% if stats.duration > 0 %}
{% set percentReached = (stats.durationBillable / (stats.duration / 100)) %}
{% endif %}
<tr>
<td>{{ 'label.billable'|trans }}</td>
<td class="text-nowrap text-right">{{ stats.durationBillable|duration }}</td>
<td class="text-nowrap text-right">{{ percentReached|number_format(2) }}%</td>
</tr>
{% if entity.timeBudget > 0 %}
<tr>
<th colspan="3" class="text-nowrap text-right">
{% if totalPercentReached < 100 %}
{{ 'stats.percentUsedLeft'|trans({'%percent%': totalPercentReached|number_format(2), '%left%': (entity.timeBudget - stats.durationBillable)|duration}) }}
{% else %}
{{ 'stats.percentUsed'|trans({'%percent%': totalPercentReached|number_format(2)}) }}
{% endif %}
</th>
</tr>
{% endif %}
</table>
</div>
<div class="col-xs-12 col-sm-7">
<div class="box-padding">
{% if entity.timeBudget > 0 %}
{{ progress.progressbar(entity.timeBudget, stats.durationBillable, durationTrans|trans, stats.durationBillable|duration ~ ' / ' ~ entity.timeBudget|duration) }}
{% endif %}
{{ progress.progressbar(stats.duration, stats.durationBillable, 'label.billable'|trans, stats.durationBillable|duration ~ ' / ' ~ stats.duration|duration, true) }}
</div>
</div>
</div>
{% endblock %}
{% endembed %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% import "macros/progressbar.html.twig" as progress %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}
{{ 'label.budget'|trans }}
{% if entity.budget > 0 %}
{{ widgets.label(entity.budget|money(currency), 'gray') }}
{% endif %}
{{ 'label.budget'|trans }} {% if entity.isMonthlyBudget() %}({{ 'label.budgetType_month'|trans }}){% endif %}
{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_attributes %}id="budget_box"{% endblock %}
{% block box_body %}
{{ progress.progressbar(stats.recordRate, stats.rateBillable, 'label.billable'|trans, stats.rateBillable|money(currency) ~ ' / ' ~ stats.recordRate|money(currency), true) }}
{% if entity.budget > 0 %}
{{ progress.progressbar(entity.budget, stats.rateBillable, 'label.budget'|trans, stats.rateBillable|money(currency) ~ ' / ' ~ entity.budget|money(currency)) }}
{% endif %}
{{ progress.progressbar(stats.rateBillable, stats.recordInternalRate, 'label.rate_internal'|trans, stats.recordInternalRate|money(currency) ~ ' / ' ~ stats.rateBillable|money(currency)) }}
{% set rateTrans = entity.isMonthlyBudget() ? 'stats.amountMonth' : 'stats.amountTotal' %}
<div class="row">
<div class="col-xs-12 col-sm-5">
<table class="table table-hover dataTable">
<tr>
<td>{{ 'label.budget'|trans }}</td>
<td class="text-nowrap text-right">
{% if entity.budget > 0 %}
{{ entity.budget|money(currency) }}
{% else %}
-
{% endif %}
</td>
<td class="text-nowrap text-right">
{% if entity.budget > 0 %}
100%
{% else %}
-
{% endif %}
</td>
</tr>
{% set totalPercentReached = 100 %}
{% if entity.budget > 0 %}
{% set totalPercentReached = (stats.rateBillable / (entity.budget / 100)) %}
{% endif %}
<tr>
<td>{{ rateTrans|trans }}</td>
<td class="text-nowrap text-right">{{ stats.rate|money(currency) }}</td>
<td class="text-nowrap text-right">{{ totalPercentReached|number_format(2) }}%</td>
</tr>
{% set percentReached = 0 %}
{% if stats.rate > 0 %}
{% set percentReached = (stats.rateBillable / (stats.rate / 100)) %}
{% endif %}
<tr>
<td>{{ 'label.billable'|trans }}</td>
<td class="text-nowrap text-right">{{ stats.rateBillable|money(currency) }}</td>
<td class="text-nowrap text-right">{{ percentReached|number_format(2) }}%</td>
</tr>
{% set percentReached = 0 %}
{% if stats.rate > 0 %}
{% set percentReached = (stats.internalRate / (stats.rateBillable / 100)) %}
{% endif %}
<tr>
<td>{{ 'label.rate_internal'|trans }}</td>
<td class="text-nowrap text-right">{{ stats.internalRate|money(currency) }}</td>
<td class="text-nowrap text-right">{{ percentReached|number_format(2) }}%</td>
</tr>
{% if entity.budget > 0 %}
<tr>
<th colspan="3" class="text-nowrap text-right">
{% if totalPercentReached < 100 %}
{{ 'stats.percentUsedLeft'|trans({'%percent%': totalPercentReached|number_format(2), '%left%': (entity.timeBudget - stats.rateBillable)|money(currency)}) }}
{% else %}
{{ 'stats.percentUsed'|trans({'%percent%': totalPercentReached|number_format(2)}) }}
{% endif %}
</th>
</tr>
{% endif %}
</table>
</div>
<div class="col-xs-12 col-sm-7">
<div class="box-padding">
{% if entity.budget > 0 %}
{{ progress.progressbar(entity.budget, stats.rateBillable, rateTrans|trans, stats.rateBillable|money(currency) ~ ' / ' ~ entity.budget|money(currency)) }}
{% endif %}
{{ progress.progressbar(stats.rate, stats.rateBillable, 'label.billable'|trans, stats.rateBillable|money(currency) ~ ' / ' ~ stats.rate|money(currency), true) }}
{{ progress.progressbar(stats.rateBillable, stats.internalRate, 'label.rate_internal'|trans, stats.internalRate|money(currency) ~ ' / ' ~ stats.rateBillable|money(currency)) }}
</div>
</div>
</div>
{% endblock %}
{% endembed %}

View File

@@ -5,16 +5,6 @@
{% block document_title %}{{ 'export.title'|trans }}{% endblock %}
{% set storageItemName = 'kimai_html_export' %}
{% set showRateBudget = false %}
{% set showTimeBudget = false %}
{% for budget in budgets %}
{% if budget.money_left > 0 %}
{% set showRateBudget = true %}
{% endif %}
{% if budget.time_left > 0 %}
{% set showTimeBudget = true %}
{% endif %}
{% endfor %}
{% set decimal = decimal|default(false) %}
{% set columnTitles = {} %}
@@ -105,7 +95,6 @@
document.getElementById('export-summary').style.display = event.target.checked ? 'block' : 'none';
saveVisibility();
});
{% if showTimeBudget %}
document.getElementById('summary-timeBudget').addEventListener('click', function(event) {
var cells = document.getElementsByClassName('export-timeBudget');
for (var columnCell of cells) {
@@ -113,8 +102,6 @@
}
saveVisibility();
});
{% endif %}
{% if showRateBudget %}
document.getElementById('summary-budget').addEventListener('click', function(event) {
var cells = document.getElementsByClassName('export-budget');
for (var columnCell of cells) {
@@ -122,7 +109,6 @@
}
saveVisibility();
});
{% endif %}
document.getElementById('summary-duration').addEventListener('click', function(event) {
var cells = document.getElementsByClassName('summary-duration');
for (var columnCell of cells) {
@@ -360,22 +346,18 @@
{{ 'label.activity'|trans }}
</label>
</div>
{% if showTimeBudget %}
<div class="form-group">
<label class="control-label" for="summary-timeBudget">
<input type="checkbox" id="summary-timeBudget" name="summary-timeBudget" checked>
{{ 'label.timeBudget'|trans }}
</label>
</div>
{% endif %}
{% if showRateBudget %}
<div class="form-group">
<label class="control-label" for="summary-budget">
<input type="checkbox" id="summary-budget" name="summary-budget" checked>
{{ 'label.budget'|trans }}
</label>
</div>
{% endif %}
<div class="form-group">
<label class="control-label" for="summary-duration">
@@ -438,12 +420,8 @@
<tr>
<th>{{ 'label.customer'|trans }}</th>
<th>{{ 'label.project'|trans }}</th>
{% if showTimeBudget %}
<th class="center export-timeBudget">{{ 'label.timeBudget'|trans }}</th>
{% endif %}
{% if showRateBudget %}
<th class="center export-budget">{{ 'label.budget'|trans }}</th>
{% endif %}
<th class="center export-timeBudget">{{ 'label.timeBudget'|trans }}</th>
<th class="center export-budget">{{ 'label.budget'|trans }}</th>
<th class="duration summary-duration">{{ 'label.duration'|trans }}</th>
<th class="cost summary-rate-internal">{{ 'label.rate_internal'|trans }}</th>
<th class="cost summary-rate">{{ 'label.rate'|trans }}</th>
@@ -463,12 +441,8 @@
{% if customer is not same as(summary.customer) %}
<tr class="summary">
<td colspan="2">&nbsp;</td>
{% if showTimeBudget %}
<td class="export-timeBudget"></td>
{% endif %}
{% if showRateBudget %}
<td class="export-budget"></td>
{% endif %}
<td class="export-timeBudget"></td>
<td class="export-budget"></td>
<td class="totals duration summary-duration">
<span class="duration-format" data-duration="{{ customerDuration }}">{{ customerDuration|duration(decimal) }}</span>
</td>
@@ -484,20 +458,16 @@
<tr>
<td>{{ summary.customer }}</td>
<td>{{ summary.project }}</td>
{% if showTimeBudget %}
<td class="center export-timeBudget">
{% if budgets[id] is defined and budgets[id].time_left > 0 %}
<span class="duration-format" data-duration="{{ budgets[id].time_left }}">{{ budgets[id].time_left|duration(decimal) }}</span>
{% endif %}
</td>
{% endif %}
{% if showRateBudget %}
<td class="center export-budget">
{% if budgets[id] is defined and budgets[id].money_left > 0 %}
{{ budgets[id].money_left|money(summary.currency) }}
{% endif %}
</td>
{% endif %}
<td class="center export-timeBudget">
{% if budgets[id] is defined and budgets[id].time_left > 0 %}
<span class="duration-format" data-duration="{{ budgets[id].time_left }}">{{ budgets[id].time_left|duration(decimal) }}</span>
{% endif %}
</td>
<td class="center export-budget">
{% if budgets[id] is defined and budgets[id].money_left > 0 %}
{{ budgets[id].money_left|money(summary.currency) }}
{% endif %}
</td>
<td class="duration summary-duration">
<span class="duration-format" data-duration="{{ summary.duration }}">{{ summary.duration|duration(decimal) }}</span>
</td>
@@ -511,12 +481,8 @@
{% if customer is not same as(null) %}
<tr class="summary">
<td colspan="2">&nbsp;</td>
{% if showTimeBudget %}
<td class="export-timeBudget"></td>
{% endif %}
{% if showRateBudget %}
<td class="export-budget"></td>
{% endif %}
<td class="export-timeBudget"></td>
<td class="export-budget"></td>
<td class="totals duration summary-duration">
<span class="duration-format" data-duration="{{ customerDuration }}">{{ customerDuration|duration(decimal) }}</span>
</td>
@@ -539,6 +505,8 @@
<th>{{ 'label.customer'|trans }}</th>
<th>{{ 'label.project'|trans }}</th>
<th>{{ 'label.activity'|trans }}</th>
<th class="center export-timeBudget">{{ 'label.timeBudget'|trans }}</th>
<th class="center export-budget">{{ 'label.budget'|trans }}</th>
<th class="duration summary-duration">{{ 'label.duration'|trans }}</th>
<th class="cost summary-rate-internal">{{ 'label.rate_internal'|trans }}</th>
<th class="cost summary-rate">{{ 'label.rate'|trans }}</th>
@@ -550,7 +518,7 @@
{% set customerRate = 0 %}
{% set customerInternalRate = 0 %}
{% set customerCurrency = null %}
{% for summary in summaries %}
{% for id, summary in summaries %}
{% if customer is same as(null) %}
{% set customer = summary.customer %}
{% set customerCurrency = summary.currency %}
@@ -558,6 +526,8 @@
{% if customer is not same as(summary.customer) %}
<tr class="summary">
<td colspan="3"></td>
<td class="export-timeBudget"></td>
<td class="export-budget"></td>
<td class="totals duration summary-duration">
<span class="duration-format" data-duration="{{ customerDuration }}">{{ customerDuration|duration(decimal) }}</span>
</td>
@@ -570,11 +540,26 @@
{% set customerRate = 0 %}
{% set customerInternalRate = 0 %}
{% endif %}
{% for activitySummary in summary.activities %}
{% for activityId, activitySummary in summary.activities %}
<tr>
<td>{{ summary.customer }}</td>
<td>{{ summary.project }}</td>
<td>{{ activitySummary.activity }}</td>
{% if activity_budgets[id] is defined and activity_budgets[id][activityId] is defined %}
<td class="center export-timeBudget">
{% if activity_budgets[id][activityId].time_left > 0 %}
<span class="duration-format" data-duration="{{ activity_budgets[id][activityId].time_left }}">{{ activity_budgets[id][activityId].time_left|duration(decimal) }}</span>
{% endif %}
</td>
<td class="center export-budget">
{% if activity_budgets[id][activityId].money_left > 0 %}
{{ activity_budgets[id][activityId].money_left|money(summary.currency) }}
{% endif %}
</td>
{% else %}
<td class="center export-timeBudget"></td>
<td class="center export-budget"></td>
{% endif %}
<td class="duration summary-duration">
<span class="duration-format" data-duration="{{ activitySummary.duration }}">{{ activitySummary.duration|duration(decimal) }}</span>
</td>
@@ -589,6 +574,8 @@
{% if customer is not same as(null) %}
<tr class="summary">
<td colspan="3"></td>
<td class="export-timeBudget"></td>
<td class="export-budget"></td>
<td class="totals duration summary-duration">
<span class="duration-format" data-duration="{{ customerDuration }}">{{ customerDuration|duration(decimal) }}</span>
</td>

View File

@@ -73,7 +73,7 @@
{% block yearpicker_widget -%}
<div class="btn-group">
<a class="btn btn-default btn-left" href="#" onclick="$('#{{ form.vars.id }}').val('{{ previousYear|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ previousYear|date_short }}">
<a class="btn btn-default btn-left" href="#" onclick="$('#{{ form.vars.id }}').val('{{ previousYear|report_date }}').change()" data-toggle="tooltip" data-placement="top" title="{{ previousYear|date_short }}">
<i class="{{ 'left'|icon }}"></i>
</a>
<a class="btn btn-default" href="#" onclick="return false;">
@@ -85,7 +85,7 @@
{% endif %}
</span>
</a>
<a class="btn btn-default btn-right" href="#" onclick="$('#{{ form.vars.id }}').val('{{ nextYear|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ nextYear|date_short }}">
<a class="btn btn-default btn-right" href="#" onclick="$('#{{ form.vars.id }}').val('{{ nextYear|report_date }}').change()" data-toggle="tooltip" data-placement="top" title="{{ nextYear|date_short }}">
<i class="{{ 'right'|icon }}"></i>
</a>
</div>
@@ -94,13 +94,13 @@
{% block monthpicker_widget -%}
<div class="btn-group">
<a class="btn btn-default btn-left" href="#" onclick="$('#{{ form.vars.id }}').val('{{ previousMonth|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ previousMonth|month_name(true) }}">
<a class="btn btn-default btn-left" href="#" onclick="$('#{{ form.vars.id }}').val('{{ previousMonth|report_date }}').change()" data-toggle="tooltip" data-placement="top" title="{{ previousMonth|month_name(true) }}">
<i class="{{ 'left'|icon }}"></i>
</a>
<a class="btn btn-default" href="#" onclick="return false;">
<span id="{{ form.vars.id }}_month_name">{{ month|month_name(true) }}</span>
</a>
<a class="btn btn-default btn-right" href="#" onclick="$('#{{ form.vars.id }}').val('{{ nextMonth|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ nextMonth|month_name(true) }}">
<a class="btn btn-default btn-right" href="#" onclick="$('#{{ form.vars.id }}').val('{{ nextMonth|report_date }}').change()" data-toggle="tooltip" data-placement="top" title="{{ nextMonth|month_name(true) }}">
<i class="{{ 'right'|icon }}"></i>
</a>
</div>
@@ -109,13 +109,13 @@
{% block weekpicker_widget -%}
<div class="btn-group">
<a class="btn btn-default btn-left" href="#" onclick="$('#{{ form.vars.id }}').val('{{ previousWeek|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ 'stats.workingTimeWeek'|trans({'%week%': previousWeek|date_format('W')}) }}">
<a class="btn btn-default btn-left" href="#" onclick="$('#{{ form.vars.id }}').val('{{ previousWeek|report_date }}').change()" data-toggle="tooltip" data-placement="top" title="{{ 'stats.workingTimeWeek'|trans({'%week%': previousWeek|date_format('W')}) }}">
<i class="{{ 'left'|icon }}"></i>
</a>
<a class="btn btn-default" href="#" onclick="return false;">
<span id="{{ form.vars.id }}_week_number">{{ 'stats.workingTimeWeek'|trans({'%week%': week|date_format('W')}) }}</span>
</a>
<a class="btn btn-default btn-right" href="#" onclick="$('#{{ form.vars.id }}').val('{{ nextWeek|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ 'stats.workingTimeWeek'|trans({'%week%': nextWeek|date_format('W')}) }}">
<a class="btn btn-default btn-right" href="#" onclick="$('#{{ form.vars.id }}').val('{{ nextWeek|report_date }}').change()" data-toggle="tooltip" data-placement="top" title="{{ 'stats.workingTimeWeek'|trans({'%week%': nextWeek|date_format('W')}) }}">
<i class="{{ 'right'|icon }}"></i>
</a>
</div>

View File

@@ -109,6 +109,9 @@
{# change the next if, once data_table_header_options() will be deleted #}
{% if classes is iterable %}
{% set classes = classes.class %}
{% if columns[column].columnClass is defined %}
{% set classes = classes ~ ' ' ~ columns[column].columnClass %}
{% endif %}
{% endif %}
{% if 'alwaysVisible' in classes %}
{# as this column should always be visible, we remove every class that includes hidden #}

View File

@@ -6,6 +6,8 @@
{% set formatter = options['format'] ?? null %}
{% set currency = options['currency'] ?? null %}
{% set percentDecimals = options['decimals'] ?? 1 %}
{% set transPercentOpen = options['trans_percent_open'] ?? 'stats.percentUsedLeft' %}
{% set transPercentSpent = options['trans_percent_spent'] ?? 'stats.percentUsed' %}
{% if formatter == 'duration' %}
{% set currentFormatted = current|duration %}
{% set maxFormatted = max|duration %}
@@ -44,9 +46,9 @@
<div class="progress-bar {{ progressbar_color(percentReached, reverseColors) }}" role="progressbar" aria-valuenow="{{ width }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ width }}%"></div>
</div>
{% if open is not null and open > 0 %}
<small>{{ 'stats.percentUsedLeft'|trans({'%percent%': percentReached|number_format(percentDecimals), '%left%': openFormatted}) }}</small>
<small>{{ transPercentOpen|trans({'%percent%': percentReached|number_format(percentDecimals), '%left%': openFormatted}) }}</small>
{% else %}
<small>{{ 'stats.percentUsed'|trans({'%percent%': percentReached|number_format(percentDecimals)}) }}</small>
<small>{{ transPercentSpent|trans({'%percent%': percentReached|number_format(percentDecimals)}) }}</small>
{% endif %}
</div>
{% endmacro %}
@@ -104,3 +106,47 @@
{% endif %}
</div>
{% endmacro %}
{#
@param \App\Model\BudgetStatisticModelInterface budgetStatisticModel
@param string currency
#}
{% macro progressbar_budget(budgetStatisticModel, currency) %}
{% if budgetStatisticModel.hasBudget() %}
{% set options = {
'max': budgetStatisticModel.getBudget(),
'current': budgetStatisticModel.getBudgetSpent(),
'open': (budgetStatisticModel.getBudgetOpen()),
'format': 'money',
'currency': currency,
} %}
{% if budgetStatisticModel.isMonthlyBudget() %}
{% set options = options|merge({
'trans_percent_open': 'stats.percentUsedLeft_month',
'trans_percent_spent': 'stats.percentUsed_month',
}) %}
{% endif %}
{{ _self.progressbar_full(options) }}
{% endif %}
{% endmacro %}
{#
@param \App\Model\BudgetStatisticModelInterface budgetStatisticModel
#}
{% macro progressbar_timebudget(budgetStatisticModel) %}
{% if budgetStatisticModel.hasTimeBudget() %}
{% set options = {
'max': budgetStatisticModel.getTimeBudget(),
'current': budgetStatisticModel.getTimeBudgetSpent(),
'open': (budgetStatisticModel.getTimeBudgetOpen()),
'format': 'duration',
} %}
{% if budgetStatisticModel.isMonthlyBudget() %}
{% set options = options|merge({
'trans_percent_open': 'stats.percentUsedLeft_month',
'trans_percent_spent': 'stats.percentUsed_month',
}) %}
{% endif %}
{{ _self.progressbar_full(options) }}
{% endif %}
{% endmacro %}

View File

@@ -504,7 +504,7 @@
{% if showTitle %}
<thead>
<tr>
<th>{{ 'label.team'|trans }}</th>
<th class="hw-min">{{ 'label.team'|trans }}</th>
<th>{{ 'label.user'|trans }}</th>
</tr>
</thead>
@@ -515,7 +515,7 @@
{% set userTeamCount = users|length %}
<tr{% if is_granted('edit', team) %} class="modal-ajax-form open-edit" data-href="{{ path('admin_team_member', {'id': team.id}) }}"{% endif %}>
<td>
{{ team.name }}
{{ _self.label_team(team) }}
</td>
<td class="avatars">
{{ _self.user_avatar(team.teamlead, ('label.teamlead'|trans ~ ': ' ~ team.teamlead.displayName), 'teamlead') }}
@@ -655,3 +655,11 @@
{% endfor %}
</div>
{% endmacro %}
{% macro percent(maximum, current) %}
{% if maximum > 0 %}
{{ (current / (maximum / 100))|number_format(2) }} %
{% else %}
{{ 0|number_format(2) }} %
{% endif %}
{% endmacro %}

View File

@@ -6,18 +6,15 @@
{% block main %}
{% set inUse = (stats.recordAmount > 0) %}
{% set inUse = (stats.counter > 0) %}
{% set params = {
'%project%': project.name,
'%customer%': project.customer.name,
'%records%': stats.recordAmount,
'%activities%': stats.activityAmount,
'%duration%': stats.recordDuration|duration
'%records%': stats.counter,
'%duration%': stats.duration|duration
} %}
{{ include(app.request.xmlHttpRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
'message': ("admin_project.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
'message': ("delete_warning.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
'form': form,
'used': inUse,
'back': path('admin_project')

View File

@@ -49,42 +49,66 @@
</td>
</tr>
{% if is_granted('details', project) %}
{% if project.orderNumber is not empty %}
<tr>
<th>{{ 'label.orderNumber'|trans }}</th>
<td colspan="3">
{{ project.orderNumber }}
</td>
</tr>
{% endif %}
{% if project.orderDate is not empty %}
<tr>
<th>{{ 'label.orderDate'|trans }}</th>
<td colspan="3">
{% if project.orderDate is not empty %}
{{ project.orderDate|date_full }}
{% else %}
&ndash;
{% endif %}
{{ project.orderDate|date_full }}
</td>
</tr>
{% endif %}
{% if project.start is not empty %}
<tr>
<th>{{ 'label.project_start'|trans }}</th>
<td colspan="3">
{% if project.start is not empty %}
{{ project.start|date_full }}
{% else %}
&ndash;
{% endif %}
{{ project.start|date_full }}
</td>
</tr>
{% endif %}
{% if project.end is not empty %}
<tr>
<th>{{ 'label.project_end'|trans }}</th>
<td colspan="3">
{% if project.end is not empty %}
{{ project.end|date_full }}
{% else %}
&ndash;
{% endif %}
{{ project.end|date_full }}
</td>
</tr>
{% endif %}
{% endif %}
{% if is_granted('budget', project) %}
{% if project.hasBudget() %}
<tr>
<th>
{{ 'label.budget'|trans }}
{% if project.isMonthlyBudget() %}
({{ 'label.budgetType_month'|trans }})
{% endif %}
</th>
<td colspan="3">
{{ project.getBudget()|money(project.customer.currency) }}
</td>
</tr>
{% endif %}
{% if project.hasTimeBudget() %}
<tr>
<th>
{{ 'label.timeBudget'|trans }}
{% if project.isMonthlyBudget() %}
({{ 'label.budgetType_month'|trans }})
{% endif %}
</th>
<td colspan="3">
{{ project.getTimeBudget()|duration }}
</td>
</tr>
{% endif %}
{% endif %}
{% for metaField in project.visibleMetaFields|sort((a, b) => a.order <=> b.order) %}
<tr>

View File

@@ -41,12 +41,15 @@
</div>
{% if form.budget is defined %}
<div class="row">
<div class="col-md-6">
<div class="col-md-4">
{{ form_row(form.budget) }}
</div>
<div class="col-md-6">
<div class="col-md-4">
{{ form_row(form.timeBudget) }}
</div>
<div class="col-md-4">
{{ form_row(form.budgetType) }}
</div>
</div>
{% endif %}
{{ form_row(form.visible) }}

View File

@@ -0,0 +1,97 @@
{% extends 'reporting/layout.html.twig' %}
{% import "macros/datatables.html.twig" as tables %}
{% block report_title %}{{ 'report_project_daterange'|trans({}, 'reporting') }}{% endblock %}
{% set columns = {
'name': {'class': 'alwaysVisible'},
'timeBudget': {'class': 'hidden-xs', 'title': 'label.timeBudget'|trans},
'budget': {'class': 'hidden-xs', 'title': 'label.budget'|trans},
'duration': {'class': 'text-center hw-min', 'title': 'stats.durationMonth'|trans, 'columnClass': 'w-min'},
'rate': {'class': 'text-center hw-min', 'title': 'stats.amountMonth'|trans, 'columnClass': 'w-min'},
'billable': {'class': 'text-center hw-min', 'columnClass': 'w-min'},
'actions': {'class': 'actions alwaysVisible'},
} %}
{% set tableName = 'project_daterange_reporting' %}
{% block main_before %}
{{ tables.data_table_column_modal(tableName, columns) }}
{% endblock %}
{% block report %}
{% set hasData = entries|length > 0 %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% import "macros/progressbar.html.twig" as progress %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %}
{% import "project/actions.html.twig" as projectActions %}
{% block box_body_class %}{{ tableName }}-box {% if hasData %}no-padding{% endif %}{% endblock %}
{% block box_before %}
{{ form_start(form, {'attr': {'class': 'form-inline form-reporting', 'id': 'project-daterange-form'}}) }}
{% endblock %}
{% block box_after %}
{{ form_end(form) }}
{% endblock %}
{% block box_tools %}
{{ widgets.action_button('visibility', {'modal': ('#modal_' ~ tableName), 'class': 'btn-sm'}) }}
{% endblock %}
{% block box_title %}
{{ form_widget(form) }}
{% endblock %}
{% block box_body %}
{% if not hasData %}
{{ widgets.nothing_found() }}
{% else %}
{{ tables.datatable_header(tableName, columns, null, {'bordered': true, 'boxClass': ''}) }}
{% for id, mapping in entries|sort((a, b) => a.customer.name <=> b.customer.name) %}
<tr class="summary">
<td colspan="{{ columns|length }}">{{ widgets.label_customer(mapping.customer) }}</td>
</tr>
{% for entry in mapping.projects|sort((a, b) => a.entity.name <=> b.entity.name) %}
{% set project = entry.entity %}
{% set currency = project.customer.currency %}
{% if is_granted('budget', project) %}
<tr {{ widgets.project_row_attr(project, queryEnd) }}>
{% for name, column_config in columns %}
<td class="{{ tables.data_table_column_class(tableName, columns, name) }}">
{% if name == 'name' %}
{{ widgets.label_project(project) }}
{% elseif name == 'duration' %}
{{ entry.statistic.duration|duration }}
{% elseif name == 'rate' %}
{{ entry.statistic.rate|money(currency) }}
{% elseif name == 'billable' %}
{{ widgets.percent(entry.statistic.rate, entry.statistic.rateBillable) }}
{% elseif name == 'timeBudget' %}
{{ progress.progressbar_timebudget(entry) }}
{% elseif name == 'budget' %}
{{ progress.progressbar_budget(entry, currency) }}
{% elseif name == 'actions' %}
{{ projectActions.project(project, 'custom') }}
{% endif %}
</td>
{% endfor %}
</tr>
{% endif %}
{% endfor %}
{% endfor %}
{{ tables.data_table_footer(entries) }}
{% endif %}
{% endblock %}
{% endembed %}
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script type="text/javascript">
document.addEventListener('kimai.initialized', function() {
jQuery('#project-daterange-form').on('change', function(ev) {
jQuery(this).submit();
});
});
</script>
{% endblock %}

View File

@@ -57,7 +57,7 @@
{% endblock %}
{% block report %}
{% set hasData = project_view is not null %}
{% set hasData = project is not null and project_view is not null %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
@@ -79,8 +79,8 @@
{% endif %}
{% if hasData %}
{{ _self.project_details(project_view, project_details, view_revenue_tab, view_revenue_user, see_users) }}
{% set currency = project_view.project.customer.currency %}
{{ _self.project_details(project, project_view, project_details, view_revenue_tab, view_revenue_user, see_users) }}
{% set currency = project.customer.currency %}
{%- for yearStat in project_details.years|reverse %}
{% set year = yearStat.year %}
@@ -241,16 +241,16 @@
{% endmacro %}
{#
entry = ProjectViewModel
project = Project
project_view = ProjectViewModel
project_details = ProjectDetailsModel
#}
{% macro project_details(entry, project_details, view_revenue_tab, view_revenue_user, see_users) %}
{% macro project_details(project, project_view, project_details, view_revenue_tab, view_revenue_user, see_users) %}
{% set activities = project_details.activities %}
{% set years = project_details.years %}
{% import "macros/progressbar.html.twig" as progress %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/charts.html.twig" as charts %}
{% set project = entry.project %}
{% set currency = project.customer.currency %}
{% set chartPrefix = 'project' ~ project.id %}
@@ -265,14 +265,18 @@
<div class="nav-tabs-custom">
<ul class="nav nav-tabs pull-right">
{% if see_users %}
{% if see_users and project_details.userStats|length > 0 %}
<li><a data-chart="{{ chartPrefix }}User" href="#user-chart" data-toggle="tab">{{ 'label.user'|trans }}</a></li>
{% endif %}
{% if activities is not empty %}
<li><a data-chart="{{ chartPrefix }}Activity" href="#activity-chart" data-toggle="tab">{{ 'label.activity'|trans }}</a></li>
{% if view_revenue_tab %}
{% endif %}
{% if view_revenue_tab and project_view.rateTotal > 0 %}
<li><a data-chart="{{ chartPrefix }}Rate" href="#revenue-chart" data-toggle="tab">{{ 'stats.revenue'|trans }}</a></li>
{% endif %}
{% if project_view.durationTotal > 0 %}
<li><a data-chart="{{ chartPrefix }}Duration" href="#time-chart" data-toggle="tab">{{ 'stats.workingTime'|trans }}</a></li>
{% endif %}
<li class="active"><a href="#details-chart" data-toggle="tab">{{ 'report_project_details'|trans({}, 'reporting') }}</a></li>
<li class="pull-left header hidden-xs">
{{ widgets.label_project(project, {'inherit': false}) }}
@@ -283,26 +287,26 @@
<div class="row">
<div class="col-xs-12 col-md-6">
<table class="table table-hover dataTable">
<tr {{ widgets.customer_row_attr(entry.project.customer) }}>
<tr {{ widgets.customer_row_attr(project.customer) }}>
<th class="w-min">
{{ 'label.customer'|trans }}
</th>
<td>
{{ widgets.label_customer(entry.project.customer) }}
{{ widgets.label_customer(project.customer) }}
</td>
</tr>
<tr>
<th class="w-min">
{{ 'stats.durationTotal'|trans }}
</th>
<td>{{ entry.durationTotal|duration }}</td>
<td>{{ project_view.durationTotal|duration }}</td>
</tr>
{% if view_revenue_tab %}
<tr>
<th class="w-min">
{{ 'stats.amountTotal'|trans }}
</th>
<td>{{ entry.rateTotal|money(currency) }}</td>
<td>{{ project_view.rateTotal|money(currency) }}</td>
</tr>
{% endif %}
</table>
@@ -314,8 +318,8 @@
{{ 'label.last_record'|trans }}
</th>
<td>
{% if entry.lastRecord is not null %}
{{ entry.lastRecord|date_short }}
{% if project_view.lastRecord is not null %}
{{ project_view.lastRecord|date_short }}
{% else %}
&ndash;
{% endif %}
@@ -328,7 +332,7 @@
</th>
<td>
<a href="{{ path('export', {'customers[]': project.customer.id, 'projects[]': project.id, 'daterange': '', 'preview': true}) }}">
{{ entry.notExportedDuration|duration }}
{{ project_view.notExportedDuration|duration }}
</a>
</td>
</tr>
@@ -340,7 +344,7 @@
</th>
<td>
<a href="{{ path('invoice', {'customers[]': project.customer.id, 'projects[]': project.id, 'daterange': ''}) }}">
{{ entry.notBilledRate|money(currency) }}
{{ project_view.notBilledRate|money(currency) }}
</a>
</td>
</tr>
@@ -349,62 +353,58 @@
</div>
</div>
{% if view_revenue_tab and (project.timeBudget > 0 or project.budget > 0) %}
<div class="row">
<div class="col-xs-12">
<table class="table table-hover dataTable">
{% if project.timeBudget > 0 %}
<tr>
<th class="w-min">
{{ 'label.timeBudget'|trans }}
</th>
<td>
{% set timeBudgetLeft = project.timeBudget - entry.billableDuration %}
{% set options = {
'max': project.timeBudget|default(0),
'current': entry.billableDuration|default(0),
'open': timeBudgetLeft,
'format': 'duration'
} %}
{{ progress.progressbar_full(options) }}
</td>
</tr>
{% endif %}
{% if project.budget > 0 %}
<tr>
<th class="w-min">
{{ 'label.budget'|trans }}
</th>
<td>
{% set budgetLeft = project.budget - entry.billableRate %}
{% set options = {
'max': project.budget|default(0),
'current': entry.billableRate|default(0),
'open': budgetLeft,
'format': 'money',
'currency': currency
} %}
{{ progress.progressbar_full(options) }}
</td>
</tr>
{% endif %}
</table>
{% set budgetStats = project_details.budgetStatisticModel %}
<div class="row">
<div class="col-xs-12">
<table class="table table-hover dataTable">
{% if project.timeBudget > 0 %}
<tr>
<th class="w-min">
{{ 'label.timeBudget'|trans }}
{% if budgetStats.isMonthlyBudget() %}
({{ 'label.budgetType_month'|trans }})
{% endif %}
</th>
<td>
{{ progress.progressbar_timebudget(budgetStats) }}
</td>
</tr>
{% endif %}
{% if project.budget > 0 %}
<tr>
<th class="w-min">
{{ 'label.budget'|trans }}
{% if budgetStats.isMonthlyBudget() %}
({{ 'label.budgetType_month'|trans }})
{% endif %}
</th>
<td>
{{ progress.progressbar_budget(budgetStats, project.customer.currency) }}
</td>
</tr>
{% endif %}
</table>
</div>
</div>
</div>
{% endif %}
</div>
{% if view_revenue_tab %}
{% if view_revenue_tab and project_view.rateTotal > 0 %}
<div class="chart tab-pane" id="revenue-chart">
{{ charts.bar_chart(chartPrefix ~ 'Rate', labels, [rates], {'height': '300px', 'renderEvent': 'render.' ~ chartPrefix ~ 'Rate'}) }}
</div>
{% endif %}
{% if project_view.durationTotal > 0 %}
<div class="chart tab-pane" id="time-chart">
{{ charts.bar_chart(chartPrefix ~ 'Duration', labels, [durations], {'height': '300px', 'renderEvent': 'render.' ~ chartPrefix ~ 'Duration'}) }}
</div>
{% endif %}
{% if activities is not empty %}
<div class="chart tab-pane" id="activity-chart">
{{ _self.activity_tab(activities, entry.durationTotal, project.customer.currency, chartPrefix, view_revenue_tab) }}
{{ _self.activity_tab(activities, project_view.durationTotal, project.customer.currency, chartPrefix, view_revenue_tab) }}
</div>
{% if see_users %}
{% endif %}
{% if see_users and project_details.userStats|length > 0 %}
<div class="chart tab-pane" id="user-chart">
<div class="row">
<div class="col-xs-12 col-sm-9 col-md-8 col-lg-6">
@@ -420,8 +420,8 @@
{% set totalDuration = totalDuration + userStat.duration %}
{% set datasets = datasets|merge([{'name': user.displayName, 'duration': userStat.duration|duration, 'value': userStat.duration, 'color': color, 'rate': userStat.rate|money(currency)}]) %}
{% set percentage = 0 %}
{% if entry.durationTotal > 0 and userStat.duration > 0 %}
{% set percentage = (100 / (entry.durationTotal / userStat.duration)) %}
{% if project_view.durationTotal > 0 and userStat.duration > 0 %}
{% set percentage = (100 / (project_view.durationTotal / userStat.duration)) %}
{% endif %}
<tr>
<td>

View File

@@ -3,30 +3,30 @@
{% block report_title %}{{ (title|default('report_project_view'))|trans({}, 'reporting') }}{% endblock %}
{% set showDurations = showDurations is defined and showDurations is same as (true) %}
{% set columns = {
{% set availableColumns = {
'name': {'class': 'alwaysVisible'},
'lastRecord': {'class': 'hidden text-center hw-min', 'title': 'label.last_record'|trans},
} %}
{% if showDurations %}
{% set columns = columns|merge({
'today': {'class': 'hidden hidden-md hidden-sm hidden-xs text-center hw-min', 'title': 'stats.durationToday'|trans},
'week': {'class': 'hidden hidden-md hidden-sm hidden-xs text-center hw-min', 'title': 'stats.durationWeek'|trans},
'month': {'class': 'hidden hidden-md hidden-sm hidden-xs text-center hw-min', 'title': 'stats.durationMonth'|trans},
}) %}
{% endif %}
{% set columns = columns|merge({
'durationTotal': {'class': 'text-center hw-min', 'title': 'stats.durationTotal'|trans},
'timeBudget': {'class': 'hidden-xs', 'title': 'label.timeBudget'|trans},
'budget': {'class': 'hidden-xs', 'title': 'label.budget'|trans},
'stateDuration': {'class': 'hidden-sm hidden-xs text-center hw-min', 'title': 'label.not_exported'|trans},
'stateMoney': {'class': 'hidden-sm hidden-xs text-center hw-min', 'title': 'label.not_invoiced'|trans},
'lastRecord': {'class': 'text-center hw-min', 'title': 'label.last_record'|trans, 'columnClass': 'w-min'},
'today': {'class': 'hidden hidden-md hidden-sm hidden-xs text-center hw-min', 'title': 'stats.durationToday'|trans},
'week': {'class': 'hidden hidden-md hidden-sm hidden-xs text-center hw-min', 'title': 'stats.durationWeek'|trans},
'month': {'class': 'hidden hidden-md hidden-sm hidden-xs text-center hw-min', 'title': 'stats.durationMonth'|trans},
'durationTotal': {'class': 'text-center hw-min', 'title': 'stats.durationTotal'|trans, 'columnClass': 'w-min'},
'exported': {'class': 'hidden-sm hidden-xs hidden text-center hw-min', 'title': 'label.not_exported'|trans, 'columnClass': 'w-min'},
'invoiced': {'class': 'hidden-sm hidden-xs hidden text-center hw-min', 'title': 'label.not_invoiced'|trans, 'columnClass': 'w-min'},
'projectStart': {'class': 'hidden-md hidden-sm hidden-xs hidden text-center w-min', 'title': 'label.project_start'|trans},
'projectEnd': {'class': 'hidden-md hidden-sm hidden-xs hidden text-center w-min', 'title': 'label.project_end'|trans},
'comment': {'class': 'hidden-md hidden-sm hidden-xs hidden', 'title': 'label.comment'|trans},
'actions': {'class': 'actions alwaysVisible'},
}) %}
} %}
{% set tableName = tableName|default('project_view_reporting') %}
{% set skipColumns = skipColumns is defined ? skipColumns : {} %}
{% set columns = {} %}
{% for name, config in availableColumns %}
{% if name not in skipColumns %}
{% set columns = columns|merge({(name): config}) %}
{% endif %}
{% endfor %}
{% block main_before %}
{{ tables.data_table_column_modal(tableName, columns) }}
@@ -66,10 +66,14 @@
</tr>
{% for entry in mapping.projects|sort((a, b) => a.project.name <=> b.project.name) %}
{% set project = entry.project %}
{% set budgetStats = entry.getBudgetStatisticModel() %}
{% set currency = project.customer.currency %}
{% if is_granted('budget', project) %}
<tr{% if is_granted('details', project) %} class="alternative-link open-edit" data-href="{{ path('report_project_details', {'project': project.id}) }}"{% endif %}>
<tr {{ widgets.project_row_attr(project, now) }}>
{% for name, column_config in columns %}
{% if name == 'name' %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">{{ widgets.label_project(project) }}</td>
{% elseif name == 'lastRecord' %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'lastRecord') }}">
{% if entry.lastRecord is not null %}
{{ entry.lastRecord|date_short }}
@@ -77,38 +81,28 @@
&ndash;
{% endif %}
</td>
{% if showDurations %}
{% elseif name == 'today' %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'today') }}">{{ entry.durationDay|duration }}</td>
{% elseif name == 'week' %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'week') }}">{{ entry.durationWeek|duration }}</td>
{% elseif name == 'month' %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'month') }}">{{ entry.durationMonth|duration }}</td>
{% endif %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'durationTotal') }}">{{ entry.durationTotal|duration }}</td>
{% elseif name == 'durationTotal' %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'durationTotal') }} w-min">{{ entry.durationTotal|duration }}</td>
{% elseif name == 'timeBudget' %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'timeBudget') }}">
{% if project.timeBudget > 0 %}
{% set timeBudgetLeft = project.timeBudget - entry.billableDuration %}
{% set options = {
'max': project.timeBudget|default(0),
'current': entry.billableDuration|default(0),
'open': timeBudgetLeft,
'format': 'duration'
} %}
{{ progress.progressbar_full(options) }}
{% if budgetStats.hasTimeBudget() %}
{{ progress.progressbar_timebudget(budgetStats) }}
{% endif %}
</td>
{% elseif name == 'budget' %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'budget') }}">
{% if project.budget > 0 %}
{% set budgetLeft = project.budget - entry.billableRate %}
{% set options = {
'max': project.budget|default(0),
'current': entry.billableRate|default(0),
'open': budgetLeft,
'format': 'money',
'currency': currency
} %}
{{ progress.progressbar_full(options) }}
{% if project.hasBudget() %}
{{ progress.progressbar_budget(budgetStats, project.customer.currency) }}
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'stateDuration') }}">
{% elseif name == 'exported' %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'exported') }}">
{% if is_granted('create_export') %}
<a href="{{ path('export', {'customers[]': project.customer.id, 'projects[]': project.id, 'daterange': '', 'preview': true}) }}">
{{ entry.notExportedDuration|duration }}
@@ -117,7 +111,8 @@
{{ entry.notExportedDuration|duration }}
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'stateMoney') }}">
{% elseif name == 'invoiced' %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'invoiced') }}">
{% if is_granted('view_invoice') %}
<a href="{{ path('invoice', {'customers[]': project.customer.id, 'projects[]': project.id, 'daterange': ''}) }}">
{{ entry.notBilledRate|money(currency) }}
@@ -126,12 +121,18 @@
{{ entry.notBilledRate|money(currency) }}
{% endif %}
</td>
{% elseif name == 'projectStart' %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'projectStart') }}">{% if project.start is not null %}{{ project.start|date_short }}{% endif %}</td>
{% elseif name == 'projectEnd' %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'projectEnd') }}">{% if project.end is not null %}{{ project.end|date_short }}{% endif %}</td>
{% elseif name == 'comment' %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ project.comment }}</td>
{% elseif name == 'actions' %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'actions') }}">
{{ projectActions.project(project, 'custom') }}
</td>
{% endif %}
{% endfor %}
</tr>
{% endif %}
{% endfor %}
@@ -147,8 +148,8 @@
{{ parent() }}
<script type="text/javascript">
document.addEventListener('kimai.initialized', function() {
$('#project-view-form').on('change', function(ev) {
$(this).submit();
jQuery('#project-view-form').on('change', function(ev) {
jQuery(this).submit();
});
});
</script>

View File

@@ -4,13 +4,6 @@
{% block report %}
{% set hasData = false %}
{% for day in days %}
{% if day.details is not empty %}
{% set hasData = true %}
{% endif %}
{% endfor %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_before %}
@@ -27,67 +20,72 @@
{% endif %}
{{ form_widget(form.date) }}
{% endblock %}
{% block box_body_class %}{{ box_id }} table-responsive{% if hasData %} no-padding{% endif %}{% endblock %}
{% block box_body_class %}{{ box_id }} table-responsive no-padding{% endblock %}
{% block box_body %}
{% if not hasData %}
{{ widgets.nothing_found() }}
{% else %}
{% set totals = {'totals': 0} %}
{% set columns = 2 %}
<table class="table table-bordered table-hover dataTable">
<tr>
<th>&nbsp;</th>
<th>&nbsp;</th>
{% for day in days %}
<th class="text-center text-nowrap{% if day.day is weekend %} weekend{% endif %}">
{{ day.day|day_name(true) }}<br>
{{ day.day|format_date('short') }}
</th>
{% for day in days.dateTimes %}
<th class="text-center text-nowrap{% if day is weekend %} weekend{% endif %}">
{{ day|day_name(true) }}<br>
{{ day|format_date('short') }}
</th>
{% set columns = columns + 1 %}
{% set totals = totals|merge({(day|report_date): 0}) %}
{% endfor %}
</tr>
{% for project in rows %}
{% set oldCustomer = null %}
{% for pid, project in rows|sort((a,b) => a.project.customer.id <=> b.project.customer.id) %}
{% if oldCustomer is null or oldCustomer != project.project.customer.id %}
{% set oldCustomer = project.project.customer.id %}
<tr class="summary">
<td colspan="{{ columns }}">{{ widgets.label_customer(project.project.customer) }}</td>
</tr>
{% endif %}
{% set totals = totals|merge({'totals': (totals['totals'] + project.duration)}) %}
<tr class="project">
<td class="text-nowrap">
<strong>{{ widgets.label_project(project.project) }}</strong>
</td>
<th class="text-nowrap text-center total">{{ project.duration|duration }}</th>
{% for day in project.days %}
{% for day in project.days.days %}
<td class="text-nowrap text-center day-total{% if day.date is weekend %} weekend{% endif %}">
{% if day.duration > 0 %}
{% set totals = totals|merge({(day.date|report_date): (totals[day.date|report_date] + day.duration)}) %}
<strong>{{ day.duration|duration }}</strong>
{% endif %}
</td>
{% endfor %}
</tr>
{% for activity in project.activities %}
<tr class="activity">
<td class="text-nowrap">
{{ widgets.label_activity(activity.activity) }}
</td>
<th class="text-nowrap text-center total">{{ activity.duration|duration }}</th>
{% for day in activity.days %}
<td class="text-nowrap text-center day-total{% if day.date is weekend %} weekend{% endif %}">
{% if day.duration > 0 %}
{{ day.duration|duration }}
{% endif %}
<tr class="activity">
<td class="text-nowrap">
{{ widgets.label_activity(activity.activity) }}
</td>
{% endfor %}
</tr>
<th class="text-nowrap text-center total">{{ activity.duration|duration }}</th>
{% for day in activity.days.days %}
<td class="text-nowrap text-center day-total{% if day.date is weekend %} weekend{% endif %}">
{% if day.duration > 0 %}
{{ day.duration|duration }}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
{% endfor %}
{% set total = 0 %}
<tr class="summary">
{% for day in days %}
{% set total = total + day.totalDuration %}
{% endfor %}
<th></th>
<th class="text-nowrap text-center total">{{ total|duration }}</th>
{% for day in days %}
<th class="text-nowrap text-center day-total{% if day.day is weekend %} weekend{% endif %}">
{{ day.totalDuration|duration }}
<th class="text-nowrap text-center total">{{ totals['totals']|duration }}</th>
{% for day in days.dateTimes %}
<th class="text-nowrap text-center day-total{% if day is weekend %} weekend{% endif %}">
{{ totals[day|report_date]|duration }}
</th>
{% endfor %}
</tr>
</table>
{% endif %}
{% endblock %}
{% endembed %}
@@ -98,12 +96,12 @@
<script type="text/javascript">
document.addEventListener('kimai.initialized', function() {
{% if form.user is defined %}
$('#{{ form.user.vars.id }}').on('change', function(ev) {
$(this).closest('form').submit();
jQuery('#{{ form.user.vars.id }}').on('change', function(ev) {
jQuery(this).closest('form').submit();
});
{% endif %}
$('#{{ form.date.vars.id }}').on('change', function(ev) {
$(this).closest('form').submit();
jQuery('#{{ form.date.vars.id }}').on('change', function(ev) {
jQuery(this).closest('form').submit();
});
});
</script>

View File

@@ -15,56 +15,63 @@
{% block box_title %}
{{ form_widget(form.date) }}
{% endblock %}
{% block box_body_class %}{{ box_id }} table-responsive no-padding{% endblock %}
{% block box_body_class %}{{ box_id }} table-responsive {% if hasData %}no-padding{% endif %}{% endblock %}
{% block box_body %}
{% set absoluteTotals = 0 %}
<table class="table table-bordered table-hover dataTable">
<tr>
<th>&nbsp;</th>
<th>&nbsp;</th>
{% for day in days %}
<th class="text-center text-nowrap{% if day is weekend %} weekend{% endif %}">
{{ day|day_name(true) }}<br>
{{ day|format_date('short') }}
</th>
{% endfor %}
</tr>
{% for userDay in rows %}
{% set usersTotalDuration = 0 %}
<tr class="user">
<td class="text-nowrap">
{{ widgets.label_dot(userDay.user.displayName, userDay.user.color) }}
</td>
{% for day in userDay.days %}
{% if day.totalDuration > 0 %}
{% set usersTotalDuration = usersTotalDuration + day.totalDuration %}
{% set absoluteTotals = absoluteTotals + day.totalDuration %}
{% endif %}
{% endfor %}
<th class="text-nowrap text-center total">
{{ usersTotalDuration|duration }}
</th>
{% for day in userDay.days %}
<td class="text-nowrap text-center day-total{% if day.day is weekend %} weekend{% endif %}">
{% if day.totalDuration > 0 %}
{{ day.totalDuration|duration }}
{% endif %}
</td>
{% if not hasData %}
{{ widgets.nothing_found() }}
{% else %}
{% set absoluteTotals = 0 %}
{% set totals = {} %}
<table class="table table-bordered table-hover dataTable">
<tr>
<th>&nbsp;</th>
<th>&nbsp;</th>
{% for day in stats.0.getDateTimes() %}
<th class="text-center text-nowrap{% if day is weekend %} weekend{% endif %}">
{{ day|day_name(true) }}<br>
{{ day|format_date('short') }}
</th>
{% set totals = totals|merge({(day|report_date): 0}) %}
{% endfor %}
</tr>
{% endfor %}
<tr>
<th>&nbsp;</th>
<th class="text-center text-nowrap">
{{ absoluteTotals|duration }}
</th>
{% for id, duration in totals %}
<th class="text-center text-nowrap">
{{ duration|duration }}
</th>
{% for userDay in stats %}
{% set usersTotalDuration = 0 %}
<tr class="user">
<td class="text-nowrap">
{{ widgets.label_dot(userDay.user.displayName, userDay.user.color) }}
</td>
{% for day in userDay.days %}
{% if day.totalDuration > 0 %}
{% set usersTotalDuration = usersTotalDuration + day.totalDuration %}
{% set absoluteTotals = absoluteTotals + day.totalDuration %}
{% endif %}
{% set totals = totals|merge({(day.date|report_date): (totals[day.date|report_date] + day.totalDuration)}) %}
{% endfor %}
<th class="text-nowrap text-center total">
<a href="{{ path(subReportRoute, {'date': subReportDate|report_date, 'user': userDay.user.id}) }}">{{ usersTotalDuration|duration }}</a>
</th>
{% for day in userDay.days %}
<td class="text-nowrap text-center day-total{% if day.date is weekend %} weekend{% endif %}">
{% if day.totalDuration > 0 %}
{{ day.totalDuration|duration }}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tr>
</table>
<tr>
<th>&nbsp;</th>
<th class="text-center text-nowrap">
{{ absoluteTotals|duration }}
</th>
{% for id, duration in totals %}
<th class="text-center text-nowrap">
{{ duration|duration }}
</th>
{% endfor %}
</tr>
</table>
{% endif %}
{% endblock %}
{% endembed %}
@@ -74,8 +81,8 @@
{{ parent() }}
<script type="text/javascript">
document.addEventListener('kimai.initialized', function() {
$('#{{ form.date.vars.id }}').on('change', function(ev) {
$(this).closest('form').submit();
jQuery('#{{ form.date.vars.id }}').on('change', function(ev) {
jQuery(this).closest('form').submit();
});
});
</script>

View File

@@ -7,7 +7,7 @@
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_before %}
{{ form_start(form, {'attr': {'class': 'form-inline'}}) }}
{{ form_start(form, {'attr': {'class': 'form-inline form-reporting'}}) }}
{% endblock %}
{% block box_after %}
{{ form_end(form) }}
@@ -15,60 +15,63 @@
{% block box_title %}
{{ form_widget(form.date) }}
{% endblock %}
{% block box_body_class %}{{ box_id }} table-responsive no-padding{% endblock %}
{% block box_body_class %}{{ box_id }} table-responsive {% if hasData %}no-padding{% endif %}{% endblock %}
{% block box_body %}
<table class="table table-bordered table-hover dataTable">
<tr>
<th>&nbsp;</th>
<th>&nbsp;</th>
{% for id, month in months %}
<th class="text-center text-nowrap">
<a href="{{ path('report_monthly_users', {'date': month|date_short}) }}">
{{ month|month_name }}<br>
{{ month|date_format('Y') }}
</a>
</th>
{% endfor %}
</tr>
{% for userYear in rows %}
{% set usersTotalDuration = 0 %}
<tr class="user">
<td class="text-nowrap">
{{ widgets.label_dot(userYear.user.displayName, userYear.user.color) }}
</td>
{% for yid, year in userYear.years %}
{% for mid, month in year.months %}
{% if month.totalDuration > 0 %}
{% set usersTotalDuration = usersTotalDuration + month.totalDuration %}
{% endif %}
{% endfor %}
{% if not hasData %}
{{ widgets.nothing_found() }}
{% else %}
{% set totals = {} %}
<table class="table table-bordered table-hover dataTable">
<tr>
<th>&nbsp;</th>
<th>&nbsp;</th>
{% for month in stats.0.getDateTimes() %}
<th class="text-center text-nowrap">
<a href="{{ path('report_monthly_users', {'date': month|report_date}) }}">
{{ month|month_name }}<br>
{{ month|date_format('Y') }}
</a>
</th>
{% set totals = totals|merge({(month|report_date): 0}) %}
{% endfor %}
<th class="text-nowrap text-center total">
{{ usersTotalDuration|duration }}
</th>
{% for yid, year in userYear.years %}
{% for mid, month in year.months %}
</tr>
{% for userYear in stats|filter(row => row.user is not null) %}
{% set usersTotalDuration = 0 %}
<tr class="user">
<td class="text-nowrap">
{{ widgets.label_dot(userYear.user.displayName, userYear.user.color) }}
</td>
{% for mid, month in userYear.months %}
{% if month.totalDuration > 0 %}
{% set usersTotalDuration = usersTotalDuration + month.totalDuration %}
{% endif %}
{% endfor %}
<th class="text-nowrap text-center total">
{{ usersTotalDuration|duration }}
</th>
{% for mid, month in userYear.getMonths() %}
<td class="text-nowrap text-center day-total">
{% if month.totalDuration > 0 %}
<a href="{{ path('report_user_month', {'date': (year.year ~'-'~month.month~'-01')|date_short, 'user': userYear.user.id}) }}" data-toggle="tooltip" title="{{ 'label.billable'|trans }}: {{ month.billableDuration|duration }}">
<a href="{{ path('report_user_month', {'date': create_date(month.date.format('Y') ~'-'~month.date.format('m')~'-01')|report_date, 'user': userYear.user.id}) }}" data-toggle="tooltip" title="{{ 'label.billable'|trans }}: {{ month.billableDuration|duration }}">
{{ month.totalDuration|duration }}
</a>
{% set totals = totals|merge({(month.date|report_date): (totals[month.date|report_date] + month.totalDuration)}) %}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
<tr>
<th>&nbsp;</th>
<th>&nbsp;</th>
{% for id, total in totals %}
<th class="text-center text-nowrap">
{{ total|duration }}
</th>
{% endfor %}
</tr>
{% endfor %}
<tr>
<th>&nbsp;</th>
<th>&nbsp;</th>
{% for id, duration in totals %}
<th class="text-center text-nowrap">
{{ duration|duration }}
</th>
{% endfor %}
</tr>
</table>
</table>
{% endif %}
{% endblock %}
{% endembed %}
@@ -78,8 +81,8 @@
{{ parent() }}
<script type="text/javascript">
document.addEventListener('kimai.initialized', function() {
$('#{{ form.date.vars.id }}').on('change', function(ev) {
$(this).closest('form').submit();
jQuery('#{{ form.date.vars.id }}').on('change', function(ev) {
jQuery(this).closest('form').submit();
});
});
</script>

View File

@@ -276,12 +276,20 @@
duration = parseFloat(duration) * 3600;
momentDuration = moment.duration('PT' + duration + 'S');
} else if (duration.indexOf('H') !== -1 || duration.indexOf('M') !== -1 || duration.indexOf('S') !== -1) {
{# D for days does not work, because 'PT1H' but with days 'P1D' is used #}
momentDuration = moment.duration('PT' + duration);
} else {
let c = parseInt(duration);
let d = parseInt(duration).toFixed();
if (!isNaN(c) && duration === d) {
duration = c * 3600;
momentDuration = moment.duration('PT' + duration + 'S');
}
}
return momentDuration;
}
function applyDateToField(field, momentObj, format)
{
field.value = momentObj.format(format);

View File

@@ -8,13 +8,12 @@
{% set inUse = (stats.recordsTotal > 0) %}
{% set params = {
'%user%': widgets.username(user),
'%records%': stats.recordsTotal,
'%duration%': stats.durationTotal|duration
} %}
{{ include(app.request.xmlHttpRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
'message': ("admin_user.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
'message': ("delete_warning.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
'form': form,
'used': inUse,
'back': path('admin_user')

View File

@@ -13,7 +13,7 @@
{% endblock %}
{% block profile_navbar %}
{{ _self.about_me(user, stats) }}
{{ _self.about_me(user, stats, firstTimesheet) }}
{% endblock %}
{% block profile_intro %}{% endblock %}
@@ -22,7 +22,7 @@
{{ _self.profile_box_horizontal(user, stats) }}
{% if years is empty %}
{% if workMonths is empty %}
{{ widgets.nothing_found() }}
{% endif %}
@@ -33,17 +33,17 @@
{{ charts.bar_javascript({'footer': 'footer', 'label': 'tooltip', 'onclick': {'url': monthRoute, 'replacer': {'__MONTH__': 'month'}}}) }}
{% for yearStat in years %}
{% for year in workMonths.years|reverse %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% import "macros/charts.html.twig" as charts %}
{% block box_title %}{{ yearStat.year }}{% endblock %}
{% block box_title %}{{ year }}{% endblock %}
{% block box_body %}
{% set dataset = [] %}
{% for month in yearStat.months %}
{% set monthDate = create_date(yearStat.year ~ '-' ~ month.month ~ '-01') %}
{% set dataset = dataset|merge([{'value': month.duration|chart_duration, 'tooltip': month.duration|duration, 'footer': ('label.billable'|trans ~ ': ' ~ month.billableDuration|duration), 'month': monthDate|date_short}]) %}
{% for month in workMonths.year(year) %}
{% set monthDate = create_date(year ~ '-' ~ month.date.format('m') ~ '-01') %}
{% set dataset = dataset|merge([{'value': month.duration|chart_duration, 'tooltip': month.duration|duration, 'footer': ('label.billable'|trans ~ ': ' ~ month.billableDuration|duration), 'month': monthDate|report_date}]) %}
{% endfor %}
{{ charts.bar_chart('userProfileChart'~yearStat.year, month_names(), [dataset], {}) }}
{{ charts.bar_chart('userProfileChart'~year, month_names(), [dataset], {}) }}
{% endblock %}
{% endembed %}
{% endfor %}
@@ -69,7 +69,7 @@
</script>
{% endblock %}
{% macro about_me(user, stats) %}
{% macro about_me(user, stats, firstTimesheet) %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}
@@ -81,13 +81,20 @@
<strong>{{ 'label.username'|trans }}</strong><br>
{{ user.username }}
</p>
{% if user.accountNumber is not empty %}
<p>
<strong>{{ 'label.account_number'|trans }}</strong><br>
{{ user.accountNumber }}
</p>
{% endif %}
<p>
<strong>{{ 'profile.first_entry'|trans }}</strong><br>
{{ stats.firstEntry|date_short }}
{% if firstTimesheet is not null %}
{{ firstTimesheet|date_short }}
{% else %}
&ndash;
{% endif %}
</p>
<p>
<strong>{{ 'profile.registration_date'|trans }}</strong><br>

View File

@@ -16,29 +16,19 @@
{% block box_body %}
<table class="table table-hover dataTable" role="grid">
<tbody>
{% for row in projectStats|sort((a, b) => a.project.name <=> b.project.name) %}
{% set stats = row.stats %}
{% set project = row.project %}
{% for row in projectStats|sort((a, b) => a.entity.name <=> b.entity.name) %}
{% set project = row.entity %}
<tr{% if is_granted('details', project) %} class="alternative-link open-edit" data-href="{{ path('project_details', {'id': project.id}) }}"{% endif %}>
<td>
{{ widgets.label_project(project) }}
<br>
<small>{{ widgets.label_customer(project.customer) }}</small>
</td>
<td class="hidden-xs hidden-sm hidden-md">
{% for team in project.teams %}
{% if app.user.isInTeam(team) %}
{{ team.name }}
{% endif %}
{% endfor %}
</td>
<td style="width:50%">
{% if project.timeBudget is not empty and project.timeBudget > 0 %}
{% set budgetLeft = project.timeBudget - stats.recordDuration %}
{{ progress.progressbar_small(project.timeBudget, stats.durationBillable, budgetLeft, budgetLeft|duration) }}
{% elseif project.budget is not empty and project.budget > 0 %}
{% set budgetLeft = project.budget - stats.recordRate %}
{{ progress.progressbar_small(project.budget, stats.rateBillable, budgetLeft, budgetLeft|money(project.customer.currency)) }}
{% if row.hasTimeBudget() %}
{{ progress.progressbar_timebudget(row) }}
{% elseif row.hasBudget() %}
{{ progress.progressbar_budget(row, row.entity.customer.currency) }}
{% endif %}
</td>
</tr>